Gitlab+Jenkins+k8s+Helm 的自動(dòng)化部署實(shí)踐
來(lái)源:https://segmentfault.com/a/1190000022637144
本文從實(shí)踐角度介紹如何結(jié)合我們常用的 Gitlab 與 Jenkins,通過(guò) K8s 來(lái)實(shí)現(xiàn)項(xiàng)目的自動(dòng)化部署,示例將包括基于 SpringBoot 的服務(wù)端項(xiàng)目與基于 Vue.js 的 Web 項(xiàng)目。
Gitlab —— 常用的源代碼管理系統(tǒng) Jenkins,Jenkins Pipeline —— 常用的自動(dòng)化構(gòu)建、部署工具,Pipeline 以流水線的方式將構(gòu)建、部署的各個(gè)步驟組織起來(lái) Docker,Dockerfile —— 容器引擎,所有應(yīng)用最終都要以 Docker 容器運(yùn)行,Dockerfile 是 Docker 鏡像定義文件 Kubernetes —— Google 開源的容器編排管理系統(tǒng) Helm —— Kubernetes 的包管理工具,類似 Linux 的 yum,apt,或 Node 的 npm 等包管理工具,能將 Kubernetes 中的應(yīng)用及相關(guān)依賴服務(wù)以包(Chart)的形式組織管理
已使用 Gitlab 做源碼管理,源碼按不同的環(huán)境建立了 develop(對(duì)應(yīng)開發(fā)環(huán)境),pre-release(對(duì)應(yīng)測(cè)試環(huán)境),master(對(duì)應(yīng)生產(chǎn)環(huán)境)分支 已搭建了 Jenkins 服務(wù) 已有 Docker Registry 服務(wù),用于 Docker 鏡像存儲(chǔ)(基于 Docker Registry 或Harbor 自建,或使用云服務(wù),本文使用阿里云容器鏡像服務(wù)) 已搭建了 K8s 集群
預(yù)期效果:
分環(huán)境部署應(yīng)用,開發(fā)環(huán)境、測(cè)試環(huán)境、生產(chǎn)環(huán)境分開來(lái),部署在同一集群的不同namespace,或不同集群中(比如開發(fā)測(cè)試部署在本地集群的不同 namespace中,生產(chǎn)環(huán)境部署在云端集群)
配置盡可能通用化,只需要通過(guò)修改少量配置文件的少量配置屬性,就能完成新項(xiàng)目的自動(dòng)化部署配置
開發(fā)測(cè)試環(huán)境在push代碼時(shí)自動(dòng)觸發(fā)構(gòu)建與部署,生產(chǎn)環(huán)境在 master 分支上添加版本 tag 并且 push tag 后觸發(fā)自動(dòng)部署
整體交互流程如下圖

項(xiàng)目配置文件

包括:
Dockerfile 文件,用于構(gòu)建 Docker 鏡像的文件(參考 Docker筆記(十一):
Dockerfile 詳解與最佳實(shí)踐)
Helm 相關(guān)配置文件,Helm 是 Kubernetes 的包管理工具,可以將應(yīng)用部署相關(guān)的Deployment,Service,Ingress 等打包進(jìn)行發(fā)布與管理(Helm 的具體介紹我們后面再補(bǔ)充)
Jenkinsfile 文件,Jenkins 的 pipeline 定義文件,定義了各個(gè)階段需執(zhí)行的任務(wù)
Dockerfile
FROM frolvlad/alpine-java:jdk8-slim#在build鏡像時(shí)可以通過(guò) --build-args profile=xxx 進(jìn)行修改ARG profileENV SPRING_PROFILES_ACTIVE=${profile}#項(xiàng)目的端口EXPOSE 8000WORKDIR /mnt#修改時(shí)區(qū)RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories \&& apk add --no-cache tzdata \&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \&& echo "Asia/Shanghai" > /etc/timezone \&& apk del tzdata \&& rm -rf /var/cache/apk/* /tmp/* /var/tmp/* $HOME/.cacheCOPY ./target/your-project-name-1.0-SNAPSHOT.jar ./app.jarENTRYPOINT ["java", "-jar", "/mnt/app.jar"]
將 SPRING_PROFILES_ACTIVE 通過(guò)參數(shù) profile 暴露出來(lái),在構(gòu)建的時(shí)候可以通過(guò) —build-args profile=xxx 來(lái)進(jìn)行動(dòng)態(tài)設(shè)定,以滿足不同環(huán)境的鏡像構(gòu)建要求。
SPRING_PROFILES_ACTIVE 本可以在 Docker 容器啟動(dòng)時(shí)通過(guò) docker run -e SPRING_PROFILES_ACTIVE=xxx 來(lái)設(shè)定,因這里使用 Helm 進(jìn)行部署不直接通過(guò)docker run 運(yùn)行,因此通過(guò) ARG 在鏡像構(gòu)建時(shí)指定
Helm 配置文件
Helm 是 Kubernetes 的包管理工具,將應(yīng)用部署相關(guān)的 Deployment,Service,Ingress 等打包進(jìn)行發(fā)布與管理(可以像 Docker 鏡像一樣存儲(chǔ)于倉(cāng)庫(kù)中)。如上圖中Helm 的配置文件包括:
helm - chart包的目錄名├── templates - k8s配置模版目錄│ ├── deployment.yaml - Deployment配置模板,定義如何部署Pod│ ├── _helpers.tpl - 以下劃線開頭的文件,helm視為公共庫(kù)定義文件,用于定義通用的子模版、函數(shù)、變量等│ ├── ingress.yaml - Ingress配置模板,定義外部如何訪問(wèn)Pod提供的服務(wù),類似于Nginx的域名路徑配置│ ├── NOTES.txt - chart包的幫助信息文件,執(zhí)行helm install命令成功后會(huì)輸出這個(gè)文件的內(nèi)容│ └── service.yaml - Service配置模板,配置訪問(wèn)Pod的服務(wù)抽象,有NodePort與ClusterIp等|── values.yaml - chart包的參數(shù)配置文件,各模版文件可以引用這里的參數(shù)├── Chart.yaml - chart定義,可以定義chart的名字,版本號(hào)等信息├── charts - 依賴的子包目錄,里面可以包含多個(gè)依賴的chart包,一般不存在依賴,我這里將其刪除了
apiVersion: v2name: your-chart-namedescription: A Helm chart for Kubernetestype: applicationversion: 1.0.0appVersion: 1.16.0
在 values.yaml 中定義模板文件中需要用到的變量,如
#部署Pod的副本數(shù),即運(yùn)行多少個(gè)容器replicaCount: 1#容器鏡像配置image:repository: registry.cn-hangzhou.aliyuncs.com/demo/demopullPolicy: Always# Overrides the image tag whose default is the chart version.tag: "dev"#鏡像倉(cāng)庫(kù)訪問(wèn)憑證imagePullSecrets:name: aliyun-registry-secret#覆蓋啟動(dòng)容器名稱nameOverride: ""fullnameOverride: ""#容器的端口暴露及環(huán)境變量配置container:port: 8000env: []#ServiceAccount,默認(rèn)不創(chuàng)建serviceAccount:# Specifies whether a service account should be createdcreate: false# Annotations to add to the service accountannotations: {}name: ""podAnnotations: {}podSecurityContext: {}# fsGroup: 2000securityContext: {}# capabilities:# drop:# - ALL# readOnlyRootFilesystem: true# runAsNonRoot: true# runAsUser: 1000#使用NodePort的service,默認(rèn)為ClusterIpservice:type: NodePortport: 8000#外部訪問(wèn)Ingress配置,需要配置hosts部分ingress:enabled: trueannotations: {}# kubernetes.io/ingress.class: nginx# kubernetes.io/tls-acme: "true"hosts:host: demo.compaths: ["/demo"]tls: []# - secretName: chart-example-tls# hosts:# - chart-example.local#.... 省略了其它默認(rèn)參數(shù)配置
這里在默認(rèn)生成的基礎(chǔ)上添加了 container 部分,可以在這里指定容器的端口號(hào)而不用去改模板文件(讓模板文件在各個(gè)項(xiàng)目通用,通常不需要做更改),同時(shí)添加env的配置,可以在helm部署時(shí)往容器里傳入環(huán)境變量。將Service type從默認(rèn)的ClusterIp改為了NodePort。部署同類型的不同項(xiàng)目時(shí),只需要根據(jù)項(xiàng)目情況配置Chart.yaml與values.yaml兩個(gè)文件的少量配置項(xiàng),templates目錄下的模板文件可直接復(fù)用。
# 登錄Docker Registry生成/root/.docker/config.json文件sudo docker login --username=your-username registry.cn-shenzhen.aliyuncs.com# 創(chuàng)建 namespace develop(我這里是根據(jù)項(xiàng)目的環(huán)境分支名稱建立namespace)kubectl create namespace develop# 在 namespace develop中創(chuàng)建一個(gè)secretkubectl create secret generic aliyun-registry-secret --from-file=.dockerconfigjson=/root/.docker/config.json --type=kubernetes.io/dockerconfigjson --namespace=develop
Jenkinsfile
image_tag = "default" //定一個(gè)全局變量,存儲(chǔ)Docker鏡像的tag(版本)pipeline {agent anyenvironment {GIT_REPO = "${env.gitlabSourceRepoName}" //從Jenkins Gitlab插件中獲取Git項(xiàng)目的名稱GIT_BRANCH = "${env.gitlabTargetBranch}" //項(xiàng)目的分支GIT_TAG = sh(returnStdout: true,script: 'git describe --tags --always').trim() //commit id或tag名稱DOCKER_REGISTER_CREDS = credentials('aliyun-docker-repo-creds') //docker registry憑證KUBE_CONFIG_LOCAL = credentials('local-k8s-kube-config') //開發(fā)測(cè)試環(huán)境的kube憑證KUBE_CONFIG_PROD = "" //credentials('prod-k8s-kube-config') //生產(chǎn)環(huán)境的kube憑證DOCKER_REGISTRY = "registry.cn-hangzhou.aliyuncs.com" //Docker倉(cāng)庫(kù)地址DOCKER_NAMESPACE = "your-namespace" //命名空間DOCKER_IMAGE = "${DOCKER_REGISTRY}/${DOCKER_NAMESPACE}/${GIT_REPO}" //Docker鏡像地址INGRESS_HOST_DEV = "dev.your-site.com" //開發(fā)環(huán)境的域名INGRESS_HOST_TEST = "test.your-site.com" //測(cè)試環(huán)境的域名INGRESS_HOST_PROD = "prod.your-site.com" //生產(chǎn)環(huán)境的域名}parameters {string(name: 'ingress_path', defaultValue: '/your-path', description: '服務(wù)上下文路徑')string(name: 'replica_count', defaultValue: '1', description: '容器副本數(shù)量')}stages {stage('Code Analyze') {agent anysteps {echo "1. 代碼靜態(tài)檢查"}}stage('Maven Build') {agent {docker {image 'maven:3-jdk-8-alpine'args '-v $HOME/.m2:/root/.m2'}}steps {echo "2. 代碼編譯打包"sh 'mvn clean package -Dfile.encoding=UTF-8 -DskipTests=true'}}stage('Docker Build') {agent anysteps {echo "3. 構(gòu)建Docker鏡像"echo "鏡像地址:${DOCKER_IMAGE}"//登錄Docker倉(cāng)庫(kù)sh "sudo docker login -u ${DOCKER_REGISTER_CREDS_USR} -p ${DOCKER_REGISTER_CREDS_PSW} ${DOCKER_REGISTRY}"script {def profile = "dev"if (env.gitlabTargetBranch == "develop") {image_tag = "dev." + env.GIT_TAG} else if (env.gitlabTargetBranch == "pre-release") {image_tag = "test." + env.GIT_TAGprofile = "test"} else if (env.gitlabTargetBranch == "master"){// master分支則直接使用Tagimage_tag = env.GIT_TAGprofile = "prod"}//通過(guò)--build-arg將profile進(jìn)行設(shè)置,以區(qū)分不同環(huán)境進(jìn)行鏡像構(gòu)建sh "docker build --build-arg profile=${profile} -t ${DOCKER_IMAGE}:${image_tag} ."sh "sudo docker push ${DOCKER_IMAGE}:${image_tag}"sh "docker rmi ${DOCKER_IMAGE}:${image_tag}"}}}stage('Helm Deploy') {agent {docker {image 'lwolf/helm-kubectl-docker'args '-u root:root'}}steps {echo "4. 部署到K8s"sh "mkdir -p /root/.kube"script {def kube_config = env.KUBE_CONFIG_LOCALdef ingress_host = env.INGRESS_HOST_DEVif (env.gitlabTargetBranch == "pre-release") {ingress_host = env.INGRESS_HOST_TEST} else if (env.gitlabTargetBranch == "master"){ingress_host = env.INGRESS_HOST_PRODkube_config = env.KUBE_CONFIG_PROD}sh "echo ${kube_config} | base64 -d > /root/.kube/config"//根據(jù)不同環(huán)境將服務(wù)部署到不同的namespace下,這里使用分支名稱sh "helm upgrade -i --namespace=${env.gitlabTargetBranch} --set replicaCount=${params.replica_count} --set image.repository=${DOCKER_IMAGE} --set image.tag=${image_tag} --set nameOverride=${GIT_REPO} --set ingress.hosts[0].host=${ingress_host} --set ingress.hosts[0].paths={${params.ingress_path}} ${GIT_REPO} ./helm/"}}}}}
Code Analyze,可以使用 SonarQube 之類的靜態(tài)代碼分析工具完成代碼檢查,這里先忽略 Maven Build,啟動(dòng)一個(gè) Maven 的 Docker 容器來(lái)完成項(xiàng)目的 maven 構(gòu)建打包,掛載 maven 本地倉(cāng)庫(kù)目錄到宿主機(jī),避免每次都需要重新下載依賴包 Docker Build,構(gòu)建 Docker 鏡像,并推送到鏡像倉(cāng)庫(kù),不同環(huán)境的鏡像通過(guò)tag區(qū)分,開發(fā)環(huán)境使用 dev.commitId 的形式,如 dev.88f5822,測(cè)試環(huán)境使用 test.commitId,生產(chǎn)環(huán)境可以將 webhook 事件設(shè)置為 tag push event,直接使用 tag名稱 Helm Deploy,使用helm完成新項(xiàng)目的部署,或已有項(xiàng)目的升級(jí),不同環(huán)境使用不同的參數(shù)配置,如訪問(wèn)域名,K8s 集群的訪問(wèn)憑證kube_config等
Jenkins 配置
Jenkins 任務(wù)配置

配置構(gòu)建觸發(fā)器,將目標(biāo)分支設(shè)置為 develop 分支,生成一個(gè) token,如圖

記下這里的“GitLab webhook URL”及token值,在Gitlab配置中使用。

保存即完成了項(xiàng)目開發(fā)環(huán)境的Jenkins配置。測(cè)試環(huán)境只需將對(duì)應(yīng)的分支修改為pre-release 即可
Jenkins 憑據(jù)配置
DOCKER_REGISTER_CREDS = credentials('aliyun-docker-repo-creds') //docker registry憑證KUBE_CONFIG_LOCAL = credentials('local-k8s-kube-config') //開發(fā)測(cè)試環(huán)境的kube憑證


添加 K8s 集群的訪問(wèn)憑證,在 master 節(jié)點(diǎn)上將 /root/.kube/config 文件內(nèi)容進(jìn)行 base64 編碼,
base64 /root/.kube/config > kube-config-base64.txtcat kube-config-base64.txt
使用編碼后的內(nèi)容在 Jenkins 中創(chuàng)建一個(gè) Secret text 類型的憑據(jù),如圖
在 Secret 文本框中輸入 base64 編碼后的內(nèi)容。
Gitlab 配置
在 Gitlab 項(xiàng)目的 Settings - Integrations 頁(yè)面配置一個(gè) webhook,在 URL 與 Secret Token 中填入前面 Jenkins 觸發(fā)器部分的“GitLab webhook URL”及token值,選中“Push events”作為觸發(fā)事件,如圖


總結(jié)
- END -
?推薦閱讀? Kubernetes 企業(yè)容器云平臺(tái)運(yùn)維實(shí)戰(zhàn)? 面試官:你都監(jiān)控 Redis 哪些指標(biāo)? Linux運(yùn)維工程師的 6 類好習(xí)慣和 23 個(gè)教訓(xùn) 一名運(yùn)維小哥對(duì)運(yùn)維規(guī)則的10個(gè)總結(jié),收藏起來(lái) 終于明白了 DevOps 與 SRE 的區(qū)別! Kubernetes上生產(chǎn)環(huán)境后,99%都會(huì)遇到這2個(gè)故障 如何用 Kubernetes 實(shí)現(xiàn) CI/CD 發(fā)布流程?| 漫畫 K8s kubectl 常用命令總結(jié)(建議收藏) Kubernetes 的這些核心資源原理,你一定要了解 我在創(chuàng)業(yè)公司的 “云原生” 之旅 基于Nginx實(shí)現(xiàn)灰度發(fā)布與AB測(cè)試 編寫 Dockerfile 最佳實(shí)踐 12年資深運(yùn)維老司機(jī)的成長(zhǎng)感悟 搭建一套完整的企業(yè)級(jí)高可用 K8s 集群(kubeadm方式)
點(diǎn)亮,服務(wù)器三年不宕機(jī)


