Static analysis of Terraform using tflint
My name is Teraoka and I am an infrastructure engineer.
This time, I would like to perform static analysis of Terraform code using tflint.
What is tflint?
This is a linter tool that specializes in tf files published by OSS.
Here the repository .
By using tflint, you can issue warnings if you are writing deprecated syntax or unused declarations, and
detect errors that may occur on cloud platforms such as AWS.
install
For Mac, you can install using brew.
$ brew install tflint
For Linux, an installation script is provided, so you can use that.
$ curl https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
How to use
Basically run the command in the directory where the tf file is located.
Just execute the following to read the files in the current directory, analyze them, 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 am 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) + 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 reflect it with 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.
There is no particular problem with the code, so it can be reflected without any problems.
Next, let's rewrite some of the code.
Check the difference with plan again.
You 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 this to reflect it.
% 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 has occurred.
Since there is no instance type called t4.micro on AWS, the correct behavior is to get an error, but
when changing the instance type on AWS, you need to stop the instance in question, so do
the same when running on Terraform. The order is Pause -> Change Type -> Start.
In this case, an error occurs when changing the type, and Terraform execution ends while the instance is stopped.
This is not a good idea... If possible, I would like to issue 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.
Plan is only an execution plan on Terraform, so
if there is a problem with the syntax of the tf file, it will issue an error, but
it will not be able to 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 and testing the differences before updating.
summary
As mentioned above, if you apply without being able to detect an error that depends on the specifications of the target cloud platform,
Terraform will not rollback even if an error occurs during execution, so it may affect the running service
or cause an error. You will need to investigate whether this is occurring.
Therefore, it is necessary to improve the accuracy of confirmation before reflection by using plan or tflint, which we introduced this time, as much as possible.
tflint is a very useful tool, so please give it a try.