🍫

TerraformのBuilt-in関数や小ネタのあんちょこを作った

に公開

このブログを一言で

今まで使ったことあるorよく使う、TerraformのBuilt-in関数の説明や動作例、小ネタをまとめてみたかったので、重い腰あげてみたよ。
今後も順次メンテしていくよ。

関数(順不同)

参考:Built-in Functions
以下によく使う関数の動作例を記載していきます。

Collection Function

flatten関数

flattenは配列で入れ子になっているものを平準化します。

# flatten
variable "example_list_of_lists" {
  default = [["a", "b"], ["c", "d"], ["e", "f"]]
}

output "as_list_of_lists" {
  value = var.example_list_of_lists
}

output "as_flattened_list" {
  value = flatten(var.example_list_of_lists)
}

Changes to Outputs:
  as_list_of_lists  = [
      [
          "a",
          "b",
        ],
      [
          "c",
          "d",
        ],
      [
          "e",
          "f",
        ],
    ]
  as_flattened_list = [
      "a",
      "b",
      "c",
      "d",
      "e",
      "f",
    ]

以下のように、list(object)の場合、env, subnet, ami, instanceのように要素を同じ階層にすることが可能です。

locals {
  default = [
    {
      name     = "dev"
      subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
      ami      = "ami-12345678"
      instance = "t3.micro"
    },
    {
      name     = "prod"
      subnets  = ["10.0.11.0/24", "10.0.12.0/24"]
      ami      = "ami-87654321"
      instance = "t3.small"
    }
  ]

  # flattenで「環境 × サブネット」の組み合わせを1次元にする
  instances = flatten([
    for env in local.default : [
      for sn in env.subnets : {
        env      = env.name
        subnet   = sn
        ami      = env.ami
        instance = env.instance
      }
    ]
  ])
}

output "instances" {
  value = local.instances
}

Changes to Outputs:
  instances = [
      {
          ami      = "ami-12345678"
          env      = "dev"
          instance = "t3.micro"
          subnet   = "10.0.1.0/24"
        },
      {
          ami      = "ami-12345678"
          env      = "dev"
          instance = "t3.micro"
          subnet   = "10.0.2.0/24"
        },
      {
          ami      = "ami-87654321"
          env      = "prod"
          instance = "t3.small"
          subnet   = "10.0.11.0/24"
        },
      {
          ami      = "ami-87654321"
          env      = "prod"
          instance = "t3.small"
          subnet   = "10.0.12.0/24"
        },
    ]

これは、以下のようにfor_eachで利用できます。
for_eachでmapを生成し、EC2を作成することも可能です。

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

  tags = {
    Name = "this"
  }
}

resource "aws_subnet" "this" {
  # キーの重複があるため、独自キーを作成して、map化する
  for_each = {
    for v in local.instances : "${v.env}-${v.subnet}" => v
  }

  vpc_id     = aws_vpc.this.id
  cidr_block = each.value.subnet
  tags = {
    Name = "${each.value.env}-subnet"
  }
}

resource "aws_instance" "this" {
  # キーの重複があるため、独自キーを作成して、map化する
  for_each = {
    for v in local.instances : "${v.env}-${v.subnet}" => v
  }

  ami           = each.value.ami
  instance_type = each.value.instance
  subnet_id     = aws_subnet.this[each.key].id

  tags = {
    Name = "${each.value.env}-instance"
  }
}

merge関数

複数のmapを結合して、重複を除いた形で出力します。
複数のKeyが重複している場合は、後続のKeyが優先されます。

variable "map1" {
  default = {
    a = 1
    b = 2
  }
}

variable "map2" {
  default = {
    b = 20
    c = 30
  }
}

output "merged_map" {
  value = merge(var.map1, var.map2)
}

Changes to Outputs:
  merged_map = {
      a = 1
      b = 20
      c = 30
    }

setintersection関数

複数のsetの重複を出力します。

locals {
  list1        = ["a", "b", "c"]
  list2        = ["b", "c", "d"]
  intersection = setintersection(toset(local.list1), toset(local.list2))
}

output "intersection" {
  value = local.intersection
}

Changes to Outputs:
  intersection = [
      "b",
      "c",
    ]

index関数

listの要素番号を返します。

locals {
  my_list = ["a", "b", "c"]
  my_index = index(local.my_list, "b")
}

output "my_index" {
  value = local.my_index
}

Changes to Outputs:
  my_index = 1

keys関数

mapのKeyを返します。

locals {
	example_map = {
		a = 1
		b = 2
		c = 3
	}
	example_keys = keys(local.example_map)
}

output "example_keys" {
	value = local.example_keys
}

Changes to Outputs:
  example_keys = [
      "a",
      "b",
      "c",
    ]

values関数

mapのValueを返します。

locals {
	example_map = {
		a = 1
		b = 2
		c = 3
	}
	example_values = values(local.example_map)
}

output "example_values" {
	value = local.example_values
}

Changes to Outputs:
  example_values = [
      1,
      2,
      3,
    ]

length関数

set, list, mapの長さを返します。

locals {
  my_list = ["a", "b", "c"]
  my_length = length(local.my_list)
}

output "my_length" {
  value = local.my_length
}

Changes to Outputs:
  my_length = 3

setproduct関数

複数のリストを1つのリストに直積でまとめます。
こんな感じで二重ループすることも可能です。
https://qiita.com/sekai/items/93aa4978f8cfc87e0913

locals {
  set1 = ["a", "b"]
  set2 = [1, 2]
  product = setproduct(local.set1, local.set2)
}

output "setproduct_example" {
  value = local.product
}

Changes to Outputs:
  setproduct_example = [
       [
           "a",
           1,
        ],
       [
           "a",
           2,
        ],
       [
           "b",
           1,
        ],
       [
           "b",
           2,
        ],
    ]

String Function

join関数

文字列のリストを区切り文字で連結した文字列を生成します。

locals {
  list = ["apple", "banana", "cherry"]
  joined = join(", ", local.list)
}

output "join_example" {
  value = local.joined
}

Changes to Outputs:
   join_example = "apple, banana, cherry"

Type Conversion Function

toset/tolist関数

for_eachはsetかmapしか受け付けないため、tosetでlistをsetに変換する場合に使われることが多い。逆にtolist関数はsetをlistに変換します。

locals {
  example_set = toset(["a", "b", "c"])
  example_list = tolist(local.example_set)
}

❯ terraform console
> type(local.example_set)
set(string)

> type(local.example_list)
list(string)

IP Network Function

cidrsubnet関数

CIDRを順番に生成する便利な関数です。
CIDR計算が不要となる、かつ計算ミスをしなくても良くなるためSubnetを複数作成する場合に便利です。
構文はcidrsubnet(prefix, newbits, netnum)。
prefix: ベースとなる CIDR(例: "10.0.0.0/16")
newbits: 追加で借りるビット数(例: 6 → /16 が /22 になる)
netnum: 生成するサブネットの番号(0, 1, 2...)
例:
cidrsubnet("10.0.0.0/16", 6, 0) → 10.0.0.0/22
cidrsubnet("10.0.0.0/16", 6, 1) → 10.0.4.0/22

locals {
  base_cidr = "10.0.0.0/16"
  new_bits  = 6
}
output "subnet_cidr1" {
  value = cidrsubnet(local.base_cidr, local.new_bits, 0)
}

output "subnet_cidr2" {
  value = cidrsubnet(local.base_cidr, local.new_bits, 1)
}

output "subnet_cidr3" {
  value = cidrsubnet(local.base_cidr, local.new_bits, 2)
}

output "subnet_cidr4" {
  value = cidrsubnet(local.base_cidr, local.new_bits, 3)
}

Changes to Outputs:
  subnet_cidr1 = "10.0.0.0/22"
  subnet_cidr2 = "10.0.4.0/22"
  subnet_cidr3 = "10.0.8.0/22"
  subnet_cidr4 = "10.0.12.0/22"

Filesystem Function

templatefile 関数

与えられたテンプレートファイルの変数をレンダリングします。

%{ for addr in ip_addrs ~}
backend ${addr}:${port}
%{ endfor ~}
> templatefile("${path.module}/backends.tftpl", { port = 8080, ip_addrs = ["10.0.0.1", "10.0.0.2"] })
backend 10.0.0.1:8080
backend 10.0.0.2:8080

使用例:
templatefileでファイルをレンダリングしつつ、AWS S3にファイルをアップロード、レンダリングした結果が異なれば、変更を検知して再度Uploadする例です。

# template.tftpl
jsonファイルにプレースホルダの変数を準備します。

{
	"message": "Hello, ${name}!",
	"environment": "${env}"
}

# modules.main.tf
templatefile関数でtemplate.tftplをレンダリングします。
さらにaws_s3_objectで etagを定義してあげて、変更を検知するようにします。
この時、レンダリング結果は実ファイルにないので、sourceでなく、contentで読み込んであげます。
さらにetagに渡す際は、md5関数でハッシュを渡します。実ファイルでないので、filemd5関数は利用できません。

locals {
  rendered = templatefile("${path.module}/template.tftpl", {
    name = var.name
    env  = var.env
  })
}

resource "aws_s3_bucket" "this" {
  bucket = "junya0530-etag-test"
}

resource "aws_s3_object" "this" {
  bucket = aws_s3_bucket.this.bucket
  key    = "example.json"

  content = local.rendered
  etag    = md5(local.rendered)
}

# main.tf(module呼び出し)

module "main" {
  source = "./modules"

  name = "junya0530"
  env  = "develop"
}

ここで、moudle呼び出し時のnameやenvといった、テンプレートのプレースホルダ用の変数の値を変えると、以下のように変更を検知できます。

  # module.etag_test.aws_s3_object.this will be updated in-place
  ~ resource "aws_s3_object" "this" {
      ~ content                       = jsonencode(
          ~ {
              ~ environment = "develop" -> "prod"
                # (1 unchanged attribute hidden)
            }
        )
      ~ etag                          = "6921d18413e25e7b8d6ceff4f6db41d2" -> "9045f6e723d986b31a029e0ea06c192d"
        id                            = "junya0530-etag-test/example.json"
        tags                          = {}
      + version_id                    = (known after apply)
        # (24 unchanged attributes hidden)
    }

小ネタ

動的にmapを作成する

locals {
  subnets = [
    "10.0.0.0/24",
    "10.0.1.0/24",
    "10.0.2.0/24",
  ]

  azs = [
    "ap-northeast-1a",
    "ap-northeast-1c",
    "ap-northeast-1d",
  ]
}

locals {
  from_az_to_public_subnet = { for i, subnet in local.subnets :
    local.azs[i] => subnet
  }
}

output "test" {
  value = local.from_az_to_public_subnet
}

Changes to Outputs:
  test = {
      ap-northeast-1a = "10.0.0.0/24"
      ap-northeast-1c = "10.0.1.0/24"
      ap-northeast-1d = "10.0.2.0/24"
    }

Availability Zonesに均等にサブネットを配置する例

locals {
	azs = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
	subnets = [
		"10.0.1.0/24",
		"10.0.2.0/24",
		"10.0.3.0/24",
		"10.0.4.0/24",
		"10.0.5.0/24",
		"10.0.6.0/24"
	]
	number_of_azs = length(local.azs)

	subnet_az_map = [
		for subnet in local.subnets : {
			cidr = subnet
			az   = local.azs[index(local.subnets, subnet) % local.number_of_azs]
		}
	]
}

output "subnet_az_map" {
	value = local.subnet_az_map
}

Changes to Outputs:
  subnet_az_map = [
      {
          az   = "ap-northeast-1a"
          cidr = "10.0.1.0/24"
        },
      {
          az   = "ap-northeast-1c"
          cidr = "10.0.2.0/24"
        },
      {
          az   = "ap-northeast-1d"
          cidr = "10.0.3.0/24"
        },
      {
          az   = "ap-northeast-1a"
          cidr = "10.0.4.0/24"
        },
      {
          az   = "ap-northeast-1c"
          cidr = "10.0.5.0/24"
        },
      {
          az   = "ap-northeast-1d"
          cidr = "10.0.6.0/24"
        },
    ]

Discussion