IT/PaaS

쿠버네티스 시작하기(8) - CI/CD 파이프라인 만들기(2/3) - Gitlab & Jenkins & Docker hub 연동

twofootdog 2020. 1. 6. 19:16

이전 장에서 쿠버네티스를 활용하여 jenkins를 설치하고 환경설정을 진행해 보았다.

이번 장에서는 설치된 jenkins와 Git Repository를 연동하여 Git Repository에 있는 소스를 서버로 전송하여 빌드/배포를 진행하고, 빌드/배포가 완료되면 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을 통한 알람 전송

 

2. CI/CD 파이프라인 구성도

위에 정리된 빌드/배포 순서를 그림으로 표현하면 다음과 같다(이전 글에서도 공유했던 내용이다. 이전 글에서 설명한대로 Sonarqube와 JUnit 파이프라인 구축은 이번 포스트에서는 제외하고 다른 포스트에서 진행할 것이다).

 

3. 로컬PC & Git Repository 연동

CI/CD 파이프라인을 구성하기 위해서는 우선 로컬PC와 Gitlab간의 연동을 해야 한다. 그래야 내 PC에서 git push 명령어를 날리면 Git Repository로 소스가 업로드 될 것이고, 업로드 된 소스를 Jenkins에서 빌드 처리할 것이기 때문이다.

 

3-1. 로컬PC에 application 소스 생성

로컬PC에 SpringBoot와 Gradle을 이용하여 프로젝트를 생성하였다. 해당 프로젝트를 생성하는 내용은 이번 장에서 다룰 내용이 아니기 때문에 넘어간다. 

 

3-2. Git Repository 생성

필자는 Git Repository로 gitlab을 사용할 것이다. github를 사용하지 않고 gitlab을 사용한 이유는 private repository가 무료이기 때문에 팀내 스터디 프로젝트를 진행할 때는 gitlab을 사용하는 편이 더 좋았기 때문이다.

gitlab 주소는 https://gitlab.com/ 이다. 이곳에서 회원가입을 하고 New Project 버튼을 눌러 신규 프로젝트를 생성한다. 

3-3. 로컬PC 소스와 Gitlab 프로젝트 Clone

다음으로 로컬PC에 있는 프로젝트 소스를 신규 생성한 Gitlab 프로젝트와 clone 시킬 것이다. 그렇게하면 로컬PC와 Gitlab Repository가 연결되며, 연결된 후에는 로컬PC에서 소스 생성/수정한 뒤 git push/merge를 하게 될 경우 변경된 소스가 Gitlab Repository에 업데이트가 되면서 소스 형상관리가 가능해진다.

gitlab 프로젝트를 신규로 생성하게 되면 아래와 같은 Command line instructions가 나오게 되는데, 해당 설명에 맞춰서 Gitlab을 연동하면 된다. 만약 로컬에 소스가 존재한다면 소스가 존재하는 디렉토리로 가서 아래 항목 중 Push an existing folder에 나와있는 명령어를 입력하면 된다. 자세한 설명은 해당 장의 주제가 아니므로 SKIP한다. 

위 command 입력을 완료하여 연동을 완료하게 되면 내 로컬PC에 있는 소스가 gitlab repository로 올라간 것을 확인할 수 있다.

 

4. Jenkins와 Git Repository 연동

이제 로컬PC소스와 Gitlab Repository연동은 완료되었다. 다음으로 Gitlab Repository와 Jenkins간의 연동을 진행해보도록 하자.

 

4-1.  Jenkins 신규 파이프라인 생성

우선 Jenkins에서 신규 파이프라인을 만들어보자. 파이프라인은 프로젝트 소스 한개당 하나의 파이프라인을 만들 것이다. 

 

그다음 Pipeline 부분에서 DefinitionPipeline script from SCM을 선택하고, SCMGit을 선택한다. 그러면 Repositories 입력란이 생길 것이다. 해당 입력란에 Git Repository url을 입력하면 된다.

 

 

 

4-2. Jenkins 시스템 설정

우선 Gitlab User Settings로 가서 Access Tokens를 발급받는다.

 

다음으로 Jenkins관리 -> 시스템 설정에서 gitlab 관련 설정을 추가해준다. Connection name은 임의로 지정하고, Gitlab host URLhttps://gitlab.com으로 작성한다. 다음에 CredentialsAdd한 후 GitLab API Token을 선택하여 좀전에 gitlab에서 받은 Access Tokens을 입력한다. 모든 사항 입력 후 Test Connection을 클릭한다. Success 메시지가 나오면 연동이 일단 성공한 것이다.

 

4-3. Jenkins Pipeline 설정

다음으로 gitlab에서 생성한 프로젝트 화면으로 이동하여 우측 상단에 있는 Clone 버튼을 눌러서 Git Repository Https url을 복사한다. (https://gitlab.com/[id]/[project명].git)

 

그 다음 Jenkins에서 생성한 파이프라인 선택 -> 구성 -> Pipeline 설정으로 이동한다.

 

Pipeline에서 복사한 Gitlab Repository url을 Repository URL 항목에 붙여주고, Credentials에서 Add 버튼을 눌러서 Gitlab의 ID와 Password를 입력하고 입력한 내용을 사용한다.

 

5. Jenkinsfile & Dockerfile & Deployment/Service yaml 작성

지금까지 설정으로 아래와 같은 프로세스가 완성되었다.

그러면 로컬 PC에서 Git Repository로 git push/merge를 하게 되면 소스가 올라가게 될까? 그렇지 않다. 아직 네가지 작업을 수행하지 않았기 때문이다. 그 네가지 작업은 아래와 같다.

  1. 각 프로젝트 별로 Jenkinsfile이 필요하다. Jenkins pipeline이 실행되면, 각 소스에 존재하는 Jenkinsfile에 정의되어 있는 Step에 맞춰서 빌드/배포 프로세스가 진행되기 때문이다. 때문에 Springboot 소스를 빌드/배포하기 위해서는 각 프로젝트별로 Jenkinsfile이 필요하다.
  2. 각 프로젝트 별로 Dockerfile이 필요하다. 이번 장에서 우리가 만드는 빌드/배포 프로세스는 컨테이너 기반의 빌드/배포 프로세스다. 따라서 Springboot소스를 빌드툴(maven/gradle)로 빌드한 후 생성된 jar/war파일을 바로 구동시키는 것이 아니라, 생성된 jar/war파일을 Docker image로 만들어서 Docker hub에 올린 후 해당 Docker image를 활용하여 쿠버네티스 클러스터에서 컨테이너로 구동시킬 것이기 때문이다. 그렇기 때문에 jar/war파일을 Docker image로 만들기 위해서는 Dockerfile이 필요하다.
  3. 각 프로젝트 별로 Deployment.yamlService.yaml이 필요하다. Deployment.yaml은 Docker image를 쿠버네티스 클러스터에 Deployment와 Pod 컨테이너로 실행시키기 위해 필요하며, Service.yaml파일은 실행시킨 컨테이너에 접속이 가능하게끔 port를 열어주기 위해 필요하다. 
  4. 마지막으로 docker hub에서 repository생성을 해야한다. Dockerfile로 docker build를 한 후 docker image를 docker repositiory로 올려야 하는데, 이 포스트에서는 docker repository로 docker hub를 사용할 것이다. 하지만 docker hub는 무료 private repository를 한개밖에 사용할 수 없기 때문에 별로 추천하고 싶진 않다. 보통 habor를 많이 사용하는 것 같다.

 

5-1. Jenkinsfile 작성

우선 Jenkinsfile을 작성해보자. Jenkinsfile은 프로젝트의 root 디렉토리에 생성하면 되고, Jenkins pipeline에서 실행하는 빌드/배포 Step을 작성하면된다. 

파일명 : Jenkinsfile

/* pipeline 변수 설정 */
def DOCKER_IMAGE_NAME = "twofootdog/project-repo"           // 생성하는 Docker image 이름
def DOCKER_IMAGE_TAGS = "batch-visualizer-auth"  // 생성하는 Docker image 태그
def NAMESPACE = "ns-project"
def VERSION = "${env.BUILD_NUMBER}"
def DATE = new Date();
  
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') {
        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 ${VERSION}"
                        sh "sed -i.bak 's#VERSION_STRING#${VERSION}#' ./k8s/k8s-deployment.yaml"*/
                        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}"
                }
            }
        }
    }
}

위 Jenkinsfile을 간략하게 설명하자면 우선 PodTemplate에서는 배포를 수행할 Jenkins slave의 label명을 builder라고 썼고, containers에는 jenkinsfile내 각 stage에서 사용할 docker image들을 선언해놓았다. 또한 volumes에서는 컨테이너가 구동되는 서버 디렉토리와 컨테이너 내부 디렉토리를 mount 시켜놨다. 

다음으로 node에서는 jenkinsfile이 수행하는 각 stage를 정의해 놓았다. 각 stage에 대한 설명은 아래와 같다.

  1. Check out : checkout scm 명령어로 gitlab에 있는 프로젝트 소스를 jenkins slave가 가져온다.
  2. Build : gradle을 활용하여 build를 수행한다.  gradle은 서버에 설치된 gradle이 아닌 PodTemplate에서 정의한 gradle docker image를 활용하여 build를 수행한다. -x test 옵션은 gradle build 프로세스 중 test 프로세스는 건너 뛴다는 의미이다. 
  3. Docker build : 프로젝트 root 디렉토리에 있는 Dockerfile을 활용하여 docker image로 만들고, docker hub에 로그인 한 후, docker image를 docker hub로 push 한다. withCredentials에 정의된 CredentialsId는 Jenkins 설정에서 미리 설정을 해야 한다. 위에서 정의한 USERNAME/PASSWORD는 docker hub의 id와 password다. 설정하는 방법은 아래 그림에서 설명하도록 하겠다.
  4. Run Kubectl : kubernetes secret 존재여부를 체크하고 없으면 신규로 생성하고, ns-project라는 프로젝트 용 namespapce 존재여부를 체크하고 없으면 신규로 생성한다. 다음에 k8s-deployment.yaml이란 파일의 DATE 필드를 수정하는데, 수정을 하는 이유는, deployment.yaml파일이 변경되지 않은 채 해당 deployment.yaml로 배포를 하게 되면 변경된 소스가 정상 배포되지 않았기 때문이다(사실 왜 그런지는 아직 의문이다). 그래서 deployment.yaml 파일을 매 배포마다 DATE라는 필드값을 변경해줘서 배포가 정상적으로 수행되게끔 하였다. 다음으로 kubectl apply 명령어를 k8s-deployment.yaml 파일과 k8s-service.yaml파일에 적용하여 deployment와 service를 쿠버네티스 클러스터에 정상 배포한다. 

다음으로 jenkins credentialsid 추가하는 곳은 Jenkins -> Credentials ->System -> Global credentials -> Add Credentials이며 아래 그림과 같이 추가하면 된다.

 

5-2. Dockerfile 작성

다음으로 Dockerfile을 작성해보자. Dockerfile도 프로젝트의 root 디렉토리에 생성하면 되며, Docker image를 만들어주는 명령어를 작성하면된다.

파일명 : Dockerfile

FROM openjdk:8-jdk
VOLUME /tmp
ADD ./build/libs/batch-visualizer-auth-0.0.1-SNAPSHOT.jar app.jar
ENV JAVA_OPTS=""
ENTRYPOINT ["java","-jar","/app.jar"]

위 Dockerfile을 간략히 설명하자면, openjdk 8버전 image를 활용하여 app.jar파일을 java -jar 명령어로 수행한다. 

 

5-3. Deployment.yaml & Service.yaml 파일 작성

다음으로 Deployment.yaml & Service.yaml을 작성해보자. 해당 파일은 [프로젝트root디렉토리]/k8s 밑에 작성하면 된다(Jenkinsfile에 명시되어있는 위치에 작성하면 된다).

파일명 : k8s-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: batch-visualizer-auth-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: batch-visualizer-auth
  template:
    metadata:
      labels:
        app: batch-visualizer-auth
    spec:
      containers:
        - name: batch-visualizer-auth
          image: twofootdog/project-repo:batch-visualizer-auth
          ports:
            - containerPort: 8080
          imagePullPolicy: Always
          env:
            - name: DATE
              value: 'DATE_STRING'
      imagePullSecrets:
        - name: my-secret

 

파일명 : k8s-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: batch-visualizer-auth-service
spec:
  ports:
    - name: "8080"
      port: 8082
      targetPort: 8080
  selector:
    app: batch-visualizer-auth
  type: NodePort

 

6. Docker hub Repository 생성

이제 docker hub repository를 만들어보자. docker repository는 docker image가 관리되는 repository다. 이 포스트에서는 docker repository로 docker hub를 사용할 것이다. docker hub의 주소는 https://hub.docker.com/  이고, 회원가입을 해야 repository를 생성할 수 있다. private repository는 계정당 1개만 제공된다. 

우선 docker hub로 가서 로그인을 한 후 repositories -> create repository를 선택한다.

 

그 다음 repository name을 입력하고, private를 선택하고 생성을 누른다.

 

그러면 Repository가 아래와 같이 생성이 된다. 해당 docker repository로 image를 올리기 위해서는 아래 오른쪽에 있는 Docker commands 를 입력하면 된다(이미 위에 Jenkinsfile에 명시되어 있다). 

 

7. 로컬PC에서 gitlab repository로 소스 push

자 이제 모든 pipeline이 완성되었으니(?) 로컬PC 소스를 gitlab repository로 push를 해보자.

필자는 명령어를 아래와 같이 수행하였다.

  • 프로젝트 디렉토리로 이동
  • git add .
  • git commit -m "[comment]"
  • git push origin master

그러면 서버로 container화 된 소스가 배포가 될까? 배포가 되지 않는다. 우선 해당 소스는 gitlab repository로는 올라간다. 하지만 jenkins에서 자동 빌드가 되진 않는다. 왜냐하면 jenkins에서는 gitlab repository가 변경되었다는 사실을 인지하지 못하기 때문이다. gitlab repository 소스가 변경되었다는 사실을 jenkins에서 인지하게 하기 위해서는 gitlab과 jenkins를 webhook으로 연동시켜야 한다(webhook으로 연동시키는 것은 다음 장에서 배워보기로 하자).

그럼 이번장에서는 배포를 할 수 없을까? 아니다. jenkins에서 수동으로 배포를 시킬 수 있다. 

Jenkins에서 배포하려는 Pipeline을 선택한 다음 Build Now 버튼을 누르게 되면 연결되어 있는 gitlab으로부터 소스를 다운받아서 Jenkinsfile을 실행시키게 된다.

 

Console Output에서 배포 로그를 보게 되면 gitlab에 있는 소스가 정상적으로 배포된 것을 확인할 수 있다.

 

배포 된 서버에 가서 쿠버네티스 Pod를 검색한 후 컨테이너 로그를 확인해보면 정상 배포된 것을 확인할 수 있다.

 

 

마무리

이렇게 해서 우선 gitlab과 Jenkins와 docker hub를 연동하여 springboot소스를 수동으로 배포해 보았다. 다음 장에서는 

gitlab과 jenkins를 webhook으로 연동시켜서 로컬PC에서 git push/merge만 날리면 gitlab에 소스가 올라간 후 자동으로 Jenkins에서 빌드/배포가 이루어지는 프로세스를 구현해볼 것이다. 더불어 빌드/배포가 이루어진 후 Slack을 통해 운영자에게 알람메시지까지 가는 것을 구현해볼 것이다. 수고하셨습니다.

 

 

 

참고

https://betsol.com/2018/11/devops-using-jenkins-docker-and-kubernetes/

 

 

DevOps Using Jenkins, Docker, and Kubernetes - Betsol

DevOps, it’s a hot trend in computing, it’s the new buzz word and everyone’s talking about it. There isn’t a single agreed upon definition of DevOps but we like to think of it as the practice of IT operations and development engineers participating togethe

betsol.com

https://akomljen.com/set-up-a-jenkins-ci-cd-pipeline-with-kubernetes/

 

Set Up a Jenkins CI/CD Pipeline with Kubernetes

Running and understanding the Jenkins pipeline with Kubernetes. How to set up Jenkins server and how to define pipeline when using containers.

akomljen.com