Add runtime layer for Bun on AWS Lambda (#2009)

This commit is contained in:
Ashcon Partovi
2023-02-22 10:34:16 -08:00
committed by GitHub
parent 2dc85c4e45
commit ee60a5c55c
10 changed files with 1174 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
// HACK: https://github.com/oven-sh/bun/issues/2081
process.stdout.getWindowSize = () => [80, 80];
process.stderr.getWindowSize = () => [80, 80];
import { createReadStream, createWriteStream } from "node:fs";
import { join } from "node:path";
import { Command, Flags } from "@oclif/core";
import JSZip from "jszip";
export class BuildCommand extends Command {
static summary = "Build a custom Lambda layer for Bun.";
static flags = {
arch: Flags.string({
description: "The architecture type to support.",
options: ["x64", "aarch64"],
default: "aarch64",
}),
release: Flags.string({
description: "The release of Bun to install.",
default: "latest",
}),
url: Flags.string({
description: "A custom URL to download Bun.",
exclusive: ["release"],
}),
output: Flags.file({
exists: false,
default: async () => "bun-lambda-layer.zip",
}),
layer: Flags.string({
description: "The name of the Lambda layer.",
multiple: true,
default: ["bun"],
}),
region: Flags.string({
description: "The region to publish the layer.",
multiple: true,
default: [],
}),
public: Flags.boolean({
description: "If the layer should be public.",
default: false,
}),
};
async run() {
const result = await this.parse(BuildCommand);
const { flags } = result;
this.debug("Options:", flags);
const { arch, release, url, output } = flags;
const { href } = new URL(url ?? `https://bun.sh/download/${release}/linux/${arch}?avx2=true`);
this.log("Downloading...", href);
const response = await fetch(href, {
headers: {
"User-Agent": "bun-lambda",
},
});
if (response.url !== href) {
this.debug("Redirected URL:", response.url);
}
this.debug("Response:", response.status, response.statusText);
if (!response.ok) {
const reason = await response.text();
this.error(reason, { exit: 1 });
}
this.log("Extracting...");
const buffer = await response.arrayBuffer();
let archive;
try {
archive = await JSZip.loadAsync(buffer);
} catch (cause) {
this.debug(cause);
this.error("Failed to unzip file:", { exit: 1 });
}
this.debug("Extracted archive:", Object.keys(archive.files));
const bun = archive.filter((_, { dir, name }) => !dir && name.endsWith("bun"))[0];
if (!bun) {
this.error("Failed to find executable in zip", { exit: 1 });
}
const cwd = bun.name.split("/")[0];
archive = archive.folder(cwd) ?? archive;
for (const filename of ["bootstrap", "runtime.ts"]) {
const path = join(__dirname, "..", filename);
archive.file(filename, createReadStream(path));
}
this.log("Saving...", output);
archive
.generateNodeStream({
streamFiles: true,
compression: "DEFLATE",
compressionOptions: {
level: 9,
},
})
.pipe(createWriteStream(output));
this.log("Saved");
}
}
await BuildCommand.run(process.argv.slice(2));

View File

@@ -0,0 +1,91 @@
import { spawnSync } from "node:child_process";
import { BuildCommand } from "./build-layer";
export class PublishCommand extends BuildCommand {
static summary = "Publish a custom Lambda layer for Bun.";
#aws(args: string[]): string {
this.debug("$", "aws", ...args);
const { status, stdout, stderr } = spawnSync("aws", args, {
stdio: "pipe",
});
const result = stdout.toString("utf-8").trim();
if (status === 0) {
return result;
}
const reason = stderr.toString("utf-8").trim() || result;
throw new Error(`aws ${args.join(" ")} exited with ${status}: ${reason}`);
}
async run() {
const { flags } = await this.parse(PublishCommand);
this.debug("Options:", flags);
try {
const version = this.#aws(["--version"]);
this.debug("AWS CLI:", version);
} catch (error) {
this.debug(error);
this.error(
"Install the `aws` CLI to continue: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html",
{ exit: 1 },
);
}
const { layer, region, arch, output, public: isPublic } = flags;
if (region.includes("*")) {
// prettier-ignore
const result = this.#aws([
"ec2",
"describe-regions",
"--query", "Regions[].RegionName",
"--output", "json"
]);
region.length = 0;
for (const name of JSON.parse(result)) {
region.push(name);
}
} else if (!region.length) {
// prettier-ignore
region.push(this.#aws([
"configure",
"get",
"region"
]));
}
this.log("Publishing...");
for (const regionName of region) {
for (const layerName of layer) {
// prettier-ignore
const result = this.#aws([
"lambda",
"publish-layer-version",
"--layer-name", layerName,
"--region", regionName,
"--description", "Bun is an incredibly fast JavaScript runtime, bundler, transpiler, and package manager.",
"--license-info", "MIT",
"--compatible-architectures", arch === "x64" ? "x86_64" : "arm64",
"--compatible-runtimes", "provided.al2", "provided",
"--zip-file", `fileb://${output}`,
"--output", "json",
]);
const { LayerVersionArn } = JSON.parse(result);
this.log("Published", LayerVersionArn);
if (isPublic) {
// prettier-ignore
this.#aws([
"lambda",
"add-layer-version-permission",
"--layer-name", layerName,
"--region", regionName,
"--version-number", LayerVersionArn.split(":").pop(),
"--statement-id", `${layerName}-public`,
"--action", "lambda:GetLayerVersion",
"--principal", "*",
]);
}
}
}
this.log("Done");
}
}
await PublishCommand.run(process.argv.slice(2));