데이터베이스 접근 관리 솔루션을 AWS에 배포하면서 Network Load Balancer와 Security Group을 활용한 Zero Trust 아키텍처를 구축한 경험을 공유합니다. Terraform으로 완전 자동화하고, 보안과 가용성을 모두 확보했습니다.
🎯 배경: 왜 데이터베이스 접근 게이트웨이가 필요한가?
많은 기업에서 여러 팀이 수십 개의 데이터베이스를 사용합니다:
- RDS MySQL/PostgreSQL 클러스터
- ElastiCache Redis
- DocumentDB (MongoDB 호환)
- EKS 클러스터 내부 데이터베이스
문제점:
- 개발자마다 각자 DB 접속 정보 관리
- 퇴사자 계정 관리의 어려움
- 접속 이력 추적 불가
- 프로덕션 DB에 대한 직접 접근
- 보안 감사의 어려움
해결책: 중앙화된 데이터베이스 접근 게이트웨이 구축
🏗️ 아키텍처 개요
전체 구성도

주요 구성 요소
- Network Load Balancer (NLB)
- Layer 4 로드 밸런서
- TLS 종료 처리
- Multi-AZ 고가용성
- Gateway EC2 Instance
- 데이터베이스 접근 프록시
- 세션 관리 및 감사 로깅
- 사용자 권한 제어
- Security Groups
- 계층별 방화벽 규칙
- 최소 권한 원칙 적용
- Cross-VPC 통신 지원
- Zero Trust Gateway
- 조건부 접근 제어
- 다중 인증 (MFA)
- 디바이스 신뢰 검증
🔧 Network Load Balancer 구성
왜 NLB를 선택했나?
비교: ALB vs NLB
| 기능 | ALB | NLB | 우리의 선택 |
|------|-----|-----|------------|
| 계층 | Layer 7 | Layer 4 | ✅ NLB |
| 프로토콜 | HTTP/HTTPS/gRPC | TCP/TLS/UDP | ✅ NLB |
| Static IP | ❌ | ✅ | ✅ NLB |
| Client IP 보존 | 헤더로 전달 | 네이티브 지원 | ✅ NLB |
| 성능 | ~50ms | <1ms | ✅ NLB |
| 비용 | 높음 | 낮음 | ✅ NLB |
NLB 선택 이유:
- 낮은 지연시간: 데이터베이스 작업에 중요
- Client IP 보존: 감사 로그에 실제 사용자 IP 필요
- TCP 지원: 커스텀 포트 (9995) 사용
- 비용 효율: 월 ~$20 (ALB는 ~$30)
NLB 설정 (Terraform)
# main.tf
resource "aws_lb" "gateway_nlb" {
name = "gateway-nlb"
internal = true # 내부 전용
load_balancer_type = "network"
ip_address_type = "ipv4"
# 보안 그룹 연결
security_groups = [aws_security_group.nlb_sg.id]
# Multi-AZ 배포
subnet_mapping {
subnet_id = data.aws_subnet.private_az_a.id
# private_ipv4_address = "10.x.x.0" # 고정 IP (선택)
}
subnet_mapping {
subnet_id = data.aws_subnet.private_az_b.id
# private_ipv4_address = "10.x.x.0" # 고정 IP (선택)
}
# Cross-zone 로드 밸런싱
enable_cross_zone_load_balancing = true
# 삭제 보호 (프로덕션)
enable_deletion_protection = true
tags = {
Name = "gateway-nlb"
Environment = "production"
ManagedBy = "Terraform"
}
}
Target Group 구성
중요한 발견: 포트 매핑 이슈 🚨
처음에는 이렇게 설정했습니다:
# ❌ 잘못된 설정
resource "aws_lb_target_group" "gateway_https" {
port = 443 # NLB가 443으로 받으면 백엔드도 443?
protocol = "TCP"
# ...
}
문제점:
- Gateway 애플리케이션은 실제로 80번 포트에서 실행 중
- NLB가 443으로 받아서 443으로 전달하니 Health Check 실패!
- Target이 계속 Unhealthy 상태
해결책: TLS 종료는 NLB에서, 백엔드는 HTTP
# ✅ 올바른 설정
resource "aws_lb_target_group" "gateway_https" {
name = "gateway-tg-https"
port = 80 # 실제 백엔드 포트!
protocol = "TCP"
vpc_id = data.aws_vpc.selected.id
target_type = "instance"
# Health Check
health_check {
enabled = true
protocol = "TCP"
port = "traffic-port" # 80 포트 체크
healthy_threshold = 3
unhealthy_threshold = 3
interval = 30
}
# Connection Draining
deregistration_delay = 300 # 5분
# Client IP 보존
preserve_client_ip = true
# Session Stickiness (Source IP 기반)
stickiness {
enabled = true
type = "source_ip"
}
lifecycle {
create_before_destroy = true
}
}
TLS Listener 구성
resource "aws_lb_listener" "gateway_https" {
load_balancer_arn = aws_lb.gateway_nlb.arn
port = 443
protocol = "TLS"
# 최신 TLS 정책
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-Res-2021-06"
# ACM 인증서
certificate_arn = data.aws_acm_certificate.gateway.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.gateway_https.arn
}
}
# 커스텀 포트 (API용)
resource "aws_lb_listener" "gateway_custom" {
load_balancer_arn = aws_lb.gateway_nlb.arn
port = 9995
protocol = "TCP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.gateway_custom.arn
}
}
Target 등록
# HTTPS Target
resource "aws_lb_target_group_attachment" "gateway_https" {
target_group_arn = aws_lb_target_group.gateway_https.arn
target_id = "i-0abcdef1234567890" # Gateway EC2 Instance
port = 80 # 실제 포트!
}
# Custom Port Target
resource "aws_lb_target_group_attachment" "gateway_custom" {
target_group_arn = aws_lb_target_group.gateway_custom.arn
target_id = "i-0abcdef1234567890"
port = 9995
}
🔒 Security Group 구성
계층별 보안 전략
┌────────────────────────────────┐
│ Zero Trust Gateway SG │
│ (Private Network) │
└────────────┬───────────────────┘
│ Allow 80,443,9995
▼
┌────────────────────────────────┐
│ NLB Security Group │
└────────────┬───────────────────┘
│ Allow 80,443,9995
▼
┌────────────────────────────────┐
│ Gateway Instance SG │
└────────────┬───────────────────┘
│ Allow DB Ports
▼
┌────────────────────────────────┐
│ Database Security Groups │
│ • MySQL: 3306 │
│ • PostgreSQL: 5432 │
│ • MongoDB: 27017 │
│ • Redis: 6379 │
└────────────────────────────────┘
NLB Security Group
# NLB용 Security Group
resource "aws_security_group" "nlb" {
name = "gateway-nlb-sg"
description = "Security group for Gateway NLB"
vpc_id = data.aws_vpc.selected.id
tags = {
Name = "gateway-nlb-sg"
}
}
# Ingress: Zero Trust Gateway에서만 접근 허용
resource "aws_security_group_rule" "nlb_ingress_https" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [
"10.x.x.0/24", # Zero Trust Gateway AZ-A
"10.x.x.0/24" # Zero Trust Gateway AZ-B
]
security_group_id = aws_security_group.nlb.id
description = "HTTPS from Zero Trust Gateway"
}
resource "aws_security_group_rule" "nlb_ingress_custom" {
type = "ingress"
from_port = 9995
to_port = 9995
protocol = "tcp"
cidr_blocks = [
"10.x.x.0/24",
"10.x.x.0/24"
]
security_group_id = aws_security_group.nlb.id
description = "Custom API port from Zero Trust Gateway"
}
# Egress: Gateway Instance로만 전달
resource "aws_security_group_rule" "nlb_egress_to_gateway" {
type = "egress"
from_port = 80
to_port = 80
protocol = "tcp"
source_security_group_id = aws_security_group.gateway_instance.id
security_group_id = aws_security_group.nlb.id
description = "Forward to Gateway Instance"
}
Gateway Instance Security Group
# Gateway Instance Security Group
resource "aws_security_group" "gateway_instance" {
name = "gateway-instance-sg"
description = "Security group for Gateway EC2 instance"
vpc_id = data.aws_vpc.selected.id
}
# === INGRESS RULES ===
# NLB에서 들어오는 트래픽
resource "aws_security_group_rule" "gateway_ingress_from_nlb" {
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
source_security_group_id = aws_security_group.nlb.id
security_group_id = aws_security_group.gateway_instance.id
description = "HTTP from NLB"
}
# Zero Trust Gateway에서 직접 접근 (백업 경로)
resource "aws_security_group_rule" "gateway_ingress_from_ztg" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["10.x.x.0/24", "10.x.x.0/24"]
security_group_id = aws_security_group.gateway_instance.id
description = "HTTPS from Zero Trust Gateway (direct)"
}
# === EGRESS RULES ===
# MySQL 데이터베이스 접근
resource "aws_security_group_rule" "gateway_egress_mysql" {
type = "egress"
from_port = 3306
to_port = 3306
protocol = "tcp"
cidr_blocks = [
"10.x.x.0/22", # DB Subnet AZ-A
"10.x.x.0/22", # DB Subnet AZ-B
"10.x.x.0/22", # DB Subnet AZ-C
"10.x.x.0/19", # EKS Private Nodes AZ-A
"10.x.x.0/19", # EKS Private Nodes AZ-B
"10.x.x.0/19" # EKS Private Nodes AZ-C
]
security_group_id = aws_security_group.gateway_instance.id
description = "MySQL access to RDS and EKS"
}
# PostgreSQL 데이터베이스 접근
resource "aws_security_group_rule" "gateway_egress_postgresql" {
type = "egress"
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = [
"10.x.x.0/22",
"10.x.x.0/22",
"10.x.x.0/22"
]
security_group_id = aws_security_group.gateway_instance.id
description = "PostgreSQL access"
}
# MongoDB/DocumentDB 접근
resource "aws_security_group_rule" "gateway_egress_mongodb" {
type = "egress"
from_port = 27017
to_port = 27019
protocol = "tcp"
cidr_blocks = [
"10.x.x.0/22",
"10.x.x.0/22",
"10.x.x.0/22"
]
security_group_id = aws_security_group.gateway_instance.id
description = "MongoDB/DocumentDB access"
}
# Redis/ElastiCache 접근
resource "aws_security_group_rule" "gateway_egress_redis" {
type = "egress"
from_port = 6379
to_port = 6379
protocol = "tcp"
cidr_blocks = [
"10.x.x.0/24", # ElastiCache AZ-A
"10.x.x.0/24", # ElastiCache AZ-B
"10.x.x.0/24" # ElastiCache AZ-C
]
security_group_id = aws_security_group.gateway_instance.id
description = "Redis/ElastiCache access"
}
# HTTPS 외부 API 접근 (Slack, AWS Services)
resource "aws_security_group_rule" "gateway_egress_https" {
type = "egress"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.gateway_instance.id
description = "HTTPS to external APIs"
}
Database Security Groups
# RDS Security Group에 Gateway 접근 허용
resource "aws_security_group_rule" "rds_from_gateway" {
type = "ingress"
from_port = 5432
to_port = 5432
protocol = "tcp"
source_security_group_id = aws_security_group.gateway_instance.id
security_group_id = data.aws_security_group.rds.id
description = "PostgreSQL from Gateway ONLY"
}
# Cross-VPC: 다른 VPC의 DB에 접근 허용 (예시)
resource "aws_security_group_rule" "external_mysql_from_gateway" {
for_each = toset(data.aws_security_groups.external_mysql.ids)
type = "ingress"
from_port = 3306
to_port = 3306
protocol = "tcp"
source_security_group_id = aws_security_group.gateway_instance.id
security_group_id = each.value
description = "MySQL from Gateway (cross-VPC)"
}
💰 비용 분석
NLB 월간 비용
| 항목 | 계산 | 비용 |
|---|---|---|
| NLB 기본 | $0.0225/시간 × 720시간 | $16.20 |
| NLCU (최소) | $0.006/NLCU × 0.5 × 720시간 | $2.16 |
| Cross-AZ 트래픽 | $0.01/GB × 100GB | $1.00 |
| 합계 | ~$19.36/월 |
비용 절감 팁
- Stickiness 활용: 무료 기능, 추가 비용 없음
- Health Check 최적화: TCP 기반 무료
- Cross-Zone LB: 필요시에만 활성화
- Old NLB 제거: 중복 NLB 삭제로 월 $16 절약
🔍 트러블슈팅
1. Target Unhealthy 문제
증상:
$ aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:region:account-id:targetgroup/... \
--region ap-northeast-2
{
"TargetHealthDescriptions": [{
"Target": { "Id": "i-xxxxx", "Port": 443 },
"HealthCheckPort": "443",
"TargetHealth": {
"State": "unhealthy",
"Reason": "Target.FailedHealthChecks"
}
}]
}
원인:
- Gateway 애플리케이션이 80번 포트에서 실행 중
- Target Group은 443번 포트를 Health Check하고 있음
해결:
# Before
resource "aws_lb_target_group" "gateway_https" {
port = 443 # ❌ 잘못됨
}
# After
resource "aws_lb_target_group" "gateway_https" {
port = 80 # ✅ 올바름
health_check {
port = "traffic-port" # 80 포트 체크
}
}
검증:
# EC2 인스턴스에서 포트 확인
sudo netstat -tlnp | grep -E ':(80|9995)'
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 12345/gateway
tcp 0 0 0.0.0.0:9995 0.0.0.0:* LISTEN 12345/gateway
# Health Check 재확인
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:region:account-id:targetgroup/... \
--region ap-northeast-2
# State: healthy ✅
2. Security Group 규칙 충돌
증상:
Error: error creating Security Group Rule: InvalidPermission.Duplicate
원인:
- AWS 콘솔에서 수동으로 생성한 규칙과 Terraform 규칙 충돌
해결:
# 조건부 생성으로 충돌 방지
resource "aws_security_group_rule" "gateway_ingress_legacy" {
count = var.enable_legacy_rules ? 1 : 0
# ...
}
# terraform.tfvars
enable_legacy_rules = false # 기존 규칙 유지
3. Cross-VPC 통신 실패
증상:
- Gateway에서 다른 VPC의 데이터베이스 연결 안 됨
원인:
- VPC Peering/Transit Gateway 미설정
- 라우팅 테이블 누락
해결:
# 1. VPC Peering 확인
aws ec2 describe-vpc-peering-connections \
--filters "Name=status-code,Values=active"
# 2. 라우팅 테이블 확인
aws ec2 describe-route-tables \
--filters "Name=vpc-id,Values=vpc-xxxxx"
# 3. Security Group CIDR 규칙 사용
resource "aws_security_group_rule" "cross_vpc_mysql" {
cidr_blocks = ["10.x.0.0/16"] # 대상 VPC CIDR
# ...
}
📊 모니터링 및 알림
CloudWatch 대시보드
resource "aws_cloudwatch_dashboard" "gateway_nlb" {
dashboard_name = "gateway-nlb-monitoring"
dashboard_body = jsonencode({
widgets = [
{
type = "metric"
properties = {
metrics = [
["AWS/NetworkELB", "ActiveFlowCount", {
stat = "Average"
id = "m1"
}],
[".", "HealthyHostCount", {
stat = "Average"
id = "m2"
}],
[".", "UnHealthyHostCount", {
stat = "Average"
id = "m3"
}]
]
period = 300
region = "ap-northeast-2"
title = "NLB Health Metrics"
}
}
]
})
}
CloudWatch Alarms
# Unhealthy Target 알림
resource "aws_cloudwatch_metric_alarm" "unhealthy_targets" {
alarm_name = "gateway-nlb-unhealthy-targets"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "UnHealthyHostCount"
namespace = "AWS/NetworkELB"
period = 60
statistic = "Average"
threshold = 0
alarm_description = "NLB has unhealthy targets"
alarm_actions = [aws_sns_topic.alerts.arn]
dimensions = {
LoadBalancer = aws_lb.gateway_nlb.arn_suffix
TargetGroup = aws_lb_target_group.gateway_https.arn_suffix
}
}
# High Connection Count 알림
resource "aws_cloudwatch_metric_alarm" "high_connections" {
alarm_name = "gateway-nlb-high-connections"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "ActiveFlowCount"
namespace = "AWS/NetworkELB"
period = 300
statistic = "Average"
threshold = 10000
alarm_description = "High number of active connections"
alarm_actions = [aws_sns_topic.alerts.arn]
dimensions = {
LoadBalancer = aws_lb.gateway_nlb.arn_suffix
}
}
🚀 배포 가이드
1. 사전 준비
# 필수 리소스 확인
- VPC ID
- Subnet IDs (최소 2개 AZ)
- ACM Certificate ARN
- Gateway EC2 Instance
2. Terraform 초기화
cd infrastructure/nlb/gateway
terraform init
3. 변수 설정
# terraform.tfvars
vpc_id = "vpc-xxxxx"
private_subnet_az_a = "subnet-xxxxx"
private_subnet_az_b = "subnet-xxxxx"
target_instance_ids = ["i-xxxxx"]
certificate_domain = "gateway.example.com"
enable_http = false
4. 계획 확인
terraform plan
5. 적용
terraform apply
6. 검증
# Target Health 확인
aws elbv2 describe-target-health \
--target-group-arn $(terraform output -raw target_group_https_arn)
# 연결 테스트
curl -k https://gateway.example.com
curl -k https://gateway.example.com:9995/api/health
📋 운영 체크리스트
일일 점검
- Target Health Status 확인
- CloudWatch Alarms 확인
- 접속 로그 리뷰
주간 점검
- NLB Metrics 리뷰 (ActiveFlowCount, ProcessedBytes)
- Security Group 규칙 감사
- 비용 모니터링
월간 점검
- SSL/TLS 인증서 만료일 확인
- Target Group 설정 최적화
- 아키텍처 리뷰
🎓 교훈 및 베스트 프랙티스
배운 점
- 포트 매핑 확인은 필수
- NLB Listener Port ≠ Backend Port
- Health Check는 실제 애플리케이션 포트로 설정
- Security Group은 계층별로
- NLB → Gateway → Database
- 각 계층마다 명확한 규칙
- CIDR vs Security Group 참조
- 같은 VPC: Security Group 참조 권장
- Cross-VPC: CIDR 기반 규칙 사용
- Terraform State 관리
- S3 Backend 사용
- State Lock (DynamoDB) 필수
베스트 프랙티스
보안:
- ✅ 최소 권한 원칙 적용
- ✅ Zero Trust 아키텍처
- ✅ 정기적인 보안 감사
- ✅ 모든 접근 로깅
가용성:
- ✅ Multi-AZ 배포
- ✅ Health Check 최적화
- ✅ Graceful Shutdown (300s delay)
- ✅ Auto Scaling 준비
성능:
- ✅ Cross-Zone LB 활성화
- ✅ Session Stickiness 활용
- ✅ Client IP 보존
- ✅ Connection Pooling
비용:
- ✅ 불필요한 리소스 제거
- ✅ NLCU 최적화
- ✅ Cross-AZ 트래픽 최소화
🔗 참고 자료
AWS 공식 문서
Terraform
보안
🎯 결론
데이터베이스 접근 게이트웨이를 구축하면서 배운 핵심:
- 아키텍처 설계가 가장 중요하다
- 계층별 보안
- 명확한 책임 분리
- 확장 가능한 구조
- 작은 디테일이 큰 차이를 만든다
- 포트 매핑 검증
- Health Check 설정
- Security Group 규칙 순서
- 자동화는 필수다
- Infrastructure as Code
- CI/CD 파이프라인
- 자동화된 테스트
- 보안은 타협할 수 없다
- Zero Trust 원칙
- 최소 권한
- 지속적인 감사
이 아키텍처 덕분에:
- ✅ 모든 DB 접근이 중앙화되고 감사 가능
- ✅ 보안 사고 위험 대폭 감소
- ✅ 개발자 생산성 향상
- ✅ 규정 준수 요구사항 충족
이 글이 도움이 되셨나요? 댓글로 여러분의 경험을 공유해주세요!
'* DevSecOps > Operations' 카테고리의 다른 글
| [Post-Mortem] 2025년 11월 18일 Cloudflare 글로벌 장애 대응 일지: 우리는 무엇을 배웠나 (0) | 2025.11.19 |
|---|---|
| Karpenter v1.5.3 노드 통합으로 인한 대규모 장애 분석 및 해결기 (0) | 2025.10.02 |
| 이메일 발송 신뢰도 높이기: SendGrid SPF, DKIM, DMARC 설정 완벽 가이드 (0) | 2025.06.05 |
| Amazon Bedrock으로 Slack 기반 AIOps 챗봇 만들기: AWS 보안 아키텍처 제안부터 장애 해결까지! 🚀 (0) | 2025.04.22 |