Skip to content

Instantly share code, notes, and snippets.

@spalladino
Created September 27, 2020 19:06
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 spalladino/12026da83c6a8bd04b20ebf37893602b to your computer and use it in GitHub Desktop.
Save spalladino/12026da83c6a8bd04b20ebf37893602b to your computer and use it in GitHub Desktop.
Deploy a subset of AWS SAM functions bypassing CloudFormation for a faster development cycle
#! /usr/bin/env node
const TEMPLATE_FILENAME = 'template.yaml';
const WEBPACK_CONFIG = './api/webpack.config.js';
const WEBPACK_CONTEXT = './api';
const STACKNAME = process.env.STACKNAME;
const { Lambda, CloudFormation } = require('aws-sdk');
const { yamlParse } = require('yaml-cfn');
const { readFileSync } = require('fs');
const { difference, keys, uniq, pick, pickBy, toPairs } = require('lodash');
const { basename, resolve, dirname, extname } = require('path');
const { promisify } = require('util');
const exec = promisify(require('child_process').exec);
const webpack = require('webpack');
function loadFunctions(fname) {
// Parse the template file and extract anything that is a Serverless::Function
// We compare the logical name against the filter provided by the user
const template = yamlParse(readFileSync(TEMPLATE_FILENAME, 'utf8'));
const functions = toPairs(pickBy(template.Resources, (resource, name) => (
name.toLowerCase().includes(fname.toLowerCase()) && resource.Type === 'AWS::Serverless::Function'
))).map(([name, fn]) => ({ Name: name, ...fn.Properties }));
if (functions.length === 0) {
console.error(`No functions found to deploy that match ${fname}`);
return;
}
console.error(`Found ${functions.length} function(s) to deploy:\n${functions.map(f => `- ${f.Name}`).join('\n')}\n`);
return functions;
}
async function compileFunctions(functions) {
if (process.env.SKIP_COMPILE) {
console.error(`Skipping compilation\n`);
return;
}
// Load config and set context to backend folder
const config = require(resolve(WEBPACK_CONFIG));
config.context = resolve(WEBPACK_CONTEXT);
// Remove unneeded entrypoints, we only care about the functions we are deploying
const entryPoints = uniq(functions.map(f => basename(f.CodeUri)));
config.entry = pick(config.entry, entryPoints);
// Alert if there is a function for which we are missing an entrypoint
const missing = difference(keys(config.entry), entryPoints);
if (missing.length > 0) throw new Error(`Could not find entrypoints ${missing.join(',')} in ${WEBPACK_CONFIG}`);
console.error(`Compiling entrypoint(s):\n${entryPoints.map(e => `- ${e}`).join('\n')}`);
// Setup compiler
// We add a hook to zip assets after compiling, so we upload them to lambda
// We cannot use compression-webpack-plugin because it doesn't support zip
// We cannot use zip-webpack-plugin because it doesn't support multiple entrypoints
// (see https://github.com/erikdesjardins/zip-webpack-plugin/issues/19)
const compiler = webpack(config);
compiler.hooks.assetEmitted.tapPromise('zip', async (filename) => {
if (extname(filename) === '.map') return;
const cwd = resolve(config.output.path, dirname(filename));
await exec(`zip index.zip index.js`, { cwd });
});
// Compile! Webpack does not throw if there are compilation errors
// They need to be extracted from the stats object returned
const stats = await promisify(compiler.run.bind(compiler))();
if (stats.hasErrors()) {
console.error(`Errors during compilation`);
console.error(stats.toJson().errors);
process.exit(1);
} else if (stats.hasWarnings()) {
console.error(`Warnings during compilation`);
console.error(stats.toJson().warnings);
} else {
console.error(`Compilation successful!\n`);
}
}
async function deployFunctions(functions) {
const lambda = new Lambda();
const cfn = new CloudFormation();
// We use describeStackResource to go from the logical name used in the stack (eg UserCreateFunction)
// to the actual name of the created resource (eg mystack-dev-user-create-fn) set in Properties.FunctionName
// We cannot directly use the value of Properties.FunctionName since it may be missing or may be a Fn::Sub
const resolvedFunctions = await Promise.all(functions.map(async (fn) => {
const response = await cfn.describeStackResource({ LogicalResourceId: fn.Name, StackName: STACKNAME }).promise();
const resourceId = response.StackResourceDetail.PhysicalResourceId;
return { ...fn, ResourceId: resourceId };
}));
// For each function, we upload the zip with the code generated by webpack
console.log(`Uploading functions:\n${resolvedFunctions.map(f => `- ${f.ResourceId}`).join('\n')}\n`);
await Promise.all(resolvedFunctions.map(async (fn) => {
const zipFilePath = resolve(fn.CodeUri, 'index.zip');
await lambda.updateFunctionCode({ FunctionName: fn.ResourceId, ZipFile: readFileSync(zipFilePath) }).promise();
}));
console.log(`Upload finished!`);
}
async function main() {
const fname = process.argv[2];
if (!fname) throw new Error(`Provide a substring of the function names to deploy`);
if (!STACKNAME) throw new Error(`STACKNAME is missing from env`);
if (!process.env.AWS_PROFILE) throw new Error(`AWS_PROFILE is missing from env`);
if (!process.env.AWS_REGION) throw new Error(`AWS_REGION is missing from env`);
const functions = loadFunctions(fname);
if (functions.length === 0) return;
await compileFunctions(functions);
await deployFunctions(functions);
}
main().catch(err => { console.error('Error:', err.message); process.exit(1); });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment