14장. ECS Fargate + ALB로 배포 (기본 완성)
드디어 2부의 마지막이자 클라이맥스입니다. 지금까지 창고에 이미지를 넣었고(10장), 네트워크를 지었고(11장), DB를 만들어(12장) 초기 데이터까지 심었습니다(13장). 이제 주인공인 컨테이너를 무대에 올려, 인터넷에서 진짜로 접속되는 투표 서비스를 완성합니다.
이 장에는 새 용어가 많이 나옵니다. 하지만 걱정 마세요. 처음에 용어를 한꺼번에 정리하고, 실무에서 어디에 쓰이는지 본 다음, 콘솔에서 하나씩 만듭니다. 그러면 화면에 그 이름들이 나올 때마다 "아, 그거"하고 알아볼 수 있습니다. 한 단계씩 천천히 따라오세요.
[지금 여기]
우리는 22단계를 거쳐 "라이브 투표 서비스"를 직접 만들고, 컨테이너에 담아, AWS 클라우드에 배포하고 있습니다. 아래가 그 전체 여정 지도입니다. 지금은 14단계, 2부의 목적지인 "기본 완성"에 도착하는 장입니다.
- 1장개발 환경 준비 (터미널 / Node.js / Git / GitHub / VS Code / Claude Code)
- 2장Claude Code 사용법 익히기
- 3장무엇을 만들지 기획 (라이브 투표 서비스)
- 4장로컬에서 서비스 만들기 (React + Express + PostgreSQL)
- 5장Docker 개념
- 6장Dockerfile 작성
- 7장컨테이너 빌드·실행
- 8장AWS 시작 (계정 / IAM / 리전 / 비용)
- 9장이미지 태깅
- 10장ECR에 이미지 올리기
- 11장VPC 네트워크 구성
- 12장RDS 데이터베이스 생성
- 13장SSM 터널로 RDS 초기화
- 14장ECS Fargate + ALB로 배포 (기본 완성)
- 15장CI/CD (GitHub Actions)
- 16장IaC (Terraform)
- 17장배포 자동화 연결
- 18장현업 운영 구조 이해
- 19장무중단 배포
- 20장신규 버전 배포 & 롤백
- 21장(심화) 프론트를 Vercel로 분리 배포
- 22장마무리 (회고 + 리소스 삭제)
- 방금 끝낸 것: 13장에서 SSM 터널로 로컬에서 RDS에 접속해, 테이블을 만들고 "점심 뭐 먹지?" 같은 초기 설문 데이터를 심었습니다. 이제 DB는 데이터까지 갖춘 채 프라이빗 서브넷에서 대기 중입니다.
- 이번 장에서 하는 것: ECR의 web·api 이미지를 하나의 ECS Fargate 태스크로 실행하고, 그 앞에 ALB(로드밸런서)를 세워, ALB 기본 DNS 주소(HTTP)로 접속하면 투표 서비스가 실제로 동작하게 만듭니다.
이번 장에서 완성되는 것
한 문장으로: ALB의 기본 DNS 주소를 브라우저에 붙여넣으면, 13장에서 심어 둔 설문이 뜨고 실제로 투표가 되는 서비스가 인터넷에 열립니다.
이렇게 되면 성공입니다: http://vote-alb-...elb.amazonaws.com 같은 주소로
접속했을 때 투표 화면이 보이고, 투표 버튼을 누르면 결과가 반영됩니다.
이 순간 사용자 브라우저 → ALB → ECS(web+api) → RDS 전 구간이 한 줄로
꿰어집니다. 2부 내내 만든 모든 조각이 처음으로 함께 작동합니다.
사전 조건
이 장은 1~13장의 결과물 위에서 시작합니다. 아래가 갖춰져 있어야 합니다. 하나라도 빠져 있으면 이 장 중간에 막히니, 먼저 확인하세요.
- ECR에 두 이미지가 올라가 있다 (10장). ECR 콘솔에
vote-web,vote-api리포지토리가 있고, 각각1.0태그 이미지가 들어 있어야 합니다. - VPC와 서브넷·보안 그룹 설계가 끝나 있다 (11장).
vote-vpc가 있고, 그 안에 퍼블릭 서브넷 2개와 프라이빗 서브넷 2개가 있어야 합니다. - RDS가 떠 있고 데이터가 들어 있다 (12·13장).
vote-db가 프라이빗 서브넷에서 실행 중이고, 13장에서 테이블과 초기 설문이 심어져 있어야 합니다. - 12장에서 기록해 둔 DB 접속 정보 5가지(엔드포인트, 포트, DB 이름, 사용자, 마스터 암호)를 손에 갖고 있어야 합니다. 이 장의 태스크 정의에서 그대로 씁니다.
- 리전이 서울(
ap-northeast-2) 인지. 콘솔 오른쪽 위 리전 표시가 "서울" 인지 확인하세요. 리전이 다르면 앞 장에서 만든 리소스들이 목록에 안 보입니다.
콘솔 화면이 이 책의 그림과 조금 달라도 괜찮습니다 AWS 콘솔은 화면 구성과 버튼 문구가 종종 바뀝니다. 특히 ECS 콘솔은 "새 콘솔 / 기존 콘솔"이 오가며 메뉴 위치나 이름(예: "Create" vs "생성")이 달라질 수 있습니다. 이 책은 눌러야 하는 항목의 이름과 넣어야 하는 값을 기준으로 안내합니다. 화면에서 같은 뜻의 항목을 찾아 그 값을 넣으면 됩니다.
14.1 ECS · Fargate · 태스크 · 서비스 · 클러스터
내 컴퓨터에서는 docker run 한 줄로 컨테이너를 실행했습니다. 그런데
실제 서비스에서는 그걸로 부족합니다. 컨테이너가 죽으면 누가 다시
살리나요? 밤 3시에 죽으면요? 새 버전으로 바꿀 때는 누가 순서대로 갈아
끼우나요? 이런 "컨테이너 돌보기"를 대신해 주는 관리자가 필요합니다.
그게 ECS입니다.
ECS(Elastic Container Service) AWS의 컨테이너 관리 서비스입니다. 컨테이너를 실행하고, 죽으면 다시 살리고, 새 버전으로 교체하는 일을 자동으로 해 주는 "컨테이너 매니저" 입니다.
Fargate(파게이트) 컨테이너를 "서버 관리 없이" 실행해 주는 방식입니다. 원래 컨테이너를 돌리려면 그 밑에 EC2 서버를 두고 관리해야 하는데, Fargate를 쓰면 AWS가 밑단 서버를 알아서 마련해 줍니다. 우리는 컨테이너만 신경 쓰면 됩니다. RDS가 "관리형 DB"였다면, Fargate는 "관리형 컨테이너 실행"인 셈입니다.
태스크(Task)와 태스크 정의(Task Definition)
- 태스크 정의: "무엇을 어떻게 실행할지" 적은 주문서입니다. 어떤
이미지를, 얼마의 CPU·메모리로, 어떤 환경변수와 함께 실행할지 적습니다.
1부의 docker-compose.yml과 비슷한 역할입니다.
- 태스크: 그 주문서대로 실제 실행된 컨테이너 묶음 하나입니다.
주문서(태스크 정의)로 태스크를 여러 개 찍어낼 수 있습니다.
서비스(Service) "태스크를 항상 N개 유지하라"는 지시를 받은 관리자입니다. 예를 들어 "태스크 2개 유지"로 설정하면, 하나가 죽는 순간 서비스가 알아차리고 새 태스크를 띄워 다시 2개를 맞춥니다. 3부 무중단 배포의 주역입니다.
클러스터(Cluster) 서비스와 태스크들을 담는 논리적인 울타리입니다. "우리 식당 지점 하나" 라고 생각하면 됩니다. 우리는 클러스터 하나에 서비스 하나를 둡니다.
식당에 비유해 정리해 봅시다.
| 개념 | 식당 비유 |
|---|---|
| 태스크 정의 | 레시피 (무엇을 어떻게 만들지 적은 문서) |
| 태스크 | 그 레시피로 만든 요리 한 상 |
| 서비스 | "항상 2상이 준비돼 있게 하라"를 지키는 지배인 |
| 클러스터 | 우리 식당 지점 하나 (요리와 지배인이 담기는 울타리) |
| ECS | 식당 운영 시스템 전체 |
| Fargate | 주방 설비를 빌려주는 방식 (주방 관리는 건물주가) |
[활용 사례] ECS·Fargate는 실무에서 어디에 쓰이나
우리 투표 서비스에만 쓰는 게 아닙니다. Fargate 방식의 컨테이너 실행은 현업에서 이렇게 다양하게 쓰입니다.
- 서버 관리 없이 웹 서비스 백엔드 운영: 서버(EC2)를 직접 사서 OS를 패치하고 관리하는 대신, 컨테이너 이미지만 올려 두면 AWS가 밑단 서버를 마련하고, 컨테이너가 죽으면 자동으로 되살립니다. 새벽에 장애가 나도 사람이 안 깨어나도 됩니다.
- 트래픽에 따라 자동 확장(오토스케일): 평소엔 태스크 2개로 돌다가, 이벤트나 세일로 사용자가 몰리면 태스크를 8개로 자동으로 늘리고, 한산해지면 다시 줄입니다. 쓴 만큼만 요금이 나가므로, 서버를 항상 최대치로 켜 둘 필요가 없습니다.
- 주기적인 배치 작업: "매일 밤 매출 리포트 만들기"처럼 잠깐만 도는 작업을, 서버를 상시 켜 두지 않고 Fargate 태스크로 그때만 띄웠다가 끕니다. 24시간 켜 둔 서버 값을 아낍니다.
- 여러 팀·여러 서비스의 독립 배포: 한 회사 안에서 결제팀·검색팀이 각자의 서비스를 각자의 태스크 정의로 독립 배포합니다. 서로의 서버를 건드리지 않고, 컨테이너 단위로 나눠서 운영합니다.
[맛보기] ECS 콘솔을 눈으로 둘러보기 (아무것도 만들지 않습니다)
용어만으로는 감이 안 잡히니, 실제 화면을 한 번 열어 봅시다. 이 맛보기는 리소스를 만들지 않으므로 요금이 나가지 않습니다. 위치만 눈에 익히는 과정입니다.
- AWS 콘솔 상단 검색창에
ECS를 입력하고 Elastic Container Service를 클릭해 들어갑니다. - 왼쪽 사이드바에서 클러스터(Clusters) 를 클릭합니다. 아직 아무것도 안 만들었으니 목록이 비어 있을 겁니다. ("식당 지점이 아직 없다"는 뜻입니다.)
- 오른쪽 위 클러스터 생성(Create cluster) 버튼을 눌러 봅니다. 생성 화면이 뜨면, 인프라(Infrastructure) 항목에 AWS Fargate(서버리스) 가 기본으로 선택돼 있는 것을 눈으로 확인하세요. 방금 배운 "서버 관리 없이 실행"이 바로 이 선택지입니다.
- 여기서는 만들지 말고, 브라우저 뒤로 가기나 화면을 벗어나 생성을 취소합니다. (실제 생성은 14.5절에서 값을 제대로 넣고 합니다.)
방금 여러분은 이 장에서 계속 쓸 ECS 콘솔의 위치와 Fargate 선택지를 직접 봤습니다. 이제 그 화면이 무섭지 않습니다.
14.2 ALB — 왜 로드밸런서가 필요한가
컨테이너(태스크)를 띄우면 각각 IP 주소를 받습니다. 그런데 문제가 있습니다.
- 태스크는 죽고 새로 뜨면서 IP가 계속 바뀝니다. 사용자에게 "오늘의 IP"를 알려 줄 수는 없죠.
- 태스크가 2개면 사용자를 어느 쪽으로 보낼지 정해야 합니다.
- 우리 태스크는 프라이빗 서브넷에 있어 인터넷에서 직접 닿지도 않습니다.
이 세 문제를 한 번에 푸는 것이 로드밸런서입니다.
ALB(Application Load Balancer) 사용자의 요청을 대표로 받아, 뒤에 있는 여러 태스크에 골고루 나눠 주는 "교통정리 요원"입니다. 항상 같은 주소를 유지하므로 사용자는 ALB 주소만 알면 되고, 뒤의 태스크가 바뀌든 늘든 신경 쓸 필요가 없습니다. 11장 설계대로 퍼블릭 서브넷에 서서, 인터넷의 요청을 받아 프라이빗의 태스크에게 전달합니다.
ALB를 다룰 때 함께 나오는 세 용어도 정리합니다.
타깃 그룹(Target Group) ALB가 요청을 나눠 줄 "대상 목록"입니다. 우리 태스크들이 이 목록에 등록되고, ALB는 이 목록을 보고 요청을 분배합니다. (ECS 서비스가 태스크를 이 목록에 자동으로 등록·해제해 줍니다.)
리스너(Listener) ALB의 "접수 창구"입니다. "80번 포트(HTTP)로 오는 요청을 받아서, 이 타깃 그룹으로 넘겨라"라는 규칙입니다.
헬스체크(Health Check) ALB가 각 태스크에게 주기적으로 "살아 있니?"라고 확인 요청을 보내는 것입니다. 응답이 없거나 이상한 태스크에게는 사용자 요청을 보내지 않습니다. 3부 무중단 배포의 핵심 장치이니 이름을 기억해 두세요.
[활용 사례] 로드밸런서는 실무에서 어디에 쓰이나
ALB 같은 로드밸런서는 규모 있는 서비스라면 거의 예외 없이 앞단에 서 있습니다. 대표적인 쓰임새는 이렇습니다.
- 트래픽 분산: 사용자가 몰릴 때, 요청을 여러 서버(태스크)에 골고루 나눠 한 대에 부하가 쏠려 멈추는 것을 막습니다. 로드밸런서라는 이름 그대로의 역할입니다.
- 무중단 배포: 새 버전 태스크를 띄워 헬스체크로 "정상"이 확인되면, 로드밸런서가 트래픽을 새 버전으로 옮기고 옛 버전을 뺍니다. 사용자는 끊김을 못 느낍니다. 이 원리를 3부(19·20장)에서 직접 씁니다.
- 장애 격리: 헬스체크에 실패한(죽은) 태스크에게는 요청을 안 보냅니다. 일부가 고장 나도 서비스 전체가 멈추지 않습니다.
- 경로 기반 라우팅:
/api로 오는 요청은 API 서버로, 나머지는 웹 서버로 보내는 식으로, 주소 경로에 따라 뒤의 다른 그룹으로 나눠 보낼 수 있습니다.
[맛보기] 여러분은 이미 "리버스 프록시"의 축소판을 만들었습니다
ALB가 하는 "요청을 받아 뒤로 넘기는" 일을, 사실 여러분은 로컬에서 이미
작은 규모로 해 봤습니다. 1부에서 프론트의 Nginx가 /api 요청을 API 서버로
넘기도록 만들었죠. 그게 바로 "요청을 대신 받아 뒤로 전달"하는 리버스
프록시입니다. ALB는 이 아이디어를 네트워크의 정문에서, 여러 태스크를
상대로, 헬스체크까지 곁들여 하는 것이라고 보면 됩니다.
콘솔 위치도 미리 눈에 익혀 둡시다. (이 역시 아무것도 만들지 않아 무료입니다.)
- 콘솔 상단 검색창에
EC2를 입력해 들어갑니다. - 왼쪽 사이드바를 아래로 내려 로드 밸런싱(Load Balancing) 아래의 로드밸런서(Load Balancers) 와 대상 그룹(Target Groups) 메뉴가 있는 것을 확인합니다. 아직 비어 있을 겁니다. 이 두 곳이 14.4절에서 실제로 만들 장소입니다. 지금은 만들지 말고 위치만 확인하세요.
전체 그림은 이렇습니다.
14.3 배포 전 손질 — 프론트와 API를 한 상자 묶음으로
콘솔로 가기 전에, 우리 이미지에 작은 손질이 하나 필요합니다.
1부 로컬에서는 프론트와 API를 다른 포트로 띄우고 브라우저가 각각 접속했습니다. 하지만 실제 서비스에서는 사용자에게 주소를 하나만 주는 것이 좋습니다. 그래서 이렇게 구성합니다.
- 하나의 태스크에 web 컨테이너와 api 컨테이너를 함께 담는다. (1부 compose에서 여러 컨테이너를 함께 띄운 것과 같은 발상입니다.)
- ALB는 web(80) 으로만 요청을 보낸다.
- web의 Nginx가
/api로 시작하는 요청만 옆의 api 컨테이너(3000)로 전달한다. 같은 태스크 안의 컨테이너끼리는localhost로 통신할 수 있습니다.
프론트도 여기서는 컨테이너로 배포합니다 이 책의 기본 완성 구조에서는 프론트(React 빌드 결과)를 web 컨테이너의 Nginx가 서빙하고, 이 web 컨테이너도 ECS에 함께 올립니다. 즉 프론트와 API 둘 다 ECS 컨테이너입니다. (프론트를 Vercel로 따로 떼어 배포하는 방식은 21장 심화에서 별도로 다룹니다. 지금은 그 방식이 아닙니다.)
프록시(Proxy) 요청을 대신 받아 뒤로 전달해 주는 중계를 말합니다. "Nginx가 /api 요청을 api 컨테이너로 프록시한다"처럼 씁니다.
이 손질과 이미지 갱신을 Claude Code에게 맡깁니다. VS Code에서 vote-app
폴더를 연 상태로, 터미널에서 claude를 실행한 뒤 아래 프롬프트를
그대로 입력하세요.
이미지 아키텍처(CPU 종류)를 꼭 맞추세요 — 안 맞으면 태스크가 죽습니다
컨테이너 이미지는 CPU 종류(아키텍처)에 묶입니다. 우리 태스크 정의는
X86_64(amd64) 로 실행하는데(14.6), 이미지가 arm64로 빌드돼 있으면
실행 순간 exec format error로 태스크가 계속 죽습니다.
- Intel 맥 · Windows: 기본이 amd64라 그냥 빌드해도 맞습니다.
- Apple Silicon 맥(M1 이상): 기본이 arm64라, 위 프롬프트처럼
docker buildx build --platform linux/amd64 ... 로 빌드해야 합니다.
(반대로, 태스크 정의를 ARM64로 만들고 arm64 이미지를 쓰는 방법도 있지만,
이 책은 X86_64로 통일합니다.)
[참고] 왜 프록시 대상을 환경변수로 받나 — 로컬과 AWS의 차이
같은 태스크 안의 컨테이너들은 한 컴퓨터처럼 localhost로 서로
통신할 수 있습니다(AWS ECS의 방식). 반면 로컬 docker compose에서는
컨테이너마다 네트워크가 분리되어 있어, 상대 컨테이너를 localhost가
아니라 서비스 이름(api)으로 불러야 합니다. 실행 환경에 따라 달라지는
값이므로 — 1부에 배운 원칙 그대로 — 환경변수로 분리하는 것입니다.
[확인] ECR 콘솔에서 vote-web, vote-api의 1.0 태그가 방금
갱신되었는지(푸시 시각) 확인하고 넘어갑니다.
14.4 ALB 만들기 — 타깃 그룹과 리스너
이제 콘솔로 갑니다. 순서는 타깃 그룹(대상 목록)을 먼저 만들고, ALB를 만들며 연결합니다.
콘솔 오른쪽 위 리전이 서울(ap-northeast-2) 인지 다시
확인하세요. 리전이 다르면 11장에서 만든 vote-vpc가 목록에 안 보입니다.
[콘솔] 1단계: 타깃 그룹 만들기
- 상단 검색창에
EC2를 입력해 들어간 뒤, 왼쪽 사이드바를 아래로 내려 로드 밸런싱 → 대상 그룹(Target Groups) 을 클릭합니다. - 대상 그룹 생성(Create target group) 을 누릅니다.
- 설정을 입력합니다.
- 대상 유형(Target type): IP 주소(IP addresses) 를 선택합니다.
- [주의] 기본값인 "인스턴스(Instances)"가 아니라 IP 주소여야 합니다. Fargate 태스크는 서버(인스턴스)가 아니라 IP로 등록되기 때문입니다. 여기를 틀리면 나중에 ECS 서비스와 연결되지 않습니다.
- 대상 그룹 이름(Target group name):
vote-tg - 프로토콜 / 포트(Protocol / Port):
HTTP/80 - VPC:
vote-vpc선택 - 헬스체크 경로(Health check path):
/(기본값 그대로 둡니다)
- 대상 유형(Target type): IP 주소(IP addresses) 를 선택합니다.
- 화면 아래 다음(Next) 을 누르면 "대상 등록" 화면이 나옵니다. 여기서는
아무것도 등록하지 않고 그대로 대상 그룹 생성(Create target group) 을
누릅니다.
- [참고] 태스크 등록은 나중에 ECS 서비스가 자동으로 합니다. 지금 비어 있는 게 정상입니다.
대상 그룹 목록에 vote-tg가 생기고, 대상(Targets)이 0개인
상태면 정상입니다.
[콘솔] 2단계: ALB 만들기
- 왼쪽 사이드바에서 로드 밸런싱 → 로드밸런서(Load Balancers) 를 클릭하고 로드 밸런서 생성(Create load balancer) 을 누릅니다.
- 유형 선택에서 Application Load Balancer의 생성(Create) 을 누릅니다.
- 설정을 입력합니다.
- 로드 밸런서 이름(Name):
vote-alb - 체계(Scheme): 인터넷 경계(Internet-facing) (인터넷의 요청을 받는 ALB라는 뜻입니다.)
- 네트워크 매핑(Network mapping): VPC에서
vote-vpc를 선택하고, 가용영역(Availability Zones) 두 개를 모두 체크한 뒤 각각 퍼블릭 서브넷을 선택합니다.- [주의] 프라이빗 서브넷을 고르면 인터넷에서 접속되지 않습니다.
이름에
public이 들어간 서브넷인지 확인하세요. (11장에서 이름을 그렇게 지었습니다.)
- [주의] 프라이빗 서브넷을 고르면 인터넷에서 접속되지 않습니다.
이름에
- 보안 그룹(Security groups): 미리 붙어 있는 기본 보안 그룹이 있으면
해제하고, 새 보안 그룹 생성(Create new security group) 링크를 눌러
새 창에서 만듭니다.
- 보안 그룹 이름:
vote-alb-sg - 설명(Description): 아무 글자나 넣어도 됩니다(예:
alb sg). 비워 두면 저장이 안 됩니다. - VPC:
vote-vpc - 인바운드 규칙(Inbound rules) 추가: 유형 HTTP / 포트 자동
80/ 소스 Anywhere-IPv4 (0.0.0.0/0) - 11장에서 설계한 "ALB 보안 그룹: 누구나 → 80"이 이것입니다.
- 보안 그룹 생성을 누른 뒤, ALB 생성 화면으로 돌아와 보안 그룹
목록의 새로고침(↻) 아이콘을 누르고
vote-alb-sg를 선택합니다.
- 보안 그룹 이름:
- 리스너 및 라우팅(Listeners and routing): 프로토콜
HTTP, 포트80, 기본 작업(전달 대상)으로 방금 만든vote-tg를 선택합니다. ("80으로 오는 요청은 vote-tg 목록의 대상들에게 전달하라"는 리스너 규칙입니다.)
- 로드 밸런서 이름(Name):
- 로드 밸런서 생성(Create load balancer) 을 누릅니다. 상태가 프로비저닝 중(Provisioning) → 활성(Active) 이 될 때까지 몇 분 기다립니다.
로드밸런서 목록에서 vote-alb를 클릭하면, 세부 정보에
DNS 이름(DNS name) 이 보입니다.
vote-alb-1234567890.ap-northeast-2.elb.amazonaws.com 같은 형태입니다.
이것이 우리 서비스의 주소가 됩니다. 복사해서 메모장에 붙여 두세요.
(지금 접속하면 오류가 납니다. 뒤에 태스크가 아직 없으니까요. 정상입니다.)
도메인 없이 진행합니다
실무에서는 이 DNS 이름에 vote.example.com 같은 커스텀 도메인을 연결
하고 HTTPS를 붙이지만, 도메인 구매가 필요하므로 이 실습에서는 ALB의
기본 DNS 이름 + HTTP로 진행합니다. 커스텀 도메인과 HTTPS 연결은 이 책의
기본 실습 범위 밖입니다. (프론트를 Vercel로 분리해 HTTPS를 얻는 방법은
21장 심화에서 다룹니다.)
14.5 ECS 클러스터 만들기
이제 태스크와 서비스를 담을 울타리(클러스터)를 만듭니다.
[콘솔] 클러스터 생성
- 상단 검색창에
ECS를 입력해 Elastic Container Service로 들어갑니다. - 왼쪽 사이드바에서 클러스터(Clusters) → 클러스터 생성(Create cluster) 을 누릅니다.
- 설정을 입력합니다.
- 클러스터 이름(Cluster name):
vote-cluster - 인프라(Infrastructure): AWS Fargate(서버리스) 가 선택돼 있는지
확인합니다. (14.1절 맛보기에서 본 그 선택지입니다. 기본값이라 보통
이미 켜져 있습니다.)
- [용어] 서버리스(Serverless): "서버 관리가 필요 없다"는 뜻의 업계 용어입니다. 서버가 없다는 게 아니라, 서버 관리를 AWS가 대신 한다는 의미로, 14.1에서 설명한 Fargate의 성격 그대로입니다.
- 클러스터 이름(Cluster name):
- 생성(Create) 을 누릅니다. 금방 만들어집니다.
클러스터 목록에 vote-cluster가 활성(Active) 상태로
보이면 성공입니다.
빈 식당(클러스터)이 하나 생겼습니다. 이제 레시피(태스크 정의)를 쓰고, 지배인(서비스)을 고용할 차례입니다.
14.6 태스크 정의 작성 — 실행 주문서
[콘솔] 태스크 정의 생성
- ECS 콘솔 왼쪽 사이드바에서 태스크 정의(Task definitions) →
새 태스크 정의 생성(Create new task definition) 을 누릅니다.
- [주의] 비슷한 이름의 "JSON을 사용하여 새 태스크 정의 생성" 메뉴가 함께 보일 수 있습니다. 우리는 화면에서 값을 채우는 일반 새 태스크 정의 생성 쪽을 고릅니다.
- 기본 설정을 입력합니다.
- 태스크 정의 패밀리(Task definition family):
vote-task- [용어] 패밀리(Family): 이 주문서의 이름입니다. 주문서를 고칠
때마다
vote-task:1,vote-task:2처럼 번호(리비전)가 붙으며 이력이 쌓입니다. 이 번호가 3부 롤백에서 다시 등장합니다.
- [용어] 패밀리(Family): 이 주문서의 이름입니다. 주문서를 고칠
때마다
- 시작 유형(Launch type): AWS Fargate 가 선택돼 있는지 확인합니다.
- 운영체제/아키텍처: 기본값(Linux/X86_64)을 그대로 둡니다.
- 태스크 크기(Task size) — CPU / 메모리: 가장 작은 조합을 고릅니다.
예: CPU
.5 vCPU, 메모리1GB.
- 태스크 정의 패밀리(Task definition family):
- 컨테이너 1(Container - 1) 을 입력합니다. (web 컨테이너)
- 이름(Name):
web - 이미지 URI(Image URI): ECR의
vote-web리포지토리 URI에 태그를 붙인 값. 예:123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/vote-web:1.0- [참고] ECR 콘솔 →
vote-web→ 이미지에서 URI 복사 버튼으로 정확한 값을 복사할 수 있습니다.123456789012자리에는 여러분의 12자리 계정 번호가 들어갑니다.
- [참고] ECR 콘솔 →
- 컨테이너 포트(Container port):
80 - 환경 변수(Environment variables) 를 펼쳐 하나 넣습니다.
API_URL=http://localhost:3000- [주의] 이 값을 빠뜨리면 화면은 떠도 투표가 안 됩니다. web의
Nginx가
/api요청을 넘길 대상 주소입니다. 로컬 docker compose에서는http://api:3000(서비스 이름)이었지만, ECS에서는 web과 api가 한 태스크 안에서localhost로 통신하므로http://localhost:3000으로 바꿔 줍니다. (7장에서 이 주소를 환경변수API_URL로 뺀 이유가 바로 이것입니다.) 안 넣으면 Dockerfile 기본값http://api:3000으로 프록시하다 실패해,/api호출이 502로 떨어집니다.
- 이름(Name):
- 화면 아래 컨테이너 추가(Add container) 를 눌러 컨테이너 2 를
입력합니다. (api 컨테이너)
- 이름(Name):
api - 이미지 URI(Image URI):
123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/vote-api:1.0(여기도 여러분의 계정 번호로.) - 컨테이너 포트(Container port):
3000 - 환경 변수(Environment variables) 항목을 펼쳐, DB 접속 정보를
넣습니다. 12장에서 기록한 값 다섯 가지입니다. 변수 이름은 1부에서
만든 프로젝트의
.env와 똑같이 맞춥니다. 다르면 api가 DB를 못 찾습니다. (아래 이름이 확실치 않으면,vote-app/backend의.env나 DB 접속 코드를 열어 실제로 쓰는 변수 이름을 확인해 그대로 씁니다.)DB_HOST= RDS 엔드포인트 (vote-db.xxxx.ap-northeast-2.rds.amazonaws.com)DB_PORT=5432DB_NAME=voteappDB_USER=postgresDB_PASSWORD= 12장에서 정한 마스터 암호DB_SSL=true
- [주의] 이 값도 빠뜨리면 api가 DB에 못 붙습니다. AWS RDS는
기본으로 SSL 접속만 허용합니다(파라미터
rds.force_ssl=1). 그래서 배포 환경에서는DB_SSL=true로 SSL 접속을 켭니다. 로컬 docker compose의 PostgreSQL은 SSL이 없어 이 값을 넣지 않았습니다. (14.3에서 앱이 이 환경변수를 읽어 SSL을 켜도록 이미 손질해 둡니다.) 빠뜨리면 화면은 떠도 설문 조회에서internal server error가 납니다.- [주의] 실무에서는 비밀번호를 이렇게 평문 환경변수로 넣지 않고 Secrets Manager를 연결합니다. 학습 단순화를 위한 구성임을 기억하세요.
- 이름(Name):
- 나머지는 기본값으로 둡니다. 이때 로그 수집(로그 구성 / Log collection)
항목이 기본으로 켜져 있는지만 봐 두세요. 콘솔로 태스크 정의를 만들면
CloudWatch Logs로 로그를 보내는 설정(
awslogs)이 기본으로 켜집니다. 14.9절에서 이 로그를 봅니다. 끝으로 생성(Create) 을 누릅니다.
태스크 정의 목록에 vote-task가 리비전 1 로 생기면
성공입니다. 주문서 완성입니다.
14.7 ECS 서비스 만들기 — ALB에 연결
이제 지배인(서비스)을 고용해 "태스크를 항상 2개 유지하고, ALB와 연결하라" 고 지시합니다.
[콘솔] 서비스 생성
- 클러스터(Clusters) →
vote-cluster로 들어가 서비스(Services) 탭에서 생성(Create) 을 누릅니다. - 배포 구성(Deployment configuration) 을 설정합니다.
- 컴퓨팅 옵션 / 시작 유형: 시작 유형(Launch type) 을 고르고 Fargate 를 선택합니다.
- 애플리케이션 유형(Application type): 서비스(Service)
- 태스크 정의 패밀리(Family):
vote-task, 리비전은 최신(1) - 서비스 이름(Service name):
vote-service - 원하는 태스크 수(Desired tasks):
2- [참고] 2개로 두면 두 가용영역(AZ)에 하나씩 배치되어, 하나가 죽어도 서비스가 유지됩니다. 11장의 고가용성이 여기서 실현됩니다.
- 네트워킹(Networking) 을 펼쳐 설정합니다.
- VPC:
vote-vpc - 서브넷(Subnets): 프라이빗 서브넷 2개를 선택합니다. (목록에서
퍼블릭 서브넷이 기본으로 선택돼 있으면 빼고, 프라이빗만 남깁니다.)
- [주의] 태스크는 인터넷에 직접 노출되면 안 되므로 프라이빗에 둡니다. 사용자 요청은 ALB가 대신 전달해 줍니다.
- 보안 그룹(Security group): 새 보안 그룹 생성(Create a new
security group) 을 선택합니다.
- 보안 그룹 이름:
vote-ecs-sg - 설명: 아무 글자나(예:
ecs sg). - 인바운드 규칙: 유형 HTTP(포트
80) / 소스에서vote-alb-sg를 선택합니다. (소스 칸에vote-alb-sg를 검색해 고릅니다.) - 11장 설계 "ECS 보안 그룹: ALB에서 온 요청만"이 이것입니다.
- 보안 그룹 이름:
- 퍼블릭 IP(Public IP): 꺼짐(Turned off)
- VPC:
- 로드 밸런싱(Load balancing) 을 펼쳐 설정합니다.
- 로드 밸런서 유형(Load balancer type): Application Load Balancer
- 기존 로드 밸런서 사용(Use an existing load balancer):
vote-alb - 로드 밸런싱할 컨테이너(Container to load balance):
web 80:80(api가 아니라 web 을 고릅니다. 사용자 요청은 web으로 들어갑니다.) - 리스너: 기존 리스너 사용 —
HTTP:80 - 대상 그룹(Target group): 기존 대상 그룹 사용(Use an existing
target group) —
vote-tg - ("ALB로 온 요청을 web 컨테이너의 80 포트로 보내라"는 연결입니다.)
- 생성(Create) 을 누릅니다. 서비스가 태스크 2개를 띄우기 시작합니다. 몇 분 걸립니다.
[콘솔] 마지막 열쇠: RDS의 문을 ECS에게 열어 주기
12장에서 미뤄 둔 마지막 보안 그룹 규칙을 완성합니다. 지금 태스크가 떠도, RDS 문이 잠겨 있으면 api가 DB에 못 붙습니다.
- EC2 콘솔 → 왼쪽 사이드바 보안 그룹(Security Groups) → 목록에서
vote-db-sg를 선택합니다. - 인바운드 규칙(Inbound rules) 탭 → 인바운드 규칙 편집(Edit inbound
rules) → 규칙 추가(Add rule) 를 누릅니다.
- 유형(Type):
PostgreSQL(포트 자동5432) - 소스(Source):
vote-ecs-sg를 검색해 선택합니다.
- 유형(Type):
- 규칙 저장(Save rules) 을 누릅니다.
이로써 11장에서 설계한 사슬이 완성됐습니다. 인터넷 → (80) → vote-alb-sg → (80) → vote-ecs-sg → (5432) → vote-db-sg
14.8 접속 확인 — 인터넷에 서비스가 열렸다
- ECS 콘솔에서
vote-cluster→vote-service→ 태스크(Tasks) 탭을 봅니다. 태스크 2개의 마지막 상태(Last status) 가 실행 중(Running) 이 되고, 상태(Health status) 가 정상(Healthy) 이 될 때까지 몇 분 기다립니다. (처음에는 Pending → Running 순서로 바뀝니다.) - 타깃 그룹도 확인합니다. EC2 콘솔 → 대상 그룹 →
vote-tg→ 대상(Targets) 에서, 등록된 대상 2개의 상태가 healthy 로 바뀌면 준비 완료입니다. (ALB 헬스체크가 통과했다는 뜻입니다. 잠깐initial이었다가 healthy로 바뀝니다.) - 14.4에서 복사해 둔 ALB DNS 이름 앞에
http://를 붙여 브라우저 주소창에 붙여넣고 접속합니다. 예:http://vote-alb-1234567890.ap-northeast-2.elb.amazonaws.com
투표 서비스 화면이 뜨면 — 성공입니다. 13장에서 심어 둔 "점심 뭐 먹지?" 설문이 보이고 투표가 된다면, 지금 이 순간:
- 여러분의 브라우저가 ALB(퍼블릭)에 접속했고
- ALB가 프라이빗의 태스크(web 컨테이너)로 요청을 넘겼고
- web의 Nginx가
/api요청을 api 컨테이너로 프록시했고 - api가 RDS에서 설문 데이터를 읽어 왔습니다.
2부 내내 만든 모든 조각이 한 줄로 꿰어진 것입니다. 이것이 2부의 목적지, 기본 완성입니다. 이 주소를 친구에게 보내 보세요. 전 세계 어디서든 접속됩니다. 여러분의 서비스가 처음으로 인터넷에 존재하게 된 순간입니다.
접속이 안 될 때 점검 순서
아래 순서대로 하나씩 확인하면 대부분 원인이 잡힙니다.
1. 태스크가 Running인지. (계속 죽었다 살아나면 → 14.9절 로그 확인)
2. 타깃 그룹(vote-tg)에서 대상 상태가 healthy인지. (unhealthy면
헬스체크 경로 /나 web 컨테이너 포트 80 설정을 다시 확인)
3. 보안 그룹 사슬이 모두 있는지: ALB(80 개방), ECS(ALB에서 80), RDS
(ECS에서 5432). 특히 14.7의 마지막 RDS 규칙을 빠뜨리기 쉽습니다.
4. ALB가 퍼블릭 서브넷에, 태스크가 프라이빗 서브넷에 있는지.
5. 주소 앞에 https://가 아니라 http:// 를 붙였는지. (우리는
HTTPS를 설정하지 않았습니다.)
14.9 컨테이너 로그 보기 — CloudWatch Logs
컨테이너가 프라이빗 서브넷 안에서 도니, 문제가 생기면 어떻게 들여다볼까요? 컨테이너가 출력하는 로그는 CloudWatch라는 곳에 자동으로 모입니다.
CloudWatch Logs
AWS의 로그 수집·보관 서비스입니다. 태스크 정의를 콘솔로 만들면 로그
전송 설정(awslogs)이 기본으로 켜져서, 컨테이너의 출력(console.log,
오류 메시지 등)이 자동으로 여기에 쌓입니다.
[콘솔] 로그 확인
- ECS 콘솔 →
vote-cluster→vote-service→ 태스크(Tasks) 탭에서 실행 중인 태스크 하나를 클릭합니다. - 로그(Logs) 탭을 누르면 web / api 컨테이너의 로그가 보입니다.
- api 컨테이너의 로그에서 서버 시작 메시지나 DB 연결 성공 메시지를 찾아보세요. (컨테이너를 위쪽에서 골라 전환할 수 있습니다.)
배포 문제의 원인은 대부분 이 로그에 적혀 있습니다. "태스크가 자꾸 죽어요", "화면이 안 떠요" 싶으면 로그부터 보는 것이 실무의 기본기입니다. 로그 내용을 복사해 Claude Code에게 붙여넣고 원인을 물어보는 것도 1부에 배운 그대로입니다.
[확인] 14장 & 2부 체크리스트
이번 장이 제대로 끝났는지 아래로 확인합니다. 하나라도 안 됐다면 해당 절로 돌아가 다시 진행하세요.
- □ ECS / Fargate / 태스크 정의 / 태스크 / 서비스 / 클러스터를 식당 비유로 설명할 수 있다
- □ ALB·타깃 그룹·리스너·헬스체크의 역할을 안다
- □ 프론트와 API를 한 태스크의 web·api 두 컨테이너로 묶고, 이미지를
1.0으로 다시 빌드해 ECR에 푸시했다 (14.3) - □ 타깃 그룹
vote-tg를 IP 유형으로 만들었다 (Fargate라서) - □ ALB
vote-alb를 퍼블릭 서브넷에, 보안 그룹 80 개방으로 만들고 DNS 이름을 복사해 뒀다 - □ 클러스터
vote-cluster(Fargate)를 만들었다 - □ 태스크 정의
vote-task에 두 컨테이너(web·api)와 DB 환경변수를 넣었다 - □ 서비스
vote-service가 태스크 2개를 프라이빗 서브넷에 유지한다 - □ 보안 그룹 사슬(ALB→ECS→RDS)을 완성했다 (RDS에 ecs-sg 허용 포함)
- □ ALB DNS 주소(http://)로 접속해 투표 서비스가 동작하는 것을 확인했다
- □ CloudWatch(태스크의 로그 탭)에서 컨테이너 로그를 찾아볼 수 있다
모두 체크됐다면 2부의 기본 완성에 도착한 겁니다.
막히면
이 장에서 자주 나는 오류와 대처법입니다.
-
ALB DNS 주소로 접속했는데
503 Service Temporarily Unavailable이 뜬다. ALB 뒤에 정상(healthy) 태스크가 아직 없다는 뜻입니다. 태스크가 뜨는 중일 수 있으니 몇 분 기다린 뒤 새로고침하세요. 계속 그러면 14.8의 "점검 순서" 1~2번(태스크 Running, 타깃 healthy)을 확인합니다. -
타깃 그룹의 대상이 계속
unhealthy다. ALB의 헬스체크(/)에 web 컨테이너가 정상 응답을 못 하는 상태입니다. 타깃 그룹을 IP 유형으로 만들었는지, ECS 서비스에서 로드밸런싱 대상 컨테이너를 web 80:80으로 지정했는지, ECS 보안 그룹(vote-ecs-sg)이 ALB(vote-alb-sg)에서 80을 허용하는지 확인하세요. -
태스크가 떴다가 자꾸 죽는다(Running → Stopped 반복). 대개 api가 DB에 못 붙거나 이미지 실행 자체가 실패한 경우입니다. 14.9절대로 태스크의 로그(Logs) 탭을 열어 오류 메시지를 봅니다. DB 연결 오류라면: (1) 태스크 정의의 DB 환경변수 이름·값이
.env와 같은지, (2) 14.7 마지막 단계에서vote-db-sg에vote-ecs-sg(5432) 규칙을 넣었는지 확인합니다. 로그를 복사해 Claude Code에게 물어보면 원인을 짚어 줍니다. -
ECS 서비스 생성 화면에서 로드밸런서로
vote-alb나 대상 그룹vote-tg가 목록에 안 보인다. 같은 VPC(vote-vpc)·같은 리전(서울)인지 확인하세요. 타깃 그룹을 "인스턴스" 유형으로 잘못 만들었다면 Fargate 서비스와 연결되지 않으니, 14.4 1단계로 돌아가 IP 유형으로 다시 만듭니다. -
화면은 뜨는데 투표가 안 되거나 설문이 비어 있다. 프론트(web)는 뜨지만 api ↔ DB 구간이 막힌 경우입니다. 14.9절 로그에서 api 컨테이너를 보고 DB 연결 상태를 확인하세요. 13장에서 초기 데이터를 실제로 심었는지도 함께 점검합니다.
14.10 2부를 마치며
2부에서 우리는 손(콘솔)으로 이만큼을 해냈습니다.
- AWS 계정을 준비하고 IAM 사용자·리전·비용 감각을 잡았다 (8장)
- 이미지 태그가 롤백의 열쇠임을 배웠다 (9장)
- 이미지를 ECR 창고에 올렸다 (10장)
- VPC로 격리된 네트워크를 짓고 보안 그룹을 설계했다 (11장)
- RDS를 프라이빗에 만들고 (12장), SSM 터널로 접속해 초기화했다 (13장)
- ECS Fargate로 컨테이너를 실행하고 ALB로 인터넷에 공개했다 (14장)
한 가지 짚을 점: 여기까지의 배포는 전부 손으로 했습니다. 이미지를 손으로 빌드해 푸시했고, 콘솔을 손으로 클릭해 구성했습니다. 코드가 바뀔 때마다 이걸 반복할 수는 없겠죠. 이 "손으로 한 일"을 코드와 자동화로 바꾸는 것이 3부의 주제입니다. (전체 여정에서 지금 어디쯤인지는 맨 앞 [지금 여기] 지도로 확인하세요.)
실습 리소스, 지울까요 둘까요? 바로 이어서 실습한다면 그대로 두는 것이 편합니다. 다만 NAT 게이트웨이·RDS·ALB·Fargate는 켜 둔 시간만큼 요금이 나옵니다(하루 이틀은 소액이지만 0은 아닙니다). 실습을 며칠 쉬어야 한다면, 22장의 "리소스 삭제 가이드"를 먼저 참고해 지웠다가 다시 만들어도 됩니다. 다시 만드는 것 자체가 훌륭한 복습입니다.
수고했습니다. 여러분의 서비스는 지금 이 순간에도 AWS 위에서 돌아가고 있습니다.