NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

Terraformのmap型を駆使して、生成されるまで分からないリソースIDを取得する

こんにちは、後藤です。今回はTerraformのお話です。
みなさま、TerraformでAWSインフラを構築するときにmap型を使っていますでしょうか。
当記事ではmap型を使って、生成されるまで分からないAWSリソースのID指定に役立つ方法をご紹介します。

そもそもmap型とは

map型とは値と値の対応を定義する記法で、キーバリュー形式をとっています。

Subnet = {
    PublicSubnet-A = "10.0.0.0/24",
    PublicSubnet-B = "10.0.1.0/24"
}

この場合はサブネット名とCIDRの対応を定義しています。
1つ目のkeyはPublicSubnet-A、valueは10.0.0.0/24となり、
2つ目のkeyはPublicSubnet-B、valueは10.0.1.0/24です。
対応関係がシンプルで分かりやすいですが、keyがサブネット名を指していると説明がないと分かりにくくなります。

一方、同じ内容ですが別の記法もあります。

Subnet = {
  PublicSubnet-1 = {
    subnet_name  = "PublicSubnet-A",
    cidr = "10.0.0.0/24"
  },
  PublicSubnet-2 = {
    subnet_name  = "PublicSubnet-B",
    cidr = "10.0.1.0/24"
  }
}

この場合1つ目のkeyはPublicSubnet-1となり
valueは{subnet_name = "PublicSubnet-A", cidr = "10.0.0.0/24"}となります。
2つ目のkeyも同様な代入がされます。この記法は項目が明示的に記述されているので、わかりやすいですね。


map型を意識しながらサブネットを作成

ではmap型の内容をおさえたところで、mapの使い方を意識しながらサブネットを作ってみます。VPCは作成済みとします。

【フォルダ構成】
フォルダ構成に関しては趣旨ではないため最小限にしています。

.
├─terraform.tf
├─main.tf
└─terrafom.tfvars


【各ファイルの記述内容】
terraform.tfファイルでTerraformのバージョン情報やリージョンの設定をしています。
teraform.tf

terraform {
  required_version = "~> 1.5.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}


terraform.tfvarsファイルで各サブネットの設定値をmap型で定義しています。リソースの追加や変更、削除を行う際はこのファイルを編集するのみになります。
terraform.tfvars

Subnet = {
  PublicSubnet-1 = {
    subnet_name = "PublicSubnet-A",
    cidr = "10.0.0.0/24"
  },
  PublicSubnet-2 = {
    subnet_name = "PublicSubnet-B",
    cidr = "10.0.1.0/24"
  },
  PublicSubnet-3 = {
    subnet_name = "PublicSubnet-C",
    cidr = "10.0.2.0/24"
  },
}


main.tfファイルでは、terrafom.tfvarsファイルから受け取った値を使ってサブネットの生成処理を行います。
main.tf

variables "Subnet" {}

resource "aws_subnet" "PublicSubnet" {
  for_each = var.Subnet
  vpc_id = "vpc-1111111111111"
  cidr_block = each.value.cidr
  availability_zone = "ap-northeast-1a"
  tags = {
    Name = each.value.subnet_name
  }
}

main.tfファイル内のresourceブロックでfor_each文を使っています。
for_each文によって、Subnet変数のkeyを一つずつ読み込んでいます。
Subnet変数の1つ目のkeyはPublicSubnet-1、valueは{subnet_name = "PublicSubnet-A", cidr = "10.0.0.0/24"}となるので、
each.value.cidrと書くことで10.0.0.0/24を代入できます。サブネット名も同様で、each.value.subnet_nameと記載することでPublicSubnet-Aが代入できます。2つ目以降のkeyも同様な代入がされます。

map型を意識しながらEC2を作成

では少し応用させて、作成したサブネットにEC2を2台作成していきます。以下の構成イメージです。EC2の記述は先ほどのファイルに追記していきます。


さて、ここが困るポイントです。
AMIのIDやインスタンスタイプなどはfor_each文で渡せるのですが、サブネットIDは渡せません。なぜならサブネットIDはサブネットが作成されるまで分からないので、書きようがないからです。

main.tf

~~省略~~

variables "EC2" {}

resource "aws_instance" "EC2" {
  for_each = var.EC2
  ami = each.value.ami_id
  instance_type  = each.value.instance_type
  subnet_id = ???
  tags = {
    Name = each.values.ec2_name
  }
}

aws_subnet.PublicSubnet.idと書いてサブネットIDを取り出す手法が一般的ですが、なるべくresourceブロックは編集せずにterrform.tfvarsファイルのみの編集で済ませたいところです。

そこでサブネット名とサブネットIDをmap化し、lookup関数を使います!

実際に書いてみた

まずは実際のファイルの中身を見て頂きましょう。

【各ファイルの記述内容】
terraform.tfvarsファイルでは各valueにsubnet_nameという項目を追記しています。
ロジックは後ほど解説しますが、どのサブネットに作成したいかサブネット名を記載するだけで、そのサブネットIDが代入される仕組みになっています。
terraform.tfvars

~~省略~~

EC2 = {
  instance-1 = {
    ec2_name = "TEST00",
    subnet_name = "PublicSubnet-A",
    ami_id = "ami-079cd5448deeace01",
    instance_type = "t2.micro",
  },
  instance-2 = {
    ec2_name = "TEST01",
    subnet_name = "PublicSubnet-B",
    ami_id = "ami-079cd5448deeace01",
    instance_type = "t2.micro",
  },
}


mian.tfファイル内ではlookup関数を使ってサブネットIDを定義しています。またlocalsブロックを追記しています。このlookup関数とlocalsブロックが肝です。
main.tf

~~省略~~
variables "EC2" {}

resource "aws_instance" "EC2" {
  for_each  = var.EC2
  ami = each.value.ami_id
  instance_type = each.value.instance_type
  subnet_id = lookup(local.PublicSubnet-A_map, eacch.value.subnet_name)
  tags = {
    Name = each.values.ec2_name
  }
}
locals {
  PublicSubnet-A_names  = [for s in aws_subnet.PublicSubnet-A : s.subnet_name]
  PublicSubnet-A_ids = [for s in aws_subnet.PublicSubnet-A : s.id]
  PublicSubnet-A_map = zipmap(local.PublicSubnet-A_names, local.PublicSubnet-A_ids)
}

解説

まずはlookup関数についてです。lookup関数とはlookup(map, hoge)の形式で、mapの中にhogeというkeyがあればそのvalueを返すという意味です。
例えば、このようなmapの場合に

instance_type = {
    "ap-northeast-1" = "t3.medium"
    "ap-northeast-3" = "t2.micro"
}

lookup関数内でap-northeast-1を指定するとt3.mediumという値が取得できます。

lookup(instance_type, "ap-northeast-1")

なにやら条件分岐に使えそうなこの構造を利用します。

次にlocalsブロックについてです。解説しやすいように各処理に番号を振りました。

locals {
  1 PublicSubnet-A_names  = [for s in aws_subnet.PublicSubnet-A : s.subnet_name]
  2 PublicSubnet-A_ids = [for s in aws_subnet.PublicSubnet-A : s.id]
  3 PublicSubnet-A_map = zipmap(local.PublicSubnet-A_names, local.PublicSubnet-A_ids)
}


①の部分でサブネット名を配列化しています。

PublicSubnet-A_names  = [for s in aws_subnet.PublicSubnet-A : s.subnet_name]

outputで表示すると、このようになります。

PublicSubnet-A_names = [
  "PublicSubnet-A",
  "PublicSubnet-B",
  "PublicSubnet-C",
]


次に②の部分で、作成されたサブネットIDを配列化しています。

PublicSubnet-A_ids    = [for s in aws_subnet.PublicSubnet-A : s.id]

outputで表示すると、このようになります。

PublicSubnet-A_ids = [
  "subnet-1111111111",
  "subnet-2222222222",
  "subnet-3333333333",
]


最後に③の部分で、zipmap関数によって配列と配列を組みあわせてmapに変換しています。 つまりサブネット名とサブネットIDをmap化しています。

PublicSubnet-A_map    = zipmap(local.PublicSubnet-A_names, local.PublicSubnet-A_ids)

outputで表示すると、このようになります。

PublicSubnet-A_map = {
  PublicSubnet-A = "subnet-1111111111",
  PublicSubnet-B = "subnet-2222222222",
  PublicSubnet-C = "subnet-3333333333",
}

これでterraform.tfvars上では書けなかったサブネット名とサブネットIDをmap化することができました。
この値とlookup関数を組み合わせます。

ではEC2リソースブロックのサブネットIDの記述に戻って見てみましょう。

  subnet_id = lookup(local.PublicSubnet-A_map, each.value.subnet_name)

「サブネット名とサブネットIDのmapの中から、valueとして渡ってくるサブネット名を探してそのサブネットIDを返す。」という処理になります。

このようにmapとlookup関数を組み合わせることで、サブネット名を指定するだけでそのサブネットIDを取得して、EC2を作成することができます!

まとめ

map型を駆使することで、生成されるまで分からないAWSリソースのIDを指定できます。 サブネットの例に限らず、DNS名やインスタンスIDにも対応できます。
ぜひ試してみてください。また、他に有用な方法があればぜひ教えてください。

執筆者: 後藤 涼太
AWSをメインとするクラウドエンジニア
AWS認定資格 全取得済み
執筆記事:https://tech.nri-net.com/archive/author/r-goto