Terraform 実践 #2 VPC ネットワークのコード化 — 2 AZ のサブネット、ルーティング、NAT ゲートウェイ

読了 5分

インフラの最初のリソースは、いつもネットワークです。この後のすべて(EC2、RDS、ECS)がサブネットの上に載るので、ここで決めた構造がシリーズ全体の骨組みになります。今回の目標は教科書どおりの 2 層ネットワークです。インターネットから到達できるパブリックサブネットと、隔離されたプライベートサブネットを 2 つのアベイラビリティゾーン(AZ)にまたがって作り、ルーティングと NAT までつなぎます。VPC の概念自体になじみがなければ、先に AWS 中級シリーズを読んでから戻ってくることをおすすめします。ここではその概念をコードに移すことに集中します。

アドレス設計 — 変数として定義する #

CIDR と AZ の構成を variables に定義します。基礎 #6で扱った for_each を使いやすいように、サブネット構成をマップで表現します。

variables.tf
# 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
# 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)へのルートがあるからです。その定義をそのままコードに書き写します。

network.tf
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 で環境ごとの変数に昇格させます。

network.tf
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
# 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 サーバーの起動まで進めます。

X