4. Helm chart 이용 Jenkins CI/CD Pipeline: helm repository통한 배포
지금까지는 gitlab에 chart file을 올리고 그 chart file을 이용하여 배포를 하였습니다.
이번에는 chartmuseum에 이미 등록되어 있는 chart를 이용해 배포를 해 보겠습니다.
이렇게 하기 위해서는 Jenkins pipeline에서 gitlab에서 가져온 소스에 chart file이 있는지 검사하고,
있다면 이전 글에서와 같이 chart file로 배포하고, 없다면 chartmuseum에 로그인한 후 chart를 가져와 배포해야 합니다.
이번에는 sonarQube를 이용한 소스정적검사와 microScanner를 이용한 image 보안취약성 검사까지 포함시켰습니다.
이 부분은 devops의 실습편을 참고하시기 바랍니다.
https://happycloud-lee.tistory.com/57?category=832248
https://happycloud-lee.tistory.com/58?category=832248
1. Jenkinsfile, pipeline.properties 파일 수정
1) Jenkinsfile
def label = "hello-helm-${UUID.randomUUID().toString()}"
/* -------- functions ---------- */
def notifySlack(STATUS, COLOR) {
slackSend (color: COLOR, message: STATUS+" : " + "${env.JOB_NAME} [${env.BUILD_NUMBER}] (${env.BUILD_URL})")
}
def notifyMail(STATUS, RECIPIENTS) {
emailext body: STATUS+" : " + "${env.JOB_NAME} [${env.BUILD_NUMBER}] (${env.BUILD_URL})",
subject: STATUS + " : " + "${env.JOB_NAME} [${env.BUILD_NUMBER}]",
to: RECIPIENTS
}
/* ------------------------------ */
def emailRecipients="hiondal@gmail.com,hiondal@daum.net"
notifySlack("STARTED", "#FFFF00")
notifyMail("STARTED", "${emailRecipients}")
podTemplate(
label: label,
containers: [
//container image는 docker search 명령 이용
containerTemplate(name: "docker", image: "docker:stable", ttyEnabled: true, command: "cat"),
containerTemplate(name: "scanner", image: "newtmitch/sonar-scanner", ttyEnabled: true, command: "cat"),
containerTemplate(name: "helm", image: "dtzar/helm-kubectl", ttyEnabled: true, command: "cat")
],
//volume mount
volumes: [
hostPathVolume(hostPath: "/var/run/docker.sock", mountPath: "/var/run/docker.sock")
]
)
{
node(label) {
stage("Get Source") {
git "http://gitlab.169.56.164.244.nip.io:31836/ondalk8s/hello-helm.git"
}
//-- 환경변수 파일 읽어서 변수값 셋팅
def props = readProperties file:"./deployment/pipeline.properties"
def tag = props["version"]
def dockerRegistry = props["dockerRegistry"]
def credentialRegistry=props["credentialRegistry"]
def image = props["image"]
def baseDeployDir = props["baseDeployDir"]
def helmRepository = props["helmRepository"]
def helmChartname = props["helmChartname"]
def helmRepositoryURI = props["helmRepositoryURI"]
def credentialRepository = props["credentialRepository"]
def crtRepository = props["crtRepository"]
def helmChartfile = "${baseDeployDir}/${helmChartname}-${tag}.tgz"
def releaseName = props["releaseName"]
def namespace = props["namespace"]
try {
stage("Inspection Code") {
container("scanner") {
sh "sonar-scanner \
-Dsonar.projectName=hello-helm \
-Dsonar.projectKey=hello-helm \
-Dsonar.sources=. \
-Dsonar.host.url=http://sonarqube.169.56.164.254.nip.io:30630 \
-Dsonar.login=213c6f71037d2dbde04359ae0b8694220e734a17"
}
}
stage("Build Microservice image") {
container("docker") {
docker.withRegistry("${dockerRegistry}", "${credentialRegistry}") {
sh "docker build -f ${baseDeployDir}/Dockerfile -t ${image}:${tag} ."
sh "docker push ${image}:${tag}"
sh "docker tag ${image}:${tag} ${image}:latest"
sh "docker push ${image}:latest"
}
}
}
stage("Image Vulnerability Scanning") {
container("docker"){
aquaMicroscanner imageName: "${image}:latest", notCompliesCmd: "", onDisallowed: "ignore", outputFormat: "html"
}
}
//--- 무중단 배포를 위해 clearup 하지 않음
/*
stage( "Clean Up Existing Deployments" ) {
container("helm") {
try {
sh "helm delete ${releaseName} --purge"
} catch(e) {
echo "Clear-up Error : " + e.getMessage()
echo "Continue process !"
}
}
}
*/
//-- 이미 설치한 차트인 경우 upgrade하고, 아니면 신규 설치함
//-- git에서 CHART파일을 보내는 경우는 CHART파일을 이용하고, 아니면 helm repository를 이용함
stage( "Deploy to Cluster" ) {
container("helm") {
boolean isExist = false
//====== 이미 설치된 chart 인지 검사 =============
String out = sh script: "helm ls -q --namespace ${namespace}", returnStdout: true
if(out.contains("${releaseName}")) isExist = true
//===========================
if(fileExists("${helmChartfile}")) {
//chart 파일이 있는 경우
echo "Helm chart exists. !"
if (isExist) {
echo "Already installed. I will upgrade it with chart file"
sh "helm upgrade ${releaseName} ${helmChartfile}"
} else {
echo "Install with chart file !"
sh "helm install ${helmChartfile} --name ${releaseName} --namespace ${namespace}"
}
} else {
//없는 경우는 helm repository에서 설치
echo "Helm chart doesn't exist !"
sh "helm init" //tiller 설치
//add repo
try {
withCredentials(
[
usernamePassword
(credentialsId: "${credentialRepository}",
usernameVariable: "helmRepositoryID",
passwordVariable: "helmRepositoryPW"
),
file
(credentialsId: "${crtRepository}",
variable: "helmRepositoryCertyfile")
]
) {
String secretDir = "tmpsecret"
//-- crt파일 처리
sh """
mkdir ${secretDir}
cp ${helmRepositoryCertyfile} ${secretDir}/tls.crt
"""
//-----
sh "helm repo add ${helmRepository} ${helmRepositoryURI} \
--ca-file ${secretDir}/tls.crt \
--username ${helmRepositoryID} \
--password ${helmRepositoryPW}"
}
} catch(e) {
error("Can't get credential ! Stop process") //종료
}
sh "helm repo update" //update chart
if (isExist) {
//upgrade
echo "Already installed. I will upgrade it from helm repository"
sh "helm upgrade ${releaseName} ${helmRepository}/${helmChartname}"
} else {
//install
echo "Install from helm repository !"
sh "helm install ${helmRepository}/${helmChartname} --name ${releaseName} --namespace ${namespace}"
}
}
}
}
notifySlack("${currentBuild.currentResult}", "#00FF00")
notifyMail("${currentBuild.currentResult}", "${emailRecipients}")
} catch(e) {
currentBuild.result = "FAILED"
notifySlack("${currentBuild.currentResult}", "#FF0000")
notifyMail("${currentBuild.currentResult}", "${emailRecipients}")
}
}
}
2) pipeline.properties
기존 파일에서 맨 아래 3라인을 추가합니다. helmRepositoryURI는 본인의 chartmuseum 주소를 등록하십시오.
또한, 맨 윗줄의 version을 0.1.0에서 다른 값으로 변경합니다. 이렇게 하는 이유는 chart file을 이용하지 않고 chartmuseum을 통해 배포를 하기 위해서 입니다. chart file명이 helmChartname+"-"+version.tgz로 셋팅되므로, version값을 바꾸면 이 파일이 없다고 판단하여 chartmuseum에서 배포하게 될겁니다.
version=0.1.1
namespace=helm
dockerRegistry=http://myreg.com
credentialRegistry=credential_localreg
image=myreg.com/hello-helm
baseDeployDir=./deployment
helmRepository=chartrepo
helmChartname=hello-helm
releaseName=release-hello-helm
credentialRepository=credential_chartrepo
crtRepository=crt_chartrepo
helmRepositoryURI= https://chartrepo.169.56.164.245.nip.io/charts
Credential 'credential_chartrepo'와 'crt_chartrepo'를 등록해야 합니다.
Jenkins에서 아래와 같이 등록하십시오. credentail 추가 방법은 여기를 참조하세요.
- credential_chartrepo : chartmuseum을 로그인하기위한 ID/PW등록
id와 pw는 chartmuseum설치 시 config.yaml에서 설정한 값을 넣으십시오. : chartmuseum설치 참조
- crt_chartrepo: chartmuseum repository 등록과 helm repo update 시 필요한 인증파일 등록
chartmuseum설치 시 생성한 인증파일(tls.crt)를 등록합니다. : chartmuseum설치 참조
3) Jenkinsfile 소스 설명
- if(fileExists("${helmChartfile}")) { ...
fileExists함수를 이용하여 chart file이 있는지 검사합니다. chart file이 gitlab에서 가져온 소스에 있는 경우에는
이전 글에서 설명한거와 같이 이미 배포 되었으면 upgrade하고, 아니면 install합니다.
아래는 chart file이 없는 경우의 수행입니다. 이 수행들이 jenkins slave POD안의 'helm' container안에서 수행된다는 것을 생각하면서 이해하시기 바랍니다.
- sh "helm init" //tiller 설치
helm container는 helm client모듈만 설치되어 있습니다. (container 생성 시 containerTemplate에 지정된 image를 이용하여 설치됨). 하지만, 아직 tiller는 설치가 안되어 있습니다. helm repository와 연결되려면 tiller부터 설치해야 합니다.
helm init을 실행할때 어떤일이 벌어질까요 ?
나중에 로그를 보시면 아시겠지만, .helm디렉토리를 만들고 stable과 local repository를 등록합니다. 그리고 로그에 보이지 않지만 부모VM의 './kube/config'파일을 읽어 kubenetes와 연결된 tiller(helm server 데몬)를 생성합니다.
이렇게 tiller가 생성되야 kubernetes API server에 리소스에 대한 handling을 요청할 수 있습니다.
- withCredentials(...
보안을 위해 id/pw, 인증파일은 Jenkins의 credential객체를 이용합니다.
usernamePassword (credentialsId: "${credentialRepository}", usernameVariable: "helmRepositoryID", passwordVariable: "helmRepositoryPW" ), |
credential "${credentialRepository}"(예제에서는 credential_chartrepo)를 읽어 username은 helmRepositoryID, password는 helmRepositoryPW라는 변수에 값을 셋팅합니다. |
file (credentialsId: "${crtRepository}", variable: "helmRepositoryCertyfile") |
credential "${crtRepository}"(예제에서는 crt_chartrepo)를 읽어 파일의 위치를 helmRepositoryCertyfile변수에 할당합니다. |
String secretDir = "tmpsecret" //-- crt파일 처리 sh """ mkdir ${secretDir} cp ${helmRepositoryCertyfile} ${secretDir}/tls.crt """ //----- |
helmRepositoryCertyfile에 할당된 tls.crt파일을 tmpsecret/tls.crt로 복사합니다. |
sh "helm repo add ${helmRepository} ${helmRepositoryURI} \ --ca-file ${secretDir}/tls.crt \ --username ${helmRepositoryID} \ --password ${helmRepositoryPW}" |
chartmuseum helm repository를 container에 등록합니다. 이때 위에서 셋팅된 tls.crt 인증파일, repository ID와 PW를 파라미터로 넘김니다. |
- if(isExist) { ... } else { ... }
//upgrade echo "Already installed. I will upgrade it from helm repository" sh "helm upgrade ${releaseName} ${helmRepository}/${helmChartname}" |
이미 배포가 되어 있으면 helm upgrade합니다. 이때 chartmuseum에 미리 배포된 chart를 이용합니다. |
//install echo "Install from helm repository !" sh "helm install ${helmRepository}/${helmChartname} --name ${releaseName} --namespace ${namespace}" |
배포가 되어 있지 않으면 helm install합니다. 역시 chartmuseum에 미리 배포된 chart를 이용합니다. |
2. 배포 및 테스트
- gitlab에 push합니다.
- Jenkins에서 pipeline을 build합니다.
- 순서대로 실습을 하였다면 이미 동일한 release(예제에서는 release-hello-helm)가 있으므로 upgrade가 될겁니다.
- install하도록 하기 위해 기존 helm을 먼저 지웁니다.
$ helm delete release-hello-helm --purge
- Blud Ocean에서 다시 run 시킵니다.
- 로그를 보면 이번엔 install 된 것을 확인할 수 있을겁니다.
- 최종적으로 웹브라우저에서 아래와 같이 나오면 성공입니다.