top of page

Posts

Terraform static analysis using tflint

I would like to try using tflint for static analysis of Terraform code in this article.


What is tflint?

tflint is an open-source linter tool specifically for Terraform files. You can find the repository here. By using tflint, you can get warnings for deprecated syntax or unused declarations and detect potential errors on cloud platforms like AWS.


Installation

On Mac, you can install it using brew.

$ brew install tflint

On Linux, there is an installation script available.



Usage

Basically, you run the command in the directory where the tf files are located. Simply executing the following will read and analyze the files in the current directory, and if there are any issues, it will report errors.

$ tflint

Let's take a closer look. Create the following code in the current directory as 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"
  }
}

When you check the difference with terraform plan, 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)
      + 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 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.

Since there is no problem with the code, it can be applied without any issues. Next, let's rewrite some of the code.

When we check the difference again with the terraform plan, 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 it 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 t4.micro does not exist as an instance type on AWS, it is correct to get an error. When changing the instance type on AWS, you need to stop the relevant instance first, so in Terraform, it will be the same sequence: stop -> change type -> start. In this case, an error occurs during the type change, so Terraform execution ends with the instance stopped. This is not ideal... we would prefer to get an error at the plan stage. Let's run 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 gave an error. Since plan is just an execution plan on Terraform, it will give an error if there is a problem with the syntax of the tf file, but it cannot detect errors dependent on the specifications of the target cloud platform. In that case, by running tflint first, you can significantly improve the accuracy of pre-implementation difference checking and testing.


Summary

As mentioned above, if you apply without detecting errors dependent on the specifications of the target cloud platform, Terraform does not roll back even if an error occurs in the middle of execution, so it may affect running services, and you may need to investigate where the error occurred. Therefore, it is necessary to improve the accuracy of pre-implementation checks as much as possible using plan and the tflint tool introduced this time. tflint is a very useful tool. 

I hope this article will help you!



This blog post is translated from a blog post written by Yuki Teraoka on our Japanese website Beyond Co..


Comments


bottom of page