はじめに
従来、AWSのVPC環境を作成するにあたり、以下の要件を実現するためにはパブリックサブネットを配置する必要がありました。
- インターネット向けのALBの設置
- Webサーバーはプライベートサブネットに置く場合にも、 ALBはパブリックサブネットに配置が必要
- NATゲートウェイの設置
- プライベートサブネット内のインスタンスへのパッチ適用が目的
ですが、前者については、昨年11月にCloudFrontからアクセスする場合に、プライベートサブネットに配置されている内部ELBやEC2インスタンスを「VPCオリジン」として、CloudFrontのオリジンに使用することができるようになりました。また、今年の11月には、このCloudFrontの「VPCオリジン」を他のAWSアカウントにも共有利用できるように機能拡張されています。
一方で後者についても、今年の11月にリージョナルNATゲートウェイという、VPC内に1個プロビジョニングしておけば、複数AZ内のプライベートサブネットから共用利用できるように機能拡張されました。しかも、このリージョナルNATゲートウェイは、インターネットゲートウェイ同様サブネットに配置する必要が無いのです。
これを踏まえると、もはやVPCにパブリックサブネットは要らない時代が到来したものと思います。本当にそうなのか、他のネットワーク関連のAWSサービスの利用ケースも踏まえて考えてみました。
※本論とは全く関係ありませんが、後述の説明時に使用したAWS構成図は、VSCに導入しているAmazon Qフィーチャーを利用し、プロンプトエンジニアリングを駆使して作成したものです。
NATゲートウェイの進化
従来のゾーンナルNATゲートウェイ
従来のゾーンナルNATゲートウェイは、下記の図に見られる通り、AZ毎、かつ、パブリックサブネットに配置し、EIP(Elastic IPアドレス)を各々のNATゲートウェイにアタッチする必要がありました。

また、サブネット単位で以下のルートをもつルートテーブルを作成する必要がありました。
- パブリックサブネットに紐づけるルートテーブル
- 送信先のIPアドレス範囲: 0.0.0.0/0、ターゲット:インターネットゲートウェイ
- 送信先のIPアドレス範囲:VPCのCIDRアドレス、ターゲット:local
- プライベートサブネットに紐づけるルートテーブル
- 送信先のIPアドレス範囲:0.0.0.0/0、ターゲット:AZ内のパブリックサブネットにあるNATゲートウェイ
- 送信先のIPアドレス範囲:VPCのCIDRアドレス、ターゲット:local
AZとサブネットが増える度に、これらのリソースを個別に用意しないといけないので、メンテナンスが大変ですし、NATゲートウェイの数だけコストも増えます。
参考までにゾーンナルNATゲートウェイのプロビジョニングを行うterraformのサンプルコードは下記になります。結構な数のコード行が必要ですね。
main.tf
# Set local variables for availability zones
locals {
az_location1 = "ap-northeast-1a"
az_location2 = "ap-northeast-1c"
}
resource "aws_vpc""test"{
cidr_block = var.vpc_cidr_addr
enable_dns_hostnames = "true"
enable_dns_support = "true"
tags = {
Name = "My-VPC"
}
}
resource "aws_subnet" "pub-sub-01"{
vpc_id = aws_vpc.test.id
cidr_block = var.public_subnet_cidr_01
availability_zone = local.az_location1
map_public_ip_on_launch = true
tags = {
Name = "Public-Subnet-01"
}
depends_on = [ aws_vpc.test ]
}
resource "aws_subnet" "pub-sub-02"{
vpc_id = aws_vpc.test.id
cidr_block = var.public_subnet_cidr_02
availability_zone = local.az_location2
map_public_ip_on_launch = true
tags = {
Name = "Public-Subnet-02"
}
depends_on = [ aws_vpc.test ]
}
resource "aws_subnet" "priv-sub-01"{
vpc_id = aws_vpc.test.id
cidr_block = var.private_subnet_cidr_01
availability_zone = local.az_location1
tags = {
Name = "Private-Subnet-01"
}
depends_on = [ aws_vpc.test ]
}
resource "aws_subnet" "priv-sub-02"{
vpc_id = aws_vpc.test.id
cidr_block = var.private_subnet_cidr_02
availability_zone = local.az_location2
tags = {
Name = "Private-Subnet-02"
}
depends_on = [ aws_vpc.test ]
}
resource "aws_internet_gateway" "gw"{
vpc_id = aws_vpc.test.id
tags = {
Name = "My-IGW"
}
depends_on = [ aws_vpc.test ]
}
resource "aws_route_table" "pub-rtbl-01"{
vpc_id = aws_vpc.test.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
tags = {
Name = "Public-RTBL-01 "
}
depends_on = [ aws_internet_gateway.gw ]
}
resource "aws_route_table_association" "pub-rtbl-assoc-01"{
subnet_id = aws_subnet.pub-sub-01.id
route_table_id = aws_route_table.pub-rtbl-01.id
depends_on = [ aws_route_table.pub-rtbl-01 ]
}
resource "aws_route_table" "pub-rtbl-02"{
vpc_id = aws_vpc.test.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
tags = {
Name = "Public-RTBL-02"
}
depends_on = [ aws_internet_gateway.gw ]
}
resource "aws_route_table_association" "pub-rtbl-assoc-02"{
subnet_id = aws_subnet.pub-sub-02.id
route_table_id = aws_route_table.pub-rtbl-02.id
depends_on = [ aws_route_table.pub-rtbl-02 ]
}
resource "aws_eip" "nat-eip-01"{
domain = "vpc"
depends_on = [ aws_vpc.test ]
}
resource "aws_eip" "nat-eip-02"{
domain = "vpc"
depends_on = [ aws_vpc.test ]
}
resource "aws_nat_gateway" "nat-gw-01"{
allocation_id = aws_eip.nat-eip-01.id
subnet_id = aws_subnet.pub-sub-01.id
tags = {
Name = "My-NAT-GW-01"
}
depends_on = [ aws_eip.nat-eip-01, aws_subnet.pub-sub-01 ]
}
resource "aws_nat_gateway" "nat-gw-02"{
allocation_id = aws_eip.nat-eip-02.id
subnet_id = aws_subnet.pub-sub-02.id
tags = {
Name = "My-NAT-GW-02"
}
depends_on = [ aws_eip.nat-eip-02, aws_subnet.pub-sub-02 ]
}
resource "aws_route_table" "priv-rtbl-01"{
vpc_id = aws_vpc.test.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat-gw-01.id
}
tags = {
Name = "Private-RTBL-01"
}
depends_on = [ aws_nat_gateway.nat-gw-01 ]
}
resource "aws_route_table_association" "priv-rtbl-assoc-01"{
subnet_id = aws_subnet.priv-sub-01.id
route_table_id = aws_route_table.priv-rtbl-01.id
depends_on = [ aws_route_table.priv-rtbl-01 ]
}
resource "aws_route_table" "priv-rtbl-02"{
vpc_id = aws_vpc.test.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat-gw-02.id
}
tags = {
Name = "Private-RTBL-02"
}
depends_on = [ aws_nat_gateway.nat-gw-02 ]
}
resource "aws_route_table_association" "priv-rtbl-assoc-02"{
subnet_id = aws_subnet.priv-sub-02.id
route_table_id = aws_route_table.priv-rtbl-02.id
depends_on = [ aws_route_table.priv-rtbl-02 ]
}
リージョナルNATゲートウェイの登場
今回発表されたリージョナルNATゲートウェイは、下記のようなシンプルな構成になっています。

インターネットゲートウェイ同様にVPCに1個あればよく、かつ、各々のAZ内のプライベートサブネットにあるEC2インスタンスから共用利用することができます。また、サブネットに配置する必要が無いので、パブリックサブネットも不要になります。EIPも、リージョナルNATゲートウェイ作成の過程で自動的にアサインされるようになるので、これまでVPC内に必要だったリソースがかなり削減されます。具体的には、以下が不要になります。
- パブリックサブネット
- パブリックサブネットに紐づけるルートテーブル
- EIP
以下、実際にマネジメントコンソールでリージョナルNATゲートウェイを作成してみたので、作成要領を下記に纏めました。(手順2,3の順は問いません。)
1.VPCを作成
2.インターネットゲートウェイを作成
3.AZにプライベートサブネットを作成
4.リージョナルNATゲートウェイを作成。作成時に以下を選択すること。
- アベイラビリティーモード:Regional
- 接続タイプ:Public
- EIP割り当ての方法:Automatic
リージョナルNATゲートウェイの作成イメージは下記図の通りです。

リージョナルNATゲートウェイの作成が終わると、下記のルートをもつルートテーブルが自動的に作成されます。中身はこれまでパブリックサブネットに紐づけていたルートテーブルです。従来は手動で作成が必要だったのが、それも不要になっています。
- 送信先:0.0.0.0/0、ターゲット:インターネットゲートウェイ
- 送信先:VPCのCIDRアドレス、ターゲット:local
参考までにこちらもリージョナルNATゲートウェイをプロビジョニングするためのサンプルterraformコードになります。コード行も少なくなり、シンプルなコードになりましたね。
main.tf
# Set local variables for availability zones
locals {
az_location1 = "ap-northeast-1a"
az_location2 = "ap-northeast-1c"
}
resource "aws_vpc""test"{
cidr_block = var.vpc_cidr_addr
enable_dns_hostnames = "true"
enable_dns_support = "true"
tags = {
Name = "My-VPC"
}
}
resource "aws_subnet" "priv-sub-01"{
vpc_id = aws_vpc.test.id
cidr_block = var.private_subnet_cidr_01
availability_zone = local.az_location1
tags = {
Name = "Private-Subnet-01"
}
depends_on = [ aws_vpc.test ]
}
resource "aws_subnet" "priv-sub-02"{
vpc_id = aws_vpc.test.id
cidr_block = var.private_subnet_cidr_02
availability_zone = local.az_location2
tags = {
Name = "Private-Subnet-02"
}
depends_on = [ aws_vpc.test ]
}
resource "aws_internet_gateway" "gw"{
vpc_id = aws_vpc.test.id
tags = {
Name = "My-IGW"
}
depends_on = [ aws_vpc.test ]
}
resource "aws_nat_gateway" "natgw"{
availability_mode = "regional"
connectivity_type = "public"
vpc_id = aws_vpc.test.id
tags = {
Name = "My-NATGW"
}
depends_on = [ aws_internet_gateway.gw ]
}
resource "aws_route_table" "priv-rtbl-01"{
vpc_id = aws_vpc.test.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.natgw.id
}
tags = {
Name = "Private-RTBL-01"
}
depends_on = [ aws_nat_gateway.natgw ]
}
resource "aws_route_table_association" "priv-rtbl-assoc-01"{
subnet_id = aws_subnet.priv-sub-01.id
route_table_id = aws_route_table.priv-rtbl-01.id
depends_on = [ aws_route_table.priv-rtbl-01 ]
}
resource "aws_route_table" "priv-rtbl-02"{
vpc_id = aws_vpc.test.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.natgw.id
}
tags = {
Name = "Private-RTBL-02"
}
depends_on = [ aws_nat_gateway.natgw ]
}
resource "aws_route_table_association" "priv-rtbl-assoc-02"{
subnet_id = aws_subnet.priv-sub-02.id
route_table_id = aws_route_table.priv-rtbl-02.id
depends_on = [ aws_route_table.priv-rtbl-02 ]
}
AZを新たに追加する場合も自動的に拡張してくれるようですが、ドキュメントを見ると下記のような記載があります。
「It may take your regional NAT Gateway up to 60 minutes to expand to a new Availability Zone after a resource is instantiated there. Until this expansion is complete, the relevant traffic from this resource is processed across zones by your regional NAT Gateway in one of the existing Availability Zones.」
追加のAZ対応には60分以上かかり、その間は既存のAZで処理がされるみたいです。検証するには時間かかりすぎるので試してみたことは無く影響は定かではないですが、少し気になりました。
ALBもパブリックサブネットが不要な時代へ
CloudFrontの「VPCオリジン」機能の登場
従来はたとえWebサーバーはプライベートサブネットに配置すれども、インターネット向けのALBはパブリックサブネットに置く必要がありました。それが、CloudFrontの「VPCオリジン」機能の登場により、インターネット向けの内部ALBもCloudFrontからのアクセスに制限するのであれば、プライベートサブネットに置くことが可能になりました。今では、この「VPCオリジン」を他のAWSアカウントのCloudFrontディストリビューションとして共有利用することが可能になるよう機能拡張がされています。これにより、「VPCオリジン」の一元管理(どこか1か所で作成すればOK)が期待できます。そこで、下記のような構成で検証してみました。
-
メインAWSアカウント側の構成
- 東京リージョンにVPCを1個作成。VPCは2個のAZを利用。
- 各々のAZにはプライベートサブネットを1個用意し、そこではEC2インスタンス(Webサーバー)を稼働。
- EC2インスタンスは、Ubuntu 24.04.04 LTS Linuxとする。
- インターネットゲートウェイ、リージョナルNATゲートウェイを配置。
- 内部ALB(※)を作成し、ACMの証明書をアタッチ。
- 内部ALBをCloudFrontの「VPCオリジン」とする。これをRAM(Resource Access Manager)で共有。
- Route 53で登録した親ドメイン(cloudlab-km.com)のパブリックホストゾーンを作成。
※内部ALBのリスナーはHTTPS(ポート443)、EC2インスタンスへのヘルスチェックはHTTP(ポート80)通信を行うものとする。


-
別AWSアカウント側の構成
- RAMで共有された「VPCオリジン」に対するCloudFrontディストリビューションを作成。
- CloudFrontディストリビューションにACM証明書をアタッチ。(ACM証明書は、米国東部リージョン(us-east-1)に作成すること。)
- メインAWSアカウント側で登録したドメインのサブドメイン(awsuser01.cloudlab-km.com)に対応するRoute 53パブリックホストゾーンを作成。
今回は事前準備として、VPC作成~ALBの作成までをterraformスクリプトで実施しましたので、参考までにスクリプトの中身を紹介します。スクリプトのツリー構造は下記の通りです。
│ main.tf
│ outputs.tf
│ versions.tf
│
└─modules
├─ALB-Setup
│ main.tf
│ outputs.tf
│ variables.tf
│
├─EC2-Setup
│ main.tf
│ outputs.tf
│ variables.tf
│
└─Network-Setup
main.tf
outputs.tf
variables.tf
スクリプト全体のmain.tf、outputs.tf、versions.tfは下記の通りです。
main.tf
# Configure the AWS provider
provider "aws" {
region = "ap-northeast-1"
}
# Set Local Variables for Availability Zones
locals {
az1 = "ap-northeast-1a"
az2 = "ap-northeast-1c"
}
# Create the VPC Network Setup
module "Network-Setup" {
source = "./modules/Network-Setup"
az1 = local.az1
az2 = local.az2
vpc_cidr_addr = "10.0.0.0/16"
private_subnet_cidr_01 = "10.0.1.0/24"
private_subnet_cidr_02 = "10.0.2.0/24"
}
# Create the private EC2 instances
module "EC2-Setup" {
source = "./modules/EC2-Setup"
private_subnet_id_01 = module.Network-Setup.private_subnet_id_01
private_subnet_id_02 = module.Network-Setup.private_subnet_id_02
ec2_sg_id_01 = module.Network-Setup.ec2_sg_id_01
ec2_sg_id_02 = module.Network-Setup.ec2_sg_id_02
az1 = local.az1
az2 = local.az2
depends_on = [module.Network-Setup]
}
# Create the Application Load Balancer
module "ALB-Setup" {
source = "./modules/ALB-Setup"
vpc_id = module.Network-Setup.vpc_id
private_subnet_id_01 = module.Network-Setup.private_subnet_id_01
private_subnet_id_02 = module.Network-Setup.private_subnet_id_02
alb_sg_id = module.Network-Setup.alb_sg_id
private_ec2_01_id = module.EC2-Setup.private_instance_01_id
private_ec2_02_id = module.EC2-Setup.private_instance_02_id
depends_on = [module.EC2-Setup]
}
outputs.tf
output "alb_dns_name" {
value = module.ALB-Setup.alb_dns_name
}
versions.tf
terraform {
required_providers {
aws={
source = "hashicorp/aws"
version = ">=6.25.0"
}
}
}
versions.tfに指定するプロバイダーのモジュールは最低でも6.24.0以上を指定してください。そうでないと、リージョナルNATゲートウェイにモジュールが対応していない可能性があります。
モジュールは下記の3構成で定義しています。
- Network-Setup:VPCネットワーク定義周りの作成
main.tf
resource "aws_vpc""test"{
cidr_block = var.vpc_cidr_addr
enable_dns_hostnames = "true"
enable_dns_support = "true"
tags = {
Name = "My-VPC"
}
}
resource "aws_subnet" "priv-sub-01"{
vpc_id = aws_vpc.test.id
cidr_block = var.private_subnet_cidr_01
availability_zone = var.az1
tags = {
Name = "Private-Subnet-01"
}
depends_on = [ aws_vpc.test ]
}
resource "aws_subnet" "priv-sub-02"{
vpc_id = aws_vpc.test.id
cidr_block = var.private_subnet_cidr_02
availability_zone = var.az2
tags = {
Name = "Private-Subnet-02"
}
depends_on = [ aws_vpc.test ]
}
resource "aws_internet_gateway" "igw"{
vpc_id = aws_vpc.test.id
tags = {
Name = "My-IGW"
}
depends_on = [ aws_vpc.test ]
}
resource "aws_nat_gateway" "natgw"{
availability_mode = "regional"
connectivity_type = "public"
vpc_id = aws_vpc.test.id
tags = {
Name = "My-NATGW"
}
depends_on = [ aws_internet_gateway.igw ]
}
resource "aws_route_table" "priv-rtbl-01"{
vpc_id = aws_vpc.test.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.natgw.id
}
tags = {
Name = "Private-RTBL-01"
}
depends_on = [ aws_nat_gateway.natgw ]
}
resource "aws_route_table_association" "priv-rtbl-assoc-01"{
subnet_id = aws_subnet.priv-sub-01.id
route_table_id = aws_route_table.priv-rtbl-01.id
depends_on = [ aws_route_table.priv-rtbl-01 ]
}
resource "aws_route_table" "priv-rtbl-02"{
vpc_id = aws_vpc.test.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.natgw.id
}
tags = {
Name = "Private-RTBL-02"
}
depends_on = [ aws_nat_gateway.natgw ]
}
resource "aws_route_table_association" "priv-rtbl-assoc-02"{
subnet_id = aws_subnet.priv-sub-02.id
route_table_id = aws_route_table.priv-rtbl-02.id
depends_on = [ aws_route_table.priv-rtbl-02 ]
}
# Create ALB Security Group
resource "aws_security_group" "alb-sg" {
name = "alb-sg"
description = "Security group for ALB"
vpc_id = aws_vpc.test.id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [var.vpc_cidr_addr]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Create EC2 Security Group
resource "aws_security_group" "ec2-sg-01" {
name = "ec2-sg"
description = "Security group for EC2 instances"
vpc_id = aws_vpc.test.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb-sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "ec2-sg-02" {
name = "ec2-sg-02"
description = "Security group for EC2 instances"
vpc_id = aws_vpc.test.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb-sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Create SSM Endpoint Security Group
resource "aws_security_group" "ssm-endpoint-sg" {
name = "ssm-endpoint-sg"
description = "Security group for SSM Endpoint"
vpc_id = aws_vpc.test.id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.test.cidr_block]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Create VPC Endpoints for SSM
resource "aws_vpc_endpoint" "ssm-endpoint" {
vpc_id = aws_vpc.test.id
service_name = "com.amazonaws.ap-northeast-1.ssm"
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.priv-sub-01.id, aws_subnet.priv-sub-02.id]
security_group_ids = [aws_security_group.ssm-endpoint-sg.id]
private_dns_enabled = true
depends_on = [ aws_security_group.ssm-endpoint-sg ]
}
resource "aws_vpc_endpoint" "ssm-messages-endpoint" {
vpc_id = aws_vpc.test.id
service_name = "com.amazonaws.ap-northeast-1.ssmmessages"
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.priv-sub-01.id, aws_subnet.priv-sub-02.id]
security_group_ids = [aws_security_group.ssm-endpoint-sg.id]
private_dns_enabled = true
depends_on = [ aws_security_group.ssm-endpoint-sg ]
}
resource "aws_vpc_endpoint" "ec2messages-endpoint" {
vpc_id = aws_vpc.test.id
service_name = "com.amazonaws.ap-northeast-1.ec2messages"
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.priv-sub-01.id, aws_subnet.priv-sub-02.id]
security_group_ids = [aws_security_group.ssm-endpoint-sg.id]
private_dns_enabled = true
depends_on = [ aws_security_group.ssm-endpoint-sg ]
}
variables.tf
variable "az1" {
description = "1st Availability Zone"
type = string
}
variable "az2" {
description = "2nd Availability Zone"
type = string
}
variable "vpc_cidr_addr" {
description = "The CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "private_subnet_cidr_01" {
description = "The CIDR block for the first private subnet"
type = string
default = "10.0.1.0/24"
}
variable "private_subnet_cidr_02" {
description = "The CIDR block for the second private subnet"
type = string
default = "10.0.2.0/24"
}
outputs.tf
output "vpc_id" {
value = aws_vpc.test.id
}
output "private_subnet_id_01" {
value = aws_subnet.priv-sub-01.id
}
output "private_subnet_id_02" {
value = aws_subnet.priv-sub-02.id
}
output "alb_sg_id" {
value = aws_security_group.alb-sg.id
}
output "ec2_sg_id_01" {
value = aws_security_group.ec2-sg-01.id
}
output "ec2_sg_id_02" {
value = aws_security_group.ec2-sg-02.id
}
- EC2-Setup:プライベートEC2インスタンスの作成
main.tf
resource "aws_network_interface" "private-eni-01"{
subnet_id = var.private_subnet_id_01
security_groups = [ var.ec2_sg_id_01 ]
}
resource "aws_network_interface" "private-eni-02"{
subnet_id = var.private_subnet_id_02
security_groups = [ var.ec2_sg_id_02 ]
}
# Create EC2 Instances
resource "aws_instance" "private-ec2-01" {
ami = "ami-0b2cd2a95639e0e5b" # Ubuntu Server 24.04 LTS (HVM), SSD Volume Type - for ap-northeast-1
instance_type = "t3.micro"
availability_zone = var.az1
iam_instance_profile = "EC2Instance_Role"
primary_network_interface {
network_interface_id = aws_network_interface.private-eni-01.id
}
user_data = file("~/test/initial.sh")
tags = {
Name = "private-ec2-01"
}
}
resource "aws_instance" "private-ec2-02" {
ami = "ami-0b2cd2a95639e0e5b" # Ubuntu Server 24.04 LTS (HVM), SSD Volume Type - for ap-northeast-1
instance_type = "t3.micro"
availability_zone = var.az2
iam_instance_profile = "EC2Instance_Role"
primary_network_interface {
network_interface_id = aws_network_interface.private-eni-02.id
}
user_data = file("~/test/initial.sh")
tags = {
Name = "private-ec2-02"
}
}
#!/bin/bash
apt update
apt install -y apache2
variables.tf
variable "az1" {
description = "The first availability zone"
type = string
}
variable "az2" {
description = "The second availability zone"
type = string
}
variable "private_subnet_id_01" {
description = "The ID of the first private subnet"
type = string
}
variable "private_subnet_id_02" {
description = "The ID of the second private subnet"
type = string
}
variable "ec2_sg_id_01" {
description = "The security group ID for the first EC2 instance"
type = string
}
variable "ec2_sg_id_02" {
description = "The security group ID for the second EC2 instance"
type = string
}
outputs.tf
output "private_instance_01_id" {
description = "The private Instance ID of the first EC2 instance"
value = aws_instance.private-ec2-01.id
}
output "private_instance_02_id" {
description = "The private Instance ID of the second EC2 instance"
value = aws_instance.private-ec2-02.id
}
- ALB-Setup:ALBの作成
main.tf
# Create the ALB
resource "aws_lb" "my-alb" {
name = "my-alb"
internal = true
load_balancer_type = "application"
security_groups = [var.alb_sg_id]
subnets = [var.private_subnet_id_01, var.private_subnet_id_02]
enable_deletion_protection = false
tags = {
Name = "my-alb"
}
}
# Create an ALB Target Group
resource "aws_lb_target_group" "alb-target-group" {
name = "alb-target-group"
port = 80
protocol = "HTTP"
target_type = "instance"
vpc_id = var.vpc_id
health_check {
path = "/"
protocol = "HTTP"
matcher = "200-399"
interval = 30
timeout = 5
healthy_threshold = 2
unhealthy_threshold = 2
}
tags = {
Name = "alb-target-group"
}
}
resource "aws_lb_target_group_attachment" "alb-target-group-attachment-01" {
target_group_arn = aws_lb_target_group.alb-target-group.arn
target_id = var.private_ec2_01_id
port = 80
}
resource "aws_lb_target_group_attachment" "alb-target-group-attachment-02" {
target_group_arn = aws_lb_target_group.alb-target-group.arn
target_id = var.private_ec2_02_id
port = 80
}
# Read an ACM Certificate in USEast1 (N. Virginia) region
data "aws_acm_certificate" "cert" {
domain = "*.cloudlab-km.com"
statuses = ["ISSUED"]
}
# Create an ALB Listener
resource "aws_lb_listener" "alb-listener" {
load_balancer_arn = aws_lb.my-alb.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = data.aws_acm_certificate.cert.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.alb-target-group.arn
}
tags = {
Name = "alb-listener"
}
}
variables.tf
variable "vpc_id" {
description = "The ID of the VPC where the ALB will be deployed"
type = string
}
variable "private_subnet_id_01" {
description = "The ID of the first Private Subnet"
type = string
}
variable "private_subnet_id_02" {
description = "The ID of the second Private Subnet"
type = string
}
variable "alb_sg_id" {
description = "The security group ID for the ALB"
type = string
}
variable "private_ec2_01_id" {
description = "The ID of the first private EC2 instance"
type = string
}
variable "private_ec2_02_id" {
description = "The ID of the second private EC2 instance"
type = string
}
outputs.tf
output "alb_dns_name" {
description = "The DNS name of the Application Load Balancer"
value = aws_lb.my-alb.dns_name
}
VPCオリジンの作成
メインのAWSアカウント側で内部ALBを対象にVPCオリジンを作成します。


作成には15分近くかかりますので、気長に待ちます。オリジンのステータスが「Deployed」になれば作成完了です。

次に「Share VPC origin」ボタンを押下し、別AWSアカウントへのオリジン共有設定を開始します。

赤枠箇所の「Create resource share」ボタンを押下します。

それぞれ以下の設定を行います。
- Principal type:AWS accountを選択
- AWS account ID:共有先のAWSアカウントIDを入力。
最後に画面右下の「Share resources」ボタンを押下します。

下記画面のように「Successfully shared 1 VPC origin with resource share xxxxx」と出力されれば、VPCオリジンの共有設定は完了です。
他のAWSサービスとかの場合には、RAMのコンソールに遷移した後に、共有設定を行う必要がありますが、VPCオリジンの共有設定作業はRAMコンソールへの遷移が不要なので便利です。
内部ALBにアタッチしているセキュリティーグループの修正
VPCオリジンの作成が終わると、「CloudFront-VPCOrigins-Service-SG」という名前のCloudFront専用セキュリティーグループが作成されています。(インバウンド通信許可はありません。)
このセキュリティーグループからのインバウンド通信許可を、内部ALBにアタッチしているセキュリティーグループに追加します。意外と忘れやすいので注意しましょう。

別AWSアカウントでのCloudFrontディストリビューション作成
共有先のAWSアカウントにて、ディストリビューションを作成します。なお、CloudFrontに月額プランというのが導入されるようになり、月額プランを使うか、従量課金制(Pay as you go)を使うかのどちらかを最初に決める必要があります。


上記を見ていただくとわかると思いますが、「VPCオリジン」をディストリビューションとする場合には、Freeプラン(月0ドル)を選択することはできません。月額プランを使う場合には、Businessプラン(月200ドル)を指定する必要があります。
月額プランを使う場合には、以下の注意点がありますので、検証目的の場合には従量課金制を選択されることをお奨めします。
- 月額プランを契約した場合、ディストリビューションは削除できない。
- 言い換えると、その間はオリジンリソースを削除できない。
- VPCオリジンの場合、ALBやEC2、NATゲートウェイ等も当然削除できなくなる。。
- 削除したい場合には、月額プランをキャンセルすること。
- ただし、キャンセルしても、その月が終わらないと従量課金制には変更されない。
- 翌月に従量課金制に変更されるので、その後にようやくディストリビューションの削除が可能。
今回は、Route 53に作成したサブドメイン「awsuser01.cloudlab-km.com」を使ってアクセスしますので、サブドメインを指定し、「check domain」ボタンを押下します。その後、画面緑色のボックスに出力されているようなメッセージが表示されればOKです。

オリジンには「VPCオリジン」を選択します。

次に共有されたVPCオリジンを指定するために「Browse VPC origins」ボタンを押下します。

メインアカウントより共有されたVPCオリジンを選択し、「Choose」ボタンを押下します。

この後、「VPC origin endpoint」を指定します。これは、プルダウンメニューから選択とかができずに、手入力する必要があります。それ故、メインAWSアカウント側のマネジメントコンソールにて、ALBのDNS名を確認し、それをコピー&ペーストして入力しました。

今回WAFは使用しないため、「セキュリティ保護を有効にしないでください」を選択します。

サブドメイン「awsuser01.cloudlab-km.com」からアクセスできるようにするため、作成済のACM証明書を選択します。

以上の設定を元に作成したCloudFrontディストリビューションのイメージは下記の通りです。


後は、サブドメイン「awsuser01.cloudlab-km.com」のパブリックホストゾーンに、CloudFrontディストリビューションのDNS名(xxxxxxx.cloudfront.net)をタイプAのエイリアスレコードとして設定すれば、下記のようにブラウザでのHTTPSアクセスが可能になります。

※検証を容易にするため、Webサーバーのinit.htmlは、apache2のデフォルト画面として実施しました。
VPC Latticeのサービス
私はこれまでVPC Latticeのサービスについては、パブリックサブネットにEC2インスタンスを配置し、それらのEC2インスタンスをターゲットとしたサービスしか作成したことはありませんでした。しかし、VPC Latticeサービスのドキュメントをよく読むと、「Internal ALB」をターゲットグループに指定したLatticeサービスの作成が可能との記載がありました。
これを踏まえると、VPC Latticeのサービスとして、内部ALBへアクセスするケースにおいても、ALBはプライベートサブネットへの配置が可能であり、パブリックサブネットへの配置は不要であると言えます。
今回、AWS re:Invent2025関連でVPC Lattice周りの機能拡張の発表はありませんでしたが、Latticeサービスに対する理解を深めるため、内部ALBをターゲットグループとしたVPC LatticeサービスにHTTPS通信ができるか否かを、下記のような構成で検証してみました。

-
VPC1
- インターネットゲートウェイ、リージョナルNATゲートウェイを配置
- 1個のプライベートサブネットにEC2インスタンスを稼働
- VPC Latticeのサービスネットワークにプライベート接続するためのサービスネットワークエンドポイント(注1)を配置
-
VPC2
- インターネットゲートウェイ、リージョナルNATゲートウェイを配置
- 2個のAZにプライベートサブネットを作成し、EC2インスタンスを稼働
- EC2インスタンスの負荷分散のための内部ALB(注2)を配置。これをVPC Latticeサービスとし、VPC Latticeサービスネットワークにアタッチする。
-
VPC3
- 2個のAZにプライベートサブネットを作成。1個のAZにRDSインスタンス(postgresql)を稼働。
- リソースゲートウェイを配置。
- RDSインスタンスをリソース設定として、VPC Latticeサービスネットワークにアタッチする。
(注1) VPC1のEC2インスタンスは、サービスネットワークエンドポイントを介して、サービスネットワークにアタッチされているLatticeサービスやリソースにプライベートアクセスが可能。
(注2) HTTPS通信が可能か否かの検証目的のため、CloudFrontのVPCオリジンとして使用した内部ALBと同じ定義を使用。
VPC Lattice及びサービスネットワークエンドポイントの詳細については、今年の7月下旬に実施されたAWSユーザーズグループのネットワーク支部のLT会での発表資料を参照ください。
下記は当時のYouTube動画になります。
Latticeサービスの作成
作成したLatticeサービス、それに紐づくターゲットグループの作成結果イメージは下記の通りです。
内部ALBにアタッチしているセキュリティーグループの修正
VPC1のサービスネットワークエンドポイントを介して、Latticeサービスへアクセスするためには、下記の図の赤枠箇所に見られるようにAWSが提供しているVPC Latticeのマネージド・プレフィックス・リストからのHTTPS通信を許可するルールを追加する必要があります。

サービスネットワークエンドポイントを介したVPC LatticeサービスへのHTTPS通信確認
サービスネットワークにアタッチされているサービスとリソースは、VPC1のサービスネットワークエンドポイントからは下記のように見えています。

- Latticeサービスの場合
- DNS名が「vpce-xxxxxxxx.vpc-lattice-svcs.ap-northeast-1.on.aws」
- VPCプライベートリソースの場合
- DNS名が「vpce-xxxxxxxx.vpc-lattice-rsc.ap-northeast1-on.aws」
サービスネットワークエンドポイントを介したVPC LatticeサービスへのHTTPS通信が可能であるか否かを確認するために「curl -I https://{VPC LatticeサービスのDSN名}」コマンドを実行します。

HTTP/2のステータスコードに「200」が返ってきており、VPC Latticeサービス(プライベートサブネットに跨る内部ALB)へのHTTPS通信が問題なく実行できたことを示しています。これにより、VPC Latticeのサービスとして内部ALBを使用する場合も、ALBをパブリックサブネットに配置する必要はないことが検証できました。
(補足)
上記の2個目のコマンドは、VPC3にあるプライベートリソース(RDSインスタンス)に対して、psqlユーティリティーを使ってアクセス確認を実施した際の結果になります。
※VPCのプライベートリソースの場合は、VPCのエンドポイントのDNS名に加えて、プライベートDNSが付与される(RDSインスタンスのエンドポイントDNS)。これを指定してコマンド実行することも可能。
プライベートサブネットのEC2インスタンスへのアクセス
プライベートサブネットのEC2インスタンスへのアクセスは、いくつかのAWSサービスを使えば対応することが可能です。代表的なサービスとしては、下記が挙げられます。
- EC2 Instance Connect
- Systems Manager Session Manager
- Verified Access
- Client VPN
オンプレミス環境からアクセスするのであれば、Direct ConnectやSite-to-Site VPNという方法もあります。
おわりに
昨年11月に発表されたCloudFrontのVPCオリジン機能の登場から1年後に、このVPCオリジンを他のAWSアカウントへも共有可能になったことで、ALBをパブリックサブネットに置く必要は完全になくなりました。また、機能拡張されたものではないですが、VPC LatticeのサービスとしてALBを使用する場合にも、パブリックサブネットにALBを配置する必要はありません。
更にダメ押しで、リージョナルNATゲートウェイが登場したことにより、もはやVPCにパブリックサブネットは要らない時代の幕開け(明治維新のように日本の夜明けではないですが。)ですね。




