インフラストラクチャ
環境一覧
| 環境 | 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/><env>-heineken-interview-arrange-web]
LambdaApp[Lambda<br/><env>-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[完了]
- 3 イメージ(アプリ / migration / sync)を
github.shaとlatestの 2 タグで ECR に push - アプリ Lambda と migration Lambda を
update-function-codeで更新し、wait function-updatedで反映を待つ - sync は ECS タスク定義を
describe-task-definition→ イメージ差し替え →register-task-definitionで新リビジョン登録 - migration Lambda を
invokeしてマイグレーションを実行。出力にFunctionErrorがあればジョブを失敗させる
frontend(.github/workflows/deploy-dev.yml / deploy-stg.yml)
NEXT_PUBLIC_API_URLを ビルド引数として焼き込んで イメージを build- ECR に push(
github.sha/latest) - 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 有効化漏れ