Skip to content

Commit

Permalink
feat(AWS Lambda): Add support for secrets to images (serverless#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
insidewhy committed Apr 14, 2024
1 parent d09dc65 commit 5aabbfa
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 1 deletion.
5 changes: 4 additions & 1 deletion docs/providers/aws/guide/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,10 +310,11 @@ In service configuration, images can be configured via `provider.ecr.images`. To
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))
- `secrets`: With the `secrets` property, you can define arguments that will be passed to `docker build` command with `--secret` flag. They might be later referenced via `--mount=type=secret` within your `Dockerfile`. (See [Documentation](https://docs.docker.com/build/building/secrets/))
- `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.
When `uri` is defined for an image, `buildArgs`, `secrets`, `cacheFrom`, and `platform` cannot be defined.

Example configuration

Expand All @@ -329,6 +330,8 @@ provider:
file: Dockerfile.dev
buildArgs:
STAGE: ${opt:stage}
secrets:
aws: src=${env:HOME}/.aws/credentials
cacheFrom:
- my-image:latest
platform: linux/amd64
Expand Down
2 changes: 2 additions & 0 deletions docs/providers/aws/guide/serverless.yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,8 @@ provider:
file: Dockerfile.dev
buildArgs:
STAGE: ${sls:stage}
secrets:
aws: src=${env:HOME}/.aws/credentials
cacheFrom:
- my-image:latest
```
Expand Down
16 changes: 16 additions & 0 deletions lib/plugins/aws/provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,7 @@ class AwsProvider {
path: { type: 'string' },
file: { type: 'string' },
buildArgs: { type: 'object', additionalProperties: { type: 'string' } },
secrets: { type: 'object', additionalProperties: { type: 'string' } },
cacheFrom: { type: 'array', items: { type: 'string' } },
platform: { type: 'string' },
},
Expand Down Expand Up @@ -2204,6 +2205,7 @@ Object.defineProperties(
imagePath,
imageFilename,
buildArgs,
secrets,
cacheFrom,
platform,
scanOnPush,
Expand Down Expand Up @@ -2236,6 +2238,10 @@ Object.defineProperties(
.map((key) => `${key}=${buildArgs[key]}`)
.reduce((accumulator, current) => [...accumulator, '--build-arg', current], []);

const secretsArr = Object.keys(secrets)
.map((key) => `id=${key},${secrets[key]}`)
.reduce((accumulator, current) => [...accumulator, '--secret', current], []);

const cacheFromArr = cacheFrom.reduce(
(accumulator, current) => [...accumulator, '--cache-from', current],
[]
Expand All @@ -2248,6 +2254,7 @@ Object.defineProperties(
'-f',
pathToDockerfile,
...buildArgsArr,
...secretsArr,
...cacheFromArr,
imagePath,
];
Expand Down Expand Up @@ -2385,6 +2392,7 @@ Object.defineProperties(
const { imageUri, imageName } = resolveImageUriOrName();
const defaultDockerfile = 'Dockerfile';
const defaultBuildArgs = {};
const defaultBuildSecrets = {};
const defaultCacheFrom = [];
const defaultScanOnPush = false;
const defaultPlatform = '';
Expand Down Expand Up @@ -2430,6 +2438,12 @@ Object.defineProperties(
'ECR_IMAGE_BOTH_URI_AND_BUILDARGS_DEFINED_ERROR'
);
}
if (imageDefinedInProvider.uri && imageDefinedInProvider.secrets) {
throw new ServerlessError(
`The "secrets" property cannot be used with "uri" property "${imageName}"`,
'ECR_IMAGE_BOTH_URI_AND_SECRETS_DEFINED_ERROR'
);
}
if (imageDefinedInProvider.uri && imageDefinedInProvider.cacheFrom) {
throw new ServerlessError(
`The "cacheFrom" property cannot be used with "uri" property "${imageName}"`,
Expand All @@ -2448,6 +2462,7 @@ Object.defineProperties(
imagePath: imageDefinedInProvider.path,
imageFilename: imageDefinedInProvider.file || defaultDockerfile,
buildArgs: imageDefinedInProvider.buildArgs || defaultBuildArgs,
secrets: imageDefinedInProvider.secrets || defaultBuildSecrets,
cacheFrom: imageDefinedInProvider.cacheFrom || defaultCacheFrom,
platform: imageDefinedInProvider.platform || defaultPlatform,
scanOnPush: imageScanDefinedInProvider,
Expand All @@ -2463,6 +2478,7 @@ Object.defineProperties(
imagePath: imageDefinedInProvider,
imageFilename: defaultDockerfile,
buildArgs: imageDefinedInProvider.buildArgs || defaultBuildArgs,
secrets: imageDefinedInProvider.secrets || defaultBuildSecrets,
cacheFrom: imageDefinedInProvider.cacheFrom || defaultCacheFrom,
platform: imageDefinedInProvider.platform || defaultPlatform,
scanOnPush: imageScanDefinedInProvider,
Expand Down
92 changes: 92 additions & 0 deletions test/unit/lib/plugins/aws/provider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,37 @@ aws_secret_access_key = CUSTOMSECRET
);
});

it('should fail if `functions[].image` references image with both secrets and uri', async () => {
await expect(
runServerless({
fixture: 'function',
command: 'package',
configExt: {
provider: {
ecr: {
images: {
invalidimage: {
secrets: {
aws: 'src=/root/.aws/credentials',
},
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_SECRETS_DEFINED_ERROR'
);
});

it('should fail if `functions[].image` references image with both cacheFrom and uri', async () => {
await expect(
runServerless({
Expand Down Expand Up @@ -1721,6 +1752,67 @@ aws_secret_access_key = CUSTOMSECRET
]);
});

it('should work correctly when image is defined with `secrets` 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',
secrets: {
aws: 'src=/root/.aws/credentials',
},
},
},
},
},
},
});

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'),
'--secret',
'id=aws,src=/root/.aws/credentials',
'./',
]);
});

it('should work correctly when image is defined with `platform` set', async () => {
const awsRequestStubMap = {
...baseAwsRequestStubMap,
Expand Down

0 comments on commit 5aabbfa

Please sign in to comment.