every Tech Blog

株式会社エブリーのTech Blogです。

AgentCore CLIを本番運用する上で詰まったポイントと対策

はじめに

今回はAgentCore CLIを使ったエージェント開発を本番運用できるかを検討した際に、複数環境のデプロイについて詰まったポイントがあったので、ご紹介させていただきます。
AgentCore CLIは2026年4月17日現在では、GA前段階のため、本記事で紹介する内容が今後変更される可能性があります。

検証に使用したエージェント構成

今回検証のために使用したエージェントの構成を簡単に紹介します。
今回はAgentCore CLIの使い方の説明が主題ではないため、使い方についての詳細は省かせていただきます。

AgentCore CLIのagentcore createコマンドで以下のようなエージェントを作成したという前提で話を進めさせていただきます。
- Project name: MyProject
- Agent name: analysis
- Type: Create new agent
- Language: Python
- Build: Direct Code Deploy
- Protocol: HTTP
- Framework: OpenAI Agents
- Advanced: defaults

コマンドを実行すると以下のような構成でファイルが生成されます。
主要なものに絞って記載していますが、実際には CDK の設定ファイルや LLM コンテキストファイルなども生成されます。

MyProject/                       # プロジェクトルート
├── AGENTS.md                    # エージェントの概要・設計ドキュメント
├── README.md                    # プロジェクトのREADME
├── agentcore/
│   ├── agentcore.json           # エージェント定義(ランタイム、Gateway、Credential等)
│   ├── aws-targets.json         # デプロイ先のAWSアカウント・リージョン
│   ├── tool-schema.json         # Gatewayターゲットのツール定義
│   ├── .env.local               # APIキー等のシークレット
│   ├── .cli/
│   │   └── deployed-state.json  # デプロイ済みリソースの状態
│   └── cdk/
│       ├── bin/cdk.ts           # CDKエントリポイント
│       └── lib/cdk-stack.ts     # CDKスタック定義
└── app/                         # エージェントのアプリケーションコード
    └── analysis/
        ├── main.py
        └── pyproject.toml

また、上記構成に含まれていませんが、今回の構成では、AgentCore CLIで作成したエージェントが Gateway 経由で、lambroll でデプロイした Lambda 関数をツールとして呼び出します。

デプロイの仕組み

agentcore deploy を実行すると、内部では以下が行われます:

  1. デプロイターゲット(aws-targets.json)の読み込み
  2. agentcore.json のバリデーション
  3. CDKプロジェクトのビルド
  4. Credential(APIキー等)のセットアップ
  5. CloudFormationテンプレートの合成(synth)
  6. CloudFormationスタックのデプロイ

CloudFormationスタックには、ランタイム、Gateway、IAMロール等のリソースがまとめて含まれます。

詰まったポイント

1. --target オプションでデプロイ先が絞り込めない

問題

AgentCore CLIでは、デプロイ先のAWSアカウント・リージョンを aws-targets.json に定義します。
dev/prodを分離するために、以下のように2つのターゲットを定義しました。

// aws-targets.json
[
  { "name": "dev", "account": "111111111111", "region": "ap-northeast-1" },
  { "name": "prod", "account": "999999999999", "region": "ap-northeast-1" }
]

agentcore deploy コマンドには --target オプションがあり、デプロイ先を指定できます。
--target dev を指定すればdev環境のみにデプロイされると期待しましたが、実際には以下のようにprodのCloudFormationスタックもdevアカウントに作成されてしまいました。

# targetをdevに指定してデプロイ
AWS_PROFILE=dev-profile agentcore deploy --target dev

実際にはdevアカウントに以下の2つのスタックが作成される
- AgentCore-MyProject-dev(意図通り)
- AgentCore-MyProject-prod(意図しない)

原因

この問題は、CLIとCDKの間でターゲット情報が連携されていないことが原因のようです。

CLIの --target オプションは、aws-targets.json からターゲット情報(account, region)を取得してIdentityのセットアップやデプロイ後の状態記録に使われますが、CDKのsynth(CloudFormationテンプレートの合成)やdeploy処理にはターゲット名が伝わりません。

agentcore create で生成される cdk.ts のデフォルトコードでは、aws-targets.json に定義された全ターゲットに対してスタックを生成するforループになっています。

// cdk.ts(デフォルト生成コード)
for (const target of targets) {
  // --target の指定に関係なく、全ターゲット分のスタックが生成される
  new AgentCoreStack(app, stackName, { ... });
}

CDKのsynthはローカルで実行されるため、アカウントIDが異なっていてもテンプレート生成自体は成功します。その結果、devアカウントのクレデンシャルで実行しているにもかかわらず、prod用のスタック定義もdevアカウントにデプロイされてしまいます。

対策

対策1:cdk.ts を修正して環境変数でフィルタ

cdk.ts に環境変数 AGENTCORE_TARGET でターゲットをフィルタするコードを追加しました。

// cdk.ts
const app = new App();

// 環境変数でターゲットをフィルタ
const targetFilter = process.env.AGENTCORE_TARGET;
const filteredTargets = targetFilter
  ? targets.filter(t => t.name === targetFilter)
  : targets;

for (const target of filteredTargets) {
  // フィルタされたターゲットのみスタックを生成
  new AgentCoreStack(app, stackName, { ... });
}

デプロイ時に環境変数を指定して実行します:

# dev環境のみデプロイ
AGENTCORE_TARGET=dev AWS_PROFILE=dev-profile agentcore deploy --target dev

# prod環境のみデプロイ
AGENTCORE_TARGET=prod AWS_PROFILE=prod-profile agentcore deploy --target prod

注意点:

  • agentcore create で新規プロジェクトを作成するたびに cdk.ts が初期状態で生成されるため、毎回この修正を適用する必要があります。
  • CLIの --target オプションの値はCDKプロセスに自動的に引き渡されないため、環境変数 AGENTCORE_TARGET として別途指定する必要があります。--target はCLI内部でのターゲット情報取得に、AGENTCORE_TARGET はCDKのsynthでのスタック絞り込みに使われるため、両方に同じ値を指定する必要があり、冗長になってしまいます。将来のCLIバージョンで改善される可能性はありますが、現時点(v0.8.0)ではこの対応が必要です。
対策2:aws-targets.json を毎回リセットしてプロファイルから自動検出

aws-targets.json を空([])にしてからデプロイすると、CLIが AWS_PROFILE からアカウントIDとリージョンを自動検出し、"default" という名前のターゲットを自動生成します。

# dev環境(aws-targets.json が空の状態で実行)
AWS_PROFILE=dev-profile agentcore deploy

# prod環境(aws-targets.json をリセットしてから実行)
echo '[]' > agentcore/aws-targets.json
AWS_PROFILE=prod-profile agentcore deploy

一見シンプルですが、実用上は問題があります。devデプロイ後に aws-targets.json にはdevターゲットが追加された状態になっています。この状態でリセットせずにprodをデプロイすると、aws-targets.json に2つのターゲットが登録され、対策1で述べたのと同じ問題(全ターゲット分のスタックがsynthされる)が発生してしまいます。

そのため、デプロイのたびに aws-targets.json をリセットする運用が必要になりますが、CI/CDを使い、 echo '[]' > agentcore/aws-targets.json を実行してからデプロイする形にすれば、毎回クリーンなワークスペースから始まるためリセット忘れは防げると思います。

対策1は agentcore create で自動生成される cdk.ts を書き換える必要があり、CLIのバージョンアップで生成内容が変わった際に手動マージが必要になったり、修正漏れで予期せぬ挙動を起こすリスクがあります。そのため、基本的には自動生成ファイルには手を入れず、対策2をCI/CDで運用するのが望ましいと考えています。

2. Lambda ARNのハードコーディング

問題

GatewayにLambda関数をターゲットとして追加するには、以下のようにコマンドを実行します。

agentcore add gateway-target \
  --gateway Gateway \
  --name DataFetcher \
  --type lambda-function-arn \
  --lambda-arn arn:aws:lambda:ap-northeast-1:111111111111:function:get-data \
  --tool-schema-file ./agentcore/tool-schema.json

./agentcore/tool-schema.json にはLambda関数が提供するツールの定義を記述したJSONファイルを指定します。
Lambda ARNターゲットの場合、Gatewayがどのツールを公開しているか知る手段がないため、このファイルを自分で用意する必要があります。

// tool-schema.json の例
{
  "tools": {
    "get-data": {
      "name": "get-data",
      "description": "分析用のデータを取得する",
      "inputSchema": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "description": "取得対象の日付"
          }
        },
        "required": ["date"]
      }
    }
  }
}

このコマンドを実行すると、agentcore.json に以下のようなGatewayターゲットが追加されます。

// agentcore.json
"agentCoreGateways": [
  {
    "name": "Gateway",
    "targets": [
      {
        "name": "DataFetcher",
        "targetType": "lambdaFunctionArn",
        "lambdaFunctionArn": {
          "lambdaArn": "arn:aws:lambda:ap-northeast-1:111111111111:function:get-data",
          "toolSchemaFile": "./agentcore/tool-schema.json"
        }
      }
    ]
  }
]

ここで問題になるのが lambdaArn の値です。Lambda ARNにはAWSアカウントIDが含まれるため、dev/prodでアカウントが異なる場合、デプロイ前に毎回この値を対象環境のARNに書き換える必要があります。

devにデプロイする場合:  arn:aws:lambda:ap-northeast-1:111111111111:function:get-data
prodにデプロイする場合: arn:aws:lambda:ap-northeast-1:999999999999:function:get-data

agentcore.json はgit管理されるファイルのため、デプロイのたびにARNを書き換えてコミットするのは手間がかかりますし、書き換え忘れにより誤った環境のARNでデプロイしてしまうリスクもあります。

対策

agentcore.json にはdev用のARNを登録しておき、cdk.ts 側で関数名だけを取り出して、ターゲットのアカウント・リージョンからARNを動的に再構築するようにしました。

// agentcore.json(dev用のARNで登録しておく)
{
  "lambdaArn": "arn:aws:lambda:ap-northeast-1:111111111111:function:get-data",
  "toolSchemaFile": "./agentcore/tool-schema.json"
}
// cdk.ts
const resolvedMcpSpec = mcpSpec ? JSON.parse(JSON.stringify(mcpSpec)) : undefined;
if (resolvedMcpSpec?.agentCoreGateways) {
  for (const gw of resolvedMcpSpec.agentCoreGateways) {
    for (const t of gw.targets ?? []) {
      if (t.lambdaFunctionArn?.lambdaArn) {
        // 元のARNから関数名を抽出し、ターゲットのアカウント・リージョンで再構築
        const functionName = t.lambdaFunctionArn.lambdaArn.split(':').pop();
        t.lambdaFunctionArn.lambdaArn =
          `arn:aws:lambda:${target.region}:${target.account}:function:${functionName}`;
      }
    }
  }
}

注意点:
cdk.tsagentcore create で新規プロジェクトを作成するたびに初期状態で生成されるため、毎回この修正を適用する必要があります。
また、前述の通り自動生成ファイルを書き換えるのはCLIのバージョンアップ等でバグを生みやすいので、CI/CDのデプロイジョブ内で agentcore.json のARNを対象環境のアカウントIDに置換してからデプロイする方が安全かなと思います。

3. APIキーをdev/prodで分けたい

問題

エージェントが外部API(OpenAI等)を利用する場合、APIキーをCredentialとして登録します。登録されたAPIキーはAgentCore Identityサービスのアウトバウンド認証(エージェントから外部サービスへの認証情報)として管理されます。

agentcore add credential --name OpenAIApiKey --api-key sk-xxxxx

このコマンドを実行すると、以下の2箇所に情報が書き込まれます。

  • agentcore/agentcore.json — Credentialのメタ情報(名前・タイプ)
  • agentcore/.env.local — APIキーの実際の値
// agentcore.json
"credentials": [
  {
    "authorizerType": "ApiKeyCredentialProvider",
    "name": "OpenAIApiKey"
  }
]

agentcore/.env.local

AGENTCORE_CREDENTIAL_OPENAIAPIKEY=sk-xxxxx

環境変数名は Credential名から AGENTCORE_CREDENTIAL_{NAME} の形式で自動生成されます。デプロイ時にこの値が読み取られ、AWS側の Token Vault(AgentCore Identityサービスのシークレットストア)に登録されます。

dev/prodで同じAPIキーを使う場合は、.env.local の値をそのまま使えるので問題ありません。しかし、セキュリティや課金管理の観点からdev/prodでAPIキーを分けたい場合、.env.local は1ファイルしかないため、デプロイのたびに値を書き換える必要があります。

対策

デプロイ時に環境変数でAPIキーを上書きします。環境変数が設定されていれば .env.local の値より優先されます。

# dev環境
AGENTCORE_CREDENTIAL_OPENAIAPIKEY=sk-dev-xxxxx \
  AGENTCORE_TARGET=dev \
  AWS_PROFILE=dev-profile \
  agentcore deploy --target dev

# prod環境
AGENTCORE_CREDENTIAL_OPENAIAPIKEY=sk-prod-xxxxx \
  AGENTCORE_TARGET=prod \
  AWS_PROFILE=prod-profile \
  agentcore deploy --target prod

ただし、毎回デプロイコマンドにAPIキーを環境変数として渡すのであれば、.env.local を直接書き換える運用と手間は変わりません。今回はCI/CDを使わずローカルからデプロイする運用のため、デプロイ前に .env.local の値を対象環境のAPIキーに書き換える方法を採用しました。

別のアプローチ:dev/prodでプロジェクトを分ける

ここまで紹介した課題は、いずれも1つのプロジェクトでdev/prodを共有することに起因しています。

これらをすべて解消するシンプルなアプローチとして、dev用とprod用でそれぞれ別のAgentCoreプロジェクトを作成する方法があります。

MyAgent-dev/
├── agentcore/
│   ├── agentcore.json    ← dev用のLambda ARN、dev用のAPIキー
│   ├── aws-targets.json  ← devアカウントのみ
│   └── cdk/
└── app/
    └── analysis/

MyAgent-prod/
├── agentcore/
│   ├── agentcore.json    ← prod用のLambda ARN、prod用のAPIキー
│   ├── aws-targets.json  ← prodアカウントのみ
│   └── cdk/
└── app/
    └── analysis/

この方法なら cdk.ts のカスタマイズは不要で、aws-targets.json にはターゲットが1つだけなのでsynthの問題も発生せず、.env.local も環境ごとに独立しています。

ただし、app/ 配下のエージェントコードが2つのプロジェクトで重複するため、ロジックを変更するたびに両方を更新する必要があります。コードの同期忘れによる環境差異が生まれるリスクもあるため、この方法を積極的に採用することはできないなと思いました。

まとめ

AgentCore CLIを使用してみて、実際に本番運用できるのかを検討しました。
CLIで必要なリソースを簡単に素早く作成できるというメリットはありますが、環境を分離するには課題が多いという検証結果になりました。
最後まで読んでいただきありがとうございました!

デリッシュキッチンのLiquid Glass対応への取り組み

はじめに

株式会社エブリーでデリッシュキッチンのiOSアプリの開発をしている成田です。
iOS 26から、Appleの新しいデザイン言語である「Liquid Glass」が導入されました。 2026年4月の現時点では設定のフラグによって適用を回避できますが、次のXcodeのメジャーアップデートではこのフラグの廃止が見込まれています。 また、2027年春頃には新しいメジャーバージョンのXcodeでのビルドが必須になると考えられ、対応は避けられない状況です。
こうした背景から、すでにLiquid Glassへの対応を進めているiOSアプリ開発者の方も多いのではないでしょうか。
デリッシュキッチンでも現在ユーザーへのリリースを目指して対応を進めています。
本記事では、以下のような流れでデリッシュキッチンにおけるLiquid Glass対応への取り組みについて紹介したいと思います。Liquid Glassの概要については他の記事でも多く紹介されているので本記事ではできるだけ割愛します。

  • Liquid Glass対応の進め方
  • 大まかな対応箇所
  • デリッシュキッチンにおける課題
  • AppleのLiquid Glassワークショップへの参加

Liquid Glass対応の進め方

キックオフと開発の流れ

今年の1月にPdMとデザイナー、エンジニアが集まりキックオフを行ってプロジェクトがスタートしました。
まず最初に、アプリのプロジェクト設定のオプトアウトフラグUIDesignRequiresCompatibilityを外した状態のアプリを社内に配布し、Liquid Glassがそのまま適用された状態で各画面をデザイナーに確認してもらいました。Appleの標準アプリや他のメジャーなアプリのUIも参考にしながら、対応が必要な箇所の洗い出しと優先度付け、そして大まかな工数見積もりを行いました。
また、対応方針については単にデザイン観点だけで決めるのではなく、技術的な実現可否や実装コストも踏まえながら、エンジニアとデザイナーで議論を重ねて整理していきました。デザインと実装の両面から検討することで、現実的かつ一貫性のある方針を定められていると感じます。
さらに、初期段階では一定期間を設けて集中的に実装を進めることで、実際の対応にどの程度の工数がかかるのかを把握することもでき、おおよそのベロシティ感を掴むことができました。

なお、参考事例としてAppleが紹介しているデザイン事例集も、実際にどのようにLiquid Glassがプロダクトに取り入れられているかを把握するうえで非常に参考になりました。

専任を置かず全員で対応する

このプロジェクトでは、iOSチーム内に専任を置かず、各プロジェクトごとに分担して対応を進めています。
専任を設けると知見が特定のメンバーに偏り、今後の機能開発においてプロジェクトごとに実装のばらつきが生じる可能性があるためです。Liquid Glassのようなデザイン言語の変化は一部の対応にとどまらず、プロダクト全体に継続的に影響していくものだと考えています。
また、UIはデザイナーだけで完結するものではなく、エンジニアと連携しながら作り上げていくものです。こうした背景もあり、プロダクトに関わるiOSエンジニア全員で取り組む形で進めています。

独自フラグでコードを先行リリース

現在対応を進めているコードは、まだユーザー向けには公開せず、以下のような独自のフィーチャーフラグを設けることで、コード自体は順次リリースしつつ、ユーザーにはLiquid Glassが適用されない状態を保ったままにしています。

public enum LiquidGlassAvailability {
    /// Liquid Glass デザインが有効かどうかを返す。
    /// iOS 26 以降かつ UIDesignRequiresCompatibility が設定されていない(または false)場合に true。
    public static let isEnabled: Bool = {
        guard #available(iOS 26.0, *) else {
            return false
        }
        // UIDesignRequiresCompatibility が true の場合は互換モードなので Liquid Glass 無効
        if let requiresCompatibility = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool,
           requiresCompatibility {
            return false
        }
        return true
    }()
}

このような進め方にしているのは、変更をため込むことでGitHub上のPRが滞留し、コンフリクトが発生しやすくなるのを防ぐためです。対応が完了した箇所から順次マージしていくことで、開発の流れをスムーズに保っています。
ユーザー向けの初回リリース時にはプロジェクト設定のオプトアウトフラグを取り除き、Liquid Glassが適用された状態で提供する予定です。また、リリース後も優先度に応じて段階的に適用範囲を広げていく方針です。

初回リリースに向けた大まかな対応箇所

ユーザーへの初回のリリースに向けて、優先度が高いのは以下の内容です。

ナビゲーションバー・タブバー周りの対応

最も優先度が高く、Liquid Glassの効果が大きい箇所がナビゲーションバーとタブバー周りです。Liquid Glassではこれらのバーが透過されることでコンテンツへの没入感が高まりますが、デリッシュキッチンでは元々これらのバーに対して背景色やボタンのスタイルなどを独自にカスタマイズしていました。Liquid Glassに対応するにあたり、これらの独自設定を取り除いていく作業が必要になりました。

レイアウトの修正

独自設定を削除していくと、画面によってコンテンツのレイアウトが崩れるケースが発生しました。Liquid Glassではナビゲーションバーやタブバーの背面にまでコンテンツが広がるレイアウトが前提となりますが、一部の画面ではそのような構造になっていなかったためです。各画面ごとにレイアウトを見直し、コンテンツがバーの裏側まで自然に潜り込むよう修正する作業も対応範囲に含まれています。

その他の表示崩れの修正

ここでは書ききれないので紹介を省きますが、上記の対応に加え、Liquid Glassの適用によって生じる細かな表示崩れについても最低限の修正を行ったうえでユーザーに向けた初回のリリースを行う予定です。

デリッシュキッチンにおける課題

ここでは、Liquid Glass対応を進める上でのデリッシュキッチンにおける課題をいくつかピックアップして紹介します。

ナビゲーションバー直下のカスタムViewの扱い

デリッシュキッチンには、ナビゲーションバーの直下にタブやカスタムViewが配置されている画面がいくつかあります。単純にナビゲーションバーを透過にするだけでは、その下に続くカスタムViewとの境界が不自然になってしまい、コンテンツの表示領域も狭まってしまいます。これはLiquid Glassが目指すコンテンツへの没入感という思想に反してしまいます。

これらのカスタムViewをコンテンツ領域の中にどう自然に溶け込ませるか、デザインと実装の両面から検討する必要があり、現在取り組んでいる課題の一つです。

幅広い環境での検証体制

デリッシュキッチンはユーザー数も多く、現在は最新から3つのメジャーバージョンのiOSをサポートしています。Liquid GlassはiOS 26以降でのみ適用されますが、それ以前のOSでもレイアウト崩れが発生しないよう、すべてのサポートバージョンで表示を確認する必要があります。そのため、単一の環境での検証にとどまらず、複数バージョンをまたいだ確認が求められる点が大きな負担となっています。
また、弊社には専任のQAチームがないため、動作検証はPdM・デザイナー・エンジニアが協力して行っています。Liquid Glass対応のように影響範囲が広い変更では、確認すべき画面やパターンも多岐にわたるため、検証の抜け漏れを防ぎつつ、いかに効率的に進めていくかが課題となっています。

並行開発による手戻りリスク

また、もう一つの課題として、通常の機能開発との並行進行があります。 現在のプロダクトでは複数のプロジェクトが並行して開発を進めており、Liquid Glass対応と並行して進行しています。そのため、新規機能の開発時にLiquid Glassの考慮が十分に行われないケースも発生しがちです。
その結果、後からデザインの調整や実装の修正が必要になり、手戻りが発生してしまう可能性があります。こうした手戻りをいかに防ぎ、現状の開発の中にLiquid Glass対応を組み込んでいくかも重要な課題となっています。

AppleのLiquid Glassワークショップへの参加

Liquid Glass対応の一環として、Appleが時折に開催しているワークショップに会社で参加する機会をいただき、3月にエンジニアとデザイナー数名で参加してきました。
ワークショップは、まずLiquid Glassの概要や設計思想、もたらす効果について一通り説明いただくところから始まり、その後はAppleのデザインのエバンジェリストの方と直接やり取りできる時間が設けられており、デリッシュキッチンにおける対応方針について質問やディスカッションを行いました。
自社アプリの課題を持ち込み、その場でフィードバックをもらえる形式だったため、抽象的なガイドラインだけではイメージしづらかった部分についても、具体的な方向性を確認することができました。
せっかくなので、ワークショップに参加して特に印象に残っている学びをいくつか紹介します。

ナビゲーションバーやタブバーで特色を出さない

ナビゲーションバーやタブバーといった操作周りのUIで個性を出すのではなく、コンテンツでプロダクトの特色を表現することが重要であるという考え方が印象に残りました。

透過させることが目的ではない

Liquid Glassは単に透過やブラーを適用すること自体が目的ではなく、コンテンツへのフォーカスを高めるための手段であるという話がありました。見た目だけをなぞるのではなく、どういう意図で使うかが重要だと感じました。

システムとの一貫性を保つ

OS全体の表現と調和することが重要で、過度に独自のスタイルを持ち込むと違和感につながるという点も印象的でした。標準の振る舞いを尊重することが結果的に良い体験につながると感じました。

おわりに

本記事では、デリッシュキッチンにおけるLiquid Glass対応の取り組み状況についてご紹介しました。
同じようにLiquid Glassへの対応を進めている方にとって、少しでも参考になれば幸いです。
デリッシュキッチンのLiquid Glass対応のリリースもぜひ楽しみにしていてください!

AWSのAIツール ― Claude Code拡張機能 × IAM Policy Autopilot × Agent Pluginsを試してみた

はじめに

エブリーでデリッシュキッチンの開発をしている本丸です。
日頃の業務でClaude Codeを活用しているのですが、AWSからリリースされたAIツール群(IAM Policy Autopilot、Agent Plugins for AWS)がClaude Codeと連携できることを知り、社内勉強会を機に実際に試してみました。
本記事では、これらのツールの概要と、素のLLMに指示した場合と専用ツールを使った場合でどのような違いが出るのかを4つのシナリオで比較した結果をまとめます。

IAM Policy Autopilot

概要

IAM Policy Autopilotは、AWS re:Invent 2025で発表されたオープンソース(Apache 2.0)のツールです。ソースコードを静的解析し、最小権限のIAMポリシーを自動生成します。

対応言語はPython / TypeScript / Goで、CLI / MCPサーバーの両方で利用できます。

仕組み

特筆すべきはLLMを使用しない決定論的な静的解析である点です。Rust製のAST解析エンジン(ast-grep)がSDK呼び出しを検出し、IAMアクションにマッピングします。同じコードからは常に同じポリシーが生成されるため、再現性があります。

ソースコード
  ↓ AST解析(ast-grep)
SDK呼び出しを検出
  ↓ IAMアクションにマッピング
IAMポリシーJSON生成

主要機能

コマンド 用途
generate-policies ソースコード解析からIAMポリシー生成
fix-access-denied AccessDeniedエラーメッセージから修正ポリシー生成

Agent Plugins for AWS

概要

Agent Plugins for AWSは、2026年2月にAWS Labsからリリースされたプラグイン群です。AIエージェントにAWSの設計・構築・運用スキルを付与します。

利用可能なプラグイン

プラグイン 用途
aws-serverless Lambda / API Gateway / EventBridge
deploy-on-aws アーキテクチャ推奨 / コスト見積もり / IaC生成
databases-on-aws Aurora DSQL含むDB設計ガイダンス
aws-amplify Amplify Gen 2 フルスタックアプリ構築
amazon-location-service マップ / ジオコーディング / ルーティング
migration-to-aws GCPからAWSへの移行支援

deploy-on-aws の5段階ワークフロー

deploy-on-awsプラグインは、以下の5段階のワークフローでプロジェクトのデプロイを支援します。

1. Analyze → 2. Recommend → 3. Estimate → 4. Generate → 5. Deploy
(解析)     (推奨)       (試算)       (生成)      (デプロイ)

各フェーズでは、ワークフローを主導するSkillと、外部データを参照するMCPサーバー(awsknowledge, awspricing, aws-iac-mcp)、さらにIaC検証を自動実行するHooksが組み合わさって動作します。これにより、最新のAWSドキュメント・料金情報・IaCベストプラクティスを参照しながら一貫したプロセスで進行します。

素のLLMに指示する場合の課題

これらのツールを使わず、素のLLMに直接指示した場合には以下のような課題があります。

  • 学習データの鮮度: 知識カットオフ以降のAPI変更・新サービスに非対応
  • ハルシネーション: 存在しないAPIパラメータやサービス名を生成するリスク
  • 一貫性の欠如: 毎回異なるアプローチ・構成を提案する可能性
  • 検証手段がない: 生成されたポリシーやIaCの正しさを確認できない

一方、ツールを利用すると以下の改善が得られます。

  • 最新情報の参照: MCPサーバー経由でリアルタイムにAWSドキュメント・料金を参照
  • 構造化プロセス: 明確なワークフローにより一貫した品質を実現
  • 最小権限の原則: 自動的に最小権限を適用、ベストプラクティスに基づく設計

比較シナリオ

ツールを使うと実際どれくらい差分が出るのかが気になったので、AWS開発でよく遭遇しそうな場面をAIに挙げてもらい、以下の4つのシナリオを用意して比較しました。各シナリオで素のLLMと専用ツール付きに対して同じプロンプトを渡し、出力を見比べています。

  • シナリオ1: Lambda関数のIAMポリシー作成(IAM Policy Autopilot)
  • シナリオ2: サーバーレスREST APIの構築(aws-serverless Plugin)
  • シナリオ3: AccessDeniedエラーの解決(IAM Policy Autopilot)
  • シナリオ4: AWSへのデプロイ設計(deploy-on-aws Plugin)

シナリオ1:Lambda関数のIAMポリシー作成

S3からファイルを読み取り、DynamoDBに書き込むLambda関数に必要な最小権限ポリシーを作成するシナリオです。

対象コード

import boto3

s3 = boto3.client('s3')
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('my-data-table')

def handler(event, context):
    bucket = event['bucket']
    key = event['key']

    response = s3.get_object(Bucket=bucket, Key=key)
    data = response['Body'].read().decode('utf-8')

    table.put_item(Item={
        'id': key,
        'content': data,
        'source_bucket': bucket
    })

    return {'statusCode': 200, 'body': 'Success'}

ツール利用時は「IAM Policy Autopilotのgenerate_application_policiesツールを使って」と追加で指示しました。

結果比較

項目 素のLLM IAM Policy Autopilot
S3アクション GetObjectのみ GetObject + LegalHold + Retention + Tagging + Version + ObjectLambda
DynamoDBアクション PutItemのみ PutItem + WriteDataForReplication
KMS暗号化 なし S3・DynamoDB向け kms:Decrypt(条件付き)
CloudWatch Logs 含む(推測で追加) 含まない(サービスロールに委任)

IAM Policy Autopilotは暗号化・バージョニング・Access Point等、本番運用で必要になる権限を網羅的にカバーしています。素のLLMが推測ベースで生成したのに対し、IAM Policy AutopilotはAST解析によりget_object()put_item()の呼び出しを検出し、関連する権限を自動的に追加しました。
一方で、IAM Policy Autopilotの出力はKMS暗号化やAccess Pointなど実際に使っていない権限まで含まれるため、過剰な権限にならないよう利用するリソースに合わせてレビューすることは必要そうです。

シナリオ2:サーバーレスREST APIの構築

ユーザー情報のCRUD APIをLambda + API Gateway + DynamoDBで構築するシナリオです。
ツール利用時はaws-serverlessプラグインのMCPサーバーを利用しました。

結果比較(template.yaml)

項目 素のLLM aws-serverless Plugin
Lambda関数数 1(ルーターパターン) 5(操作ごとに分離)
IAMポリシー 全操作にDynamoDBCrudPolicy Read → ReadPolicy / Write → CrudPolicy(粒度分離)
CPUアーキテクチャ x86_64(デフォルト) arm64(コスト最適化)
トレーシング なし Tracing: Active(X-Ray)

ツール利用時は5回のMCP呼び出しが行われました。最初のget_serverless_templatesでは条件が具体的すぎて失敗しましたが、エージェントが自動で条件を緩めて再試行する適応的な動作が見られました。最後にvalidate_cloudformation_templateでテンプレートの妥当性検証も実施されています。
興味深かったのは、aws-serverless Pluginが単一のLambda関数ではなく、CRUD操作ごとに5つに分割した関数を生成した点です。これは最小権限の原則を徹底するためで、Read系の関数にはDynamoDBReadPolicy、Write系の関数にはDynamoDBCrudPolicyと、操作ごとに必要最低限のIAMポリシーだけを付与できるようにするための構成だと考えられます。単一関数にするとどうしてもCRUD全ての権限を付けざるを得ないため、関数を分割することで権限分離をしっかり行うベストプラクティスが反映されているようでした。

シナリオ3:AccessDeniedエラーの解決

Policies: []でDynamoDB権限を付け忘れたLambdaのAccessDeniedエラーを解決するシナリオです。
ツール利用時はIAM Policy Autopilotのgenerate_policy_for_access_deniedツールを利用しました。

注:実際にAWS上へリソースを作成して再現したわけではなく、あらかじめ用意したエラーメッセージ(JSON)とLambdaコード・SAMテンプレートを入力として渡し、修正ポリシーがどのように生成されるかを確認しています。

エラーメッセージ

{
  "statusCode": 500,
  "body": "{\"error\": \"AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/scenario3-data-writer-role/scenario3-data-writer is not authorized to perform: dynamodb:PutItem on resource: arn:aws:dynamodb:ap-northeast-1:123456789012:table/scenario3-data-table\"}"
}

結果比較

項目 素のLLM IAM Policy Autopilot
原因特定 正しく特定 正しく特定
ポリシーJSON 一般的な記述(アカウントIDなし) 完全修飾ARN(リージョン+アカウントID)
検証手順 なし sam buildsam deploy → テストの手順を提示

どちらも根本原因(Policies: [])は正しく特定できました。差が出たのはポリシーの精度で、IAM Policy AutopilotはエラーメッセージからアクションとリソースARNをパースし、ピンポイントの修正ポリシーを生成しました。

シナリオ4:AWSへのデプロイ設計

Express.js(PostgreSQL / Redis / WebSocket / 画像アップロード)アプリケーションのAWS構成設計とコスト見積もりを行うシナリオです。
ツール利用時はdeploy-on-awsプラグインのMCPサーバー(awsiac + awspricing)を利用しました。

アーキテクチャ比較

項目 素のLLM deploy-on-aws Plugin
NAT Gateway あり($36/月) なし(Public Subnet + Public IP)
RDS構成 Multi-AZ(高可用性) Single-AZ(コスト重視)
Fargate 512 CPU / 1024 MB × 2タスク 256 CPU / 512 MB × 1タスク(ARM64)
セキュリティ 標準的 enforceSSL, allowAllOutbound: false
設計方針 可用性・冗長性重視 コスト効率重視(必要十分)

コスト比較

サービス 素のLLM deploy-on-aws Plugin
ECS Fargate $29.55 $8.99 -70%
ALB $22.40 $20.66 -8%
RDS PostgreSQL $27.36 $20.55 -25%
ElastiCache Redis $11.68 $18.25 +56%
NAT Gateway $36.14 $0 -100%
合計 ≈$134/月 ≈$71/月 -47%

ElastiCache Redisのようにdeploy-on-aws側の方が高くなる項目もありますが、NAT Gatewayの削除やARM64採用などのコスト最適化により全体では約半額に収まっています。

ツール利用時は14回のMCP呼び出しが行われましたが、その中で試行錯誤も発生しました。たとえばECS Fargateの料金取得でフィルタが不正だったり、ALBのサービスコード名がAPI正式名称(AWSELB)と異なるためにエラーになったりと、エージェントがget_pricing_service_codesで正しいコードを探索する過程が見られました。

/deploy Skill による実行

シナリオ4を/deployスラッシュコマンドでも実行してみました。Skillが5段階のワークフローを主導し、各フェーズで選定理由をテーブルで明示するなど、よりプロセスの透明性が高い出力が得られました。

注:/deployは最後にAWSへ実際にデプロイするステップまで含むワークフローですが、今回はAnalyze → Recommend → Estimate → GenerateまでのIaCコード生成フェーズで停止させ、実際のデプロイは行っていません。

3方比較

観点 素のLLM MCP直接 /deploy Skill
正確性 一般知識に基づく推測 静的解析・API参照で裏付けあり 同左 + 構造化ワークフローで漏れを防止
コスト 冗長性重視で高コスト(≈$134/月) リアルタイム料金でコスト最適化(≈$71/月) コスト最適化 + 代替案の差額も提示(≈$87/月)
プロセス Read/Writeのみ MCP呼び出し多数 Skill + MCP + cdk synth検証ループ

まとめ

今回はIAM Policy AutopilotとAgent Plugins for AWSを実際に使い、素のLLMとの出力の違いを4つのシナリオで比較してみました。

全体を通して感じた共通する価値は以下の点です。

  • 最新のAWS情報に基づいた提案: MCPサーバー経由でリアルタイムにドキュメント・料金を参照するため、知識カットオフの影響を受けない
  • 実コード解析による根拠ベースの出力: 推測ではなく、AST解析やAPI参照に基づくため信頼性が高い(IAM Policy Autopilotの特徴)
  • 構造化ワークフローによる一貫した品質: 毎回同じプロセスで進行するため、出力のばらつきが少ない
  • 最小権限・ベストプラクティスの自動適用: ARM64、関数分離、権限の粒度分離などが自動で適用される

一方、ツールがあればすべて完璧というわけではなく、料金取得の試行錯誤やテンプレート検索の条件調整など、エージェントが適応的に動作する場面も多く見られました。また、静的解析ベースのIAMポリシー生成では実際に使わないリソースへの権限まで含まれる場合があるため、生成されたコード・ポリシーは必ず人間がレビューしてからデプロイすることが重要です。

今回のように素のLLMとの出力差分を実際に確認してみると、ツールがどのような前提・ベストプラクティスに基づいて出力を生成しているかを把握することの重要性も感じました。便利だからと漫然と使うのではなく、ツールを導入することで何が変わるのか・どこまで任せられるのかをきちんと理解した上で、日々の開発に取り入れていきたいと思います。

参考文献

Vercel Labs の「emulate」を試してみた

はじめに

こんにちは、リテールハブ開発部の杉森です。

Vercel Labs が開発しているローカル API エミュレータ「emulate」が面白そうだったので、実際に触りながら AWS SDK (S3) との互換性、GitHub / Google の OAuth フロー、本番 API への切り替えまでを試してみました。

emulate とは

emulate は Vercel Labs が開発しているオープンソースのローカル API エミュレータです。GitHub、Google、Slack、Stripe、AWS など 12 のサービスをローカルで再現でき、単なるモック(固定レスポンスを返すだけ)ではなく、ステートフルなデータストアと OAuth フローを備えています。

npx emulate の1コマンドで 12 サービスがポート 4000〜4010 で起動します。設定ファイルは不要で、デフォルトのユーザーとトークンが自動で作成されます。

emulate v0.4.1

vercel       http://localhost:4000
github       http://localhost:4001
google       http://localhost:4002
slack        http://localhost:4003
apple        http://localhost:4004
microsoft    http://localhost:4005
okta         http://localhost:4006
aws          http://localhost:4007
resend       http://localhost:4008
stripe       http://localhost:4009
mongoatlas   http://localhost:4010

Tokens
test_token_admin -> admin

起動直後から Authorization: Bearer test_token_admin で全サービスの API を呼び出せます。

# ユーザー情報を取得
curl http://localhost:4001/user -H "Authorization: Bearer test_token_admin"

# リポジトリを作成
curl -X POST http://localhost:4001/user/repos \
  -H "Authorization: Bearer test_token_admin" \
  -H "Content-Type: application/json" \
  -d '{"name":"hello-world"}'

レスポンスは本物の GitHub API と同じ JSON 構造です。ステートフルなので、上で作成したリポジトリに対して Issue や PR を追加するといった操作もできます。

実際に試してみた

emulate v0.4.1 / Node.js v22 の環境で、以下の3つの観点から検証しました。

  1. AWS SDK (S3) との互換性: SDK 経由でそのまま使えるか
  2. GitHub / Google OAuth フローの実装: OAuth を含むアプリをローカルで開発できるか
  3. 本番 API への切り替え: 2 で作ったアプリのコードを変えずに本番で動かせるか

AWS SDK (S3) との互換性を検証する

emulate の AWS エミュレータが、AWS SDK v3 からそのまま使えるかを検証しました。検証には @aws-sdk/client-s3 3.1028.0 を使用しています。

emulate の認証の仕組み

emulate ではすべてのサービスが Bearer トークン認証に統一されています。実際のサービスではそれぞれ認証方式が異なりますが、emulate 上ではどれも同じ Authorization: Bearer でアクセスできるようになっています。

また、登録されていないトークンでリクエストしても、フォールバック機構によりデフォルトユーザー(admin)として認証が通ります。テストコードでトークンの値を気にせず書けるのは便利でした。

この仕組みが AWS SDK との互換性に直接関わってきます。

困った点と対応内容

AWS SDK v3 から emulate の S3 エミュレータを使うには、以下の2つの対応が必要でした。

1. endpoint のパスが合わない

emulate のルートは /s3/:bucket/:key 形式ですが、AWS SDK は /bucket/key にリクエストを送るため、パスが一致しません。endpoint を localhost:4007/s3 にすることでパスを合わせます。

2. 末尾スラッシュでルートが一致しない

AWS SDK は PUT /s3/bucket-name/ のように末尾スラッシュ付きでリクエストを送りますが、emulate のルート定義にスラッシュがないためマッチしません。SDK ミドルウェアで末尾スラッシュを除去することで対応しました。

対応後の動作結果

上記2つの対応を入れることで、CreateBucket、PutObject、GetObject、ListObjectsV2、ListBuckets、DeleteObject といった主要な S3 操作はすべて動作しました。

なお、AWS SDK は SigV4 署名を送りますが、emulate は SigV4 を解釈しません。前述のフォールバック認証によりデフォルトユーザーとして通るため、credentials の値は { accessKeyId: "dummy", secretAccessKey: "dummy" } で動きます。

ただし、Presigned URL、S3 イベント通知、SQS との連携などは未対応です。AWS SDK のより広い互換性が必要な場合は別のライブラリ等を検討した方が良さそうです。emulate の AWS エミュレータは「REST API の形状をテストする」用途向けという印象です。

補足: 本記事で触れたルートのパス不一致や Presigned URL 未対応については、修正の PR がすでに存在しております。マージされれば上記の回避策は不要になりそうです。

OAuth フローを組み込む

emulate を使ってみて特に便利だと思ったのは OAuth フローのエミュレーションです。GitHub OAuth でサインインして PR 一覧を表示するアプリを作って検証しました。

シード設定で初期データを定義する

emulate は YAML で初期データ(シード)を定義できます。起動時にユーザー、リポジトリ、OAuth App が自動で作成されます。

github:
  users:
    - login: admin
      name: Admin User
      email: admin@example.com
  repos:
    - owner: admin
      name: test-repo
      auto_init: true
  oauth_apps:
    - client_id: emu_github_client_id
      client_secret: emu_github_client_secret
      name: PR Viewer App
      redirect_uris:
        - http://localhost:3000/auth/callback

以下は、今回作ったアプリの OAuth フローです。

emulate にアクセスすると、以下のような認可画面が表示されます。

シードで定義したユーザーをクリックするだけで認可が完了します。トークン交換や API コールは本物の GitHub API と同じエンドポイントで動作するため、アプリ側のコードは本番と同じ実装がそのまま使えます。

なお、シード設定は宣言的なデータ定義のみに対応しており、PR のようなリソースはシードで作成できません。API 経由で投入する必要があります。

Node.js から直接起動して開発環境を自動化する

emulate は CLI( npx emulate )だけでなく、Node.js のコードから直接起動する API( createEmulator )も提供しています。これを使って、emulate の起動、テストデータの投入、Web サーバーの起動を1コマンドにまとめました。

import { createEmulator } from "emulate";

const github = await createEmulator({ service: "github", port: 4001, seed: config });
const google = await createEmulator({ service: "google", port: 4002, seed: config });

npm run dev だけで全部起動する体験は快適でした。

本番 API への切り替えを検証する

server.js はすべてのエンドポイント URL を process.env から読み取る設計にしました。emulate と本番の切り替えは .env.local と .env の読み分けだけで行えます。

# .env.local(emulate 用)
GITHUB_URL=http://localhost:4001
GITHUB_API_URL=http://localhost:4001
GITHUB_CLIENT_ID=emu_github_client_id
GITHUB_CLIENT_SECRET=emu_github_client_secret
GITHUB_OWNER=admin
GITHUB_REPO=test-repo

# .env(本番用)
GITHUB_URL=https://github.com
GITHUB_API_URL=https://api.github.com
GITHUB_CLIENT_ID=<実際の Client ID>
GITHUB_CLIENT_SECRET=<実際の Client Secret>
GITHUB_OWNER=<実際のオーナー名>
GITHUB_REPO=<実際のリポジトリ名>
{
  "scripts": {
    "dev": "node --env-file=.env.local dev.js",
    "start:prod": "node --env-file=.env server.js"
  }
}

本番の GitHub OAuth App を作成し .env に設定して npm run start:prod で起動したところ、コード変更なしで本物の GitHub 認可画面が表示され、実際の PR 一覧が取得できました。

観点 emulate 本番 GitHub
認可画面 emulate のユーザー選択画面 GitHub の実際の認可画面
認可の操作 ユーザーをクリック 「Authorize」ボタン
データ テストデータ リポジトリの実際の PR
API キー 不要 実際の Client ID / Secret

Google OAuth + Gmail API も同じパターンで追加しました。emulate 用の環境変数は以下の通りです。

# .env.local(emulate 用)
GOOGLE_URL=http://localhost:4002
GOOGLE_TOKEN_URL=http://localhost:4002/oauth2/token
GOOGLE_API_URL=http://localhost:4002
GOOGLE_CLIENT_ID=emu_google_client_id
GOOGLE_CLIENT_SECRET=emu_google_client_secret

# .env(本番用)
GOOGLE_URL=https://accounts.google.com
GOOGLE_TOKEN_URL=https://oauth2.googleapis.com
GOOGLE_API_URL=https://www.googleapis.com
GOOGLE_CLIENT_ID=<実際の Client ID>
GOOGLE_CLIENT_SECRET=<実際の Client Secret>

まとめ

3つの検証観点ごとに結論を整理します。

  1. AWS SDK (S3) との互換性: endpoint のプレフィックス追加と末尾スラッシュ除去の2つの回避策を入れれば、主要な S3 操作は動作する。ただし Presigned URL 等は未対応で、より広い互換性が必要なら 別のライブラリを検討した方が良い。

  2. GitHub / Google OAuth フローの実装: シード設定と認可画面の自動生成により、OAuth App の登録やテストユーザーの作成なしで OAuth フローを含むアプリの開発を始められる。OAuth フローをローカルで手軽にテストできるのは便利だった。

  3. 本番 API への切り替え: エンドポイント URL を環境変数に切り出しておけば、コード変更なしで本番に切り替えられる。

興味がある方はぜひ試してみてください。

参考リンク

emulate GitHub リポジトリ
emulate 公式ドキュメント

ECR イメージスキャンでコンテナの脆弱性を検知する

目次

はじめに

こんにちは、開発本部開発1部トモニテグループのエンジニアの パンダム/rymiyamoto です。

2025年末に Next.js の React Server Components に DoS(サービス拒否)とソースコード露出の脆弱性が公開され、App Router を使用するサービスでのアップグレード対応が求められました。

このように、利用しているフレームワークやライブラリに深刻な脆弱性が見つかることは珍しくありません。 こうした脆弱性が公開中のサービスに影響していないかを素早く把握できる体制を整えるべく、弊社でも ECR のイメージスキャンを導入しました。

本記事では、その取り組みの一つとして ECR のイメージスキャンを導入した際の設計・構築・運用について紹介します。 同じように ECR のイメージスキャンをこれから導入しようとしている方の参考になれば幸いです。

ECR イメージスキャンとは

Amazon ECR のイメージスキャンは、コンテナイメージに含まれるソフトウェアの脆弱性(CVE)を検出する機能です。

スキャンには Basic Scanning と Enhanced Scanning の2種類があります。

項目 Basic Scanning Enhanced Scanning
スキャンエンジン Clair(オープンソース) Amazon Inspector2
検出対象 OS パッケージの脆弱性 OS パッケージ + プログラミング言語パッケージ(npm, pip, Maven 等)
スキャンタイミング プッシュ時 / 手動 プッシュ時 / 継続スキャン
料金 無料 有料(スキャンしたイメージ数に応じた従量課金)

構成の全体像

導入した構成は以下の通りです。

ECR Enhanced Scanning (Inspector2)
    ↓ 脆弱性検知
EventBridge Rule (CRITICAL のみフィルタ)
    ↓
SNS Topic
    ↓
AWS Chatbot → Slack チャンネルに通知

設計にあたって意識したのは以下です。

検知の網羅性

OS パッケージだけでなく言語パッケージもカバーしたかったため、Enhanced Scanning を採用しました。対応言語の詳細は公式ドキュメントを参照してください。

docs.aws.amazon.com

一方で、OS パッケージの脆弱性検知だけで十分なケースや、まずは無料で始めたいケースでは Basic Scanning も有力な選択肢です。自社の要件に合わせて検討してみてください。

通知のノイズ低減

すべての severity を通知すると対応が追いつかなくなるため、まずは CRITICAL に絞って運用を開始しました。実際に HIGH まで含めて試してみたところ、本当に対応すべき通知が埋もれかねないと感じたので、まずは CRITICAL で運用を開始し、必要に応じてフィルタを広げる方針としています。

認知のスピード

脆弱性の存在に気づかないことが一番のリスクなので、Slack への即時通知を組み込みました。Slack への通知方法としては EventBridge → Lambda で通知内容をカスタマイズする方法もありますが、今回はまず検知できる状態を素早く作ることを優先し、コードを書かずに構築できる AWS Chatbot を採用しました。

コスト

Enhanced Scanning は Amazon Inspector2 の料金体系に基づきます。料金は以下の2つで構成されます(2026年4月時点)。

最新の料金は公式ドキュメントをご確認ください。

aws.amazon.com

  • 初回スキャン: イメージがプッシュされた時のスキャン、$0.09 / イメージ
  • 再スキャン: 継続スキャンにより新しい CVE が公開された際の自動再スキャン、$0.01 / イメージ

試算の考え方

スキャン頻度によってコストの構造が異なります。

スキャン頻度 発生するコスト 計算式
プッシュ時 初回スキャンのみ 月間プッシュ数 × $0.09
継続スキャン 初回スキャン + 再スキャン 上記 + 保持イメージ数 × 再スキャン回数/月 × $0.01

弊社では本番環境は継続スキャン、開発環境はプッシュ時スキャンで運用しています。本番環境では新しい CVE が公開されたタイミングでも即座に検知したいため継続スキャン、開発環境では脆弱性を含む実装が入った時点で素早く検知しつつコストも抑えたいためプッシュ時スキャンが適しています。

試算例

例えば、5つのリポジトリに対して月間100回プッシュし、本番では各リポジトリに2イメージを保持(計10イメージ)するケースで試算します。再スキャン回数は月にどれくらいの頻度で対象の CVE が新たに公開されるかに依存しますが、ここでは月15回程度を見込みました。

項目 計算式 コスト
初回スキャン 100 push × $0.09 $9.00
再スキャン 10 images × 15回 × $0.01 $1.50
月額合計 $10.50

実際のコストはリポジトリ数・プッシュ頻度・保持イメージ数によって変わるので、自社の運用に合わせて試算してみてください。

Basic Scanning(無料)と比較するとコストはかかりますが、言語パッケージの脆弱性検知や新規 CVE の自動再スキャンが得られることを考えると、検討する価値はあると思います。

Terraform による構築

1. ECR スキャン設定

まず ECR レジストリに対して Enhanced Scanning を有効化します。

resource "aws_ecr_registry_scanning_configuration" "this" {
  scan_type = "ENHANCED"

  rule {
    scan_frequency = "CONTINUOUS_SCAN"
    repository_filter {
      filter      = "*"
      filter_type = "WILDCARD"
    }
  }
}

filter = "*" でレジストリ内のすべてのリポジトリをスキャン対象にしています。リポジトリを個別に指定する方法もありますが、新しいリポジトリを追加した際にスキャン対象への追加を忘れるリスクがあるため、ワイルドカードで全体を対象にしています。

scan_frequency は環境によって使い分けています。本番環境では CONTINUOUS_SCAN、開発環境では SCAN_ON_PUSH を設定しています。

2. EventBridge ルール

resource "aws_cloudwatch_event_rule" "ecr_scan_finding" {
  name = "ecr-scan-finding-notification"
  event_pattern = jsonencode({
    "source" : ["aws.inspector2"],
    "detail-type" : ["Inspector2 Finding"],
    "detail" : {
      "status" : ["ACTIVE"],
      "severity" : ["CRITICAL"],
      "resources" : {
        "type" : ["AWS_ECR_CONTAINER_IMAGE"]
      }
    }
  })
  state = "ENABLED"
}

resource "aws_cloudwatch_event_target" "ecr_scan_finding_sns" {
  rule = aws_cloudwatch_event_rule.ecr_scan_finding.name
  arn  = var.ecr_scan_finding_sns_topic_arn
}

Enhanced Scanning では Inspector2 がスキャンエンジンとなるため、イベントソースは aws.inspector2 になります。 Basic Scanning の場合は aws.ecr になるので注意が必要です。

3. SNS トピック

EventBridge から受け取ったイベントを AWS Chatbot に渡すための SNS トピックを作成します。

resource "aws_sns_topic" "ecr_scan_finding_topic" {
  name = "ecr-scan-finding-topic"
}

resource "aws_sns_topic_policy" "ecr_scan_finding_topic_policy" {
  arn    = aws_sns_topic.ecr_scan_finding_topic.arn
  policy = data.aws_iam_policy_document.sns_ecr_scan_finding_topic_policy.json
}

data "aws_iam_policy_document" "sns_ecr_scan_finding_topic_policy" {
  # EventBridge からの Publish を許可
  statement {
    sid    = "AllowEventBridgeToPublishSNS"
    effect = "Allow"
    actions   = ["sns:Publish"]
    principals {
      type        = "Service"
      identifiers = ["events.amazonaws.com"]
    }
    resources = [aws_sns_topic.ecr_scan_finding_topic.arn]
    condition {
      test     = "StringEquals"
      variable = "AWS:SourceAccount"
      values   = [data.aws_caller_identity.current.account_id]
    }
    condition {
      test     = "ArnEquals"
      variable = "aws:SourceArn"
      values   = ["arn:aws:events:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:rule/ecr-scan-finding-notification"]
    }
  }

  # Chatbot からの Subscribe を許可
  statement {
    sid    = "AllowChatbotToSubscribe"
    effect = "Allow"
    actions   = ["sns:Subscribe"]
    principals {
      type        = "Service"
      identifiers = ["chatbot.amazonaws.com"]
    }
    resources = [aws_sns_topic.ecr_scan_finding_topic.arn]
    condition {
      test     = "StringEquals"
      variable = "AWS:SourceAccount"
      values   = [data.aws_caller_identity.current.account_id]
    }
    condition {
      test     = "ArnEquals"
      variable = "aws:SourceArn"
      values   = ["arn:aws:chatbot::${data.aws_caller_identity.current.account_id}:chat-configuration/slack-channel/alert-to-slack"]
    }
  }
}

SNS トピックポリシーでは、EventBridge からの Publish と Chatbot からの Subscribe のみを許可しています。 condition で発信元を絞ることで、意図しないリソースからの操作を防いでいます。

4. AWS Chatbot(Slack 通知)

最後に、SNS トピックのメッセージを Slack に転送する Chatbot の設定です。

resource "aws_chatbot_slack_channel_configuration" "chatbot_alert_to_slack" {
  configuration_name = "alert-to-slack"
  slack_channel_id   = "XXXXXXXXX" # 通知先の Slack チャンネル ID
  slack_team_id      = "XXXXXXXXX" # Slack ワークスペース ID
  iam_role_arn       = var.chatbot_role_arn
  sns_topic_arns = [
    var.ecr_scan_finding_topic_arn,
    # 他の通知用 SNS トピックもここに追加できる
  ]
  guardrail_policy_arns = [
    "arn:aws:iam::aws:policy/ReadOnlyAccess"
  ]
  logging_level = "ERROR"
}

これで CRITICAL な脆弱性が検知された際に、Slack チャンネルに通知が届くようになります。

なお、AWS Chatbot では同じ Slack チャンネルに対して複数の configuration を作成できません。そのため configuration_namealert-to-slack のように汎用的な名前にしています。こうしておけば、今後 WAF のアラートなど別の通知を追加したくなっても sns_topic_arns にトピックを足すだけで済みます。

実際の通知と運用

実際に届く通知は以下のような形式です。

最初は CVE の詳細まで Slack で確認できるものだと思っていたのですが、実際に届く通知には Inspector2 Finding というイベント名と対象の ECR イメージの ARN が表示されるだけで、CVE 名もパッケージ名も表示されませんでした。

そのため、EventBridge の input_transformer を使い、Chatbot のカスタム通知で通知内容を改善しました。

resource "aws_cloudwatch_event_target" "ecr_scan_finding_sns" {
  rule      = aws_cloudwatch_event_rule.ecr_scan_finding.name
  target_id = "SendToSNS"
  arn       = var.ecr_scan_finding_sns_topic_arn

  input_transformer {
    input_paths = {
      "severity"    = "$.detail.severity"
      "title"       = "$.detail.title"
      "description" = "$.detail.description"
      "repository"  = "$.detail.resources[0].details.awsEcrContainerImage.repositoryName"
    }

    input_template = <<TEMPLATE
{
  "version": "1.0",
  "source": "custom",
  "content": {
    "textType": "client-markdown",
    "title": ":rotating_light: ECR <severity> 脆弱性検出 [環境名 (AWSアカウントID)]",
    "description": "*重要度*: <severity>\n*リポジトリ*: <repository>\n*脆弱性*: <title>\n*詳細*: <description>"
  }
}
TEMPLATE
  }
}

ポイントは input_paths でイベントから必要な項目を抽出し、カスタム通知フォーマットで整形している点です。改善後の通知は以下のような形式です。

CVE-ID やパッケージ名、リポジトリ名が表示されるようになり、Slack 上で脆弱性の概要を把握できるようになりました。詳細な対応判断が必要な場合は Inspector2 のダッシュボードを確認する運用ですが、通知を見ただけで対応要否がわかることが増えました。

さらに通知内容を自由にカスタマイズしたい場合は、EventBridge → SNS → Chatbot の経路ではなく、EventBridge → Lambda で整形する方法もあります。

導入してみて

CRITICAL に絞った判断はうまくいきました。最初の通知が来たときも「これは本当に対応が必要なものだ」と落ち着いて対処できたので、狙い通りでした。

一方で、Chatbot のデフォルトの通知では CVE の詳細が出ず、正直もう少し情報が出ると思っていました。実際に使ってみて初めて気づいた部分で、 input_transformer を使ってカスタマイズできることも後から知りました。

Terraform での複数環境展開やスキャン頻度の使い分けはすんなりいきました。

まとめ

今回は、フレームワークやライブラリの脆弱性に素早く対応できる体制づくりの一環として、ECR の Enhanced Scanning を導入した事例を紹介しました。

構成としては ECR Enhanced Scanning → EventBridge → SNS → Chatbot → Slack というシンプルなパイプラインですが、Terraform でコード化することで再現性のある形で複数環境に展開できました。

まず検知できる状態を作ることが第一歩、そこさえ超えれば運用しながら精度を上げていけます。本記事がその一歩を踏み出すきっかけになれば嬉しいです。

最後まで読んでいただきありがとうございました!