https://pabeba.tistory.com/301
지난 포스팅에서 커스텀 VPC와 퍼블릭 서브넷(Public Subnet)의 기본 뼈대를 만든 데 이어, 오늘은 보안의 핵심인 프라이빗 서브넷(Private Subnet)을 구축하고, 외부 패키지 설치를 위한 NAT 게이트웨이(NAT Gateway) 연동 및 Web-WAS-DB 3-Tier 보안 그룹 체계를 테라폼(Terraform)으로 구현해 보겠습니다.

1. 목표 시스템 아키텍처
우리가 구축할 네트워크 아키텍처는 외부 인터넷 망과 직접 통신하는 Public 영역과, 외부 직접 접근이 전면 차단된 안전한 Private 영역으로 명확히 나뉩니다.
Internet
│
Internet Gateway
│
Public Route Table
│
┌──────────────────┴──────────────────┐
│ │
Public Subnet (10.0.1.0/24) Private Subnet (10.0.2.0/24)
│ │
┌─────────┴─────────┐ ┌─────────┴─────────┐
│ Web EC2 │ ───(SSH)───► │ WAS EC2 │
│ Nginx │ │ FastAPI │
└───────────────────┘ └─────────┬─────────┘
│
┌─────────┴─────────┐
│ DB EC2 │
│ MySQL │
└───────────────────┘
리소스 계층 구조
- VPC (10.0.0.0/16)
- Internet Gateway (IGW)
- Public Route Table
- Public Subnet (10.0.1.0/24): Web EC2 (EIP 및 Nginx) 배치
- Public Route Table
- NAT Gateway (Public Subnet에 소속, EIP 할당)
- Private Route Table
- Private Subnet (10.0.2.0/24): WAS EC2 (FastAPI), DB EC2 (MySQL) 배치
- Private Route Table
- Internet Gateway (IGW)
2. 핵심 설계 포인트 & 지식 더하기
1) 보안 그룹 체인 (Security Group Chaining)
WAS나 DB와 같은 핵심 리소스는 CIDR(0.0.0.0/0) 기준 오픈이 아니라, "오직 이전 계층의 보안 그룹에서 들어오는 트래픽만 허용"하도록 aws_security_group_rule을 활용해 타이트하게 제한합니다.
- Web SG: 0.0.0.0/0에 대해 80(HTTP), 22(SSH) 개방
- WAS SG: 오직 Web SG로부터 들어오는 8000 포트 및 SSH(22)만 허용
- DB SG: 오직 WAS SG로부터 들어오는 3306 포트 및 SSH(22)만 허용
2) Bastion 동작 방식을 대체하는 Jump-Host SSH 접근
외부에서 Private Subnet의 WAS/DB에 직접 SSH 접속은 불가능합니다. 따라서 Public Subnet에 위치한 Web EC2에 SSH 키(.pem)를 업로드한 후, Web EC2 터미널 내부에서 Private IP를 지정해 WAS/DB 서버로 접속하는 방식을 사용합니다.
💡 실무 팁 (Systems Manager)
프로덕션 환경에서는 SSH 키 보관 보안 위험 때문에 AWS SSM (Systems Manager Session Manager) 방식을 주로 적용합니다. 이번 실습에서는 기본 SSH 체이닝 방식을 이용합니다.
3) NAT Gateway의 역할 및 쿼터(Limit) 이슈
Private Subnet 내부 리소스는 외부 인터넷에서 들어오는(Inbound) 접속은 불가능하지만, 파이프라인 상 파이썬 패키지 설치(pip install)나 리눅스 패키지 업데이트(dnf update)를 위해 외부로 나가는(Outbound/Pull) 통신이 필요합니다. 이를 위해 Public Subnet에 NAT Gateway를 배치하고 Private Route Table에 0.0.0.0/0 -> NAT GW 경로를 설정합니다.
⚠️ 테라폼 실행 시 흔히 만나는 에러 해결 (NatGatewayLimitExceeded)
Error: creating EC2 NAT Gateway: ... api error NatGatewayLimitExceeded: Performing this operation would exceed the limit of 5 NAT gateways원인: AWS 계정 및 리전당 할당된 EIP 및 NAT Gateway 기본 쿼터(기본 5개)를 초과했을 때 발생합니다.
해결: AWS 콘솔에서 기존에 사용하지 않고 남아있는 NAT Gateway나 EIP를 제거 후 다시 terraform apply를 수행해야 합니다.
3. 전체 테라폼 코드 실습
이제 복잡한 네트워크와 인스턴스, 보안 그룹 설정을 깔끔하게 작성된 테라폼 통합 코드 하나로 구축해 보겠습니다.
아래 코드 분석은 다음시간에 진행해보겠습니다. ㅎㅎ
main.tf / vpc.tf 통합 코드
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.region
}
# ================= ==============================================
# 1. VPC & 게이트웨이 구성
# ================================================================
resource "aws_vpc" "DE-AI-07-COMPANY" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "DE-AI-07-COMPANY"
}
}
# Internet Gateway (Public용)
resource "aws_internet_gateway" "company" {
vpc_id = aws_vpc.DE-AI-07-COMPANY.id
tags = {
Name = "DE-AI-07-COMPANY-igw"
}
}
# NAT Gateway용 Elastic IP
resource "aws_eip" "DE-AI-07-COMPANY-nat-eip" {
domain = "vpc"
tags = {
Name = "DE-AI-07-COMPANY-nat-eip"
}
}
# Public 서브넷에 NAT Gateway 배치 (Outbound 전용)
resource "aws_nat_gateway" "company_nat_gw" {
allocation_id = aws_eip.DE-AI-07-COMPANY-nat-eip.id
subnet_id = aws_subnet.public.id
tags = {
Name = "DE-AI-07-COMPANY-nat-gw"
}
depends_on = [aws_internet_gateway.company]
}
# ================= ==============================================
# 2. 서브넷 & 라우팅 테이블 구성
# ================================================================
# Public Subnet
resource "aws_subnet" "public" {
vpc_id = aws_vpc.DE-AI-07-COMPANY.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-2a"
map_public_ip_on_launch = true
tags = {
Name = "DE-AI-07-COMPANY-public-subnet"
}
}
# Private Subnet
resource "aws_subnet" "private" {
vpc_id = aws_vpc.DE-AI-07-COMPANY.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-northeast-2a"
tags = {
Name = "DE-AI-07-COMPANY-private-subnet"
}
}
# Public Route Table (IGW 바인딩)
resource "aws_route_table" "public" {
vpc_id = aws_vpc.DE-AI-07-COMPANY.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.company.id
}
tags = {
Name = "DE-AI-07-COMPANY-public-rt"
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
# Private Route Table (NAT Gateway 바인딩)
resource "aws_route_table" "private" {
vpc_id = aws_vpc.DE-AI-07-COMPANY.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.company_nat_gw.id
}
tags = {
Name = "DE-AI-07-COMPANY-private-rt"
}
}
resource "aws_route_table_association" "private" {
subnet_id = aws_subnet.private.id
route_table_id = aws_route_table.private.id
}
# ================= ==============================================
# 3. 보안 그룹(Security Group) 동적 생성 및 룰 정의
# ================================================================
locals {
security_groups = {
web = {
ingress = {
ssh = { port = 22, cidr = ["0.0.0.0/0"] }
http = { port = 80, cidr = ["0.0.0.0/0"] }
}
}
was = {
ingress = {
ssh = { port = 22, cidr = ["0.0.0.0/0"] }
}
}
db = {
ingress = {
ssh = { port = 22, cidr = ["0.0.0.0/0"] }
}
}
}
}
resource "aws_security_group" "sg" {
for_each = local.security_groups
name_prefix = "DE-AI-07-${each.key}-SG-"
description = "${upper(each.key)} Security Group"
vpc_id = aws_vpc.DE-AI-07-COMPANY.id
dynamic "ingress" {
for_each = each.value.ingress
content {
protocol = "tcp"
from_port = ingress.value.port
to_port = ingress.value.port
cidr_blocks = ingress.value.cidr
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "DE-AI-07-${each.key}-SG"
}
}
# 보안 그룹 체이닝 룰: Web -> WAS (8000 포트)
resource "aws_security_group_rule" "web-to-was" {
type = "ingress"
source_security_group_id = aws_security_group.sg["web"].id
security_group_id = aws_security_group.sg["was"].id
from_port = 8000
to_port = 8000
protocol = "tcp"
}
# 보안 그룹 체이닝 룰: WAS -> DB (3306 포트)
resource "aws_security_group_rule" "was-to-db" {
type = "ingress"
source_security_group_id = aws_security_group.sg["was"].id
security_group_id = aws_security_group.sg["db"].id
from_port = 3306
to_port = 3306
protocol = "tcp"
}
# ================= ==============================================
# 4. EC2 인스턴스 자동 생성 (Web, WAS, DB)
# ================================================================
locals {
servers = {
web = {
subnet = aws_subnet.public.id
sg = aws_security_group.sg["web"].id
}
was = {
subnet = aws_subnet.private.id
sg = aws_security_group.sg["was"].id
}
db = {
subnet = aws_subnet.private.id
sg = aws_security_group.sg["db"].id
}
}
}
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
}
resource "aws_instance" "server" {
for_each = local.servers
instance_type = var.instance_type
ami = data.aws_ami.amazon_linux.id
subnet_id = each.value.subnet
key_name = var.key_name
vpc_security_group_ids = [each.value.sg]
tags = {
Name = "DE-AI-07-ap2-${upper(each.key)}"
}
user_data = <<-EOF
#!/bin/bash
dnf install -y ec2-instance-connect
EOF
}
# ================= ==============================================
# 5. 변수 선언
# ================================================================
variable "region" {
type = string
default = "ap-northeast-2"
}
variable "instance_type" {
type = string
default = "t3.micro"
}
variable "key_name" {
type = string
default = "de-ai-07-rsa"
}
4. 검증 및 SSH 연결 테스트
1) 테라폼 명령어 실행
terraform fmt
terraform validate
terraform plan
terraform apply
테라폼 적용이 완료되면 AWS 콘솔의 VPC ➔ 리소스 맵(Resource Map)에서 Public/Private 서브넷과 각각 연결된 라우팅 테이블, 인터넷/NAT 게이트웨이 연결 구성을 한눈에 확인할 수 있습니다.
2) SSH 연결 테스트 (Web ➔ WAS)
- pem 키 Web EC2에 이동
scp -i .\de-ai-07-rsa.pem de-ai-07-rsa.pem ec2-user@[web ec2 ip]:~/
- Web EC2에 SSH 접속: AWS Console 또는 로컬 터미널을 통해 Public IP가 부여된 Web 서버에 접속합니다.
- SSH 키파일 업로드 및 권한 조정:
# Web EC2 터미널 내부 chmod 400 DE-AI-25-TEST.pem - WAS 서버(Private IP) 접속:
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes , #_ ~\_ ####_ Amazon Linux 2023 ~~ \_#####\ ~~ \###| ~~ \#/ ___ https://aws.amazon.com/linux/amazon-linux-2023
정상적으로 Amazon Linux 2023 웰컴 메시지가 나타난다면, Public 서브넷을 통한 Private 서브넷 보안 접속이 성공한 것입니다!
-
ssh -i DE-AI-25-TEST.pem ec2-user@10.0.2.180
🖐️ 요약 및 정리
- Private Subnet: 외부 직접 접근을 막고, 내부 통신 및 NAT Gateway를 통한 Outbound 인터넷 접근만 허용합니다.
- 보안 그룹 체이닝: CIDR 기반 접속 허용 대신 source_security_group_id를 지정하여 이전 계층의 보안 그룹에서 들어오는 특정 포트만 차단/허용합니다.
- Terraform 유연성: locals, for_each, dynamic 블록을 적극 활용하여 Web, WAS, DB 인스턴스와 보안 그룹을 코드 중복 없이 효율적으로 작성할 수 있습니다.