CI/CD 파이프라인 구축
지난 절에서는 NestJS 애플리케이션을 Docker를 이용해 컨테이너화하는 방법을 알아보았습니다. 이제 11장의 두 번째 절로, 개발된 애플리케이션을 신속하고 안정적으로 배포하기 위한 핵심 전략인 CI/CD(Continuous Integration/Continuous Delivery/Continuous Deployment) 파이프라인 구축에 대해 살펴보겠습니다.
CI/CD는 현대적인 소프트웨어 개발의 필수적인 부분입니다. 개발자가 코드 변경 사항을 자주 통합하고, 자동으로 테스트하며, 궁극적으로 프로덕션 환경에 배포하는 과정을 자동화하여 개발 주기를 단축하고, 오류를 줄이며, 출시 빈도를 높입니다.
먼저 커밋이 어떤 품질 게이트를 지나 배포 가능한 산출물이 되는지 전체 흐름으로 정리해 보겠습니다.
NestJS에서 CI/CD란 무엇인가?
CI/CD는 세 가지 주요 개념으로 구성됩니다.
CI 통합 루프
- 정의: 여러 개발자가 작업한 코드 변경 사항을 정기적으로 메인 브랜치(예:
main또는master)에 통합하고, 이 통합된 코드에 대해 자동화된 빌드 및 테스트를 수행하는 개발 관행입니다. - 목표: 통합으로 인해 발생하는 충돌이나 버그를 조기에 발견하고 해결하여, 개발 팀이 항상 작동하는 코드베이스를 유지하도록 돕습니다.
-
핵심
- 코드 커밋: 개발자는 작은 단위의 코드 변경 사항을 자주 커밋합니다.
- 자동 빌드: 커밋된 코드는 자동으로 컴파일되고(NestJS의 경우 TypeScript에서 JavaScript로), Docker 이미지로 빌드됩니다.
- 자동 테스트: 단위 테스트, 통합 테스트 등이 자동으로 실행되어 코드의 유효성을 검증합니다.
지속적 제공
- 정의: CI 단계를 통과한 코드를 항상 배포 가능한 상태로 유지하고, 필요할 때 수동으로 프로덕션 환경에 배포할 준비가 되어 있도록 하는 프로세스입니다.
- 목표: 언제든지 고객에게 새로운 기능이나 버그 수정을 출시할 수 있는 유연성을 제공합니다.
-
핵심
- 릴리스 준비: 테스트를 통과한 아티팩트(예: Docker 이미지)는 릴리스 준비 상태가 되어 이미지 레지스트리(예: Docker Hub, ECR)에 푸시됩니다.
- 수동 배포 트리거: 운영 팀이나 책임자가 수동으로 버튼을 눌러 프로덕션 환경에 배포를 시작합니다.
지속적 배포
- 정의: 지속적 제공의 확장으로, CI 단계를 통과하고 모든 테스트를 통과한 코드를 자동으로 프로덕션 환경에 배포하는 프로세스입니다.
- 목표: 개발부터 배포까지의 전체 과정을 완전 자동화하여, 새로운 기능을 고객에게 가장 빠르게 전달하고 피드백을 받을 수 있도록 합니다.
-
핵심
- 자동 배포: 모든 자동화된 테스트를 통과한 코드는 사람의 개입 없이 자동으로 운영 환경에 배포됩니다.
- 신속한 릴리스: 버그 수정이나 새로운 기능이 개발 완료 즉시 사용자에게 제공됩니다.
CI/CD 파이프라인의 주요 단계
일반적인 CI/CD 파이프라인은 다음과 같은 단계로 구성됩니다.
소스 코드 관리 (SCM): 개발자가 Git과 같은 버전 관리 시스템에 코드를 커밋합니다. (예: GitHub, GitLab, Bitbucket)
빌드: 커밋된 코드를 실행 가능한 아티팩트(NestJS의 경우 JavaScript 번들, Docker 이미지)로 변환합니다.
테스트: 단위 테스트, 통합 테스트, E2E(End-to-End) 테스트 등 자동화된 테스트를 실행하여 코드의 기능과 품질을 검증합니다.
이미지 푸시 (Container Registry): 빌드된 Docker 이미지를 Docker Hub, AWS ECR, Google Container Registry와 같은 컨테이너 이미지 레지스트리에 푸시합니다.
배포 (Deployment): 컨테이너 레지스트리에 있는 이미지를 실제 서버(VM, Kubernetes 클러스터)에 배포합니다.
모니터링 & 알림: 배포 후 애플리케이션의 성능과 상태를 지속적으로 모니터링하고, 문제 발생 시 관련자에게 알림을 보냅니다.
NestJS CI/CD 파이프라인 구축 예시
가장 널리 사용되는 CI/CD 도구 중 하나인 GitHub Actions를 사용하여 NestJS 애플리케이션의 CI/CD 파이프라인을 구축하는 과정을 살펴보겠습니다.
CI/CD 실행 전 준비
- GitHub 레포지토리: NestJS 프로젝트가 GitHub에 푸시되어 있어야 합니다.
- Docker Hub 또는 클라우드 레지스트리: 빌드된 Docker 이미지를 저장할 공간이 필요합니다. (예시에서는 Docker Hub 사용)
- GitHub Secrets: Docker Hub 로그인 정보와 같은 민감한 정보는 GitHub Secrets에 저장합니다.
DOCKER_USERNAME: Docker Hub 사용자 이름DOCKER_PASSWORD: Docker Hub 비밀번호 또는 액세스 토큰
Dockerfile 및 .dockerignore
지난 절에서 작성한 Dockerfile과 .dockerignore 파일을 준비합니다. (이 파일들은 Git 레포지토리에 포함되어야 합니다.)
GitHub Actions 워크플로우 파일 작성
.github/workflows/main.yml 파일을 생성하고 다음 내용을 작성합니다.
name: NestJS CI/CD Pipeline
# 이 워크플로우를 트리거할 이벤트 정의
on:
push:
branches:
- main # main 브랜치에 푸시될 때 워크플로우 실행
pull_request:
branches:
- main # main 브랜치로 풀 리퀘스트가 열릴 때도 실행 (선택 사항)
# 환경 변수 정의 (전역 또는 스텝별)
env:
NODE_VERSION: '20' # 사용할 Node.js 버전
DOCKER_IMAGE_NAME: nestjs-app # Docker 이미지 이름
DOCKER_REGISTRY: docker.io # Docker Hub 레지스트리
jobs:
# 1. 빌드 및 테스트 (CI)
build-and-test:
runs-on: ubuntu-latest # 워크플로우를 실행할 가상 환경 (GitHub 호스팅 러너)
steps:
- name: Checkout repository # 레포지토리 코드 체크아웃
uses: actions/checkout@v4
- name: Set up Node.js # Node.js 환경 설정
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm' # npm 캐싱 활성화 (의존성 설치 속도 향상)
- name: Install dependencies # 의존성 설치
run: npm ci
- name: Run unit and integration tests # 단위 및 통합 테스트 실행
run: npm run test
# 2. Docker 이미지 빌드 및 푸시 (CD - Delivery)
# 테스트가 성공해야 다음 단계로 진행
- name: Log in to Docker Hub # Docker Hub 로그인
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image # Docker 이미지 빌드 및 푸시
uses: docker/build-push-action@v5
with:
context: . # Dockerfile이 있는 경로
push: true # 이미지 푸시 활성화
tags: ${{ env.DOCKER_REGISTRY }}/${{ github.repository_owner }}/${{ env.DOCKER_IMAGE_NAME }}:${{ github.sha }} # 이미지 태그 (커밋 SHA 사용)
# 또는 tags: ${{ env.DOCKER_REGISTRY }}/${{ github.repository_owner }}/${{ env.DOCKER_IMAGE_NAME }}:latest # latest 태그 사용
cache-from: type=gha # GitHub Actions 캐시 사용
cache-to: type=gha,mode=max # GitHub Actions 캐시 저장
# 3. 배포 (CD - Deployment)
deploy:
needs: build-and-test # build-and-test job이 성공적으로 완료된 후에만 실행
runs-on: ubuntu-latest # 배포를 실행할 가상 환경
environment: production # 배포 환경 지정 (GitHub 환경 보호 규칙 적용 가능)
steps:
- name: Deploy to EC2 instance via SSH # SSH를 통해 EC2 인스턴스에 배포
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.EC2_HOST }} # EC2 인스턴스 IP 주소 (GitHub Secret)
username: ${{ secrets.EC2_USERNAME }} # EC2 사용자 이름 (예: ubuntu)
key: ${{ secrets.EC2_SSH_KEY }} # EC2 SSH 프라이빗 키 (GitHub Secret)
script: | # 서버에서 실행할 스크립트
# 1. Docker Hub에서 최신 이미지 풀
# 태그는 build-and-test job에서 사용한 커밋 SHA 또는 latest를 사용
sudo docker pull ${{ env.DOCKER_REGISTRY }}/${{ github.repository_owner }}/${{ env.DOCKER_IMAGE_NAME }}:${{ github.sha }}
# 2. 기존 컨테이너 중지 및 삭제 (선택 사항: 롤링 업데이트 시에는 다른 전략 사용)
if [ "$(sudo docker ps -q -f name=my-nestjs-container)" ]; then
sudo docker stop my-nestjs-container
sudo docker rm my-nestjs-container
fi
# 3. 새로운 컨테이너 실행
# 운영 환경 변수를 -e 옵션으로 전달하거나, 환경 변수 파일(예: .env.production)을 서버에 준비
sudo docker run -d -p 3000:3000 \
--name my-nestjs-container \
-e DATABASE_HOST=${{ secrets.PROD_DB_HOST }} \
-e DATABASE_USER=${{ secrets.PROD_DB_USER }} \
-e DATABASE_PASSWORD=${{ secrets.PROD_DB_PASSWORD }} \
-e DATABASE_NAME=${{ secrets.PROD_DB_NAME }} \
-e JWT_SECRET=${{ secrets.PROD_JWT_SECRET }} \
${{ env.DOCKER_REGISTRY }}/${{ github.repository_owner }}/${{ env.DOCKER_IMAGE_NAME }}:${{ github.sha }}
echo "Deployment successful!"배포 보안용 GitHub Secrets 설정
GitHub 레포지토리의 Settings > Secrets and variables > Actions에서 다음 Secret들을 추가합니다.
DOCKER_USERNAMEDOCKER_PASSWORDEC2_HOST(예:ec2-xx-xx-xx-xx.ap-northeast-2.compute.amazonaws.com또는 IP 주소)EC2_USERNAME(예:ubuntu,ec2-user)EC2_SSH_KEY(EC2 인스턴스에 접근할 수 있는 SSH 프라이빗 키 전체 내용을 붙여넣기)PROD_DB_HOSTPROD_DB_USERPROD_DB_PASSWORDPROD_DB_NAMEPROD_JWT_SECRET
보안 경고: EC2_SSH_KEY는 매우 민감한 정보이므로, 접근 권한을 최소화하고 매우 신중하게 다루어야 합니다. 실제 운영 환경에서는 CI/CD 도구에서 직접 SSH 키를 관리하거나, 클라우드 공급자의 배포 서비스를 사용하는 것이 더 안전합니다.
파이프라인 실행
이제 main 브랜치에 코드를 푸시하거나, 풀 리퀘스트를 생성하면 GitHub Actions 워크플로우가 자동으로 실행됩니다. GitHub 레포지토리의 Actions 탭에서 파이프라인의 진행 상황을 모니터링할 수 있습니다.
실제 운영 파이프라인은 테스트를 통과한 변경만 이미지로 만들고, 배포에 필요한 민감한 값은 Secrets 경계를 통해 주입한 뒤, 배포 후 모니터링 결과에 따라 롤백 여부를 판단하는 흐름으로 읽으면 됩니다.
추가적인 CI/CD 고려사항
- 테스트 커버리지: 코드 테스트 커버리지 도구를 CI/CD에 통합하여, 일정 수준 이상의 테스트 커버리지를 강제하고 코드 품질을 유지합니다.
- 코드 품질 검사: ESLint, Prettier 등의 도구를 파이프라인에 포함하여 코드 스타일과 품질을 자동 검사합니다.
- 취약점 스캐닝:
npm audit이나 Snyk 같은 도구를 CI 단계에 추가하여 의존성 취약점을 자동으로 검사합니다. - 롤백 전략: 배포 실패 시 이전 버전으로 빠르게 롤백할 수 있는 전략을 마련합니다 (예: Docker 이미지 태깅, Kubernetes 롤아웃 히스토리).
-
배포 전략
- 재시작 배포 (Recreate): 기존 컨테이너를 모두 중지하고 새 컨테이너를 시작합니다. (다운타임 발생)
- 롤링 업데이트 (Rolling Update): 점진적으로 새 버전의 컨테이너를 배포하고 이전 버전을 제거합니다. (다운타임 최소화)
- 블루/그린 배포 (Blue/Green Deployment): 두 개의 동일한 환경을 유지하며, 한 번에 전체 트래픽을 새로운 환경으로 전환합니다. (다운타임 거의 없음, 롤백 용이)
- 카나리 배포 (Canary Deployment): 소수의 사용자에게만 새로운 버전을 먼저 배포하여 테스트하고, 문제가 없으면 점진적으로 모든 사용자에게 확대합니다.
- 환경 변수 관리: 운영 환경에서 민감한 환경 변수는 클라우드 Secrets Manager(AWS Secrets Manager, GCP Secret Manager)나 HashiCorp Vault와 같은 전용 도구를 통해 안전하게 관리하고 파이프라인에서 주입받도록 합니다.
CI/CD 파이프라인은 빌드, 테스트, 이미지 생성, 배포 단계를 자동화하고 실행 이력을 남기는 구조입니다. NestJS 애플리케이션을 컨테이너화해 파이프라인에 연결하면 어떤 코드가 어떤 이미지로 배포됐는지 추적할 수 있습니다.
파이프라인은 단계가 많을수록 좋은 것이 아니라, 실패해야 할 지점에서 빠르게 멈추고 운영 비밀을 안전한 경계 안에서만 쓰는 것이 중요합니다. 아래 다이어그램은 CI/CD 설계의 최소 통과 기준을 정리합니다.