🚀

【Terraform】GitHub Actions を使って、planやapplyを自動化しよう

2025/01/12に公開

Terraform での plan, apply を GitHub Actions で自動化してみます

ゴール

↓ の表のような PR 作成 → マージの処理を作っていきます

No. 処理 備考
1 ブランチを作成し、PRを作成する
2 PR作成時に terraform plan が実行される 結果をPRにコメントで残す
3 planの結果をレビューする
4 PRをマージする
5 マージ時に terraform apply が実行される

つくってみる

以下の流れで自動化していきます

  1. tfstate ファイルを S3 で管理
  2. OIDC(OpenID Connect)を作成
  3. GitHub Actions で AWS にアクセスできるかテスト
  4. GitHub Actions に plan と apply を組み込む

今回は「terraform_test」という名前の Git リポジトリを使用します

tfstate ファイルを S3 で管理

下記のように 2 つのファイルを作成します

terraform_test
├── .gitignore     // 作成
├── README.md
└── main.tf      // 作成

tfstate を保存する S3 バケットを作成

各ファイルの中身

# Local .terraform directories
**/.terraform/*

# .tfstate files
*.tfstate
*.tfstate.*

# Crash log files
crash.log

# Ignore any .tfvars files that are generated automatically for each Terraform run. Most
# .tfvars files are managed as part of configuration and so should be included in
# version control.
#
# example.tfvars

# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Include override files you do wish to add to version control using negated pattern
#
# !example_override.tf

# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*

*.hcl

main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.16"
    }
  }

  required_version = ">= 1.2.0"
}

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

# S3バケットの定義
resource "aws_s3_bucket" "terraform_state" {
  # バケット名は一意の必要があるので、bucketの値は各自変更してください
  bucket = "terraform-state-hisui"
}

# バージョニング設定
resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  versioning_configuration {
    status = "Enabled"
  }
}

main.tf 内のコメントにも書きましたが、バケット名は一意である必要がありますなので、バケット名は変更してください!!

次のコマンドを実行していき、S3 を作成します

1. terraform init
2. terraform plan
3. terraform apply -auto-approve

AWS コンソールで確認してみます。

無事作成できました

tfstate の保存先を S3 へ変更

main.tf のterraform ブロック内に backend を追記します

main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.16"
    }
  }

  required_version = ">= 1.2.0"

+ # tfstateの保存先
+ backend "s3" {
+   bucket = "terraform-state-hisui" # 作成したS3バケット
+   region = "ap-northeast-1"
+   # バケット内の保存先
+   # 適宜変更してください
+   key = "test/terraform.tfstate"
+   encrypt = true
+ }
}

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

# S3バケットの定義
resource "aws_s3_bucket" "terraform_state" {
  # バケット名は一意の必要があるので、bucketの値は各自変更してください
  bucket = "terraform-state-hisui"
}

# バージョニング設定
resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  versioning_configuration {
    status = "Enabled"
  }
}

backend の設定をしたので、terraform init を実行します

terraform_test$ terraform init

Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "s3" backend. No existing state was found in the newly
  configured "s3" backend. Do you want to copy this state to the new "s3"
  backend? Enter "yes" to copy and "no" to start with an empty state.

 // ローカルのtfstateファイルをS3へコピーするか聞かれているので、「yes」
  Enter a value: yes


Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

tfstate ファイルが S3 にコピーされているか確認します

コピーされていますねー

OIDC(OpenID Connect)を作成

GitHub の Actions secrets and variables から AWS の認証情報を読み込む方法もありですが、 セキュリティ面を考慮して OIDC を使います。

こちらを参考にiam.tf を作成します

(ディレクトリ)

terraform_test
├── .gitignore
├── README.md
├── iam.tf    // 作成
└── main.tf

iam.tf
#============================================================================
# Github Actions 用ロール
#============================================================================
# -----------------------------------------------------------------------------
# GitHub Actions プロバイダー設定
# -----------------------------------------------------------------------------
resource "aws_iam_openid_connect_provider" "terraform_cicd" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  # このコードは固定値
  # OIDC ID プロバイダーのサムプリント
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

# -----------------------------------------------------------------------------
# GitHub Actions 用ロール作成
# -----------------------------------------------------------------------------
resource "aws_iam_role" "terraform_cicd_oidc_role" {
  name = "TerraCICDDemoOIDCRole"
  path = "/"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = "sts:AssumeRoleWithWebIdentity"
      Principal = {
        Federated = aws_iam_openid_connect_provider.terraform_cicd.arn
      }
      Condition = {
        StringLike = {
          "token.actions.githubusercontent.com:sub" = [
            # リポジトリ制限
            # xxxxx:GitHubのアカウント名
            "repo:xxxxx/terraform_test:*",
          ]
        }
      }
    }]
  })
}

# -----------------------------------------------------------------------------
# ポリシーのアタッチ(AdministratorAccess_attachment)
# -----------------------------------------------------------------------------
resource "aws_iam_role_policy_attachment" "AdministratorAccess_attachment" {
  role       = aws_iam_role.terraform_cicd_oidc_role.name
  # Admin権限を指定
  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}

上記を terraform apply してID プロバイダを作成します

terraform_test$ terraform plan
aws_s3_bucket.terraform_state: Refreshing state... [id=terraform-state-hisui]
aws_s3_bucket_versioning.terraform_state: Refreshing state... [id=terraform-state-hisui]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_iam_openid_connect_provider.terraform_cicd will be created
  + resource "aws_iam_openid_connect_provider" "terraform_cicd" {
      + arn             = (known after apply)
      + client_id_list  = [
          + "sts.amazonaws.com",
        ]
      + id              = (known after apply)
      + tags_all        = (known after apply)
      + thumbprint_list = [
          + "6938fd4d98bab03faadb97b34396831e3780aea1",
        ]
      + url             = "https://token.actions.githubusercontent.com"
    }

  # aws_iam_role.terraform_cicd_oidc_role will be created
  + resource "aws_iam_role" "terraform_cicd_oidc_role" {
      + arn                   = (known after apply)
      + assume_role_policy    = (known after apply)
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "TerraCICDOIDCRole"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + role_last_used        = (known after apply)
      + tags_all              = (known after apply)
      + unique_id             = (known after apply)
    }

  # aws_iam_role_policy_attachment.AdministratorAccess_attachment will be created
  + resource "aws_iam_role_policy_attachment" "AdministratorAccess_attachment" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
      + role       = "TerraCICDOIDCRole"
    }

Plan: 3 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
terraform_test$ terraform apply -auto-approve
aws_s3_bucket.terraform_state: Refreshing state... [id=terraform-state-hisui]
aws_s3_bucket_versioning.terraform_state: Refreshing state... [id=terraform-state-hisui]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_iam_openid_connect_provider.terraform_cicd will be created
  + resource "aws_iam_openid_connect_provider" "terraform_cicd" {
      + arn             = (known after apply)
      + client_id_list  = [
          + "sts.amazonaws.com",
        ]
      + id              = (known after apply)
      + tags_all        = (known after apply)
      + thumbprint_list = [
          + "6938fd4d98bab03faadb97b34396831e3780aea1",
        ]
      + url             = "https://token.actions.githubusercontent.com"
    }

  # aws_iam_role.terraform_cicd_oidc_role will be created
  + resource "aws_iam_role" "terraform_cicd_oidc_role" {
      + arn                   = (known after apply)
      + assume_role_policy    = (known after apply)
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "TerraCICDOIDCRole"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + role_last_used        = (known after apply)
      + tags_all              = (known after apply)
      + unique_id             = (known after apply)
    }

  # aws_iam_role_policy_attachment.AdministratorAccess_attachment will be created
  + resource "aws_iam_role_policy_attachment" "AdministratorAccess_attachment" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
      + role       = "TerraCICDOIDCRole"
    }

Plan: 3 to add, 0 to change, 0 to destroy.
aws_iam_openid_connect_provider.terraform_cicd: Creating...
aws_iam_openid_connect_provider.terraform_cicd: Creation complete after 1s [id=arn:aws:iam::××××××××××××:oidc-provider/token.actions.githubusercontent.com]
aws_iam_role.terraform_cicd_oidc_role: Creating...
aws_iam_role.terraform_cicd_oidc_role: Creation complete after 1s [id=TerraCICDOIDCRole]
aws_iam_role_policy_attachment.AdministratorAccess_attachment: Creating...
aws_iam_role_policy_attachment.AdministratorAccess_attachment: Creation complete after 1s [id=TerraCICDOIDCRole-××××××××××××]

AWS コンソールでも作成が確認できました

いったんここまでの内容を main ブランチに commit、push しておきます

$ git add .
$ git commit -m "create_S3_OIDC"
$ git push

GitHub Actions で AWS にアクセスできるかテスト

main ブランチからテスト用の test ブランチをきります

terraform_test$ git branch
* main
terraform_test$ git checkout -b test
Switched to a new branch 'test'
terraform_test$ git branch
  main
* test

GitHub Actions のワークフローを定義するため.github/workflows/aws_access_test.ymlを作成します

.github/workflows/ 配下の YAML ファイルに ワークフローを定義することで GitHub Actions を使用できます

(ディレクトリ)

terraform_test
├── .github
│ └── workflows
│   └── aws_access_test.yml   // 作成
├── .gitignore
├── README.md
├── iam.tf
└── main.tf

こちらを参考に aws_access_test.yml に記述していきます

.github/workflows/aws_access_test.yml
name: AWS Deploy
on:
  push:
    branches:
      # 作業ブランチ
      - test

env:
  # ×××××××××:AWSアカウントID
  AWS_ROLE_ARN: arn:aws:iam::×××××××××:role/TerraCICDOIDCRole

permissions:
  id-token: write
  contents: read
jobs:
  aws-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{ env.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1
      - run: aws sts get-caller-identity

test ブランチに push した際に

aws sts get-caller-identity

が実行されるワークフローが定義できました!

このワークフローでGitHub Actions から AWS にアクセスできるか push して確認します

terraform_test$ git add .
terraform_test$ git commit -m "commit aws_access_test.yml"
terraform_test$ git push origin test
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 16 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 743 bytes | 743.00 KiB/s, done.
Total 5 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
remote:
remote: Create a pull request for 'test' on GitHub by visiting:
remote:      https://github.com/×××××××××/terraform_test/pull/new/test
remote:
To https://github.com/×××××××××/terraform_test.git
 * [new branch]      test -> test

GitHub > リポジトリ > Actions から結果を確認できます

成功していれば、.github/workflows/aws_access_test.yml は もういらないので削除しておきます

GitHub Actions に plan と apply を組み込む

plan 用と apply 用のワークフローを作成します

ポイントは

  • PR 作成時に plan を実行し、結果をコメントに残す
  • PR マージ時に apply を実行する

.github/workflows/に plan.yml と apply.yml の2つのファイルを作成します

(ディレクトリ)

terraform_test
├── .github
│ └── workflows
│   └── plan.yml    // 作成
│   └── apply.yml   // 作成
├── .gitignore
├── README.md
├── iam.tf
└── main.tf

こちらを参考に yml ファイルに記述していきます

.github/workflows/plan.yml
name: TerraformPlan

on:
  pull_request:
    branches:
      - main

env:
  TF_VERSION: 1.4.2
  AWS_DEFAULT_REGION: ap-northeast-1
  # ×××××××××:AWSアカウントID
  AWS_ROLE_ARN: arn:aws:iam::xxxxxxxx:role/TerraCICDOIDCRole

permissions:
  id-token: write
  contents: read
  actions: read
  pull-requests: write

jobs:
  Terraform_plan_and_Comment:
    runs-on: ubuntu-latest

    defaults:
      run:
        shell: bash

    steps:
      # リポジトリのチェックアウトをする。
      - name: Check out repository code
        uses: actions/checkout@v3

      # OICDでAssumeRoleをする。
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: ${{ env.AWS_DEFAULT_REGION }}
          role-to-assume: ${{ env.AWS_ROLE_ARN }}

      - name: Setup Terraform
        # バージョン2を使用する。
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Exec Terraform fmt check
        id: fmt
        run: terraform fmt -check -recursive
        # exit code 3でエラーになり終了してしまうため
        # continue-on-error: true で後続の処理も続ける。
        continue-on-error: true

      - name: Exec Terraform init
        id: init
        run: terraform init

      - name: Validate
        run: terraform validate

      - name: Exec Terraform plan
        id: plan
        run: terraform plan -no-color

      # terraform plan の結果をコメント欄に出力する。
      - name : comment
        uses: actions/github-script@v4
        env:
          # ここのstdoutでterraform planの結果をPLANに保存している。
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"

        with:
          script: |

            const output = `### terraform cicd
            #### Terraform Format and Style \`${{ steps.fmt.outcome }}\`
            #### Terraform Initialization \`${{ steps.init.outcome }}\`
            #### Terraform Plan \`${{ steps.plan.outcome }}\`
            #### Terraform Validation \`${{ steps.validate.outcome }}\`

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

            \`\`\`${process.env.PLAN}\`\`\`

            </details>`;

            github.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })
.github/workflows/apply.yml
name: TerraformApply

on:
  push:
    branches:
      - master

env:
  TF_VERSION: 1.4.2
  AWS_DEFAULT_REGION: ap-northeast-1
  # ×××××××××:AWSアカウントID
  AWS_ROLE_ARN: arn:aws:iam::xxxxxxxxx:role/TerraCICDOIDCRole

permissions:
  id-token: write
  contents: read
  actions: read
  pull-requests: write

jobs:
  Terraform_Apply:
    runs-on: ubuntu-latest

    defaults:
      run:
        shell: bash

    steps:
      # リポジトリのチェックアウトをする。
      - name: Check out repository code
        uses: actions/checkout@v3

      # OICDでAssumeRoleをする。
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: ${{ env.AWS_DEFAULT_REGION }}
          role-to-assume: ${{ env.AWS_ROLE_ARN }}

      - name: Setup Terraform
        # バージョン2を使用する
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Exec Terraform fmt check
        id: fmt
        working-directory: "${{ env.WORK_DIR }}"
        run: terraform fmt -recursive -check
        # exit code 3でエラーになり終了してしまうため
        # continue-on-error: true で後続の処理も続ける。
        continue-on-error: true

      - name: Exec Terraform init
        id: init
        working-directory: "${{ env.WORK_DIR }}"
        run: terraform init

      - name: terraform apply
        id: apply
        working-directory: "${{ env.WORK_DIR }}"
        run: terraform apply -auto-approve

ためしてみる

GitHub Actions のワークフローが作成できたので、テストしてみます

main.tf に下記を追記して、EC2 を定義します

# EC2を定義
resource "aws_instance" "server" {
  ami = "ami-0c3fd0f5d33134a76"
  instance_type = "t2.micro"
  # サブネットIDを指定して下さい
  subnet_id = "<subnetのID>"
}

commit → push します

$ git add .
$ git commit -m "create_ec2_test"
$ git push origin test

GitHub のサイトで PR を作成して少し待ってみると。。。

↓ のように PR に plan の結果がコメントされます

Q:PR 作成後に追加でコミットしたら plan は再実行されるのでしょうか? A:plan.yml で定義した内容が再実行されます。

マージして、GitHub > リポジトリ > Actions で成功を確認したら、 AWS コンソールでも確認してみます

これで自動化できましたー

テストで作成した EC2 は不要なので、削除しておきましょう

main.tf の下記の部分を削除して push→PR 作成・マージします

# EC2を定義
resource "aws_instance" "server" {
  ami = "ami-0c3fd0f5d33134a76"
  instance_type = "t2.micro"
  # サブネットIDを指定して下さい
  subnet_id = "<subnetのID>"
}

AWS コンソールから削除されていることを確認したら OK です!

やり残したこと

tf ファイルに差分があるときのみ plan や apply を実行するようにしたいなーと思いました

いつか取り組んでみますー

参考

https://developer.hashicorp.com/terraform/tutorials/aws-get-started

https://zenn.dev/rinchsan/articles/de981e561eb36ebfab70

https://go-journey.club/archives/17053

https://rurukblog.com/post/GitHub-Actions-Terraform/

https://qiita.com/tsukakei/items/2751e245e38c814225f1

Discussion