本文我们将基于Jenkins和Kubernetes流水线过程实现自动化构建Java应用,即把流水线的过程转换为pipeline语法,实现应用的自动发版。
一、 创建Java测试用例本书为读者准备了一个简单的Java测试用例,用于读者的学习,可以从 https://github.com/andanyoung/springboot-hello.git 找到该项目(也可以使用公司的Java项目)。
接下来将该项目导入自己的GitLab中。首先找到之前创建的Kubernetes组,然后单击New project,如图1所示。
图1 创建测试项目
选择Import project,如图2所示。
图2 导入项目
单击Repo by URL
图3 导入Java测试项目
在Git repository URL中输入示例地址,然后单击Create project,如图4所示。
图4 创建Project
导入后,结果如图5所示。
图5 查看导入的项目
二、定义JenkinsfileJenkinsfile定义流水线的步骤包括拉取代码、执行构建、生成镜像、更新Kubernetes资源等。本书将Jenkinsfile放置于项目源代码一并管理,也可以单独放置于一个Git仓库进行管理。
接下来在GitLab的源代码中添加Jenkinsfile。首先单击代码首页的“+”,然后单击New file,如图6所示。
图6 添加文件
在窗口中添加如下内容(完整内容见本书代码):
pipeline {
...
parameters {
gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue: '', description: 'Branch for build and deploy', name: 'BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'PT_BRANCH')
}
}
这个Jenkinsfile比较长,接下来我们分别解析一下。
三、 Jenkinsfile详解首先是顶层的agent,定义Kubernetes的Pod作为Jenkins的Slave:
agent {
# 定义使用Kubernetes作为agent
kubernetes {
选择的云为之前配置的名字
cloud 'kubernetes-study'
slaveConnectTimeout 1200
#将workspace改成hostPath,因为该Slave会固定节点创建,如果有存储可用,可以改成PVC的模式
workspaceVolume hostPathWorkspaceVolume(hostPath: "/opt/workspace", readOnly: false)
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
# jnlp容器,和Jenkins主节点通信
- args: ['$(JENKINS_SECRET)', '$(JENKINS_NAME)']
image: 'registry.cn-beijing.aliyuncs.com/citools/jnlp:alpine'
name: jnlp
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: "/etc/localtime"
name: "localtime"
readOnly: false
# build容器,包含执行构建的命令,比如Java的需要mvn构建,就可以用一个maven的镜像
- command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
# 使用Maven镜像,包含mvn工具。NodeJS可以用node的镜像
image: "registry.cn-beijing.aliyuncs.com/citools/maven:3.5.3"
imagePullPolicy: "IfNotPresent"
# 容器的名字,流水线的stage可以直接使用该名字
name: "build"
tty: true
volumeMounts:
- mountPath: "/etc/localtime"
name: "localtime"
# Pod单独创建了一个缓存的volume,将其挂载到了maven插件的缓存目录,默认是/root/.m2
- mountPath: "/root/.m2/"
name: "cachedir"
readOnly: false
# 发版容器,因为最终是发版至Kubernetes的,所以需要有一个kubectl命令
- command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
# 镜像的版本可以替换为其他的版本,也可以不替换,因为只执行set命令,所以版本是兼容的
image: "registry.cn-beijing.aliyuncs.com/citools/kubectl:self-1.17"
imagePullPolicy: "IfNotPresent"
name: "kubectl"
tty: true
volumeMounts:
- mountPath: "/etc/localtime"
name: "localtime"
readOnly: false
# 用于生成镜像的容器,需要包含docker命令
- command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
image: "registry.cn-beijing.aliyuncs.com/citools/docker:20.10.9-git"
imagePullPolicy: "IfNotPresent"
name: "docker"
tty: true
volumeMounts:
- mountPath: "/etc/localtime"
name: "localtime"
readOnly: false
# 由于容器没有启动docker服务,因此将宿主机的docker直接挂载至容器即可
- mountPath: "/var/run/docker.sock"
name: "dockersock"
readOnly: false
restartPolicy: "Never"
# 固定节点部署
nodeSelector:
build: "true"
securityContext: {}
volumes:
# Docker socket volume
- hostPath:
path: "/var/run/docker.sock"
name: "dockersock"
- hostPath:
path: "/usr/share/zoneinfo/Asia/Shanghai"
name: "localtime"
# 缓存目录
- name: "cachedir"
hostPath:
path: "/opt/m2"
'''
}
之后看一下Jenkinsfile最后的环境变量和parameters的配置:
# 定义一些全局的环境变量
environment {
COMMIT_ID = ""
HARBOR_ADDRESS = "10.0.0.204" # Harbor地址
REGISTRY_DIR = "kubernetes" # Harbor的项目目录
IMAGE_NAME = "spring-boot-project" # 镜像的名称
NAMESPACE = "kubernetes" # 该应用在Kubernetes中的命名空间
TAG = "" # 镜像的Tag,在此用BUILD_TAG+COMMIT_ID组成
}
parameters {
# 之前讲过一些choice、input类型的参数,本次使用的是GitParameter插件
# 该字段会在Jenkins页面生成一个选择分支的选项
gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue: '', description: 'Branch for build and deploy', name: 'BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'PT_BRANCH')
}
接下来是拉取代码的stage,这个stage是一个并行的stage,因为考虑了该流水线是手动触发还是自动触发:
stage('Pulling Code') {
parallel {
stage('Pulling Code by Jenkins') {
when {
expression {
# 若env.gitlabBranch为空,则该流水线为手动触发,执行该stage;若不为空,则会执行同级的另一个stage
env.gitlabBranch == null
}
}
steps {
# 这里使用git插件拉取代码,BRANCH变量取自前面介绍的parameters配置
# git@xxxxxx:root/spring-boot-project.git代码地址
# credentialsId: 'gitlab-key',之前创建的拉取代码的key
git(changelog: true, poll: true, url: 'git@xxxxxx:root/ spring-boot-project.git', branch: "${BRANCH}", credentialsId: 'gitlab-key')
script {
# 定义一些变量用于生成镜像的Tag
# 获取最近一次提交的Commit ID
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
# 赋值给TAG变量,后面的docker build可以取到该TAG的值
TAG = BUILD_TAG + '-' + COMMIT_ID
println "Current branch is ${BRANCH}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
}
}
}
stage('Pulling Code by trigger') {
when {
expression {
# 如果env.gitlabBranch不为空,说明该流水线是通过webhook触发的,则此时执行该stage,上述的stage不再执行。此时BRANCH变量为空
env.gitlabBranch != null
}
}
steps {
# 以下配置和前面一致,只是此时branch: env.gitlabBranch取的值为env.gitlabBranch
git(url: 'git@xxxxxxxxxxx:root/spring-boot-project.git', branch: env.gitlabBranch, changelog: true, poll: true, credentialsId: 'gitlab-key')
script {
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
TAG = BUILD_TAG + '-' + COMMIT_ID
println "Current branch is ${env.gitlabBranch}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
}
}
}
}
}
代码拉下来后,就可以执行构建命令,由于本次实验是Java示例,因此需要使用mvn命令进行构建:
stage('Building') {
steps {
# 使用Pod模板里面的build容器进行构建
container(name: 'build') {
sh """
# 编译命令,需要根据自己项目的实际情况进行修改,可能会不一致
mvn clean install -DskipTests
# 构建完成后,一般情况下会在target目录下生成jar包
ls target/*
"""
}
}
}
生成编译产物后,需要根据该产物生成对应的镜像,此时可以使用Pod模板的docker容器:
stage('Docker build for creating image') {
# 首先取出HARBOR的账号和密码
environment {
HARBOR_USER = credentials('HARBOR_ACCOUNT')
}
steps {
# 指定使用docker容器
container(name: 'docker') {
sh """
# 执行build命令,Dockerfile会在下一小节创建,也是放在代码仓库,和Jenkinsfile同级
docker build -t ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} .
# 登录Harbor,HARBOR_USER_USR和HARBOR_USER_PSW由上述environment生成
docker login -u ${HARBOR_USER_USR} -p ${HARBOR_USER_PSW} ${HARBOR_ADDRESS}
# 将镜像推送至镜像仓库
docker push ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG}
"""
}
}
}
最后一步就是将该镜像发版至Kubernetes集群中,此时使用的是包含kubectl命令的容器:
stage('Deploying to k8s') {
# 获取连接Kubernetes集群证书
environment {
MY_KUBECONFIG = credentials('study-k8s-kubeconfig')
}
steps {
# 指定使用kubectl容器
container(name: 'kubectl'){
sh """
# 直接set更改Deployment的镜像即可
/usr/local/bin/kubectl --kubeconfig $MY_KUBECONFIG set image deploy -l app=${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n $NAMESPACE
"""
}
}
}
}
四、 定义Dockerfile注意
本例发版的命令为/usr/local/bin/kubectl --kubeconfig M Y K U B E C O N F I G s e t i m a g e d e p l o y − l a p p = MY_KUBECONFIG set image deploy -l app= MYKUBECONFIGsetimagedeploy−lapp={IMAGE_NAME} I M A G E N A M E = {IMAGE_NAME}= IMAGENAME={HARBOR_ADDRESS}/ R E G I S T R Y D I R / {REGISTRY_DIR} / REGISTRYDIR/{IMAGE_NAME}{TAG} -n $NAMESPACE。由于本书示例的Deployment名称、Label、容器名称均和项目名称一致,因此就统一了IMAGE_NAME,如果读者不是按照此规范命名的,就需要进行更改,可以参考本书5.2节Deployment部分的kubectl set命令。
在执行流水线过程时,需要将代码的编译产物做成镜像。Dockerfile主要写的是如何生成公司业务的镜像,而本示例是Java项目,只需要把JAR包放在有JRE环境的镜像中,然后启动该JAR包即可:
# 基础镜像可以按需修改,可以更改为公司自有的镜像 FROM registry.cn-beijing.aliyuncs.com/dotbalo/jre:8u211-data # JAR包名称改成实际的名称,本示例为spring-cloud-eureka-0.0.1-SNAPSHOT.jar COPY target/spring-cloud-eureka-0.0.1-SNAPSHOT.jar ./ # 启动JAR包 CMD java -jar spring-cloud-eureka-0.0.1-SNAPSHOT.jar五、 定义Kubernetes资源
本书的示例在GitLab创建的Group为kubernetes,可以认为其是一个项目,同一个项目可以部署至Kubernetes集群中的同一个Namespace中,本示例为kubernetes命名空间。由于使用的是私有仓库,因此需要先配置拉取私有仓库镜像的密钥:
# kubectl create ns kubernetes namespace/kubernetes created # kubectl create secret docker-registry harborkey --docker-server=CHANGE_HERE_FOR_YOUR_HARBOR_ADDRESS --docker-username=admin --docker-password=Harbor12345 --docker-email=mail@kubeasy.com -n kubernetes secret/harborkey created
配置该应用的Deployment(部分内容),需要注意文件的文字注释部分:
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: spring-boot-project # Deployment标签,和流水线的set -l一致
name: spring-boot-project # Deployment名称
namespace: kubernetes
spec:
replicas: 1
selector:
matchLabels:
app: spring-boot-project # Pod的标签
template:
metadata:
creationTimestamp: null
labels:
app: spring-boot-project # Pod的标签
spec:
...
- name: LANG
value: C.UTF-8
image: nginx # 此处使用的nginx作为原始的镜像,通过Jenkins构建并发版后,变成Java应用的镜像
imagePullPolicy: IfNotPresent
lifecycle: {}
livenessProbe:
failureThreshold: 2
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
tcpSocket:
port: 8761 # 端口号和健康检查按照实际情况进行修改
timeoutSeconds: 2
name: spring-boot-project # 容器的名称,需要和流水线set命令的容器名称一致
ports:
- containerPort: 8761 # 端口号按照实际情况进行修改
name: web
protocol: TCP
readinessProbe:
failureThreshold: 2
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
tcpSocket:
port: 8761 # 端口号和健康检查按照实际情况进行修改
timeoutSeconds: 2
resources: # 资源请求按照实际情况修改
limits:
cpu: 994m
memory: 1170Mi
requests:
cpu: 10m
memory: 55Mi
dnsPolicy: ClusterFirst
imagePullSecrets:
- name: harborkey # Harbor仓库密钥,需要和上述创建的Secret一致
restartPolicy: Always
securityContext: {}
serviceAccountName: default
定义该应用的Service和Ingress:
---
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: spring-boot-project
name: spring-boot-project
namespace: kubernetes
spec:
ports: # 端口按照实际情况进行修改
- name: web
port: 8761
protocol: TCP
targetPort: 8761
selector:
app: spring-boot-project
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}
---(部分内容)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
creationTimestamp: null
name: spring-boot-project
...
path: /
pathType: ImplementationSpecific
需要注意的是,在实际使用时,Java一般是后台应用,不需要使用Ingress暴露出去,只需要配置Service即可。如果需要被集群外部的用户直接访问,需要配置Ingress,通过域名发布。
创建该资源:
# kubectl create -f spring-boot-project.yaml service/spring-boot-project created ingress.networking.k8s.io/spring-boot-project created deployment.apps/spring-boot-project created



