前回のIaCの記事でいくつかIaCツールを紹介しました。今回はそのツールの中からTerraformの使い方について紹介します。Terraformはオンプレミスに加えてAWSやAzureなどのクラウドにも対応しており、使用する機会が多いかと思います。そこで今回はコードの書き方やデプロイなどの方法を、実際のAWS環境構築の様子を交えながら紹介します。Terraform を扱ったことがない人でもどういう風に動くのか、どのようにインフラストラクチャーの構築、管理を行うのかが理解できるようになれば幸いです。

Terraformとは

TerraformはHashicorp社が提供するオープンソースのIaCツールです。Terraformを使用することでインフラストラクチャーの構築、変更、管理などの作業をコード化できます。コード化することにより構築作業の自動化やリソース管理の簡略化ができます。Terraformは多くのクラウドサービスをサポートしているため複数のクラウドサービスを使用してマルチクラウド環境の構築もできます。

Terraformの基本的な使い方

次にTerraformの基本的な使い方を紹介します。先ほども述べたようにTerraformはコードを用いてインフラストラクチャーの構築、管理を行います。そのため、コードを記述したファイルが必要になります。以下にTerraformの代表的なファイルの概要を記載します。

ファイル概要

・tfファイル

拡張子が「.tf」のファイル(以下、tfファイル)にリソース定義を記述します。作成するリソースの設定項目とその値をtfファイルに記述しデプロイすることで、対象の環境にリソースが自動作成されます。一つのtfファイル(main.tfと命名することが多い)に全てのリソースの設定を記述することもできますが、サービスごとにtfファイルを作成した方がリソースが増えたときにコードの確認や修正がしやすくなります。

また、拡張子が「.tf」のファイルにリソース定義以外を記述することもあります。

・providers.tf

Terraformのバージョンやプロバイダー(AWSやGCPなどTerraformでリソースを構築する環境)の情報などを記述します。

・variables.tf

AWSであればVPC IDやインスタンスIDなど、環境構築時によく使う値を変数としてこのファイルに定義します。変数を利用することでコードが見やすくなりますし、コードを複製して利用する際も修正する必要がなくなるため大変便利です。

・tfstateファイル

tfファイルのデプロイ時にtfstateというファイルが自動作成されます。このファイルは作成したリソース情報などを自動保存してくれます。チームでTerraformを実行する際はこのtfstateファイルをS3バケットなどに保存するよう指定し、作業者によってリソース情報のズレが発生しないようにする必要があります。

以上がTerraformの主なファイルです。tfファイルの記述が完了したら次に紹介するterraformコマンドを使ってデプロイし、リソースの作成や変更を環境に反映させます。以下に、使用頻度の高いterraformコマンドとそれぞれの用途を紹介します。 

terrafotmコマンド

・terraform init

Terraformの設定を初期化するためのコマンドです。モジュール、プロバイダー、バックエンドなどを初期化し、Terraformを利用できるようにするために使用します。初めてTerraformを実行する際や、プロバイダーやモジュールを追加した場合などにこのコマンドを実行する必要があります。

・terraform plan

作成したtfファイルを適用した際にどのような変更が加わるのかを事前確認するためのコマンドです。 tfファイルをデプロイする前には必ずこのコマンドを実行するように徹底し、コードの記述ミスなどがないか確認することで作業ミスの削減につながります。

・terraform apply

作成した tfファイルをデプロイし、リソース作成や変更などを行うためのコマンドです。コマンドを実行すると作成されるリソースの情報が表示されます。内容に問題が無いことを確認して「yes」を入力するとリソースの作成や変更が行われます。

・terraform destroy

リソースの削除を行うコマンドです。カレントディレクトリ内のtfファイルで管理している全てのリソースを一括で削除できます。コマンド実行する際はディレクトリが間違っていないか、削除してはいけないリソースがないかしっかり確認するようにしましょう。

以上のコマンドが使えれば最低限Terraformを使ったインフラ構築ができます。実際にTerraformでリソース管理を行う際には、tfファイルの記述後、terraform planで変更内容を事前確認し、内容に問題なければterraform applyでデプロイするようにすると作業ミスを減らすことができるかと思います。

TerraformでAWS環境を構築してみよう

Terraformの基礎をある程度紹介したところで実際にTerraformを使ってAWS環境を構築してみましょう。

AWSにはCloud9という簡易的に作業インスタンスを起動できるサービスがあります。今回はこのCloud9のインスタンスからTerraformを利用してVPCを作成し、その中にEC2を作成していきます。

Terraformの初期設定

まず、Terraformの実行環境としてCloud9を作成します。Cloud9の作成方法は割愛しますがインスタンスタイプ:t2.micro、プラットフォーム:Amazon Linux 2023で作成しました。

Cloud9の作成が完了したらIDEを開いて以下コマンドを実行してTerraformの設定を行います。

まず、Cloud9にデフォルトでインストールされているaws-cliとgitのバージョンを下記コマンドで確認します。

$ aws –version
$ git -v

次に、Terraformのバージョン管理ツールであるtfenvを下記コマンドでインストールします。tfenvを使うことでTerraformの複数バージョンの切り替えができるようになります。

$ git clone https://github.com/tfutils/tfenv.git ~/.tfenv

tfenvがインストールできたらシンボリックリンクを作成します。

$ sudo ln -s ~/.tfenv/bin/* /usr/local/bin

インストールしたtfenvのバージョンを確認します。

$ tfenv -v

インストール可能なTerraformバージョンの一覧を出力します。 

$ tfenv list-remote

インストール可能なTerraformバージョンの一覧から最新のバージョンを選択してインストールします。今回はバージョン1.9.0をインストールしました。

$ tfenv install 1.9.0

下記コマンドで指定したバージョンがインストールできたことを確認します。

$ tfenv list

インストールしたTerraformのバージョンを利用するよう指定します。

$ tfenv use 1.9.0

利用しているTerraformのバージョンを確認します。先ほど指定したバージョンが出力されていれば問題ありません。

$ terraform -v

以上の作業でTerraformのインストールが完了し、terraformコマンドが使えるようになりました。

tfファイルの作成

次はtfファイルに作成するAWSリソースの定義を記述していきます。

まず、下記コマンドで作業ディレクトリの作成&移動を行います。

$ mkdir work
$ cd work

作業ディレクトリにviコマンド等でtfファイルを作成します。今回はまずvpc.tfにVPCやサブネットなどの定義を記述し、それらの作成後にec2.tfにEC2の定義を記述してEC2を作成したいと思います。

まず、providers.tfにプロバイダーの設定を記述します。プロバイダーはリソース定義のtfファイルに記述することもできますが、今回のようにtfファイルを複数に分ける場合は毎回記述するのが面倒なのでproviders.tfにまとめました。

今回は下記のようにTerraformのバージョン、AWSのバージョン、リソースを作成するリージョンを記載しました。AWSのバージョンはこちら で最新のものを確認しました。基本的にTerraformで使用するファイルでは、左辺に定義する項目、右辺にその値を記述します。

・providers.tf

terraform {
  required_version = ">= 1.9"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.56"
    }
  }
}
provider "aws" {
  region = "ap-northeast-3"
}

次に、vpc.tfに作成するリソースの定義を記述します。作成するリソースはVPC、サブネット、ルートテーブル、インターネットゲートウェイの4つです。

記述する際はリソースごとにresourceブロックを作成し、resource “作成するリソースの種類””リソース名”とします。そしてその下に各リソースの設定値を記述します。サブネットなどの作成に必要なVPC IDはVPCを作成した際に判明する情報なのでaws_vpc.tf-vpc.idと記述しておきます。これにより、Terraformが作成後のVPC IDを自動で取得して代入してくれます。

・vpc.tf

resource "aws_vpc" "tf-vpc" {
  cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "tf-sb" {
  vpc_id            = aws_vpc.tf-vpc.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "ap-northeast-3a"
}
resource "aws_internet_gateway" "tf-igw" {
  vpc_id = aws_vpc.tf-vpc.id
}
resource "aws_route_table" "tf-rt" {
  vpc_id = aws_vpc.tf-vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.tf-igw.id
  }
}
resource "aws_route_table_association" "tf-rta" {
  subnet_id      = aws_subnet.tf-sb.id
  route_table_id = aws_route_table.tf-rt.id
}

リソース作成(tfファイルの適用)

tfファイルが作成できたらtfファイルをデプロイしてAWS上にリソースを作成します。まず、terraform init でTerraformを初期化します。コマンド実行時に「Terraform has been successfully initialized! 」と出力されていれば問題ありません。

次にterraform planを実行し、作成されるリソース内容を確認します。コマンド実行時にエラーが出力された場合はtfファイルの記述等に誤りがあるのでエラーメッセージを読んで対応します。

terraform planの実行結果に問題がなければterraform applyを実行し、リソースを作成します。下記実行結果のように作成されるリソース情報が出力されるので確認し、問題なければ「yes」と入力し「Enter」を押下します。

コマンド実行結果

~/environment/work $ 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_vpc.tf-vpc will be created
  + resource "aws_vpc" "tf-vpc" {
      + arn                                  = (known after apply)
      + cidr_block                           = "10.0.0.0/16"
      + default_network_acl_id               = (known after apply)
      + default_route_table_id               = (known after apply)
      + default_security_group_id            = (known after apply)
      + dhcp_options_id                      = (known after apply)
      + enable_dns_hostnames                 = (known after apply)
      + enable_dns_support                   = true
      + enable_network_address_usage_metrics = (known after apply)
      + id                                   = (known after apply)
      + instance_tenancy                     = "default"
      + ipv6_association_id                  = (known after apply)
      + ipv6_cidr_block                      = (known after apply)
      + ipv6_cidr_block_network_border_group = (known after apply)
      + main_route_table_id                  = (known after apply)
      + owner_id                             = (known after apply)
      + tags_all                             = (known after apply)
    }

Plan: 5 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_vpc.tf-vpc: Creating...
aws_vpc.tf-vpc: Creation complete after 1s [id=vpc-0e64a36add9b0521d]
aws_subnet.tf-sb: Creating...
aws_internet_gateway.tf-igw: Creating...
aws_internet_gateway.tf-igw: Creation complete after 1s [id=igw-0a604c13a79165acc]
aws_route_table.tf-rt: Creating...
aws_subnet.tf-sb: Creation complete after 1s [id=subnet-0277a01c9f1cd5ee4]
aws_route_table.tf-rt: Creation complete after 0s [id=rtb-043d9b0f23f89a401]
aws_route_table_association.tf-rta: Creating...
aws_route_table_association.tf-rta: Creation complete after 0s [id=rtbassoc-04b6cee8c4bc06f96]

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

コマンド実行後にコンソールを確認し、VPCやサブネットなどが作成できていれば問題ありません。 

VPCなどの作成が完了したら、次にec2.tfを記述してEC2を作成します。今回はec2.tfに記述する値の一部を変数にするということも試してみます。

先ほど作成したVPCのIDやサブネットのIDなどをec2.tfに記載する必要がありますが、この値を変数としてvariables.tfに格納します。

下記のようにVPC ID、サブネットID、キーペア名を変数にしました。
variable “変数名”のブロックを作成し、変数の説明や代入する値を記述します。

・variables.tf

variable "vpc_id" {
  description = "The ID of the VPC"
  type        = string
  default     = "vpc-0e64a36add9b0521d"
}
variable "subnet_id" {
  description = "The ID of the Subnet"
  type        = string
  default     = "subnet-0277a01c9f1cd5ee4"
}
variable "key_name" {
  description = "The name of the existing key pair"
  type        = string
  default     = "tf-key"
}

次に定義した変数を使ってec2.tfを作成します。変数を使用する時は「var.変数名」と記述することで実行時にTerraformが自動で値を読み込んでくれます。その他はvpc.tfと同様に記述します。ec2.tfの記述内容は以下の通りです。

・ec2.tf

resource "aws_security_group" "tf-sg" {
  name        = "tf-security-group"
  description = "Allow SSH inbound traffic"
  vpc_id      = var.vpc_id
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0 
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
resource "aws_instance" "tf-instance" {
  ami             = "ami-06a554cdc92401b61"
  instance_type   = "t2.micro"
  subnet_id       = var.subnet_id
  key_name        = var.key_name
  security_groups = [aws_security_group.tf-sg.id]
  tags = {
    Name = "tf-instance"
  }
}

tfファイルの作成が完了したら先ほどと同様にterraform plan → terraform applyの順にコマンド実行していきます。

コマンドの実行が完了すると下図のように先ほど作成したVPCとサブネットの中にEC2が作成できていました。

以上が、Terraformを使ったAWS環境構築の流れです。適宜tfファイルの追加や修正をして再度terraform applyを実行することでリソースの追加や設定変更ができます。

リソース削除

最後に作成したリソースを削除します。リソース削除はterraform destroyコマンドを実行します。

以下、terraform destroyの実行結果です。実行時に削除されるリソースが一覧で出力されるので、問題なければ「yes」を入力して「Enter」を押下します。

実行結果

~/environment/work $ terraform destroy
aws_security_group.tf-sg: Refreshing state... [id=sg-05e3e996ab46983cf]
aws_vpc.tf-vpc: Refreshing state... [id=vpc-0e64a36add9b0521d]
aws_instance.tf-instance: Refreshing state... [id=i-07444268887b4881f]
aws_subnet.tf-sb: Refreshing state... [id=subnet-0277a01c9f1cd5ee4]
aws_internet_gateway.tf-igw: Refreshing state... [id=igw-05c75c1d9da221cd0]
aws_route_table.tf-rt: Refreshing state... [id=rtb-08f0056e09ef04fa8]
aws_route_table_association.tf-rta: Refreshing state... [id=rtbassoc-04568299f05ab056f]

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_vpc.tf-vpc will be destroyed
  - resource "aws_vpc" "tf-vpc" {
      - arn                                  = "arn:aws:ec2:ap-northeast-3:529871963353:vpc/vpc-0e64a36add9b0521d" -> null
      - assign_generated_ipv6_cidr_block     = false -> null
      - cidr_block                           = "10.0.0.0/16" -> null
      - default_network_acl_id               = "acl-0d778cf9635f0fb4e" -> null
      - default_route_table_id               = "rtb-0e30d8630ba44af0b" -> null
      - default_security_group_id            = "sg-09c00febc1d15a155" -> null
      - dhcp_options_id                      = "dopt-4698e22f" -> null
      - enable_dns_hostnames                 = false -> null
      - enable_dns_support                   = true -> null
      - enable_network_address_usage_metrics = false -> null
      - id                                   = "vpc-0e64a36add9b0521d" -> null
      - instance_tenancy                     = "default" -> null
      - ipv6_netmask_length                  = 0 -> null
      - main_route_table_id                  = "rtb-0e30d8630ba44af0b" -> null
      - owner_id                             = "529871963353" -> null
      - tags                                 = {} -> null
      - tags_all                             = {} -> null
        # (4 unchanged attributes hidden)
    }

Plan: 0 to add, 0 to change, 7 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: yes

aws_route_table_association.tf-rta: Destroying... [id=rtbassoc-04568299f05ab056f]
aws_instance.tf-instance: Destroying... [id=i-07444268887b4881f]
aws_route_table_association.tf-rta: Destruction complete after 0s
aws_subnet.tf-sb: Destroying... [id=subnet-0277a01c9f1cd5ee4]
aws_route_table.tf-rt: Destroying... [id=rtb-08f0056e09ef04fa8]
aws_route_table.tf-rt: Destruction complete after 0s
aws_internet_gateway.tf-igw: Destroying... [id=igw-05c75c1d9da221cd0]
aws_internet_gateway.tf-igw: Destruction complete after 1s
aws_instance.tf-instance: Still destroying... [id=i-07444268887b4881f, 10s elapsed]
aws_subnet.tf-sb: Still destroying... [id=subnet-0277a01c9f1cd5ee4, 10s elapsed]
aws_instance.tf-instance: Still destroying... [id=i-07444268887b4881f, 20s elapsed]
aws_subnet.tf-sb: Still destroying... [id=subnet-0277a01c9f1cd5ee4, 20s elapsed]
aws_instance.tf-instance: Still destroying... [id=i-07444268887b4881f, 30s elapsed]
aws_subnet.tf-sb: Still destroying... [id=subnet-0277a01c9f1cd5ee4, 30s elapsed]
aws_subnet.tf-sb: Destruction complete after 37s
aws_vpc.tf-vpc: Destroying... [id=vpc-0e64a36add9b0521d]
aws_instance.tf-instance: Destruction complete after 40s
aws_security_group.tf-sg: Destroying... [id=sg-05e3e996ab46983cf]
aws_security_group.tf-sg: Destruction complete after 0s
aws_vpc.tf-vpc: Destruction complete after 4s

Destroy complete! Resources: 7 destroyed.

コマンドの実行が完了したらコンソールを確認し、リソースが表示されなければ削除完了です。

terraform destroyではカレントディレクトリにあるすべてのtfファイルに記載されているリソースが削除されます。削除する際はディレクトリがあっているか、削除してはいけないリソースがないか、確認してからコマンド実行するようにしましょう。

また、terraform applyでもリソースの削除ができます。terraform applyを使ってリソースを削除するときはまず、削除したいリソースの記述をtfファイルから削除します。その後terraform applyを実行するとtfファイルから記述を削除したリソースのみ削除されます。terraform destroyと違い、tfファイルの修正が必要ですが、その分特定のリソースだけ削除できます。リソース削除を実施する時の状況によってうまく使い分けると良いでしょう。

Terraformの応用的な使い方

次にTerraformの応用的な使い方を紹介します。基本的な使い方の所で紹介した内容だけでも十分Terraformは利用できますが、より応用的なtfファイルの記述方法やterraformコマンドの使い方があります。私が実務で活用したものを紹介するのでぜひ参考にしていただければと思います。

terraformコマンドの応用

・terraform apply -target=リソース名

複数人でTerraformを管理していると、terraform planなどを実行した際に自分が修正を加えたtfファイル以外のところで差分が出ることがあります。他のメンバが加えた修正は反映せずに自分が加えた修正は反映させる必要があるときなどに、terraform applyコマンドに-target=オプションを付けて実行します。このオプションを使うことで他の修正を反映させることなく、特定のリソースのみ設定を変更できます。

tfファイルの書き方の応用

・ループ処理

同じサービスのリソースを複数用意したい時などに便利な記述方法がループ処理です。リソース定義が全く同じ場合と少しずつ設定が異なる場合で記述方法も変わるためそれぞれの記述方法を紹介します。

・count

同じ設定で複数のリソースを作成したい時はcountを使ってtfファイルに記述します。記述する際は、リソース定義に”count =作成する数”を追記します。これだけで指定した数だけ同じ設定のリソースが作成されます。リソース名は”${count.index + 1}”とすることで tf-instance- 1, tf-instance- 2, tf-instance- 3という名前で作成されます。例として以下にcountを使ってEC2インスタンスを3台作成する時の記述内容を記載します。

resource "aws_instance" "tf-instance" {
  count         = 3
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  subnet_id     = var.subnet_id
  key_name      = var.key_name
  security_groups = [aws_security_group.tf-sg.name]
  tags = {
    Name = "tf-instance-${count.index + 1}"
  }
}

・for_each

リソースによって少しずつ設定を変えてループ処理を行いたい時はfor_eachを使います。for_eachを使う時はeach.keyとeach.valueをそれぞれ記述します。以下に instance_typeの異なるEC2を3台(A,B,C)を作成する時の記述例を記載します。

記述方法としては、for_eachブロックの中にリソース名ごとのブロック(A, B, C) を用意します。そしてそれぞれのブロックにEC2ごとに設定を変えたい項目(instance_type)を記述します。ここで左辺に記載する項目がeach.key、右辺に記載する値がeach.valueとなります。そしてその下の各設定項目の箇所に”instance_type = each.value.instance_type”と記述することで instance_typeの値はfor_eachブロックから各EC2ごとにeach.valueを取得して作成されます。

このように記述することで異なるinstance_type(その他の設定は同じ)のEC2を3台作成できます。

resource "aws_instance" "tf-instance" {
  for_each = {
    "A" = {
      instance_type = "t2.micro"
    }
    "B" = {
      instance_type = "t2.small"
    }
    "C" = {
      instance_type = "t2.medium"
    }
  }
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = each.value.instance_type
  subnet_id     = var.subnet_id
  key_name      = var.key_name
  security_groups = [aws_security_group.tf-sg.name]
}

このように、Terraformには覚えておくと便利なコマンドオプションやtfファイルの記述方法があります。Terraformの扱いに慣れてきた方やもっと便利に使いたいという方はぜひこういった応用的な使い方も参考にしてみてください。

まとめ

今回はTerraformの使い方について、実際にAWS環境を構築する様子を交えて紹介しました。Terraformの基本的な使い方やリソース構築の流れがイメージできるようになっていれば幸いです。

今回実施した内容は、AWSの無料利用枠でも実施することができるので、自分で実際にterraformを使って構築してみたいという方はぜひ試してみてください。また、AWSリソースをterraformで構築するためのコードはインターネットや生成AIを活用することで書けると思います。それらを参考に他のリソースも作成してみるとterraformとAWS両方の良い学習になるのではないかと思います。

また、実務で活用したterraformの応用的な使い方も紹介したのでそちらも参考にしていただければと思います。terraformなどのIaCツールはインフラストラクチャーの構築、管理には欠かせないものです。インフラエンジニアは身につけておいて損はないと思いますので皆さんもぜひ使ってみてください。