Terraform開発時のDeveloper Experienceを爆上げする

2020/12/02に公開

はじめに1

この記事は terraform Advent Calendar 2020 2日目の記事です。

1日目は rakiさん2020年の terraform-jp 振り返り
です!
3日目は rakiさんaws iam policy で s3 の bucket 制限を書く時に便利かもしれない小技 です!

はじめに2

こんにちは。みなさんterraform書いてますか?
最近は日に日にterraformがIaCのデファクトになりつつあるように感じます。

google trend
Google Trend

もちろんterraform本体の開発が活発なのも一因ですが、terraformを取り巻くecosystemの隆盛も近年普及してきた要因の一つなのかなと感じます。適切にecosystemを使いこなせば一般的なwebアプリケーションの開発時のライフサイクルと同じようなDeveloper Experienceをterraformの開発時でも受けられるようになってきています。
さて、それではどのようなツールがあるでしょうか?

https://github.com/shuaibiyy/awesome-terraform

こちらのリポジトリに関連ツールやドキュメント等一覧がまとまっているので、迷ったらここを見れば問題なさそうです。(全部読んだならこの記事を読む必要はもう全然ない)ですが、いかんせん量が多いので今回は 開発時のエンジニアの体験(DX) を良くするもの/自分が使ってみて実際に良くなったものにフォーカスして紹介させていただこうと思います。

この記事で触れること

  • DX系のterraform関連ツール

この記事で触れないこと

  • terraformそのもののベストプラクティス

一覧

  1. setup-terraform
  2. tfenv
  3. tflint
  4. tfnotify
  5. tfsec
  6. tfupdate

今回は作成したばかりのこちらのリポジトリを使って、0からの構築まで行っていきます。
https://github.com/HomMarkHunt/awesome-terraform-impl

1. tfenv

https://github.com/tfutils/tfenv

まずは最初は何はともあれterraformコマンドを使えるようにしましょう!
もちろん brew install terraform でも問題ありませんが、1つ問題があります。複数のリポジトリやチームでterraformのversionが違った場合その都度brewで入れ直す事になりとても精神が持ちそうにありません。
簡単にterraformのversionを切り替えられるようにweb開発でも多く用いられる バージョンマネージャー を導入しましょう!nodenvとかrbenvとかそういうやつです。
terraformにおけるバージョンマネージャーは tfenv が一般的です。

installしたらversionを固定するためにプロジェクトルートに .terraform-version を作成しておくとチーム開発時も困らなくてとてもいいですね?

$ brwe install tfenv

$ cd ~/YOUR_WORKING_DIR

# remoteのversionを調べる
$ tfenv list-remote
0.14.0-rc1
0.14.0-beta2
0.14.0-beta1
0.14.0-alpha20201007
0.14.0-alpha20200923
0.14.0-alpha20200910
0.13.5
0.13.4
0.13.3
...

$ echo 0.13.5 > .terraform-version

$ tfenv install
Installing Terraform v0.13.5
Downloading release tarball from https://releases.hashicorp.com/terraform/0.13.5/terraform_0.13.5_darwin_amd64.zip
######################################################################## 100.0%
Downloading SHA hash file from https://releases.hashicorp.com/terraform/0.13.5/terraform_0.13.5_SHA256SUMS
No keybase install found, skipping OpenPGP signature verification
Archive:  tfenv_download.rqwb9E/terraform_0.13.5_darwin_amd64.zip
  inflating: /usr/local/Cellar/tfenv/2.0.0/versions/0.13.5/terraform
Installation of terraform v0.13.5 successful. To make this your default version, run 'tfenv use 0.13.5'

$ terraform -v
Terraform v0.13.5

詳しくは作者の方のQiitaの記事があるのでそちらを掲載させていただきます。
https://qiita.com/kamatama_41/items/ba59a070d8389aab7694

ひとまずterraformをinstallしてinitしてplanできるところまで作成できました!

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_vpc.example will be created
  + resource "aws_vpc" "example" {
      + arn                              = (known after apply)
      + assign_generated_ipv6_cidr_block = false
      + cidr_block                       = "10.0.0.0/16"
      + default_network_acl_id           = (known after apply)
      + default_route_table_id           = (known after apply)
      + default_security_group_id        = (known after apply)
      + dhcp_options_id                  = (known after apply)
      + enable_classiclink               = (known after apply)
      + enable_classiclink_dns_support   = (known after apply)
      + enable_dns_hostnames             = (known after apply)
      + enable_dns_support               = true
      + id                               = (known after apply)
      + instance_tenancy                 = "default"
      + ipv6_association_id              = (known after apply)
      + ipv6_cidr_block                  = (known after apply)
      + main_route_table_id              = (known after apply)
      + owner_id                         = (known after apply)
    }

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

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

この章のまとめ

  • versionが複数あると切り替えるのが辛い
  • terraformのバージョンマネージャーはtfenv
  • 今回のコミット

2. tflint

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

先ほど無事にterraformを使えるようになりましたが問題があります。
tabを連打して見たこともないインデントでコミットしていました😭
https://github.com/HomMarkHunt/awesome-terraform-impl/commit/c34bc63fdcb5f12210b435623d52fdbfd4923b56#diff-dc46acf24afd63ef8c556b77c126ccc6e578bc87e3aa09a931f33d9bf2532fbbR21-R23

今回は私のミスでしたが、チーム開発では人によってインデントがずれたりタブの数が違ったりして最悪死人が出るケースがあります。これを防ぐためにビルドインの fmt コマンドを使うのが一般的かと思われます。

$ terraform fmt -diff
main.tf
--- old/main.tf
+++ new/main.tf
@@ -19,5 +19,5 @@
 }

 resource "aws_vpc" "example" {
-          cidr_block = "10.0.0.0/16"
+  cidr_block = "10.0.0.0/16"
 }

無事インデントが修正されました。
ですがこれ以外にも問題があります。fmtを実施してかつvalidateも実施したにも関わらず、terraform applyを実行するとerrorになる場合があります。(EC2の存在しないリソースタイプを指定した時など)これは実際に開発しているとかなり心にくるものがあります。
こんな事にならないためにterraformのLinter tflintを導入しましょう!

$ brew install tflint

合わせてproject rootに.tflint.hclを配備しておくと自動で読み込んでくれます!.tflint.hcl内で設定できる ルールの一覧はこちらです!

最後にcommit時にfmttflintがかかるようにしておきましょう!
pre-commitのファイル作成と設定変更をしました!(ここは本編にあまり関係ないので割愛します こちらの記事 を参考にさせて頂きました)

$ cat .githooks/pre-commit
terraform fmt -diff
tflint

for FILE in `git diff --diff-filter=d --staged --name-only`; do
  git add $FILE
done

$ git cm -m "add tflint"
main.tf
--- old/main.tf
+++ new/main.tf
@@ -19,5 +19,5 @@
 }

 resource "aws_vpc" "example" {
-                                                       cidr_block = "10.0.0.0/16"
+  cidr_block = "10.0.0.0/16"
 }
[main 39a878f] add tflint
 Date: Tue Dec 1 06:15:41 2020 +0900
 3 files changed, 10 insertions(+), 1 deletion(-)
 create mode 100755 .githooks/pre-commit
 create mode 100644 .tflint.hcl

この章のまとめ

3. setup-terraform

https://github.com/marketplace/actions/hashicorp-setup-terraform

terraformのplan/applyと開発時のLintチェックまで実施されるようになりました!👏
ここまできたらいよいよterraformをCIに組み込みましょう!
勿論CIは 今使っているもの/お好きなもの で構いませんが、おそらく最も手軽なのはGitHub Actionsとsetup-terraformを使う方法でしょう。
setup-terraform はGitHub Actionsを利用する際に簡単に plan/apply を実行できるようmarcketplaceに公開されているHashicorp公式が作っているActionです!

GitHub Actionsの説明は割愛しますが作成したyamlはこんな感じです!

name: pr-plan

on:
  push:
    branches-ignore:
      - main

jobs:
  terraform:
    name: Terraform
    runs-on: ubuntu-latest

    defaults:
      run:
        shell: bash
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: 0.13.5

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

      - name: Terraform Plan
        id: plan
        run: terraform plan

setup-terraformを利用したので面倒な環境設定は不要でこれだけ!簡単にPRにPlanを自動で実行されるようになりました!

おまけ

ついでにmain branchにmergeされた時に自動でapplyされるActionも作成しておきました。
terraformのCI戦略は多数あるかと思いますが私はkimさんのこちらの記事がベストと思っています。

https://qiita.com/kimh/items/2d3d8269fbc016918f80

この章のまとめ

4. tfnotify

https://github.com/mercari/tfnotify

CIもできてチーム開発もバッチリ!と思いきや実際に こちらのPR をレビューしてみましょう。
レビューするとなるとシンプルな作成だけならまだいいですが更新や削除など複数存在すると流石にコードからのみ判断するのはすごく難しいです。おそらくほとんどの方はPlanの結果を見て「不要なリソースが作られていないか」「更新箇所は合っているか」などレビューをするのではないかと思います。

ではもう一度レビュワーの気持ちになってみましょう。
確かにGitHub Actionsの実行結果を見ればPlanの結果はわかりますが、少し導線が遠いです😥
また、planの結果を見てConversationで議論するなどといったこともできません。😥

もう少しレビュワーフレンドリーにしていきましょう!

もし、PRにPlan結果がコメントされていたらどうでしょう?
PRを開くと同時にPlan結果が見れますし、続けてコメントすることもできます!とても良さそうです!
それを実現するためにtfnotifyを利用します。
tfnotifyはterraformのplanやapply結果をGitHubやSlackに送信するのを手助けしてくれるツールです。

先ほどのActionsの設定にtfnotifyのinstallを追加します

- name: Install tfnotify
  id: install
  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

既存のplanも少し修正します

- name: Terraform Plan
  id: plan
  run: terraform plan | tfnotify -config .tfnotify/github.yaml plan
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Slack, GitHubへの通知のフォーマットの設定も作成すれば完了です!
無事PRにplan結果がコメントされました!!👏👏👏

Tips

GitHub ActionsではGITHUB_TOKENが最初から用意されておりこのTokenを使えばbot userからのコメントが簡単にできるようです!

env:
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

この章まとめ

5. tfsec

https://github.com/tfsec/tfsec

先ほどのPRはマージして無事リソースを作成することができました!👏
このまま作業を続けていきましょう!今度はECRが必要になりました!早速PRを作っておきます!

resource "aws_ecr_repository" "example" {
  name = "sample"
}

こちらを追加して

  • コミット時のformat, lintもおkで
  • PRのcheckもおk
  • 差分もおk


作ったものがうまく機能していますね!問題なさそうです!早速マージしましょう!

...と言いたいところですが一つ問題があります🤯
ECRにはpush時にimageをスキャンして脆弱性を検知してくれる仕組みがあります。セキュリティー的にこちらのimage scanも有効にしておきたいです!しかし、image scanの設定 image_scanning_configuration はOptionalな項目であり、設定しなくても構文エラーがあるわけでもないので今の仕組みでは検知できません🤯

このようにformatやLintではなくセキュリティ的に問題がある場合も自動で検知しておきたいです。
このようなユースケースでは tfsec を利用しましょう!

tfsec はterraformのコードからセキュリティ問題を検知してくれるツールです。setup-terraform と同じように公式からActionがMarketplaceで公開されています!
こちらを利用してもいいのですが、せっかくなので簡単にPRで検知できるようにしていきます reviewdogtfsec をラップしてPRで使いやすいようにしてこちらもActionが公開されていますのでこちらを利用していきます!
https://github.com/marketplace/actions/run-tfsec-with-reviewdog

planとはcheck結果を分けたかったので新しくworkflowを作っておきます。一緒にしたい場合は先ほどのファイルに追記する形でも問題ないと思います。

name: pr-tfsec

on: [pull_request]

jobs:
  tfsec:
    name: tfsec
    runs-on: ubuntu-latest

    defaults:
      run:
        shell: bash
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: tfsec
        uses: reviewdog/action-tfsec@master
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          fail_on_error: "true"
          filter_mode: "nofilter"

各種設定は こちら にありますので、お好みで設定してみてください。

早速Actionが起動してcheckがfailしました!👏

ECRのimage scanの部分にもコメントが来ています!修正箇所や何をすればいいのかすごく分かり易いです!

修正して push しておきます。
今度は無事にcheck通りました!

memo

tfsec 用のActionを作成した後、 reviewdog: this is not PullRequest build. と表示されcheckが走らない状態になりました。
PRをcloseして再度開いたら無事に起動し、以降再現しませんでした。

この章のまとめ

  • セキュリティの観点からもPRをチェックしたい
  • tfsecのreviewdogがPRに便利
  • PRはこちら

6. tfupdate

https://github.com/minamijoyo/tfupdate

ここまででかなり開発時の体験を向上できました!(できたな?)
最後にterraform利用者なら誰もが悩むポイントに触れていこうと思います!
冒頭でもお話しした通りterraformの開発は非常に活発に行われており、またterraform本体だけでなくAWSも日夜開発が行われておりそれに対応するためaws providerも非常に高い頻度で開発が行われています。そこで発生するのがバージョンアップ問題です😧
webアプリの開発でもライブラリやフレームワークのバージョンなど油断すると置いていかれてしまいバージョンアップに苦労した。なんて経験がある方も多いと思いますが、だいたい同じような問題がterraform開発時にも発生します。webアプリ開発では DependabotScala Steward などが用いられますがterraformの場合は tfupdate があります。

tfupdateは作者の方のQiitaの記事から抜粋させていただくと

これをCIとかジョブスケジューラで流せば、毎日最新版をチェックして、差分があれば自動でバージョンアップのPull Requestを生成したりできます。つまり、「ぼくのかんがえたさいきょうのTerraform版Dependabot」ができるのだ。

https://qiita.com/minamijoyo/items/1350fb28ae82a3dbb354

こちらもGitHub Actionsで動かして毎朝起動して、version upがあればPRを送ってもらうようにしましょう!
使い方に関しては上記記事に詳細に書かれているのでここでは省略しますが、主に terraform, provider, module, releaseのversionを更新できます。今回はprovider等は人によって違うと思うのでterraformを自動で検知するようにしてみましょう。

name: tfupdate

on:
  schedule:
    - cron: "00 23 * * 0-4" # 8:00 JST/SUN-THU
  workflow_dispatch:

jobs:
  tfupdate:
    name: tfupdate
    runs-on: ubuntu-latest

    defaults:
      run:
        shell: bash
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Install
        run: |
          sudo curl -fL -o tfupdate.tar.gz https://github.com/minamijoyo/tfupdate/releases/download/v0.4.2/tfupdate_0.4.2_linux_amd64.tar.gz
          sudo tar -C /usr/bin -xzf ./tfupdate.tar.gz

      - name: git setting
        run: |
          git config --local user.email ${{ secrets.GITHUB_EMAIL }}
          git config --local user.name ${{ secrets.GITHUB_USERNAME }}

      - name: Update terraform to latest
        run: |
          VERSION=$(tfupdate release latest hashicorp/terraform)
          UPDATE_MESSAGE="[tfupdate] Update terraform to v${VERSION}"
          if hub pr list -s "open" -f "%t: %U%n" | grep -F "$UPDATE_MESSAGE"; then
            echo "A pull request already exists"
          elif hub pr list -s "merged" -f "%t: %U%n" | grep -F "$UPDATE_MESSAGE"; then
            echo "A pull request is already merged"
          else
            git checkout -b update-terraform-to-v${VERSION}
            tfupdate terraform -v ${VERSION} -r ./
            if git add . && git diff --cached --exit-code --quiet; then
            echo "No changes"
            else
            git commit -m "$UPDATE_MESSAGE"
            PULL_REQUEST_BODY="For details see: https://github.com/hashicorp/terraform/releases"
            git push origin HEAD && hub pull-request -m "$UPDATE_MESSAGE" -m "$PULL_REQUEST_BODY" -b main
            fi
          fi
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

無事PRが送られてきました👏👏👏

この章のまとめ

さいごに

ご覧いただきありがとうございました!
自分の知ってる範囲でDX向上に繋がりそうなものを紹介させていただきました。「こんなのもあるよ!」とか「ここちょっとおかしくない?」等ご意見あれば(優しい言葉で)ドシドシコメントいただけると嬉しいです!
課題感としては今回紹介した構成ですと 一括plan/一括apply が前提の作りになっています。この場合最大の敵が 構成ドリフト になると思います。手動でGHAを書いて検知してもいいですが、何か素敵なツールをご存知の方がいらっしゃいましたらこちらもご教授いただけると嬉しいです!
以上ありがとうございました!

参考

Discussion