Summary
- Fargate で常時起動していた「アクセスは少ないが RDB と密に連携する業務アプリ」を安くしたいという動機で、2026年10月にGAしたばかりの新機能 Lambda Web Functions を試した
- Bob に計画を作らせた段階で、Lambda Web Functions のランタイムが Node.js 24.x に限られることに気づいた。Perl Catalyst はそのままでは動かせず、Bob が薄い Node.js ラッパーを書き、Perl 本体や共有ライブラリまで ZIP に詰め込むことで動いた
- Node.js 以外の言語はランタイム一式を自前で持ち込む必要がある。そんなことをするくらいなら、Lambda Web Adapter を使って Docker コンテナで動かした方が簡単だと感じた。「コンテナを指定するだけで動く」ようになってほしい
やらないこと
- Lambda Web Functions の本番運用設計とコスト最適化
- Lambda Web Adapter との性能・コストの定量比較
- コールドスタート時間の計測・チューニング
本記事における課題
過去に、アクセス頻度はあまり高くないが、RDB とがっつり連携している業務アプリケーションをコンテナ化して Fargate に置いたことがある。常時起動なので、もう少し安くできないかと悩んでいた。
そこに Lambda Web Functions が登場した。ポートをリッスンする通常の Web アプリをそのまま Lambda で動かせるのであれば、アプリをアクセスされた分だけの従量課金に移せるかもしれない。とりあえず試してみようと思った。
今回確認したかったことは以下の2点である。
- 既存の Web アプリが動くか:手元にある Perl製の社内備品管理アプリ(ダミーでBobが実装)を、Lambda のイベントハンドラ形式に書き換えずに動かせるか
- RDB と連携できるか:Aurora Serverless v2 (PostgreSQL) にVPCを介して接続できるか
やったこと
IBM Bob との進め方
Plan モードで計画を作ったら、Node.js 限定だと分かった
Bob の Plan モードに、上記2点を満たす要件(IP 制限とセルフサインアップ認証付き、個人検証なのでコスト最小、構築と破棄は Terraform)を渡した。Bob が生成した計画書(plan.md)には、次のような内容が含まれていた。
- Lambda Web Functions のランタイムは Node.js 24.x のみ。Perl アプリはそのまま動かせないため、薄い Node.js ブリッジを入口にして、裏で Perl を子プロセスとして起動する
- Aurora は最小の 0.5 ACU、NAT Gateway は置かない
- クライアント IP 制限は Node.js ブリッジ(アプリ層)で
ALLOWED_IPSにより実装する - Web Function 系リソースは Terraform で直接扱えない可能性を見越して、
aws_cloudformation_stackでラップして管理する - 全体を4つのサブタスク(Catalyst アプリ、ブリッジとパッケージング、Terraform、手順書)に分割する
正直なところ、試そうと決めた時点ではランタイムの制約に気づいていなかった。計画の段階で Bob が明示してくれたおかげで、実装に入る前に構成を切り替えられた。
Agent モードで実装し、ローカルで先に動かす
計画を渡した後は Agent モードで実装させた。あわせて local-compose-plan.md として別途計画を立て、ビルド済みの dist/function.zip をローカルで動かす環境も作らせた。
-
Dockerfile.local:公式の Lambdanodejs24.xイメージ上に ZIP を展開してnode index.jsを起動する -
docker-compose.yml:PostgreSQL 16 を立ち上げ、db/schema.sqlとdb/seeds.sqlを自動投入する
本番と同じ成果物を AWS に出す前にローカルで動かせるため、デプロイの試行錯誤を減らせる。
アーキテクチャ
[ Webブラウザ / curl ]
│ HTTPS
▼
[ Lambda Web Function Endpoint ](ApplicationManaged)
│
▼
[ Lambda 実行環境 nodejs24.x / arm64 ]
Node.js ブリッジ (index.js)
├─ ALLOWED_IPS による IP 制限
├─ Perl (Plack) 子プロセスの起動と待機
└─ localhost:5000 へのリバースプロキシ
Perl Catalyst (app.psgi)
├─ サインアップ / ログイン / セッション
└─ 備品の一覧・登録・貸出・返却
│ VPC Egress (5432)
▼
[ AWS::Lambda::NetworkConnector ] → プライベートサブネット
▼
[ Aurora Serverless v2 (PostgreSQL 16, 0.5〜1.0 ACU) ]
構築と破棄の手段には Terraform を使った。Web Function 関連の4リソースだけを CloudFormation テンプレートとして lambda_web.tf に埋め込んでいる。(最新サービスにTerraformがまだ対応していないため)
terraform/
├── main.tf # provider と suffix
├── vpc.tf # VPC / 2AZ のプライベートサブネット / SG(NAT なし)
├── aurora.tf # Aurora Serverless v2 と Secrets Manager
├── iam.tf # 実行ロール / Network Connector 用オペレーターロール
├── s3.tf # コード置き場(バージョニング必須)
└── lambda_web.tf # aws_cloudformation_stack で WebFunction 等を定義
Node.js の薄いラッパーで Perl を動かす
ブリッジ(bridge/index.js)の役割は3つだけである。
- Perl の
HTTP::Server::PSGIを子プロセスとして起動し、/api/healthが返るまで待つ -
AWS_LAMBDA_HTTP_ENDPOINTで指定された host:port をリッスンする -
ALLOWED_IPSに含まれないクライアントには 403 を返し、それ以外を127.0.0.1:5000へ転送する
Perl の起動を待ってから server.listen する作りにしている。
async function bootstrap() {
startPerlBackend();
await waitForBackend(); // /api/health が通るまで最大 50 回 × 200ms
server.listen(Number(LISTEN_PORT), LISTEN_HOST, () => {
console.log(`[Bridge] Lambda Web Function is LISTENING on http://${LISTEN_HOST}:${LISTEN_PORT}`);
});
}
デプロイ後に Lambda コンソールのエディタで中身を開くと、ランタイムは Node.js 24.x で、ZIP には Node.js のブリッジ(index.js)とともに、Perl 本体(bin/)、共有ライブラリ(lib/)、CPAN モジュール(local/)、アプリ(app/)が同梱されている。
Perl は動いた。ただ、ここで引っかかりが残った。「ポートをリッスンするアプリをそのまま動かせる」という触れ込みから私が期待していた姿とは、かなり違っていたからである。
-
Perl のアプリなのに、Node.js のコードが要る。Perl アプリを動かすためだけに、
index.js(約230行、Bob が作成)を足している。アプリを変えずに済んだというより、アプリの前に別のアプリを置いた形である -
Perl の実行環境を自前で ZIP に詰めて持ち回る。Node.js ランタイムには Perl がないため、Perl 本体と共有ライブラリを ZIP に入れ、ブリッジから
LD_LIBRARY_PATHなどを組み立てて起動している。次の節で詳しく書くが、ここが一番重い - コンテナイメージを指定できない。コードの置き方は、コンソールでの直接編集、ZIP のアップロード、S3 からの取得のいずれかである。Fargate で動かしていたコンテナをそのまま持ち込むことはできない
Node.js 以外は、ランタイム一式を自前で持ち込むことになる
今回の検証で一番引っかかったのがここである。Node.js 以外の言語で動かすなら、その言語のランタイムと依存ライブラリを、すべて自分で ZIP に詰めて持ち込む必要がある。
Perl は大抵の Linux に入っているイメージがあるが、Lambda の Node.js 24 ランタイムには入っていない。念のため、公式の Lambda Node.js 24 コンテナイメージで確認した(Web Functions のマネージド環境そのものではなく、同じ nodejs:24 の公式イメージでの確認である)。
$ podman run --rm --entrypoint /bin/sh public.ecr.aws/lambda/nodejs:24 \
-c 'perl -v; ls /usr/lib64/libpq* /usr/lib64/libperl*'
/bin/sh: line 1: perl: command not found
ls: cannot access '/usr/lib64/libpq*': No such file or directory
ls: cannot access '/usr/lib64/libperl*': No such file or directory
OS は Amazon Linux 2023 だが、Node.js を動かすための最小構成であり、Perl も PostgreSQL クライアントライブラリ(DBD::Pg が必要とする libpq)もない。ビルド用コンテナ(Amazon Linux 2023 arm64)には入っているので、そこからコピーして ZIP に同梱した。ZIP に CPAN モジュールだけ入れても、起動した瞬間に Perl が見つからずに終わる。
| 同梱物 | ZIP 内のパス |
|---|---|
| Perl 実行バイナリ | bin/perl |
libperl.so / libpq.so / libldap / libsasl2 / libkrb5 ほか共有ライブラリ |
lib/ |
| システム Perl モジュール |
perl-system/lib64, perl-system/share
|
| cpanm で入れた CPAN モジュール | local/ |
RUN mkdir -p /build/bin /build/lib /build/perl-system && \
cp $(which perl) /build/bin/perl && \
cp -r /usr/lib64/perl5 /build/perl-system/lib64 && \
cp -r /usr/share/perl5 /build/perl-system/share && \
for lib in /lib64/libperl.so* /usr/lib64/libpq.so* ...; do \
if [ -e "$lib" ]; then cp -d "$lib" /build/lib/ 2>/dev/null || true; fi; \
done
ブリッジ側では、同梱した Perl を優先して使い、LD_LIBRARY_PATH と PERL5LIB を明示的に組み立てて子プロセスに渡す。
この作業は、アプリの機能とは無関係である。ところが、持ち込んだものは今後すべて自分で面倒を見ることになる。
- Perl 本体や
libpqなどのバージョンアップ、セキュリティパッチへの追従 - 足りない共有ライブラリの洗い出し(
DBD::Pgがlibpqを、そのlibpqがlibldapなどを要求する、というように芋づる式に増える) - ビルド環境と実行環境の差異への対応(今回は Amazon Linux 2023 の arm64 でビルドして、そのまま持ち込んでいる)
しかも、これだけやってもコードの置き場所は ZIP か S3 である。Fargate に置いていたコンテナは使えず、ZIP を作り直す作業が別に増える。
Lambda Web Adapter を使い、Docker コンテナで動かした方が簡単
Lambda Web Adapter の公式 README によれば、Docker イメージに対しては Dockerfile に次の 1 行を足すだけで、Web アプリを Lambda で動かせる。
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:1.1.0 /lambda-adapter /opt/extensions/lambda-adapter
さらに README には、HTTP 1.1/1.0 を話すものなら言語やフレームワークを問わず動かせること、コード側に追加の依存を入れる必要がないこと、同じ Docker イメージを Lambda、EC2、Fargate、ローカルで動かせることが書かれている。
つまり、Perl のアプリなら、今回 Fargate に載せていたのと同じ Dockerfile(Perl も libpq もイメージの中にある)にこの 1 行を足せば済む。Node.js のラッパーも要らず、Perl 本体や共有ライブラリを手でコピーすることもない。ローカルでも Fargate でも、同じイメージで動かせる。
私が今回やったことは、これと比べると、わざわざ遠回りをしたことになる。ただし、今回は Lambda Web Adapter を実際には試していない。README の記述に基づいた見立てであり、性能やコストの比較もしていない。
構築で踏んだ落とし穴
Network Connector のオペレーターロールに Condition を付けると 400 になる
オペレーターロールの信頼ポリシーに、一般的なセキュリティ対策として aws:SourceAccount の Condition を付けていた。これだとスタック作成が 400 エラーで失敗する。Condition を外すと通る。
# Condition なしが必須 — NetworkConnector が AssumeRole する際に
# aws:SourceAccount 条件を満たせず 400 エラーになるため
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
ENI の権限は公式ドキュメントの形に合わせる
当初は ec2:CreateNetworkInterface などを Resource = "*" でまとめて許可していた。公式ドキュメント(Configuring networking for AWS Lambda Web Functions)に合わせ、ENI の作成とタグ付けを分け、タグ付けには ec2:ManagedResourceOperator 条件を付ける形に直した。
{
Sid = "TagENI"
Effect = "Allow"
Action = "ec2:CreateTags"
Resource = "arn:aws:ec2:*:*:network-interface/*"
Condition = {
StringEquals = {
"ec2:ManagedResourceOperator" = "network-connectors.lambda.amazonaws.com"
}
}
}
動作確認
./builder/build.sh で arm64 の dist/function.zip を生成し、ローカルの Docker Compose で動作を確認したうえで、terraform apply で Aurora、IAM、S3、Lambda Web Function、Network Connector を一括構築した。
Lambda コンソールでは、Web Function とそのエンドポイントが Active になっていることが確認できる。
エンドポイントの URL(*.lambda-web.ap-northeast-1.on.aws)にブラウザでアクセスすると、admin でログインした状態の備品管理画面が表示された。
後片付け
検証が終われば terraform destroy -auto-approve で終わる。S3 バケットは force_destroy = true、Secrets Manager は recovery_window_in_days = 0 にしてあるので、削除待ちで残るものはない。
cd terraform
terraform destroy -auto-approve
ちょっと宣伝
この記事もBobに下書きしてもらいました!
生成AIによる開発プロセスの効率化に関心がある場合は、IBMのAIエージェント製品コミュニティであるBobサロンのような場もおすすめです!実際にBobを活用されているユーザー様の声にも触れてみてください!
私がBobを試した記事もご参考になれば幸いです。
- IBM Bobと一緒に約75円でAzure BatchのPoCをやってみた
- IBM Bob で作る Personal Knowlege Management デスクトップアプリ on Tauri
- WebブラウザからIBM Bobを使ってみた ― ACPで開発環境を変えずにAIエージェントを使う
- IBM Bobに自作MCPサーバーを組み合わせ、他社AIにレビューさせてみた
- IBM Bob + Foam で構築する Personal Knowledge Management 環境
- IBM Bobを使って就活管理アプリをExcel VBAからWebアプリへ移行した話
- IBM BobでAWS&AzureのDocumentation MCPサーバーを使ったクラウドサービス比較レポートを作成させてみた
- IBM Bobと一緒にOpen Notebookのモバイル版UIを開発してみた
まとめ・所感
Bob のおかげで、Lambda Web Functions 上で Perl Catalyst と Aurora Serverless v2 を連携させるところまで確認できた。ただ、Lambda Web Functions は、私が期待していた「Fargate で動かしているような既存のコンテナをそのまま安く動かせる機能」ではなかった。あくまで、Node.js で書かれた Web アプリのコードを、そのまま Lambda で動かせることがメリットの機能である。
ランタイムが Node.js 24.x に限られるため、Perl のアプリを動かすには、薄い Node.js ラッパーを書き、Perl 本体や共有ライブラリまで ZIP に詰め込む必要があった。しかもコードの置き場所は、コンソール上での直接編集、ZIP のアップロード、S3 からの取得のいずれかである。
結局、 Node.js 以外の言語は、ランタイム一式を自前で持ち込まなければならない。そんなことをするくらいなら、Lambda Web Adapter を使い、Docker コンテナで動かした方が簡単ではないか、というのが今回の結論である。Dockerfile に 1 行足せば、言語を問わず、Fargate と同じイメージが Lambda で動く。
Lambda Web Functions は、Node.js 製の Web フレームワークをそのままアップして使いたい、というユースケースに向いた機能だと感じた。期待外れというより、私の期待と機能の想定がずれていた、というのが正確なところである。
願望としては、Web 系のコンテナやソースコードをそのまま指定するだけで動くようになってほしい。そうなれば、Web Adapter のような追加の仕組みも、ランタイム一式の持ち込みも要らなくなり、Fargate で常時起動している既存のコンテナ資産を、使った分だけの課金へ移せる。
Bob との進め方では、Plan モードで要件を渡すと、試そうとした時点では気づいていなかったランタイムの制約が計画の段階で明らかになった。実装に入る前に構成を切り替えられたのは大きい。Network Connector の IAM ロールのように、ドキュメントを読んでも気づきにくい落とし穴は、デプロイして初めて分かるものが多い。AWS へデプロイする前に十分に手元で計画しておくと、試行錯誤の回数を減らせる。




