実践で学ぶDevOpsツールの使いこなし術 18

「GitHub Actions」で「Terraform」を使用してCI/CD統合を試してみよう

第18回の今回は、「GitHub Actions」で「Terraform」を使用したCI/CD統合の実行方法について解説します。

田中 智明

1月6日 6:30

はじめに

第16回では、Terraformのコードをモジュール化し、ディレクトリ分離によって環境を適切に管理する方法を学びました。また、第17回では状態ファイルをS3に保存するリモートバックエンドを設定し、チーム全体で状態を共有して同時実行を防ぐロック機構を実現しました。

これらの仕組みにより、Terraformをチーム開発で使用するための基盤は整いましたが、terraformコマンドの操作は依然として手動操作に依存しています。

例えば、プルリクエストで変更内容をレビューする際、コードを見るのは当然ですが、コードと実環境の差分を確認するのも忘れてはなりません。しかし、この差分を確認するにはterraform planを実行する必要があり、この操作を各自が手元で行うのは非効率でしょう。また、mainブランチへのマージ後、誰かが手動でterraform applyを実行する必要があります。本番環境であれば代表者がタイミングを見計らってapplyするのも悪くないですが、開発環境やステージング環境ではマージと共に自動でapplyを実行できた方がDevOps的です。

コードを書いたらセキュリティスキャンも必要です。セキュリティチェックのような定型的なコマンドを実行する処理、特に時間のかかる処理は、各自が手元で実行するよりもCIで自動実行する方が効率的でしょう。

本記事では、これらの課題を解決するために、GitHub ActionsでのCI/CD統合について解説します。プルリクエスト時にterraform planを自動実行してレビューし、mainブランチへのマージをトリガーにterraform applyを自動実行することで、開発環境への変更を即時反映できます。これはCI/CDを使用する大きなメリットです。

一方で、本番環境へのapplyは上長の承認が必要だったり、デプロイのタイミングをコントロールしたい場合もあります。そこで、開発環境は自動デプロイ、本番環境は承認フロー付きでデプロイする構成を紹介します。より保守的な運用(applyは常に手動実行)を希望する場合は、ワークフローからterraform-applyジョブを削除し、必要に応じてローカルまたはGitHub ActionsのWorkflow Dispatch機能を使って手動実行してください。

ディレクトリ分離の構成では、変更されたファイルから対象環境を検出し、その環境のみにTerraformを実行する必要があります。

GitHub ActionsでCI/CD統合

Terraformをチーム開発で使用する際、CI/CDパイプラインとの統合は不可欠です。以下のようなワークフローを実装します。

ワークフローの設計

  1. プルリクエスト時
    • 変更されたファイルから対象環境を検出
    • 対象環境に対してterraform fmtでコードフォーマットをチェック
    • terraform validateで構文をチェック
    • terraform planを実行し、変更内容をコメントに投稿
  2. mainブランチへのマージ時
    • 変更されたファイルから対象環境を検出
    • 対象環境に対してterraform applyを実行(本番環境は承認後に実行)

AWS認証情報の設定

GitHub ActionsからAWSにアクセスするため、OIDCを使った認証を設定します。これは、長期的なアクセスキーを使用するよりも安全です。

ステップ1: IAMロールの作成
AWSマネジメントコンソールまたはAWS CLIで、GitHub Actions用のIAMロールを作成します。詳細はAWS公式ガイドを参照してください。

trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:*"
        }
      }
    }
  ]
}

123456789012はAWSアカウントID、your-orgはGitHub組織名またはユーザー名、your-repoはリポジトリ名に置き換えてください。

IAMロールを作成します。

$ aws iam create-role \
  --role-name GitHubActionsRole \
  --assume-role-policy-document file://trust-policy.json

必要な権限をアタッチします。

$ aws iam attach-role-policy \
  --role-name GitHubActionsRole \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

本番環境では最小権限の原則に従って、必要な権限のみを付与するカスタムポリシーを作成することを推奨します。本記事では、ポリシー設定の複雑さを避けるためAdministratorAccessをアタッチしていますが、実運用では適切に権限を絞り込んでください。

ステップ2: GitHub Actionsワークフローの作成
リポジトリに.github/workflows/terraform.ymlを作成します。このワークフローは、変更されたファイルから対象環境を検出し、その環境に対してのみTerraformを実行します。

.github/workflows/terraform.yml
name: Terraform CI/CD

on:
  pull_request:
    branches:
      - main
    paths:
      - 'environments/**'
      - 'modules/**'
  push:
    branches:
      - main
    paths:
      - 'environments/**'
      - 'modules/**'

permissions:
  id-token: write      # OIDC認証のためのトークン発行に必要
  contents: read       # リポジトリのコードを読み取るために必要
  pull-requests: write # PRにコメントを投稿するために必要

jobs:
  detect-changes:
    name: Detect Changed Environments
    runs-on: ubuntu-latest
    outputs:
      environments: ${{ steps.detect.outputs.environments }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Detect changed environments
        id: detect
        run: |
          # PRの場合とpushの場合で比較対象のコミットを切り替え
          if [ "${{ github.event_name }}" == "pull_request" ]; then
            BASE_SHA="${{ github.event.pull_request.base.sha }}"
            HEAD_SHA="${{ github.event.pull_request.head.sha }}"
          else
            BASE_SHA="${{ github.event.before }}"
            HEAD_SHA="${{ github.sha }}"
          fi

          # 変更されたファイルから環境名を抽出し、JSON配列に変換
          CHANGED_FILES=$(git diff --name-only $BASE_SHA $HEAD_SHA)
          ENVIRONMENTS=$(echo "$CHANGED_FILES" | grep -E '^environments/[^/]+' | cut -d'/' -f2 | sort -u | jq -R -s -c 'split("\n")[:-1]')

          # モジュールが変更された場合は全環境を対象にする
          # 共通モジュールの変更はすべての環境に影響するため
          if echo "$CHANGED_FILES" | grep -q '^modules/'; then
            ENVIRONMENTS='["dev","prod"]'
          fi

          echo "environments=$ENVIRONMENTS" >> $GITHUB_OUTPUT
          echo "Detected environments: $ENVIRONMENTS"

  terraform-plan:
    name: Terraform Plan
    needs: detect-changes
    if: github.event_name == 'pull_request' && needs.detect-changes.outputs.environments != '[]'
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: ${{ fromJson(needs.detect-changes.outputs.environments) }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v5
        with:
          # 環境ごとに異なるAWSアカウントを使用する場合の例
          # prod環境: 111111111111(本番環境のAWSアカウントID)
          # dev環境: 222222222222(開発環境のAWSアカウントID)
          # 同じAWSアカウントを使用する場合は、三項演算子を使わず直接ARNを指定してください
          # 例: role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          role-to-assume: ${{ matrix.environment == 'prod' && 'arn:aws:iam::111111111111:role/GitHubActionsRole' || 'arn:aws:iam::222222222222:role/GitHubActionsRole' }}
          aws-region: ap-northeast-1

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.13.5

      - name: Terraform Format Check
        id: fmt
        run: terraform fmt -check -recursive
        working-directory: .
        continue-on-error: true

      - name: Terraform Init
        id: init
        run: terraform init
        working-directory: ./environments/${{ matrix.environment }}

      - name: Terraform Validate
        id: validate
        run: terraform validate
        working-directory: ./environments/${{ matrix.environment }}

      # Trivyセキュリティスキャン
      - name: Run Trivy security scanner
        uses: aquasecurity/trivy-action@0.30.0
        with:
          scan-type: 'config'
          scan-ref: '.'
          format: 'sarif'
          output: 'trivy-results.sarif'
          exit-code: '0'  # セキュリティ問題が見つかってもワークフローを継続(警告として扱う)
                          # 本番運用でCRITICAL/HIGHを検出時にPRをブロックしたい場合は '1' に設定
          severity: 'CRITICAL,HIGH,MEDIUM'
          scanners: 'misconfig,secret'

      - name: Upload Trivy results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color -out=tfplan
        working-directory: ./environments/${{ matrix.environment }}
        continue-on-error: true

      - name: Comment PR
        uses: actions/github-script@v8
        if: github.event_name == 'pull_request'
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const output = `### Terraform Plan - \`${{ matrix.environment }}\` Environment

            #### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
            #### Terraform Initialization ⚙\`${{ steps.init.outcome }}\`
            #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
            <details><summary>Validation Output</summary>

            \`\`\`
            ${{ steps.validate.outputs.stdout }}
            \`\`\`

            </details>

            #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`

            <details><summary>Show Plan</summary>

            \`\`\`
            ${{ steps.plan.outputs.stdout }}
            \`\`\`

            </details>

            *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Workflow: \`${{ github.workflow }}\`*`;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })

      - name: Terraform Plan Status
        if: steps.plan.outcome == 'failure'
        run: exit 1

  terraform-apply:
    name: Terraform Apply
    needs: detect-changes
    if: github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.detect-changes.outputs.environments != '[]'
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: ${{ fromJson(needs.detect-changes.outputs.environments) }}
    # この行により、GitHub Environmentsで設定した承認フローが適用されます
    # 本番環境(prod)の場合は承認者の承認が必要になります
    environment: ${{ matrix.environment }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v5
        with:
          # 環境ごとに異なるAWSアカウントを使用する場合の例
          # prod環境: 111111111111(本番環境のAWSアカウントID)
          # dev環境: 222222222222(開発環境のAWSアカウントID)
          # 同じAWSアカウントを使用する場合は、三項演算子を使わず直接ARNを指定してください
          # 例: role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          role-to-assume: ${{ matrix.environment == 'prod' && 'arn:aws:iam::111111111111:role/GitHubActionsRole' || 'arn:aws:iam::222222222222:role/GitHubActionsRole' }}
          aws-region: ap-northeast-1

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.13.5

      - name: Terraform Init
        run: terraform init
        working-directory: ./environments/${{ matrix.environment }}

      - name: Terraform Apply
        run: terraform apply -auto-approve
        working-directory: ./environments/${{ matrix.environment }}

このワークフローは、以下のように動作します。

まず、変更検出ジョブ(detect-changes)で変更されたファイルから対象環境を検出します。environments/dev/配下のファイルが変更されていればdevを、modules/配下が変更されていれば、すべての環境を出力します。

次に、プルリクエスト時のterraform-planジョブでは、検出された環境ごとに並列実行(matrix strategy)します。環境ごとに適切なAWS認証情報を使用し、terraform planの結果を環境ごとにPRにコメントします。

最後に、mainブランチへのマージ時のterraform-applyジョブでは、検出された環境ごとにterraform applyを実行します。本番環境(prod)は承認が必要です(environment: ${{ matrix.environment }})。

承認フローの追加

本番環境への変更は慎重に行う必要があります。GitHub Actionsのenvironment機能を使って、環境ごとに承認フローを設定できます。

ステップ1: Environment設定
GitHubリポジトリの「Settings」→「Environments」から、環境ごとにEnvironmentを作成します。GitHub Environmentsの詳細は公式ドキュメントを参照してください。

  • dev: 承認不要(自動デプロイ)
  • prod: 2名以上の承認が必要

devは承認不要なため、特別な設定は不要です。下図の通り、デフォルト設定のまま進めていきます。

GitHub Environmentsの画面
GitHub Environmentsの追加画面
GitHub Environmentsの設定画面

prodは2名以上の承認が必要なため、ステップ2以降の手順を実行し承認を有効化していきましょう。

ステップ2: Required reviewersの設定
本番環境(prod)には「Required reviewers」を有効にし、シニアエンジニアやインフラチームのメンバーを承認者として指定します。これにより、terraform applyの実行前に指定したメンバーの承認が必要になります。

Required reviewersの設定画面

ステップ3: Environment protection rulesの設定
さらに、以下の保護ルールを追加できます。

  • Wait timer: デプロイ前に一定時間待機させる
  • Deployment branches: 特定のブランチからのみデプロイを許可

・ワークフローでの動作
上記のワークフローではenvironment: ${{ matrix.environment }}を指定しているため、環境ごとに適切な承認フローが適用されます。

  • 開発環境への変更: mainへのマージ後、即座にterraform applyが実行
  • 本番環境への変更: mainへのマージ後、承認者の承認を待ってからterraform applyが実行
PRへのterraform planコメント

PRをマージするとEnvironmentsの設定でレビュアーに追加した人にレビュー依頼のメールが送信されます。そこに記載のリンクから「Approve and deploy」をクリックすることでデプロイが動き始めます。

本番環境デプロイの承認待ち画面

これにより、開発環境では迅速なイテレーションを維持しつつ、本番環境では慎重なデプロイを実現できます。

セキュリティスキャンの詳細

ワークフローに含まれている「Trivy」はAqua Securityが提供する包括的なセキュリティスキャナーで、tfsecの後継として推奨されています。IaCの設定ミス、脆弱性、シークレットの検出が可能です。

Trivyステップの設定を詳しく見てみましょう。

- name: Run Trivy security scanner
  uses: aquasecurity/trivy-action@0.30.0
  with:
    scan-type: 'config'          # IaCスキャンを実行
    scan-ref: '.'                # カレントディレクトリをスキャン
    format: 'sarif'              # SARIF形式で出力
    output: 'trivy-results.sarif'
    exit-code: '0'               # エラーでもワークフローを継続
    severity: 'CRITICAL,HIGH,MEDIUM'  # 検出する深刻度
    scanners: 'misconfig,secret' # 設定ミスとシークレットを検出

Trivyは、AWSリソースのセキュリティベストプラクティスに違反していないかをチェックします。例えば、以下のような問題を検出します。

  • S3バケットが公開設定になっていないか
  • セキュリティグループが0.0.0.0/0からのアクセスを許可していないか
  • シークレットがハードコードされていないか
  • EKSクラスターのログが有効化されているか
  • IAMロールが過度な権限を持っていないか

結果はSARIF形式で出力され、GitHubのSecurityタブ > Code scanningでPR番号を指定すると確認できます。または、PR画面のChecksタブ > Code scanning results > Trivy > View all branch alertsでも確認できます。

Trivyの実行結果
Trivyで検出した画面

検出された問題は優先度(CRITICAL、HIGH、MEDIUM、LOW)に応じて対処します。CRITICALやHIGHの問題はマージ前に必ず修正することを推奨します。

ワークフローの実行例

実際のワークフローの動作を確認してみましょう。

プルリクエスト時の動作

  1. プルリクエストを作成
    開発環境のEKSクラスターのノード数を1から2に増やすPRを作成します。
    # environments/dev/main.tf
    module "eks" {
      source = "../../modules/eks"
    
      # ...
      node_desired_size  = 2  # 1から2に変更
      # ...
    }
  2. 自動的にワークフローが実行される
    GitHub Actionsが自動的に起動し、以下のステップを実行します。
    • 変更検出: dev環境が変更されたことを検出
    • Terraform Format Check: コードフォーマットをチェック
    • Terraform Init: 初期化
    • Terraform Validate: 構文をチェック
    • Trivy: セキュリティスキャン
    • Terraform Plan: 変更内容を確認
  3. PRにコメントが投稿される
    terraform planの結果がPRにコメントされ、レビュアーが変更内容を確認できます。

mainブランチへのマージ時の動作

  1. PRをマージ
    レビューが完了し、PRをmainブランチにマージします。
  2. 開発環境への自動デプロイ
    mainブランチへのマージをトリガーに、GitHub Actionsが自動的にterraform applyを実行します。開発環境(dev)は承認不要なため、即座にデプロイされます。
  3. 本番環境への承認待ち
    本番環境(prod)のファイルも変更されている場合、承認者に通知が送信されます。承認者が「Approve and deploy」をクリックすることで本番環境へのデプロイが開始されます。

おわりに

本記事では、GitHub ActionsでのTerraform CI/CD統合について解説しました。

プルリクエスト時にterraform planを自動実行してレビューし、mainブランチへのマージ後にterraform applyを自動実行することで開発環境への変更を即時反映できるようになりました。また、本番環境への変更には承認フローを設け、安全なデプロイを実現しました。Trivyによるセキュリティスキャンにより、設定ミスやシークレットの漏洩を事前に防ぐことができます。

第16回から第18回までの3回シリーズを通じて、Terraformをチーム開発で活用するためのベストプラクティスを学びました。

  • 第16回: モジュール化とディレクトリ分離による環境管理
  • 第17回: リモートバックエンドによる状態管理とロック機構
  • 第18回: CI/CDパイプラインとの統合と承認フロー

これらの仕組みを導入することで、以下のメリットが得られます。

  • 協調作業の実現: チーム全員が同じ状態を共有し、安全に作業できる
  • 変更の可視化: プルリクエストで変更内容をレビューできる
  • 環境の分離: 開発・本番環境を適切に分離し、誤操作を防げる
  • 安全なデプロイ: 承認フローにより、本番環境への変更を慎重に行える
  • 自動化: 手動操作を減らし、人的ミスを防げる
  • 監査証跡: GitとCIログにより、すべての変更を追跡できる

特に重要なのは、Terraformワークスペースを環境分離に使わないという点です。公式ドキュメントでも明記されている通り、ワークスペースは同じバックエンドと認証情報を共有するため、セキュリティの隔離メカニズムとしては不十分です。環境分離にはディレクトリ分離とモジュール化の組み合わせを使用しましょう。

最初は設定が複雑に感じるかもしれませんが、一度構築してしまえば、その後の運用は大幅に効率化されます。小さなプロジェクトから始めて、徐々に適用範囲を広げていくことをお勧めします。

次回は、Terraformで構築したインフラの運用監視とコスト管理について解説します。「AWS CloudWatch」や「Cost Explorer」と連携して、インフラの健全性とコストを継続的に監視する方法を学びます。

この記事をシェアしてください

人気記事トップ10

人気記事ランキングをもっと見る

企画広告も役立つ情報バッチリ! Sponsored