コンテンツにスキップ

インフラストラクチャ

環境一覧

環境 URL 用途
dev(開発) フロント: https://interview-arrange-web.heineken.dev.4digit.ai / API: https://interview-arrange-api.heineken.dev.4digit.ai dev への push で自動デプロイされる検証環境。外部連携はすべて無効
stg(検証) フロント: https://interview-arrange-web.heineken.stg.4digit.ai / API: https://interview-arrange-api.heineken.stg.4digit.ai stg への push で自動デプロイ。メール送信と Google Calendar への書き込みが実際に動く
ローカル フロント: http://localhost:3000 / API: http://localhost:8080 ローカル開発

Note

本番(prod)環境は未構築。prod ブランチだけ先に用意してある。

dev と stg は VPC も DB も完全に独立している。共用しているのは Databricks のサービスプリンシパルと Google のサービスアカウント/OAuth クライアントだけ。

環境ごとの機能フラグ

変数 dev stg 効果
PROTOTYPE_MODE true(既定) false false にするとメールとカレンダーの副作用が有効になる
EMAIL_ENABLED 未設定 true PROTOTYPE_MODE=false かつこれが true のときだけ実送信する
GMAIL_SENDER_EMAIL 空 送信元アドレス サービスアカウントがこのアドレスを代理送信する
ALLOWED_SURVEY_IDS 未設定(制限なし) 連携する調査 ID 一覧・詳細・メタ同期の対象をこの ID に限定する

PROTOTYPE_MODE はメールとカレンダーをまとめて制御する。stg でメールだけを有効にすることはできず、カレンダーへの書き込みも同時に有効になる。

Terraform(genai-infrastructure)

AWS リソースは別リポジトリ genai-infrastructure の Terraform モノレポで管理する。1 ディレクトリ = 1 tfstate。

スタック tfstate
aws/envs/dev/heineken-interview-arrange-backend 4d-dev-genai-terraform / dev.heineken-interview-arrange-backend.tfstate
aws/envs/dev/heineken-interview-arrange-web 同上(...-web.tfstate)
aws/envs/stg/heineken-interview-arrange-backend 4d-stg-genai-terraform / staging.heineken-interview-arrange-backend.tfstate
aws/envs/stg/heineken-interview-arrange-web 同上(staging....-web.tfstate)
  • VPC CIDR は dev が 10.20.0.0/16、stg が 10.21.0.0/16
  • シークレットは各スタックの secrets/values.yml に SOPS(KMS)で暗号化して置く。dev と stg で KMS キーが違うため、dev のファイルをコピーしただけでは復号できない
  • 秘匿情報でない設定値(ALLOWED_SURVEY_IDS など)は locals.tf に平文で置く。SOPS の復号・再暗号化なしに変更できる
  • Lambda のイメージ更新はアプリ側の GitHub Actions が行うため、Terraform 側は lifecycle { ignore_changes = [image_uri] } を付けている

初回構築は 2 段階

ECR にイメージがないと Lambda も App Runner サービスも作れない。terraform apply -target=module.ecr でリポジトリだけ作り、イメージを push してから全体を apply する。App Runner のカスタムドメインはさらに、-target=aws_apprunner_custom_domain_association.web を先に流して証明書検証レコードが確定してから全体を apply する必要がある(count が apply 前に決まらないため)。

構成図

graph TD
  User[管理画面ユーザー]
  Candidate[候補者]
  AR[App Runner<br/>&lt;env&gt;-heineken-interview-arrange-web]
  LambdaApp[Lambda<br/>&lt;env&gt;-heineken-interview-arrange-backend]
  LambdaMig[Lambda<br/>...-migration]
  ECS[ECS Fargate タスク<br/>...-sync]
  RDS[(PostgreSQL)]
  DBX[Databricks<br/>Unity Catalog]
  Google[Google APIs<br/>OAuth / Calendar / Gmail]
  ECR[ECR<br/>3 リポジトリ]

  User --> AR
  Candidate --> AR
  AR -->|REST| LambdaApp
  LambdaApp --> RDS
  LambdaApp --> Google
  LambdaApp --> DBX
  LambdaApp -->|RunTask| ECS
  LambdaMig --> RDS
  ECS --> DBX
  ECS --> RDS
  ECR -.イメージ.-> AR
  ECR -.イメージ.-> LambdaApp
  ECR -.イメージ.-> LambdaMig
  ECR -.イメージ.-> ECS

主要サービス一覧

サービス名 用途
AWS Lambda(コンテナイメージ) API 本体。aws-lambda-adapter を拡張として同梱し、Bun の HTTP サーバをそのまま動かす
AWS Lambda(migration) scripts/migrate.ts を実行するマイグレーション専用関数
Amazon ECS(Fargate) 候補者インポートと調査メタ更新のバッチ。Dockerfile.sync の container override でスクリプトパスと JSON 引数を渡す
AWS App Runner フロントエンド(Next.js)のホスティング
Amazon ECR 4 つのイメージの保管(backend アプリ / migration / sync / frontend web)
PostgreSQL データ永続化
Databricks Creative Survey / Ask One の回答データ。読み取り専用(Databricks 連携)
Google Workspace OAuth 認証、Calendar、Gmail

コンテナイメージ

リポジトリ名は環境ごとにプレフィックスが付く(dev- / stg-)。

Dockerfile ECR リポジトリ ベース 実行
Dockerfile <env>-heineken-interview-arrange-backend-lambda oven/bun:1-alpine + aws-lambda-adapter bun run src/server.ts
Dockerfile.migration <env>-heineken-interview-arrange-backend-migration oven/bun:1-alpine bun run scripts/migrate.ts
Dockerfile.sync <env>-heineken-interview-arrange-backend-sync oven/bun:1-alpine ENTRYPOINT ["bun", "run"]。スクリプトパスは override で渡す
frontend Dockerfile <env>-heineken-interview-arrange-web Next.js App Runner で起動

ブランチ運用

dev → stg → prod の一方向で流す。

graph LR
  F[作業ブランチ] -->|PR| D[dev]
  D -->|PR| S[stg]
  S -->|PR| P[prod]
  • 3 ブランチとも保護(PR 必須・承認 1 人・force push 禁止・削除禁止)。管理者は対象外
  • デフォルトブランチは dev。作業ブランチはここから切る
  • delete_branch_on_merge を有効にしてあるので、PR をマージすると作業ブランチは自動で消える

デプロイフロー

ブランチ デプロイ先 タイミング ワークフロー
dev dev の Lambda ×2 + ECS タスク定義 / App Runner 自動(push 時) deploy-dev.yml
stg stg の同上 自動(push 時) deploy-stg.yml
prod 未構築 - -

2 つのワークフローは対象のリソース名(dev- / stg-)と concurrency グループが違うだけで、手順は同じ。

backend(.github/workflows/deploy-dev.yml / deploy-stg.yml)

graph TD
  A[dev または stg へ push] --> B[AWS 認証 / ECR ログイン]
  B --> C[アプリイメージを build & push]
  B --> D[migration イメージを build & push]
  B --> E[sync イメージを build & push]
  C --> F[アプリ Lambda の update-function-code]
  D --> G[migration Lambda の update-function-code]
  E --> H[ECS タスク定義を新リビジョンで登録]
  F --> I[migration Lambda を invoke]
  G --> I
  H --> I
  I --> J{FunctionError?}
  J -->|あり| K[ジョブ失敗]
  J -->|なし| L[完了]
  1. 3 イメージ(アプリ / migration / sync)を github.sha と latest の 2 タグで ECR に push
  2. アプリ Lambda と migration Lambda を update-function-code で更新し、wait function-updated で反映を待つ
  3. sync は ECS タスク定義を describe-task-definition → イメージ差し替え → register-task-definition で新リビジョン登録
  4. migration Lambda を invoke してマイグレーションを実行。出力に FunctionError があればジョブを失敗させる

frontend(.github/workflows/deploy-dev.yml / deploy-stg.yml)

  1. NEXT_PUBLIC_API_URL を ビルド引数として焼き込んで イメージを build
  2. ECR に push(github.sha / latest)
  3. App Runner サービスを名前から検索し、start-deployment を実行

NEXT_PUBLIC_API_URL はビルド時に確定する

フロントエンドの API URL はビルド引数で埋め込まれるため、実行時の環境変数では変更できない。向き先を変えるにはイメージを再ビルドする。

必要な GitHub Secrets

Secret 名 用途
AWS_ACCESS_KEY_ID ECR / Lambda / ECS / App Runner の操作
AWS_SECRET_ACCESS_KEY 同上

リージョンは両ワークフローとも ap-northeast-1 固定。

バッチの実行

src/batch/*.ts は API とは独立して動く。getEnvConfig() を経由せず DATABASE_URL だけで自前の pg プールを張るため、アプリ用の必須環境変数が揃わない実行環境でも動作する。引数は JSON 文字列 1 個で渡す。

bun run src/batch/import.ts '{"action":"import","importLogId":1,"projectId":1,"surveyId":"123","mapping":{}}'
bun run src/batch/databricks-meta-sync.ts '{"dryRun":true}'
bun run src/batch/pii-cleanup.ts '{"dryRun":true}'

services/s3-sync.ts が batch/import.ts のキッカーで、開発時は spawn でローカル実行、本番は ECS Fargate タスクとして起動する(ファイル名は経緯上 s3-sync だが S3 とは無関係)。

databricks-meta-sync.ts はアプリから起動されない。Databricks 側のパイプラインの後に ECS タスクとして走らせる想定だが、その連携は未設定。

デプロイは直列化してある

2 つのデプロイが並行すると UpdateFunctionCode が ResourceConflictException で失敗する。ワークフローに concurrency: deploy-dev / deploy-stg(cancel-in-progress: false)を入れて直列化した。キャンセルすると app Lambda だけ更新されてマイグレーションが走らない状態になりうるため、後続はキューで待つ。環境ごとにグループが違うので dev と stg は並行できる。

マイグレーションの成否は MIGRATION_COMPLETE で判定する

migration イメージは Lambda ハンドラではなく素の scripts/migrate.ts を実行するため、成功してもコンテナ終了時に Lambda 側はエラーになる。CI は CloudWatch のログに MIGRATION_COMPLETE があるかで判定している。

git push は実行しない

運用ルールとして、エージェント/自動化からの git push は行わない。dev / stg への push はそのままデプロイを意味するため、人が明示的に行う。

Google Workspace の設定

サービスアカウントは自分のカレンダーや Gmail を使わない。ドメイン内のユーザーになりすまして(impersonation)操作するため、Workspace 管理コンソールでドメイン全体の委任が必要になる。

操作 なりすます相手
メール送信 GMAIL_SENDER_EMAIL
プロジェクトカレンダーの作成・イベントの読み書き プロジェクト作成者
空き時間(freebusy)の照会 各面接官

管理コンソール(セキュリティ → アクセスとデータ管理 → API の制御 → ドメイン全体の委任)で、サービスアカウントのクライアント ID に対して次のスコープを許可する。

https://www.googleapis.com/auth/gmail.send
https://www.googleapis.com/auth/gmail.compose
https://www.googleapis.com/auth/calendar
https://www.googleapis.com/auth/calendar.events
https://www.googleapis.com/auth/calendar.freebusy

GCP 側では Gmail API と Google Calendar API を有効化し、OAuth クライアントの承認済みリダイレクト URI に各環境の /api/v1/auth/google/callback を登録する。

委任の設定漏れはログで切り分けられる

  • unauthorized_client: Client is unauthorized to retrieve access tokens... → 委任のスコープが足りない。スコープは 1 つでも欠けるとその API だけ失敗するため、カレンダーは動くのにメールだけ落ちる、といった状態になる
  • Delegation denied for <address> → 指定したアドレスが実在しない
  • ... API has not been used in project ... before or it is disabled → GCP 側の API 有効化漏れ