Terraform static analysis using tflint

I'm Teraoka, an infrastructure engineer.
This time, I'd like to try static analysis of Terraform code using tflint.

What is tflint?

This is an open-source linter tool specifically for tf files.
The repositoryhere.
Using tflint, you can get warnings about deprecated syntax and unused declarations, and it
can also detect errors that may occur on cloud platforms such as AWS.

install

On Mac, you can install it using brew

$ brew install tflint

For Linux, an installation script is available, so you can use that

$ curl https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash

How to use

Basically, you execute the command in the directory where the tf file is located.
Simply executing the following will read and analyze the files in the current directory and
display errors if there are any problems.

$ tflint

Let's take a closer look.
Write the following code as main.tf in the current directory.

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" } }

When I check the difference with terraform plan, I see that it is trying to create one EC2 instance

$ 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 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.

Let's actually apply it using 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 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.

There's nothing particularly wrong with the code, so it should be reflected without any issues.
Next, let's rewrite some of the code.

Let's check the differences again with plan.
It looks like you're trying to change t3.micro to 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.

Let's apply it as is

% 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" {

An error occurred.
The correct behavior is to get an error because the instance type t4.micro does not exist on AWS. However,
when changing the instance type in AWS, you need to stop the instance first, so the same
sequence applies when executing with Terraform: pause -> change type -> start.
In this case, an error occurs when changing the type, so the Terraform execution finishes with the instance still stopped.
This is not good... Ideally, we want the error to occur at the plan stage.
Let's try running tflint here.

% 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"

It did give me an error.
Since `plan` is merely an execution plan on Terraform,
it will give an error if there is a syntax problem in the tf file, but
it cannot detect errors that depend on the specifications of the target cloud platform.
In that case, running `tflint` first will dramatically improve the accuracy of checking differences and testing before deployment.

summary

As mentioned above, if you apply a change without detecting an error that depends on the specifications of the target cloud platform,
Terraform will not roll back even if an error occurs during execution. This can affect running services and necessitate
investigating where the error occurred.
Therefore, it is necessary to improve the accuracy of pre-implementation checks using tools such as plan and tflint, which we introduced here, as much as possible.
tflint is a very useful tool, so please try using it.

If you found this article helpful,please give it a "Like"!
3
Loading...
3 votes, average: 1.00 / 13
5,846
X Facebook Hatena Bookmark pocket

The person who wrote this article

About the author

Yuki Teraoka

Joined Beyond in 2016, I am currently
in my sixth year as an infrastructure engineer and MSP. I handle troubleshooting during incidents and
also design and build infrastructure using public clouds such as AWS. Recently, I have been working
container infrastructure such as Docker and Kubernetes, and
with HashiCorp tools such as Terraform and Packer as part of building and automating
I also take on the role of an evangelist, speaking at external study groups and seminars.

・GitHub
https://github.com/nezumisannn

• Speaking Engagements
: https://github.com/nezumisannn/my-profile

• Presentation materials (SpeakerDeck)
https://speakerdeck.com/nezumisannn

・Certification:
AWS Certified Solutions Architect - Associate
Google Cloud Professional Cloud Architect