1) Kaoni Cloud/CI CD

05. Jenkins Pipeline 구축

러시안블루 크레아 의 집 2024. 6. 7. 17:51

 

앞서 Jenkins를 설치했다면, 이번 chapter에서는 Jenkins pipeline에 대해 기술하겠다.

다소 복잡하고 어렵게 느껴질 수 있으니, 침착하게 잘 따라오길 바란다.

 

아래의 CI/CD flow를 참고하면 Jenkinsfile을 이해하는데 도움이 될 것이다.

 

 

 

 

 

 


1. Jenkinsfile 과 Dockerfile

 1) gitlab repo 구성

Jenkinsfile : script  for Jenkins pipeline 

Dockerfile : script for Docker build 

ezApprovalG : App source Code Package

build : Docker build에 필요한 lib package

 

 

 2) Jenkinsfile

@Library('jenkins-lib') _

import java.text.SimpleDateFormat;
import java.util.Date;

def microServiceName = "ezapprovalg"
def packageName = "ezApprovalG"
def imgRegistry  = "10.0.50.10:31110"   // Kaoni Cloud NEXUS
def sonarqube = "http://10.0.50.10:31373"
def gitops = "gitlab-msa2.kaoni.com/msa1.0/gitops/ezApprovalG.git"
def today = new SimpleDateFormat("yyyyMMdd").format(new Date())


podTemplate(
    label : 'ezapprovalg-pipeline',
    containers: [
        containerTemplate(name: 'jnlp', image: 'jenkins/inbound-agent:latest', resourceRequestMemory: '512Mi', resourceRequestCpu: '500m', workingDir: '/home/jenkins/agent'),
        containerTemplate(name: 'kaniko', image: 'gcr.io/kaniko-project/executor:latest', resourceRequestMemory: '2Gi', resourceRequestCpu: '500m', ttyEnabled: true, command: '/busybox/cat'),
        containerTemplate(name: 'maven', image: 'maven:3.8.8-ibmjava-8', resourceRequestMemory: '256Mi', resourceRequestCpu: '500m', ttyEnabled: true, command: 'cat'),
        containerTemplate(name: 'kustomize', image: 'alpine/git', resourceRequestMemory: '256Mi', resourceRequestCpu: '200m', ttyEnabled: true, command: 'cat'),
        containerTemplate(name: 'sonarqube-scanner', image: 'sonarsource/sonar-scanner-cli:latest', resourceRequestMemory: '256Mi', resourceRequestCpu: '200m', ttyEnabled: true, command: 'cat')
    ],
    volumes: [
        persistentVolumeClaim(mountPath: '/root', claimName: 'maven-jenkins-pvc'),
        secretVolume(secretName: 'docker-config', mountPath: '/kaniko/.docker')  
    ]
) {
    node('ezapprovalg-pipeline') {

        stage("0. Agent offline check") {
            script {
                def nodeName = env.NODE_NAME ?: 'master'
                echo "Node Name: ${nodeName}"

                if (!isAgentOnline(nodeName)) {
                    error "Agent is offline. Aborting pipeline."
                }
            }
        }    

        stage("1. Checkout") {
            checkout scm
        }

        stage("2. Maven build") {
            container('maven') {
                script {
                        sh "cd ${packageName} && mvn clean package -DskipTests=true"
                        sh "mkdir ROOT && cd ROOT && jar xvf ../${packageName}/output/ezFlow.war"
                        echo "Maven build completed"
                }
            }
        }

        stage("3. SonarQube Analysis and Quality Gate Check") {
            container('sonarqube-scanner') {
                script {
                    withSonarQubeEnv('SonarQube') {
                        withCredentials([string(credentialsId: 'sonarqubeToken', variable: 'SONARQUBE_TOKEN')]) {
                            sh """
                                sonar-scanner \
                                -Dsonar.projectKey=${microServiceName} \
                                -Dsonar.sources=${packageName} \
                                -Dsonar.host.url=${sonarqube} \
                                -Dsonar.token=${SONARQUBE_TOKEN} \
                                -Dsonar.java.binaries=${packageName}/output/classes \
                                -Dsonar.java.libraries=ROOT/WEB-INF/lib \
                                -Dsonar.sourceEncoding=UTF-8
                            """
                        }
                    }

                    def qualityGate = waitForQualityGate()
                    echo "Status: ${qualityGate.status}"
                    if (qualityGate.status != 'OK') {
                        error "Pipeline aborted due to Quality Gate failure: ${qualityGate.status}"
                    } else {
                        echo "Quality Gate passed: ${qualityGate.status}"
                    }
                }
            }
        }

        stage("4. Kaniko build") {
            container('kaniko') {
                script {
                    withCredentials([[$class: 'UsernamePasswordMultiBinding',
                        credentialsId: 'kcr-credential',
                        usernameVariable: 'USERNAME',
                        passwordVariable: 'PASSWORD'
                    ]]) {
                        sh "/kaniko/executor --context `pwd` --dockerfile `pwd`/Dockerfile --destination ${imgRegistry}/ktg-${microServiceName}:${today}bn${env.BUILD_NUMBER} --insecure --insecure-registry ${imgRegistry}"
                        echo "Kaniko build and push to Kaoni Cloud Nexus completed"
                    }
                }
            }
        }

        stage("5. Update Manifest and Push to GitOps") {
            container('kustomize') {
                script {
                    withCredentials([[$class: 'UsernamePasswordMultiBinding',
                        credentialsId: 'gitlab-credentials',
                        usernameVariable: 'GIT_USERNAME',
                        passwordVariable: 'GIT_PASSWORD'
                    ]]) {
                        sh "apk add --no-cache kustomize"
                        sh 'git config --global user.email "jenkins@kaoniDevops.com"'
                        sh 'git config --global user.name "Jenkins"'
                        sh "git clone http://${GIT_USERNAME}:${GIT_PASSWORD}@${gitops} gitops"
                        sh "cd gitops/overlays/staging && \
                            kustomize edit set image ${imgRegistry}/ktg-${microServiceName}:${today}bn${env.BUILD_NUMBER} && \
                            git add . && \
                            git commit -m \"Update ${microServiceName} image to ${today}bn${env.BUILD_NUMBER}\" && \
                            git push http://${GIT_USERNAME}:${GIT_PASSWORD}@${gitops}"
                        echo "Update Manifest and push to GitOps completed"
                    }
                }
            }
        }
    }
}

Devops에 security를 추가해 Devsops 를 구성해보았다.

Sonarqube와 ArgoCD는 곧 이어질 chapter에서 다루겠다.

(참고로 "stage 4. docker build"에서, 초기의 Jenkinsfile에서는 dind(Docker in docker)를 사용했으나 이 구조는 여전히 Docker Conatiner 내부에서 Docker Daemon이 실행되어야 한다는 점 때문에 자원 소모량이 많고, Root 권한을 필요로 해 보안 취약점이 발생하였다.게다가 Storage Driver 문제, 캐시 공유 불가능 등의 문제를 가지고 있기 때문에 Docker를 이미지 빌드 도구 비교 대상에서 제외하였고, Kaniko로 교채하게 되었다.)

 

 

 3) Dockerfile

#-----------------------------
# 1. Docker Image Production
# Use an official Rocky linux image as the base image
FROM rockylinux:8

RUN useradd -u 2222 jmocha

# Install jdk-1.8 on the container
RUN yum -y install java-1.8.0-openjdk-devel

# Install net-tools, telnet, procps on the container
RUN yum -y install net-tools telnet procps

# Copy jq on the container
COPY /build/jq  /usr/bin/jq
RUN chmod a+x   /usr/bin/jq

# Copy the 'linux_v5.7bin', 'tomcat_v5.1.bin', 'trivy' to the container (script for #CSAP#)
COPY --chown=jmocha:jmocha /build/csap_script_file /csap_script_file

# Copy fontconfig package on the container
Copy /build/fontconfig /usr/share/fontconfig
Copy /build/local.conf /etc/fonts/local.conf

# Copy tomcat package 'ezFlow' to the container
COPY --chown=jmocha:jmocha /build/ezFlow /home/jmocha/ezEKP/ezFlow

# Copy the Application 'ezApprovalG' from builder to the container
COPY --chown=jmocha:jmocha /ROOT /home/jmocha/ezEKP/ezFlow/webapps/ROOT

# Copy the jmx_exporter(for Prometheus) to the container
COPY --chown=jmocha:jmocha /build/jmx_exporter /jmx_exporter

RUN dnf -y update

RUN chmod -s /usr/bin/newgrp && chmod -s /sbin/unix_chkpwd

VOLUME /volumes/shared/ezFlow/fileroot

RUN ln -s /volumes/shared/ezFlow/fileroot /home/jmocha/ezEKP/ezFlow/webapps/ROOT/fileroot

RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime

RUN ln -s /usr/lib/jvm/java /usr/local/java

# Copy entrypoint to the container
COPY build/entrypoint.sh /

RUN chmod a+x /entrypoint.sh

USER jmocha

ENTRYPOINT /entrypoint.sh

OS = Rockylinux8 

WAS = Tomcat 9.0.89

JDK 1.8

 

 

 4) Entrypoint.sh

#!/bin/sh

CONFIG_FILE_PATH=/home/jmocha/ezEKP/ezFlow/webapps/ROOT/WEB-INF/classes/egovframework/egovProps/config.properties
GLOBALS_FILE_PATH=/home/jmocha/ezEKP/ezFlow/webapps/ROOT/WEB-INF/classes/egovframework/egovProps/globals.properties
CATALINA_FILE_PATH=/home/jmocha/ezEKP/ezFlow/bin/catalina.sh
SERVER_CONFIG_FILE_PATH=/home/jmocha/ezEKP/ezFlow/conf/server.xml
WEB_CONFIG_FILE_PATH=/home/jmocha/ezEKP/ezFlow/conf/web.xml
KUKUDOCS_LIC_PATH=/home/jmocha/ezEKP/ezFlow/webapps/ROOT/js/ezEditor/kukudocsEditor/kukudocs.lic

if [ ! -z "$CERT_FILE_NAME" ]; then
    sed -i 's/<!-- ssl start//' $SERVER_CONFIG_FILE_PATH
    sed -i 's/ssl end -->//' $SERVER_CONFIG_FILE_PATH
    sed -i "s/keystoreFile=\"\"/keystoreFile=\"\/etc\/cert\/$CERT_FILE_NAME\"/" $SERVER_CONFIG_FILE_PATH
    sed -i 's/<!-- ssl start//' $WEB_CONFIG_FILE_PATH
    sed -i 's/ssl end -->//' $WEB_CONFIG_FILE_PATH
fi

if [ ! -z "$CERT_FILE_PASS" ]; then
    sed -i "s/keystorePass=\"\"/keystorePass=\"$CERT_FILE_PASS\"/" $SERVER_CONFIG_FILE_PATH
fi

if [ ! -z "$KUKUDOCS_LIC_FILE_NAME" ]; then
    if [ ! -L $KUKUDOCS_LIC_PATH ]; then
        ln -sf /etc/kukudocs/$KUKUDOCS_LIC_FILE_NAME $KUKUDOCS_LIC_PATH
    fi
fi

if [ ! -z "$HEAP_MIN_SIZE" ] && [ ! -z "$HEAP_MAX_SIZE" ]; then
    sed -i "s/^CATALINA_OPTS=.*/CATALINA_OPTS=\"-javaagent:\/jmx_exporter\/jmx_prometheus_javaagent-0.20.0.jar=9191:\/jmx_exporter\/config.yaml -Xms$HEAP_MIN_SIZE -Xmx$HEAP_MAX_SIZE\"/" $CATALINA_FILE_PATH
fi

if [ -z "$GATEWAY" ]; then
    GATEWAY=$(netstat -rn | grep '^0.0.0.0' | awk '{print $2}')
fi

NETWORK=$(echo $GATEWAY | awk -F'.' '{print $1"."$2}')

if [ ! -z "$MAIL_SERVER_ADDRESS" ]; then
    sed -i "s/^config.MailServerAddress = .*/config.MailServerAddress = $MAIL_SERVER_ADDRESS/" $CONFIG_FILE_PATH
fi

if [ ! -z "$SMTP_PORT" ]; then
    sed -i "s/^config.SMTPPort = .*/config.SMTPPort = $SMTP_PORT/" $CONFIG_FILE_PATH
fi

if [ ! -z "$JGW_SERVER_ADDRESS" ]; then
    sed -i "s/^config.JGwServerURL = .*/config.JGwServerURL = http:\/\/$JGW_SERVER_ADDRESS:9090\/jgw_server/" $CONFIG_FILE_PATH
fi


if [ ! -z "$EZ_ETC_SERVER_ADDRESS" ]; then
    sed -i "s/^config.ezETC = .*/config.ezETC = http:\/\/$EZ_ETC_SERVER_ADDRESS/" $CONFIG_FILE_PATH
fi


if [ ! -z "$EZ_EMAIL_SERVER_ADDRESS" ]; then
    sed -i "s/^config.ezEmail = .*/config.ezEmail = http:\/\/$EZ_EMAIL_SERVER_ADDRESS/" $CONFIG_FILE_PATH
fi


if [ ! -z "$EZ_APPROVALG_SERVER_ADDRESS" ]; then
    sed -i "s/^config.ezApprovalG = .*/config.ezApprovalG = http:\/\/$EZ_APPROVALG_SERVER_ADDRESS/" $CONFIG_FILE_PATH
fi


if [ ! -z "$EZ_APPROVALG2_SERVER_ADDRESS" ]; then
    sed -i "s/^config.ezApprovalG2 = .*/config.ezApprovalG2 = http:\/\/$EZ_APPROVALG2_SERVER_ADDRESS/" $CONFIG_FILE_PATH
fi


if [ ! -z "$EZ_BOARD_SERVER_ADDRESS" ]; then
    sed -i "s/^config.ezBoard = .*/config.ezBoard = http:\/\/$EZ_BOARD_SERVER_ADDRESS/" $CONFIG_FILE_PATH
fi


PORT=3306

if [ ! -z "$DB_SERVER_PORT" ]; then
    PORT=$DB_SERVER_PORT
fi

DATABASE=jmocha

if [ ! -z "$DB_NAME" ]; then
    DATABASE=$DB_NAME
fi

if [ ! -z "$DB_SERVER_ADDRESS" ]; then
    sed -i "s/^Globals.Url=.*/Globals.Url=jdbc:mariadb:\/\/$DB_SERVER_ADDRESS:$PORT\/$DATABASE\?useSSL=false/" $GLOBALS_FILE_PATH
fi

if [ ! -z "$DB_USERNAME" ]; then
    sed -i "s/^Globals.UserName=.*/Globals.UserName= $DB_USERNAME/" $GLOBALS_FILE_PATH
else
    sed -i "s/^Globals.UserName=.*/Globals.UserName= ezEKP2017/" $GLOBALS_FILE_PATH
fi

if [ ! -z "$DB_PASSWORD" ]; then
    sed -i "s/^Globals.Password=.*/Globals.Password= $DB_PASSWORD/" $GLOBALS_FILE_PATH
fi

if [ ! -z "$USE_PRIMARY_LANG_ONLY" ]; then
    sed -i "s/^config.UsePrimaryLangOnly = .*/config.UsePrimaryLangOnly = $USE_PRIMARY_LANG_ONLY/" $CONFIG_FILE_PATH
fi

if [ ! -z "$USE_ADDRESS_OPEN_API" ]; then
    sed -i "s/^config.USE_AddressOpenAPI = .*/config.USE_AddressOpenAPI = $USE_ADDRESS_OPEN_API/" $CONFIG_FILE_PATH
fi

if [ ! -z "$SENT_MAIL_STORED_IN_SENTBOX" ]; then
    sed -i "s/^config.SentMailStoredInSentbox = .*/config.SentMailStoredInSentbox = $SENT_MAIL_STORED_IN_SENTBOX/" $CONFIG_FILE_PATH
fi

sed -i "s/^config.SchedulerServer = .*/config.SchedulerServer = $(hostname)/" $CONFIG_FILE_PATH
sed -i "s/^config.mobileClientServerURL = .*/config.mobileClientServerURL = $NETWORK,-$GATEWAY/" $CONFIG_FILE_PATH

export JAVA_HOME=/usr/local/java
export PATH=$JAVA_HOME/bin:$PATH

cd /home/jmocha/ezEKP/ezFlow/bin
exec ./startup.sh

entrypoint.sh는 환경변수를 설치하고, tomcat을 실행하는 script 이다.

deployment.yaml에서 환경변수를 명시하면,  app 소스코드 내의 properties 파일을 수정한 후 컨테이너를 실행시켜 pod를 생성한다. 

 

쉽게 생각하면 이런 것이다.

예를들어 application src code 중에 config.properties 가 있다.

여기서 작성된 script 중 일부(ex, IP:Port) 를  수정하고 싶다면 docker image를 다시 build해야 하는가?

결론은 NO.

entrypoint.sh로 환경변수 설치를 해 놓으면, deployment.yaml 에서 환경변수 값을 바꿔주기만 하면 된다.

바뀐 환경변수를 반영해서 새로운 pod가 생성되서 동작한다.

 

 

 

 

 


2. Credential 등록

 0) Jenkins관리 > 플러그인 관리 > Availavle plugins

   플러그인 Credential 설치 

 

 

 

 1) Jenkins관리 > Credential > System > Global creedential 

Jenkins 관리 클릭

 

 

credential 클릭

 

 

system 클릭

 

 

Global credentials 클릭

 

 

 

 

이곳은 CI에서 사용할 Credentials 정보를 Jenkins에 등록하는 곳이다.

Kubernetes, Gitlab, SonarQube, Nexus 등의 Credential 정보를 미리 등록해 둘 것이다.

 

 

 

2) Credential : kubeconfig (K8S) 등록 

Add Credentials 클릭

 

 

 

kind = Secret file 선택,

File (파일선택) 클릭 후, kubeconfig 파일 업로드  

  # kubeconfig 란 kubernetes 설정파일로, kubectl 명령어로 api server에 접근할 때 사용할 인증 정보를 담고 있다.

ID = kubeconfig 입력

Description = kubeconfig 입력

 

아래 Create 클릭

 

 

 

Credential : kubeconfig 등록 완료

 

 

 

 

3) Credential : gitlab-credential (GIT) 등록 

Add Credentials 클릭

 

 

 

kind = username with password 선택

username = [Gitlab ID]

passowrd = [Gitlab Password]

ID = gitlab-credentials 입력

 

아래 Create 클릭

 

 

 

Credential : gitlab-credentials 등록 완료

 

 

4) Credential : kcr-credential (NEXUS) 등록 

Add Credentials 클릭

 

 

 

kind = username with password 선택

username = [Nexus ID]

passowrd = [Nexus Password]

ID = kcr-credential 입력 (혹은 Nexus-credential)

 (kcr 이란, Kaoni container registry의 약자)

 

아래 Create 클릭

 

 

Credential : kcr-credential 등록 완료

 

 

 

 

우선 3개의 credential만 추가하고 계속 진행하겠다. 

 

 

 

 

 


3. Configure Clouds

 

0) Jenkins관리 > 플러그인 관리 > Availavle plugins

플러그인에서 kubernetes 설치

 

 

 

 1) Jenkins관리 > 노드관리 > configure Clouds

Jenkins 관리 클릭

 

 

노드 관리 클릭

 

 

Configure Clouds 클릭

 

 

 

Add a new cloud  클릭

 

 

kubernetes 항목이 새로 생성

 

 

 

 

 2) kubernetes > kubernetes Cloud details

kubernetes 항목이 새로 생성되었으면,  kubrnetes Cloud details 클릭 (펼침)

 

 

 

kubernetes URL = [your_kubernetes_URL] 입력

kubernetes namespace  =  cicd 또는 [Jenkins가 설치되어 있는 namespace]  입력

Credential = kubeconfig  선택

 

 

 

Jenkins URL 에는 Jenkins - Service의  ClusterIp : 8080  입력

 

 

 

 

Jenkins turnnel 에는 Jenkins - Service의 ClusterIp : 50000 입력

 

 

 

 

 

3) kubernetes > Pod Templates

pod Template 클릭 > Add Pod Template 클릭

 

 

 

Name = kube-agent

Pod Template details 클릭 (펼침)  

 

 

 

Namespace = cicd

Labels = kubeagent

 

 

가장 아래로 내려서 Save 클릭

 

 

 


4. JENKINS Pipeline 구축

  1) 새로운 item  생성

새로운 Item 클릭

 

 

 2) Pipeline 생성

 

Enter an item name 에 자신의 [Pipeline name] 입력, 

아래의 Pipeline 클릭

 

 

 

Configure 화면이 나온다. 여기서 pipeline에 대한 상세 설정을 setting한다.

 

 

 

Build when a change is pushed to GitLab. GitLab webhook URL: http://[Jenkins_domain]/project/[pipeline_name]-pipeline  클릭

 ## 위의 GitLab webhook URL 을 복사해둔다 ##

 

고급  클릭(펼침)

 

 

 

Generate 클릭,  secret token 이 생성된다.

  ## 위의 secret token을 복사해둔다. ##

 

 

 

Definition =  Pipeline script from SCM 선택   (Jenkinsfile를 gitlab 저장소에 가져오겠다는 의미)

SCM = Git  선택

Repository URL = [Git_clone_URL]  입력

Credential = gitlab credential 선택

Branch Specifier  = */main  입력   

 

 

아래로 내려서 저장 클릭

 

 

 

홈 화면에 pipeline이 구축된 것을 확인할 수 있다.

 

 

 

 

이로서 Jenkins pipeline을 구축 완료했다.

이제 GITLAB에 webhook을 trigger를 구성할 차래다.

Gitlab에 push event가 일어나면 해당 Jenkins-pipeline의 API를 호출하도록 만들어 보겠다.

즉, Gitlab에 push가 일어나면, 호출된 Jenkins pipeline이 자동으로 build를 진행하게 된다.

 

 

 


5. GITLAB Webhook 구성

  1) Gitlab 저장소 진입

Jenkins-pipeline과 연동할 GITLAB 저장소 클릭

 

 

 

 2) settings  >  webhook 

URL = 위에서 복사한 값(GitLab webhook URL)  입력

Secret token = 위에서 복사한 값(secret token)  입력

Triger = push events  클릭

   >branch name을 입력하면, 해당 branch에서 일어나는 push event 에만 trigger가 동작한다.

   >blank 이면 모든 branch에 대해 trigger가 동작한다는 의미이다.

 

 

 

 


 

 

 

이로서 pipeline 구성이 완료되었다.

여기까지 따라오느라 고생 많았다.

이번 chapter는 이래적으로 매우 길었다.

이 단계는 얽혀있는 게 상당히 많고 복잡한데, 어중간하게 잘라서 포스팅을 하면 더욱 큰 혼동을 줄 것 같아 긴 호흡을 한번에 가져가기로 판단했다.

 

 

다음 chapter 에서는 Sonarqube 에 대해 다루겠다. 

 

'1) Kaoni Cloud > CI CD' 카테고리의 다른 글

07. Jenkins와 SonarQube 연동  (0) 2024.06.08
06. Kubernetes에 SonarQube 설치  (1) 2024.06.08
04. Kubernetes에 Jenkins 설치  (0) 2024.06.07
03. Kubernetes에 NEXUS 설치  (0) 2024.05.29
02. Ubuntu에 GITLAB 설치  (0) 2024.05.29