Skip to content

Commit

Permalink
feat(AWS Lambda): Add platform option for container images (#10237)
Browse files Browse the repository at this point in the history
Co-authored-by: Matthieu Napoli <matthieu@mnapoli.fr>
  • Loading branch information
zanemountcastle and mnapoli committed Nov 25, 2021
1 parent c9fefce commit 5b61b41
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 2 deletions.
11 changes: 10 additions & 1 deletion docs/providers/aws/guide/functions.md
Expand Up @@ -206,7 +206,15 @@ Alternatively lambda environment can be configured through docker images. Image

Serverless will create an ECR repository for your image, but it currently does not manage updates to it. An ECR repository is created only for new services or the first time that a function configured with an `image` is deployed. In service configuration, you can configure the ECR repository to scan for CVEs via the `provider.ecr.scanOnPush` property, which is `false` by default. (See [documentation](https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html))

In service configuration, images can be configured via `provider.ecr.images`. To define an image that will be built locally, you need to specify `path` property, which should point to valid docker context directory. Optionally, you can also set `file` to specify Dockerfile that should be used when building an image. It is also possible to define images that already exist in AWS ECR repository. In order to do that, you need to define `uri` property, which should follow `<account>.dkr.ecr.<region>.amazonaws.com/<repository>@<digest>` or `<account>.dkr.ecr.<region>.amazonaws.com/<repository>:<tag>` format. Additionally, with `buildArgs` property, you can define arguments that will be passed to `docker build` command with `--build-arg` flag. They might be later referenced via `ARG` within your `Dockerfile`. The `cacheFrom` property can be used to specify which images to use as a source for layer caching in the `docker build` command with `--cache-from` flag. When `uri` is defined for an image, `buildArgs` and `cacheFrom` cannot be defined.
In service configuration, images can be configured via `provider.ecr.images`. To define an image that will be built locally, you need to specify `path` property, which should point to valid docker context directory. Optionally, you can also set `file` to specify Dockerfile that should be used when building an image. It is also possible to define images that already exist in AWS ECR repository. In order to do that, you need to define `uri` property, which should follow `<account>.dkr.ecr.<region>.amazonaws.com/<repository>@<digest>` or `<account>.dkr.ecr.<region>.amazonaws.com/<repository>:<tag>` format.

Additionally, you can define arguments that will be passed to the `docker build` command via the following properties:

- `buildArgs`: With the `buildArgs` property, you can define arguments that will be passed to `docker build` command with `--build-arg` flag. They might be later referenced via `ARG` within your `Dockerfile`. (See [Documentation](https://docs.docker.com/engine/reference/builder/#arg))
- `cacheFrom`: The `cacheFrom` property can be used to specify which images to use as a source for layer caching in the `docker build` command with `--cache-from` flag. (See [Documentation](https://docs.docker.com/engine/reference/builder/#usage))
- `platform`: The `platform` property can be used to specify the architecture target in the `docker build` command with the `--platform` flag. If not specified, Docker will build for your computer's architecture by default. AWS Lambda typically uses `x86` architecture unless otherwise specified in the Lambda's runtime settings. In order to avoid runtime errors when building on an ARM-based machine (e.g. Apple M1 Mac), `linux/amd64` must be used here. The options for this flag are `linux/amd64` (`x86`-based Lambdas), `linux/arm64` (`arm`-based Lambdas), or `windows/amd64`. (See [Documentation](https://docs.docker.com/engine/reference/builder/#from))

When `uri` is defined for an image, `buildArgs`, `cacheFrom`, and `platform` cannot be defined.

Example configuration

Expand All @@ -224,6 +232,7 @@ provider:
STAGE: ${opt:stage}
cacheFrom:
- my-image:latest
platform: linux/amd64
anotherimage:
uri: 000000000000.dkr.ecr.sa-east-1.amazonaws.com/test-lambda-docker@sha256:6bb600b4d6e1d7cf521097177dd0c4e9ea373edb91984a505333be8ac9455d38
```
Expand Down
23 changes: 22 additions & 1 deletion lib/plugins/aws/provider.js
Expand Up @@ -994,6 +994,7 @@ class AwsProvider {
file: { type: 'string' },
buildArgs: { type: 'object', additionalProperties: { type: 'string' } },
cacheFrom: { type: 'array', additionalProperties: { type: 'string' } },
platform: { type: 'string' },
},
additionalProperties: false,
},
Expand Down Expand Up @@ -1953,7 +1954,15 @@ Object.defineProperties(
{ promise: true }
),
resolveImageUriAndShaFromPath: d(
async function ({ imageName, imagePath, imageFilename, buildArgs, cacheFrom, scanOnPush }) {
async function ({
imageName,
imagePath,
imageFilename,
buildArgs,
cacheFrom,
platform,
scanOnPush,
}) {
const imageProgress = progress.get(`containerImage:${imageName}`);
await this.ensureDockerIsAvailable();

Expand Down Expand Up @@ -1998,6 +2007,9 @@ Object.defineProperties(
imagePath,
];

// This is an optional argument, so we only append to the arguments if "platform" is specified.
if (platform !== '') buildDockerArgs.push(`--platform=${platform}`);

let imageSha;
try {
imageProgress.notice(`Building image "${imageName}"`);
Expand Down Expand Up @@ -2123,6 +2135,7 @@ Object.defineProperties(
const defaultBuildArgs = {};
const defaultCacheFrom = [];
const defaultScanOnPush = false;
const defaultPlatform = '';

if (imageUri) {
return await this.resolveImageUriAndShaFromUri(imageUri);
Expand Down Expand Up @@ -2171,13 +2184,20 @@ Object.defineProperties(
'ECR_IMAGE_BOTH_URI_AND_CACHEFROM_DEFINED_ERROR'
);
}
if (imageDefinedInProvider.uri && imageDefinedInProvider.platform) {
throw new ServerlessError(
`The "platform" property cannot be used with "uri" property "${imageName}"`,
'ECR_IMAGE_BOTH_URI_AND_PLATFORM_DEFINED_ERROR'
);
}
if (imageDefinedInProvider.path) {
return await this.resolveImageUriAndShaFromPath({
imageName,
imagePath: imageDefinedInProvider.path,
imageFilename: imageDefinedInProvider.file || defaultDockerfile,
buildArgs: imageDefinedInProvider.buildArgs || defaultBuildArgs,
cacheFrom: imageDefinedInProvider.cacheFrom || defaultCacheFrom,
platform: imageDefinedInProvider.platform || defaultPlatform,
scanOnPush: imageScanDefinedInProvider,
});
}
Expand All @@ -2192,6 +2212,7 @@ Object.defineProperties(
imageFilename: defaultDockerfile,
buildArgs: imageDefinedInProvider.buildArgs || defaultBuildArgs,
cacheFrom: imageDefinedInProvider.cacheFrom || defaultCacheFrom,
platform: imageDefinedInProvider.platform || defaultPlatform,
scanOnPush: imageScanDefinedInProvider,
});
},
Expand Down
147 changes: 147 additions & 0 deletions test/unit/lib/plugins/aws/provider.test.js
Expand Up @@ -842,6 +842,95 @@ aws_secret_access_key = CUSTOMSECRET
);
});

it('should fail if `functions[].image` references image with both buildArgs and uri', async () => {
await expect(
runServerless({
fixture: 'function',
command: 'package',
configExt: {
provider: {
ecr: {
images: {
invalidimage: {
buildArgs: {
TESTKEY: 'TESTVAL',
},
uri: '000000000000.dkr.ecr.sa-east-1.amazonaws.com/test-lambda-docker@sha256:6bb600b4d6e1d7cf521097177dd0c4e9ea373edb91984a505333be8ac9455d38',
},
},
},
},
functions: {
fnProviderInvalidImage: {
image: 'invalidimage',
},
},
},
})
).to.be.eventually.rejected.and.have.property(
'code',
'ECR_IMAGE_BOTH_URI_AND_BUILDARGS_DEFINED_ERROR'
);
});

it('should fail if `functions[].image` references image with both cacheFrom and uri', async () => {
await expect(
runServerless({
fixture: 'function',
command: 'package',
configExt: {
provider: {
ecr: {
images: {
invalidimage: {
cacheFrom: ['my-image:latest'],
uri: '000000000000.dkr.ecr.sa-east-1.amazonaws.com/test-lambda-docker@sha256:6bb600b4d6e1d7cf521097177dd0c4e9ea373edb91984a505333be8ac9455d38',
},
},
},
},
functions: {
fnProviderInvalidImage: {
image: 'invalidimage',
},
},
},
})
).to.be.eventually.rejected.and.have.property(
'code',
'ECR_IMAGE_BOTH_URI_AND_CACHEFROM_DEFINED_ERROR'
);
});

it('should fail if `functions[].image` references image with both platform and uri', async () => {
await expect(
runServerless({
fixture: 'function',
command: 'package',
configExt: {
provider: {
ecr: {
images: {
invalidimage: {
platform: 'TESTVAL',
uri: '000000000000.dkr.ecr.sa-east-1.amazonaws.com/test-lambda-docker@sha256:6bb600b4d6e1d7cf521097177dd0c4e9ea373edb91984a505333be8ac9455d38',
},
},
},
},
functions: {
fnProviderInvalidImage: {
image: 'invalidimage',
},
},
},
})
).to.be.eventually.rejected.and.have.property(
'code',
'ECR_IMAGE_BOTH_URI_AND_PLATFORM_DEFINED_ERROR'
);
});

it('should fail if `functions[].image` references image without path and uri', async () => {
await expect(
runServerless({
Expand Down Expand Up @@ -1664,6 +1753,64 @@ aws_secret_access_key = CUSTOMSECRET
]);
});

it('should work correctly when image is defined with `platform` set', async () => {
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
...baseAwsRequestStubMap.ECR,
describeRepositories: describeRepositoriesStub.resolves({
repositories: [{ repositoryUri }],
}),
createRepository: createRepositoryStub,
},
};
const {
awsNaming,
cfTemplate,
fixtureData: { servicePath: serviceDir },
} = await runServerless({
fixture: 'ecr',
command: 'package',
awsRequestStubMap,
modulesCacheStub,
configExt: {
provider: {
ecr: {
images: {
baseimage: {
path: './',
file: 'Dockerfile.dev',
platform: 'TESTVAL',
},
},
},
},
},
});

const functionCfLogicalId = awsNaming.getLambdaLogicalId('foo');
const functionCfConfig = cfTemplate.Resources[functionCfLogicalId].Properties;
const versionCfConfig = Object.values(cfTemplate.Resources).find(
(resource) =>
resource.Type === 'AWS::Lambda::Version' &&
resource.Properties.FunctionName.Ref === functionCfLogicalId
).Properties;

expect(functionCfConfig.Code.ImageUri).to.deep.equal(`${repositoryUri}@sha256:${imageSha}`);
expect(versionCfConfig.CodeSha256).to.equal(imageSha);
expect(describeRepositoriesStub).to.be.calledOnce;
expect(createRepositoryStub.notCalled).to.be.true;
expect(spawnExtStub).to.be.calledWith('docker', [
'build',
'-t',
`${awsNaming.getEcrRepositoryName()}:baseimage`,
'-f',
path.join(serviceDir, 'Dockerfile.dev'),
'./',
'--platform=TESTVAL',
]);
});

it('should work correctly when `functions[].image` is defined with explicit name', async () => {
const awsRequestStubMap = {
...baseAwsRequestStubMap,
Expand Down

0 comments on commit 5b61b41

Please sign in to comment.