Terraform 実践 #2 VPC ネットワークのコード化 — 2 AZ のサブネット、ルーティング、NAT ゲートウェイ
インフラの最初のリソースは、いつもネットワークです。この後のすべて(EC2、RDS、ECS)がサブネットの上に載るので、ここで決めた構造がシリーズ全体の骨組みになります。今回の目標は教科書どおりの 2 層ネットワークです。インターネットから到達できるパブリックサブネットと、隔離されたプライベートサブネットを 2 つのアベイラビリティゾーン(AZ)にまたがって作り、ルーティングと NAT までつなぎます。VPC の概念自体になじみがなければ、先に AWS 中級シリーズを読んでから戻ってくることをおすすめします。ここではその概念をコードに移すことに集中します。
アドレス設計 — 変数として定義する #
CIDR と AZ の構成を variables に定義します。基礎 #6で扱った for_each を使いやすいように、サブネット構成をマップで表現します。
# variables.tf に追加
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
variable "public_subnets" {
type = map(string)
default = {
"ap-northeast-2a" = "10.0.1.0/24"
"ap-northeast-2c" = "10.0.2.0/24"
}
}
variable "private_subnets" {
type = map(string)
default = {
"ap-northeast-2a" = "10.0.11.0/24"
"ap-northeast-2c" = "10.0.12.0/24"
}
}AZ をキーに、CIDR を値にしました。パブリックを 1 番台、プライベートを 11 番台に離しておくと、IP を見ただけでどの層か読み取れますし、間にサブネットを追加する余地も残ります。
VPC とサブネット #
# network.tf(新規ファイル)
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = { Name = "myapp-${var.env}" }
}
resource "aws_subnet" "public" {
for_each = var.public_subnets
vpc_id = aws_vpc.main.id
availability_zone = each.key
cidr_block = each.value
map_public_ip_on_launch = true
tags = { Name = "myapp-${var.env}-public-${each.key}" }
}
resource "aws_subnet" "private" {
for_each = var.private_subnets
vpc_id = aws_vpc.main.id
availability_zone = each.key
cidr_block = each.value
tags = { Name = "myapp-${var.env}-private-${each.key}" }
}count ではなく for_each を使った理由は、基礎 #6 で見たとおりです。後で AZ を追加しても外しても、他のサブネットが再作成されません。アドレスが aws_subnet.public["ap-northeast-2a"] のように AZ 名で決まるので、plan も読みやすくなります。enable_dns_hostnames はデフォルトでは無効ですが、後で RDS エンドポイントのような DNS 名を使うには有効にしておく必要があります。
インターネットゲートウェイとパブリックルーティング #
パブリックサブネットがパブリックである理由は名前ではなく、ルーティングテーブルにインターネットゲートウェイ(IGW)へのルートがあるからです。その定義をそのままコードに書き写します。
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
}
resource "aws_route_table_association" "public" {
for_each = aws_subnet.public
subnet_id = each.value.id
route_table_id = aws_route_table.public.id
}関連付け(association)の for_each には、変数のマップではなくリソースそのものである aws_subnet.public を渡している点に注目してください。リソースを for_each で作ると結果もマップになるので、そのまま別の for_each の入力になります。each.value がサブネットリソースなので、.id で参照します。
NAT ゲートウェイ — 費用と構造の割り切り #
プライベートサブネットのインスタンスにもアウトバウンドは必要です(パッケージのインストール、外部 API の呼び出し)。その通り道が NAT ゲートウェイで、パブリックサブネットに置かれ、プライベート側のトラフィックを代わりに外へ送り出します。ここで設計判断が 1 つあります。NAT は時間あたりの料金がかかる費用ランキングの常連なので、AZ ごとに置くか 1 つだけ置くかを選ぶ必要があります。AZ ごとに置けば、1 つの AZ に障害が起きても残りの AZ のアウトバウンドは生き残りますが、費用は 2 倍です。実習(そして多くの dev 環境)は 1 つで十分なので、1 つでいきます。この選択は #9 で環境ごとの変数に昇格させます。
resource "aws_eip" "nat" {
domain = "vpc"
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public["ap-northeast-2a"].id
depends_on = [aws_internet_gateway.main]
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
}
resource "aws_route_table_association" "private" {
for_each = aws_subnet.private
subnet_id = each.value.id
route_table_id = aws_route_table.private.id
}depends_on が登場しました。NAT ゲートウェイは IGW がないと実際には動作しませんが、両者の間に直接の参照はありません。基礎 #6で整理した「参照のない順序依存」の、数少ない正当な事例です。AWS provider のドキュメントが推奨するパターンでもあります。
output と検証 #
次回以降が使う値を output に整理します。
# outputs.tf に追加
output "vpc_id" {
value = aws_vpc.main.id
}
output "public_subnet_ids" {
value = [for s in aws_subnet.public : s.id]
}
output "private_subnet_ids" {
value = [for s in aws_subnet.private : s.id]
}apply すると、VPC 1、サブネット 4、IGW 1、NAT 1、EIP 1、ルーティングテーブル 2、関連付け 4 の合わせて 14 個のリソースが作られます。plan で個数を確認し、apply 後に terraform output で ID が出てくることを確かめれば、今回の検証は完了です。NAT が動いていることを忘れずに、今日の実習を終えるなら destroy を忘れないでください。
まとめ #
今回扱った内容です。
- サブネット構成を AZ をキーにしたマップ変数で定義し、for_each で作っています。AZ の追加・削除が他のサブネットに影響しません
- パブリックサブネットの定義は IGW へのルートです。ルーティングテーブルと関連付けまでコードで明示してあります
- for_each で作ったリソースはマップなので、そのまま別のリソースの for_each の入力になります
- NAT ゲートウェイは費用の都合で実習・dev では 1 つ、この選択は #9 で環境ごとの変数になります。IGW との関係は depends_on の正当な事例です
- vpc_id とサブネット ID のリストを output に整理し、次回の入力を準備してあります
次回(#3 EC2 とセキュリティグループ)では、このネットワークの上に最初のコンピューティングを載せます。セキュリティグループのチェーン設計と、user_data による Web サーバーの起動まで進めます。