From 091a79d3222e6020be09edf1eaf91f417fd71e21 Mon Sep 17 00:00:00 2001 From: Calvin Combs <66279577+comcalvi@users.noreply.github.com> Date: Tue, 18 Jul 2023 02:11:20 -0700 Subject: [PATCH] fix(batch): SSM parameters can't be used as ECS Container secrets (#26373) 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* --- packages/@aws-cdk/aws-batch-alpha/README.md | 4 +- .../lib/ecs-container-definition.ts | 97 ++++++- .../test/ecs-container-definition.test.ts | 190 ++++++++++++- .../manifest.json | 14 +- .../stack.assets.json | 4 +- .../stack.template.json | 220 +++++++++++---- .../tree.json | 254 ++++++++++++++---- .../test/integ.ecs-job-definition.ts | 10 +- 8 files changed, 659 insertions(+), 134 deletions(-) diff --git a/packages/@aws-cdk/aws-batch-alpha/README.md b/packages/@aws-cdk/aws-batch-alpha/README.md index 434d108e303ae..6e76d196450db 100644 --- a/packages/@aws-cdk/aws-batch-alpha/README.md +++ b/packages/@aws-cdk/aws-batch-alpha/README.md @@ -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`: @@ -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), } }), }); diff --git a/packages/@aws-cdk/aws-batch-alpha/lib/ecs-container-definition.ts b/packages/@aws-cdk/aws-batch-alpha/lib/ecs-container-definition.ts index 34a95037a33e3..b5ef1d8fb358b 100644 --- a/packages/@aws-cdk/aws-batch-alpha/lib/ecs-container-definition.ts +++ b/packages/@aws-cdk/aws-batch-alpha/lib/ecs-container-definition.ts @@ -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'; @@ -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 */ @@ -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 @@ -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 @@ -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[]; @@ -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({ diff --git a/packages/@aws-cdk/aws-batch-alpha/test/ecs-container-definition.test.ts b/packages/@aws-cdk/aws-batch-alpha/test/ecs-container-definition.test.ts index 378c4a08e58b4..0bd0e4457d801 100644 --- a/packages/@aws-cdk/aws-batch-alpha/test/ecs-container-definition.test.ts +++ b/packages/@aws-cdk/aws-batch-alpha/test/ecs-container-definition.test.ts @@ -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'; @@ -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')), }, }), }); @@ -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, diff --git a/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/manifest.json b/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/manifest.json index 930242adf5963..e046c309a8285 100644 --- a/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/manifest.json @@ -17,7 +17,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/c8dd0c72ae74118e7b13f5ad3ad3bd3daaa9115456be74990c685759252458a6.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/4c2c5be0c984c7727839ce1c8317ff866f53e08c586077ca24ffebb2db889325.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -201,6 +201,18 @@ "data": "mySecretE4D0A59C" } ], + "/stack/anotherSecret/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "anotherSecret11974229" + } + ], + "/stack/ssm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ssm85049941" + } + ], "/stack/myContainer/ExecutionRole/Resource": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/stack.assets.json b/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/stack.assets.json index 2ca8cbd4fec95..da02f3d0d8623 100644 --- a/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/stack.assets.json +++ b/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/stack.assets.json @@ -1,7 +1,7 @@ { "version": "32.0.0", "files": { - "c8dd0c72ae74118e7b13f5ad3ad3bd3daaa9115456be74990c685759252458a6": { + "4c2c5be0c984c7727839ce1c8317ff866f53e08c586077ca24ffebb2db889325": { "source": { "path": "stack.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "c8dd0c72ae74118e7b13f5ad3ad3bd3daaa9115456be74990c685759252458a6.json", + "objectKey": "4c2c5be0c984c7727839ce1c8317ff866f53e08c586077ca24ffebb2db889325.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/stack.template.json b/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/stack.template.json index 072500c6c0127..f60175d9e2d05 100644 --- a/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/stack.template.json +++ b/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/stack.template.json @@ -18,9 +18,6 @@ "vpcPublicSubnet1Subnet2E65531E": { "Type": "AWS::EC2::Subnet", "Properties": { - "VpcId": { - "Ref": "vpcA2121C38" - }, "AvailabilityZone": { "Fn::Select": [ 0, @@ -44,21 +41,24 @@ "Key": "Name", "Value": "stack/vpc/PublicSubnet1" } - ] + ], + "VpcId": { + "Ref": "vpcA2121C38" + } } }, "vpcPublicSubnet1RouteTable48A2DF9B": { "Type": "AWS::EC2::RouteTable", "Properties": { - "VpcId": { - "Ref": "vpcA2121C38" - }, "Tags": [ { "Key": "Name", "Value": "stack/vpc/PublicSubnet1" } - ] + ], + "VpcId": { + "Ref": "vpcA2121C38" + } } }, "vpcPublicSubnet1RouteTableAssociation5D3F4579": { @@ -75,12 +75,12 @@ "vpcPublicSubnet1DefaultRoute10708846": { "Type": "AWS::EC2::Route", "Properties": { - "RouteTableId": { - "Ref": "vpcPublicSubnet1RouteTable48A2DF9B" - }, "DestinationCidrBlock": "0.0.0.0/0", "GatewayId": { "Ref": "vpcIGWE57CBDCA" + }, + "RouteTableId": { + "Ref": "vpcPublicSubnet1RouteTable48A2DF9B" } }, "DependsOn": [ @@ -102,15 +102,15 @@ "vpcPublicSubnet1NATGateway9C16659E": { "Type": "AWS::EC2::NatGateway", "Properties": { - "SubnetId": { - "Ref": "vpcPublicSubnet1Subnet2E65531E" - }, "AllocationId": { "Fn::GetAtt": [ "vpcPublicSubnet1EIPDA49DCBE", "AllocationId" ] }, + "SubnetId": { + "Ref": "vpcPublicSubnet1Subnet2E65531E" + }, "Tags": [ { "Key": "Name", @@ -126,9 +126,6 @@ "vpcPublicSubnet2Subnet009B674F": { "Type": "AWS::EC2::Subnet", "Properties": { - "VpcId": { - "Ref": "vpcA2121C38" - }, "AvailabilityZone": { "Fn::Select": [ 1, @@ -152,21 +149,24 @@ "Key": "Name", "Value": "stack/vpc/PublicSubnet2" } - ] + ], + "VpcId": { + "Ref": "vpcA2121C38" + } } }, "vpcPublicSubnet2RouteTableEB40D4CB": { "Type": "AWS::EC2::RouteTable", "Properties": { - "VpcId": { - "Ref": "vpcA2121C38" - }, "Tags": [ { "Key": "Name", "Value": "stack/vpc/PublicSubnet2" } - ] + ], + "VpcId": { + "Ref": "vpcA2121C38" + } } }, "vpcPublicSubnet2RouteTableAssociation21F81B59": { @@ -183,12 +183,12 @@ "vpcPublicSubnet2DefaultRouteA1EC0F60": { "Type": "AWS::EC2::Route", "Properties": { - "RouteTableId": { - "Ref": "vpcPublicSubnet2RouteTableEB40D4CB" - }, "DestinationCidrBlock": "0.0.0.0/0", "GatewayId": { "Ref": "vpcIGWE57CBDCA" + }, + "RouteTableId": { + "Ref": "vpcPublicSubnet2RouteTableEB40D4CB" } }, "DependsOn": [ @@ -210,15 +210,15 @@ "vpcPublicSubnet2NATGateway9B8AE11A": { "Type": "AWS::EC2::NatGateway", "Properties": { - "SubnetId": { - "Ref": "vpcPublicSubnet2Subnet009B674F" - }, "AllocationId": { "Fn::GetAtt": [ "vpcPublicSubnet2EIP9B3743B1", "AllocationId" ] }, + "SubnetId": { + "Ref": "vpcPublicSubnet2Subnet009B674F" + }, "Tags": [ { "Key": "Name", @@ -234,9 +234,6 @@ "vpcPrivateSubnet1Subnet934893E8": { "Type": "AWS::EC2::Subnet", "Properties": { - "VpcId": { - "Ref": "vpcA2121C38" - }, "AvailabilityZone": { "Fn::Select": [ 0, @@ -260,21 +257,24 @@ "Key": "Name", "Value": "stack/vpc/PrivateSubnet1" } - ] + ], + "VpcId": { + "Ref": "vpcA2121C38" + } } }, "vpcPrivateSubnet1RouteTableB41A48CC": { "Type": "AWS::EC2::RouteTable", "Properties": { - "VpcId": { - "Ref": "vpcA2121C38" - }, "Tags": [ { "Key": "Name", "Value": "stack/vpc/PrivateSubnet1" } - ] + ], + "VpcId": { + "Ref": "vpcA2121C38" + } } }, "vpcPrivateSubnet1RouteTableAssociation67945127": { @@ -291,21 +291,18 @@ "vpcPrivateSubnet1DefaultRoute1AA8E2E5": { "Type": "AWS::EC2::Route", "Properties": { - "RouteTableId": { - "Ref": "vpcPrivateSubnet1RouteTableB41A48CC" - }, "DestinationCidrBlock": "0.0.0.0/0", "NatGatewayId": { "Ref": "vpcPublicSubnet1NATGateway9C16659E" + }, + "RouteTableId": { + "Ref": "vpcPrivateSubnet1RouteTableB41A48CC" } } }, "vpcPrivateSubnet2Subnet7031C2BA": { "Type": "AWS::EC2::Subnet", "Properties": { - "VpcId": { - "Ref": "vpcA2121C38" - }, "AvailabilityZone": { "Fn::Select": [ 1, @@ -329,21 +326,24 @@ "Key": "Name", "Value": "stack/vpc/PrivateSubnet2" } - ] + ], + "VpcId": { + "Ref": "vpcA2121C38" + } } }, "vpcPrivateSubnet2RouteTable7280F23E": { "Type": "AWS::EC2::RouteTable", "Properties": { - "VpcId": { - "Ref": "vpcA2121C38" - }, "Tags": [ { "Key": "Name", "Value": "stack/vpc/PrivateSubnet2" } - ] + ], + "VpcId": { + "Ref": "vpcA2121C38" + } } }, "vpcPrivateSubnet2RouteTableAssociation007E94D3": { @@ -360,12 +360,12 @@ "vpcPrivateSubnet2DefaultRouteB0E07F99": { "Type": "AWS::EC2::Route", "Properties": { - "RouteTableId": { - "Ref": "vpcPrivateSubnet2RouteTable7280F23E" - }, "DestinationCidrBlock": "0.0.0.0/0", "NatGatewayId": { "Ref": "vpcPublicSubnet2NATGateway9B8AE11A" + }, + "RouteTableId": { + "Ref": "vpcPrivateSubnet2RouteTable7280F23E" } } }, @@ -383,11 +383,11 @@ "vpcVPCGW7984C166": { "Type": "AWS::EC2::VPCGatewayAttachment", "Properties": { - "VpcId": { - "Ref": "vpcA2121C38" - }, "InternetGatewayId": { "Ref": "vpcIGWE57CBDCA" + }, + "VpcId": { + "Ref": "vpcA2121C38" } } }, @@ -473,6 +473,21 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "anotherSecret11974229": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "GenerateSecretString": {} + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ssm85049941": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "myString" + } + }, "myContainerExecutionRoleEBACF86C": { "Type": "AWS::IAM::Role", "Properties": { @@ -521,6 +536,53 @@ ] ] } + }, + { + "Action": [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetSecretValue" + ], + "Effect": "Allow", + "Resource": [ + { + "Ref": "anotherSecret11974229" + }, + { + "Ref": "mySecretE4D0A59C" + } + ] + }, + { + "Action": [ + "ssm:DescribeParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory", + "ssm:GetParameters" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/", + { + "Ref": "ssm85049941" + } + ] + ] + } } ], "Version": "2012-10-17" @@ -536,7 +598,6 @@ "ECSJobDefn48425E25": { "Type": "AWS::Batch::JobDefinition", "Properties": { - "Type": "container", "ContainerProperties": { "Environment": [ { @@ -582,6 +643,46 @@ "ValueFrom": { "Ref": "mySecretE4D0A59C" } + }, + { + "Name": "ANOTHER_ONE", + "ValueFrom": { + "Fn::Join": [ + "", + [ + { + "Ref": "anotherSecret11974229" + }, + "::bar:foo" + ] + ] + } + }, + { + "Name": "SSM_TIME", + "ValueFrom": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/", + { + "Ref": "ssm85049941" + } + ] + ] + } } ], "Ulimits": [ @@ -612,7 +713,8 @@ "EC2" ], "RetryStrategy": {}, - "Timeout": {} + "Timeout": {}, + "Type": "container" } }, "myFargateContainerExecutionRoleB9EB79EA": { @@ -678,7 +780,6 @@ "ECSFargateJobDefn327BE725": { "Type": "AWS::Batch::JobDefinition", "Properties": { - "Type": "container", "ContainerProperties": { "Environment": [], "EphemeralStorage": { @@ -739,7 +840,8 @@ "SchedulingPriority": 10, "Timeout": { "AttemptDurationSeconds": 600 - } + }, + "Type": "container" } }, "EcsDockerContainerExecutionRole7AA53A24": { @@ -841,7 +943,6 @@ "ECSDockerJobDefnF388CFCF": { "Type": "AWS::Batch::JobDefinition", "Properties": { - "Type": "container", "ContainerProperties": { "Environment": [], "ExecutionRoleArn": { @@ -869,7 +970,8 @@ "EC2" ], "RetryStrategy": {}, - "Timeout": {} + "Timeout": {}, + "Type": "container" } } }, diff --git a/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/tree.json b/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/tree.json index 45e4ea126882c..889845c017340 100644 --- a/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/tree.json @@ -45,9 +45,6 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", "aws:cdk:cloudformation:props": { - "vpcId": { - "Ref": "vpcA2121C38" - }, "availabilityZone": { "Fn::Select": [ 0, @@ -71,7 +68,10 @@ "key": "Name", "value": "stack/vpc/PublicSubnet1" } - ] + ], + "vpcId": { + "Ref": "vpcA2121C38" + } } }, "constructInfo": { @@ -93,15 +93,15 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", "aws:cdk:cloudformation:props": { - "vpcId": { - "Ref": "vpcA2121C38" - }, "tags": [ { "key": "Name", "value": "stack/vpc/PublicSubnet1" } - ] + ], + "vpcId": { + "Ref": "vpcA2121C38" + } } }, "constructInfo": { @@ -134,12 +134,12 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::Route", "aws:cdk:cloudformation:props": { - "routeTableId": { - "Ref": "vpcPublicSubnet1RouteTable48A2DF9B" - }, "destinationCidrBlock": "0.0.0.0/0", "gatewayId": { "Ref": "vpcIGWE57CBDCA" + }, + "routeTableId": { + "Ref": "vpcPublicSubnet1RouteTable48A2DF9B" } } }, @@ -174,15 +174,15 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::NatGateway", "aws:cdk:cloudformation:props": { - "subnetId": { - "Ref": "vpcPublicSubnet1Subnet2E65531E" - }, "allocationId": { "Fn::GetAtt": [ "vpcPublicSubnet1EIPDA49DCBE", "AllocationId" ] }, + "subnetId": { + "Ref": "vpcPublicSubnet1Subnet2E65531E" + }, "tags": [ { "key": "Name", @@ -212,9 +212,6 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", "aws:cdk:cloudformation:props": { - "vpcId": { - "Ref": "vpcA2121C38" - }, "availabilityZone": { "Fn::Select": [ 1, @@ -238,7 +235,10 @@ "key": "Name", "value": "stack/vpc/PublicSubnet2" } - ] + ], + "vpcId": { + "Ref": "vpcA2121C38" + } } }, "constructInfo": { @@ -260,15 +260,15 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", "aws:cdk:cloudformation:props": { - "vpcId": { - "Ref": "vpcA2121C38" - }, "tags": [ { "key": "Name", "value": "stack/vpc/PublicSubnet2" } - ] + ], + "vpcId": { + "Ref": "vpcA2121C38" + } } }, "constructInfo": { @@ -301,12 +301,12 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::Route", "aws:cdk:cloudformation:props": { - "routeTableId": { - "Ref": "vpcPublicSubnet2RouteTableEB40D4CB" - }, "destinationCidrBlock": "0.0.0.0/0", "gatewayId": { "Ref": "vpcIGWE57CBDCA" + }, + "routeTableId": { + "Ref": "vpcPublicSubnet2RouteTableEB40D4CB" } } }, @@ -341,15 +341,15 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::NatGateway", "aws:cdk:cloudformation:props": { - "subnetId": { - "Ref": "vpcPublicSubnet2Subnet009B674F" - }, "allocationId": { "Fn::GetAtt": [ "vpcPublicSubnet2EIP9B3743B1", "AllocationId" ] }, + "subnetId": { + "Ref": "vpcPublicSubnet2Subnet009B674F" + }, "tags": [ { "key": "Name", @@ -379,9 +379,6 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", "aws:cdk:cloudformation:props": { - "vpcId": { - "Ref": "vpcA2121C38" - }, "availabilityZone": { "Fn::Select": [ 0, @@ -405,7 +402,10 @@ "key": "Name", "value": "stack/vpc/PrivateSubnet1" } - ] + ], + "vpcId": { + "Ref": "vpcA2121C38" + } } }, "constructInfo": { @@ -427,15 +427,15 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", "aws:cdk:cloudformation:props": { - "vpcId": { - "Ref": "vpcA2121C38" - }, "tags": [ { "key": "Name", "value": "stack/vpc/PrivateSubnet1" } - ] + ], + "vpcId": { + "Ref": "vpcA2121C38" + } } }, "constructInfo": { @@ -468,12 +468,12 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::Route", "aws:cdk:cloudformation:props": { - "routeTableId": { - "Ref": "vpcPrivateSubnet1RouteTableB41A48CC" - }, "destinationCidrBlock": "0.0.0.0/0", "natGatewayId": { "Ref": "vpcPublicSubnet1NATGateway9C16659E" + }, + "routeTableId": { + "Ref": "vpcPrivateSubnet1RouteTableB41A48CC" } } }, @@ -498,9 +498,6 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", "aws:cdk:cloudformation:props": { - "vpcId": { - "Ref": "vpcA2121C38" - }, "availabilityZone": { "Fn::Select": [ 1, @@ -524,7 +521,10 @@ "key": "Name", "value": "stack/vpc/PrivateSubnet2" } - ] + ], + "vpcId": { + "Ref": "vpcA2121C38" + } } }, "constructInfo": { @@ -546,15 +546,15 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", "aws:cdk:cloudformation:props": { - "vpcId": { - "Ref": "vpcA2121C38" - }, "tags": [ { "key": "Name", "value": "stack/vpc/PrivateSubnet2" } - ] + ], + "vpcId": { + "Ref": "vpcA2121C38" + } } }, "constructInfo": { @@ -587,12 +587,12 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::Route", "aws:cdk:cloudformation:props": { - "routeTableId": { - "Ref": "vpcPrivateSubnet2RouteTable7280F23E" - }, "destinationCidrBlock": "0.0.0.0/0", "natGatewayId": { "Ref": "vpcPublicSubnet2NATGateway9B8AE11A" + }, + "routeTableId": { + "Ref": "vpcPrivateSubnet2RouteTable7280F23E" } } }, @@ -632,11 +632,11 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::EC2::VPCGatewayAttachment", "aws:cdk:cloudformation:props": { - "vpcId": { - "Ref": "vpcA2121C38" - }, "internetGatewayId": { "Ref": "vpcIGWE57CBDCA" + }, + "vpcId": { + "Ref": "vpcA2121C38" } } }, @@ -799,6 +799,55 @@ "version": "0.0.0" } }, + "anotherSecret": { + "id": "anotherSecret", + "path": "stack/anotherSecret", + "children": { + "Resource": { + "id": "Resource", + "path": "stack/anotherSecret/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SecretsManager::Secret", + "aws:cdk:cloudformation:props": { + "generateSecretString": {} + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_secretsmanager.CfnSecret", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_secretsmanager.Secret", + "version": "0.0.0" + } + }, + "ssm": { + "id": "ssm", + "path": "stack/ssm", + "children": { + "Resource": { + "id": "Resource", + "path": "stack/ssm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SSM::Parameter", + "aws:cdk:cloudformation:props": { + "type": "String", + "value": "myString" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ssm.CfnParameter", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ssm.StringParameter", + "version": "0.0.0" + } + }, "myContainer": { "id": "myContainer", "path": "stack/myContainer", @@ -878,6 +927,53 @@ ] ] } + }, + { + "Action": [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetSecretValue" + ], + "Effect": "Allow", + "Resource": [ + { + "Ref": "anotherSecret11974229" + }, + { + "Ref": "mySecretE4D0A59C" + } + ] + }, + { + "Action": [ + "ssm:DescribeParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory", + "ssm:GetParameters" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/", + { + "Ref": "ssm85049941" + } + ] + ] + } } ], "Version": "2012-10-17" @@ -931,7 +1027,6 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::Batch::JobDefinition", "aws:cdk:cloudformation:props": { - "type": "container", "containerProperties": { "image": "amazon/amazon-ecs-sample", "environment": [ @@ -967,6 +1062,46 @@ "valueFrom": { "Ref": "mySecretE4D0A59C" } + }, + { + "name": "ANOTHER_ONE", + "valueFrom": { + "Fn::Join": [ + "", + [ + { + "Ref": "anotherSecret11974229" + }, + "::bar:foo" + ] + ] + } + }, + { + "name": "SSM_TIME", + "valueFrom": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/", + { + "Ref": "ssm85049941" + } + ] + ] + } } ], "mountPoints": [ @@ -1007,7 +1142,8 @@ "EC2" ], "retryStrategy": {}, - "timeout": {} + "timeout": {}, + "type": "container" } }, "constructInfo": { @@ -1153,7 +1289,6 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::Batch::JobDefinition", "aws:cdk:cloudformation:props": { - "type": "container", "containerProperties": { "image": "amazon/amazon-ecs-sample", "environment": [], @@ -1214,7 +1349,8 @@ "schedulingPriority": 10, "timeout": { "attemptDurationSeconds": 600 - } + }, + "type": "container" } }, "constructInfo": { @@ -1422,7 +1558,6 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::Batch::JobDefinition", "aws:cdk:cloudformation:props": { - "type": "container", "containerProperties": { "image": { "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:8b518243ecbfcfd08b4734069e7e74ff97b7889dfde0a60d16e7bdc96e6c593b" @@ -1450,7 +1585,8 @@ "EC2" ], "retryStrategy": {}, - "timeout": {} + "timeout": {}, + "type": "container" } }, "constructInfo": { diff --git a/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.ts b/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.ts index 7dcf064a36cf8..986cc4e056efb 100644 --- a/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.ts +++ b/packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.ts @@ -2,12 +2,13 @@ import { Vpc } from 'aws-cdk-lib/aws-ec2'; import { ContainerImage, FargatePlatformVersion } from 'aws-cdk-lib/aws-ecs'; import * as efs from 'aws-cdk-lib/aws-efs'; import * as ecs from 'aws-cdk-lib/aws-ecs'; +import * as ssm from 'aws-cdk-lib/aws-ssm'; import { App, Duration, Size, Stack } from 'aws-cdk-lib'; import * as integ from '@aws-cdk/integ-tests-alpha'; import * as batch from '../lib'; import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; import * as path from 'path'; -import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; const app = new App(); const stack = new Stack(app, 'stack'); @@ -42,7 +43,12 @@ new batch.EcsJobDefinition(stack, 'ECSJobDefn', { softLimit: 10, }], secrets: { - MY_SECRET_ENV_VAR: new Secret(stack, 'mySecret'), + MY_SECRET_ENV_VAR: batch.Secret.fromSecretsManager(new secretsmanager.Secret(stack, 'mySecret')), + ANOTHER_ONE: batch.Secret.fromSecretsManagerVersion(new secretsmanager.Secret(stack, 'anotherSecret'), { + versionId: 'foo', + versionStage: 'bar', + }), + SSM_TIME: batch.Secret.fromSsmParameter(new ssm.StringParameter(stack, 'ssm', { stringValue: 'myString' })), }, }), });