どうもこんにちは。
最近、AIエージェント構築を色々と試している中で、「どうやったら人間がやっているタスクを正確無比にやってくれるエージェントを構築できるのだろうか」と考えている今日この頃です。
AIエージェント構築によく使うSDKは、PythonのStrands Agentsなんですが、リファレンスを読んでいると知らないツールが結構出てきたりしてます。(「new」の文字が結構早いペースで増えていっているような...)
そんな中で、長いこと気にはなっていたけど検証ができていなかった、workflowツールの検証をしてみようと思います。
題材は、何か新しいサービスを開発する時に調査を依頼したり、自分の頭の中に浮かんでいる構成を評価してもらうための「技術選定&調査エージェント」です。
たとえば、次のような入力を渡します。
{
"prompt": "新規B2B SaaSを、Next.js + FastAPI + Aurora PostgreSQL + ECS Fargate + Bedrockで作る案を評価して"
}
この時、想定している動作としては、以下の観点に分けてレビューし、最後に採用判断つきのMarkdownレポートを返す。ということを想定しています。
- 要件整理
- フロントエンド
- バックエンド
- インフラ
- セキュリティ
- コスト
- 運用
また、今回は、「マルチエージェント構成」が前提となるので、マルチエージェント構成でエージェントデプロイしてみたいねんって方は参考にしてみてくださいませ。
この記事は誰向け?
この記事は以下のような方向けに執筆しています。
- Strands Agents のマルチエージェント構成を試してみたい方
-
strands_tools.workflowが何をしてくれるのか知りたい方 - Amazon Bedrock AgentCore Runtime 上で Strands Agents を動かしたい方
- AWS CDK(Python)で AgentCore Runtime へエージェントをデプロイしたい方
デモコードの場所
実際に動かしたい方は、以下を参照ください。
この検証でやりたいこと
今回のゴールは次の3点です。
- Strands Agents の
workflowツールで、複数タスクの依存関係を定義する - 親エージェントからタスクごとの子エージェントを呼び出し、技術選定レビューを複数の専門タスクに分割して実行する
- 最終的にはレポートをレスポンスとして返す
構成としては以下のようになります。
上記のフローの中で、各タスク(「要件整理」以降)は子エージェントによって実行されるようになっています。
実装の流れ
0. 作業プロジェクトの用意
mkdir strands-agents-agent-workflows
cd strands-agents-agent-workflows
1. CDKプロジェクトの雛形を整える
1-1. 依存パッケージ
CDK側の依存関係は以下です。
aws-cdk-lib==2.255.0
constructs>=10.0.0,<11.0.0
cdk-ecr-deployment>=4.0.0,<5.0.0
開発用には pytest も入れます。
-r requirements.txt
pytest>=8.0.0,<9.0.0
cdk-ecr-deployment は、CDK がビルドした Docker image asset を、Runtime 用に作成した専用 ECR Repository へコピーするために使っています。
Runtime コンテナ側の依存関係は以下です。
bedrock-agentcore>=1.3.0,<2.0.0
strands-agents>=1.0.0,<2.0.0
strands-agents-tools>=0.2.0,<1.0.0
boto3>=1.42.0
今回の主役は strands-agents-tools に含まれる workflow ツールであるため、strands-agents-toolsのパッケージは必要となります。
1-2. CDKエントリーポイント
app.py は通常の CDK エントリーポイントです。
#!/usr/bin/env python3
import os
import aws_cdk as cdk
from infra.agentcore_tech_selection_stack import AgentCoreTechSelectionWorkflowStack
app = cdk.App()
AgentCoreTechSelectionWorkflowStack(
app,
"AgentCoreTechSelectionWorkflowStack",
env=cdk.Environment(
account=os.environ.get("CDK_DEFAULT_ACCOUNT"),
region=os.environ.get("CDK_DEFAULT_REGION", "ap-northeast-1"),
),
)
app.synth()
1-3. cdk.json
cdk.json は、CDK CLI が常にプロジェクト内の仮想環境を使うようにしています。
{
"app": ".venv/bin/python app.py",
"context": {
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws"
]
}
}
.venv/bin/python を固定しておくと、仮想環境を activate していない状態でも cdk synth や cdk deploy が同じ Python 環境を使ってくれます。
1-4. 仮想環境
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
AWS認証とリージョンを確認します。
aws sts get-caller-identity
export AWS_REGION=ap-northeast-1
必要であれば bootstrap します。
cdk bootstrap
2. workflow で技術選定レビューの流れを定義する
まず、今回の技術選定会議をタスクとして定義します。
実装は agent/tech_selection_agent/workflow_spec.py に置きました。
agent/
└── tech_selection_agent/
├── agent.py # エージェントを実装
├── runtime_app.py # エントリーポイントを実装
└── workflow_spec.py # ワークフロー内のタスクなどを定義
2-1. タスクの全体像
今回の workflow は次の形です。
最初に requirements_reader が評価対象の技術選定案を整理します。
その結果を受けて、6つのレビュータスク(サブエージェント)が並列に動きます。
frontend_reviewerbackend_reviewerinfra_reviewersecurity_reviewercost_revieweroperability_reviewer
最後に、decision_maker がレビュー結果を統合して、final_report がMarkdownレポートを作成します。
2-2. タスク(サブエージェント)定義
workflow ツールに渡すタスクは、Python の辞書として定義します。
def build_tasks(prompt: str) -> list[dict[str, Any]]:
proposal = prompt.strip()
base_context = (
"評価対象の技術選定案:\n"
f"{proposal}\n\n"
"日本語で、実務上の判断に使える具体性で回答してください。"
)
return [
# 要件を整理するタスク(サブエージェント)
{
"task_id": "requirements_reader",
"description": (
f"{base_context}\n\n"
"この案の目的、想定プロダクト、暗黙の前提、未確定事項、"
"評価軸を整理してください。後続レビューが参照しやすい箇条書きにしてください。"
),
"system_prompt": "あなたは要件定義と技術評価のファシリテーターです。",
"tools": NO_TASK_TOOLS,
"priority": 5,
},
# 要件整理後、フロントエンドの観点で評価を行うタスク(サブエージェント)
{
"task_id": "frontend_reviewer",
"description": (
f"{base_context}\n\n"
"フロントエンド観点で評価してください。UI開発生産性、型安全性、"
"認証連携、SSR/CSR、パフォーマンス、テスト、チーム運用を扱ってください。"
),
# ここで「何のタスクの後に実行するのか」を定義する
"dependencies": ["requirements_reader"],
"system_prompt": "あなたはB2B SaaSのフロントエンドアーキテクトです。",
"tools": NO_TASK_TOOLS,
"priority": 4,
},
]
dependencies に依存タスクを指定すると、そのタスクが完了するまで実行されません。
この例だと、frontend_reviewer は requirements_reader が終わってから実行されます。
2-3. タスク(サブエージェント)には tools を明示しておく
今回、少し気をつけたのが tools の指定です。
strands_tools.workflow は、タスクに tools を指定しない場合、親Agentが持っているツールを子Agentへ継承します。
今回の親Agentは workflow ツールを持っています。つまり、何もしないと子Agentも workflow を持つことになります。
これが必ず悪いわけではないですが、子エージェントが新たにワークフローツールを実行してしまうと、無駄なループ処理が発生してしまうため、避けた方が良いかなと思い、以下のように存在しないツール名を指定して子Agentにはツールを渡さないようにしました。
# 最初に`agent/tech_selection_agent/workflow_spec.py`に定数として定義しておく
NO_TASK_TOOLS = ["__no_task_tools__"]
# タスク(サブエージェント)の引数に`NO_TASK_TOOLS`を渡しておく
"tools": NO_TASK_TOOLS
これで、各タスクはツールなしの専門レビューAgentとして動作します。
3. 親Agentから workflow を実行する
次に、workflow ツールを持った親Agentを作成します。
実装は agent/tech_selection_agent/agent.py です。
3-1. workflow import 前に保存先を決める
strands_tools.workflow は、workflow の状態をJSONファイルとして保存します。
デフォルトでは ~/.strands/workflows ですが、AgentCore Runtime のコンテナ上では /tmp 配下に寄せておきます。
これは好みです!
# JSONファイルの保存先を指定
DEFAULT_WORKFLOW_DIR = "/tmp/strands-workflows"
os.environ.setdefault("STRANDS_WORKFLOW_DIR", DEFAULT_WORKFLOW_DIR)
from strands_tools import workflow
workflow は import 時に STRANDS_WORKFLOW_DIR を読むため、from strands_tools import workflow より前に環境変数を設定しています。
3-2. 親Agentを作る
def create_agent() -> Agent:
region = get_region()
model = BedrockModel(
model_id=get_model_id(),
boto_session=boto3.Session(region_name=region),
)
return Agent(
model=model,
tools=[workflow],
system_prompt=SYSTEM_PROMPT,
callback_handler=None,
)
この親Agentは、技術選定レビュー本文を直接書く係ではありません。
役割は workflow ツールを呼び出すことです。
各レビュー担当のAgentは、workflow ツール内部でタスクごとに生成されます。
3-3. create と start
workflow ツールには、アクションという引数が存在します。
| 値 | 説明 |
|---|---|
create |
タスク定義を保存する |
start |
タスク定義されたワークフローを実行する |
まず create でタスク定義を保存し、次に start で実行します。
def execute_tech_selection_workflow(prompt: str, workflow_id: str) -> dict[str, Any]:
# 3-2 で作成した親エージェントを引数に格納する
agent = create_agent()
# 2-2 で定義したタスクを引数に格納する
tasks = build_tasks(prompt)
# タスク定義を保存する
create_result = agent.tool.workflow(
action="create", # ここで`create`が指定されているので、「タスク定義が保存されている」と理解できる
workflow_id=workflow_id,
tasks=tasks,
)
ensure_tool_success(create_result, "create")
# ここで`start`が指定されているので、「保存したタスクが実行される」と理解できる
start_result = agent.tool.workflow(action="start", workflow_id=workflow_id)
ensure_tool_success(start_result, "start")
create は状態ファイルを作るだけで、まだLLM実行は始まりません。
実際に各タスクが動くのは start です。start は同期実行なので、final_report まで終わるまで戻ってきません。
3-4. final_report を状態ファイルから取り出す
workflow start の戻り値は、「workflow が完了しました」というメッセージが中心です。
今回欲しいのは、最後の final_report タスクが生成したMarkdown本文です。そのため、workflow が保存したJSONを読みます。
workflow_data = load_workflow_data(workflow_id, get_workflow_dir())
final_report = extract_task_text(workflow_data, "final_report")
if not final_report:
final_report = extract_content_text(start_result)
状態ファイルは次の場所に保存されます。
/tmp/strands-workflows/<workflow_id>.json
この中の task_results.final_report.result から text block だけを抜き出して、最終レスポンスにしています。
親エージェントが最終レポートを読んでユーザにレスポンスを返す ということはしていません。最終レポートの内容をそのまま出力するようにしています。
4. AgentCore Runtime の entrypoint を作る
AgentCore Runtime から呼ばれる entrypoint は agent/tech_selection_agent/runtime_app.py に置きます。
from bedrock_agentcore.runtime import BedrockAgentCoreApp, PingStatus
from .agent import execute_tech_selection_workflow
app = BedrockAgentCoreApp()
入力は {"prompt": "..."} に寄せています。
def invoke_agent(payload: dict) -> dict:
prompt = payload.get("prompt")
if not isinstance(prompt, str) or not prompt.strip():
raise ValueError("payload.prompt must be a non-empty string")
request_id = str(uuid.uuid4())
workflow_id = f"tech-selection-{request_id}"
result = execute_tech_selection_workflow(
prompt=prompt.strip(),
workflow_id=workflow_id,
)
return {
"response": result["response"],
"evidence": {
"requestId": request_id,
"workflow": {
"workflowId": workflow_id,
"status": result["workflow_status"],
"taskCount": result["task_count"],
},
},
}
workflow_id は状態ファイル名にもなるため、リクエストごとに uuid を付けて衝突しないようにしています。
AgentCore Runtime の entrypoint と health check は以下です。
@app.entrypoint
def invoke(payload: dict) -> dict:
return invoke_agent(payload)
@app.ping
def ping() -> PingStatus:
return PingStatus.HEALTHY
レスポンスには、最終レポート本文と、どの workflow が動いたかを確認するための evidence を含めています。
応答イメージは以下のようになります。
{
"response": "# 技術選定レビュー\n\n## 結論\n条件付き採用...",
"evidence": {
"requestId": "5f3d...",
"workflow": {
"workflowId": "tech-selection-5f3d...",
"status": "completed",
"taskCount": 9
}
}
}
5. Runtime コンテナを作る
AgentCore Runtime へ載せるため、エージェントを Docker 化します。
FROM public.ecr.aws/docker/library/python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PORT=8080
ENV STRANDS_WORKFLOW_DIR=/tmp/strands-workflows
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY tech_selection_agent ./tech_selection_agent
EXPOSE 8080
CMD ["python", "-m", "tech_selection_agent.runtime_app"]
ここでも STRANDS_WORKFLOW_DIR=/tmp/strands-workflows を指定しています。
agent.py 側でもデフォルト値を設定していますが、Runtime コンテナとしては Dockerfile と CDK の環境変数でも明示しておくと挙動を追いやすいのかなと思っています。(差分が生まれないように注意が必要かなとは思います。)
6. CDKでAgentCore Runtimeを作る
CDKスタックは infra/agentcore_tech_selection_stack.py に実装しました。
今回作るリソースは主に以下です。
- ECR Repository
- DockerImageAsset
- ECRDeployment
- AgentCore Runtime
- Runtime 用 IAM Role
6-1. 専用ECR Repository
image_repository = ecr.Repository(
self,
"AgentImageRepository",
repository_name="agentcore-tech-selection-workflow-demo",
image_scan_on_push=True,
image_tag_mutability=ecr.TagMutability.IMMUTABLE,
lifecycle_rules=[
ecr.LifecycleRule(
description="Keep the ten most recent agent images",
max_image_count=10,
)
],
removal_policy=RemovalPolicy.DESTROY,
empty_on_delete=True,
)
6-2. Docker image asset を専用ECRへコピーする
CDK の Docker image asset は CDK 管理の asset repository にpushされます。
今回は AgentCore Runtime 用に専用ECR Repositoryを作っているため、cdk-ecr-deployment でコピーします。
image_asset = ecr_assets.DockerImageAsset(
self,
"AgentImageAsset",
directory=str(Path(__file__).resolve().parents[1] / "agent"),
platform=ecr_assets.Platform.LINUX_ARM64,
)
image_tag = image_asset.asset_hash
runtime_image_uri = image_repository.repository_uri_for_tag(image_tag)
image_deployment = ecr_deployment.ECRDeployment(
self,
"DeployAgentImage",
src=ecr_deployment.DockerImageName(image_asset.image_uri),
dest=ecr_deployment.DockerImageName(runtime_image_uri),
)
AgentCore Runtime のコンテナは ARM64 でビルドしています。
6-3. Runtime 用 IAM Role
Runtime 用ロールには、以下の権限を付与します。
- ECR pull
- ECR authorization token
- CloudWatch Logs
- Bedrock model invoke
runtime_role.add_to_policy(
iam.PolicyStatement(
actions=[
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
],
resources=self._model_resource_arns(model_id),
)
)
model_id が ARN で渡された場合はその ARN を使い、それ以外は foundation model と inference profile の ARN を許可する形にしています。
def _model_resource_arns(self, model_id: str) -> list[str]:
if model_id.startswith("arn:"):
return [model_id]
return [
f"arn:{Aws.PARTITION}:bedrock:*::foundation-model/*",
(
f"arn:{Aws.PARTITION}:bedrock:{Aws.REGION}:{Aws.ACCOUNT_ID}:"
"inference-profile/*"
),
]
6-4. AgentCore Runtime
Runtime は AWS::BedrockAgentCore::Runtime を作成します。
runtime = bedrock_agentcore.CfnRuntime(
self,
"AgentRuntime",
agent_runtime_name="AgentCoreTechSelectionWorkflowRuntime",
description="Strands workflow agent that reviews technology selection proposals.",
agent_runtime_artifact=bedrock_agentcore.CfnRuntime.AgentRuntimeArtifactProperty(
container_configuration=bedrock_agentcore.CfnRuntime.ContainerConfigurationProperty(
# ECRにプッシュした、DockerイメージのURI
container_uri=runtime_image_uri,
)
),
network_configuration=bedrock_agentcore.CfnRuntime.NetworkConfigurationProperty(
network_mode="PUBLIC",
),
# エージェントを構築する時は、`HTTP`を指定しておけばOKです。MCPサーバを構築する時は、`MCP`を指定します。
protocol_configuration="HTTP",
role_arn=runtime_role.role_arn,
environment_variables={
"AWS_REGION": self.region,
"BEDROCK_MODEL_ID": model_id,
"STRANDS_WORKFLOW_DIR": "/tmp/strands-workflows",
},
lifecycle_configuration=bedrock_agentcore.CfnRuntime.LifecycleConfigurationProperty(
idle_runtime_session_timeout=900,
max_lifetime=3600,
),
)
ここで STRANDS_WORKFLOW_DIR を Runtime 環境変数として渡しています。
この検証では workflow の状態を /tmp に保存しています。長期的に保存されるものではないので、長期保存や再実行管理が必要な場合は、最低限DynamoDBが必要になるのかなと思っています。
(DynamoDBでworkflow_idをキーとして、タスク定義を保存しておく想像をしています。)
7. テストを書く
今回も最低限のユニットテストを書いています。
tests/
├── test_agentcore_tech_selection_stack.py
├── test_runtime_app.py
└── test_workflow_spec.py
test_workflow_spec.py では、タスクの依存関係が想定どおりかを確認しています。
def test_build_tasks_creates_expected_dependency_shape() -> None:
tasks = build_tasks("Next.js + FastAPI + Aurora PostgreSQL を評価して")
task_by_id = {task["task_id"]: task for task in tasks}
assert task_by_id["frontend_reviewer"]["dependencies"] == ["requirements_reader"]
assert set(task_by_id["decision_maker"]["dependencies"]) == {
"frontend_reviewer",
"backend_reviewer",
"infra_reviewer",
"security_reviewer",
"cost_reviewer",
"operability_reviewer",
}
assert task_by_id["final_report"]["dependencies"] == ["decision_maker"]
test_runtime_app.py では、AgentCore Runtime の entrypoint が期待したレスポンス形式を返すか確認しています。
CDK のテストでは、Runtime、ECR Repository、IAM Policy、Output が作られることを確認しています。
手元では以下でテストしました。
env JSII_RUNTIME_PACKAGE_CACHE_ROOT=/tmp/jsii-package-cache \
JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION=1 \
.venv/bin/pytest
結果は以下です。
8 passed
8. デプロイして確認する
デプロイは通常どおりです。
pytest
cdk synth
cdk deploy
別の Bedrock モデル ID を使う場合は、CDK context で指定できます。
cdk deploy -c modelId=your-model-or-inference-profile-id
デプロイできたら、AWSマネジメントコンソールにアクセスして、Amazon Bedrock AgentCore Runtimeのコンソールへ遷移します。
そうすると、デプロイしたランタイムが表示されているかと思いますので、クリックして、画面右上の「テスト」をクリックします。
そうしたら、「入力」のエディタにJSON形式で入力文を入れます。
今回はサンプルで以下のように入力してみます。
{
"prompt": "新規B2B SaaSを、Next.js + FastAPI + Aurora PostgreSQL + ECS Fargate + Bedrockで作る案を評価して"
}
入力したら、「実行」をクリックしましょう。
複数のサブエージェントによるタスクを実行しているので、レスポンスにはそこそこ時間がかかります。
焦るなよぉ... 焦るなよぉ...
処理が完了すると、出力のresponseに技術選定レポートが入っていることが確認できるかと思います。evidence.workflow に workflow の実行情報が入ります。
{
"response": "# 技術選定最終レポート\n\n## Next.js + FastAPI + Aurora PostgreSQL + ECS Fargate + Bedrock\n\n**作成:シニアソフトウェアアーキテクト**\n**対象読者:経営・開発リード**\n**ステータス:最終判定済み/実行可能**\n\n---\n\n## 結論\n\n### ✅ 条件付き採用\n\n```\nこの技術スタックを採用する。\nただし、以下に示す7つの採用条件を「開発着手前」に全て解決すること。\n\n条件未達のまま走り出した場合、フェーズ2以降で\n高コストな手戻りまたはセキュリティインシデントが発生する。\n```\n\n**なぜこのスタックが正しいか、一言で:**\n\n> FastAPI・Next.js・PostgreSQLはOpenAPIを軸に型連携が成立し、BedrockはAWSエコシステムに閉じてテナントデータのセキュリティ説明責任を果たせる。ECS Fargateはスタートアップ期〜成長期に必要な「管理負荷とスケーラビリティのバランス」において最適な中間点だ。テナント5社→500社への成長パスが確立された構成であり、エンジニア採用市場との親和性も高い。\n\n---\n\n## 総合評価スコア\n\n| 評価軸 | スコア | 判定根拠 |\n|--------|--------|----------|\n| 技術適合性 | ★★★★☆ | スタック間の相性は良好。OpenAPI連携が特に強み |\n| セキュリティ | ★★★☆☆ | 構成要素は正しいが、実装設計がまだ存在しない |\n| スケーラビリティ | ★★★★☆ | Aurora・ECSともに段階的拡張パスが明確 |\n| コスト妥当性 | ★★★☆☆ | 初期フェーズは構成変更なしだと割高。要見直し |\n| 運用成熟度 | ★★★☆☆ | 5レイヤー分散構成。AWS経験者が不在ならNo-Go |\n| 採用市場親和性 | ★★★★☆ | 3技術とも求人・応募市場が活発 |\n| **総合** | **★★★☆☆** | **条件付きGo。条件充足で★★★★に上がる** |\n\n---\n\n## 採用条件\n\n**以下の7条件は、開発着手の前提とする。未解決のまま進めた場合の想定損害を各条件に併記する。**\n\n---\n\n### 条件1|マルチテナント分離方式の確定\n\n**解決期限:設計フェーズ完了前**\n**未解決時の損害:全テーブル・全リポジトリ層の大規模改修、最悪ケースでテナント間データ漏洩**\n\n採用方式は **「RLS(Row Level Security)+アプリ層二重防御」** とする。\n\n```sql\n-- 全テーブルに必ずFORCEオプション付きで適用する\n-- FORCE なしでは superuser 接続時に RLS がバイパスされる\nALTER TABLE orders ENABLE ROW LEVEL SECURITY;\nALTER TABLE orders FORCE ROW LEVEL SECURITY;\n\nCREATE POLICY tenant_isolation ON orders\n USING (tenant_id = current_setting('app.current_tenant_id')::uuid);\n```\n\n```python\n# FastAPI 側:全エンドポイントにテナントコンテキストを強制注入\n# Depends で強制することで「うっかり忘れ」を構造的に防ぐ\nasync def get_tenant_context(\n credentials: HTTPAuthorizationCredentials = Security(security),\n db: AsyncSession = Depends(get_db)\n) -> TenantContext:\n payload = verify_jwt(credentials.credentials)\n tenant_id = payload.get(\"custom:tenant_id\")\n if not tenant_id:\n raise HTTPException(status_code=403, detail=\"テナントID未設定\")\n await db.execute(\n text(\"SET LOCAL app.current_tenant_id = :tid\"), {\"tid\": tenant_id}\n )\n return TenantContext(tenant_id=UUID(tenant_id))\n```\n\n**やってはいけないこと:**\n- テナントごとに別Auroraクラスターを立てる(コストがN倍になり初期フェーズで事業が成立しない)\n- RLSなしでアプリ層のみに依存する(実装ミス1箇所で全テナントデータが漏洩する)\n\n---\n\n### 条件2|OpenAPI→TypeScript型自動生成パイプラインのCI組み込み\n\n**解決期限:Sprint 1 Day 1**\n**未解決時の損害:バックエンド型変更がフロントに伝わらず、本番での`undefined`エラーが慢性化。中規模プロジェクトで2〜3日の修正コストが複数回発生**\n\n```bash\n# CI/CDに組み込む型生成パイプライン(GitHub Actions等)\n\n# Step 1: FastAPIからOpenAPIスキーマを自動エクスポート\npython -c \"\nimport json\nfrom app.main import app\nwith open('openapi.json', 'w') as f:\n json.dump(app.openapi(), f, indent=2)\n\"\n\n# Step 2: TypeScript型を自動生成\nnpx openapi-typescript openapi.json \\\n -o frontend/src/types/api.generated.ts\n\n# Step 3: 型変更をPRで検知して警告を出す\ngit diff --exit-code frontend/src/types/api.generated.ts || \\\n echo \"::warning::API型定義に変更があります。フロントエンド側の影響確認を行ってください\"\n```\n\n**重要:** このパイプラインは後から追加すると既存の型不整合を全件修正する必要が生じる。Day 1に組み込まないと技術的負債が確定する。\n\n---\n\n### 条件3|認証基盤の選定とテナントID付与フローの確定\n\n**解決期限:設計フェーズ完了前**\n**未解決時の損害:認証基盤を変更すると全APIエンドポイントのDependsを書き直すことになる**\n\n採用基盤は **AWS Cognito + NextAuth.js v5(Auth.js)** とする。\n\n```\n選定理由:\n - AWSエコシステム内でセキュリティ境界が閉じる\n - B2B企業顧客へのデータレジデンシー説明が容易\n - IAM・VPCとのネイティブ統合が可能\n\nテナントID付与フロー:\n Cognito Pre Token Generation Lambda Trigger\n → custom:tenant_id をJWTに付与\n → FastAPI Depends で検証\n → 全DBクエリに tenant_id フィルタを強制適用\n```\n\n```python\n# FastAPI JWT検証(Cognito公開鍵で署名検証)\nasync def verify_cognito_token(\n token: str,\n jwks_client: PyJWKClient = Depends(get_jwks_client)\n) -> dict:\n signing_key = jwks_client.get_signing_key_from_jwt(token)\n return jwt.decode(\n token,\n signing_key.key,\n algorithms=[\"RS256\"],\n audience=settings.COGNITO_CLIENT_ID,\n options={\"verify_exp\": True}\n )\n```\n\n---\n\n### 条件4|Bedrockの非同期処理設計とフォールバック実装\n\n**解決期限:Sprint 1完了前**\n**未解決時の損害:boto3のイベントループブロッキングにより全APIの応答が劣化。Bedrock障害でサービス全体が停止**\n\n```python\n# boto3はネイティブ非同期非対応のため、run_in_executorで回避する\n_executor = ThreadPoolExecutor(max_workers=10)\n\nasync def invoke_bedrock(self, prompt: str) -> str:\n loop = asyncio.get_event_loop()\n return await loop.run_in_executor(\n _executor,\n lambda: self._sync_invoke(prompt)\n )\n\n# 30秒超えが見込まれる処理はキューに分離する\n# FastAPI → 202 Accepted → SQS → Worker ECS Task → Bedrock\n\n# フォールバック実装は必ずリリース前に組み込む\nasync def invoke_with_fallback(self, prompt: str) -> AIResult:\n try:\n return await self.invoke_bedrock(prompt)\n except ThrottlingException:\n await self._enqueue_for_retry(prompt)\n return DegradedResult(\n message=\"AI処理を予約しました。完了後にメールで通知します\",\n status=\"queued\"\n )\n except (ServiceException, ModelNotReadyException):\n return DegradedResult(\n message=\"AI機能は一時的に利用できません。しばらく後にお試しください\",\n status=\"unavailable\"\n )\n```\n\n---\n\n### 条件5|IAMロールの最小権限設計(TaskRole/ExecutionRole分離)\n\n**解決期限:インフラ構築開始前**\n**未解決時の損害:コンテナ侵害時に全AWSリソースへのアクセスが可能になり、顧客データ全漏洩に直結**\n\n```json\n// FastAPI ECS TaskRole:アプリが実際に使うリソースのみ\n{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Sid\": \"BedrockInvoke\",\n \"Effect\": \"Allow\",\n \"Action\": [\n \"bedrock:InvokeModel\",\n \"bedrock:InvokeModelWithResponseStream\"\n ],\n \"Resource\": \"arn:aws:bedrock:ap-northeast-1::foundation-model/anthropic.claude-3-5-sonnet-*\"\n },\n {\n \"Sid\": \"SecretsManagerRead\",\n \"Effect\": \"Allow\",\n \"Action\": [\"secretsmanager:GetSecretValue\"],\n \"Resource\": \"arn:aws:secretsmanager:ap-northeast-1:ACCOUNT_ID:secret:prod/fastapi/*\"\n },\n {\n \"Sid\": \"SQSSend\",\n \"Effect\": \"Allow\",\n \"Action\": [\"sqs:SendMessage\", \"sqs:ReceiveMessage\", \"sqs:DeleteMessage\"],\n \"Resource\": \"arn:aws:sqs:ap-northeast-1:ACCOUNT_ID:bedrock-job-queue\"\n }\n ]\n}\n```\n\n```\n分離の原則:\n ECSExecutionRole → コンテナ起動に必要(ECRからのイメージPull、CloudWatch Logsへの書き込み)\n ECSTaskRole → アプリ実行時に必要(Bedrock、SecretsManager、SQS等)\n\n絶対禁止:AdministratorAccess / PowerUserAccess の付与\n```\n\n---\n\n### 条件6|Auroraバックアップ設定とRDS Proxyの初期組み込み\n\n**解決期限:インフラ構築時(作成後変更不可の設定あり)**\n**未解決時の損害:バックアップ不備でデータ復旧不能。RDS Proxy後付けは接続断を伴う**\n\n```\nAurora作成時に確定する設定(後から変更できない項目に★):\n ★ ストレージ暗号化:有効(作成後変更不可)\n ★ マルチAZ:有効(WriterのAZ障害でもフェイルオーバー可能)\n 自動バックアップ保持期間:7日以上(デフォルト1日では本番不可)\n 拡張モニタリング:有効(60秒間隔)\n Performance Insights:有効(7日保持)\n\nSQLAlchemy接続設定(フェイルオーバー対策):\n pool_pre_ping=True # 切断済み接続の自動検知\n pool_recycle=1800 # 30分でコネクション強制再作成\n pool_size=5 # Fargateタスク1台あたりの接続数\n max_overflow=10 # バースト時の最大追加接続数\n\nRDS Proxyの初期組み込み:\n Fargateタスクが10台を超える見込みがあれば初期から必須\n └ 後付けは接続断が発生するため、初期設計に含めること\n```\n\n---\n\n### 条件7|初期フェーズのコスト構成の経営合意\n\n**解決期限:設計フェーズ完了前**\n**未解決時の損害:テナント5社時点でインフラ固定費が$63〜$130/社/月に達し、ARR $1,000〜$2,000/社ティアでは採算が取れない**\n\n```\n現状構成の月次固定費試算:$400〜$700/月\n\n推奨する初期フェーズの構成変更:\n\n[変更1] Aurora → RDS PostgreSQL db.t4g.medium(MultiAZ)\n 削減額:$40〜$60/月\n Aurora移行の判断基準(KPIとして事前に定義する):\n 「DBのCPU使用率が2週間継続して60%超」\n 「月間I/Oが3億回超」\n 「テナント50社到達」\n のいずれかを満たした時点でAurora Serverless v2へ移行\n\n[変更2] Next.js on ECS → Next.js on Vercel Pro(初期)\n 削減額:Fargateタスク分$76/月 → Vercel $20/月(差額$56/月)\n 注意:Vercel固有機能(Edge Functions等)への依存を最小化し、\n ECS移行への逃げ道を残しておく\n\n[変更3] Bedrockにテナント別月次トークン上限を実装\n 方式:DynamoDB または Redis で月次使用量を記録し上限で429を返す\n 目的:コスト爆発防止 兼 DDoS的な使用からの保護\n\n変更後の想定月額:$150〜$220/月(現行比 最大$480削減)\n```\n\n---\n\n## 主要リスク\n\n### リスクマトリクス\n\n| リスク | 発生確率 | 影響度 | 対処タイミング |\n|--------|----------|--------|----------------|\n| テナント間データ漏洩(RLS未実装) | 中 | 致命的 | 設計フェーズ |\n| 型断絶による本番サイレントエラー | 高 | 大 | Sprint 1 Day 1 |\n| Bedrockブロッキングによる全API劣化 | 高 | 大 | Sprint 1以内 |\n| IAM過剰権限によるデータ全漏洩 | 中 | 致命的 | インフラ構築前 |\n| 初期コスト過重による採算悪化 | 高 | 中 | 設計フェーズ |\n| チームのAWS経験不足 | 環境依存 | 大 | 採用判定前 |\n| Bedrock障害によるサービス全停止 | 低〜中 | 大 | Sprint 1以内 |\n\n### 特に注意が必要なリスク2点\n\n**リスクA:チームにAWS経験者が不在の場合**\n\n```\n判定:AWS経験者(ECS・VPC・IAMの実務経験)が1名もいない場合は\n この判定を「再検討」に引き下げる。\n\n根拠:本構成は5レイヤー分散構成であり、VPC設計ミス・\n IAMポリシー誤設定・ECSネットワーキングのトラブルは\n 経験がないと解決に数日〜数週間を要する。\n\n対処策:\n ① AWS経験者を1名採用またはアサインしてから着手する\n ② AWSパートナー企業と初期3ヶ月の支援契約を結ぶ\n ③ 代替案Aまたは代替案B(軽量版)から始める\n```\n\n**リスクB:Aurora/RDSのストレージ暗号化は作成後変更不可**\n\n```\n本番稼働後に「暗号化を有効にしたい」となった場合、\nスナップショットから新クラスターを作成し直す必要がある。\nその際にダウンタイムが発生する。\n\n必ずAurora/RDS作成時に暗号化有効を確認すること。\n```\n\n---\n\n## 代替案\n\n### 代替案A:Next.js + Supabase + Vercel(プロトタイプ〜10社規模)\n\n```\n対象:PMF未確定・エンジニア1〜2名・検証速度最優先の場合\n\n構成:\n フロント:Next.js on Vercel\n バックエンド:Supabase(PostgreSQL + Edge Functions)\n AI:OpenAI API(Bedrock代替)\n\n月額コスト:$50以下\n\nメリット:\n ✅ インフラ管理ほぼゼロ\n ✅ SupabaseがRLSをネイティブサポート(テナント分離が楽)\n ✅ 開発速度が最速\n ✅ 初期固定費が最小\n\nデメリット:\n ❌ Supabaseへのベンダーロック\n ❌ AWSエコシステムとの統合が複雑\n ❌ OpenAI APIを使うためデータレジデンシーの顧客説明が困難\n ❌ 大規模化時の移行コストが高い\n\n採用推奨状況:\n 「まず3ヶ月で10社に売れるか検証したい」という判断の場合\n```\n\n### 代替案B:現構成の軽量版(推奨採用案)\n\n```\n今回の推奨案から Aurora → RDS、ECS Next.js → Vercel に変更した構成。\nコードベースの変更はほぼなく、将来のAurora/ECS移行パスも明確。\n\n月額:$150〜$220(現状比最大$480削減)\n移行トリガーKPIを事前に定義した上での採用を推奨する。\n```\n\n### 代替案C:FastAPI → Go(高負荷Bedrock並行呼び出し専用)\n\n```\n対象:Bedrockへの大量並行呼び出しがコア機能である場合のみ検討\n\n現時点での結論:採用しない\n理由:\n - FastAPI + run_in_executor で現段階は十分対処可能\n - PydanticによるOpenAPI自動生成・型安全性を失う\n - PythonエコシステムのML/AIライブラリが使えなくなる\n - ベンチマーク結果が出てから改めて検討すれば十分\n```\n\n---\n\n## 次の検証タスク\n\n**以下を優先順位順に実行する。各タスクに「完了基準」を明記する。**\n\n### Week 1:設計フェーズ(全員)\n\n```\n優先度:最高\n\n□ マルチテナント分離方式の確定とER図レビュー\n 完了基準:RLSポリシーのSQL草案とFastAPI Dependsの設計が\n レビュー済みでドキュメント化されていること\n\n□ Cognito User PoolのPoC構築(テナントID付与フロー検証)\n 完了基準:custom:tenant_id が JWTに入り、\n FastAPIで正しく検証できることを動作確認済みであること\n\n□ Bedrockモデル選定と東京リージョン動作確認\n 完了基準:Claude 3.5 Sonnet が ap-northeast-1 で\n boto3から呼び出せることを確認済みであること\n\n□ コスト構成の経営合意\n 完了基準:「RDS採用+Vercel採用」または「Aurora採用の場合のARR設定」\n について経営判断が記録されていること\n\n□ AWSチーム経験レベルの確認\n 完了基準:ECS・VPC・IAM実務経験者の有無を確認し、\n 不在の場合は補完策(採用/外部支援)が決定していること\n```\n\n### Week 2〜3:基盤構築フェーズ(インフラ担当)\n\n```\n優先度:高\n\n□ VPC / セキュリティグループ / VPCエンドポイント構築\n 完了基準:Terraform管理下でコードレビュー済みであること\n (VPCエンドポイント:S3・Bedrock・SecretsManager・ECR)\n\n□ RDS PostgreSQL + RDS Proxy 構築\n 完了基準:暗号化有効・MultiAZ有効・バックアップ7日保持を\n AWSコンソールで確認済みであること\n\n□ ECSクラスター + ALB 構築\n 完了基準:TaskRole/ExecutionRoleが分離されており、\n IAMポリシーがレビュー済みであること\n\n□ OpenAPI→TypeScript型生成パイプラインのCI組み込み\n 完了基準:PRを出すと型変更が自動検知されることを\n テストPRで動作確認済みであること\n```\n\n### Week 4:アプリ結合フェーズ(全員)\n\n```\n優先度:高\n\n□ FastAPI → RDS疎通確認(マイグレーション含む)\n 完了基準:Alembicマイグレーションが適用され、\n RLSポリシーが全テーブルに設定されていること\n\n□ FastAPI → Bedrock疎通確認(フォールバック込み)\n 完了基準:Bedrockが応答しない状態を意図的に作り、\n DegradedResultが返ることを確認済みであること\n\n□ Next.js → FastAPI疎通確認\n 完了基準:自動生成された型を使ったAPIコールが\n 型エラーなく動作することを確認済みであること\n\n□ CloudWatchアラート設定(最低7項目)\n 完了基準:以下7項目が設定され、テストアラートが発報されること\n ① ECSタスクCPU 80%超\n ② ECSタスク起動失敗\n ③ RDS接続数 80%超\n ④ RDSレプリカラグ 30秒超\n ⑤ ALB 5xxエラー率 1%超\n ⑥ Bedrock ThrottlingException 急増\n ⑦ 月次コスト予算超過アラート\n```\n\n### Sprint 1完了前:Go/No-Goゲート(全員)\n\n```\n以下4項目が全て✅でなければ本番リリースをしない。\n\n□ テナント横断アクセステストが自動化されている\n 確認方法:テナントAのJWTでテナントBのリソースにアクセスして\n 403/404が返ることがCIで自動テストされていること\n\n□ Bedrockフォールバック実装が動作している\n 確認方法:Bedrockをモックして障害状態にした際に\n DegradedResultが返ることをテストで確認済みであること\n\n□ IAMロールが最小権限でレビュー済み\n 確認方法:TaskRole/ExecutionRoleの分離と\n 全ActionのResourceが具体的ARNで指定されていること\n\n□ RDS/Auroraバックアップが7日保持で動作している\n 確認方法:AWSコンソールでバックアップ保持期間と\n 直近バックアップの存在を確認済みであること\n```\n\n---\n\n## 判定サマリー\n\n```\n┌─────────────────────────────────────────────┐\n│ │\n│ 判定:✅ 条件付き採用 │\n│ │\n│ 採用の前提:7条件を設計フェーズで全て解決 │\n│ 推奨構成:代替案B(軽量版)を初期採用 │\n│ AWS経験者:最低1名の確保が必須 │\n│ Go/No-Goゲート:Sprint 1完了時に再評価 │\n│ │\n│ 条件未達の場合:代替案A(Supabase)で │\n│ PMF検証フェーズを先行させること │\n│ │\n└─────────────────────────────────────────────┘\n```\n\n---\n\n*本レポートはフロントエンド・バックエンド・インフラ・セキュリティ・コスト・運用の6観点レビューを統合した最終判断である。採用条件の充足確認はSprint 1完了時点で実施し、本レポートのGo/No-Goゲート4項目を基準に本番リリースの可否を判断すること。*",
"evidence": {
"requestId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"workflow": {
"workflowId": "tech-selection-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"status": "completed",
"taskCount": 9
}
}
}
お手元でデプロイした場合には、何か他の題材でお試しいただけると、より良いかなと思います。
実際に出力されたレポートはこちらを開いて参照してください。
実際に生成された 技術選定最終レポート
Next.js + FastAPI + Aurora PostgreSQL + ECS Fargate + Bedrock
作成:シニアソフトウェアアーキテクト
対象読者:経営・開発リード
ステータス:最終判定済み/実行可能
結論
✅ 条件付き採用
この技術スタックを採用する。
ただし、以下に示す7つの採用条件を「開発着手前」に全て解決すること。
条件未達のまま走り出した場合、フェーズ2以降で
高コストな手戻りまたはセキュリティインシデントが発生する。
なぜこのスタックが正しいか、一言で:
FastAPI・Next.js・PostgreSQLはOpenAPIを軸に型連携が成立し、BedrockはAWSエコシステムに閉じてテナントデータのセキュリティ説明責任を果たせる。ECS Fargateはスタートアップ期〜成長期に必要な「管理負荷とスケーラビリティのバランス」において最適な中間点だ。テナント5社→500社への成長パスが確立された構成であり、エンジニア採用市場との親和性も高い。
総合評価スコア
| 評価軸 | スコア | 判定根拠 |
|---|---|---|
| 技術適合性 | ★★★★☆ | スタック間の相性は良好。OpenAPI連携が特に強み |
| セキュリティ | ★★★☆☆ | 構成要素は正しいが、実装設計がまだ存在しない |
| スケーラビリティ | ★★★★☆ | Aurora・ECSともに段階的拡張パスが明確 |
| コスト妥当性 | ★★★☆☆ | 初期フェーズは構成変更なしだと割高。要見直し |
| 運用成熟度 | ★★★☆☆ | 5レイヤー分散構成。AWS経験者が不在ならNo-Go |
| 採用市場親和性 | ★★★★☆ | 3技術とも求人・応募市場が活発 |
| 総合 | ★★★☆☆ | 条件付きGo。条件充足で★★★★に上がる |
採用条件
以下の7条件は、開発着手の前提とする。未解決のまま進めた場合の想定損害を各条件に併記する。
条件1|マルチテナント分離方式の確定
解決期限:設計フェーズ完了前
未解決時の損害:全テーブル・全リポジトリ層の大規模改修、最悪ケースでテナント間データ漏洩
採用方式は 「RLS(Row Level Security)+アプリ層二重防御」 とする。
-- 全テーブルに必ずFORCEオプション付きで適用する
-- FORCE なしでは superuser 接続時に RLS がバイパスされる
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
# FastAPI 側:全エンドポイントにテナントコンテキストを強制注入
# Depends で強制することで「うっかり忘れ」を構造的に防ぐ
async def get_tenant_context(
credentials: HTTPAuthorizationCredentials = Security(security),
db: AsyncSession = Depends(get_db)
) -> TenantContext:
payload = verify_jwt(credentials.credentials)
tenant_id = payload.get(\"custom:tenant_id\")
if not tenant_id:
raise HTTPException(status_code=403, detail=\"テナントID未設定\")
await db.execute(
text(\"SET LOCAL app.current_tenant_id = :tid\"), {\"tid\": tenant_id}
)
return TenantContext(tenant_id=UUID(tenant_id))
やってはいけないこと:
- テナントごとに別Auroraクラスターを立てる(コストがN倍になり初期フェーズで事業が成立しない)
- RLSなしでアプリ層のみに依存する(実装ミス1箇所で全テナントデータが漏洩する)
条件2|OpenAPI→TypeScript型自動生成パイプラインのCI組み込み
解決期限:Sprint 1 Day 1
未解決時の損害:バックエンド型変更がフロントに伝わらず、本番でのundefinedエラーが慢性化。中規模プロジェクトで2〜3日の修正コストが複数回発生
# CI/CDに組み込む型生成パイプライン(GitHub Actions等)
# Step 1: FastAPIからOpenAPIスキーマを自動エクスポート
python -c \"
import json
from app.main import app
with open('openapi.json', 'w') as f:
json.dump(app.openapi(), f, indent=2)
\"
# Step 2: TypeScript型を自動生成
npx openapi-typescript openapi.json \\
-o frontend/src/types/api.generated.ts
# Step 3: 型変更をPRで検知して警告を出す
git diff --exit-code frontend/src/types/api.generated.ts || \\
echo \"::warning::API型定義に変更があります。フロントエンド側の影響確認を行ってください\"
重要: このパイプラインは後から追加すると既存の型不整合を全件修正する必要が生じる。Day 1に組み込まないと技術的負債が確定する。
条件3|認証基盤の選定とテナントID付与フローの確定
解決期限:設計フェーズ完了前
未解決時の損害:認証基盤を変更すると全APIエンドポイントのDependsを書き直すことになる
採用基盤は AWS Cognito + NextAuth.js v5(Auth.js) とする。
選定理由:
- AWSエコシステム内でセキュリティ境界が閉じる
- B2B企業顧客へのデータレジデンシー説明が容易
- IAM・VPCとのネイティブ統合が可能
テナントID付与フロー:
Cognito Pre Token Generation Lambda Trigger
→ custom:tenant_id をJWTに付与
→ FastAPI Depends で検証
→ 全DBクエリに tenant_id フィルタを強制適用
# FastAPI JWT検証(Cognito公開鍵で署名検証)
async def verify_cognito_token(
token: str,
jwks_client: PyJWKClient = Depends(get_jwks_client)
) -> dict:
signing_key = jwks_client.get_signing_key_from_jwt(token)
return jwt.decode(
token,
signing_key.key,
algorithms=[\"RS256\"],
audience=settings.COGNITO_CLIENT_ID,
options={\"verify_exp\": True}
)
条件4|Bedrockの非同期処理設計とフォールバック実装
解決期限:Sprint 1完了前
未解決時の損害:boto3のイベントループブロッキングにより全APIの応答が劣化。Bedrock障害でサービス全体が停止
# boto3はネイティブ非同期非対応のため、run_in_executorで回避する
_executor = ThreadPoolExecutor(max_workers=10)
async def invoke_bedrock(self, prompt: str) -> str:
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
_executor,
lambda: self._sync_invoke(prompt)
)
# 30秒超えが見込まれる処理はキューに分離する
# FastAPI → 202 Accepted → SQS → Worker ECS Task → Bedrock
# フォールバック実装は必ずリリース前に組み込む
async def invoke_with_fallback(self, prompt: str) -> AIResult:
try:
return await self.invoke_bedrock(prompt)
except ThrottlingException:
await self._enqueue_for_retry(prompt)
return DegradedResult(
message=\"AI処理を予約しました。完了後にメールで通知します\",
status=\"queued\"
)
except (ServiceException, ModelNotReadyException):
return DegradedResult(
message=\"AI機能は一時的に利用できません。しばらく後にお試しください\",
status=\"unavailable\"
)
条件5|IAMロールの最小権限設計(TaskRole/ExecutionRole分離)
解決期限:インフラ構築開始前
未解決時の損害:コンテナ侵害時に全AWSリソースへのアクセスが可能になり、顧客データ全漏洩に直結
FastAPI ECS TaskRole:アプリが実際に使うリソースのみ
{
\"Version\": \"2012-10-17\",
\"Statement\": [
{
\"Sid\": \"BedrockInvoke\",
\"Effect\": \"Allow\",
\"Action\": [
\"bedrock:InvokeModel\",
\"bedrock:InvokeModelWithResponseStream\"
],
\"Resource\": \"arn:aws:bedrock:ap-northeast-1::foundation-model/anthropic.claude-3-5-sonnet-*\"
},
{
\"Sid\": \"SecretsManagerRead\",
\"Effect\": \"Allow\",
\"Action\": [\"secretsmanager:GetSecretValue\"],
\"Resource\": \"arn:aws:secretsmanager:ap-northeast-1:ACCOUNT_ID:secret:prod/fastapi/*\"
},
{
\"Sid\": \"SQSSend\",
\"Effect\": \"Allow\",
\"Action\": [\"sqs:SendMessage\", \"sqs:ReceiveMessage\", \"sqs:DeleteMessage\"],
\"Resource\": \"arn:aws:sqs:ap-northeast-1:ACCOUNT_ID:bedrock-job-queue\"
}
]
}
分離の原則:
ECSExecutionRole → コンテナ起動に必要(ECRからのイメージPull、CloudWatch Logsへの書き込み)
ECSTaskRole → アプリ実行時に必要(Bedrock、SecretsManager、SQS等)
絶対禁止:AdministratorAccess / PowerUserAccess の付与
条件6|Auroraバックアップ設定とRDS Proxyの初期組み込み
解決期限:インフラ構築時(作成後変更不可の設定あり)
未解決時の損害:バックアップ不備でデータ復旧不能。RDS Proxy後付けは接続断を伴う
Aurora作成時に確定する設定(後から変更できない項目に★):
★ ストレージ暗号化:有効(作成後変更不可)
★ マルチAZ:有効(WriterのAZ障害でもフェイルオーバー可能)
自動バックアップ保持期間:7日以上(デフォルト1日では本番不可)
拡張モニタリング:有効(60秒間隔)
Performance Insights:有効(7日保持)
SQLAlchemy接続設定(フェイルオーバー対策):
pool_pre_ping=True # 切断済み接続の自動検知
pool_recycle=1800 # 30分でコネクション強制再作成
pool_size=5 # Fargateタスク1台あたりの接続数
max_overflow=10 # バースト時の最大追加接続数
RDS Proxyの初期組み込み:
Fargateタスクが10台を超える見込みがあれば初期から必須
└ 後付けは接続断が発生するため、初期設計に含めること
条件7|初期フェーズのコスト構成の経営合意
解決期限:設計フェーズ完了前
未解決時の損害:テナント5社時点でインフラ固定費が$63〜$130/社/月に達し、ARR $1,000〜$2,000/社ティアでは採算が取れない
現状構成の月次固定費試算:$400〜$700/月
推奨する初期フェーズの構成変更:
[変更1] Aurora → RDS PostgreSQL db.t4g.medium(MultiAZ)
削減額:$40〜$60/月
Aurora移行の判断基準(KPIとして事前に定義する):
「DBのCPU使用率が2週間継続して60%超」
「月間I/Oが3億回超」
「テナント50社到達」
のいずれかを満たした時点でAurora Serverless v2へ移行
[変更2] Next.js on ECS → Next.js on Vercel Pro(初期)
削減額:Fargateタスク分$76/月 → Vercel $20/月(差額$56/月)
注意:Vercel固有機能(Edge Functions等)への依存を最小化し、
ECS移行への逃げ道を残しておく
[変更3] Bedrockにテナント別月次トークン上限を実装
方式:DynamoDB または Redis で月次使用量を記録し上限で429を返す
目的:コスト爆発防止 兼 DDoS的な使用からの保護
変更後の想定月額:$150〜$220/月(現行比 最大$480削減)
主要リスク
リスクマトリクス
| リスク | 発生確率 | 影響度 | 対処タイミング |
|---|---|---|---|
| テナント間データ漏洩(RLS未実装) | 中 | 致命的 | 設計フェーズ |
| 型断絶による本番サイレントエラー | 高 | 大 | Sprint 1 Day 1 |
| Bedrockブロッキングによる全API劣化 | 高 | 大 | Sprint 1以内 |
| IAM過剰権限によるデータ全漏洩 | 中 | 致命的 | インフラ構築前 |
| 初期コスト過重による採算悪化 | 高 | 中 | 設計フェーズ |
| チームのAWS経験不足 | 環境依存 | 大 | 採用判定前 |
| Bedrock障害によるサービス全停止 | 低〜中 | 大 | Sprint 1以内 |
特に注意が必要なリスク2点
リスクA:チームにAWS経験者が不在の場合
判定:AWS経験者(ECS・VPC・IAMの実務経験)が1名もいない場合は
この判定を「再検討」に引き下げる。
根拠:本構成は5レイヤー分散構成であり、VPC設計ミス・
IAMポリシー誤設定・ECSネットワーキングのトラブルは
経験がないと解決に数日〜数週間を要する。
対処策:
① AWS経験者を1名採用またはアサインしてから着手する
② AWSパートナー企業と初期3ヶ月の支援契約を結ぶ
③ 代替案Aまたは代替案B(軽量版)から始める
リスクB:Aurora/RDSのストレージ暗号化は作成後変更不可
本番稼働後に「暗号化を有効にしたい」となった場合、
スナップショットから新クラスターを作成し直す必要がある。
その際にダウンタイムが発生する。
必ずAurora/RDS作成時に暗号化有効を確認すること。
代替案
代替案A:Next.js + Supabase + Vercel(プロトタイプ〜10社規模)
対象:PMF未確定・エンジニア1〜2名・検証速度最優先の場合
構成:
フロント:Next.js on Vercel
バックエンド:Supabase(PostgreSQL + Edge Functions)
AI:OpenAI API(Bedrock代替)
月額コスト:$50以下
メリット:
✅ インフラ管理ほぼゼロ
✅ SupabaseがRLSをネイティブサポート(テナント分離が楽)
✅ 開発速度が最速
✅ 初期固定費が最小
デメリット:
❌ Supabaseへのベンダーロック
❌ AWSエコシステムとの統合が複雑
❌ OpenAI APIを使うためデータレジデンシーの顧客説明が困難
❌ 大規模化時の移行コストが高い
採用推奨状況:
「まず3ヶ月で10社に売れるか検証したい」という判断の場合
代替案B:現構成の軽量版(推奨採用案)
今回の推奨案から Aurora → RDS、ECS Next.js → Vercel に変更した構成。
コードベースの変更はほぼなく、将来のAurora/ECS移行パスも明確。
月額:$150〜$220(現状比最大$480削減)
移行トリガーKPIを事前に定義した上での採用を推奨する。
代替案C:FastAPI → Go(高負荷Bedrock並行呼び出し専用)
対象:Bedrockへの大量並行呼び出しがコア機能である場合のみ検討
現時点での結論:採用しない
理由:
- FastAPI + run_in_executor で現段階は十分対処可能
- PydanticによるOpenAPI自動生成・型安全性を失う
- PythonエコシステムのML/AIライブラリが使えなくなる
- ベンチマーク結果が出てから改めて検討すれば十分
次の検証タスク
以下を優先順位順に実行する。各タスクに「完了基準」を明記する。
Week 1:設計フェーズ(全員)
優先度:最高
□ マルチテナント分離方式の確定とER図レビュー
完了基準:RLSポリシーのSQL草案とFastAPI Dependsの設計が
レビュー済みでドキュメント化されていること
□ Cognito User PoolのPoC構築(テナントID付与フロー検証)
完了基準:custom:tenant_id が JWTに入り、
FastAPIで正しく検証できることを動作確認済みであること
□ Bedrockモデル選定と東京リージョン動作確認
完了基準:Claude 3.5 Sonnet が ap-northeast-1 で
boto3から呼び出せることを確認済みであること
□ コスト構成の経営合意
完了基準:「RDS採用+Vercel採用」または「Aurora採用の場合のARR設定」
について経営判断が記録されていること
□ AWSチーム経験レベルの確認
完了基準:ECS・VPC・IAM実務経験者の有無を確認し、
不在の場合は補完策(採用/外部支援)が決定していること
Week 2〜3:基盤構築フェーズ(インフラ担当)
優先度:高
□ VPC / セキュリティグループ / VPCエンドポイント構築
完了基準:Terraform管理下でコードレビュー済みであること
(VPCエンドポイント:S3・Bedrock・SecretsManager・ECR)
□ RDS PostgreSQL + RDS Proxy 構築
完了基準:暗号化有効・MultiAZ有効・バックアップ7日保持を
AWSコンソールで確認済みであること
□ ECSクラスター + ALB 構築
完了基準:TaskRole/ExecutionRoleが分離されており、
IAMポリシーがレビュー済みであること
□ OpenAPI→TypeScript型生成パイプラインのCI組み込み
完了基準:PRを出すと型変更が自動検知されることを
テストPRで動作確認済みであること
Week 4:アプリ結合フェーズ(全員)
優先度:高
□ FastAPI → RDS疎通確認(マイグレーション含む)
完了基準:Alembicマイグレーションが適用され、
RLSポリシーが全テーブルに設定されていること
□ FastAPI → Bedrock疎通確認(フォールバック込み)
完了基準:Bedrockが応答しない状態を意図的に作り、
DegradedResultが返ることを確認済みであること
□ Next.js → FastAPI疎通確認
完了基準:自動生成された型を使ったAPIコールが
型エラーなく動作することを確認済みであること
□ CloudWatchアラート設定(最低7項目)
完了基準:以下7項目が設定され、テストアラートが発報されること
① ECSタスクCPU 80%超
② ECSタスク起動失敗
③ RDS接続数 80%超
④ RDSレプリカラグ 30秒超
⑤ ALB 5xxエラー率 1%超
⑥ Bedrock ThrottlingException 急増
⑦ 月次コスト予算超過アラート
Sprint 1完了前:Go/No-Goゲート(全員)
以下4項目が全て✅でなければ本番リリースをしない。
□ テナント横断アクセステストが自動化されている
確認方法:テナントAのJWTでテナントBのリソースにアクセスして
403/404が返ることがCIで自動テストされていること
□ Bedrockフォールバック実装が動作している
確認方法:Bedrockをモックして障害状態にした際に
DegradedResultが返ることをテストで確認済みであること
□ IAMロールが最小権限でレビュー済み
確認方法:TaskRole/ExecutionRoleの分離と
全ActionのResourceが具体的ARNで指定されていること
□ RDS/Auroraバックアップが7日保持で動作している
確認方法:AWSコンソールでバックアップ保持期間と
直近バックアップの存在を確認済みであること
判定サマリー
┌─────────────────────────────────────────────┐
│ │
│ 判定:✅ 条件付き採用 │
│ │
│ 採用の前提:7条件を設計フェーズで全て解決 │
│ 推奨構成:代替案B(軽量版)を初期採用 │
│ AWS経験者:最低1名の確保が必須 │
│ Go/No-Goゲート:Sprint 1完了時に再評価 │
│ │
│ 条件未達の場合:代替案A(Supabase)で │
│ PMF検証フェーズを先行させること │
│ │
└─────────────────────────────────────────────┘
本レポートはフロントエンド・バックエンド・インフラ・セキュリティ・コスト・運用の6観点レビューを統合した最終判断である。採用条件の充足確認はSprint 1完了時点で実施し、本レポートのGo/No-Goゲート4項目を基準に本番リリースの可否を判断すること。
workflow ツールを使ってみた所感
まず、ツールとしての所感ですが、strands_tools.workflowは、「決まった流れでエージェントに処理を実行させたい」という場合には向いているかなと思います。(当たり前のこと言ってる気がする。)
以下の動作を理解することで、開発しやすいツールだと感じるかなと思っております。
-
workflowツールの引数であるactionのcreate,startでの動作を理解する - 各タスクの定義で
dependenciesを使うことで、「このタスクが終わったらこのタスクを実行する」という定義ができることを理解する -
tools未指定だと親Agentのツールを子Agentが継承してしまうため、別のツールを明示的に定義する必要があることを理解する
一方で、現時点では注意点もあるかなと感じました。
-
startは同期実行なので、長い workflow は Runtime のタイムアウト設計に注意が必要 - workflow の状態はファイル保存なので、AgentCore Runtime 上では永続化設計を別途考えた方がよい
ただし、生成されたレポートはかなり詳細な情報が記載されており、技術選定を決定するには十分すぎる情報が含まれているのがわかります。これを見るだけでも、ワークフローは強力なツールであることが理解できましたでしょうか。
Claude CodeやCodexなどで一連のフローの実行を委譲したい場合、ワークフロースキルを構築して実行することで、ユーザが設定した目標まで処理を実行してくれます。
※ ワークフローの中にレビュー・評価用エージェントを用意しておき、評価が FALSE であれば再実行するようにすることで、より強く質の高いワークフロースキルになります。
まとめ
今回は、Strands Agents の strands_tools.workflow を使って、技術選定エージェントを AgentCore Runtime 上にデプロイする構成を作りました。
やったことは以下です。
- 技術選定レビューを9つの workflow タスクに分割
-
dependenciesで並列レビューと最終統合の流れを定義 - 親Agentから
workflow createとworkflow startを実行 -
final_reportの結果を状態JSONから取り出して Runtime レスポンスへ返却 - CDKでECR、AgentCore Runtime、IAM Roleを作成し、デプロイ
いかがでしたでしょうか。特に「サブエージェントを必ず実行させてみたい」というような方には、お勧めできるツールかなと思っています。
また、GoalLoopプラグインと組み合わせて使ったら強力なエージェントになる気がしますね。
次は、似たようなツールでgraphというツールがあるので、これを試してみたいなと思っています。
また、workflow start が非同期で実行することができるのか、試してみたいところです。
以上

