이 글에서는 AWS CLI를 사용하여 VPC 생성부터 EC2 웹 서버 생성까지의 여정을 다룹니다.
최종 목표는 웹 서버에 접속해서 응답을 확인하는 것입니다.
1. VPC 생성
AWS에서 서버와 같은 리소스를 만들려면 네트워크가 필요한데, VPC가 그 네트워크이다.
VPC는 내 리소스들이 쓸 IP 범위 (10.0.0.0/16), 통신 규칙, 인터넷과의 연결 방식을 정의하고, 다른 AWS 계정 / VPC와 격리된다.
"AWS에서 나만의 격리된 네트워크를 만드는 것" 라고 이해하자.
1.1 VPC 생성
먼저 최대 65,536개의 IP 주소를 제공하는 CIDR 블록이 10.0.0.0/16인 VPC를 생성해보자.
aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=MyVPC}]'
aws ec2 create-vpc --cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=TutorialVPC}]' \
--profile qa
[TutorialVPC라는 이름으로 생성]
응답으로 JSON이 올텐데, 내부에 키가 VpcId인 값을 기억해두자.
1.2 VPC에서 dns-support 및 dns-hostnames 활성화
`dns-support`는 VPC에서 DNS 질의를 할 수 있는지 여부를 설정한다.
활성화하면 VPC 내부에서 AWS가 관리하는 DNS 서버 (Route 53 Resolver) 10.0.0.2로 접근할 수 있다.
VPC를 생성하면 기본값이 true이다.
`dns-hostnames`는 VPC에서 생성한 퍼블릭 IP 주소가 있는 EC2 인스턴스에 DNS hostname을 붙여줄지 여부를 설정한다.
활성화하면 인스턴스 hostname이 `ec2-54-180-123-45.ap-northeast-2.compute.amazonaws.com` 와 같이 붙는다.
기본값은 false다.
aws ec2 modify-vpc-attribute --vpc-id [VPC_ID] --enable-dns-support
aws ec2 modify-vpc-attribute --vpc-id [VPC_ID] --enable-dns-hostnames
[dns-support와 dns-hostnames를 활성화]

사실 VPC 만으로는 AWS 리소스를 배치할 수 없다.
실제 리소스는 Subnet 위에 배치할 수 있다.
2. Subnet 생성
문단 1에서 VPC (10.0.0.0/16) = 65,536개 IP가 있는 네트워크를 만들었다. (서울 리전에 생성했다)
리소스는 이 네트워크를 Subnet으로 자르고, 그 Subnet 위에 배치할 수 있다.
2개의 AZ에 Public 및 Private Subnet을 생성해보자.
2.1 Public Subnet 생성
Public Subnet은 웹 서버와 같이 인터넷에서 접근해야하는 리소스에 사용된다.
aws ec2 create-subnet --vpc-id <VPC_ID> \
--cidr-block 10.0.0.0/24 \
--availability-zone ap-northeast-2a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=PublicSubnet1}]' \
--profile qa
[ap-northeast-2a에 10.0.0.0/24 PublicSubnet1 생성]
aws ec2 create-subnet --vpc-id <VPC_ID> \
--cidr-block 10.0.1.0/24 \
--availability-zone ap-northeast-2c \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=PublicSubnet2}]' \
--profile qa
[ap-northeast-2c에 10.0.1.0/24 PublicSubnet2을 생성]
2.2 Private Subnet
Private Subnet은 DB와 같이 인터넷에서 직접 접근할 수 없어야 하는 리소스에 사용된다.
Public Subnet의 명령어를 참고하여 각 AZ에 CIDR 블록과 이름에 유의하여 생성하자.

VPC (10.0.0.0/16)
├─ PublicSubnet1 (10.0.0.0/24, AZ-a)
├─ PublicSubnet2 (10.0.1.0/24, AZ-c)
├─ PrivateSubnet1 (10.0.2.0/24, AZ-a)
└─ PrivateSubnet2 (10.0.3.0/24, AZ-c)
이제 2개의 AZ에 분산된 2개의 Public Subnet과 2개의 Private Subnet, 총 4개의 Subnet이 있다.
3. 인터넷 연결 구성
VPC의 리소스가 외부 인터넷과 통신하려면 Internet Gateway (IGW)를 생성하고 연결해야한다.
IGW를 생성하고 VPC에 연결해보자.
3.1 IGW 생성
aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=TutorialIGW}]' \
--profile qa
[TutorialIGW 생성]
3.2 IGW를 VPC에 연결
aws ec2 attach-internet-gateway \
--internet-gateway-id <IGW_ID> \
--vpc-id <VPC_ID> \
--profile qa
[IGW를 VPC에 연결]
3.3 Route Table 생성 및 구성
VPC와 IGW는 연결했지만,
Subnet 내부 리소스는 인터넷에 Outbound 트래픽을 어디로 전달해야하는지 모른다.
Route Table(RT)로 길을 알려줘야 한다.
3.3.1 Public Subnet용 Route Table
먼저 PublicSubnet의 RT를 생성하자
aws ec2 create-route-table --vpc-id <VPC_ID> \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=PublicRT}]' \
--profile qa
[PublicRT 생성]
현재는 껍데기만 있는 상태. 이제 트래픽을 전달할 방향을 명시하자.
aws ec2 create-route \
--route-table-id <PublicRT_ID> \
--destination-cidr-block 0.0.0.0/0 \
--gateway-id <IGW_ID> \
--profile qa
[PublicRT에 0.0.0.0/0 → IGW 추가]
PublicRT를 사용하는 Subnet의 모든 Outbound 트래픽은 IGW로 보내진다.
aws ec2 associate-route-table \
--route-table-id <PublicRT_ID> \
--subnet-id <PublicSubnet1_ID> \
--profile qa
aws ec2 associate-route-table \
--route-table-id <PublicRT_ID> \
--subnet-id <PublicSubnet2_ID> \
--profile qa
[Public Subnet 2개에 Public RT 연결]
이제서야 PublicSubnet1과 PublicSubnet2가 진정한 Public Subnet이 되었다.
(Public Subnet은 인터넷과 연결되어 있는 Subnet을 의미하기 때문이다)

3.3.2 Private Subnet용 Route Table
Private Subnet은 DB와 같이 인터넷에서 직접 접근할 수 없어야 하는 리소스에 사용된다.
따라서 Outbound 트래픽이 IGW로 직접 라우팅 되어서는 안된다.
일단 RT를 생성하고 Subnet에 연결하기만 하자.

4. NAT Gateway 생성
Private Subnet 에서도 인터넷과 연결하고 싶을 수 있다.
그때 NAT Gateway를 사용하면 Outbound 트래픽을 시작함과 동시에 인터넷에서의 Inbound 트래픽을 방지할 수 있다.
어떤 외부 리소스를 다운로드 받거나 외부 서비스에 접근해야하는 인스턴스(ex. npm 패키지 설치)에겐 필수 설정이다.
4.1 Elastic IP 할당
NAT Gateway를 생성하려면 선행 작업으로 EIP가 필요하다.
aws ec2 allocate-address --domain vpc
[EIP 생성]
4.2 NAT Gateway 생성
생성한 EIP를 사용해 Public중 하나에 NAT Gateway를 생성하자.
aws ec2 create-nat-gateway \
--subnet-id <PublicSubnet1_ID> \
--allocation-id <EIPAllocationId> \
--tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=TutorialNATGW}]'
[PublicSubnet1에 TutorialNATGW 생성]
4.2.1 Route 추가
아까 만들었던 PrivateRT에 TutorialNATGW로 향하는 Route를 추가해주면
PrivateSubnet에서 인터넷에 접근할 수 있다.
aws ec2 create-route --route-table-id <PrivateRT_ID>
--destination-cidr-block 0.0.0.0/0
--nat-gateway-id <NAT_GW_ID> --profile qa
[PrivateRT에 0.0.0.0/0 → TutorialNATGW 추가]

지금은 튜토리얼이라 PublicSubnet1에 NATGW 하나만 배치해두고 두 PrivateSubnet에서 공통으로 사용하는 구조이다.
실제 프로덕션 환경의 경우 단일 장애 지점(SPOF)을 제거하기 위해 Private Subnet이 있는 각 AZ에 NATGW를 생성하는 것이 좋다.
5. Subnet에 --map-public-ip-on-launch 설정
시작한 인스턴스에 Public IP를 자동으로 할당하도록 Public Subnet 설정을 해주자.
aws ec2 modify-subnet-attribute
--subnet-id <PublicSubnet1_ID>
--map-public-ip-on-launch
aws ec2 modify-subnet-attribute
--subnet-id <PublicSubnet2_ID>
--map-public-ip-on-launch
이렇게 하면 Public Subnet에서 시작된 EC2 Instance가 기본적으로 Public IP 주소를 가지므로 인터넷에 접근할 수 있다.
6. Security Group
Security Group(SG)는 인스턴스에 대한 InBound 및 OutBound 트래픽을 제어하는 가상 방화벽이다.
웹 서버용 인스턴스와 데이터베이스 서버용 인스턴스를 생성하기전에, 먼저 그 두 서버에 붙일 SG를 생성해보자.
6.1 웹 서버용 SG 생성
aws ec2 create-security-group
--group-name WebServerSG
--description "Security group for web servers"
--vpc-id <VPC_ID>
--profile qa
[WebServerSG 생성]
aws ec2 authorize-security-group-ingress
--group-id <WebServerSG_ID>
--protocol tcp
--port 80
--cidr 0.0.0.0/0
--profile qa
aws ec2 authorize-security-group-ingress
--group-id <WebServerSG_ID>
--protocol tcp
--port 443
--cidr 0.0.0.0/0
--profile qa
[웹 서버로의 Inbound HTTP, HTTPS 트래픽 허용]
6.2 DB 서버용 SG 생성
aws ec2 create-security-group \
--group-name DBServerSG \
--description "Security group for database servers" \
--vpc-id <VPC_ID> \
--profile qa
[DBServerSG 생성]
aws ec2 authorize-security-group-ingress \
--group-id <DBServerSG_ID> \
--protocol tcp \
--port 3306 \
--source-group <WebServerSG_ID> \
--profile qa
[웹 서버에서의 MySQL/Aurora 트래픽만 허용]
포인트는 Inbound 패킷 출발지 IP가 WebServerSG에 존재하는 경우만 port 3306로 접근할 수 있다.
즉 WebServerSG를 사용하는 인스턴스만 DB 서버의 port 3306 포트로 연결할 수 있다.

7. EC2 인스턴스 배포
이제 인스턴스 배포를 위한 VPC 인프라 설정이 완료되었다.
마지막으로 EC2 인스턴스를 배포하고 아키텍처의 동작을 확인해보자.
Public Subnet에서 웹 서버를 시작하고 Private Subnet에서 DB 서버를 시작하자.
7.1 SSH 접속을 위한 key-pair 생성
aws ec2 create-key-pair \
--key-name TutorialKey \
--query 'KeyMaterial' \
--output text \
--profile qa > TutorialKey.pem
chmod 400 TutorialKey.pem
[SSH 접속용 TutorialKey key-pair 생성]
TutorialKey의 공개키는 AWS에 저장되고, 개인키는 로컬에 TutorialKey.pem으로 저장된다.
이후 보안을 위해 400(나만 Read 가능)으로 권한을 바꿔주자.
7.2 최신 Amazon Linux 2023 AMI 찾기
EC2 생성은 그저 빈 컴퓨터를 할당받는 것일 뿐이라서, 그 컴퓨터에 깔 OS가 필요하다.
AMI는 Amazon Machine Image의 약자로, EC2의 OS 설치 이미지이다.
그래서 EC2 인스턴스는 AMI 선택 → EC2 생성을 거쳐 OS가 이미 설치된 상태로 부팅된다.
먼저 인스턴스에 사용할 AMI를 찾자
aws ec2 describe-images --owners amazon \
--filters "Name=name,Values=al2023-ami-2023*-x86_64" "Name=state,Values=available" \
--query "sort_by(Images, &CreationDate)[-1].[ImageId,Name]" \
--output text \
--profile qa
[응답: ami-0ecfdfd1c8ae01aec al2023-ami-2023.10.20260302.1-kernel-6.1-x86_64]
응답으로 온 AMI ID를 이용해 인스턴스를 생성해보자.
7.3 Public Subnet에서 웹 서버 시작 후 인터넷으로 접근하기
웹 서버 역할의 EC2 인스턴스를 시작하자.
aws ec2 run-instances \
--image-id <AMI_ID> \
--count 1 \
--instance-type t2.micro \
--key-name TutorialKey \
--security-group-ids <WebServerSG_ID> \
--subnet-id <PublicSubnet1_ID> \
--user-data '#!/bin/bash
dnf update -y
dnf install -y httpd
rm -f /etc/httpd/conf.d/welcome.conf
echo "<h1>안녕하세요 박영진입니다</h1>" > /var/www/html/index.html
systemctl start httpd
systemctl enable httpd' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=WebServer}]' \
--profile qa
[PublicSubnet1에 WebServer 인스턴스 생성]
명령어의 자세한 설명은 다음과 같다.

aws ec2 describe-instances
--filters Name=tag:Name,Values=WebServer Name=instance-state-name,Values=running \
--query "Reservations[0].Instances[0].[InstanceId,PublicIpAddress]" \
--output text \
--profile qa
이 명령을 실행하면 웹 서버의 퍼블릭 IP 주소가 출력된다 (3.39.228.231)
이제 웹 브라우저에서 http://3.39.228.231/ URL로 접속할 수 있다.


웹 서버가 Public Subnet에 있고 Public IP가 있으며 SG로 80 포트를 열어두었기 때문에, 인터넷을 통해 접속할 수 있다.
7.4 Private Subnet에서 DB 서버 시작
aws ec2 run-instances \
--image-id <AMI_ID> \
--count 1 \
--instance-type t2.micro \
--key-name TutorialKey \
--security-group-ids <DBServerSG_ID> \
--subnet-id <PrivateSubnet1_ID> \
--user-data '#!/bin/bash
dnf update -y
dnf install -y mariadb105-server
systemctl start mariadb
systemctl enable mariadb' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=DBServer}]' \
--profile qa
[PrivateSubnet1에 DBServer 인스턴스 생성]

Private Subnet에서 생성된 DBServer는 Public IP가 없고 Private IP만 존재한다(10.0.2.60).
그래서 인터넷으로는 DB Server에 접속할 수 없다.
아까 DBServerSG에서 WebServerSG는 3306포트로 접속할 수 있게 열어주었다.
다만 지금은 WebServer에서 DB를 연동하진 않을거라, 웹 서버에서 DB 서버로 SSH 접속이 되는걸 확인함으로써 Security Group이 잘 동작하는지 알아보자.
먼저 DBServerSG에서 22번 포트를 열어주고,
로컬에서 웹 서버로 SSH 연결을 한 뒤 웹 서버에서 DB 서버로 SSH 연결을 해보자.

DB 서버를 만들때도 웹 서버와 동일한 key-pair를 사용했기 때문에 같은 key로 SSH 접속을 할 수 있다.
마지막으로 DB 서버에 MariaDB가 잘 돌아가고 있는지 확인해보자.
sudo systemctl status mariadb
[mariadb 상태 보기]

즉 DB 서버 인스턴스 생성 시 작성해준 user-data 스크립트가 잘 실행되어 인터넷에서 mariadb 패키지 다운로드에 성공한 것이다.
정리하면,
Inbound는 안 되지만 Outbound는 NAT Gateway를 통해 가능하고, 내부 접속은 같은 VPC의 Private IP로 통신할 수 있다.
8. 리소스 정리
VPC 사용이 끝나면 요금이 발생하지 않도록 리소스를 정리해야한다.
의존성 때문에 생성의 역순으로 삭제해야한다.
그림과 함께 보면서 명령어를 하나씩 따라가보자.
# 1. EC2 인스턴스 종료
aws ec2 terminate-instances --instance-ids <WebServer_ID> <DBServer_ID> --profile qa
aws ec2 wait instance-terminated --instance-ids <WebServer_ID> <DBServer_ID> --profile qa
# 2. Key Pair 삭제
aws ec2 delete-key-pair --key-name TutorialKey --profile qa
rm TutorialKey.pem
# 3. NAT Gateway 삭제
aws ec2 delete-nat-gateway --nat-gateway-id <NAT_GW_ID> --profile qa
aws ec2 wait nat-gateway-deleted --nat-gateway-ids <NAT_GW_ID> --profile qa
# 4. Elastic IP 해제
aws ec2 release-address --allocation-id <EIP_ID> --profile qa
# 5. Security Group 삭제 (DBServerSG가 WebServerSG를 참조하므로 먼저 삭제)
aws ec2 delete-security-group --group-id <DBServerSG_ID> --profile qa
aws ec2 delete-security-group --group-id <WebServerSG_ID> --profile qa
# 6. Route Table 삭제
# 먼저 연결 ID를 확인한다
aws ec2 describe-route-tables --route-table-id <PublicRT_ID> --profile qa
aws ec2 describe-route-tables --route-table-id <PrivateRT_ID> --profile qa
# 서브넷에서 Route Table 연결을 해제한다
aws ec2 disassociate-route-table --association-id <연결ID_1> --profile qa
aws ec2 disassociate-route-table --association-id <연결ID_2> --profile qa
aws ec2 disassociate-route-table --association-id <연결ID_3> --profile qa
aws ec2 disassociate-route-table --association-id <연결ID_4> --profile qa
# Route Table을 삭제한다
aws ec2 delete-route-table --route-table-id <PublicRT_ID> --profile qa
aws ec2 delete-route-table --route-table-id <PrivateRT_ID> --profile qa
# 7. Internet Gateway 분리 및 삭제
aws ec2 detach-internet-gateway --internet-gateway-id <IGW_ID> --vpc-id <VPC_ID> --profile qa
aws ec2 delete-internet-gateway --internet-gateway-id <IGW_ID> --profile qa
# 8. Subnet 삭제
aws ec2 delete-subnet --subnet-id <PublicSubnet1_ID> --profile qa
aws ec2 delete-subnet --subnet-id <PublicSubnet2_ID> --profile qa
aws ec2 delete-subnet --subnet-id <PrivateSubnet1_ID> --profile qa
aws ec2 delete-subnet --subnet-id <PrivateSubnet2_ID> --profile qa
# 9. VPC 삭제
aws ec2 delete-vpc --vpc-id <VPC_ID> --profile qa
9. 마무리
9.1 프로덕션 환경에서 고려할 점
이 튜토리얼은 VPC의 기본 구조를 이해하기 위해 설계되었다. 프로덕션 환경에서는 다음을 고려하자.
Security Group 규칙 강화 — 튜토리얼에서는 Bastion 접속을 위해 SSH(22)를 0.0.0.0/0으로 열었다. 웹 서버의 HTTP/HTTPS는 0.0.0.0/0이 맞지만, SSH와 같은 관리용 포트는 회사 VPN IP 등 특정 범위로 제한해야 한다.
고가용성 — NAT Gateway를 PublicSubnet1 하나에만 배치했는데, AZ-a에 장애가 나면 AZ-c의 Private Subnet도 인터넷 연결이 끊긴다. 각 AZ에 NAT Gateway를 배치하여 단일 장애 지점(SPOF)을 제거하자.
Network ACL — Security Group은 인스턴스 수준, Network ACL은 Subnet 수준의 방화벽이다. 두 계층을 함께 사용하면 SG를 잘못 설정해도 NACL이 2차 방어선 역할을 한다.
VPC Flow Logs — VPC 내 네트워크 트래픽을 로깅하는 기능이다. 보안 사고 발생 시 어떤 IP에서 어떤 포트로 접근했는지 추적할 수 있다.
리소스 태그 지정 — 튜토리얼에서는 Name 태그만 붙였지만, 프로덕션에서는 Environment, Team, Project 등 체계적인 태그가 필요하다. 리소스가 수십~수백 개가 되면 태그 없이는 비용 추적과 관리가 불가능하다.
9.2 다음 단계
VPC 인프라가 갖춰졌으니 다음을 시도해볼 수 있다.
- ALB를 배포하여 여러 인스턴스에 트래픽을 분산한다
- Auto Scaling 그룹으로 트래픽에 따라 인스턴스를 자동 확장/축소한다
- Private Subnet에 RDS를 배치하여 관리형 데이터베이스를 사용한다
- VPC Peering으로 다른 VPC와 연결한다
- VPN 연결로 온프레미스 네트워크와 연결한다
'Infrastructure' 카테고리의 다른 글
| CLI로 만든 VPC를 CloudFormation으로 다시 만들기 (2) — 전체 네트워크 (0) | 2026.03.27 |
|---|---|
| CLI로 만든 VPC를 CloudFormation으로 다시 만들기 (1) — 템플릿 기초 (0) | 2026.03.24 |
| AWS ECS 키워드 (0) | 2025.10.27 |
| 쉘 파일 이용하여 AWS Parameter store에 .env 파일 한번에 업로드 하기 (0) | 2025.08.22 |
| AWS SQS + Lambda 구성 (2) | 2025.08.21 |