GitHub ActionsでTerraformを実行する方法

1. 概要

  • GitHub ActionsでTerraformのCI/CDを構築する基本構成
  • plan(PRレビュー用)とapply(mainマージ後)の2段階ワークフロー
  • 認証(OIDC vs アクセスキー)の選択
  • Stateバックエンド(S3+DynamoDB)の準備

GitHub ActionsはGitHubに統合されたCI/CDサービスです。Terraformと組み合わせることで、インフラをコードとしてレビュー・承認・適用するワークフローを構築できます。


2. 前提:S3バックエンドの準備

# backend_setup/main.tf(一度だけ手動applyする)
terraform {
  required_version = ">= 1.9"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

variable "environment" {
  description = "環境名"
  type        = string
  default     = "dev"
}

resource "aws_s3_bucket" "tfstate" {
  bucket = "my-company-tfstate-${var.environment}"

  tags = {
    Name        = "my-company-tfstate-${var.environment}"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

resource "aws_s3_bucket_versioning" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_dynamodb_table" "tflock" {
  name         = "terraform-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }

  tags = {
    Name        = "terraform-lock"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

3. メインのTerraform設定

# main.tf
terraform {
  required_version = ">= 1.9"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    bucket         = "my-company-tfstate-dev"
    key            = "app/terraform.tfstate"
    region         = "ap-northeast-1"
    encrypt        = true
    dynamodb_table = "terraform-lock"
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

variable "environment" {
  description = "環境名"
  type        = string
  default     = "dev"
}

data "aws_ami" "amazon_linux_2023" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux_2023.id
  instance_type = "t3.micro"

  root_block_device {
    volume_size = 20
    volume_type = "gp3"
    encrypted   = true
  }

  tags = {
    Name        = "${var.environment}-web"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

4. GitHub Actionsワークフロー

# .github/workflows/terraform.yml
name: Terraform

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  id-token: write     # OIDC認証に必要
  contents: read
  pull-requests: write  # PRコメントに必要

env:
  TF_VERSION: "~> 1.9"
  AWS_REGION: "ap-northeast-1"

jobs:
  terraform:
    name: Terraform Plan / Apply
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./environments/production

    steps:
      # 1. ソースコードをチェックアウト
      - name: Checkout
        uses: actions/checkout@v4

      # 2. AWS認証(OIDC — アクセスキー不要)
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: ${{ env.AWS_REGION }}

      # 3. Terraform CLIをセットアップ
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      # 4. terraform init
      - name: Terraform Init
        run: terraform init

      # 5. フォーマットチェック
      - name: Terraform Format Check
        run: terraform fmt -check -recursive

      # 6. バリデーション
      - name: Terraform Validate
        run: terraform validate

      # 7. plan(PRのみ)
      - name: Terraform Plan
        id: plan
        if: github.event_name == 'pull_request'
        run: |
          terraform plan -no-color -out=tfplan 2>&1 | tee plan_output.txt
          echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT
        continue-on-error: true

      # 8. plan結果をPRコメントに投稿
      - name: Comment Plan on PR
        uses: actions/github-script@v7
        if: github.event_name == 'pull_request'
        with:
          script: |
            const fs = require('fs');
            const plan = fs.readFileSync('environments/production/plan_output.txt', 'utf8');
            const output = plan.length > 65000 ? plan.substring(0, 65000) + '
...(truncated)' : plan;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Terraform Plan
\`\`\`
${output}
\`\`\``
            });

      # 9. apply(mainへのpushのみ)
      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve

5. OIDC IAMロールの設定

# iam_oidc.tf(一度だけ適用)
data "aws_iam_openid_connect_provider" "github" {
  url = "https://token.actions.githubusercontent.com"
}

resource "aws_iam_role" "github_actions" {
  name = "GitHubActionsRole"

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

  tags = {
    Name        = "GitHubActionsRole"
    Environment = "dev"
    ManagedBy   = "terraform"
  }
}

resource "aws_iam_role_policy_attachment" "github_actions_terraform" {
  role       = aws_iam_role.github_actions.name
  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
  # 本番環境では最小権限ポリシーに絞ること
}

6. よくある問題と対処

問題原因対処
terraform initでバックエンドエラーS3バケットが未作成backend_setupを先にapply
OIDC認証エラーIAMロールのConditionが合わないsubのパターンを確認
fmt -checkが失敗フォーマット未適用ローカルでterraform fmt -recursive
planがPRコメントに表示されないpull-requests: write権限がないpermissionsブロックを確認

7. 関連記事


8. まとめ

  • GitHub ActionsでTerraformを動かす基本構成はplan(PR用)+ apply(main用)の2段階
  • AWS認証はOIDCを使えばアクセスキーが不要でセキュア
  • StateはS3+DynamoDB(ロック)をバックエンドとして使う
  • plan結果はPRコメントに自動投稿することでチームレビューが可能になる
  • fmt -checkvalidateをplanの前に入れることで品質を担保する

対象バージョン: Terraform >= 1.9 / GitHub Actions (2024) 公式ドキュメント: https://developer.hashicorp.com/terraform/tutorials/automation/github-actions