Skip to content

Commit adc0368

Browse files
authored
feat(aws-ecr-assets): support the --platform option when building docker images (#20439)
This PR adds support for specifying the desired build platform when building docker images (ie: build an arm64 image on an amd64/x86_64 host). Closes #12472 This PR does NOT touch Lambda builders, only ECR assets. #16770 attempted to implement support for ECR and Lambda but was abandoned. Meanwhile #16858 implemented lambda platform support. This implements the ECR side I have run `yarn integ` ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)? * [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 4b837df commit adc0368

File tree

22 files changed

+318
-10
lines changed

22 files changed

+318
-10
lines changed

packages/@aws-cdk/aws-ecr-assets/README.md

+12
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@ const asset = new DockerImageAsset(this, 'MyBuildImage', {
9292
})
9393
```
9494

95+
You can optionally pass an alternate platform to the `docker build` command by specifying
96+
the `platform` property:
97+
98+
```ts
99+
import { DockerImageAsset, Platform } from '@aws-cdk/aws-ecr-assets';
100+
101+
const asset = new DockerImageAsset(this, 'MyBuildImage', {
102+
directory: path.join(__dirname, 'my-image'),
103+
platform: Platform.LINUX_ARM64,
104+
})
105+
```
106+
95107
## Images from Tarball
96108

97109
Images are loaded from a local tarball, uploaded to ECR by the CDK toolkit and/or your app's CI-CD pipeline, and can be

packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts

+46
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,36 @@ export class NetworkMode {
5656
private constructor(public readonly mode: string) {}
5757
}
5858

59+
/**
60+
* platform supported by docker
61+
*/
62+
export class Platform {
63+
/**
64+
* Build for linux/amd64
65+
*/
66+
public static readonly LINUX_AMD64 = new Platform('linux/amd64');
67+
68+
/**
69+
* Build for linux/arm64
70+
*/
71+
public static readonly LINUX_ARM64 = new Platform('linux/arm64');
72+
73+
/**
74+
* Used to specify a custom platform
75+
* Use this if the platform name is not yet supported by the CDK.
76+
*
77+
* @param platform The platform to use for docker build
78+
*/
79+
public static custom(platform: string) {
80+
return new Platform(platform);
81+
}
82+
83+
/**
84+
* @param platform The platform to use for docker build
85+
*/
86+
private constructor(public readonly platform: string) {}
87+
}
88+
5989
/**
6090
* Options to control invalidation of `DockerImageAsset` asset hashes
6191
*/
@@ -101,6 +131,13 @@ export interface DockerImageAssetInvalidationOptions {
101131
* @default true
102132
*/
103133
readonly networkMode?: boolean;
134+
135+
/**
136+
* Use `platform` while calculating the asset hash
137+
*
138+
* @default true
139+
*/
140+
readonly platform?: boolean;
104141
}
105142

106143
/**
@@ -153,6 +190,13 @@ export interface DockerImageAssetOptions extends FingerprintOptions, FileFingerp
153190
*/
154191
readonly networkMode?: NetworkMode;
155192

193+
/**
194+
* Platform to build for. _Requires Docker Buildx_.
195+
*
196+
* @default - no platform specified (the current machine architecture will be used)
197+
*/
198+
readonly platform?: Platform;
199+
156200
/**
157201
* Options to control which parameters are used to invalidate the asset hash.
158202
*
@@ -286,6 +330,7 @@ export class DockerImageAsset extends CoreConstruct implements IAsset {
286330
if (props.invalidation?.file !== false && props.file) { extraHash.file = props.file; }
287331
if (props.invalidation?.repositoryName !== false && props.repositoryName) { extraHash.repositoryName = props.repositoryName; }
288332
if (props.invalidation?.networkMode !== false && props.networkMode) { extraHash.networkMode = props.networkMode; }
333+
if (props.invalidation?.platform !== false && props.platform) { extraHash.platform = props.platform; }
289334

290335
// add "salt" to the hash in order to invalidate the image in the upgrade to
291336
// 1.21.0 which removes the AdoptedRepository resource (and will cause the
@@ -318,6 +363,7 @@ export class DockerImageAsset extends CoreConstruct implements IAsset {
318363
dockerFile: props.file,
319364
sourceHash: staging.assetHash,
320365
networkMode: props.networkMode?.mode,
366+
platform: props.platform?.platform,
321367
});
322368

323369
this.repository = ecr.Repository.fromRepositoryName(this, 'Repository', location.repositoryName);
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,33 @@
1+
#!/usr/bin/python
2+
import sys
3+
import textwrap
4+
import http.server
5+
import socketserver
6+
7+
PORT = 8000
8+
9+
10+
class Handler(http.server.SimpleHTTPRequestHandler):
11+
def do_GET(self):
12+
self.send_response(200)
13+
self.send_header('Content-Type', 'text/html')
14+
self.end_headers()
15+
self.wfile.write(textwrap.dedent('''\
16+
<!doctype html>
17+
<html><head><title>It works</title></head>
18+
<body>
19+
<h1>Hello from the integ test container</h1>
20+
<p>This container got built and started as part of the integ test.</p>
21+
<img src="https://media.giphy.com/media/nFjDu1LjEADh6/giphy.gif">
22+
</body>
23+
''').encode('utf-8'))
24+
25+
26+
def main():
27+
httpd = http.server.HTTPServer(("", PORT), Handler)
28+
print("serving at port", PORT)
29+
httpd.serve_forever()
30+
31+
32+
if __name__ == '__main__':
33+
main()
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"version":"17.0.0"}
1+
{"version":"20.0.0"}

packages/@aws-cdk/aws-ecr-assets/test/assets-docker.integ.snapshot/integ-assets-docker.template.json

+42
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,48 @@
7474
]
7575
]
7676
}
77+
},
78+
"ImageUri2": {
79+
"Value": {
80+
"Fn::Join": [
81+
"",
82+
[
83+
{
84+
"Ref": "AWS::AccountId"
85+
},
86+
".dkr.ecr.",
87+
{
88+
"Ref": "AWS::Region"
89+
},
90+
".",
91+
{
92+
"Ref": "AWS::URLSuffix"
93+
},
94+
"/aws-cdk/assets:0a3355be12051c9984bf2b0b2bba4e6ea535968e5b6e7396449701732fe5ed14"
95+
]
96+
]
97+
}
98+
},
99+
"ImageUri3": {
100+
"Value": {
101+
"Fn::Join": [
102+
"",
103+
[
104+
{
105+
"Ref": "AWS::AccountId"
106+
},
107+
".dkr.ecr.",
108+
{
109+
"Ref": "AWS::Region"
110+
},
111+
".",
112+
{
113+
"Ref": "AWS::URLSuffix"
114+
},
115+
"/aws-cdk/assets:394b24fcdc153a83b1fc400bf2e812ee67e3a5ffafdf977d531cfe2187d95f38"
116+
]
117+
]
118+
}
77119
}
78120
}
79121
}

packages/@aws-cdk/aws-ecr-assets/test/assets-docker.integ.snapshot/integ.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"version": "18.0.0",
2+
"version": "20.0.0",
33
"testCases": {
4-
"aws-ecr-assets/test/integ.assets-docker": {
4+
"integ.assets-docker": {
55
"stacks": [
66
"integ-assets-docker"
77
],

packages/@aws-cdk/aws-ecr-assets/test/assets-docker.integ.snapshot/manifest.json

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "17.0.0",
2+
"version": "20.0.0",
33
"artifacts": {
44
"Tree": {
55
"type": "cdk:tree",
@@ -26,6 +26,18 @@
2626
"path": "asset.0a3355be12051c9984bf2b0b2bba4e6ea535968e5b6e7396449701732fe5ed14",
2727
"sourceHash": "0a3355be12051c9984bf2b0b2bba4e6ea535968e5b6e7396449701732fe5ed14"
2828
}
29+
},
30+
{
31+
"type": "aws:cdk:asset",
32+
"data": {
33+
"repositoryName": "aws-cdk/assets",
34+
"imageTag": "394b24fcdc153a83b1fc400bf2e812ee67e3a5ffafdf977d531cfe2187d95f38",
35+
"id": "394b24fcdc153a83b1fc400bf2e812ee67e3a5ffafdf977d531cfe2187d95f38",
36+
"packaging": "container-image",
37+
"path": "asset.394b24fcdc153a83b1fc400bf2e812ee67e3a5ffafdf977d531cfe2187d95f38",
38+
"sourceHash": "394b24fcdc153a83b1fc400bf2e812ee67e3a5ffafdf977d531cfe2187d95f38",
39+
"platform": "linux/arm64"
40+
}
2941
}
3042
],
3143
"/integ-assets-docker/MyUser/Resource": [
@@ -45,6 +57,18 @@
4557
"type": "aws:cdk:logicalId",
4658
"data": "ImageUri"
4759
}
60+
],
61+
"/integ-assets-docker/ImageUri2": [
62+
{
63+
"type": "aws:cdk:logicalId",
64+
"data": "ImageUri2"
65+
}
66+
],
67+
"/integ-assets-docker/ImageUri3": [
68+
{
69+
"type": "aws:cdk:logicalId",
70+
"data": "ImageUri3"
71+
}
4872
]
4973
},
5074
"displayName": "integ-assets-docker"

packages/@aws-cdk/aws-ecr-assets/test/assets-docker.integ.snapshot/tree.json

+44-2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,32 @@
6868
"version": "0.0.0"
6969
}
7070
},
71+
"DockerImage3": {
72+
"id": "DockerImage3",
73+
"path": "integ-assets-docker/DockerImage3",
74+
"children": {
75+
"Staging": {
76+
"id": "Staging",
77+
"path": "integ-assets-docker/DockerImage3/Staging",
78+
"constructInfo": {
79+
"fqn": "@aws-cdk/core.AssetStaging",
80+
"version": "0.0.0"
81+
}
82+
},
83+
"Repository": {
84+
"id": "Repository",
85+
"path": "integ-assets-docker/DockerImage3/Repository",
86+
"constructInfo": {
87+
"fqn": "@aws-cdk/aws-ecr.RepositoryBase",
88+
"version": "0.0.0"
89+
}
90+
}
91+
},
92+
"constructInfo": {
93+
"fqn": "@aws-cdk/aws-ecr-assets.DockerImageAsset",
94+
"version": "0.0.0"
95+
}
96+
},
7197
"MyUser": {
7298
"id": "MyUser",
7399
"path": "integ-assets-docker/MyUser",
@@ -99,8 +125,8 @@
99125
{
100126
"Action": [
101127
"ecr:BatchCheckLayerAvailability",
102-
"ecr:BatchGetImage",
103-
"ecr:GetDownloadUrlForLayer"
128+
"ecr:GetDownloadUrlForLayer",
129+
"ecr:BatchGetImage"
104130
],
105131
"Effect": "Allow",
106132
"Resource": {
@@ -164,6 +190,22 @@
164190
"fqn": "@aws-cdk/core.CfnOutput",
165191
"version": "0.0.0"
166192
}
193+
},
194+
"ImageUri2": {
195+
"id": "ImageUri2",
196+
"path": "integ-assets-docker/ImageUri2",
197+
"constructInfo": {
198+
"fqn": "@aws-cdk/core.CfnOutput",
199+
"version": "0.0.0"
200+
}
201+
},
202+
"ImageUri3": {
203+
"id": "ImageUri3",
204+
"path": "integ-assets-docker/ImageUri3",
205+
"constructInfo": {
206+
"fqn": "@aws-cdk/core.CfnOutput",
207+
"version": "0.0.0"
208+
}
167209
}
168210
},
169211
"constructInfo": {

packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import * as fs from 'fs';
2-
import * as path from 'path';
31
import { Template } from '@aws-cdk/assertions';
42
import * as iam from '@aws-cdk/aws-iam';
53
import { describeDeprecated, testDeprecated, testFutureBehavior } from '@aws-cdk/cdk-build-tools';
64
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
75
import { App, DefaultStackSynthesizer, IgnoreMode, Lazy, LegacyStackSynthesizer, Stack, Stage } from '@aws-cdk/core';
86
import * as cxapi from '@aws-cdk/cx-api';
9-
import { DockerImageAsset, NetworkMode } from '../lib';
7+
import * as fs from 'fs';
8+
import * as path from 'path';
9+
import { DockerImageAsset, NetworkMode, Platform } from '../lib';
1010

1111
/* eslint-disable quote-props */
1212

@@ -156,6 +156,20 @@ describe('image asset', () => {
156156
expect(assetMetadata && (assetMetadata.data as cxschema.ContainerImageAssetMetadataEntry).networkMode).toEqual('default');
157157
});
158158

159+
testFutureBehavior('with platform', flags, App, (app) => {
160+
// GIVEN
161+
const stack = new Stack(app);
162+
// WHEN
163+
new DockerImageAsset(stack, 'Image', {
164+
directory: path.join(__dirname, 'demo-image'),
165+
platform: Platform.LINUX_ARM64,
166+
});
167+
168+
// THEN
169+
const assetMetadata = stack.node.metadataEntry.find(({ type }) => type === cxschema.ArtifactMetadataEntryType.ASSET);
170+
expect(assetMetadata && (assetMetadata.data as cxschema.ContainerImageAssetMetadataEntry).platform).toEqual('linux/arm64');
171+
});
172+
159173
testFutureBehavior('asset.repository.grantPull can be used to grant a principal permissions to use the image', flags, App, (app) => {
160174
// GIVEN
161175
const stack = new Stack(app);

packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts

+8
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,18 @@ const asset2 = new assets.DockerImageAsset(stack, 'DockerImage2', {
1919
directory: path.join(__dirname, 'demo-image'),
2020
});
2121

22+
const asset3 = new assets.DockerImageAsset(stack, 'DockerImage3', {
23+
directory: path.join(__dirname, 'demo-image'),
24+
platform: assets.Platform.LINUX_ARM64,
25+
});
26+
2227
const user = new iam.User(stack, 'MyUser');
2328
asset.repository.grantPull(user);
2429
asset2.repository.grantPull(user);
30+
asset3.repository.grantPull(user);
2531

2632
new cdk.CfnOutput(stack, 'ImageUri', { value: asset.imageUri });
33+
new cdk.CfnOutput(stack, 'ImageUri2', { value: asset2.imageUri });
34+
new cdk.CfnOutput(stack, 'ImageUri3', { value: asset3.imageUri });
2735

2836
app.synth();

packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts

+9
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ export interface DockerImageSource {
7171
* @default - no networking mode specified
7272
*/
7373
readonly networkMode?: string;
74+
75+
/**
76+
* Platform to build for. _Requires Docker Buildx_.
77+
*
78+
* Specify this property to build images on a specific platform/architecture.
79+
*
80+
* @default - current machine platform
81+
*/
82+
readonly platform?: string;
7483
}
7584

7685
/**

0 commit comments

Comments
 (0)