시스템 아키텍처 개요
지속적 통합 및 지속적 배포(CI/CD) 환경을 구축하기 위해 소스 코드 저장소부터 실제 서비스 환경까지의 흐름을 다음과 같이 설계합니다.
소스 코드 저장소 (Git Push)
↓
Jenkins (웹훅 트리거 및 파이프라인 실행)
↓
Docker (멀티 스테이지 빌드 및 이미지 생성)
↓
Nginx (컨테이너 기반 정적 파일 서빙)
인프라 환경 설정
1. 도커 엔진 설치
호스트 머신에 컨테이너 런타임을 설치하고 데몬을 활성화합니다.
# 패키지 목록 업데이트 및 필수 종속성 설치
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release
# 도커 공식 GPG 키 추가 및 저장소 설정
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# 도커 엔진 및 CLI 도구 설치
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 서비스 등록 및 시작
sudo systemctl enable --now docker
2. 젠킨스 컨테이너 프로비저닝
호스트의 도커 소켓을 마운트하여 젠킨스 내부에서 도커 명령어를 실행할 수 있도록(Docker-outside-of-Docker) 구성합니다.
# 영속적 데이터 디렉토리 생성 및 권한 할당
sudo mkdir -p /data/jenkins_volume
sudo chown -R 1000:1000 /data/jenkins_volume
# 젠킨스 LTS 버전 컨테이너 실행
docker run -d \
--name ci-jenkins \
--restart=always \
-p 9090:8080 \
-p 50000:50000 \
-v /data/jenkins_volume:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /usr/bin/docker:/usr/bin/docker \
jenkins/jenkins:lts-jdk17
브라우저에서 http://[서버_IP]:9090으로 접속하여 초기 관리자 비밀번호(/data/jenkins_volume/secrets/initialAdminPassword)를 입력하고 설정을 완료합니다.
젠킨스 서버 초기화 및 도구 구성
1. 필수 플러그인 설치
Jenkins 관리 > 플러그인 관리 메뉴로 이동하여 다음 플러그인을 검색하고 설치합니다.
- Git Plugin
- NodeJS Plugin
- Docker Pipeline
2. 빌드 도구 전역 설정
Jenkins 관리 > 전역 도구 구성에서 NodeJS 자동 설치 프로그램을 추가합니다. (예: Node 18.x LTS 버전 지정)
3. 파이프라인 잡 생성
새로운 아이템을 생성할 때 Pipeline을 선택합니다. 파이프라인 섹션에서 정의를 Pipeline script from SCM으로 변경하고, Git 저장소 URL과 인증 정보를 입력한 후 스크립트 경로를 Jenkinsfile로 지정합니다.
프론트엔드 프로젝트 배포 설정 파일 작성
프로젝트 루트 디렉토리에 다음 세 가지 설정 파일을 추가하여 빌드 및 배포 로직을 정의합니다.
1. Dockerfile (멀티 스테이지 빌드)
이미지 크기를 최소화하기 위해 빌드 단계와 실행 단계를 분리합니다.
# 1단계: 의존성 설치 및 에셋 컴파일
FROM node:18-alpine AS compile-stage
WORKDIR /workspace
COPY package*.json ./
RUN npm ci --production=false
COPY . .
RUN npm run build
# 2단계: 경량 웹 서버에 정적 파일 주입
FROM nginx:stable-alpine AS production-stage
RUN rm /etc/nginx/conf.d/default.conf
COPY deploy/nginx.conf /etc/nginx/conf.d/
COPY --from=compile-stage /workspace/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
2. Nginx 라우팅 설정 (deploy/nginx.conf)
SPA(Single Page Application)의 클라이언트 사이드 라우팅을 지원하고 정적 파일 캐싱을 활성화합니다.
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip 압축 활성화
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
location / {
try_files $uri $uri/ /index.html;
}
# 정적 자산 캐싱 설정
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
3. Jenkinsfile (파이프라인 스크립트)
도커 이미지를 빌드하고 기존 컨테이너를 교체하는 배포 로직을 선언형 파이프라인으로 작성합니다.
pipeline {
agent any
environment {
APP_NAME = 'web-client'
IMAGE_TAG = "${env.BUILD_ID}"
FULL_IMAGE = "${APP_NAME}:${IMAGE_TAG}"
}
tools {
nodejs 'Node-18'
}
stages {
stage('소스 코드 체크아웃') {
steps {
checkout scm
}
}
stage('도커 이미지 빌드') {
steps {
script {
docker.build(FULL_IMAGE)
}
}
}
stage('컨테이너 롤아웃') {
steps {
script {
sh """
docker stop ${APP_NAME} || true
docker rm ${APP_NAME} || true
docker run -d \
--name ${APP_NAME} \
--restart unless-stopped \
-p 80:80 \
${FULL_IMAGE}
"""
}
}
}
}
post {
always {
// 빌드 후 사용하지 않는 dangling 이미지 정리
sh 'docker image prune -f'
}
}
}
배포 프로세스 및 검증
- 트리거: 개발자가 feature 브랜치를 main 브랜치로 병합하면 웹훅을 통해 젠킨스 파이프라인이 자동 실행됩니다.
- 빌드: 젠킨스가 노드 환경을 활용해 프로젝트를 컴파일하고, 도커 멀티 스테이지 빌드를 통해 최적화된 엔진엑스 이미지를 생성합니다.
- 배포: 호스트 머신에서 기존 컨테이너를 중지 및 제거한 뒤, 새로 빌드된 이미지를 기반으로 컨테이너를 기동합니다.
배포가 완료된 후 다음 명령어로 컨테이너 상태와 서비스 응답을 확인합니다.
# 실행 중인 컨테이너 목록 및 포트 매핑 확인
docker ps --filter "name=web-client"
# HTTP 응답 헤더 및 상태 코드 검증
curl -I http://localhost
프로덕션 환경 최적화 및 트러블슈팅
최적화 전략
- 태그 전략: 빌드 번호 대신 Git Commit SHA를 이미지 태그로 사용하여 버전 추적성을 높입니다.
- 레지스트리 연동: 빌드된 이미지를 AWS ECR, Docker Hub 또는 사설 Harbor 레지스트리에 푸시하여 다중 호스트 배포를 준비합니다.
- 무중단 배포: 단일 컨테이너 교체 방식에서 벗어나 Docker Compose나 Kubernetes를 활용한 롤링 업데이트를 고려합니다.
주요 오류 및 해결 방안
- Permission Denied (Docker 소켓): 젠킨스 컨테이너 내부의 사용자가 호스트의 도커 소켓에 접근할 권한이 없는 경우입니다. 호스트에서
sudo usermod -aG docker jenkins명령어를 실행하거나, 소켓 파일의 권한을 조정해야 합니다. - 포트 충돌 (Address already in use): 호스트의 80번 포트가 다른 프로세스에 의해 점유되어 있을 때 발생합니다.
sudo lsof -i :80명령어로 충돌 프로세스를 식별하고 종료하거나, 젠킨스 스크립트의 포트 매핑을 변경합니다. - OOM (Out of Memory) 발생: 프론트엔드 빌드 단계(Node.js)에서 메모리 부족으로 프로세스가 종료되는 경우,
NODE_OPTIONS="--max-old-space-size=4096"환경 변수를 Dockerfile에 추가하여 힙 메모리 제한을 늘립니다.