DEV Community

Archist
Archist

Posted on

EC2 Dev 서버 운영기 — PM2에서 시작해서 ECS Production까지

EC2 Dev 서버 운영기 — PM2에서 시작해서 ECS Production까지

배경

Node.js 마이크로서비스 4개를 운영하고 있다. API Gateway 1개와 백엔드 서비스 3개. Production은 AWS ECS Fargate로 돌리고, Dev 서버는 EC2 한 대에 PM2로 올려놓고 쓰고 있다.

어느 날 Dev 서버 SSH 접속이 느려졌다. 들어가보니 PM2 로그가 수 GB, Docker dangling 이미지가 디스크를 채우고 있었다. 그때부터 정리한 운영 스크립트와, EC2와 ECS를 동시에 운영하면서 느낀 점을 정리한다.

Dev 서버 (EC2) 구조

EC2 (Ubuntu, t3.medium)
├── PM2 (Fork mode)
│   ├── gateway          512MB
│   ├── service-1        512MB
│   ├── service-2        512MB
│   ├── service-3-a      1GB
│   ├── service-3-b      1GB
│   ├── admin-dashboard  256MB
│   └── nginx            (sticky session)
│
├── Docker
│   ├── PostgreSQL 15    (port 5432)
│   └── Redis 7.4        (port 6379)
│
└── Nginx (ip_hash로 SSE sticky session)
Enter fullscreen mode Exit fullscreen mode

PM2를 fork 모드로 쓰는 이유가 있다. SSE(Server-Sent Events) 연결이 cluster 모드에서 끊기기 때문이다. service-3은 2개 인스턴스로 띄우고, Nginx ip_hash로 sticky session을 건다.

Production (ECS Fargate) 구조

ALB (단일, 멀티 포트)
├── :443  → Gateway (Blue/Green, 2~10 tasks)
├── :444  → Service-3 (Blue/Green, Sticky, 1~6 tasks)
├── :446  → Service-2 (Blue/Green, Sticky, 1~6 tasks)
├── :448  → Service-1 (Rolling, Sticky, 1~6 tasks)
└── :3333 → Admin Dashboard (Rolling, 1 task)

Service Discovery (Cloud Map)
├── gateway.internal:4001
├── service-1.internal:4001
├── service-2.internal:4002
└── service-3.internal:4003
Enter fullscreen mode Exit fullscreen mode

각 서비스가 독립적인 Fargate Task로 돌아간다. TCP 마이크로서비스 간 통신은 AWS Cloud Map으로 서비스 디스커버리를 하고, ALB에서 SSE가 필요한 서비스에는 sticky session 쿠키를 붙인다.

배포 전략도 서비스마다 다르다

서비스 배포 방식 이유
Gateway CodeDeploy Blue/Green 진입점이라 무중단 필수
Service-3 CodeDeploy Blue/Green SSE 연결 graceful draining 120초
Service-1 ECS Rolling 상태 없음, 빠른 배포 우선
Service-2 ECS Rolling 상태 없음
Admin Dashboard ECS Rolling 내부 도구, 중단 허용

두 환경의 현실적인 차이

EC2가 Dev에서 좋은 이유

# 코드 수정 → 반영: 30초
git pull && pnpm build && pm2 restart all

# 디버깅: SSH 한 방
ssh dev-server
pm2 logs service-1 --lines 100
docker exec -it postgres psql
Enter fullscreen mode Exit fullscreen mode

ECS에서 같은 걸 하려면:

# 코드 수정 → 반영: 3~5분
# Docker 빌드 → ECR 푸시 → Task 재배포

# 디버깅: 의식의 흐름이 필요
aws ecs execute-command \
  --cluster my-cluster \
  --task arn:aws:ecs:... \
  --container my-service \
  --interactive --command "/bin/sh"
# + Session Manager Plugin, IAM 권한, ECS Exec 활성화 필요
Enter fullscreen mode Exit fullscreen mode

.env 하나 바꾸는 것도 EC2는 vim으로 수정 후 재시작이면 끝이지만, ECS는 Secrets Manager 수정 → 태스크 재배포다.

단, ALB가 붙으면 EC2 장점이 사라진다

EC2의 가장 큰 장점은 Stop하면 과금이 멈추는 것이다. 그런데 ALB가 붙어있으면:

  • ALB 고정비: ~$16/월
  • NAT Gateway: ~$32/월 + 트래픽
  • Stop해도 $48/월은 계속 나간다

이 정도면 ECS Fargate로 desired_count: 0으로 내려놓는 것과 비용 차이가 없다. ALB가 필요한 Dev 서버라면 차라리 ECS로 통일하는 게 인프라 관리 포인트가 줄어든다.

EC2 Dev 서버가 의미있으려면 ALB 없이 IP 직접 접근으로 써야 한다.

EC2 Dev 서버 운영 스크립트

ALB 없이 EC2를 Dev 서버로 쓰고 있다면, 이 자동화 정도는 깔아둬야 한다.

Cron 자동 정리

crontab -e
Enter fullscreen mode Exit fullscreen mode
# [매시] Docker 미사용 리소스 + systemd 로그 정리
0 * * * * docker system prune -f && sudo journalctl --vacuum-size=500M && sudo find /var/log -name "*.gz" -delete && sudo find /tmp -atime +1 -delete 2>/dev/null

# [매일 03시] PM2 로그 비우기 — GB 단위로 쌓인다
0 3 * * * pm2 flush && find ~/.pm2/logs -name "*.log" -size +500M -exec truncate -s 0 {} \;

# [매일 04시] 72시간 지난 Docker 이미지 삭제
0 4 * * * docker image prune -a -f --filter "until=72h" && docker volume prune -f

# [매일 05시] 7일 넘은 로그 파일 삭제
0 5 * * * sudo find /var/log -name "*.log.*" -mtime +7 -delete 2>/dev/null

# [매주 일요일] apt 캐시 정리
0 4 * * 0 sudo apt clean && sudo apt autoremove -y -q
Enter fullscreen mode Exit fullscreen mode

30초 진단

서버가 느릴 때 이것부터:

uptime && free -h && df -h /
Enter fullscreen mode Exit fullscreen mode

세 가지를 동시에 본다:

  • load average — 코어 수 이상이면 CPU 과부하
  • available — 전체 메모리의 10% 이하면 위험
  • Use% — 디스크 90% 이상이면 긴급

더 파고 싶으면:

# CPU/메모리 Top 5
ps aux --sort=-%cpu | head -5
ps aux --sort=-%mem | head -5

# CPU steal — 높으면 t3 같은 버스터블 인스턴스 한계
top -bn1 | grep "%Cpu" | awk '{print "steal:", $16"%"}'

# OOM Killer 발생 여부
sudo dmesg | grep -i "oom\|killed" | tail -5
Enter fullscreen mode Exit fullscreen mode

Docker 인프라 모니터링

# 컨테이너 리소스
docker stats --no-stream

# PostgreSQL idle 연결 정리 (10분 이상)
docker exec $(docker ps -qf "name=postgres") psql -U postgres -c "
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle'
AND query_start < now() - interval '10 minutes';"

# Redis BullMQ 잔여 데이터 정리
docker exec $(docker ps -qf "name=redis") \
  redis-cli --scan --pattern "bull:*:completed" | \
  xargs -I{} docker exec $(docker ps -qf "name=redis") redis-cli DEL {}
Enter fullscreen mode Exit fullscreen mode

긴급 대응

# 디스크 100%: 범인 찾고 즉시 확보
sudo du -sh /var/log /tmp ~/.pm2/logs /var/lib/docker 2>/dev/null | sort -rh
pm2 flush && docker system prune -a -f --volumes && sudo journalctl --vacuum-size=100M

# 메모리 부족: 캐시 해제 + PM2 재시작
sync && echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null
pm2 restart all
Enter fullscreen mode Exit fullscreen mode

ECS Production에서는 이게 다 필요 없다

위 스크립트들이 EC2에서 필요한 이유는 서버가 stateful하기 때문이다. ECS Fargate는:

EC2에서 수동으로 하는 것 ECS에서 자동으로 되는 것
PM2 로그 정리 Cron CloudWatch Logs (30일 retention 자동)
Docker prune Cron 컨테이너 재생성 시 자동 정리
PM2 헬스체크 ECS 헬스체크 + 자동 재시작
ps aux 수동 모니터링 CloudWatch 메트릭 + 알림
fail2ban SSH 방어 SSH 자체가 없음
Swap 설정 Fargate가 메모리 관리
디스크 용량 관리 Ephemeral 스토리지, 상태 없음

ECS에서 대신 필요한 것

# 서비스 상태 확인
aws ecs describe-services --cluster my-cluster \
  --services gateway --query 'services[0].{running:runningCount,desired:desiredCount,deployments:deployments[*].status}'

# 실시간 로그
aws logs tail /ecs/my-service --follow

# 스케일링
aws ecs update-service --cluster my-cluster --service gateway --desired-count 4

# 롤백
aws ecs update-service --cluster my-cluster --service gateway \
  --task-definition my-service:$(( $(aws ecs describe-services --cluster my-cluster --services gateway --query 'services[0].taskDefinition' --output text | grep -o '[0-9]*$') - 1 ))
Enter fullscreen mode Exit fullscreen mode

개선 로드맵

현재 구조에서 아직 남은 과제들:

1. Dev 환경도 ECS로 통합

EC2 Dev 서버에 ALB가 붙어있다면, 비용 이점이 없다. Terraform 모듈을 환경별로 분리하면 된다:

terraform/environments/
├── prod/    # 현재 구성
├── staging/ # desired_count: 1, 작은 리소스
└── dev/     # desired_count: 1, FARGATE_SPOT 100%
Enter fullscreen mode Exit fullscreen mode

2. Auto Scaling 정책 추가

현재 ECS에 min/max만 정의되어 있고 실제 스케일링 정책이 없다. Target Tracking 추가가 필요하다:

resource "aws_appautoscaling_target" "gateway" {
  max_capacity       = 10
  min_capacity       = 2
  resource_id        = "service/my-cluster/gateway"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}

resource "aws_appautoscaling_policy" "gateway_cpu" {
  name               = "gateway-cpu-scaling"
  policy_type        = "TargetTrackingScaling"
  resource_id        = aws_appautoscaling_target.gateway.resource_id
  scalable_dimension = aws_appautoscaling_target.gateway.scalable_dimension
  service_namespace  = aws_appautoscaling_target.gateway.service_namespace

  target_tracking_scaling_policy_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageCPUUtilization"
    }
    target_value = 70.0
  }
}
Enter fullscreen mode Exit fullscreen mode

3. FARGATE_SPOT 활용

상태 없는 서비스(Service-1, Service-2)는 FARGATE_SPOT으로 전환하면 최대 70% 비용 절감이 가능하다. 중단 시 ECS가 자동으로 다른 인스턴스를 띄운다.

4. Docker 로그 제한 (EC2 운영 시)

정리보다 애초에 안 쌓이게 하는 게 낫다:

// /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "3"
  }
}
Enter fullscreen mode Exit fullscreen mode

AI 활용 포인트

이 글의 시작은 "서버가 느린데 어떡하지"였다. Claude Code에게 물어보니 진단 → cron 자동화 → 문서화까지 한 세션에서 끝났다.

특히 도움된 부분:

  • 코드베이스의 ecosystem.config.js, docker-compose.yml, Terraform 설정을 자동으로 분석해서 프로젝트 스택에 맞는 스크립트 생성
  • cron 표현식, docker exec 파이프라인처럼 문법 실수하기 쉬운 명령어를 바로 사용 가능한 형태로 제공
  • EC2 vs ECS 비교를 현재 인프라 비용 기준으로 구체적으로 계산

Top comments (0)