本文主要用于展现CI/CD相关的流程,首先展示如何基于Docker 容器搭建Jenkins环境,该Jenkins容器环境可以编译Java、NodeJS,并且可以在Docker容器内编译Jenkins镜像的基本环境搭建。
同时将展示利用JenkinsFile编写混合声明式pipline和脚本式pipline来实现通过maven编译一个Java程序为Jar,通过Dockerfile将Jar和相关环境配置编译成为docker镜像并发布到阿里云的私有Regisery上,最后发布的过程。
这里Jenkins环境将基于docker容器来实现,选用的基础镜像是
jenkinsci/blueocean:latest。 这个镜像是Jenkins官方的含有blue ocean镜像,基于alphine Linux,有效的减少了镜像的基本大小。由于日常环境中的需要,在容器中同时按照了一些nodejs和py相关的一些包。由于需要编译docker镜像也安装了docker相关组件。在国内下载Jenkins的插件是非常缓慢的过程,所以在该镜像中也配置了利用jenkins-zh.cn镜像来加速镜像下载的相关配置。下面看下Dockerfile的内容。
FROM jenkinsci/blueocean:latest
ENV JENKINS_UC https://updates.jenkins-zh.cn
ENV JENKINS_UC_DOWNLOAD https://mirrors.huaweicloud.com/jenkins/
ENV JENKINS_OPTS="-Dhudson.model.UpdateCenter.updateCenterUrl=https://updates.jenkins-zh.cn/update-center.json"
ENV JENKINS_OPTS="-Djenkins.install.runSetupWizard=false"
COPY init.groovy /usr/share/jenkins/ref/init.groovy.d/init.groovy
COPY hudson.model.UpdateCenter.xml /usr/share/jenkins/ref/hudson.model.UpdateCenter.xml
COPY mirror-adapter.crt /usr/share/jenkins/ref/mirror-adapter.crt
USER root
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
USER root
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk update && apk add --no-cache --repository=http://mirrors.ustc.edu.cn/alpine/edge/main curl vim maven nodejs yarn python3 py3-pip
COPY UpdateReleaseTool /usr/local/UpdateReleaseTool
RUN chmod 777 /usr/local/UpdateReleaseTool
USER jenkins
RUN jenkins-plugin-cli --plugins generic-webhook-trigger:1.72
RUN jenkins-plugin-cli --plugins ssh-steps:2.0.0
解释:
2-19行代码主要配置了国内的Jenkins更新中心的镜像,并且复制了相关证书
21行安装了需要的一些组件,包括CURL、VIM、Maven、nodejs和python相关
23-25行:是一个自己用golang开发的发布到公司服务器工具。
30-31行:安装了generic-webhook-trigger用于在pipline中通过git提交来触发Job,ssh-steps是一个可以在jenkinsfile中连接远程Linux服务器执行相关命令的插件
由于我们需要在容器内编译docker镜像,因此需要使用 docker:dind 镜像来启动一个容器,在容器内运行容器来编译容器,这主要是用来开发。
通过建立一个容器间的网络,将docker:dind 和 jenkins放在这个网络中,并隔离网络环境,使得这两个容器互相可以通信
docker network create jenksin
docker run \
--name jenkins-docker \
--detach \
--privileged \
--network jenkins \
--network-alias docker \
--env DOCKER_TLS_CERTDIR=/certs \
--volume jenkins-docker-certs:/certs/client \
--volume /external-data/jenkins:/var/jenkins_home \
--volume /data/docker:/var/lib/docker/ \
--publish 2376:2376 \
--restart=always \
docker:dind --registry-mirror https://u7g484go.mirror.aliyuncs.com
这里设置容器镜像加速到阿里云相关容器加速工具,暴露来2376端口用来给Jenkins容器连接。
docker run \
--name jenkins-blueocean \
--detach \
--network jenkins \
--env DOCKER_HOST=tcp://docker:2376 \
--env DOCKER_CERT_PATH=/certs/client \
--env DOCKER_TLS_VERIFY=1 \
--publish 8080:8080 \
--publish 50000:50000 \
--volume /external-data/jenkins:/var/jenkins_home \
--volume jenkins-docker-certs:/certs/client:ro \
--restart=always macco_jenkins:latest
这里设置了Docker的容器host变量为docker:dind 的容器实例。macco_jenkis镜像是用之前的dockerfile编译出来的镜像。当容器启动后,稍等一些时就可以从 localhost:8080 访问jenkins。
第一次启动的时候会要求输入token,并进行一些配置,token可以通过docker logins命令获取,如下图
获取Token
输入Token后,进行基本的配置,可以安装一些插件,如ssh-agent,publish-over-ssh等,也可以之后安装,之后就是设置用户名和密码,完成后jenkins会重启,稍等就可以了。
同样由于国内网络的原因,必须修改mvn的镜像地址到阿里云或者其他镜像,用以加速mvn的下载。
docker exec -it --user root jenkins-blueocean bash
以为在dockerfile中,容器采用jenkins这个用户运行,所以默认 情况下是没有办法修改mvn的配置文件的。所以采用--user 切换到root用户。先通过如下命令获取maven的安装路径
mvn -v
得到如下输出
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: /usr/share/java/maven-3
Java version: 1.8.0_275, vendor: AdoptOpenJDK, runtime: /opt/java/openjdk/jre
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "3.10.0-1127.8.2.el7.x86_64", arch: "amd64", family: "unix"
很明显mvn被安装在 /usr/share/java/maven-3下,之后修改配置文件到阿里云,方法如下:
vi /usr/share/java/maven-3/conf/settings.xml
在mirros下修改加入如下节点:
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
通过系统管理-》全局工具配置来配置工具的位置,主要是jdk mvn、node、docker的,如图:
配置JDK相关
配置Maven:
配置NodeJS:
在系统管理》插件管理安装如下docker相关插件
4.docker compose build step
安装完成后,在系统管理-》全局工具配置来配置工具的位置中配置docker相关的位置
首先在私有的gitlab账号下,通过管理员权限配置一个共用的deploy key,如图
需要在jenkins容器中产生一对公钥和私钥
docker exec -it jenkins-blueocean bash
ssh-keygen -t rsa -b 4096 -C "lokie@5imakeup.com"
cat ~/.ssh/id_rsa.pub #获取公钥
之后在Jenkins系统管理》》凭据管理 中增加这个共钥的私钥,这里我们将在全局凭据中添加。
输入ID,这个ID可以在pipeline中使用,作为凭据的唯一标示。
输入用户名,输入私钥,私钥通过如下方式获取:
docker exec -it jenkins-blueocean bash
cat ~/.ssh/id_rsa
到这里完成了关于jenkins基本环境的配置,之后的设置是和工程特定pipeline以及任务相关的的。
简单的来说,pipeline是Jenkins从2.0开始引入的功能,使得Jenkins能够通过编程或者脚本文件的方式来定义如何使用CI/CD相关的功能,而不是通过UI界面上的操作和配置。编写一个pipeline可以通过创建一个pipeline项目,将相关的代码在其中输入,也可以通过在需要集成CI/CD功能的项目中创建一个Jenkinsfile的文本文件并在文件中编写pipeline相关的代码来实现相关功能。
从以上描述可以知道Jenkinsfile具有明显的优势,其本身就能被源代码管理工具管理,具有最大的灵活性。
存在两种不同类型的方式来创建一个pipeline即声明式和脚本式。Jenkins官方推荐初学者采用声明式来编写,声明式Pipeline脚本是一种特定的DSL语言,而脚本式就是Groovy脚本,具有更强大的功能,当然在声明式Pipeline中是可以集成groovy脚本的。
// 标示一个Pipeline 脚本的开始
pipeline{
// 指定运行的代理,Jenkins本身是一个分布式的理念,可以在不同的agent上执行不同的功能,agent可以理解为一个机器
agent{
label "node"
}
// pipeline有多个不同的Stage 组成,这里是顺序执行的
stages{
stage("A"){
// 运行的条件
when {
}
// 一个Stage又有1个或多个Step,step就是一个基本的CI/CD操作
steps{
echo "========executing A========"
}
// Stage 中所有的steps执行后的一些动作
post{
always{
echo "========always========"
}
success{
echo "========A executed successfully========"
}
failure{
echo "========A execution failed========"
}
}
}
}
// // 所有的stages s执行后的一些动作
post{
always{
echo "========always========"
}
success{
echo "========pipeline executed successfully ========"
}
failure{
echo "========pipeline execution failed========"
}
}
}
node {
stage("Prepare") {
def a = “”
if(env.BRANCH_NAME == 'PPE') {
}
}
}
可以看到script pipeline是由node开头的,其中可以包含groovy的脚本。
下面的例子是我司一个实际的需求
由于需要一些自定义版本号类似的功能,为实现相关需求选择了结合声明式和脚本一起的功能来时实现。
对于企业微信提示,选择自己开发了基于Groove的脚本作为Jenkins全局库引用。
该插件主要用于在Jenkinsfile文件中执行远程的SSH命令,详细见:https://plugins.jenkins.io/ssh-steps/ 支持从远程执行ssh命令,主要用于在远程节点上执行ssh命令用于部署
该插件主要用于在pipeline中用脚本的方式获取存储在Jenkins Credentials中的各种凭据,这样就可以不要在代码中暴露生产环境的各种凭据信息,见https://plugins.jenkins.io/credentials-binding/
用Script的方式编程做各种docker操作,这里用于镜像的产生。见https://plugins.jenkins.io/docker-workflow/
用于Webhhok的实现,实现当gitlab提交后自动触发Jenkins的相关Job,见https://plugins.jenkins.io/generic-webhook-trigger/
首先需要增加一些凭据,比如gitlab的私有ssh key,阿里云相关Regisery的登陆用户和密码、远端部署服务器的用户名和密码等,具体操作都是通过系统管理》凭据管理来实现的,如图:
添加ssh key:
这里的SSH key可以从通过 在某一个Linux或者macos下运行,如下命令:
ssh-keygen -t rsa -b 4096 -C "email@example.com"
cat ~/.ssh/id_rsa #私钥
cat cat ~/.ssh/id_rsa.pub #公钥
将公钥加入Jenkins的Deploy keys上
私钥则输入到
添加阿里云相关Regisery的登陆用户和密码:
利用Grovvy发送企业微信机器信息主要就是调用restful接口,代码如下:
首先定义个Http clinet:
package com.imacco.devops
class HttpClient {
static String PostJsonRequest(String urlStr,String jsonInputString) {
def result = ""
try {
URL url = new URL(urlStr)
def conn = (HttpURLConnection)url.openConnection()
conn.setRequestMethod("POST")
conn.setRequestProperty("Content-Type", "application/json; utf-8")
conn.setRequestProperty("Accept", "application/json")
conn.setDoOutput(true)
// 设置不用缓存
conn.setUseCaches(false);
// 设置传递方式
conn.setRequestMethod("POST");
// 设置维持长连接
conn.setRequestProperty("Connection", "Keep-Alive");
// 设置文件字符集:
conn.setRequestProperty("Charset", "UTF-8");
//转换为字节数组
byte[] data = jsonInputString.getBytes();
// 设置文件长度
conn.setRequestProperty("Content-Length", String.valueOf(data.length))
// 设置文件类型:
conn.setRequestProperty("contentType", "application/json")
// 开始连接请求
conn.connect()
OutputStream out = conn.getOutputStream()
// 写入请求的字符串
out.write(data)
out.flush()
out.close()
println("connection:"+conn.getResponseCode())
// 请求返回的状态
if (conn.getResponseCode() == 200) {
println("http success 200");
// 请求返回的数据
InputStream inputStream = conn.getInputStream()
String a = null;
try {
byte[] data1 = new byte[inputStream.available()]
inputStream.read(data1);
// 转成字符串
a = new String(data1);
println(a);
} catch (Exception e1) {
e1.printStackTrace()
}
}
} catch(IOException ioException) {
println(ioException.getMessage())
}
return result
}
}
Jenkins groovy 库必须遵守如下规范:
src中用于存放相关包和自己的帮助类而vars就是入口点:
代码如下:
import com.imacco.devops.HttpClient
def call(Map config=[:]) {
println("message:${config["message"]}")
if(config["message"] != '') {
def url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=微信机器Key"
def jsonMessage = """
{
"msgtype": "text",
"text": {
"content": "${config.message}"
}
}
"""
def result = HttpClient.PostJsonRequest(url,jsonMessage)
println(result)
}
}
库必须是一个名为call的函数,可以用Map config=[:]来传值,当然也可以用其他类型,但是Jenksin会认为这个类型是不安全,需要在设置中确认。
首先将上面的代码放到gitlab上建立一个单独的分支并正确配置gitlab相关的私钥和公钥,并设置权限等
在Jenkins系统管理》系统配置中找到Global Pipeline Libraries,增加一个新的
这里的Name可以填写和vars中文件一样的名字,版本号可以是git的分支名字或者任意一个commit sha值,通过设置后,在Jenkinsfile文件中我们可以通过如下方式引入
@Library('WechatAlerter')
下面看下完整的Jenkinsfile
@Library('WechatAlerter') // 导企业微信库
import java.text.SimpleDateFormat // 导入java日期计算的库
pipeline {
agent any
triggers {
// 声明一个GenericTrigger用于gitlab的触发,具体见后面的描述
GenericTrigger (
genericVariables: [
[key: 'ref', value: '$.ref']
],
causeString: ' Triggered on $branch' ,
printContributedVariables: true,
printPostContent: true,
silentResponse: false,
regexpFilterText: '$ref',
regexpFilterExpression: 'refs/heads/' + BRANCH_NAME,
token: 'tokn‘'
)
}
stages {
// 准备docker镜像相关版本好
stage("Prepare") {
steps {
echo "prepare"
script {
checkout scm
docker_host = "registry.cn-shanghai.aliyuncs.com/test"
img_name = "xunzhao_businessapi"
docker_img_name = "${docker_host}/${img_name}"
def date = new Date()
def sdf = new SimpleDateFormat("yyyyMMdd")
def datestr = sdf.format(date)
def version = ""
def pcount = ""
def pversion = ""
def flag = ""
if("${ref}" == 'refs/heads/ppe') {
// 在Jenkins服务器上执行Docker images命令,并获取已经有的版本情况
pcount = sh(
script: "docker images | grep xunzhao_businessapi | awk '{print \$2}' | grep 'ppe\$' | grep ${datestr} | wc -l",
returnStdout: true
).trim()
echo 'pcount = ${pcount}'
pversion = sh(
script: "docker images | grep xunzhao_businessapi | awk '{print \$2}' | grep 'ppe\$' | grep ${datestr} | head -n 1",
returnStdout: true
).trim()
echo "pversion=${pversion}"
flag = "-ppe"
} else if("${ref}" == 'refs/heads/master') {
pcount = sh(
script: "docker images | grep xunzhao_businessapi | awk '{print \$2}' | grep -v 'ppe\$' | grep ${datestr} | wc -l",
returnStdout: true
).trim()
echo 'pcount = ${pcount}'
pversion = sh(
script: "docker images | grep xunzhao_businessapi | awk '{print \$2}' | grep -v 'ppe\$' | grep ${datestr} | head -n 1",
returnStdout: true
).trim()
echo "pversion=${pversion}"
}
//只有是PPE或者master环境触发
if("${ref}" == 'refs/heads/ppe' || "${ref}" == 'refs/heads/master') {
if(pcount == '0') {
echo "pcount equal zero"
version="v" + datestr + "01"
} else {
echo "pcount largeten zero"
def tag='v' + datestr
def sn = pversion.replace(tag,'').replace(flag,'')
def seq = Integer.parseInt(sn) + 1
if(seq < 10) {
version = "v" + datestr + "00" + seq.toString()
} else if(seq>=10 && seq<=99) {
version = "v" + datestr + "0" + seq.toString()
} else {
version = "v" + datestr + seq.toString()
}
}
echo "version=${version}"
build_tag = version
WechatAlerter([message:"Business Api start build,Version:${build_tag}"])
}
}
}
}
//用maven编译Java 的jar
stage("Build Jar") {
steps {
echo "Build Jar"
script {
if ("${ref}" == 'refs/heads/ppe') {
sh 'mvn install -Pppe'
} else if("${ref}" == 'refs/heads/master') {
sh 'mvn install -Pproduction'
}
}
}
}
// 编译Dockerfile
stage("Build Docker image") {
steps {
echo "Build Docker image"
script {
if ("${ref}" == 'refs/heads/ppe') {
// 因为dockerfile中用到这个register先登陆,然后根据版本号编译并推送
docker.withRegistry('http://registry.cn-shanghai.aliyuncs.com', 'xunzhao') {
def dockImage = docker.build "${docker_img_name}:${build_tag}-ppe"
dockImage.push()
}
// 发送企业微信
WechatAlerter([message:"Business Api PPE Build Success"])
} else if("${ref}" == 'refs/heads/master') {
docker.withRegistry('http://registry.cn-shanghai.aliyuncs.com', 'xunzhao') {
def dockImage = docker.build "${docker_img_name}:${build_tag}"
dockImage.push()
}
WechatAlerter([message:"Business Api Production Build Success"])
}
}
}
}
// 用ssh执行 docker-compose部署
stage("Deploy with docker-compose") {
steps {
echo "Deploy with docker-compose"
script {
if("${ref}" == 'refs/heads/ppe') {
withCredentials([usernamePassword(credentialsId: 'localppddev', passwordVariable: 'password', usernameVariable: 'userName')]) {
def remote = [:]
remote.name = "node-ppe"
remote.host = "mup007.5imakeup.com"
remote.port = 2222
remote.allowAnyHosts = true
remote.user = userName
remote.password = password
withCredentials([usernamePassword(usernameVariable:'dockerUserName',passwordVariable:'dockerPass',credentialsId:'xunzhao')]) {
sshCommand remote: remote,command: "docker login --username=${dockerUserName} registry.cn-shanghai.aliyuncs.com --password ${dockerPass}"
sshCommand remote: remote,command: "export businessapiversion=${build_tag}-ppe;docker-compose -f /workspace/xunzhao/docker-compose.yml rm -f -s xunzhao_businessapi && docker-compose -f /workspace/xunzhao/docker-compose.yml up -d xunzhao_businessapi"
}
}
WechatAlerter([message:"Business Api Production Deploy Success:${build_tag}"])
}
}
}
}
}
}
GenericTrigger 相关说明:
GenericTrigger可以从http://jenkis-url/generic-webhook-trigger/invoke 中获取各种scm来的webhook,通过解析JSON或者xml来实现那
GenericTrigger (
genericVariables: [
[key: 'ref', value: '$.ref'] //定义一个ref变量,就是gliblab的中相关字段
],
causeString: ' Triggered on $branch' ,
printContributedVariables: true,
printPostContent: true,
silentResponse: false,
regexpFilterText: '$ref', // 通过正则表达式匹配
regexpFilterExpression: 'refs/heads/' + BRANCH_NAME,
token: 'tokn‘' // gitlab中填写一个安全相关Token,自己生成一个,注意保密
)
gitlab设置:
至此完成一个比较完善的CI/CD流程,当然不包含自动化测试,下个这个系列文字,将会展现Jenkins pipeline关于nodejs编译和应用。
本文为Lokie.Wang原创文章,转载无需和我联系,但请注明来自lokie博客http://lokie.wang