실습 예제 · 1
3계층 VPC 직접 구성
SAA 시나리오 문항의 기본 배경이 되는 구성입니다. 퍼블릭 · 프라이빗 · 데이터베이스 세 계층을 두 가용 영역에 배치하고, 인터넷 게이트웨이와 가용 영역별 NAT 게이트웨이로 아웃바운드 경로를 만듭니다. 콘솔 클릭 대신 CloudFormation 템플릿과 AWS CLI로 구성합니다.
학습 목표
- 퍼블릭 서브넷과 프라이빗 서브넷을 라우팅 테이블로 구분해 만들 수 있습니다.
- 가용 영역마다 NAT 게이트웨이를 두고 라우팅을 분리할 수 있습니다.
- CloudFormation 템플릿으로 네트워크 계층을 한 번에 배포할 수 있습니다.
- 구성이 의도대로 되었는지 CLI로 검증할 수 있습니다.
- 의존 관계 때문에 삭제가 막히는 순서를 이해하고 정리할 수 있습니다.
시나리오
웹 애플리케이션을 올릴 네트워크를 처음부터 만듭니다. 요구는 이렇습니다.
- 로드 밸런서는 인터넷에서 접근 가능해야 합니다.
- 애플리케이션 서버는 인터넷에서 직접 접근할 수 없어야 하지만, 패키지 내려받기 같은 아웃바운드는 가능해야 합니다.
- 데이터베이스는 인터넷 경로가 아예 없어야 합니다.
- 가용 영역 하나가 손실돼도 나머지 가용 영역에서 계속 동작해야 합니다.
이 요구가 그대로 세 계층 구조가 됩니다. 배경 개념은 3장 VPC 네트워킹 기초에 있고, 이 구성을 잘못 두면 어떤 일이 벌어지는지는 케이스 4 · 퍼블릭 서브넷에 데이터베이스를 뒀다에서 다룹니다.
아키텍처 개요
| 계층 | 서브넷 수 | 기본 경로(0.0.0.0/0) |
퍼블릭 IP 자동 할당 |
|---|---|---|---|
| 퍼블릭 | 가용 영역마다 1개 | 인터넷 게이트웨이 | 필요에 따라 켠다 |
| 프라이빗(애플리케이션) | 가용 영역마다 1개 | 자기 가용 영역의 NAT 게이트웨이 | 끈다 |
| 데이터베이스 | 가용 영역마다 1개 | 없음 — 로컬 경로만 | 끈다 |
주소 계획은 10.20.0.0/16 안에서 계층별로 /24씩 자릅니다.
서브넷마다 AWS가 5개 주소를 예약하므로
/24에서 실제로 쓸 수 있는 주소는 그만큼 줄어듭니다.
| 서브넷 | 가용 영역 A | 가용 영역 B |
|---|---|---|
| 퍼블릭 | 10.20.0.0/24 |
10.20.1.0/24 |
| 프라이빗 | 10.20.10.0/24 |
10.20.11.0/24 |
| 데이터베이스 | 10.20.20.0/24 |
10.20.21.0/24 |
구성 단계
1 · CloudFormation 템플릿
네트워크는 리소스 사이의 의존 관계가 많아 CLI로 순서를 지키기 번거롭습니다. CloudFormation은 의존 관계를 추론해 순서를 잡아 주므로 이런 계층에 특히 잘 맞습니다.
AWSTemplateFormatVersion: '2010-09-09'
Description: Three-tier VPC with public, private, and database subnets across two AZs
Parameters:
VpcCidr:
Type: String
Default: 10.20.0.0/16
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: lab01-vpc
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: lab01-igw
IgwAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref Vpc
InternetGatewayId: !Ref InternetGateway
PublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: 10.20.0.0/24
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: lab01-public-a
PublicSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: 10.20.1.0/24
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: lab01-public-b
PrivateSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: 10.20.10.0/24
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: lab01-private-a
PrivateSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: 10.20.11.0/24
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: lab01-private-b
DbSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: 10.20.20.0/24
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: lab01-db-a
DbSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: 10.20.21.0/24
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: lab01-db-b
NatEipA:
Type: AWS::EC2::EIP
DependsOn: IgwAttachment
Properties:
Domain: vpc
NatEipB:
Type: AWS::EC2::EIP
DependsOn: IgwAttachment
Properties:
Domain: vpc
NatGatewayA:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatEipA.AllocationId
SubnetId: !Ref PublicSubnetA
Tags:
- Key: Name
Value: lab01-nat-a
NatGatewayB:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatEipB.AllocationId
SubnetId: !Ref PublicSubnetB
Tags:
- Key: Name
Value: lab01-nat-b
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: lab01-rtb-public
PublicDefaultRoute:
Type: AWS::EC2::Route
DependsOn: IgwAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicAssocA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnetA
PublicAssocB:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnetB
PrivateRouteTableA:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: lab01-rtb-private-a
PrivateDefaultRouteA:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTableA
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGatewayA
PrivateAssocA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTableA
SubnetId: !Ref PrivateSubnetA
PrivateRouteTableB:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: lab01-rtb-private-b
PrivateDefaultRouteB:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTableB
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGatewayB
PrivateAssocB:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTableB
SubnetId: !Ref PrivateSubnetB
DbRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: lab01-rtb-db
DbAssocA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref DbRouteTable
SubnetId: !Ref DbSubnetA
DbAssocB:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref DbRouteTable
SubnetId: !Ref DbSubnetB
Outputs:
VpcId:
Value: !Ref Vpc
PublicSubnets:
Value: !Join [',', [!Ref PublicSubnetA, !Ref PublicSubnetB]]
PrivateSubnets:
Value: !Join [',', [!Ref PrivateSubnetA, !Ref PrivateSubnetB]]
DbSubnets:
Value: !Join [',', [!Ref DbSubnetA, !Ref DbSubnetB]]
데이터베이스 라우팅 테이블에는 기본 경로를 아예 넣지 않습니다. VPC 안의 로컬 경로만 남으므로 인터넷으로 나가는 방법이 존재하지 않습니다. 이것이 "정책으로 막았다"보다 강한 보장입니다.
2 · 배포
aws cloudformation deploy \
--stack-name lab01-network \
--template-file three-tier-vpc.yaml \
--region ap-northeast-2
aws cloudformation describe-stacks \
--stack-name lab01-network \
--query 'Stacks[0].Outputs' \
--output table
3 · 같은 구성을 CLI로 만들 때
템플릿 없이 CLI로 만들면 순서를 직접 지켜야 합니다. 아래는 퍼블릭 경로까지의 최소 흐름입니다.
VPC_ID=$(aws ec2 create-vpc \
--cidr-block 10.20.0.0/16 \
--query 'Vpc.VpcId' --output text)
aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-hostnames
SUBNET_ID=$(aws ec2 create-subnet \
--vpc-id "$VPC_ID" \
--cidr-block 10.20.0.0/24 \
--availability-zone ap-northeast-2a \
--query 'Subnet.SubnetId' --output text)
aws ec2 modify-subnet-attribute --subnet-id "$SUBNET_ID" --map-public-ip-on-launch
IGW_ID=$(aws ec2 create-internet-gateway \
--query 'InternetGateway.InternetGatewayId' --output text)
aws ec2 attach-internet-gateway --vpc-id "$VPC_ID" --internet-gateway-id "$IGW_ID"
RTB_ID=$(aws ec2 create-route-table \
--vpc-id "$VPC_ID" \
--query 'RouteTable.RouteTableId' --output text)
aws ec2 create-route \
--route-table-id "$RTB_ID" \
--destination-cidr-block 0.0.0.0/0 \
--gateway-id "$IGW_ID"
aws ec2 associate-route-table --route-table-id "$RTB_ID" --subnet-id "$SUBNET_ID"
NAT 게이트웨이는 탄력적 IP를 먼저 할당하고, 퍼블릭 서브넷에 만든 뒤, 프라이빗 라우팅 테이블이 그것을 가리키게 하는 순서입니다.
ALLOC_ID=$(aws ec2 allocate-address \
--domain vpc \
--query 'AllocationId' --output text)
NAT_ID=$(aws ec2 create-nat-gateway \
--subnet-id "$SUBNET_ID" \
--allocation-id "$ALLOC_ID" \
--query 'NatGateway.NatGatewayId' --output text)
# 사용 가능해질 때까지 기다립니다.
aws ec2 wait nat-gateway-available --nat-gateway-ids "$NAT_ID"
aws ec2 create-route \
--route-table-id rtb-0private00000000a \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id "$NAT_ID"
검증 방법
"만들어졌다"가 아니라 "의도한 경로가 생겼다"를 확인합니다.
# 1) 라우팅 테이블마다 기본 경로의 대상이 무엇인지 봅니다.
aws ec2 describe-route-tables \
--filters Name=vpc-id,Values=vpc-0123456789abcdef0 \
--query 'RouteTables[].{Rtb:RouteTableId,
Routes:Routes[?DestinationCidrBlock==`0.0.0.0/0`].[GatewayId,NatGatewayId],
Subnets:Associations[].SubnetId}' \
--output json
# 2) 서브넷의 퍼블릭 IP 자동 할당 설정을 봅니다.
aws ec2 describe-subnets \
--filters Name=vpc-id,Values=vpc-0123456789abcdef0 \
--query 'Subnets[].[Tags[?Key==`Name`].Value|[0],CidrBlock,AvailabilityZone,MapPublicIpOnLaunch]' \
--output table
# 3) NAT 게이트웨이가 어느 서브넷(=어느 가용 영역)에 있는지 봅니다.
aws ec2 describe-nat-gateways \
--filter Name=vpc-id,Values=vpc-0123456789abcdef0 \
--query 'NatGateways[].[NatGatewayId,SubnetId,State]' \
--output table
| 항목 | 통과 기준 |
|---|---|
| 퍼블릭 서브넷 | 기본 경로의 대상이 igw-로 시작한다 |
| 프라이빗 서브넷 | 기본 경로의 대상이 같은 가용 영역의 nat-다 |
| 데이터베이스 서브넷 | 기본 경로가 없다 (로컬 경로만 존재) |
| 가용 영역 분산 | 각 계층의 서브넷이 서로 다른 가용 영역에 하나씩 있다 |
| 아웃바운드 확인 | 프라이빗 서브넷의 인스턴스에서 외부 HTTPS 호출이 성공한다 |
비용 고려
| 구성 요소 | 요금 발생 | 비고 |
|---|---|---|
| VPC · 서브넷 · 라우팅 테이블 | 리소스 자체로는 발생하지 않는다 | 만들어 두는 것만으로는 부담이 없다 |
| 인터넷 게이트웨이 | 게이트웨이 자체가 아니라 지나는 트래픽이 항목이다 | 송신 방향에 요금이 붙는다 |
| NAT 게이트웨이 | 가동 시간과 처리한 데이터양 | 이 실습에서 가장 큰 항목이다. 실습이 끝나면 반드시 삭제한다 |
| 탄력적 IP | 사용하지 않는 상태로 보유하면 항목이 된다 | NAT를 지우고 주소를 놓아 주지 않으면 계속 남는다 |
| 가용 영역 간 전송 | 가용 영역을 넘는 트래픽에 붙는다 | 가용 영역별 NAT 설계가 이 항목을 줄인다 |
NAT 게이트웨이를 가용 영역마다 두면 가동 항목이 늘지만, 가용 영역을 넘는 트래픽이 사라지고 가용 영역 장애에도 견딥니다. 비용 구조는 케이스 6 · 데이터 전송 요금이 예산을 넘겼다에서 자세히 다룹니다.
프로덕션 고려사항
- 가용 영역을 셋으로. 두 곳이면 하나가 빠질 때 남은 한 곳이 전체 부하를 받아야 합니다. 셋이면 필요한 여유 비율이 줄어듭니다.
- 주소 공간을 넉넉하게. VPC CIDR은 나중에 좁히기 어렵고, 서브넷마다 5개 주소가 예약됩니다. 컨테이너를 쓰면 주소 소모가 훨씬 빠릅니다.
- VPC 흐름 로그를 켭니다. 문제가 생긴 뒤에 켜면 그 이전 기록은 존재하지 않습니다.
- S3와 DynamoDB에는 게이트웨이 엔드포인트를 추가합니다. NAT를 지나는 데이터양이 줄어듭니다 (실습 10).
- 보안 그룹은 계층 간 참조로 연결합니다. 소스를 CIDR이 아니라 상위 계층의 보안 그룹 ID로 지정합니다.
- 네트워크 ACL은 기본값을 유지하고, 필요할 때만 손댑니다. 상태 비저장이라 반환 트래픽을 명시 허용해야 하므로 실수하기 쉽습니다.
- 템플릿을 환경별 파라미터로 재사용합니다. 개발·스테이징·운영이 같은 구조를 갖는 것이 운영 부담을 줄입니다.
정리 — 리소스 삭제
aws cloudformation delete-stack --stack-name lab01-network
aws cloudformation wait stack-delete-complete --stack-name lab01-network
# 사용되지 않는 탄력적 IP 가 남아 있지 않은지 확인합니다.
aws ec2 describe-addresses \
--query 'Addresses[?AssociationId==null].[AllocationId,PublicIp]' \
--output table
CLI로 직접 만들었다면 아래 순서로 지웁니다. 의존 관계 때문에 순서를 바꾸면 삭제가 거부됩니다.
aws ec2 delete-nat-gateway --nat-gateway-id nat-0123456789abcdef0
aws ec2 wait nat-gateway-deleted --nat-gateway-ids nat-0123456789abcdef0
aws ec2 release-address --allocation-id eipalloc-0123456789abcdef0
aws ec2 detach-internet-gateway \
--internet-gateway-id igw-0123456789abcdef0 \
--vpc-id vpc-0123456789abcdef0
aws ec2 delete-internet-gateway --internet-gateway-id igw-0123456789abcdef0
aws ec2 delete-subnet --subnet-id subnet-0123456789abcdef0
aws ec2 delete-route-table --route-table-id rtb-0123456789abcdef0
aws ec2 delete-vpc --vpc-id vpc-0123456789abcdef0
자주 하는 실수
| 실수 | 증상 | 고치는 법 |
|---|---|---|
| 인터넷 게이트웨이를 만들고 연결하지 않음 | 퍼블릭 경로를 만들려 할 때 실패하거나 통신이 되지 않는다 | VPC에 연결한 뒤 경로를 만든다 (템플릿에서는 DependsOn) |
| NAT를 프라이빗 서브넷에 만듦 | 프라이빗 서브넷에서 인터넷으로 나가지 못한다 | NAT는 퍼블릭 서브넷에 만든다 |
| NAT 하나로 모든 가용 영역을 처리 | 가용 영역 장애 시 전면 중단, 평상시 가용 영역 간 전송 발생 | 가용 영역마다 NAT와 라우팅 테이블을 만든다 |
| 서브넷을 라우팅 테이블에 연결하지 않음 | VPC의 기본 라우팅 테이블이 적용되어 의도와 다르게 동작한다 | 모든 서브넷에 명시적으로 연결한다 |
이름만 private이고 인터넷 게이트웨이 경로가 있음 |
프라이빗이라고 믿은 서브넷이 실제로는 퍼블릭이다 | 라우팅 테이블로 검증한다 |
| 서브넷 CIDR이 겹침 | 서브넷 생성이 거부된다 | 계층별로 대역을 미리 나눠 문서화한다 |
| 실습 후 NAT를 지우지 않음 | 사용하지 않는데도 요금이 계속 발생한다 | 정리 절에 따라 삭제하고 탄력적 IP까지 회수한다 |
이어서 볼 곳
- 3장 · VPC 네트워킹 기초 CIDR 설계와 예약 주소, 라우팅 테이블, 보안 그룹과 네트워크 ACL을 다룹니다.
- 실습 2 · ALB + ASG 웹 티어 이 실습에서 만든 네트워크 위에 로드 밸런서와 오토스케일링을 얹습니다.
- 실습 10 · VPC 엔드포인트로 S3 비공개 접근 이 네트워크에 게이트웨이 엔드포인트를 추가해 NAT 경로를 걷어냅니다.
- 케이스 4 · 퍼블릭 서브넷에 데이터베이스를 뒀다 이 구성을 지키지 않았을 때 어떤 노출이 생기는지 분해합니다.
- 17장 · 모니터링 · 운영 · 계정 관리 CloudFormation 스택 수명주기와 변경 세트, 드리프트 감지를 다룹니다.
공식 문서 출처
- Amazon VPC 사용 설명서 — NAT 게이트웨이 — NAT가 지정한 서브넷에 인터페이스를 만든다는 동작과 퍼블릭·프라이빗 NAT의 구분
- Amazon EC2 API 레퍼런스 — CreateSubnet — 서브넷이 하나의 가용 영역에 속한다는 제약과 가용 영역 ID 사용 권고
- Amazon EC2 API 레퍼런스 — ModifySubnetAttribute
—
MapPublicIpOnLaunch속성 - Amazon EC2 API 레퍼런스 — CreateNatGateway — 탄력적 IP 연결과 네트워크 경계 그룹 일치 요구
- AWS CloudFormation 사용 설명서 — AWS::EC2::VPC
—
CidrBlock·EnableDnsSupport·EnableDnsHostnames속성 - AWS CloudFormation 사용 설명서 — AWS::EC2::Route
—
GatewayId와NatGatewayId중 하나를 대상으로 지정하는 방식 - AWS CloudFormation 사용 설명서 — AWS::EC2::NatGateway
—
AllocationId와SubnetId속성