LESSON 40分

Terraform + GitHub Actions連携

ストーリー

「CI/CDとIaCを組み合わせる。これが今月の最終目標だ」

木村先輩がアーキテクチャ図を更新した。

「PRを出したら自動でterraform planが走り、 変更内容がPRにコメントされる。 マージしたら自動でterraform applyが走る。 インフラの変更もアプリケーションと同じように、 コードレビューとCI/CDで管理できるんだ」

「GitOps......ですか?」

「そうだ。インフラの全ての変更がGitを通じて行われる。 誰が、いつ、何を変更したかが全て記録される。 これが本当の意味でのInfrastructure as Codeだ」


Terraform CI/CDパイプラインの全体像

Terraform CI/CD フロー

PR作成時:
  .tf ファイル変更 → PR → CI
                         │
                    ┌────┴────┐
                    │ fmt     │
                    │ validate │
                    │ plan    │
                    └────┬────┘
                         │
                    PRにplan結果を
                    コメントとして投稿
                         │
                    レビュー & 承認

マージ後:
  main ← マージ → CD
                    │
               terraform apply
                    │
               インフラ更新完了

GitHub Actions ワークフロー

PR時のplanワークフロー

yaml
# .github/workflows/terraform-plan.yml
name: Terraform Plan

on:
  pull_request:
    branches: [ main ]
    paths:
      - 'infrastructure/**'

permissions:
  contents: read
  pull-requests: write

env:
  TF_VERSION: '1.7.0'
  WORKING_DIR: 'infrastructure/environments/staging'

jobs:
  terraform-plan:
    name: Terraform Plan
    runs-on: ubuntu-latest
    timeout-minutes: 15

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

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}
          terraform_wrapper: true

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1

      - name: Terraform Format Check
        id: fmt
        run: terraform fmt -check -recursive
        working-directory: ${{ env.WORKING_DIR }}
        continue-on-error: true

      - name: Terraform Init
        id: init
        run: terraform init
        working-directory: ${{ env.WORKING_DIR }}

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color
        working-directory: ${{ env.WORKING_DIR }}

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

      - name: Comment Plan on PR
        uses: actions/github-script@v7
        with:
          script: |
            const output = `#### Terraform Format: \`${{ steps.fmt.outcome }}\`
            #### Terraform Init: \`${{ steps.init.outcome }}\`
            #### Terraform Validate: \`${{ steps.validate.outcome }}\`
            #### Terraform Plan: \`${{ steps.plan.outcome }}\`

            <details><summary>Plan Output</summary>

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

            </details>

            *Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

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

      - name: Fail if plan failed
        if: steps.plan.outcome == 'failure'
        run: exit 1

マージ後のapplyワークフロー

yaml
# .github/workflows/terraform-apply.yml
name: Terraform Apply

on:
  push:
    branches: [ main ]
    paths:
      - 'infrastructure/**'

permissions:
  contents: read
  id-token: write

env:
  TF_VERSION: '1.7.0'
  WORKING_DIR: 'infrastructure/environments/staging'

jobs:
  terraform-apply:
    name: Terraform Apply
    runs-on: ubuntu-latest
    timeout-minutes: 30
    environment: staging

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

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1

      - name: Terraform Init
        run: terraform init
        working-directory: ${{ env.WORKING_DIR }}

      - name: Terraform Plan
        run: terraform plan -no-color -out=plan.tfplan
        working-directory: ${{ env.WORKING_DIR }}

      - name: Terraform Apply
        run: terraform apply -auto-approve plan.tfplan
        working-directory: ${{ env.WORKING_DIR }}

      - name: Terraform Output
        run: terraform output -json
        working-directory: ${{ env.WORKING_DIR }}

複数環境のデプロイ

staging → production の段階デプロイ

yaml
name: Terraform Deploy

on:
  push:
    branches: [ main ]
    paths:
      - 'infrastructure/**'

jobs:
  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    environment: staging
    env:
      WORKING_DIR: 'infrastructure/environments/staging'
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: '1.7.0'
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN_STAGING }}
          aws-region: ap-northeast-1
      - run: terraform init
        working-directory: ${{ env.WORKING_DIR }}
      - run: terraform apply -auto-approve
        working-directory: ${{ env.WORKING_DIR }}

  deploy-production:
    name: Deploy to Production
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production         # 手動承認が必要
    env:
      WORKING_DIR: 'infrastructure/environments/production'
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: '1.7.0'
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN_PRODUCTION }}
          aws-region: ap-northeast-1
      - run: terraform init
        working-directory: ${{ env.WORKING_DIR }}
      - run: terraform apply -auto-approve
        working-directory: ${{ env.WORKING_DIR }}
段階デプロイのフロー

  main ← マージ
         │
         ▼
    [staging apply]  ← 自動実行
         │
         ▼
    [承認ゲート]      ← 手動承認(Environment protection rule)
         │
         ▼
    [production apply] ← 承認後に自動実行

AWS OIDC設定

GitHub Actionsから安全にAWSにアクセスするためのOIDC設定です。

hcl
# oidc/main.tf
# GitHub OIDC Provider
resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

# GitHub Actions用IAMロール
resource "aws_iam_role" "github_actions" {
  name = "github-actions-terraform"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.github.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
          }
          StringLike = {
            "token.actions.githubusercontent.com:sub" = "repo:myorg/myrepo:*"
          }
        }
      }
    ]
  })
}

# 必要な権限を付与
resource "aws_iam_role_policy_attachment" "terraform" {
  role       = aws_iam_role.github_actions.name
  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"  # 本番では最小権限に
}

まとめ

ポイント内容
PR時のplan変更プレビューをPRコメントとして投稿
マージ後のapply自動的にインフラ変更を適用
段階デプロイstaging → 承認 → production
OIDCシークレットレスのAWS認証

チェックリスト

  • PR時にterraform planを実行するワークフローを書ける
  • plan結果をPRコメントに投稿する方法を理解した
  • マージ後のterraform applyワークフローを構成できる
  • OIDC認証の仕組みを理解した

次のステップへ

次のセクションでは、IaCにおけるセキュリティとベストプラクティスを学びます。


推定読了時間: 40分