tflintを利用したTerraformの静的解析

インフラエンジニアの寺岡です。
今回はtflintを利用してTerraformのコードの静的解析をやってみたいと思います。
tflintとは
OSSで公開されているtfファイルに特化したlinterツールです。
リポジトリはこちらです。
tflintを使うことにより、非推奨の構文や未使用の宣言を記述している場合に警告を出したり
AWSなどのクラウドプラットフォーム上で発生する可能性のあるエラーを検知してくれたりします。
インストール
Macの場合はbrewを使ってインストール可能です。
$ brew install tflint
Linuxの場合はインストールスクリプトが用意されているのでそちらを利用できます。
$ curl https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
使い方
基本的にtfファイルが置かれているディレクトリでコマンドを実行します。
以下を実行するだけでカレントディレクトリ内のファイルを読み込んで解析して
問題があればエラーを出してくれます。
$ tflint
もう少し詳しく見ていきましょう。
カレントディレクトリにmain.tfとして以下のコードを記述します。
variable "role_arn" {
description = "AWS Role Arn"
}
provider "aws" {
region = "ap-northeast-1"
assume_role {
role_arn = var.role_arn
}
}
resource "aws_instance" "test" {
ami = "ami-0ca38c7440de1749a"
instance_type = "t3.micro"
tags = {
Name = "tflint-test"
}
}
terraform planで差分を確認するとEC2インスタンスを1台作成しようとしています。
$ terraform plan
------------------------------------------------------------------------
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.test will be created
+ resource "aws_instance" "test" {
+ ami = "ami-0ca38c7440de1749a"
+ 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 = "t3.micro"
+ 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)
+ tags = {
+ "Name" = "tflint-test"
}
+ tenancy = (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)
+ tags = (known after apply)
+ throughput = (known after apply)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
+ enclave_options {
+ enabled = (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)
+ tags = (known after apply)
+ throughput = (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.
実際にterraform applyで反映してみましょう。
$ terraform apply
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.test will be created
+ resource "aws_instance" "test" {
+ ami = "ami-0ca38c7440de1749a"
+ 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 = "t3.micro"
+ 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)
+ tags = {
+ "Name" = "tflint-test"
}
+ tenancy = (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)
+ tags = (known after apply)
+ throughput = (known after apply)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
+ enclave_options {
+ enabled = (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)
+ tags = (known after apply)
+ throughput = (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.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_instance.test: Creating...
aws_instance.test: Still creating... [10s elapsed]
aws_instance.test: Still creating... [20s elapsed]
aws_instance.test: Still creating... [30s elapsed]
aws_instance.test: Still creating... [40s elapsed]
aws_instance.test: Still creating... [50s elapsed]
aws_instance.test: Creation complete after 53s [id=i-085049c8fe2c58383]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
これはコードに特に問題はないので何事もなく反映できます。
次に一部コードを書き換えてみましょう。
再度planで差分を確認します。
t3.microをt4.microに変更しようとしていますね。
% 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.
aws_instance.test: Refreshing state... [id=i-085049c8fe2c58383]
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# aws_instance.test will be updated in-place
~ resource "aws_instance" "test" {
ami = "ami-0ca38c7440de1749a"
arn = "arn:aws:ec2:ap-northeast-1:485076298277:instance/i-085049c8fe2c58383"
associate_public_ip_address = true
availability_zone = "ap-northeast-1a"
cpu_core_count = 1
cpu_threads_per_core = 2
disable_api_termination = false
ebs_optimized = false
get_password_data = false
hibernation = false
id = "i-085049c8fe2c58383"
instance_state = "running"
~ instance_type = "t3.micro" -> "t4.micro"
ipv6_address_count = 0
ipv6_addresses = []
monitoring = false
primary_network_interface_id = "eni-0c78d105cbfaddc16"
private_dns = "ip-172-31-40-11.ap-northeast-1.compute.internal"
private_ip = "172.31.40.11"
public_dns = "ec2-54-249-80-70.ap-northeast-1.compute.amazonaws.com"
public_ip = "54.249.80.70"
secondary_private_ips = []
security_groups = [
"default",
]
source_dest_check = true
subnet_id = "subnet-9621c1de"
tags = {
"Name" = "tflint-test"
}
tenancy = "default"
vpc_security_group_ids = [
"sg-485f1735",
]
credit_specification {
cpu_credits = "unlimited"
}
enclave_options {
enabled = false
}
metadata_options {
http_endpoint = "enabled"
http_put_response_hop_limit = 1
http_tokens = "optional"
}
root_block_device {
delete_on_termination = true
device_name = "/dev/xvda"
encrypted = false
iops = 100
tags = {}
throughput = 0
volume_id = "vol-012c9259f69420c1c"
volume_size = 8
volume_type = "gp2"
}
}
Plan: 0 to add, 1 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.
このままapplyで反映してみましょう。
% terraform apply
aws_instance.test: Refreshing state... [id=i-085049c8fe2c58383]
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# aws_instance.test will be updated in-place
~ resource "aws_instance" "test" {
ami = "ami-0ca38c7440de1749a"
arn = "arn:aws:ec2:ap-northeast-1:485076298277:instance/i-085049c8fe2c58383"
associate_public_ip_address = true
availability_zone = "ap-northeast-1a"
cpu_core_count = 1
cpu_threads_per_core = 2
disable_api_termination = false
ebs_optimized = false
get_password_data = false
hibernation = false
id = "i-085049c8fe2c58383"
instance_state = "running"
~ instance_type = "t3.micro" -> "t4.micro"
ipv6_address_count = 0
ipv6_addresses = []
monitoring = false
primary_network_interface_id = "eni-0c78d105cbfaddc16"
private_dns = "ip-172-31-40-11.ap-northeast-1.compute.internal"
private_ip = "172.31.40.11"
public_dns = "ec2-54-249-80-70.ap-northeast-1.compute.amazonaws.com"
public_ip = "54.249.80.70"
secondary_private_ips = []
security_groups = [
"default",
]
source_dest_check = true
subnet_id = "subnet-9621c1de"
tags = {
"Name" = "tflint-test"
}
tenancy = "default"
vpc_security_group_ids = [
"sg-485f1735",
]
credit_specification {
cpu_credits = "unlimited"
}
enclave_options {
enabled = false
}
metadata_options {
http_endpoint = "enabled"
http_put_response_hop_limit = 1
http_tokens = "optional"
}
root_block_device {
delete_on_termination = true
device_name = "/dev/xvda"
encrypted = false
iops = 100
tags = {}
throughput = 0
volume_id = "vol-012c9259f69420c1c"
volume_size = 8
volume_type = "gp2"
}
}
Plan: 0 to add, 1 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_instance.test: Modifying... [id=i-085049c8fe2c58383]
aws_instance.test: Still modifying... [id=i-085049c8fe2c58383, 10s elapsed]
aws_instance.test: Still modifying... [id=i-085049c8fe2c58383, 20s elapsed]
aws_instance.test: Still modifying... [id=i-085049c8fe2c58383, 30s elapsed]
aws_instance.test: Still modifying... [id=i-085049c8fe2c58383, 40s elapsed]
aws_instance.test: Still modifying... [id=i-085049c8fe2c58383, 50s elapsed]
aws_instance.test: Still modifying... [id=i-085049c8fe2c58383, 1m0s elapsed]
Error: Client.InvalidParameterValue: Invalid value 't4.micro' for InstanceType.
status code: 400, request id: 326502a4-5564-449f-a37d-73f651ffb151
on main.tf line 13, in resource "aws_instance" "test":
13: resource "aws_instance" "test" {
エラーが出てしまいました。
AWS上にt4.microというインスタンスタイプが存在しないのでエラーが出るのが正しい挙動ですが
AWSでインスタンスタイプを変更する際は一度該当のインスタンスを停止させる必要があるので
Terraformで実行する際も同様に一時停止 -> タイプ変更 -> 起動の順序になります。
この場合、タイプ変更時にエラーとなるためインスタンスが停止したままTerraformの実行が終了してしまいます。
これはよろしくない、、、出来ればplanの段階でエラーを出したいですよね。
ここでtflintを実行してみます。
% tflint 1 issue(s) found: Error: instance_type is not a valid value (aws_instance_invalid_type) on main.tf line 15: 15: instance_type = "t4.micro"
エラーを出してくれました。
planはあくまでTerraform上の実行計画なので
tfファイルの構文的に問題がある場合はエラーを出してくれますが
反映先のクラウドプラットフォームの仕様に依存したエラーは検知できません。
その場合はtflintを先に実行することで反映前の差分確認やテストの精度が飛躍的に上がります。
まとめ
上記のように反映先のクラウドプラットフォームの仕様に依存したエラーを検知できずにapplyした場合
Terraformは実行途中でエラーになってもロールバックしないので稼働中のサービスに影響が出てしまったり
どこでエラーが発生しているのかを調査する必要が出てきます。
そのため、可能な限りplanや今回ご紹介したtflintを用いて反映前の確認の精度を上げていく必要があります。
tflintは非常に便利なツールなので皆様是非利用してみてください。
2