Skip to content

Instantly share code, notes, and snippets.

@1oglop1
Last active April 4, 2024 23:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 1oglop1/1b4f878dab0db7e112464dedc149618d to your computer and use it in GitHub Desktop.
Save 1oglop1/1b4f878dab0db7e112464dedc149618d to your computer and use it in GitHub Desktop.
pulumi build lambda function with esbuild and set zip hash to be deterministic
export const exampleLambda = new ESBuildNodeFunction('example', {
entry: path.resolve(__dirname, 'handler.ts'),
role: role.arn,
timeout: 8,
memorySize: 128,
environment: {
variables: {
DB_HOST: db.address,
DB_USER: db.username,
DB_PASS: dbPassword,
DB_PORT: db.port.apply((port) => port.toString()),
DB_NAME: db.dbName,
},
},
vpcConfig: {
securityGroupIds: [lambdaSecurityGroup.id],
subnetIds: publicSubnetIds,
},
esbuild: {
external: [...knexExternals],
},
});
// source https://archive.pulumi.com/t/16648692/are-there-any-existing-modules-or-tools-to-glob-a-few-files-#4e8c0225-123e-4dde-bbfd-4ca92a7fc640
import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';
import * as esbuild from 'esbuild';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import * as crypto from 'node:crypto';
import * as fflate from 'fflate';
import deepmerge from 'deepmerge';
interface NodeFunctionArgs extends aws.lambda.FunctionArgs {
/**
* The file path for the lambda
*/
entry: string;
/**
* A custom esbuild configuration
*/
esbuild?: esbuild.BuildOptions;
/**
* Zip the bundled function into a zip archive called lambda.zip
* @default true
*/
zip?: boolean;
}
const esbuildDefaultOpts: esbuild.BuildOptions = {
bundle: true,
minify: false,
sourcemap: true,
platform: 'node',
format: 'cjs',
target: 'esnext',
// outExtension: { '.js': '.mjs' },
};
export class ESBuildNodeFunction extends aws.lambda.Function {
constructor(name: string, args: NodeFunctionArgs) {
const options = deepmerge<esbuild.BuildOptions>(
esbuildDefaultOpts,
args.esbuild || {}
);
const outdir = fs.mkdtempSync(path.join(os.tmpdir(), '/'));
const { outputFiles } = esbuild.buildSync({
entryPoints: [args.entry],
...options,
outdir, // important or all filenames become <stdout>
write: false,
// plugins: [commonjs()], // don't work in sync builds
});
const filenamesAndContents = Object.values(outputFiles).reduce(
function collectFilenameAndContents(acc, curr) {
return { ...acc, [path.basename(curr.path)]: curr.contents };
},
{}
);
// the mtime causes the zip file hash to be deterministic
// 0 should work but Pulumi has some weird validation where "date not
// in range 1980-2099", so I picked the best date during that range
const zipContent = fflate.zipSync(filenamesAndContents, {
os: 0,
mtime: '1987-12-26',
});
const zipFile = path.join(outdir, 'lambda.zip');
// we have to write this to disk because FileArchive requires a zip
// and using StringAsset doesn't support reading in the buffer even
// when it's a string for whatever reason
fs.writeFileSync(zipFile, zipContent);
// handler format is file-without-extension.export-name so the .ts
// messes this up and we need to remove it from the filename
const entry = path.basename(args.entry, path.extname(args.entry));
const method = args.handler || 'default';
const handler = `${entry}.${method}`;
// Check that the expected method is exported by the module otherwise it
// bundles, then lambda fails to call it and its hard to spot until runtime
import(args.entry).then((mod) => {
if (mod[method as string] === undefined) {
throw new Error(`${method} is not exported by ${args.entry}`);
}
});
// this will override NODE_OPTIONS if set by the caller so really
// this needs more complicated logic to add this option to
// NODE_OPTIONS if present
const environment = deepmerge<typeof args.environment>(
args.environment || {},
{
variables: {
NODE_OPTIONS: options.sourcemap ? '--enable-source-maps' : '',
},
}
);
super(name, {
architectures: ['arm64'],
runtime: 'nodejs20.x',
...args,
code: new pulumi.asset.FileArchive(zipFile),
handler,
packageType: 'Zip',
environment,
sourceCodeHash: crypto
.createHash('sha256')
.update(zipContent)
.digest('base64'),
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment