Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/guides/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ 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))

You can also configure an ECR lifecycle policy to automatically clean up old images by setting `provider.ecr.maxImageCount` to a positive integer. When set, images exceeding this count will be expired. (See [documentation](https://docs.aws.amazon.com/AmazonECR/latest/userguide/LifecyclePolicies.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, you can define arguments that will be passed to the `docker build` command via the following properties:
Expand All @@ -313,6 +315,7 @@ provider:
name: aws
ecr:
scanOnPush: true
maxImageCount: 10
images:
baseimage:
path: ./path/to/context
Expand Down Expand Up @@ -382,7 +385,7 @@ functions:
- flag
```

During the first deployment when locally built images are used, Framework will automatically create a dedicated ECR repository to store these images, with name `serverless-<service>-<stage>`. Currently, the Framework will not remove older versions of images uploaded to ECR as they still might be in use by versioned functions. During `sls remove`, the created ECR repository will be removed. During deployment, Framework will attempt to `docker login` to ECR if needed. Depending on your local configuration, docker authorization token might be stored unencrypted. Please refer to documentation for more details: https://docs.docker.com/engine/reference/commandline/login/#credentials-store
During the first deployment when locally built images are used, Framework will automatically create a dedicated ECR repository to store these images, with name `serverless-<service>-<stage>`. By default, older versions of images uploaded to ECR are not removed as they still might be in use by versioned functions. To automatically expire old images, set `provider.ecr.maxImageCount` to limit the number of images retained in the repository. During `sls remove`, the created ECR repository will be removed. During deployment, Framework will attempt to `docker login` to ECR if needed. Depending on your local configuration, docker authorization token might be stored unencrypted. Please refer to documentation for more details: https://docs.docker.com/engine/reference/commandline/login/#credentials-store

## Instruction set architecture

Expand Down
1 change: 1 addition & 0 deletions docs/guides/serverless.yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ Configure [deployment via Docker images](./functions.md#referencing-container-im
provider:
ecr:
scanOnPush: true
maxImageCount: 10 # Max number of images to retain in ECR (enables lifecycle policy)
# Definitions of images that later can be referenced by key in `function.image`
images:
baseimage:
Expand Down
39 changes: 37 additions & 2 deletions lib/plugins/aws/provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,7 @@ class AwsProvider {
type: 'object',
properties: {
scanOnPush: { type: 'boolean' },
maxImageCount: { type: 'integer', minimum: 1 },
images: {
type: 'object',
patternProperties: {
Expand Down Expand Up @@ -2337,7 +2338,7 @@ Object.defineProperties(
{ promise: true }
),
getOrCreateEcrRepository: d(
async function (scanOnPush) {
async function (scanOnPush, maxImageCount) {
const registryId = await this.getAccountId();
const repositoryName = this.naming.getEcrRepositoryName();
let repositoryUri;
Expand All @@ -2357,6 +2358,27 @@ Object.defineProperties(
});
repositoryUri = result.repository.repositoryUri;
}

// Set ECR Lifecycle policy. See https://docs.aws.amazon.com/AmazonECR/latest/userguide/LifecyclePolicies.html
if (maxImageCount > 0) {
await this.request('ECR', 'putLifecyclePolicy', {
repositoryName,
lifecyclePolicyText: JSON.stringify({
rules: [
{
rulePriority: 1,
action: { type: 'expire' },
selection: {
tagStatus: 'any',
countType: 'imageCountMoreThan',
countNumber: maxImageCount,
},
},
],
}),
});
}

return {
repositoryUri,
repositoryName,
Expand All @@ -2375,6 +2397,7 @@ Object.defineProperties(
platform,
provenance,
scanOnPush,
maxImageCount,
}) {
const imageProgress = progress.get(`containerImage:${imageName}`);
await this.ensureDockerIsAvailable();
Expand All @@ -2395,7 +2418,10 @@ Object.defineProperties(
);
}

const { repositoryUri, repositoryName } = await this.getOrCreateEcrRepository(scanOnPush);
const { repositoryUri, repositoryName } = await this.getOrCreateEcrRepository(
scanOnPush,
maxImageCount
);

const localTag = `${repositoryName}:${imageName}`;
const remoteTag = `${repositoryUri}:${imageName}`;
Expand Down Expand Up @@ -2561,6 +2587,7 @@ Object.defineProperties(
const defaultScanOnPush = false;
const defaultPlatform = '';
const defaultProvenance = '';
const defaultMaxImageCount = 0;

if (imageUri) {
return await this.resolveImageUriAndShaFromUri(imageUri);
Expand All @@ -2577,6 +2604,12 @@ Object.defineProperties(
defaultScanOnPush
);

const maxImageCountProvider = _.get(
this.serverless.service.provider,
'ecr.maxImageCount',
defaultMaxImageCount
);

if (!imageDefinedInProvider) {
throw new ServerlessError(
`Referenced "${imageName}" not defined in "provider.ecr.images"`,
Expand Down Expand Up @@ -2638,6 +2671,7 @@ Object.defineProperties(
platform: imageDefinedInProvider.platform || defaultPlatform,
provenance: imageDefinedInProvider.provenance || defaultProvenance,
scanOnPush: imageScanDefinedInProvider,
maxImageCount: maxImageCountProvider,
});
}
return await this.resolveImageUriAndShaFromUri(imageDefinedInProvider.uri);
Expand All @@ -2655,6 +2689,7 @@ Object.defineProperties(
platform: imageDefinedInProvider.platform || defaultPlatform,
provenance: imageDefinedInProvider.provenance || defaultProvenance,
scanOnPush: imageScanDefinedInProvider,
maxImageCount: maxImageCountProvider,
});
},
{ promise: true }
Expand Down
44 changes: 44 additions & 0 deletions test/unit/lib/plugins/aws/provider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,7 @@ aws_secret_access_key = CUSTOMSECRET
const describeRepositoriesStub = sinon.stub();
const createRepositoryStub = sinon.stub();
const createRepositoryStubScanOnPush = sinon.stub();
const putLifecyclePolicyStub = sinon.stub();
const baseAwsRequestStubMap = {
STS: {
getCallerIdentity: {
Expand Down Expand Up @@ -1420,6 +1421,49 @@ aws_secret_access_key = CUSTOMSECRET
expect(versionCfConfig.CodeSha256).to.equal(imageSha);
expect(describeRepositoriesStub).to.be.calledOnce;
expect(createRepositoryStub).to.be.calledOnce;
expect(putLifecyclePolicyStub).to.not.have.been.called;
});

it('should set ECR lifecycle policy correctly', async () => {
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
...baseAwsRequestStubMap.ECR,
describeRepositories: describeRepositoriesStub.throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
createRepository: createRepositoryStub.resolves({ repository: { repositoryUri } }),
putLifecyclePolicy: putLifecyclePolicyStub.resolves(),
},
};

await runServerless({
fixture: 'ecr',
command: 'package',
awsRequestStubMap,
modulesCacheStub,
configExt: {
provider: {
ecr: {
maxImageCount: 10,
},
},
},
});

expect(JSON.parse(putLifecyclePolicyStub.args[0][0].lifecyclePolicyText)).to.deep.equal({
rules: [
{
rulePriority: 1,
action: { type: 'expire' },
selection: {
tagStatus: 'any',
countType: 'imageCountMoreThan',
countNumber: 10,
},
},
],
});
});

it('should login and retry when docker push fails with no basic auth credentials error', async () => {
Expand Down