CI/CDシリーズの5本目、これで最終回。1本目はデプロイパイプラインの土台、2本目はPRごとのプレビュー環境、3本目はDB操作のワークフロー化、前回はAI並列開発でのコンフリクト自動解消について書いた。
今回は地味な話。
背景
開発しているプロダクトの機能が増えるにつれて、Stripe決済やGoogle Calendar連携といった外部サービスとの統合が必要になってきた。
これらは単にアプリケーションコードを足すだけでは済まなくて、デプロイパイプライン側にも影響が出る。新しいシークレットの管理が主要なところになる。最初から全部入りで組んでおけば良かったのかもしれないが、必要になってから差し込むのが普通だと思うので、そういう前提でやっていく。
条件付きシークレット注入
StripeやGoogle Calendarやメールで使うResendなどのシークレットは、すべての環境で必須というわけではない。例えばdev環境の初期段階ではResendのシークレットを設定していないことがある。これまでのdeploy.ymlは全シークレットが存在することを前提にしていたので、シークレットが未設定の環境でデプロイすると失敗してしまう。
ということで、deploy.ymlのget-secret-arnsステップにオプショナルなシークレット取得ロジックを足した。GitHub Environment Variablesにシークレット名が設定されている場合のみシークレットを取得する形にする。ここではStripeを例にしているが同様の性質を持つものは同じようにしている。
- name: Get secret ARNs id: get-secret-arns run: | # 必須シークレット(これらは常に存在する前提) DATABASE_URL_ARN=$(aws secretsmanager describe-secret \ --secret-id ${{ vars.DATABASE_URL_SECRET_NAME }} \ --query 'ARN' --output text) echo "database_url_arn=$DATABASE_URL_ARN" >> $GITHUB_OUTPUT # オプショナル: Stripe STRIPE_SECRET_KEY_ARN="" STRIPE_WEBHOOK_SECRET_ARN="" if [ -n "${{ vars.STRIPE_SECRET_KEY_SECRET_NAME }}" ]; then STRIPE_SECRET_KEY_ARN=$(aws secretsmanager describe-secret \ --secret-id ${{ vars.STRIPE_SECRET_KEY_SECRET_NAME }} \ --query 'ARN' --output text 2>/dev/null || echo "") fi if [ -n "${{ vars.STRIPE_WEBHOOK_SECRET_SECRET_NAME }}" ]; then STRIPE_WEBHOOK_SECRET_ARN=$(aws secretsmanager describe-secret \ --secret-id ${{ vars.STRIPE_WEBHOOK_SECRET_SECRET_NAME }} \ --query 'ARN' --output text 2>/dev/null || echo "") fi # 両方揃った場合のみ有効化 if [ -n "$STRIPE_SECRET_KEY_ARN" ] && [ -n "$STRIPE_WEBHOOK_SECRET_ARN" ]; then echo "has_stripe_creds=true" >> $GITHUB_OUTPUT echo "stripe_secret_key_arn=$STRIPE_SECRET_KEY_ARN" >> $GITHUB_OUTPUT echo "stripe_webhook_secret_arn=$STRIPE_WEBHOOK_SECRET_ARN" >> $GITHUB_OUTPUT else echo "has_stripe_creds=false" >> $GITHUB_OUTPUT fi
タスク定義への注入はjqの条件式で制御する。ここがdeploy.ymlの中でも特に読みづらい部分で、jqでJSONを動的に組み立てている。
- name: Build task definition run: | # ベースのシークレット SECRETS='[ {"name": "DATABASE_URL", "valueFrom": "${{ steps.get-secret-arns.outputs.database_url_arn }}"}, {"name": "FIREBASE_CREDENTIALS", "valueFrom": "${{ steps.get-secret-arns.outputs.firebase_arn }}"}, {"name": "HUBSPOT_ACCESS_TOKEN", "valueFrom": "${{ steps.get-secret-arns.outputs.hubspot_arn }}"} ]' # Stripeシークレットの条件付き追加 if [ "${{ steps.get-secret-arns.outputs.has_stripe_creds }}" = "true" ]; then SECRETS=$(echo "$SECRETS" | jq '. + [ {"name": "STRIPE_SECRET_KEY", "valueFrom": "${{ steps.get-secret-arns.outputs.stripe_secret_key_arn }}"}, {"name": "STRIPE_WEBHOOK_SECRET", "valueFrom": "${{ steps.get-secret-arns.outputs.stripe_webhook_secret_arn }}"} ]') fi # タスク定義に注入 cat task-definition.json | jq \ --argjson secrets "$SECRETS" \ '.containerDefinitions[0].secrets = $secrets' \ > updated-task-definition.json
これでStripe未設定の環境でもデプロイが正常に通る。片方だけ設定されている場合はスキップ、両方揃って初めて有効化、というシンプルな仕組み。Google Calendarなども同じパターンで実装している。
Dockerビルドの罠
その他に地味だがハマった問題として、Dockerfile内でseedスクリプトのtscコンパイルがdist/ディレクトリを上書きしてしまい、NestJSのビルド出力が消えるというのがあった。
# Before(問題のあった構成) FROM node:22-alpine AS builder WORKDIR /app COPY . . RUN yarn install RUN npx prisma generate RUN yarn build # NestJSビルド → dist/ RUN cd prisma && tsc # seedのtsc → dist/ を上書き!
NestJSのビルド出力もseedスクリプトのコンパイル出力も同じdist/を使っていて、後からコンパイルしたseedスクリプトが先にビルドされたNestJSの出力を上書きしてしまっていた。
# After(修正後) FROM node:22-alpine AS builder WORKDIR /app COPY . . RUN yarn install RUN npx prisma generate RUN yarn build # NestJSビルドにseedスクリプトも含まれる # seed用の個別tscは削除
seed用の個別tscを削除して、seedスクリプトをNestJSのビルドに含める構成にした。NestJSのtsconfig.build.jsonでseedスクリプトのパスも入れておけば、yarn build一発で全部ビルドされるので、これでよさそう。
シリーズのまとめ
5本にわたってCI/CD環境の構築と発展について書いてきた。最後に設計上の主な判断をまとめておく。
| 判断 | 選択 | 理由 |
|---|---|---|
| デプロイワークフロー | 単一 deploy.yml + GitHub Environments |
dev/prodで同一ロジック。環境差分はEnvironment変数で吸収 |
| AWSアカウント | dev / prodで分離 | 本番環境の隔離性確保、IAMポリシーの最小権限化 |
| 認証方式 | GitHub Actions OIDC | 長期クレデンシャル不使用 |
| プレビュー環境のDB | スキーマ分離(pr_{番号}) |
クラスタ単位の分離はコスト過大 |
| DB操作の実行環境 | ECS Run Task | VPC内から安全にAuroraへアクセス |
| コンテナの多目的利用 | entrypoint.sh + DB_COMMAND |
1イメージで全用途に対応 |
| コンフリクト解消 | Devin API自動化 | AI並列開発でのコンフリクト頻発に対応 |
| オプションサービス | 条件付きシークレット注入 | 未設定環境でのデプロイ失敗を防止 |
振り返ってみると、当然ながら設計段階で全部を見通せたわけじゃなくて、実際に動かしてみて初めて見つかる問題も結構あった。Vercel環境変数の事前設定、ALBルール優先度の上限、mergeableのnull状態、Dockerビルドの出力ディレクトリ衝突。並べると結構ある。
これらは設計時点での見落としではあるが、最初から全部を見通そうとすると永遠に作り始められないので、コンセプトを決めたらまず動かして、問題が出たら直す。そういうサイクルを素早く回せる方が良い。生成AIがそれを可能にしたし、新規構築なら尚更だ。こうやって後から外部サービスを差し込んでも何とかなった、という感じ。
さて、初期構築での話は一通り出し切ったのでシリーズはここでひと区切り。とはいえまだいじりたいところはちょこちょこあって、例えば3本目のDB操作ワークフローには機微な情報が流入しないよう仕組みで縛りたい、というのが残っている。custom-script や shell で任意コマンドを受けている都合上、インプットファイルを用意して処理させるような物だとそのインプットファイル自体もコミットする必要がある。そのインプットファイルに機微な情報が含まれていたらアウトだ。また、引数次第ではログに何が残るかわからないので、ここも押さえておきたい。この辺りは今は自分が目を光らせているのでまだ大丈夫だがやはり仕組み化したいところ。issueには起票済みなので何かやったらまた書く。