AWS Lambda Web AdapterでConnect RPCサービスをAWS Lambdaにもデプロイする
はじめに
Connect RPCは、gRPC互換かつHTTP/1.1でも動作するRPCプロトコルです。バクラク事業部では、Connect RPCをサービス間通信の標準プロトコルとして採用し、connect-goやconnect-esを利用してサービスを実装しています。
Connect RPCを実装したサービスは、AWS Fargate (ECS) にデプロイしています。しかし、サービスのワークロードによっては、弾力性やコスト最適化の観点からAWS Lambda (以降Lambda) にデプロイしたいモチベーションがありました。Lambdaにデプロイする場合、通常はLambdaが期待するハンドラを用意する必要があります。もし、アプリケーションコードを変更することなく、Connect RPCサービスとしてそのままLambdaにデプロイできると便利です。
AWS Lambda Web Adapter (以降LWA) はこの願いを叶えてくれます。LWAはLambda extensionsとして動作し、LambdaのイベントをHTTP/1.1に変換してアプリケーションとやりとりしてくれます。Dockerイメージにしておくことで、ECSとLambdaの両方に(ほぼ)同じイメージを使ってデプロイできます。この記事では、Lambda extensionsとLWAの説明をしつつ、サービス定義によるデプロイメント先の切り替えを紹介します。
Lambdaに関する前提知識
ここでは、LWAの動作を理解するために前提となる、Lambdaの知識を説明します。
Lambda external extensions
Lambda extensionsにはInternal extensionsとExternal extensionsの2種類がありますが、LWAはExternal extensionsとして動作するため、以降ではExternal extensionsのみを扱います。
External extensionは /opt/extensions
以下に配置された実行ファイルとして認識されます。また、Extensionが余分に消費した実行時間は課金対象となります[1]。
Lambdaの実行環境とライフサイクル
Lambdaの実行環境にはRuntimeプロセスとExtensionプロセスの2系統があります。
- Runtimeプロセス: Lambdaが起動する言語ランタイムとハンドラ (アプリケーションコード) を含む、本体となるプロセス
- Extensionプロセス: Runtimeとは独立して起動・終了する補助プロセス
また、基本的に3つのフェーズからなるライフサイクルを持ちます[2]。ざっくりとした説明は次の通りです。
- Init: コールドスタート時。各プロセスを初期化
- Invoke: 各リクエスト処理。ウォーム状態ではこのフェーズのみループ
- Shutdown: 実行環境破棄前の後処理。スケールインや関数の更新などで発生
Lambda Runtime Interface
LambdaにデプロイしたRuntimeプロセスは、Runtime APIを通してLambdaサービスとやりとりをすることでLambda関数として動作します。Runtime APIを実装したソフトウェアはRuntime Interface Client (以降RIC) と呼ばれます。AWS-managed Runtime Dockerイメージでは、ENTRYPOINT (/lambda-entrypoint.sh
) がRICを起動する
ため、ハンドラを指定するだけでLambda関数として動作します。各言語のRICはOSSとして公開されており、例えばNode.jsは https://github.com/aws/aws-lambda-nodejs-runtime-interface-client です。独自のRICを実装して使用することも可能です。
Lambda Web Adapterの動作
LWAの実体は、Rustで実装されたバイナリです。これはExtensionでありながら、Runtime APIを実装したRICの役割を果たします。なお、LWAはZip/Layerの場合とDockerイメージの場合で挙動が異なります。本記事ではDockerイメージを前提にしているため、ここではDockerイメージの場合のみ説明します。ざっくり挙動を表したものが次の図です。
Extensions APIではInvokeイベントを購読せず[3]、代わりにRuntime APIでイベントを受け取ることがポイントです。これにより、アプリケーションコードを変更することなく、Extensionを配置するだけで、Lambda関数がHTTP/1.1を喋れるようになります。
サービス定義によるデプロイメント先の切り替え
ここでは、実際にLWAを使ってLambdaにデプロイする様子を紹介します。社内の運用をベースにしていますが、運用のイメージを多少でも伝えられればと思いながら書いています。
バクラク事業部では、各サービスを独自のJsonnetスキーマにより管理しつつ、その設定からアプリケーションとインフラのコードを自動生成をするようにしています。例えば、connect-goのサービスは次のイメージです。
// カードサービス
{
name: 'CardService',
owners: ['card']
lang: 'go',
features: {
connect: true,
},
deployment: {
destination: 'ecs',
},
}
この定義を入力とし、アプリケーションとインフラのコードを自動生成しています。具体的には以下が含まれます。
- Go: connect-goをベースとしたサーバーのテンプレート
- Dockerfile
- GitHub Actions (YAML)
- Dockerイメージをビルド・プッシュするワークフロー
- ECSにデプロイするワークフロー
- Terraform
- Dockerイメージのプッシュ先となるECRリポジトリ
- ECSサービスに必要なIAMロールやセキュリティグループ、ターゲットグループ、リスナールールなど
- ecspressoの設定ファイル
次のように deployment.destination
を変更すると、
@@ -6,6 +6,6 @@
connect: true,
},
deployment: {
- destination: 'ecs',
+ destination: 'lambda',
},
}
アプリケーションコード以外の生成物は次のように変化します。
- Dockerfile: LWAを配置
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 /lambda-adapter /opt/extensions/lambda-adapter
- GitHub Actions (YAML): Lambdaにデプロイするワークフロー
- Terraform: Lambda関数に必要なIAMロールやセキュリティグループ、ターゲットグループ、リスナールールなど
- lambrollの設定ファイル
このように、サービス定義の変更だけでConnect RPCサービスをLambdaにデプロイでき、かつECSサービスと同じインターフェースを維持できます。
まとめ
Lambda Web Adapterを使うことで、既存のHTTPサーバをアプリケーションコードの変更なしでLambdaで動かせます。特にConnect RPCをサービス間通信の基盤プロトコルとしている場合、HTTP/1.1互換であることのメリットが活きます。サービス定義と自動生成コードで紹介したように、デプロイ先を柔軟に選択できるプラットフォームの構築にも役立っています。
-
正確には、Init/ShutdownフェーズでExtensionが長く残った分が上乗せされます ↩︎
-
Lambda SnapStartが有効な場合はRestoreフェーズが追加されます ↩︎
-
events配列を空にして登録しているためSHUTDOWN以外のイベントは届きません https://github.com/awslabs/aws-lambda-web-adapter/blob/92b4993f4b7668b8cd3aa93faf0d0e34e6f0c0e9/src/lib.rs#L229 ↩︎
Discussion