From 146f13ecb38bbb547c7ebfc34e83ea67a6f331d7 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 21 Jun 2019 15:35:42 +0200 Subject: [PATCH] feat(ecs): support secret environment variables Add a union class to treat environment variable values whether they are given as clear text, from a SSM parameter or a secret. Closes #1478 BREAKING CHANGE: `environment` in `ecs.ContainerDefinition` now takes an object whose values are of `ecs.EnvironmentValue` type. --- .../lib/base/load-balanced-service-base.ts | 2 +- .../lib/base/queue-processing-service-base.ts | 6 +- .../lib/ecs/scheduled-ecs-task.ts | 2 +- ...integ.scheduled-ecs-task.lit.expected.json | 8 +- .../test/ec2/integ.scheduled-ecs-task.lit.ts | 2 +- .../aws-ecs-patterns/test/ec2/test.l3s.ts | 12 +-- .../ec2/test.queue-processing-ecs-service.ts | 4 +- .../test/ec2/test.scheduled-ecs-task.ts | 8 +- .../test.queue-processing-fargate-service.ts | 4 +- .../aws-ecs/lib/container-definition.ts | 75 ++++++++++++++++--- .../aws-ecs/test/test.container-definition.ts | 67 ++++++++++++++++- 11 files changed, 150 insertions(+), 40 deletions(-) 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 77299cdeea30f..311a0c9e722c0 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 @@ -65,7 +65,7 @@ export interface LoadBalancedServiceBaseProps { * * @default - No environment variables. */ - readonly environment?: { [key: string]: string }; + readonly environment?: { [key: string]: ecs.EnvironmentValue }; /** * 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 f35fa64a6cfb0..4a68be5ddee6e 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 @@ -43,7 +43,7 @@ export interface QueueProcessingServiceBaseProps { * * @default 'QUEUE_NAME: queue.queueName' */ - readonly environment?: { [key: string]: string }; + readonly environment?: { [key: string]: ecs.EnvironmentValue }; /** * A queue for which to process items from. @@ -88,7 +88,7 @@ export abstract class QueueProcessingServiceBase extends cdk.Construct { /** * Environment variables that will include the queue name */ - public readonly environment: { [key: string]: string }; + public readonly environment: { [key: string]: ecs.EnvironmentValue }; /** * The minimum number of tasks to run */ @@ -121,7 +121,7 @@ export abstract class QueueProcessingServiceBase extends cdk.Construct { this.logDriver = enableLogging ? this.createAWSLogDriver(this.node.id) : undefined; // Add the queue name to environment variables - this.environment = { ...(props.environment || {}), QUEUE_NAME: this.sqsQueue.queueName }; + this.environment = { ...(props.environment || {}), QUEUE_NAME: ecs.EnvironmentValue.fromString(this.sqsQueue.queueName) }; // 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/ecs/scheduled-ecs-task.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts index b9b851ab7144f..29b7ae1f8674b 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 @@ -49,7 +49,7 @@ export interface ScheduledEc2TaskProps { * * @default none */ - readonly environment?: { [key: string]: string }; + readonly environment?: { [key: string]: ecs.EnvironmentValue }; /** * The hard limit (in MiB) of memory to present to the container. 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 61916c2affd30..c890abcf4048b 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 @@ -649,11 +649,7 @@ "Cpu": 1, "Environment": [ { - "Name": "name", - "Value": "TRIGGER" - }, - { - "Name": "value", + "Name": "TRIGGER", "Value": "CloudWatch Events" } ], @@ -868,4 +864,4 @@ "Default": "/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id" } } -} \ No newline at end of file +} 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 d17daca0799d9..6d12087d61ea2 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: ecs.EnvironmentValue.fromString('CloudWatch Events') }, schedule: events.Schedule.rate(cdk.Duration.minutes(1)), }); /// !hide diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts index 4050b72674cab..ea704cf3ec542 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts @@ -22,8 +22,8 @@ export = { image: ecs.ContainerImage.fromRegistry('test'), desiredCount: 2, environment: { - TEST_ENVIRONMENT_VARIABLE1: "test environment variable 1 value", - TEST_ENVIRONMENT_VARIABLE2: "test environment variable 2 value" + TEST_ENVIRONMENT_VARIABLE1: ecs.EnvironmentValue.fromString("test environment variable 1 value"), + TEST_ENVIRONMENT_VARIABLE2: ecs.EnvironmentValue.fromString("test environment variable 2 value") } }); @@ -96,8 +96,8 @@ export = { image: ecs.ContainerImage.fromRegistry('test'), desiredCount: 2, environment: { - TEST_ENVIRONMENT_VARIABLE1: "test environment variable 1 value", - TEST_ENVIRONMENT_VARIABLE2: "test environment variable 2 value" + TEST_ENVIRONMENT_VARIABLE1: ecs.EnvironmentValue.fromString("test environment variable 1 value"), + TEST_ENVIRONMENT_VARIABLE2: ecs.EnvironmentValue.fromString("test environment variable 2 value") } }); @@ -153,8 +153,8 @@ export = { desiredCount: 2, enableLogging: false, environment: { - TEST_ENVIRONMENT_VARIABLE1: "test environment variable 1 value", - TEST_ENVIRONMENT_VARIABLE2: "test environment variable 2 value" + TEST_ENVIRONMENT_VARIABLE1: ecs.EnvironmentValue.fromString("test environment variable 1 value"), + TEST_ENVIRONMENT_VARIABLE2: ecs.EnvironmentValue.fromString("test environment variable 2 value") } }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts index 09a05dfe8e80f..7da03687fc91e 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts @@ -84,8 +84,8 @@ export = { enableLogging: false, desiredTaskCount: 2, environment: { - TEST_ENVIRONMENT_VARIABLE1: "test environment variable 1 value", - TEST_ENVIRONMENT_VARIABLE2: "test environment variable 2 value" + TEST_ENVIRONMENT_VARIABLE1: ecs.EnvironmentValue.fromString("test environment variable 1 value"), + TEST_ENVIRONMENT_VARIABLE2: ecs.EnvironmentValue.fromString("test environment variable 2 value") }, queue, maxScalingCapacity: 5 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 d1c4ba987318c..cff8af7008aaf 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: ecs.EnvironmentValue.fromString('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/test.queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts index 6d79f0003906c..e17ffe59477af 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts @@ -82,8 +82,8 @@ export = { enableLogging: false, desiredTaskCount: 2, environment: { - TEST_ENVIRONMENT_VARIABLE1: "test environment variable 1 value", - TEST_ENVIRONMENT_VARIABLE2: "test environment variable 2 value" + TEST_ENVIRONMENT_VARIABLE1: ecs.EnvironmentValue.fromString("test environment variable 1 value"), + TEST_ENVIRONMENT_VARIABLE2: ecs.EnvironmentValue.fromString("test environment variable 2 value") }, queue, maxScalingCapacity: 5 diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 2342a78fc5fab..a934590fed248 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/cdk'); import { NetworkMode, TaskDefinition } from './base/task-definition'; import { ContainerImage, ContainerImageConfig } from './container-image'; @@ -6,6 +8,52 @@ import { CfnTaskDefinition } from './ecs.generated'; import { LinuxParameters } from './linux-parameters'; import { LogDriver } from './log-drivers/log-driver'; +/** + * Environment variable value type. + */ +export enum EnvironmentValueType { + /** + * A string in clear text. + */ + STRING = 'string', + + /** + * The full ARN of the AWS Secrets Manager secret or the full ARN of the + * parameter in the AWS Systems Manager Parameter Store. + */ + SECRET = 'secret', +} + +/** + * An environment variable value. + */ +export class EnvironmentValue { + /** + * Creates a environment variable value from a string. + */ + public static fromString(value: string) { + return new EnvironmentValue(value, EnvironmentValueType.STRING); + } + + /** + * Creates a environment variable value from a parameter stored in AWS + * Systems Manager Parameter Store. + */ + public static fromSsmParameter(parameter: ssm.IParameter) { + return new EnvironmentValue(parameter.parameterArn, EnvironmentValueType.SECRET); + } + + /** + * Creates a environment variable value from a secret stored in AWS Secrets + * Manager. + */ + public static fromSecretsManager(secret: secretsmanager.ISecret) { + return new EnvironmentValue(secret.secretArn, EnvironmentValueType.SECRET); + } + + constructor(public readonly value: string, public readonly type: EnvironmentValueType) {} +} + export interface ContainerDefinitionOptions { /** * The image to use for a container. @@ -81,7 +129,7 @@ export interface ContainerDefinitionOptions { * * @default - No environment variables. */ - readonly environment?: { [key: string]: string }; + readonly environment?: { [key: string]: EnvironmentValue }; /** * Indicates whether the task stops if this container fails. @@ -386,6 +434,17 @@ export class ContainerDefinition extends cdk.Construct { * Render this container definition to a CloudFormation object */ public renderContainerDefinition(): CfnTaskDefinition.ContainerDefinitionProperty { + const environment = []; + const secrets = []; + for (const [k, v] of Object.entries(this.props.environment || {})) { + if (v.type === EnvironmentValueType.STRING) { + environment.push({ name: k, value: v.value }); + } + if (v.type === EnvironmentValueType.SECRET) { + secrets.push({ name: k, valueFrom: v.value }); + } + } + return { command: this.props.command, cpu: this.props.cpu, @@ -411,8 +470,10 @@ export class ContainerDefinition extends cdk.Construct { volumesFrom: this.volumesFrom.map(renderVolumeFrom), workingDirectory: this.props.workingDirectory, logConfiguration: this.props.logging && this.props.logging.renderLogDriver(), - environment: this.props.environment && renderKV(this.props.environment, 'name', 'value'), - extraHosts: this.props.extraHosts && renderKV(this.props.extraHosts, 'hostname', 'ipAddress'), + environment: environment.length !== 0 ? environment : undefined, + secrets: secrets.length !== 0 ? secrets : undefined, + extraHosts: this.props.extraHosts && Object.entries(this.props.extraHosts) + .map(([k, v]) => ({ hostname: k, ipAddress: v })), healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), links: this.links, linuxParameters: this.linuxParameters && this.linuxParameters.renderLinuxParameters(), @@ -468,14 +529,6 @@ export interface HealthCheck { readonly timeout?: cdk.Duration; } -function renderKV(env: { [key: string]: string }, keyName: string, valueName: string): any { - const ret = []; - for (const [key, value] of Object.entries(env)) { - ret.push({ [keyName]: key, [valueName]: value }); - } - return ret; -} - function renderHealthCheck(hc: HealthCheck): CfnTaskDefinition.HealthCheckProperty { return { command: getHealthCheckCommand(hc), 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 ccf9a0ee2c78d..e4c4c8b7bfde9 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/cdk'); import { Test } from 'nodeunit'; import ecs = require('../lib'); @@ -265,7 +266,7 @@ export = { image: ecs.ContainerImage.fromRegistry('test'), memoryLimitMiB: 1024, environment: { - TEST_ENVIRONMENT_VARIABLE: "test environment variable value" + TEST_ENVIRONMENT_VARIABLE: ecs.EnvironmentValue.fromString("test environment variable value") } }); @@ -285,6 +286,70 @@ 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, + environment: { + SECRET: ecs.EnvironmentValue.fromSecretsManager(secret), + PARAMETER: ecs.EnvironmentValue.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" + ] + ] + } + }, + ] + } + ] + })); + + test.done(); + + }, + 'can add AWS logging to container definition'(test: Test) { // GIVEN const stack = new cdk.Stack();