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 Artifact Registry와 같은 컨테이너 이미지 레지스트리에 푸시합니다.
배포 (Deployment): 컨테이너 레지스트리에 있는 이미지를 실제 서버(VM, Kubernetes 클러스터)에 배포합니다.
모니터링 & 알림: 배포 후 애플리케이션의 성능과 상태를 지속적으로 모니터링하고, 문제 발생 시 관련자에게 알림을 보냅니다.
NestJS CI/CD 파이프라인 구축 예시
가장 널리 사용되는 CI/CD 도구 중 하나인 GitHub Actions를 사용하여 NestJS 애플리케이션의 CI/CD 파이프라인을 구축하는 과정을 살펴보겠습니다.
CI/CD 실행 전 준비
- GitHub 레포지토리: NestJS 프로젝트가 GitHub에 푸시되어 있어야 합니다.
- Docker Hub 또는 클라우드 레지스트리: 빌드된 Docker 이미지를 저장할 공간이 필요합니다. 예시에서는 Docker Hub를 사용하지만, 운영에서는 AWS ECR, Google Artifact Registry, GitHub Container Registry 같은 레지스트리를 선택할 수 있습니다.
- GitHub Secrets: Docker Hub 로그인 정보와 같은 민감한 정보는 GitHub Secrets에 저장합니다.
DOCKER_USERNAME: Docker Hub 사용자 이름DOCKERHUB_TOKEN: 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: '24' # 사용할 Node.js LTS 버전
DOCKER_IMAGE_NAME: nestjs-app # Docker 이미지 이름
DOCKER_REGISTRY: docker.io # Docker Hub 레지스트리
permissions:
contents: read
jobs:
# 1. 빌드 및 테스트 (CI)
ci:
runs-on: ubuntu-latest # 워크플로우를 실행할 가상 환경 (GitHub 호스팅 러너)
steps:
- name: Checkout repository # 레포지토리 코드 체크아웃
uses: actions/checkout@v7
- name: Set up Node.js # Node.js 환경 설정
uses: actions/setup-node@v6
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. 이미지 빌드 및 푸시 (Delivery)
image:
needs: ci
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Log in to Docker Hub # Docker Hub 로그인
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image # Docker 이미지 빌드 및 푸시
id: build
uses: docker/build-push-action@v7
with:
context: . # Dockerfile이 있는 경로
push: true # 이미지 푸시 활성화
tags: ${{ env.DOCKER_REGISTRY }}/${{ github.repository_owner }}/${{ env.DOCKER_IMAGE_NAME }}:${{ github.sha }} # 이미지 태그 (커밋 SHA 사용)
cache-from: type=gha # GitHub Actions 캐시 사용
cache-to: type=gha,mode=max # GitHub Actions 캐시 저장
# 3. 배포 (CD - Deployment)
deploy:
needs: image # 검증된 이미지가 만들어진 뒤에만 실행
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest # 배포를 실행할 가상 환경
environment: production # 배포 환경 지정 (GitHub 환경 보호 규칙 적용 가능)
concurrency:
group: production
cancel-in-progress: false
steps:
- name: Deploy to EC2 instance via SSH # SSH를 통해 EC2 인스턴스에 배포
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.EC2_HOST }} # EC2 인스턴스 IP 주소 (GitHub Secret)
username: ${{ secrets.EC2_USERNAME }} # EC2 사용자 이름 (예: ubuntu)
key: ${{ secrets.EC2_SSH_KEY }} # EC2 SSH 프라이빗 키 (GitHub Secret)
fingerprint: ${{ secrets.EC2_HOST_FINGERPRINT }} # 서버 호스트 키 확인
script: | # 서버에서 실행할 스크립트
set -euo pipefail
IMAGE="${{ env.DOCKER_REGISTRY }}/${{ github.repository_owner }}/${{ env.DOCKER_IMAGE_NAME }}@${{ needs.image.outputs.digest }}"
cd /opt/nestjs-app
# compose.yml의 app.image가 ${IMAGE}를 참조하고,
# 운영 비밀은 서버의 /etc/nestjs-app/production.env 또는 Secrets Manager에서 관리한다.
IMAGE="$IMAGE" sudo docker compose pull app
IMAGE="$IMAGE" sudo docker compose up -d --no-deps app
curl -fsS http://127.0.0.1:3000/health배포 보안용 GitHub Secrets 설정
GitHub 레포지토리의 Settings > Secrets and variables > Actions에서 다음 Secret들을 추가합니다.
DOCKER_USERNAMEDOCKERHUB_TOKENEC2_HOST(예:ec2-xx-xx-xx-xx.ap-northeast-2.compute.amazonaws.com또는 IP 주소)EC2_HOST_FINGERPRINTEC2_USERNAME(예:ubuntu,ec2-user)EC2_SSH_KEY(EC2 인스턴스에 접근할 수 있는 SSH 프라이빗 키 전체 내용을 붙여넣기)
운영 DB 비밀번호와 JWT 비밀값은 GitHub job에서 docker run -e로 직접 흘려보내기보다 서버의 제한된 env 파일, 클라우드 Secrets Manager, SSM Parameter Store, Vault 같은 운영 비밀 저장소에서 주입하는 편이 안전합니다.
보안 경고: DOCKERHUB_TOKEN과 EC2_SSH_KEY는 매우 민감한 정보이므로 접근 권한을 최소화하고, PR 검증 job에서는 열리지 않게 해야 합니다. 실제 운영 환경에서는 장기 SSH 키보다 클라우드 배포 서비스나 OIDC 기반 임시 자격 증명을 우선 검토합니다.
파이프라인 실행
이제 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 설계의 최소 통과 기준을 정리합니다.