CI/CD 파이프라인 구축
이전 절들에서는 타입스크립트 기반의 API 서버와 React 프론트엔드 애플리케이션을 성공적으로 개발했습니다. 이제 개발된 애플리케이션을 사용자에게 제공하고, 향후 업데이트를 효율적으로 배포하기 위한 핵심 단계인 CI/CD 파이프라인 구축에 대해 알아보겠습니다.
CI/CD는 지속적 통합(Continuous Integration)과 지속적 배포(Continuous Deployment) 또는 지속적 전달(Continuous Delivery)을 의미합니다. 이는 개발부터 배포까지의 모든 과정을 자동화하여, 소프트웨어 개발 주기를 단축하고 안정성을 높이며, 더 빠른 피드백 루프를 가능하게 합니다.
이 절에서는 타입스크립트 기반의 풀 스택 Monorepo 프로젝트를 위한 CI/CD 파이프라인을 GitHub Actions를 사용하여 구축하는 방법을 설명합니다. GitHub Actions는 GitHub 저장소에서 직접 워크플로우를 자동화할 수 있는 강력한 CI/CD 도구입니다.
CI/CD의 개념 및 중요성
지속적 통합
- 개념: 개발자들이 각자 작업한 코드를 메인 코드베이스에 자주(하루에도 여러 번) 통합하는 것을 의미합니다. 통합 시에는 자동화된 빌드 및 테스트 과정을 거쳐 새로운 변경 사항이 기존 코드에 문제를 일으키지 않는지 검증합니다.
- 목표: 통합으로 인한 버그를 조기에 발견하고 해결하여, 통합에 드는 시간과 노력을 최소화합니다.
- 주요 활동
- 코드 변경 감지: 저장소에 새로운 코드가 푸시될 때마다 워크플로우가 자동으로 트리거됩니다.
- 의존성 설치: 프로젝트에 필요한 라이브러리 및 패키지를 설치합니다.
- 코드 빌드: 타입스크립트 코드를 자바스크립트로 컴파일하고, 프론트엔드 애플리케이션을 빌드합니다.
- 테스트 실행: 단위 테스트, 통합 테스트, E2E 테스트 등을 자동으로 실행하여 코드의 정확성을 검증합니다.
- 정적 분석: 린트(Lint) 검사 등을 통해 코드 스타일과 잠재적 문제를 확인합니다.
지속적 배포/전달
- 개념: CI 단계를 통과한 코드를 자동으로 테스트 환경, 스테이징 환경, 또는 프로덕션 환경까지 배포하는 것을 의미합니다.
- 지속적 전달(Continuous Delivery): CI를 통과한 코드를 '배포 가능한 상태'로 유지하고, 수동 승인 후 언제든지 배포할 수 있도록 준비합니다.
- 지속적 배포(Continuous Deployment): CI를 통과한 코드가 자동으로 프로덕션 환경까지 배포됩니다.
- 목표: 소프트웨어를 빠르고 안전하게 사용자에게 제공하여, 새로운 기능과 버그 수정을 신속하게 반영합니다.
- 주요 활동
- 도커 이미지 빌드 및 푸시: 애플리케이션을 도커 이미지로 만들고 컨테이너 레지스트리(Docker Hub, GitHub Container Registry 등)에 업로드합니다.
- 배포: 빌드된 이미지를 클라우드 환경(AWS EC2/ECS/EKS, Azure, GCP 등) 또는 자체 서버에 배포합니다.
- 롤백: 문제가 발생할 경우 이전 버전으로 되돌릴 수 있는 전략을 포함합니다.
GitHub Actions 기본 개념
GitHub Actions는 GitHub 저장소의 /.github/workflows
디렉토리에 YAML 파일을 생성하여 워크플로우를 정의합니다.
- Workflow (워크플로우): 하나 이상의 Job으로 구성되며, 특정 이벤트(예:
push
,pull_request
)에 의해 트리거됩니다. - Job (작업): 여러 Step으로 구성되며, 독립적인 가상 환경(Runner)에서 실행됩니다. Job 간에는 의존성을 설정할 수 있습니다.
- Step (단계): Job 내에서 순차적으로 실행되는 개별 명령 또는 액션입니다.
- Action (액션): 재사용 가능한 코드 블록으로, 복잡한 작업을 단순화합니다 (예:
actions/checkout@v4
,actions/setup-node@v4
).
Monorepo를 위한 CI/CD 파이프라인 구축
우리가 설계한 Monorepo 프로젝트를 위한 CI/CD 파이프라인을 main
브랜치에 푸시될 때마다 실행되도록 구성해봅시다. 이 예시에서는 백엔드(NestJS)와 프론트엔드(React)를 각각 도커 이미지로 빌드하고 배포하는 과정을 포함합니다.
전제 조건
- GitHub 저장소에 프로젝트가 푸시되어 있어야 합니다.
- 각 애플리케이션(
client
,server
)에 대한Dockerfile
이 준비되어 있어야 합니다. - AWS EC2 또는 Docker 컨테이너를 실행할 수 있는 원격 서버가 있다고 가정합니다. SSH 키와 접속 정보가 필요합니다.
- GitHub Secrets에 배포 관련 민감 정보(AWS 자격 증명, SSH 키 등)를 저장합니다.
Dockerfile
준비
각 패키지 폴더(packages/client
, packages/server
)에 Dockerfile
을 생성합니다.
packages/server/Dockerfile
(NestJS)
# packages/server/Dockerfile
# 1단계: 빌드 환경
FROM node:20-alpine AS builder
# 작업 디렉토리 설정
WORKDIR /app
# 루트의 package.json, lock 파일 복사 (monorepo root)
COPY package.json pnpm-lock.yaml ./
# server 및 shared 패키지의 package.json, lock 파일 복사
COPY packages/server/package.json packages/server/pnpm-lock.yaml ./packages/server/
COPY packages/shared/package.json packages/shared/pnpm-lock.yaml ./packages/shared/
# pnpm install (monorepo root에서 실행)
# pnpm install -r --frozen-lockfile 또는 npm ci --prefix packages/server
RUN npm install -g pnpm && pnpm install --frozen-lockfile --prod=false
# 모든 소스 코드 복사
COPY . .
# NestJS 서버 빌드
WORKDIR /app/packages/server
RUN pnpm build
# 2단계: 프로덕션 환경
FROM node:20-alpine AS runner
WORKDIR /app
# pnpm install --prod (프로덕션 의존성만 설치)
COPY --from=builder /app/package.json /app/pnpm-lock.yaml ./
COPY --from=builder /app/packages/server/package.json /app/packages/server/pnpm-lock.yaml ./packages/server/
COPY --from=builder /app/packages/shared/package.json /app/packages/shared/pnpm-lock.yaml ./packages/shared/
RUN npm install -g pnpm && pnpm install --frozen-lockfile --prod
# 빌드된 NestJS 애플리케이션 복사
COPY --from=builder /app/packages/server/dist ./packages/server/dist
COPY --from=builder /app/packages/server/node_modules ./packages/server/node_modules # prod dependencies (pnpm)
COPY --from=builder /app/packages/shared/dist ./packages/shared/dist
COPY --from=builder /app/packages/shared/node_modules ./packages/shared/node_modules
# 환경 변수 (예: DATABASE_URL 등)
ENV NODE_ENV=production
ENV PORT=4000
EXPOSE 4000
WORKDIR /app/packages/server
CMD ["node", "dist/main.js"]
packages/client/Dockerfile
(React)
# packages/client/Dockerfile
# 1단계: 빌드 환경 (React 앱 빌드)
FROM node:20-alpine AS builder
WORKDIR /app
# 루트의 package.json, lock 파일 복사 (monorepo root)
COPY package.json pnpm-lock.yaml ./
# client 및 shared 패키지의 package.json, lock 파일 복사
COPY packages/client/package.json packages/client/pnpm-lock.yaml ./packages/client/
COPY packages/shared/package.json packages/shared/pnpm-lock.yaml ./packages/shared/
# pnpm install (monorepo root에서 실행)
RUN npm install -g pnpm && pnpm install --frozen-lockfile --prod=false
# 모든 소스 코드 복사
COPY . .
# React 앱 빌드
WORKDIR /app/packages/client
RUN pnpm build
# 2단계: 프로덕션 서빙 환경 (Nginx 사용)
FROM nginx:alpine
# Nginx 기본 설정 파일 삭제
RUN rm /etc/nginx/conf.d/default.conf
# 사용자 정의 Nginx 설정 파일 복사
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 빌드된 React 앱 정적 파일 복사
COPY --from=builder /app/packages/client/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
packages/client/nginx.conf
: (Nginx 설정 파일)
# packages/client/nginx.conf
server {
listen 80;
server_tokens off; # Nginx 버전 숨기기
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html; # SPA 라우팅을 위한 설정
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
GitHub Secrets 설정
GitHub 저장소의 Settings
-> Secrets and variables
-> Actions
에서 다음 Secret들을 추가합니다.
DOCKERHUB_USERNAME
: Docker Hub 사용자 이름DOCKERHUB_TOKEN
: Docker Hub Access Token (Settings -> Security -> New Access Token)AWS_ACCESS_KEY_ID
: AWS IAM 사용자 액세스 키AWS_SECRET_ACCESS_KEY
: AWS IAM 사용자 시크릿 액세스 키EC2_SSH_PRIVATE_KEY
: EC2 인스턴스에 접근할 수 있는 SSH 프라이빗 키 (base64 인코딩 권장)EC2_USER
: EC2 인스턴스 사용자 이름 (예:ubuntu
,ec2-user
)EC2_HOST
: EC2 인스턴스 퍼블릭 IP 또는 DNS
GitHub Actions 워크플로우 정의
프로젝트 루트에 /.github/workflows/ci-cd.yml
파일을 생성합니다.
# .github/workflows/ci-cd.yml
name: Fullstack CI/CD Pipeline
on:
push:
branches:
- main # main 브랜치에 푸시될 때마다 실행
env:
NODE_VERSION: '20' # 사용할 Node.js 버전
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
# 서버 이미지 이름/태그
SERVER_IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/my-nest-app
SERVER_IMAGE_TAG: latest
# 클라이언트 이미지 이름/태그
CLIENT_IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/my-react-app
CLIENT_IMAGE_TAG: latest
jobs:
# CI Job: 빌드 및 테스트 (서버 및 클라이언트)
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm' # pnpm 캐싱 설정
- name: Install pnpm
run: npm install -g pnpm
- name: Install Dependencies (Monorepo Root)
run: pnpm install --frozen-lockfile --prod=false # 모든 의존성 설치
- name: Build Server
run: pnpm --filter server build # server 패키지 빌드
- name: Run Server Tests
run: pnpm --filter server test # server 패키지 테스트 (단위/통합)
- name: Build Client
run: pnpm --filter client build # client 패키지 빌드
- name: Run Client Tests
run: pnpm --filter client test # client 패키지 테스트 (단위)
# E2E 테스트는 별도 Job 또는 Cypress Cloud 등 이용
# CD Job: 도커 이미지 빌드 및 푸시, 서버 배포
deploy:
needs: build-and-test # build-and-test Job이 성공해야 실행
runs-on: ubuntu-latest
environment: production # GitHub Environment 설정 (선택 사항, 배포 보호 등)
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
# --- Server (NestJS) Docker Image Build and Push ---
- name: Build and Push Server Docker Image
uses: docker/build-push-action@v5
with:
context: . # Monorepo 루트에서 Dockerfile을 찾음
file: ./packages/server/Dockerfile
push: true
tags: ${{ env.SERVER_IMAGE_NAME }}:${{ env.SERVER_IMAGE_TAG }}
cache-from: type=gha # GitHub Actions 캐시 사용
cache-to: type=gha,mode=max
# --- Client (React) Docker Image Build and Push ---
- name: Build and Push Client Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: ./packages/client/Dockerfile
push: true
tags: ${{ env.CLIENT_IMAGE_NAME }}:${{ env.CLIENT_IMAGE_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
# --- Deploy to EC2 via SSH ---
- name: Deploy to EC2
uses: appleboy/ssh-action@v1.0.3 # SSH를 통해 원격 서버에 명령 실행
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
script: |
# Docker 컨테이너 중지 및 삭제
docker stop my-nest-app || true
docker rm my-nest-app || true
docker stop my-react-app || true
docker rm my-react-app || true
# 최신 이미지 풀
docker pull ${{ env.SERVER_IMAGE_NAME }}:${{ env.SERVER_IMAGE_TAG }}
docker pull ${{ env.CLIENT_IMAGE_NAME }}:${{ env.CLIENT_IMAGE_TAG }}
# 서버 컨테이너 실행
# Docker Hub로부터 이미지를 가져와 실행 시 .env 파일 직접 설정
docker run -d --restart always \
-p 4000:4000 \
--name my-nest-app \
-e DATABASE_HOST=${{ secrets.PROD_DATABASE_HOST }} \
-e DATABASE_PORT=${{ secrets.PROD_DATABASE_PORT }} \
-e DATABASE_USER=${{ secrets.PROD_DATABASE_USER }} \
-e DATABASE_PASSWORD=${{ secrets.PROD_DATABASE_PASSWORD }} \
-e DATABASE_NAME=${{ secrets.PROD_DATABASE_NAME }} \
-e JWT_SECRET=${{ secrets.PROD_JWT_SECRET }} \
${{ env.SERVER_IMAGE_NAME }}:${{ env.SERVER_IMAGE_TAG }}
# 클라이언트 컨테이너 실행
docker run -d --restart always \
-p 80:80 \
--name my-react-app \
${{ env.CLIENT_IMAGE_NAME }}:${{ env.CLIENT_IMAGE_TAG }}
echo "Deployment finished successfully!"
워크플로우 설명
name
: 워크플로우 이름.
on
: main
브랜치에 push
이벤트가 발생할 때 워크플로우를 트리거합니다.
env
: 워크플로우 전체에서 사용될 환경 변수를 정의합니다.
jobs
: 두 개의 주요 작업(build-and-test
, deploy
)을 정의합니다.
build-and-test
uses: actions/checkout@v4
: 저장소 코드를 워크플로우 러너로 체크아웃합니다.uses: actions/setup-node@v4
: 지정된 Node.js 버전을 설정합니다.cache: 'pnpm'
으로 의존성 캐싱을 활성화하여 빌드 시간을 단축합니다.pnpm install --frozen-lockfile --prod=false
: Monorepo 루트에서 모든 개발 및 프로덕션 의존성을 설치합니다.--frozen-lockfile
은pnpm-lock.yaml
파일과 정확히 일치하는 의존성을 설치하도록 강제하여 빌드의 일관성을 보장합니다.pnpm --filter server build
/pnpm --filter client build
: Monorepo 도구(pnpm)의filter
기능을 사용하여 특정 패키지만 빌드합니다.pnpm --filter server test
/pnpm --filter client test
: 마찬가지로 특정 패키지의 테스트를 실행합니다.
deploy
needs: build-and-test
:build-and-test
작업이 성공적으로 완료된 후에만deploy
작업이 실행되도록 합니다.uses: docker/login-action@v3
: Docker Hub에 로그인합니다. GitHub Secrets에 저장된 자격 증명을 사용합니다.uses: docker/build-push-action@v5
: 각 패키지의Dockerfile
을 사용하여 도커 이미지를 빌드하고 Docker Hub에 푸시합니다.context: .
은 Monorepo 루트에서Dockerfile
을 찾도록 합니다.uses: appleboy/ssh-action@v1.0.3
: SSH를 통해 원격 EC2 인스턴스에 접속하여 배포 스크립트를 실행합니다.docker stop/rm
: 현재 실행 중인 컨테이너를 중지하고 삭제합니다.docker pull
: Docker Hub에서 최신 이미지를 가져옵니다.docker run
: 새로운 도커 컨테이너를 실행합니다.-p
로 포트 매핑,-d
로 백그라운드 실행,--restart always
로 서버 재시작 시 자동 실행,-e
로 컨테이너 내 환경 변수를 설정합니다. 이때 프로덕션 환경용 데이터베이스 정보 등을 Secrets로 전달해야 합니다.
CI/CD 워크플로우 실행 및 모니터링
코드 푸시: main
브랜치에 코드 변경 사항을 푸시합니다.
git add .
git commit -m "feat: setup CI/CD pipeline"
git push origin main
GitHub Actions 확인: GitHub 저장소에서 Actions
탭으로 이동하면, 새로운 워크플로우 실행이 시작된 것을 확인할 수 있습니다.
실행 로그 확인: 실행 중인 워크플로우를 클릭하면 각 Job과 Step의 실시간 로그를 볼 수 있습니다. 빌드, 테스트, 도커 이미지 푸시, 그리고 최종 배포 명령어가 성공적으로 실행되는지 확인합니다.
배포 확인: 배포가 완료되면, 원격 서버의 도커 컨테이너가 정상적으로 실행되고 애플리케이션에 접근 가능한지 웹 브라우저나 API 클라이언트(Postman)로 확인합니다.
CI/CD 파이프라인 최적화 및 고려사항
- 테스트 커버리지: CI 단계에서 충분한 테스트 커버리지를 확보하여 잠재적인 버그를 미리 걸러냅니다.
- 환경별 설정: 개발, 스테이징, 프로덕션 환경별로 다른 환경 변수나 설정 파일을 관리해야 합니다. GitHub Secrets는 프로덕션 환경의 민감한 정보를 안전하게 관리하는 데 필수적입니다.
- 데이터베이스 마이그레이션: 배포 시 데이터베이스 스키마 변경(마이그레이션)이 필요한 경우, CI/CD 파이프라인에 마이그레이션 스크립트 실행 단계를 추가해야 합니다.
- 롤백 전략: 배포 후 문제가 발생했을 때 신속하게 이전 안정 버전으로 되돌릴 수 있는 롤백 전략을 수립합니다. 도커 태그를 활용하거나 클라우드 서비스의 롤백 기능을 사용할 수 있습니다.
- 캐싱: CI/CD 파이프라인에서 의존성 설치, 도커 이미지 빌드 등의 단계에서 캐싱을 적극적으로 활용하여 빌드 시간을 단축합니다.
- 알림: CI/CD 워크플로우의 성공/실패 여부를 슬랙, 이메일 등으로 알림을 받도록 설정하여 빠르게 대응할 수 있습니다.
- 환경 설정: 실제 운영 환경에서는 AWS ECS/EKS, Kubernetes, Heroku, Vercel 등 관리형 서비스를 사용하여 배포를 더욱 자동화하고 안정화할 수 있습니다. 각 서비스는 고유한 CI/CD 통합 기능을 제공합니다.
결론
CI/CD 파이프라인 구축은 현대 소프트웨어 개발에서 빠르고 안정적인 소프트웨어 배포를 위한 필수적인 요소입니다. GitHub Actions는 Monorepo 프로젝트에서도 효율적인 CI/CD 워크플로우를 구축할 수 있는 강력하고 유연한 도구입니다.
이 절에서는 타입스크립트 기반의 NestJS 백엔드와 React 프론트엔드 애플리케이션을 위한 CI/CD 파이프라인을 GitHub Actions를 사용하여 설정하고, 도커 이미지를 빌드 및 푸시하며, 원격 서버에 배포하는 과정을 살펴보았습니다.
CI/CD를 통해 개발 프로세스를 자동화하면, 개발자는 기능 구현에 더 집중할 수 있고, 사용자들은 더 빠르고 안정적인 서비스를 경험할 수 있습니다. 이로써 16장 "실전 프로젝트"와 함께 책의 모든 내용이 마무리됩니다. 학습한 지식들을 바탕으로 더욱 복잡하고 견고한 타입스크립트 애플리케이션을 구축하는 데 성공하시기를 바랍니다.