Skip to content

Commit 572f781

Browse files
feat(ecs): add function for adding secrets to containers after instantiating them (#21826)
### Description Similar to `addEnvironment()`, an `addSecret()` method is useful to add secrets to ECS Containers after instantiating them via the constructor. ### Use Case The most important use-case is when writing Task Definition Extensions or Aspects to augment ECS services. For example, setting environment variables and secrets for a logging or monitoring solution. Right now, this can be done only using Escape Hatches and there is no higher level functionality to obtain this behaviour. ### Proposed Solution ```typescript const container = taskDefinition.addContainer('nginx', { image: ecs.ContainerImage.fromRegistry('nginx'), }); container.addSecret('SECRET_1', ecs.Secret.fromSecretsManager(secret)); container.addSecret('SECRET_2', ecs.Secret.fromSecretsManager(secretField, 'password')); ``` closes #18959 ---- ### All Submissions: * [X] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [X] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [X] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 3adf841 commit 572f781

25 files changed

+469
-86
lines changed

packages/@aws-cdk/aws-ecs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,8 @@ const newContainer = taskDefinition.addContainer('container', {
479479
},
480480
});
481481
newContainer.addEnvironment('QUEUE_NAME', 'MyQueue');
482+
newContainer.addSecret('API_KEY', ecs.Secret.fromSecretsManager(secret));
483+
newContainer.addSecret('DB_PASSWORD', ecs.Secret.fromSecretsManager(secret, 'password'));
482484
```
483485

484486
The task execution role is automatically granted read permissions on the secrets/parameters. Support for environment

packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,6 @@ export class TaskDefinition extends TaskDefinitionBase {
379379

380380
private _passRoleStatement?: iam.PolicyStatement;
381381

382-
private _referencesSecretJsonField?: boolean;
383-
384382
private runtimePlatform?: RuntimePlatform;
385383

386384
/**
@@ -614,9 +612,6 @@ export class TaskDefinition extends TaskDefinitionBase {
614612
if (this.defaultContainer === undefined && container.essential) {
615613
this.defaultContainer = container;
616614
}
617-
if (container.referencesSecretJsonField) {
618-
this._referencesSecretJsonField = true;
619-
}
620615
}
621616

622617
/**
@@ -695,7 +690,12 @@ export class TaskDefinition extends TaskDefinitionBase {
695690
* specific JSON field of a secret stored in Secrets Manager.
696691
*/
697692
public get referencesSecretJsonField(): boolean | undefined {
698-
return this._referencesSecretJsonField;
693+
for (const container of this.containers) {
694+
if (container.referencesSecretJsonField) {
695+
return true;
696+
}
697+
}
698+
return false;
699699
}
700700

701701
/**

packages/@aws-cdk/aws-ecs/lib/container-definition.ts

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -435,12 +435,6 @@ export class ContainerDefinition extends Construct {
435435
*/
436436
public readonly logDriverConfig?: LogDriverConfig;
437437

438-
/**
439-
* Whether this container definition references a specific JSON field of a secret
440-
* stored in Secrets Manager.
441-
*/
442-
public readonly referencesSecretJsonField?: boolean;
443-
444438
/**
445439
* The name of the image referenced by this container.
446440
*/
@@ -458,7 +452,7 @@ export class ContainerDefinition extends Construct {
458452

459453
private readonly imageConfig: ContainerImageConfig;
460454

461-
private readonly secrets?: CfnTaskDefinition.SecretProperty[];
455+
private readonly secrets: CfnTaskDefinition.SecretProperty[] = [];
462456

463457
private readonly environment: { [key: string]: string };
464458

@@ -486,16 +480,8 @@ export class ContainerDefinition extends Construct {
486480
}
487481

488482
if (props.secrets) {
489-
this.secrets = [];
490483
for (const [name, secret] of Object.entries(props.secrets)) {
491-
if (secret.hasField) {
492-
this.referencesSecretJsonField = true;
493-
}
494-
secret.grantRead(this.taskDefinition.obtainExecutionRole());
495-
this.secrets.push({
496-
name,
497-
valueFrom: secret.arn,
498-
});
484+
this.addSecret(name, secret);
499485
}
500486
}
501487

@@ -602,6 +588,18 @@ export class ContainerDefinition extends Construct {
602588
this.environment[name] = value;
603589
}
604590

591+
/**
592+
* This method adds a secret as environment variable to the container.
593+
*/
594+
public addSecret(name: string, secret: Secret) {
595+
secret.grantRead(this.taskDefinition.obtainExecutionRole());
596+
597+
this.secrets.push({
598+
name,
599+
valueFrom: secret.arn,
600+
});
601+
}
602+
605603
/**
606604
* This method adds one or more resources to the container.
607605
*/
@@ -658,6 +656,19 @@ export class ContainerDefinition extends Construct {
658656
return undefined;
659657
}
660658

659+
/**
660+
* Whether this container definition references a specific JSON field of a secret
661+
* stored in Secrets Manager.
662+
*/
663+
public get referencesSecretJsonField(): boolean | undefined {
664+
for (const secret of this.secrets) {
665+
if (secret.valueFrom.endsWith('::')) {
666+
return true;
667+
}
668+
}
669+
return false;
670+
}
671+
661672
/**
662673
* The inbound rules associated with the security group the task or service will use.
663674
*
@@ -726,7 +737,7 @@ export class ContainerDefinition extends Construct {
726737
logConfiguration: this.logDriverConfig,
727738
environment: this.environment && Object.keys(this.environment).length ? renderKV(this.environment, 'name', 'value') : undefined,
728739
environmentFiles: this.environmentFiles && renderEnvironmentFiles(cdk.Stack.of(this).partition, this.environmentFiles),
729-
secrets: this.secrets,
740+
secrets: this.secrets.length ? this.secrets : undefined,
730741
extraHosts: this.props.extraHosts && renderKV(this.props.extraHosts, 'hostname', 'ipAddress'),
731742
healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck),
732743
links: cdk.Lazy.list({ produce: () => this.links }, { omitEmpty: true }),

packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,6 @@ export class FargateService extends BaseService implements IFargateService {
128128
throw new Error('Only one of SecurityGroup or SecurityGroups can be populated.');
129129
}
130130

131-
if (props.taskDefinition.referencesSecretJsonField
132-
&& props.platformVersion
133-
&& SECRET_JSON_FIELD_UNSUPPORTED_PLATFORM_VERSIONS.includes(props.platformVersion)) {
134-
throw new Error(`The task definition of this service uses at least one container that references a secret JSON field. This feature requires platform version ${FargatePlatformVersion.VERSION1_4} or later.`);
135-
}
136131
super(scope, id, {
137132
...props,
138133
desiredCount: props.desiredCount,
@@ -154,6 +149,14 @@ export class FargateService extends BaseService implements IFargateService {
154149

155150
this.configureAwsVpcNetworkingWithSecurityGroups(props.cluster.vpc, props.assignPublicIp, props.vpcSubnets, securityGroups);
156151

152+
this.node.addValidation({
153+
validate: () => this.taskDefinition.referencesSecretJsonField
154+
&& props.platformVersion
155+
&& SECRET_JSON_FIELD_UNSUPPORTED_PLATFORM_VERSIONS.includes(props.platformVersion)
156+
? [`The task definition of this service uses at least one container that references a secret JSON field. This feature requires platform version ${FargatePlatformVersion.VERSION1_4} or later.`]
157+
: [],
158+
});
159+
157160
this.node.addValidation({
158161
validate: () => !this.taskDefinition.defaultContainer ? ['A TaskDefinition must have at least one essential container'] : [],
159162
});

packages/@aws-cdk/aws-ecs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"@aws-cdk/aws-efs": "0.0.0",
8686
"@aws-cdk/cdk-build-tools": "0.0.0",
8787
"@aws-cdk/integ-runner": "0.0.0",
88+
"@aws-cdk/integ-tests": "0.0.0",
8889
"@aws-cdk/cfn2ts": "0.0.0",
8990
"@aws-cdk/cx-api": "0.0.0",
9091
"@aws-cdk/pkglint": "0.0.0",

packages/@aws-cdk/aws-ecs/test/container-definition.test.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ describe('container definition', () => {
548548
const actual = container.containerPort;
549549
const expected = 8080;
550550
expect(actual).toEqual(expected);
551-
}).toThrow(/Container MyContainer hasn't defined any ports. Call addPortMappings()./);
551+
}).toThrow(/Container MyContainer hasn't defined any ports. Call addPortMappings\(\)./);
552552

553553

554554
});
@@ -597,7 +597,7 @@ describe('container definition', () => {
597597
const actual = container.ingressPort;
598598
const expected = 8080;
599599
expect(actual).toEqual(expected);
600-
}).toThrow(/Container MyContainer hasn't defined any ports. Call addPortMappings()./);
600+
}).toThrow(/Container MyContainer hasn't defined any ports. Call addPortMappings\(\)./);
601601

602602

603603
});
@@ -1258,7 +1258,7 @@ describe('container definition', () => {
12581258
});
12591259

12601260
// WHEN
1261-
taskDefinition.addContainer('cont', {
1261+
const container = taskDefinition.addContainer('cont', {
12621262
image: ecs.ContainerImage.fromRegistry('test'),
12631263
memoryLimitMiB: 1024,
12641264
secrets: {
@@ -1268,6 +1268,7 @@ describe('container definition', () => {
12681268
SECRET_STAGE: ecs.Secret.fromSecretsManagerVersion(secret, { versionStage: 'version-stage' }),
12691269
},
12701270
});
1271+
container.addSecret('LATER_SECRET', ecs.Secret.fromSecretsManager(secret, 'field'));
12711272

12721273
// THEN
12731274
Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', {
@@ -1331,6 +1332,20 @@ describe('container definition', () => {
13311332
],
13321333
},
13331334
},
1335+
{
1336+
Name: 'LATER_SECRET',
1337+
ValueFrom: {
1338+
'Fn::Join': [
1339+
'',
1340+
[
1341+
{
1342+
Ref: 'SecretA720EF05',
1343+
},
1344+
':field::',
1345+
],
1346+
],
1347+
},
1348+
},
13341349
],
13351350
}),
13361351
],

packages/@aws-cdk/aws-ecs/test/ec2/integ.secret-json-field.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as secretsmanager from '@aws-cdk/aws-secretsmanager';
22
import * as cdk from '@aws-cdk/core';
3+
import * as integ from '@aws-cdk/integ-tests';
34
import * as ecs from '../../lib';
45

56
const app = new cdk.App();
@@ -14,12 +15,18 @@ const secret = new secretsmanager.Secret(stack, 'Secret', {
1415

1516
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef');
1617

17-
taskDefinition.addContainer('web', {
18+
const container = taskDefinition.addContainer('web', {
1819
image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
1920
memoryLimitMiB: 256,
2021
secrets: {
2122
PASSWORD: ecs.Secret.fromSecretsManager(secret, 'password'),
2223
},
2324
});
2425

26+
container.addSecret('APIKEY', ecs.Secret.fromSecretsManager(secret, 'apikey'));
27+
28+
new integ.IntegTest(app, 'aws-ecs-ec2-integ-secret-json-field', {
29+
testCases: [stack],
30+
});
31+
2532
app.synth();

packages/@aws-cdk/aws-ecs/test/ec2/secret-json-field.integ.snapshot/aws-ecs-integ-secret-json-field.assets.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
2-
"version": "20.0.0",
2+
"version": "21.0.0",
33
"files": {
4-
"b5f76aca81afb4973dcb922e825af78755db9cb1eb4fe457a9cd07c05e8bf0c7": {
4+
"df25aa5385ee86c1e4753a1f126bc9ed3bd97ffd266ccb980e6bc49e8c32f124": {
55
"source": {
66
"path": "aws-ecs-integ-secret-json-field.template.json",
77
"packaging": "file"
88
},
99
"destinations": {
1010
"current_account-current_region": {
1111
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
12-
"objectKey": "b5f76aca81afb4973dcb922e825af78755db9cb1eb4fe457a9cd07c05e8bf0c7.json",
12+
"objectKey": "df25aa5385ee86c1e4753a1f126bc9ed3bd97ffd266ccb980e6bc49e8c32f124.json",
1313
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
1414
}
1515
}

packages/@aws-cdk/aws-ecs/test/ec2/secret-json-field.integ.snapshot/aws-ecs-integ-secret-json-field.template.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@
5151
]
5252
]
5353
}
54+
},
55+
{
56+
"Name": "APIKEY",
57+
"ValueFrom": {
58+
"Fn::Join": [
59+
"",
60+
[
61+
{
62+
"Ref": "SecretA720EF05"
63+
},
64+
":apikey::"
65+
]
66+
]
67+
}
5468
}
5569
]
5670
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"version": "21.0.0",
3+
"files": {
4+
"21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": {
5+
"source": {
6+
"path": "awsecsec2integsecretjsonfieldDefaultTestDeployAssert5B8058F0.template.json",
7+
"packaging": "file"
8+
},
9+
"destinations": {
10+
"current_account-current_region": {
11+
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
12+
"objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json",
13+
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
14+
}
15+
}
16+
}
17+
},
18+
"dockerImages": {}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"Parameters": {
3+
"BootstrapVersion": {
4+
"Type": "AWS::SSM::Parameter::Value<String>",
5+
"Default": "/cdk-bootstrap/hnb659fds/version",
6+
"Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
7+
}
8+
},
9+
"Rules": {
10+
"CheckBootstrapVersion": {
11+
"Assertions": [
12+
{
13+
"Assert": {
14+
"Fn::Not": [
15+
{
16+
"Fn::Contains": [
17+
[
18+
"1",
19+
"2",
20+
"3",
21+
"4",
22+
"5"
23+
],
24+
{
25+
"Ref": "BootstrapVersion"
26+
}
27+
]
28+
}
29+
]
30+
},
31+
"AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
32+
}
33+
]
34+
}
35+
}
36+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"version":"20.0.0"}
1+
{"version":"21.0.0"}
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
{
2-
"version": "20.0.0",
2+
"version": "21.0.0",
33
"testCases": {
4-
"integ.secret-json-field": {
4+
"aws-ecs-ec2-integ-secret-json-field/DefaultTest": {
55
"stacks": [
66
"aws-ecs-integ-secret-json-field"
77
],
8-
"diffAssets": false,
9-
"stackUpdateWorkflow": true
8+
"assertionStack": "aws-ecs-ec2-integ-secret-json-field/DefaultTest/DeployAssert",
9+
"assertionStackName": "awsecsec2integsecretjsonfieldDefaultTestDeployAssert5B8058F0"
1010
}
11-
},
12-
"synthContext": {},
13-
"enableLookups": false
11+
}
1412
}

0 commit comments

Comments
 (0)