「サーバー管理から解放されて、イベントに反応するコードだけ書きたい」——AWS Lambda を選ぶ動機は一行で言えます。けれど本番に載せた瞬間、問いが一気に増えます。同じイベントが2回来たら? 起動が遅い(コールドスタート)のはなぜ? DBコネクションは毎回張り直すのか? 失敗したイベントはどこへ消えるのか? 秘密情報はどこに置くのか? そして請求書はなぜこの金額なのか?
この記事は、AWS Lambda を本番品質で設計・運用するための実装ガイドです。題材として、私が中核開発者(主要開発者3名)として構築した、環境・カーボンクレジット/地域通貨のマルチテナント決済プラットフォーム(サーバーレス決済プラットフォームの信頼性設計)での設計判断も交えます。本番稼働中の二重課金・残高不整合 0件を、冪等性・失敗設計・可観測性で支えた実例です。
この記事のルール:Lambda の仕様・パラメータ名・既定値・クォータは AWS 公式ドキュメント(2026年6月時点) に基づきます。クォータや料金は改定され、リージョンや新規アカウントで初期値が異なります。本番投入前に必ず公式の各ページ(末尾「参考」)で最新値を確認してください。最重要の前提:Lambda は「少なくとも1回(at-least-once)」起動されうる。リトライは異常ではなく仕様です。だから関数を冪等に作るのが、信頼性設計の出発点になります。
0. メンタルモデル:Lambda=「イベント → 使い捨ての実行環境 → 使った分だけ課金」
最初に、この記事を貫く5つのメンタルモデルを固定します。ここを腹落ちさせれば、あとの実装はすべてこの帰結です。
- 関数=イベントハンドラ。Lambda は「リクエストを受けたら実行環境を1つ用意し、コードを走らせ、終わったら凍結(または破棄)する」サービス。サーバーは見えない。
- 実行環境=使い捨て、でも一定時間は再利用される。同時リクエストごとに別々の実行環境が割り当てられる(=同時実行数)。1つの環境は終わると凍結され、しばらく次の呼び出しで再利用(ウォームスタート)される。
- ハンドラ外=1度だけ、ハンドラ内=毎回。コールドスタート時にハンドラ外のコード(INIT)が1度走り、以降のウォーム呼び出しではハンドラ内だけが走る。だから接続やSDKクライアントはハンドラ外で張る(第2章)。
- 再試行は前提。非同期呼び出しもイベントソースも at-least-once。「2回来ても結果が同じ」になるよう冪等に作る(第7章)。
- 課金は『割当メモリ × 実行時間(1ms単位)+ リクエスト数』。速くする=安くする、が基本的に一致する(第10章)。
適材適所:Lambda は「イベント駆動・スパイク追従・スケールトゥゼロ」に強く、標準関数の最大実行時間は900秒(15分)・同期ペイロードは6MBという制約があります。長時間処理・常駐サーバー・任意TCP/UDP が要るなら Fargate が本命です。どの計算基盤を選ぶかは姉妹記事 AWS Fargate vs Lambda vs App Runner 技術選定ガイド に委ね、本稿は**「Lambda を選んだ後、どう本番で作るか」**に集中します。
1. 実行環境のライフサイクル:3フェーズと「ハンドラ外」の意味
Lambda の挙動を支配するのは、実行環境(execution environment)のライフサイクルです。公式は3フェーズで説明します。
| フェーズ | 何が起きるか | いつ |
|---|---|---|
| Init | ① Extension init(拡張の起動)② Runtime init(ランタイム起動)③ Function init(ハンドラ外の静的コード実行) | コールドスタート時に1度 |
| Invoke | ハンドラ関数の実行。関数のタイムアウトはこのフェーズ全体に効く | リクエストごと |
| Shutdown | 一定時間呼び出しが無いと環境を破棄。拡張に猶予を与える | アイドル後 |
ここで本番運用に効く事実を3つ押さえます(すべて公式に基づく)。
- Init には10秒の制限がある(標準のオンデマンド関数)。ハンドラ外の初期化が10秒を超えると、最初の呼び出しでタイムアウト扱いになりリトライされる。重い初期化は要注意。
- コールドスタートは全呼び出しの1%未満に収まるのが典型で、所要は「100ms未満〜1秒超」と幅がある。頻度より**裾の重さ(P99)**が問題になる。詳しくは姉妹記事 Lambda コールドスタート最適化ガイド で深掘りします。
- 2025年8月1日から、INIT 時間も課金対象に統一された。以前は「マネージドランタイム+ZIP のオンデマンド関数」だけ INIT が無課金だったが、現在は全構成で INIT が
Billed Durationに含まれる(カスタムランタイム・プロビジョンド同時実行・コンテナイメージは従来から課金)。コールドスタートの初期化はもうタダではない——これは第10章のコスト設計に直結します。
1.1 「ハンドラ外で接続を張る」が最重要パターン
凍結された実行環境はメモリ状態ごと再利用されます。公式も明言する通り、ハンドラ外で宣言したオブジェクトはウォーム呼び出し間で生き続ける。だから DB 接続・SDK クライアントはハンドラ外で1度だけ初期化し、再利用します。これを怠ると毎回接続を張り直し、レイテンシとコストとコネクション枯渇を招きます。
// handler.ts — 「薄いアダプタ」。重い初期化はハンドラ外(INIT)で1度だけ。
import { Logger } from "@aws-lambda-powertools/logger";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import type { SQSEvent, SQSBatchResponse, Context } from "aws-lambda";
// ── INIT フェーズ:コールドスタート時に1度だけ実行され、ウォーム呼び出しで再利用される ──
const logger = new Logger({ serviceName: "orders-worker" });
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({})); // 接続はここで張る
const TABLE = process.env.ORDERS_TABLE; // 設定の読み出しもここで(毎回 process.env を引かない)
// ── INVOKE フェーズ:リクエストごとにここだけが走る ──
export const handler = async (
event: SQSEvent,
_context: Context,
): Promise<SQSBatchResponse> => {
if (!TABLE) throw new Error("ORDERS_TABLE is not configured"); // 起動時設定の検証
const batchItemFailures: { itemIdentifier: string }[] = [];
for (const record of event.Records) {
try {
await processOrder(JSON.parse(record.body), { ddb, table: TABLE, logger });
} catch (err) {
// 失敗した messageId だけを再表示させる(部分バッチ失敗。第6章)
logger.error("record processing failed", { messageId: record.messageId, err });
batchItemFailures.push({ itemIdentifier: record.messageId });
}
}
return { batchItemFailures };
};
注意:ハンドラ外の状態は「再利用されることがある」だけで、保証はされない。コールドスタートのたびにリセットされ、同時実行ごとに別環境になります。だから「グローバル変数をキャッシュ代わりに使う」のは可(高速化)でも、「グローバル変数を一意性や整合性の拠り所にする」のは不可。一意なIDやトークンはハンドラ内で都度生成します(SnapStart では特に重要——姉妹記事参照)。
2. ハンドラの作り方:薄いアダプタ+テスト容易性
ハンドラは「イベントを受け取り、ビジネスロジックに渡す薄いアダプタ」に徹します。これが SRP(単一責任)であり、テスト容易性の鍵です。
公式の標準シグネチャは言語ごとに決まっています。
// Node.js(推奨は async ハンドラ。Node.js 24 以降は callback 形式は不可)
export const handler = async (event: MyEvent, context: Context): Promise<MyResult> => { /* ... */ };
# Python(async ハンドラは不可。引数は必ず event, context の2つ)
def lambda_handler(event, context):
...
context には運用で効く情報が入っています。context.aws_request_id(Python)/ context.awsRequestId(Node.js)は相関IDとして全ログに通す。get_remaining_time_in_millis() / getRemainingTimeInMillis() は残り時間を返すので、タイムアウト直前に安全に切り上げる制御に使えます。
2.1 ビジネスロジックは「Lambda を知らない関数」に切り出す
ハンドラ=アダプタ、ロジック=純粋関数。こう分けると、ロジックは Lambda ランタイム無しで単体テストできます(第11章)。AWS のテストガイドも「ハンドラは slim adapter にし、ビジネスロジックを分離して単体テストせよ」と推奨しています。
# domain.py — Lambda を一切 import しない純粋ロジック。単体テストが容易。
from dataclasses import dataclass
@dataclass(frozen=True)
class Order:
id: str
amount: int # 最小通貨単位(整数)。金額に float を使わない
def total_with_tax(order: Order, tax_rate: float) -> int:
if order.amount < 0:
raise ValueError("amount must be non-negative") # 境界で検証
return round(order.amount * (1 + tax_rate))
# handler.py — 薄いアダプタ。境界での検証+ロジック呼び出しに専念。
import json
from domain import Order, total_with_tax
def lambda_handler(event, context):
body = json.loads(event["body"]) # 外部入力は境界でパース
order = Order(id=str(body["id"]), amount=int(body["amount"]))
return {"statusCode": 200, "body": json.dumps({"total": total_with_tax(order, 0.10)})}
3. パッケージング:ZIP / コンテナ / レイヤー
デプロイ方式は2つ、依存の共有手段が1つあります。判断軸を表で固定します。
| 方式 | サイズ上限(公式) | 向くケース |
|---|---|---|
| ZIP アーカイブ | 直接アップロード50MB / 解凍後250MB(レイヤー・カスタムランタイム込み) | 一般的な関数。最速のデプロイ。レイヤーが使える |
| コンテナイメージ(OCI) | 最大10GB | 大きな依存(MLライブラリ等)、独自OS/ランタイム、既存コンテナCIとの統合 |
| レイヤー | 関数あたり最大5個、/opt に展開、解凍後250MBの枠を共有 | 共通依存・共通ユーティリティをZIP関数群で共有 |
実務の指針:
- まずは ZIP。デプロイが速く、レイヤーで共通依存を切り出せる。250MBの解凍後上限に当たったらコンテナへ。
- コンテナは「大きい・特殊・既存資産」のとき。10GBまで積めるが、コールドスタートと運用は重くなりがち。レイヤーは使えない(依存はイメージに同梱)。
- Go / Rust ではレイヤー非推奨(公式)。実行ファイルに依存を同梱した方がコールドスタートに有利。
アンチパターン:
node_modulesを丸ごと固める。AWS SDK v3 は必要なサブクライアントだけ import し、不要な依存を削るとパッケージが小さくなり、ダウンロード=コールドスタートが速くなります。バンドラ(esbuild 等)でツリーシェイクするのが定石です。
4. 同時実行とスケール:予約・プロビジョンド・スロットリング
Lambda の「スケール」を数字で理解します。
- 同時実行数(concurrency)=同時に処理中のリクエスト数。1リクエストにつき実行環境1つ。
- リトルの法則:
同時実行数 = 平均リクエスト/秒 × 平均処理時間(秒)。例:100 req/s × 0.5s = 50。これが必要な同時実行数の見積もり式。 - アカウント既定の同時実行上限は1,000(リージョン単位、引き上げ申請可)。さらにRPS上限はアカウント同時実行の10倍(既定なら10,000 RPS)——処理が数十msと短い関数で効いてきます。
- スケール速度は「1関数あたり10秒で1,000環境ずつ」。各関数が独立してこの速度でバーストできる(リージョン共有の旧バースト枠は撤廃済み)。
同時実行の制御レバーは2つあります。混同しないよう表で固定します。
| レバー | 何をするか | 課金 | 主な用途 |
|---|---|---|---|
| 予約済み同時実行(Reserved) | 関数の同時実行に上限と下限を設定。他関数はその枠を使えない | 追加課金なし(ただしアカウント枠を消費) | 下流(RDS等)の保護、暴走の封じ込め、0で緊急停止(キルスイッチ) |
| プロビジョンド同時実行(Provisioned) | 環境を事前に初期化しておきコールドスタートを消す | 追加課金あり | 二桁ミリ秒の応答が必須のレイテンシ重視API |
スロットリング:同時実行(または10倍のRPS)上限を超えると、同期呼び出しは HTTP 429(TooManyRequestsException) を返します。予約済み同時実行 = 0 にすると全リクエストを 429 で弾くので、インシデント時のキルスイッチとして使えます。非同期・イベントソースはスロットル時に内部キューで保持・再試行されます(第6章)。
プロビジョンド同時実行・SnapStart・コールドスタートの使い分けと費用対効果は、これだけで一本の記事になります。Lambda コールドスタート最適化ガイド に詳細を分離しました。
5. 呼び出しモデルと失敗の扱い:sync / async / イベントソース
「誰がリトライするのか」は呼び出しモデルで決まります。ここを取り違えると、失敗が静かに消えるか永遠に詰まるかのどちらかになります。
| モデル | 例 | リトライの主体 |
|---|---|---|
| 同期(RequestResponse) | API Gateway、Function URL、直接 Invoke | Lambda は自動リトライしない。呼び出し側が判断 |
| 非同期(Event) | S3通知、SNS、EventBridge ルール | Lambda が内部キューで再試行(既定2回・計3回) |
| イベントソースマッピング | SQS、Kinesis、DynamoDB Streams | Lambda がポーリングして同期起動。再試行はソース仕様に従う |
5.1 非同期呼び出し:既定2回リトライ、証跡は Destinations で受ける
非同期呼び出しは内部キューに入り、関数エラー時に**既定で2回再試行(計3回)**します(1回目まで1分、2回目まで2分待つ)。制御パラメータは2つ:
MaximumRetryAttempts:0〜2(既定2)。MaximumEventAgeInSeconds:60〜21600(=最大6時間)。古すぎるイベントは捨てる。
失敗イベントの行き先は、旧来のDLQ(SQS/SNSのみ)ではなく On-failure Destinations を推奨します。Destinations は **SQS / SNS / Lambda / EventBridge(失敗時はS3も)**を宛先にでき、関数のレスポンス詳細も記録されます。成功時の宛先(on-success)も指定できます。
# Terraform: 非同期呼び出しのリトライ上限・イベント寿命・失敗時の宛先
resource "aws_lambda_function_event_invoke_config" "orders" {
function_name = aws_lambda_function.orders.function_name
maximum_retry_attempts = 2 # 0〜2(既定2)
maximum_event_age_in_seconds = 3600 # 60〜21600。1時間より古い失敗は捨てる
destination_config {
on_failure {
destination = aws_sqs_queue.orders_dlq.arn # 調査・再処理できる隔離先へ
}
}
}
5.2 イベントソース(SQS/ストリーム):部分バッチ失敗で「全部やり直し」を防ぐ
イベントソースマッピングは関数をバッチで起動します。ここで初学者が必ず踏む地雷:バッチ処理中に例外を投げると、成功済みも含めてバッチ全体が再処理される。
これを防ぐのが部分バッチ失敗(partial batch response)。FunctionResponseTypes=ReportBatchItemFailures を設定し、関数は失敗した ID だけを batchItemFailures に返します(第1章のTSコード参照)。SQS・Kinesis・DynamoDB Streams で共通の仕組みです。ストリームでは BisectBatchOnFunctionError(失敗時にバッチを二分割して再試行)や MaximumRetryAttempts / MaximumRecordAgeInSeconds、On-failure 宛先で「毒レコードがシャードを最大7日詰まらせる」事故を防ぎます。
SQS×Lambda の冪等consumer・可視性タイムアウト・DLQ 設計の詳細は SQS + Lambda + EventBridge で冪等な非同期処理を作る、DynamoDB Streams を起点にした CDC/イベント駆動は DynamoDB Streams × Lambda イベント駆動アーキテクチャ に、それぞれ専用記事があります。
6. 冪等性:再試行を「正常系」に変える
at-least-once の世界では、冪等性こそが信頼性の土台です。「同じイベントが2回来ても、課金は1回・在庫引当は1回」にする。これを自前で作ると、冪等性キーの条件付き挿入・TTL・進行中ロックを正しく実装する必要があり、競合状態で穴が開きがちです。
新規実装なら Powertools for AWS Lambda の冪等化ユーティリティが定石です。ペイロード(または特定フィールド)から冪等性キーを導出し、DynamoDB に処理状態を記録。進行中は INPROGRESS レコードでロックし、並行する重複呼び出しを弾く。完了後は結果をキャッシュして返します。
# Python: Powertools の冪等化。デコレータ1つで「2回来ても1回」にする。
from aws_lambda_powertools.utilities.idempotency import (
DynamoDBPersistenceLayer, idempotent_function, IdempotencyConfig,
)
persistence = DynamoDBPersistenceLayer(table_name="idempotency")
# 全ペイロードではなく注文IDだけを冪等性キーにする(再送で payload が微妙に変わっても同一視)
config = IdempotencyConfig(event_key_jmespath="id", expires_after_seconds=3600)
@idempotent_function(data_keyword_argument="order", persistence_store=persistence, config=config)
def charge(order: dict) -> dict:
# ここが2回呼ばれても、課金は1回しか実行されない
return run_charge(order)
// TypeScript: @aws-lambda-powertools/idempotency(peer依存に @aws-sdk/client-dynamodb と lib-dynamodb)
import { makeIdempotent } from "@aws-lambda-powertools/idempotency";
import { DynamoDBPersistenceLayer } from "@aws-lambda-powertools/idempotency/dynamodb";
const persistenceStore = new DynamoDBPersistenceLayer({ tableName: "idempotency" });
export const handler = makeIdempotent(async (event: { id: string }) => runCharge(event), {
persistenceStore,
});
設計の勘所:冪等性キーは「同じ業務操作なら同じ値」になるよう選ぶ(注文ID・請求期間など)。全ペイロードをキーにすると、リトライ時にタイムスタンプ等が混ざって別物と誤認します。TTL(既定3600秒)は「再送が来うる窓」より長く。私が決済プラットフォームで二重課金0件を達成できたのは、この冪等性キー設計を業務の不変条件に合わせたからです。冪等化の理論的背景は 冪等な決済設計(二重課金対策) も参照ください。
7. 可観測性:コード変更ゼロの構造化ログ+メトリクス+トレース
「止まった処理を一目で追える」状態を作ります。Lambda は CloudWatch Logs に自動でログを送ります(実行ロールに AWSLambdaBasicExecutionRole が必要)。本番では3点セットで固めます。
- JSON 構造化ログ+ログレベル制御(コード変更不要)。Lambda の「高度なロギング設定」で、ログ形式(Text/JSON)とアプリケーションログレベル(TRACE〜FATAL、既定INFO)を関数設定で切り替えられます(JSON形式が前提)。
console.logを JSON で検索・集計可能にし、本番は INFO・調査時だけ DEBUG に上げる、をデプロイ無しで実現します。 - Powertools の Logger / Tracer / Metrics。Logger は相関ID付きの構造化ログ、Tracer は X-Ray の薄いラッパ、Metrics は EMF(Embedded Metric Format)で標準出力にメトリクスを書き出し、CloudWatch が非同期に抽出します(
PutMetricDataを同期呼びしないので速くて安い)。 - X-Ray アクティブトレーシング。
Activeにするとサンプリングされたリクエストのトレースを自動生成。xray:PutTraceSegments等の権限(AWSXRayDaemonWriteAccess)が要ります。
# Powertools: 構造化ログ・トレース・EMFメトリクスを最小コードで
from aws_lambda_powertools import Logger, Tracer, Metrics
from aws_lambda_powertools.metrics import MetricUnit
logger = Logger(service="orders") # JSON構造化ログ+相関ID
tracer = Tracer(service="orders") # X-Ray セグメント
metrics = Metrics(namespace="Orders") # EMFでCloudWatchメトリクス(同期API呼び出し不要)
@metrics.log_metrics # 集計したメトリクスをフラッシュ
@tracer.capture_lambda_handler # ハンドラをトレース
@logger.inject_lambda_context # request_id等を全ログに付与
def lambda_handler(event, context):
metrics.add_metric(name="OrdersProcessed", unit=MetricUnit.Count, value=1)
logger.info("processing order", extra={"order_id": event.get("id")})
return {"ok": True}
アラートは症状で鳴らす:
Errors/Throttles/Duration(P99) /ConcurrentExecutions/ Destinations の失敗キュー件数(1件で即通知)。Lambda Insights(拡張レイヤー)を足すと、CPU・メモリ・コールドスタートまで1呼び出し1イベントで取れます。可観測性の体系は OpenTelemetryで本番の可観測性を作る を参照。
8. セキュリティ:最小権限・秘密情報・VPC・公開エンドポイント
Lambda のセキュリティは「2つのポリシー」を分けて理解するところから始まります。
- 実行ロール(execution role):関数がassume する IAM ロール。AWSサービスへのアクセス権はここ。最小権限で。まずは
AWSLambdaBasicExecutionRole(ログ権限のみ)から始め、必要な操作だけ足す。IAM Access Analyzer で CloudTrail の利用実績からポリシーを生成するのが安全。 - リソースベースポリシー:誰がこの関数を呼べるか。AWSサービスが起動する場合はこちらだけが評価されます。
// 実行ロールの最小権限例:この関数が触る1テーブルへの必要操作のみ。ワイルドカード '*' を避ける
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["dynamodb:PutItem", "dynamodb:GetItem"],
"Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/orders"
}]
}
秘密情報:環境変数は KMS で保存時暗号化されます(既定は AWS マネージドキー、要件次第で CMK)。ただしAPI キーやDB資格情報を環境変数に平文で置くのは非推奨。公式の指針は Secrets Manager / SSM Parameter Store に置き、AWS Parameters and Secrets Lambda Extension で取得・キャッシュ(既定TTL 300秒)すること。アプリは「拡張のローカルエンドポイントから読む」だけになり、ローテーションにも追従できます。
VPC:プライベートな RDS 等へは Lambda を VPC にアタッチして接続します。外向き通信には NAT ゲートウェイが必要(プライベートサブネット配置だけではインターネットに出られない)。Lambda が作る ENI は Hyperplane ENI で、同じサブネット+セキュリティグループの関数間で共有・再利用されるため、VPC でも高速にスケールします。AWSサービスへは VPC エンドポイントで NAT を介さず到達できます。
Function URL:関数に直結する HTTPS エンドポイント。認証は AWS_IAM か NONE。公開API以外は AWS_IAM を選ぶ。NONE(公開)にするなら、最低限 WAF・レート制限(予約済み同時実行)・lambda:InvokedViaFunctionUrl 条件キーで締める。2025年10月以降、新規 Function URL は lambda:InvokeFunctionUrl と lambda:InvokeFunction の両権限を要します。
9. コスト最適化:請求書の式を理解し、速さで安くする
Lambda の料金は2要素です。式が分かれば請求書が読めます。
月額 ≒ リクエスト課金 + 実行時間課金
リクエスト課金 = 総リクエスト数 × $0.20 / 100万
実行時間課金 = 割当メモリ(GB) × 実行時間(秒, 1ms単位で切り上げ) × 単価(GB-秒)
- x86 の実行時間単価は約 $0.0000166667 / GB-秒(第1ティア・US East基準)。Arm64(Graviton2)は実行料金が x86 比20%安い。互換性があるなら Arm64 が基本的に有利。
- 無料利用枠:月100万リクエスト+40万 GB-秒。
- 実行時間は1ms単位で切り上げ。
/tmpを512MB超にする・プロビジョンド同時実行を使う、は別課金。 - 2025年8月から INIT 時間も課金対象(マネージドランタイム+ZIPのオンデマンド関数も対象に統一)。重い初期化はコストにも乗る。
9.1 メモリ調整は「速さ=安さ」の単一ノブ
CPU はメモリに比例配分され、1,769MB で1 vCPU 相当。つまりメモリを上げると CPU も増え、CPUバウンドな処理は短時間化してトータルコストが下がることがよくあります(128MBで遅く回すより1024MBで速く終える方が安い、という逆転が起きる)。メモリは推測せず計測して決めるのが鉄則です。AWS Lambda Power Tuning(公式サンプル)でコスト最小点を探します。
| 安くする手 | 効き方 |
|---|---|
| Arm64 に切り替え | 実行料金20%減。多くのランタイムでビルドし直すだけ |
| メモリを計測で最適化 | CPUバウンドは増メモリで短時間化→総額減のことが多い |
| パッケージを小さく | コールドスタート短縮=INIT課金とレイテンシ減 |
| 接続をハンドラ外で再利用 | ウォーム呼び出しの実行時間短縮 |
| 不要な同期待ちを消す | 外部API待ちの間も課金される。並行化・非同期化 |
10. テスト容易性:ロジックは手元で、結合はクラウドで
Lambda のテストは2層で考えます。
- 単体テスト:ハンドラを薄く保ち(第2章)、ビジネスロジックは Lambda 非依存の純粋関数として普通の単体テストで回す。
contextはモックでよい。最速・最安・最も多くのバグをここで捕まえる。 - 結合テスト:AWS は**「クラウドでのテストを優先せよ」と明言しています。エミュレータ(
sam local invoke/start-api、LocalStack 等)は速い反面、IAM・サービス設定・クォータ・API差分を再現しきれず「手元では通るがクラウドで落ちる」を生む。エミュレータは控えめに**使い、本物のサービスに対する結合・E2E をクラウドで回すのが公式の推奨です。
# 単体テスト:純粋ロジックは Lambda 無しで即テストできる(第2章の domain.py)
import pytest
from domain import Order, total_with_tax
def test_total_with_tax_rounds_half_up():
assert total_with_tax(Order(id="o1", amount=1000), 0.10) == 1100
def test_negative_amount_rejected():
with pytest.raises(ValueError):
total_with_tax(Order(id="o2", amount=-1), 0.10)
# 結合の入口:手元で1度だけ叩く(Docker必須)。本格的な検証はクラウドにデプロイして行う
sam local invoke OrdersFunction --event events/order.json
11. まとめ:本番Lambdaチートシート
最後に、迷ったときの早見表です。
- 実行モデル:Init→Invoke→Shutdown。接続・SDKクライアントはハンドラ外で1度だけ。一意なIDはハンドラ内で都度生成。
- ハンドラ:薄いアダプタ。ビジネスロジックはLambda非依存の純粋関数に切り出してテストする。
- パッケージ:まずZIP+レイヤー、250MB超でコンテナ。
node_modules丸ごとはNG、ツリーシェイクする。 - スケール:必要同時実行=RPS×処理秒。下流保護は予約済み、レイテンシ保証はプロビジョンド、
予約=0で緊急停止。 - 失敗設計:同期は自動リトライ無し。非同期は既定2回+On-failure Destinations。イベントソースは部分バッチ失敗で全件やり直しを防ぐ。
- 冪等性:再試行は仕様。Powertoolsの冪等化+業務に沿った冪等性キー+TTL。
- 可観測性:JSON構造化ログ+ログレベル制御(コード変更ゼロ)、Powertools Metrics(EMF)、X-Ray。症状でアラート。
- セキュリティ:実行ロールは最小権限。秘密はSecrets Manager+拡張でキャッシュ。VPCはNAT/エンドポイント。Function URLは原則
AWS_IAM。 - コスト:
メモリ×時間(1ms)+リクエスト。Arm64で20%減、メモリは計測で最適化、INITも課金される。
Lambda は「サーバーレスだから楽」ではなく、at-least-once・コールドスタート・最小権限・課金の式という制約の上で、冪等性・可観測性・コストを設計する仕事です。私は環境分野のマルチテナント決済プラットフォームで、月次一括課金・CO2集計を冪等・自動再処理・最小権限・症状ベースのアラートで固め、本番稼働中の二重課金・残高不整合 0件を達成しました。
「自社のこのワークロードを、Lambda で速く・安く・落ちても自動で立ち直る形にしたい」——その設計から実装・運用・コスト最適化まで、一人 × 生成AI(Claude Code)の速さで一気通貫に伴走します。 計算基盤の選定段階(Lambda か Fargate か)からでも、お気軽にご相談ください。
参考(公式ドキュメント)
- Lambda execution environment lifecycle — Init/Invoke/Shutdown の3フェーズ、コールドスタート、ハンドラ外オブジェクトの再利用
- AWS Lambda standardizes billing for the INIT phase — 2025年8月1日からの INIT 課金統一
- Lambda quotas — メモリ128MB〜10,240MB(1,769MBで1vCPU)、タイムアウト900秒、ペイロード、
/tmp、パッケージサイズ、同時実行1,000 - Lambda runtimes — サポートランタイムと非推奨スケジュール、x86_64/arm64
- Building Lambda functions with Node.js / Python — ハンドラと context
- Lambda concurrency / Scaling behavior — 同時実行、リトルの法則、予約/プロビジョンド、1関数10秒1,000環境
- Asynchronous invocation / Error handling and retries — 既定2回リトライ、
MaximumRetryAttempts、MaximumEventAge、Destinations - Idempotency - Powertools for AWS Lambda —
@idempotent/IdempotencyConfig/DynamoDBPersistenceLayer - Advanced logging controls — JSON形式・ログレベル制御(コード変更不要)
- Lambda security & permissions / Env var encryption / VPC — 実行ロール、リソースベースポリシー、KMS、Hyperplane ENI
- Testing Lambda functions — クラウド優先のテスト戦略
- AWS Lambda Pricing — リクエスト/実行時間課金、無料枠、Arm/プロビジョンド/
/tmp料金