VPC 다시 배우기 : 서브넷과 Dual-StackRelearning VPC: Subnets and Dual-Stack
서브넷은 격리 경계가 아니다, 그리고 Dual-Stack VPC와 E-IGWSubnets are not isolation boundaries, plus Dual-Stack VPCs and the E-IGW
2019년 AWS Architecture Blog에 올라온 One to Many: Evolving VPC Design을 다시 읽으면서 VPC를 처음부터 정리하고 있습니다. 꽤 오래된 글이지만 기본 개념은 지금도 그대로 유효합니다.
오히려 요즘 기능인 Dual-Stack VPC, IPv6-only 서브넷과 이어서 보니 ‘왜 이렇게 설계했는가’가 더 선명해졌습니다. 이번 글에서는 서브넷의 정체부터 Dual-Stack, 그리고 E-IGW까지 다뤄보겠습니다.
정리하면서 howtodraw.cloud로 그려둔 다이어그램이 있어서, 그 스토리를 구간별로 잘라 해당 문단에 붙였습니다.
서브넷은 격리 경계가 아닙니다
VPC를 처음 배울 때 흔히 하는 오해가 ‘서브넷을 나누면 네트워크가 격리된다’는 생각입니다. 저도 한동안 그렇게 이해하고 있었습니다. 정확히는 이렇습니다.
-
서브넷 : 라우팅 정책을 담는 컨테이너입니다. CIDR 범위(
/16~/28)를 갖고, 하나의 AZ에 속하며, 반드시 하나의 라우트 테이블과 연결됩니다. - 격리 : Security Group(상태 저장 방화벽)이 인스턴스, 정확히는 ENI 단위로 담당합니다.
즉 서브넷 자체에는 격리 기능이 내장되어 있지 않습니다. 이전 글에서도 같은 이야기를 했었습니다.
사실 서브넷은 기본적으로 격리되어있지만, ‘인터넷 게이트웨이로 라우팅이 가능한가’에 따라 Public / Private 서브넷으로 나눠질 뿐입니다.
Public 서브넷 / Private 서브넷이라는 구분도 결국 그 서브넷의 라우트 테이블이 어디를 가리키는가의 문제입니다. AWS 콘솔 어디에도 ‘이 서브넷을 Private으로 만들기’ 같은 체크박스는 없습니다. 우리가 라우트 테이블을 그렇게 구성했기 때문에 그렇게 부르는 것뿐입니다.
그럼 격리는 누가 하나요
| Security Group | Network ACL | |
|---|---|---|
| 적용 단위 | 인스턴스(ENI) | 서브넷 |
| 상태 | 상태 저장(Stateful) | 상태 비저장(Stateless) |
| 규칙 | 허용(Allow)만 가능 | 허용 + 거부(Deny) |
| 평가 | 모든 규칙을 종합 | 번호 순서대로 |
정리하면 서브넷은 ‘어디로 보낼지’를 정하고, SG와 NACL은 ‘무엇을 통과시킬지’를 정합니다. 서로 하는 일이 다릅니다.
예약 주소도 서브넷 단위입니다
서브넷 CIDR 안에서 앞의 4개와 마지막 1개, 총 5개의 IPv4 주소는 AWS가 예약해 사용할 수 없습니다. /28 서브넷을 만들면 16개 중 11개만 쓸 수 있다는 뜻입니다.
$$2^{32-28} - 5 = 16 - 5 = 11$$
IPv6 서브넷도 예약 주소가 있습니다. 다만 브로드캐스트 주소가 없기 때문에 앞의 4개만 예약됩니다.
Dual-Stack VPC : IPv4는 필수, IPv6는 선택
VPC를 만들면 IPv4 CIDR은 무조건 따라붙습니다. 그리고 이 기본 IPv4 CIDR은 제거할 수 없습니다. VPC를 삭제하는 방법밖에 없습니다.
IPv6는 원하면 추가로 붙이는 방식입니다. Amazon이 제공하는 /56 대역을 받거나, 보유한 대역을 BYOIP로 가져올 수 있습니다. 이렇게 IPv6를 추가한 VPC를 Dual-Stack VPC라고 부릅니다.
서브넷은 세 가지로 섞을 수 있습니다
흥미로운 건 서브넷 레벨입니다. Dual-Stack VPC 안에서는 서브넷을 세 가지로 자유롭게 조합할 수 있습니다.
| 서브넷 타입 | IPv4 | IPv6 | 설명 |
|---|---|---|---|
| IPv4-only | O | X | 기존 방식 |
| Dual-Stack | O | O | 둘 다 할당 |
| IPv6-only | X | O | IPv4 주소를 아예 할당받지 않음 |
IPv6-only 서브넷의 인스턴스는 ENI에 IPv4 주소가 전혀 없습니다. VPC는 IPv4를 갖고 있어도, 그 안의 특정 서브넷은 IPv4를 완전히 배제할 수 있다는 뜻입니다.
IPv6 서브넷의 제약
IPv4 서브넷과 달리 IPv6 서브넷 CIDR은 /64로 고정입니다. Prefix 길이를 마음대로 정할 수 없습니다. VPC가 받은 /56 대역을 /64 단위로 쪼개면 256개의 서브넷을 만들 수 있는 셈입니다.
$$2^{64-56} = 2^{8} = 256$$
그 외에 IPv6-only 서브넷을 쓸 때 미리 알아두면 좋은 것들입니다.
- Nitro 기반 인스턴스 타입에서만 지원됩니다.
- IPv4 전용 대상(외부 API 등)과 통신하려면 DNS64 + NAT64 조합이 필요합니다. Route 53 Resolver가 IPv4 주소를 IPv6로 합성해주면, NAT 게이트웨이가 실제 변환을 수행합니다.
- 일부 서비스는 여전히 IPv4 엔드포인트만 제공하므로, 적용 전에 사용 중인 서비스의 IPv6 지원 여부를 확인해야 합니다.
왜 지금 IPv6를 보는가
2024년 2월 1일부터 모든 퍼블릭 IPv4 주소에 시간당 요금이 부과되기 시작했습니다. 사용 중인 주소든 미사용 EIP든 예외가 없습니다. 인스턴스가 수백 대 규모가 되면 무시할 수 없는 금액이 됩니다.
그래서 IPv6-only 서브넷을 실무에서 검토하는 사례가 늘고 있습니다. 예전에는 ‘언젠가 해야 할 일’이었다면, 지금은 청구서에 바로 찍히는 문제가 되었습니다.
E-IGW : IPv6에 NAT가 필요 없는 이유
IPv6는 주소 공간이 방대해서 IPv4처럼 ‘부족한 공인 IP를 나눠 쓰기 위한 NAT’가 필요 없습니다. 인스턴스에 할당되는 IPv6 주소 자체가 이미 Global Unicast, 즉 공인 주소입니다.
그런데 여기서 문제가 하나 생깁니다.
그럼 Private 서브넷처럼 인바운드는 막고 아웃바운드만 허용하고 싶을 때는 뭘 쓰나요?
여기서 등장하는 것이 Egress-Only Internet Gateway(E-IGW) 입니다.
주소를 숨기는 게 아니라 방향을 통제합니다
핵심은 E-IGW가 NAT처럼 주소를 바꾸거나 숨기지 않는다는 점입니다. 인스턴스의 IPv6 주소는 그대로 노출된 채 나갑니다. E-IGW가 하는 일은 연결의 방향을 통제하는 것뿐입니다.
%%{
init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#2a3844',
'lineColor': '#fff',
'primaryTextColor': '#fff',
'primaryBorderColor': '#fff',
'tertiaryColor': '#fff'
}
}
}%%
flowchart LR
EC2A["EC2
Public Subnet"]
EC2B["EC2
Private Subnet"]
IGW["Internet
Gateway"]
EIGW["Egress-Only
Internet Gateway"]
NET(["Internet"])
EC2A <--> IGW
IGW <--> NET
EC2B -- "아웃바운드 개시" --> EIGW
EIGW -- "리턴 트래픽만" --> EC2B
EIGW <--> NET
> 그림 1. IGW는 양방향, E-IGW는 아웃바운드 개시만 허용
- 일반 IGW : 양방향 통신을 허용합니다. (Public 서브넷)
- E-IGW : 상태 저장(Stateful) 게이트웨이로, 아웃바운드로 나간 연결의 리턴 트래픽만 통과시킵니다. 인터넷 쪽에서 먼저 시작하는 연결은 통과하지 못합니다. (Private 서브넷)
그리고 하나의 서브넷에 연결된 라우트 테이블에서 ::/0 경로는 IGW든 E-IGW든 하나만 가리킬 수 있습니다. 어느 쪽을 쓸지는 그 서브넷을 Public으로 설계할지 Private으로 설계할지에 달려 있습니다.
그냥 IGW 쓰고 SG로 막으면 안 되나요?
기술적으로는 가능합니다. IGW를 연결한 뒤 SG에서 인바운드 규칙을 하나도 열지 않으면 결과적으로는 비슷해 보입니다. 하지만 이건 방어가 걸리는 계층이 다릅니다.
| IGW + SG/NACL | E-IGW | |
|---|---|---|
| 인바운드 연결 시도 | 인스턴스까지 도달 → SG/NACL이 평가 후 drop | 게이트웨이에서 차단, 애초에 들어올 경로가 없음 |
| 실패 시나리오 | SG 규칙 하나 잘못 열면 즉시 인바운드 노출 | SG를 열어도 경로가 없어 여전히 차단 |
| 방어 계층 | 인스턴스 레벨 | 라우팅 레벨 |
‘IGW + SG’는 인바운드 차단을 인스턴스 레벨 설정에 전적으로 의존하는 구조입니다. 누군가 실수로 SG에 ::/0 인바운드 규칙을 추가하는 순간 인터넷에 그대로 노출됩니다.
반면 E-IGW는 라우팅 레벨에서 원천적으로 경로를 없애는 구조입니다. IPv4의 Private 서브넷도 원래 ‘NAT 게이트웨이가 라우팅 레벨에서 인바운드를 막아주는’ 설계였다는 걸 생각하면, E-IGW는 그 설계 철학을 IPv6로 그대로 옮긴 것에 가깝습니다.
SG는 인스턴스 레벨, E-IGW는 라우팅 레벨. 서로 다른 계층에서 같은 목적(인바운드 차단)을 이중으로 보장하는 Defense in Depth 구조입니다.
라우트 테이블로 정리하기
지금까지의 이야기를 결국 라우트 테이블 한 장으로 정리하면 이렇게 됩니다. VPC CIDR이 10.0.0.0/16, IPv6 CIDR이 2001:db8:1a00::/56인 경우입니다.
Public 서브넷의 라우트 테이블
| Destination | Target |
|---|---|
| 10.0.0.0/16 | local |
| 2001:db8:1a00::/56 | local |
| 0.0.0.0/0 | igw-id |
| ::/0 | igw-id |
Private 서브넷의 라우트 테이블
| Destination | Target |
|---|---|
| 10.0.0.0/16 | local |
| 2001:db8:1a00::/56 | local |
| 0.0.0.0/0 | nat-gateway-id |
| ::/0 | eigw-id |
IPv4는 NAT 게이트웨이, IPv6는 E-IGW. 역할은 같지만 담당하는 프로토콜이 다릅니다. 이 표만 이해하면 Public / Private 서브넷의 정체는 사실상 끝입니다.
마무리
VPC를 다시 보면서 정리한 핵심은 다음과 같습니다.
- 서브넷은 격리 경계가 아니라 라우팅 컨테이너이며, 격리는 SG와 NACL의 역할입니다.
- VPC는 IPv4가 항상 필수이고, IPv6는 선택적으로 추가합니다. 이를 Dual-Stack VPC라 부릅니다.
- 서브넷 단위로는 IPv4-only / Dual-Stack / IPv6-only를 자유롭게 조합할 수 있습니다.
- IPv6에 NAT가 필요 없는 이유는 주소가 이미 공인 주소이기 때문입니다.
- E-IGW는 주소를 숨기는 게 아니라, 인바운드 연결 시작을 라우팅 레벨에서 차단하는 방향 제어 장치입니다.
다음 편에서는 Public 서브넷과 IGW를 조금 더 깊게 보고, Private 서브넷의 NAT 게이트웨이 HA 구성을 다뤄보겠습니다.
감사합니다! 😊
참고 문헌
I’ve been going over VPCs from scratch while rereading One to Many: Evolving VPC Design, published on the AWS Architecture Blog in 2019. It’s a fairly old post, but its basic concepts still hold today.
If anything, reading it alongside newer features like Dual-Stack VPCs and IPv6-only subnets made ‘why it was designed this way’ even clearer. In this post we’ll cover what a subnet really is, Dual-Stack, and the E-IGW.
While organizing my notes I had a diagram drawn with howtodraw.cloud, so I cut its story into segments and attached each one to the matching paragraph.
A Subnet Is Not an Isolation Boundary
A common misconception when you first learn VPCs is ‘splitting into subnets isolates the network’. I understood it that way for a while myself. More precisely:
-
Subnet: a container for routing policy. It has a CIDR range (
/16~/28), belongs to one AZ, and is always associated with exactly one route table. - Isolation: handled by Security Groups (stateful firewalls), per instance, or more precisely per ENI.
In other words, a subnet has no isolation built into it. I said the same thing in a previous post:
In fact, every subnet starts out isolated; they’re simply split into public and private subnets depending on ‘whether they can route to an internet gateway’.
The split between a Public subnet and a Private subnet ultimately comes down to where that subnet’s route table points. Nowhere in the AWS console is there a checkbox like ‘make this subnet private’. We just call it that because we configured the route table that way.
So who does the isolating?
| Security Group | Network ACL | |
|---|---|---|
| Applies to | Instance (ENI) | Subnet |
| State | Stateful | Stateless |
| Rules | Allow only | Allow + Deny |
| Evaluation | All rules together | In rule-number order |
To sum up, a subnet decides ‘where to send traffic’, while SGs and NACLs decide ‘what to let through’. They do different jobs.
Reserved addresses are per subnet too
Within a subnet’s CIDR, five IPv4 addresses (the first four and the last one) are reserved by AWS and can’t be used. So if you create a /28 subnet, only 11 of its 16 addresses are usable.
$$2^{32-28} - 5 = 16 - 5 = 11$$
IPv6 subnets have reserved addresses too. But since there’s no broadcast address, only the first four are reserved.
Dual-Stack VPC: IPv4 Is Required, IPv6 Is Optional
When you create a VPC, an IPv4 CIDR always comes with it. And this primary IPv4 CIDR can’t be removed. The only way is to delete the VPC.
IPv6 is something you add on if you want it. You can take a /56 block provided by Amazon, or bring your own block with BYOIP. A VPC with IPv6 added like this is called a Dual-Stack VPC.
Subnets come in three flavors you can mix
The interesting part is at the subnet level. Inside a Dual-Stack VPC, you can freely combine three kinds of subnets.
| Subnet type | IPv4 | IPv6 | Description |
|---|---|---|---|
| IPv4-only | O | X | The traditional way |
| Dual-Stack | O | O | Gets both |
| IPv6-only | X | O | Never gets an IPv4 address at all |
Instances in an IPv6-only subnet have no IPv4 address on their ENI at all. Even though the VPC has IPv4, a particular subnet inside it can leave IPv4 out entirely.
Constraints of IPv6 subnets
Unlike IPv4 subnets, an IPv6 subnet CIDR is fixed at /64. You can’t choose the prefix length. Splitting the VPC’s /56 block into /64 pieces gives you 256 possible subnets.
$$2^{64-56} = 2^{8} = 256$$
Other things worth knowing before you use IPv6-only subnets:
- Only Nitro-based instance types are supported.
- To talk to IPv4-only destinations (external APIs and so on), you need the DNS64 + NAT64 combination. Route 53 Resolver synthesizes an IPv6 address from the IPv4 address, and the NAT gateway does the actual translation.
- Some services still offer only IPv4 endpoints, so check whether the services you use support IPv6 before adopting it.
Why look at IPv6 now?
Since February 1, 2024, every public IPv4 address has been billed by the hour. No exceptions, whether the address is in use or an unused EIP. Once you’re running hundreds of instances, the amount is hard to ignore.
That’s why more teams are looking at IPv6-only subnets in practice. It used to be ‘something we’ll have to do someday’; now it shows up right on the bill.
E-IGW: Why IPv6 Doesn’t Need NAT
IPv6 has such a huge address space that, unlike IPv4, it doesn’t need ‘NAT to share scarce public IPs’. The IPv6 address assigned to an instance is already a Global Unicast address — in other words, a public address.
But that raises a problem.
Then what do I use when I want to block inbound and allow only outbound, like a private subnet?
That’s where the Egress-Only Internet Gateway (E-IGW) comes in.
It controls direction, not hides addresses
The key point is that an E-IGW doesn’t change or hide addresses the way NAT does. The instance’s IPv6 address goes out fully visible. All the E-IGW does is control the direction of connections.
%%{
init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#2a3844',
'lineColor': '#fff',
'primaryTextColor': '#fff',
'primaryBorderColor': '#fff',
'tertiaryColor': '#fff'
}
}
}%%
flowchart LR
EC2A["EC2
Public Subnet"]
EC2B["EC2
Private Subnet"]
IGW["Internet
Gateway"]
EIGW["Egress-Only
Internet Gateway"]
NET(["Internet"])
EC2A <--> IGW
IGW <--> NET
EC2B -- "initiates outbound" --> EIGW
EIGW -- "return traffic only" --> EC2B
EIGW <--> NET
> Figure 1. An IGW is bidirectional; an E-IGW only allows outbound-initiated connections
- Regular IGW: allows two-way communication. (Public subnet)
- E-IGW: a stateful gateway that lets through only the return traffic of connections initiated outbound. Connections started from the internet side can’t get through. (Private subnet)
Also, in the route table associated with a subnet, the ::/0 route can point to only one target, IGW or E-IGW. Which one you use depends on whether you’re designing that subnet as public or private.
Can’t I just use an IGW and block with SGs?
Technically, yes. Attach an IGW and open no inbound rules in the SG, and the result looks similar. But the defense sits at a different layer.
| IGW + SG/NACL | E-IGW | |
|---|---|---|
| Inbound connection attempt | Reaches the instance → SG/NACL evaluates and drops it | Blocked at the gateway; there’s no path in to begin with |
| Failure scenario | Open one SG rule by mistake and inbound is exposed immediately | Even if the SG is opened, there’s no path, so it stays blocked |
| Defense layer | Instance level | Routing level |
‘IGW + SG’ relies entirely on instance-level settings for inbound blocking. The moment someone accidentally adds a ::/0 inbound rule to an SG, the instance is exposed to the internet.
The E-IGW, by contrast, removes the path at the routing level altogether. Given that IPv4 private subnets were originally designed so that ‘the NAT gateway blocks inbound at the routing level’, the E-IGW is essentially that same design philosophy carried over to IPv6.
SGs at the instance level, the E-IGW at the routing level: two different layers guaranteeing the same goal (blocking inbound) twice over, a Defense in Depth setup.
Summing It Up in Route Tables
Everything so far boils down to a single set of route tables. Here the VPC CIDR is 10.0.0.0/16 and the IPv6 CIDR is 2001:db8:1a00::/56.
Route table for the public subnet
| Destination | Target |
|---|---|
| 10.0.0.0/16 | local |
| 2001:db8:1a00::/56 | local |
| 0.0.0.0/0 | igw-id |
| ::/0 | igw-id |
Route table for the private subnet
| Destination | Target |
|---|---|
| 10.0.0.0/16 | local |
| 2001:db8:1a00::/56 | local |
| 0.0.0.0/0 | nat-gateway-id |
| ::/0 | eigw-id |
A NAT gateway for IPv4, an E-IGW for IPv6. Same role, different protocol. Understand this table and you’ve essentially figured out what public and private subnets really are.
Wrapping Up
Here are the key takeaways from revisiting VPCs:
- A subnet is a routing container, not an isolation boundary; isolation is the job of SGs and NACLs.
- A VPC always requires IPv4, and IPv6 is added optionally. This is called a Dual-Stack VPC.
- Per subnet, you can freely combine IPv4-only / Dual-Stack / IPv6-only.
- IPv6 doesn’t need NAT because its addresses are already public.
- An E-IGW doesn’t hide addresses; it’s a direction-control device that blocks inbound connection initiation at the routing level.
In the next part, we’ll look more closely at public subnets and the IGW, and cover a highly available NAT gateway setup for private subnets.
Thank you! 😊