Static analysis of Terraform using tflint

table of contents
My name is Teraoka, and I am an infrastructure engineer.
Today I would like to use tflint to perform static analysis of Terraform code.
What is tflint?
tflint is an open-source linter tool specialized for tf files.
The repository here .
By using tflint, you can get warnings for deprecated syntax and unused declarations, and
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, the command is executed in the directory where the tf file is located.
Just execute the following and it will read and analyze the file in the current directory and
display an error if there is a problem.
$ 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.
This doesn't have any particular problem with the code, so it can be reflected without any issues.
Next, let's rewrite some of the code.
Let’s check the difference again with plan.
You can see that we are 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.
Since the t4.micro instance type does not exist on AWS, it is correct to get an error. However,
when changing the instance type on AWS, the instance must be stopped first, so
when running Terraform, the order is similar: Pause -> Change type -> Start.
In this case, an error occurs when changing the type, so the instance remains stopped and Terraform execution ends.
This is not good... Ideally, we would like to get an error at the plan stage.
Now let's run 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"
It gave me an error.
Since plan is merely an execution plan on Terraform,
it will give me an error if there is a syntax problem with the tf file, but
it cannot detect errors that depend on the specifications of the cloud platform to which the changes are being reflected.
In that case, running tflint first will dramatically improve the accuracy of the difference check and testing before reflecting the changes.
summary
As mentioned above, if an error dependent on the specifications of the target cloud platform cannot be detected and applied,
Terraform will not roll back even if an error occurs during execution, which could affect running services and
require investigation into where the error occurred.
For this reason, it is necessary to improve the accuracy of pre-application checks as much as possible using plan or tflint, as introduced in this article.
tflint is a very useful tool, so we encourage everyone to try it out.
2