Open alexeagle opened 1 year ago
Rough design notes:
We need the AWS provided "Runtime Interface Emulator" to be able to run lambda logic locally. See https://github.com/aws/aws-lambda-runtime-interface-emulator
WORKSPACE
something like
oci_pull(
name = "aws_lambda_nodejs",
digest = "sha256:715a39e5d7eb88bca7b25617b734087c12724178978401e038bd8e4964757dc0",
image = "public.ecr.aws/lambda/nodejs",
platforms = [
"linux/amd64",
"linux/arm64/v8",
],
)
# Bundle the function with its dependencies because AWS Lambda requires that
# external dependencies be bundled in a flat node_modules structure without
# symlinks, which rules_js (which uses a pnpm dependency layout) cannot produce.
# TODO: Look into layering dependencies into a separate archive as an optimization
# if the bundle file becomes too large.
# https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html
esbuild(
name = "bundle",
srcs = [":revert"],
config = {
"resolveExtensions": [".js"],
"banner": {
"js": "// Copyright 2022 Aspect Build Systems, Inc. All rights reserved.",
},
},
entry_point = "index.js",
minify = True,
output = "bundle.js",
platform = "node",
sourcemap = "inline",
)
# Copy bundle.js into a new directory so that we can rename it to index.js and not
# conflict with the application's transpiled index.js. AWS Lambda requires an index.js
# file in the root of the zip archive.
copy_to_directory(
name = "package",
srcs = [
"bundle.js",
],
replace_prefixes = {
"bundle.js": "index.js",
},
)
pkg_tar(
name = "tar",
srcs = [":package"],
package_dir = "/var/task",
strip_prefix = "package",
)
# See https://docs.aws.amazon.com/lambda/latest/dg/nodejs-image.html
oci_image(
name = "image",
base = "@aws_lambda_nodejs",
cmd = ["index.handler"],
tars = [":tar"],
)
We could probably provide a macro to make this convenient.
# Allow the image to be run locally for integration testing.
# See https://docs.aws.amazon.com/lambda/latest/dg/images-test.html#images-test-AWSbase
#
# bazel run //path/to:tarball
# docker run -p 9000:8080 --env ENVIRONMENT=INTEGRATION_TEST --env APP_ID=test-123 --env APP_WEBHOOK_SECRET=secretX --env APP_PRIVATE_KEY=keyX --rm revert:latest
#
# In another terminal:
# curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'
# -> {"statusCode":418,"body":""}
oci_tarball(
name = "tarball",
image = ":image",
repo_tags = ["revert:latest"],
)
And the user would then write a normal testcontainers-style test like this:
// Copyright 2022 Aspect Build Systems, Inc. All rights reserved.
const { spawnSync } = require('node:child_process');
import { join } from 'node:path';
import axios from 'axios';
import { GenericContainer, StartedTestContainer } from 'testcontainers';
const IMAGE_TARBALL = join(
process.env['TEST_SRCDIR']!,
process.env['TEST_WORKSPACE']!,
'path/to/tarball/tarball.tar'
);
describe('Revert lambda', () => {
let container: StartedTestContainer | undefined;
let endpoint: string;
beforeAll(async () => {
const res = spawnSync('docker', ['load', '-i', IMAGE_TARBALL]);
if (res.status) {
process.stderr.write(res.stderr);
throw new Error('failed to load docker image' + res.status);
}
container = await new GenericContainer('revert:latest')
.withExposedPorts(8080)
.withEnvironment({
// See comment in src/entrypoint.ts
ENVIRONMENT: 'INTEGRATION_TEST',
APP_ID: 'test-123',
APP_WEBHOOK_SECRET: 'secretX',
APP_PRIVATE_KEY: 'keyX',
})
.start();
const port = container.getMappedPort(8080);
endpoint = `http://localhost:${port}/2015-03-31/functions/function/invocations`;
}, /* On a busy machine, the docker operations can take longer than 5sec */ 50000);
afterAll(async () => {
await container?.stop();
});
it('Handles a request', async () => {
const resp = await axios.post(endpoint, {
/* empty post data */
});
// We return this code when INTEGRATION_TEST is in the environment.
expect(resp.data.statusCode).toBe(418);
});
});
Either deploy the same image we tested:
# Push the image to ECR.
# See "Deploying the image": https://docs.aws.amazon.com/lambda/latest/dg/typescript-image.html
oci_push(
name = "push",
image = ":image",
remote_tags = ":tags",
repository = "12345.dkr.ecr.us-west-2.amazonaws.com/revert-lambda",
)
or deploy a zip file:
# Production release is a zip-package.
# See https://docs.aws.amazon.com/lambda/latest/dg/nodejs-package.html
pkg_zip(
name = "zip",
srcs = [":index.js"],
out = "my-lambda.zip",
)
Then refer to that ZIP from a terraform deploy rule.
What is the current behavior?
AWS documents how to build, test, and release NodeJS lambdas.
They can be released as container images or zip files.
Describe the feature
Provide easy support in Bazel for doing all the build/test/release tasks related to NodeJS lambda.