ForgeVision Engineer Blog

フォージビジョン エンジニア ブログ

ゼロから始める Terraform 講座~その1~

こんにちは、こんばんわ!クラウドインテグレーション事業部の魚介系エンジニア松尾です。

このタイミングでのブログの投稿は AWS Summit Tokyo 2023 についての内容かと思われるかもしれませんが、全く関係ありません!(笑)

今回の内容は Terraform 知識ゼロの私が仕事でどうしても Terraform を利用する必要があり、設計・コーディングを進めていく中で、この数週間で悩み学んできたことを紹介していきたいと思います。
今回はその第1弾ということで、Terraform の始め方から最初に知っておきたい Tips などをご紹介できればと思います。

注:本記事は AWS 利用者向けの記事となり、AWS 環境をベースにコードの記載をさせていただいております。

Terraform の開始

Terraform のインストール

まず初めに、作業端末で Terraform をインストールしましょう。 ソフトウェアは以下 HashiCorp 社サイトから入手可能です。 developer.hashicorp.com

筆者は Windows を利用していたため、Binary をダウンロード後、解凍、Path 設定を行いました。
作業端末の OS の種類に応じてご対処下さい。

なお、インストールするバージョンに関して、これから Terraform を利用する方、新規で環境を作る方は、基本的に latest の利用で問題無いです。
古いバーションを利用すると、クラウドサービス側の細かいオプション設定などに対応していない場合がありますのでご注意下さい。
※ドキュメントで「未対応」となっているオプションも新しいバージョンで解消されていることがあり、ドキュメント側の更新が遅れている場合があります。

インストールが完了したら、コマンドが実行できるか確認しましょう。

$ terraform -version

AWS 環境の構築準備

環境を構築するに当たり、初めにクラウドサービスに応じた資格情報を登録する必要があります。
クラウドサービスを端末から API で操作する際にはアクセスキーが必要となりますが、Terraform も同様に登録した認証情報を利用します。
細かい説明は割愛しますが、AWS の場合は権限を持つユーザのアクセスキーをコンソール画面で発行後、認証情報を端末で aws configure コマンドで登録したり環境変数に登録することで、実際に API が実行可能となります。
また、EC2 や Cloud9 を利用する場合は IAM ロールの資格情報を利用可能ですので、アクセスキーの発行が不要となり、より安全に Terraform を実行することが可能です。
aws configure でアクセスキーを登録した場合、IAM ロールをアタッチしていても default profile の資格情報が利用されるためご注意ください。

なお、Terraform のコードの中でも以下のような形で資格情報を定義することは可能です。

  • provider.tf
provider "aws" {
  region     = "ap-northeast-1"
  access_key = "XXXXXXXXXXXXXXXXXXXXXX"
  secret_key = "XXXXXXXXXXXXXXXXXXXXXX"
}

ただし、資格情報をハードコードし Github などで誤って公開すると大事故につながりかねませんので、前述の方法で資格情報を登録/利用することをお勧めします。
※変数定義ファイル「variables.tf」、「.tfvars」ファイルを組み合わせることで安全性を高めることは可能ですが、最も安全なのは書かないことです。

資格情報を登録したら、作業ディレクトリを作成しましょう。
Terraform は固有の作業ディレクトリで実施する必要があります。
ディレクトリ名は何でも構いません。
作業ディレクトリを作成したら、作成したディレクトリに移動し、以降の対応を進めます。

Terraform 実行ファイルの作成

環境の準備が整ったら、Terraform の実行ファイルを作成します。
ファイル名は「xxx.tf」の形式で作成する必要があります。
「xxx」部分は何でも構いませんが、初めは一般的な名前として「main.tf」と定義するとよいでしょう。

ファイルを作成したらコードを記述していきます。
以下は公式ドキュメントを参考に記載している内容になります。
各コードブロックの意味に関しましてはコメントをご確認下さい。

  • main.tf
# Terrfarm の設定ブロックになります
terraform {
  # 以下は構築するプロバイダーを指定するブロックです
  required_providers {
    aws = {
      # 以下でAWS プロバイダーのソースレジストリを指定しています
      source  = "hashicorp/aws"
      # 以下はプロバイダーのバージョンを指定しています  ※指定しない場合、Terraform は初期化中に最新バージョンを自動的にダウンロードします
      # バージョンによって作成できないリソースなどあるのでご注意下さい
      version = "~> 4.35.0"
    }
  }
  # Terraform 自体の実行バージョンを指定しています
  required_version = ">= 1.2.4"
}

# 以下はプロバイダーの設定ブロックになります  ※上記「required_providers」に含まれている名前である必要があります
provider "aws" {
  # プロバイダーで利用するリージョンを指定しています
  region  = "ap-northeast-1"
}

# 以下は実際にリソースを作成するブロックになります
# 今回の内容は EC2 を作成する例で、「aws_instance」がリソースタイプ(固定名)、「nyan_server」が Terraform 内で参照される名前(任意の名前)になります  ※名前は同じ階層の tf ファイルで一意である必要があります
resource "aws_instance" "nyan_server" {
  ami           = "ami-030cf0a1edb8636ab"
  instance_type = "t3.micro"

  tags = {
    Name = "cocaibin"
  }
}

上記は必要最低減のコードになりますが、Terraform ブロック、Providor ブロックに関して、特別な要件が無い場合は概ね上記で問題無いでしょう。
resource ブロックは EC2 の作成を例としておりますが、オプションがかなり少ないと思われるかもしれません。
上記の場合、細かくオプションを指定しておりませんので、ネットワーク関連はデフォルトの VPC、サブネットなどが適用され、その他もデフォルトの定義が適用されます。

Terraform ディレクトリの初期化

初めて Terraform を実行する場合や Terraform 作業ディレクトリの構成変更を行った際はディレクトリの初期化が必要になります。
以下のコマンドを実行し、前項で定義したプロバイダーのプラグイン(今回の場合は AWS)をダウンロードします。

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 4.35.0"...
- Installing hashicorp/aws v4.35.0...
- Installed hashicorp/aws v4.35.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ディレクトリが作成され、Terraform 実行に必要なプラグインの一式が配置されます。
また、.terraform.lock.hclが作成され、プロバイダーとモジュールの依存関係、互換性を記録します。
terraform initを行うごとに記録され、利用者側で編集する必要はありません。

ちなみにモジュールについては、「1つのディレクトリにある一連の Terraform 構成ファイル(.tf ファイル)」を指します。
今回は単純な1階層のディレクトリ構成となるため、ルートモジュールとして実行されます。
このモジュールをどう活用していくかが Terraform を利用する上で重要な要素となり、筆者も Terraform を触り始めたころ頭を悩ませた部分であります。
階層を分けて複数のモジュールにすることで様々なメリットがあるのですが、詳細は次回以降のブログにてご紹介したいと思います。

Terraform の実行

Terraform のリハーサル

ここまでで Terraform の実行準備が整いました。
このまま Apply(リソースの展開) を行うこともできるのですが、以下のコマンドで実行前のリハーサルを行います。

$ 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.nyan_server will be created
  + resource "aws_instance" "nyan_server" {
      + ami                                  = "ami-030cf0a1edb8636ab"
      + 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)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (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)
      + 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" = "cocaibin"
        }
      + tags_all                             = {
          + "Name" = "cocaibin"
        }

      ~ 省略~

      + 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 use the -out option to save this plan, so Terraform can't guarantee to take exactly 
these actions if you run "terraform apply" now.

恐らく最もお世話になるコマンドで、準備したモジュールで作成/更新されるリソースをチェックできるコマンドになります。
作成するリソースと作成するリソースに設定されるパラメータが「+」で表示されます。
今回は EC2 を一台作成するため、「Plan: 1 to add, 0 to change, 0 to destroy.」という結果になります。
なお、構文が間違っていたり、ファイル間の依存関係や必要なパラメータが揃っていない場合などはエラーとなります。
基本的なエラーはterraform planで確認できるのですが、Apply が確実に成功するわけではない点はご注意ください。

Terraform の展開

terraform planが成功したら実際にリソースの作成を行います。
以下のコマンドを実行します。

$ terraform apply

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.nyan_server will be created
  + resource "aws_instance" "nyan_server" {
      + ami                                  = "ami-030cf0a1edb8636ab"
      + 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)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (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)
      + 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" = "cocaibin"
        }
      + tags_all                             = {
          + "Name" = "cocaibin"
        }

      ~ 省略~

      + 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:

terraform applyを行ってもすぐにリソースが作成されるわけでなく、terraform plan実行時と同様の結果が出力され、本当に適用するかの応答が求められます。
出力の通り、yesを入力して Apply 処理を実施します。
yes以外の文字列を入力すると Apply がキャンセルされます。

$ terraform apply

      ~ 省略~

  Enter a value: yes

aws_instance.nyan_server: Creating...
aws_instance.nyan_server: Still creating... [10s elapsed]
aws_instance.nyan_server: Creation complete after 12s [id=i-05ece900410607b3f]

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

Apply complete!」が表示されれば Apply は成功です。
筆者は Apply で失敗することも多いので、新たに追加する AWS サービスなどは、とりあえず Apply してリソースが作成されることの確認をお勧めします。

Terraform で作成されたリソースの確認

Terraform の Apply が成功すると、作業ディレクトリにterraform.tfstateファイルが作成されます。
terraform.tfstateファイルは Terraform が管理しているリソースの一式を記述したファイルです。
今回 Apply したリソースの場合は以下の内容となります。

{
  "version": 4,
  "terraform_version": "1.3.8",
  "serial": 1,
  "lineage": "XXXX",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "aws_instance",
      "name": "nyan_server",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 1,
          "attributes": {
            "ami": "ami-030cf0a1edb8636ab",
            "arn": "arn:aws:ec2:ap-northeast-1:123456789123:instance/i-123456789abcdefg",
            "associate_public_ip_address": true,
            "availability_zone": "ap-northeast-1c",

      ~ 省略~

            "subnet_id": "subnet-123456789abcdef",
            "tags": {
              "Name": "cocaibin"
            },
            "tags_all": {
              "Name": "cocaibin"
            },
            "tenancy": "default",
            "timeouts": null,
            "user_data": null,
            "user_data_base64": null,
            "user_data_replace_on_change": false,
            "volume_tags": null,
            "vpc_security_group_ids": [
              "sg-123456789abcdefg"
            ]
          },
          "sensitive_attributes": [],
          "private": "XXXXXXXX"
        }
      ]
    }
  ],
  "check_results": null
}

Apply 前には表示されていなかった「(known after apply)」部分の全てのパラメータが表示されています。
多くの機密情報が含まれるファイルになるので、取り扱いには十分ご注意ください。

また前半の項でも説明しましたが、本来 EC2 を作成する際に必要な各種パラメータやネットワーク周りの設定は今回はほぼ指定しておりませんので、デフォルトのパラーメータや Apply 先の AWS アカウント作成時のデフォルト VPC 設定が適用されています。

なお、Terraform で作成されたリソースを確認したい場合、terraform.tfstateを直接参照せず、以下のコマンドで確認しましょう。

$ terraform show
# aws_instance.nyan_server:
resource "aws_instance" "nyan_server" {
    ami                                  = "ami-030cf0a1edb8636ab"
    arn                                  = "arn:aws:ec2:ap-northeast-1:123456789123:instance/i-123456789abcdefg"
    associate_public_ip_address          = true
    availability_zone                    = "ap-northeast-1c"
    cpu_core_count                       = 1
    cpu_threads_per_core                 = 2
    disable_api_stop                     = false
    disable_api_termination              = false
    ebs_optimized                        = false
    get_password_data                    = false
    hibernation                          = false
    id                                   = "i-123456789abcdefg"
    instance_initiated_shutdown_behavior = "stop"
    instance_state                       = "running"
    instance_type                        = "t3.micro"
    ipv6_address_count                   = 0
    ipv6_addresses                       = []

      ~ 省略~

    root_block_device {
        delete_on_termination = true
        device_name           = "/dev/xvda"
        encrypted             = false
        iops                  = 100
        tags                  = {}
        throughput            = 0
        volume_id             = "vol-123456789abcdefg"
        volume_size           = 8
        volume_type           = "gp2"
    }
}

Terraform で作成したリソースの削除

Terraform で作成したリソースは以下のコマンドで削除することができます。

$ terraform destroy
aws_instance.nyan_server: Refreshing state... [id=i-05ece900410607b3f]

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.nyan_server will be destroyed
  - resource "aws_instance" "nyan_server" {
      - ami                                  = "ami-030cf0a1edb8636ab" -> null
      - arn                                  = "arn:aws:ec2:ap-northeast-1:123456789123:instance/i-123456789abcdefg" -> null
      - associate_public_ip_address          = true -> null
      - availability_zone                    = "ap-northeast-1c" -> null
      - cpu_core_count                       = 1 -> null
      - cpu_threads_per_core                 = 2 -> 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-05ece900410607b3f" -> null
      - instance_initiated_shutdown_behavior = "stop" -> null
      - instance_state                       = "running" -> null
      - instance_type                        = "t3.micro" -> null
      - ipv6_address_count                   = 0 -> null
      - ipv6_addresses                       = [] -> null

      ~ 省略~

      - root_block_device {
          - delete_on_termination = true -> null
          - device_name           = "/dev/xvda" -> null
          - encrypted             = false -> null
          - iops                  = 100 -> null
          - tags                  = {} -> null
          - throughput            = 0 -> null
          - volume_id             = "vol-123456789abcdefg" -> null
          - volume_size           = 8 -> null
          - volume_type           = "gp2" -> null
        }
    }

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

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value:

Apply 時とは逆に削除されるリソースは「-」で表示されます。
EC2 を一台削除するため、「Plan: 0 to add, 0 to change, 1 to destroy.」という plan 結果になります。
また、Apply 時と同様にすぐにリソースが削除されるわけでなく、本当に削除するかの応答が求められます。
出力の通り、yesを入力して Destroy 処理を実施します。
yes以外の文字列を入力すると Apply がキャンセルされます。

$ terraform destroy

      ~ 省略~

  Enter a value: yes

aws_instance.nyan_server: Destroying... [id=i-123456789abcdefg]
aws_instance.nyan_server: Still destroying... [id=i-123456789abcdefg, 10s elapsed]
aws_instance.nyan_server: Still destroying... [id=i-123456789abcdefg, 20s elapsed]
aws_instance.nyan_server: Still destroying... [id=i-123456789abcdefg, 30s elapsed]
aws_instance.nyan_server: Still destroying... [id=i-123456789abcdefg, 40s elapsed]
aws_instance.nyan_server: Destruction complete after 50s

Destroy complete! Resources: 1 destroyed.

Destroy complete!」が表示されれば Destroy は成功です。

今回の記事について

今回は「ゼロから始める Terraform 講座~その1~」ということで、基本的な Terraform の利用開始方法をご紹介させていただきました。

今回ご紹介したのは最小の構成になるので、既に Terraform を利用中の環境などを見ると「ファイルが結構分かれているんだな・・・」と思われる方は多いのではないでしょうか。
実際公式ドキュメント含め、筆者が Terraform に関する多くの記事を見た所感では、ファイルは目的に応じて複数作成することが多いようです。
ここで、覚えておいていただきたいのは、「.tf」の拡張子を持つファイルはファイル名に関わらず全て読み込まれる点です。
なので、1ファイルに全て記述をしてもよいですし、10ファイルに分けても動作に変わりありません。
しかしながら、実際のワークロードを1ファイルのみで管理している場合、リソースが増えるたびにファイルがどんどん肥大化し、可読性が悪くなるため、複数のファイルに分けることをお勧めします。(ファイル名は自分が分かり易いものでOK!)
ただ、初めて Terraform を触る場合、まずは1つのファイルで実行を試してみた方が非常に分かり易いのでお勧めです!

Tips

IaC を行う理由の一つとして、インフラをコードで管理することで、パラメータシートなどのドキュメント管理負荷を軽減する目的があるかと思います。
私が Terraform を利用した所感では、小規模な構成、シンプルな構成であればコードのみ管理すればよいと考えますが、リソースの種類が多かったり、ファイルの肥大化を抑えるためにモジュールを多用したり、ファイルを分割したりしていると、どんどん可読性が悪くなり、どこでどうパラメータが連携されているか追い切るのが困難になり、初見での構成の理解に時間がかかる可能性が高いです。
あくまで個人的な意見ですが、少なくとも構成図やパラメータシートは別途準備した方がよいと考えます。

最後に

皆さん、Terraform を触り始めの頃、機能が多すぎてどういった構成にするか悩まれたことは多かったのではないでしょうか? 次回は個人的にお勧めの.tfファイルの分け方や、最も悩まれる方が多いであろうディレクトリ構成について投稿させていただこうかと思います。

では次回もお楽しみに!!!