앞서 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 |