Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(batch): SSM parameters can't be used as ECS Container secrets #26373

Merged
merged 5 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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