はじめに
こんにちは、mori (@morimori) です。
きっかけは、最近 JAWS-UG の勉強会に参加させていただいている中で、気になった発表があって「試してみたい」と思ったことでした。今年はオンラインだけではなく、オフラインにもちょくちょく参加させていただいています。人や会社も違えば、使い方や目の付け所もそれぞれです。そういった発見がたくさん見つけられるので、JAWS のようなコミュニティの集まりはとても好きです。
その中で気になったのが、今回の主役 AWS Lambda Web Adapter (LWA) でした。「普通の Web アプリを (ほぼ) そのまま Lambda に載せられる」と聞いて、これは触ってみたいと一気に興味が湧きました。
そこで本記事では、まず LWA がどんなものかを実際に手を動かして確かめ(FastAPI の JSON サンプルを Lambda + Function URL で動かす)、その上で「社内ツールのホスト先」として使えるか、IP 制限・コスト・ネットワーク構成といった実運用の論点まで踏み込んで検討してみます。
AWS Lambda Web Adapter とは
AWS Lambda Web Adapter は、普通の Web アプリ(HTTP ポートで待ち受けるアプリ)を、コードを書き換えずにそのまま Lambda で動かせるようにするツールです。
- AWS が公式に提供する OSS(
awslabsOrganization でホスト、ライセンスは Apache License 2.0) - Rust 製の小さなバイナリで、Lambda Extension の仕組みで動作する
- コードも公開されているので実装の詳細まで追える
通常、Lambda で関数を動かすには handler(event, context) という決まった形のハンドラーを書く必要があります。LWA はこの前段に入って、Lambda のイベントを HTTP リクエストに変換し、アプリが待ち受けているポートに転送してくれる仕組みです。
出典: aws/aws-lambda-web-adapter(lambda-adapter-overview.png、Apache License 2.0)
API Gateway・Lambda Function URL・Application Load Balancer のいずれから来たイベントでも、Lambda 関数の中で LWA(Lambda-adapter)がそれを HTTP リクエストに変換し、同じイメージ内で動く Web アプリ(FastAPI など)に転送します。アプリは 8080 ポートで待ち受ける普通の Web サーバーのままでいられます。
ポイントは、アプリ側は「ポートで待ち受ける普通の Web サーバー」でいられること。つまり同じコード・同じコンテナイメージが、ローカルでも、Amazon ECS でも、Lambda でも動くということです。
まず試してみる:FastAPI の JSON サンプルを Function URL で動かす
先週、有志のメンバーで実際に手を動かしてみました。題材は FastAPI(Python) で、JSON を返すだけの最小アプリです。
手順は、AWS builders.flash の 「Lambda Web Adapter でウェブアプリを (ほぼ) そのままサーバーレス化する」 を参考にしています。LWA を初めて触る方は、まずこちらに目を通しておくとスムーズです。
1. サンプルアプリを用意する
LWA 用の特別な記述は一切なく、ただの FastAPI アプリです。
# main.py
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello from Lambda Web Adapter, deployed via ECR!"}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8080)
ポイントは uvicorn.run(app, host="0.0.0.0", port=8080) の部分です。LWA はデフォルトで 8080 ポートにリクエストを転送するので、アプリ側もそのポートで待ち受けます。
依存は requirements.txt にまとめます。
fastapi
uvicorn
2. Dockerfile に LWA を追加する
ここが LWA の肝です。やることは単純で、LWA のバイナリを /opt/extensions/ にコピーする 1 行を足すだけです。
# Dockerfile
FROM python:3.11-slim
# ★ この 1 行が LWA 本体。Lambda の拡張機能としてバイナリを配置するだけ
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:1.0.1 /lambda-adapter /opt/extensions/lambda-adapter
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
EXPOSE 8080
CMD ["python", "main.py"]
アプリ本体には一切手を入れず、CMD も普通に python main.py でアプリを起動するだけです(main.py 末尾の uvicorn.run(...) がそのまま 8080 ポートで待ち受けます)。
LWA のバージョン(1.0.1 の部分)は更新されていきます。参考にした builders.flash の記事では 0.9.0 でしたが、本記事執筆時点の最新は 1.0.1 でした。利用可能なタグは Amazon ECR Public Gallery(awsguru/aws-lambda-adapter) で確認できます。
3. ローカルで動作確認する
ECR に上げる前に、コンテナをビルドしてローカルで動くか確認します。
docker build --platform linux/amd64 -t my-lambda-web-app .
docker run -p 8080:8080 my-lambda-web-app
# 別ターミナルで
curl http://localhost:8080/
# => {"message":"Hello from Lambda Web Adapter, deployed via ECR!"}
--platform linux/amd64 を付けているのは、Lambda(x86_64)で動かすイメージを作るためです。Apple Silicon の Mac でそのままビルドすると arm64 になってしまうので、明示しておくのが無難でした。
4. ECR にプッシュして Lambda + Function URL で公開する
# リポジトリ作成
aws ecr create-repository --repository-name my-lambda-web-app --region ap-northeast-1
# タグ付け(<ACCOUNT_ID> は自分の AWS アカウント ID)
docker tag my-lambda-web-app:latest \
<ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/my-lambda-web-app:latest
# ECR にログイン
aws ecr get-login-password --region ap-northeast-1 \
| docker login --username AWS --password-stdin <ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com
# プッシュ
docker push \
<ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/my-lambda-web-app:latest
AWS SSO(IAM Identity Center)でログインしている場合は、aws ecr get-login-password でプロファイルの指定が必要でした。実際に試したときも「あれ、ログインできない」となり、--profile を付けて解決しました。
aws ecr get-login-password --region ap-northeast-1 --profile <SSO プロファイル名> \
| docker login --username AWS --password-stdin <ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com
あとはマネジメントコンソールから、
- Lambda コンソールで「関数の作成」→「コンテナイメージ」を選択し、ECR イメージ URI を指定
- 作成後、「設定」→「関数 URL」から Function URL を作成(動作確認用は認証タイプ
NONE)
発行された Function URL にアクセスすると、
curl https://xxxxxxxxxxxxxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/
# => {"message":"Hello from Lambda Web Adapter, deployed via ECR!"}
ローカルで動かしたときとまったく同じ JSON が返ってきました。アプリ側のコードは Lambda のことを何も知らないのに、ちゃんと動いています。これが LWA の気持ちよさですね。
LWA の使いどころを本気で考えてみる
検証で「LWA、いいじゃん」となったので、ここからは具体的な使いどころを考えてみます。
LWA の持ち味は、普通の Web アプリをほぼそのまま載せられて、アクセスがない時間は課金されないこと。これをふまえて真っ先に浮かんだのが、社内ツールのデプロイ先でした。
社内には、週に数回だけ、あるいは月末・月初しか使わないツールがそれなりにあります。この手のものを EC2 で 24 時間動かし続けるのは、使っていない時間のぶんがもったいないな…とずっと思っていました。社内向けだと優先度も上がりにくく、OS の EOL(サポート終了)対応のようなメンテナンスも後回しになりがちです。
その点 LWA なら、コードはほぼそのままで、使わない間は課金もされません。コンテナにしてしまえば OS のパッチ管理からも離れられます。「使うときだけ動けばいい」社内ツールとは、素直に噛み合いそうです。
そこで以降は、内製した社内ツールのホスト先として LWA が使えるか、もう少し具体的に考えてみます。意識した要件は次の 2 つで、軸になるのは IP 制限です。
- 社内からのアクセスだけに絞りたい。できればインターネットにも露出させたくない(IP 制限 + 非露出)← いちばん重視
- アクセスがない時間帯はコストを抑えたい(対象は月数回程度しかアクセスされず、24 時間使うものではない)
まず、Function URL は IP 制限でつまずく
検証では手軽な Lambda Function URL を使いましたが、IP 制限を考えた瞬間につまずきます。AWS で IP 制限といえば WAF ですが、
- WAF を Function URL に直接アタッチできない
- リソースポリシーの
aws:SourceIpも実質機能しない(AWS のプロキシ経由のため、クライアントの実 IP が見えない) - そもそもインターネットへの露出を消せない(VPC 内限定にできない)
と、「インターネットに一切露出させず、きっちり IP 制限したい」という社内ツールの要件には構造的に不向きでした。手軽さは抜群なので、サービス間連携や検証・Webhook には今でも第一候補ですが、今回はもう一工夫いります。
CloudFront VPC Origins が効いてくる(ALB をオリジン指定)
この「インターネットに露出させたくない」を解決してくれるのが、2024 年 11 月に GA した CloudFront VPC Origins です。
プライベートサブネットに置いた ALB / NLB / EC2 を、インターネットに公開せずそのまま CloudFront のオリジンに指定できる機能で、追加料金もかかりません。今回のケースでは、プライベートサブネットの ALB を VPC オリジンとして指定することで、CloudFront → ALB 間をインターネット非経由にできます。
ポイントは、VPC Origins が対応するのは ALB / NLB / EC2 のみで、Lambda は直接オリジンにできないこと。そこで CloudFront と Lambda(LWA) の間に ALB を挟むのが要になります。ALB は単なるコスト要因ではなく、VPC Origins と Lambda をつなぐ構造上必要なコンポーネントという位置づけです。
これで「インターネット非露出 + 厳格な IP 制限(WAF は CloudFront 側で)」という要件が、まとめて素直に収まる見通しが立ちました。
わたしの推し構成:WAF + CloudFront + VPC Origins + ALB(private) + Lambda(LWA)
あれこれ考えてたどり着いたのが、この構成です。
構成のポイントはこの 5 つです。
- WAF は CloudFront にアタッチして、許可した IP だけ通す(これで IP 制限を実現)
- ALB はプライベートサブネットに置き、インターネットから直接叩けないようにする
- CloudFront VPC Origins(2024/11 GA)でつなぎ、CloudFront → ALB 間もインターネットを経由させない
- ALB の Lambda ターゲットはヘルスチェックがデフォルト無効。EC2 / IP ターゲットと違って何もしなくてよく、「ヘルスチェックで Lambda が呼ばれて余計なコストが…」という心配も要らない
-
CloudFront のキャッシュは
CachingDisabled。動的な社内ツールなのでキャッシュは不要
図のとおり、ユーザーは CloudFront(WAF で IP 制限)→ VPC Origins → プライベートサブネットの ALB(internal)→ Lambda(LWA) という経路で届きます。コンテナイメージは ECR に置いておき、Lambda がそこから pull するだけ。Function URL の弱点だった「インターネット露出」を完全に消しつつ、社内の許可した IP だけを通す形に収まりました。
最後にコストの話
LWA 自体は OSS で無料、Lambda もアクセスがなければ課金されないので、月数回しか使わない社内ツールとの相性は抜群です。すでに EC2 で動かしているものは、使っていない時間のアイドルコストがそのまま無駄になっていました。
一方で、この構成には常時かかる固定費もあります。
| 項目 | 目安 | 備考 |
|---|---|---|
| ALB | ~$18/月〜 | 時間課金 $0.0243/h × 約730h ≒ $17.7(東京)+ LCU 従量。VPC Origins と Lambda をつなぐため必須 |
| WAF(WebACL) | ~$6/月 | IP 制限用 |
| ECR ストレージ | ~$0.1/GB/月 | コンテナイメージ保存 |
「完全従量でゼロ円」とまではいきませんが、月数回のアクセスのために EC2 を 24 時間動かし続けるよりは…という温度感です。
それに、固定費の大半を占める ALB は、1 つのツール専用にする必要はありません。ALB はリスナールール(パスやホスト名でのルーティング)で複数の Lambda ターゲットに振り分けられるので、複数の社内ツールを同じ ALB にぶら下げてしまえば、ALB や WAF の固定費はツール間で分け合えます。
まとめ
ここまで社内ツールのホスト先として、あれこれ構成を考えてきました。先日遊んだ AWS BuilderCards もそうですが、こうやってアーキテクチャを考えてるのって、楽しいですね。
- Function URL は手軽だが、IP 制限・インターネット非露出には構造的に不向き
- 非露出 + IP 制限を満たすなら WAF + CloudFront + VPC Origins + ALB(private) + Lambda(LWA)
- VPC Origins は Lambda 直結できないので ALB が必須
- 月数回しか使わない社内ツールにおすすめ
なお、今回足した CloudFront VPC Origins はわりと最近のアップデートで、試してみたさもあって盛り込んだ部分です。シンプルな構成のほうが運用や属人化の面でトラブルになりにくいとも思うので、そこまで求めないなら CloudFront を挟まず ALB + WAF で IP 制限だけかける構成のほうが素直かもしれません。
もっとも、実際の社内ツールは RDS への接続や社内リソースへのアクセスを伴うケースが多く、今回のように簡単に「そのまま載せ替えるだけ」とはいかないことがほとんどだと思います。それでも、要件を並べて「ここがネックだな」「じゃあこう繋ぐか」と構成を組み立てていく時間は、それだけで純粋に楽しいものでした。

