CI/CD 파이프라인 구축
지난 절에서는 NestJS 애플리케이션을 Docker를 이용해 컨테이너화하는 방법을 알아보았습니다. 이제 11장의 두 번째 절로, 개발된 애플리케이션을 신속하고 안정적으로 배포하기 위한 핵심 전략인 CI/CD(Continuous Integration/Continuous Delivery/Continuous Deployment) 파이프라인 구축에 대해 살펴보겠습니다.
CI/CD는 현대적인 소프트웨어 개발의 필수적인 부분입니다. 개발자가 코드 변경 사항을 자주 통합하고, 자동으로 테스트하며, 궁극적으로 프로덕션 환경에 배포하는 과정을 자동화하여 개발 주기를 단축하고, 오류를 줄이며, 출시 빈도를 높입니다.
CI/CD란 무엇인가?
CI/CD는 세 가지 주요 개념으로 구성됩니다.
지속적 통합
- 정의: 여러 개발자가 작업한 코드 변경 사항을 정기적으로 메인 브랜치(예:
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 파이프라인을 구축하는 과정을 살펴보겠습니다.
사전 준비
- 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
파일을 생성하고 다음 내용을 작성합니다.
# .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_USERNAME
DOCKER_PASSWORD
EC2_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_HOST
PROD_DB_USER
PROD_DB_PASSWORD
PROD_DB_NAME
PROD_JWT_SECRET
보안 경고: EC2_SSH_KEY
는 매우 민감한 정보이므로, 접근 권한을 최소화하고 매우 신중하게 다루어야 합니다. 실제 운영 환경에서는 CI/CD 도구에서 직접 SSH 키를 관리하거나, 클라우드 공급자의 배포 서비스를 사용하는 것이 더 안전합니다.
파이프라인 실행
이제 main
브랜치에 코드를 푸시하거나, 풀 리퀘스트를 생성하면 GitHub Actions 워크플로우가 자동으로 실행됩니다. GitHub 레포지토리의 "Actions" 탭에서 파이프라인의 진행 상황을 모니터링할 수 있습니다.
추가적인 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 파이프라인을 구축함으로써, 개발자는 코드 작성에 더 집중하고 운영상의 부담을 줄일 수 있습니다.