바이브코딩 서비스를 Docker 컨테이너로 실전서비스 론칭하기 · 2부 — 이미지 레지스트리, AWS 인프라(콘솔)

14장. ECS Fargate + ALB로 배포 (기본 완성)

드디어 2부의 마지막이자 클라이맥스입니다. 지금까지 창고에 이미지를 넣었고(10장), 네트워크를 지었고(11장), DB를 만들어(12장) 초기 데이터까지 심었습니다(13장). 이제 주인공인 컨테이너를 무대에 올려, 인터넷에서 진짜로 접속되는 투표 서비스를 완성합니다.

이 장에는 새 용어가 많이 나옵니다. 하지만 걱정 마세요. 처음에 용어를 한꺼번에 정리하고, 실무에서 어디에 쓰이는지 본 다음, 콘솔에서 하나씩 만듭니다. 그러면 화면에 그 이름들이 나올 때마다 "아, 그거"하고 알아볼 수 있습니다. 한 단계씩 천천히 따라오세요.

[지금 여기]

우리는 22단계를 거쳐 "라이브 투표 서비스"를 직접 만들고, 컨테이너에 담아, AWS 클라우드에 배포하고 있습니다. 아래가 그 전체 여정 지도입니다. 지금은 14단계, 2부의 목적지인 "기본 완성"에 도착하는 장입니다.

  1. 1장개발 환경 준비 (터미널 / Node.js / Git / GitHub / VS Code / Claude Code)
  2. 2장Claude Code 사용법 익히기
  3. 3장무엇을 만들지 기획 (라이브 투표 서비스)
  4. 4장로컬에서 서비스 만들기 (React + Express + PostgreSQL)
  5. 5장Docker 개념
  6. 6장Dockerfile 작성
  7. 7장컨테이너 빌드·실행
  8. 8장AWS 시작 (계정 / IAM / 리전 / 비용)
  9. 9장이미지 태깅
  10. 10장ECR에 이미지 올리기
  11. 11장VPC 네트워크 구성
  12. 12장RDS 데이터베이스 생성
  13. 13장SSM 터널로 RDS 초기화
  14. 14장ECS Fargate + ALB로 배포 (기본 완성)
  15. 15장CI/CD (GitHub Actions)
  16. 16장IaC (Terraform)
  17. 17장배포 자동화 연결
  18. 18장현업 운영 구조 이해
  19. 19장무중단 배포
  20. 20장신규 버전 배포 & 롤백
  21. 21장(심화) 프론트를 Vercel로 분리 배포
  22. 22장마무리 (회고 + 리소스 삭제)

이번 장에서 완성되는 것

한 문장으로: ALB의 기본 DNS 주소를 브라우저에 붙여넣으면, 13장에서 심어 둔 설문이 뜨고 실제로 투표가 되는 서비스가 인터넷에 열립니다.

이렇게 되면 성공입니다: http://vote-alb-...elb.amazonaws.com 같은 주소로 접속했을 때 투표 화면이 보이고, 투표 버튼을 누르면 결과가 반영됩니다. 이 순간 사용자 브라우저 → ALB → ECS(web+api) → RDS 전 구간이 한 줄로 꿰어집니다. 2부 내내 만든 모든 조각이 처음으로 함께 작동합니다.

사전 조건

이 장은 1~13장의 결과물 위에서 시작합니다. 아래가 갖춰져 있어야 합니다. 하나라도 빠져 있으면 이 장 중간에 막히니, 먼저 확인하세요.

참고

콘솔 화면이 이 책의 그림과 조금 달라도 괜찮습니다 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 방식의 컨테이너 실행은 현업에서 이렇게 다양하게 쓰입니다.

[맛보기] ECS 콘솔을 눈으로 둘러보기 (아무것도 만들지 않습니다)

용어만으로는 감이 안 잡히니, 실제 화면을 한 번 열어 봅시다. 이 맛보기는 리소스를 만들지 않으므로 요금이 나가지 않습니다. 위치만 눈에 익히는 과정입니다.

  1. AWS 콘솔 상단 검색창에 ECS를 입력하고 Elastic Container Service를 클릭해 들어갑니다.
  2. 왼쪽 사이드바에서 클러스터(Clusters) 를 클릭합니다. 아직 아무것도 안 만들었으니 목록이 비어 있을 겁니다. ("식당 지점이 아직 없다"는 뜻입니다.)
  3. 오른쪽 위 클러스터 생성(Create cluster) 버튼을 눌러 봅니다. 생성 화면이 뜨면, 인프라(Infrastructure) 항목에 AWS Fargate(서버리스) 가 기본으로 선택돼 있는 것을 눈으로 확인하세요. 방금 배운 "서버 관리 없이 실행"이 바로 이 선택지입니다.
  4. 여기서는 만들지 말고, 브라우저 뒤로 가기나 화면을 벗어나 생성을 취소합니다. (실제 생성은 14.5절에서 값을 제대로 넣고 합니다.)

방금 여러분은 이 장에서 계속 쓸 ECS 콘솔의 위치와 Fargate 선택지를 직접 봤습니다. 이제 그 화면이 무섭지 않습니다.

14.2 ALB — 왜 로드밸런서가 필요한가

컨테이너(태스크)를 띄우면 각각 IP 주소를 받습니다. 그런데 문제가 있습니다.

이 세 문제를 한 번에 푸는 것이 로드밸런서입니다.

용어

ALB(Application Load Balancer) 사용자의 요청을 대표로 받아, 뒤에 있는 여러 태스크에 골고루 나눠 주는 "교통정리 요원"입니다. 항상 같은 주소를 유지하므로 사용자는 ALB 주소만 알면 되고, 뒤의 태스크가 바뀌든 늘든 신경 쓸 필요가 없습니다. 11장 설계대로 퍼블릭 서브넷에 서서, 인터넷의 요청을 받아 프라이빗의 태스크에게 전달합니다.

ALB를 다룰 때 함께 나오는 세 용어도 정리합니다.

용어

타깃 그룹(Target Group) ALB가 요청을 나눠 줄 "대상 목록"입니다. 우리 태스크들이 이 목록에 등록되고, ALB는 이 목록을 보고 요청을 분배합니다. (ECS 서비스가 태스크를 이 목록에 자동으로 등록·해제해 줍니다.)

용어

리스너(Listener) ALB의 "접수 창구"입니다. "80번 포트(HTTP)로 오는 요청을 받아서, 이 타깃 그룹으로 넘겨라"라는 규칙입니다.

용어

헬스체크(Health Check) ALB가 각 태스크에게 주기적으로 "살아 있니?"라고 확인 요청을 보내는 것입니다. 응답이 없거나 이상한 태스크에게는 사용자 요청을 보내지 않습니다. 3부 무중단 배포의 핵심 장치이니 이름을 기억해 두세요.

[활용 사례] 로드밸런서는 실무에서 어디에 쓰이나

ALB 같은 로드밸런서는 규모 있는 서비스라면 거의 예외 없이 앞단에 서 있습니다. 대표적인 쓰임새는 이렇습니다.

[맛보기] 여러분은 이미 "리버스 프록시"의 축소판을 만들었습니다

ALB가 하는 "요청을 받아 뒤로 넘기는" 일을, 사실 여러분은 로컬에서 이미 작은 규모로 해 봤습니다. 1부에서 프론트의 Nginx가 /api 요청을 API 서버로 넘기도록 만들었죠. 그게 바로 "요청을 대신 받아 뒤로 전달"하는 리버스 프록시입니다. ALB는 이 아이디어를 네트워크의 정문에서, 여러 태스크를 상대로, 헬스체크까지 곁들여 하는 것이라고 보면 됩니다.

콘솔 위치도 미리 눈에 익혀 둡시다. (이 역시 아무것도 만들지 않아 무료입니다.)

  1. 콘솔 상단 검색창에 EC2를 입력해 들어갑니다.
  2. 왼쪽 사이드바를 아래로 내려 로드 밸런싱(Load Balancing) 아래의 로드밸런서(Load Balancers)대상 그룹(Target Groups) 메뉴가 있는 것을 확인합니다. 아직 비어 있을 겁니다. 이 두 곳이 14.4절에서 실제로 만들 장소입니다. 지금은 만들지 말고 위치만 확인하세요.

전체 그림은 이렇습니다.

사용자ALB퍼블릭 · 창구 80타깃 그룹대상 목록태스크들프라이빗헬스체크: "살아 있는 태스크에게만 보낸다"

14.3 배포 전 손질 — 프론트와 API를 한 상자 묶음으로

콘솔로 가기 전에, 우리 이미지에 작은 손질이 하나 필요합니다.

1부 로컬에서는 프론트와 API를 다른 포트로 띄우고 브라우저가 각각 접속했습니다. 하지만 실제 서비스에서는 사용자에게 주소를 하나만 주는 것이 좋습니다. 그래서 이렇게 구성합니다.

참고

프론트도 여기서는 컨테이너로 배포합니다 이 책의 기본 완성 구조에서는 프론트(React 빌드 결과)를 web 컨테이너의 Nginx가 서빙하고, 이 web 컨테이너도 ECS에 함께 올립니다. 즉 프론트와 API 둘 다 ECS 컨테이너입니다. (프론트를 Vercel로 따로 떼어 배포하는 방식은 21장 심화에서 별도로 다룹니다. 지금은 그 방식이 아닙니다.)

용어

프록시(Proxy) 요청을 대신 받아 뒤로 전달해 주는 중계를 말합니다. "Nginx가 /api 요청을 api 컨테이너로 프록시한다"처럼 씁니다.

이 손질과 이미지 갱신을 Claude Code에게 맡깁니다. VS Code에서 vote-app 폴더를 연 상태로, 터미널에서 claude를 실행한 뒤 아래 프롬프트를 그대로 입력하세요.

프롬프트
배포 전에 두 가지를 확인·손질하고, 이미지를 다시 빌드해 ECR에 올려 줘.

1) (7장에서 만든) frontend Nginx의 /api 프록시가 대상 주소를 환경변수
   API_URL로 받는지 확인해 줘. ECS에서는 이 값을 http://localhost:3000 으로
   줄 거야(같은 태스크 안의 api를 localhost로 부름). 프론트 코드는 계속
   상대 경로(/api)로 부르면 돼.
2) DB 접속에 SSL 옵션을 추가해 줘. 환경변수 DB_SSL=true 이면 SSL로
   접속하고, 값이 없으면 지금처럼(로컬 compose) SSL 없이 접속하게 해 줘.
   AWS RDS가 기본으로 SSL 접속을 요구하기 때문이야(rds.force_ssl=1).

그런 다음 vote-web, vote-api 두 이미지를 1.0 태그로 다시 빌드해서 ECR에
올려 줘. 이미지는 AWS Fargate에서 돌릴 X86_64(amd64)용으로 빌드해야 해.
특히 내 노트북이 Apple Silicon(arm64) 맥이면, docker buildx로
--platform linux/amd64 를 지정해 빌드해 줘. 안 그러면 arm64 이미지가
만들어져서 배포 때 'exec format error'가 나.
주의

이미지 아키텍처(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-api1.0 태그가 방금 갱신되었는지(푸시 시각) 확인하고 넘어갑니다.

14.4 ALB 만들기 — 타깃 그룹과 리스너

이제 콘솔로 갑니다. 순서는 타깃 그룹(대상 목록)을 먼저 만들고, ALB를 만들며 연결합니다.

주의

콘솔 오른쪽 위 리전이 서울(ap-northeast-2) 인지 다시 확인하세요. 리전이 다르면 11장에서 만든 vote-vpc가 목록에 안 보입니다.

[콘솔] 1단계: 타깃 그룹 만들기

  1. 상단 검색창에 EC2를 입력해 들어간 뒤, 왼쪽 사이드바를 아래로 내려 로드 밸런싱 → 대상 그룹(Target Groups) 을 클릭합니다.
  2. 대상 그룹 생성(Create target group) 을 누릅니다.
  3. 설정을 입력합니다.
    • 대상 유형(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): / (기본값 그대로 둡니다)
  4. 화면 아래 다음(Next) 을 누르면 "대상 등록" 화면이 나옵니다. 여기서는 아무것도 등록하지 않고 그대로 대상 그룹 생성(Create target group) 을 누릅니다.
    • [참고] 태스크 등록은 나중에 ECS 서비스가 자동으로 합니다. 지금 비어 있는 게 정상입니다.
확인

대상 그룹 목록에 vote-tg가 생기고, 대상(Targets)이 0개인 상태면 정상입니다.

[콘솔] 2단계: ALB 만들기

  1. 왼쪽 사이드바에서 로드 밸런싱 → 로드밸런서(Load Balancers) 를 클릭하고 로드 밸런서 생성(Create load balancer) 을 누릅니다.
  2. 유형 선택에서 Application Load Balancer생성(Create) 을 누릅니다.
  3. 설정을 입력합니다.
    • 로드 밸런서 이름(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 목록의 대상들에게 전달하라"는 리스너 규칙입니다.)
  4. 로드 밸런서 생성(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 클러스터 만들기

이제 태스크와 서비스를 담을 울타리(클러스터)를 만듭니다.

[콘솔] 클러스터 생성

  1. 상단 검색창에 ECS를 입력해 Elastic Container Service로 들어갑니다.
  2. 왼쪽 사이드바에서 클러스터(Clusters)클러스터 생성(Create cluster) 을 누릅니다.
  3. 설정을 입력합니다.
    • 클러스터 이름(Cluster name): vote-cluster
    • 인프라(Infrastructure): AWS Fargate(서버리스) 가 선택돼 있는지 확인합니다. (14.1절 맛보기에서 본 그 선택지입니다. 기본값이라 보통 이미 켜져 있습니다.)
      • [용어] 서버리스(Serverless): "서버 관리가 필요 없다"는 뜻의 업계 용어입니다. 서버가 없다는 게 아니라, 서버 관리를 AWS가 대신 한다는 의미로, 14.1에서 설명한 Fargate의 성격 그대로입니다.
  4. 생성(Create) 을 누릅니다. 금방 만들어집니다.
확인

클러스터 목록에 vote-cluster활성(Active) 상태로 보이면 성공입니다.

빈 식당(클러스터)이 하나 생겼습니다. 이제 레시피(태스크 정의)를 쓰고, 지배인(서비스)을 고용할 차례입니다.

14.6 태스크 정의 작성 — 실행 주문서

[콘솔] 태스크 정의 생성

  1. ECS 콘솔 왼쪽 사이드바에서 태스크 정의(Task definitions)새 태스크 정의 생성(Create new task definition) 을 누릅니다.
    • [주의] 비슷한 이름의 "JSON을 사용하여 새 태스크 정의 생성" 메뉴가 함께 보일 수 있습니다. 우리는 화면에서 값을 채우는 일반 새 태스크 정의 생성 쪽을 고릅니다.
  2. 기본 설정을 입력합니다.
    • 태스크 정의 패밀리(Task definition family): vote-task
      • [용어] 패밀리(Family): 이 주문서의 이름입니다. 주문서를 고칠 때마다 vote-task:1, vote-task:2처럼 번호(리비전)가 붙으며 이력이 쌓입니다. 이 번호가 3부 롤백에서 다시 등장합니다.
    • 시작 유형(Launch type): AWS Fargate 가 선택돼 있는지 확인합니다.
    • 운영체제/아키텍처: 기본값(Linux/X86_64)을 그대로 둡니다.
    • 태스크 크기(Task size) — CPU / 메모리: 가장 작은 조합을 고릅니다. 예: CPU .5 vCPU, 메모리 1GB.
  3. 컨테이너 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자리 계정 번호가 들어갑니다.
    • 컨테이너 포트(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로 떨어집니다.
  4. 화면 아래 컨테이너 추가(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 = 5432
      • DB_NAME = voteapp
      • DB_USER = postgres
      • DB_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를 연결합니다. 학습 단순화를 위한 구성임을 기억하세요.
  5. 나머지는 기본값으로 둡니다. 이때 로그 수집(로그 구성 / Log collection) 항목이 기본으로 켜져 있는지만 봐 두세요. 콘솔로 태스크 정의를 만들면 CloudWatch Logs로 로그를 보내는 설정(awslogs)이 기본으로 켜집니다. 14.9절에서 이 로그를 봅니다. 끝으로 생성(Create) 을 누릅니다.
확인

태스크 정의 목록에 vote-task리비전 1 로 생기면 성공입니다. 주문서 완성입니다.

14.7 ECS 서비스 만들기 — ALB에 연결

이제 지배인(서비스)을 고용해 "태스크를 항상 2개 유지하고, ALB와 연결하라" 고 지시합니다.

[콘솔] 서비스 생성

  1. 클러스터(Clusters) → vote-cluster 로 들어가 서비스(Services) 탭에서 생성(Create) 을 누릅니다.
  2. 배포 구성(Deployment configuration) 을 설정합니다.
    • 컴퓨팅 옵션 / 시작 유형: 시작 유형(Launch type) 을 고르고 Fargate 를 선택합니다.
    • 애플리케이션 유형(Application type): 서비스(Service)
    • 태스크 정의 패밀리(Family): vote-task, 리비전은 최신(1)
    • 서비스 이름(Service name): vote-service
    • 원하는 태스크 수(Desired tasks): 2
      • [참고] 2개로 두면 두 가용영역(AZ)에 하나씩 배치되어, 하나가 죽어도 서비스가 유지됩니다. 11장의 고가용성이 여기서 실현됩니다.
  3. 네트워킹(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)
  4. 로드 밸런싱(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 포트로 보내라"는 연결입니다.)
  5. 생성(Create) 을 누릅니다. 서비스가 태스크 2개를 띄우기 시작합니다. 몇 분 걸립니다.

[콘솔] 마지막 열쇠: RDS의 문을 ECS에게 열어 주기

12장에서 미뤄 둔 마지막 보안 그룹 규칙을 완성합니다. 지금 태스크가 떠도, RDS 문이 잠겨 있으면 api가 DB에 못 붙습니다.

  1. EC2 콘솔 → 왼쪽 사이드바 보안 그룹(Security Groups) → 목록에서 vote-db-sg 를 선택합니다.
  2. 인바운드 규칙(Inbound rules) 탭 → 인바운드 규칙 편집(Edit inbound rules)규칙 추가(Add rule) 를 누릅니다.
    • 유형(Type): PostgreSQL(포트 자동 5432)
    • 소스(Source): vote-ecs-sg 를 검색해 선택합니다.
  3. 규칙 저장(Save rules) 을 누릅니다.

이로써 11장에서 설계한 사슬이 완성됐습니다. 인터넷 → (80) → vote-alb-sg → (80) → vote-ecs-sg → (5432) → vote-db-sg

14.8 접속 확인 — 인터넷에 서비스가 열렸다

  1. ECS 콘솔에서 vote-clustervote-service → 태스크(Tasks) 탭을 봅니다. 태스크 2개의 마지막 상태(Last status)실행 중(Running) 이 되고, 상태(Health status)정상(Healthy) 이 될 때까지 몇 분 기다립니다. (처음에는 Pending → Running 순서로 바뀝니다.)
  2. 타깃 그룹도 확인합니다. EC2 콘솔 → 대상 그룹 → vote-tg → 대상(Targets) 에서, 등록된 대상 2개의 상태가 healthy 로 바뀌면 준비 완료입니다. (ALB 헬스체크가 통과했다는 뜻입니다. 잠깐 initial이었다가 healthy로 바뀝니다.)
  3. 14.4에서 복사해 둔 ALB DNS 이름 앞에 http:// 를 붙여 브라우저 주소창에 붙여넣고 접속합니다. 예: http://vote-alb-1234567890.ap-northeast-2.elb.amazonaws.com

투표 서비스 화면이 뜨면 — 성공입니다. 13장에서 심어 둔 "점심 뭐 먹지?" 설문이 보이고 투표가 된다면, 지금 이 순간:

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, 오류 메시지 등)이 자동으로 여기에 쌓입니다.

[콘솔] 로그 확인

  1. ECS 콘솔 → vote-clustervote-service → 태스크(Tasks) 탭에서 실행 중인 태스크 하나를 클릭합니다.
  2. 로그(Logs) 탭을 누르면 web / api 컨테이너의 로그가 보입니다.
    • api 컨테이너의 로그에서 서버 시작 메시지나 DB 연결 성공 메시지를 찾아보세요. (컨테이너를 위쪽에서 골라 전환할 수 있습니다.)

배포 문제의 원인은 대부분 이 로그에 적혀 있습니다. "태스크가 자꾸 죽어요", "화면이 안 떠요" 싶으면 로그부터 보는 것이 실무의 기본기입니다. 로그 내용을 복사해 Claude Code에게 붙여넣고 원인을 물어보는 것도 1부에 배운 그대로입니다.

[확인] 14장 & 2부 체크리스트

이번 장이 제대로 끝났는지 아래로 확인합니다. 하나라도 안 됐다면 해당 절로 돌아가 다시 진행하세요.

모두 체크됐다면 2부의 기본 완성에 도착한 겁니다.

막히면

이 장에서 자주 나는 오류와 대처법입니다.

14.10 2부를 마치며

2부에서 우리는 손(콘솔)으로 이만큼을 해냈습니다.

한 가지 짚을 점: 여기까지의 배포는 전부 손으로 했습니다. 이미지를 손으로 빌드해 푸시했고, 콘솔을 손으로 클릭해 구성했습니다. 코드가 바뀔 때마다 이걸 반복할 수는 없겠죠. 이 "손으로 한 일"을 코드와 자동화로 바꾸는 것이 3부의 주제입니다. (전체 여정에서 지금 어디쯤인지는 맨 앞 [지금 여기] 지도로 확인하세요.)

주의

실습 리소스, 지울까요 둘까요? 바로 이어서 실습한다면 그대로 두는 것이 편합니다. 다만 NAT 게이트웨이·RDS·ALB·Fargate는 켜 둔 시간만큼 요금이 나옵니다(하루 이틀은 소액이지만 0은 아닙니다). 실습을 며칠 쉬어야 한다면, 22장의 "리소스 삭제 가이드"를 먼저 참고해 지웠다가 다시 만들어도 됩니다. 다시 만드는 것 자체가 훌륭한 복습입니다.

수고했습니다. 여러분의 서비스는 지금 이 순간에도 AWS 위에서 돌아가고 있습니다.