こんにちは~ 浮田です。
使われてないIAMユーザー放置してませんか?
今回は社内のAWSコミュニティが再編されたのでその整理をするために、Step Functions + Cognito 認証付き Web App で
「不要な IAM ユーザー」を自動検出 → 人間承認 → 連鎖削除する」ワークフロー を組みました。
完成した画面はこんな感じです。フロントにはAmplifyのコンポーネントを使用して作成しました。
退職・異動したメンバーのアカウント放置はセキュリティリスクになります。
⚠️ 先に大事なことを
IAM ユーザー削除は 不可逆 な操作です。本記事の構成を自分の環境に流用する際は、以下の点に留意してください。
間違って消したら復元できません。
今回の作成物のアーキテクチャ図こんな感じです!
主要コンポーネント:
|
レイヤ |
採用技術 |
役割 |
|
認証 |
Amazon Cognito (MFA 必須 / TOTP) |
承認者の本人確認 |
|
API |
API Gateway (REST + Cognito Authorizer) |
SPA からのリクエスト受口 |
|
ワークフロー |
Step Functions Standard (JSONata) |
人間承認待機・並列削除 |
|
計算 |
AWS Lambda x 7 (Python 3.13, arm64) |
IAM API / 承認 API / Cognito トリガー |
|
監査 |
DynamoDB (オンデマンド + GSI x2 + TTL 2 年) |
イベントソーシング風ログ |
|
UI |
React + Vite (S3 + CloudFront) |
承認・管理画面 |
社内に GAWS という有志の技術コミュニティがあり、メンバーごとに IAM ユーザーが払い出されており、
これがコミュニティ再編で整理対象になった。
iam:GetCredentialReport を叩いてみると 90 日以上アクティビティのないユーザーが 16 人、最古は 600 日前 が最終活動日の人や
アクセスキーが残っている人や MFA を解除している人もいた。
これはよくないですよね。
最初は CLI スクリプトで済ませようとしましたが、要件を整理すると以下が必要になった:
=> ワークフロー化して、承認だけ人間がやればよい状態にする。
最初は Bedrock Agents で IAM API ツールを定義し、Claude にオーケストレーションさせる 方式を考えた。
「古いユーザー検出して削除して」とチャットすれば動く未来を想像していた。
よく考えたら、LLM に削除のオーケストレーションをさせるのは怖すぎる。
|
LLM オーケストレーションの懸念 |
影響 |
|
ツール選択が確率的 (温度 0 でも厳密ではない) |
削除対象の取り違え |
|
承認待機をどう実装する? Agent をポーリング? |
アーキテクチャが複雑化 |
|
監査トレイルが「Agent の判断」だけになる |
監査担当に説明できない |
結論: ワークフローは Step Functions が固定する。
承認 UI として最初は Teams Bot を考えたが、運用に Azure サブスクリプション + M365 管理者権限が要る。これがなかったので
業務監査要件 (誰がいつ承認したかを厳格に記録) を満たす選択肢として、Cognito MFA + Web App を採用。
Bedrock Agents で作れたら格好いい、と最初は思いました。
でも要件を見ると 不可逆操作 + 監査 + 承認 が中心で、LLM の必然性がゼロでした。「使うと便利か」ではなく「使わないと困る要件があるか」で判断したら、答えは No だった。
不可逆操作は「待つ方が安全」
その代わり Step Functions の waitForTaskToken で 72 時間の人間承認を挟む決定論的ワークフロー に振り切りました。
このパターンは承認・本番リリース・課金処理・データ削除・外部送金など「待ってる方が安全な操作」全般に効くと思います。
Lambda は 15 分で切れるので 72 時間待てません。
待機中も課金され、落ちれば承認待ちが消える。
承認の待機自体を Lambda から外に出したい — そこでStep Functionsの waitForTaskToken を使うことにしました。
これに完璧に当てはまるのが waitForTaskToken。Step Functions が「待機」を引き受けてくれるコールバックパターンです。
ASL (Amazon States Language) ではリソース ARN の末尾に .waitForTaskToken を付けるだけ。
※ASL は Step Functions のワークフローを JSON で記述する専用フォーマット です。
本プロジェクトはステートマシン全体を JSONata モード ("QueryLanguage": "JSONata") で書いているので、サンプルもその記法で示します:
※JSONata は JSON データを抽出・変換するための問い合わせ言語。
SQL が「テーブルに対する問い合わせ言語」なら、JSONata は「JSON に対する問い合わせ言語」です。
※JSONには本来コメントは書けませんがわかりやすいようにコメントを付けてあります。ご留意ください。
------------------------------
{
"SendDetectionCard": {
"Type": "Task",
// ★ 末尾の .waitForTaskToken がコールバックモードのスイッチ。
これが付いた Task は「外から token が戻るまで待つ」状態になる
"Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken",
"Arguments": {
"FunctionName": "${StateWriterFunctionArn}",
"Payload": {
"card_type": "DETECTION_RESULT",
"old_users": "",
// ★ Step Functions が自動生成する一意の token。
// Lambda はこれを DynamoDB に保存して return するだけでよい
"task_token": "",
"execution_arn": ""
}
},
"Output": "",
// 72 時間 (= 259,200 秒) 待機したらタイムアウト
"TimeoutSeconds": 259200,
"Catch": [
{ "ErrorEquals": ["States.Timeout"], "Next": "NotifyTimeout" }
],
"Next": "HasSelectedUsers"
}
}
------------------------------
少し直感に反するのが、Lambda が return しても Step Functions は次の State に進まない こと。
普通の Task State なら return 値がそのまま次の入力になりますが、.waitForTaskToken モードでは「token が外から SendTaskSuccess で戻ってくるまで動かない」という挙動に変わります。
Lambda 側は意外とシンプルで、token を DynamoDB に保存してすぐ return するだけ。
ここで token を保存し忘れると、誰も Step Functions を再開できないまま 72 時間待機することになるので、
put_item が成功するまで return しないことだけ守れば OK。
実装を進めて承認 UI を作ってる最中に、ふと「承認者 2 人が同時にボタンを押したらどうなる?」と気になって調べました。
両方が status=PENDING を読んで、両方が SendTaskSuccess を呼ぶと、後発の方は token が既に消費済みなので TaskTimedOut で例外。Lambda が落ちて 500 エラーが UI に返る、という気持ち悪い状態になります。
これを防ぐには、Step Functions を再開する前に DynamoDB 側で 1 人だけを通す 仕掛けが要ります。
ConditionExpression を使った楽観ロックです:
こうすると、2 人同時押しでも DynamoDB レベルで 1 人しか通らないので、SendTaskSuccess も 1 回しか呼ばれません。通らなかった人は 409 がすぐ返るので、UI で「すでに承認済み」と表示できる。
仮に SendTaskSuccess の途中で Lambda が死んでも、DynamoDB の状態を見れば再投入可能、というのも嬉しい副作用でした。
実は最初これを書いたとき、HeartbeatSeconds: 259200 と書いていて、記事化のレビューで気付いて直しました。
Heartbeat は人間承認には使えません。
|
設定 |
何を計る |
比喩 |
|
TimeoutSeconds |
タスク開始からの総経過時間 |
「全体で 30 分以上かかったら諦める」 |
|
HeartbeatSeconds |
「最後の生存報告から」の経過時間 |
「5 分に 1 回は『生きてます』と連絡して」 |
HeartbeatSeconds は「長時間動くジョブが定期的に『生きてます』と通知する」用途のもので、人間承認のように何時間も放置するケースには使えません。誰も heartbeat なんて打たないので。
しかも厄介なのが、HeartbeatSeconds: 259200 でも初回実行から 72時間後にちゃんとタイムアウトするんですよ。
なので動作テストのときは「あれ?ちゃんと動いてるじゃん」と通り過ぎてしまう。Catch で通知を握りつぶしていると、いつまで経っても気づかない地雷になります。ひとつ学びになりました。
|
方式 |
待ち時間 |
コスト |
監査性 |
実装複雑度 |
|
Lambda ポーリング |
〜15 分 |
Lambda 実行時間 |
△ |
低 |
|
EventBridge Scheduler + DynamoDB |
〜数年(運用次第) |
スケジューラ料金 |
○ |
中 |
|
Lambda Durable Functions |
〜1 年 |
Lambda 実行時間 + 状態保存料金 |
○(自分でログ設計) |
中(コード中心) |
|
Step Functions waitForTaskToken |
〜1 年 |
状態遷移ベース |
◎ |
低〜中 |
|
SQS + 別 Lambda |
〜14 日(最大保持期間) |
SQS 料金 |
△ |
中 |
人間承認に限って言えば Step Functions が圧倒的に楽だと思います。72 時間放置されても財布が痛まないのが地味に効きます。
ちなみに Lambda Durable Functions も「長時間待機 + コールバック再開」を Lambda のコード上で書ける選択肢で、宣言的な ASL ではなく Python / TypeScript でロジックを組みたい チームには有力な対抗馬です。
今回は監査担当に提出する都合で 承認の証跡が実行履歴 UI に自動で残ること、半年ごとの承認者交代で引き継ぐときにフロー図のままレビューできること を優先して Step Functions を選びました。
逆に「複雑な業務ロジックを if/try でコードに直接書きたい」「ワークフローが頻繁に変わる」ケースなら Durable Functions の方が嵌まりそうです。
Durable Functions については別のブログ記事で紹介しています。
要は Step Functions に「門番」をやらせているだけです。token を握って 72 時間立ち続け、人間が OK と言うまで誰も次に通さない。
これを Lambda や自前のコードで再現しようとすると、タイムアウト・課金・落ちたときの状態消失・レースコンディションが全部覆いかぶさってきますが、`waitForTaskToken` は AWS 側がこの門番役を丸ごと引き受けてくれる。
「不可逆な操作には「待つのが得意な道具」を据える」 — IAM 削除に限らず、承認・本番リリース・課金・データ削除・外部送金などにそのまま有効なパターンだと思います。
iam:DeleteUser は 依存リソースが残っているとエラー になる API です。
本体を消す前に、ユーザーにぶら下がっている以下を すべて削除/解除しておく必要があります(順序は問わない、並列で消しても OK):
Deactivate までで DeleteUser 自体は通るが、Delete までやらないと持ち主のいない未関連デバイスが残る
本番テストで DeleteVirtualMFADevice の権限不足を踏んで 「Login Profile と Access Keys は消えたが MFA から先で失敗 → ユーザー本体は残ったまま」 という部分的に削除された状態を作ってしまいました。
最終ログイン取得手段の比較:
|
方法 |
範囲 |
速度 |
コスト |
|
cloudtrail:LookupEvents |
90 日 |
遅い (2 req/sec) |
低 |
|
iam:GetUser.PasswordLastUsed |
全期間 |
早い (ユーザー単位) |
低 |
|
iam:GetCredentialReport |
全期間, CSV 一括 |
超早い (1 req) |
無料 |
100 ユーザー超えるアカウントだと CloudTrail 個別 API は絶望的に遅い。Credential Report が圧勝。
メモ程度に実装中に踏んだ罠と修正の概要だけ置いておきます。
多く見積もっても月 100 回の検出実行を想定して、
|
サービス |
月額 |
|
Step Functions Standard (約 2,500 状態遷移) |
$0.06 |
|
Lambda (約 2,000 呼び出し) |
$0.10 |
|
API Gateway (約 500 req) |
$0.02 |
|
DynamoDB オンデマンド |
$0.50 |
|
CloudWatch Logs (5 GB) |
$2.50 |
|
CloudFront + S3 |
$0.55 |
|
Cognito (5 MAU、無料枠内) |
$0 |
|
合計 |
約 $4/月 |
業務システムとしてほぼ無料。
GAWS 再編に伴う IAM 整理がスタート地点でしたが、結果的に Step Functions + Cognito + Web App という「不要 IAM ユーザーの自動整理・削除システム」に育ちました。GAWS 専用ではなく、他チームでもそのまま使えます。
設計面の最大の学びは、Bedrock Agents 案を捨てて Step Functions に振り切った こと。「AI でやれそう」と「AI でやるべき」は別物 で、不可逆操作・監査要件のある業務では、決定論的なワークフローに任せた方が安全で、結果的に実装も簡潔になりました。
同じような「コミュニティの再編で IAM 整理が要る」「定期的に不要 IAM ユーザーを棚卸ししたい」「人間承認付きワークフローを AWS で組みたい」というケースの参考になれば。
ありがとうございました。