👋 はじめに
開発環境向けの CDK デプロイ(GitHub Actions)を回したら、いつもは10分ちょっとで終わるはずが1時間経っても終わらず、最後は次のエラーで落ちました。
Assuming role failed: ExpiredToken
The security token included in the request is expired
メッセージだけ見ると IAM ロールのセッション期限切れなので、「じゃあセッション時間を延ばせばいい」と考えたくなります。
しかし、今まで10分程度で終わっていたパイプラインが急に1時間たっても終わらないようになってしまい、急いで原因解析して修正する必要が出てきて現場でアタフタしていました。
(コマンド例・数値・構成はすべて説明用に一般化・匿名化しています)
🎯 TL;DR
- 症状:CDK デプロイが 10分 → 1時間超 に悪化し、
ExpiredTokenで失敗するようになった - 一次診断ミス:
ExpiredTokenを見て「IAM セッション期限が原因」と考えがち。でもそれは 結果 であって原因ではない - 真因:AWS CDK の
NodejsFunction(aws-lambda-nodejs)が、bundling.nodeModules指定によって Lambda 1個ごとにnpm ciを実行 していた。そのnpm ciがデフォルトで走らせる audit 通信 が、退役中の npm audit エンドポイント待ちで 1回あたり数分ストール - Lambda が十数個に増えていたため、synth 時の
npm ciだけで 約50分 を消費 → CloudFormation に到達する前に1時間のセッション期限を超過 - 対策:
npm_config_audit=false/npm_config_fund=falseをジョブに設定して audit を無効化。各インストールが 数分 → 数秒 に短縮され、全体7分半で復旧
🏗️ 前提の構成
よくある「CDK で Lambda 群をデプロイする」構成です。
- インフラは AWS CDK(TypeScript)で定義
- バッチ処理などを担う Node.js 製の Lambda が十数個
- 各 Lambda は
aws-lambda-nodejsのNodejsFunctionでバンドル - DB ドライバ(
mysql2)だけ esbuild でバンドルせずnodeModulesで個別インストールする設定
コードにすると、Lambda 側の共通設定はこんなイメージです(識別子は例示用)。
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
new NodejsFunction(this, 'SomeBatchFunction', {
runtime: Runtime.NODEJS_22_X,
entry: 'src/handler.ts',
bundling: {
target: 'node22',
externalModules: ['@aws-sdk/*'],
nodeModules: ['mysql2'], // ← これが後の伏線
},
});
GitHub Actions 側は「Docker ビルド → 依存インストール → cdk deploy」という素直な流れです。
📅 まず起きたことを実測で押さえる
GitHub Actions のステップ別所要時間 を取りました。失敗したデプロイの内訳がこれです。
| ステップ | 所要 |
|---|---|
| Checkout 〜 認証情報の取得 | 約20秒 |
| Docker build & push | 約2.5分(キャッシュ良好) |
CDK の npm install
|
約4.3分 |
Lambda 用依存の npm ci
|
約1.5分 |
| CDK Deploy | 約55分 → 失敗 |
認証情報を取得したのは実行開始直後(仮に 04:05)です。aws-actions/configure-aws-credentials の OIDC セッションはデフォルトで1時間なので、05:05 頃には失効します。実際にデプロイが落ちたのはその直後でした。時刻を見れば、たしかにセッション期限切れではあります。
ただ、それはあくまで「時間切れになった結果」でしかありません。見てのとおり、時間のほとんどは CDK Deploy に吸い込まれています。問題はこの55分で何が起きているかなので、ログを追いました。
🔍 真犯人:Lambda ごとの npm ci と audit 通信
CDK Deploy のログを見ると、各 Lambda のバンドルでこのパターンが繰り返されていました。
Bundling asset .../SomeFunction/Code/Stage...
...index.js 497.0kb
Done in 55ms ← esbuild 本体は爆速
added 13 packages ... in 2m ← ここで数分ストール
- esbuild によるバンドルは毎回 100ms 前後 で終わっている。ここは無罪
- 問題はその直後の
added 13 packages ... in 2m(時にはin 7m)。これはnodeModules指定のmysql2を入れるためのnpm ci
aws-lambda-nodejs は、nodeModules を指定すると ロックファイルを検出して npm ci を実行 します。そして npm ci はデフォルトで セキュリティ audit の通信 を行います。ログには決定的な一文が出ていました。
npm notice This endpoint is being retired.
退役中の audit エンドポイントへの通信がリトライやタイムアウトを繰り返し、たった13パッケージのインストールに2〜7分もかかっていました。キャッシュが温まっていた回は8〜32秒で終わっていて、この振れ幅の大きさも「インストール処理そのものではなく通信待ち」であることを示しています。
実際、各 Lambda のインストール時間をすべて足すと——
2 + 2 + 3 + 7 + 3 + 0.1 + 3 + 2 + 2 + 0.25 + 7 + 7 + 3 + 3 + 7 + 0.5 + 2 ≒ 約54分
CDK Deploy の55分とほぼ一致します。CloudFormation でリソースを反映するより前、synth 時のバンドル(npm ci)だけで1時間の壁を越えていたわけです。
コードは遅くなっていない、という決定的な証拠
気になったのは、同じスタックでも速い日は5分、遅い日は55分、と日によって差が出ていたことです。速い日のコミットと遅い日のコミットを見比べても、CDK と Lambda まわりの構成には差分がありませんでした(変わっていたのは別領域の小さな修正だけ)。
要するに、こちらのコードは遅くなっていません。変わったのは外部の npm audit エンドポイントの応答です。「Lambda ごとに audit 通信する」という前々からの作りが、外部要因が悪化したことで一気に表面化した、というのが実際のところでした。
📌 学び
ExpiredTokenは「原因」ではなく「結果」。まずステップ別に時間を測り、どこで時間を溶かしているか を特定する。今回は「認証の話」ではなく「synth 時の npm 通信の話」だった。
🧯 対策:audit / fund を無効化する(低リスクな一手)
数分かかっていた npm ci の正体が audit の通信待ちなら、audit を止めれば解決します。CDK が内部で叩く npm ci にも効かせたいので、ジョブ全体の環境変数で無効化するのが手っ取り早いです。
jobs:
deploy:
runs-on: ubuntu-latest
env:
# CDK の NodejsFunction バンドルは Lambda ごとに `npm ci` を実行する。
# `npm ci` は既定で audit 通信を行い、退役中の audit エンドポイント待ちで
# 1回あたり数分ストールする。Lambda 数ぶん積み上がると synth だけで
# IAM ロールのセッション期限(1時間)を超え、ExpiredToken で失敗する。
# audit/fund を切ると各インストールが数秒で完了する。
npm_config_audit: 'false'
npm_config_fund: 'false'
steps:
# ... 以降は既存のまま
npm_config_* 形式の環境変数は、CDK が子プロセスとして起動する npm ci にもそのまま継承されます。プロジェクト直下に .npmrc(audit=false / fund=false)を置くと、ローカル開発や他ワークフローでも安定します。
結果
同じブランチを再デプロイした実測がこれです。
| ステップ | 対策前 | 対策後 |
|---|---|---|
CDK の npm install
|
4.3分 | 13秒 |
Lambda 用依存の npm ci
|
1.5分 | 15秒 |
| CDK Deploy | 55分で失敗 | 約5分16秒で成功 |
| 全体 | 約64分(失敗) | 約7分半(成功) |
audit を切っただけで、64分かけて失敗していたのが7分半で成功するようになりました。1時間のセッション期限にも十分間に合います。
🧭 本命の恒久対策も考える
今回は「急場を止める」ことを優先して audit 無効化にしましたが、構造的にはもう一歩踏み込めます。
-
そもそも
nodeModulesを使わない:mysql2はピュア JS なので、多くの場合 esbuild で直接バンドルできます。nodeModules指定を外せば Lambda ごとのnpm ci自体が消滅 し、外部の npm 通信に一切依存しなくなります(ただしバンドル後のランタイム動作確認は必須) - CDK デプロイを回すワークフローが複数あるなら横展開する:dev だけでなく本番用・システムテスト用など、同じ構造の yml すべてに同じ設定を入れておく。片方だけ直すと、次は別の環境で同じ罠を踏みます
📌 学び
「その場を止める最小の一手(audit 無効化)」と「構造的に断つ一手(nodeModulesを外す)」を分けて考える。まず止血し、落ち着いてから検証付きで根治する。
🎁 まとめ
-
ExpiredTokenを見ても、いきなり「セッションを延ばそう」と飛びつかない。まずステップ別に時間を測る - CDK の
NodejsFunctionはnodeModules指定で Lambda ごとにnpm ciを走らせる。数が増えると synth の時間が積み上がる -
npm ciの audit は外部エンドポイントの状態しだいで数分ストールしうる。CI ではnpm_config_audit=false/npm_config_fund=falseが効く - コードを疑う前に、そもそもコードが変わったのかを差分で確認する。今回は自分たちのコードではなく、外部要因を増幅していた作りが問題だった
同じように「ある日突然デプロイが遅くなった」人の役に立てばうれしいです。
📚 参考
- aws-lambda-nodejs (AWS CDK API Reference)
- configure-aws-credentials (GitHub Actions)
- npm-config: audit / fund (npm Docs)
注記: 本記事中の外部ドキュメントの内容は、ライセンス配慮のため要約・言い換えを行っています。
本ブログに掲載している内容は、私個人の見解であり、所属する組織の立場や戦略、意見を代表するものではありません。