VSCodeのTerraform拡張でtflintが効いてなかったのでスクリプトで対処する

この記事は [DENSO アドベントカレンダー 2021](https://qiita.com/advent-calendar/2021/denso) の 19 日目の記事です。

業務で Terraform を使用してインフラを管理しています。VSCode で[Terraform 拡張](https://github.com/hashicorp/vscode-terraform)を利用してコードを書いているのですが、現状この拡張機能は tflint と連携していません。コーディング中も自動解析したいと思いスクリプトを作りました。

ファイルの編集差分を検知したら tflint を自動実行します。使用技術は昨日の記事（[[Go]ローカル環境でのテスト自動実行スクリプトにより開発体験を向上する](https://qiita.com/ktr03/items/6b20cd1d401c88c4bfd8)）と変わりないので恐縮ですが、アイデア次第で色々転用ができるというメッセージを発信するという建前のもと始めたいと思います。

# Terraform 拡張で tflint が効かない要因を調査する

とにかくスクリプトが知りたいという方は読み飛ばしてください → [#tflint を自動実行する](#tflint-を自動実行する)

VSCode で言語サポートの拡張機能を利用すると、静的解析の結果をエディタ中に波線強調したり PROBLEMS タブに問題が一覧表示されますが、Terraform 拡張を利用しても tflint の機能の恩恵は受けられません。

過去の記事を検索すると、VSCode の Terraform 拡張 (以下 tf 拡張と呼ぶ) の導入時に tflint を設定する内容が見つかります。この記事の表題について疑問を持った方もおられるかと思います。結論から言うと tf 拡張の v2.0.0 リリース時に tflint との連携機能は削除され、その後のアップデートでも復活していません。

> External commands such as terraform validate and tflint are removed from the extension, but we plan to add hooks for these and/or additional integrations via the language server.

https://github.com/hashicorp/vscode-terraform/releases/tag/v2.0.0

リリースノートを確認すると、tf 拡張自身の機能として tflint との連携機能を提供していたが、連携機能を language server に寄せて tf 拡張はそれを呼び出す形で機能提供するという方針に基づく変更のようです。この変更自体は language server を利用する他のエディタでも恩恵を受けられることから良いことだと思います。

対応状況は下記で確認できます。起票から１年以上経過していますが、先日 language server 側で更新があったので今後の動きに期待したいところです。いち早くエディタの機能として tflint を利用したい方はこれらをウォッチしておくと良いでしょう。

https://github.com/hashicorp/vscode-terraform/issues/337

https://github.com/hashicorp/terraform-ls/issues/334

# tflint を自動実行する

language server と拡張機能の対応が完了するまでのワークアラウンドとして、ターミナルで tflint を自動実行する仕組みを用意します。

## 事前準備

tflint、make をインストールします。手順はこの記事では割愛します。

## スクリプト

プロジェクトのルートディレクトリに以下２つのファイルを用意します。Makefile はインデントがタブであることに注意。

```makefile:makefile
.PHONY: tfcheck

tfcheck:
	reflex -r '\.tf$$' ./check.sh
```

```shell:check.sh
#!/bin/bash

cd "$(dirname "$0")" || exit 1

find . -type f -name '*.tf' -exec dirname {} \; | sort -u | xargs -n 1 tflint --enable-rule=terraform_unused_declarations

if [ "$?" -ne 0 ]; then
  echo "======================================================================="
else
  echo "Ok. ==================================================================="
fi
```

`chmod +x check.sh`で実行権限をつけておきます。

実行してみましょう。

```
$ make tfcheck
reflex -r '\.tf$' ./check.sh
```

## 処理内容の説明

ファイル編集により拡張子`.tf`ファイルに差分が生じたら、その都度`check.sh`を実行します。`check.sh`では、カレントディレクトリ配下の`.tf`ファイルが存在する全てのディレクトリを検索し、ディレクトリ毎に tflint を実行します。

`--enable-rule=`はデフォルトで無効となっている評価ルールを有効にするオプションです。今回使用する`terraform_unused_declarations`は未使用の変数定義を検出するルールでおすすめです。その他のデフォルトで無効になっている評価ルールは[こちら](https://github.com/terraform-linters/tflint/tree/master/docs/rules)で確認できます。型を宣言していない変数定義を検出するなどのルールがあります。

## module オプションに注意

tflint は子ディレクトリを再帰的に解析しません。前述のスクリプトでもディレクトリを検出し個別に tflint を実行しています。スマートに書くためのオプションが用意されていないかと公式ドキュメントを見ると`module`オプションが目を引きます。

> Does TFLint check modules recursively?
>
> > No. TFLint always checks only the current root module (no recursive check). However, you can check calling child modules based on module arguments by enabling Module Inspection. This allows you to check that you are not passing illegal values to the module.

https://github.com/terraform-linters/tflint#does-tflint-check-modules-recursively

一見 Module Inspection (`module` オプション) を使用すると、モジュールとして呼び出す子ディレクトリも評価されると勘違いしてしまいそうですが注意が必要です。解析するのはあくまで**モジュールに渡す引数**です。

以下のコードは`instance_type`をモジュールに渡しているため評価対象となり不正検出します。(`t1.2xlarge`は不正なインスタンスタイプです)

```terraform:main.tf
module "aws_instance" {
  source        = "./module"

  instance_type = "t1.2xlarge"
}
```

```terraform:module/web.tf
resource "aws_instance" "web" {
  instance_type        = var.instance_type
}
```

`instance_type`をモジュール内で定義した以下のコードでは不正を検出できません。

```terraform:main.tf
module "aws_instance" {
  source        = "./module"
}
```

```terraform:module/web.tf
resource "aws_instance" "web" {
  instance_type        = "t1.2xlarge"
}
```

公式ドキュメントでも [Caveats (警告)](https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/module-inspection.md
)の文言で`module`オプションの使用に注意を促しています。

ではどのようなユースケースがあるかというと、レジストリに登録された module を利用していて、モジュールは解析しないがモジュールに渡す引数が間違っていないかを解析するときなどが一例として挙げられそうです。その場合、`check.sh`を修正する必要があります。

```shell:check.sh
find . -type f -name '*.tf' -exec dirname {} \; | sort -u | xargs -n 1 tflint --enable-rule=terraform_unused_declarations
```

この部分を下記に書き換えます。

```shell:check.sh
find . -type d -name .terraform -prune -o -type f -name '*.tf' -exec dirname {} \; | sort -u | xargs -n 1 tflint --enable-rule=terraform_unused_declarations --module
```

`terraform init`実行時に作成される`./terraform`配下へ、レジストリからモジュールがダウンロードされます。そのディレクトリは解析対象外として除外しています。代わりに`module`オプションにより、モジュールに渡す値を解析対象としています。

https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/module-inspection.md

# 終わりに

アドベントカレンダーなので年末らしく締めたいと思います。今年を振り返ると AWS 資格５冠を達成しました。来年の抱負は AWS 資格 11 冠達成と OSS にコントリビュートすることです。それでは皆様良いお年を。