[AWS 배포기 #1] 배포를 내가 해보려고함 - 현재 프로젝트의 아키텍처 구조
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로 자동화되어 있다.
- 코드가 main 브랜치에 반영되고 CI(빌드와 테스트)가 통과하면 배포가 시작된다.
- app 이미지와 nginx 이미지를 빌드해서 Amazon ECR에 올린다.
- 새 이미지를 가리키는 태스크 정의를 등록한다.
- ECS 서비스를 새 태스크 정의로 갱신하면 ECS가 컨테이너를 새 버전으로 바꾼다.
- 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를 표준 방식으로 운영하는 구성으로 직접 바꿔 보면서 정리해 볼 생각이다.