Lambda Layersで快適に開発する方法


みなさん、Lambda Layers使ってますか?
共通で利用するロジックをライブラリにできる、ということで登場時は大変盛り上がりました。
(それまでは関数ごとにライブラリ部分にシンボリックリンク貼って利用してた)

ただ開発を重ねるにつれて、「これ、色々不便じゃ、、、?」と思うことが湧き上がってきました。

根本原因:ローカルで/optを参照するの辛い

Layersのデプロイ先は/optになるので、ローカルでもそれに対応する方策を色々と取る必要があります。

docker使って/optにLayersをマウントする

可能です。可能ですが、テストやビルド時にわざわざdockerに入るのめんどくさい、、
加えて、IDEにdockerを認識させるのが環境によりますが、面倒くさかったり、有料じゃないとできなかったりします。
IDEが「/optなんてないよ」と認識してくれない状態での開発は無理ゲー

シンボリックリンクを使う

こちらも可能です。可能ですが、プロジェクトが複数に跨がってくるとその管理がまた面倒になってきます。
ローカルのグローバル領域荒らしたくないんじゃ、、、cloneしたら即開発したいんじゃ、、、

解決策

関数のソースからLayersのソースを呼び出す、という構成を取る以上、上記の/opt参照問題は避けられません。

色んなプロジェクトで利用してると環境構築やり直したりとかもちょいちょい出てきて、「もうIDE参照めんどいよ!」と、ある程度はローカルでそのまま開発できるよう、できる限り関数からLayersを参照しないように、、ロジックはLayersに寄せるように、、、としてたら、あることに気づきました。

全部Layersに書けば良くね?

「共通で利用するロジックをライブラリにできる」という触れ込みによる先入観で開発していましたが、極限まで参照を削っていったらこうなりました。

BaseFunction.ts
// @ts-ignore
import {invokeLayerFunction} from '/opt/lambda_handlers/LayerRooter';

export async function handler(event: any) {
    return await invokeLayerFunction(event);
}

このソースは一度書いたらもう、二度と触れずに存在を忘れられます。
Typescriptならjsファイルごとコミットしても良いくらい。

で、この呼び出し先であるLayers側のコードはこんな感じです。

LayerRooter.ts
const AWS_LAMBDA_FUNCTION_NAME = process.env.AWS_LAMBDA_FUNCTION_NAME;
const SOME_FUNCTION_NAME = 'hoge';

export async function invokeLayerFunction(event: any) {
    switch (FUNCTION_SUFFIX) {
        case SOME_FUNCTION_NAME:
            // call some metho
        default:
            break;
    }
}

この方式は、全ての関数のコードをBaseFunction.tsにし、関数名によって呼び出し先ロジックを変えるという方式です。
パフォーマンス的にはSomeFunctionA.tsSomeFunctionB.tsがLayerのメソッドを一つだけ呼び出す、という方がわずかに優れていますが、そうすると関数の追加のたびに/optを参照したコードを扱う必要があり、それすら面倒になったのでこの方式を採用しています。

他にも「全て一つの関数とし、引数のheader等で呼び出し先ロジックを変える」という方法もありますが、これは常時起動するコンテナ数という観点でパフォーマンスは良いものの、変なコードを仕込んでした待った場合のダメージが全体に及ぶ点や、CloudWatchのログ解析が大変、という問題もあるので関数は分けて運用しています。

マイクロサービスなのにモノリシックっぽくない?という気もしなくもないですが、Layer内のコードをちゃんと書けば問題ないかと考えています。

お役に立てましたら幸いです。
良い開発ライフを!