こんにちは!SRE一年生が終わりかけている墨(YOUTRUST/X)です!
「terraform plan
は成功したのに、いざterraform apply
したらエラーで失敗...」
こんな経験に悩まされているインフラエンジニアの方は多いのではないでしょうか?
今回はその打開策として弊社で行なった取り組みをご紹介します。
修正前の状態
現状把握のため、弊社のインフラ構成を整理します。
弊社では過去記事でも記載しましたが、インフラをTerraformにて管理しています。
- 環境
- ほぼ同じインフラ構成のsandboxとproductionが存在しています。
- リポジトリ
- Terraformリソースをモジュール化し、環境変数を注入することでリポジトリ1つで2つの環境を管理運用しています。
- ブランチ
- sandbox・main・feature/XXXが存在しており、mainブランチはproduction、featureブランチが作業ブランチとなります。
- デプロイ
- デプロイはGitHub Actionsを使用し、sandboxデプロイ後に検証後、問題なければproduction環境に対応するmainにマージすることで自動で反映されるようになっています。
- デプロイフロー
- ローカルで作業ブランチを切り修正
- sandboxブランチに向けてプッシュ
- PR作成
- レビュー
- sandboxへマージ&デプロイ
- mainブランチへ向けたPRが自動作成
- レビュー
- production(mainブランチ)へマージ&デプロイ
tech.youtrust.co.jp tech.youtrust.co.jp
課題
一見問題なさそうですが、マージ後にデプロイ(terraform apply
)されることでマージ後に失敗した場合問題のあるコードがマージされ、PR作成からやり直す必要があります。
実際に遭遇したエラーは下記のようなものがあり、plan段階ではエラーを検知できませんでした。
- RDSストレージタイプ変更時:gp2からgp3への変更で、
plan
では問題なく表示されるが、apply
で互換性エラーが発生 - セキュリティグループ依存関係:削除順序の問題により
apply
が失敗 - IAMポリシー整合性:記述は正しいが、実際のリソースとの整合性で
apply
時にエラーが発生
デプロイ戦略を変更
考えられる解決策は2点あります。
1. テストコードを書く
2. デプロイフローを変更
今回悩まされた問題の多くは、Terraformの記述方法は正しいものの、AWSリソースの実際の制約や依存関係によってエラーとなる場合でした。
テストコードは品質保証を行えるが、継続的なメンテナンスコストが発生し、全てのエッジケースを考慮した網羅性のあるテストコードを書くことは現状工数的にも難しいと判断し、2. を選択しました。
そこでブランチデプロイ戦略を導入しました。
ブランチデプロイ戦略とは、従来の「マージ後デプロイ」から「デプロイ後マージ」に変更する手法です。つまり、PRをマージする前に実際の環境でデプロイを実行し、成功を確認してからマージを行います。
弊社環境に落とし込むと下記のようなフローになります。
- ローカルで作業ブランチを切り修正
- sandboxブランチに向けてプッシュ
- PR作成
- レビュー
- sandbox環境へデプロイ
- sandboxブランチへマージ
- mainブランチへ向けたPRが自動作成
- レビュー
- productionへデプロイ
- mainブランチへマージ
このようにデプロイ後にマージすることで常に正しく、検証されたコードがブランチへ反映されます。
また、apply時にエラーが出た場合も最小の手戻りで修正・検証を行えます。
デプロイ方法の実装についてはbranch deploy actionを使用し、PRコメントからコマンド入力でterraform plan
、terraform apply
を実行できるようにしました。
セキュリティ対策として、production環境のみterraform apply
は管理者権限を持つユーザーのみ実行可能としました。
実装した内容は下記となりますので参考にしてください。
※ assume role名、GitHubチーム名、リポジトリ名はマスクしてあります。
sandbox向けGitHub Action Workflow
name: Command Plan and Apply for Sandbox Terraform on: issue_comment: types: [created] env: SANDBOX_DIR: environments/sandbox PROD_DIR: environments/prod permissions: id-token: write contents: write pull-requests: write deployments: write statuses: read checks: read issues: write jobs: # Branch Deployの検証ジョブ branch_deploy_check: name: branch-deploy-check runs-on: ubuntu-latest if: ${{ github.event.issue.pull_request && (contains(github.event.comment.body, '.apply') || contains(github.event.comment.body, '.plan'))}} outputs: continue: ${{ steps.branch-deploy.outputs.continue }} noop: ${{ steps.branch-deploy.outputs.noop }} sha: ${{ steps.branch-deploy.outputs.sha }} pr_number: ${{ steps.get_pr_target_branch.outputs.pr_number }} steps: # ターゲットブランチを取得 - name: Get PR details id: get_pr_target_branch run: | PR_NUMBER=${{ github.event.issue.number }} PR_INFO=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ "https://api.github.com/repos/${{ github.repository }}/pulls/${PR_NUMBER}") BASE_BRANCH=$(echo "$PR_INFO" | jq -r .base.ref) echo "BASE_BRANCH: ${BASE_BRANCH}" echo "base_branch=$BASE_BRANCH" >> $GITHUB_OUTPUT echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT # ターゲットブランチがsandboxであるか確認 - name: Check base branch sandbox if: steps.get_pr_target_branch.outputs.base_branch != 'sandbox' run: | echo "Target branch: ${{ steps.get_pr_target_branch.outputs.base_branch }}" echo "❌ ターゲットブランチがsandboxではないためこのワークフローを停止しました。" exit 1 # Branch Deploy Actionの設定 - name: branch-deploy id: branch-deploy uses: github/branch-deploy@v10.4.2 with: trigger: '.apply' noop_trigger: '.plan' stable_branch: sandbox allow_non_default_target_branch_deployments: true # Sandbox環境のPlan実行ジョブ plan_sandbox: name: plan-sandbox runs-on: ubuntu-latest needs: branch_deploy_check if: ${{ needs.branch_deploy_check.outputs.continue == 'true' && needs.branch_deploy_check.outputs.noop == 'true' }} steps: # ブランチをチェックアウト - name: Checkout uses: actions/checkout@v4 with: ref: ${{ needs.branch_deploy_check.outputs.sha }} # AWS認証情報の設定(Sandbox) - name: Configure aws credentials for sandbox uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::XXXXXXXXXX:role/[your-assume-role] aws-region: ap-northeast-1 # Terraformのセットアップ - name: Setup terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.11.1 # tfcmtのセットアップ - name: Setup tfcmt uses: shmokmt/actions-setup-tfcmt@v2 # github-commentのセットアップ - name: Setup github-comment uses: shmokmt/actions-setup-github-comment@v2 # Sandbox環境のTerraform初期化 - name: Initialize Sandbox run: terraform init -upgrade working-directory: ${{ env.SANDBOX_DIR }} # 古いコメントを非表示にする - name: Hide old comments run: | github-comment hide -org [your-team] -repo [your-repo] -pr ${{ needs.branch_deploy_check.outputs.pr_number }} -token ${GITHUB_TOKEN} \ -condition 'Comment.HasMeta && (Comment.Meta.SHA1 != Commit.SHA1)' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Sandbox環境のTerraform planを実行 - name: Terraform plan for sandbox id: plan_sandbox run: | tfcmt -log-level DEBUG -owner [your-team]-repo [your-repo] -pr ${{ needs.branch_deploy_check.outputs.pr_number }} -var target:sandbox plan -- terraform plan -no-color continue-on-error: true working-directory: ${{ env.SANDBOX_DIR }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Terraform planのエラーチェック(Sandbox) - name: Check Terraform plan output for sandbox if: steps.plan_sandbox.outcome == 'failure' run: | tfcmt -log-level DEBUG -owner [your-team] -repo [your-repo] -pr ${{ needs.branch_deploy_check.outputs.pr_number }} -var target:sandbox error -- "Terraform plan for sandbox failed" exit 1 working-directory: ${{ env.SANDBOX_DIR }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Prod環境のPlan実行ジョブ plan_prod: name: plan-prod runs-on: ubuntu-latest needs: branch_deploy_check if: ${{ needs.branch_deploy_check.outputs.continue == 'true' && needs.branch_deploy_check.outputs.noop == 'true' }} steps: # ブランチをチェックアウト - name: Checkout uses: actions/checkout@v4 with: ref: ${{ needs.branch_deploy_check.outputs.sha }} # AWS認証情報の設定(Prod) - name: Configure aws credentials for prod uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::XXXXXXXXXX:role/[your-assume-role] aws-region: ap-northeast-1 # Terraformのセットアップ - name: Setup terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.11.1 # tfcmtのセットアップ - name: Setup tfcmt uses: shmokmt/actions-setup-tfcmt@v2 # github-commentのセットアップ - name: Setup github-comment uses: shmokmt/actions-setup-github-comment@v2 # Prod環境のTerraform初期化 - name: Initialize Prod run: terraform init -upgrade working-directory: ${{ env.PROD_DIR }} # Prod環境のTerraform planを実行 - name: Terraform plan for prod id: plan_prod run: | tfcmt -log-level DEBUG -owner [your-team] -repo [your-repo] -pr ${{ needs.branch_deploy_check.outputs.pr_number }} -var target:production plan -- terraform plan -no-color continue-on-error: true working-directory: ${{ env.PROD_DIR }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Terraform planのエラーチェック(Prod) - name: Check Terraform plan output for prod if: steps.plan_prod.outcome == 'failure' run: | tfcmt -log-level DEBUG -owner [your-team] -repo [your-repo] -pr ${{ needs.branch_deploy_check.outputs.pr_number }} -var target:production error -- "Terraform plan for production failed" exit 1 working-directory: ${{ env.PROD_DIR }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Sandbox環境へのApply実行ジョブ apply_sandbox: name: apply-sandbox runs-on: ubuntu-latest needs: branch_deploy_check if: ${{ needs.branch_deploy_check.outputs.continue == 'true' && needs.branch_deploy_check.outputs.noop != 'true' }} steps: # ブランチをチェックアウト - name: Checkout uses: actions/checkout@v4 with: ref: ${{ needs.branch_deploy_check.outputs.sha }} # AWS認証情報の設定(Sandbox) - name: Configure aws credentials for sandbox uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::XXXXXXXXXX:role/[your-assume-role] aws-region: ap-northeast-1 # Terraformのセットアップ - name: Setup terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.11.1 # tfcmtのセットアップ - name: Setup tfcmt uses: shmokmt/actions-setup-tfcmt@v2 # github-commentのセットアップ - name: Setup github-comment uses: shmokmt/actions-setup-github-comment@v2 # Sandbox環境のTerraform初期化 - name: Initialize Sandbox run: terraform init -upgrade working-directory: ${{ env.SANDBOX_DIR }} # 古いコメントを非表示にする - name: Hide old comments run: | github-comment hide -org [your-team] -repo [your-repo] -pr ${{ needs.branch_deploy_check.outputs.pr_number }} -token ${GITHUB_TOKEN} \ -condition 'Comment.HasMeta && (Comment.Meta.SHA1 != Commit.SHA1)' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Terraform applyを実行(Sandboxのみ) - name: Terraform apply for sandbox id: apply run: | tfcmt -log-level DEBUG -owner [your-team]-repo [your-repo] -pr ${{ needs.branch_deploy_check.outputs.pr_number }} -var target:sandbox apply -- terraform apply -auto-approve -no-color continue-on-error: true working-directory: ${{ env.SANDBOX_DIR }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Terraform applyのエラーチェック - name: Check Terraform apply output if: steps.apply.outcome == 'failure' run: | tfcmt -log-level DEBUG -owner [your-team] -repo [your-repo] -pr ${{ needs.branch_deploy_check.outputs.pr_number }} -var target:sandbox error -- "Terraform apply failed" exit 1 working-directory: ${{ env.SANDBOX_DIR }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
production向けGitHub Action Workflow
name: Command Plan and Apply for Production Terraform on: issue_comment: types: [created] env: PROD_DIR: environments/prod permissions: id-token: write contents: write pull-requests: write deployments: write statuses: read checks: read issues: write jobs: # Branch Deployの検証ジョブ branch_deploy_check: name: branch-deploy-check runs-on: ubuntu-latest if: ${{ github.event.issue.pull_request && (contains(github.event.comment.body, '.apply') || contains(github.event.comment.body, '.plan')) }} outputs: continue: ${{ steps.branch-deploy.outputs.continue }} noop: ${{ steps.branch-deploy.outputs.noop }} sha: ${{ steps.branch-deploy.outputs.sha }} pr_number: ${{ steps.get_pr_target_branch.outputs.pr_number }} is_apply: ${{ steps.check-command.outputs.is_apply }} has_admin_permission: ${{ steps.check-admin-permission.outputs.has_admin_permission }} steps: # ターゲットブランチの確認 - name: Get PR details id: get_pr_target_branch run: | PR_NUMBER=${{ github.event.issue.number }} PR_INFO=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ "https://api.github.com/repos/${{ github.repository }}/pulls/${PR_NUMBER}") BASE_BRANCH=$(echo "$PR_INFO" | jq -r .base.ref) echo "BASE_BRANCH: ${BASE_BRANCH}" echo "base_branch=$BASE_BRANCH" >> $GITHUB_OUTPUT echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT # ターゲットブランチがmainであるか確認 - name: Check base branch main if: steps.get_pr_target_branch.outputs.base_branch != 'main' run: | echo "❌ ターゲットブランチがmainではないためこのワークフローを停止しました。" exit 1 # コマンドタイプの確認 - name: Check command type id: check-command run: | if [[ "${{ github.event.comment.body }}" == *".apply"* ]]; then echo "is_apply=true" >> $GITHUB_OUTPUT else echo "is_apply=false" >> $GITHUB_OUTPUT fi # Branch Deploy Actionの設定 - name: branch-deploy id: branch-deploy uses: github/branch-deploy@v10.4.2 with: trigger: '.apply' noop_trigger: '.plan' stable_branch: main allow_non_default_target_branch_deployments: true # 管理者権限チェック(applyコマンドの場合のみ) - name: Check admin permission id: check-admin-permission if: steps.branch-deploy.outputs.continue == 'true' && steps.check-command.outputs.is_apply == 'true' run: | # コマンド実行者の情報を取得 COMMENTER="${{ github.event.comment.user.login }}" # リポジトリのコラボレーター情報を取得して権限をチェック COLLAB_INFO=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ "https://api.github.com/repos/[your-team]/[your-repo]/collaborators/$COMMENTER/permission") # 権限レベルを取得 PERMISSION=$(echo "$COLLAB_INFO" | jq -r '.permission') # 管理者権限を持っているかチェック if [[ "$PERMISSION" == "admin" ]]; then echo "✅ 管理者権限を持つユーザーです!コマンド実行を許可します。" echo "has_admin_permission=true" >> $GITHUB_OUTPUT exit 0 else echo "❌ 管理者権限を持つユーザーのみがapplyコマンドを実行できます!" echo "現在の権限: $PERMISSION" echo "has_admin_permission=false" >> $GITHUB_OUTPUT exit 1 fi # Production環境のPlan実行ジョブ plan_prod: name: plan-prod runs-on: ubuntu-latest needs: branch_deploy_check if: ${{ needs.branch_deploy_check.outputs.continue == 'true' && needs.branch_deploy_check.outputs.noop == 'true' && needs.branch_deploy_check.outputs.is_apply == 'false' }} steps: # ブランチをチェックアウト - name: Checkout uses: actions/checkout@v4 with: ref: ${{ needs.branch_deploy_check.outputs.sha }} # AWS認証情報の設定(Prod) - name: Configure aws credentials for prod uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::XXXXXXXXXX:role/[your-assume-role] aws-region: ap-northeast-1 # Terraformのセットアップ - name: Setup terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.11.1 # tfcmtのセットアップ - name: Setup tfcmt uses: shmokmt/actions-setup-tfcmt@v2 # github-commentのセットアップ - name: Setup github-comment uses: shmokmt/actions-setup-github-comment@v2 # Prod環境のTerraform初期化 - name: Initialize Prod run: terraform init -upgrade working-directory: ${{ env.PROD_DIR }} # 古いコメントを非表示にする - name: Hide old comments run: | github-comment hide -org [your-team] -repo [your-repo] -pr ${{ needs.branch_deploy_check.outputs.pr_number }} -token ${GITHUB_TOKEN} \ -condition 'Comment.HasMeta && (Comment.Meta.SHA1 != Commit.SHA1)' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Prod環境のTerraform planを実行 - name: Terraform plan for prod id: plan_prod run: | tfcmt -log-level DEBUG -owner [your-team] -repo [your-repo] -pr ${{ needs.branch_deploy_check.outputs.pr_number }} -var target:production plan -- terraform plan -no-color working-directory: ${{ env.PROD_DIR }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Production環境へのApply実行ジョブ apply_prod: name: apply-prod runs-on: ubuntu-latest needs: branch_deploy_check if: ${{ needs.branch_deploy_check.outputs.continue == 'true' && needs.branch_deploy_check.outputs.noop != 'true'&& needs.branch_deploy_check.outputs.has_admin_permission == 'true' && needs.branch_deploy_check.outputs.is_apply == 'true' }} steps: # ブランチをチェックアウト - name: Checkout uses: actions/checkout@v4 with: ref: ${{ needs.branch_deploy_check.outputs.sha }} # AWS認証情報の設定(Prod) - name: Configure aws credentials for prod uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::XXXXXXXXXX:role/[your-assume-role] aws-region: ap-northeast-1 # Terraformのセットアップ - name: Setup terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.11.1 # tfcmtのセットアップ - name: Setup tfcmt uses: shmokmt/actions-setup-tfcmt@v2 # github-commentのセットアップ - name: Setup github-comment uses: shmokmt/actions-setup-github-comment@v2 # Prod環境のTerraform初期化 - name: Initialize Prod run: terraform init -upgrade working-directory: ${{ env.PROD_DIR }} # 古いコメントを非表示にする - name: Hide old comments run: | github-comment hide -org [your-team] -repo [your-repo] -pr ${{ needs.branch_deploy_check.outputs.pr_number }} -token ${GITHUB_TOKEN} \ -condition 'Comment.HasMeta && (Comment.Meta.SHA1 != Commit.SHA1)' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Terraform applyを実行(Prod) - name: Terraform apply for prod id: apply run: | tfcmt -log-level DEBUG -owner [your-team] -repo [your-repo] -pr ${{ needs.branch_deploy_check.outputs.pr_number }} -var target:production apply -- terraform apply -auto-approve -no-color working-directory: ${{ env.PROD_DIR }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
先人のブログを参考にさせていただいたので載せておきます。 kakakakakku.hatenablog.com
まとめ
今回はデプロイ戦略を変更することでマージ後にエラーとなる課題を解消しました。
導入後の効果としては下記が挙げられます。
- 手戻り工数の削減:マージ後のデプロイ失敗によるPR作成からのやり直しを根絶
- 検証品質の向上:sandbox環境でのマージ前検証により、問題の早期発見が可能
- デプロイ安定性の向上:検証済みコードのみがブランチにマージされる仕組みを確立
また、現在Claude Codeでの網羅的なレビュー体制も整えている段階です。
弊社では積極的に新しいことを試していける環境が整っています。
もし挑戦したいという方がいれば最高の環境なので下記を覗いてみてください!
youtrust.jp