マルチVPCにおける名前解決の集約構成をTerraformで構築してみた

2023.04.18

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AWS事業本部のイシザワです。

マルチVPC環境での各種リソースの集約をやってみたかったので、手始めに名前解決の集約構成を構築してみます。

また、個人的にTerraformの勉強をしているのでTerraformで構築してみようと思います。

構成要素

Route 53 Resolver

Route 53 ResolverはEC2等のAWSリソースからのDNSクエリに再帰的に応答するDNSサーバーです。VPCの構築時に自動的に作成され、IPアドレスはVPCのネットワークアドレス+2(VPC+2アドレス)となります。例えばVPCのCIDRが10.8.0.0/24の場合は10.8.0.2に接続することでRoute 53 Resolverにアクセスできます。

EC2が名前解決に利用するDNSサーバーはデフォルトでVPC+2アドレスとなっています。

$ resolvectl dns
Global:
Link 2 (ens5): 10.8.0.2
Link 3 (docker0):

このエンドポイントにVPC外からアクセスすることはできません。VPCにRoute 53 Resolver Inbound Endpointを作成することでVPC外からのアクセスを受け付けることができます。

DHCPオプションセット

DHCPオプションセットでEC2インスタンスのネットワーク構成を設定できます。EC2インスタンスが利用するDNSサーバーはデフォルトでVPC+2アドレスとなっていますが、DHCPオプションセットを使えば変更することができます。

Transit Gateway

Transit GatewayはVPCやオンプレミスネットワークの中継ハブです。VPCどうしを接続するために利用します。

VPC IPAM (IP Address Manager)

IPAMはCIDRの払い出しやIPアドレスの管理を行うリソースです。名前解決の集約を行うのに必須ではありませんが、重複するCIDRが無いことを保証できるのがTransit Gatewayを使う上で便利なので使っています。

VPCの構築時にIPAMプールを指定することで、VPCのCIDRをプール内から自動的に払い出して設定してくれます。

IPAMプールは階層構造を作ることができ、今回は以下のような階層構造のIPAMプールを作成します。

  • トップレベルIPAMプール(10.0.0.0/12)
    • 基盤用IPAMプール(10.0.0.0/13)
    • ワークロード用IPAMプール(10.8.0.0/13)

全体構成

以下の設定によりDNSの設定を名前解決用VPCに集約します。

  • 名前解決用VPCにRoute 53 Resolver Inbound Endpointを作成する
  • 他VPCのDHCPオプションセットでDNSサーバーとして↑で作成したエンドポイントを指定する。

全体の構成は以下の図の通りです。

Terraformコード

ソースコードはGitHubに載せています。いくつかピックアップして解説をしていきます。

ソースコード

Transit Gateway

今回の構成だとVPC間の接続はAny-to-Any接続で十分のため、Transit Gatewayのデフォルト関連付けルートテーブルとデフォルト伝播ルートテーブルを有効にしています。

modules/transit_gateway/transit_gateway.tf

resource "aws_ec2_transit_gateway" "main" {
  default_route_table_association = "enable"
  default_route_table_propagation = "enable"
  auto_accept_shared_attachments  = "enable"
  dns_support                     = "enable"

  tags = {
    Name = "${var.system_id}-tgw"
  }
}

Route 53 Resolver Inbound Endpoint

Route 53 Resolver Inbound Endpointにはセキュリティグループを設定する必要があります。名前解決を行うVPCからのtcp/53とudp/53を許可すればOKです。ここではトップレベルIPAMプールのCIDRからのtcp/53とudp/53を許可しています。

エンドポイントはip_addressを2つ以上設定する必要があります。今回は同じサブネット内に設定していますが、高可用性構成の場合は異なるAZのサブネットを指定します。

modules/dns_aggregation/dns.tf

resource "aws_Route 53_resolver_endpoint" "main" {
  name      = "${var.system_id}-dns-endpoint"
  direction = "INBOUND"

  security_group_ids = [
    aws_security_group.allow_dns.id
  ]

  ip_address {
    subnet_id = aws_subnet.private.id
    ip        = local.primary_dns_ip_address
  }

  ip_address {
    subnet_id = aws_subnet.private.id
    ip        = local.secondary_dns_ip_address
  }
}

resource "aws_security_group" "allow_dns" {
  name        = "allow_dns"
  description = "Allow DNS inbound traffic"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 53
    to_port     = 53
    protocol    = "udp"
    cidr_blocks = [var.toplevel_pool_cidr]
  }

  ingress {
    from_port   = 53
    to_port     = 53
    protocol    = "tcp"
    cidr_blocks = [var.toplevel_pool_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "allow_dns"
  }
}

DHCPオプションセット

DHCPオプションセットの設定でRoute 53 Resolver Inbound EndpointのIPアドレスをDNSサーバーに指定します。このDHCPオプションセットをワークロード用VPCと関連付けます。

modules/workload/network.tf

resource "aws_vpc_dhcp_options" "main" {
  domain_name = "${var.region_name}.compute.internal"
  domain_name_servers = [
    var.primary_dns_ip_address,
    var.secondary_dns_ip_address,
  ]

  tags = {
    Name = "${local.prefix}-dhcp-options"
  }
}

resource "aws_vpc_dhcp_options_association" "main" {
  vpc_id          = aws_vpc.main.id
  dhcp_options_id = aws_vpc_dhcp_options.main.id
}

テスト用インスタンス

テスト用インスタンスにはSSMセッションマネージャーでログインするため、それ用のIAMロールを作成してアタッチします。

modules/common_iam/iam_role.tf

data "aws_iam_policy_document" "instance_assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "instance_ssm_role" {
  name               = "${var.system_id}-iamrole-instance-ssm"
  path               = "/"
  assume_role_policy = data.aws_iam_policy_document.instance_assume_role_policy.json
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
  ]
}

resource "aws_iam_instance_profile" "instance_ssm_role" {
  name = "${var.system_id}-instance-profile-instance-ssm"
  role = aws_iam_role.instance_ssm_role.name
}

疎通確認のためのICMPを許可するセキュリティグループをアタッチします。

modules/workload/instance.tf

resource "aws_instance" "test_instance" {
  ami                    = data.aws_ami.latest_amazon_linux_2023.id
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public.id
  iam_instance_profile   = var.instance_profile_name
  vpc_security_group_ids = [aws_security_group.allow_icmp.id]

  tags = {
    Name = "${local.prefix}-test-instance"
  }
}

resource "aws_security_group" "allow_icmp" {
  name        = "allow_icmp"
  description = "Allow ICMP inbound traffic"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = -1
    to_port     = -1
    protocol    = "icmp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "allow_icmp"
  }
}

また、名前解決用VPCのPrivate Hosted Zoneにテスト用インスタンスのDNSレコードを登録します。

modules/workload/instance.tf

resource "aws_Route 53_record" "test_instance" {
  zone_id = var.Route 53_zone_id
  name    = "${local.domain_host_name}.${var.parent_domain_name}"
  type    = "A"
  ttl     = 300
  records = [aws_instance.test_instance.private_ip]
}

動作確認

以下の変数の設定でリソースの構築を行います。

terraform.tfvars

system_id              = "sample"
ipam_topleve_pool_cidr = "10.0.0.0/12"
domain_name            = "example.com"

作成されたテスト用インスタンスsample-workload0-test-instanceにSSMセッションマネージャーでログインします。

DNSサーバーにRoute 53 Resolver Inbound EndpointのIPアドレスが設定されていることが確認できます。

$ resolvectl dns
Global:
Link 2 (ens5): 10.0.0.253 10.0.0.254
Link 3 (docker0):

名前解決用VPCのPrivate Hosted Zoneに、別のワークロードVPCに作成したインスタンスのDNS名が登録されています。 このDNS名が解決できることを確認できます。

$ resolvectl query workload1.example.com
workload1.example.com: 10.8.2.237              -- link: ens5

-- Information acquired via protocol DNS in 4.0ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: network

このDNS名に対してpingが通ることが確認できます。

$ ping workload1.example.com -c 5
PING workload1.example.com (10.8.2.237) 56(84) bytes of data.
64 bytes from ip-10-8-2-237.ap-northeast-1.compute.internal (10.8.2.237): icmp_seq=1 ttl=126 time=0.565 ms
64 bytes from ip-10-8-2-237.ap-northeast-1.compute.internal (10.8.2.237): icmp_seq=2 ttl=126 time=0.395 ms
64 bytes from ip-10-8-2-237.ap-northeast-1.compute.internal (10.8.2.237): icmp_seq=3 ttl=126 time=0.379 ms
64 bytes from ip-10-8-2-237.ap-northeast-1.compute.internal (10.8.2.237): icmp_seq=4 ttl=126 time=0.391 ms
64 bytes from ip-10-8-2-237.ap-northeast-1.compute.internal (10.8.2.237): icmp_seq=5 ttl=126 time=0.384 ms

--- workload1.example.com ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4005ms
rtt min/avg/max/mdev = 0.379/0.422/0.565/0.071 ms

注意

リソース削除時、IPAMプールの削除に失敗することがあります。これはVPCの削除とIPAMプールの割り当てIPアドレス領域の解放との間でタイムラグがあるのが原因です。少し時間を置けば削除できます。

まとめ

マルチVPC環境における名前解決の集約構成をTerraformで構築しました。今回は単独アカウントで行いましたが、マルチアカウントでも同様の構成を取ることができると思います。

次回はVPCエンドポイントの集約構成をTerraformで構築してみたいと思います。

2023/04/20 追記)VPCエンドポイントの集約構成も試してみました。