🤖

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

2021/01/04に公開

この記事では、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の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 していきたいと思います。

Discussion