kitak blog

Kみたいなエンジニアになりたいブログ

Webpack を利用した複数の Lambda 関数の管理

こんなかんじでやってみたらどうでしょ、という話。

AWS Lambda を中心にいわゆるサーバーレスのアプリケーションを構築するには、複数の Lambda 関数を作成することになります。
普通のウェブアプリケーションであれば、ひとつのリポジトリでコードを管理して、起動時に実行環境でアプリケーションのコードをまとめて読み込みますが、Lambda 関数を組み合わせてアプリケーションを構築する場合は、アプリケーションの機能ごとに実行環境が分かれることになり、それに応じてコードも分割しなければいけない難しさがあります(その制約を受け入れるメリットのひとつは、リクエスト数が突然跳ねた場合にスケールすることです)。

各 Lambda 関数は「一つのことだけうまくやる」ことを意識して書きますが、設定や汎用的なロジック、API クライアントなど Lambda 関数に跨った共通のコードをどうやって扱うか、という課題があります。
関数単位や共通のロジックで複数のリポジトリに分けるアプローチは様々な作業が煩雑になる(開発時のリポジトリの行き来、イシューやバージョンの管理、各リポジトリの更新...)ので、なるだけ単一のリポジトリでいきたいです。

ひとつのやり方として、Webpack で各 Lambda 関数の index.js をバンドルファイルとして生成するのがいいんじゃないかと思ってます。

Webpack の設定はこんなかんじです。Webpack のバージョンは 4.5.0 です。

const path = require('path');

module.exports = {
    mode: 'none',
    target: 'node',
    entry: {
        foo: path.resolve(__dirname, './src/foo.js'),
        bar: path.resolve(__dirname, './src/bar.js'),
    },
    output: {
        filename: '[name]/index.js',
        path: path.resolve(__dirname, 'dist'),
        libraryTarget: 'commonjs2',
    }
};

Lambda 関数ごとに entry を定義する。 Lambda で動かすために特筆することとして、target オプションを node にするのと、output の libraryTarget オプションを commonjs2 にしていることです。
共通のロジックは別ファイルに分けて、各 entry のファイルでインポートします。

Webpack のようなモジュールバンドラを使う副次的な効果として、アップロード制限の回避があります。モジュールバンドラ無しで Lambda 関数をデプロイする場合、node_modules も含めて zip を作成することになるので、いくつかパッケージを依存に追加した程度でファイルサイズが膨れ上がり、アップロードできなくなります(S3経由でデプロイする必要がある)。
モジュールバンドラを使えば、node_modules から必要な内容だけ取り出して、ひとまとまりのファイルが生成されます。関数がよほど複雑にならなければ、制限に達することはなさそうです(達した場合は、関数分割をすべきタイミングかもしれません)。

Webpack でビルドしたファイルは、デプロイのために zip にする必要があります。
Webpack には zip を生成するサードパーティプラグインがありますが、今回は Webpack に過度に依存することを避けるため、あくまで JavaScript のモジュールバンドラとしての役割のみに徹することにします。
postbuild のタスクで以下のスクリプトを走らせて entry 毎に zip ファイルを生成します。

#!/usr/bin/env node
const path = require('path');
const {promisify} = require('util');
const {exec} = require('child_process');
const globby = require('globby');

const execAsync = promisify(exec);

(async () => {
    const paths = await globby(['dist/*'], {
        onlyDirectories: true,
    });
    // Lambda 関数の数が極端に多い場合は、一定数ずつ作成したほうがよさそう
    await Promise.all(paths.map((path) => {
        console.log(`Create zip in ${path}...`);
        return execAsync(`zip -r Lambda-Deployment.zip * -x *.zip`, {
            cwd: path,
        });
    }));
})();

あとは、生成された zip をウェブのコンソールでアップロードするか、CLI を経由してデプロイします。