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は非常に便利なツールなので皆様是非利用してみてください。