Terraform + GitHub ActionsでInfra CI/CDを試す

8 min read

この記事では、GitHub Actionsを利用したInfra CI/CDの実装例をまとめています。
今回は、GCPを利用したGKEのCluster作成を題材にしています。

.gitignoreに含まれているようなファイル類(ex. *.tfvars)はpushしていないので自身で作成して下さい。

実装のサンプル

下記のRepositoryにInfra CI/CD用の設定ファイル等をまとめています。

https://github.com/taxintt/sample-actions-infra-cicd

jobの概要

GitHub Actionsでは複数のjobを別のファイルで定義することが可能なため、下記のような構成でjobを定義しています。
(単一ファイルでの定義も可能ですが、可読性やカスタマイズの容易性などを考慮して分けています)

  • tf-ci.yaml: commitごとに実行する(format + validate +α)
  • tf-pr-check.yaml: Pull Reuqestの作成をトリガーとして実行する(terraform plan +α)
  • tf-cd.yaml: Pull RequestのMergeをトリガーとして実行する(terraform apply +α)

想定しているフローは下記の通りです。

1. branchを切って、xxx.tfの修正をpushする
2. Pull Request(PR)を作成して、レビューを行う
3. レビューが通った段階でPRをmergeする( → terraform apply)

jobの詳細

jobのstepは各jobで大きな差分はなく、トリガーとなるイベントやplan結果の通知先などがjobによって変わっています。

<!-- tf-ci.yaml -->
name: terraform-plan
on:
  push:
    branches:
      - '**'
      - '!main'
    paths:
      - '.tfnotify.yaml'
      - '**.tf'

env:
  TF_VAR_project_id: ${{ secrets.GCP_PROJECT_ID }}

jobs:
  tf-ci:
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash
        working-directory: .
    steps:
      - name: clone repository
        uses: actions/checkout@v2

      - name: setup gcloud sdk
        uses: google-github-actions/setup-gcloud@master
        with:
          project_id: ${{ secrets.GCP_PROJECT_ID }}
          service_account_key: ${{ secrets.GCP_SA_KEY }}
          export_default_credentials: true
      
      - name: setup tfnotify
        run: |
          sudo curl -fL -o tfnotify.tar.gz https://github.com/mercari/tfnotify/releases/download/v0.7.0/tfnotify_linux_amd64.tar.gz
          sudo tar -C /usr/bin -xzf ./tfnotify.tar.gz

      - name: set up terraform
        uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: 0.13.4
          cli_config_credentials_token: ${{ secrets.GCP_TF_TOKEN }}
      
      - name: terraform fmt
        id: fmt
        run: terraform fmt
        continue-on-error: true

      - name: terraform init
        id: tf-init
        run: terraform init

      - name: tflint
        uses: reviewdog/action-tflint@master
        with:
          github_token: ${{ secrets.github_token }}
          reporter: github-pr-review 
          fail_on_error: "false" 
          filter_mode: "nofilter" 

      - name: terraform validate
        id: validate
        run: terraform validate -no-color
        continue-on-error: true

      - name: terraform plan with notification to GitHub
        id: plan
        run: |
          terraform plan -no-color >> result.temp
          cat result.temp | tfnotify --config .tfnotify/github.yml plan --message "$(date)"
        env:
          SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
          SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL }}
          SLACK_BOT_NAME: tf-notify-bot
          GITHUB_TOKEN: ${{ secrets.github_token }}
        continue-on-error: true

Terraform用のService Accountの権限

検証用の利用用途であれば、GCPプロジェクトのオーナー権限(roles/owner)を付与するのが手っ取り早いかと思います。
実環境では、作成対象のリソースとGCSなど必要最低限な権限のみを付与することを推奨します。

また、tfstateに関してはGCSをRemote Backendとして指定しています。
そのため、terraform plan, applyに関してはGCS Bucket(tfstate)のRead権限は必須となります。

権限周りの調整は厳密にはできていないのですが、Terraform用のService Accountには下記のような権限を付与しています。

- Compute 管理者
- Kubernetes Engine 管理者
- ストレージ管理者
- サービス アカウント管理者
- サービス アカウント ユーザー
- 組織の管理者

Service Accountのkeyはbase64でencodeしたものをGitHub RepoのSecretとして登録しています。

// 出力された文字列をSecretとして登録する
cat test-xx.json | base64

GitHub Actionsのjob内では、Service Accountのセットアップを全てのjobで最初に実行します。
(terraform initの段階で、Remote Backendのinitialize処理が走るため)

https://www.terraform.io/docs/commands/init.html
  - name: setup gcloud sdk
    uses: google-github-actions/setup-gcloud@master
    with:
        project_id: ${{ secrets.GCP_PROJECT_ID }}
        service_account_key: ${{ secrets.GCP_SA_KEY }}
        export_default_credentials: true

GKE用のService Accountの権限

node_config内のservice_accountでGKE NodePool用のService Accountを指定します。
これによって、GKE Nodepoolに指定したRoleを紐づけることができます。

https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_cluster
node_config {
    preemptible     = true
    machine_type    = "e2-medium"

    // recommended
    service_account = "least-privilege-sa-for-gke@${var.project_id}.iam.gserviceaccount.com"

    metadata = {
      disable-legacy-endpoints = "true"
    }

    oauth_scopes = [
      "https://www.googleapis.com/auth/cloud-platform"
    ]

    tags = ["istio"]
  }

今回は下記のようなRoleを付与したService AccountをNode用に作成しました。
(最低限の権限のみしか付与していないため、必要に応じてRoleは追加して下さい。)

resource "google_project_iam_member" "gke_node_pool_roles" {
  project = var.project_id
  for_each = toset([
    "roles/logging.logWriter",
    "roles/monitoring.metricWriter",
    "roles/monitoring.viewer",
    "roles/storage.objectViewer"
  ])
  role   = each.value
  member = "serviceAccount:${google_service_account.least-privilege-sa-for-gke.email}"
}

jobの実行トリガー

GitHub Actionsでは、on.<events>のようにトリガーとなるイベントを指定することが可能です。

https://docs.github.com/ja/free-pro-team@latest/actions/reference/events-that-trigger-workflows

Pull Requestのmergeをjob実行のトリガーとする場合は少し工夫が必要で、下記のように定義します。

name: terraform-apply
on:
  pull_request:
    types: [closed]
    branches:
      - '**'
    paths:
      - '**.tf'

jobs:
  tf-cd:
    runs-on: ubuntu-latest
    if: github.event.pull_request.merged == true

tflint

InfraのCI/CDを行っていく上で、(特にPROD環境では)validation + linterによるチェックの実施は必須条件になります。
(厳密には異なりますが、AppのCI/CDにおける自動テストに相当するものとして利用されている方もいるかと思います。)

今回作成したCI/CD Pipelineではterraform validate + tflintを利用する想定です。

tflintに関しては、commit pushを行う前にローカルで利用します。

GitHub Actionsに対応したtflintのrepositoryもありますがGCPでは正常に動作しなかったため、
ローカル環境で変更を加えたtfファイルに対してlintをかけるようにしています。
https://github.com/reviewdog/action-tflint

// tflintのinstall
brew install tflint

// rulesetのdownload
git clone -b master git@github.com:terraform-linters/tflint-ruleset-google.git

// rulesetのinstall
cd tflint-ruleset-google-master
make install

du -s .tflint.d   
21M	.tflint.d

GitHub Actions側で、commit push + PR作成時にlintを自動実行することができればbetterです。
(build後のRulesetのディレクトリ(.tflint.d)をProject root配下に配置しましたが動作は確認できませんでした)

https://github.com/terraform-linters/tflint-ruleset-google

下記のように不正なMachine typeなどterraform validateでは拾えない部分に対して対応します。

node_config {
    preemptible     = true

    // invalid machime type 
    machine_type    = "e2-medium-xxxxxxxxxx"
    service_account = "least-privilege-sa-for-gke@${var.project_id}.iam.gserviceaccount.com"
      disable-legacy-endpoints = "true"
    }
  }

ローカル環境では、DefaultのRuleで動作することを確認済です。

https://github.com/terraform-linters/tflint-ruleset-google/blob/master/docs/README.md
$ tflint --config ./.tflint.hcl
1 issue(s) found:

Error: "e2-medium-xxxxxxxxxxxx" is an invalid as machine type (google_container_node_pool_invalid_machine_type)

  on k8s.tf line 59:
  59:     machine_type    = "e2-medium-xxxxxxxxxxxx"

tfnotify

terraform plan, terraform applyの結果通知に関しては、tfnotifyを利用します。

https://github.com/mercari/tfnotify

利用方法としては、各コマンドの出力結果をパイプで渡すだけです。
(GitHub, Slackなど複数種類の通知を同時に行う場合は、一時ファイルに書き出して通知処理を実行しています)

  - name: terraform apply
    id: plan
    run: |
        terraform plan -no-color >> result.temp
        cat result.temp | tfnotify --config .tfnotify/github.yml plan --message "$(date)"
        cat result.temp | tfnotify --config .tfnotify/slack.yml plan --message "$(date)"
    env:
        SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
        SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL }}
        SLACK_BOT_NAME: tf-notify-bot
        GITHUB_TOKEN: ${{ secrets.github_token }}

終わりに

今回は、シンプルなディレクトリ構造でInfra CI/CDの実装例をまとめました。

tflintのGCPでのDeep checkingが非対応であったり細かい部分の追加実装は今後もありそうなので、引き続き関連する部分はWatchしていきたいと思います。