Terraformで始めるインフラ管理 – 第4回

目次

はじめに

前回の記事では、Terraform におけるリソースのライフサイクルについて学びました。今回は、Terraform でどのようにプログラミングを行うかについて学んでいきます。

Terraform は関数型プログラミング(Functional Programming)スタイルでのプログラミングをサポートしています。

EC2の作成

Terraform でのプログラミングの概念を理解するために、EC2 の例を使ってみましょう。
main.tf という名前のファイルを作成します。

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

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  owners = ["099720109477"]
}

resource "aws_instance" "hello" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
}

terraform initterraform apply を実行すると、AWS 上に自分の EC2 インスタンスが作成されているのが確認できます。
上記のコードでは、EC2 の instance_type は常に t2.micro に固定されています。
では、異なる instance_type の EC2 を作成したい場合はどうすればよいでしょうか?Terraform のコードを毎回修正する必要があるのでしょうか?

それでは柔軟性に欠けますよね。
このような場合には、「変数」(プログラミングでは variable と呼ばれる)を使うことで対応できます。

入力変数の宣言を行います。

以下のような構文で、Terraform に変数を定義することができます。

これは、変数を宣言するための variable ブロックの構文です。
上記の例では、変数を宣言するために variable.tf という別のファイルを作成しています(ちなみに、ファイル名は自由に付けても大丈夫ですよ)。

variable "instance_type" {
  type = string
  description = "Instance type of the EC2"
}

type 属性は、その変数がどのようなデータ型を持つかを指定するために使用されます。
description 属性は、その変数が何を意味するのか、読み手に説明するためのものです。

このうち、必須で指定する必要があるのは type 属性のみです。

Terraform では、変数には以下のようなデータ型を使用することができます:

  • 基本型(Basic Type): string, number, bool
  • 複合型(Complex Type): list, set, map, object, tuple

Terraform では、numberbool 型の値は、必要に応じて自動的に string 型に変換されます。
つまり、1"1" に、true"true" に変換されるということです。

変数の値にアクセスするには、var.<VARIABLE_NAME> という構文を使用します。
では、main.tf ファイルを更新してみましょう。

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

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  owners = ["099720109477"]
}

resource "aws_instance" "hello" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type # change here
}

instance_type 属性には、これまでのように固定の値を直接指定するのではなく、
代わりに変数 var.instance_type を使用するように変更します。

変数に値を割り当てる

変数に値を割り当てるために、terraform.tfvars という名前のファイルを作成します。

instance_type = "t2.micro"

terraform apply を実行すると、Terraform は terraform.tfvars ファイルを読み込んで、変数に対するデフォルト値として使用します。
もしそのデフォルトを使いたくない場合は、apply コマンドを実行する際に -var-file オプションを付けることで、別のファイルを指定できます。

例として、production.tfvars という名前のファイルを作成します。

instance_type = "t3.small"

本番環境(production)で CI/CD を実行する際は、次のようにファイルを指定します

terraform apply -var-file="production.tfvars"

これで、instance_type の値はずっと柔軟に扱えるようになりました。

変数のエラーを確認する

この変数に対して、許可された値のみを割り当てられるように制限したい場合は、validation 属性を使用して定義することもできます。

variable "instance_type" {
  type = string
  description = "Instance type of the EC2"

  validation {
    condition = contains(["t2.micro", "t3.small"], var.instance_type)
    error_message = "Value not allow."
  }
}

上記のファイルでは、contains 関数を使って、変数 instance_type の値が許可された配列内にあるかどうかを確認します。
もし指定した値がその配列に含まれていない場合、terraform apply を実行したときに、error_message に記述されたエラーメッセージが表示されます。

terraform.tfvars ファイルも、それに合わせて修正しましょう。

instance_type = "t3.micro"

terraform apply を実行します。

╷
│ Error: Invalid value for variable
│
│   on variable.tf line 1:
│    1: variable "instance_type" {
│
│ Value not allow.
│
│ This was checked by the validation rule at variable.tf:5,3-13.
╵

validation を使うことで、変数に設定できる値を制限することができます。
terraform.tfvars ファイルは、元の正しい値に戻しておきましょう。

通常、EC2 を作成した後は、その IP アドレスを確認したくなりますよね。
それを実現するために、Terraform では output ブロックを使用します。

出力値

output ブロックの値は、Terraform 実行後にターミナルに出力されます。
構文は以下のようになります

EC2 の public_ip を出力するには、main.tf ファイルに以下のコードを追加します

...

output "ec2" {
  value = {
    public_ip = aws_instance.hello.public_ip
  }
}

terraform apply -auto-approve コマンドを再実行すると、EC2 の IP アドレスがターミナルに出力されているのが確認できます。

...

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

Outputs:

ec2 = {
  "public_ip" = "52.36.124.230"
}

これで、変数と output の使い方がわかりましたね。
では次に、もう 1 台 EC2 インスタンスを追加したい場合はどうすればよいでしょうか?

その場合は、main.tf ファイル内で、既存の EC2 のリソースブロックをコピーして、もう 1 台分の定義を追加します。

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

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  owners = ["099720109477"]
}

resource "aws_instance" "hello1" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type
}

resource "aws_instance" "hello2" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type
}

output "ec2" {
  value = {
    public_ip1 = aws_instance.hello1.public_ip
    public_ip2 = aws_instance.hello2.public_ip
  }
}

EC2 の 2 台目を作成するには、もう 1 つの resource ブロックを追加し、
output ブロックも更新して、それぞれの EC2 の IP アドレスを出力できるようにします。

ここまでは特に難しいことはありません
しかし…
もし EC2 を 100 台作りたい としたらどうでしょう?

100 個の resource ブロックをコピペして書くこともできますが、
そんなことをする人はいませんよね 😂

代わりに、Terraform では count 属性 を使って、
1 つの resource 定義から複数のリソースをまとめて作成することができます。

Countの属性

カウント属性(count)は Meta Argument(メタ引数)であり、Terraform の属性であって、Provider に属する resource type の属性ではありません。第1回で説明したように、resource type は Provider が提供する属性のみを含みます。一方、Meta Argument は Terraform 自体の属性であり、つまり、任意の resource ブロックで使用することができます。
main.tf ファイルを更新して、5 台の EC2 を作成しましょう。

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

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  owners = ["099720109477"]
}

resource "aws_instance" "hello" {
  count         = 5
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type
}

output "ec2" {
  value = {
    public_ip1 = aws_instance.hello[0].public_ip
    public_ip2 = aws_instance.hello[1].public_ip
    public_ip3 = aws_instance.hello[2].public_ip
    public_ip4 = aws_instance.hello[3].public_ip
    public_ip5 = aws_instance.hello[4].public_ip
  }
}

今では、apply を実行すると Terraform は 5 台の EC2 を作成してくれます。
出力(output)の部分に注目すると、リソースにアクセスするには [] とリソースのインデックス値を使う必要があります。
通常、リソースには <RESOURCE TYPE>.<NAME> の構文でアクセスしますが、count を使う場合は <RESOURCE TYPE>.<NAME>[index] の構文でアクセスします。

これで、リソースを大量に作る際のコピー問題は解決できましたが、output の部分ではまだ個別に書く必要があります。
これを解決するために、for 式(for 表現)を使います。

For 式

For を使うことで、リストをループ処理することができます。

for <value> in <list> : <return value>

例:

  • 値を大文字に変換して新しい配列を作成する: [for s in var.words : upper(s)]
  • 値を大文字に変換して新しいオブジェクトを作成する: { for k, v in var.map : k => upper(v) }

for を使って、EC2 の output 部分を簡潔にします。
main.tf ファイルを更新しましょう。

resource "aws_instance" "hello" {
  count         = 5
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type
}

output "ec2" {
  value = {
    public_ip = [ for v in aws_instance.hello : v.public_ip ]
  }
}

上記の output 部分では、すべての EC2 インスタンスの public_ip を配列として出力します。
一方で、もし { public_ip1: <value>, public_ip2: <value> } のような形式で出力したい場合は、format 関数を使うことができます。

Format式

format 関数は文字列の連結に役立ちます。output を次のように更新しましょう

resource "aws_instance" "hello" {
  count         = 5
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type
}

output "ec2" {
  value = { for i, v in aws_instance.hello : format("public_ip%d", i + 1) => v.public_ip }
}

terraform plan を実行して確認すると、現在の output{ public_ip1: <value>, public_ip2: <value> } の形式になっていることがわかります。

Changes to Outputs:
  + ec2 = {
      + public_ip1 = (known after apply)
      + public_ip2 = (known after apply)
      + public_ip3 = (known after apply)
      + public_ip4 = (known after apply)
      + public_ip5 = (known after apply)
    }

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

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 における簡単なプログラミングの方法についていくつか学びました。
variable を使って変数を保持し、output を使って出力値を表示し、for を使って配列をループ処理しました。
次回は、Terraform を使って Web サイトを S3 にデプロイする例を通して、いくつかの関数をさらに学んでいきます。