Claude Code のような AI エージェント(=指示を受けて、自分でファイルを読み書きしたりコマンドを実行したりする AI)に、会社の仕事を任せる場面が増えてきました。便利な一方で、「AI にどこまで見せてよいのか」「パスワードを渡して大丈夫なのか」は、使い始めてから気になることが多いところです。
「AI に仕事を任せたいけれど、社内の資料やパスワードを全部見せてしまうのは不安…」
この連載では、AI エージェントに仕事を安全に任せるための「土台」を、AWS の上に作っていきます。結論を先にお伝えすると、AI には API のトークン(=入場券のような文字列)だけを渡し、パスワードや資料は AWS 側に置いて、役割ごとに必要なものだけを返す形にします。
第1回は、当社(株式会社THIRD HERO)がこの形にたどり着いた理由と全体の設計を説明し、サンプルのプロジェクトを作り始めます。
この連載で作るもの
連載のサンプルは、「渡された文章を WordPress に投稿するだけ」の小さな中継の API です。小さいですが、AI に仕事を任せるときに必要な考え方はひととおり入っています。
| できること | 仕組み |
|---|---|
| Claude Code のセッションから「この文章を投稿して」と頼むと、AWS の API が WordPress に投稿する | API は Python の FastAPI で書き、AWS の ECS Fargate で動かす |
| WordPress のパスワードは AI から見えない | パスワードは AWS Secrets Manager にだけ置き、API が投稿のときに付ける |
| 役割ごとに投稿できるサイトを決め、範囲の外は断る | トークン → 役割 → 投稿できるサイト、の順に API が判定する |
| アクセスキーを置かずに AWS へ反映する | GitHub Actions から OIDC で AWS に入る。本番は人がタグを作ったときだけ |
| おかしな呼び出しに気付き、費用を抑える | トークン違いが続いたら知らせる。WAF で回数を制限する。確認用はふだん止める |
当社の実際の社内基盤は、資料の読み書きやナレッジの検索まで扱いますが、連載ではここまでに絞ります。
1つのリポジトリで AI に仕事をさせていたころ
当社は、アフィリエイトブログ(記事の指示書・困りごとの台帳・SEO・市場調査・設定)と困りごとと連携した営業用の資料作成スクリプトを、1つの GitHub のリポジトリ(=ファイルと変更の履歴をまとめて置く場所)に入れて、Claude Code 経由で作業していました。
最初は「ブログを別々のリポジトリに分ければよいのでは」と考えました。ただ、分けるとブログ用のスクリプトがリポジトリごとに重複します。そこで発想を変えて、リポジトリを分けるのではなく、資料を AWS に置き、役割ごとに必要なものだけを渡す形に切り替えました。
困っていたこと
| 困りごと | 中身 |
|---|---|
| 見えすぎる | ブログの記事を書くセッションからも、営業の単価や案件の資料が見えてしまう |
| 止められない | 資料を直すたびに git の操作が要り、AI が別の場所の資料を読みに行くことを止められない |
| 重く遅い | 記事を作るたびに、全ブログと営業の資料が入ったリポジトリを丸ごと取得してから動いていた |
リポジトリの大きさは、2026年10月末の時点で、管理しているファイルが約5,000、git の履歴を含めて約290MB でした。記事を1本作るまでの待ち時間も、そのぶん長くなっていました。
使用料も増えていた見込み
📰 出典:How Claude remembers your project(Claude Code Docs)
Claude Code は、セッションの始めにリポジトリの CLAUDE.md(=AI への指示書。セッションのたびに読み込まれる)を読みます。当社の CLAUDE.md には全役割の決まりが書いてあり、関係のない資料まで読み込まれていました。
1回あたりの読み込み量(トークン数)が増えると、利用の上限に早く届き、使用料も増えます。測った数字ではないので「見込み」ですが、API に切り替えたあとは、セッションが取得するのは小さな土台のリポジトリと、その役割の資料(技術ブログならおよそ10ファイル)だけになりました。
分けるのは「リポジトリ」ではなく「AI に渡すもの」
切り替えにあたって決めたことは、次の5つです。
- 役割を分ける:ブログはサイトごとに、営業で1つ、計7つの役割にする
- 役割ごとに Claude Code の環境を作る:環境ごとに、その役割専用のトークンを持たせる
- AWS に API を置く:トークンから役割を判定し、資料(S3)と外部サービスのパスワード(Secrets Manager)は API 側で持つ
- 外部サービスへは API が中継する:WordPress や Google へは API がパスワードを付けて送る。AI にはパスワードを返さない
- 権限の決まりは1か所に書く:トークンには役割だけを記録し、何ができるかは API のコードの1か所で決める
最後の「1か所に書く」が、今回作るコードの中心です。トークンに「投稿できるサイト」まで書き込んでしまうと、範囲を直すたびにトークンを作り直すことになります。役割だけを記録しておけば、コードを直すだけで全員の範囲を変えられます。
全体の構成
Claude Code(役割ごとの環境)
│ トークンは Network secrets が付ける(AI からは見えない)
▼ HTTPS
DNS → ALB(HTTPS のみ)── WAF(本番だけ。呼び出し回数の制限)
▼
API(ECS Fargate・NAT なし)
├ トークン → 役割 → 投稿できるサイト(DynamoDB にはハッシュだけ)
├ 指示書は「共通+その役割」の分だけ返す
├ 401 を記録 → CloudWatch で数えて知らせる
└ WordPress のパスワードは Secrets Manager から API が付ける
▼
WordPress(REST API)
反映:GitHub Actions ──OIDC──> AWS(main で確認用、v タグで本番)
ALB(Application Load Balancer)は、インターネットからの HTTPS を受けて API に渡す入口です。DynamoDB はトークンの情報を置くデータベース、CloudWatch は記録と監視のサービスです。それぞれ、出てくる回で詳しく説明します。
各回の予定
| 回 | 内容 |
|---|---|
| 第1回(この記事) | なぜ役割ごとの API にしたか。プロジェクトを作り、権限の表を書く |
| 第2回 | AWS の4アカウントと、Google Workspace でのログイン |
| 第3回 | GitHub Actions から AWS へ入る(OIDC)とその落とし穴 |
| 第4回 | ECS・ALB を小さく始める(NAT なし)と、証明書・DNS の落とし穴 |
| 第5回 | Claude Code の環境とトークンで、役割ごとに見える情報を分ける |
| 第6回 | CLAUDE.md は入口だけにする(役割ごとの指示書を API で配る) |
| 第7回 | 既存のリポジトリからの移行と、WordPress への投稿の中継 |
| 第8回 | 守りと費用(401 の監視、WAF、確認用はふだん止める) |
プロジェクトを作る
ここからは手を動かします。Python 3.12 以上(執筆時点では 3.13 で確認)が入っていれば始められます。この回では、プロジェクトの土台と「役割ごとに投稿できるサイトの表」を作り、テストで確かめます。
フォルダと仮想環境を作る
mkdir ai-agent-kiban && cd ai-agent-kiban
python3 -m venv .venv
source .venv/bin/activate
mkdir -p app config tests
touch app/__init__.py
.venv は仮想環境(=このプロジェクト専用に Python の部品を入れる場所)です。以降のコマンドは、source .venv/bin/activate を実行したターミナルで行います。
pyproject.toml(使う部品とテストの設定)
使う部品(パッケージ)とテスト・検査の設定を書きます。バージョンは執筆時点(2026年10月)に PyPI で公開日を確かめたものを下限にしています。
# pyproject.toml
[project]
name = "ai-agent-kiban"
version = "0.1.0"
description = "Claude CodeとAWSで作るAIエージェントの土台(連載のサンプル):渡された文章を WordPress に投稿するだけの中継 API"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.162",
"uvicorn>=0.54",
"httpx>=0.28",
"boto3>=1.43",
]
[project.optional-dependencies]
dev = ["pytest>=9", "httpx2>=2", "moto>=5", "ruff>=0.17"]
# パッケージは app だけ(infra・config・instructions はパッケージにしない)
[tool.setuptools]
packages = ["app"]
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.ruff]
line-length = 120
[tool.ruff.lint]
select = ["E", "F", "W", "I"]
fastapi・uvicorn:API を書いて動かす部品httpx:API から WordPress を呼ぶ部品(第7回)boto3:AWS(DynamoDB・Secrets Manager)を呼ぶ部品pytest・httpx2:テスト。httpx2は FastAPI のテスト用クライアントが使う部品です(入れないと非推奨の警告が出ます)moto:AWS の偽物(テストで使う)、ruff:書き方の検査[tool.setuptools]のpackages = ["app"]:パッケージはappだけにします。書かないと、後の回で作るinfra・configもパッケージの候補に見えて、インストールが止まります
.gitignore と .env.example
git に入れないものと、環境変数の見本です。パスワードやトークンの本物の値は、どちらにも書きません。
# .gitignore
.env
.tokens.local.json
.venv/
__pycache__/
*.egg-info/
.pytest_cache/
.ruff_cache/
infra/cdk.out/
# .env.example
# 手元で動かすときの見本。本物の値はここに書かない(.env は git に入れない)
# 確認用(staging)か本番(prod)か。staging では下書きしか作れない
STAGE=staging
# 投稿先のサイトの一覧
SITES_FILE=config/sites.json
# トークンを置く DynamoDB の表の名前(手元では LOCAL_TOKENS_FILE を使う)
TOKENS_TABLE=
LOCAL_TOKENS_FILE=.tokens.local.json
# WordPress のユーザー名とアプリケーションパスワードを置く Secrets Manager の名前の頭(<頭>/wp/<サイト>)
SECRET_PREFIX=ai-agent-kiban/staging
AWS_REGION=ap-northeast-1
ここにある値は、後の回で順に使います。STAGE が staging(確認用)のときは、公開を頼まれても下書きにする作りにします(第7回)。
投稿先のサイトの一覧
投稿先のサイトを JSON で書きます。ここにもパスワードは書きません。
config/sites.json
{
"_comment": "投稿先のサイト。キーは API の URL に使う名前。秘密(パスワード)はここに書かない",
"sample": {"base_url": "https://blog.example.com/"}
}
blog.example.com は例のためのドメインです。手元で試すだけなら、このままで動きます。
権限の表を1か所に書く(app/roles.py)
この回の中心です。役割と、その役割が投稿できるサイトを、1つの辞書にまとめます。
# app/roles.py
"""役割と、その役割が投稿できるサイト。権限の決まりはこの1か所にまとめる。
トークンには役割だけを記録し、投稿できるサイトは常にここから引く。
(トークンを作り直さずに範囲を直せるように)
"""
ROLE_SITES = {
'blog-sample': {'sample'},
}
def can_post(role: str, site: str) -> bool:
return site in ROLE_SITES.get(role, set())
- 役割は
blog-sampleの1つだけです。当社の実際の社内基盤では、ここにブログ6つと営業の7つの役割が並んでいます - 表にない役割は、どのサイトにも投稿できません(
ROLE_SITES.get(role, set())が空になるため)。「書いていないことはできない」を既定にしておくと、役割を足し忘れたときに安全側に倒れます
設定を環境変数から読む(app/settings.py)
設定はすべて環境変数から読みます。後の回で使う項目も、ここで先に用意しておきます。
# app/settings.py
"""設定。すべて環境変数から読む(キーやパスワードはここに来ない)。"""
import os
from dataclasses import dataclass
@dataclass(frozen=True)
class Settings:
stage: str = os.environ.get('STAGE', 'staging')
sites_file: str = os.environ.get('SITES_FILE', 'config/sites.json')
instructions_dir: str = os.environ.get('INSTRUCTIONS_DIR', 'instructions')
tokens_table: str = os.environ.get('TOKENS_TABLE', '')
local_tokens_file: str = os.environ.get('LOCAL_TOKENS_FILE', '.tokens.local.json')
secret_prefix: str = os.environ.get('SECRET_PREFIX', 'ai-agent-kiban/staging')
region: str = os.environ.get('AWS_REGION', 'ap-northeast-1')
@property
def can_publish(self) -> bool:
"""本番だけ公開できる。確認用は下書きにしかしない(間違って公開しないため)"""
return self.stage == 'prod'
can_publish は「本番のときだけ公開できる」という決まりです。確認用の環境で、間違って記事を公開してしまわないようにします。
テストで権限の表を確かめる
権限の表は、書き間違えると「投稿できてはいけないサイトに投稿できる」事故につながります。テストで2つのことを確かめます。
# tests/test_roles.py
"""役割の表(app/roles.py):書いてあるサイトにだけ投稿でき、表のサイトは設定に実在すること。"""
import json
from pathlib import Path
from app.roles import ROLE_SITES, can_post
ROOT = Path(__file__).resolve().parents[1]
def test_role_can_post_only_to_listed_sites():
assert can_post('blog-sample', 'sample')
assert not can_post('blog-sample', 'other')
assert not can_post('unknown-role', 'sample') # 表にない役割は何もできない
def test_sites_in_roles_exist_in_config():
config = json.loads((ROOT / 'config' / 'sites.json').read_text(encoding='utf-8'))
sites = {k for k in config if not k.startswith('_')}
for role, allowed in ROLE_SITES.items():
assert allowed <= sites, f'{role} に設定に無いサイトがある: {allowed - sites}'
- 1つ目:表に書いたサイトにだけ投稿でき、表にない役割は何もできない
- 2つ目:表に書いたサイトが、
config/sites.jsonに実在する(サイト名の打ち間違いに気付ける)
動作確認
部品を入れて、検査とテストを実行します。
pip install -e ".[dev]"
ruff check .
pytest -q
次のように表示されれば完成です。
All checks passed!
.. [100%]
2 passed
試しに app/roles.py の 'sample' を 'sampel' に変えて pytest -q を実行すると、2つ目のテストが「設定に無いサイトがある」と失敗します。確かめたら元に戻してください。
つまずきやすい点・セキュリティ上の注意
pip install -e .が「Multiple top-level packages」で止まる:pyproject.tomlの[tool.setuptools]のpackages = ["app"]が抜けていないか確かめてください.envを git に入れない:見本は.env.exampleだけにし、本物の値は.env(.gitignore済み)か、後の回で使う Secrets Manager に置きます- 権限を「トークンに書き込む」形にしない:トークンに範囲まで書くと、範囲を直すたびに作り直しになり、古いトークンが広い範囲のまま残りがちです。役割だけを記録し、範囲はコードの1か所で決めます
発注者向けメモ
AI エージェントに仕事を任せる仕組みを開発会社に頼むときは、技術の話の前に、次の表を発注者側で作っておくと話が早くなります。
| 決めること | 例 |
|---|---|
| どんな役割があるか | 技術ブログの担当、旅行ブログの担当、営業 |
| 役割ごとに見てよい資料 | 技術ブログ:記事の指示書・台帳。営業の単価は見せない |
| 役割ごとにしてよい操作 | 下書きまで/公開まで/メール送信は不可 |
| 人が必ず行うこと | 本番への反映、トークンの発行、外部への送信の最終確認 |
開発会社には、次のように確認すると認識がそろいます。
- 「権限の決まりは、コードのどこに書かれますか。1か所にまとまっていますか」
- 「役割を足したとき、ほかの役割から見えないことをどうやって確かめますか(テストはありますか)」
- 「AI に渡すのはトークンだけですか。パスワードや APIキーが AI から見える場所はありませんか」
役割の数が増える、外部サービスへの中継が増える、人の承認の手順を入れる、といった条件があると、工数が増えやすくなります。
まとめと次回予告
- 1つのリポジトリで AI に仕事をさせると、見えすぎる・重い・止められない、という問題が出やすい
- 分けるのは「リポジトリ」ではなく「AI に渡すもの」。AI にはトークンだけを渡し、資料とパスワードは API 側に置く
- 権限の決まりは1か所(
app/roles.py)に書き、テストで確かめる
次回は、この API を置く AWS の側の準備として、AWS を4つのアカウントに分ける理由と、Google Workspace のアカウントで AWS にログインする方法を説明します。
この連載の記事一覧
この記事は連載「Claude CodeとAWSで作るAIエージェントの土台」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【Claude CodeとAWSで作るAIエージェントの土台 第1回】なぜリポジトリをやめて、役割ごとの API にしたのか(この記事)
- 【Claude CodeとAWSで作るAIエージェントの土台 第2回】AWS を4つのアカウントに分け、Google Workspace でログインする
- 【Claude CodeとAWSで作るAIエージェントの土台 第3回】GitHub Actions から AWS へ入る(OIDC)と、sub が番号付きになる落とし穴
- 【Claude CodeとAWSで作るAIエージェントの土台 第4回】ECS Fargate と ALB を NAT なしで小さく始める:証明書・DNS の落とし穴
- 【Claude CodeとAWSで作るAIエージェントの土台 第5回】トークンで役割を判定し、Claude Code の環境に持たせる(Network secrets)
- 【Claude CodeとAWSで作るAIエージェントの土台 第6回】CLAUDE.md は入口だけにする:役割ごとの指示書を API で配る
- 【Claude CodeとAWSで作るAIエージェントの土台 第7回】既存のリポジトリから移す:秘密の値の検査と、WordPress への投稿の中継
- 【Claude CodeとAWSで作るAIエージェントの土台 第8回】守りと費用:401 の監視、WAF の回数制限、確認用はふだん止める










コメント
コメント一覧 (6件)
[…] […]
[…] […]
[…] […]
[…] […]
[…] […]
[…] […]