なるべくコストを抑えて簡単にバックエンドを実装したいという場合に、AWS Lambdaは選択肢の一つになるかと思います。今回は、Expressを使ったバックエンドをCDKでデプロイする手順についてまとめたいと思います。
本記事は Lambda Web Adapter でウェブアプリを (ほぼ) そのままサーバーレス化する を大いに参考にしております。本記事では、元の記事では触れられていない、NodejsFunction内でLambda Web Adapterを利用する方法の詳細について記載しています。
実際のコードはこちら
AWS構成とデプロイ方法
本記事では、CDKを使ってExpressアプリケーションをLambda上にデプロイし、API Gateway経由でアクセスできるようにすることを前提とします。
Express on AWS Lambda
ExpressをLambdaで実行可能にするためには、LambdaへのリクエストをうまくExpressが受け取れるようにしてやる必要があります。以下のように index.js
をLambda上で実行したとしても、LambdaのリクエストをExpressアプリケーションは受け取れないためです。
const app = express();
app.use('/', (req, res) => {
res.status(200).send('OK');
});
app.listen(3000);
2024年2月現在で、私が調べた限りだと、LambdaのリクエストをExpressアプリケーションが処理できるようにするために、主に2つの方法があります。
どちらもLambdaのリクエストとExpressアプリケーション間のやり取りをうまく変換してくれるもののようなイメージですが、Lambda Web Adapterの方は、Expressに限らず、httpのやり取りをするアプリケーション全般で機能します。
また、それぞれの方法では、Dockerfileを使ってデプロイするか、AWS Lambda Node.js Library のNodejsFunctionsを使ってデプロイするかの方法を取ることができます。
利用状況によるかもしれませんが、Dockerfileを用いる方法の方がNodejsFunctionよりも、コンパイルやデプロイ時の細かい設定が自由度高く行いやすいかもしれません。
Lambda Web Adapter を NodejsFunctions でデプロイする
Dockerfileを使う場合では、以下のように、Lambda Web Adapterをイメージにコピーする必要があります。
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.1 /lambda-adapter /opt/extensions/lambda-adapter
この方法は多くのサンプルコードで実装されている方法ですが、Nodejsのランタイムを使った、NodejsFunctions経由でLambda Web Adapterをデプロイすることもできます。
公式のサンプルコードでは、SAMによるデプロイですが、以下のようにすることで、CDKでも同様の実装が可能です。
export class WebAdapterNodejsStack extends cdk.Stack {
...
const handler = new NodejsFunction(this, 'Handler', {
runtime: Runtime.NODEJS_20_X,
architecture: Architecture.ARM_64, // M1 Macの環境では必要
layers: [
// Lambda Web Adapterをレイヤーとして追加する
LayerVersion.fromLayerVersionArn(
this,
'WebAdapter',
`arn:aws:lambda:${this.region}:753240598075:layer:LambdaAdapterLayerArm64:20`
),
],
entry: path.join(__dirname, '../express/src/index.ts'),
handler: 'run.sh', // handlerではなく、shファイルで実行する
bundling: {
minify: true,
commandHooks: {
beforeInstall: () => [],
beforeBundling: () => [],
// shファイルをコピーする
afterBundling: (inputDir: string, outputDir: string) => {
return [`cp ${inputDir}/run.sh ${outputDir}`];
},
},
},
environment: {
AWS_LAMBDA_EXEC_WRAPPER: '/opt/bootstrap', // Lambda Web Adapterを利用するためには必要
PORT: '3000', // 使用するポートを指定する
},
memorySize: 256,
timeout: Duration.seconds(30),
logRetention: RetentionDays.ONE_MONTH,
description: props.description,
});
...
layersを指定する
公式のサンプルコードで指定されているコンテナをlayersとして指定する必要があります。このとき、architectureを指定していないと、M1 Macの環境では exec format error
Extension.LaunchError
が出てしまいました。
run.shで実行する
通常、NodejsFunctionでは、entryファイルhandlerを指定しておく必要がありますが、この形式はExpressでは存在しないため、実行がうまくいきません。そのため、以下のようなrun.sh
を作成して実行できるようにする必要があります。
node index.js
afterBundlingでは、run.shをLambda環境にコピーしています。
また、environmentでAWS_LAMBDA_EXEC_WRAPPER = '/opt/bootstrap'
を指定する必要があります。
PORT
はExpressでlistenするPORTを受け付けられるようにするために必要になります。
各デプロイとコールドスタート時の速度の比較(予想)
Lambda Web Adapter でウェブアプリを (ほぼ) そのままサーバーレス化する ではかなり計測回数を取って比較しているのですが、同じ比較を行おうとすると結構リソースかかってしまいそうなので、あくまで予想という形で速度の比較を考えたいと思います。
比較はコールドスタートにかかる時間の平均とp99を4つのデプロイ方法で比較するものになります。
①SE Dockerfile | ②SE NodejsFunction | ③WA Dockerfile | ④WA NodejsFunction | |
---|---|---|---|---|
コールドスタート(平均) | ||||
コールドスタート(p99) |
元記事では、①、②、③を比較しており、平均、p99において、コンテナのランタイム起因によって②(NodejsFunction)がコールドスタートの時間が最も短くなる傾向が見られていました。また、同じDockerfileによる実装の①と③にはほとんど違いがないこともわかっていました。
元記事の計測結果を踏まえると、今回実装した④のデプロイは、コンテナのランタイム的に、コールドスタートの時間が①や③よりも短くなり、②とほとんど変わらない結果になるのではないかと予想できます。
まとめ
今回は、CDKを使ってExpressのアプリケーションをAWS Lambdaにデプロイする際に、NodejsFunction内でLambda Web Adapterを利用する方法の詳細について記載しました。
この方法ならコールドスタートの時間的にDockerfileによるデプロイよりも短くできるはずなので、個人的には、NodejsFunctionを使ってデプロイする方がいいのではと思いました。
また、AWSが公式に出している汎用的なフレームワークに乗る方が、Expressのみに特化したライブラリに頼るよりも安心感があるかもなと思いました。
コメント