diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/load-balanced-service-base.ts index c5914c92c421e..9eb7a2b2b7286 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/load-balanced-service-base.ts @@ -67,6 +67,13 @@ export interface LoadBalancedServiceBaseProps { */ readonly environment?: { [key: string]: string }; + /** + * Secret environment variables to pass to the container + * + * @default - No secret environment variables. + */ + readonly secrets?: { [key: string]: ecs.Secret }; + /** * Whether to create an AWS log driver * diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts index adf2bfdb695c4..fd23c6209a477 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts @@ -41,10 +41,20 @@ export interface QueueProcessingServiceBaseProps { /** * The environment variables to pass to the container. * + * The variable `QUEUE_NAME` with value `queue.queueName` will + * always be passed. + * * @default 'QUEUE_NAME: queue.queueName' */ readonly environment?: { [key: string]: string }; + /** + * Secret environment variables to pass to the container + * + * @default - No secret environment variables. + */ + readonly secrets?: { [key: string]: ecs.Secret }; + /** * A queue for which to process items from. * @@ -89,18 +99,27 @@ export abstract class QueueProcessingServiceBase extends cdk.Construct { * Environment variables that will include the queue name */ public readonly environment: { [key: string]: string }; + + /** + * Secret environment variables + */ + public readonly secrets?: { [key: string]: ecs.Secret }; + /** * The minimum number of tasks to run */ public readonly desiredCount: number; + /** * The maximum number of instances for autoscaling to scale up to */ public readonly maxCapacity: number; + /** * The scaling interval for autoscaling based off an SQS Queue size */ public readonly scalingSteps: autoscaling.ScalingInterval[]; + /** * The AwsLogDriver to use for logging if logging is enabled. */ @@ -122,6 +141,7 @@ export abstract class QueueProcessingServiceBase extends cdk.Construct { // Add the queue name to environment variables this.environment = { ...(props.environment || {}), QUEUE_NAME: this.sqsQueue.queueName }; + this.secrets = props.secrets; // Determine the desired task count (minimum) and maximum scaling capacity this.desiredCount = props.desiredTaskCount || 1; diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts index e5dda075b454a..4227aacc597c4 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts @@ -44,6 +44,13 @@ export interface ScheduledTaskBaseProps { * @default none */ readonly environment?: { [key: string]: string }; + + /** + * Secret environment variables to pass to the container + * + * @default - No secret environment variables. + */ + readonly secrets?: { [key: string]: ecs.Secret }; } /** diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/load-balanced-ecs-service.ts index 6b7beb9b0eff6..e11b5cf7e3e3e 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/load-balanced-ecs-service.ts @@ -53,6 +53,7 @@ export class LoadBalancedEc2Service extends LoadBalancedServiceBase { memoryLimitMiB: props.memoryLimitMiB, memoryReservationMiB: props.memoryReservationMiB, environment: props.environment, + secrets: props.secrets, logging: this.logDriver, }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts index 82528c761d72d..451dff98ee2f3 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts @@ -62,6 +62,7 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { cpu: props.cpu, command: props.command, environment: this.environment, + secrets: this.secrets, logging: this.logDriver }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts index 1aacd592fa57a..2cb713f2591bb 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts @@ -53,6 +53,7 @@ export class ScheduledEc2Task extends ScheduledTaskBase { cpu: props.cpu, command: props.command, environment: props.environment, + secrets: props.secrets, logging: this.createAWSLogDriver(this.node.id) }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/load-balanced-fargate-service.ts index db1854031cd7c..018ca77293788 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/load-balanced-fargate-service.ts @@ -98,7 +98,8 @@ export class LoadBalancedFargateService extends LoadBalancedServiceBase { const container = taskDefinition.addContainer(containerName, { image: props.image, logging: this.logDriver, - environment: props.environment + environment: props.environment, + secrets: props.secrets, }); container.addPortMappings({ diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts index 429c402b5b0d6..a9a04b627c457 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts @@ -65,6 +65,7 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { image: props.image, command: props.command, environment: this.environment, + secrets: this.secrets, logging: this.logDriver }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts index 2381b47153bb8..08ccd5228dac9 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts @@ -48,6 +48,7 @@ export class ScheduledFargateTask extends ScheduledTaskBase { image: props.image, command: props.command, environment: props.environment, + secrets: props.secrets, logging: this.createAWSLogDriver(this.node.id) }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json index 1dcc0bd26014e..7327cc7d3fac6 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json @@ -665,11 +665,7 @@ "Cpu": 1, "Environment": [ { - "Name": "name", - "Value": "TRIGGER" - }, - { - "Name": "value", + "Name": "TRIGGER", "Value": "CloudWatch Events" } ], diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.ts index 41b11d347fd9c..30210026c1062 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.ts @@ -26,7 +26,7 @@ class EventStack extends cdk.Stack { desiredTaskCount: 2, memoryLimitMiB: 512, cpu: 1, - environment: { name: 'TRIGGER', value: 'CloudWatch Events' }, + environment: { TRIGGER: 'CloudWatch Events' }, schedule: events.Schedule.rate(cdk.Duration.minutes(1)), }); /// !hide diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.scheduled-ecs-task.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.scheduled-ecs-task.ts index 235801868c998..2fbfd50692e0d 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.scheduled-ecs-task.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.scheduled-ecs-task.ts @@ -85,7 +85,7 @@ export = { desiredTaskCount: 2, memoryLimitMiB: 512, cpu: 2, - environment: { name: 'TRIGGER', value: 'CloudWatch Events' }, + environment: { TRIGGER: 'CloudWatch Events' }, schedule: events.Schedule.expression('rate(1 minute)') }); @@ -111,11 +111,7 @@ export = { Cpu: 2, Environment: [ { - Name: "name", - Value: "TRIGGER" - }, - { - Name: "value", + Name: "TRIGGER", Value: "CloudWatch Events" } ], diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.scheduled-fargate-task.lit.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.scheduled-fargate-task.lit.expected.json index e685231b6a121..689641c744b28 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.scheduled-fargate-task.lit.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.scheduled-fargate-task.lit.expected.json @@ -258,11 +258,7 @@ { "Environment": [ { - "Name": "name", - "Value": "TRIGGER" - }, - { - "Name": "value", + "Name": "TRIGGER", "Value": "CloudWatch Events" } ], diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.scheduled-fargate-task.lit.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.scheduled-fargate-task.lit.ts index ee32ac5dfd6d4..de1b39b615d85 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.scheduled-fargate-task.lit.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.scheduled-fargate-task.lit.ts @@ -22,7 +22,7 @@ class EventStack extends cdk.Stack { desiredTaskCount: 2, memoryLimitMiB: 512, cpu: 256, - environment: { name: 'TRIGGER', value: 'CloudWatch Events' }, + environment: { TRIGGER: 'CloudWatch Events' }, schedule: events.Schedule.rate(cdk.Duration.minutes(2)), }); } diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts index 528566f5696ec..835287b2e4284 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts @@ -78,7 +78,7 @@ export = { desiredTaskCount: 2, memoryLimitMiB: 512, cpu: 2, - environment: { name: 'TRIGGER', value: 'CloudWatch Events' }, + environment: { TRIGGER: 'CloudWatch Events' }, schedule: events.Schedule.expression('rate(1 minute)') }); @@ -103,11 +103,7 @@ export = { { Environment: [ { - Name: "name", - Value: "TRIGGER" - }, - { - Name: "value", + Name: "TRIGGER", Value: "CloudWatch Events" } ], diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index f471ab3233581..493a39aa78f36 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -203,6 +203,26 @@ obtained from either DockerHub or from ECR repositories, or built directly from * `ecs.ContainerImage.fromAsset('./image')`: build and upload an image directly from a `Dockerfile` in your source directory. +### Environment variables + +To pass environment variables to the container, use the `environment` and `secrets` props. + +```ts +taskDefinition.addContainer('container', { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 1024, + environment: { // clear text, not for sensitive data + STAGE: 'prod', + }, + secrets: { // Retrieved from AWS Secrets Manager or AWS Systems Manager Parameter Store at container start-up. + SECRET: ecs.Secret.fromSecretsManager(secret), + PARAMETER: ecs.Secret.fromSsmParameter(parameter), + } +}); +``` + +The task execution role is automatically granted read permissions on the secrets/parameters. + ## Service A `Service` instantiates a `TaskDefinition` on a `Cluster` a given number of @@ -330,5 +350,3 @@ rule.addTarget(new targets.EcsTask({ }] })); ``` - -> Note: it is currently not possible to start AWS Fargate tasks in this way. diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index a16367afc8f3e..3694e754174a7 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -265,7 +265,7 @@ export class TaskDefinition extends TaskDefinitionBase { }); const taskDef = new CfnTaskDefinition(this, 'Resource', { - containerDefinitions: Lazy.anyValue({ produce: () => this.containers.map(x => x.renderContainerDefinition()) }), + containerDefinitions: Lazy.anyValue({ produce: () => this.containers.map(x => x.renderContainerDefinition(this)) }), volumes: Lazy.anyValue({ produce: () => this.volumes }), executionRoleArn: Lazy.stringValue({ produce: () => this.executionRole && this.executionRole.roleArn }), family: this.family, diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 25fa86faae901..6cf207ad4d1fd 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -1,4 +1,6 @@ import iam = require('@aws-cdk/aws-iam'); +import secretsmanager = require('@aws-cdk/aws-secretsmanager'); +import ssm = require('@aws-cdk/aws-ssm'); import cdk = require('@aws-cdk/core'); import { NetworkMode, TaskDefinition } from './base/task-definition'; import { ContainerImage, ContainerImageConfig } from './container-image'; @@ -7,6 +9,36 @@ import { LinuxParameters } from './linux-parameters'; import { LogDriver, LogDriverConfig } from './log-drivers/log-driver'; /** + * A secret environment variable. + */ +export abstract class Secret { + /** + * Creates an environment variable value from a parameter stored in AWS + * Systems Manager Parameter Store. + */ + public static fromSsmParameter(parameter: ssm.IParameter): Secret { + return { + arn: parameter.parameterArn, + grantRead: grantee => parameter.grantRead(grantee), + }; + } + + /** + * Creates a environment variable value from a secret stored in AWS Secrets + * Manager. + */ + public static fromSecretsManager(secret: secretsmanager.ISecret): Secret { + return { + arn: secret.secretArn, + grantRead: grantee => secret.grantRead(grantee), + }; + } + + public abstract readonly arn: string; + public abstract grantRead(grantee: iam.IGrantable): iam.Grant; +} + +/* * The options for creating a container definition. */ export interface ContainerDefinitionOptions { @@ -89,6 +121,13 @@ export interface ContainerDefinitionOptions { */ readonly environment?: { [key: string]: string }; + /** + * The secret environment variables to pass to the container. + * + * @default - No secret environment variables. + */ + readonly secrets?: { [key: string]: Secret }; + /** * Specifies whether the container is marked essential. * @@ -412,8 +451,10 @@ export class ContainerDefinition extends cdk.Construct { /** * Render this container definition to a CloudFormation object + * + * @param taskDefinition [disable-awslint:ref-via-interface] (made optional to avoid breaking change) */ - public renderContainerDefinition(): CfnTaskDefinition.ContainerDefinitionProperty { + public renderContainerDefinition(taskDefinition?: TaskDefinition): CfnTaskDefinition.ContainerDefinitionProperty { return { command: this.props.command, cpu: this.props.cpu, @@ -439,7 +480,17 @@ export class ContainerDefinition extends cdk.Construct { volumesFrom: this.volumesFrom.map(renderVolumeFrom), workingDirectory: this.props.workingDirectory, logConfiguration: this.logDriverConfig, - environment: this.props.environment && renderKV(this.props.environment, 'name', 'value'), + environment: this.props.environment && renderKV(this.props.environment, 'name', 'value'), + secrets: this.props.secrets && Object.entries(this.props.secrets) + .map(([k, v]) => { + if (taskDefinition) { + v.grantRead(taskDefinition.obtainExecutionRole()); + } + return { + name: k, + valueFrom: v.arn + }; + }), extraHosts: this.props.extraHosts && renderKV(this.props.extraHosts, 'hostname', 'ipAddress'), healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), links: this.links, diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index f4d76ce48f53b..31ef86d0c1031 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -1,5 +1,6 @@ import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import secretsmanager = require('@aws-cdk/aws-secretsmanager'); +import ssm = require('@aws-cdk/aws-ssm'); import cdk = require('@aws-cdk/core'); import { Test } from 'nodeunit'; import ecs = require('../lib'); @@ -285,6 +286,113 @@ export = { }, + 'can add secret environment variables to the container definition'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + const secret = new secretsmanager.Secret(stack, 'Secret'); + const parameter = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Parameter', { + parameterName: '/name', + version: 1 + }); + + // WHEN + taskDefinition.addContainer('cont', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + secrets: { + SECRET: ecs.Secret.fromSecretsManager(secret), + PARAMETER: ecs.Secret.fromSsmParameter(parameter), + } + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Secrets: [ + { + Name: "SECRET", + ValueFrom: { + Ref: "SecretA720EF05" + } + }, + { + Name: "PARAMETER", + ValueFrom: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":ssm:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":parameter/name" + ] + ] + } + }, + ] + } + ] + })); + + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'secretsmanager:GetSecretValue', + Effect: 'Allow', + Resource: { + Ref: 'SecretA720EF05' + } + }, + { + Action: [ + 'ssm:DescribeParameters', + 'ssm:GetParameter', + 'ssm:GetParameterHistory' + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition' + }, + ':ssm:', + { + Ref: 'AWS::Region' + }, + ':', + { + Ref: 'AWS::AccountId' + }, + ':parameter/name' + ] + ] + } + } + ], + Version: '2012-10-17' + } + })); + + test.done(); + + }, + 'can add AWS logging to container definition'(test: Test) { // GIVEN const stack = new cdk.Stack();