1. kubernetes之CI/CD第二篇-jenkins结合helm部署应用:
1. 概述:
?? 在前期的博文中我已经初步介绍过kubernetes环境下的CI/CD的使用。主要是jenkins slave pod自动创建和销毁,当有jenkins job任务执行的时候,就会自动创建一个jenkins slave pod。在本篇博文中,我们将介绍jenkins生成slave pod的另外一种方法,就是在pipeline脚本里面定义slave pod的镜像等,同时将Dockerfile、Jenkinsfile、YAML清单文件全部放到gitlab上面,通过jenkins的插件Blue Ocean自动创建多任务的Job。主要的流程包括如下:
- Jenkins服务器安装Blue Ocean插件;
- 开发人员提交代码到 Gitlab 代码私有仓库;
- 通过 Gitlab 配置的 Jenkins Webhook 触发 Pipeline 自动构建;
- 配置Blue Ocean,添加git地址和认证信息,创建多分支流水线任务;
- Jenkins 触发构建构建任务,根据 Pipeline 脚本定义分步骤构建;
- 先进行代码静态分析,单元测试;
- 然后进行 Maven 构建(Java 项目);
- 根据构建结果构建 Docker 镜像;
- 推送 Docker 镜像到 test 仓库;
- 触发更新服务阶段,使用 Helm 安装/更新 Release;
- 查看服务是否更新成功。
2. 实施过程:
??2.1 安装插件Blue Ocean:
??选择系统管理---插件管理---Available---输入Blue Ocean搜素插件名称---选择安装插件并重启服务生效
??2.2 编写DockerFile:
??这里的DockerFile主要用途是产生应用的镜像,并上传到私有仓库。一般一个微服务模块就是一个Dockerfile,也就是一个独立的镜像;我这边演示的一个demo程序,有两个微服务模块,所以就需要两个Dockerfile,文件分别放置到微服务pom.xml打包文件的同级目录;
??这个是后端应用的Dockerfile
# Start with a base image containing Java runtime
FROM openjdk:8-jdk-alpine
# Add Maintainer Info
LABEL maintainer="[email protected]"
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
ENV TZ=Asia/Shanghai
RUN mkdir /app
WORKDIR /app
COPY target/polls-0.0.1-SNAPSHOT.jar /app/polls.jar
# Make port 8080 available to the world outside this container
EXPOSE 8080
# Run the jar file
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app/polls.jar"]
??Dockerfile解释如下:
??1. 定义基础镜像,一般是alpine镜像;这种镜像比较小,此处使用jdk1.8的镜像;
??2. 定义环境变量,创建应用目录,拷贝maven编译之后的jar包到容器内的应用目录;
??3. 定义暴露端口,此处并不是真的暴露,只是描述;真正暴露的端口在yaml清单文件里面定义;
??4. 定义入口命令,也就是程序启动的命令;
??这个是前端应用的Dockerfile,前端应用是一个nginx服务器,将前端的html文件放在nginx根目录
FROM nginx:1.15.10-alpine
ADD build /usr/share/nginx/html
ADD nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
ENTRYPOINT ["nginx","-g","daemon off;"]
??Dockerfile解释如下:
??1. 定义基础镜像,一般是alpine镜像;这种镜像比较小;此处使用nginx镜像作为前端的web服务器;
??2. 添加网页html文件到nginx网页根目录;
??3. 定义暴露的端口,此处并不是真的暴露,只是描述;真正暴露的端口在yaml清单文件里面定义;
??4. 定义nginx的启动命令;
??nginx的配置文件如下:
server {
gzip on;
listen 80;
server_name localhost;
root /usr/share/nginx/html;
location / {
try_files $uri /index.html;
expires 1h;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
??2.3 编写YAML清单文件:
??如果是用helm部署应用的话,实际上是可以不用定义Deployment、service等资源清单文件的。只需要配置好helm的value.yaml就可以。但是helm的chart一般是别人写好的,要自己针对应用写一个chart,一般要熟悉go语言模板,学习成本比较高;
??后端的服务k8s配置清单文件:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: polling-server
namespace: sit
labels:
app: polling-server
spec:
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
type: RollingUpdate
template:
metadata:
labels:
app: polling-server
spec:
restartPolicy: Always
imagePullSecrets:
- name: myreg
containers:
- image: <IMAGE>:<IMAGE_TAG>
name: polling-server
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: api
env:
- name: DB_HOST
value: my-mysql.kube-system
- name: DB_PORT
value: "3306"
- name: DB_NAME
value: polling_app
- name: DB_USER
value: polling
- name: DB_PASSWORD
value: polling321
---
kind: Service
apiVersion: v1
metadata:
name: polling-server
namespace: sit
spec:
selector:
app: polling-server
type: ClusterIP
ports:
- name: api-port
port: 8080
targetPort: api
??yaml清单文件解释如下:
?? 1. 注意这里的镜像名称和镜像版本是变量的形式,是通过前面的Dockerfile build的镜像,每次的镜像版本不一样。
?? 2. 定义滚动更新策略,每次只能有一个pod副本不可用,每次只能升级一个pod副本;
?? 3. 定义镜像下载策略,这里是一直下载,也可以使用IfNotpresent,如果本地有镜像就不下载;
?? 4. 创建一个myreg的secrets,用于镜像认证的imagePullsecrets;
?? 5. 定义容器的镜像,端口,环境变量;
?? 6. 定义service,包括类型为ClusterIP,只能集群内访问,如果需要外部访问就要用到Ingress.定义集群端口和容器端口;
??2.4 编写Jenkinsfile:
def label = "slave-${UUID.randomUUID().toString()}"
def helmLint(String chartDir) {
println "校验 chart 模板"
sh "helm lint ${chartDir}"
}
def helmInit() {
println "初始化 helm client"
sh "helm init --client-only --stable-repo-url https://mirror.azure.cn/kubernetes/charts/"
}
def helmRepo(Map args) {
println "添加 myrepo repo"
sh "helm repo add --username ${args.username} --password ${args.password} myrepo http://k8s.harbor.test.site/chartrepo/system"
println "update repo"
sh "helm repo update"
println "fetch chart package"
sh """
helm fetch myrepo/polling
tar xzvf polling-0.1.0.tgz
"""
}
def helmDeploy(Map args) {
helmInit()
helmRepo(args)
if (args.dry_run) {
println "Debug 应用"
sh "helm upgrade --dry-run --debug --install ${args.name} ${args.chartDir} --set persistence.persistentVolumeClaim.database.storageClass=dynamic --set database.type=external --set database.external.database=polling_app --set database.external.username=polling --set database.external.password=polling321 --set api.image.repository=${args.image} --set api.image.tag=${args.tag} --set imagePullSecrets[0].name=myreg --namespace=${args.namespace}"
} else {
println "部署应用"
sh "helm upgrade --install ${args.name} ${args.chartDir} --set persistence.persistentVolumeClaim.database.storageClass=dynamic --set database.type=external --set database.external.database=polling_app --set database.external.username=polling --set database.external.password=polling321 --set api.image.repository=${args.image} --set api.image.tag=${args.tag} --set imagePullSecrets[0].name=myreg --namespace=${args.namespace}"
echo "应用 ${args.name} 部署成功. 可以使用 helm status ${args.name} 查看应用状态"
}
}
podTemplate(label: label, containers: [
containerTemplate(name: ‘maven‘, image: ‘maven:3.6-alpine‘, command: ‘cat‘, ttyEnabled: true),
containerTemplate(name: ‘docker‘, image: ‘docker‘, command: ‘cat‘, ttyEnabled: true),
containerTemplate(name: ‘kubectl‘, image: ‘cnych/kubectl‘, command: ‘cat‘, ttyEnabled: true),
containerTemplate(name: ‘helm‘, image: ‘cnych/helm‘, command: ‘cat‘, ttyEnabled: true)
], volumes: [
hostPathVolume(mountPath: ‘/root/.m2‘, hostPath: ‘/var/run/m2‘),
hostPathVolume(mountPath: ‘/home/jenkins/.kube‘, hostPath: ‘/root/.kube‘),
hostPathVolume(mountPath: ‘/var/run/docker.sock‘, hostPath: ‘/var/run/docker.sock‘)
]) {
node(label) {
def myRepo = checkout scm
def gitCommit = myRepo.GIT_COMMIT
def gitBranch = myRepo.GIT_BRANCH
def imageTag = sh(script: "git rev-parse --short HEAD",returnStdout:true).trim()
def dockerRegistryUrl = "k8s.harbor.test.site"
def imageEndpoint = "system/polling-app-server"
def image = "${dockerRegistryUrl}/${imageEndpoint}"
stage(‘单元测试‘) {
input id: ‘ncpprd‘, message: ‘发布生产请找-张三--批准?‘, ok: ‘确认‘, submitter: ‘admin‘
echo "1.测试阶段"
}
stage(‘代码编译打包‘) {
try {
container(‘maven‘) {
echo "2. 代码编译打包阶段"
sh "cd polling-app-server && mvn clean package -Dmaven.test.skip=true"
}
} catch (exc) {
println "构建失败 - ${currentBuild.fullDisplayName}"
throw (exc)
}
}
stage(‘构建 Docker 镜像‘) {
withCredentials ([[$class: ‘UsernamePasswordMultiBinding‘,
credentialsId: ‘k8sharbor‘,
usernameVariable: ‘DOCKER_HUB_USER‘,
passwordVariable: ‘DOCKER_HUB_PASSWORD‘]]) {
container(‘docker‘) {
echo "3. 构建 Docker 镜像阶段"
sh """
docker login ${dockerRegistryUrl} -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD}
cd polling-app-server && docker build -t ${image}:${imageTag} .
docker push ${image}:${imageTag}
"""
}
}
}
// stage(‘运行 Kubectl‘) {
// container(‘kubectl‘) {
// echo "查看 K8S 集群 Pod 列表"
// sh "kubectl get pods"
// sh """
// sed -i "s#<IMAGE>#${image}#g" polling-app-server/polling-app-server.yaml
// sed -i "s#<IMAGE_TAG>#${imageTag}#g" polling-app-server/polling-app-server.yaml
// kubectl apply -f polling-app-server/polling-app-server.yaml
// """
// }
//}
stage(‘运行 Helm‘) {
withCredentials([[$class: ‘UsernamePasswordMultiBinding‘,
credentialsId: ‘k8sharbor‘,
usernameVariable: ‘DOCKER_HUB_USER‘,
passwordVariable: ‘DOCKER_HUB_PASSWORD‘]]) {
container(‘helm‘) {
// todo,也可以做一些其他的分支判断是否要直接部署
echo "4. [INFO] 开始 Helm 部署"
helmDeploy(
dry_run : false,
name : "polling",
chartDir : "polling",
namespace : "prd",
tag : "${imageTag}",
image : "${image}",
username : "${DOCKER_HUB_USER}",
password : "${DOCKER_HUB_PASSWORD}"
)
echo "[INFO] Helm 部署应用成功..."
}
}
}
}
}
??注意这里的Jenkinsfile文件名称,并且这个文件一定要放置在gitlab项目目录的根目录下面;关于Jenkinsfile的配置解释如下:
- 第一行定义jenkins slave pod的名称是随机生成的;
- 紧接着定义一个helmLint的函数,用途是检查helm模板的语法是否正确;
- 紧接着定义helmInit函数,用途是在jenkins slave pod里面安装一个helm的命令,并且使用微软的azure仓库;
- 紧接着定义helmRepo函数,此函数执行的时候需要传参数进去,函数用到了用途是添加内部的helm仓库,并且下载helm包。前提是必须定义好helm包里面的value.yaml文件,并且将helm包打包上传到私有仓库;
- 紧接着定义helmDeploy函数,此函数执行的时候需要传参数进去,如果传了dry_run干跑的参数,就是不实际部署helm,只是运行检查;同时helm命令可以通过--set来设置value.yaml文件的内容;
- 定义Pod模板,这里的slave pod定义就和上次在Jenkins系统管理---kubernetes云接入---Pod模板的方法不一样,这里的salve pod定义更灵活。可以定义一个pod模板,定义需要的镜像。比如JAVA项目需要maven打包,就需要maven镜像;同样将kubelet、docker命令的镜像和挂载的配置文件和socket文件;
- 从node(label)开始jenkins pipeline流程,定义git的使用,这里用到了插件 checkout。定义变量imageTag,获取git的提交编号;定义docker仓库,定义镜像的名称;
- 开始单元测试,生产环境使用了批准的机制;代码编译,需要进入到代码模块目录打包;
- 构建docker镜像,使用插件来完成credentialsId用户名和密码的定义,构建镜像和推送到私有仓库;
10.可以选择使用kubectl命令部署应用,前期是有yaml清单文件;
11.也可以选择使用helm部署,执行helmDepoly函数的时候,可以传入参数进去;
12.关于helm的仓库,harbor自带了helm仓库,需要将chart包上传到helm仓库;
helm repo add --username=admin --password=Harbor12345 myrepo http://k8s.harbor.test.site/chartrepo/system
helm push polling-helm/ myreop
??2.5 GitLab和jenkins结合web-hook配置
??2.6 Blue Ocean创建多分支JOB
??2.7 GitLab和jenkins结合web-hook配置
??2.8 代码引用
??本文档中的代码引用地址如下:
??helm chart: https://github.com/cnych/polling-helm
??代码引用:https://github.com/callicoder/spring-security-react-ant-design-polls-app。
博文的更详细内容请关注我的个人微信公众号 “云时代IT运维”,本公众号旨在共享互联网运维新技术,新趋势; 包括IT运维行业的咨询,运维技术文档分享。重点关注devops、jenkins、zabbix监控、kubernetes、ELK、各种中间件的使用,比如redis、MQ等;shell和python等运维编程语言;本人从事IT运维相关的工作有十多年。2008年开始专职从事Linux/Unix系统运维工作;对运维相关技术有一定程度的理解。本公众号所有博文均是我的实际工作经验总结,基本都是原创博文。我很乐意将我积累的经验、心得、技术与大家分享交流!希望和大家在IT运维职业道路上一起成长和进步;
原文地址:https://blog.51cto.com/zgui2000/2396800