1. ホーム
  2. 記事一覧
  3. Terraformの組み込み関数とは?elementやlookupなどの基本構文と使用例を学ぶ

2023.08.30

Terraformの組み込み関数とは?elementやlookupなどの基本構文と使用例を学ぶ

Terraformの組み込み関数とは、Terraformに最初から組み込まれている、特定の操作や値の変換を行うための関数です。ユーザーが新しい関数を作成することはできませんが、公式に提供されるこれらの関数を利用して、設定値の操作や変更を行うことができます。

例えば、リストや文字列、数値の操作など、多岐にわたる関数が提供されています。

https://developer.hashicorp.com/terraform/language/functions

こちらの記事では、Terraformの組み込み関数についての基本的な構文と使用例について解説します。

Terraformとは?

はじめに、Terraformについて簡単に解説します。

Terraformは、IaCInfrastructure as Code)を実現するためのツールで、アメリカのソフトウェア企業であるHashiCorp社が開発しました。

HCL(HashiCorp Configuration Language)と呼ばれる独自の言語を使用しており、JSONやYAMLと同じような形式で記述をすることができ、読みやすく保守しやすいといった特徴があります。Terraformを利用することで、サーバーやネットワーク、ストレージなどをはじめとしたインフラの構成をコード化し、その構成や管理を自動化することができます。

IaCについてのメリットや、Terraformなどの構成管理ツールについては以下の記事で解説しています。

https://envader.plus/article/136

Terraformにおける組み込み関数の基本

Terraformの組み込み関数を呼び出すには、基本的に以下のように記述します。

関数名(引数1, 引数2)

# 関数名がexampleだった場合
example(A, B)

冒頭でも触れましたが、独自の関数を定義することはできないため、Terraformで使用できるのは組み込み関数のみになります。使用する関数によりますが、terraform consoleコマンドを使用して簡易的に関数を試すことも可能です。

terraform console
> max(100, 200, 300)
300

ただし、terraform consoleコマンドを使用するにはTerraformのinstallなど環境構築を行う必要があります。

Terraformの環境構築やハンズオンについては、以下の記事にて解説しています。

https://envader.plus/article/162

次の章からは、Terraformの組み込み関数について具体的にどのような関数があるのか、またその関数の使用方法について解説します。

解説の中で変数の型が出てきますが、型の詳細については別の記事にてご確認ください。

https://envader.plus/article/190

String Functions

「String Functions」は文字列を変換したり、文字列の間に指定した文字を挿入したりすることができる関数群です。

format

format関数は、指定したフォーマットの文字列に従って値を組み合わせて一つの文字列を作成します。

# 基本構文
format("<FORMAT_STRING>", arg1, arg2, ...)

<FORMAT_STRING> には、挿入したい位置を示すプレースホルダーとして**%s**などのverbsと呼ばれるフォーマット指定子を使用します。argの部分で与えられた引数は、このフォーマット指定子の位置に順に適用されます。

# 参考例
output "formatted_string" {
  value = format("私の名前は%s、年齢は%d歳です。", "田中", 30)
}

# 結果
私の名前は田中、年齢は30歳です。

AWSでインスタンスを作成し、タグを付与する場合には次のように関数を使用することも可能です。

# var.environment = dev
# var.region = ap-northeast-1
resource "aws_instance" "example" {
  tags = {
    Name = format("webserver-%s-%s", var.environment, var.region)
  }
}

# 結果
webserver-dev-ap-northeast-1

format関数のフォーマット指定子には型のようなものがあり、%の後に指定することで関数が受け取る値を指定することができます。

フォーマット指定子説明結果
%s文字列としての値を表示します。format("%s", "a")a
%d整数としての値を表示します。format("%d", 123)123
%f浮動小数点数としての値を表示します。format("%f", 1.23)1.230000
%v値のデフォルトの型を自動で判断して表示します。format("%v", true)true
%%パーセンテージ記号 (%) を表示します。format("%%")%
%[n]s指定した位置の引数を文字列として表示します。順番を指定することができます。format("%[2]s %[1]s", "first", "second")second first

型に対応していない値を受け取った場合にはエラーが発生します。

# %dは数値型を指定するため、文字列を渡すとエラーになる
$ format("webserver-%d-%d", "develop", "ap-northeast1a")
│ Error: Error in function call
on <console-input> line 1:
│   (source code not available)
Call to function "format" failed: unsupported value for "%d" at 10: a number is required.

そのほかのフォーマット指定子については、公式ドキュメントで解説されています。

https://developer.hashicorp.com/terraform/language/functions/format#verbs

join

join関数はリスト型の文字列を、指定した区切り文字で結合させることができます。リスト型以外の値に使用した場合にはエラーが発生するため注意が必要です。

# 基本構文
join(区切り文字, list)

このままではわかりにくいため、次に例を示します。

$ join("-", ["a", "b", "c"])
"a-b-c"

$ join("?", ["a", "b", "c"])
"a?b?c"

以上のように、join関数はリスト型の文字列を区切り文字で結合することができます。

この方法をもとにAWSでサブネットを作成し、作成したサブネットのCIDRブロックを出力する際には次のようにjoin関数を使うことができます。

variable "azs" {
  description = "Map of Availability Zones and their CIDRs"
  type = map(object({
    public_cidr  = string
    private_cidr = string
  }))
  default = {
    "ap-northeast-1a" = {
      public_cidr  = "10.0.1.0/24"
      private_cidr = "10.0.10.0/24"
    }
    "ap-northeast-1c" = {
      public_cidr  = "10.0.2.0/24"
      private_cidr = "10.0.11.0/24"
    }
  }
}

resource "aws_subnet" "public" {
  for_each = var.azs

  availability_zone = each.key
  cidr_block        = each.value.public_cidr
  vpc_id            = aws_vpc.main.id

  tags = {
    Name = "public-${each.key}"
  }
}

output "public_cidrs" {
  value = join(",", [for s in aws_subnet.public : s.cidr_block])
}

# planの結果
Changes to Outputs:
  + public_cidrs = "10.0.1.0/24,10.0.2.0/24"

split

split関数は、指定した文字列を指定した区切り文字のところで分割し、リスト型の文字列を生成してくれます。

# 基本構文
split(区切り文字, 文字列)

次に例を示します。

$ split(",", "apple,banana,orange")
tolist(["apple", "banana", "orange",])

$ split("-", "2023-08-25")
tolist(["2023", "08", "25",])

例のように、文字列の中に指定した区切り文字があった場合、そこを起点としてリスト型の文字列を生成してくれます。指定した区切り文字が文字列内にない場合、元の文字列がそのままリスト型に生成されて返されます。

$ split(",", "2023-08-25")
tolist([ "2023-08-25",])

VPCを作成する際に、プロジェクト、環境、リージョンのタグを付与する場面を想定した場合は次のようにjoin関数を使用することができます。

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = join("-", ["testproject", "development", "ap-northeast-1" ])
  }
}

# 結果
+ 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                                 = {
  + "Name" = "testproject-development-ap-northeast-1"
    }

Collection Functions

「Collection Functions」は、Terraformの型の一つであるコレクション型(list、map、set)を操作するための関数群です。Cllection Functionsの関数を使用することで、コレクションの要素を効率的に操作、変換、または取得することができます。

length

Terraformのlength関数は、リスト、マップ、または文字列の長さ(要素の数または文字数)を返すために使用することができます。

# 基本構文
length(リスト or マップ or 文字列)

リストの要素の数を計算する場合は次のようになります。

variable "mylist" {
  description = "A sample list"
  default     = ["a", "b", "c", "d"]
}

# 結果
$ terraform console
> length(var.mylist)
4

マップの要素数を計算する場合

variable "mymap" {
  description = "A sample map"
  default = {
    key1 = "value1"
    key2 = "value2"
    key3 = "value3"
  }
}

# 結果
$ terraform console
> length(var.mymap)
3

文字列の場合

variable "mystring" {
  description = "A sample string"
  default     = "Hello, Terraform!"
}

# 結果
$ terraform console
> length(var.mystring)
17

変数でサブネットのリストを定義し、複数のサブネットを作成する際にlength関数を使用する例は次のようになります。

variable "subnets" {
  description = "CIDR blocks for the subnets"
  default = [
    "10.0.1.0/24",
    "10.0.2.0/24",
    "10.0.3.0/24"
  ]
}

resource "aws_subnet" "public" {
  count      = length(var.subnets) # サブネット変数の要素数を取得。ここでは3
  vpc_id     = aws_vpc.main.id
  cidr_block = var.subnets[count.index] # CIDRを動的に取得し割り当てている

  tags = {
    Name = "subnet-${count.index}"
  }
}

lookup

Terraformのlookup関数は、マップ型の値に対して使用することができ、マップ内の特定のキーに関連付けられた値を取得するために使用します。もし指定されたキーが存在しない場合、デフォルト値を返します。

# 基本構文
lookup(map, key, default)
  • map・・・値を検索したいマップ
  • key・・・検索したいキー値
  • default・・・キーが存在しなかった場合に返す値。オプションのためこの値はなくても問題ないです。
# 使用例
variable "settings" {
  default = {
    "env"  = "development"
    "size" = "small"
  }
}

$ terraform console
> lookup(var.settings, "env", "default_env") # envというキーが存在しているため、値を返している。
"development"

$ terraform console
> lookup(var.settings, "region", "ap-northeast-1") # regionというキーがないため、デフォルト値を返す。
"ap-northeast-1"

element

element関数は、リストから特定のインデックスの要素を取得するために使用します。リストのインデックスは0から始まるため、1番目の要素はlist[0]、2番目の要素はlist[1]というように取得します。万が一指定したインデックスがリストの長さを超えている場合、**element**関数はリストをループするような動作をします。

# 基本構文
element(list, index)
  • list・・・取得したい要素を含むリストを指定します。
  • index・・・取得したいリストのインデックスを指定します。
# 使用例
variable "sample_list" {
  description = "sample list"
  default     = ["apple", "banana", "cherry"]
}

 # 結果
$ terraform console
> element(var.sample_list, 1)
"banana"
> element(var.sample_list, 2)
"cherry"

indexで指定したインデックスがリストの長さを超えた場合、ループしたような動作をするのがelement関数の特徴です。

# 使用例
variable "sample_list" {
  description = "sample list"
  default     = ["apple", "banana", "cherry"]
}

# 結果
$ terraform console
> element(var.sample_list, 4)
"banana"

また、リストの要素を取得する際にはelement関数ではなく、以下のように記述することで指定したインデックスの要素を取得することができます。

$ terraform console
> var.sample_list[1]
"banana"

この方法でリストへアクセスした場合、インデックスの値がリストの長さを超えた場合、ループしたような動作はせずにエラーが発生するため注意が必要です。

values

values関数は、マップやオブジェクトの値をリスト化して返してくれます。

# 基本構文
values(MAP_OR_OBJECT)

実際の使用例は次のようになります。

# 使用例
variable "azs" {
  description = "Availability Zones and their CIDR mappings"
  type = map(object({
    public_cidr  = string
    private_cidr = string
  }))
  default = {
    "ap-northeast-1a" = {
      public_cidr  = "10.0.1.0/24"
      private_cidr = "10.0.10.0/24"
    }
    "ap-northeast-1c" = {
      public_cidr  = "10.0.2.0/24"
      private_cidr = "10.0.11.0/24"
    }
  }
}

# 結果
$ terraform console
> values(var.azs)
tolist([
  {
    "private_cidr" = "10.0.10.0/24"
    "public_cidr" = "10.0.1.0/24"
  },
  {
    "private_cidr" = "10.0.11.0/24"
    "public_cidr" = "10.0.2.0/24"
  },
])

terraform consoleで実行すると上記のような表示になりますが、実際に返ってきている値としては、以下の値が返ってきていることになります。リストの中に複数のマップが含まれています。

[{"private_cidr" = "10.0.10.0/24", "public_cidr" = "10.0.1.0/24"}, {"private_cidr" = "10.0.11.0/24", "public_cidr" = "10.0.2.0/24"}]

Filesystem Functions

「Filesystem Functions」は、ファイルやディレクトリに関連する操作を行うための関数群です。これらの関数を使用することで、ファイルシステムの内容の読み取りや、特定のパスに関連する情報の取得など、ファイルシステムのタスクを簡単に扱うことができます。

file

file関数は、指定したファイルの内容を文字列として読み取ることができます。file関数を利用することで、設定ファイルなどの外部ファイルをTerraformで使用することができます。

# 基本構文
file(path)

同じディレクトリに使用したいファイルが存在する場合は、ファイル名を指定することで内容を読み取ることができます。

.
├── README.md
├── main.tf
├── terraform.tfstate
├── terraform.tfstate.backup
├── test.txt
├── variables.tf
└── versions.tf

# 結果
$ terraform console
> file("test.txt")
<<EOT
"Hello World"

EOT

別のディレクトリにファイルが存在している場合には、相対パスまたは絶対パスを指定することもできます。

.
├── README.md
├── modules
│   └── test.txt
├── network.tf
├── terraform.tfstate
├── terraform.tfstate.backup
├── terraform.tfvars
├── variables.tf
└── versions.tf

# 結果
$ terraform console
> file("./modules/test.txt")
<<EOT
"Hello World"

EOT

# 絶対パス
$ terraform console
> file("/Users/user/terraform/modules/test.txt")
<<EOT
"Hello World"

EOT

AWSのIAMポリシーをJSONファイルとして管理している場面を想定してみます。s3-full-access-policy.jsonというファイルが以下の内容で存在するとします。

# s3-full-access-policy.json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "s3:*",
    "Resource": "*"
  }]
}

このJSONファイルを使用して、IAMポリシーを作成する例は以下のようになります。

resource "aws_iam_policy" "s3_full_access" {
  name        = "S3FullAccess"
  description = "My policy that grants full access to S3"
  
  # `file` 関数でポリシーの内容を読み取る
  policy = file("s3-full-access-policy.json")
}

IP Network Functions

「IP Network Functions」は、IPアドレスやネットワークに関連する操作をサポートするための関数群です。これらの関数を使用することで、IPネットワークの計算や変換タスクを簡単に行うことができます。

IPアドレスの詳細については、以下の記事にて解説しています。

https://envader.plus/article/51

CIDRについて理解したい方は次の記事をご参照ください。

https://envader.plus/article/52

cidrhost

cidrhost関数は、指定したCIDRブロックのネットワークアドレスに、指定したホスト番号(hostnumber)を加算したIPアドレス(ホストアドレス)を返す関数です。

# 基本構文
cidrhost(prefix, hostnumber)
  • prefix・・・CIDR表記のIPアドレスの範囲
  • hostnumber・・・CIDR表記のネットワークアドレスから加算する番号

cidrhost関数を使用する際には、常にCIDR表記のネットワークアドレスを意識しておく必要があります。指定したCIDRブロックから加算するわけではなく、指定したCIDRブロックのネットワークアドレスから数値分加算したIPアドレスを返す関数がcidrhost関数です。

# 使用例 10.0.0.0/24のネットワークアドレスは10.0.0.0のため、以下のような結果になる。

$ terraform console
> cidrhost("10.0.0.0/24", 7)
"10.0.0.7"
> cidrhost("10.0.0.5/24", 7)
"10.0.0.7"
> cidrhost("10.0.0.10/24", 7)
"10.0.0.7"
> cidrhost("10.0.0.10/24", 9)
"10.0.0.9"

cidrsubnet

cidrsubnet関数は、ベースとなるCIDRに基づいて新しいサブネットのアドレスを計算する関数です。

# 基本構文
cidrsubnet(prefix, newbits, netnum)
  • prefix・・・計算のベースとなるCIDRブロックを指定します。
  • newbits・・・基となるCIDRブロックから、何ビット拡張するかを指定します。
  • netnum・・・ここで指定した数値が2進数に変換され、新しく拡張されたビットに適用されます。
# 例
$  terraform console
> cidrsubnet("10.0.0.0/16", 8, 1)
# 結果
"10.0.1.0/24"

> cidrsubnet("10.0.0.0/16", 8, 16)
"10.0.16.0/24"

実際に2進数に変換して考えると、以下のように考えることができます。

# cidrsubnet("10.0.0.0/16", 8, 16)の場合
ベースアドレス: 10.0.0.0/16
10進数:10.0.0.0
2進数:00001010.00000000.00000000.00000000

ベースアドレス /16 + 引数で拡張するビット数 8 = 24
# ここまでで10.0.0.0/24となる

引数netnumの値 16 これを2進数へ変換
10進数:16
2進数:00010000

10.0.0.0/24に対して00010000の値を当てはめる(24ビットのところから値を当てはめる)
10進数:10.0.16.0/24
2進数:00001010.00000000.00010000.00000000

このようにしてcidrsubnet関数は計算することが可能になります。複雑に感じますが、2進数に変換して考えることで理解しやすくなります。

cidrsubnets

cidrsubnets関数は、指定したCIDRのネットワークアドレスから複数の小さなサブネットに分割するためのものです。この関数を使用すると、指定したCIDRブロックから複数のサブネットを簡単に計算して生成できます。

# 基本構文
cidrsubnets(prefix, newbits, newbits, ..)
  • prefix・・・計算のベースとなるCIDRブロックを指定します。
  • newbits・・・基となるCIDRブロックから、何ビット拡張するかを指定します。

先ほどのcidrsubnet関数と考え方はほぼ同じになります。違いとして、cidrsubnet関数は単純に数値で指定した場合一つのサブネットしか計算できませんが、cidrsubnets関数であれば一度に複数のサブネットを計算することができます。

# 例
$ terraform console
> cidrsubnets("10.0.0.0/16", 8, 8, 8, 8)
tolist([
  "10.0.0.0/24",
  "10.0.1.0/24",
  "10.0.2.0/24",
  "10.0.3.0/24",
])

上記の例では、/16に対して8ビット拡張した24ビットでサブネットを複数作成しています。この例では全て8ビットの拡張を指定していますが、拡張するビット数を変更しても作成することが可能です。

$ terraform console
> cidrsubnets("10.0.0.0/16", 8, 10, 15, 12)
tolist([
  "10.0.0.0/24",
  "10.0.1.0/26",
  "10.0.1.64/31",
  "10.0.1.80/28",
])

Type Conversion Functions

「Type Conversion Functions」は、値のデータ型を他のデータ型に変換するための関数群です。データ型の変換は、異なるリソースやプロバイダ間でのデータの受け渡しや、期待される型に合わせて値を調整する際に特に有用です。

tostring

tostring関数は、数値やboolなどのプリミティブ型を文字列に変換する際に使用し、リストやマップをvalueで指定するとエラーを返します。

# 基本構文
tostring(value)

数値やbool値を文字列に変換することができるため、タグに値を文字列として使用したい時に利用することができます。

# 例
variable "is_enabled" {
  description = "Flag to indicate if the feature is enabled"
  type        = bool
  default     = true
}

resource "aws_instance" "example" {
  # ... その他の設定 ...

  tags = {
    FeatureEnabled = tostring(var.is_enabled) # true/false を "true"/"false" に変換
  }
}

toset

toset関数は、引数として渡した値をセット型に変換する関数です。セット型に変換することで値に含まれる重複した値は削除され、要素の順序も保証されないことが特徴です。また、リストの中に異なる型の値がある場合、自動的にTerraformが判断し型変換が行われます。

# 基本構文
toset(list or taple)

# 例
$ terraform console
> toset(["apple", "banana", "apple", "cherry", "strawberry"])
[
  "apple",
  "banana",
  "cherry",
  "strawberry",
]

> toset(["apple", "banana", "apple", "cherry", "strawberry", 5])
toset[
  "5",
  "apple",
  "banana",
  "cherry",
  "strawberry",
]

ループ処理を行うfor_eachを使う際に、値をtoset関数でセット値に変換するといった場面が考えられます。

variable "subnet_cidrs" {
  type        = list(string)
  description = "subnet CIDR"
	default     = ["10.1.10.0/24", "10.1.20.0/24"]
}

resource "aws_subnet" "public" {
  for_each   = toset(var.subnet_cidrs)
  vpc_id     = aws_vpc.main_vpc.id
  cidr_block = each.value

  tags = {
    Name = "public-${each.value}"
  }
}

まとめ

こちらの記事では、Terraformの組み込み関数について解説しました。紹介した組み込み関数をうまく活用することで、コードの可読性や再利用性を上げることができます。場面ごとにうまく組み合わせて利用し、作業効率をアップさせましょう。

また、「IP Network Functions」ではTerraformの知識以外にIPアドレスやCIDRに関する知識も必要になってくるため、ネットワークの知識もしっかりと理解しておきたいところです。

【番外編】USBも知らなかった私が独学でプログラミングを勉強してGAFAに入社するまでの話

IT未経験者必見 USBも知らなかった私が独学でプログラミングを勉強してGAFAに入社するまでの話

プログラミング塾に半年通えば、一人前になれると思っているあなた。それ、勘違いですよ。「なぜ間違いなの?」「正しい勉強法とは何なの?」ITを学び始める全ての人に知って欲しい。そう思って書きました。是非読んでみてください。

「フリーランスエンジニア」

近年やっと世間に浸透した言葉だ。ひと昔まえ、終身雇用は当たり前で、大企業に就職することは一種のステータスだった。しかし、そんな時代も終わり「優秀な人材は転職する」ことが当たり前の時代となる。フリーランスエンジニアに高価値が付く現在、ネットを見ると「未経験でも年収400万以上」などと書いてある。これに釣られて、多くの人がフリーランスになろうとITの世界に入ってきている。私もその中の1人だ。数年前、USBも知らない状態からITの世界に没入し、そこから約2年間、毎日勉学を行なった。他人の何十倍も努力した。そして、企業研修やIT塾で数多くの受講生の指導経験も得た。そこで私は、伸びるエンジニアとそうでないエンジニアをたくさん見てきた。そして、稼げるエンジニア、稼げないエンジニアを見てきた。

「成功する人とそうでない人の違いは何か?」

私が出した答えは、「量産型エンジニアか否か」である。今のエンジニア市場には、量産型エンジニアが溢れている!!ここでの量産型エンジニアの定義は以下の通りである。

比較的簡単に学習可能なWebフレームワーク(WordPress, Rails)やPython等の知識はあるが、ITの基本概念を理解していないため、単調な作業しかこなすことができないエンジニアのこと。

多くの人がフリーランスエンジニアを目指す時代に中途半端な知識や技術力でこの世界に飛び込むと返って過酷な労働条件で働くことになる。そこで、エンジニアを目指すあなたがどう学習していくべきかを私の経験を交えて書こうと思った。続きはこちらから、、、、

note記事3000いいね超えの殿堂記事 今すぐ読む

エンベーダー編集部

エンベーダーは、ITスクールRareTECHのインフラ学習教材として誕生しました。 「遊びながらインフラエンジニアへ」をコンセプトに、インフラへの学習ハードルを下げるツールとして運営されています。

RareTECH 無料体験授業開催中! オンラインにて実施中! Top10%のエンジニアになる秘訣を伝授します! RareTECH講師への質疑応答可

関連記事