바이브코딩 서비스를 Docker 컨테이너로 실전서비스 론칭하기 · 3부 — CI/CD, IaC, 무중단 배포, 롤백

15장. CI/CD — GitHub Actions로 자동 배포

3부를 시작하며. 2부까지 우리는 서비스를 직접 만들어 컨테이너에 담고, AWS에 손으로 하나씩 올려 배포까지 마쳤습니다. 브라우저에서 ALB 주소로 접속하면 투표 서비스가 실제로 뜹니다. 여기까지 온 것만으로도 큰 성취입니다.

그런데 실무의 배포는 여기서 끝나지 않습니다. 코드는 매일 바뀌고, 바뀔 때마다 다시 배포해야 합니다. 2부처럼 매번 손으로 빌드하고, 태그를 붙이고, 푸시하고, 콘솔을 클릭한다면 곧 지치고, 반드시 실수합니다. 3부는 이 반복을 기계에게 넘기고(15~17장), 실무가 실제로 어떻게 굴러가는지(18~20장)를 익히는 부입니다. 그 첫 장이 바로 CI/CD입니다.

[지금 여기]

우리는 22단계를 거쳐 "라이브 투표 서비스"를 만들고, 컨테이너에 담아, AWS에 배포하고 있습니다. 아래가 그 전체 여정 지도입니다. 지금은 15단계, 자동 배포 파이프라인을 만드는 자리에 있습니다.

  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장마무리 (회고 + 리소스 삭제)

이번 장에서 완성되는 것

한 문장으로: 코드를 main 브랜치에 푸시하면, 사람이 손대지 않아도 두 이미지(vote-api·vote-web)가 자동으로 빌드·푸시되고 ECS에 새로 배포됩니다.

이렇게 되면 성공입니다: GitHub 리포지토리의 Actions 탭에 방금 푸시로 시작된 배포 실행이 나타나고, 단계마다 초록 체크가 하나씩 늘어나 전부 초록이 되면, ALB 주소로 접속했을 때 서비스가 여전히 잘 뜹니다. 그동안 여러분은 콘솔을 한 번도 클릭하지 않습니다.

사전 조건

이 장은 14장까지 마친 상태 위에서 시작합니다. 아래가 갖춰져 있어야 합니다.

15.1 CI/CD가 뭔가 — 손배포의 문제부터

먼저 왜 이게 필요한지, 2부의 배포를 떠올리며 짚어 봅시다. 손으로 하는 배포(수동 배포, "손배포")에는 세 가지 문제가 있습니다.

  1. 느리다: 빌드하고, 태그 붙이고, 푸시하고, 콘솔에서 새 리비전을 만드는 동안 사람이 계속 붙어 있어야 합니다.
  2. 실수한다: 태그를 잘못 붙이거나, 한 단계를 빼먹거나, 지난번과 미묘하게 다르게 합니다. 사람은 똑같은 반복 작업에서 반드시 실수합니다.
  3. 기록이 없다: 누가 언제 무엇을 어떻게 배포했는지 남지 않습니다. 문제가 생겨도 "무엇이 바뀌었는지" 되짚기 어렵습니다.

해결책은 단순합니다. 배포 절차를 기계가 실행하는 각본으로 한 번 적어 두고, 코드가 바뀔 때마다 기계가 그 각본을 똑같이 실행하게 하는 것. 이것이 CI/CD의 핵심 발상입니다.

용어

CI(Continuous Integration, 지속적 통합) 코드가 바뀔 때마다 자동으로 빌드하고 검사하는 것입니다. "코드를 올리면 기계가 곧바로 이미지를 만들어 보고, 문제가 없는지 확인한다"고 이해하면 됩니다. CI는 "만들기 자동화"입니다.

용어

CD(Continuous Deployment/Delivery, 지속적 배포) CI를 통과한 결과물을 자동으로 서버까지 배포하는 것입니다. CD는 "내보내기 자동화"입니다.

용어

파이프라인(Pipeline) CI와 CD를 이어 붙인 전체 흐름을 부르는 말입니다. 코드가 한쪽으로 들어가면 빌드 → 검사 → 배포가 관(pipe)을 따라 흐르듯 순서대로 진행된다고 해서 파이프라인이라고 부릅니다.

[활용 사례] CI/CD는 실제로 어디에 쓰이나

CI/CD는 우리 프로젝트만의 이야기가 아니라, 소프트웨어를 만드는 거의 모든 현장의 기본 장비입니다. 몇 가지만 봅시다.

우리가 만들 파이프라인은 이렇게 흐릅니다.

코드 수정 → GitHub에 푸시여기부터 전부 자동자동 (GitHub Actions)이미지 빌드ECR 푸시커밋 해시 태그ECS 새 버전 배포

사람이 하는 일은 "코드를 푸시한다"까지입니다. 나머지는 기계가, 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가 실제로 도는 모습을 눈으로 확인합니다. 전부 브라우저에서 합니다.

  1. 웹브라우저에서 내 리포지토리(github.com/내아이디/vote-app)로 갑니다.
  2. 상단 탭에서 Actions를 클릭합니다.
    • 처음이라면 "Get started with GitHub Actions" 안내 화면이 나옵니다.
    • 그 화면 맨 위의 set up a workflow yourself(직접 워크플로 설정하기) 링크를 클릭합니다. (안 보이면 아래 "Simple workflow" 카드의 Configure 버튼을 눌러도 됩니다.)
  3. .github/workflows/main.yml을 편집하는 화면이 열리고, 예시 내용이 채워져 있습니다. 그 내용을 모두 지우고, 아래를 그대로 붙여넣습니다. 그리고 위쪽 파일 이름 칸의 main.ymlhello.yml로 바꿉니다.
name: Hello Actions        # 이 워크플로의 이름 (Actions 탭에 표시됨)
on: [push]                 # [트리거] 아무 브랜치든 push가 일어나면 실행
jobs:
  say-hello:
    runs-on: ubuntu-latest # GitHub가 빌려주는 깨끗한 리눅스 가상 컴퓨터
    steps:
      - run: echo "안녕하세요! GitHub Actions가 나 대신 이 명령을 실행했습니다."
  1. 오른쪽 위 Commit changes...(변경 사항 커밋) 버튼을 누르고, 이어 나오는 창에서 다시 Commit changes를 눌러 저장합니다. (별도 설정 없이 기본값 그대로 커밋하면 됩니다.)
  2. 다시 상단 Actions 탭으로 갑니다. 방금 커밋 때문에 Hello Actions 실행이 하나 생겨 있습니다. 그걸 클릭해 들어가면, say-hello 작업이 돌아가는 것이 보입니다.
  3. say-hello를 클릭하고 그 안의 단계를 펼치면, 우리가 적은 인사말이 출력된 로그가 보입니다. 초록 체크가 뜨면 성공입니다.

방금 여러분은 "코드 저장소에 무슨 일이 생기면(push), 기계가 시킨 명령을 대신 실행한다"는 GitHub Actions의 전부를 경험했습니다. 진짜 배포 워크플로도 원리는 똑같고, echo 대신 "빌드·푸시·배포" 명령이 들어갈 뿐입니다.

주의

맛보기 워크플로는 지우고 갑니다hello.ymlon: [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에 액세스 키 등록하기

  1. 브라우저에서 내 리포지토리(github.com/내아이디/vote-app)로 갑니다.
  2. 상단 탭에서 Settings(설정)를 클릭합니다.
  3. 왼쪽 메뉴에서 Secrets and variables를 펼쳐 Actions를 클릭합니다.
  4. 오른쪽의 New repository secret 버튼을 누르고, 아래 두 개를 하나씩 등록합니다. (하나 등록하고 다시 New repository secret을 눌러 두 번째를 등록.)
    • Name: AWS_ACCESS_KEY_ID / Secret: 8장에서 만든 액세스 키 ID를 붙여넣기
    • Name: AWS_SECRET_ACCESS_KEY / Secret: 시크릿 액세스 키를 붙여넣기
  5. 등록하면 목록에 두 이름이 보입니다. 값은 다시 볼 수 없지만, 이름이 정확히 위 두 개인지 확인하세요. (워크플로가 이 이름으로 값을 꺼냅니다. 철자가 하나만 틀려도 배포가 실패합니다.)
참고

실무에서는 OIDC라는 더 안전한 방식도 씁니다 액세스 키는 "오래 사는 비밀번호"라서, 어딘가에 저장하는 순간 유출 위험을 안고 갑니다. 그래서 실무에서는 키를 저장하지 않고, GitHub와 AWS가 서로를 신뢰하도록 미리 연결해 두어 실행할 때마다 임시 자격 증명을 발급받는 방식(OIDC, OpenID Connect) 을 권장합니다. 설정이 더 복잡해서 이 입문 과정에서는 Secrets 방식으로 진행하지만, "실무에서는 OIDC로 키 자체를 없애는 게 더 안전하다"는 방향은 기억해 두세요.

15.4 워크플로 만들기 — 빌드에서 배포까지

이제 각본(워크플로)을 작성합니다. 우리 방식대로, Claude Code에게 시키고 만들어진 내용을 함께 읽으며 이해합니다. 아래 작업은 모두 VS Code에서 vote-app 폴더를 연 상태에서, 그 안의 터미널로 claude를 실행해 진행합니다.

준비: 태스크 정의 파일을 리포지토리에 두기

워크플로가 "새 이미지 주소로 태스크 정의(주문서)를 갱신"하려면, 그 주문서의 원본 파일이 리포지토리 안에 있어야 합니다. 2부에서 콘솔로 만든 태스크 정의(vote-task)를 파일로 내려받아 저장해 둡니다. 새 이미지가 나올 때마다 워크플로가 이 파일의 이미지 주소만 바꿔 새 리비전을 만들게 됩니다.

프롬프트
AWS CLI로 현재 ECS 태스크 정의(vote-task)를 조회해서,
배포에 다시 쓸 수 있는 형태로 정리해(등록 시 못 쓰는 읽기 전용 필드는
빼고) vote-app 폴더 안의 .aws/task-definition.json 파일로 저장해 줘.
이 태스크 정의에는 web, api 두 컨테이너가 들어 있어.

Claude Code가 aws ecs describe-task-definition으로 현재 주문서를 가져와, 등록에 못 쓰는 필드(예: taskDefinitionArn, revision, status, requiresAttributes, compatibilities, registeredAt 등)를 걷어낸 깨끗한 .aws/task-definition.json을 만들어 줍니다. 만들어진 파일을 VS Code에서 열어, containerDefinitions 안에 이름이 webapi인 컨테이너 두 개가 있는지 눈으로 확인하세요. 이 두 이름은 잠시 뒤 워크플로에서 그대로 씁니다.

주의

이 파일에는 DB 접속 정보가 들어 있습니다 2부에서 태스크 정의의 환경변수로 DB 비밀번호를 넣었으므로, 이 파일에도 그 값이 담겨 있을 수 있습니다. 1부에서 저장소를 프라이빗(비공개)으로 만든 이유가 여기서도 유효합니다 — 반드시 비공개 저장소인지 확인하고 커밋하세요. 근본적인 해법은 이 값을 파일에 남기지 않고 AWS Secrets Manager를 태스크 정의에 연결하는 것입니다. (실무 전환 시 필수 과제로 기억해 두세요.)

워크플로 작성 요청

프롬프트
GitHub Actions 워크플로를 vote-app의 .github/workflows/deploy.yml로
만들어 줘. 요구사항은 다음과 같아.
- main 브랜치에 푸시되면 실행된다
- AWS 자격 증명은 GitHub Secrets(AWS_ACCESS_KEY_ID,
  AWS_SECRET_ACCESS_KEY)를 쓴다. 리전은 ap-northeast-2
- backend와 frontend 이미지를 빌드해서 ECR(vote-api, vote-web)에
  푸시한다. 태그는 커밋 해시를 쓴다
- .aws/task-definition.json의 api, web 컨테이너 이미지를 새 태그로
  바꿔서 ECS(vote-cluster의 vote-service)에 배포한다
- 각 단계에 이해를 돕는 주석을 달아 줘

만들어진 워크플로(.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   # 배포가 안정될 때까지 기다린다

구역별로 정리하면 이렇습니다.

참고

두 번 render 하는 이유 태스크 정의 안에 컨테이너가 web·api 두 개라서, 이미지 주소도 두 번 갈아 끼워야 합니다. 그래서 render 단계가 두 개입니다. 앞 단계(render-api)의 결과물을 뒤 단계(render-web)가 이어받아, 두 컨테이너 이미지가 모두 새 태그로 바뀐 완성된 주문서를 마지막 배포 단계에 넘깁니다.

15.5 첫 자동 배포 — 커밋만 하면 끝

이제 이 워크플로 파일과 태스크 정의 파일을 GitHub에 올리는 순간, 파이프라인이 살아납니다. VS Code의 vote-app 터미널에서 실행 중인 Claude Code에게 시킵니다.

프롬프트
.github/workflows/deploy.yml 과 .aws/task-definition.json 을
커밋하고 main 브랜치에 푸시해 줘.

푸시가 되면 곧바로 확인해 봅시다.

[실습] Actions 탭에서 실행 지켜보기

  1. 브라우저에서 리포지토리의 상단 Actions 탭을 엽니다.
  2. 방금 푸시로 시작된 Deploy to Amazon ECS 실행이 목록 맨 위에 보입니다. 클릭해 들어가면 왼쪽에 deploy 작업이 있고, 그 안에서 단계(steps)들이 위에서부터 실행되는 것이 실시간으로 보입니다. 완료된 단계에는 초록 체크가 하나씩 붙습니다.
  3. 마지막 "ECS에 배포" 단계는 wait-for-service-stability: true 때문에 배포가 안정될 때까지 몇 분간 기다립니다. 전부 초록 체크가 되면 배포 완료입니다.
확인

자동 배포가 실제로 됐는지 세 군데서 확인 - ECR(콘솔 → Elastic Container Registry): vote-api·vote-web 각각에 커밋 해시 태그의 이미지가 새로 생겼다. - ECS(콘솔 → vote-clustervote-service): 태스크 정의 리비전 번호가 올라갔고, 서비스가 새 리비전으로 갈아탔다(배포 상태가 완료됨). - 브라우저: 14장에서 쓰던 ALB 주소로 접속하면 서비스가 여전히 잘 나온다.

이 세 가지가 콘솔을 한 번도 클릭하지 않았는데 저절로 이루어졌다면, 자동 배포가 완성된 것입니다.

15.6 무엇이 달라졌나 — 손배포와 자동화

같은 배포지만, 손배포와 CI/CD는 이렇게 다릅니다.

손배포 (2부) CI/CD (지금)
빌드·푸시 사람이 명령 입력 자동
태그 사람이 정함 (실수 여지) 커밋 해시로 자동, 항상 고유
배포 콘솔 클릭 자동
걸리는 시간 사람이 붙어서 10분+ 푸시 후 방치, 몇 분 뒤 완료
기록 없음 Actions 탭에 전부 남음

이제부터 배포는 "코드를 main에 푸시한다" 한 문장으로 끝납니다. 이후의 실습(무중단 배포, 새 버전 배포와 롤백)도 전부 이 파이프라인 위에서 이루어집니다.

[확인]

이번 장이 제대로 끝났는지 눈으로 확인합니다.

여기까지 보였다면, 콘솔을 클릭하지 않고 커밋만으로 배포되는 파이프라인이 완성된 것입니다.

막히면

이 장에서 자주 나는 오류와 대처법입니다. 실패(빨간 X)가 떠도 당황하지 마세요. 실패한 단계를 클릭하면 로그가 나옵니다. 그 로그를 복사해 Claude Code에 붙여넣고 원인을 물어보는 것이 가장 빠른 해결법입니다.

[체크리스트]

아래가 모두 됐는지 확인해 보세요. 하나라도 안 됐다면 해당 절로 돌아갑니다.

배포는 자동화됐습니다.