【大阪 / 横浜】インフラ / サーバーサイドエンジニア募集中!

【大阪 / 横浜】インフラ / サーバーサイドエンジニア募集中!

【25卒向け】AI×バーチャル面接の募集を開始いたしました!

【25卒向け】AI×バーチャル面接の募集を開始いたしました!

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

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

【CentOS 後継】AlmaLinux OS サーバー構築・移行サービス

【CentOS 後継】AlmaLinux OS サーバー構築・移行サービス

【WordPress 専用】クラウドサーバー『ウェブスピード』

【WordPress 専用】クラウドサーバー『ウェブスピード』

【格安】Webサイト セキュリティ自動診断「クイックスキャナー」

【格安】Webサイト セキュリティ自動診断「クイックスキャナー」

【低コスト】Wasabi オブジェクトストレージ 構築・運用サービス

【低コスト】Wasabi オブジェクトストレージ 構築・運用サービス

【予約システム開発】EDISONE カスタマイズ開発サービス

【予約システム開発】EDISONE カスタマイズ開発サービス

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

この記事がお役に立てば【 いいね 】のご協力をお願いいたします!
1
読み込み中...
1 票, 平均: 1.00 / 11
4,784
X facebook はてなブックマーク pocket
【2024.6.30 CentOS サポート終了】CentOS サーバー移行ソリューション

【2024.6.30 CentOS サポート終了】CentOS サーバー移行ソリューション

【25卒向け】AI×バーチャル面接の募集を開始いたしました!

【25卒向け】AI×バーチャル面接の募集を開始いたしました!

【大阪 / 横浜】インフラエンジニア・サーバーサイドエンジニア 積極採用中!

【大阪 / 横浜】インフラエンジニア・サーバーサイドエンジニア 積極採用中!

この記事をかいた人

About the author

寺岡佑樹

2016年ビヨンド入社、現在6年目のインフラエンジニア
MSPの中の人として障害対応時のトラブルシューティングを行いながら
AWSなどのパブリッククラウドを用いたインフラの設計/構築も行っている。
最近はDockerやKubernetesなどのコンテナ基盤の構築や
運用自動化の一環としてTerraformやPackerなどのHashicorpツールを扱うことが多く
外部の勉強会やセミナーで登壇するEvangelistの役割も担っている。

・GitHub
https://github.com/nezumisannn

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

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

・所有資格
AWS Certified Solutions Architect - Associate
Google Cloud Professional Cloud Architect