はじめに
週末に BASTARD に嵌まっている @___nix___ です。
2026年9月9日、Lambda の関数タイムアウトが最大90分になりました。
早速 Timeout を 5400 にしてみたいと感じた方は多いと思いますが、単にタイムアウトの時間が延ばせるという単純な話ではありません。15分の壁を越える方法を移行手順としてまとめます。
背景
Lambda の15分制限、みなさん一度はぶつかる課題かと思います。「あと3分あれば終わるのに」という処理を、わざわざ分割したり、途中経過を DynamoDB に書いて次の実行で拾い直したり。そんな手間を解消しましょう。
概要
90分が使えるのは Lambda Managed Instances(以下 LMI)に載せた関数です。LMI は昨年の re:Invent で登場した、Lambda を自前アカウントの EC2 上で動かす実行形態ですね。
ただし LMI に載せれば無条件で90分、ではありません。
| 呼び出し方 | 標準の Lambda | LMI |
|---|---|---|
| 同期(API Gateway / Step Functions Task など) | 15分 | 15分のまま |
| 非同期(EventBridge / S3 / SNS など) | 15分 | 90分 |
| ESM※(SQS / Kinesis / DynamoDB Streams など) | 15分 | 90分 |
| ESM のうち Amazon MQ / DocumentDB | 15分 | 15分のまま |
※Event Source Mapping(イベントソースマッピング)の略。SQS や Kinesis、DynamoDB Streams などを Lambda 側がポーリングし、取得したレコードをバッチで関数に渡す仕組みです。送信側から呼ばれる非同期呼び出しとは向きが逆です。
初期化(Init)フェーズも15分のままです。やることは2つです。関数を LMI に載せ替えることと、呼び出しを非同期か ESM にすること。
現状
移行元は、EventBridge Scheduler で毎晩叩かれる日次バッチです。標準の Lambda なので、そもそも 5400 は設定できません。
$ aws lambda update-function-configuration --function-name batch-job --timeout 5400
ValidationException: Value '5400' at 'timeout' failed to satisfy constraint:
Member must have value less than or equal to 900
移行前の Terraform(クリックで展開)
resource "aws_lambda_function" "batch_job" {
function_name = "batch-job"
role = aws_iam_role.execution.arn
handler = "app.handler"
runtime = "python3.12" # LMI は 3.13 以上が必須なので後で上げる
memory_size = 512 # LMI は 2048 以上が必須なので後で上げる
timeout = 900 # 15 分が上限
filename = data.archive_file.job.output_path
source_code_hash = data.archive_file.job.output_base64sha256
}
対応
Terraform で書くリソースの差分はこれだけです。
新しく足すのは オペレーターロール と キャパシティプロバイダ の2つ、既存の関数に手を入れるのが1つ。VPC は既存のものを流用します。順に見ていきます。
1. オペレーターロールを作る
LMI では実行ロールとは別に、Lambda が EC2 を管理するためのオペレーターロールが要ります。
resource "aws_iam_role" "operator" {
name = "batch-job-cp-operator"
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
}
resource "aws_iam_role_policy_attachment" "operator_ec2" {
role = aws_iam_role.operator.name
policy_arn = "arn:aws:iam::aws:policy/AWSLambdaManagedEC2ResourceOperator"
}
apply する側には lambda:PassCapacityProvider と、オペレーターロールを渡す iam:PassRole が要ります。アカウントで初めて作るときは iam:CreateServiceLinkedRole も必要です。
2. キャパシティプロバイダを作る
関数を動かす EC2 の置き場です。LMI は VPC 必須なので、サブネットとセキュリティグループを渡します。
resource "aws_lambda_capacity_provider" "batch" {
name = "batch-jobs"
vpc_config {
subnet_ids = var.private_subnet_ids # 複数 AZ に分散させる
security_group_ids = [aws_security_group.lmi.id]
}
permissions_config {
capacity_provider_operator_role_arn = aws_iam_role.operator.arn
}
instance_requirements {
architectures = ["arm64"] # 関数側と一致させること
}
capacity_provider_scaling_config {
scaling_mode = "Auto"
max_vcpu_count = 16
}
}
今回のようなプライベートサブネット構成では CloudWatch Logs への送信も VPC 経由になります。logs の VPC エンドポイントか NAT を用意しておかないと関数は成功しているのにログが1行も出ません。
3. 関数をキャパシティプロバイダに紐付ける
resource "aws_lambda_function" "batch_job" {
function_name = "batch-job"
role = aws_iam_role.execution.arn
handler = "app.handler"
runtime = "python3.13" # ← 3.13 以上が必須
architectures = ["arm64"]
memory_size = 2048 # ← 最低 2048
filename = data.archive_file.job.output_path
source_code_hash = data.archive_file.job.output_base64sha256
publish = true # ← バージョン発行が必須
publish_to = "LATEST_PUBLISHED"
capacity_provider_config { # ← 追加
lambda_managed_instances_capacity_provider_config {
capacity_provider_arn = aws_lambda_capacity_provider.batch.arn
execution_environment_memory_gib_per_vcpu = 2.0
per_execution_environment_max_concurrency = 4
}
}
# vpc_config は書けない(capacity_provider_config と併用不可)
timeout = 900 # ← 5400 にできない。後述
lifecycle {
ignore_changes = [timeout]
}
}
対応ランタイムは Python 3.13+ / Node.js 22+ / Java 21+ / .NET 8+ / Rust のみです。1つ下の 3.12 でもこう弾かれるので、古いままの関数はまず上げる必要があります。
Runtime Enum python3.12 does not support specified feature: Lambda Managed Instances
メモリと vCPU は vCPU 数 = memory_size ÷ 1024 ÷ 比率 の関係で、比率は 2 / 4 / 8 しか指定できません。この例は 2048 ÷ 1024 ÷ 2 = 1 vCPU。これを同一実行環境の最大4リクエストで共有します。
そしてバージョン発行が必須です。発行前の $LATEST は ActiveNonInvocable で呼べません。publish_to = "LATEST_PUBLISHED" にしておくと $LATEST.PUBLISHED を上書き発行し続けられます。
4. timeout = 5400 は Terraform で書けない
肝心のタイムアウトですが、plan ですらなく validate で落ちます。
Error: expected timeout to be in the range (1 - 900), got 5400
プロバイダ v6.64.0 時点で validation.IntBetween(1, 900) のままでした。API 側は対応済みなので、単純に追随待ちですね。
というわけで timeout だけ Terraform の管理から外し(前掲の ignore_changes)、apply 後に CLI で設定します。
aws lambda update-function-configuration --function-name batch-job --timeout 5400
aws lambda wait function-updated-v2 --function-name batch-job
aws lambda publish-version --function-name batch-job \
--publish-to LATEST_PUBLISHED
更新や発行の処理中は ResourceConflictException で弾かれることがあります。その時は少し待って再実行してください。
$ aws lambda get-function-configuration --function-name batch-job --qualifier '$LATEST.PUBLISHED'
{ "Timeout": 5400, "Version": "$LATEST.PUBLISHED", "State": "Active" }
5. 呼び出しが非同期か確認する
今回の EventBridge Scheduler は非同期なので、呼び出し方の変更は不要です。EventBridge Rules、S3 イベント通知、SNS も非同期、SQS などは ESM なのでそのまま使えます。
ただし SQS はキュー側の見直しが要ります。可視性タイムアウトが関数タイムアウトより短いと重複処理になるので、AWS 推奨の6倍なら 32,400 秒です。
実験
15分を超えて動くのか確かめます。ハンドラの先頭で残り時間を出すようにしました。
print(json.dumps({"remaining_ms": context.get_remaining_time_in_millis(),
"request_id": context.aws_request_id}))
1,000秒(16分40秒)かかる処理を投げます。違いは --invocation-type Event を足すかどうかだけです。同期側は AWS CLI の読み取りタイムアウトが既定60秒なので、そこだけ延ばしています。
# 同期(既定の RequestResponse)
aws lambda invoke --function-name 'batch-job:$LATEST.PUBLISHED' \
--payload '{"sleep":1000}' --cli-binary-format raw-in-base64-out \
--cli-read-timeout 1200 sync.json
# 非同期
aws lambda invoke --function-name 'batch-job:$LATEST.PUBLISHED' --invocation-type Event \
--payload '{"sleep":1000}' --cli-binary-format raw-in-base64-out async.json
CloudWatch Logs に出た1行目がこちらです。
{"remaining_ms": 5399998, "request_id": "ca5213fb-..."} ← 非同期
{"remaining_ms": 899996, "request_id": "02dccf26-..."} ← 同期
同じ関数、同じ設定。それでも残り時間は 5,400秒 と 900秒 に分かれます。get-function-configuration はどちらから見ても Timeout: 5400 を返すので、関数の設定を眺めても同期側が15分だとは分かりません。
結果は数値のとおりでした。同期側は約900秒でタイムアウトエラーになり、
{"errorType":"Sandbox.Timedout","errorMessage":"Task timed out after 900.00 seconds"}
非同期側はその後も走り続け、16分40秒(elapsed_sec: 1000.002073)で正常終了。15分の壁は確かに越えられます。
ひとつ注意で、LMI はタイムアウトしてもコードを強制停止しません。呼び出し元にはエラーが返る一方、処理はそのまま動き続け、副作用も起こりえます。get_remaining_time_in_millis() を見て自分で切り上げる実装が要ります。
請求
LMI にすると自分のアカウントに EC2 が立ちます。検証では m9g.xlarge が3台(3AZ 分散)。最小実行環境数がデフォルト3なので、トラフィックがゼロでも動き続けます。
しかもこの EC2、既定では describe-instances に出てきません。aws ec2 modify-managed-resource-visibility --default-visibility visible で見えるようになります。
請求も分かれます。以下は東京リージョンでの実績で、検証期間(インスタンス時間で約1時間ぶん)の明細から LMI 分を抜き出したものです。
| サービス | 使用タイプ | 金額 |
|---|---|---|
| EC2 - Compute | APN1-BoxUsage:m9g.xlarge |
$0.2609 |
| Lambda | APN1-Lambda-Managed-Instances-m9g.xlarge-Management-Hours |
$0.0391 |
| EC2 - Other | APN1-EBS:VolumeUsage.gp3 |
$0.0115 |
インスタンス料金は Lambda ではなく EC2 に計上され、Lambda 側に出るのは管理手数料とリクエスト料金です。EBS も別途かかります。管理手数料は EC2 オンデマンド価格の15%で、EC2 部分に Compute Savings Plans や Reserved Instances を効かせても手数料は割引対象外です。
止めるときは、そのキャパシティプロバイダで動いている関数バージョンをすべて消せば、プロバイダを残したまま EC2 が終了します(検証では約3分)。ほかの関数が同じプロバイダに載っていれば、その分は残ります。
終わりに
15分の壁は壊れましたが、条件が付きます。非同期か ESM であること、そして LMI に載せ替えること(VPC 必須、メモリ 2GB 以上、ランタイム更新、バージョン発行)。Terraform 勢は当面 timeout = 5400 が書けない点も頭の隅に置いておきましょう。
それでも、あの中断・再開ロジックを捨てられるなら十分に見合うと思います。なお90分でも足りない場合は Durable Functions もありますので以下の記事も参考にしてください。
一言
この記事良かったと少しでも思って頂けたら是非 @___nix___ をフォローしてあげてください。或いは記事に対してリアクションをお願い致します。
