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.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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"
}
}
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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.