← Projects
완료LG CNS AM INSPIRE CAMP 3기, 미니 프로젝트 2차

Jenkins 기반 CI/CD 파이프라인 구축

Jenkins와 Docker로 이미지 빌드와 Docker Hub push를 자동화하고, Docker Compose 배포 단계까지 파이프라인 스크립트로 구현한 프로젝트

LG CNS 블로그 서비스를 MSA로 전환하는 팀 프로젝트에서 ai-fortune-service의 Jenkins CI/CD 파이프라인을 맡았습니다. 구축 중 Jenkins가 반복해서 끊기는 장애가 있었는데, 멘토와 함께 docker logs를 확인해 볼륨 마운트 권한 불일치가 원인임을 찾아 해결했습니다.

Jenkins 기반 CI/CD 파이프라인 구축 대표 화면
기간
2025.10.23 ~ 10.29 (1주)
인원
7명
내 역할
CI/CD 파이프라인 구축 담당

기술 스택

  • AWS EC2
  • Jenkins (lts-jdk17)
  • Docker
  • Docker Hub
  • Docker Compose
  • Bash
  • GitHub

아키텍처

GitHub에서 소스를 받아 EC2 위 Jenkins 컨테이너가 docker build를 하고, 빌드 번호 태그와 latest 태그로 Docker Hub에 push합니다. 필요하면 같은 EC2에서 docker compose로 배포합니다.
GitHub에서 소스를 받아 EC2 위 Jenkins 컨테이너가 docker build를 하고, 빌드 번호 태그와 latest 태그로 Docker Hub에 push합니다. 필요하면 같은 EC2에서 docker compose로 배포합니다.

내가 한 일

  • EC2에 Jenkins 컨테이너를 구성하고, docker.sock을 마운트해 호스트의 Docker로 이미지를 빌드하도록 설정했습니다.
  • Docker Hub Access Token을 Jenkins Credentials에 등록하고, 빌드할 때 환경변수로 주입되도록 했습니다.
  • 이미지 빌드, Docker Hub push, Docker Compose 배포까지 이어지는 최종 파이프라인 스크립트를 작성했습니다.
  • 실패했을 때 원인을 빨리 찾을 수 있도록 필수값 검증, 클린 clone, 단계별 로그 같은 방어 장치를 넣었습니다.
  • 배포 구조를 SSH 원격 배포에서 단일 EC2 로컬 compose 배포로 다시 설계했습니다.
팀 맥락

7명이 세 팀(nGrinder 부하 테스트, Jenkins CI/CD, GitHub Actions CI)으로 나눠 진행했고, nGrinder와 GitHub Actions는 다른 팀원이 맡았습니다. Jenkins 초기 셋업과 docker.sock 권한 해결, SSH 원격 배포 스크립트 초안은 팀원이 만들었고, 저는 이를 바탕으로 단일 EC2 로컬 배포 방식의 최종 파이프라인 스크립트를 직접 작성했습니다.

트러블슈팅

01첫 배포 실패: 인증 환경변수가 주입되지 않음

증상
Docker Hub 계정 환경변수(DOCKERHUB_USR, DOCKERHUB_PSW)가 비어 있어 unbound variable 오류로 스크립트가 멈췄습니다.
원인
세 가지가 겹쳤습니다. Jenkins Credentials가 빌드 환경변수로 바인딩되지 않았고, 스크립트가 참조한 변수명(DOCKERHUB_USR)과 바인딩한 변수명도 달랐습니다. 게다가 Docker Hub를 구글 계정으로 가입해서 비밀번호 자체가 없었습니다.
해결
Docker Hub Access Token을 발급해 Jenkins Credentials에 등록하고, Credentials Binding 플러그인으로 환경변수에 주입했습니다. 변수명을 DOCKERHUB_USER / DOCKERHUB_PSW로 맞추고, 값이 비어 있으면 바로 멈추는 필수 변수 검증(${VAR:?})도 넣었습니다.
빌드 #1 콘솔: DOCKERHUB_PSW / DOCKERHUB_USR unbound variable로 실패
빌드 #1 콘솔: DOCKERHUB_PSW / DOCKERHUB_USR unbound variable로 실패

02Jenkins 접속 불가와 플러그인 설치 실패

증상
8080 포트로 접속하면 무한 로딩이 걸렸고, 재구축한 뒤에는 플러그인 설치가 매우 느리고 대부분 실패했습니다.
대응
포트를 8085로 바꾸고 보안그룹을 다시 설정해 처음부터 재구축하면서 원인 가설을 정리했습니다.
원인 가설
  1. 업데이트 서버 타임아웃
  2. 컨테이너 DNS 불안정
  3. 설치 lock 파일 잔존
  4. 디스크 부족
  5. jenkins_home 권한 꼬임
  6. 플러그인 의존성 충돌
재구축 후 초기 플러그인 설치에서 대부분의 플러그인이 실패한 화면
재구축 후 초기 플러그인 설치에서 대부분의 플러그인이 실패한 화면

03진짜 원인: 볼륨 마운트 권한 불일치

증상
빌드 직전에 Jenkins 연결이 다시 끊겼습니다.
원인
멘토와 함께 docker logs를 확인해 보니, 마운트한 디렉토리의 권한이 Jenkins 실행 사용자와 맞지 않아 read/write가 안 된다는 로그가 남아 있었습니다.
해결
마운트할 디렉토리를 미리 만들고 Jenkins 실행 사용자(uid 1000)에게 소유권을 준 뒤 재실행하니 정상적으로 동작했습니다.
배운 점
정리했던 가설 중 권한 꼬임이 실제 원인이었고, 플러그인 설치 실패와 접속 끊김도 같은 원인이었을 가능성이 컸습니다. 이후로는 장애가 나면 docker logs부터 확인합니다.

설계 결정

배포 구조 결정

선택지
CI 서버와 서비스 서버를 분리해 SSH 원격 배포 vs Jenkins가 있는 단일 EC2에 로컬 배포
결정
1주라는 짧은 일정 안에 배포를 성공시키는 것을 우선해 단일 EC2 로컬 배포를 골랐습니다.
근거와 한계
단일 EC2에서는 Jenkins와 서비스가 리소스를 나눠 써서 실제 운영에는 맞지 않습니다. 실제로 메모리 부족도 의심됐기 때문에, 운영 환경이라면 서버를 분리하는 게 맞다고 판단했습니다.

최종 스크립트의 방어 장치

펼쳐 보기
겪은 문제반영한 장치
환경변수 미주입${VAR:?} 필수값 검증
로그인 캐시 꼬임docker logout 후 --password-stdin으로 재로그인
이전 작업물 잔존rm -rf 후 --depth 1 클린 clone
원인 파악이 어려운 실패버전 출력, Dockerfile 존재 체크, 단계별 로그
롤백 불가:BUILD_NUMBER + :latest 이중 태깅
배포 환경 차이compose 파일이 있을 때만 배포하도록 옵션화

최종 파이프라인 스크립트

펼쳐 보기
bash
APP="ai-fortune"
REPO_URL="https://github.com/lgcns2-mini/inspire3_fortune.git"
BRANCH="main"
: "${DOCKERHUB_USER:?DOCKERHUB_USER is empty}"   # Jenkins가 주입
: "${DOCKERHUB_PSW:?DOCKERHUB_PSW is empty}"     # Jenkins가 주입
IMAGE="${DOCKERHUB_USER}/${APP}"
TAG="${BUILD_NUMBER:-latest}"
# (배포 옵션) EC2 로컬에 준비해 둔 compose 파일 경로
COMPOSE_FILE="/opt/compose/fortune.yml"
echo "==> APP=${APP}"
echo "==> REPO=${REPO_URL} (${BRANCH})"
echo "==> IMAGE=${IMAGE}:${TAG}"
### ── 1) 디버그용 버전 확인 ─────────────────────────────────────────────────
docker version || true
docker compose version || true
# ⭐(추가) 캐시 로그인 비우고 새 토큰으로 로그인, 베이스 이미지 미리 당김
docker logout || true
echo "${DOCKERHUB_PSW}" | docker login -u "${DOCKERHUB_USER}" --password-stdin
docker pull openjdk:17-jdk-slim
### ── 2) 소스 체크아웃(깨끗하게) ───────────────────────────────────────────
rm -rf repo
git clone -b "${BRANCH}" --depth 1 "${REPO_URL}" repo
cd repo
# 필요 시 Gradle 빌드가 선행되어야 하는 프로젝트면 주석 해제
# ./gradlew -x test build
# Dockerfile 존재 확인(있으면 진행, 없으면 실패)
test -f Dockerfile || { echo "❌ Dockerfile not found in $(pwd)"; exit 1; }
### ── 3) 이미지 빌드 ────────────────────────────────────────────────────────
echo "==> docker build ${IMAGE}:${TAG}"
docker build --pull -t "${IMAGE}:${TAG}" .
### ── 4) DockerHub 푸시 ─────────────────────────────────────────────────────
echo "==> docker push ${IMAGE}:${TAG}"
docker push "${IMAGE}:${TAG}"
echo "==> tag & push :latest"
docker tag "${IMAGE}:${TAG}" "${IMAGE}:latest"
docker push "${IMAGE}:latest"
docker logout || true
### ── 5) (옵션) 배포: compose pull/up ──────────────────────────────────────
if [ -f "${COMPOSE_FILE}" ]; then
  echo "==> Deploy with compose: ${COMPOSE_FILE}"
  docker compose -f "${COMPOSE_FILE}" pull
  docker compose -f "${COMPOSE_FILE}" up -d
else
  echo "==> Skip deploy (compose file not found: ${COMPOSE_FILE})"
fi
### ── 6) 완료 ───────────────────────────────────────────────────────────────
echo "==> DONE. Pushed ${IMAGE}:${TAG} and ${IMAGE}:latest"
Jenkins Execute shell에 작성한 최종 스크립트 (원문 그대로)

결과

Docker Hub 이미지 태그
7 / latest
두 태그의 digest
0d69a56e1d90 (동일)
push 날짜
2025-10-28

한계

  • Docker Compose 배포 단계는 스크립트로 구현했지만, 실제로 실행한 기록은 남아 있지 않습니다.
  • 단일 EC2 구성이라 Jenkins와 서비스가 리소스를 나눠 씁니다.

다음 개선 과제

  • CI 서버와 서비스 서버 분리
  • docker.sock 마운트 대신 권한을 최소화한 빌드 방식 검토
  • deprecated 상태인 openjdk 베이스 이미지를 eclipse-temurin으로 교체
Jenkins 기반 CI/CD 파이프라인 구축 | Changmin.dev