15장. CI/CD — GitHub Actions로 자동 배포
3부를 시작하며. 2부까지 우리는 서비스를 직접 만들어 컨테이너에 담고, AWS에 손으로 하나씩 올려 배포까지 마쳤습니다. 브라우저에서 ALB 주소로 접속하면 투표 서비스가 실제로 뜹니다. 여기까지 온 것만으로도 큰 성취입니다.
그런데 실무의 배포는 여기서 끝나지 않습니다. 코드는 매일 바뀌고, 바뀔 때마다 다시 배포해야 합니다. 2부처럼 매번 손으로 빌드하고, 태그를 붙이고, 푸시하고, 콘솔을 클릭한다면 곧 지치고, 반드시 실수합니다. 3부는 이 반복을 기계에게 넘기고(15~17장), 실무가 실제로 어떻게 굴러가는지(18~20장)를 익히는 부입니다. 그 첫 장이 바로 CI/CD입니다.
[지금 여기]
우리는 22단계를 거쳐 "라이브 투표 서비스"를 만들고, 컨테이너에 담아, AWS에 배포하고 있습니다. 아래가 그 전체 여정 지도입니다. 지금은 15단계, 자동 배포 파이프라인을 만드는 자리에 있습니다.
- 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장마무리 (회고 + 리소스 삭제)
- 방금 끝낸 것: 14장에서 ECS Fargate와 ALB로 서비스를 AWS에 배포해, ALB의 HTTP 주소로 접속하면 투표 서비스가 뜨는 기본 완성 상태를 만들었습니다.
- 이번 장에서 하는 것: 그 배포 과정을 손이 아니라 GitHub Actions가
대신 하게 만듭니다.
main에 코드를 푸시하기만 하면 빌드 → ECR 푸시 → ECS 배포까지 자동으로 흐르는 파이프라인을 만듭니다.
이번 장에서 완성되는 것
한 문장으로: 코드를 main 브랜치에 푸시하면, 사람이 손대지 않아도
두 이미지(vote-api·vote-web)가 자동으로 빌드·푸시되고 ECS에 새로 배포됩니다.
이렇게 되면 성공입니다: GitHub 리포지토리의 Actions 탭에 방금 푸시로 시작된 배포 실행이 나타나고, 단계마다 초록 체크가 하나씩 늘어나 전부 초록이 되면, ALB 주소로 접속했을 때 서비스가 여전히 잘 뜹니다. 그동안 여러분은 콘솔을 한 번도 클릭하지 않습니다.
사전 조건
이 장은 14장까지 마친 상태 위에서 시작합니다. 아래가 갖춰져 있어야 합니다.
- 1~14장을 끝낸 상태. 특히 서비스가 AWS에 배포되어, 14장에서 확인한 ALB의 HTTP 주소로 접속하면 투표 서비스가 뜨는 상태여야 합니다. (지금 그 주소로 접속이 되는지 먼저 확인하고 시작하세요.)
- 코드가 GitHub 프라이빗 저장소에 올라가 있음. 1부에서
vote-app프로젝트를 GitHub의 비공개(Private) 저장소로 올려 뒀습니다. 이 장의 자동화는 그 저장소 위에서 동작합니다. - 8장에서 만든 AWS 액세스 키(액세스 키 ID + 시크릿 액세스 키)를 찾을 수 있음. GitHub에게 AWS 접근 권한을 주는 데 씁니다. 어디에 적어 뒀는지 미리 찾아 두세요. (분실했다면 8장을 참고해 새 키를 발급하면 됩니다.)
-
AWS에 있는 리소스 이름(2부에서 만든 것): ECR 리포지토리
vote-api·vote-web, ECS 클러스터vote-cluster, 서비스vote-service, 태스크 정의vote-task, 리전은 서울ap-northeast-2. 이 이름들을 그대로 씁니다. -
VS Code로
vote-app폴더가 열려 있고, 그 안에 터미널이 준비됨. 이 장의 터미널 명령은 모두 이vote-app폴더에서 실행합니다.
15.1 CI/CD가 뭔가 — 손배포의 문제부터
먼저 왜 이게 필요한지, 2부의 배포를 떠올리며 짚어 봅시다. 손으로 하는 배포(수동 배포, "손배포")에는 세 가지 문제가 있습니다.
- 느리다: 빌드하고, 태그 붙이고, 푸시하고, 콘솔에서 새 리비전을 만드는 동안 사람이 계속 붙어 있어야 합니다.
- 실수한다: 태그를 잘못 붙이거나, 한 단계를 빼먹거나, 지난번과 미묘하게 다르게 합니다. 사람은 똑같은 반복 작업에서 반드시 실수합니다.
- 기록이 없다: 누가 언제 무엇을 어떻게 배포했는지 남지 않습니다. 문제가 생겨도 "무엇이 바뀌었는지" 되짚기 어렵습니다.
해결책은 단순합니다. 배포 절차를 기계가 실행하는 각본으로 한 번 적어 두고, 코드가 바뀔 때마다 기계가 그 각본을 똑같이 실행하게 하는 것. 이것이 CI/CD의 핵심 발상입니다.
CI(Continuous Integration, 지속적 통합) 코드가 바뀔 때마다 자동으로 빌드하고 검사하는 것입니다. "코드를 올리면 기계가 곧바로 이미지를 만들어 보고, 문제가 없는지 확인한다"고 이해하면 됩니다. CI는 "만들기 자동화"입니다.
CD(Continuous Deployment/Delivery, 지속적 배포) CI를 통과한 결과물을 자동으로 서버까지 배포하는 것입니다. CD는 "내보내기 자동화"입니다.
파이프라인(Pipeline) CI와 CD를 이어 붙인 전체 흐름을 부르는 말입니다. 코드가 한쪽으로 들어가면 빌드 → 검사 → 배포가 관(pipe)을 따라 흐르듯 순서대로 진행된다고 해서 파이프라인이라고 부릅니다.
[활용 사례] CI/CD는 실제로 어디에 쓰이나
CI/CD는 우리 프로젝트만의 이야기가 아니라, 소프트웨어를 만드는 거의 모든 현장의 기본 장비입니다. 몇 가지만 봅시다.
- 커밋하면 자동으로 테스트·빌드: 개발자가 코드를 올리면 기계가 곧바로 테스트를 돌려 "이 변경이 뭔가를 깨뜨렸는지"를 몇 분 안에 알려 줍니다. 깨진 코드가 서비스에 나가기 전에 걸러집니다.
- 사람의 실수를 원천 차단: "이번엔 태그를 잘못 붙였네", "배포 스크립트 한 줄을 빼먹었네" 같은 일이 사라집니다. 기계는 매번 정확히 같은 순서로 같은 일을 합니다.
- 하루에도 여러 번 배포: 큰 회사들은 하루에 수십, 수백 번 배포합니다. 손으로는 불가능하지만, 파이프라인이 있으면 "코드를 올린다" 한 문장으로 끝나므로 가능합니다. 작게 자주 배포하니 문제가 생겨도 원인을 좁히기 쉽습니다.
- 팀 협업의 공용 절차: 여러 명이 각자 코드를 올려도, 배포는 모두가 똑같은 파이프라인을 통과합니다. "그 사람만 아는 배포 방법" 같은 게 없어지고, 누가 언제 무엇을 배포했는지 기록으로 남습니다.
우리가 만들 파이프라인은 이렇게 흐릅니다.
사람이 하는 일은 "코드를 푸시한다"까지입니다. 나머지는 기계가, 2부에서 우리가 손으로 한 것과 똑같은 일을, 매번 똑같은 순서로 해 줍니다.
15.2 GitHub Actions — 리포지토리에 사는 자동화 로봇
이 각본을 실행해 줄 기계로 우리는 GitHub Actions를 씁니다. 1부에서 코드를 이미 GitHub에 올려 뒀기 때문에, 별도의 서버를 빌리거나 설치할 것 없이 바로 쓸 수 있습니다.
GitHub Actions GitHub에 내장된 자동화 기능입니다. "리포지토리에 이런 일이 생기면 (예: 코드가 푸시되면), 이 작업들을 실행하라"는 각본을 등록해 두면, GitHub가 자기 서버에서 그 각본을 대신 실행해 줍니다.
워크플로(Workflow)
GitHub Actions에 등록하는 각본 하나를 워크플로라고 부릅니다. 리포지토리
안의 .github/workflows/ 폴더에 YAML 파일로 저장합니다. 파일 하나에
"언제(트리거) / 무엇을(단계들)"이 적혀 있습니다.
YAML(야믈)
설정을 사람이 읽기 쉽게 적는 파일 형식입니다. 1부에서 본
docker-compose.yml과 같은 형식으로, 들여쓰기(칸 맞추기)로 구조를
표현합니다. 들여쓰기가 틀어지면 오류가 나므로 칸을 잘 맞춰야 합니다.
트리거(Trigger)
워크플로를 "언제 실행할지"를 정하는 조건입니다. 우리 경우는 "main
브랜치에 코드가 푸시되면"이 트리거입니다.
브랜치(Branch)와 main
브랜치는 코드의 작업 줄기입니다. main은 기본 줄기로, 보통 "실제
서비스에 나가는 코드"가 이곳에 모입니다. 그래서 "main에 푸시되면
배포하라"는 규칙이 실무의 기본형입니다.
동작 방식은 이렇습니다. 우리가 main 브랜치에 코드를 푸시하면, GitHub가
그걸 감지하고 → 깨끗한 가상 컴퓨터를 하나 빌려서 → 워크플로에 적힌 단계들을
위에서부터 차례로 실행합니다. 실행 과정과 결과는 리포지토리의 Actions 탭
에서 실시간으로 볼 수 있습니다.
큰 개념이니, 우리 프로젝트에 적용하기 전에 아주 작게 한 번 만져 봅시다.
[맛보기] 인사하는 워크플로 하나 만들어 보기
우리 투표 서비스와는 상관없는, "화면에 인사말 한 줄을 출력하는" 워크플로를 직접 하나 만들어 GitHub Actions가 실제로 도는 모습을 눈으로 확인합니다. 전부 브라우저에서 합니다.
- 웹브라우저에서 내 리포지토리(
github.com/내아이디/vote-app)로 갑니다. - 상단 탭에서 Actions를 클릭합니다.
- 처음이라면 "Get started with GitHub Actions" 안내 화면이 나옵니다.
- 그 화면 맨 위의 set up a workflow yourself(직접 워크플로 설정하기) 링크를 클릭합니다. (안 보이면 아래 "Simple workflow" 카드의 Configure 버튼을 눌러도 됩니다.)
.github/workflows/main.yml을 편집하는 화면이 열리고, 예시 내용이 채워져 있습니다. 그 내용을 모두 지우고, 아래를 그대로 붙여넣습니다. 그리고 위쪽 파일 이름 칸의main.yml을hello.yml로 바꿉니다.
name: Hello Actions # 이 워크플로의 이름 (Actions 탭에 표시됨)
on: [push] # [트리거] 아무 브랜치든 push가 일어나면 실행
jobs:
say-hello:
runs-on: ubuntu-latest # GitHub가 빌려주는 깨끗한 리눅스 가상 컴퓨터
steps:
- run: echo "안녕하세요! GitHub Actions가 나 대신 이 명령을 실행했습니다."
- 오른쪽 위 Commit changes...(변경 사항 커밋) 버튼을 누르고, 이어 나오는 창에서 다시 Commit changes를 눌러 저장합니다. (별도 설정 없이 기본값 그대로 커밋하면 됩니다.)
- 다시 상단 Actions 탭으로 갑니다. 방금 커밋 때문에 Hello Actions
실행이 하나 생겨 있습니다. 그걸 클릭해 들어가면,
say-hello작업이 돌아가는 것이 보입니다. say-hello를 클릭하고 그 안의 단계를 펼치면, 우리가 적은 인사말이 출력된 로그가 보입니다. 초록 체크가 뜨면 성공입니다.
방금 여러분은 "코드 저장소에 무슨 일이 생기면(push), 기계가 시킨 명령을
대신 실행한다"는 GitHub Actions의 전부를 경험했습니다. 진짜 배포 워크플로도
원리는 똑같고, echo 대신 "빌드·푸시·배포" 명령이 들어갈 뿐입니다.
맛보기 워크플로는 지우고 갑니다
이 hello.yml은 on: [push]라서 앞으로 push할 때마다 계속 실행됩니다.
맛보기가 끝났으니 지워 둡시다. 리포지토리에서 Code 탭 →
.github/workflows/hello.yml 파일 열기 → 오른쪽 위 휴지통(삭제) 아이콘 →
Commit changes로 삭제하면 됩니다. (이 삭제 커밋은 아직 배포 워크플로가
없으니 아무 일도 일으키지 않습니다.)
15.3 AWS 자격 증명을 안전하게 — GitHub Secrets
이제 진짜 배포 워크플로를 준비합니다. 그 전에 한 가지 문제를 풀어야 합니다.
워크플로가 ECR에 이미지를 푸시하고 ECS에 배포하려면 AWS에 접근할 권한이
필요합니다. 8장에서 만든 액세스 키를 줘야 하는데, 이걸 워크플로 파일에
그대로 적으면 코드와 함께 GitHub에 올라가 노출됩니다. 절대 안 됩니다.
(1부에서 비밀값을 .env에 넣고 저장소에 올리지 않았던 원칙과 같은 이야기입니다.)
GitHub는 이를 위한 금고를 제공합니다.
GitHub Secrets(시크릿)
리포지토리에 딸린 비밀 값 보관함입니다. 여기에 넣은 값은 화면에 다시
표시되지 않고, 워크플로 실행 중에만 ${{ secrets.이름 }} 형태로 꺼내
쓸 수 있습니다. 실행 로그에도 자동으로 가려져(마스킹) 나옵니다.
Secrets는 우리 액세스 키 말고도 실무에서 다양하게 쓰입니다. 외부 서비스 API 키, 배포 대상 서버 접속 정보, 알림용 슬랙 토큰처럼 "코드에 적으면 안 되지만 자동화에는 필요한 비밀값"을 넣어 두는 표준 자리입니다.
[실습] Secrets에 액세스 키 등록하기
- 브라우저에서 내 리포지토리(
github.com/내아이디/vote-app)로 갑니다. - 상단 탭에서 Settings(설정)를 클릭합니다.
- 왼쪽 메뉴에서 Secrets and variables를 펼쳐 Actions를 클릭합니다.
- 오른쪽의 New repository secret 버튼을 누르고, 아래 두 개를 하나씩
등록합니다. (하나 등록하고 다시 New repository secret을 눌러 두 번째를 등록.)
- Name:
AWS_ACCESS_KEY_ID/ Secret: 8장에서 만든 액세스 키 ID를 붙여넣기 - Name:
AWS_SECRET_ACCESS_KEY/ Secret: 시크릿 액세스 키를 붙여넣기
- Name:
- 등록하면 목록에 두 이름이 보입니다. 값은 다시 볼 수 없지만, 이름이 정확히 위 두 개인지 확인하세요. (워크플로가 이 이름으로 값을 꺼냅니다. 철자가 하나만 틀려도 배포가 실패합니다.)
실무에서는 OIDC라는 더 안전한 방식도 씁니다 액세스 키는 "오래 사는 비밀번호"라서, 어딘가에 저장하는 순간 유출 위험을 안고 갑니다. 그래서 실무에서는 키를 저장하지 않고, GitHub와 AWS가 서로를 신뢰하도록 미리 연결해 두어 실행할 때마다 임시 자격 증명을 발급받는 방식(OIDC, OpenID Connect) 을 권장합니다. 설정이 더 복잡해서 이 입문 과정에서는 Secrets 방식으로 진행하지만, "실무에서는 OIDC로 키 자체를 없애는 게 더 안전하다"는 방향은 기억해 두세요.
15.4 워크플로 만들기 — 빌드에서 배포까지
이제 각본(워크플로)을 작성합니다. 우리 방식대로, Claude Code에게 시키고
만들어진 내용을 함께 읽으며 이해합니다. 아래 작업은 모두 VS Code에서
vote-app 폴더를 연 상태에서, 그 안의 터미널로 claude를 실행해 진행합니다.
준비: 태스크 정의 파일을 리포지토리에 두기
워크플로가 "새 이미지 주소로 태스크 정의(주문서)를 갱신"하려면, 그 주문서의
원본 파일이 리포지토리 안에 있어야 합니다. 2부에서 콘솔로 만든 태스크
정의(vote-task)를 파일로 내려받아 저장해 둡니다. 새 이미지가 나올 때마다
워크플로가 이 파일의 이미지 주소만 바꿔 새 리비전을 만들게 됩니다.
Claude Code가 aws ecs describe-task-definition으로 현재 주문서를 가져와,
등록에 못 쓰는 필드(예: taskDefinitionArn, revision, status,
requiresAttributes, compatibilities, registeredAt 등)를 걷어낸 깨끗한
.aws/task-definition.json을 만들어 줍니다. 만들어진 파일을 VS Code에서
열어, containerDefinitions 안에 이름이 web과 api인 컨테이너 두 개가
있는지 눈으로 확인하세요. 이 두 이름은 잠시 뒤 워크플로에서 그대로 씁니다.
이 파일에는 DB 접속 정보가 들어 있습니다 2부에서 태스크 정의의 환경변수로 DB 비밀번호를 넣었으므로, 이 파일에도 그 값이 담겨 있을 수 있습니다. 1부에서 저장소를 프라이빗(비공개)으로 만든 이유가 여기서도 유효합니다 — 반드시 비공개 저장소인지 확인하고 커밋하세요. 근본적인 해법은 이 값을 파일에 남기지 않고 AWS Secrets Manager를 태스크 정의에 연결하는 것입니다. (실무 전환 시 필수 과제로 기억해 두세요.)
워크플로 작성 요청
만들어진 워크플로(.github/workflows/deploy.yml)의 뼈대는 아래와 같습니다.
전체를 외울 필요는 없고, 구역별로 무슨 뜻인지만 읽어 봅시다.
name: Deploy to Amazon ECS # 이 워크플로의 이름 (Actions 탭에 표시)
on: # [언제] 실행할 것인가 = 트리거
push:
branches: [ main ] # main 브랜치에 푸시되면 실행
env: # 워크플로 전체에서 쓸 공통 값
AWS_REGION: ap-northeast-2
jobs:
deploy:
runs-on: ubuntu-latest # GitHub가 빌려주는 가상 컴퓨터(리눅스)
steps: # [무엇을] 위에서부터 순서대로 실행
- name: 코드 가져오기
uses: actions/checkout@v6 # 리포지토리 코드를 가상 컴퓨터로 복사
- name: AWS 자격 증명 설정
uses: aws-actions/configure-aws-credentials@v6
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: ECR 로그인
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: 이미지 빌드 & 푸시 (태그 = 커밋 해시)
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
TAG: ${{ github.sha }} # 이 커밋의 고유 해시
run: |
docker build -t $REGISTRY/vote-api:$TAG ./backend
docker push $REGISTRY/vote-api:$TAG
docker build -t $REGISTRY/vote-web:$TAG ./frontend
docker push $REGISTRY/vote-web:$TAG
- name: 태스크 정의에 새 api 이미지 끼워 넣기
id: render-api
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: .aws/task-definition.json
container-name: api
image: ${{ steps.login-ecr.outputs.registry }}/vote-api:${{ github.sha }}
- name: 태스크 정의에 새 web 이미지 끼워 넣기
id: render-web
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ steps.render-api.outputs.task-definition }}
container-name: web
image: ${{ steps.login-ecr.outputs.registry }}/vote-web:${{ github.sha }}
- name: ECS에 배포
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render-web.outputs.task-definition }}
cluster: vote-cluster
service: vote-service
wait-for-service-stability: true # 배포가 안정될 때까지 기다린다
구역별로 정리하면 이렇습니다.
- on: "언제" —
main에 푸시되면 실행됩니다. (15.2에서 말한 트리거입니다.) - env: 워크플로 전체에서 쓸 공통 값입니다. 리전을 여기 한 번만 적어 두고
아래에서
${{ env.AWS_REGION }}으로 꺼내 씁니다. - runs-on: GitHub가 빌려주는 깨끗한 가상 컴퓨터(리눅스)에서 실행됩니다.
- steps: "무엇을" — 2부에서 손으로 한 일이 그대로 늘어서 있습니다. ① 코드 가져오기 → ② AWS 로그인(Secrets 사용) → ③ ECR 로그인 → ④ 두 이미지(vote-api·vote-web) 빌드·푸시 → ⑤⑥ 주문서(태스크 정의)의 api·web 컨테이너에 새 이미지 주소 끼워 넣기 → ⑦ ECS 배포.
${{ github.sha }}: 지금 푸시된 커밋의 고유 해시입니다. 9장에서 배운 "구분되는 고유 태그"가 여기서 자동으로 실현됩니다. 배포마다 ECR에 고유 태그가 쌓이고, 이것이 나중에 롤백의 재료가 됩니다(20장).- uses: 남이 만들어 둔 작업 부품(액션)을 가져다 쓰는 것입니다.
@v6,@v2처럼 뒤에 붙은 건 그 부품의 버전입니다.aws-actions/...는 AWS가 공식 제공하는 부품들이라, 우리가 세부 명령을 직접 안 짜도 됩니다.
두 번 render 하는 이유 태스크 정의 안에 컨테이너가 web·api 두 개라서, 이미지 주소도 두 번 갈아 끼워야 합니다. 그래서 render 단계가 두 개입니다. 앞 단계(render-api)의 결과물을 뒤 단계(render-web)가 이어받아, 두 컨테이너 이미지가 모두 새 태그로 바뀐 완성된 주문서를 마지막 배포 단계에 넘깁니다.
15.5 첫 자동 배포 — 커밋만 하면 끝
이제 이 워크플로 파일과 태스크 정의 파일을 GitHub에 올리는 순간, 파이프라인이
살아납니다. VS Code의 vote-app 터미널에서 실행 중인 Claude Code에게 시킵니다.
푸시가 되면 곧바로 확인해 봅시다.
[실습] Actions 탭에서 실행 지켜보기
- 브라우저에서 리포지토리의 상단 Actions 탭을 엽니다.
- 방금 푸시로 시작된 Deploy to Amazon ECS 실행이 목록 맨 위에 보입니다.
클릭해 들어가면 왼쪽에
deploy작업이 있고, 그 안에서 단계(steps)들이 위에서부터 실행되는 것이 실시간으로 보입니다. 완료된 단계에는 초록 체크가 하나씩 붙습니다. - 마지막 "ECS에 배포" 단계는
wait-for-service-stability: true때문에 배포가 안정될 때까지 몇 분간 기다립니다. 전부 초록 체크가 되면 배포 완료입니다.
자동 배포가 실제로 됐는지 세 군데서 확인
- ECR(콘솔 → Elastic Container Registry): vote-api·vote-web 각각에
커밋 해시 태그의 이미지가 새로 생겼다.
- ECS(콘솔 → vote-cluster → vote-service): 태스크 정의 리비전
번호가 올라갔고, 서비스가 새 리비전으로 갈아탔다(배포 상태가 완료됨).
- 브라우저: 14장에서 쓰던 ALB 주소로 접속하면 서비스가 여전히 잘 나온다.
이 세 가지가 콘솔을 한 번도 클릭하지 않았는데 저절로 이루어졌다면, 자동 배포가 완성된 것입니다.
15.6 무엇이 달라졌나 — 손배포와 자동화
같은 배포지만, 손배포와 CI/CD는 이렇게 다릅니다.
| 손배포 (2부) | CI/CD (지금) | |
|---|---|---|
| 빌드·푸시 | 사람이 명령 입력 | 자동 |
| 태그 | 사람이 정함 (실수 여지) | 커밋 해시로 자동, 항상 고유 |
| 배포 | 콘솔 클릭 | 자동 |
| 걸리는 시간 | 사람이 붙어서 10분+ | 푸시 후 방치, 몇 분 뒤 완료 |
| 기록 | 없음 | Actions 탭에 전부 남음 |
이제부터 배포는 "코드를 main에 푸시한다" 한 문장으로 끝납니다. 이후의
실습(무중단 배포, 새 버전 배포와 롤백)도 전부 이 파이프라인 위에서 이루어집니다.
[확인]
이번 장이 제대로 끝났는지 눈으로 확인합니다.
- 리포지토리에
.github/workflows/deploy.yml과.aws/task-definition.json이 올라가 있다. - GitHub Secrets에
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY두 개가 있다. main에 푸시했더니 Actions 탭에 Deploy to Amazon ECS 실행이 생겼고, 모든 단계가 초록 체크로 끝났다.- ECR에 커밋 해시 태그의 새 이미지(vote-api·vote-web)가 생기고, ECS 서비스가 새 리비전으로 갈아탔으며, ALB 주소로 서비스가 여전히 잘 뜬다.
여기까지 보였다면, 콘솔을 클릭하지 않고 커밋만으로 배포되는 파이프라인이 완성된 것입니다.
막히면
이 장에서 자주 나는 오류와 대처법입니다. 실패(빨간 X)가 떠도 당황하지 마세요. 실패한 단계를 클릭하면 로그가 나옵니다. 그 로그를 복사해 Claude Code에 붙여넣고 원인을 물어보는 것이 가장 빠른 해결법입니다.
-
"AWS 자격 증명 설정" 또는 ECR/ECS 단계에서 권한 오류가 난다. GitHub Secrets 이름이 정확히
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY인지 확인하세요(대소문자·언더바까지). 값을 붙여넣을 때 앞뒤 공백이 섞이지 않았는지도 확인합니다. Settings → Secrets and variables → Actions에서 다시 등록할 수 있습니다. -
ECR 푸시 단계에서 "repository does not exist" 류 오류가 난다. ECR 리포지토리 이름이 워크플로의
vote-api·vote-web과 정확히 같아야 합니다. 콘솔(Elastic Container Registry)에서 이름을 확인하세요. 리전이ap-northeast-2가 맞는지도 함께 봅니다. -
배포 단계에서 "cluster/service not found" 오류가 난다. 워크플로의
cluster: vote-cluster,service: vote-service가 2부에서 만든 이름과 정확히 같은지, 리전이ap-northeast-2인지 확인하세요. -
render 단계에서 "container ... not found" 오류가 난다.
.aws/task-definition.json안의 컨테이너 이름이api와web이어야 워크플로의container-name: api/container-name: web과 맞습니다. 파일을 열어containerDefinitions의name값을 확인하세요. -
YAML 오류(예: "did not find expected key")가 난다. YAML은 들여쓰기(칸 맞추기)에 민감합니다. 탭 대신 공백을 쓰고, 칸이 어긋나지 않았는지 확인하세요. 확신이 안 서면 파일 내용을 Claude Code에 붙여넣고 "이 YAML의 들여쓰기 오류를 고쳐 줘"라고 요청하세요.
-
"ECS에 배포" 단계에서 오래 멈춰 있다가 실패한다.
wait-for-service-stability가 새 태스크가 안정되기를 기다리는데, 새 컨테이너가 계속 죽으면 시간 초과로 실패합니다. ECS 콘솔에서vote-service의 이벤트/태스크 로그를 열어 이유를 확인하세요(이미지 문제, DB 접속 실패 등). 로그를 Claude Code에 붙여넣어 원인을 함께 진단하면 빠릅니다.
[체크리스트]
아래가 모두 됐는지 확인해 보세요. 하나라도 안 됐다면 해당 절로 돌아갑니다.
- □ 수동 배포의 세 가지 문제(느림·실수·무기록)를 말할 수 있다
- □ CI / CD / 파이프라인 / 워크플로 / 트리거 / 브랜치(main)의 뜻을 안다
- □ 맛보기 워크플로를 만들어 Actions 탭에서 도는 것을 보고, 다시 지웠다
- □ 액세스 키를 GitHub Secrets에 넣었고, 왜 코드에 넣으면 안 되는지 안다
- □
.aws/task-definition.json을 만들었고, api·web 두 컨테이너를 확인했다 - □ 워크플로 YAML의 on / env / jobs / steps 구역이 각각 무슨 뜻인지 안다
- □ 커밋 해시(
github.sha)가 이미지 태그로 자동으로 붙는 이유를 설명할 수 있다 - □
main에 푸시해서 Actions 탭에서 자동 배포가 성공하는 것을 봤다 - □ ECR·ECS·브라우저 세 군데서 자동 배포 결과를 확인했다
배포는 자동화됐습니다.