티스토리 뷰

코드를 작성하고 GitHub에 Push한 경험이 있으신가요?

방금 Push한 코드를 서버에 반영하려면 어떻게 해야될까요?

매번 GitHub에서 새 버전의 코드를 다운받고, Jar를 빌드해서 서버에 옮겨주고, 서버를 재시작하는 일련의 과정을 반복하겠죠.

이런 과정이 자동화된다면 얼마나 편할까요?

이번 글에서는 배포를 자동화해주는 CD(Continous Delivery) 파이프라인 구축 과정을 살펴보겠습니다.

 

CI/CD 파이프라인 구조. 빨간색 영역은 CD에 해당하는 영역

 

글을 시작하기 전에 먼저 CD 파이프라인이 어떻게 동작하는지 워크플로우를 살펴보겠습니다.

 

 

CD 파이프라인 워크플로우

  1. GitHub 에서 관리하는 레포지토리의 master 브랜치에 코드가 push되면, Jenkins에게 wehook을 날린다.
  2. Jenkins가 빌드, 도커 이미지를 생성한다.
  3. 생성된 도커 이미지를 Docker Registry에 등록한다.
  4. 애플리케이션 서버가 도커 이미지를 받아 실행한다.

위 그림에서 확인한 워크플로우를 사람이 매번 수동으로 하는 것은 복잡합니다. GitHub webhook, Jenkins 를 활용하여 이 과정을 자동화하고자 합니다.

 

 

실습 환경

  • Mac Pro
  • Naver Cloud Platfrom
    • Jenkins Server - CentOS 7.3
    • Application Server - Ubuntu 18.04
    • NCP Container Registry

 

 

NAVER CLOUD Container Registry 이용 신청

네이버 클라우드 Container Registry는 Docker Image를 저장하고 관리하는 서버로 사용할 수 있습니다. 애플리케이션 이미지를 저장할 서버가 필요하므로 신청해줍니다.

자세한 사항은 https://guide.ncloud-docs.com/docs/containerregistry-start 에 소개되어 있습니다.

 

 

Jenkins 서버 ↔︎ 애플리케이션 서버 ssh 키 생성하기

Jenkins 서버는 애플리케이션 서버에게 워크플로우 상 4번 동작을 수행하도록 지시합니다. 그러기 위해서 Jenkins 서버와 애플리케이션 서버 간 ssh 통신이 가능하도록 설정이 필요합니다.

ssh-keygen -t rsa

1. Jenkins 서버 ssh 키 생성하기

터미널을 이용하여 Jenkins 서버에 원격 접속을 한 후, 아래의 ssh 키 생성 명령을 입력합니다.

Jenkins 서버에서 ssh 키 생성 후 ~/.ssh/ 디렉토리 파일 목록

 

2. Application 서버 ssh 키 생성하기

ssh-keygen -t rsa -f id_rsa
Application 서버에서 ssh 키 생성 후 ~/.ssh/ 디렉토리 파일 목록

 

3. Jenkins 서버에서 Application Server를 known_hosts로 등록하기

ssh-keyscan -H [Application 서버 ip] >> ~/.ssh/known_hosts

 

4. Application 서버의 authorized_keys에 Jenkins 서버의 공개키(id_rsa.pub) 입력하기

Jenkins 서버의 공개키(id_rsa.pub)
Application 서버의 vi ~/ssh/authorized_keys

 

 

Jenkins ↔︎ Application Server SSH 원격 접속 Credentials 등록

1. Jenkins 서버에서 cat ~/.ssh/id\_rsa 명령어를 입력하여 출력된 비밀키를 복사합니다. (BEGIN~ 부터 파일 끝까지)

 

2. [Jenkins 관리] - [Credentials] 페이지로 이동합니다.

[Jenkins 관리] - [Credentials] 페이지

 

3. [Add Credentials] 버튼을 눌러 Credentials 등록 페이지로 이동합니다.

4. 복사한 비밀키 값을 key 란에 입력합니다.

  • Kind 는 SSH Username with private key 를 선택합니다.
  • Username 에는 Application Server에서 사용되는 OS 계정명을 적습니다. (ex. root)
  • ID 에는 Jenkins 시스템 내에서 해당 Credentials를 구분할 ID 값을 적어줍니다. 여러분이 기억하기에 편한 값을 입력하면 됩니다.

 

 

Jenkins ↔︎ Docker Registry Credentials 등록

1. [네이버클라우드 포탈] - [마이페이지] - [인증키 관리] 페이지로 이동하여 신규 API 인증키를 생성합니다.

 

2. 발급받은 API 인증키를 앞선 과정과 똑같이 Jenkins 서버의 Credentials로 등록합니다.

  • Kind는 Username with password 를 선택합니다.
  • Username 에는 API 인증키의 Access Key ID 입력합니다.
  • Password 에는 API 인증키의 Secret Key 입력합니다.
  • ID에는 Jenkins 시스템 내에서 해당 Credentials를 구분할 ID 값을 적어줍니다. 여러분이 기억하기에 편한 값을 입력하면 됩니다.

 

 

Dockerfile 생성하기

도커는 이미지를 빌드할 때, DockerFile이라는 스크립트 파일 기반으로 동작합니다. Dockerfile에는 이미지의 구성(기반 이미지, 파일 등)을 기록하게 됩니다. 또한, 컨테이너(이미지)가 수행될 때 동작할 명령어 등을 정의하게 됩니다.

 

Dockerfile은 별도의 문법을 지켜 작성해야 합니다. 공식문서 https://docs.docker.com/engine/reference/builder/ 를 참고하길 바랍니다. 저의 경우 FROM 키워드 전에 ARG 키워드를 사용해서 COPY 동작이 예상과 다르게 동작했습니다...

 

제가 사용한 Dockerfile은 아래와 같습니다. 

# openjdk 17 이미지를 상속
FROM openjdk:17

# JAR 파일 위치
ARG JAR_FILE_PATH=build/libs/*.jar

# jar파일을 app.jar로 복사
COPY ${JAR_FILE_PATH} /app.jar

# 컨테이너가 최종 실행할 명령어 정의: jar 파일 실행
ENTRYPOINT ["java", "-jar", "/app.jar"]

 

작성한 Dockerfile은 프로젝트의 최상단에 넣어둡니다.

 

 

배포 작업 유발하기

GitHub은 GitHub에서 이벤트가 발생할 때, 특정 API가 호출되도록 하는 webhook 기능을 제공합니다. 이 기능을 이용하여 lol-judge 레포지토리(master 브랜치)에 코드가 Push 되면, Jenkins 서버의 API가 호출되도록 만들어보겠습니다. 또한, 해당 API가 [브랜치 다운]-[빌드]-[도커 이미지 생성]-[도커 이미지 등록] 일련의 과정을 수행하도록 만들어보겠습니다.

 

1. Generic Webhook Trigger 플러그인을 설치해줍니다.

Generic Webhook Trigger 플러그인 설치

 

2. 좌측 메뉴의 [새로운 Item] 버튼을 눌러, 배포 작업을 담당할 Item을 만들어줍니다.

 

3. Generic Webhook Trigger 체크를 해줍니다. Generic Webhook Trigger 플러그인을 통해 배포 작업이 유발됩니다.

 

4. Generic Webhook 설정을 해줍니다. 구체적으로 어떤 상황에서 배포 작업이 유발될지 설정해줍니다.

 

 

4번 과정은 구체적으로 어떤 webhook일 때, API가 호출될 것인지 조건을 정의하는 과정입니다.

만약에, dev 브랜치에 push 이벤트가 발생했고, Jenkins 서버로 webhook이 날라왔습니다.

본래 master 브랜치의 push 이벤트가 발생했을 때, 배포 작업을 수행하는 것이 목적이었습니다. 하지만 webhook 조건이 지정되어 있지 않은 경우, dev 브랜치의 배포 작업을 수행해버립니다. 이런 가능성 때문에 webhook 조건을 설정하는 것입니다.

 

4번 과정이 끝났다면, 다음과 같은 webhook만 인정됩니다.

# master 브랜치에 코드가 push되는 이벤트 발생시) GitHub은 다음과 같은 API를 호출함
http://jenkins-server-ip:18080/generic-webhook-trigger/invoke?token=merged-hook
  • token 값이 "merged-hook"
  • 브랜치가 master브랜치

 

5. GitHub webhook 을 등록합니다.

[GitHub 레포지토리] - [Settings] - [Webhooks] - [Add webhook]

 

Payload URL에 http://Jenkins서버IP:18080/generic-webhook-trigger/invoke?token=merged-hook 을 입력해줍니다.

 

Let me select individual events 체크박스를 선택하고 아래와 같이 설정해줍니다.

이렇게 설정을 마치면 GitHub 레포지토리에서 Pushes 이벤트가 발생할 때마다 Payload URL을 호출하게 됩니다.

 

6. 파이프라인 스크립트를 작성해줍니다. webhook 조건을 만족할 경우, 파이프라인 스크립트가 동작해서 배포 자동화 작업을 수행합니다.

pipeline {
    environment { 
        IMAGE_NAME = "lol-judge-app" // 이미지 이름
        REGISTRY_CONTAINER = "레지스트리 컨테이너 endpoint"
        REGISTRY_CREDENTIAL = "Jenkins ↔︎ Docker Registry Credentials 등록 과정에서 등록한 Credentials ID값"
        SSH_CONNECTION_IP = "계정명@Application서버IP"
        SSH_CONNECTION_CREDENTIAL = "Jenkins ↔︎ Application Server SSH 원격 접속 Credentials 등록 과정에서 등록한 Credentials ID값"
    }

    agent any

    stages {

        // GitHub master 브랜치 코드를 다운로드
        stage('GitHub Clone') {
            steps {
                git branch: 'master',
                credentialsId: 'CI/CD 파이프라인 구축(2) Jenkins ↔︎ GitHub Webhook 설정 3-3 과정에서 등록한 Credentials ID',
                url: 'GitHub 레포지토리 주소'
            }
        }

        // 빌드 및 Jar 생성
        stage('Clean Build Test') {
            steps {
                sh'''
                    echo build start
                    pwd
                    ./gradlew clean bootJar
                '''
            }
        }

        // 도커 이미지 빌드
        stage('Build Container Image') {
            steps {
                script {
                    image = docker.build("${REGISTRY_CONTAINER}/${IMAGE_NAME}")
                }
            }
        }

        // 생성된 도커 이미지를 네이버 클라우드 Docker Registry에 등록
        stage('Push Container Image') {
            steps {
                script {
                    docker.withRegistry("https://${REGISTRY_CONTAINER}", REGISTRY_CREDENTIAL) {
                        image.push("${env.BUILD_NUMBER}")
                        image.push("latest")
                        image
                    }
                }
            }
        }

        // 애플리케이션 서버가 도커 이미지를 다운받고 실행하도록 제어
        stage('Server run') {
            steps {
                sshagent(credentials: [SSH_CONNECTION_CREDENTIAL]) {
                    // 최신 컨테이너 삭제
                    sh "ssh -o StrictHostKeyChecking=no ${SSH_CONNECTION_IP} 'docker rm -f ${IMAGE_NAME}'"
                    // 최신 이미지 삭제
                    sh "ssh -o StrictHostKeyChecking=no ${SSH_CONNECTION_IP} 'docker rmi -f ${REGISTRY_CONTAINER}/${IMAGE_NAME}:latest'"
                    // 최신 이미지 PULL
                    sh "ssh -o StrictHostKeyChecking=no ${SSH_CONNECTION_IP} 'docker pull ${REGISTRY_CONTAINER}/${IMAGE_NAME}:latest'"
                    // 이미지 확인
                    sh "ssh -o StrictHostKeyChecking=no ${SSH_CONNECTION_IP} 'docker images'"
                    // 최신 이미지 RUN
                    sh "ssh -o StrictHostKeyChecking=no ${SSH_CONNECTION_IP} 'docker run -d --name ${IMAGE_NAME} -p 8080:8080 ${REGISTRY_CONTAINER}/${IMAGE_NAME}:latest'"
                    // 컨테이너 확인
                    sh "ssh -o StrictHostKeyChecking=no ${SSH_CONNECTION_IP} 'docker ps'"
                }
            }
        }
    }
}

 

 

master 브랜치 Push 및 자동 배포 확인하기

 

아주 간단한 SpringBoot 기반 애플리케이션을 작성하고 PR을 올립니다. 

 

PR을 merge하게 되면 Jenkins 서버에서 자동화 배포 작업이 수행됩니다.

 

서버 주소로 접속하면 Whitelabel 페이지가 출력되는 것을 확인할 수 있습니다!

 

참고 자료

 

다른 글을 보고 싶다면

댓글