コンテンツにスキップ

Claude Code ランタイムモデル

採用方針: automedia は bootstrap メタプロジェクト + AWS 上の中央サービス として構築する完全 AWS 化モデル(Phase 1b)。Webhook 受信・スケジュール配信・Claude 実行のすべてを spin-dd AWS アカウント(profile=spindd, region ap-northeast-1)に集約し、認証は Claude Platform on AWS の IAM 統合で完結させる。GH Actions は OpenTofu deploy と CI 検証のみに使う。

なぜこのモデルか

Webhook 即応 5 秒以内の要件

LINE follow 直後の挨拶を 5 秒以内に reply したい。spin-dd/spindd-hubspot-theme で GH Actions 経路を実測したところ約 1 分 かかった(queue + provisioning が本質的に削れない)。Webhook 即応経路は AWS Lambda 必須。

Claude Platform on AWS の発見

Claude Platform on AWSAnthropic API そのもの に AWS の IAM 認証・請求・監査を統合した形態。Bedrock とは別物。

Claude Platform on AWS Bedrock Anthropic API(直接)
運営 Anthropic AWS Anthropic
データ処理場所 Anthropic AWS 内 Anthropic
認証 AWS IAM AWS IAM API キー
請求 AWS 集約 AWS 集約 Anthropic 個別
監査 CloudTrail CloudTrail Anthropic ログ
最新モデル 最速(同等) 数週〜数ヶ月遅れ 最速
Anthropic ネイティブ機能 フル 一部のみ フル
価格 Claude API と同価格 別建て 公式価格

Claude Platform を使えば「最新モデル即時利用 + AWS 統合のメリット」を両取りでき、API キーの Secrets 管理が不要になる。

全 AWS 化の利点

観点
Webhook 反応時間 ≤ 5 秒 (ウォーム時 300〜500 ms)
スケジュール時刻精度 秒精度 (EventBridge 動的 at())
認証管理 AWS IAM 一本 (Claude / 全 secret は IAM Role 経由)
課金 AWS 請求書に集約
監査ログ CloudTrail + CloudWatch に集約
運用ハード spin-dd 側ゼロ (AWS マネージド)

メタプロ + 中央サービスの両立

automedia リポは以下を兼ねる:

  • bootstrap メタプロ: 各テーマリポの .automedia/ 雛形・スキルソース・CLI を配布
  • AWS 中央サービス: API Gateway + Lambda 群 + EventBridge + Secrets Manager + S3 を抱える

テーマリポ側の責務は最小化:

  • .automedia/project.yml — テナント識別 + Backlog プロジェクトキー + LINE Channel ID
  • .automedia/templates/ — メッセージテンプレ
  • .automedia/rules/ — 配信ルール(時間帯ガード、上限等)

「テナント境界」は IAM ロール / Secrets Manager prefix / S3 prefix = 境界。Phase 2 で per-tenant AWS アカウント分散も可能。

実行ホスト一覧

処理 実行ホスト
LINE Webhook 即応 AWS Lambda + API Gateway HTTP API (spin-dd)
Backlog Webhook (status / コメント) AWS Lambda + API Gateway HTTP API (spin-dd)
配信時刻トリガ(動的 schedule) AWS EventBridge Scheduler(課題ごとに at(投稿日時) で登録)
配信走査(日次セーフティネット) AWS EventBridge Scheduler rate(1 day) → Scan Lambda(Webhook 取りこぼし救済)
配信実行(Claude 呼び出し) AWS Lambda (Node, Claude Agent SDK) (spin-dd)
重処理(長尺生成時) AWS ECS Fargate Task(Phase 1b 後半検討)
Claude モデル呼び出し Claude Platform on AWS(IAM 統合)
テナント設定同期 AWS Lambda(GitHub Push Webhook 駆動)
OpenTofu deploy 実行 GitHub Actions(OIDC で AWS ロール assume)
bootstrap / upgrade CLI 開発者ローカル
Backlog / LINE / HubSpot API SaaS

automedia リポは AWS 上の全リソースを OpenTofu で管理する。GH Actions は配信ランタイムには関与しない。

全体図

automedia Phase 1b 全体図

実行フローはすべて AWS 内で完結。各テーマリポは設定の SoT (Source of Truth) に専念。

AWS 構成図(詳細)

AWS 内部のリソース構成と依存関係を 8 レイヤーに分けて整理した詳細図:

automedia Phase 1b AWS Architecture

レイヤー構成

レイヤー リソース 役割
Inbound Route 53 / ACM / API Gateway (HTTP API) カスタムドメイン api.automedia.spin-dd.com で TLS 終端、/line/webhook/{channel_id} /backlog/webhook /github/webhook の 3 エンドポイント
Compute Lambda × 5(webhook-line / webhook-backlog / sync / scan / deliver) + ECS Fargate(長尺ジョブ用、Phase 1b 後半) 配信フローの実体。deliver のみが Claude Platform on AWS を呼ぶ
Async / Trigger SQS + DLQ / EventBridge Schedule (at(投稿日時) 動的) / EventBridge Scheduler (rate(1 day) 日次) 非同期実行と時刻トリガ
State / Storage S3(テンプレ cache) / Secrets Manager(provider 単位 JSON) / DynamoDB(アプリ層 deliver locks) / OpenTofu state backend (S3 のみ、use_lockfile) 永続化層
AI Claude Platform on AWS Claude Agent SDK を IAM 認証で呼び出し(API キー不要)
Observability CloudWatch Logs / Alarm 全 Lambda のログ集約、Webhook 失敗率・Lambda エラー・Secret rotation 期限を監視
IAM & Identity IAM Roles × 6(Lambda 用 5 + deploy 用 1) Lambda ごとに最小権限。deploy は GitHub OIDC で assume
Deploy GitHub Actions (aws-deploy.yml) → OpenTofu (aws/tofu/) OIDC で AWS にロール assume → 全リソースを tofu apply

主要フロー(色分け)

  • 🟢 LINE 系: LINE Platform → API GW → webhook-line Lambda → (reply ≤5s) / SQS → deliver
  • 🔵 Backlog 系: Backlog → API GW → webhook-backlog Lambda → EventBridge Schedule at(投稿日時) → deliver(コマンド即時の場合は直接 deliver invoke)
  • GitHub Push 系: GitHub → API GW → sync Lambda → S3
  • 🔴 Claude 呼び出し: deliver Lambda → Claude Platform on AWS(IAM)
  • 🟩 LINE Reply (5s 制約): webhook-line → LINE Platform(破線で 5 秒の SLO を明示)

設計の要点(図中サマリ)

  • 反応時間: LINE follow ≤5s(Webhook Lambda)、Backlog トリガ 秒精度(Webhook + at() Schedule)
  • 冪等性: Backlog「自動配信実行日時」+ DynamoDB lock の二重防御
  • テナント分離: IAM ロール × Secrets Manager prefix × S3 prefix の三重分離
  • Secret 管理: /automedia/<tenant>/<provider> に provider 単位 JSON で格納
  • 観測: CloudWatch Logs / Alarm 集約 + Backlog コメントへの結果書き戻し
  • セーフティネット: 日次 Scan Lambda が Webhook 取りこぼしを救済(通常は検知 0 件)

コンポーネント

1. automedia リポ(メタプロ + AWS 中央サービス)

メタプロ責務:

  • 設計ドキュメント(このサイト)
  • 共有スキル skills/automedia-*/ — SDK 呼び出しから読まれる MD
  • 雛形 templates/.automedia/ — テーマリポの初期構造
  • bootstrap CLI automedia init / automedia upgrade

AWS 中央サービス責務:

  • aws/lambdas/webhook-line/ — LINE Webhook 即応(Node + TypeScript)
  • aws/lambdas/webhook-backlog/Backlog Webhook 受信(status 変化 / コメントコマンド / 投稿日時更新)。Deliver invoke または EventBridge Schedule の at(投稿日時) 登録/更新/削除
  • aws/lambdas/scan/日次セーフティネット(Webhook 取りこぼしを救済、rate(1 day) 起動)
  • aws/lambdas/deliver/ — Claude 呼び出し + 各 SaaS 連携
  • aws/lambdas/sync/ — GitHub Push を受けて .automedia/ を S3 にミラー
  • aws/tofu/ — OpenTofu モジュール群(API Gateway / Lambda / EventBridge / Secrets Manager / S3 / SQS / IAM / CloudWatch)
  • .github/workflows/aws-deploy.yml — GitHub OIDC で AWS ロール assume → tofu apply

2. 各テーマリポ spin-dd/<key>-hubspot-theme

ランタイム責務なし。設定 SoT + コントロールプレーン (Claude Code セッションの操作起点) を兼ねる。

  • .automedia/project.yml — テナント識別子 / Backlog プロジェクトキー / LINE Channel ID
  • .automedia/templates/line/*.yml — メッセージテンプレ
  • .automedia/rules/*.yml — 配信ルール(時間帯ガード、上限等)
  • .automedia/state/ — オーディエンスキャッシュ等(commit 不要なら gitignore)
  • .claude/skills/automedia-* — automedia への指示用スキル群 (read 系は直接呼び出し、write 系は Backlog 承認経由)

GitHub Push → automedia の Sync Lambda が S3 prefix s3://automedia-tenants/<tenant>/ にミラー。テーマリポ Claude Code セッションをコントロールプレーンとする設計、Backlog 承認フロー、役割境界などは コントロールプレーンと責務分担 を参照。

3. Webhook Lambda

POST /line/webhook/{channel_id} を受ける。

// aws/lambdas/webhook/handler.ts (概念)
export const handler = async (event: APIGatewayProxyEventV2) => {
  await verifyLineSignature(event);                         // ~50ms
  const tenant = await lookupTenant(event.pathParameters!.channel_id!);  // S3 cache ~10ms
  const body   = JSON.parse(event.body!) as LineWebhook;

  for (const ev of body.events) {
    if (ev.type === 'follow') {
      const welcome = await loadTemplate(tenant, 'line/welcome.yml');    // S3 ~30ms
      await lineReply(ev.replyToken, welcome);              // ~200ms
    } else if (requiresGeneration(ev)) {
      await sqs.send({ tenant, event: ev });                // 非同期、ACK のみ
    }
  }
  return { statusCode: 200, body: '' };
};
  • 挨拶パスは Claude を経由しない(5 秒予算確保)
  • 重処理は SQS → Deliver Lambda へ非同期
  • LINE Channel Secret / Access Token は Secrets Manager のテナント別 prefix

4. Backlog Webhook Lambda

POST /backlog/webhook を受ける。Phase 1b の主トリガ(status 変化・コメントコマンド・投稿日時更新を即応で拾う)。

// aws/lambdas/webhook-backlog/handler.ts (概念)
export const handler = async (event: APIGatewayProxyEventV2) => {
  await verifyBacklogSignature(event);                   // 共有シークレットで検証
  const payload = JSON.parse(event.body!) as BacklogWebhookPayload;

  switch (payload.type) {
    case 'IssueUpdated': {
      const issue = payload.content;
      if (issue.status === 'automedia承認') {
        // 即時送信も予約送信も常に EventBridge Schedule を経由する。
        // 即時の場合は at(now + 5s) で登録 → 短時間のうちに deliver が起動する。
        // これにより監査ログが EventBridge に統一され、誤承認時の Schedule 削除による
        // 取り消しも一貫した方法で可能になる。
        const scheduled = parseJST(issue.customField('投稿日時'));
        const fireAt = new Date(Math.max(scheduled.getTime(), Date.now() + 5_000));
        await upsertSchedule(issue.key, fireAt);         // EventBridge Schedule `at(fireAt)`
      } else if (issue.previousStatus === 'automedia承認') {
        await deleteSchedule(issue.key);                 // cancel (invalidate / 取り消し)
      }
      break;
    }
    case 'IssueCommented': {
      // コメントコマンドも同様に Schedule 登録/削除/preview に集約する。
      const cmd = parseAutomediaCommand(payload.content.comment);
      if (cmd?.action === 'send')    await upsertSchedule(cmd.issueKey, new Date(Date.now() + 5_000));
      if (cmd?.action === 'cancel')  await deleteSchedule(cmd.issueKey);
      if (cmd?.action === 'preview') await invokePreview({ ...cmd });   // preview は read 系
      break;
    }
  }
  return { statusCode: 200, body: '' };
};
  • 動的 schedule (at(投稿日時)) は秒精度
  • コマンド (/automedia send 等) も EventBridge Schedule at(now+5s) で登録 (即時系も統一)
  • 冪等性は既存の Backlog カスタムフィールド「自動配信実行日時」で担保

5. Scan Lambda(日次セーフティネット)

rate(1 day) で起動し、Webhook 取りこぼしの最終防衛線として動作。

// aws/lambdas/scan/handler.ts (概念)
export const handler = async () => {
  for (const tenant of await listAllTenants()) {
    const missed = await backlogIssuesFiltered(tenant, {
      status: 'automedia承認',
      issueType: 'コンテンツ運用',
      customField: { '投稿日時': '<= now()', '自動配信実行日時': 'IS NULL' },
    });
    for (const issue of missed) {
      await invokeDeliver({ tenant, issueKey: issue.key });
      await notifyMissedWebhook(tenant, issue);          // CloudWatch Alarm 連動
    }
  }
};

通常運用では検知件数 0 になるはず。検知された場合は Backlog Webhook 配信の問題として調査対象になる。

5. Deliver Lambda(Claude 呼び出し)

Claude Platform on AWS の IAM 統合で認証。Claude Agent SDK を Node でネイティブ呼び出し(CLI ではなく)。

// aws/lambdas/deliver/handler.ts (概念)
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic({
  // Claude Platform on AWS を IAM 認証で叩く
  awsRegion: 'ap-northeast-1',
});

export const handler = async (event: { tenant: string; issueKey: string }) => {
  const skill   = await loadSkill('automedia-deliver');     // S3 から MD を読む
  const context = await loadContext(event.tenant, event.issueKey);  // Backlog 課題 + .automedia/

  const result = await client.messages.create({
    model: 'claude-opus-4-7',
    max_tokens: 8192,
    system: skill,
    messages: [{ role: 'user', content: buildPrompt(context) }],
    tools: defineTools(event.tenant),  // line_push / hubspot_create_post / backlog_comment 等を TS で実装
  });

  await applyToolCalls(event.tenant, event.issueKey, result);
};

Claude Code CLI ではなく Claude Agent SDK 呼び出し を採る理由:

  • Lambda は container 不要、起動軽量
  • .claude/skills/ 文化を MD のまま継承しつつ、SDK の system / tools として展開できる
  • 実行制御(タイムアウト、リトライ、tool 実行)が TypeScript で書ける

長尺ジョブ (>15 分) は ECS Fargate Task に escalate(Phase 1b 後半検討)。

6. Tenant Sync Lambda

GitHub Push Webhook を受け、.automedia/ を S3 にミラー。

// aws/lambdas/sync/handler.ts (概念)
export const handler = async (event: GitHubPushEvent) => {
  const tenant = resolveTenantFromRepo(event.repository.full_name);
  await syncFromGitHub(event.repository.full_name, '.automedia/', `s3://automedia-tenants/${tenant}/`);
};

これで Lambda 群は S3 を SoT として読める(GitHub Raw API への都度 fetch は不要)。

ワークフロー:LINE Webhook 即応(≤ 5 秒)

LINE → API Gateway → Webhook Lambda
                        ├ 署名検証 (~50ms)
                        ├ tenant lookup (S3 cache ~10ms)
                        ├ event type 判定
                        │   ├ follow  → welcome.yml → LINE reply (~200ms)
                        │   └ message → 同上 or SQS enqueue
                        └ 200 OK

ウォーム時 ~300ms、コールドスタート時でも 1.5〜3 秒。5 秒予算に余裕。provisioned concurrency = 1 を Phase 1b 後半で検討。

ワークフロー:Backlog Webhook トリガ(主経路)

Backlog (status → automedia承認 / コメント / カスタムフィールド更新)
   ↓ webhook
API Gateway ──→ Backlog Webhook Lambda
                  ├ 署名検証
                  ├ event type 判定
                  │   ├ IssueUpdated (status → automedia承認)
                  │   │   └ EventBridge Schedule at(max(投稿日時, now+5s)) 登録
                  │   │     (即時送信も予約送信も常に Schedule 経由)
                  │   ├ IssueUpdated (status ← automedia承認 / 内容変更 invalidate)
                  │   │   → Schedule 削除
                  │   ├ IssueUpdated (投稿日時 更新)      → Schedule update
                  │   └ IssueCommented (/automedia …)     → Schedule 登録 / 削除 / Preview
                  └ 200 OK

ワークフロー:スケジュール時刻到達

EventBridge Schedule `at(投稿日時)` 到達
   ↓ (秒精度)
Deliver Lambda ──→ Claude Platform on AWS (IAM)
              ──→ LINE / HubSpot / Backlog API
              ──→ CloudWatch Logs
              ──→ Backlog コメント (実行報告) + 「自動配信実行日時」更新

ワークフロー:日次セーフティネット

EventBridge Scheduler `rate(1 day)` ──→ Scan Lambda
                                            ├ 全テナント Backlog scan
                                            ├ 「automedia承認 + 投稿日時 ≤ now() + 自動配信実行日時 IS NULL」を抽出
                                            ├ 検知件数 > 0 なら CloudWatch Alarm (Webhook 異常検知)
                                            └ Deliver Lambda invoke (漏れ救済)

通常運用では Scan Lambda の検知件数は 0。検知されたら Backlog Webhook の配信失敗 / 設定漏れを示すシグナルとして扱う。

ワークフロー:テナント設定同期

spin-dd/bato-hubspot-theme  (push of .automedia/**)
GitHub Webhook  →  API Gateway  →  Sync Lambda
                              S3: s3://automedia-tenants/bato/

bootstrap / upgrade フロー

# 初回セットアップ(テーマリポで実行)
cd bato-hubspot-theme
npx @spin-dd/automedia init
#   ↳ .automedia/{project.yml, templates/, rules/} を生成
#   ↳ GitHub Push hook URL を出力(管理者が登録)
#   ↳ automedia の tenants.yml に追記する PR を spin-dd/automedia へ作る
git checkout -b chore/automedia-bootstrap && git add . && git commit
# テンプレ雛形のアップグレード
cd bato-hubspot-theme
npx @spin-dd/automedia upgrade

.claude/skills/, .github/workflows/ はテーマリポに作らない(automedia 中央集約)。

スキル設計の指針(SDK 呼び出し対応)

スキルは MD で管理し、Deliver Lambda が S3 から読んで Claude Agent SDK の system プロンプトに展開する。

---
name: automedia-deliver
description: Backlog 課題から配信先に応じた SNS 投稿を実行
tools: [backlog_get, line_push, hubspot_create_post, backlog_comment]
---

## 役割

## 配信前チェック

## 配信

## 失敗ハンドリング

tools: は MD で宣言し、Deliver Lambda が SDK の tool 定義として展開。実装は Lambda 内 TypeScript(旧設計の Bash + curl を fetch に置き換え)。

状態の在処

状態 在処
ContentTask 論理状態 Backlog 課題(SoT
配信実行履歴 CloudWatch Logs + Backlog コメント
Webhook 受信履歴 CloudWatch Logs
テナント設定 SoT 各テーマリポの .automedia/ (GitHub)
テナント設定 cache S3 automedia-tenants/<tenant>/
Channel ID → tenant マップ automedia リポ aws/config/tenants.yml → S3 sync
シークレット AWS Secrets Manager /automedia/<tenant>/<provider> (provider 単位 JSON)
Claude API キー 不要(Claude Platform on AWS の IAM 統合)
Lambda コード automedia リポ aws/lambdas/(OpenTofu deploy)

GH Actions Secrets は OpenTofu deploy 用の OIDC ロール ARN のみ。

リソース命名規約

AWS リソースは全て automedia- プレフィックスで統一する。spin-dd アカウント内の他リソースと混在しても識別可能、IAM ポリシーが prefix match で書ける、CloudWatch / Cost Explorer でフィルタしやすい。

リソース種別 命名規則
Lambda function automedia-<function> (ZIP, Node 20, arm64) automedia-webhook-line, automedia-webhook-backlog, automedia-deliver, automedia-scan, automedia-sync
IAM Role automedia-<purpose>-role automedia-deliver-role, automedia-deploy-role
IAM Policy automedia-<purpose>-policy automedia-deliver-secrets-policy
API Gateway HTTP API automedia-api (1 個)
S3 Bucket(グローバル一意) spin-dd-automedia-<purpose> spin-dd-automedia-tenants, spin-dd-automedia-tofu-state
Secrets Manager /automedia/<tenant>/<provider> セキュリティ・シークレット で既に決定済み)
DynamoDB automedia-<purpose> automedia-deliver-locks
SQS automedia-<purpose> automedia-deliver-queue, automedia-deliver-dlq
EventBridge Schedule Group automedia-deliver-schedules 動的 schedule のグループ
EventBridge Schedule(動的) automedia-deliver-<tenant>-<issueKey> 配信予約用
EventBridge Scheduler(rate) automedia-scan-daily 日次セーフティネット
CloudWatch Log Group /aws/lambda/automedia-<function> Lambda 標準
CloudWatch Alarm automedia-<resource>-<metric> automedia-webhook-line-errors
Route 53 Record api.automedia.spin-dd.com 既存ドメイン spin-dd.com の sub
ACM Certificate automedia-api-cert (name tag)

共通タグ

全リソースに以下のタグを付与(OpenTofu の default_tags で一括設定)。

# aws/tofu/provider.tf
provider "aws" {
  region = "ap-northeast-1"

  default_tags {
    tags = {
      Project     = "automedia"
      ManagedBy   = "opentofu"
      Environment = "prod"
      Owner       = "spin-dd"
    }
  }
}

これで Project = automedia タグで全リソースが Cost Explorer / Resource Groups で集計可能になる。

locals 例

# aws/tofu/locals.tf
locals {
  project_name  = "automedia"
  prefix        = "${local.project_name}-"
  global_prefix = "spin-dd-${local.project_name}-"   # S3 等のグローバル一意リソース用
}

resource "aws_lambda_function" "webhook_line" {
  function_name = "${local.prefix}webhook-line"
  # ...
}

resource "aws_s3_bucket" "tenants" {
  bucket = "${local.global_prefix}tenants"
  # ...
}

OpenTofu Bootstrap(backend 自己ホスト)

OpenTofu の state backend である S3 bucket 自体も OpenTofu で管理するchicken-and-egg 問題(state bucket を作るリソースが、その state bucket を必要とする)は bootstrap module を分離 + state migration で解消する。

ディレクトリ構成

aws/tofu/
├── bootstrap/                          # backend (S3 bucket) を作る最小モジュール
│   ├── README.md                       # 初回手順
│   ├── main.tf                         # spin-dd-automedia-tofu-state + versioning + encryption + bucket policy
│   ├── backend.tf                      # 初回は local → apply 後に S3 へ migrate
│   ├── provider.tf
│   ├── variables.tf
│   └── outputs.tf                      # bucket name / arn
├── modules/                            # 再利用モジュール
│   ├── lambda-function/
│   ├── webhook-api/
│   └── ...
├── backend.tf                          # bootstrap で作った bucket を指す(use_lockfile = true)
├── provider.tf
├── locals.tf
├── lambda-webhook-line.tf
├── lambda-webhook-backlog.tf
├── lambda-deliver.tf
├── lambda-scan.tf
├── lambda-sync.tf
├── apigateway.tf
├── eventbridge.tf
├── secretsmanager.tf
├── s3.tf                               # tenants bucket 等(tofu-state bucket は bootstrap 側)
├── dynamodb.tf                         # automedia-deliver-locks
├── sqs.tf
├── iam.tf
├── route53.tf
├── acm.tf
├── cloudwatch.tf
└── variables.tf

bootstrap/main.tf(概念)

# aws/tofu/bootstrap/main.tf
resource "aws_s3_bucket" "tofu_state" {
  bucket = "spin-dd-automedia-tofu-state"
}

resource "aws_s3_bucket_versioning" "tofu_state" {
  bucket = aws_s3_bucket.tofu_state.id
  versioning_configuration { status = "Enabled" }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "tofu_state" {
  bucket = aws_s3_bucket.tofu_state.id
  rule {
    apply_server_side_encryption_by_default { sse_algorithm = "AES256" }
  }
}

resource "aws_s3_bucket_public_access_block" "tofu_state" {
  bucket                  = aws_s3_bucket.tofu_state.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# TLS 強制 bucket policy
resource "aws_s3_bucket_policy" "tofu_state" {
  bucket = aws_s3_bucket.tofu_state.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid       = "DenyInsecureTransport"
      Effect    = "Deny"
      Principal = "*"
      Action    = "s3:*"
      Resource  = [aws_s3_bucket.tofu_state.arn, "${aws_s3_bucket.tofu_state.arn}/*"]
      Condition = { Bool = { "aws:SecureTransport" = "false" } }
    }]
  })
}

backend.tf(bootstrap / main 共通形式)

# aws/tofu/bootstrap/backend.tf
terraform {
  backend "s3" {
    bucket       = "spin-dd-automedia-tofu-state"
    key          = "bootstrap/terraform.tfstate"
    region       = "ap-northeast-1"
    encrypt      = true
    use_lockfile = true                  # ← S3 native locking、DynamoDB 不要
  }
}

# aws/tofu/backend.tf
terraform {
  backend "s3" {
    bucket       = "spin-dd-automedia-tofu-state"
    key          = "phase1b/terraform.tfstate"
    region       = "ap-northeast-1"
    encrypt      = true
    use_lockfile = true
  }
}

初回 Bootstrap 手順(手動・1 回だけ)

cd aws/tofu/bootstrap

# 1. backend.tf を一時的に無効化(local state で初回 apply)
mv backend.tf backend.tf.disabled

# 2. local state で apply(state bucket を作る)
tofu init
tofu apply
#   ↳ spin-dd-automedia-tofu-state バケットが作られる(state は手元の terraform.tfstate)

# 3. backend.tf を有効化 → state を S3 に migrate
mv backend.tf.disabled backend.tf
tofu init -migrate-state
#   ↳ "Do you want to copy existing state to the new backend?" → yes

# 4. ローカル state を削除
rm terraform.tfstate terraform.tfstate.backup

# 5. 以降 bootstrap module も S3 backend で管理される
cd ../
tofu init
tofu apply
#   ↳ メインスタックも同じ bucket の別 key (phase1b/) で管理される

以降の deploy は bootstrap の差分は no-op(変化なし)、main 側だけが変わる。

GitHub Actions からの deploy

# .github/workflows/aws-deploy.yml
- name: Tofu init & apply (bootstrap)
  working-directory: aws/tofu/bootstrap
  run: |
    tofu init
    tofu plan -detailed-exitcode
    tofu apply -auto-approve

- name: Tofu init & apply (main)
  working-directory: aws/tofu
  run: |
    tofu init
    tofu plan
    tofu apply -auto-approve

bootstrap は通常差分ゼロで終わる。state bucket 設定を変えた時のみ更新される。

自己ホスト分離のトレードオフ

bootstrap 分離(採用) bootstrap なし(state を別管理)
chicken-and-egg 回避(初回手動 1 回のみ) 常時手動
削除順序 明確(bootstrap は最後) 不明瞭
権限分離 bootstrap は初回 Admin、main は専用ロール 全部 Admin
運用負荷 初回 1 回手動、以降自動 永続的に手動管理

→ bootstrap 分離を採用。

Lambda デプロイ形式

Lambda 群は全て ZIP package でデプロイする。container image (ECR) は使わない。

観点 採用案
パッケージング ZIP(OpenTofu の archive_filedist/ を zip 化)
Runtime nodejs20.x
Architecture arm64(Graviton、コスト・性能で有利)
サイズ上限 250MB(展開後)— automedia の各 Lambda は 50MB 以下に収まる
Container (ECR) 使わない(Lambda 用途では)。ECS Fargate Task のみ ECR を使う(長尺ジョブ向け、Phase 1b 後半検討)

サイズ見積もり

Lambda 主要依存 推定 ZIP サイズ
automedia-webhook-line @aws-sdk/client-secrets-manager, @aws-sdk/client-sqs, crypto (built-in) ~5MB
automedia-webhook-backlog @aws-sdk/client-scheduler, @aws-sdk/client-lambda ~5MB
automedia-sync @aws-sdk/client-s3, @octokit/request ~3MB
automedia-scan @aws-sdk/client-lambda, fetch ~3MB
automedia-deliver @anthropic-ai/sdk, @aws-sdk/client-secrets-manager, @aws-sdk/client-dynamodb ~15MB

最大の deliver Lambda でも 15MB 程度。ZIP の 250MB 上限に対し十分余裕。

Lambda リソース例(OpenTofu)

# aws/tofu/lambda-webhook-line.tf
data "archive_file" "webhook_line" {
  type        = "zip"
  source_dir  = "${path.module}/../lambdas/webhook-line/dist"
  output_path = "${path.module}/.build/webhook-line.zip"
}

resource "aws_lambda_function" "webhook_line" {
  function_name    = "${local.prefix}webhook-line"
  filename         = data.archive_file.webhook_line.output_path
  source_code_hash = data.archive_file.webhook_line.output_base64sha256
  runtime          = "nodejs20.x"
  handler          = "handler.handler"
  role             = aws_iam_role.webhook_line.arn
  timeout          = 10
  memory_size      = 256
  architectures    = ["arm64"]

  environment {
    variables = {
      AWS_NODEJS_CONNECTION_REUSE_ENABLED = "1"
    }
  }
}

source_code_hash で差分検出されるので、dist/ の中身が変わった時だけ Lambda が更新される。

GitHub Actions の build step

- name: Build Lambda functions
  run: |
    for fn in webhook-line webhook-backlog deliver scan sync; do
      pushd aws/lambdas/$fn
      npm ci --production
      npm run build      # tsc → dist/
      popd
    done

- name: Tofu apply
  working-directory: aws/tofu
  run: |
    tofu init
    tofu apply -auto-approve

OpenTofu の archive_filedist/ を zip 化、aws_lambda_functionsource_code_hash 差分で更新を判定。

ECR を採用する場面(将来)

  • ECS Fargate Task(Amazon ECS の Fargate launch type、EKS Fargate ではない): 長尺ジョブ(>15 分 Lambda タイムアウト超過)を escalate する場合
  • ネイティブバイナリ依存(headless Chrome 等)が増えた場合の Lambda 検討(現状なし)

ECS Fargate Task を追加する場合の構成(参考)

リソース 命名 役割
ECS Cluster automedia-cluster Fargate の管理単位(料金無料)
ECS Task Definition automedia-deliver-long image_uri / cpu / memory / 環境変数
ECR Repository automedia-deliver-long container image の保存先
VPC + Subnet automedia-vpc Fargate は VPC 必須(Lambda と違う)
Security Group automedia-deliver-long-sg egress: Anthropic API / AWS endpoints
起動方式 Deliver Lambda or Step Functions が RunTask API でオンデマンド起動

VPC 設定が追加で必要(Lambda は VPC 不要)。Phase 1b 着地時点では Lambda 5 個で完結し、Fargate は使わない。長尺ジョブが必要になった時点で VPC + ECS Cluster + automedia-<purpose>-long ECR repository をまとめて追加する想定。

ECS Cluster / Fargate Task の課金モデル

ECS には「常駐サービス」と「ad-hoc タスク」の 2 つの実行モデルがある。automedia は 後者(ad-hoc) を使うので、タスクが動いている時間だけ課金、アイドル時は完全 0 円 になる。

ECS Service(常駐) ECS Task / RunTask(ad-hoc) ← automedia
用途 Web サーバ / API / 常時 worker バッチ・長尺生成(処理が終わったら消える)
desired_count 常時 N タスク維持 不要(呼び出しごとに 1 タスク)
Auto Scaling / ALB あり 不要
アイドル時の課金 あり(タスクが常時動いている) 0 円
起動方法 Service Definition で常時起動 RunTask API(Deliver Lambda or Step Functions から都度起動)

ECS Cluster 自身は料金無料(タスクが所属する論理スコープに過ぎない)。「Cluster を作ったら月額固定が発生する」という誤解は不要。

実行パターン:

Deliver Lambda
  ├─ ジョブが 15 分以内 → そのまま Lambda で完結(通常経路)
  └─ ジョブが 15 分超過の見込み → ecs.RunTask({ cluster: "automedia-cluster", ... })
                                  Fargate Task が起動(数秒のコールドスタート)
                                  処理完了 → タスク終了 → 課金停止
コスト試算(ap-northeast-1, arm64 / Graviton)
  • vCPU: 約 $0.05056/時(arm64 で約 20% 安)
  • メモリ: 約 $0.00553/GB/時
  • 1 vCPU + 2GB × 5 分/ジョブ ≒ $0.005 ≒ 0.8 円/ジョブ
  • 月 100 ジョブ → 約 80 円/月

Lambda メイン構成なので Fargate コストはほぼ無視できる規模。コスト懸念で Cluster 作成を躊躇する必要はない。

セキュリティ・権限

AWS(spin-dd account)

  • AWS profile: spindd(account 695590128753
  • Region: ap-northeast-1
  • IaC: OpenTofutofu CLI、aws/tofu/ 配下)。state backend は S3 native locking、bootstrap module は別ディレクトリで自己ホスト
  • IAM ロール(全て automedia-<purpose>-role 命名):
  • automedia-webhook-line-role — LINE Webhook Lambda 用。Secrets Manager read / S3 read / SQS send / CloudWatch / Claude Platform invoke
  • automedia-webhook-backlog-role — Backlog Webhook Lambda 用。Secrets Manager read / S3 read / EventBridge Scheduler write / Lambda invoke (deliver) / CloudWatch
  • automedia-scan-role — Scan Lambda 用。S3 read / Lambda invoke (deliver) / CloudWatch
  • automedia-deliver-role — Deliver Lambda 用。Secrets Manager read / S3 read / DynamoDB write (lock) / Claude Platform invoke / CloudWatch
  • automedia-sync-role — Sync Lambda 用。S3 write / GitHub Raw fetch / CloudWatch
  • automedia-deploy-role — GitHub Actions OIDC で assume、OpenTofu apply 権限
  • Secrets Manager prefix /automedia/<tenant>/<provider>provider 単位の JSON 形式 で格納(LINE / HubSpot / Backlog 等)。詳細は セキュリティ・シークレット
  • Claude 認証は Claude Platform on AWS の IAM 統合(API キー Secrets 配置不要)

GitHub

  • 各テーマリポの Actions Secrets は 不要(ランタイム責務なし)
  • automedia リポの Secrets: AWS OIDC ロール ARN + GitHub Webhook シークレット(Sync Lambda 署名検証用)

テナント分離

  • IAM ポリシーで Secrets / S3 prefix をテナント別に制限
  • Lambda は invoke 時に tenant を引数で受け、対応する prefix のみアクセス
  • Phase 2 で per-tenant AWS アカウントへ cross-account assume で分散可能

Phase 1b → Phase 2 (進化方向)

Phase 1b (現行) は単一 AWS アカウント内のテナント分離 (IAM Role + Secrets / S3 prefix)。

Phase 2 の選択肢:

  • per-tenant AWS account への cross-account assume で完全分離 (ISMS / SOC2 要件があれば)
  • per-tenant Lambda / Schedule を OpenTofu module で展開
  • ZDR (Zero Data Retention) 対応

詳細は ロードマップ を参照。

このモデルが向かないケース

  • データレジデンシー要件で「データを Anthropic 側で処理させたくない」場合 → Bedrock 経由に切り替え(Claude Platform on AWS は Anthropic 側処理)
  • spin-dd AWS アカウントが使えない、または AWS 採用ポリシー上の制約 → Hybrid 案にフォールバック
  • 完全オフライン環境 → 対象外

決定事項

  • automedia = bootstrap メタプロ + AWS 中央サービス(Phase 1b、2026-05-20 確定)
  • AWS account = spin-dd 既存(profile=spindd, account 695590128753)、region = ap-northeast-1(2026-05-20 確定)
  • Claude 認証 = Claude Platform on AWS の IAM 統合(API キー管理不要、2026-05-20 確定)
  • Claude 呼び出し方式 = Claude Agent SDK ネイティブ(CLI ではなく、Lambda 適合)(2026-05-20 確定)
  • IaC = OpenTofutofu CLI、aws/tofu/ 配下にモジュール管理)(2026-05-20 確定)
  • リソース命名規約 = automedia- プレフィックス統一(グローバル一意な S3 等は spin-dd-automedia-)(2026-05-20 確定)
  • OpenTofu state backend は自己ホストaws/tofu/bootstrap/ で S3 bucket を OpenTofu 自身で管理)(2026-05-20 確定)
  • Lambda デプロイ形式 = ZIP(Node 20 / arm64)。ECR (container image) は ECS Fargate Task のみ(長尺ジョブ、Phase 1b 後半検討)(2026-05-20 確定)
  • Webhook (LINE) / Webhook (Backlog) / Scan / Deliver / Sync すべて AWS Lambda(2026-05-20 確定)
  • Backlog Webhook を主トリガに採用(status 変化 / コメントコマンド / 投稿日時更新の即応)(2026-05-20 確定)
  • 配信時刻トリガは EventBridge 動的 at(投稿日時) schedulerate(5 minutes) ポーリングは採らない(2026-05-20 確定)
  • Scan Lambda は日次セーフティネットrate(1 day)、Webhook 取りこぼし救済)(2026-05-20 確定)
  • GH Actions の役割は OpenTofu deploy と CI 検証のみ。配信ランタイムには関与しない
  • テナント設定 SoT各テーマリポの .automedia/。GitHub Push → Sync Lambda → S3 mirror

未決

  1. Lambda 言語: Node + TypeScript(推奨、SDK ネイティブ) / Python
  2. OpenTofu state 管理: S3 backend + native locking (use_lockfile = true、OpenTofu 1.9+) の bucket 命名と bootstrap 手順。DynamoDB lock table は不要
  3. Backlog Webhook 仕様確認: Backlog (spindd.backlog.com) の Webhook 再送ポリシー / 署名方式 / IssueUpdated の payload 仕様
  4. 長尺ジョブ対応: 15 分超過時に ECS Fargate Task に escalate するかどうか
  5. テナント設定 sync 方式: GitHub Webhook → Sync Lambda(推奨) / Lambda が起動時に GitHub Raw を都度 fetch
  6. Secrets 保管: Secrets Manager vs SSM Parameter Store
  7. 非同期キュー: SQS / EventBridge Bus どちらを採るか
  8. bootstrap CLI の配布形態: npm (npx @spin-dd/automedia) / GitHub Action / degit テンプレート
  9. Phase 1b 移行スケジュール: 馬頭ゴルフ MVP (Minimum Viable Product) 配信稼働前に間に合うか
  10. Bedrock fallback 手順: データレジデンシー要件が出た場合の切替パスを事前定義