Skip to content

Commit

Permalink
feat(ecs): support secret environment variables
Browse files Browse the repository at this point in the history
Add a union class to treat environment variable values whether they are given as clear text, from
a SSM parameter or a secret.

Closes aws#1478

BREAKING CHANGE: `environment` in `ecs.ContainerDefinition` now takes an object whose values are of
`ecs.EnvironmentValue` type.
  • Loading branch information
jogold committed Jun 21, 2019
1 parent 9101161 commit 146f13e
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 40 deletions.
Expand Up @@ -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
Expand Down
Expand Up @@ -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.
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;
Expand Down
Expand Up @@ -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.
Expand Down
Expand Up @@ -649,11 +649,7 @@
"Cpu": 1,
"Environment": [
{
"Name": "name",
"Value": "TRIGGER"
},
{
"Name": "value",
"Name": "TRIGGER",
"Value": "CloudWatch Events"
}
],
Expand Down Expand Up @@ -868,4 +864,4 @@
"Default": "/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id"
}
}
}
}
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts
Expand Up @@ -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")
}
});

Expand Down Expand Up @@ -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")
}
});

Expand Down Expand Up @@ -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")
}
});

Expand Down
Expand Up @@ -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
Expand Down
Expand Up @@ -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)')
});

Expand All @@ -111,11 +111,7 @@ export = {
Cpu: 2,
Environment: [
{
Name: "name",
Value: "TRIGGER"
},
{
Name: "value",
Name: "TRIGGER",
Value: "CloudWatch Events"
}
],
Expand Down
Expand Up @@ -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
Expand Down
75 changes: 64 additions & 11 deletions packages/@aws-cdk/aws-ecs/lib/container-definition.ts
@@ -1,11 +1,59 @@
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';
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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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(),
Expand Down Expand Up @@ -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),
Expand Down
67 changes: 66 additions & 1 deletion 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');
Expand Down Expand Up @@ -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")
}
});

Expand All @@ -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();
Expand Down

0 comments on commit 146f13e

Please sign in to comment.