Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(AWS Lambda): Add support for secrets to images (#12427) #12428

Open
wants to merge 1 commit into
base: v3
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
@@ -1,6 +1,5 @@
[![sfv4_00000](https://github.com/serverless/serverless/assets/2752551/6cf62477-7834-49dc-9a41-f71b76d70a18)](https://github.com/serverless/serverless/tree/v4.0)


insidewhy marked this conversation as resolved.
Show resolved Hide resolved
<h1></h1>
<br/>

Expand Down
5 changes: 4 additions & 1 deletion docs/providers/aws/guide/functions.md
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
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
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
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