Skip to content

Commit b3d0d57

Browse files
authored
fix(batch): JobDefinition's ContainerDefinition's Image is synthesized with [Object object] (#25466)
We were using `Tokenization.resolve()`, which uses a generic token resolver that does `${token1}${token2}` instead of creating an `Fn::Split` object. The only reason that token existed was to mutate the L1 object after synthesis so subclasses could fill in the `executionRole`. The `executionRole` was meant to be optional for EC2 jobs and required for Fargate jobs. Setting the `loggingConfig` being defined would force it to be defined it for EC2 Jobs. However, the `loggingConfig` was always defined, because it was set to a `Lazy` (which could result in `undefined`, but that doesn't matter: it was defined during synthesis). So the `executionRole` was always being set: ``` this.executionRole = props.executionRole ?? (this.logDriverConfig ? createExecutionRole(this, 'ExecutionRole') : undefined); ``` This PR makes it clear that the `executionRole` is created by default. Closes #25250 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent b76c182 commit b3d0d57

File tree

13 files changed

+624
-197
lines changed

13 files changed

+624
-197
lines changed

Diff for: docs/DESIGN_GUIDELINES.md

+21
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,27 @@ information that can be obtained from the stack trace.
15231523

15241524
* Do not use FnSub
15251525

1526+
### Lazys
1527+
1528+
Do not use a `Lazy` to perform a mutation on the construct tree. For example:
1529+
1530+
```ts
1531+
constructor(scope: Scope, id: string, props: MyConstructProps) {
1532+
this.lazyProperty = Lazy.any({
1533+
produce: () => {
1534+
return props.logging.bind(this, this);
1535+
},
1536+
});
1537+
}
1538+
```
1539+
1540+
`bind()` methods mutate the construct tree, and should not be called from a callback
1541+
in a `Lazy`.
1542+
1543+
* The why:
1544+
- `Lazy`s are called after the construct tree has already been sythesized. Mutating it
1545+
at this point could have not-obvious consequences.
1546+
15261547
## Documentation
15271548

15281549
Documentation style (copy from official AWS docs) No need to Capitalize Resource

Diff for: packages/@aws-cdk/aws-batch-alpha/lib/ecs-container-definition.ts

+36-70
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as ecs from 'aws-cdk-lib/aws-ecs';
22
import { IFileSystem } from 'aws-cdk-lib/aws-efs';
33
import * as iam from 'aws-cdk-lib/aws-iam';
44
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
5-
import { DefaultTokenResolver, Lazy, PhysicalName, Size, StringConcat, Tokenization } from 'aws-cdk-lib';
5+
import { Lazy, PhysicalName, Size } from 'aws-cdk-lib';
66
import { Construct, IConstruct } from 'constructs';
77
import { CfnJobDefinition } from 'aws-cdk-lib/aws-batch';
88
import { LinuxParameters } from './linux-parameters';
@@ -237,6 +237,7 @@ export interface HostVolumeOptions extends EcsVolumeOptions {
237237
*/
238238
readonly hostPath?: string;
239239
}
240+
240241
/**
241242
* Creates a Host volume. This volume will persist on the host at the specified `hostPath`.
242243
* If the `hostPath` is not specified, Docker will choose the host path. In this case,
@@ -306,6 +307,13 @@ export interface IEcsContainerDefinition extends IConstruct {
306307
*/
307308
readonly environment?: { [key:string]: string };
308309

310+
/**
311+
* The role used by Amazon ECS container and AWS Fargate agents to make AWS API calls on your behalf.
312+
*
313+
* @see https://docs.aws.amazon.com/batch/latest/userguide/execution-IAM-role.html
314+
*/
315+
readonly executionRole: iam.IRole;
316+
309317
/**
310318
* The role that the container can assume.
311319
*
@@ -411,6 +419,15 @@ export interface EcsContainerDefinitionProps {
411419
*/
412420
readonly environment?: { [key:string]: string };
413421

422+
/**
423+
* The role used by Amazon ECS container and AWS Fargate agents to make AWS API calls on your behalf.
424+
*
425+
* @see https://docs.aws.amazon.com/batch/latest/userguide/execution-IAM-role.html
426+
*
427+
* @default - a Role will be created
428+
*/
429+
readonly executionRole?: iam.IRole;
430+
414431
/**
415432
* The role that the container can assume.
416433
*
@@ -474,6 +491,7 @@ abstract class EcsContainerDefinitionBase extends Construct implements IEcsConta
474491
public readonly memory: Size;
475492
public readonly command?: string[];
476493
public readonly environment?: { [key:string]: string };
494+
public readonly executionRole: iam.IRole;
477495
public readonly jobRole?: iam.IRole;
478496
public readonly linuxParameters?: LinuxParameters;
479497
public readonly logDriverConfig?: ecs.LogDriverConfig;
@@ -482,8 +500,6 @@ abstract class EcsContainerDefinitionBase extends Construct implements IEcsConta
482500
public readonly user?: string;
483501
public readonly volumes: EcsVolume[];
484502

485-
public abstract readonly executionRole?: iam.IRole;
486-
487503
private readonly imageConfig: ecs.ContainerImageConfig;
488504

489505
constructor(scope: Construct, id: string, props: EcsContainerDefinitionProps) {
@@ -493,52 +509,40 @@ abstract class EcsContainerDefinitionBase extends Construct implements IEcsConta
493509
this.cpu = props.cpu;
494510
this.command = props.command;
495511
this.environment = props.environment;
512+
this.executionRole = props.executionRole ?? createExecutionRole(this, 'ExecutionRole');
496513
this.jobRole = props.jobRole;
497514
this.linuxParameters = props.linuxParameters;
498515
this.memory = props.memory;
499516

500-
// Lazy so this.executionRole can be filled by subclasses
501-
this.logDriverConfig = Lazy.any({
502-
produce: () => {
503-
if (props.logging) {
504-
return props.logging.bind(this, {
505-
...this as any,
506-
// TS!
507-
taskDefinition: {
508-
obtainExecutionRole: () => this.executionRole,
509-
},
510-
});
511-
}
512-
513-
return undefined;
514-
},
515-
}) as any;
517+
if (props.logging) {
518+
this.logDriverConfig = props.logging.bind(this, {
519+
...this as any,
520+
// TS!
521+
taskDefinition: {
522+
obtainExecutionRole: () => this.executionRole,
523+
},
524+
});
525+
}
516526

517527
this.readonlyRootFilesystem = props.readonlyRootFilesystem ?? false;
518528
this.secrets = props.secrets;
519529
this.user = props.user;
520530
this.volumes = props.volumes ?? [];
521531

522-
// Lazy so this.executionRole can be filled by subclasses
523-
this.imageConfig = Lazy.any({
524-
produce: () => props.image.bind(this, {
525-
...this as any,
526-
taskDefinition: {
527-
obtainExecutionRole: () => this.executionRole,
528-
},
529-
}),
530-
}) as any;
532+
this.imageConfig = props.image.bind(this, {
533+
...this as any,
534+
taskDefinition: {
535+
obtainExecutionRole: () => this.executionRole,
536+
},
537+
});
531538
}
532539

533540
/**
534541
* @internal
535542
*/
536543
public _renderContainerDefinition(): CfnJobDefinition.ContainerPropertiesProperty {
537544
return {
538-
image: Tokenization.resolve(this.imageConfig, {
539-
scope: this,
540-
resolver: new DefaultTokenResolver(new StringConcat()),
541-
}).imageName,
545+
image: this.imageConfig.imageName,
542546
command: this.command,
543547
environment: Object.keys(this.environment ?? {}).map((envKey) => ({
544548
name: envKey,
@@ -792,15 +796,6 @@ export interface EcsEc2ContainerDefinitionProps extends EcsContainerDefinitionPr
792796
* @default - no gpus
793797
*/
794798
readonly gpu?: number;
795-
796-
/**
797-
* The role used by Amazon ECS container and AWS Fargate agents to make AWS API calls on your behalf.
798-
*
799-
* @see https://docs.aws.amazon.com/batch/latest/userguide/execution-IAM-role.html
800-
*
801-
* @default - a Role will be created if logging is specified, no role otherwise
802-
*/
803-
readonly executionRole?: iam.IRole;
804799
}
805800

806801
/**
@@ -811,21 +806,11 @@ export class EcsEc2ContainerDefinition extends EcsContainerDefinitionBase implem
811806
public readonly ulimits: Ulimit[];
812807
public readonly gpu?: number;
813808

814-
/**
815-
* The role used by Amazon ECS container and AWS Fargate agents to make AWS API calls on your behalf.
816-
*
817-
* @see https://docs.aws.amazon.com/batch/latest/userguide/execution-IAM-role.html
818-
*
819-
* @default - a Role will be created if logging is specified, no role otherwise
820-
*/
821-
public readonly executionRole?: iam.IRole;
822-
823809
constructor(scope: Construct, id: string, props: EcsEc2ContainerDefinitionProps) {
824810
super(scope, id, props);
825811
this.privileged = props.privileged;
826812
this.ulimits = props.ulimits ?? [];
827813
this.gpu = props.gpu;
828-
this.executionRole = props.executionRole ?? (this.logDriverConfig ? createExecutionRole(this, 'ExecutionRole') : undefined);
829814
}
830815

831816
/**
@@ -919,15 +904,6 @@ export interface EcsFargateContainerDefinitionProps extends EcsContainerDefiniti
919904
* @default LATEST
920905
*/
921906
readonly fargatePlatformVersion?: ecs.FargatePlatformVersion;
922-
923-
/**
924-
* The role used by Amazon ECS container and AWS Fargate agents to make AWS API calls on your behalf.
925-
*
926-
* @see https://docs.aws.amazon.com/batch/latest/userguide/execution-IAM-role.html
927-
*
928-
* @default - a Role will be created
929-
*/
930-
readonly executionRole?: iam.IRole;
931907
}
932908

933909
/**
@@ -937,20 +913,10 @@ export class EcsFargateContainerDefinition extends EcsContainerDefinitionBase im
937913
public readonly fargatePlatformVersion?: ecs.FargatePlatformVersion;
938914
public readonly assignPublicIp?: boolean;
939915

940-
/**
941-
* The role used by Amazon ECS container and AWS Fargate agents to make AWS API calls on your behalf.
942-
*
943-
* @see https://docs.aws.amazon.com/batch/latest/userguide/execution-IAM-role.html
944-
*
945-
* @default - a Role will be created
946-
*/
947-
public readonly executionRole: iam.IRole;
948-
949916
constructor(scope: Construct, id: string, props: EcsFargateContainerDefinitionProps) {
950917
super(scope, id, props);
951918
this.assignPublicIp = props.assignPublicIp;
952919
this.fargatePlatformVersion = props.fargatePlatformVersion;
953-
this.executionRole = props.executionRole ?? createExecutionRole(this, 'ExecutionRole');
954920
}
955921

956922
/**

Diff for: packages/@aws-cdk/aws-batch-alpha/test/ecs-container-definition.test.ts

+81
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Template } from 'aws-cdk-lib/assertions';
33
import * as path from 'path';
44
import { Vpc } from 'aws-cdk-lib/aws-ec2';
55
import * as ecs from 'aws-cdk-lib/aws-ecs';
6+
import * as ecr from 'aws-cdk-lib/aws-ecr';
67
import * as efs from 'aws-cdk-lib/aws-efs';
78
import { ArnPrincipal, Role } from 'aws-cdk-lib/aws-iam';
89
import * as logs from 'aws-cdk-lib/aws-logs';
@@ -11,6 +12,7 @@ import { Size, Stack } from 'aws-cdk-lib';
1112
import { EcsContainerDefinitionProps, EcsEc2ContainerDefinition, EcsFargateContainerDefinition, EcsJobDefinition, EcsVolume, IEcsEc2ContainerDefinition, LinuxParameters, UlimitName } from '../lib';
1213
import { CfnJobDefinitionProps } from 'aws-cdk-lib/aws-batch';
1314
import { capitalizePropertyNames } from './utils';
15+
import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';
1416

1517
// GIVEN
1618
const defaultContainerProps: EcsContainerDefinitionProps = {
@@ -528,6 +530,85 @@ describe.each([EcsEc2ContainerDefinition, EcsFargateContainerDefinition])('%p',
528530
},
529531
});
530532
});
533+
534+
test('correctly renders docker images', () => {
535+
// WHEN
536+
new EcsJobDefinition(stack, 'ECSJobDefn', {
537+
container: new ContainerDefinition(stack, 'EcsContainer', {
538+
...defaultContainerProps,
539+
image: ecs.ContainerImage.fromDockerImageAsset(new DockerImageAsset(stack, 'dockerImageAsset', {
540+
directory: path.join(__dirname, 'batchjob-image'),
541+
})),
542+
}),
543+
});
544+
545+
// THEN
546+
Template.fromStack(stack).hasResourceProperties('AWS::Batch::JobDefinition', {
547+
...pascalCaseExpectedProps,
548+
ContainerProperties: {
549+
...pascalCaseExpectedProps.ContainerProperties,
550+
Image: {
551+
'Fn::Sub': '${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:8b518243ecbfcfd08b4734069e7e74ff97b7889dfde0a60d16e7bdc96e6c593b',
552+
},
553+
},
554+
});
555+
});
556+
557+
test('correctly renders images from repositories', () => {
558+
// GIVEN
559+
const repo = new ecr.Repository(stack, 'Repo');
560+
561+
// WHEN
562+
new EcsJobDefinition(stack, 'ECSJobDefn', {
563+
container: new ContainerDefinition(stack, 'EcsContainer', {
564+
...defaultContainerProps,
565+
image: ecs.ContainerImage.fromEcrRepository(repo, 'my-tag'),
566+
}),
567+
});
568+
569+
// THEN
570+
Template.fromStack(stack).hasResourceProperties('AWS::Batch::JobDefinition', {
571+
...pascalCaseExpectedProps,
572+
ContainerProperties: {
573+
...pascalCaseExpectedProps.ContainerProperties,
574+
Image: {
575+
'Fn::Join': [
576+
'',
577+
[
578+
{
579+
'Fn::Select': [
580+
4,
581+
{
582+
'Fn::Split': [
583+
':',
584+
{ 'Fn::GetAtt': ['Repo02AC86CF', 'Arn'] },
585+
],
586+
},
587+
],
588+
},
589+
'.dkr.ecr.',
590+
{
591+
'Fn::Select': [
592+
3,
593+
{
594+
'Fn::Split': [
595+
':',
596+
{ 'Fn::GetAtt': ['Repo02AC86CF', 'Arn'] },
597+
],
598+
},
599+
],
600+
},
601+
'.',
602+
{ Ref: 'AWS::URLSuffix' },
603+
'/',
604+
{ Ref: 'Repo02AC86CF' },
605+
':my-tag',
606+
],
607+
],
608+
},
609+
},
610+
});
611+
});
531612
});
532613

533614
describe('EC2 containers', () => {

Diff for: packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/BatchEcsJobDefinitionTestDefaultTestDeployAssertE5BAAC9B.assets.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "30.1.0",
2+
"version": "31.0.0",
33
"files": {
44
"21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": {
55
"source": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FROM public.ecr.aws/lambda/python:3.6
2+
EXPOSE 8000
3+
WORKDIR /src
4+
ADD . /src
5+
CMD python3 index.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/python
2+
import os
3+
import pprint
4+
5+
print('Hello from Batch!')
6+
pprint.pprint(dict(os.environ))
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"version":"30.1.0"}
1+
{"version":"31.0.0"}

Diff for: packages/@aws-cdk/aws-batch-alpha/test/integ.ecs-job-definition.js.snapshot/integ.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "30.1.0",
2+
"version": "31.0.0",
33
"testCases": {
44
"BatchEcsJobDefinitionTest/DefaultTest": {
55
"stacks": [

0 commit comments

Comments
 (0)