AIエージェント前提のCLI「Swamp」でClaude CodeにAWS棚卸しを作ってもらった

AIエージェント前提のCLI「Swamp」でClaude CodeにAWS棚卸しを作ってもらった

2026.04.23

System Initiative社がOSSで公開している swamp というCLIを試しました。AIエージェントと組み合わせて運用ワークフローを作るためのツールで、READMEには "AI Native Automation, built for agents" とあります。

https://github.com/systeminit/swamp

AWS EC2の棚卸しを題材に、Claude Codeから swamp を使ってもらう流れを一通り触ってみました。

Swampとは

  • System Initiative製のCLI。Deno製で、手元の環境で完結する
  • AIエージェント(Claude Code / Cursor / OpenCode / Codex)と組み合わせて使う前提で設計されている
  • モデル・ワークフロー・データを軸とした自動化フレームワーク
  • モデル・ワークフロー・拡張の定義はGitで管理。ランタイムデータ(実行結果・ログ・秘密情報)は .swamp/ 配下(gitignore済み)
  • .swamp/ のバックエンドはローカルFS・NFS・S3から選べる。チームで共有したい場合はS3等に切り替える

READMEの使い方イメージはこんな感じです。

  • "Manage my EC2 fleet — inventory every instance across all regions and flag anything without a cost-center tag"
  • "Audit our DNS records and compare them against what's actually running"
  • "Build a workflow that rotates database credentials and stores them in the vault"

エージェントに自然言語で依頼すると、エージェントが swamp コマンドを組み立ててモデルやワークフローを作り、実行してくれる、という設計です。

コア概念

主要な要素は以下です。Terraform経験があれば右列の対応が参考になります。

Swamp 概要 Terraform
モデル型(Model Type) 外部システム(AWSリソースやAPI)の型定義。スキーマとメソッドを持つ リソース種類(aws_instance
モデル(Definition YAML) モデル型を具体的な設定値で実体化したインスタンス resource "aws_instance" "web" { ... }
拡張(Extension) モデル型を配布するためのパッケージ プロバイダ
データ(Data) メソッド実行結果。バージョン付きで .swamp/data/ に保存される .tfstate
swamp.club レジストリ 拡張を公開・配布する中央サーバー registry.terraform.io

Terraformとの違いは、Swampのメソッドは create / update / delete に限らず任意の操作を定義できる点です。棚卸しや監査のような「調査系」の操作もモデル化できます。

インストールとリポジトリ初期化

インストールはワンライナーです。デフォルトで ~/.local/bin に入ります。

curl -fsSL https://swamp.club/install.sh | sh
swamp --version
# swamp 20260421.213501.0-sha.0432a31a

適当なディレクトリで swamp repo init すると必要なファイルが一式生成されます。デフォルトはClaude Code向けです。

mkdir swamp-tryout && cd swamp-tryout
swamp repo init

生成されたのは以下です。

.
├── .swamp.yaml              # リポジトリメタデータ
├── .swamp/                  # ランタイムデータ(SQLite、ログ、監査)
├── .claude/
   └── skills/              # 14個のswamp-*スキル(合計4700行超)
├── CLAUDE.md                # エージェント向けプロジェクト指示
├── .gitignore               # .swamp/ と .claude/ を無視
├── models/                  # モデル定義用
├── workflows/               # ワークフロー定義用
└── vaults/                  # シークレット用

ポイントは .claude/CLAUDE.md です。swamp repo init がClaude Code向けのスキル14個とプロジェクト指示を書き込みます。エージェントはこれを読んで swamp の使い方を理解します。

スキルの一覧は以下です。

swamp-getting-started     swamp-extension-model
swamp-model               swamp-extension-driver
swamp-workflow            swamp-extension-datastore
swamp-vault               swamp-extension-vault
swamp-data                swamp-extension-publish
swamp-repo                swamp-issue
swamp-report              swamp-troubleshooting

CLAUDE.md には8個のルールが書かれています。印象的だったのはこのあたりです。

  • Rule 1: Search before you build 新しい連携を作る前に既存のローカル型とコミュニティ拡張を検索する。shell/commandモデルはワンオフ限定で、CLIラッパーには使うな
  • Rule 2: Extend, don't be clever モデルにメソッドが足りなければ拡張する。shellスクリプトで迂回するな
  • Rule 6: Prefer fan-out methods over loops 同一モデルに対するメソッド呼び出しは直列化されるので、複数ターゲットを扱うときはfan-outメソッドを使うかモデルを分ける

つまりエージェントが雑にshellで済ませないように縛りをかける設計です。

エージェントにAWS棚卸しをお願いする

別ターミナルでClaude Codeを起動して、指示を出します。

「Swampを初めて使います。READMEで見たAWS EC2の棚卸しをやりたいのですが、まず何をしたらいいですか?」

エージェントはまず CLAUDE.md の指示どおり swamp model search --json を実行します。モデルが0件なので swamp-getting-started スキルが起動し、ステートマシーンベースのオンボーディングに入りました。

start → goals_understood → model_created → method_run → output_inspected → graduated

各ステップで Verify が通るまで次に進めない作りです。

goals_understood ステップでエージェントから「どこまで棚卸しするか」と聞かれたので、EC2 / EBS / AMI / セキュリティグループ の4つに絞りました。この後の拡張選定はこの範囲を前提に進みます。

拡張の選び方: 公式→コミュニティ→自作

公開されている拡張は swamp.club/extensions で一覧できます。CLIからも swamp extension search <keyword> で検索できます。

最初は公式拡張 @swamp/aws/ec2

エージェントが最初に見つけたのは公式拡張でした。

swamp extension pull @swamp/aws/ec2
# → 179個のモデル型が取り込まれる

EC2関連で179型あり、Instance / VPC / Subnet / SecurityGroup / NATGateway / TransitGateway などが揃っています。

同じEC2でも「管理型」と「調査型」で別物

@swamp/aws/ec2/instance のソースを見たところ、メソッドは create / read / update / delete / sync でした。

.swamp/pulled-extensions/@swamp/aws/ec2/models/instance.ts
// Auto-generated extension model for @swamp/aws/ec2/instance
// Do not edit manually. Re-generate with: deno task generate:aws

// ...

methods: {
  create: { /* ... */ },
  read:   { /* ... */ },
  update: { /* ... */ },
  delete: { /* ... */ },
  sync:   { /* ... */ },
},

1モデル=1リソースで、CRUD操作を提供する作りです。実体はAWS Cloud Control APIを叩いています。

.swamp/pulled-extensions/@swamp/aws/ec2/models/_lib/aws.ts
import {
  CloudControlClient,
  CreateResourceCommand,
  DeleteResourceCommand,
  GetResourceCommand,
  UpdateResourceCommand,
} from "@aws-sdk/client-cloudcontrol";

CloudFormationスタックを作るわけではありません。CloudFormationのリソース型スキーマをそのまま借りて、Cloud Control APIで個別リソースをCRUDしています。

このつくりは「リソースを宣言的に管理する」用途には合うのですが、棚卸し(未知のリソースを全件リストアップする)には向きません。インスタンスIDが事前に分かっていないと read できず、100台あればモデル定義も100個必要になるためです。

この「リソース管理型」に対して、棚卸し向けのモデル設計は「リソース調査型」と呼べるもので、作りが違います。

リソース管理型 リソース調査型
目的 リソースを作る・更新する・消す リソースを調べる
1モデル 1リソース アカウント/リージョン全体
メソッド create / read / update / delete / sync list_* / describe_*
実体 AWS Cloud Control API、Google Cloud REST API AWS SDK の Describe API など
@swamp/aws/ec2/instance@swamp/gcp/compute/disks @webframp/aws/inventory
適する用途 構成管理、IaC 監査、棚卸し、コスト可視化

Terraformの概念に例えると、リソース管理型は resource ブロック、リソース調査型は data ブロックに近い役割です。

Google Cloud用の @swamp/gcp/* も同じくリソース管理型でした。Google Cloud は Cloud Control API 相当のものがないため、Google APIs の Discovery Document から生成して Google Cloud REST API を直接叩く実装になっています。

コミュニティ拡張 @webframp/aws/inventory に切り替え

棚卸し向けの拡張を探したら、コミュニティ拡張の @webframp/aws/inventory が見つかりました。中身はリソース調査型で、list_ec2 / list_ebs / list_rds / list_dynamodb / list_lambda / list_s3 が並んでいます。

swamp extension pull @webframp/aws/inventory

ただしAMIとセキュリティグループはこの拡張にはありません。

足りない分はカスタム拡張を自作

AMI と SG 用のカスタム拡張も、Claude Codeに書いてもらいました。swamp-extension-model スキルにテンプレートや実装上の規約が書かれているので、エージェントはそれを読み、@webframp/aws/inventory のソースも参考にしながら実装していました。こちらは方針だけ伝えて、実装とvalidateは任せています。

中身はTypeScriptで1ファイルです。

extensions/models/aws_inventory_extra.ts
import { z } from "npm:zod@4.3.6";
import {
  DescribeImagesCommand,
  DescribeSecurityGroupsCommand,
  EC2Client,
} from "npm:@aws-sdk/client-ec2@3.1010.0";

// ...スキーマ定義は省略...

export const model = {
  type: "@sato/aws-inventory-extra",
  version: "2026.04.22.1",
  globalArguments: GlobalArgsSchema,

  resources: {
    inventory: {
      schema: InventoryResultSchema,
      lifetime: "1h" as const,
    },
  },

  methods: {
    list_ami: {
      description: "List AMIs owned by this account",
      arguments: z.object({}),
      execute: async (_args, context) => {
        const client = new EC2Client({ region: context.globalArgs.region });
        // DescribeImages(Owners: ["self"]) をページングしながら呼ぶ
        // ...
        const handle = await context.writeResource(
          "inventory",
          `ami-${context.globalArgs.region}`,
          { region, resourceType: "ami", resources: amis, count, fetchedAt },
        );
        return { dataHandles: [handle] };
      },
    },
    list_sg: {
      // DescribeSecurityGroups を同様に
    },
  },
};

出力スキーマとデータ名の規約は @webframp/aws/inventory に揃えてもらいました。あとでワークフローやレポートから同じ形で扱えるようにするためです。

棚卸しを動かす

モデルを作って、validate して、メソッドを実行します。

swamp model create aws-inventory \
  --type @webframp/aws/inventory \
  -a region=ap-northeast-1

swamp model validate aws-inventory
swamp model method run aws-inventory list_ec2

検証用AWSアカウントでの結果はこうなりました。

対象 件数 モデル / メソッド データ名
EC2 (running) 0 aws-inventory / list_ec2 ec2-ap-northeast-1 v1
EBS 0 aws-inventory / list_ebs ebs-ap-northeast-1 v1
AMI (self-owned) 0 aws-inventory-extra / list_ami ami-ap-northeast-1 v1
SG 3 aws-inventory-extra / list_sg sg-ap-northeast-1 v1

データは .swamp/data/ 配下にバージョン付きで残ります。

.swamp/data/@webframp/aws/inventory/{modelId}/ec2-ap-northeast-1/
├── 1/
   ├── raw              # JSONの生データ
   └── metadata.yaml    # タグ、チェックサム、lifetime
├── 2/
└── latest               # 最新版へのポインタ

同じメソッドを2回実行すると v2 が、3回目で v3 が増えていきます。ワークフローやレポートからはCEL式で参照できます。

CEL(Common Expression Language)はGoogle発祥のシンプルな式言語で、YAMLやJSONに動的な値を埋め込みたいときに使います。Google CloudのIAM ConditionsやKubernetesのCRDバリデーションでも使われています。SwampではモデルやVaultの値を定義ファイルから参照するのに使います。

data.latest("aws-inventory", "ec2-ap-northeast-1").attributes.count

ワークフロー化

4つのメソッドをまとめて実行できるようにワークフローを作りました。

ここで効いてくるのが CLAUDE.md の Rule 6 です。同一モデルに対するメソッド呼び出しはロック競合を避けるため直列化されるので、以下の構造にする必要があります。

  • 同一モデル内のメソッドは直列
  • 別モデル間は並列

エージェントがRule 6に従って自発的にワークフローを構築してくれました。

workflows/workflow-xxx.yaml
jobs:
  - name: inventory
    steps:
      - name: list_ec2
        task: { type: model_method, modelIdOrName: aws-inventory, methodName: list_ec2 }
        dependsOn: []
      - name: list_ebs
        task: { type: model_method, modelIdOrName: aws-inventory, methodName: list_ebs }
        dependsOn: [{ step: list_ec2, condition: { type: succeeded } }]
      - name: list_ami
        task: { type: model_method, modelIdOrName: aws-inventory-extra, methodName: list_ami }
        dependsOn: []
      - name: list_sg
        task: { type: model_method, modelIdOrName: aws-inventory-extra, methodName: list_sg }
        dependsOn: [{ step: list_ami, condition: { type: succeeded } }]

実行すると list_ec2list_ami が別モデルなので同時スタートし、各々の後続がシリアルに流れます。

inventory job (709ms total)
├─ list_ec2 (450ms) ──► list_ebs (258ms)     [aws-inventory model, sequential]
└─ list_ami (446ms) ──► list_sg  (259ms)     [aws-inventory-extra model, sequential]

以降は swamp workflow run aws-inventory の一発で全件棚卸しできます。

レポート化

最後に、棚卸し結果をMarkdownサマリにまとめるレポート拡張を書きました。スコープは workflow で、ワークフローが成功するたびに自動で生成される作りです。

extensions/reports/aws_inventory_summary.ts
export const report = {
  name: "@sato/aws-inventory-summary",
  scope: "workflow" as const,
  labels: ["inventory", "aws"],
  execute: async (context) => {
    // context.stepExecutions を走査して、各ステップが書き出した
    // データ(ec2/ebs/ami/sg)を dataRepository から取得し、
    // Markdownのサマリを組み立てる
    // ...
    return { markdown: lines.join("\n"), json: { /* ... */ } };
  },
};

ワークフローYAMLに reports.require を追加すると、ワークフロー実行後に自動でレポートが動きます。

workflows/workflow-xxx.yaml
reports:
  require:
    - "@sato/aws-inventory-summary"

生成されたMarkdownはこんな感じです。

# AWS Inventory Summary

- **Workflow**: aws-inventory (succeeded)
- **Region**: ap-northeast-1
- **Fetched at**: 2026-04-22T07:53:07.487Z

## Counts

| Resource | Count |
|---|---:|
| EC2 (running) | 0 |
| EBS volumes | 0 |
| AMI (self-owned) | 0 |
| Security Groups | 3 |

## Security Groups

| groupId | groupName | vpcId | ingress | egress |
|---|---|---|---:|---:|
| sg-xxx... | ec2-rds-1 | vpc-xxx... | 0 | 1 |
| sg-xxx... | rds-ec2-1 | vpc-xxx... | 1 | 0 |
| sg-xxx... | default   | vpc-xxx... | 1 | 1 |

レポート実装中に1回エラーで失敗したのですが、エージェントがSwamp本体のソースまで遡って原因を特定し、そのままワークアラウンドも入れてくれました。WorkflowReportContext.stepExecutions[].modelType は型定義上は文字列ですが、dataRepository.getContent()ModelType バリューオブジェクト(.toDirectoryPath().normalized を使う)を期待していて、そこでズレていたという話です。value-object相当の構造体を渡すワークアラウンドで回避していました。本来は swamp 本体側で揃えるか、getContent を文字列でも受け付ける形にするのが筋で、swamp-issue スキルを使えばそのままissueを投げる導線もあります。

触ってみて

使ってみて感じたのは、Swampは人間向けのCLI使い勝手よりも、エージェントが使うことを優先して設計されているように見える ことです。

  • swamp repo init を打つと、CLIだけでなくClaude Code向けのスキルとCLAUDE.mdが配置される
  • エージェントは swamp-getting-started からオンボーディングされ、Search before you build の原則で挙動が縛られる
  • ワークフローの並列/直列配置のような実装上の勘どころが CLAUDE.md に書かれていて、エージェントが自発的に守る
  • swamp help はデフォルトでJSONを返す。人間向けの swamp --help とは別に、エージェントが読みやすい形式が標準で用意されている

実際に swamp help を実行した結果は以下です。

{
  "version": "20260421.213501.0-sha.0432a31a",
  "root": {
    "name": "swamp",
    "description": "AI Native Automation CLI",
    "arguments": [],
    "options": [
      {
        "flags": "--json",
        "description": "Output in JSON format (non-interactive)",
        "required": false,
        "collect": false
      },
      ...
    ],
    "subcommands": [ ... ]
  }
}

arguments / options / subcommands が構造化されているため、エージェントが jq でサブコマンド一覧を取るのも .root.subcommands[].name で一発です。特定の値を抽出しやすい設計になっています。

触る前は「CLIそのものはシンプルで、AI連携がおまけ」くらいの想像でしたが、逆でした。CLIのコマンド体系はシンプルな作りで、むしろ4700行を超えるスキル群とCLAUDE.mdのほうが内容の中心です。

成果物(extensions/models/workflows/)はYAMLとTypeScriptのファイルとして残るので、エージェントが作ったものを人間がGitでレビューする流れが成り立ちます。.swamp/ 配下のランタイムデータは .gitignore で除外されるので、Git管理するのは定義ファイルだけです。

この記事をシェアする

関連記事