Stripeやサードパーティ連携を追加するときにデプロイパイプラインで困ったこと

Keisuke Nishitani

Keisuke Nishitani

プロフィール →

CI/CDシリーズの5本目、これで最終回。1本目はデプロイパイプラインの土台2本目はPRごとのプレビュー環境3本目はDB操作のワークフロー化前回はAI並列開発でのコンフリクト自動解消について書いた。

今回は地味な話。

背景

開発しているプロダクトの機能が増えるにつれて、Stripe決済やGoogle Calendar連携といった外部サービスとの統合が必要になってきた。

これらは単にアプリケーションコードを足すだけでは済まなくて、デプロイパイプライン側にも影響が出る。新しいシークレットの管理が主要なところになる。最初から全部入りで組んでおけば良かったのかもしれないが、必要になってから差し込むのが普通だと思うので、そういう前提でやっていく。

条件付きシークレット注入

StripeやGoogle Calendarやメールで使うResendなどのシークレットは、すべての環境で必須というわけではない。例えばdev環境の初期段階ではResendのシークレットを設定していないことがある。これまでのdeploy.ymlは全シークレットが存在することを前提にしていたので、シークレットが未設定の環境でデプロイすると失敗してしまう。

ということで、deploy.ymlget-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-scriptshell で任意コマンドを受けている都合上、インプットファイルを用意して処理させるような物だとそのインプットファイル自体もコミットする必要がある。そのインプットファイルに機微な情報が含まれていたらアウトだ。また、引数次第ではログに何が残るかわからないので、ここも押さえておきたい。この辺りは今は自分が目を光らせているのでまだ大丈夫だがやはり仕組み化したいところ。issueには起票済みなので何かやったらまた書く。

©Keisuke Nishitani, 2023   プライバシーポリシー