tflint の --deep オプション便利

## 2020-01-09 追記

--deep オプションは v0.23.0 で廃止されました。`.tflint.hcl` で設定するようです。
https://github.com/terraform-linters/tflint/releases/tag/v0.23.0

## tflint
便利ですよね。

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

まず軽く動かします。

リポジトリはこちら。

https://github.com/chaspy/try-tflint-deep-option

tflint の example にあるように、こういう tf file の plan は通ってしまいますが

```hcl
resource "aws_instance" "foo" {
  ami           = "ami-0ff8a91507f77f867"
  instance_type = "t1.2xlarge" # invalid type!
}
```

plan

```diff
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_instance.foo will be created
  + resource "aws_instance" "foo" {
      + ami                          = "ami-0ff8a91507f77f867"
      + arn                          = (known after apply)
      + associate_public_ip_address  = (known after apply)
      + availability_zone            = (known after apply)
      + cpu_core_count               = (known after apply)
      + cpu_threads_per_core         = (known after apply)
      + get_password_data            = false
      + host_id                      = (known after apply)
      + id                           = (known after apply)
      + instance_state               = (known after apply)
      + instance_type                = "t1.2xlarge"
      + ipv6_address_count           = (known after apply)
      + ipv6_addresses               = (known after apply)
      + key_name                     = (known after apply)
      + outpost_arn                  = (known after apply)
      + password_data                = (known after apply)
      + placement_group              = (known after apply)
      + primary_network_interface_id = (known after apply)
      + private_dns                  = (known after apply)
      + private_ip                   = (known after apply)
      + public_dns                   = (known after apply)
      + public_ip                    = (known after apply)
      + secondary_private_ips        = (known after apply)
      + security_groups              = (known after apply)
      + source_dest_check            = true
      + subnet_id                    = (known after apply)
      + tenancy                      = (known after apply)
      + volume_tags                  = (known after apply)
      + vpc_security_group_ids       = (known after apply)

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      + metadata_options {
          + http_endpoint               = (known after apply)
          + http_put_response_hop_limit = (known after apply)
          + http_tokens                 = (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      + root_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (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.
```

ちゃんと怒ってくれるわけですね。

```bash
$ tflint
1 issue(s) found:

Error: "t1.2xlarge" is an invalid value as instance_type (aws_instance_invalid_type)

  on main.tf line 3:
   3:   instance_type = "t1.2xlarge" # invalid type!
```

## --deep option

>--deep                                      Enable deep check mode

deep check mode なるものがあるんですね。

https://github.com/terraform-linters/tflint/tree/master/docs/rules#possible-errors

apply のときにこける系のやつで、api 問い合わせをしないとわからないようなやつをチェックしてくれるみたいです。

さて、instance type を valid のものにします。こうすると tflint は通りますね。

```hcl
$ cat main.tf
resource "aws_instance" "foo" {
  ami           = "ami-0ff8a91507f77f867"
  instance_type = "t3.small"
}
```

```
$ tflint
$ echo $?
0 # ok
```

ところで tflint はどういった仕組みで instance_type をチェックしているかというと、今回の例でいえば [tflint/rules/awsrules/models/aws_instance_invalid_type.go](https://github.com/terraform-linters/tflint/blob/9844546c5ae6391e42a8a746f9af7ea1325d946c/rules/awsrules/models/aws_instance_invalid_type.go#L36) に 定義されていますね。新しいインスタンスタイプがサポートされた場合はコントリビューションチャンスかもしれませんね。

ざっとコードを見ると、resource type と attribute name と valid な list として enum を定義して model Rules ととして呼び出すだけでいいんですね。ルールの追加も簡単そうで良い。

さて、次に ami を不正なものに変えてみます。

```hcl
resource "aws_instance" "foo" {
  ami           = "ami-1234xxxx1234yyyy" # invalid ami!
  instance_type = "t3.small"
}
```

引数なしだと通っちゃいますね。

```bash
$ tflint
$ echo $?
0 # ok
```

deep option つけてみましょう。

```bash
$ tflint --deep
1 issue(s) found:

Error: "ami-1234xxxx1234yyyy" is invalid AMI ID. (aws_instance_invalid_ami)

  on main.tf line 2:
   2:   ami           = "ami-1234xxxx1234yyyy" # invalid ami!
```

ちゃんと怒ってくれました！すごい！

deep check mode では api を call するためなんらかの方法で credential を渡してあげる必要があります。

普通に Terraform で plan や apply を実行するように、こんな感じに環境変数でもいいですし、他のパターンもサポートしているようです。

```bash
$ cat .envrc
export AWS_ACCESS_KEY_ID="secret-secret"
export AWS_SECRET_ACCESS_KEY="secret-secret-secret-secret"
export AWS_DEFAULT_REGION="ap-northeast-1"
```

ref: https://github.com/terraform-linters/tflint/blob/9844546c5ae6391e42a8a746f9af7ea1325d946c/docs/guides/credentials.md#credentials

今回適用されたルールは `aws_instance_invalid_ami` ですね。

ちなみに tflint は loglevel をオプションで指定できます。挙動に対してソースコードを追うのに便利でいいですね。

```bash
$ tflint --deep --loglevel=debug                                                                                                                                                               [main]
14:58:53 config.go:98: [INFO] Load config: .tflint.hcl
14:58:53 config.go:110: [INFO] Default config file is not found. Ignored
14:58:53 config.go:119: [INFO] Load fallback config: /Users/chaspy/.tflint.hcl
14:58:53 config.go:127: [INFO] Fallback config file is not found. Ignored
14:58:53 config.go:129: [INFO] Use default config
14:58:53 option.go:54: [DEBUG] CLI Options
14:58:53 option.go:55: [DEBUG]   Module: false
14:58:53 option.go:56: [DEBUG]   DeepCheck: true
14:58:53 option.go:57: [DEBUG]   Force: false
14:58:53 option.go:58: [DEBUG]   IgnoreModules: map[string]bool{}
14:58:53 option.go:59: [DEBUG]   EnableRules: []string(nil)
14:58:53 option.go:60: [DEBUG]   DisableRules: []string(nil)
14:58:53 option.go:61: [DEBUG]   Only: []string(nil)
14:58:53 option.go:62: [DEBUG]   Varfiles: []string{}
14:58:53 option.go:63: [DEBUG]   Variables: []string{}
14:58:53 loader.go:57: [INFO] Initialize new loader
14:58:53 loader.go:82: [INFO] Load configurations under .
14:58:53 loader.go:90: [INFO] Module inspection is disabled. Building a root module without children...
14:58:53 loader.go:170: [INFO] Load values files
14:58:53 runner.go:52: [INFO] Initialize new runner for root
14:58:53 provider.go:48: [INFO] `access_key` is not found in the provider block.
14:58:53 provider.go:48: [INFO] `secret_key` is not found in the provider block.
14:58:53 provider.go:48: [INFO] `profile` is not found in the provider block.
14:58:53 provider.go:48: [INFO] `shared_credentials_file` is not found in the provider block.
14:58:53 runner_eval.go:67: [WARN] Unevaluable expression found in provider.tf:2; TFLint ignores an unevaluable expression.
14:58:53 provider.go:76: [INFO] `assume_role` is not found in the provider block.
14:58:53 aws.go:83: [INFO] Initialize AWS Client
14:58:53 awsauth.go:242: [INFO] AWS Auth provider used: "EnvProvider"
14:58:53 awsauth.go:143: [DEBUG] Trying to get account information via sts:GetCallerIdentity
14:58:54 provider.go:87: [INFO] Prepare rules
14:58:54 provider.go:93: [DEBUG] Deep check mode is enabled. Add deep check rules
14:58:54 provider.go:123: [INFO]   794 rules enabled
14:58:54 runner_walk.go:36: [DEBUG] Walk `aws_instance.foo.instance_type` attribute
14:58:54 runner_walk.go:36: [DEBUG] Walk `aws_instance.foo.instance_type` attribute
14:58:54 runner_walk.go:36: [DEBUG] Walk `aws_instance.foo.ami` attribute
14:58:54 aws_instance_invalid_ami.go:60: [DEBUG] Fetch AMI images: ami-1234xxxx1234yyyy
1 issue(s) found:

Error: "ami-1234xxxx1234yyyy" is invalid AMI ID. (aws_instance_invalid_ami)

  on main.tf line 2:
   2:   ami           = "ami-1234xxxx1234yyyy" # invalid ami!
```

trace はあまりに多かったので debug のものを載せてみました。

valid な ami かどうかを確かめるために api call しているのはここですね。

https://github.com/terraform-linters/tflint/blob/9844546c5ae6391e42a8a746f9af7ea1325d946c/rules/awsrules/aws_instance_invalid_ami.go#L60

## おわりに
tflint, デフォルトだと credentrial 不要というのもイケてますし、--deep 多少 api call に時間かかるものの、CI で実行してあげると Apply 時に爆死するパターンが減りそうです。

どんどん導入して使っていきたいと思いました。