Terraformのテンプレート開発環境を構築する

Terraform(以下TF)のツールとして以下のツールをインストールした。

  • tfenv: Terraformバージョン管理ツール

  • tflint: 静的解析ツール

  • terraform-docs: モジュールドキュメント生成ツール

  • tfsec: 静的セキュリティ解析ツール

  • checkov: 静的コード分析ツール

他のツールは shuaibiyy/awesome-terraform を参照

使用したDockerfileは以下の通り

FROM debian:bullseye-20230411-slim
ARG DEFAULT_TERRAFROM_VERSION=1.4.4
ARG TFENV_VERSION=v3.0.0
ARG TFLINT_VERSION=v0.45.0
ARG TFDOCS_VERSION=v0.16.0
ARG TFSEC_VERSION=v1.28.1
ARG CHECKOV_VERSION=2.3.160

RUN apt-get update -y && \
    # 日本語化
    apt-get install -y locales && \
    sed -i -E 's/# (ja_JP.UTF-8)/\1/' /etc/locale.gen && \
    locale-gen && \
    apt-get install -y curl git unzip
    # tfenvインストール
ENV TFENV_HOME=/usr/local/tfenv
ENV TFENV_TAR_URL=https://github.com/tfutils/tfenv/archive/refs/tags/${TFENV_VERSION}.tar.gz
ENV PATH=$PATH:${TFENV_HOME}/bin
RUN mkdir -p "${TFENV_HOME}" && \
    curl -fSL "${TFENV_TAR_URL}" | tar zxvf - --strip-components=1 -C "${TFENV_HOME}"
    # デフォルトのterraformインストール
RUN tfenv install "${DEFAULT_TERRAFROM_VERSION}"
    # tflintインストール
ENV TFLINT_VERSION="${TFLINT_VERSION}"
RUN curl -fSL https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
    # tfdocインストール
ENV TFDOCS_HOME=/usr/local/tfdocs
ENV PATH=$PATH:${TFDOCS_HOME}/bin
ENV TFDOCS_TAR_URL=https://github.com/terraform-docs/terraform-docs/releases/download/${TFDOCS_VERSION}/terraform-docs-${TFDOCS_VERSION}-Linux-amd64.tar.gz
RUN mkdir -p "${TFDOCS_HOME}/bin" && \
    curl -fSL "${TFDOCS_TAR_URL}" | tar zxvf - -C "${TFDOCS_HOME}" && \
    chmod +x "${TFDOCS_HOME}/terraform-docs" && \
    mv "${TFDOCS_HOME}/terraform-docs" "${TFDOCS_HOME}/bin/"
    # tfsecインストール
ENV TFSEC_VERSION="${TFSEC_VERSION}"
RUN curl -fSL https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash
    # checkovインストール
RUN apt-get install -y python3 && \
    apt-get install -y python3-pip && \
    python3 -m pip install -U checkov==2.2.5

公式のVSCode用の拡張 Marketplace/HashiCorp Terraform も用意されている。評判は悪そう。。。

  • UI: HashiCorp.terraform

tfenv

TFのバージョン管理ツール

tfenv install バージョン番号 で指定バージョンのTFをインストールできる他、.terraform-versionファイルを作成し tfenv install を実行することで記述されたバージョンのTFがインストールされ使用できるようになる。

.terraform-version
1.4.4
実行
tfenv install
terraform --version で確認
Terraform v1.4.4
on linux_amd64

Your version of Terraform is out of date! The latest version
is 1.4.5. You can update by downloading from https://www.terraform.io/downloads.html

Terraform

terraformを実行してみる

【AWS編】Terraform公式チュートリアル【翻訳】 を参考にmain.tfを作成し実行してみる。

main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.16"
    }
  }

  required_version = ">= 1.2.0"
}

provider "aws" {
  region = "us-west-2"
}

resource "aws_instance" "app_server" {
  ami           = "ami-830c94e3"
  instance_type = "t2.micro"

  tags = {
    Name = "ExampleAppServerInstance"
  }
}

準備

terraform init を実行し初期化してプロバイダーをインストールする。

terraform init の実行
Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 4.16"...
- Installing hashicorp/aws v4.62.0...
- Installed hashicorp/aws v4.62.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

実行すると以下のファイルとディレクトリが作成された

  • .terraform.lock.hcl

  • .terraform

    • .terraform/providers/registry.terraform.io/hashicorp/aws/4.62.0/linux_amd64/terraform-provider-aws_v4.62.0_x5

確認

terraform validate で有効なテンプレートであるか確認

terraform validate を実行
Success! The configuration is valid.

計画

terraform plan を実行する

terraform plan を実行
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_instance.app_server will be created
  + resource "aws_instance" "app_server" {
      + ami                                  = "ami-830c94e3"
      + 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)
      + disable_api_stop                     = (known after apply)
      + disable_api_termination              = (known after apply)
      + ebs_optimized                        = (known after apply)
      + get_password_data                    = false
      + host_id                              = (known after apply)
      + host_resource_group_arn              = (known after apply)
      + iam_instance_profile                 = (known after apply)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (known after apply)
      + instance_state                       = (known after apply)
      + instance_type                        = "t1.micro"
      + ipv6_address_count                   = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + key_name                             = (known after apply)
      + monitoring                           = (known after apply)
      + outpost_arn                          = (known after apply)
      + password_data                        = (known after apply)
      + placement_group                      = (known after apply)
      + placement_partition_number           = (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" = "ExampleAppServerInstance"
        }
      + tags_all                             = {
          + "Name" = "ExampleAppServerInstance"
        }
      + tenancy                              = (known after apply)
      + user_data                            = (known after apply)
      + user_data_base64                     = (known after apply)
      + user_data_replace_on_change          = false
      + vpc_security_group_ids               = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

適用

terraform apply を実行する

terraform apply -auto-approve を実行
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_instance.app_server will be created
  + resource "aws_instance" "app_server" {
      + ami                                  = "ami-830c94e3"
      + 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)
      + disable_api_stop                     = (known after apply)
      + disable_api_termination              = (known after apply)
      + ebs_optimized                        = (known after apply)
      + get_password_data                    = false
      + host_id                              = (known after apply)
      + host_resource_group_arn              = (known after apply)
      + iam_instance_profile                 = (known after apply)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (known after apply)
      + instance_state                       = (known after apply)
      + instance_type                        = "t1.micro"
      + ipv6_address_count                   = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + key_name                             = (known after apply)
      + monitoring                           = (known after apply)
      + outpost_arn                          = (known after apply)
      + password_data                        = (known after apply)
      + placement_group                      = (known after apply)
      + placement_partition_number           = (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" = "ExampleAppServerInstance"
        }
      + tags_all                             = {
          + "Name" = "ExampleAppServerInstance"
        }
      + tenancy                              = (known after apply)
      + user_data                            = (known after apply)
      + user_data_base64                     = (known after apply)
      + user_data_replace_on_change          = false
      + vpc_security_group_ids               = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.
aws_instance.app_server: Creating...
aws_instance.app_server: Still creating... [10s elapsed]
aws_instance.app_server: Still creating... [20s elapsed]
aws_instance.app_server: Still creating... [30s elapsed]
aws_instance.app_server: Creation complete after 34s [id=i-08f43a9387eabd711]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

AWSコンソールをのぞくとリソースが作成されていた。

-auto-approve オプションは強制的に適用させるためのオプション。
terraform apply だとInteractiveに動くため-auto-approveで強制実行させてみた。 ただし、CIでやるにはよろしくなさそう。

別の方法として`terraform plan` 時に -out オプションを使用するとplan fileが出力される。
それを terraform apply ${PLAN_FILE} と指定して実行できるそうな。
また、planの中身をMDに変換するツールもあるらしい。

削除

terraform destroy を実行するとInteractiveに動く
terraform destroyterraform apply -destroy のaliasらしい。

terraform apply -auto-approve -destroy を実行
aws_instance.app_server: Refreshing state... [id=i-08f43a9387eabd711]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_instance.app_server will be destroyed
  - resource "aws_instance" "app_server" {
      - ami                                  = "ami-830c94e3" -> null
      - arn                                  = "arn:aws:ec2:us-west-2:123456789101:instance/i-08f43a9387eabd711" -> null
      - associate_public_ip_address          = true -> null
      - availability_zone                    = "us-west-2c" -> null
      - cpu_core_count                       = 1 -> null
      - cpu_threads_per_core                 = 1 -> null
      - disable_api_stop                     = false -> null
      - disable_api_termination              = false -> null
      - ebs_optimized                        = false -> null
      - get_password_data                    = false -> null
      - hibernation                          = false -> null
      - id                                   = "i-08f43a9387eabd711" -> null
      - instance_initiated_shutdown_behavior = "stop" -> null
      - instance_state                       = "running" -> null
      - instance_type                        = "t1.micro" -> null
      - ipv6_address_count                   = 0 -> null
      - ipv6_addresses                       = [] -> null
      - monitoring                           = false -> null
      - placement_partition_number           = 0 -> null
      - primary_network_interface_id         = "eni-09de77ee395cbbd15" -> null
      - private_dns                          = "ip-172-31-11-125.us-west-2.compute.internal" -> null
      - private_ip                           = "172.31.11.125" -> null
      - public_dns                           = "ec2-35-89-169-164.us-west-2.compute.amazonaws.com" -> null
      - public_ip                            = "35.89.169.164" -> null
      - secondary_private_ips                = [] -> null
      - security_groups                      = [
          - "default",
        ] -> null
      - source_dest_check                    = true -> null
      - subnet_id                            = "subnet-092f134f" -> null
      - tags                                 = {
          - "Name" = "ExampleAppServerInstance"
        } -> null
      - tags_all                             = {
          - "Name" = "ExampleAppServerInstance"
        } -> null
      - tenancy                              = "default" -> null
      - user_data_replace_on_change          = false -> null
      - vpc_security_group_ids               = [
          - "sg-2da27048",
        ] -> null

      - capacity_reservation_specification {
          - capacity_reservation_preference = "open" -> null
        }

      - enclave_options {
          - enabled = false -> null
        }

      - maintenance_options {
          - auto_recovery = "default" -> null
        }

      - metadata_options {
          - http_endpoint               = "enabled" -> null
          - http_put_response_hop_limit = 1 -> null
          - http_tokens                 = "optional" -> null
          - instance_metadata_tags      = "disabled" -> null
        }

      - private_dns_name_options {
          - enable_resource_name_dns_a_record    = false -> null
          - enable_resource_name_dns_aaaa_record = false -> null
          - hostname_type                        = "ip-name" -> null
        }

      - root_block_device {
          - delete_on_termination = true -> null
          - device_name           = "/dev/sda1" -> null
          - encrypted             = false -> null
          - iops                  = 0 -> null
          - tags                  = {} -> null
          - throughput            = 0 -> null
          - volume_id             = "vol-0d0c8c535b33f7906" -> null
          - volume_size           = 8 -> null
          - volume_type           = "standard" -> null
        }
    }

Plan: 0 to add, 0 to change, 1 to destroy.
aws_instance.app_server: Destroying... [id=i-08f43a9387eabd711]
aws_instance.app_server: Still destroying... [id=i-08f43a9387eabd711, 10s elapsed]
aws_instance.app_server: Still destroying... [id=i-08f43a9387eabd711, 20s elapsed]
aws_instance.app_server: Still destroying... [id=i-08f43a9387eabd711, 30s elapsed]
aws_instance.app_server: Destruction complete after 31s

Apply complete! Resources: 0 added, 0 changed, 1 destroyed.

試しに terraform plan -destroy を実行してみた。

terraform plan -destroy を実行
aws_instance.app_server: Refreshing state... [id=i-08f43a9387eabd711]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_instance.app_server will be destroyed
  - resource "aws_instance" "app_server" {
      - ami                                  = "ami-830c94e3" -> null
      - arn                                  = "arn:aws:ec2:us-west-2:123456789101:instance/i-08f43a9387eabd711" -> null
      - associate_public_ip_address          = true -> null
      - availability_zone                    = "us-west-2c" -> null
      - cpu_core_count                       = 1 -> null
      - cpu_threads_per_core                 = 1 -> null
      - disable_api_stop                     = false -> null
      - disable_api_termination              = false -> null
      - ebs_optimized                        = false -> null
      - get_password_data                    = false -> null
      - hibernation                          = false -> null
      - id                                   = "i-08f43a9387eabd711" -> null
      - instance_initiated_shutdown_behavior = "stop" -> null
      - instance_state                       = "running" -> null
      - instance_type                        = "t1.micro" -> null
      - ipv6_address_count                   = 0 -> null
      - ipv6_addresses                       = [] -> null
      - monitoring                           = false -> null
      - placement_partition_number           = 0 -> null
      - primary_network_interface_id         = "eni-09de77ee395cbbd15" -> null
      - private_dns                          = "ip-172-31-11-125.us-west-2.compute.internal" -> null
      - private_ip                           = "172.31.11.125" -> null
      - public_dns                           = "ec2-35-89-169-164.us-west-2.compute.amazonaws.com" -> null
      - public_ip                            = "35.89.169.164" -> null
      - secondary_private_ips                = [] -> null
      - security_groups                      = [
          - "default",
        ] -> null
      - source_dest_check                    = true -> null
      - subnet_id                            = "subnet-092f134f" -> null
      - tags                                 = {
          - "Name" = "ExampleAppServerInstance"
        } -> null
      - tags_all                             = {
          - "Name" = "ExampleAppServerInstance"
        } -> null
      - tenancy                              = "default" -> null
      - user_data_replace_on_change          = false -> null
      - vpc_security_group_ids               = [
          - "sg-2da27048",
        ] -> null

      - capacity_reservation_specification {
          - capacity_reservation_preference = "open" -> null
        }

      - enclave_options {
          - enabled = false -> null
        }

      - maintenance_options {
          - auto_recovery = "default" -> null
        }

      - metadata_options {
          - http_endpoint               = "enabled" -> null
          - http_put_response_hop_limit = 1 -> null
          - http_tokens                 = "optional" -> null
          - instance_metadata_tags      = "disabled" -> null
        }

      - private_dns_name_options {
          - enable_resource_name_dns_a_record    = false -> null
          - enable_resource_name_dns_aaaa_record = false -> null
          - hostname_type                        = "ip-name" -> null
        }

      - root_block_device {
          - delete_on_termination = true -> null
          - device_name           = "/dev/sda1" -> null
          - encrypted             = false -> null
          - iops                  = 0 -> null
          - tags                  = {} -> null
          - throughput            = 0 -> null
          - volume_id             = "vol-0d0c8c535b33f7906" -> null
          - volume_size           = 8 -> null
          - volume_type           = "standard" -> null
        }
    }

Plan: 0 to add, 0 to change, 1 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

tflint

静的解析ツール

.tflint.hcl を準備し tflint --init で初期化することで対象ルールセットをインストールして使用する。
以前はtflint腹持ちのTFと使用するTFのバージョンの同期が必要だったようだが、今はv1.x系ならほぼ問題ないらしい。(Compatibility with Terraform)
.tflint.hcl の中身は terraform-linters/tflint-ruleset-aws のものを使用

.tflint.hcl
plugin "aws" {
    enabled = true
    version = "0.22.1"
    source  = "github.com/terraform-linters/tflint-ruleset-aws"
}
初期化
tflint --init
結果
root@1fbd264426b9:/workspace# tflint --version
TFLint version 0.45.0
+ ruleset.aws (0.22.1)
+ ruleset.terraform (0.2.2-bundled)

実行

そのままmain.tfを使用する。

tflint を実行(何も表示されない)

試しに存在しないインスタンスタイプに変更してやってみる

main
  instance_type = "x.micro"
tflint を実行
1 issue(s) found:

Error: "x.micro" is an invalid value as instance_type (aws_instance_invalid_type)

  on main.tf line 18:
  18:   instance_type = "x.micro"

tfsec

tfsecを実行する(main.tfは元に戻した)

tfsec を実行
Result #1 HIGH Instance does not require IMDS access to require a token
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  main.tf:16-23
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   16    resource "aws_instance" "app_server" {
   17      ami           = "ami-830c94e3"
   18      instance_type = "t2.micro"
   19
   20      tags = {
   21        Name = "ExampleAppServerInstance"
   22      }
   23    }
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
          ID aws-ec2-enforce-http-token-imds
      Impact Instance metadata service can be interacted with freely
  Resolution Enable HTTP token requirement for IMDS

  More Information
  - https://aquasecurity.github.io/tfsec/v1.28.1/checks/aws/ec2/enforce-http-token-imds/
  - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance#metadata-options
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────


Result #2 HIGH Root block device is not encrypted.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  main.tf:16-23
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   16    resource "aws_instance" "app_server" {
   17      ami           = "ami-830c94e3"
   18      instance_type = "t2.micro"
   19
   20      tags = {
   21        Name = "ExampleAppServerInstance"
   22      }
   23    }
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
          ID aws-ec2-enable-at-rest-encryption
      Impact The block device could be compromised and read from
  Resolution Turn on encryption for all block devices

  More Information
  - https://aquasecurity.github.io/tfsec/v1.28.1/checks/aws/ec2/enable-at-rest-encryption/
  - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance#ebs-ephemeral-and-root-block-devices
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────


  timings
  ──────────────────────────────────────────
  disk i/o             2.880857ms
  parsing              284.605µs
  adaptation           95.502µs
  checks               16.294024ms
  total                19.554988ms

  counts
  ──────────────────────────────────────────
  modules downloaded   0
  modules processed    1
  blocks processed     3
  files read           1

  results
  ──────────────────────────────────────────
  passed               3
  ignored              0
  critical             0
  high                 2
  medium               0
  low                  0

  3 passed, 2 potential problem(s) detected.

テンプレートを以下の用に直し再度実行

resource "aws_instance" "app_server" {
  ami           = "ami-830c94e3"
  instance_type = "t2.micro"

  metadata_options {
  http_tokens = "required"
  }

  root_block_device {
      encrypted = true
  }

  tags = {
    Name = "ExampleAppServerInstance"
  }
}
tfsec を実行
  timings
  ──────────────────────────────────────────
  disk i/o             3.699285ms
  parsing              278.107µs
  adaptation           109.703µs
  checks               7.237266ms
  total                11.324361ms

  counts
  ──────────────────────────────────────────
  modules downloaded   0
  modules processed    1
  blocks processed     3
  files read           1

  results
  ──────────────────────────────────────────
  passed               5
  ignored              0
  critical             0
  high                 0
  medium               0
  low                  0


No problems detected!