现有混合云平台的场景下,即有线下和线上的环境,又有测试与正式的场景,并且结合了Docker,致使打包内容有所区分,且服务的发布流程复杂起来,手工打包须要在编译阶段就要根据环境处处更改配置,所以纯手工发布增长了实施的难度,须要一个统一的适应各类环境部署的方案。html
手动/自动构建 -> Jenkins 调度 K8S API ->动态生成 Jenkins Slave pod -> Slave pod 拉取 Git 代码/编译/打包镜像 ->推送到镜像仓库 Harbor -> Slave工做完成,Pod 自动销毁 ->部署到测试或生产 Kubernetes(K8S)平台。前端
上面是理想情况下的将服务编译打包成镜像上传到镜像库后部署到Kubernetes平台的一个流程,但问题是:vue
就上面现实问题,咱们将发布流程简化:java
关键点:node
Docker镜像的打包使用com.spotify的docker-maven-plugin插件结合Dockerfile,调用远程服务器的Docker环境生成镜像。git
K8S服务部署采用的是ssh方式,将Deployment文件上传到K8S集群服务器,而后执行部署命令。程序员
以前也是用com.spotify的docker-maven-plugin插件来打包镜像并推送到私有镜像仓库,但问题是没法根据环境写条件判断,如动态选择是否须要启动pinpoint,线上线下库地址动态更换,致使镜像名前缀也是要动态变化的,此时直接配置没法知足,须要结合Dockerfile来实现。web
先更改pom文件,指定本项目的Dockerfile文件地址,默认是放在项目根目录下:spring
<plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>1.2.0</version> <configuration> <!--覆盖相同标签镜像--> <forceTags>true</forceTags> <!-- 与maven配置文件settings.xml一致 --> <serverId>nexus-releases</serverId> <!--私有仓库地址 --> <registryUrl>https://${docker.repostory}</registryUrl> <!--远程Docker地址 --> <dockerHost>http://10.3.87.210:2375</dockerHost> <!-- 注意imageName必定要是符合正则[a-z0-9-_.]的,不然构建不会成功 --> <!--指定镜像名称 仓库/镜像名:标签--> <imageName>${docker.repostory}/${project.artifactId}:${project.version}</imageName> <dockerDirectory>${project.basedir}</dockerDirectory> <resources> <resource> <!-- 指定要复制的目录路径,这里是当前目录 --> <!-- 将打包文件放入dockerDirectory指定的位置 --> <targetPath>/app/</targetPath> <!-- 指定要复制的根目录,这里是target目录 --> <directory>${project.build.directory}</directory> <!-- 指定须要拷贝的文件,这里指最后生成的jar包 --> <include>${project.build.finalName}.jar</include> </resource> </resources> </configuration> </plugin>
<registryUrl>https://${docker.repostory}</registryUrl>docker
指定远程仓库地址,在主项目的<properties>中指定,这里默认线上仓库<docker.repostory>39.95.40.97:5000</docker.repostory>
<dockerHost>http://10.3.87.210:2375</dockerHost>
指定Docker镜像打包服务器,这里指定线下服务器。
<imageName>${docker.repostory}/${project.artifactId}:${project.version}</imageName>
指定镜像名称 仓库/镜像名:标签
<dockerDirectory>${project.basedir}</dockerDirectory>
指定Dockerfile文件地址,此处指定项目根目录
Dockerfile内容
FROM join:0.2
MAINTAINER {description} Join
ADD /app/{artifactId}-{version}.jar /app/
ENTRYPOINT ["java", "-Xmx512m","-Dspring.profiles.active={active}",{jarparam} "-jar", "/app/{artifactId}-{version}.jar"]
基础镜像用join:0.2,里面包含了pinpoint和监控jvm的promethus客户端包。
Jarparam会在Jenkins中动态替换运行时参数,active 指定当前运行环境,这里可能有人提议根据项目yml文件中指定内容自动匹配,由于要考虑到若是自动匹配 更换线上线下环境就须要更改yml配置文件后又要上传到gitlab,如此没有必要多作一步,直接在Jenkins中看成参数指定最为便捷。
此处Dockerfile是通用模板,若是有特殊内容添加,可自行更改,此时的模板须要在Jenkins运行时替换参数后才有用,若是想直接在本机运行打包,可手动替换参数内容后运行:
clean package -DskipTests docker:build
推送
clean package -DskipTests docker:build -DpushImage
Pipeline也就是构建流水线,对于程序员来讲,最好的解释是:使用代码来控制项目的构建、测试、部署等。使用它的好处有不少,包括但不限于:
l 使用Pipeline能够很是灵活的控制整个构建过程;
l 能够清楚的知道每一个构建阶段使用的时间,方便构建的优化;
l 构建出错,使用stageView能够快速定位出错的阶段;
l 一个job能够搞定整个构建,方便管理和维护等。
Pipeline 支持两种语法,声明式和脚本式。这两种方法都支持构建持续交付流水线,均可以经过 web UI 或 Jenkinsfile 文件来定义 Pipeline(一般认为建立 Jenkinsfile 文件并上传到源代码控制仓库是最佳实践)
Jenkinsfile 就是一个包含对 Jenkins Pipeline 定义的文本文件,会上传到版本控制中。下面的 Pipeline 实现了基本的 3 段持续交付流水线。
声明式 Pipeline:
// Jenkinsfile (Declarative Pipeline) pipeline { agent any stages { stage('Build') { steps { echo 'Building..' } } stage('Test') { steps { echo 'Testing..' } } stage('Deploy') { steps { echo 'Deploying....' } } } }
对应的脚本式 Pipeline:
// Jenkinsfile (Scripted Pipeline) node { stage('Build') { echo 'Building....' } stage('Test') { echo 'Building....' } stage('Deploy') { echo 'Deploying....' } }
注意,全部的 Pipeline 都会有这三个相同的 stage,能够在全部项目的一开始就定义好它们。下面演示在 Jenkins 的测试安装中建立和执行一个简单的 Pipeline。
假设项目已经设置好了源代码控制仓库,而且已经按照入门章节的描述在 Jenkins 中定义好了 Pipeline。
使用文本编辑器(最好支持 Groovy 语法高亮显示),在项目根目录中建立 Jenkinsfile。
上面的声明式 Pipeline 示例包含了实现一个持续交付流水线所需的最少步骤。必选指令 agent 指示 Jenkins 为 Pipeline 分配执行程序和工做空间。没有 agent 指令的话,声明式 Pipeline 无效,没法作任何工做!默认状况下 agent 指令会确保源代码仓库已经检出,而且可用于后续步骤。
stage 和 step 指令在声明式 Pipeline 中也是必须的,用于指示 Jenkins 执行什么及在哪一个 stage 中执行。
对于脚本式 Pipeline 的更高级用法,上面的示例节点是相当重要的第一步,由于它为 Pipeline 分配了一个执行程序和工做空间。若是没有 node,Pipeline 不能作任何工做!在 node 内,业务的第一阶段是检出此项目的源代码。因为 Jenkinsfile 是直接从源代码控制中提取的,所以 Pipeline 提供了一种快速简单的方法来访问源代码的正确版本:
// Jenkinsfile (Scripted Pipeline)
node {
checkout scm
/* .. snip .. */
}
这个 checkout 步骤会从源代码控制中检查代码,scm 是特殊变量,它指示运行检出步骤,复制触发了此次 Pipeline 运行的指定版本。
最终的流程样式:
通常用声明式来构建流水,实际操做过程当中仍是发现脚本式构建更顺手,并且Groovy语言更方便查资料,所以下面以脚本构建为主演示一个流程。
1.新建任务
2.填写任务名和描述,因为防止构建历史太多,只保留3个。
3.添加构建时全局构建参数,用来构建流程动态选择环境,这里有两种方式,一种是直接在页面上添加,以下图,一种是在Jenkinsfile中添加(第一次构建时不会出现选项,第二次构建才会出现,所以首次构建须要试构建,暂停再刷新页面才会有选择框),两种最张效果同样,这里为了方便采用Jenkinsfile来添加全局参数。
Jenkinsfile中添加
properties([
parameters([string(name: 'PORT', defaultValue: '7082', description: '程序运行端口'),choice(name: 'ACTIVE_TYPE', choices: ['dev', 'prd', 'local'], description: '程序打包环境'),choice(name: 'ENV_TYPE', choices: ['online', 'offline'], description: '线上、仍是线下环境'),booleanParam(name: 'ON_PINPOINT', defaultValue: true, description: '是否添加Pinpoint监控'),booleanParam(name: 'ON_PROMETHEUS', defaultValue: true, description: '是否添加Prometheus监控'),string(name: 'EMAIL', defaultValue: '104@qq.com', description: '打包结果通知')])
])
4.选择源码代码库:
须要添加认证,将Jenkins的ssh秘钥添加到GitLab的页面中,且须要将此处gitlab中joint用户添加到须要拉取代码的项目中才有权限拉取代码。
Jenkinsfile位置放在项目的根目录。
5. Jenkinsfile中指定maven目录地址
MVNHOME = '/opt/maven354'
为防止手工填写项目名和版本号等一系列信息,所以直接读取pom文件中要编译项目的这些信息给全局变量:
pom = readMavenPom file: 'pom.xml' echo "group: ${pom.groupId}, artifactId: ${pom.artifactId}, version: ${pom.version} ,description: ${pom.description}" artifactId = "${pom.artifactId}" version = "${pom.version}" description = "${pom.description}"
根据选择的线上环境仍是线下环境,替换镜像仓库ip
if (params.ENV_TYPE == 'offline' || params.ENV_TYPE == null) { sh "sed -i 's#39.95.40.97:7806#10.3.87.51:8080#g' pom.xml" image = "10.3.87.51:8080/${artifactId}:${version}" }
6.编译
利用maven构建,利用上面的内容先替换掉Dockerfile、Deployment中的变量,再根据选择的条件是否启用pinpoint和promethus,最后编译。
def jarparam='' def pinname = artifactId if( pinname.length() > 23) { pinname = artifactId.substring(0,23) } //添加pinpoint if(params.ON_PINPOINT) { jarparam = '"-javaagent:/app/pinpoint-agent/pinpoint-bootstrap-1.8.0.jar","-Dpinpoint.agentId={pinname}", "-Dpinpoint.applicationName={pinname}",' } //添加prometheus if(params.ON_PROMETHEUS) { jarparam = jarparam + '"-javaagent:/app/prometheus/jmx_prometheus_javaagent-0.11.0.jar=1234:/app/prometheus/jmx.yaml",' } sh "sed -i 's#{jarparam}#${jarparam}#g' Dockerfile" sh "sed -i 's#{description}#${description}#g;s#{artifactId}#${artifactId}#g;s#{version}#${version}#g;s#{active}#${params.ACTIVE_TYPE}#g;s#{pinname}#${pinname}#g' Dockerfile" sh "sed -i 's#{artifactId}#${artifactId}#g;s#{version}#${version}#g;s#{port}#${params.PORT}#g;s#{image}#${image}#g' Deployment.yaml" sh "'${MVNHOME}/bin/mvn' -DskipTests clean package"
须要注意的是pinpoint的pinpoint.applicationName不能操做24个字符,不然启用不成功,所以超过的直接截断。
Department文件详情看后文。
跳过测试编译打包 '${MVNHOME}/bin/mvn' -DskipTests clean package 须要在Jenkins服务器安装maven环境,还有指定maven的jar包私有仓库地址。
7. Docker打包
前提是上一步指定pom文件中的镜像仓库和Dockerfile中的内容是替换后的完整内容。
sh "'${MVNHOME}/bin/mvn' docker:build"
8. 推送镜像
sh "'${MVNHOME}/bin/mvn' docker:push"
前面几步已经将项目打包并生成了镜像并推送到了私有仓库,下面就是部署服务到K8S集群。
先看看Department.yaml文件:
--- apiVersion: apps/v1 kind: Deployment metadata: name: {artifactId} namespace: default labels: app: {artifactId} version: {version} spec: selector: matchLabels: app: {artifactId} replicas: 1 template: metadata: labels: app: {artifactId} annotations: prometheus.io.jmx: "true" prometheus.io.jmx.port: "1234" spec: containers: - name: {artifactId} image: {image} # IfNotPresent\Always imagePullPolicy: Always ports: - name: prometheusjmx containerPort: 1234 livenessProbe: #kubernetes认为该pod是存活的,不存活则须要重启 httpGet: path: /health port: {port} scheme: HTTP initialDelaySeconds: 60 ## 设置为系统彻底启动起来所需的最大时间+若干秒 timeoutSeconds: 5 successThreshold: 1 failureThreshold: 5 readinessProbe: #kubernetes认为该pod是启动成功的 httpGet: path: /health port: {port} scheme: HTTP initialDelaySeconds: 40 ## 设置为系统彻底启动起来所需的最少时间 timeoutSeconds: 5 successThreshold: 1 failureThreshold: 5 env: - name: eureka-server value: "eureka-server.default.svc.cluster.local" - name: eureka-server-replica value: "eureka-server-replica.default.svc.cluster.local" resources: # 5%的CPU时间和700MiB的内存 requests: # cpu: 50m memory: 700Mi # 最多容许它使用 limits: # cpu: 100m memory: 1000Mi # 指定在容器中挂载路径 volumeMounts: - name: logs-volume mountPath: /logs - name: host-time mountPath: /etc/localtime readOnly: true - name: host-timezone mountPath: /etc/timezone readOnly: true - name: pinpoint-config mountPath: /app/pinpoint-agent/pinpoint.config volumes: - name: logs-volume hostPath: # 宿主机上的目录 path: /logs - name: host-time hostPath: path: /etc/localtime - name: host-timezone hostPath: path: /usr/share/zoneinfo/Asia/Shanghai - name: pinpoint-config configMap: name: pinpoint-config # 运行在指定标签的节点,前提是先给节点打标 kubectl label nodes 192.168.0.113 edgenode=flow # nodeSelector: # edgenode: flow --- apiVersion: v1 kind: Service metadata: name: {artifactId} namespace: default labels: app: {artifactId} version: {version} spec: selector: app: {artifactId} ports: - name: tcp-{port}-{port} protocol: TCP port: {port} targetPort: {port}
里面的变量会在前面几步自动替换掉。
添加了prometheus收集jvm的内容:
prometheus.io.jmx: "true"
prometheus.io.jmx.port: "1234"
containerPort: 1234
将pinpoint的配置内容pinpoint.config用configMap 保存,方便更改内容。
其它内容不在此详解,可自行google。
网上资料通常发布服务都是直接kubectl deploy,这种状况只适用于jenkins的服务器已包含在K8S服务器集群中。第二种状况是在K8S集群服务器里面生成Jenkins的一个slave节点,而后在pipeline里面设置node(“k8s”){ ……} 里面发布,具体方法自行google。
这里为了不麻烦,采用直接SSH到K8S服务器集群的方案发布服务。
SSH Agent Plugin :sshagent方法支持,用于上传构建产物到目标服务器,使用详情见:
https://wiki.jenkins.io/display/JENKINS/SSH+Agent+Plugin
在Jenkins插件库搜索后直接下载安装(须要连外网环境),生产环境已安装,直接使用。
使用:
sshagent(credentials: ['deploy_ssh_key_23']) { sh "scp -P 2222 -r Deployment.yaml root@39.95.40.97:/docker/yaml/Deployment-${artifactId}.yaml" sh "ssh -p 2222 root@39.95.40.97 'kubectl apply -f /docker/yaml/Deployment-${artifactId}.yaml && kubectl set env deploy/${artifactId} DEPLOY_DATE=${env.BUILD_ID}'" }
先用ssh远程到K8S集群中的服务器,将Deployment文件上传,而后再远程执行kubectl apply发布服务。
为了不误操做,在发布前作了发布确认提示判断。
timeout(time: 10, unit: 'MINUTES') {
input '确认要部署吗?'
}
以上流程已完成整个流程,而后能够去K8S环境去看服务是否有正常运行。
上面的过程没有加入代码测试、代码质量分析SonarQube、发布后服务测试的阶段(Selenium是一套完整的Web应用程序测试系统http://www.51testing.com/zhuanti/selenium.html),后续能够接入。
不少项目采用的是多模块构成,所以每一个项目配置和发布要求不同,须要单独编译到部署,因此每一个模块都须要独立的Dockerfile和Deployment文件,Jenkinsfile通用一份,而后在发布时自动弹出模块列表,选择须要发布的模块进行编译发布。
//须要处理的项目多项目时先进入子项目 projectwk = "." mainpom = readMavenPom file: 'pom.xml' //存在多个模块时,选择其中一个进行编译 if(mainpom.modules.size() > 0 ) { echo "项目拥有模块==${mainpom.modules}" timeout(time: 10, unit: 'MINUTES') { def selproj = input message: '请选择须要处理的项目', parameters: [choice(choices: mainpom.modules, description: '请选择须要处理的项目', name: 'selproj')] //, submitterParameter: 'project' projectwk = selproj echo "选择项目=${projectwk}" } }
读取主项目的pom中的modules判断是否包含多个模块,供用户选择。
而后根据选择的模块进行编译,dir进入选择的模块读取信息并编译。
dir("${projectwk}") { pom = readMavenPom file: 'pom.xml' echo "group: ${pom.groupId}, artifactId: ${pom.artifactId}, version: ${pom.version} ,description: ${pom.description}" artifactId = "${pom.artifactId}" version = "${pom.version}" description = "${pom.description}" }
完整的Jenkinsfile
properties([ parameters([string(name: 'PORT', defaultValue: '7082', description: '程序运行端口'),choice(name: 'ACTIVE_TYPE', choices: ['dev', 'prd', 'local'], description: '程序打包环境'),choice(name: 'ENV_TYPE', choices: ['online', 'offline'], description: '线上、仍是线下环境'),booleanParam(name: 'ON_PINPOINT', defaultValue: true, description: '是否添加Pinpoint监控'),booleanParam(name: 'ON_PROMETHEUS', defaultValue: true, description: '是否添加Prometheus监控'),string(name: 'EMAIL', defaultValue: '1041126478@qq.com', description: '打包结果通知')]) ]) node { stage('Prepare') { echo "1.Prepare Stage" MVNHOME = '/opt/maven354' //echo "UUID=${UUID.randomUUID().toString()}" checkout scm //须要处理的项目多项目时先进入子项目 projectwk = "." mainpom = readMavenPom file: 'pom.xml' repostory = "${mainpom.properties['docker.repostory']}" //存在多个模块时,选择其中一个进行编译 if(mainpom.modules.size() > 0 ) { echo "项目拥有模块==${mainpom.modules}" timeout(time: 10, unit: 'MINUTES') { def selproj = input message: '请选择须要处理的项目', parameters: [choice(choices: mainpom.modules, description: '请选择须要处理的项目', name: 'selproj')] //, submitterParameter: 'project' projectwk = selproj echo "选择项目=${projectwk}" } } dir("${projectwk}") { pom = readMavenPom file: 'pom.xml' echo "group: ${pom.groupId}, artifactId: ${pom.artifactId}, version: ${pom.version} ,description: ${pom.description}" artifactId = "${pom.artifactId}" version = "${pom.version}" description = "${pom.description}" } script { GIT_TAG = sh(returnStdout: true, script: '/usr/local/git/bin/git rev-parse --short HEAD').trim() echo "GIT_TAG== ${GIT_TAG}" } image = "192.168.4.2:5000/${artifactId}:${version}" if (params.ENV_TYPE == 'offline' || params.ENV_TYPE == null) { sh "sed -i 's#39.95.40.97:5000#10.3.80.50:5000#g' pom.xml" image = "10.3.80.50:5000/${artifactId}:${version}" } } if(mainpom.modules.size() > 0 ) { stage('编译总项目') { sh "'${MVNHOME}/bin/mvn' -DskipTests clean install" } } dir("${projectwk}") { stage('编译模块') { echo "2.编译模块 ${artifactId}" def jarparam='' def pinname = artifactId if( pinname.length() > 23) { pinname = artifactId.substring(0,23) } //添加pinpoint if(params.ON_PINPOINT) { jarparam = '"-javaagent:/app/pinpoint-agent/pinpoint-bootstrap-1.8.0.jar","-Dpinpoint.agentId={pinname}", "-Dpinpoint.applicationName={pinname}",' } //添加prometheus if(params.ON_PROMETHEUS) { jarparam = jarparam + '"-javaagent:/app/prometheus/jmx_prometheus_javaagent-0.11.0.jar=1234:/app/prometheus/jmx.yaml",' } sh "sed -i 's#{jarparam}#${jarparam}#g' Dockerfile" sh "sed -i 's#{description}#${description}#g;s#{artifactId}#${artifactId}#g;s#{version}#${version}#g;s#{active}#${params.ACTIVE_TYPE}#g;s#{pinname}#${pinname}#g' Dockerfile" sh "sed -i 's#{artifactId}#${artifactId}#g;s#{version}#${version}#g;s#{port}#${params.PORT}#g;s#{image}#${image}#g' Deployment.yaml" sh "'${MVNHOME}/bin/mvn' -DskipTests clean package" stash includes: 'target/*.jar', name: 'app' } stage('Docker打包') { echo "3.Docker打包" unstash 'app' sh "'${MVNHOME}/bin/mvn' docker:build" } stage('推送镜像') { echo "4.Push Docker Image Stage" sh "'${MVNHOME}/bin/mvn' docker:push" } timeout(time: 10, unit: 'MINUTES') { input '确认要部署吗?' } stage('发布') { if (params.ENV_TYPE == 'offline' || params.ENV_TYPE == null) { sshagent(credentials: ['deploy_ssh_key_34']) { sh "scp -r Deployment.yaml root@10.2.85.30:/docker/yaml/Deployment-${artifactId}.yaml" sh "ssh root@10.2.85.30 'kubectl apply -f /docker/yaml/Deployment-${artifactId}.yaml && kubectl set env deploy/${artifactId} DEPLOY_DATE=${env.BUILD_ID}'" } } else { sshagent(credentials: ['deploy_ssh_key_238']) { sh "scp -P 22 -r Deployment.yaml root@39.95.40.97:/docker/yaml/Deployment-${artifactId}.yaml" sh "ssh -p 22 root@39.95.40.97 'kubectl apply -f /docker/yaml/Deployment-${artifactId}.yaml && kubectl set env deploy/${artifactId} DEPLOY_DATE=${env.BUILD_ID}'" } } echo "发布完成" } } stage('通知负责人'){ // emailext body: "构建项目:${description}\r\n构建完成", subject: '构建结果通知【成功】', to: "${EMAIL}" echo "构建项目:${description}\r\n构建完成" } }
将Jenkinsfile文件放在项目根目录,而后将源码都上传到GitLab。
打开BlueOcean,这是Jenkins新出的美化页面。
选择本身的项目。
进入后点击运行,其中会弹出框选择发布参数(这里须要手工填写发布的端口,因为采用配置中心化,端口没法自动读取)。
进入查看流程状态,失败会有相应的提示:
显示发布服务
在K8S内查看部署的服务启动状况。
Jenkinsfile Pipeline语法内容可参考官网:https://jenkins.io/doc/book/pipeline/jenkinsfile/
还能够进入项目后,有个流水线语法:
选择想要的功能,生成:
Jenkins还可用做发布Vue前端项目,具体内容可参考 Jenkins自动化构建vue项目而后发布到远程服务器 文档。
Jenkins要发布Net服务须要有一台windows的Jenkins slave,还须要在此节点上安装编译器MSBuild框架,Git框架、更改服务器上的IIS权限等功能,最后文件分发到其它windows服务器,过程比较繁琐,若无发布审核建议直接经过VS自带发布功能发布程序。