학습 목표

시나리오

웹 애플리케이션을 올릴 네트워크를 처음부터 만듭니다. 요구는 이렇습니다.

이 요구가 그대로 세 계층 구조가 됩니다. 배경 개념은 3장 VPC 네트워킹 기초에 있고, 이 구성을 잘못 두면 어떤 일이 벌어지는지는 케이스 4 · 퍼블릭 서브넷에 데이터베이스를 뒀다에서 다룹니다.

아키텍처 개요

서브넷 계층과 라우팅 설계
계층 서브넷 수 기본 경로(0.0.0.0/0) 퍼블릭 IP 자동 할당
퍼블릭 가용 영역마다 1개 인터넷 게이트웨이 필요에 따라 켠다
프라이빗(애플리케이션) 가용 영역마다 1개 자기 가용 영역의 NAT 게이트웨이 끈다
데이터베이스 가용 영역마다 1개 없음 — 로컬 경로만 끈다

주소 계획은 10.20.0.0/16 안에서 계층별로 /24씩 자릅니다. 서브넷마다 AWS가 5개 주소를 예약하므로 /24에서 실제로 쓸 수 있는 주소는 그만큼 줄어듭니다.

CIDR 할당 계획
서브넷 가용 영역 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은 의존 관계를 추론해 순서를 잡아 주므로 이런 계층에 특히 잘 맞습니다.

three-tier-vpc.yaml — 3계층 VPC 템플릿 (가용 영역은 배포 리전의 첫 두 곳을 씁니다)
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 · 배포

deploy.sh — 스택 배포와 출력 확인 (스택 이름과 리전은 예시입니다)
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로 만들면 순서를 직접 지켜야 합니다. 아래는 퍼블릭 경로까지의 최소 흐름입니다.

create-network.sh — CLI로 VPC와 퍼블릭 경로 만들기 (리소스 ID는 실행 결과로 대체합니다)
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를 먼저 할당하고, 퍼블릭 서브넷에 만든 뒤, 프라이빗 라우팅 테이블이 그것을 가리키게 하는 순서입니다.

create-nat.sh — 탄력적 IP 할당과 NAT 게이트웨이 생성
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"

검증 방법

"만들어졌다"가 아니라 "의도한 경로가 생겼다"를 확인합니다.

verify-routes.sh — 서브넷별 라우팅 테이블과 기본 경로 확인 (VPC 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 · 데이터 전송 요금이 예산을 넘겼다에서 자세히 다룹니다.

프로덕션 고려사항

정리 — 리소스 삭제

cleanup.sh — 스택 삭제와 잔여 리소스 확인
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로 직접 만들었다면 아래 순서로 지웁니다. 의존 관계 때문에 순서를 바꾸면 삭제가 거부됩니다.

cleanup-manual.sh — CLI로 만든 리소스의 삭제 순서 (ID는 예시입니다)
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까지 회수한다

이어서 볼 곳

공식 문서 출처