Skip to content

Commit

Permalink
fix(batch): SSM parameters can't be used as ECS Container secrets (#2…
Browse files Browse the repository at this point in the history
…6373)

ECS Containers can take both Secrets Manager Secrets and SSM parameters. Currently, only Secrets manager is supported.

Closes #26339.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
comcalvi authored Jul 18, 2023
1 parent 979cbff commit bc3d6a7
Show file tree
Hide file tree
Showing 8 changed files with 659 additions and 134 deletions.
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-batch-alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ jobDefn.container.addVolume(batch.EcsVolume.efs({

### Secrets

You can expose SecretsManager Secret ARNs to your container as environment variables.
You can expose SecretsManager Secret ARNs or SSM Parameters to your container as environment variables.
The following example defines the `MY_SECRET_ENV_VAR` environment variable that contains the
ARN of the Secret defined by `mySecret`:

Expand All @@ -512,7 +512,7 @@ const jobDefn = new batch.EcsJobDefinition(this, 'JobDefn', {
memory: cdk.Size.mebibytes(2048),
cpu: 256,
secrets: {
MY_SECRET_ENV_VAR: mySecret,
MY_SECRET_ENV_VAR: batch.Secret.fromSecretsManager(mySecret),
}
}),
});
Expand Down
97 changes: 93 additions & 4 deletions packages/@aws-cdk/aws-batch-alpha/lib/ecs-container-definition.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as ecs from 'aws-cdk-lib/aws-ecs';
import { IFileSystem } from 'aws-cdk-lib/aws-efs';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import { Lazy, PhysicalName, Size } from 'aws-cdk-lib/core';
import { Construct, IConstruct } from 'constructs';
Expand All @@ -11,6 +12,92 @@ import { LogGroup } from 'aws-cdk-lib/aws-logs';
const EFS_VOLUME_SYMBOL = Symbol.for('aws-cdk-lib/aws-batch/lib/container-definition.EfsVolume');
const HOST_VOLUME_SYMBOL = Symbol.for('aws-cdk-lib/aws-batch/lib/container-definition.HostVolume');

/**
* Specify the secret's version id or version stage
*/
export interface SecretVersionInfo {
/**
* version id of the secret
*
* @default - use default version id
*/
readonly versionId?: string;
/**
* version stage of the secret
*
* @default - use default version stage
*/
readonly versionStage?: string;
}

/**
* 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.
*
* @param secret the secret stored in AWS Secrets Manager
* @param field the name of the field with the value that you want to set as
* the environment variable value. Only values in JSON format are supported.
* If you do not specify a JSON field, then the full content of the secret is
* used.
*/
public static fromSecretsManager(secret: secretsmanager.ISecret, field?: string): Secret {
return {
arn: field ? `${secret.secretArn}:${field}::` : secret.secretArn,
hasField: !!field,
grantRead: grantee => secret.grantRead(grantee),
};
}

/**
* Creates a environment variable value from a secret stored in AWS Secrets
* Manager.
*
* @param secret the secret stored in AWS Secrets Manager
* @param versionInfo the version information to reference the secret
* @param field the name of the field with the value that you want to set as
* the environment variable value. Only values in JSON format are supported.
* If you do not specify a JSON field, then the full content of the secret is
* used.
*/
public static fromSecretsManagerVersion(secret: secretsmanager.ISecret, versionInfo: SecretVersionInfo, field?: string): Secret {
return {
arn: `${secret.secretArn}:${field ?? ''}:${versionInfo.versionStage ?? ''}:${versionInfo.versionId ?? ''}`,
hasField: !!field,
grantRead: grantee => secret.grantRead(grantee),
};
}

/**
* The ARN of the secret
*/
public abstract readonly arn: string;

/**
* Whether this secret uses a specific JSON field
*/
public abstract readonly hasField?: boolean;

/**
* Grants reading the secret to a principal
*/
public abstract grantRead(grantee: iam.IGrantable): iam.Grant;
}

/**
* Options to configure an EcsVolume
*/
Expand Down Expand Up @@ -350,7 +437,7 @@ export interface IEcsContainerDefinition extends IConstruct {
*
* @default - no secrets
*/
readonly secrets?: { [envVarName: string]: secretsmanager.ISecret };
readonly secrets?: { [envVarName: string]: Secret };

/**
* The user name to use inside the container
Expand Down Expand Up @@ -467,7 +554,7 @@ export interface EcsContainerDefinitionProps {
*
* @default - no secrets
*/
readonly secrets?: { [envVarName: string]: secretsmanager.ISecret };
readonly secrets?: { [envVarName: string]: Secret };

/**
* The user name to use inside the container
Expand Down Expand Up @@ -498,7 +585,7 @@ abstract class EcsContainerDefinitionBase extends Construct implements IEcsConta
public readonly linuxParameters?: LinuxParameters;
public readonly logDriverConfig?: ecs.LogDriverConfig;
public readonly readonlyRootFilesystem?: boolean;
public readonly secrets?: { [envVarName: string]: secretsmanager.ISecret };
public readonly secrets?: { [envVarName: string]: Secret };
public readonly user?: string;
public readonly volumes: EcsVolume[];

Expand Down Expand Up @@ -557,9 +644,11 @@ abstract class EcsContainerDefinitionBase extends Construct implements IEcsConta
readonlyRootFilesystem: this.readonlyRootFilesystem,
resourceRequirements: this._renderResourceRequirements(),
secrets: this.secrets ? Object.entries(this.secrets).map(([name, secret]) => {
secret.grantRead(this.executionRole);

return {
name,
valueFrom: secret.secretArn,
valueFrom: secret.arn,
};
}) : undefined,
mountPoints: Lazy.any({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { Vpc } from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as efs from 'aws-cdk-lib/aws-efs';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import { ArnPrincipal, Role } from 'aws-cdk-lib/aws-iam';
import * as logs from 'aws-cdk-lib/aws-logs';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import { Size, Stack } from 'aws-cdk-lib';
import * as cdk from 'aws-cdk-lib';
import { EcsContainerDefinitionProps, EcsEc2ContainerDefinition, EcsFargateContainerDefinition, EcsJobDefinition, EcsVolume, IEcsEc2ContainerDefinition, LinuxParameters, UlimitName } from '../lib';
import { EcsContainerDefinitionProps, EcsEc2ContainerDefinition, EcsFargateContainerDefinition, EcsJobDefinition, EcsVolume, IEcsEc2ContainerDefinition, LinuxParameters, Secret, UlimitName } from '../lib';
import { CfnJobDefinitionProps } from 'aws-cdk-lib/aws-batch';
import { capitalizePropertyNames } from './utils';
import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';
Expand Down Expand Up @@ -311,13 +312,13 @@ describe.each([EcsEc2ContainerDefinition, EcsFargateContainerDefinition])('%p',
});
});

test('respects secrets', () => {
test('respects secrets from secrestsmanager', () => {
// WHEN
new EcsJobDefinition(stack, 'ECSJobDefn', {
container: new ContainerDefinition(stack, 'EcsContainer', {
...defaultContainerProps,
secrets: {
envName: new Secret(stack, 'testSecret'),
envName: Secret.fromSecretsManager(new secretsmanager.Secret(stack, 'testSecret')),
},
}),
});
Expand All @@ -335,10 +336,189 @@ describe.each([EcsEc2ContainerDefinition, EcsFargateContainerDefinition])('%p',
],
},
});

Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: {
Statement: [
{
Action: ['logs:CreateLogStream', 'logs:PutLogEvents'],
Effect: 'Allow',
Resource: {
'Fn::Join': [
'', [
'arn:',
{ Ref: 'AWS::Partition' },
':logs:',
{ Ref: 'AWS::Region' },
':',
{ Ref: 'AWS::AccountId' },
':log-group:/aws/batch/job:*',
],
],
},
},
{
Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'],
Effect: 'Allow',
Resource: { Ref: 'testSecretB96AD12C' },
},
],
},
});
});

test('respects user', () => {
test('respects versioned secrets from secrestsmanager', () => {
// WHEN
new EcsJobDefinition(stack, 'ECSJobDefn', {
container: new ContainerDefinition(stack, 'EcsContainer', {
...defaultContainerProps,
secrets: {
envName: Secret.fromSecretsManagerVersion(new secretsmanager.Secret(stack, 'testSecret'), {
versionId: 'versionID',
versionStage: 'stage',
}),
},
}),
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::Batch::JobDefinition', {
...pascalCaseExpectedProps,
ContainerProperties: {
...pascalCaseExpectedProps.ContainerProperties,
Secrets: [
{
Name: 'envName',
ValueFrom: {
'Fn::Join': [
'', [
{ Ref: 'testSecretB96AD12C' },
'::stage:versionID',
],
],
},
},
],
},
});

Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: {
Statement: [
{
Action: ['logs:CreateLogStream', 'logs:PutLogEvents'],
Effect: 'Allow',
Resource: {
'Fn::Join': [
'', [
'arn:',
{ Ref: 'AWS::Partition' },
':logs:',
{ Ref: 'AWS::Region' },
':',
{ Ref: 'AWS::AccountId' },
':log-group:/aws/batch/job:*',
],
],
},
},
{
Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'],
Effect: 'Allow',
Resource: { Ref: 'testSecretB96AD12C' },
},
],
},
});
});

test('respects secrets from ssm', () => {
// WHEN
new EcsJobDefinition(stack, 'ECSJobDefn', {
container: new ContainerDefinition(stack, 'EcsContainer', {
...defaultContainerProps,
secrets: {
envName: Secret.fromSsmParameter(new ssm.StringParameter(stack, 'myParam', { stringValue: 'super secret' })),
},
}),
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::Batch::JobDefinition', {
...pascalCaseExpectedProps,
ContainerProperties: {
...pascalCaseExpectedProps.ContainerProperties,
Secrets: [
{
Name: 'envName',
ValueFrom: {
'Fn::Join': [
'', [
'arn:',
{
Ref: 'AWS::Partition',
},
':ssm:',
{ Ref: 'AWS::Region' },
':',
{ Ref: 'AWS::AccountId' },
':parameter/',
{ Ref: 'myParam03610B68' },
],
],
},
},
],
},
});

Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: {
Statement: [
{
Action: ['logs:CreateLogStream', 'logs:PutLogEvents'],
Effect: 'Allow',
Resource: {
'Fn::Join': [
'', [
'arn:',
{ Ref: 'AWS::Partition' },
':logs:',
{ Ref: 'AWS::Region' },
':',
{ Ref: 'AWS::AccountId' },
':log-group:/aws/batch/job:*',
],
],
},
},
{
Action: ['ssm:DescribeParameters', 'ssm:GetParameters', 'ssm:GetParameter', 'ssm:GetParameterHistory'],
Effect: 'Allow',
Resource: {
'Fn::Join': [
'',
[
'arn:',
{ Ref: 'AWS::Partition' },
':ssm:',
{ Ref: 'AWS::Region' },
':',
{ Ref: 'AWS::AccountId' },
':parameter/',
{ Ref: 'myParam03610B68' },
],

],
},
},
],
},
});
});

test('respects user', () => {
// WHEN
new EcsJobDefinition(stack, 'ECSJobDefn', {
container: new ContainerDefinition(stack, 'EcsContainer', {
...defaultContainerProps,
Expand Down
Loading

0 comments on commit bc3d6a7

Please sign in to comment.