본문 바로가기

* DevSecOps/Operations

AWS에서 안전한 데이터베이스 접근 게이트웨이 구축하기: NLB + Security Group 완벽 가이드

데이터베이스 접근 관리 솔루션을 AWS에 배포하면서 Network Load Balancer와 Security Group을 활용한 Zero Trust 아키텍처를 구축한 경험을 공유합니다. Terraform으로 완전 자동화하고, 보안과 가용성을 모두 확보했습니다.

🎯 배경: 왜 데이터베이스 접근 게이트웨이가 필요한가?

많은 기업에서 여러 팀이 수십 개의 데이터베이스를 사용합니다:

  • RDS MySQL/PostgreSQL 클러스터
  • ElastiCache Redis
  • DocumentDB (MongoDB 호환)
  • EKS 클러스터 내부 데이터베이스

문제점:

  1. 개발자마다 각자 DB 접속 정보 관리
  2. 퇴사자 계정 관리의 어려움
  3. 접속 이력 추적 불가
  4. 프로덕션 DB에 대한 직접 접근
  5. 보안 감사의 어려움

해결책: 중앙화된 데이터베이스 접근 게이트웨이 구축

🏗️ 아키텍처 개요

전체 구성도

주요 구성 요소

  1. Network Load Balancer (NLB)
    • Layer 4 로드 밸런서
    • TLS 종료 처리
    • Multi-AZ 고가용성
  2. Gateway EC2 Instance
    • 데이터베이스 접근 프록시
    • 세션 관리 및 감사 로깅
    • 사용자 권한 제어
  3. Security Groups
    • 계층별 방화벽 규칙
    • 최소 권한 원칙 적용
    • Cross-VPC 통신 지원
  4. 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 선택 이유:

  1. 낮은 지연시간: 데이터베이스 작업에 중요
  2. Client IP 보존: 감사 로그에 실제 사용자 IP 필요
  3. TCP 지원: 커스텀 포트 (9995) 사용
  4. 비용 효율: 월 ~$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/월

비용 절감 팁

  1. Stickiness 활용: 무료 기능, 추가 비용 없음
  2. Health Check 최적화: TCP 기반 무료
  3. Cross-Zone LB: 필요시에만 활성화
  4. 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 설정 최적화
  • 아키텍처 리뷰

🎓 교훈 및 베스트 프랙티스

배운 점

  1. 포트 매핑 확인은 필수
    • NLB Listener Port ≠ Backend Port
    • Health Check는 실제 애플리케이션 포트로 설정
  2. Security Group은 계층별로
    • NLB → Gateway → Database
    • 각 계층마다 명확한 규칙
  3. CIDR vs Security Group 참조
    • 같은 VPC: Security Group 참조 권장
    • Cross-VPC: CIDR 기반 규칙 사용
  4. 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

보안

🎯 결론

데이터베이스 접근 게이트웨이를 구축하면서 배운 핵심:

  1. 아키텍처 설계가 가장 중요하다
    • 계층별 보안
    • 명확한 책임 분리
    • 확장 가능한 구조
  2. 작은 디테일이 큰 차이를 만든다
    • 포트 매핑 검증
    • Health Check 설정
    • Security Group 규칙 순서
  3. 자동화는 필수다
    • Infrastructure as Code
    • CI/CD 파이프라인
    • 자동화된 테스트
  4. 보안은 타협할 수 없다
    • Zero Trust 원칙
    • 최소 권한
    • 지속적인 감사

이 아키텍처 덕분에:

  • ✅ 모든 DB 접근이 중앙화되고 감사 가능
  • ✅ 보안 사고 위험 대폭 감소
  • ✅ 개발자 생산성 향상
  • ✅ 규정 준수 요구사항 충족

이 글이 도움이 되셨나요? 댓글로 여러분의 경험을 공유해주세요!