본문 바로가기
eomoff
Dev Log/Projects

[AWS 배포기 #1] 배포를 내가 해보려고함 - 현재 프로젝트의 아키텍처 구조

eomoff 2026. 9. 12. 05:18

nginx와 ECS on EC2로 구성한 배포 아키텍처

팀 프로젝트로 만든 서비스는 Spring Boot 애플리케이션 하나에 PostgreSQL, Redis, Kafka, Elasticsearch가 붙어 있고, WebSocket과 SSE로 실시간 기능도 제공합니다. 이 서비스가 AWS 위에서 실제로 어떻게 돌아가는지 요청이 들어오는 순서대로 정리해 봤습니다. 정리하면서 보인 이 구조의 한계도 마지막에 적었습니다.

전체 구조

크게 네 부분으로 나눠서 보면 이해하기 쉽습니다.

영역 구성 요소 역할
진입점 nginx HTTPS를 종료하고 app 서버로 요청을 나눕니다
애플리케이션 ECS 클러스터의 EC2 인스턴스 3대 Spring Boot 컨테이너를 실행합니다
데이터 RDS, ElastiCache, Kafka, Elasticsearch, S3 데이터 저장, 캐시, 이벤트 전달, 검색, 이미지 보관을 맡습니다
배포 GitHub Actions, ECR, ECS, SSM Parameter Store 이미지를 빌드해서 배포하고 비밀값을 관리합니다

가장 눈에 띄는 점은 로드밸런서로 AWS의 ALB 대신 nginx를 쓴다는 것입니다.

요청이 들어오는 길

사용자의 요청은 가장 먼저 nginx에 도착합니다. nginx도 ECS 태스크로 실행되고, HTTP와 HTTPS를 둘 다 받습니다. HTTPS는 Let's Encrypt 인증서로 nginx에서 복호화(TLS 종료)하고, app 서버로는 평문 HTTP로 넘깁니다.

nginx가 app 서버를 고르는 방식은 요청 종류에 따라 두 가지입니다.

일반 요청은 라운드로빈으로 나눕니다.
REST API 요청은 어느 서버가 처리해도 결과가 같습니다. 그래서 3대에 차례대로 보냅니다.

실시간 연결과 소셜 로그인은 IP 해시로 서버를 고정합니다.
같은 IP에서 온 요청은 항상 같은 서버로 보냅니다. 처음에는 왜 굳이 나눴는지 궁금했는데, 이유는 두 가지였습니다.

  • WebSocket과 SSE는 처음 연결을 받은 서버가 그 연결 상태를 메모리에 들고 있습니다. 다음 요청이 다른 서버로 가면 그 서버에는 연결 정보가 없습니다.
  • 구글·카카오 소셜 로그인은 "로그인 시작" 요청과 "로그인 완료 콜백" 요청이 따로 들어옵니다. 시작할 때 저장해 둔 정보를 콜백에서 다시 꺼내야 해서 두 요청이 같은 서버로 가야 합니다.

애플리케이션 서버

app 서버는 ECS 클러스터에서 돌아갑니다. 실행 방식은 Fargate가 아니라 EC2입니다.

  • EC2 인스턴스 3대를 Auto Scaling 그룹이 관리하고, 인스턴스마다 app 컨테이너가 하나씩 뜹니다.
  • 컨테이너가 인스턴스의 고정 포트에 연결되기 때문에 인스턴스 하나에 app 컨테이너를 하나만 올릴 수 있습니다.
  • nginx는 app 서버 3대의 사설 IP를 직접 알고 있고, 그 주소로 요청을 보냅니다.

ECS를 쓰면 서버에 하나하나 접속해서 컨테이너를 바꿀 필요가 없습니다. ECS "서비스"에 새 버전을 알려주면 ECS가 컨테이너를 교체해 줍니다.

데이터 계층

app 서버는 다섯 가지 저장소와 통신합니다.

구성 요소 용도 비고
Amazon RDS (PostgreSQL) 주 데이터베이스 AWS 관리형 서비스입니다
Amazon ElastiCache (Redis) 캐시, 분산 락 AWS 관리형 서비스이고 TLS로 통신합니다
Kafka 알림 같은 이벤트를 비동기로 전달합니다
Elasticsearch 콘텐츠·사용자·플레이리스트 검색 별도 EC2 인스턴스에서 직접 운영하고 TLS로 통신합니다
Amazon S3 이미지 저장 브라우저가 S3에서 이미지를 직접 불러옵니다

RDS와 ElastiCache는 AWS가 운영해 주는 관리형 서비스입니다. Elasticsearch는 EC2 인스턴스를 따로 띄워서 직접 운영합니다. S3는 이미지를 공개 읽기로 열어 둬서, 이미지 요청은 app 서버를 거치지 않고 S3에서 바로 나갑니다.

네트워크

ECS 인스턴스와 Elasticsearch 인스턴스는 AWS 계정에 처음부터 만들어져 있는 기본 VPC 안에 있습니다.

이 구조에는 NAT 게이트웨이가 없습니다. NAT 게이트웨이는 프라이빗 서브넷에 있는 서버가 인터넷으로 나갈 수 있게 해 주는 장치인데, 켜 두는 것만으로 시간당 요금이 나갑니다. 그 대신 ECS 인스턴스를 퍼블릭 서브넷에 두고 퍼블릭 IP를 붙였습니다.

app 서버는 이 퍼블릭 IP로 외부 API를 직접 호출합니다.

  • TMDB: 영화·드라마 콘텐츠 정보 수집
  • 구글·카카오: 소셜 로그인
  • 메일 발송 서비스

배포 흐름

배포는 GitHub Actions로 자동화되어 있습니다.

  1. 코드가 main 브랜치에 반영되고 CI(빌드와 테스트)가 통과하면 배포가 시작됩니다.
  2. app 이미지와 nginx 이미지를 빌드해서 Amazon ECR에 올립니다.
  3. 새 이미지를 가리키는 태스크 정의를 등록합니다.
  4. ECS 서비스를 새 태스크 정의로 갱신하면 ECS가 컨테이너를 새 버전으로 바꿉니다.
  5. nginx와 app 두 서비스가 안정 상태가 될 때까지 기다리면 끝납니다.

DB 비밀번호나 API 키 같은 비밀값은 서버에 .env 파일로 두지 않습니다. 전부 SSM Parameter Store에 저장해 두고, ECS가 컨테이너를 시작할 때 환경변수로 넣어 줍니다. 그래서 서버나 이미지 안에 비밀값이 파일로 남지 않습니다.

이 구조의 한계

구조를 하나씩 뜯어보니 아쉬운 점도 보였습니다.

1. nginx에 장애가 나면 서비스 전체에 장애가 납니다

모든 요청이 nginx를 거쳐야 하는데, nginx 앞에는 로드밸런서가 없습니다. app 서버는 3대라서 1대가 죽어도 버틸 수 있지만, nginx에 장애가 나면 요청이 들어올 곳 자체가 사라집니다. 이렇게 한 곳의 장애가 전체 장애로 이어지는 지점을 단일 장애 지점(SPOF)이라고 합니다. 이 구조에서는 nginx가 그 자리에 있습니다.

2. 서버가 늘거나 바뀌어도 nginx에 반영되지 않습니다

nginx는 app 서버 3대의 사설 IP를 설정으로 들고 있습니다. 그래서 Auto Scaling 그룹이 인스턴스를 교체하면 IP가 바뀌고, 사람이 새 IP를 넣은 뒤 nginx를 다시 배포해야 합니다.

부하가 늘어서 Auto Scaling 그룹이 서버를 4대로 늘려도 마찬가지입니다. nginx에 새 서버의 주소가 없으니 트래픽이 가지 않습니다. 서버는 자동으로 늘어나는데 트래픽 분산은 자동으로 따라가지 못하는 셈입니다.

3. 서버 1대에 컨테이너 1개만 올릴 수 있습니다

app 컨테이너가 인스턴스의 고정 포트를 쓰기 때문에, 같은 인스턴스에 두 번째 컨테이너를 띄우면 포트가 충돌합니다. 그래서 서버 자원이 남아도 컨테이너를 더 올릴 수 없습니다.

배포할 때도 영향이 있습니다. 새 버전을 기존 버전 옆에 먼저 띄워 두고 넘기는 방식을 쓸 수 없습니다. 기존 컨테이너를 먼저 내려서 포트를 비워야 새 컨테이너가 올라갑니다.

4. 인증서를 서버에 직접 두고 관리해야 합니다

nginx는 인스턴스에 저장된 Let's Encrypt 인증서를 읽어서 씁니다. 인증서가 특정 서버의 디스크에 있기 때문에, nginx가 뜨는 인스턴스가 교체되면 새 인스턴스에 인증서를 다시 준비해야 합니다. Let's Encrypt 인증서는 90일마다 만료되므로 갱신도 직접 챙겨야 합니다.

ALB에 AWS Certificate Manager(ACM) 인증서를 붙이면 발급과 갱신을 AWS가 알아서 해 줍니다. 그것과 비교하면 관리할 거리가 많은 편입니다.

5. IP 해시 고정에도 한계가 있습니다

IP 해시는 간단하지만 IP만 보고 서버를 고르기 때문에 생기는 문제가 있습니다.

  • 회사나 학교처럼 여러 사람이 같은 공인 IP로 나가는 곳에서는 사용자가 한 서버로 몰립니다.
  • 모바일에서 Wi-Fi와 LTE를 오가면 IP가 바뀝니다. 그러면 다른 서버로 연결돼서 실시간 연결이 끊깁니다.

6. app 서버가 퍼블릭 서브넷에 있습니다

NAT 게이트웨이 비용을 아끼는 대신 app 서버가 퍼블릭 IP를 가진 채 퍼블릭 서브넷에 있습니다. 외부에서 app 서버로 직접 들어오는 걸 막는 건 보안 그룹 설정뿐입니다. 보통은 app 서버를 프라이빗 서브넷에 두고 로드밸런서만 인터넷에 노출하는 구성을 많이 씁니다.

정리

  • 진입점은 nginx 한 곳입니다. HTTPS 종료와 요청 분산을 둘 다 맡고, 실시간 연결과 소셜 로그인은 IP 해시로 같은 서버에 고정합니다.
  • 애플리케이션은 ECS on EC2로 3대를 운영하고, nginx가 각 서버의 사설 IP로 요청을 보냅니다.
  • DB와 캐시는 관리형 서비스(RDS, ElastiCache), 검색은 직접 운영하는 Elasticsearch, 이미지는 S3를 씁니다.
  • NAT 게이트웨이 없이 퍼블릭 서브넷에서 외부 API를 직접 호출합니다.
  • 배포는 GitHub Actions → ECR → ECS로 자동화되어 있고, 비밀값은 SSM Parameter Store가 관리합니다.
  • 대신 nginx가 단일 장애 지점이고, 서버 주소와 인증서를 사람이 관리해야 한다는 한계가 있습니다.

이 한계들을 어떻게 풀 수 있는지는 ALB를 쓰는 구성과 nginx를 표준 방식으로 운영하는 구성으로 직접 바꿔 보면서 정리해 볼 생각입니다.