Computer Science/Setup

[Jenkins, Docker] Jenkins CI/CD Docker-out-of-docker 방식으로 구현하기

_혀니 2025. 4. 27. 22:05
728x90
반응형

저번 프로젝트 때 Jenkins pipeline을 쉘 접속을 활용했던 게 마음에 안 들어서
이번 프로젝트에서는 꼭 DooD를 구현하겠다 마음먹고 다시 구현해봤다.
 
다른 방식으로 구현하면서 깨달은 점이 있다면
 

1. .env파일은 프로덕션 환경에서 만들거나 관리할 필요가 없다.

전부 jenkins credential에 만들어두고 compose up 할 때 환경 변수를 withcredentials 이용하여 주입해주기만 하면 되는 것이었다...!
이 사실을 알고 나서 빌드 프로세스의 이해가 쉬워지고, 구현이 간편해졌다.
- 그럼 jenkins credential이 무엇이냐?: 환경 변수나 계정 정보와 같은 민감한 정보를 관리해주는 역할을 한다.
 

2. scm checkout하면 jenkins 아래에 jenkins_workspace가 생기고, 그곳에 소스코드가 clone된다.

호스트 ec2에 있는 소스코드와는 전혀 관계가 없다... 헐
그리고 파이프라인 끝에 post action으로 clean해주면 소스코드와 환경변수가 정리되어 ec2에는 코드가 남아있지 않게 된다. 보안상 정말 유용한 방식인듯
 

폴더 구조

/home/ubuntu 안에
~/opt (호스트의 소스코드)
~/jenkins
~/jenkins_workspace
~/Dockerfile
~/jenkins-docker-compose.yml
구조로 구성했다.
 
 

1. jenkins-docker-compose.yml

services:
  jenkins:
    build: 
      context: .
      dockerfile: Dockerfile
    container_name: jenkins
    environment:
      JENKINS_JAVA_OPTIONS: >-
        -Dhudson.model.DirectoryBrowserSupport.CSP="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;" -Dhttp.nonProxyHosts="lab.ssafy.com|updates.jenkins.io" -Dhttps.nonProxyHosts="lab.ssafy.com|updates.jenkins.io"
      NO_PROXY: "lab.ssafy.com,updates.jenkins.io"
      JENKINS_OPTS: "--prefix=/jenkins"
    ports:
      - "11027:8080"
    volumes:
      - ./jenkins:/var/jenkins_home
      - /var/jenkins_home/workspace:/var/jenkins_home/workspace
      - /home/ubuntu/opt/build:/app/build
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker
      - /usr/local/bin/docker-compose:/usr/local/bin/docker-compose:ro
    networks:
      - see_through

networks:
  see_through:
    external: true

 
1) 볼륨 마운트
- ./jenkins:/var/jenkins_home
- /var/jenkins_home/workspace:/var/jenkins_home/workspace
jenkins 관련 정적 파일들을 보관할 jenkins_home과
scm checkout할 시 생성하는 워크스페이스를 호스트와 공유하여 사라지지 않도록 한다.
 
- /home/ubuntu/opt/build:/app/build
jenkins workspace에서 빌드한 파일들을 호스트 os와 공유 (그래야 nginx에서 받아 쓸 수 있음)
 
- /var/run/docker.sock:/var/run/docker.sock
- /usr/bin/docker:/usr/bin/docker
- /usr/local/bin/docker-compose:/usr/local/bin/docker-compose:ro
docker와 docker-compose를 jenkins와 마운트한다.
마운트하지 않으면 ec2 호스트에 설치된 docker에 접근할 수 없어 DooD 구현이 불가능하다.
 
2) 네트워크 설정
networks:
      - mynetwork
jenkins 컨테이너의 네트워크를 설정한다 (이름은 자유)
 
networks:
  mynetwork:
    external: true
전역 네트워크 설정으로, external을 허용해주어야 호스트에서의 mynetwork 네트워크가 jenkins 컨테이너에서도 접근이 가능하다.
 
3) environment 설정
- HTTPS 연결을 적용하면서 필요한 환경변수를 몇 개 설정했다.
일단 Git 연결은 프록시를 타면 안 돼서 no_proxy에 추가하고, 
CSP 헤더 설정의 경우 일부 CSS/JS 리소스가 https 사용 시 보안 정책으로 제대로 로드되지 않아
해당 부분을 허용해주었다.
 

2. Dockerfile

FROM jenkins/jenkins:lts
USER root
RUN groupadd -g 999 docker || true && \
    usermod -aG docker jenkins
USER jenkins

 
유저 jenkins를 docker group에 추가하는 과정이다.
이 과정이 없으면 호스트의 docker에 접근하기 불가능해져서 추가로 설정했다.
 

3. Jenkinsfile

Jenkins는 파이프라인 방식으로 만들었다.
credential은 global에 전부 추가했고...
git 연결 후 (만약 이 연결 부분이 뜨지 않으면 plugins에서 Git관련 플러그인을 설치 후 재시작하면 된다) 작성한다.

pipeline {
    agent any
    stages {
        stage('Checkout') {
            // steps {
            //     checkout scm
            // }
            steps {
                git branch: 'dev', credentialsId: 'gitlab-access-token', url: 'https://lab.ssafy.com/s12-s-project/S12P21S002.git'
            }
        }
        stage('Deploy') {
            steps {
                withCredentials([
                    string(credentialsId: 'DB_URL', variable: 'DB_URL'),
                    string(credentialsId: 'DB_USERNAME', variable: 'DB_USERNAME'),
                    string(credentialsId: 'DB_PASSWORD', variable: 'DB_PASSWORD'),
                    string(credentialsId: 'LLM_URL', variable: 'LLM_URL'),
                    string(credentialsId: 'NICKNAME_URL', variable: 'NICKNAME_URL'),
                    string(credentialsId: 'SWAGGER_USERNAME', variable: 'SWAGGER_USERNAME'),
                    string(credentialsId: 'SWAGGER_PASSWORD', variable: 'SWAGGER_PASSWORD'),
                    string(credentialsId: 'ALARM_UUID', variable: 'ALARM_UUID'),
                    string(credentialsId: 'VITE_API_SERVER_URL', variable: 'VITE_API_SERVER_URL'),
                    string(credentialsId: 'VITE_LOCAL_SERVER_URL', variable: 'VITE_LOCAL_SERVER_URL'),
                    string(credentialsId: 'VITE_WS_LOCAL_SERVER_URL', variable: 'VITE_WS_LOCAL_SERVER_URL'),
                    string(credentialsId: 'VITE_TYPECAST_API_KEY', variable: 'VITE_TYPECAST_API_KEY'),
                    string(credentialsId: 'OPENAI_API_KEY', variable: 'OPENAI_API_KEY'),
                    string(credentialsId: 'LLM_DB_URL', variable: 'LLM_DB_URL'),
                    string(credentialsId: 'DB_NAME', variable: 'DB_NAME'),
                    string(credentialsId: 'TYPECAST_API_KEY', variable: 'TYPECAST_API_KEY'),
                    string(credentialsId: 'TYPECAST_BASE_URL', variable: 'TYPECAST_BASE_URL'),
                    file(credentialsId: 'FCM_TOKEN', variable: 'FCM_TOKEN')
                ]) {
                    sh '''
                        mkdir -p api/src/main/resources/firebase
                        cp $FCM_TOKEN api/src/main/resources/firebase/seethrough-cf145-firebase-adminsdk-fbsvc-05904b7ab9.json
                    '''
                    sh "docker-compose down api client llm --volumes"
                    
                    sh "docker-compose up -d api client"
                    sh "docker-compose up -d --build llm"
                    
                    sleep time: 90, unit: 'SECONDS'
                }
            }
        }
    }
    post {
        always {
            cleanWs()
        }
    }
}

checkout

- scm 해도 되는데 나는... 그냥 직접 연결했다.
 

deploy

- withcredentials로 환경변수를 전부 불러온다. 그리고 이걸 docker-compose를 이용하여 주입시킨다.
- compose는 아래에..
- 이후 쉘에서 필요한 컨테이너를 compose를 이용하여 재실행한다. 
  - 참고로 ec2에서 실행한 컨테이너와 jenkins가 실행한 컨테이너는 같은 이름을 가져도 다른 컨테이너 취급을 하므로 만약 중복 오류가 발생하면 호스트에서 해당 컨테이너를 docker remove를 이용하여 삭제해주면 된다(예: docker remove client)
- sleep울 90초 설정해뒀는데 그 이유가.. 컨테이너를 -d 옵션 써서 백그라운드로 실행하니까 얘가 소스 다 읽어서 빌드하기도 전에 post action을 실행하며 워크스페이스를 정리해버려서 설정해뒀다. 다른 좋은 방법 없나...
 

post

- workspace를 정리하는 cleanWs()를 항상(always) 실행한다.


완성

이 정도 하면 이제 jenkins가 컨테이너를 새로 올려준다. 만약 호스트에서 올린 컨테이너가 있다면 내려줘야 중복을 피할 수 있다. 호스트와 젠킨스는 다른 유저로 분리돼서 서로 다른 놈이 올린 컨테이너는 내려주지 못하는듯.

728x90
반응형