Skip to content

Commit

Permalink
feat(aws-ecs): addSecret() method on ContainerDefinition
Browse files Browse the repository at this point in the history
closes aws#18959
  • Loading branch information
FlorinAsavoaie committed Aug 30, 2022
1 parent 82ce4a1 commit 75557f5
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 40 deletions.
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-ecs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,8 @@ const newContainer = taskDefinition.addContainer('container', {
},
});
newContainer.addEnvironment('QUEUE_NAME', 'MyQueue');
newContainer.addSecret('API_KEY', ecs.Secret.fromSecretsManager(secret));
newContainer.addSecret('DB_PASSWORD', ecs.Secret.fromSecretsManager(secret, 'password'));
```

The task execution role is automatically granted read permissions on the secrets/parameters. Support for environment
Expand Down
12 changes: 6 additions & 6 deletions packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,6 @@ export class TaskDefinition extends TaskDefinitionBase {

private _passRoleStatement?: iam.PolicyStatement;

private _referencesSecretJsonField?: boolean;

private runtimePlatform?: RuntimePlatform;

/**
Expand Down Expand Up @@ -614,9 +612,6 @@ export class TaskDefinition extends TaskDefinitionBase {
if (this.defaultContainer === undefined && container.essential) {
this.defaultContainer = container;
}
if (container.referencesSecretJsonField) {
this._referencesSecretJsonField = true;
}
}

/**
Expand Down Expand Up @@ -695,7 +690,12 @@ export class TaskDefinition extends TaskDefinitionBase {
* specific JSON field of a secret stored in Secrets Manager.
*/
public get referencesSecretJsonField(): boolean | undefined {
return this._referencesSecretJsonField;
for (const container of this.containers) {
if (container.referencesSecretJsonField) {
return true;
}
}
return false;
}

/**
Expand Down
45 changes: 28 additions & 17 deletions packages/@aws-cdk/aws-ecs/lib/container-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,12 +435,6 @@ export class ContainerDefinition extends Construct {
*/
public readonly logDriverConfig?: LogDriverConfig;

/**
* Whether this container definition references a specific JSON field of a secret
* stored in Secrets Manager.
*/
public readonly referencesSecretJsonField?: boolean;

/**
* The name of the image referenced by this container.
*/
Expand All @@ -458,7 +452,7 @@ export class ContainerDefinition extends Construct {

private readonly imageConfig: ContainerImageConfig;

private readonly secrets?: CfnTaskDefinition.SecretProperty[];
private readonly secrets: CfnTaskDefinition.SecretProperty[] = [];

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

Expand Down Expand Up @@ -486,16 +480,8 @@ export class ContainerDefinition extends Construct {
}

if (props.secrets) {
this.secrets = [];
for (const [name, secret] of Object.entries(props.secrets)) {
if (secret.hasField) {
this.referencesSecretJsonField = true;
}
secret.grantRead(this.taskDefinition.obtainExecutionRole());
this.secrets.push({
name,
valueFrom: secret.arn,
});
this.addSecret(name, secret);
}
}

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

/**
* This method adds a secret as environment variable to the container.
*/
public addSecret(name: string, secret: Secret) {
secret.grantRead(this.taskDefinition.obtainExecutionRole());

this.secrets.push({
name,
valueFrom: secret.arn,
});
}

/**
* This method adds one or more resources to the container.
*/
Expand Down Expand Up @@ -658,6 +656,19 @@ export class ContainerDefinition extends Construct {
return undefined;
}

/**
* Whether this container definition references a specific JSON field of a secret
* stored in Secrets Manager.
*/
public get referencesSecretJsonField(): boolean | undefined {
for (const secret of this.secrets) {
if (secret.valueFrom.endsWith('::')) {
return true;
}
}
return false;
}

/**
* The inbound rules associated with the security group the task or service will use.
*
Expand Down Expand Up @@ -726,7 +737,7 @@ export class ContainerDefinition extends Construct {
logConfiguration: this.logDriverConfig,
environment: this.environment && Object.keys(this.environment).length ? renderKV(this.environment, 'name', 'value') : undefined,
environmentFiles: this.environmentFiles && renderEnvironmentFiles(cdk.Stack.of(this).partition, this.environmentFiles),
secrets: this.secrets,
secrets: this.secrets.length ? this.secrets : undefined,
extraHosts: this.props.extraHosts && renderKV(this.props.extraHosts, 'hostname', 'ipAddress'),
healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck),
links: cdk.Lazy.list({ produce: () => this.links }, { omitEmpty: true }),
Expand Down
13 changes: 8 additions & 5 deletions packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,6 @@ export class FargateService extends BaseService implements IFargateService {
throw new Error('Only one of SecurityGroup or SecurityGroups can be populated.');
}

if (props.taskDefinition.referencesSecretJsonField
&& props.platformVersion
&& SECRET_JSON_FIELD_UNSUPPORTED_PLATFORM_VERSIONS.includes(props.platformVersion)) {
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.`);
}
super(scope, id, {
...props,
desiredCount: props.desiredCount,
Expand All @@ -154,6 +149,14 @@ export class FargateService extends BaseService implements IFargateService {

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

this.node.addValidation({
validate: () => this.taskDefinition.referencesSecretJsonField
&& props.platformVersion
&& SECRET_JSON_FIELD_UNSUPPORTED_PLATFORM_VERSIONS.includes(props.platformVersion)
? [`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.`]
: [],
});

this.node.addValidation({
validate: () => !this.taskDefinition.defaultContainer ? ['A TaskDefinition must have at least one essential container'] : [],
});
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-ecs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"@aws-cdk/aws-efs": "0.0.0",
"@aws-cdk/cdk-build-tools": "0.0.0",
"@aws-cdk/integ-runner": "0.0.0",
"@aws-cdk/integ-tests": "0.0.0",
"@aws-cdk/cfn2ts": "0.0.0",
"@aws-cdk/cx-api": "0.0.0",
"@aws-cdk/pkglint": "0.0.0",
Expand Down
21 changes: 18 additions & 3 deletions packages/@aws-cdk/aws-ecs/test/container-definition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ describe('container definition', () => {
const actual = container.containerPort;
const expected = 8080;
expect(actual).toEqual(expected);
}).toThrow(/Container MyContainer hasn't defined any ports. Call addPortMappings()./);
}).toThrow(/Container MyContainer hasn't defined any ports. Call addPortMappings\(\)./);


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


});
Expand Down Expand Up @@ -1258,7 +1258,7 @@ describe('container definition', () => {
});

// WHEN
taskDefinition.addContainer('cont', {
const container = taskDefinition.addContainer('cont', {
image: ecs.ContainerImage.fromRegistry('test'),
memoryLimitMiB: 1024,
secrets: {
Expand All @@ -1268,6 +1268,7 @@ describe('container definition', () => {
SECRET_STAGE: ecs.Secret.fromSecretsManagerVersion(secret, { versionStage: 'version-stage' }),
},
});
container.addSecret('LATER_SECRET', ecs.Secret.fromSecretsManager(secret, 'field'));

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', {
Expand Down Expand Up @@ -1331,6 +1332,20 @@ describe('container definition', () => {
],
},
},
{
Name: 'LATER_SECRET',
ValueFrom: {
'Fn::Join': [
'',
[
{
Ref: 'SecretA720EF05',
},
':field::',
],
],
},
},
],
}),
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as secretsmanager from '@aws-cdk/aws-secretsmanager';
import * as cdk from '@aws-cdk/core';
import * as integ from '@aws-cdk/integ-tests';
import * as ecs from '../../lib';

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

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

taskDefinition.addContainer('web', {
const container = taskDefinition.addContainer('web', {
image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
memoryLimitMiB: 256,
secrets: {
PASSWORD: ecs.Secret.fromSecretsManager(secret, 'password'),
},
});

container.addSecret('APIKEY', ecs.Secret.fromSecretsManager(secret, 'apikey'));

new integ.IntegTest(app, 'aws-ecs-ec2-integ-secret-json-field', {
testCases: [stack],
});

app.synth();
17 changes: 10 additions & 7 deletions packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,13 +587,16 @@ describe('fargate service', () => {
},
});

// Errors on validation, not on construction.
new ecs.FargateService(stack, 'FargateService', {
cluster,
taskDefinition,
platformVersion: ecs.FargatePlatformVersion.VERSION1_3,
});

// THEN
expect(() => {
new ecs.FargateService(stack, 'FargateService', {
cluster,
taskDefinition,
platformVersion: ecs.FargatePlatformVersion.VERSION1_3,
});
Template.fromStack(stack);
}).toThrow(new RegExp(`uses at least one container that references a secret JSON field.+platform version ${ecs.FargatePlatformVersion.VERSION1_4} or later`));
});

Expand Down Expand Up @@ -1262,7 +1265,7 @@ describe('fargate service', () => {
containerPort: 8001,
})],
});
}).toThrow(/No container named 'SideContainer'. Did you call "addContainer()"?/);
}).toThrow(/No container named 'SideContainer'. Did you call "addContainer\(\)"?/);
});
});

Expand Down Expand Up @@ -2178,7 +2181,7 @@ describe('fargate service', () => {
],
});
});
}),
});

test('with serviceArn old format', () => {
// GIVEN
Expand Down
9 changes: 8 additions & 1 deletion packages/@aws-cdk/aws-ecs/test/fargate/integ.secret.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as secretsmanager from '@aws-cdk/aws-secretsmanager';
import * as cdk from '@aws-cdk/core';
import * as integ from '@aws-cdk/integ-tests';
import * as ecs from '../../lib';

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

const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef');

taskDefinition.addContainer('web', {
const container = taskDefinition.addContainer('web', {
image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
secrets: {
SECRET: ecs.Secret.fromSecretsManager(secret),
PASSWORD: ecs.Secret.fromSecretsManager(secret, 'password'),
},
});

container.addSecret('APIKEY', ecs.Secret.fromSecretsManager(secret, 'apikey'));

new integ.IntegTest(app, 'aws-ecs-fargate-integ-secret', {
testCases: [stack],
});

app.synth();

0 comments on commit 75557f5

Please sign in to comment.