온프레미스에서 잘 알던 서브넷과 방화벽 개념이 AWS로 오자 VPC, Route Table, Security Group이라는 이름으로 바뀌었습니다. 새 서비스가 프라이빗 서브넷에 배포됐는데 인터넷 업데이트가 안 되고, 어디를 봐야 할지 막힙니다.
클라우드 네트워크는 이름만 다를 뿐 패킷의 길을 그리는 일입니다. 매핑 관계를 알면 장애 지점을 빠르게 찾을 수 있습니다.
클라우드 네트워크 아키텍처 (AWS VPC)
AWS에 EC2를 띄우고 배포까지 마쳤는데 브라우저에서 아무것도 열리지 않았다. 보안 그룹도 열었고 서비스도 돌고 있는데 — 결국 팀장이 VPC 콘솔을 열어서 한 마디 했다. "인터넷 게이트웨이가 없잖아." 그날 처음 알았다, AWS에서는 그냥 서버를 만든다고 인터넷이 연결되는 게 아니라는 걸. VPC, 서브넷, 라우트 테이블, 인터넷 게이트웨이 — 이 개념들이 서로 어떻게 연결돼야 트래픽이 흐르는지 모르면 AWS 배포는 운에 맡기는 작업이 된다. 온프레미스에서 배운 네트워크 지식이 쓸모없어지는 게 아니다 — VPC의 모든 구성 요소는 스위치, 라우터, 방화벽의 소프트웨어 구현이다. 이 챕터에서 그 매핑을 정확히 짚으면, 다음 배포에서 같은 실수는 반복하지 않는다.
- 1온프레미스 네트워크 개념(VLAN, 라우터, 방화벽)을 AWS VPC 구성 요소에 매핑할 수 있다
- 2VPC CIDR을 설계하고 Public/Private Subnet 분리 구조를 구성할 수 있다
- 3Internet Gateway, Route Table, NAT Gateway의 동작 원리를 설명할 수 있다
- 4Security Group(Stateful)과 Network ACL(Stateless)의 차이와 적용 범위를 구분할 수 있다
- 53-Tier 웹 아키텍처에서 ALB/WAS/DB를 서브넷별로 배치할 수 있다
aws configureaws ec2 create-vpc --cidr-block 10.0.0.0/16 --region ap-northeast-2AWS 계정, IAM 사용자(VPC 권한), AWS CLI v2 이상
온프레미스 → AWS 개념 매핑
온프레미스에서 VLAN, 라우터, 방화벽을 다루던 경험이 있는데, AWS 콘솔을 열면 VPC, Route Table, Security Group이라는 낯선 이름들이 가득합니다. 개념은 같은데 이름이 달라서 처음에는 어디서부터 시작해야 할지 모르게 됩니다. 온프레미스 개념과 AWS 구성 요소의 매핑을 먼저 파악해두면, VPC 설계 문서나 아키텍처 다이어그램이 빠르게 읽히고 장애 진단에서도 "어느 레이어를 봐야 하는가"를 바로 판단할 수 있습니다.
확대
핵심 매핑 테이블
온프레미스에서 쓰던 장비 이름이 AWS에서 어떤 서비스 이름으로 불리는지 먼저 대응 관계를 파악하면, 이후 VPC 설계 문서나 아키텍처 다이어그램이 훨씬 빠르게 읽힙니다.
| 온프레미스 개념 | AWS 대응 개념 | 역할 |
|---|---|---|
| VLAN / 논리 네트워크 분리 | VPC (Virtual Private Cloud) | 완전히 격리된 가상 네트워크 공간 |
| L2 스위치 세그먼트 | Subnet | VPC 내 IP 대역 분할 |
| 방화벽 (Firewall) | Security Group | 인스턴스 레벨 Stateful 방화벽 |
| ACL (Access Control List) | Network ACL | 서브넷 레벨 Stateless 방화벽 |
| 라우터 (Router) | Route Table | 트래픽 경로 결정 |
| 인터넷 게이트웨이 / 공인 IP 연결 | Internet Gateway (IGW) | VPC와 인터넷 연결 |
| NAT 장비 | NAT Gateway | 프라이빗 인스턴스 아웃바운드 인터넷 |
| 전용선 / MPLS | Direct Connect | 온프레미스-AWS 전용 연결 |
| VPN 장비 | Virtual Private Gateway | Site-to-Site VPN 종단 |
VPC: 논리적으로 격리된 내 데이터센터
VPC(Virtual Private Cloud)는 AWS 클라우드 안에 여러분만의 사설 네트워크 공간을 만드는 것입니다. 온프레미스에서 VLAN을 이용해 네트워크를 논리적으로 분리하듯, VPC는 AWS 인프라 안에서 완전히 격리된 네트워크 환경을 제공합니다.
예를 들어 VPC CIDR을 10.0.0.0/16(65,536개 IP)으로 잡으면, 이 범위 안에서 서브넷을 자유롭게 나눠 쓸 수 있습니다.
VPC의 특징:
- 리전(Region) 단위로 생성됩니다 (서울 리전, 도쿄 리전 등)
- 다른 VPC와 완전히 격리되어 있습니다 (기본적으로 통신 불가)
- 하나의 AWS 계정에 여러 VPC를 만들 수 있습니다
- VPC끼리 연결이 필요하면 VPC Peering 또는 Transit Gateway를 사용합니다
Subnet: L2 스위치 세그먼트
온프레미스에서 L2 스위치로 네트워크를 세그먼트로 나누듯, VPC 안에서 서브넷으로 IP 대역을 분할합니다.
VPC 10.0.0.0/16을 다음과 같이 서브넷으로 나눕니다.
| 서브넷 | CIDR | 가용 영역 |
|---|---|---|
| Public Subnet A | 10.0.1.0/24 | 서울 AZ-a |
| Public Subnet B | 10.0.2.0/24 | 서울 AZ-c |
| Private Subnet A | 10.0.11.0/24 | 서울 AZ-a |
| Private Subnet B | 10.0.12.0/24 | 서울 AZ-c |
서브넷은 가용 영역(AZ) 단위로 생성됩니다. 고가용성을 위해 여러 AZ에 동일한 역할의 서브넷을 배포하는 것이 표준 패턴입니다.
Security Group: Stateful 방화벽
온프레미스 방화벽과 가장 큰 차이는 Stateful 동작입니다.
온프레미스 Stateless 방화벽:
→ 인바운드 허용: TCP 443
→ 아웃바운드 허용: TCP 1024-65535 (응답 포트 별도 허용 필요)
AWS Security Group (Stateful):
→ 인바운드 허용: TCP 443
→ 응답 트래픽은 자동 허용 (아웃바운드 규칙 불필요)
Security Group은 인스턴스(EC2)에 붙이는 가상 방화벽입니다. 여러 인스턴스에 동일한 Security Group을 적용할 수 있어, 역할별로 그룹을 만들어 관리합니다 (web-sg, app-sg, db-sg 등).
Route Table: 라우터의 소프트웨어 구현
온프레미스 라우터의 라우팅 테이블처럼, AWS Route Table은 트래픽의 다음 경유지를 결정합니다.
Public Subnet Route Table:
Destination Target
10.0.0.0/16 local ← VPC 내부 통신
0.0.0.0/0 igw-xxxxxxxx ← 인터넷으로 (IGW)
Private Subnet Route Table:
Destination Target
10.0.0.0/16 local ← VPC 내부 통신
0.0.0.0/0 nat-xxxxxxxx ← 아웃바운드만 인터넷으로 (NAT GW)
라우팅 테이블에 0.0.0.0/0 → igw-xxxx 경로가 있느냐 없느냐가 Public/Private Subnet을 구분하는 핵심입니다.
AWS VPC 3-Tier 아키텍처 구조
EC2에 서비스를 배포했는데 웹 서버에서 직접 DB를 공인 IP로 접근하고, DB 포트가 인터넷에 노출되어 있습니다. 빠르게 만들다 보니 서브넷 구분 없이 전부 Public에 넣은 것입니다. 보안 감사에서 이 구조가 지적되면 VPC를 전면 재설계해야 합니다. 3-Tier 구조를 처음부터 이해하고 설계하면 이 수정 비용을 피할 수 있고, 각 계층이 왜 다른 서브넷에 있어야 하는지 논리적으로 설명할 수 있게 됩니다.
실무에서 가장 많이 쓰이는 VPC 설계 패턴은 3-Tier 구조입니다. 웹, 애플리케이션, 데이터베이스 계층을 각각 다른 서브넷에 배치하여 네트워크 격리를 구현합니다.
3-Tier VPC 아키텍처 전체 그림
확대
Public Subnet: 인터넷과 직접 통신 가능한 영역
조건: Route Table에 0.0.0.0/0 → igw-xxxx 경로 존재
Public Subnet에 위치한 EC2 인스턴스에 Elastic IP(공인 IP)를 할당하면, 인터넷에서 직접 접근 가능합니다.
Public Subnet에 배치하는 것들:
- Application Load Balancer (ALB)
- NAT Gateway
- Bastion Host (점프 서버)
- 공인 IP가 필요한 웹 서버
# 실습 디렉토리 준비
mkdir -p /tmp/networking/part6/exam_31 && cd /tmp/networking/part6/exam_31
# Public Subnet Route Table 예시
$ aws ec2 describe-route-tables --route-table-ids rtb-public
Routes:
- DestinationCidrBlock: 10.0.0.0/16
GatewayId: local
- DestinationCidrBlock: 0.0.0.0/0
GatewayId: igw-0abc1234def56789 ← 이것이 있으면 Public
- VPC state — available 확인—aws ec2 describe-vpcs 출력에서 "State": "available"인지 먼저 확인 — pending 상태면 아직 프로비저닝 중, available이어야 서브넷과 라우팅 설정이 가능
- Subnet MapPublicIpOnLaunch 필드—"MapPublicIpOnLaunch": true면 해당 서브넷에 EC2를 띄울 때 자동으로 공인 IP 할당 — false인데 공인 IP가 필요하면 인스턴스 설정에서 Enable 또는 Elastic IP를 수동 연결해야 함
- 보안그룹 inbound 룰 — 포트와 출발지 조합—aws ec2 describe-security-groups에서 IpPermissions 배열의 FromPort/ToPort와 CidrIpv4를 동시에 확인 — 포트가 맞아도 CidrIpv4가 0.0.0.0/0이 아닌 특정 IP 대역이면 해당 대역 외 접근은 차단됨
Private Subnet: 인터넷에서 직접 접근 불가한 영역
조건: Route Table에 IGW 경로 없음
Private Subnet의 EC2 인스턴스는 공인 IP를 할당해도 외부에서 접근할 수 없습니다. 라우팅 경로 자체가 없기 때문입니다.
Private Subnet에 배치하는 것들:
- 애플리케이션 서버 (WAS)
- 데이터베이스 (RDS)
- 내부 마이크로서비스
Private → 인터넷 아웃바운드 경로: Private 서브넷은 직접 인터넷에 접근할 수 없고 NAT Gateway를 거쳐야 합니다.
Private EC2 → NAT Gateway (Public Subnet) → IGW → 인터넷
# Private Subnet Route Table 예시
$ aws ec2 describe-route-tables --route-table-ids rtb-private
Routes:
- DestinationCidrBlock: 10.0.0.0/16
GatewayId: local
- DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: nat-0abc1234def56789 ← IGW 대신 NAT GW
NAT Gateway: 프라이빗 인스턴스의 아웃바운드 인터넷 창구
NAT Gateway는 온프레미스 NAT 장비와 동일한 역할을 합니다. Private Subnet 인스턴스의 사설 IP를 공인 IP로 변환하여 인터넷으로 내보냅니다.
NAT Gateway 배치 규칙:
- 반드시 Public Subnet에 배치해야 합니다
- Elastic IP가 필요합니다 (고정 공인 IP)
- Private Subnet의 Route Table에서 NAT GW를 가리켜야 합니다
비용 주의사항: NAT Gateway는 시간당 요금 + 데이터 처리 요금이 발생합니다. 개발/테스트 환경에서 비용을 아끼려면 사용하지 않을 때 삭제를 고려하세요.
Internet Gateway (IGW): VPC의 인터넷 관문
IGW는 VPC와 인터넷 사이의 게이트웨이입니다. 온프레미스의 인터넷 연결 장비(라우터 + 공인 IP)에 해당합니다.
IGW 특징:
- VPC당 하나만 연결 가능
- 수평 확장(Scale-out) 가능한 완전 관리형 서비스
- 가용성은 AWS가 보장 (단일 장애점 없음)
- IGW 자체는 무료 (데이터 전송 요금은 별도)
인스턴스가 보낸 패킷이 목적지에 닿기까지 — 서브넷 배치가 경로를 가른다
서브넷을 퍼블릭/프라이빗으로 나눠 인스턴스를 배치했는데, 막상 "이 인스턴스가 보낸 패킷이 실제로 어디를 거쳐 나가는가"를 그려보라면 막힙니다. 같은 VPC 안 다른 서브넷으로 갈 때와 인터넷으로 나갈 때의 경로가 다르고, 그 인터넷 경로마저 퍼블릭이면 IGW로 직행, 프라이빗이면 NAT를 한 번 더 거칩니다. 이 경로가 머릿속에 있어야 "왜 프라이빗은 나가고 퍼블릭은 안 되지", "왜 서브넷 안에서는 되는데 밖으로는 안 나가지"를 단계로 좁힐 수 있습니다. 인스턴스에서 나간 패킷은 아래 여섯 단계를 지납니다.
[EC2 인스턴스] 10.0.11.20 (프라이빗 서브넷-a) 에서 패킷 송신
│
① 목적지 분류 내 VPC(10.0.0.0/16) 안인가, 밖(0.0.0.0/0)인가
│
② 라우트 테이블 조회 가장 구체적(longest-prefix) 경로 매칭
│ ├─ 10.0.0.0/16 → local → VPC 내부, ⑤로 직행
│ └─ 0.0.0.0/0 → 서브넷 종류가 타깃을 가름
│
③ 퍼블릭/프라이빗 분기
│ ├─ 퍼블릭 서브넷: 0.0.0.0/0 → IGW (공인 IP로 직접 인터넷)
│ └─ 프라이빗 서브넷: 0.0.0.0/0 → NAT GW (퍼블릭 서브넷의 NAT 경유)
│
④ (프라이빗만) NAT GW가 사설 IP를 자기 공인 IP로 SNAT → IGW → 인터넷
│
⑤ 보안그룹(인스턴스)·NACL(서브넷) 통과 나갈 때/도착할 때 각각 평가
│
⑥ 목적지 도달 VPC 내부면 대상 서브넷으로, 외부면 IGW 밖으로
▼
[목적지] 다른 서브넷의 인스턴스 · 인터넷의 외부 API
각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:
| 단계 | 하는 일 | 여기서 막히면 |
|---|---|---|
| ① 목적지 분류 | 출발 인스턴스가 목적지 IP를 자기 VPC 대역 안(local)인지 밖(0.0.0.0/0)인지로 경로 후보를 나눔 | 여기 자체는 막힘이 없음 — 다음 라우트 매칭 결과로 드러남 |
| ② 라우트 테이블 조회 | 서브넷에 연결된 라우트 테이블에서 목적지에 가장 구체적으로 맞는 경로 선택. VPC 내부는 항상 local, 그 외는 0.0.0.0/0 기본 경로 | 0.0.0.0/0 경로가 아예 없음 → 나갈 길이 없어 아웃바운드 전부 타임아웃 (프라이빗의 NAT 경로 누락이 단골) |
| ③ 퍼블릭/프라이빗 분기 | 서브넷의 0.0.0.0/0이 IGW를 가리키면 퍼블릭·NAT GW를 가리키면 프라이빗. 이 한 줄이 노출 여부를 가름 | 퍼블릭인데 인스턴스에 공인 IP 없음 → IGW 경로가 있어도 응답이 못 돌아옴 / 프라이빗인데 NAT 경로 없음 → 아웃바운드 실패 |
| ④ NAT SNAT (프라이빗만) | 퍼블릭 서브넷의 NAT GW가 프라이빗 인스턴스의 사설 IP를 NAT의 공인 IP(EIP)로 치환해 IGW로 내보냄. 응답은 역치환돼 돌아옴 | NAT GW가 프라이빗 서브넷에 잘못 배치·EIP 미연결 → SNAT 불가로 아웃바운드 실패 |
| ⑤ SG·NACL 평가 | 인스턴스 레벨 보안그룹(stateful·허용전용)과 서브넷 레벨 NACL(stateless·허용/거부)을 나갈 때·도착할 때 각각 통과 | SG에 대상 포트 미허용, 또는 NACL 응답용 ephemeral 포트(1024-65535) 누락 → "나가는데 응답이 없다" |
| ⑥ 목적지 도달 | VPC 내부 목적지는 local 경로로 대상 서브넷에 직접, 외부 목적지는 IGW를 지나 인터넷으로 | 대상 서브넷 NACL/SG가 출발지 대역 미허용 → VPC 내부인데 서브넷 간 통신 실패 |
즉 "이 인스턴스가 왜 못 나가나"는 이 여섯 단계 중 어디서 끊겼는지의 문제입니다. 순서는 항상 라우트(②③)를 먼저, 그다음 방화벽(⑤) 입니다 — aws ec2 describe-route-tables로 그 서브넷의 0.0.0.0/0 타깃이 IGW인지 NAT인지(없는지)를 먼저 확인하고, 경로가 멀쩡한데 막히면 그때 SG·NACL을 봅니다. VPC 내부만 되고 밖이 안 되면 ②③(라우팅), 밖은 되는데 특정 서브넷만 안 되면 ⑤⑥(SG·NACL)로 범위가 좁혀집니다. 퍼블릭/프라이빗은 서브넷 이름이 아니라 이 0.0.0.0/0 경로의 타깃이 결정한다는 점이 핵심입니다.
AWS Management Console이 없어도 CLI로 VPC 구조 전체를 파악할 수 있습니다. 실무에서 원인 분석 시 CLI가 훨씬 빠릅니다.
사전 준비
# AWS CLI 설치 확인
aws --version
# aws-cli/2.x.x Python/3.x.x ...
# 자격 증명 설정 확인
aws sts get-caller-identity
# {
# "Account": "123456789012",
# "Arn": "arn:aws:iam::123456789012:user/myuser"
# }
# 리전 설정 (서울)
export AWS_DEFAULT_REGION=ap-northeast-2
Step 1: VPC 목록 조회
# 현재 계정의 모든 VPC 조회
aws ec2 describe-vpcs \
--query 'Vpcs[*].{VpcId:VpcId,CIDR:CidrBlock,Name:Tags[?Key==`Name`]|[0].Value,Default:IsDefault}' \
--output table
# 출력 예시:
# ---------------------------------------------------------------
# | DescribeVpcs |
# +---------------+-----------------+-----------------+---------+
# | CIDR | Default | Name | VpcId |
# +---------------+-----------------+-----------------+---------+
# | 10.0.0.0/16 | False | prod-vpc | vpc-abc|
# | 172.31.0.0/16| True | None | vpc-xyz|
# +---------------+-----------------+-----------------+---------+
Step 2: 서브넷 구조 파악
# VPC_ID 변수 설정
VPC_ID="vpc-0abc1234def56789"
# 해당 VPC의 서브넷 목록
aws ec2 describe-subnets \
--filters "Name=vpc-id,Values=${VPC_ID}" \
--query 'Subnets[*].{SubnetId:SubnetId,CIDR:CidrBlock,AZ:AvailabilityZone,Name:Tags[?Key==`Name`]|[0].Value,MapPublicIP:MapPublicIpOnLaunch}' \
--output table
# MapPublicIpOnLaunch=True이면 Public Subnet 가능성 높음
# (Route Table 확인이 최종 판단 기준)
Step 3: Route Table로 Public/Private 판별
# 서브넷별 Route Table 연결 확인
aws ec2 describe-route-tables \
--filters "Name=vpc-id,Values=${VPC_ID}" \
--query 'RouteTables[*].{RTId:RouteTableId,Routes:Routes[*].{Dest:DestinationCidrBlock,Target:GatewayId || NatGatewayId}}' \
--output json
# IGW 경로 있는 Route Table → Public
# NAT GW 경로 있는 Route Table → Private (아웃바운드 가능)
# 0.0.0.0/0 경로 없는 Route Table → 완전 격리 Private
# 특정 서브넷의 Route Table 직접 확인
SUBNET_ID="subnet-0abc1234"
aws ec2 describe-route-tables \
--filters "Name=association.subnet-id,Values=${SUBNET_ID}" \
--query 'RouteTables[*].Routes'
Step 4: Security Group 규칙 확인
# 특정 Security Group 규칙 상세 조회
SG_ID="sg-0abc1234def56789"
aws ec2 describe-security-groups \
--group-ids ${SG_ID} \
--query 'SecurityGroups[0].{
Name:GroupName,
Inbound:IpPermissions[*].{
Protocol:IpProtocol,
FromPort:FromPort,
ToPort:ToPort,
CIDR:IpRanges[*].CidrIp
},
Outbound:IpPermissionsEgress[*].{
Protocol:IpProtocol,
FromPort:FromPort,
ToPort:ToPort,
CIDR:IpRanges[*].CidrIp
}
}' \
--output json
Step 5: EC2 인스턴스 네트워크 정보 확인
# 인스턴스의 VPC/서브넷/SG/IP 정보 한번에 확인
INSTANCE_ID="i-0abc1234def56789"
aws ec2 describe-instances \
--instance-ids ${INSTANCE_ID} \
--query 'Reservations[0].Instances[0].{
InstanceId:InstanceId,
State:State.Name,
PrivateIP:PrivateIpAddress,
PublicIP:PublicIpAddress,
VpcId:VpcId,
SubnetId:SubnetId,
SecurityGroups:SecurityGroups[*].GroupId
}' \
--output json
# PublicIP가 null이고 SubnetId가 Private Subnet이면
# 이 인스턴스는 인터넷에서 직접 접근 불가
Private Subnet의 EC2 인스턴스에서 yum update나 패키지 설치가 필요할 때 NAT Gateway를 올바르게 설정하는 절차입니다.
NAT Gateway 생성 전 체크리스트
# 1. Public Subnet ID 확인 (NAT GW는 여기에 배치)
aws ec2 describe-subnets \
--filters "Name=vpc-id,Values=${VPC_ID}" \
"Name=tag:Name,Values=*public*" \
--query 'Subnets[*].{SubnetId:SubnetId,Name:Tags[?Key==`Name`]|[0].Value}'
# 2. Elastic IP 할당 (NAT GW에 연결할 공인 IP)
aws ec2 allocate-address --domain vpc
# {
# "AllocationId": "eipalloc-0abc1234",
# "PublicIp": "3.34.xx.xx"
# }
EIP_ALLOC_ID="eipalloc-0abc1234"
PUBLIC_SUBNET_ID="subnet-0abc1234"
NAT Gateway 생성 및 연결
# NAT Gateway 생성 (Public Subnet에)
aws ec2 create-nat-gateway \
--subnet-id ${PUBLIC_SUBNET_ID} \
--allocation-id ${EIP_ALLOC_ID} \
--tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=prod-nat-gw}]'
# 출력에서 NatGatewayId 저장
NAT_GW_ID="nat-0abc1234def56789"
# NAT Gateway 상태 확인 (available 될 때까지 대기, 약 1-2분 소요)
aws ec2 describe-nat-gateways \
--nat-gateway-ids ${NAT_GW_ID} \
--query 'NatGateways[0].State'
# "available"
# Private Subnet Route Table에 NAT GW 경로 추가
PRIVATE_RT_ID="rtb-private0abc1234"
aws ec2 create-route \
--route-table-id ${PRIVATE_RT_ID} \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id ${NAT_GW_ID}
# { "Return": true }
Private 인스턴스에서 인터넷 연결 테스트
# Private 인스턴스에 Bastion Host를 통해 접속 후
# (또는 SSM Session Manager 사용)
# 인터넷 연결 테스트
ping -c 3 8.8.8.8
# PING 8.8.8.8: 56 bytes of data.
# 64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=1.5 ms
# DNS 해석 테스트
ping -c 3 google.com
# PING google.com (142.250.x.x)
# yum 업데이트 테스트
sudo yum update -y
# Loaded plugins: amazon-id, instance-id
# Resolving Dependencies...
# curl로 외부 연결 테스트
curl -I https://aws.amazon.com
# HTTP/2 200
Bastion Host를 통한 Private 인스턴스 접속
# 로컬 PC에서 Bastion Host를 통해 Private 인스턴스로 SSH 점프
# ~/.ssh/config 설정
cat >> ~/.ssh/config << 'EOF'
Host bastion
HostName 3.34.xx.xx # Bastion Host 공인 IP
User ec2-user
IdentityFile ~/.ssh/my-key.pem
Host private-app
HostName 10.0.11.10 # Private 인스턴스 사설 IP
User ec2-user
IdentityFile ~/.ssh/my-key.pem
ProxyJump bastion
EOF
# SSH 접속 (자동으로 Bastion 경유)
ssh private-app
Security Group이나 NACL 설정 후 통신이 안 될 때, VPC Flow Logs가 허용/거부 이력을 기록합니다. 실무에서 네트워크 보안 감사 및 트러블슈팅에 핵심적으로 사용됩니다.
VPC Flow Logs 활성화
# CloudWatch Logs 그룹 생성
aws logs create-log-group --log-group-name /vpc/flow-logs
# IAM 역할 ARN 확인 (Flow Logs용)
FLOW_LOG_ROLE_ARN="arn:aws:iam::123456789012:role/VPCFlowLogsRole"
# VPC Flow Logs 활성화
aws ec2 create-flow-logs \
--resource-type VPC \
--resource-ids ${VPC_ID} \
--traffic-type ALL \
--log-destination-type cloud-watch-logs \
--log-group-name /vpc/flow-logs \
--deliver-logs-permission-arn ${FLOW_LOG_ROLE_ARN}
Flow Logs 레코드 해석
# Flow Log 레코드 형식:
# version account-id interface-id srcaddr dstaddr srcport dstport protocol packets bytes start end action log-status
# 예시 레코드 해석:
# 2 123456789012 eni-abc123 203.0.113.1 10.0.1.10 54321 443 6 10 4096 1620000000 1620000060 ACCEPT OK
# ↑ ACCEPT: Security Group/NACL에서 허용된 트래픽
# 2 123456789012 eni-abc123 192.168.1.1 10.0.1.10 12345 22 6 5 2048 1620000000 1620000060 REJECT OK
# ↑ REJECT: Security Group/NACL에서 차단된 트래픽
# CloudWatch Insights 쿼리로 거부된 트래픽 분석
aws logs start-query \
--log-group-name /vpc/flow-logs \
--start-time $(date -d '1 hour ago' +%s) \
--end-time $(date +%s) \
--query-string 'fields @timestamp, srcAddr, dstAddr, dstPort, action
| filter action = "REJECT"
| stats count(*) as rejectCount by srcAddr, dstPort
| sort rejectCount desc
| limit 20'
특정 포트 차단 확인
# 8080 포트로 들어오는 트래픽이 거부되는지 확인
aws logs filter-log-events \
--log-group-name /vpc/flow-logs \
--filter-pattern '[version, accountId, interfaceId, srcAddr, dstAddr, srcPort, dstPort="8080", protocol, packets, bytes, startTime, endTime, action="REJECT", logStatus]' \
--start-time $(($(date +%s) - 3600))000 \
--query 'events[*].message'
상황
Private Subnet의 EC2 인스턴스에서 패키지를 설치하려는데 계속 실패합니다.
# Private EC2 인스턴스에서 실행
sudo yum update -y
# 출력:
# Loaded plugins: amazon-id, instance-id
# http://amazonlinux.ap-northeast-2.amazonaws.com/2/extras/docker/latest/x86_64/mirror.list:
# [Errno 14] curl#6 - "Could not resolve host: amazonlinux.ap-northeast-2.amazonaws.com"
# Trying other mirror.
# Error: Cannot find a valid baseurl for repo: amzn2-core
ping으로 확인해도 마찬가지입니다.
ping 8.8.8.8
# PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
# (응답 없음 — Ctrl+C로 중단)
진단 과정
1단계: 인스턴스 기본 정보 확인
# 인스턴스 메타데이터로 서브넷 확인
curl -s http://169.254.169.254/latest/meta-data/network/interfaces/macs/
# eth0 MAC 주소 확인 후
MAC=$(curl -s http://169.254.169.254/latest/meta-data/network/interfaces/macs/)
curl -s http://169.254.169.254/latest/meta-data/network/interfaces/macs/${MAC}subnet-id
# subnet-0abc1234def56789 ← Private Subnet
# 공인 IP 없음 확인
curl -s http://169.254.169.254/latest/meta-data/public-ipv4
# 없거나 null
2단계: Route Table 확인
# AWS CLI로 해당 서브넷의 Route Table 확인
aws ec2 describe-route-tables \
--filters "Name=association.subnet-id,Values=subnet-0abc1234def56789" \
--query 'RouteTables[*].Routes'
# 출력:
# [
# [
# {
# "DestinationCidrBlock": "10.0.0.0/16",
# "GatewayId": "local",
# "State": "active"
# }
# ]
# ]
# ← 0.0.0.0/0 경로가 없다! NAT Gateway 경로 누락
3단계: NAT Gateway 존재 여부 확인
aws ec2 describe-nat-gateways \
--filter "Name=vpc-id,Values=${VPC_ID}" \
"Name=state,Values=available" \
--query 'NatGateways[*].{Id:NatGatewayId,State:State,Subnet:SubnetId}'
# 출력:
# [] ← NAT Gateway가 아예 없거나
# 또는 NAT GW는 있지만 Route Table에 연결이 안 된 경우
# [ { "Id": "nat-0xyz...", "State": "available", "Subnet": "subnet-public-..." } ]
해결 방법
케이스 A: NAT Gateway 자체가 없는 경우
# 1. Elastic IP 할당
EIP=$(aws ec2 allocate-address --domain vpc --query 'AllocationId' --output text)
# 2. Public Subnet ID 확인
PUBLIC_SUBNET=$(aws ec2 describe-subnets \
--filters "Name=vpc-id,Values=${VPC_ID}" \
"Name=tag:Name,Values=*public*" \
--query 'Subnets[0].SubnetId' --output text)
# 3. NAT Gateway 생성
NAT_GW=$(aws ec2 create-nat-gateway \
--subnet-id ${PUBLIC_SUBNET} \
--allocation-id ${EIP} \
--query 'NatGateway.NatGatewayId' --output text)
# 4. available 상태 대기
aws ec2 wait nat-gateway-available --nat-gateway-ids ${NAT_GW}
# 5. Private Route Table에 경로 추가
PRIVATE_RT=$(aws ec2 describe-route-tables \
--filters "Name=association.subnet-id,Values=subnet-0abc1234def56789" \
--query 'RouteTables[0].RouteTableId' --output text)
aws ec2 create-route \
--route-table-id ${PRIVATE_RT} \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id ${NAT_GW}
케이스 B: NAT GW는 있지만 Route Table 연결 누락
# 기존 NAT GW ID 확인
NAT_GW_ID=$(aws ec2 describe-nat-gateways \
--filter "Name=vpc-id,Values=${VPC_ID}" "Name=state,Values=available" \
--query 'NatGateways[0].NatGatewayId' --output text)
# Route Table에 경로만 추가
aws ec2 create-route \
--route-table-id ${PRIVATE_RT} \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id ${NAT_GW_ID}
해결 확인
# Private 인스턴스에서 다시 테스트
ping -c 3 8.8.8.8
# 64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=1.5 ms
sudo yum update -y
# ... 정상 업데이트
예방 방법
인프라 코드(Terraform/CloudFormation)를 사용할 때 Private Subnet Route Table에 NAT GW 경로 설정을 잊는 경우가 많습니다. 코드 리뷰 시 반드시 확인하세요.
# Terraform 예시 - 이 부분을 빠뜨리면 안 됨!
resource "aws_route" "private_nat" {
route_table_id = aws_route_table.private.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id # ← 필수!
}
상황
웹 서버 EC2에 Security Group 80번 포트 인바운드를 추가했는데도 브라우저에서 접속이 안 됩니다.
# 로컬에서 테스트
curl -v http://3.34.xx.xx:80
# * Trying 3.34.xx.xx:80...
# * Connection timed out
Security Group은 분명히 설정했습니다.
# Security Group 확인
aws ec2 describe-security-groups --group-ids sg-web
# Inbound Rules:
# - Type: HTTP, Port: 80, Source: 0.0.0.0/0 ← 있음!
# - Type: SSH, Port: 22, Source: My IP
원인 분석
Security Group 외에 **Network ACL(NACL)**이 서브넷 레벨에서 트래픽을 차단하고 있습니다.
# 서브넷의 NACL 확인
aws ec2 describe-network-acls \
--filters "Name=association.subnet-id,Values=${SUBNET_ID}" \
--query 'NetworkAcls[0].Entries'
# 출력:
# [
# { "RuleNumber": 100, "Protocol": "6", "RuleAction": "allow",
# "Egress": false, "CidrBlock": "10.0.0.0/8", "PortRange": {"From": 0, "To": 65535} },
# { "RuleNumber": 32767, "Protocol": "-1", "RuleAction": "deny",
# "Egress": false, "CidrBlock": "0.0.0.0/0" }
# ]
# ← 내부 IP(10.x.x.x)만 허용, 외부 0.0.0.0/0은 전체 차단!
NACL과 Security Group의 차이
| 항목 | Security Group | Network ACL |
|---|---|---|
| 적용 레벨 | 인스턴스(ENI) | 서브넷 |
| 상태 | Stateful | Stateless |
| 규칙 우선순위 | 모든 규칙 평가 | 번호 순서대로 평가 |
| 기본 정책 | 인바운드 전체 거부 | 기본 NACL은 전체 허용 |
NACL은 Stateless이므로 인바운드 허용 시 아웃바운드 응답 포트도 별도로 허용해야 합니다.
해결 방법
# NACL ID 확인
NACL_ID=$(aws ec2 describe-network-acls \
--filters "Name=association.subnet-id,Values=${SUBNET_ID}" \
--query 'NetworkAcls[0].NetworkAclId' --output text)
# 인바운드 HTTP(80) 허용 규칙 추가 (Rule 90 = Rule 100보다 먼저 평가)
aws ec2 create-network-acl-entry \
--network-acl-id ${NACL_ID} \
--ingress \
--rule-number 90 \
--protocol 6 \
--rule-action allow \
--cidr-block 0.0.0.0/0 \
--port-range From=80,To=80
# NACL은 Stateless! 응답 트래픽을 위한 아웃바운드 Ephemeral 포트도 허용
aws ec2 create-network-acl-entry \
--network-acl-id ${NACL_ID} \
--egress \
--rule-number 90 \
--protocol 6 \
--rule-action allow \
--cidr-block 0.0.0.0/0 \
--port-range From=1024,To=65535 # Ephemeral ports
교훈
트래픽이 차단될 때는 Security Group과 NACL을 모두 확인하는 습관을 가지세요. 두 계층 모두 허용해야 통신이 됩니다.
확대
심화 — NAT Gateway의 SNAT 포트: 대역폭이 남아도 막힌다
심화: SNAT 포트 고갈 — NAT Gateway의 진짜 상한은 대역폭이 아니다
NAT Gateway 지표에서 대역폭·CPU는 여유인데 특정 외부 API 호출만 간헐적으로 실패하는 사고가 있습니다. 원인은 대역폭이 아니라 포트입니다. 이 상한을 알아야 규모가 커질 때의 함정을 피합니다.
- NAT은 포트로 연결을 구분한다: NAT Gateway는 프라이빗 서브넷의 여러 인스턴스가 하나의 공인 IP로 나가도록 SNAT(출발지 주소·포트 치환)를 합니다. 동일한 목적지
IP:포트로 가는 동시 연결마다 서로 다른 소스 포트가 필요합니다. - 약 55,000이라는 벽: 소스 포트는 대략 55,000개(
1024~65535)뿐입니다. 따라서 NAT Gateway 1개는 동일 목적지 튜플당 약 55,000 동시 연결이 상한이며, 이는 대역폭과 무관합니다. - 넘으면 조용히 드롭: 상한을 넘으면 새 연결에 줄 포트가 없어 SNAT가 실패하고, CloudWatch의
ErrorPortAllocation이 0보다 커지며 패킷이 버려집니다. 트래픽 지표는 한가해 보여 원인을 놓치기 쉽습니다. - 흔한 트리거: 수십 대 인스턴스가 같은 외부 엔드포인트(결제·푸시·서드파티 API)를 초당 대량으로, 그것도 짧은 연결을 계속 새로 맺으며 호출하는 패턴.
TIME_WAIT까지 겹치면 포트가 더 빨리 고갈됩니다. - 완화: 애플리케이션에서 keep-alive·커넥션 풀로 연결을 재사용하고, 트래픽을 서브넷별 여러 NAT Gateway로 분산하며, AWS 서비스(S3·DynamoDB 등)는 VPC Endpoint로 NAT을 우회합니다.
상황: 다른 목적지로의 통신은 멀쩡한데 특정 외부 API 호출만 피크 시간에 간헐적으로 실패·타임아웃하고, 재시도하면 성공하기도 합니다.
원인: 그 API 하나의 목적지 튜플로 가는 동시 연결이 NAT Gateway의 SNAT 포트 상한(약 55,000)에 근접·초과한 것입니다. 대역폭이 아니라 포트 고갈이라 트래픽 지표는 한가해 보입니다.
진단: CloudWatch의 NAT Gateway 지표에서 ErrorPortAllocation이 0보다 커지는지, ActiveConnectionCount가 상한 부근으로 오르는지 피크 시간대와 겹쳐 봅니다. 실패가 그 목적지에 국한되는지 애플리케이션 로그로 교차 확인하면 확정됩니다.
해결: 애플리케이션에서 HTTP keep-alive·커넥션 풀로 동시 연결 수를 줄이는 것이 근본책입니다. 트래픽을 서브넷별 여러 NAT Gateway로 나누고, 대상이 AWS 서비스라면 VPC Endpoint로 NAT을 우회해 포트 압력을 낮춥니다.
실제 업무에서 VPC를 다루는 상황들
클라우드 인프라 엔지니어나 DevOps 엔지니어로 일하다 보면 VPC 관련 작업이 일상입니다.
신규 프로젝트 VPC 설계
신규 서비스를 AWS에 구축할 때 가장 먼저 VPC 설계를 합니다. 아키텍처 회의에서 나오는 전형적인 질문들:
"이 서비스 CIDR은 뭘로 할까요?"
→ 기존 VPC들과 겹치지 않게 설계 (나중에 VPC Peering 고려)
→ 충분한 IP 여유 확보 (/16 추천, 65,536개)
"DB는 퍼블릭 서브넷에 올리면 안 되죠?"
→ 절대 안 됨. RDS는 반드시 Private Subnet에
"개발/스테이징/프로덕션 VPC를 분리해야 할까요?"
→ 보안과 비용 트레이드오프 논의
→ 최소한 프로덕션은 별도 VPC 권장
보안 감사 대응
보안팀이나 컴플라이언스 감사에서 VPC 설정을 검토하는 경우:
# 퍼블릭 서브넷에 있는 인스턴스 목록 (감사용)
# "퍼블릭 IP를 가진 모든 인스턴스를 보여주세요"
aws ec2 describe-instances \
--filters "Name=network-interface.association.public-ip,Values=*" \
--query 'Reservations[*].Instances[*].{
Id:InstanceId,
Name:Tags[?Key==`Name`]|[0].Value,
PublicIP:PublicIpAddress,
Role:Tags[?Key==`Role`]|[0].Value
}' \
--output table
비용 최적화
NAT Gateway 비용은 생각보다 많이 나옵니다. 월말 비용 리포트에서 NAT Gateway 비용이 크게 잡히면:
월 NAT Gateway 기본 비용: 약 $32 (한 개당)
데이터 처리 비용: $0.045/GB
절감 방법:
1. 개발 환경 NAT GW → 업무 시간에만 켜두기
2. S3, DynamoDB 등 AWS 서비스는 VPC Endpoint 사용 (NAT GW 통하지 않음)
3. 불필요한 외부 패키지 다운로드 줄이기
인시던트 대응
프로덕션 서비스 장애 시 네트워크 레이어 확인:
장애 알람이 발생하면 네트워크 레이어를 순서대로 점검합니다.
- Security Group 최근 변경 이력 확인 (CloudTrail)
- NACL 규칙 변경 이력 확인
- Route Table 변경 이력 확인
- VPC Flow Logs에서 REJECT 트래픽 패턴 분석
- NAT Gateway 상태 확인 (available?)
자격증 연계
AWS Certified Solutions Architect(SAA, SAP), AWS Certified Advanced Networking(ANS) 시험에서 VPC 관련 문제가 다수 출제됩니다. 이 챕터의 내용은 특히 SAA-C03 시험의 네트워킹 도메인과 직접 연결됩니다.
핵심 정리
| 개념 | 온프레미스 | AWS |
|---|---|---|
| 논리 네트워크 분리 | VLAN | VPC |
| L2 세그먼트 | L2 스위치 포트 | Subnet |
| 인스턴스 방화벽 | 호스트 방화벽 | Security Group (Stateful) |
| 서브넷 ACL | ACL | Network ACL (Stateless) |
| 라우팅 | 라우터 라우팅 테이블 | Route Table |
| 인터넷 연결 | 인터넷 라우터+공인 IP | Internet Gateway |
| 사설망 아웃바운드 | NAT 장비 | NAT Gateway |
Public vs Private Subnet 판별 기준:
- Route Table에
0.0.0.0/0 → igw-xxxx있으면 → Public - Route Table에
0.0.0.0/0 → nat-xxxx있으면 → Private (아웃바운드 가능) - Route Table에
0.0.0.0/0경로 없으면 → 완전 격리 Private
NAT Gateway 필수 조건:
- Public Subnet에 배치
- Elastic IP 연결
- Private Subnet Route Table에 경로 추가
명령어·단축키 빠른 참조
이 모듈에서 다룬 VPC 구조 파악·NAT 설정 명령을 실전 옵션과 함께 모았습니다. 콘솔보다 CLI가 원인 분석에 훨씬 빠릅니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
aws ec2 describe-vpcs | VPC 목록·CIDR 확인 | --query 'Vpcs[*].{VpcId:VpcId,CIDR:CidrBlock}' |
aws ec2 describe-subnets | 서브넷·AZ·MapPublicIp 확인 | --filters "Name=vpc-id,Values=$VPC_ID" |
aws ec2 describe-route-tables | Public/Private 판별(0.0.0.0/0 대상) | --filters "Name=association.subnet-id,Values=subnet-xxx" |
aws ec2 describe-nat-gateways | NAT GW 존재·available 확인 | --filter "Name=vpc-id,Values=$VPC_ID" "Name=state,Values=available" |
aws ec2 create-nat-gateway | 프라이빗 아웃바운드용 NAT 생성 | --subnet-id $PUBLIC_SUBNET --allocation-id $EIP |
aws ec2 create-route | 라우트 테이블에 NAT/IGW 경로 추가 | --route-table-id $RT --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NAT |
aws ec2 describe-security-groups | SG 인/아웃 규칙 확인 | --group-ids $SG_ID |
aws ec2 describe-network-acls | 서브넷 NACL 규칙 확인 | --filters "Name=association.subnet-id,Values=subnet-xxx" |
aws ec2 create-network-acl-entry | NACL 규칙 추가(Stateless 양방향) | --egress ... --port-range From=1024,To=65535 (응답용 ephemeral) |
aws logs filter-log-events | Flow Logs에서 REJECT 확인 | --log-group-name /vpc/flow-logs --filter-pattern REJECT |
curl http://169.254.169.254/... | 인스턴스 메타데이터로 서브넷·공인IP | curl -s .../latest/meta-data/public-ipv4 (없으면 Private) |
ping / curl (Private에서) | NAT 경로 정상 여부 검증 | ping -c3 8.8.8.8, curl -I https://aws.amazon.com |
다음 모듈에서는 Bash 스크립트로 여러 서버의 포트 연결 상태를 자동 점검하는 방법을 다룹니다.
클라우드 위에서 VPC·서브넷·라우팅·IGW/NAT를 처음부터 체계적으로 설계하고 싶다면 VPC와 서브넷(Cloud Engineering 트랙)에서 퍼블릭·프라이빗 분리와 3티어 구조를 단계적으로 다룹니다.