코드의 여백

Jenkins Pipeline와 DooD를 결합한 CI/CD 환경 구성하기

by rowing0328

Intro

이번 포스팅에서는 Docker DooD 방식을 활용해 Jenkins 컨테이너가 별도의 Docker 데몬 없이

호스트의 Docker 엔진을 직접 제어하는 CI/CD 파이프라인 구축 방법을 간략히 소개한다.

 

또한, Docker Compose와 DockerFile을 활용해

Jenkins 설치부터 GitHub 연동, AWS EC2 배포까지의 전 과정을 다룬다.

 

 

Docker DooD 란

Jenkins와 같은 컨테이너 내 애플리케이션이 별도의 Docker 데몬을 설치하지 않고도, 호스트 머신에 이미 설치되어 있는 Docker 엔진을 활용해 컨테이너를 실행할 수 있도록 하는 방식이다.

 

이를 위해 Jenkins 컨테이너 내부에 호스트의 Docker 소켓(일반적으로 /var/run/docker.sock)을 마운트 하여, 컨테이너에서 직접 호스트의 Docker 데몬에 접근할 수 있도록 구성한다.

 

[ 장점 ]

  • Docker 소켓을 공유하기 때문에 오버헤드가 적다.
  • 호스트의 Docker 데몬을 사용하므로 자원 사용이 효율적이다.

 

[ 단점 ]

  • 컨테이너가 호스트의 Docker 소켓에 접근할 수 있기 때문에,
    컨테이너 내에서 실행되는 프로세스가 호스트 전체에 영향을 미칠 가능성이 있다.
  • 권한이 탈취될 경우 호스트의 모든 컨테이너와 시스템에 접근할 수 있는 위험이 있다.

 

 

Docker Compose와 DockerFile를 활용한 Jenkins 설치

 

[ 사전 준비 사항 ]

  • AWS EC2 인스턴스 (Ubuntu 20.04 기준)
  • Docker 및 Docker Compose 설치
  • GitHub 리포지토리 (파이프라인에서 사용할 코드 포함)
  • Jenkins 관련 포트 오픈 (8080, 50000)
  • AWS 보안 그룹 설정 (SSH, HTTP/HTTPS 허용)

 

1. 컨테이너와 호스트 작업 디렉터리 마운트를 위한 디렉터리 생성

mkdir jenkins_home

 

2. 컨테이너의 호스트 디렉토리 접근을 위한 파일 소유자 및 그룹 변경

sudo chown -R 1000:1000 jenkins_home

 

3. DockerFile 작성

  • 각 명령어의 역할과 목적을 이해할 수 있도록 라인별 주석을 통해 자세히 설명한다.
# Jenkins LTS 버전 (JDK 17 포함)을 베이스 이미지로 사용합니다.
FROM jenkins/jenkins:lts-jdk17

# 빌드 시점에 호스트의 Docker 그룹 및 사용자 ID를 인자로 받아 설정합니다.
# Docker 그룹 ID를 일치시켜 Jenkins 컨테이너에서 Docker를 제어할 수 있도록 합니다.
ARG HOST_DOCKER_GID=your-host-docker-gid
ARG HOST_UID=your-host-uid

# 루트 사용자로 전환하여 필요한 시스템 패키지를 설치합니다.
# - 가령 루트 권한이 있어야 시스템 수준 패키지 및 권한 설정이 가능합니다.
USER root

# Jenkins와 Docker 통합에 필요한 필수 패키지를 설치합니다.
# ca-certificates: 보안 인증서 처리
# curl: 데이터 다운로드
# gnupg: 패키지 인증을 위한 암호화
# lsb-release: OS 정보 확인
RUN apt-get update && apt-get install -y \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

# Docker 공식 GPG 키를 호스트 keyring 디렉토리에 저장한다.
# Docker 패키지의 무결성을 검증하기 위한 필수 단계이다.
RUN mkdir -p /etc/apt/keyrings && \
    curl -fsSL https://download.docker.com/linux/debian/gpg | \
    gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# Docker 저장소를 호스트에 추가한다.
# 최신 Docker CLI 설치를 위해 Docker의 안정화된 저장소를 참조한다.
RUN echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | \
    tee /etc/apt/sources.list.d/docker.list > /dev/null

# Docker CLI를 설치한다.
# Jenkins 파이프라인에서 Docker 명령어를 실행할 수 있도록 지원한다.
RUN apt-get update && apt-get install -y docker-ce-cli

# Docker 그룹을 생성한다.
# 호스트와 동일한 그룹 ID를 사용하여 권한 충돌을 방지한다.
RUN groupadd -g ${HOST_DOCKER_GID} docker

# Jenkins 사용자에게 Docker 그룹 권한을 부여한다.
# Jenkins가 Docker 명령어를 root 권한 없이 실행할 수 있도록 한다.
RUN groupmod -g ${HOST_UID} jenkins && \
    usermod -aG docker jenkins

# 보안성을 위해 루트 사용자를 피하기 위해 Jenkins 사용자로 전환한다.
USER jenkins

 

4. docker-compose.yml 작성

services:
  jenkins:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 8080:8080
    container_name: jenkins
    environment:
      TZ: Asiz/Seoul
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock"
      - ./jenkins_home:/var/jenkins_home
    networks:
      - jenkins

networks:
  jenkins:
    name: jenkins
    driver: bridge

 

5. Jenkins 컨테이너 실행

docker-compose up -d

 

 

Jenkins 초기 설정

1. Jenkins 웹 인터페이스 (http://<EC2-IP>:8080) 접속

 

2. 초기 비밀번호 확인

docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

 

3. 권장 플러그인 설치 및 관리자 계정 생성

 

 

SSH 기반 GitHub 연동 설정

1. Jenkins 컨테이너 내부 기준 SSH 키 생성

docker exec -it jenkins ssh-keygen -t rsa -b 4096 -C "jenkins-github"

 

2. 생성된 Public Key를 GitHub에 등록한다.

  • GitHub 저장소 -> Settings ->Deploy keys

 

3. 생성된 Private Key를 Jenkins에 등록한다.

  • Jenkins Dashboard → Jenkins 관리 → Credentials → System → Global credentials

 

 

SCM 활성화를 위한 GitHub Webhook 연동 설정

Jenkins의 주요 기능 중 하나인 SCM은 GitHub Webhook 요청을 받아 파이프라인을 자동 트리거하여 지속적 통합 및 배포 프로세스를 실행한다.

 

[ GitHub Webhook이란? ]

GitHub Webhook은 특정 이벤트가 발생할 때(예: Push, Pull Request) 외부 서버에 HTTP POST 요청을 전송하여 자동으로 작업을 수행할 수 있도록 한다.

 

Jenkins & GitHub Webhook 연동 설정

  • GitHub 저장소 → Settings -> Webhooks -> Add webhook

 

 

Docker Spring Boot Image 실행 환경 구성

1. 애플리케이션 프로젝트 최상단 루트에 DockerFile을 생성한다.

# Java 21을 포함한 경량 JRE 이미지를 기반으로 설정
FROM bellsoft/liberica-openjdk-alpine:21

# 빌드된 JAR 파일을 컨테이너 이미지 내로 복사
COPY build/libs/itzip-0.0.1-SNAPSHOT.jar app.jar

# 컨테이너가 시작될 때 JAR 파일을 실행하는 명령어 설정
CMD ["java", "-jar", "app.jar"]

 

2. 이미지 빌드를 위한 docker-compose.yml 파일 작성

services:
  springboot:
    image: toastit/v1:${BUILD_NUMBER}
    container_name: springboot
    ports:
      - "8080:8080"

networks:
  springboot:
    name: springboot
    driver: bridge

 

3. 작성된 docker-compose.yml 파일을 Jenkins Credentials에 등록한다.

 

 

Docker Hub 접속 환경 구성

사용 중인 Docker Personal access token을 Password에 등록하고, DockerHub 사용자 이름을 Username에 등록한다.

  • Jenkins Dashboard → Jenkins 관리 → Credentials → System → Global credentials

 

 

SSH 서버 설정 구성

1. Publish Over SSH를 설치한다.

  • Jenkins Dashboard → Jenkins 관리 → Plugins

 

2. 원격 서버 정보를 등록하고, Test Configuration 버튼을 눌러 연결 테스트를 진행한다.

  • SSH Server: 등록할 원격 서버의 별칭을 입력한다.
  • Hostname: 원격 서버의 IP 주소나 도메인 이름을 입력한다.
  • Username: SSH 접속에 사용할 사용자 이름을 입력한다.
  • Remote Directory: 파일 전송 시 기본으로 사용할 원격 경로를 지정한다.
  • Key: 개인 키 내용을 직접 입력하거나, Jenkins에 저장된 키를 사용할 수 있다.
  • Password: 암호 인증을 사용하는 경우 비밀번호를 입력한다.

 

3. 원격 서버 authorized_keys에 젠킨스 컨테이너 공개 키를 추가한다.

vi ~/.ssh/authorized_keys

 

 

Pipeline 구성 및 스크립트 작성

 

[ 주 의 사 항 ]

젠킨스는 자바 17을 베이스로 지원하기 때문에 Gradle 프로젝트의 자바 버전이 21인 경우 아래 링크를 참고하시길 바랍니다.

https://velog.io/@habins226/Jenkins-JDK-21-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0

 

Jenkins JDK 21 빌드하기!

Jenkins에서 JDK 21 빌드가 안된다고...? 난 되는데? ㅋ

velog.io

 

1. 상황에 맞게 Pipeline 전역 설정을 한다.

  • Do not allow the pipeline to resume if the controller restars: 파이프라인 충돌 시 재시작을 방지한다.
  • GitHub Project: GitHub 프로젝트 주소를 연결한다.
  • GitHub hook trigger for GITScm polling: GitHub Webhook를 연결한다.

 

2. 프로젝트 환경에 맞게 파이프라인을 작성하자.

  • 아래는 각 단계별 Stage에 대한 설명을 정리한다.

Checkout

Git 저장소에서 main 브랜치의 소스를 가져온다.

 

Production Environment Setup & Test Environment Setup

Prod와 Test 환경 각각에 대해, 저장된 자격 증명을 사용하여 환경 설정 파일(env.properties)을 해당 경로에 복사한다.

 

Build Phase - Build Source

Gradle을 사용해 소스 코드를 빌드하되, 테스트 단계는 제외한다.

 

Docker Image
Docker Hub 자격 증명을 사용하여 로그인한 후, 애플리케이션 이미지를 빌드하고 최신 태그로 푸시한다.

 

Deploy to EC2 via SSH

SSH를 통해 EC2 서버에 접속하여 기존 컨테이너를 내려놓고,

새로운 컨테이너를 실행하며, 불필요한 리소스를 정리한다.

 

[ Pipeline Script ]

pipeline {
    agent any

    tools {
        jdk ("JDK 21")
    }

    environment {
        IMAGE_NAME = 'toastit/v1'
    }
    
    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/ToastitProject/toastit_v1.git'
            }
        }
        
        stage('Environment Setup') {
            steps {
                withCredentials([file(credentialsId: 'prod-env-file', variable: 'dbConfigFile')]) {
                    sh '''
                        cp $dbConfigFile ./src/main/resources/properties
                        cat $dbConfigFile
                    '''
                }
                withCredentials([file(credentialsId: 'test-env', variable: 'dbConfigFile')]) {
                    sh '''
                        cp $dbConfigFile ./src/test/resources/properties
                        cat $dbConfigFile
                    '''
                }
            }
        }
        
        stage('Build Phase - Build Source') {
            steps {
                sh './gradlew build -x test'
            }
        }
        
        stage('Docker Image') {
            steps {
                withCredentials([usernamePassword(credentialsId: 'dockerhub', passwordVariable: 'DOCKERHUB_PSW', usernameVariable: 'DOCKERHUB_USR')]) {
                    sh '''
                        echo $DOCKERHUB_PSW | docker login -u $DOCKERHUB_USR --password-stdin && \
                        docker build -t ${IMAGE_NAME}:latest . && \
                        docker push ${IMAGE_NAME}:latest
                    '''
                }
            }
        }
        
        stage('Deploy to EC2 via SSH') {
            steps {
                sshPublisher(
                    publishers: [
                        sshPublisherDesc(
                            configName: 'remote server',
                            transfers: [
                                sshTransfer(
                                    execCommand: """
                                        docker-compose down || true
                                        docker-compose up -d
                                        docker system prune -a -f
                                    """
                                )
                            ]
                        )
                    ]
                )
            }
        }
    }
}

블로그의 정보

코드의 여백

rowing0328

활동하기