Agile&DevOps/helm

4. Helm chart 이용 Jenkins CI/CD Pipeline: helm repository통한 배포

Happy@Cloud 2019. 9. 14. 17:55

지금까지는 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 된 것을 확인할 수 있을겁니다.

 

- 최종적으로 웹브라우저에서 아래와 같이 나오면 성공입니다.