CI/CD란?
CI/CD는 소프트웨어의 변경 사항을 검증하고 배포하는 과정을 자동화하는 개발 프로세스다. 개발자가 코드를 수정할 때마다 직접 빌드하고 테스트한 뒤 서버에 배포하는 작업을 반복하면 시간이 많이 소요되고, 사람의 실수로 인해 배포 오류가 발생할 가능성도 높아진다.
CI/CD는 이러한 반복 작업을 파이프라인으로 구성해 코드 변경부터 배포까지 일관된 절차로 처리한다.
CI는 Continuous Integration의 약자로, 지속적 통합을 의미한다. 개발자가 코드를 저장소에 반영하면 자동으로 빌드와 테스트를 수행해 변경 사항의 문제를 빠르게 발견한다.
CD는 Continuous Delivery 또는 Continuous Deployment를 의미한다. Continuous Delivery는 검증된 코드를 배포 가능한 상태로 유지하고 실제 배포를 승인 절차에 따라 진행하는 방식이다. Continuous Deployment는 검증을 통과한 변경 사항을 운영 환경까지 자동으로 배포하는 방식이다.
CI/CD의 핵심은 단순히 배포 명령을 자동으로 실행하는 것이 아니다. 코드 변경을 빠르게 검증하고, 배포 과정을 표준화하며, 문제가 발생했을 때 신속하게 복구할 수 있는 구조를 만드는 것이다.
CI/CD 파이프라인의 전체 흐름
일반적인 CI/CD 파이프라인은 다음과 같은 단계로 구성된다.
코드 변경
↓
Git 저장소 반영
↓
자동 빌드
↓
자동 테스트
↓
Docker 이미지 빌드
↓
이미지 레지스트리에 업로드
↓
배포 환경 및 설정 확인
↓
스테이징 또는 운영 환경 배포
↓
헬스 체크 및 배포 결과 검증
각 단계는 이전 단계의 성공 여부에 따라 실행된다. 빌드나 테스트가 실패하면 이후 배포 단계로 진행하지 않도록 구성하는 것이 일반적이다.
이를 통해 오류가 포함된 코드가 검증 없이 운영 환경에 반영되는 위험을 줄일 수 있다.
자동 빌드
자동 빌드는 코드가 변경될 때 애플리케이션을 컴파일하거나 실행 가능한 형태로 패키징하는 작업을 자동으로 수행하는 과정이다.
개발자가 직접 빌드하면 로컬 환경의 설정, 설치된 라이브러리, 빌드 도구 버전 등에 따라 결과가 달라질 수 있다. CI 환경에서 빌드를 수행하면 정해진 도구와 절차를 사용해 이러한 차이를 줄일 수 있다.
Node.js 프로젝트에서는 의존성을 설치하고 코드 검사 및 빌드 명령을 실행할 수 있다.
npm ci
npm run lint
npm run build
npm ci는 lock 파일을 기준으로 의존성을 설치하는 명령이다. CI 환경에서 npm install 대신 사용하면 의존성 설치 결과를 보다 일관되게 유지할 수 있다.
npm run lint는 정적 코드 검사로 잠재적인 오류나 코딩 규칙 위반을 탐지한다. npm run build는 프로젝트에 정의된 빌드 작업을 실행한다. Node.js 서버 프로젝트는 별도의 컴파일 단계가 없을 수도 있으므로 프로젝트의 구조에 맞게 빌드 작업을 구성해야 한다.
자동 빌드는 코드가 실행 가능한 상태인지 확인하는 첫 번째 검증 단계다. 빌드가 성공하더라도 실제 기능이 정상적으로 동작한다는 의미는 아니므로 자동 테스트와 함께 사용해야 한다.
자동 테스트
자동 테스트는 코드 변경으로 인해 기존 기능이 손상되지 않았는지 확인하는 과정이다.
테스트를 수동으로 수행하면 검증 범위가 제한되고, 반복적인 배포 과정에서 테스트를 생략하기 쉽다. CI 파이프라인에 테스트를 포함하면 코드 변경이 발생할 때마다 정해진 테스트를 자동으로 실행할 수 있다.
자동 테스트는 검증 대상과 범위에 따라 구분할 수 있다.
- 단위 테스트: 개별 함수나 모듈의 동작을 검증한다.
- 통합 테스트: 데이터베이스, Redis, 외부 서비스 등 여러 구성 요소가 함께 동작하는지 확인한다.
- API 테스트: 요청과 응답, 상태 코드, 데이터 형식 및 오류 처리를 검증한다.
- 계약 테스트: 서비스 간에 약속된 API 인터페이스가 유지되는지 확인한다.
- 부하 테스트: 동시 요청이 증가할 때 처리량과 응답 시간이 목표 범위에 있는지 확인한다.
Node.js 프로젝트에서 Jest를 사용한다면 다음과 같이 테스트를 실행할 수 있다.
npm test -- --runInBand
이 명령은 Jest를 사용하는 프로젝트에서 테스트를 순차적으로 실행하도록 설정하는 예시다. 실제 명령은 프로젝트의 package.json에 정의된 스크립트에 따라 달라질 수 있다.
모든 테스트를 모든 커밋에서 동일한 강도로 실행할 필요는 없다. 빠르게 실행되는 단위 테스트는 코드 변경마다 수행하고, 시간이 오래 걸리는 부하 테스트나 전체 시스템 테스트는 별도의 파이프라인에서 수행할 수 있다.
테스트가 실패하면 배포를 중단하고 실패 원인을 확인할 수 있도록 로그와 테스트 결과를 보존해야 한다.
Docker 이미지 빌드
Docker 이미지 빌드는 애플리케이션과 실행에 필요한 파일, 라이브러리 및 런타임 환경을 이미지로 패키징하는 과정이다.
애플리케이션을 개발 환경에서 정상적으로 실행하더라도 운영 서버에서는 Node.js 버전, 운영체제 라이브러리, 환경 설정 등의 차이로 오류가 발생할 수 있다.
Docker 이미지를 사용하면 실행 환경을 명시적으로 정의하고, 동일한 이미지를 여러 환경에서 사용할 수 있다.
다음은 Node.js 서버를 위한 간단한 Dockerfile 예시다.
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist ./dist
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/server.js"]
이 예시는 빌드된 결과물이 dist 디렉터리에 존재한다는 가정으로 작성했다. 실제 프로젝트에서는 빌드 단계와 실행 단계를 분리하는 멀티 스테이지 빌드를 적용할 수도 있다.
이미지를 빌드할 때는 다음과 같은 명령을 사용할 수 있다.
docker build -t game-server:latest .
CI/CD에서는 latest 태그에만 의존하기보다 Git 커밋 해시나 릴리스 버전을 사용해 이미지를 식별하는 것이 좋다.
docker build -t game-server:COMMIT_SHA .
COMMIT_SHA는 설명을 위한 자리표시자이며, 실제 파이프라인에서는 환경 변수로 전달되는 커밋 해시를 사용한다.
빌드가 완료된 이미지는 Amazon ECR, Docker Hub 등의 이미지 레지스트리에 업로드할 수 있다. 이후 배포 서버는 해당 레지스트리에서 이미지를 가져와 실행한다.
중요한 원칙은 빌드와 배포를 분리하고, 검증된 동일 이미지를 다음 환경으로 승격하는 것이다. 개발 환경과 운영 환경에서 각각 이미지를 다시 빌드하면 이미지 내용이 달라질 가능성이 있으므로, 가능하면 동일한 이미지 다이제스트를 유지하는 것이 좋다.
배포 자동화
배포 자동화는 검증이 완료된 애플리케이션을 목표 환경에 반영하는 작업을 자동으로 수행하는 과정이다.
수동 배포에서는 서버 접속, 파일 복사, 프로세스 재시작, 설정 변경 등의 작업을 직접 수행한다. 이 방식은 배포 절차가 사람마다 달라질 수 있고, 작업 누락이나 잘못된 명령으로 장애가 발생할 수 있다.
CI/CD 파이프라인에서는 이러한 절차를 코드와 설정으로 정의해 반복 가능한 배포 프로세스로 만든다.
배포 대상에 따라 자동화 방식은 달라질 수 있다.
- 가상 머신: SSH, 배포 스크립트, 프로세스 관리자 등을 이용해 애플리케이션을 갱신한다.
- Docker 기반 서버: 새로운 이미지를 가져온 뒤 컨테이너를 교체한다.
- Kubernetes: Deployment의 이미지 버전을 변경하고 롤링 업데이트를 수행한다.
- 클라우드 관리형 서비스: 클라우드 배포 서비스나 컨테이너 오케스트레이션 기능을 활용한다.
배포 전략은 서비스의 가용성과 복구 요구사항에 따라 선택해야 한다.
롤링 업데이트는 기존 인스턴스를 순차적으로 교체해 서비스 중단을 줄이는 방식이다. 블루-그린 배포는 기존 환경과 새 환경을 별도로 준비한 뒤 트래픽을 전환하는 방식이다. 카나리 배포는 일부 트래픽만 새 버전으로 전달하면서 문제가 없는지 확인한 후 점진적으로 확대하는 방식이다.
배포 자동화에는 배포 명령 실행뿐 아니라 배포 이후의 검증도 포함되어야 한다.
- 애플리케이션 프로세스가 정상적으로 실행되는지 확인한다.
- 헬스 체크 엔드포인트가 정상 응답하는지 확인한다.
- 오류율과 응답 시간이 비정상적으로 증가하지 않는지 확인한다.
- 문제가 발생하면 이전 버전으로 되돌릴 수 있도록 준비한다.
배포 명령이 성공했다는 사실만으로 서비스가 정상이라는 결론을 내릴 수는 없다. 실제 서비스 상태를 검증하는 단계가 필요하다.
환경별 설정 관리
환경별 설정 관리는 개발, 스테이징, 운영 환경에서 달라지는 설정을 애플리케이션 코드와 분리해 관리하는 방식이다.
일반적으로 개발 환경에서는 테스트용 데이터베이스와 외부 서비스를 사용하고, 운영 환경에서는 실제 데이터와 운영용 서비스에 연결한다. 이때 환경별 설정을 소스 코드에 직접 작성하면 잘못된 환경에 연결하거나 민감한 정보가 저장소에 노출될 수 있다.
따라서 환경별로 달라지는 설정은 배포 환경에서 주입하도록 구성하는 것이 좋다.
예를 들어 Node.js 애플리케이션은 다음과 같은 환경 변수를 사용할 수 있다.
NODE_ENV=production
PORT=3000
DATABASE_HOST=production-db
REDIS_HOST=production-redis
이 값은 설정 방식의 예시이며, 실제 운영 환경에서는 데이터베이스 주소와 연결 방식 등을 인프라 구조에 맞게 정의해야 한다.
환경별 설정은 다음과 같이 구분할 수 있다.
- 애플리케이션 설정: 포트, 로그 레벨, 기능 활성화 여부 등
- 외부 서비스 설정: 데이터베이스 주소, Redis 주소, 외부 API 엔드포인트 등
- 민감한 정보: 데이터베이스 비밀번호, API 키, 인증서, 액세스 토큰 등
일반 설정은 환경 변수나 환경별 설정 파일로 관리할 수 있다. 민감한 정보는 Git 저장소에 직접 저장하지 않고 AWS Secrets Manager, AWS Systems Manager Parameter Store, Kubernetes Secret 등 적절한 비밀 정보 관리 수단을 활용해야 한다.
Kubernetes에서는 ConfigMap으로 일반 설정을 관리하고 Secret으로 민감한 정보를 전달할 수 있다. 다만 Secret이라는 리소스를 사용했다는 사실만으로 데이터가 충분히 보호되는 것은 아니다. 접근 권한, 암호화 설정, 로그 노출 방지 등을 함께 고려해야 한다.
환경별 설정을 분리하더라도 애플리케이션의 핵심 동작이 환경마다 달라지지 않도록 주의해야 한다. 동일한 애플리케이션 이미지에 환경별 설정을 주입하는 구조를 사용하면 배포 결과의 일관성을 유지하기 쉽다.
GitHub Actions를 이용한 CI/CD 구성 예시
GitHub Actions는 GitHub 저장소에서 워크플로를 정의하고 자동 빌드, 테스트, 이미지 빌드 등의 작업을 실행할 수 있는 도구다.
다음은 Node.js 프로젝트의 빌드와 테스트를 자동화하는 기본 워크플로 예시다.
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Run lint
run: npm run lint
- name: Run tests
run: npm test
- name: Build application
run: npm run build
이 워크플로는 main 브랜치에 대한 Pull Request와 직접적인 코드 반영을 기준으로 실행된다. 저장소를 체크아웃하고 Node.js 환경을 구성한 다음 의존성 설치, 정적 코드 검사, 테스트, 빌드를 순서대로 수행한다.
실제 프로젝트에 적용하려면 package.json에 lint, test, build 스크립트가 정의되어 있어야 한다. 사용 중인 Node.js 버전과 GitHub Actions의 액션 버전도 프로젝트의 유지보수 정책에 맞게 관리해야 한다.
Docker 이미지 빌드와 운영 배포는 이 워크플로에 추가하거나 별도의 워크플로로 분리할 수 있다. 다만 운영 배포 단계에서는 이미지 레지스트리 인증, 배포 권한, 환경별 승인 정책 등을 추가로 구성해야 한다.
운영 환경의 비밀 정보는 GitHub Actions Secrets나 OIDC 기반의 클라우드 인증 방식으로 관리할 수 있다. AWS를 사용하는 경우 장기 액세스 키를 저장하는 대신 OIDC를 통해 필요한 역할을 제한적으로 부여하는 방식이 유용하다.
또한 pull_request에서 실행되는 워크플로는 외부 기여자의 코드를 실행할 수 있으므로, 신뢰할 수 없는 코드에 운영용 비밀 정보가 노출되지 않도록 권한과 이벤트 구성을 검토해야 한다.
CI/CD 운영 시 고려사항
CI/CD 파이프라인은 한 번 구성하는 것으로 끝나지 않는다. 파이프라인 자체의 신뢰성, 보안, 실행 시간, 복구 가능성을 지속적으로 관리해야 한다.
파이프라인 실패 처리
빌드, 테스트, 이미지 업로드, 배포 중 어느 단계에서 실패했는지 명확하게 확인할 수 있어야 한다. 실패한 작업의 로그와 테스트 결과를 보존하고, 오류를 수정한 뒤 다시 실행할 수 있는 구조를 마련해야 한다.
배포 권한 최소화
빌드 작업과 운영 배포 작업의 권한을 분리하는 것이 좋다. 모든 워크플로가 운영 서버나 클라우드 리소스를 수정할 수 있다면 저장소의 취약점이나 잘못된 설정이 큰 장애로 이어질 수 있다.
의존성과 이미지 보안
외부 패키지와 베이스 이미지의 취약점을 검사하고, 불필요한 실행 파일과 권한을 줄여야 한다. 배포에 사용하는 이미지는 검증된 출처에서 가져오고, 가능하면 이미지 다이제스트를 기준으로 추적한다.
배포 후 모니터링
배포 이후 오류율, 응답 시간, CPU 및 메모리 사용량 등을 확인해야 한다. 배포 직후 발생하는 오류를 빠르게 감지할 수 있도록 알림 정책을 마련하고, 문제가 발생했을 때 자동 또는 수동 롤백이 가능한지 검증해야 한다.
데이터베이스 변경 관리
애플리케이션만 이전 버전으로 되돌린다고 해서 데이터베이스까지 자동으로 복구되는 것은 아니다. 스키마 변경은 이전 버전과 호환되는 방식으로 진행하고, 데이터 마이그레이션과 복구 전략을 별도로 설계해야 한다.
파이프라인 실행 시간
테스트와 빌드가 지나치게 오래 걸리면 개발 피드백이 늦어진다. 의존성 캐시, 작업 병렬화, 변경 범위에 따른 선택적 테스트 등을 활용해 실행 시간을 줄일 수 있다. 다만 캐시로 인해 오래된 결과가 재사용되지 않도록 캐시 키와 무효화 정책을 함께 관리해야 한다.
실시간 게임 서버에서의 CI/CD
실시간 게임 서버는 일반적인 웹 애플리케이션보다 배포 시 고려해야 할 요소가 많다. 서버 인스턴스가 WebSocket 연결을 유지하고, 플레이어 세션이나 Room 상태를 관리하기 때문이다.
새로운 버전을 배포할 때 실행 중인 서버를 즉시 종료하면 연결된 플레이어가 강제로 끊기거나 게임 진행 상태가 유실될 수 있다.
따라서 게임 서버의 CI/CD에는 연결 종료와 세션 이전을 고려한 배포 전략이 필요하다.
- 새 서버 인스턴스를 먼저 실행하고 헬스 체크를 통과한 뒤 트래픽을 전달한다.
- 종료 대상 서버는 신규 연결을 받지 않도록 상태를 변경한다.
- 기존 플레이어의 게임이 종료되거나 안전하게 재접속할 수 있도록 유예 시간을 제공한다.
- 필요한 게임 상태를 Redis나 데이터베이스 등에 저장하고, 복구 가능한 상태를 유지한다.
- 서버 종료 시 진행 중인 작업과 데이터 저장이 완료되도록 Graceful Shutdown을 구현한다.
- 새 버전 배포 이후 연결 실패율, 재접속률, 게임 진행 오류, 서버 지연 시간을 확인한다.
단, 모든 게임 상태를 외부 저장소에 기록하는 방식이 항상 적합한 것은 아니다. 실시간으로 갱신되는 상태를 매번 저장하면 지연 시간과 비용이 증가할 수 있다. 게임의 일관성 요구사항과 복구 가능성을 기준으로 영속화할 상태와 메모리에 유지할 상태를 구분해야 한다.
또한 서버 버전이 혼재하는 동안 프로토콜이나 메시지 형식이 호환되지 않으면 클라이언트와 서버 간 통신 오류가 발생할 수 있다. 점진적 배포가 필요한 시스템에서는 메시지 형식과 API의 하위 호환성을 고려해야 한다.