使用 Terraform 實作 Egress Only Gateway
前陣子 AWS 宣布 IPv4 將要從明年開始收費了,為了減少 IPv4 的使用,公司前輩提到 AWS 的 egress only gateway 或許是個不錯的選擇。
什麼是 Egress Only Gateway?
AWS 的 egress only gateway,其功能類似 NAT,讓私有子網域無法被外部直接訪問,但子網域內是可以透過 egress only gateway 連上外部網路,詳細可以看官方文件介紹。
但 egress only gateway 只支援 IPv6,所以如果你時常訪問的服務不支援 IPv6,那麼 egress only gateway 基本上也不適合使用。
好加在目前常見的服務,例如 apt-get 的套件庫或是 docker,都已經開始支援 IPv6。
接下來就簡單示範如何使用 Terraform 建立一個 egress only gateway。
網路架構
我會建立兩個子網,一個是私有子網,另一個是公共子網。私有子網會使用 egress only gateway。每個子網都將擁有一台機器,這些機器會允許 SSH 連線。
由於私有子網中的機器無法直接從外部訪問,我們需要在公共子網中設置一台堡壘伺服器 (bastion server)。我們可以透過這台堡壘伺服器來建立跳板,以方便我們從外部連接到私有子網中的機器。
使用 Terraform 建立 Egress Only Gateway
首先使用 data source 設定 availability zones。
# data.tf
data "aws_availability_zones" "available" {
state = "available"
}
建立 VPC 與私有子網,這個 VPC 會有一個 IPv4 與 IPv6 的 CIDR block。
AWS 的 IPv6 不提供 private address。
# vpc.tf
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
assign_generated_ipv6_cidr_block = true
enable_dns_support = true
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
availability_zone = data.aws_availability_zones.available.names[0]
cidr_block = "10.0.0.0/24"
ipv6_cidr_block = cidrsubnet(aws_vpc.main.ipv6_cidr_block, 8, 1)
assign_ipv6_address_on_creation = true
}
Terraform 中的 cidrsubnet
函式可以用來計算子網路的 CIDR block,它的定義如下:
cidrsubnet(prefix, newbits, netnum)
prefix:為符合規範的 CIDR block notation,例如 10.0.0.0/16
。
newbits:增加 mask 的值,假設設定為 8,如果 prefix 的 mask 是 /16
,那麼計算出來的子網路的 mask 就是 /24
(16+8)。
netnum:網段的索引,從 0 開始。根據前一個例子,如果設定為 1,那麼計算出來的子網路的 CIDR block 就是 10.0.1.0/24
,如果設定為 2,那麼計算出來的子網路的 CIDR block 就是 10.0.2.0/24
。
官方有提供範例,可以自己使用 terraform console
測試一下。
> cidrsubnet("172.16.0.0/12", 4, 2)
172.18.0.0/16
> cidrsubnet("10.1.2.0/24", 4, 15)
10.1.2.240/28
> cidrsubnet("fd00:fd12:3456:7890::/56", 16, 162)
fd00:fd12:3456:7800:a200::/72
接下來建立一個 egress only gateway,並且將它綁定到 VPC 上。
# vpc.tf
# ...
resource "aws_egress_only_internet_gateway" "private" {
vpc_id = aws_vpc.main.id
}
建立一個路由表,並且將它與私有子網進行綁定。
# vpc.tf
# ...
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
ipv6_cidr_block = "::/0"
gateway_id = aws_egress_only_internet_gateway.private.id
}
}
resource "aws_route_table_association" "private" {
subnet_id = aws_subnet.private.id
route_table_id = aws_route_table.private.id
}
設定 ami 的 data resource,這裡我使用 ARM 架構的 Ubuntu OS。
# data.tf
# ...
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "architecture"
values = ["arm64"]
}
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-arm64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
}
建立一個虛擬機器,並且將它放到私有子網中。然後設定 security group,除了允許外部使用 IPv4 對機器進行 SSH 連線,也允許機器可以訪問外部的 IPv6 地址。
# ec2.tf
resource "aws_key_pair" "main" {
key_name = "playground_instance"
public_key = file("~/.ssh/id_ed25519.pub")
}
resource "aws_instance" "main" {
ami = data.aws_ami.ubuntu.id
instance_type = "t4g.small"
key_name = aws_key_pair.main.key_name
availability_zone = data.aws_availability_zones.available.names[0]
network_interface {
device_index = 0
network_interface_id = aws_network_interface.playground.id
}
tags = {
Name = "main"
}
}
# 用來指定 instance 的私有 IP
resource "aws_network_interface" "main" {
subnet_id = aws_subnet.private.id
# 使用 cidrhost 取得 private subnet 底下的 IPv4 與 IPv6 地址
private_ips = [cidrhost(aws_subnet.private.cidr_block, 10)]
ipv6_addresses = [cidrhost(aws_subnet.private.ipv6_cidr_block, 10)]
security_groups = [
aws_security_group.egress_only.id,
aws_security_group.allow_ssh.id,
]
}
resource "aws_security_group" "allow_ssh" {
name = "allow ssh"
vpc_id = aws_vpc.main.id
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "egress_only" {
name = "egress only for ipv6"
vpc_id = aws_vpc.main.id
# ipv6 egress
egress {
from_port = 0
to_port = 0
protocol = "-1"
ipv6_cidr_blocks = ["::/0"]
}
}
因為外部無法直接訪問私有子網底下開的機器,所以可以另外開一個公共子網與堡壘伺服器來連入私有子網底下的機器。
首先建立一個公共子網。
# vpc.tf
# ...
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
availability_zone = data.aws_availability_zones.available.names[0]
cidr_block = "10.0.1.0/24"
ipv6_cidr_block = cidrsubnet(aws_vpc.main.ipv6_cidr_block, 8, 2)
}
resource "aws_internet_gateway" "public" {
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.public.id
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
在公共子網底下建立一台機器,並給予一個 elastic IP。
# ec2.tf
# ...
resource "aws_instance" "bastion" {
ami = data.aws_ami.ubuntu.id
instance_type = "t4g.small"
key_name = aws_key_pair.instance_key_pair.key_name
availability_zone = data.aws_availability_zones.available.names[0]
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.allow_ssh.id]
# set elastic block storage (EBS)
root_block_device {
encrypted = true
volume_type = "gp3"
volume_size = 20
}
tags = {
Name = "bastion"
}
}
resource "aws_eip" "bastion" {
instance = aws_instance.bastion.id
}
透過堡壘伺服器連入私有子網底下的機器後,可以使用以下的指令確認是否可以使用 IPv6 連網。
curl 'https://api64.ipify.org?format=json'
IPv6 好像還不是很普及
當我滿心歡喜地想要將自己 side project 用的環境改為使用 egress only gateway,結果竟然發現 …
GitHub 不支援 IPv6
導致 K3s 也裝不起來 …,只好繼續使用 NAT。