[Amazon VPC] Terraformのaws_vpcを読み解いてみた

[Amazon VPC] Terraformのaws_vpcを読み解いてみた

Clock Icon2025.02.21

こんにちは!コンサルティング部のくろすけです!
入社してからAWSの検証作業において、さまざまなリソースを構築しています。
もちろん常設しているとそれだけコストがかかるサービスも多いので、迅速にスクラップ・アンド・ビルドできるようにTerraformを活用しております。

そこで改めて、知らないオプションが結構多いなと思いましたのでTerraform側から読み解いていきたいと思います。
今回はaws_vpcにフォーカスしたいと思います。

前提

使用バージョン

バージョン
Terraform 1.10.4
Terraform AWS Provider v5.87.0

Amazon VPCのTerraformドキュメントはこちら

Amazon VPCとは

Amazon Virtual Private Cloud (Amazon VPC) を使用すると、論理的に隔離されている定義済みの仮想ネットワーク内で AWS リソースを起動できます。仮想ネットワークは、お客様自身のデータセンターで運用されていた従来のネットワークによく似ていますが、AWS のスケーラブルなインフラストラクチャを使用できるというメリットがあります。

Amazon VPC ユーザーガイドより抜粋

改めてですが、Amazon VPCとは仮想ネットワーク環境を作成・管理できるサービスですね。

表記について

以降 Amazon VPC は、Amazon VPC もしくは VPC と記載させていただきます。

aws_vpc リソース

以下に各オプションについて記載します。
※引用文はTerraformドキュメントの機械翻訳になります。

assign_generated_ipv6_cidr_block

(オプション) VPC の /56 プレフィックス長を持つ Amazon 提供の IPv6 CIDR ブロックをリクエストします。IP アドレスの範囲や CIDR ブロックのサイズは指定できません。デフォルトは false です。ipv6_ipam_pool_id と競合します。

VPC内でIPv6を有効/無効化するオプションです。
2021/11にはIPv6のみのサブネットおよびEC2の作成が可能となっているとのこと。
ただし、VPC自体は現在でもデュアルスタックでの作成のみとなっているようです。

Amazon Virtual Private Cloud (VPC) のお客様は、IPv6 のみのサブネットと EC2 インスタンスの作成が可能に
[アップデート] IPv6オンリーのサブネットとEC2インスタンスを作成出来るようになりました! | DevelopersIO

cidr_block

(オプション) VPC の IPv4 CIDR ブロック。CIDR は明示的に設定することも、ipv4_netmask_length を使用して IPAM から取得することもできます。

こちらは言わずもがな IPv4 CIDR ブロック ですね。
上述の通り現状VPCはIPv4なしでは構築できないため、cidr_blockもしくはipv4_netmask_lengthが必須のようです。

enable_dns_hostnames

(オプション) VPC で DNS ホスト名を有効/無効にするブール フラグ。デフォルトは false です。

VPC 内の EC2 インスタンスに対して、DNS ホスト名を有効にするかどうかを制御する設定とのこと。
こちらは有効化の前提として、後述のenable_dns_supporttrueである必要があります。
また、挙動の詳細は下記のようになるようです。

設定値 説明
true パブリック IP アドレスを持つインスタンスに、対応するパブリック DNS ホスト名を割り当てる。
false (デフォルト) パブリック IP アドレスを持つインスタンスに、対応するパブリック DNS ホスト名を割り当てない。

当たり前のように有効化していましたが、このような仕組みになっていたんですね。

参考

enable_dns_support

(オプション) VPC で DNS サポートを有効/無効にするブール フラグ。デフォルトは true です。

Amazon Route 53 Resolver サーバーでDNS解決がされるようです。
確かにコンソールから Amazon Route 53 Resolver を確認したところリソースが作成されていました。(知らなかった...)
ちなみにfalseでも Amazon Route 53 Resolver の作成自体はされていました。
挙動の詳細は下記になります。

設定値 説明
true (デフォルト) Amazon Route 53 Resolver サーバーが、Amazon が提供するプライベート DNS ホスト名を解決できる。
false Amazon Route 53 Resolver サーバーが、Amazon が提供するプライベート DNS ホスト名を解決できない。

参考

enable_network_address_usage_metrics

(オプション) VPC でネットワーク アドレス使用率メト​​リックが有効になっているかどうかを示します。デフォルトは false です。

ネットワークアドレス使用状況(NAU)を計測する設定のようです。

ネットワークアドレス使用状況 (NAU) は、仮想ネットワーク内のリソースに適用されるメトリクスで、VPC のサイズを計画および監視するのに役立ちます。各 NAU ユニットは、VPC のサイズを表す合計に寄与します。

VPC のネットワークアドレスの使用状況 - Amazon Virtual Private Cloudより抜粋

とのこと。単純にプライベートipの使用率という訳では無いようです。

参考

instance_tenancy

(オプション) VPC に起動されたインスタンスのテナンシー オプション。デフォルトは default で、これにより、この VPC で起動された EC2 インスタンスは、EC2 インスタンスの起動時に指定された EC2 インスタンス テナンシー属性を使用します。他の唯一のオプションは、dedicated です。これにより、この VPC で起動された EC2 インスタンスは、起動時に指定されたテナンシー属性に関係なく、専用テナンシー インスタンスで実行されます。これには、リージョンごとに 1 時間あたり 2 ドルの専用料金と、インスタンスごとの時間単位の使用料金がかかります。

こちらはVPC内で起動するインスタンスのテナンシーの設定です。
dedicatedに設定すると強制的に全てのインスタンスが専用テナンシーなるようです。
ちょっと割高になるので、検証ではほぼ使わないかと思います。

ipv4_ipam_pool_id

(オプション) この VPC の CIDR を割り当てるために使用する IPv4 IPAM プールの ID。 IPAM は、AWS リージョンおよびアカウント全体で IP アドレスの割り当て、追跡、トラブルシューティング、監査などの IP アドレス管理ワークフローを自動化するために使用できる VPC 機能です。IPAM を使用すると、AWS 組織全体で IP アドレスの使用状況を監視できます。

初め勘違いしましたが、IPAMはVPCごとではなくアカウントもしくはワークロードに対する管理を提供するサービスのようです。
よって、CIDRの設定はIPAM側に依存するためVPCではcidr_blockもしくはipv4_netmask_lengthを設定となるようです。

参考

ipv4_netmask_length

(オプション) この VPC に割り当てる IPv4 CIDR のネットマスク長。ipv4_ipam_pool_id を指定する必要があります。

こちらは上述の通り、IPAM使用時のネットマスク長ですね。

参考

ipv6_cidr_block

(オプション) IPAM プールから要求する IPv6 CIDR ブロック。明示的に設定することも、ipv6_netmask_length を使用して IPAM から取得することもできます。

IPv6のIPAM使用時のCIDRですね。
IPv6の場合は、CIDRで設定することも可能なようです。

ipv6_cidr_block_network_border_group

(オプション) デフォルトでは、IPv6 CIDR が VPC に割り当てられると、デフォルトの ipv6_cidr_block_network_border_group が VPC のリージョンに設定されます。これを変更して、パブリック IP アドレスのアドバタイズを LocalZones などの特定のネットワーク境界グループに制限することができます。

IPv6アドレスのアドバタイズする領域を制限する用途で使用するようです。
意図しないリージョンなどで使用されることを防ぐ目的があるとのこと。

ipv6_ipam_pool_id

(オプション) IPv6 プールの IPAM プール ID。assign_generated_ipv6_cidr_block と競合します。

こちらはIPv6におけるIPAM使用時のIPAM IDですね。

ipv6_netmask_length

(オプション) IPAM プールから要求するネットマスク長。 ipv6_cidr_block と競合します。allocation_default_netmask_length として IPAM プールが設定されている場合は省略できます。有効な値は 44 から 60 までで、4 ずつ増加します。

こちらはIPv6におけるIPAM使用時のネットマスク長ですね。

tags

(オプション) リソースに割り当てるタグのマップ。プロバイダーの default_tags 構成ブロックが存在するように設定されている場合、一致するキーを持つタグはプロバイダー レベルで定義されたタグを上書きします。

こちらはリソースにアタッチするお馴染みのタグですね。

やってみた

構成

下記の構成をTerraformで作成してみます。
(IPv6 や IPAM はまたの機会に記事にできるといいなと思います。)

Untitled.png

Terraform AWS modulesもありますが、今回は使用していません。
というのも、azs, public_subets, private_subnetsがそれぞれlist型でインデックス管理されていることで変更(特に削除)に弱いため個人的にはあまり好みでは無いためです。

Terraform

今回は下記を自作しました。

main.tf
module "vpc" {
  source     = "../../../modules/vpc"
  name       = "${var.sys_name}-${var.env}-vpc"
  cidr_block = var.vpc_cidr_block

  enable_dns_hostnames = true
  enable_dns_support   = true

  igw = {
    name = "${var.sys_name}-${var.env}-igw"
  }

  public_subnets = {
    pbsb_a = {
      name              = "${var.sys_name}-${var.env}-pbsb-a"
      cidr_block        = var.pbsb_a_cidr_block
      availability_zone = "ap-northeast-1a"
    },
    pbsb_c = {
      name              = "${var.sys_name}-${var.env}-pbsb-c"
      cidr_block        = var.pbsb_c_cidr_block
      availability_zone = "ap-northeast-1c"
    }
  }
  public_subnets_route_table = {
    name = "${var.sys_name}-${var.env}-public-rtb"
  }

  private_subnets = {
    pvsb_a = {
      name              = "${var.sys_name}-${var.env}-pvsb-a"
      cidr_block        = var.pvsb_a_cidr_block
      availability_zone = "ap-northeast-1a"
    },
    pvsb_c = {
      name              = "${var.sys_name}-${var.env}-pvsb-c"
      cidr_block        = var.pvsb_c_cidr_block
      availability_zone = "ap-northeast-1c"
    }
  }
  private_subnets_route_tables = {
    pvsb_a = {
      name = "${var.sys_name}-${var.env}-pvsb-a-rtb"
    },
    pvsb_c = {
      name = "${var.sys_name}-${var.env}-pvsb-c-rtb"
    }
  }

  env = var.env
}
main-variable.tf
variable "sys_name" {
  description = "The system name"
  type        = string
}

variable "vpc_cidr_block" {
  description = "The CIDR block for the VPC"
  type        = string
}

variable "pbsb_a_cidr_block" {
  description = "The CIDR block for the public subnet a"
  type        = string
}

variable "pbsb_c_cidr_block" {
  description = "The CIDR block for the public subnet c"
  type        = string
}

variable "pvsb_a_cidr_block" {
  description = "The CIDR block for the private subnet a"
  type        = string
}

variable "pvsb_c_cidr_block" {
  description = "The CIDR block for the private subnet c"
  type        = string
}

variable "env" {
  description = "The environment"
  type        = string
}
module.tf
################################################################################
# VPC                                                                          #
################################################################################
resource "aws_vpc" "common" {
  assign_generated_ipv6_cidr_block     = var.assign_generated_ipv6_cidr_block
  cidr_block                           = var.cidr_block
  enable_dns_hostnames                 = var.enable_dns_hostnames
  enable_dns_support                   = var.enable_dns_support
  enable_network_address_usage_metrics = var.enable_network_address_usage_metrics
  instance_tenancy                     = var.instance_tenancy
  ipv4_ipam_pool_id                    = var.ipv4_ipam_pool_id
  ipv4_netmask_length                  = var.ipv4_netmask_length
  ipv6_cidr_block                      = var.ipv6_cidr_block
  ipv6_cidr_block_network_border_group = var.ipv6_cidr_block_network_border_group
  ipv6_ipam_pool_id                    = var.ipv6_ipam_pool_id
  ipv6_netmask_length                  = var.ipv6_netmask_length

  tags = {
    Name   = var.name
    Module = "vpc"
    Env    = var.env
  }
}

################################################################################
# Internet Gateway                                                             #
################################################################################
resource "aws_internet_gateway" "common" {
  vpc_id = aws_vpc.common.id

  tags = {
    Name   = var.igw.name
    Module = "vpc"
    Env    = var.env
  }
}

################################################################################
# Public Subnet                                                                #
################################################################################
resource "aws_subnet" "public" {
  for_each                                       = var.public_subnets
  assign_ipv6_address_on_creation                = each.value.assign_ipv6_address_on_creation
  availability_zone                              = each.value.availability_zone
  cidr_block                                     = each.value.cidr_block
  customer_owned_ipv4_pool                       = each.value.customer_owned_ipv4_pool
  enable_dns64                                   = each.value.enable_dns64
  enable_lni_at_device_index                     = each.value.enable_lni_at_device_index
  enable_resource_name_dns_aaaa_record_on_launch = each.value.enable_resource_name_dns_aaaa_record_on_launch
  enable_resource_name_dns_a_record_on_launch    = each.value.enable_resource_name_dns_a_record_on_launch
  ipv6_cidr_block                                = each.value.ipv6_cidr_block
  ipv6_native                                    = each.value.ipv6_native
  map_customer_owned_ip_on_launch                = each.value.map_customer_owned_ip_on_launch
  map_public_ip_on_launch                        = each.value.map_public_ip_on_launch
  outpost_arn                                    = each.value.outpost_arn
  private_dns_hostname_type_on_launch            = each.value.private_dns_hostname_type_on_launch
  vpc_id                                         = aws_vpc.common.id

  tags = {
    Name   = each.value.name
    Module = "vpc"
    Env    = var.env
  }
}

resource "aws_route_table" "public" {
  vpc_id           = aws_vpc.common.id
  propagating_vgws = var.public_subnets_route_table.propagating_vgws

  tags = {
    Name   = var.public_subnets_route_table.name
    Module = "vpc"
    Env    = var.env
  }
}

resource "aws_route" "public" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.common.id
}

resource "aws_route_table_association" "public" {
  for_each       = aws_subnet.public
  route_table_id = aws_route_table.public.id
  subnet_id      = aws_subnet.public[each.key].id
}

################################################################################
# Private Subnet                                                               #
################################################################################
resource "aws_subnet" "private" {
  for_each                                       = var.private_subnets
  assign_ipv6_address_on_creation                = each.value.assign_ipv6_address_on_creation
  availability_zone                              = each.value.availability_zone
  cidr_block                                     = each.value.cidr_block
  customer_owned_ipv4_pool                       = each.value.customer_owned_ipv4_pool
  enable_dns64                                   = each.value.enable_dns64
  enable_lni_at_device_index                     = each.value.enable_lni_at_device_index
  enable_resource_name_dns_aaaa_record_on_launch = each.value.enable_resource_name_dns_aaaa_record_on_launch
  enable_resource_name_dns_a_record_on_launch    = each.value.enable_resource_name_dns_a_record_on_launch
  ipv6_cidr_block                                = each.value.ipv6_cidr_block
  ipv6_native                                    = each.value.ipv6_native
  map_customer_owned_ip_on_launch                = each.value.map_customer_owned_ip_on_launch
  map_public_ip_on_launch                        = each.value.map_public_ip_on_launch
  outpost_arn                                    = each.value.outpost_arn
  private_dns_hostname_type_on_launch            = each.value.private_dns_hostname_type_on_launch
  vpc_id                                         = aws_vpc.common.id

  tags = {
    Name   = each.value.name
    Module = "vpc"
    Env    = var.env
  }
}

resource "aws_route_table" "private" {
  for_each         = var.private_subnets_route_tables
  vpc_id           = aws_vpc.common.id
  propagating_vgws = each.value.propagating_vgws

  tags = {
    Name   = each.value.name
    Module = "vpc"
    Env    = var.env
  }
}

resource "aws_route_table_association" "private" {
  for_each       = var.private_subnets
  route_table_id = aws_route_table.private[each.key].id
  subnet_id      = aws_subnet.private[each.key].id
}
module-variables.tf
################################################################################
# VPC                                                                          #
################################################################################
variable "name" {
  description = "The name of the VPC"
  type        = string
}

variable "assign_generated_ipv6_cidr_block" {
  description = "A boolean flag to enable/disable the assignment of an IPv6 CIDR block"
  type        = bool
  default     = null
}

variable "cidr_block" {
  description = "The CIDR block for the VPC"
  type        = string
  default     = null
}

variable "enable_dns_hostnames" {
  description = "A boolean flag to enable/disable DNS hostnames in the VPC"
  type        = bool
  default     = true
}

variable "enable_dns_support" {
  description = "A boolean flag to enable/disable DNS support in the VPC"
  type        = bool
  default     = null
}

variable "enable_network_address_usage_metrics" {
  description = "A boolean flag to enable/disable network address usage metrics in the VPC"
  type        = bool
  default     = null
}

variable "instance_tenancy" {
  description = "The tenancy of instances launched into the VPC"
  type        = string
  default     = null
}

variable "ipv4_ipam_pool_id" {
  description = "The ID of the IPv4 IPAM pool"
  type        = string
  default     = null
}

variable "ipv4_netmask_length" {
  description = "The netmask length for the IPv4 CIDR block"
  type        = number
  default     = null
}

variable "ipv6_cidr_block" {
  description = "The IPv6 CIDR block"
  type        = string
  default     = null
}

variable "ipv6_cidr_block_network_border_group" {
  description = "The network border group for the IPv6 CIDR block"
  type        = string
  default     = null
}

variable "ipv6_ipam_pool_id" {
  description = "The ID of the IPv6 IPAM pool"
  type        = string
  default     = null
}

variable "ipv6_netmask_length" {
  description = "The netmask length for the IPv6 CIDR block"
  type        = number
  default     = null
}

################################################################################
# Internet Gateway                                                             #
################################################################################
variable "igw" {
  description = "The Internet Gateway"
  type = object({
    name = string
  })
}

################################################################################
# Public Subnet                                                                #
################################################################################
variable "public_subnets" {
  description = "The public subnets"
  type = map(object({
    name                                           = string
    assign_ipv6_address_on_creation                = optional(bool, null)
    availability_zone                              = string
    cidr_block                                     = string
    customer_owned_ipv4_pool                       = optional(string, null)
    enable_dns64                                   = optional(bool, null)
    enable_lni_at_device_index                     = optional(number, null)
    enable_resource_name_dns_aaaa_record_on_launch = optional(bool, null)
    enable_resource_name_dns_a_record_on_launch    = optional(bool, null)
    ipv6_cidr_block                                = optional(string, null)
    ipv6_native                                    = optional(bool, null)
    map_customer_owned_ip_on_launch                = optional(bool, null)
    map_public_ip_on_launch                        = optional(bool, null)
    outpost_arn                                    = optional(string, null)
    private_dns_hostname_type_on_launch            = optional(string, null)
  }))

  validation {
    condition     = length(keys(var.public_subnets)) > 0
    error_message = "At least one public subnet must be defined"
  }
  validation {
    condition = alltrue([
      for key in keys(var.public_subnets) : can(regex("pbsb_[a-d]", key))
    ])
    error_message = "The public subnets must be key prefixed with 'pbsb_' followed by a letter between 'a' and 'd'"
  }
}

variable "public_subnets_route_table" {
  description = "The route table"
  type = object({
    name             = string
    propagating_vgws = optional(list(string), null)
  })
}

################################################################################
# Private Subnet                                                               #
################################################################################
variable "private_subnets" {
  description = "The private subnets"
  type = map(object({
    name                                           = string
    assign_ipv6_address_on_creation                = optional(bool, null)
    availability_zone                              = string
    cidr_block                                     = string
    customer_owned_ipv4_pool                       = optional(string, null)
    enable_dns64                                   = optional(bool, null)
    enable_lni_at_device_index                     = optional(number, null)
    enable_resource_name_dns_aaaa_record_on_launch = optional(bool, null)
    enable_resource_name_dns_a_record_on_launch    = optional(bool, null)
    ipv6_cidr_block                                = optional(string, null)
    ipv6_native                                    = optional(bool, null)
    map_customer_owned_ip_on_launch                = optional(bool, null)
    map_public_ip_on_launch                        = optional(bool, null)
    outpost_arn                                    = optional(string, null)
    private_dns_hostname_type_on_launch            = optional(string, null)
  }))

  validation {
    condition     = length(keys(var.private_subnets)) > 0
    error_message = "At least one private subnet must be defined"
  }
  validation {
    condition = alltrue([
      for key in keys(var.private_subnets) : can(regex("pvsb_[a-d]", key))
    ])
    error_message = "The private subnets must be key prefixed with 'pvsb_' followed by a letter between 'a' and 'd'"
  }
}

variable "private_subnets_route_tables" {
  description = "The route table"
  type = map(object({
    name             = string
    propagating_vgws = optional(list(string), null)
  }))
}

################################################################################
# Others                                                                       #
################################################################################
variable "env" {
  description = "The environment"
  type        = string
  default     = "pvt"
}

実行結果

実際に実行してみます。

tf init
tf plan
tf apply -auto-approve

下記リソースマップを確認したところ問題なさそうです。

  • リソースマップ
    CleanShot 2025-02-21 at 14.43.30@2x.png

Amazon Route 53 リゾルバー も確認してみました。
確かに作成されていますね。

  • Amazon Route 53 リゾルバー
    CleanShot 2025-02-21 at 14.44.22@2x.png

あとがき

今までその都度調査・作成する機会が多かったですが、改めて読み解いてみると引き出しを増やせたような気がしました。
今後も少しずつこちらのシリーズやっていければなと思っております。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.