Skip to content

Commit

Permalink
feat(ecs): support secret environment variables (#2994)
Browse files Browse the repository at this point in the history
Add support for runtime secrets in containers by adding a union class to treat secret environment
variable values whether they are pulled from a SSM parameter or a AWS Secrets secret.

https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html

Closes #1478
  • Loading branch information
jogold authored and Elad Ben-Israel committed Jul 29, 2019
1 parent 6c0bf4a commit bc233fa
Show file tree
Hide file tree
Showing 19 changed files with 231 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export class LoadBalancedEc2Service extends LoadBalancedServiceBase {
memoryLimitMiB: props.memoryLimitMiB,
memoryReservationMiB: props.memoryReservationMiB,
environment: props.environment,
secrets: props.secrets,
logging: this.logDriver,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase {
cpu: props.cpu,
command: props.command,
environment: this.environment,
secrets: this.secrets,
logging: this.logDriver
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase {
image: props.image,
command: props.command,
environment: this.environment,
secrets: this.secrets,
logging: this.logDriver
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -665,11 +665,7 @@
"Cpu": 1,
"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 @@ -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
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: '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 @@ -258,11 +258,7 @@
{
"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 @@ -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)),
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)')
});

Expand All @@ -103,11 +103,7 @@ export = {
{
Environment: [
{
Name: "name",
Value: "TRIGGER"
},
{
Name: "value",
Name: "TRIGGER",
Value: "CloudWatch Events"
}
],
Expand Down
22 changes: 20 additions & 2 deletions packages/@aws-cdk/aws-ecs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -330,5 +350,3 @@ rule.addTarget(new targets.EcsTask({
}]
}));
```

> Note: it is currently not possible to start AWS Fargate tasks in this way.
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
55 changes: 53 additions & 2 deletions packages/@aws-cdk/aws-ecs/lib/container-definition.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading

0 comments on commit bc233fa

Please sign in to comment.