들어가며
뉴스낵의 백엔드는 메인 API 서버(Spring Boot), AI 서버(FastAPI), 오케스트레이터(Airflow)라는 성격이 다른 세 시스템이 하나의 DB를 공유하는 구조로 되어있다. 이 복합적인 시스템을 구축하기 위해 보안·비용·재현성 세 가지를 동시에 만족하는 인프라 아키텍처를 설계했다.
이 글에서는 초기 VPC 설계부터 Nginx 컨테이너화까지, 뉴스낵의 AWS 인프라를 완성해 나가는 과정을 정리한다.
초기에는 서비스별로
t3.small3대를 분리 운영했으나, 유휴 자원 낭비와 비용 문제로 이후 ARM64 기반t4g.medium단일 인스턴스로 통합했다. 이 과정은 이전 글인 단일 인프라 환경 구축기에 상세히 정리했다. 이 글에서 다루는 네트워크 설계와 Nginx 컨테이너화는 최종 단일 인스턴스 환경을 기준으로 설명한다.
1. VPC 설계
우선 AWS의 ‘VPC 등’ 생성 마법사를 활용해 2-AZ 기반의 서브넷 분리 구조를 구성했다.
- 퍼블릭 서브넷 × 2: 인터넷 게이트웨이(IGW)에 연결되는 공간. 외부와 직접 통신해야 하는 EC2 웹 서버와 백엔드 컨테이너가 위치한다.
- 프라이빗 서브넷 × 2: 외부 라우팅이 원천 차단된 공간. RDS가 이 안에 격리된다.
NAT Gateway를 사용하지 않은 이유
일반적인 3-Tier 아키텍처(Web - WAS - DB)에서는 보안을 위해 WAS를 프라이빗 서브넷에 배치한다. 이 경우 외부 통신을 위해 NAT Gateway가 필수적이지만, 이는 시간당 고정 요금($0.059, 월 약 $42)과 트래픽 비용이 동시에 부과되어 초기 프로젝트 단계에서는 큰 부담이다.
우리 팀은 인프라 비용 최소화를 위해 아키텍처를 전략적으로 설계했다. 외부 통신이 전혀 필요 없는 데이터베이스(RDS)만 프라이빗 서브넷에 격리하고, 인터넷 통신이 필수적인 앱 컨테이너들(메인 API 서버, AI 서버, Airflow)은 퍼블릭 서브넷에 배치했다. 이를 통해 인터넷 게이트웨이(IGW)를 직접 활용함으로써 NAT Gateway 고정 비용을 원천적으로 제거했다. 퍼블릭 배치에 따른 보안 우려는 다중 보안 그룹 설계를 통해 애플리케이션 포트 접근을 엄격히 제어하는 방식으로 보완했다.
또한, VPC 구성 시 S3 Gateway Endpoint(newsnack-vpce-s3)를 추가로 생성하여 활용했다. 이는 추가 과금이 없는 무료 리소스로, 퍼블릭 서브넷의 EC2가 S3와 통신할 때 일반 인터넷망(IGW)이 아닌 AWS 내부 백본망을 거치도록 라우팅한다. 이를 통해 데이터 전송 비용을 절감함과 동시에 네트워크 성능과 보안성을 동시에 확보했다.
2. 보안 그룹: 다중 방화벽 설계
네트워크를 공간으로 분리했다면, 그 방을 넘나드는 트래픽의 신원을 엄격히 통제하는 소프트웨어 방화벽이 필요하다. 보안 그룹을 직렬로 연결해 다중 방어선을 구축했다.
1차 방화벽: 앱 서버용 퍼블릭 그룹 (newsnack-common-sg)
퍼블릭 서브넷의 EC2에 적용한다.
| 포트 | 소스 | 목적 |
|---|---|---|
| 22 (SSH) | 0.0.0.0/0 | Github Actions 자동 배포 및 서버 관리 |
| 80 (HTTP) | 0.0.0.0/0 | API 관문 (Nginx). Cloudflare 프록시와 통신. |
| 8000, 8081 | 내 IP | FastAPI Swagger UI, Airflow Web UI 접속. |
| 8080 | - | 인바운드에서 개방 안 함 (도커 네트워크 내부 통신 전용) |
특히 HTTPS(443) 통신의 경우 EC2에서 직접 인증서를 관리하지 않고 Cloudflare 프록시를 통해 SSL Offloading을 처리하므로, 외부 클라이언트와의 통신은 Cloudflare를 거쳐 80 포트로만 들어오게 된다.
또한 22 (SSH) 포트의 경우 Github Actions를 사용한 자동 배포 과정에서 EC2 서버로 접근하기 위해 활용한다. Github 제공 runner의 IP 대역이 유동적으로 변하기 때문에 소스 IP를 0.0.0.0/0으로 전체 개방해 두었으며, 대신 인증키(PEM)를 통해 접근을 엄격히 제어하는 방식을 택했다.
2차 방화벽: RDS 전용 보안 그룹 (newsnack-rds-sg)
프라이빗 서브넷의 RDS를 지키는 핵심 방화벽이다. 인바운드 허용 규칙의 소스를 막연한 IP 대역이 아닌, newsnack-common-sg 보안 그룹 ID 자체로 설정했다. 외부 인터넷은 이미 서브넷 차원에서 막혀 있고, 내부망이라 할지라도 퍼블릭 방화벽 그룹의 허가를 받은 앱 서버의 트래픽만 DB 접근을 허용하는 것이다.
이렇게 허가받지 않은 내부 접근까지도 원천 차단하는 이중 방어선이 구축됐다.
3. S3 퍼블릭 서빙: 정적 리소스 호스팅과 CORS 전략
AI가 생성한 이미지(뉴스툰)는 기사 URL만 확보하면 누구나 열람할 수 있는 퍼블릭 콘텐츠다. 이를 원활하게 서빙하면서도 미래의 프론트엔드 확장성을 보장하기 위해, S3를 단순한 이미지 저장소를 넘어 웹 정적 리소스를 제공하는 퍼블릭 스토리지로 설계했다.
1. Bucket Policy (공개 읽기 허용)
1
2
3
4
5
6
7
8
9
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::team-leekim-assets/*"
}]
}
뉴스툰은 불특정 다수가 열람할 수 있는 공개 콘텐츠다. 따라서 임시 권한을 발급하는 Pre-signed URL이나 추가 인프라 비용과 복잡도를 유발하는 CloudFront를 의도적으로 배제했다. 대신 S3 버킷 레벨의 정책(Bucket Policy)만으로 s3:GetObject 권한을 개방하여, 가장 직관적이고 비용 효율적인 아키텍처를 선택했다.
2. CORS 설정: 클라이언트 확장성 확보
단순히 <img> 태그나 Next.js의 <Image> 컴포넌트로 이미지를 렌더링하는 현재의 방식에서는 타 도메인이라도 브라우저의 동일 출처 정책(SOP) 제약을 개별적으로 받지 않는다. 클라이언트 렌더링 자체가 교차 출처 에러를 유발하지 않거나 서버 간 통신으로 처리되기 때문이다.
그럼에도 AllowedMethods: ["GET", "HEAD"]를 CORS 정책에 포함했다. 프론트엔드의 리소스 활용 방식이 다변화될 때 발생할 수 있는 잠재적 차단 이슈를 원천적으로 예방하기 위함이다. 추후 자바스크립트의 fetch API를 활용한 직접 다운로드 기능 구현, <canvas> 태그를 이용한 픽셀 조작, 혹은 웹 폰트(.woff) 파일처럼 브라우저가 엄격하게 CORS를 검사하는 기타 정적 리소스 서빙이 요구될 때, 인프라의 변경 없이 클라이언트가 유연하게 통신할 수 있도록 대비했다.
4. Nginx 컨테이너화: 재현성 확보와 망 격리
왜 Nginx를 컨테이너로 옮겼는가?
처음에는 빠른 구축을 위해 호스트 EC2에 Nginx를 직접 설치(apt install nginx)하여 리버스 프록시로 사용했다. 그러나 점차 다음과 같은 한계가 드러났다.
- 실행 환경의 재현성 부재:
nginx.conf같은 설정 파일을 별도로 백업하더라도, Nginx 프로세스 자체는 EC2 호스트에 종속되어 있어 서버 스펙업이나 장애 시 타이핑 한 번(docker-compose up)에 인프라를 복구할 수 있는 상태가 아니었다. - 호스트 네트워크 종속성: 호스트 머신의 네트워크를 공용으로 사용하므로, 컨테이너 환경의 가장 큰 장점인 ‘도커 기반 이름 통신(Internal DNS)’ 흐름에 프록시가 적극적으로 개입할 수 없어 통제와 확장이 까다로웠다.
Nginx를 컨테이너로 편입함으로써 Docker Compose를 통해 인프라 실행 환경을 완벽히 재현 가능하게 만들었다. 또한 프록시 서버를 도커 안으로 편입시켜 호스트망과 철저히 분리된 컨테이너 전용 사설 통신망을 구축할 수 있었다.
도커 네트워크 구성
복잡한 네트워크 관리를 덜기 위해 백엔드, AI 엔진, 파이프라인(Airflow)까지 모두 newsnack-network라는 단일 브릿지 네트워크로 묶었다.
graph LR
Internet((인터넷)) -->|HTTPS 443| CF[Cloudflare<br>SSL Proxy]
CF -->|HTTP 80| NGINX[Nginx]
subgraph "newsnack-network"
NGINX -->|8080| API["Core API Server<br>(Spring Boot)"]
AIRFLOW[Airflow Scheduler] <-->|8000| FASTAPI["AI Server<br>(FastAPI)"]
end
단일 네트워크에 소속된 컨테이너들은 별도의 IP 설정 없이 컨테이너 이름(http://newsnack-api:8080, http://newsnack-ai:8000 등)만으로 서로를 찾아 통신할 수 있는 도커 내장 DNS의 이점을 얻게 된다. 물리적으로 네트워크를 잘게 쪼개는 대신 논리적 단일 망 안에서 통신을 유연하게 가져가는 방식을 택함으로써, 향후 서비스 컴포넌트가 추가되더라도 인프라 설정의 복잡도 없이 언제든 쉽게 도커 네트워크에 편입시킬 수 있는 확장성을 확보했다.
메인 API 서버의 포트를 바인딩한 이유?
메인 API 서버 컨테이너의 경우, CI/CD 배포 스크립트에서 자동화된 헬스체크(curl http://localhost:8080)를 매우 간편하게 수행하기 위해 도커 런타임에 호스트 포트를 바인딩(-p 8080:8080)해두었다. 앞서 구축한 AWS 보안 그룹(newsnack-common-sg)에서 8080 인바운드 트래픽을 원천 차단하고 있으므로 호스트 포트가 열려있음에도 외부 공격 위협은 없다.
환경과 설정의 형상 관리
Nginx 설정 및 Docker Compose 기반의 인프라 실행 환경을 통째로 깃에 올리기 위해 newsnack-infra 레포지토리를 신설했다. GitHub Actions를 통해 main 브랜치에 코드가 푸시되면 자동으로 EC2에 반영된다.
이때 단순 git pull 대신 git fetch + reset --hard 방식을 선택했다. 서버에서 실수로 설정 파일을 수동으로 건드렸더라도 git pull 시 충돌로 배포가 중단되는 것을 방지하기 위해서다. 서버는 항상 GitHub의 상태를 미러링해야 한다는 원칙을 코드로 강제했다.
.github/workflows/deploy.yml1 2 3 4 5 6 7 8
run: | cd ~/newsnack-infra - git pull origin main # 기존 방식: 충돌 발생 위험 존재 + git fetch origin main + git reset --hard origin/main # 서버 변경사항 무시, GitHub 상태로 강제 동기화 docker network create newsnack-internal || true # 네트워크 없으면 생성 (멱등성) docker-compose up -d --remove-orphans docker exec nginx nginx -s reload
docker network create ... || true 패턴은 Docker Compose가 파일 내 서비스가 사용하지 않는 네트워크를 생성하지 않는 최적화 동작을 보완하기 위한 조치다. 배포 스크립트에서 명시적으로 네트워크 존재 여부를 확인하고 생성하도록 하여 배포 스크립트의 멱등성을 확보했다.
5. 트러블슈팅: Nginx Upstream Host Not Found
Nginx 컨테이너화 직후 컨테이너가 실행되지 않고 계속 재시작되는 현상이 발생했다.
1
2
// Nginx 에러 로그
2026/02/18 08:16:04 [emerg] 1#1: host not found in upstream "newsnack-api"
Nginx는 구동 시점에 nginx.conf에 정의된 Upstream 호스트(newsnack-api:8080)의 DNS를 해석하려 시도한다. 당시 백엔드 컨테이너가 아직 newsnack-network에 포함되지 않은 상태였기에 DNS 해석에 실패하고 Nginx 프로세스가 종료된 것이었다.
해결: newsnack-backend 레포지토리의 CI/CD 배포 스크립트에 --network newsnack-network 옵션을 추가하여 백엔드 컨테이너가 공유 네트워크에 속하도록 수정했다. 이후 백엔드 컨테이너를 먼저 정상 실행한 뒤 Nginx를 재시작하여 해결했다.
마치며
위와 같은 과정을 거쳐 뉴스낵의 인프라는 다음과 같이 구성되었다.
인프라 아키텍처 수준에서의 엄격한 보안 설정은 상위 애플리케이션 레이어의 취약점을 보완하는 필수적인 요소다. 위와 같은 과정을 거치며 단일 인스턴스 위에서 4개의 서비스 컴포넌트가 강력하게 통제되면서 외부 노출은 최소화된 인프라를 완성할 수 있었다.




