IT/PaaS

쿠버네티스 시작하기(9) - CI/CD 파이프라인 만들기(3/3) - Gitlab webhook 설정 및 Slack 연동

twofootdog 2020. 1. 11. 02:45

이전 글에서 쿠버네티스 클러스터에 올라간 Jenkins와 Gitlab간의 연동을 시킨 후 컨테이너 배포까지 수행해 보았다.

이번 글에서는 Gitlab으로 로컬PC의 소스 Push/Merge 시 자동으로 Jenkins에서 빌드/배포가 수행되는 프로세스를 만들어 볼 것이고, 빌드/배포가 완료된 후에는 Slack을 통해 알람 메시지를 전송할 것이다.

 

1. 시작하기 전에

현재까지 완성된 빌드/배포 프로세스를 보면 아래와 같다. 

  1. 개발/운영자가 로컬PC에서 소스 변경 후 git push/merge를 통해 Git Repository(gitlab/github ...)로 소스 업로드 -> 완료
  2. 소스가 업로드되면 Git Repository에서 Jenkins로 Webhook 전송 ->미완료
  3. jenkins에서 Webhook을 통해 소스 변경 사실을 인지한 후, 빌드툴(maven/gradle/npm ...)을 활용하여 소스 빌드 수행 -> 미완료
  4. 소스 빌드 완료 후 Docker Image로 빌드 -> 완료
  5. 빌드된 Docker Image를 Docker Registry(Docker hub, Habor ...)로 전송 -> 완료
  6. kubectl create/apply를 통해 업로드된 image를 down 후 pod container로 생성/배포 -> 완료
  7. 배포 성공/실패 후 Slack을 통한 알람 전송 -> 미완료

그럼 우선 Git Repository(Gitlab)에서 Jenkins로 Webhook을 전송하는 것에 대해서 공부해보도록 하자.

 

 

2. Gitlab Webhook 등록

Gitlab Webhook은 Gitlab Repository의 소스 변경 시 해당 작업이 수행되었다는 것을 HTTP POST로 알리는 작업을 말한다. Gitlab Webhook을 등록하는 방법은 아래와 같다.

 

2-1. Jenkins에서 Gitlab Hook Plugin 설치

해당 작업은 쿠버네티스 클러스터에 Jenkins 설치 시 진행을 했었다. 때문에 이전 장에서 진행한대로 수행했다면 생략해도 된다(https://twofootdog.tistory.com/11).

 

2-2. Jenkins Wehook 설정

Jenkins 웹페이지로 이동하여 설정을 진행할 Jenkins pipeline 선택 -> 구성 -> Build Triggers로 가서 Build when a change is pushed to GitLab를 선택한다.

 

Allowed branches의 include에 원하는 git branch 값을 입력하고(필자는 master branch입력), Secret token 우측에 Generate 버튼을 클릭해서 token을 발급받는다. 해당 Token은 Gitlab에 webhook 등록 시 사용할 것이다.

 

2-3. Gitlab Webhook 설정

이제 Gitlab webhook 설정을 해보자. gitlab 사이트(https://gitlab.com/)로 가서 로그인 한 후 Jenkins와 연동된 프로젝트를 선택한다. 

그리고 좌측에 Settings -> Integration 을 선택한다.

URL에는 2-2 Jenkins 설정 에서 Build when a change is pushed to GitLab 체크 시 옆에 있던 URL을 입력하고 Secret Token은 Jenkins 설정에서 생성한 Secret Token 값을 입력한다.

 

다음에 밑에 있는 Add webhook 버튼을 눌러 Webhook을 등록하고 Test-> Push events를 클릭한다.

 

그러면 Jenkins에서 해당 Push event를 받고 정상적으로 배포가 진행되는 것을 확인할 수 있다.

 

3. Slack 알람 등록

이제 Webhook으로 Jenkins 자동 빌드/배포 프로세스를 완성했으니, 빌드/배포가 수행된 후 Slack을 통해 알람메시지가 전송되는 프로세스를 만들어보자. 

 

3-1. Slack 신규 Workspace 생성

Slack(https://slack.com/)에 가서 GET STARTED를 선택한다.

 

본인의 이메일을 입력하고 Continue 버튼을 클릭한다.

 

이메일로 가서 Confirm Email Address 버튼을 클릭한다.

 

Get Started 버튼을 클릭해서 새로운 Workspace를 생성해준다. 

 

팀 이름 & 프로젝트 명 & 팀원의 이메일을 입력한다.

 

생성한 Workspace로 들어가면 해당 Workspace의 URL를 확인할 수 있다. 해당 URL은 추후 Workspace 로그인 할 때 사용하게 되며, 바로 아래 단계에서 Slack Token을 생성 할 때에도 활용하게 된다.

 

3-2. Slack Token 생성

이제 Slack Token을 생성할 차례다. Workspace 생성 시 제공받은 URL/apps로 들어가서 Jenkins CI를 검색한다(이 포스트에서는 http://testteam-49b6088.slack.com/apps 이다).

 

좌측 상단의 Install 혹은 Add to Slack 버튼을 클릭한다.

 

채널을 선택한다. 필자는 채널명을 auth라고 지었으므로 # auth라고 입력하고 Add Jenkins CI integration 버튼을 클릭한다. 해당 채널명은 조금 전 Slack Workspace를 만들 때 입력했던 프로젝트명과 동일하다(만약 해당 Workspace에 여러개의 채널이 존재한다면, 그 중 한개의 채널명만 입력하고 Jenkinsfile에서 여러개의 채널 중 알람 전송할 채널명을 작성하면 해당 채널로 알람 전송이 가능하다).

 

그러면 아래와 같은 Setup Instructions가 나오는데, 설명대로 진행을 하면 된다. Post ChannelToken은 Jenkins에서 입력을 해야 하므로 기억해두도록 하자. 그리고 Save Settings 를 클릭한다.

 

우선 STEP2에 나온대로 Jenkins 웹페이지로 접속 -> Jenkins관리 -> Plugin 관리에서 Slack Notification을 설치해준다.

 

 

다음 STEP3에 나온대로 Jenkins 관리 -> 시스템설정 -> Slack 으로 가서 아래와 같이 입력해준다. Credential 에는 Secret Text를 선택한 후 위 Slack 설정 시 얻은 Token 값을 입력하면 된다. Default channel/member id에는 Slack 알람을 날릴 채널명을 입력하면 된다. Slack 설정 시 Post to Channel에서 입력했던 채널명을 입력하자. 셋팅이 완료되었으면 Test Connection 버튼을 눌러준다. Success 메시지가 뜨면서 Slack으로도 알람이 온다면 설정이 완료된 것이다.

 

4. Jenkinsfile 변경

위 설정을 통해 Slack 환경설정까지 완료되었다. 그러면 로컬PC 소스에서 git push 를 하게 되면 Slack을 통해 알람이 올까? 그렇지 않다. 왜냐하면 Jenkinsfile을 수정해주지 않았기 때문이다. 위와 같이 Slack 환경설정을 한 후 변경/배포 프로젝트의 소스 내 Jenkinsfile에 Slack 함수를 호출해서 어느 시점에 Slack알람이 올지를 지정해줘야 한다.

때문에 아래와 같이 Jenkinsfile을 수정해보자.

Jenkinsfile : 

/* pipeline 변수 설정 */
def DOCKER_IMAGE_NAME = "twofootdog/project-repo"           // 생성하는 Docker image 이름
def DOCKER_IMAGE_TAGS = "batch-visualizer-auth"  // 생성하는 Docker image 태그
def DOCKER_CONTAINER_NAME = "auth-container"    // 생성하는 Docker Container 이름
def NAMESPACE = "ns-project"
def SLACK_CHANNEL = "auth"
def DATE = new Date();

/* Slack 시작 알람 함수 */
def notifyStarted(slack_channel) {
    slackSend (channel: "${slack_channel}", color: '#FFFF00', message: "STARTED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
/* Slack 성공 알람 함수 */
def notifySuccessful(slack_channel) {
    slackSend (channel: "${slack_channel}", color: '#00FF00', message: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
/* Slack 실패 알람 함수 */
def notifyFailed(slack_channel) {
  slackSend (channel: "${slack_channel}", color: '#FF0000', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
  
podTemplate(label: 'builder',
            containers: [
                containerTemplate(name: 'gradle', image: 'gradle:5.6-jdk8', command: 'cat', ttyEnabled: true),
                containerTemplate(name: 'docker', image: 'docker', command: 'cat', ttyEnabled: true),
                containerTemplate(name: 'kubectl', image: 'lachlanevenson/k8s-kubectl:v1.15.3', command: 'cat', ttyEnabled: true)
            ],
            volumes: [
                hostPathVolume(mountPath: '/home/gradle/.gradle', hostPath: '/home/admin/k8s/jenkins/.gradle'),
                hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
                //hostPathVolume(mountPath: '/usr/bin/docker', hostPath: '/usr/bin/docker')
            ]) {
    node('builder') {
        try {
            stage('Start') {
                /* Slack 메시지 전송 */
                notifyStarted(SLACK_CHANNEL)
            }
            stage('Checkout') {
                 checkout scm   // gitlab으로부터 소스 다운
            }
            stage('Build') {
                container('gradle') {
                    /* 도커 이미지를 활용하여 gradle 빌드를 수행하여 ./build/libs에 jar파일 생성 */
                    sh "gradle -x test build"
                }
            }
            stage('Docker build') {
                container('docker') {
                    withCredentials([usernamePassword(
                        credentialsId: 'docker_hub_auth',
                        usernameVariable: 'USERNAME',
                        passwordVariable: 'PASSWORD')]) {
                            /* ./build/libs 생성된 jar파일을 도커파일을 활용하여 도커 빌드를 수행한다 */
                            sh "docker build -t ${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAGS} ."
                            sh "docker login -u ${USERNAME} -p ${PASSWORD}"
                            sh "docker push ${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAGS}"
                    }
                }
            }
            stage('Run kubectl') {
                container('kubectl') {
                    withCredentials([usernamePassword(
                        credentialsId: 'docker_hub_auth',
                        usernameVariable: 'USERNAME',
                        passwordVariable: 'PASSWORD')]) {
                            /* namespace 존재여부 확인. 미존재시 namespace 생성 */
                            sh "kubectl get ns ${NAMESPACE}|| kubectl create ns ${NAMESPACE}"
                            
                            /* secret 존재여부 확인. 미존재시 secret 생성성 */
                            sh """
                                kubectl get secret my-secret -n ${NAMESPACE} || \
                                kubectl create secret docker-registry my-secret \
                                --docker-server=https://index.docker.io/v1/ \
                                --docker-username=${USERNAME} \
                                --docker-password=${PASSWORD} \
                                --docker-email=ekfrl2815@gmail.com \
                                -n ${NAMESPACE}
                            """
                            /* k8s-deployment.yaml 의 env값을 수정해준다(DATE로). 배포시 수정을 해주지 않으면 변경된 내용이 정상 배포되지 않는다. */
                            sh "echo ${DATE}"
                            sh "sed -i.bak 's#DATE_STRING#${DATE}#' ./k8s/k8s-deployment.yaml"

                            /* yaml파일로 배포를 수행한다 */
                            sh "kubectl apply -f ./k8s/k8s-deployment.yaml -n ${NAMESPACE}"
                            sh "kubectl apply -f ./k8s/k8s-service.yaml -n ${NAMESPACE}"

                    }
                }
            }
            notifySuccessful(SLACK_CHANNEL)
        } catch(e) {
        /* 배포 실패 시 */
            currentBuild.result = "FAILED"
            notifyFailed(SLACK_CHANNEL)
        }
    }
}

 

위 Jenkinsfile을 보면 Slack 시작을 알리는 함수(notifyStarted), 배포 성공을 알리는 함수(notifySuccessful), 배포 실패를 알리는 함수(notifyFailed)를 각각 선언하고, 필요한 시점에 해당 함수를 호출하였다. 

 

5. 변경/배포 테스트

자 이제 변경/배포 파이프라인이 완성되었다. 그러면 로컬PC에서 소스를 변경 한 후 git push를 해보자.

Jenkins log를 확인해보면 아래와 같이 배포가 정상적으로 완료되는 것을 확인할 수 있다.

 

또한 Slack을 확인해보면 알람까지 온 것을 확인할 수 있다. 성공이다!!!!!!!!!!

 

마치며

지금까지 3개의 포스트에 걸쳐서 쿠버네티스 클러스터 & Jenkins & Gitlab & Slack을 활용하여 CI/CD 파이프라인을 구축해보았다. 사실 이번 파이프라인에서는 Git Branch도 나누지 않았고(오직 master로 push), 개발기와 운영기를 분리해서 배포하지도 않았고, CI/CD가 완벽하게 구현된 것은 아니다(JUnit 테스트도 수행되지 않았다). 하지만 쿠버네티스 클러스터 환경에 대한 이해 및 CI/CD 파이프라인 구축의 기본기는 다질 수 있을 거라 확신하며, On-Premise 환경에서의 CI/CD 파이프라인을 구축하는데 조금은 도움이 될 거라 확신한다(필자도 이 파이프라인을 구축해보면서 정말 많은 공부가 되었다). 

다음장부터는 이 파이프라인에 살을 붙이는 내용을 포스팅할 것이다.

  1. 우선 Sonarqube & Jacoco & JUnit TEST를 붙여서 CI/CD 파이프라인에 TEST Step을 넣는 방법에 대해 포스팅할 것이다.
  2. 그 다음으로 NodeExporter & Prometheus & AlertManager를 붙여서 쿠버네티스 클러스터 & 서버CPU/Memory/저장소 & SpringBoot API 모니터링 프로세스를 구축하는 내용도 포스팅할 것이다.
  3. 그 다음엔 Grafana를 붙여서 해당 모니터링 프로세스를 시각화 하는 내용도 포스팅할 것이다.

수고하셨습니다.

 

참고(내 예전 블로그임)

https://twofootdog.github.io/Docker-Docker%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EB%A1%9C-jenkins-%EC%8B%A4%ED%96%89-%ED%9B%84-%EC%86%8C%EC%8A%A4-push-%EC%8B%9C-%EC%9E%90%EB%8F%99-%EB%B9%8C%EB%93%9C%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0(Springboot)/

 

[Docker]Docker컨테이너로 jenkins 실행 후 gitlab으로 spring boot 소스 push 시 Docker Container로 자동 빌드/배포 | 두발로걷는개

두발로걷는개의 Blog

twofootdog.github.io