【新卒 / 中途採用】サーバー / クラウドエンジニア 募集中!【大阪】

【新卒 / 中途採用】サーバー / クラウドエンジニア 募集中!【大阪】

【導入実績300社以上】AWS 構築・運用保守サービス

【導入実績300社以上】AWS 構築・運用保守サービス

【ECサイト構築】Shopify カスタムアプリ開発サービス

【ECサイト構築】Shopify カスタムアプリ開発サービス

【スマホ決済】PayPay ミニアプリ開発サービス

【スマホ決済】PayPay ミニアプリ開発サービス

【メッセージアプリ】LINE アプリ開発サービス

【メッセージアプリ】LINE アプリ開発サービス

【100URLの登録が0円】Webサイト監視サービス「Appmill」

【100URLの登録が0円】Webサイト監視サービス「Appmill」

【対談記事】彼らはどのようにサービスを移管しているのか?インフラ担当会社ビヨンドと共に内情を明かす

【対談記事】彼らはどのようにサービスを移管しているのか?インフラ担当会社ビヨンドと共に内情を明かす

ビヨンド公式YouTubeチャンネル「びよまるチャンネル」

ビヨンド公式YouTubeチャンネル「びよまるチャンネル」

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


この記事をかいた人

About the author

寺岡佑樹

2016年新卒入社、現在5年目。
SREとして、社内の運用業務の仕組みの検討・実装をしつつ
社外で技術的な登壇を行うエバンジェリスト的な側面も持つ。

・GitHub
https://github.com/nezumisannn

・登壇経歴
https://github.com/nezumisannn/my-profile

・発表資料(SpeakerDeck)
https://speakerdeck.com/nezumisannn