Skip to content

Commit

Permalink
feat(AWS Lambda): Support referencing images with tags
Browse files Browse the repository at this point in the history
  • Loading branch information
pgrzesik committed Dec 29, 2020
1 parent cf2d475 commit 68b7ed5
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 8 deletions.
2 changes: 1 addition & 1 deletion docs/providers/aws/guide/functions.md
Expand Up @@ -199,7 +199,7 @@ See the documentation about [IAM](./iam.md) for function level IAM roles.

Alternatively lambda environment can be configured through docker images. Image published to AWS ECR registry can be referenced as lambda source (check [AWS Lambda – Container Image Support](https://aws.amazon.com/blogs/aws/new-for-aws-lambda-container-image-support/)).

In service configuration existing AWS ECR image should be referenced via `image` property (which should follow `<account>.dkr.ecr.<region>.amazonaws.com/<repository>@<digest>` format). `handler` and `runtime` properties are not supported in such case.
In service configuration existing AWS ECR image should be referenced via `image` property (which should follow `<account>.dkr.ecr.<region>.amazonaws.com/<repository>@<digest>` or `<account>.dkr.ecr.<region>.amazonaws.com/<repository>:<tag>` format). `handler` and `runtime` properties are not supported in such case.

Example configuration:

Expand Down
55 changes: 50 additions & 5 deletions lib/plugins/aws/package/compile/functions.js
Expand Up @@ -8,6 +8,8 @@ const _ = require('lodash');
const path = require('path');
const deepSortObjectByKey = require('../../../../utils/deepSortObjectByKey');
const getHashForFilePath = require('../lib/getHashForFilePath');
const memoizeeMethods = require('memoizee/methods');
const d = require('d');

class AwsCompileFunctions {
constructor(serverless, options) {
Expand Down Expand Up @@ -156,6 +158,15 @@ class AwsCompileFunctions {
);
}

let functionImageUri;
let functionImageSha;

if (functionObject.image) {
({ functionImageUri, functionImageSha } = await this.resolveImageUriAndSha(
functionObject.image
));
}

// publish these properties to the platform
functionObject.memory =
functionObject.memorySize || this.serverless.service.provider.memorySize || 1024;
Expand Down Expand Up @@ -199,7 +210,7 @@ class AwsCompileFunctions {
}/${artifactFilePath.split(path.sep).pop()}`;
functionResource.Properties.Runtime = functionObject.runtime;
} else {
functionResource.Properties.Code.ImageUri = functionObject.image;
functionResource.Properties.Code.ImageUri = functionImageUri;
functionResource.Properties.PackageType = 'Image';
}
functionResource.Properties.FunctionName = functionObject.name;
Expand Down Expand Up @@ -427,10 +438,8 @@ class AwsCompileFunctions {

const versionResource = this.cfLambdaVersionTemplate();

if (functionObject.image) {
versionResource.Properties.CodeSha256 = functionObject.image.slice(
functionObject.image.lastIndexOf('@sha256:') + '@sha256:'.length
);
if (functionImageSha) {
versionResource.Properties.CodeSha256 = functionImageSha;
} else {
const fileHash = await getHashForFilePath(artifactFilePath);
versionResource.Properties.CodeSha256 = fileHash;
Expand Down Expand Up @@ -711,4 +720,40 @@ function extractLayerConfigurationsFromFunction(functionProperties, cfTemplate)
return layerConfigurations;
}

Object.defineProperties(
AwsCompileFunctions.prototype,
memoizeeMethods({
resolveImageUriAndSha: d(
async function(image) {
const providedImageSha = image.split('@')[1];
if (providedImageSha) {
return {
functionImageSha: providedImageSha.slice('sha256:'.length),
functionImageUri: image,
};
}

const [repositoryName, imageTag] = image.split('/')[1].split(':');
const registryId = image.split('.')[0];
const describeImagesResponse = await this.provider.request('ECR', 'describeImages', {
imageIds: [
{
imageTag,
},
],
repositoryName,
registryId,
});
const imageDigest = describeImagesResponse.imageDetails[0].imageDigest;
const functionImageUri = `${image.split(':')[0]}@${imageDigest}`;
return {
functionImageUri,
functionImageSha: imageDigest.slice('sha256:'.length),
};
},
{ promise: true }
),
})
);

module.exports = AwsCompileFunctions;
2 changes: 1 addition & 1 deletion lib/plugins/aws/provider.js
Expand Up @@ -951,7 +951,7 @@ class AwsProvider {
image: {
type: 'string',
pattern:
'^\\d+\\.dkr\\.ecr\\.[a-z0-9-]+..amazonaws.com\\/[^@:]+@sha256:[a-f0-9]{64}$',
'^\\d+\\.dkr\\.ecr\\.[a-z0-9-]+..amazonaws.com\\/([^@]+)|([^@:]+@sha256:[a-f0-9]{64})$',
},
kmsKeyArn: { $ref: '#/definitions/awsKmsArn' },
layers: { $ref: '#/definitions/awsLambdaLayers' },
Expand Down
32 changes: 31 additions & 1 deletion test/unit/lib/plugins/aws/package/compile/functions.test.js
Expand Up @@ -1831,6 +1831,13 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => {
let serverless;
let serviceConfig;
let iamRolePolicyStatements;
const imageDigestFromECR =
'sha256:2e6b10a4b1ca0f6d3563a8a1f034dde7c4d7c93b50aa91f24311765d0822186b';
const awsRequestStubMap = {
ECR: {
describeImages: { imageDetails: [{ imageDigest: imageDigestFromECR }] },
},
};

before(async () => {
const {
Expand Down Expand Up @@ -1873,8 +1880,12 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => {
image:
'000000000000.dkr.ecr.sa-east-1.amazonaws.com/test-lambda-docker@sha256:6bb600b4d6e1d7cf521097177dd0c4e9ea373edb91984a505333be8ac9455d38',
},
fnImageWithTag: {
image: '000000000000.dkr.ecr.sa-east-1.amazonaws.com/test-lambda-docker:stable',
},
},
},
awsRequestStubMap,
});
cfResources = cfTemplate.Resources;
naming = awsNaming;
Expand Down Expand Up @@ -2173,7 +2184,7 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => {
});
});

it('should support `functions[].image`', () => {
it('should support `functions[].image` with sha', () => {
const functionServiceConfig = serviceConfig.functions.fnImage;
const functionCfLogicalId = naming.getLambdaLogicalId('fnImage');
const functionCfConfig = cfResources[functionCfLogicalId].Properties;
Expand All @@ -2194,6 +2205,25 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => {
).Properties;
expect(versionCfConfig.CodeSha256).to.equal(imageDigestSha);
});

it('should support `functions[].image` with tag', () => {
const functionServiceConfig = serviceConfig.functions.fnImageWithTag;
const functionCfLogicalId = naming.getLambdaLogicalId('fnImageWithTag');
const functionCfConfig = cfResources[functionCfLogicalId].Properties;

expect(functionCfConfig.Code).to.deep.equal({
ImageUri: `${functionServiceConfig.image.split(':')[0]}@${imageDigestFromECR}`,
});
expect(functionCfConfig).to.not.have.property('Handler');
expect(functionCfConfig).to.not.have.property('Runtime');

const versionCfConfig = Object.values(cfResources).find(
resource =>
resource.Type === 'AWS::Lambda::Version' &&
resource.Properties.FunctionName.Ref === functionCfLogicalId
).Properties;
expect(versionCfConfig.CodeSha256).to.equal(imageDigestFromECR.slice('sha256:'.length));
});
});

describe('Validation', () => {
Expand Down

0 comments on commit 68b7ed5

Please sign in to comment.