Last active
April 4, 2024 23:03
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | |
}, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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