はじめに
こんにちは。この記事では、Terraformでリソースを管理する際に、コードが冗長になったり可読性が低下したりする課題を解決するためのアプローチを紹介します。
Terraformは非常に便利ですが、同じようなリソースを複数作成する際にコードが長くなりがちです。今回は、for_each
などの機能を活用して、いかにコードをDRY(Don't Repeat Yourself)に保ち、メンテナンスしやすくするかという点に焦点を当てます。
Terraformを使い始めたけれど、コードの書き方に悩んでいる方の参考になれば幸いです。
※記事内のTerraformのコードはAWS前提としています。適宜、読み替えてください。
概要
TerraformでVPCやサブネットを構築する際、リソース定義を一つずつコピー&ペーストしていくと、コードはあっという間に長大化し、メンテナンスが困難になります。例えば、サブネットを6つ(パブリック3つ、プライベート3つ)作るのに、aws_subnet
リソースを6回書くのは避けたいところです。
このリポジトリで紹介するコードは、こうした課題を解決するために、以下のプラクティスを導入しています。
-
設定値の集約とループ処理: サブネットのCIDRブロックやAZといった設定を
locals
やvariables
に集約し、for_each
を使ってリソースをループで一括作成します。これにより、コードの重複を排除し、サブネットの追加や変更が設定値の修正だけで済むようになります
冗長な書き方(Before)
まずは、改善の対象となる冗長なコードを見てみましょう。
以下は、9つのサブネット(public, compute, databaseをそれぞれ3AZに)を定義する例です。resource "aws_subnet"
ブロックが9回も繰り返されており、少し設定を変えたいだけでも多くの箇所を修正する必要があります。
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "test"
}
}
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1a"
cidr_block = "10.0.0.0/24"
tags = {
Name = "test-public-a"
}
}
resource "aws_subnet" "public_c" {
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1c"
cidr_block = "10.0.1.0/24"
tags = {
Name = "test-public-c"
}
}
resource "aws_subnet" "public_d" {
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1d"
cidr_block = "10.0.2.0/24"
tags = {
Name = "test-public-d"
}
}
resource "aws_subnet" "compute_a" {
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1a"
cidr_block = "10.0.3.0/24"
tags = {
Name = "test-compute-a"
}
}
resource "aws_subnet" "compute_c" {
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1c"
cidr_block = "10.0.4.0/24"
tags = {
Name = "test-compute-c"
}
}
resource "aws_subnet" "compute_d" {
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1d"
cidr_block = "10.0.5.0/24"
tags = {
Name = "test-compute-d"
}
}
resource "aws_subnet" "database_a" {
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1a"
cidr_block = "10.0.6.0/24"
tags = {
Name = "test-database-a"
}
}
resource "aws_subnet" "database_c" {
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1c"
cidr_block = "10.0.7.0/24"
tags = {
Name = "test-database-c"
}
}
resource "aws_subnet" "database_d" {
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1d"
cidr_block = "10.0.8.0/24"
tags = {
Name = "test-database-d"
}
}
改善後の書き方(After)
先ほどの冗長なコードを、localsとfor_eachを使って改善します。設定をlocals.tfに、リソース定義をvpc.tfに分離することで、コードの責務が明確になり、メンテナンス性が劇的に向上します
locals.tf
locals.tf:設定値の集約
まず、作成したいサブネットの設定をlocalsブロックにマップとして一元管理します。どのようなサブネットが存在するかが一目でわかり、追加や削除もこのマップを編集するだけで完了します。
locals {
subnets = {
"public-a" = "10.0.0.0/24"
"public-c" = "10.0.1.0/24"
"public-d" = "10.0.2.0/24"
"compute-a" = "10.0.3.0/24"
"compute-c" = "10.0.4.0/24"
"compute-d" = "10.0.5.0/24"
"database-a" = "10.0.6.0/24"
"database-c" = "10.0.7.0/24"
"database-d" = "10.0.8.0/24"
}
}
vpc.tf
vpc.tf:for_eachによるリソースの動的作成
次に、aws_subnetリソースを定義します。for_each = local.subnetsと指定することで、locals.tfで定義したマップの各要素に対してaws_subnetリソースを動的に作成します。9つあったリソース定義が、たった1つのブロックに集約されました。
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "test"
}
}
resource "aws_subnet" "main" {
for_each = local.subnets
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1${substr(each.key, -1, -1)}"
cidr_block = each.value
tags = {
Name = "main-${each.key}"
}
}
作成したリソースの参照方法(応用)
for_eachで作成したリソースは、for式を組み合わせることで、柔軟に参照できます。
これにより、他のTerraformリソースから参照したりすることが可能になります。
特定のkeyのすべての情報
for_eachで作成されたリソース(この例ではaws_subnet.main)はマップとして格納されます。for式とif条件を使えば、キー名("compute"で始まるなど)でフィルタリングしたリソース群をまとめて取得できます。
output "subnets_compute" {
value = { for k, v in aws_subnet.main : k => v if startswith(k, "compute") }
}
例えば、ルートテーブルを定義する場合には以下のような利用の仕方になります。
resource "aws_route_table" "example" {
for_each = { for k, v in aws_subnet.main : k => v }
vpc_id = aws_vpc.main.id
tags = {
Name = "example"
}
}
さらに、特定のルートテーブルにのみNAT Gatewayのルーティングを追加したい場合は以下のような利用の仕方になります。
resource "aws_route" "r" {
for_each = { for k, v in aws_subnet.main : k => v if startswith(k, "compute") }
route_table_id = aws_route_table.example[each.key].id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.example.id
}
特定のkeyの特定の属性を配列で取得
さらに、フィルタリングしたリソースから特定の属性(ここではid)だけを抜き出して、リスト(配列)として出力することも可能です。これにより、他のモジュールやリソースがサブネットIDを簡単に参照できるようになります。
output "subnets_compute_id" {
value = [for subnet in { for k, v in aws_subnet.main : k => v if startswith(k, "compute") } : subnet.id]
}
例えば、RDSのDBサブネットグループを定義する場合には以下のような利用の仕方になります。
resource "aws_db_subnet_group" "default" {
name = "main"
subnet_ids = [for subnet in { for k, v in aws_subnet.main : k => v if startswith(k, "database") } : subnet.id]
tags = {
Name = "My DB subnet group"
}
}