🔖

TFLint: Terraform用静的解析ツールの導入手順

2024/02/04に公開

アプリケーションのコードに対して、フォーマッタやリンタを適用すべきであるのと同様に、Terraformの設定ファイルにもフォーマッタやリンタを適用するべきである。

フォーマッタについてはterraform fmtコマンドがビルトインで提供されているが、リンタについては提供されていない。

よってサードパーティのリンタツールを探す必要があるが、おそらく最も有名なのがTFLintである。

TFLintはチェックしてくれるルールはそこまで多くないものの、リソース名をスネークケースに統一したり、コメントを//ではなく#だけを使うように強制したり、variableブロックやoutputブロックをそれぞれvariables.tfやoutputs.tfに集約することをルール化したりと、地味に嬉しいチェックが揃っている。

なお最近HashiCorp社が発表したTerraformのコードスタイルガイドでも、このTFLintについて言及しており、公式からも存在を認められているツールである。

それではTFLintを導入する手順を説明する。

手順

Macのhomebrewを使用している場合、TFLintは以下のコマンドでインストールできる。

$ brew install tflint

その他のインストール方法はこちら

CIについては、Github ActionsのActionのみが提供されている。

TFLintを検証するにあたって、まず以下のような簡単なサンプルプロジェクトを作成する。

$ mkdir -p /tmp/sample-project/terraform/modules/storage
$ vi /tmp/sample-project/terraform/modules/storage/main.tf
# 設定内容は下記
/tmp/sample-project/terraform/modules/storage/main.tf
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "5.24.0"
    }
  }
  required_version = "1.7.5"
}

provider "google" {
  project = "your-gcp-project"
  region  = "asia-northeast1"
}

resource "google_storage_bucket" "example" {
  name          = "${var.env}-example-bucket"
  location      = "ASIA"
  storage_class = "MULTI_REGIONAL"
  versioning {
    enabled = true
  }
  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"
}
$ cd /tmp/sample-project
$ tree -a
.
└── terraform
    └── modules
        └── storage
            └── main.tf

4 directories, 1 file

プロジェクトの作成が完了したら、次はTFLintの設定ファイルを定義する。

個人的に使用している設定ファイルは以下のようになる。

/tmp/sample-project/terraform/.tflint.hcl
$ vi /tmp/sample-project/terraform/.tflint.hcl
# ref. https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/config.md
config {
}

# ref. https://github.com/terraform-linters/tflint-ruleset-google
plugin "google" {
  enabled = true
  version = "0.27.1"
  source  = "github.com/terraform-linters/tflint-ruleset-google"
}

# resource名はスネークケース表記にする必要がある
rule "terraform_naming_convention" {
  enabled = true
}

# コメントは#を使う(//は使わない)
rule "terraform_comment_syntax" {
  enabled = true
}

# variableブロックやoutputブロックはvariables.tfやoutputs.tfに定義する必要がある
rule "terraform_standard_module_structure" {
  enabled = true
}

# terraformブロック内に必ずrequired_versionを宣言する必要がある
# なお、自分のプロジェクトのフォルダ構造では、このルールを満たすのは不可能なので無効化
# ref. https://github.com/erueru-tech/infra-testing-google-sample
# しかし通常は有効にした方がいいかもしれない
rule "terraform_required_version" {
  enabled = false
}

設定ファイルでは、まず最初にconfigブロックを定義しているが設定は何も記述していない。

これは設定ドキュメントを確認した上で、特に運用上必要な設定が現状無かったため、このようになっている。

次にplugin "google"を指定しているが、プラグインはTFLintのチェック機能を拡張するための機能である。

googleプラグインはGCPリソース用のチェックを追加で行うためのプラグインとなっていて、googleプラグイン以外にもAWSAzure用のプラグインがあるので、各自利用しているクラウドに合わせたプラグインをインストールすることになる。

残りのruleブロックはデフォルトのルールに対して、一部ルールを有効化/無効化するように変更している。

以上がTFLintの設定ファイルに関する説明となる。

この設定ファイルをtflintコマンド実行時に読み込む必要があるが、その方法はいくつかある。

まずtflintコマンド実行時に--config /path/to/.tflint.hclオプションを指定する方法だが、この方法はtflintコマンドの実行ディレクトリ次第でチェックがまったく上手く動かないケースがあるため、個人的にはお勧めできない。

それに対して、環境変数TFLINT_CONFIG_FILE.tflint.hclファイルのパスを指定して、tflintコマンドを実行する方法は挙動が安定している気がするため、以降ではこの方法を採用している。

以上でTFLintを使用する準備は完了となる。

あとは以下のようにコマンドを実行することで、HCLファイルに対してTFLintのチェックを実行出来る。

# 設定ファイルが存在するディレクトリに移動
$ cd /tmp/sample-project/terraform/

# 設定ファイルで指定されたプラグインをインストール
$ tflint --init
Installing "google" plugin...
Installed "google" (source: github.com/terraform-linters/tflint-ruleset-google, version: 0.27.1)

# ホームディレクトリ配下にプラグインがダウンロードされる
$ ls -l ~/.tflint.d/

# TFLintの実行
$ cd modules/storage/
$ TFLINT_CONFIG_FILE=/tmp/sample-project/terraform/.tflint.hcl tflint
2 issue(s) found:

Warning: variable "env" should be moved from main.tf to variables.tf (terraform_standard_module_structure)

  on main.tf line 16:
  16: variable "env" {

Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.5.0/docs/rules/terraform_standard_module_structure.md

Warning: Module should include an empty outputs.tf file (terraform_standard_module_structure)

  on outputs.tf line 1:
   (source code not available)

Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.5.0/docs/rules/terraform_standard_module_structure.md

tflintコマンドの実行結果メッセージを確認すると、main.tfにvariableを定義するのではなく、variables.tfに定義するべきと警告を受けている。

あとTerraformのモジュールにはoutputs.tfを作成すべきとの警告も行われている。

警告に従って、variableの定義をmain.tfからvariables.tfに移動して、さらにstorageディレクトリ配下にoutputs.tfを作成することで、警告は消える。

$ vi variables.tf
variable "env" {
  type = string
}

$ vi main.tf
# variable "env"の設定を消す

$ touch outputs.tf
$ TFLINT_CONFIG_FILE=/tmp/sample-project/terraform/.tflint.hcl tflint
$ echo $?
0

なお、一般的な静的解析ツール同様に、コメントによってチェックを限定的に無効化することも出来る。

試しにmain.tfのrequired_providersのgoogleプロバイダのバージョン情報をコメントアウトして、tflintコマンドを実行すると以下のような警告が出る。

/tmp/sample-project/terraform/modules/storage/main.tf
$ vi main.tf
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      # 以下をコメントアウト
      # version = "5.24.0"
    }
  }
  required_version = "1.7.5"
}

$ TFLINT_CONFIG_FILE=/tmp/sample-project/terraform/.tflint.hcl tflint

1 issue(s) found:

Warning: Missing version constraint for provider "google" in `required_providers` (terraform_required_providers)

  on main.tf line 3:
   3:     google = {
   4:       source  = "hashicorp/google"
   5:       #version = "5.24.0"
   6:     }

Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.5.0/docs/rules/terraform_required_providers.md

これに対して、tflint-ignore: terraform_required_providersコメントを宣言することで、チェックを無効にすることができる。

/tmp/sample-project/terraform/modules/storage/main.tf
$ vi main.tf
terraform {
  required_providers {
    # tflint-ignore: terraform_required_providers # ignoreを行う理由をここに書く
    google = {
      source  = "hashicorp/google"
      #version = "5.24.0"
    }
  }
  required_version = "1.7.5"
}
...

$ TFLINT_CONFIG_FILE=/tmp/sample-project/terraform/.tflint.hcl tflint
$ echo $?
0

行単位ではなくファイル全体、さらに複数同時にignoreを行いたい場合は、以下のようにtflint-ignore-fileをHCLファイルの1行目に定義する。

# tflint-ignore-file: terraform_standard_module_structure, terraform_unused_declarations
...

tflint-ignoreコメントに関するドキュメントはこちら

追記

この記事を最初に投稿した際にはtflintコマンドの-recursiveオプションを使用して、/tmp/sample-project/terraform ディレクトリから再起的にチェックを行う手順を書いていたが、0.49.0から0.50.3にバージョンアップした際に、以下のような相対パスを使用したモジュール定義があるとパス解決がうまくできないといった問題に遭遇した。

module "storage" {
  source                  = "../../../modules/storage"
  ...
}

よって現在では-recursiveオプションを諦めて、HCLファイルが存在する全ディレクトリに移動した上でtflintコマンドを実行するようにしている。

ただHashiCorpのスタイルガイドで推奨している以下のようなモジュールのディレクトリ構成だと、何度もディレクトリを移動する必要が出てきて、チェックに時間がかかる。

.
├── modules
│   ├── function
│   │   ├── main.tf      # contains aws_iam_role, aws_lambda_function
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── queue
│   │   ├── main.tf      # contains aws_sqs_queue
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── vpc
│       ├── main.tf      # contains aws_vpc, aws_subnet
│       ├── outputs.tf
│       └── variables.tf
...

そのため以下のようなスクリプト等を用意して、実行の手間を減らす必要がある。

tflint.sh
#!/bin/bash

set -eu

ROOT_DIR=/tmp/sample-project

# modules配下のディレクトリ一覧を抽出して、1ディレクトリずつtflintコマンドを実行
while IFS= read MODULE_DIR; do
  echo "run tflint at $MODULE_DIR"
  (cd $MODULE_DIR && TFLINT_CONFIG_FILE=$ROOT_DIR/terraform/.tflint.hcl tflint)
done <<< "$(find $ROOT_DIR/terraform/modules -type d -mindepth 1 -maxdepth 1)"

Discussion