AWS上級 #3 Lambda 基礎
#1 ECS / Fargate、#2 ECR は コンテナが常に立ち上がっている モデルでした。トラフィックが 0 のときも 1 個のコンテナは生きています。トラフィックの変動が大きい、短い処理だけが必要、運用負担をさらに減らしたい場合には別の選択肢がしっくりきます — Lambda。
今回は Lambda の動作、モデル (runtime / handler / event)、呼び出し方式、コールドスタート、並行性と上限、ロギングまで一気に整理します。
Lambda がすること #
AWS Lambda はサーバーレス関数実行プラットフォーム。イベント が入ってきたらようやく関数が目を覚まし、終わると再び消えます。トラフィックが 0 ならコストも 0。
イベント (HTTP / S3 アップロード / SQS メッセージ / Cron)
│
▼
Lambda がコンテナを hot または cold で立ち上げ
│
▼
私のハンドラ関数を実行 (数 ms 〜 15 分)
│
▼
レスポンス / 結果 → 呼び出し元
│
▼
一定時間 idle 後コンテナを終了いつ Lambda がしっくりくるか #
Lambda がしっくりくる場合 #
- イベント駆動 — S3 アップロード → サムネイル生成、SQS メッセージ → 処理、EventBridge スケジュール → バッチ
- 変動の大きいトラフィック — 普段 0、たまに急増。0 のときコストかからない
- 短い処理 — 通常数秒〜数分
- サイド / 補助ワークロード — メインシステムの隣の補助関数
- API の一部だけ — すべての API が Lambda である必要なし。一部だけ
Lambda が合わない場合 #
| 場合 | 理由 |
|---|---|
| 常にトラフィックが回る大きな API | 並行性 / コールドスタート / 費用で ECS が有利 |
| 15 分超の処理 | Lambda 1 回の呼び出しは最大 15 分 |
| 非常に大きいメモリ / GPU | Lambda は最大 10 GB メモリ、GPU なし |
| Stateful 接続 (WebSocket のバックエンド等) | 可能だが設計が複雑 |
| 常に on の DB connection pool | 呼び出し毎に新しく接続される方式 |
比較 #
| EC2 | ECS / Fargate | Lambda | |
|---|---|---|---|
| 立ち上がっている時間 | 常に | 常に (Service) | 呼び出し毎 |
| 運用負担 | 大 | 中 (Fargate は小) | 小 |
| コールドスタート | なし | 小 | あり (数十 ms 〜 数秒) |
| 時間上限 | 無限 | 無限 | 15 分 |
| トラフィック 0 のコスト | 大 | 中 | 0 |
| 同時処理 | OS レベル | 1 コンテナで多重 | 関数インスタンス 1 つ当たり同時 1 つ |
最後の行が重要: Lambda は 関数インスタンス 1 つが同時に 1 つの呼び出しだけ を処理。同時呼び出しが N 個ならインスタンスも N 個に自動拡張。
最初の Lambda — Hello, World #
関数を作る (コンソール) #
コンソール → Lambda → “関数の作成” → 直接作成 → Python 3.13。
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": "Hello, Lambda"
}保存して “テスト” — 空のイベントで呼び出し。CloudWatch Logs に自動でロググループ (/aws/lambda/<関数名>) 作成。
CLI で作る #
コードを zip でまとめ:
zip function.zip lambda_function.py
aws lambda create-function \
--function-name hello \
--runtime python3.13 \
--role arn:aws:iam::123456789012:role/lambda-basic-role \
--handler lambda_function.lambda_handler \
--zip-file fileb://function.zipIaC (Terraform を少し) #
resource "aws_lambda_function" "hello" {
function_name = "hello"
runtime = "python3.13"
handler = "lambda_function.lambda_handler"
role = aws_iam_role.lambda.arn
filename = "function.zip"
source_code_hash = filebase64sha256("function.zip")
memory_size = 256
timeout = 10
}モデル — Runtime / Handler / Event / Context #
Lambda のモデルを 4 つのキーワードで整理。
Runtime #
言語 + バージョン。マネージド runtime:
- Python (3.10 〜 3.13)
- Node.js (18、20、22)
- Java (8、11、17、21)
- .NET、Ruby、Go (
provided.al2023の上) - Custom Runtime — Rust / Zig / Swift 等を直接対応
または コンテナイメージ (ECR) でデプロイ — 上の #2 ECR と自然に接続。大きな依存があるとき有利 (zip 上限 250MB、コンテナは 10GB)。
Handler #
Lambda が呼び出す関数の名前。<ファイル名>.<関数名> 形式。
def main(event, context):
...→ Handler 設定: myapp.handler.main
Event #
呼び出し元が送ったデータ。JSON オブジェクト。呼び出しソース毎に形が違う。
{
"version": "2.0",
"routeKey": "GET /hello",
"headers": {...},
"queryStringParameters": {...},
"body": "..."
}{
"Records": [
{
"eventSource": "aws:s3",
"s3": {
"bucket": {"name": "my-bucket"},
"object": {"key": "uploads/photo.jpg"}
}
}
]
}Lambda Powertools (Python / TypeScript / Java) のようなライブラリがこの event 達を型安全に扱うのに役立ちます — 実戦では積極的に活用。
Context #
ランタイム情報。関数実行時間上限 (get_remaining_time_in_millis())、request id、関数名等。
def handler(event, context):
print(context.aws_request_id)
print(context.function_name)
print(context.get_remaining_time_in_millis()) # 残り時間 ms呼び出し方式 — 同期 vs 非同期 vs ストリーム #
呼び出しソースに応じて Lambda の動作が変わります。
1) 同期 (Synchronous) #
呼び出し元が結果を待つ。レスポンスを受け取るまでブロック。
| 呼び出しソース |
|---|
| API Gateway |
| ALB |
| Cognito |
直接 Invoke API |
aws lambda invoke \
--function-name hello \
--payload '{"name": "world"}' \
--cli-binary-format raw-in-base64-out \
out.json2) 非同期 (Asynchronous) #
呼び出し元が「キューに入れて」終わり。Lambda がバックグラウンドで処理。失敗時に自動再試行 (デフォルト 2 回) + DLQ (Dead Letter Queue) に送付。
| 呼び出しソース |
|---|
| S3 ObjectCreated |
| SNS |
| EventBridge |
InvocationType=Event の Invoke |
aws lambda invoke \
--function-name hello \
--invocation-type Event \
--payload '{"key": "value"}' \
--cli-binary-format raw-in-base64-out \
out.json3) ストリーム / ポーリング #
Lambda がキュー / ストリームを自動ポーリング。
| 呼び出しソース |
|---|
| SQS |
| DynamoDB Streams |
| Kinesis |
| MSK (Managed Kafka) |
設定だけしておけば新メッセージが来るたびに Lambda が batch で受け取り処理。失敗すれば retry + DLQ。
並行性 (Concurrency) の意味 #
Lambda の最も重要な点の 1 つです。関数インスタンス 1 つが同時に 1 つの呼び出しだけ処理するので、同時に入ってきた呼び出し数 = 立ち上がったインスタンス数。
秒 10 個の呼び出し、各呼び出し 1 秒 → 同時 〜10 個のインスタンス
秒 100 個の呼び出し、各呼び出し 100ms → 同時 〜10 個のインスタンスアカウント上限 (Account Concurrency) #
リージョン別デフォルト 1,000。運用ワークロードはしばしば不足 — Service Quotas コンソールで増額リクエスト。
Reserved Concurrency #
特定の関数に「最大 N 個」を保証 + 制限。
aws lambda put-function-concurrency \
--function-name hello \
--reserved-concurrent-executions 100用途:
- 危険な関数の暴走を遮断 (例: 有料の外部 API を呼ぶ関数)
- 他の重要な関数に並行性の余裕を残す (この関数が 1,000 を独占しないように)
- DB connection 暴走防止 (RDS connection pool 保護)
Provisioned Concurrency — コールドスタート回避 #
事前に N 個のインスタンスを温めておく。同時呼び出しが N 以下ならコールドスタート 0。
aws lambda put-provisioned-concurrency-config \
--function-name hello \
--qualifier prod \
--provisioned-concurrent-executions 10費用: 温めておいたインスタンスの時間分課金 (やや安い単価)。API の入口でコールドスタートがユーザー体験に直接影響なら検討。
コールドスタート — 最もよく出会う落とし穴 #
関数インスタンスが新たに作られるときの初期コスト。2 段階。
[INIT 段階] — 1 度だけ
├─ コンテナ環境準備
├─ runtime 起動
├─ ハンドラモジュール import
└─ ハンドラ関数の外のコード実行 (グローバル)
[INVOKE 段階] — 呼び出し毎に
└─ ハンドラ関数の実行INIT は インスタンスの最初の呼び出しでのみ コスト。同じインスタンスが次の呼び出しを受ければ INIT スキップ (warm)。
コールドスタート時間 #
おおよその目安:
| 言語 / 形態 | INIT 時間 |
|---|---|
| Python (小さな依存) | 〜150 ms |
| Python (大きな依存、e.g. boto3 + pandas) | 〜1 〜 2 秒 |
| Node.js (小さい) | 〜100 ms |
| Java | 〜500 ms 〜 数秒 |
| コンテナイメージ (大きい) | 〜数秒 |
コールドスタートを減らす #
- メモリを増やす — Lambda はメモリに比例して vCPU も増える。256MB → 1024MB だけでも INIT が速くなる
- 依存のスリミング — 使わないパッケージを除外、tree-shaking
- グローバル変数の活用 — ハンドラ外で 1 度作ったオブジェクト (boto3 client、DB connection 等) を warm なインスタンスが再利用
- Provisioned Concurrency — 上で見た通り
- Lambda SnapStart (Java) — INIT 結果のスナップショットを取って高速復帰。Java 限定
グローバル変数パターン #
import boto3
# Lambda インスタンス 1 度だけ — INIT 段階
s3 = boto3.client("s3")
def handler(event, context):
# ハンドラはきれいに — 呼び出し毎に client を作らない
return s3.list_buckets()def handler(event, context):
# 呼び出し毎に boto3 client — 遅い
s3 = boto3.client("s3")
return s3.list_buckets()メモリと時間上限 #
| 項目 | 上限 |
|---|---|
| メモリ | 128 MB 〜 10,240 MB (1 MB 単位) |
| 時間 | 1 秒 〜 900 秒 (15 分) |
一時ディスク (/tmp) | 512 MB 〜 10 GB |
| 環境変数 | 4 KB |
| Payload (同期) | 6 MB |
| Payload (非同期) | 256 KB |
| zip (ソース) | 50 MB (圧縮) |
| zip (展開) | 250 MB |
| コンテナイメージ | 10 GB |
メモリは vCPU と紐づいています。1,769 MB で vCPU 1 個分。メモリを増やすと CPU も増えて関数が速くなる場合がよくあります — メモリを増やすだけで費用が減ることも (Lambda Power Tuning で最適値を探す)。
ロギング — CloudWatch Logs #
Lambda の全ての stdout / stderr は自動で CloudWatch Logs の /aws/lambda/<関数名> ロググループに入ります。
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def handler(event, context):
logger.info("Hello %s", event.get("name"))
return "ok"運用では 構造化ログ 推奨 — JSON で出力すれば CloudWatch Logs Insights でフィールド別クエリ可能。
import json, logging
logger = logging.getLogger()
def handler(event, context):
logger.info(json.dumps({
"request_id": context.aws_request_id,
"user_id": event.get("user_id"),
"action": "process",
"duration_ms": 42
}))Powertools の Logger がこれを 1 行できれいに。
Layers — コードの再利用 #
複数の関数が同じ依存 / ユーティリティを使うとき Lambda Layer で分離。
# python の依存
mkdir -p python
pip install requests -t python/
zip -r layer.zip python
aws lambda publish-layer-version \
--layer-name my-utils \
--zip-file fileb://layer.zip \
--compatible-runtimes python3.13aws lambda update-function-configuration \
--function-name hello \
--layers arn:aws:lambda:ap-northeast-2:123456789012:layer:my-utils:1長所: 関数 zip が小さく、依存の更新が 1 回。 短所: 多くなりすぎると追跡が困難。5 個まで。
Lambda の費用 #
呼び出し当たり費用 = (呼び出し数 × $0.0000002)
+ (実行時間 GB-秒 × $0.0000166667)月 100 万呼び出し + 呼び出し当たり 100ms + 256MB:
- 呼び出し費用: 100 万 × $0.0000002 = $0.20
- 時間費用: 100 万 × 0.1 秒 × 0.25GB × $0.0000166667 = $0.42
- 合計: 〜$0.62 / 月
非常に安い。無料枠で月 100 万呼び出し + 400,000 GB-秒が無料。小さなワークロードは事実上 0。
費用が大きくなる場合:
- 呼び出し当たりの時間が長く、メモリが大きい関数 (e.g. 5 分 × 3GB)
- 並行性が常に高い関数 — ECS / Fargate の方が安いことも
よく出会う落とし穴 #
1) コールドスタートで最初の呼び出しが遅い #
API 入口の Lambda が 0.5 〜 2 秒ずつかかる。ユーザーが耐えられません。上の「コールドスタートを減らす」5 つ + Provisioned Concurrency を検討。
2) ハンドラの中で毎回オブジェクトを作る #
def handler(event, context):
db = create_db_connection()
boto = boto3.client("s3")
...呼び出し毎に 100ms の無駄。グローバルに引き上げる。
3) RDS connection pool の暴走 #
Lambda 同時 100 個 → DB 接続 100 個 → DB の connection 上限超過。 代替:
- RDS Proxy — Lambda 達が共有する connection pool
- Reserved Concurrency で関数並行性を制限
- DynamoDB のようなサーバーレス DB を使用
4) 非同期呼び出しの失敗が静かに消える #
S3 → Lambda が失敗しても呼び出し元は知りません。DLQ (SQS) または Lambda Destination で失敗をキャプチャ。
aws lambda put-function-event-invoke-config \
--function-name hello \
--maximum-retry-attempts 2 \
--destination-config '{"OnFailure":{"Destination":"arn:aws:sqs:...:dlq"}}'5) 時間切れで切られる #
15 分上限を知らずに大きな処理を 1 つの関数で — 14 分 59 秒で強制終了。長い処理は #7 Step Functions で分割 または ECS / Fargate。
6) Payload が 6MB を超える #
API Gateway → Lambda 同期呼び出しの上限。大きなファイルは S3 presigned URL パターンで回避 — Lambda が presigned URL だけ発行、クライアントが直接 S3 にアップロード。
7) 環境変数に秘密 #
平文の環境変数に DB パスワード → ログ / コンソールで露出。#6 Secrets Manager に移す。
まとめ #
今回つかんだもの:
- Lambda の使いどころ — イベント駆動、変動の大きいトラフィック、短い処理、サイドワークロード。トラフィック 0 なら費用 0
- Lambda が合わない場合 — 常に大きいトラフィック、15 分超過、大きいメモリ / GPU、stateful
- モデル — Runtime / Handler / Event / Context。Event の形は呼び出しソース毎に
- 呼び出し方式 — 同期 / 非同期 / ストリーム。非同期は自動 retry + DLQ
- 並行性 — 関数インスタンス 1 つ = 同時 1 呼び出し。呼び出しが増えるとインスタンスが自動拡張
- Reserved Concurrency — 暴走遮断 + 他の関数を保護
- Provisioned Concurrency — コールドスタート回避
- コールドスタート — INIT のコスト。メモリを増やす / 依存スリミング / グローバル変数 / SnapStart / Provisioned で緩和
- 上限 — メモリ 10GB、時間 15 分、payload 6MB、コンテナ 10GB
- ロギング — CloudWatch Logs に自動。JSON 構造化ログ推奨
- Layers — 依存の共有 (5 個まで)
- 費用 — 呼び出し + 実行時間。小さなワークロードはほぼ 0
- 落とし穴 — コールドスタート、ハンドラ内オブジェクト作成、RDS 暴走 (RDS Proxy)、非同期失敗の漏れ (DLQ)、15 分上限、Payload 6MB、環境変数の秘密
次回 — API Gateway + Lambda #
関数だけでは呼び出す経路が足りません。HTTP リクエスト で Lambda を呼ぶ最もよくあるパターン — API Gateway + Lambda — を次回。
#4 API Gateway + Lambda では REST API と HTTP API の違い、Lambda 統合、ルート / メソッド、権限 (IAM / Cognito / Lambda authorizer)、ステージ / デプロイまで — サーバーレス API の使いどころを一気に整理します。