본문 바로가기
CI·CD

[CI/CD] 도커 기반 어플리케이션 CI/CD 구성하기

by okms1017 2024. 12. 12.
728x90

✍ Posted by Immersive Builder  Seong

 

 

도커 기반 어플리케이션 CI/CD 구성하기 

이번 실습은 아래와 같이 CI/CD 워크플로우를 구성하는 것이 목표입니다. 개발자가 깃 저장소에 새로운 코드를 푸시할 때 웹훅을 트리거하여 CI/CD 서버를 동작시키고, CI/CD 서버는 소스코드를 가져와서 컨테이너 이미지를 빌드하여 이미지 저장소에 푸시합니다. 이후 이미지 저장소로부터 컨테이너 이미지를 가져와서 신규 버전을 배포하게 됩니다.  

 

CI/CD Workflow

 

Gogs Webhooks 설정

우선 Gogs 저장소에 이벤트가 발생했을 때 웹훅을 트리거하도록 설정합니다. Gogs의 dev-app 저장소 설정으로 들어가 'Webhooks' 탭을 클릭하고 추가할 웹훅으로 Gogs를 선택합니다. 그리고 이벤트에 의해 트리거된 요청을 전달하기 위한 대상 호스트 URL을 지정합니다. 여기서는 Jenkins 컨테이너의 URL 주소(http://192.168.35.251:8080/gogs-webhook/?job=SCM-Pipeline/)를 입력합니다. Gogs에서 웹훅을 추가할 때는 URL 주소 끝에 jog 또는 item 명칭을 반드시 명시해주어야 합니다. 

 

 

Gogs와 Jenkins 컨테이너가 동일한 로컬 네트워크에서 실행되고 있다면 아래와 같이 오류가 발생합니다. Gogs는 기본적으로 로컬 네트워크 주소로 보내는 요청을 차단합니다. 따라서 Gogs 설정 파일에 로컬 네트워크 요청을 허용하는 옵션을 추가(LOCAL_NETWORK_ALLOWLIST=192.168.35.251)한 후 재기동을 한 번 해주어야 합니다. 

 

 

Gogs 컨테이너가 재기동하고 나면 웹훅이 정상적으로 추가됩니다. 

 

 


SCM 파이프라인 생성하기 

Gogs의 웹훅을 받을 수 있도록 Pipeline 유형의 item(SCM-Pipeline)을 하나 생성합니다. 

 

 

'GitHub project' 옵션을 체크하고 Project url 항목에 Gogs의 dev-app 저장소 URL(http://192.168.35.251:3000/devops/dev-app)을 입력합니다. 그리고 Gogs의 dev-app 저장소로부터 트리거된 웹훅만 받을 수 있도록 서명값을 입력합니다. 서명은 Gogs 웹훅에 설정한 Secret 값과 동일해야 합니다. 

 

 

'Build Triggers' 탭으로 이동하여 Build when a change is pushed to Gogs 옵션을 체크합니다. 말그대로 Gogs 저장소에서 변경 사항에 대한 푸시가 발생하였을 때 빌드를 실행한다는 의미입니다. 

 

 

'Pipeline' 탭으로 이동하여 Pipeline script from SCM 옵션을 선택합니다. SCM 항목은 Git으로 선택하고 마찬가지로 Gogs의 dev-app 저장소의 URL(http://192.168.35.251:3000/devops/dev-app)과 자격 증명(devops/******)을 입력합니다. 그리고 브랜치는 */main로 변경하고, Script Path는 Jenkinsfile로 설정합니다. 즉, 자격 증명을 통해 소스 저장소로부터 최신 소스코드를 가져오고 메인 브랜치 하위의 Jenkinsfile을 찾아 빌드를 실행한다는 의미입니다. 이렇게 하면 Jenkins 컨테이너에 별도로 접속하여 조작할 필요 없이, 오로지 소스 저장소에서만 파이프라인을 작성하거나 수정함으로써 깃옵스 방식으로 자동화 빌드를 구성할 수 있습니다. 

 

 

이전 포스팅에서 작성한 CI 파이프라인 스크립트를 Jenkinsfile로 생성합니다. 

VERSION 파일의 버전 정보를 0.0.2로 업데이트합니다. 

 

 

그리고 커밋/푸시 명령어를 실행하여 파이프라인이 자동으로 빌드되는지 확인합니다. 

 

 

Jenkins에서 SCM-Pipeline이 자동으로 빌드됩니다. 또한, dev-app 저장소에 신규 컨테이너 이미지(okms1017/dev-app:0.0.2)가 업로드된 것을 확인할 수 있습니다. Gogs 웹훅에서는 Recent Deliveries로 로그를 확인할 수 있습니다. 

 

Jenkins : SCM-Pipeline
Docker Hub : okms1017/dev-app:0.0.2
Gogs Webhook : Recent Deliveries

 


CI/CD 파이프라인 구성하기

이제 빌드한 컨테이너 이미지를 자동으로 배포해보겠습니다. SCM-Pipeline은 그대로 사용하되, Jenkinsfile을 일부 수정하여 CD 파이프라인을 구성합니다. 기존 Jenkinsfile CI 파이프라인 스크립트에 latest 태깅하는 구문과 컨테이너를 실행하는 구문을 추가합니다.

 

  • appImage.push("latest") : latest 버전을 추가로 업로드합니다. 즉, 현재 버전과 latest 버전 두 개의 이미지를 업로드합니다. 
  • sh "docker stop ${CONTAINER_NAME}" : 실행 중인 컨테이너가 있다면 중지합니다.
  • sh "docker rm ${CONTAINER_NAME}" : 중지한 컨테이너를 삭제합니다. 
  • docker run -d --name ${CONTAINER_NAME} -p ${HOST_PORT}:80 ${DOCKER_IMAGE}:${DOCKER_TAG} : 신규 컨테이너를 실행합니다. 

 

pipeline {
    agent any
    environment {
        DOCKER_IMAGE = 'okms1017/dev-app' 
        CONTAINER_NAME = 'dev-app'
        HOST_PORT = '8081'
    }
    stages {
        stage('Checkout') {
            steps {
                 git branch: 'main',
                 url: 'http://192.168.35.251:3000/devops/dev-app.git', 
                 credentialsId: 'gogs-dev-app' 
            }
        }
        stage('Read VERSION') {
            steps {
                script {
                    def version = readFile('VERSION').trim()
                    echo "Version found: ${version}"
                    env.DOCKER_TAG = version
                }
            }
        }
        stage('Docker Build and Push') {
            steps {
                script {
                    docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-credentials') {
                        def appImage = docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
                        appImage.push()
                        appImage.push("latest")
                    }
                }
            }
        }
        stage('Check, Stop and Run Docker Container') {
            steps {
                script {
                    def isRunning = sh(
                        script: "docker ps -q -f name=${CONTAINER_NAME}",
                        returnStdout: true
                    ).trim()
                    
                    if (isRunning) {
                        echo "Container '${CONTAINER_NAME}' is already running. Stopping it..."
                        sh "docker stop ${CONTAINER_NAME}"
                        sh "docker rm ${CONTAINER_NAME}"
                        echo "Container '${CONTAINER_NAME}' stopped and removed."
                    } else {
                        echo "Container '${CONTAINER_NAME}' is not running."
                    }
                    
                    echo "Waiting for 5 seconds before starting the new container..."
                    sleep(5)
                    
                    echo "Starting a new container '${CONTAINER_NAME}'..."
                    sh """
                    docker run -d --name ${CONTAINER_NAME} -p ${HOST_PORT}:80 ${DOCKER_IMAGE}:${DOCKER_TAG}
                    """
                }
            }
        }
    }
    post {
        success {
            echo "Docker image ${DOCKER_IMAGE}:${DOCKER_TAG} has been built and pushed successfully!"
        }
        failure {
            echo "Pipeline failed. Please check the logs."
        }
    }
}

 

위와 같이 Jenkinsfile 수정 작업이 완료되면 0.0.3 버전으로 업데이트하여 커밋/푸시합니다. 그리고 자동으로 배포되는지 확인합니다. 

 

 

로컬에 컨테이너 이미지가 생성되고 이후 컨테이너가 실행되는 것을 확인할 수 있습니다. 

 

 

8081 포트로 실행되고 있는 dev-app 컨테이너에 접속이 잘 되고 있습니다. 

 

 

Docker Hub의 프라이빗 저장소에도 0.0.3 버전과 latest 버전 두 개의 컨테이너 이미지가 업로드된 것을 확인할 수 있습니다. 

 

 

이번에는 server.py 코드 일부를 수정하고 0.0.4 버전으로 재배포한 후 다운타임이 발생하는지 확인합니다. 

 

 

기존 실행 중인 컨테이너가 중지 및 삭제되고 신규 컨테이너가 실행되기까지 약 10초 가량의 다운타임이 발생합니다. 따라서 서비스 다운타임을 완화하거나 없애기 위한 방법으로 블루/그린 배포, 카나리 배포 등 다양한 배포 전략을 고려해 볼 수 있습니다. 이러한 배포 전략을 최대한 편리하게 도와주는 플랫폼이 바로 쿠버네티스입니다. 

 

 


[출처]

1) CloudNet@, CI/CD Study

 

728x90