上篇我们讲解了如何将.netCore程序以Docker部署的两种方法,https://www.jianshu.com/p/a6c78a2c15f2,这是我们本文自动化部署k8s的基础,便于我们理解自动化过程主要的步骤。
今天我们尝试与Jenkins集成k8s自动化发布K8S集群节点中。
基本思路:
发起流水线构造时我们传入一系列参数(JSON结构),然后由jenkins从git拉取流水线脚本,脚本根据我们传入的参数对脚本进行动态替换并执行,构建完docker镜像后,将镜像推送到私有仓库,然后我们会根据json数据,docker镜像路径等数据动态生成k8s的部署yaml文件并交由k8s执行部署。
这一系列构建过程是由jenkins连接在k8s 某一个master节点进行执行。
1.准备工作
我们没有搭建自己的GIT代码仓库,为了测试我们使用Gitee进行托管(github太慢)
Jenkins下载并安装插件
下载gitee插件,配置gitee凭据
此外脚本中使用了readJSON,writeJSON
要使用这两个方法,必须安装插件Pipeline Utility Steps,否则报错:java.lang.NoSuchMethodError: No such DSL method 'readJSON'
另外需要安装pipline的基本插件:
Pipeline: GitHub
Pipeline: Basic Steps
Jenkins创建gitee帐户凭据
然后在jenkins中创建一个凭据供使用:
Jenkins添加k8s Master节点,用于部署
jenkins 系统管理--节点管理,添加一个节点
节点名称及标签取为:k8s-master
master节点安装依赖项
yum install lttng-ust libcurl openssl-libs krb5-libs libicu zlib -y
master节点安装JDK
我们把JDK包直接放到 /root/jenkins/jdk目录即可,jenkins我们前面指定了节点工作目录是/root/jenkins,则添加节点时jenkins会自动查找到这个jdk目录。
master节点安装git
由于我们使用master节点拉取代码,master节点需要安装git,运行以下命令安装
yum install -y git
master节点放置dotnetsdk3.1包
从微软下载https://dotnet.microsoft.com/download/dotnet-core/3.1
目录:/root/jenkins/tools/dotnetsdk3.1
mkdir /root/jenkins/tools/dotnetsdk3.1
cd /root/jenkins/tools
rz 上传压缩包dotnet-sdk-3.1.102-linux-x64.tar.gz
tar -zxf dotnet-sdk-3.1.102-linux-x64.tar.gz -C dotnetsdk3.1
2.Jenkins创建流水线构建模板
创建模板的好处是后续其他流水线可以直接使用该模板脚本,不用重复配置,实现标准化。
Jenkins中新建一个任务,模板选择【流水线】
添加一文本参数
Jenkins中的参数是,用于向流水线脚本传递动态数据,参数可以简单理解为脚本替换用的占位符。
说明:因为模板只是定义了整体执行过程,模板不关注项目信息,比如项目服务名,JDK/.net版本,代码路径,部署资源,发布后的名称等信息,这些信息我们可以通过参数形式传递进来。
通常我们会定义定义很多个文本参数,但每一参数都定义一个的模式我们传参麻烦,另外也不易扩展,我这里采用一个文本参数类型(内容用JSON结构)解决所有,避免频繁的变更模板,因为现实中参数的变化的频率是比较高的。
参数名称我们命名为JSON_BODY,值随意写一个JSON结构,因为是一个模板,我们这里的值只是一个参考,实际上是会被使用该模板的流水线重写掉的。
流水线脚本我们采用可以采用从GIT拉取,也可以直接编写脚本,为了便于脚本管理与更新,我们采用GIT来管理,目前测试我们托管到Gitee平台。
流水线脚本参考
println("#############################################开始流水线##################################################")
//env.JOB_NAME ***
//env.WORKSPACE /var/jenkins_home/workspace/***
//env.K8S_TYPE="$params.K8S_TYPE"
//env.DOCKER_TYPE="$params.DOCKER_TYPE"
def jobName = "${JOB_NAME}"
def defaultEnv,defaultApiHost,defaultImageHost,defaultApolloMeta, imageVersion,versionTimestamp, codeUrl, branch, appId, serviceAndversion, sdkVersion, namespace, nodes, host, replicas, cpu, memory, sonarUrl, sonarKey, sonarToken, sonarResult, devopsUrl, version, service, imageName,nodePort
try {
def paraBodyJson = readJSON text: "${params.JSON_BODY}"
defaultImageHost = paraBodyJson.defaultImageHost
if (!defaultImageHost?.trim()) {
println("defaultImageHost 为空")
defaultImageHost = "192.168.101.101:30083/janet/";
}
service = paraBodyJson.service
if (!service?.trim()) {
println("service 不能为空")
sh "exit 1"
}
version = paraBodyJson.version
if (!version?.trim()) {
println("version 不能为空")
sh "exit 1"
}
serviceAndversion = service + "-" + version
println("service-version:"+serviceAndversion)
appId = paraBodyJson.appId
if (!appId?.trim()) {
println("appId 不能为空")
sh "exit 1"
}
nodePort=paraBodyJson.nodePort
if (!nodePort?.trim()) {
println("nodePort 不能为空")
sh "exit 1"
}
codeUrl = paraBodyJson.codeUrl
imageVersion = paraBodyJson.imageVersion
if (!codeUrl?.trim() && !imageVersion?.trim()) {
println("codeUrl和imageVersion 不能同时为空")
sh "exit 1"
}
//分支
branch = paraBodyJson.branch
if (!branch?.trim()) {
branch = "master"
}
namespace = paraBodyJson.namespace
if (!namespace?.trim()) {
println("namespace 不能为空")
sh "exit 1"
}
//版本
versionTimestamp = paraBodyJson.versionTimestamp
if (!versionTimestamp?.trim()) {
versionTimestamp = version + "." + System.currentTimeMillis()
}
//sdk版本,java/.net均有不同的SDK版本,如果不填写默认jdk8
sdkVersion = paraBodyJson.sdkVersion
if (!sdkVersion?.trim()) {
sdkVersion = "openjdk8"
}
defaultApolloMeta=paraBodyJson.defaultApolloMeta;
nodes = paraBodyJson.node
if (!nodes?.trim()) {
nodes = ""
}
replicas = paraBodyJson.replicas
if (!replicas?.trim()) {
replicas = "1"
}
cpu = paraBodyJson.cpu
if (!cpu?.trim()) {
cpu = "0"
}
memory = paraBodyJson.memory
if (!memory?.trim()) {
memory = "0"
}
defaultApiHost = paraBodyJson.defaultApiHost
if (!defaultApiHost?.trim()) {
println("defaultApiHost 为空")
defaultApiHost = "api.test.com";
}
host = paraBodyJson.host
if (!host?.trim()) {
host = defaultApiHost
}
} catch (errx) {
println("参数解析错误" + errx)
sh "exit 1"
}
println("参数解析完毕,开始构建准备")
//使用k8s节点执行
node('k8s-master') {
imageName = defaultImageHost + service + ":" +versionTimestamp
stage('Clone Code') {
println("#############################################开始拉取代码##################################################")
sh 'find /root/.m2/repository/ -name "*lastUpdated*" | xargs rm -rf'
git branch: branch, url: codeUrl
println("#############################################拉取代码成功##################################################")
}
stage('Dotnet Build') {
println("#############################################开始打包##################################################")
def dockerfile = """
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
ENV ASPNETCORE_URLS http://+:8020
EXPOSE 8020
COPY ./build .
RUN sed -i 's/TLSv1.2/TLSv1.0/g' /etc/ssl/openssl.cnf
ENTRYPOINT ["dotnet", "{{dllname}}"]
"""
if ("dotnetsdk3.1".equals(sdkVersion.toString())) {
dockerfile = dockerfile.replace("aspnet:3.1", "aspnet:3.1")
}
else if("dotnetsdk2.0".equals(sdkVersion.toString())) {
dockerfile = dockerfile.replace("aspnet:3.1", "aspnet:2.0")
}
withEnv(["DOTNET_HOME=/root/jenkins/tools/${sdkVersion}"]) {
sh '"$DOTNET_HOME/dotnet" --version'
def csprojname = sh(script: 'echo *.csproj', returnStdout: true).replace('\n', "")
def out=sh(script:"ls "+csprojname,returnStatus:true)
if(out == 2){
println("文件:" + csprojname+" 不存在")
sh "exit 1"
}
// sh '"$DOTNET_HOME/dotnet" restore '+csprojname+' -s http://xxxx.com/repository/nuget-group' //如果有私有nuget仓库可以加上-s 仓库地址
sh '"$DOTNET_HOME/dotnet" restore '+csprojname
sh '"$DOTNET_HOME/dotnet" build '+csprojname+' -c Release -o ./build '
sh 'rm -rf ${WORKSPACE}/docker'
sh 'mkdir -p ${WORKSPACE}/docker'
sh 'cp -r ${WORKSPACE}/build/ ./docker/'
def dllname = csprojname.replace("csproj","dll")
out=sh(script:"ls ./build/"+dllname,returnStatus:true)
if(out == 2){
println("文件:" + dllname+" 不存在")
sh "exit 1"
}
dockerfile = dockerfile.replace("{{dllname}}", dllname)
sh "echo '${dockerfile}' >./docker/Dockerfile"
}
println("#############################################打包成功##################################################")
}
stage('Build Image') {
println("#############################################开始build docker镜像##################################################")
sh "docker build -t ${imageName} ${WORKSPACE}/docker/."
sh "docker push ${imageName}"
println("#############################################build docker镜像件成功##################################################")
}
stage('K8S Deploy') {
println("#############################################开始部署到集群##################################################")
def yamldir = "/root/jenkins/deploy/deploy-"
def yamlTemplatedir = "/root/jenkins/deploy-template/deploy-project.yaml"
println(yamlTemplatedir)
def fileContents = readFile file: yamlTemplatedir, encoding: "UTF-8"
println("开始处理yaml模板文件");
fileContents = fileContents.replace("{{namespace}}", namespace)
fileContents = fileContents.replace("{{name}}", serviceAndversion)
fileContents = fileContents.replace("{{replicas}}", replicas)
fileContents = fileContents.replace("{{cpu}}", cpu)
fileContents = fileContents.replace("{{host}}", host)
fileContents = fileContents.replace("{{memory}}", memory)
//println("yaml02...")
fileContents = fileContents.replace("{{image}}", imageName)
fileContents = fileContents.replace("{{appId}}", appId)
// println("yaml03...")
fileContents = fileContents.replace("{{apolloMeta}}", defaultApolloMeta)
fileContents = fileContents.replace("{{nodePort}}", nodePort)
println("yaml模板处理完毕")
println "${yamldir}${serviceAndversion}.yaml"
sh "rm -rf ${yamldir}${serviceAndversion}.yaml"
sh "echo '${fileContents}' >${yamldir}${serviceAndversion}.yaml"
println "kubectl apply -f ${yamldir}${serviceAndversion}.yaml"
//sh "kubectl apply -f /root/jenkins/deployment/deployment-${serviceAndversion}.yaml"
def consoleApply = sh(script: 'kubectl apply -f '+yamldir + serviceAndversion + '.yaml', returnStdout: true)
String[] consoleArr = consoleApply.split("\n|\r")
for (console in consoleArr) {
/* if(console.startsWith("deployment.apps") && console.endsWith("unchanged")){
println("#############################################部署到集群没有变化,流水线退出##################################################")
sh "exit 1"
}*/
}
sleep 10
println("#############################################部署到集群成功##################################################")
}
println("#############################################流水线执行成功##################################################")
}
/root/jenkins/deploy-template/deploy-project.yaml文件模板参考:
这个Yaml文件就是一个yaml程序完整部署的模板,我们通过JSON传入,然后进行替换,再交由k8s执行。
#create namespace
apiVersion: v1
kind: Namespace
metadata:
name: {{namespace}}
spec:
finalizers:
- kubernetes
---
#deploy
apiVersion: apps/v1
kind: Deployment
#kind: StatefulSet
metadata:
name: {{name}}
namespace: {{namespace}}
spec:
selector:
matchLabels:
app: {{name}}
replicas: {{replicas}}
#serviceName: {{name}}
template:
metadata:
labels:
app: {{name}}
spec:
containers:
- name: {{name}}
image: {{image}}
imagePullPolicy: Always
env:
- name: image
value: "{{name}}>{{image}}"
- name: app.id
value: "{{appId}}"
- name: TZ
value: Asia/Shanghai
ports:
- containerPort: 8020
resources:
limits:
cpu: {{cpu}}
memory: {{memory}}
requests:
cpu: {{cpu}}
memory: {{memory}}
livenessProbe:
httpGet:
path: /healthy
port: 8020
scheme: HTTP
initialDelaySeconds: 30
periodSeconds: 60
failureThreshold: 2
successThreshold: 1
timeoutSeconds: 30
readinessProbe:
httpGet:
path: /healthy
port: 8020
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 2
successThreshold: 1
timeoutSeconds: 10
---
#service
apiVersion: v1
kind: Service
metadata:
name: {{name}}
namespace: {{namespace}}
spec:
ports:
- port: 80
protocol: TCP
targetPort: 8020
nodePort: {{nodePort}}
selector:
app: {{name}}
type: NodePort
sessionAffinity: ClientIP
---
#router 配合kong/nginx等任一网关使用,可以对外暴露统一API
#如service1 api.test.com/server1/user/gettoken
#如service2 api.test.com/service2/bill/getBill
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: {{name}}
namespace: {{namespace}}
spec:
rules:
#host 网关域名或IP
- host: {{host}}
http:
paths:
#path路径,如service1
- path: /{{name}}/
backend:
serviceName: {{name}}
servicePort: 80
2.使用模板创建一个流水线
新建一流水线,取名netCore01,复制自template-netCore模板
点击保存,到下一步,无需任何修改,直接保存即可。
3 . netCore环境准备
** 下载.netcore sdk 3.1**
https://dotnet.microsoft.com/download/dotnet-core/3.1
下载到本地
上传sdk包并解压
master节点
mkdir /root/jenkins/tools/dotnetsdk3.1
cd /root/jenkins/tools/
rz 上传压缩包
tar -zxf dotnet-sdk-3.1.102-linux-x64.tar.gz -C dotnetsdk3.1
查看及安装依赖项
yum install lttng-ust libcurl openssl-libs krb5-libs libicu zlib -y
4. 执行自动化部署
代码我们提交到git,然后Jenkins选择netCore01流水线,选择使用参数构建,输入JSON参数,如下图:
构建过程会下载aspnet:3.1-buster-slim,建议提前docker pull下载好,这样构建会快很多。
jenkins自动下载有点慢
Step 1/6 : FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
3.1-buster-slim: Pulling from dotnet/core/aspnet
68ced04f60ab: Pulling fs layer
4ddb1a571238: Pulling fs layer
我们通过jenkins控制台输出查看构建过程,已经成功。
[Pipeline] echo
#############################################部署到集群成功##################################################
[Pipeline] }
[Pipeline] // stage
[Pipeline] echo
#############################################流水线执行成功##################################################
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
访问测试下:
kubectl get pods --all-namespaces -owide
http://ip:31001/default/gettime #我们自己写的一个测试api
另外我们看下镜像仓库中,该镜像已经存在
k8s查看pod信息
[root@k8s-master Controllers]# kubectl get pods -n mydemos
NAME READY STATUS RESTARTS AGE
netcore-01-blue-7fdff4f9f7-9ll6p 1/1 Running 0 64s
附:JSON中主要参数解读:
codeUrl:当前项目git地址
sdkVersion:使用的.netcoreSDK版本
replicas:部署几个pod
branch:拉取代码的哪个分支
defaultImageHost:镜像仓库地址,镜像构建完成后需要推送到仓库,供pod所在节点获取生成容器。
nodePort 映射到主机的端口,如果你搭建了网关,可以不将端口映射到主机,可配合API网关+域名实现动态路由。