1. 分布式构建
jenkins 采用的是 master + slave(agent) 结构,其中master承担HTTP请求和任务配置,slave 承担具体的构建任务。在Jenkins架构如下图:
涉及到的名词:
Node:master 和 slave 统称为 node
Master:负责接收HTTP请求和分配任务的节点,Jenkins 不支持多Master,一般通过分片或者Pod方式尽可能提高可用性
Slave:仅仅负责执行任务的节点,不同的slave可以通过名称和lable进行区分
Agent:和Slave含义相同
Executor:执行器,只各个节点上负责执行任务的单元,通常节点上Executor的数量和CPU核心数一致
注意:master只有在启动的过程中读取磁盘中的文件,运行阶段无法读取。不能使用多个master共享JENKINS_HOME。
1.1. 使用虚拟机作为agent
这种是最常见的一种方式,操作也很简单,支持不同的方式启动。个人比较推荐使用 SSH 方式连接Agent,这种方式不需要额外维护Jenkins Agent进程,只需要保证SSH方式能正常连接即可。
1.1.1. 使用SSH连接Agent
创建agent上创建jenkins用户,并添加公钥
[root@centos-81 ~]# useradd jenkins
[root@centos-81 ~]# su - jenkins
[jenkins@centos-81 ~]$ mkdir .ssh
[jenkins@centos-81 ~]$ vim .ssh/authorized_keys # 添加ssh公钥
[jenkins@centos-81 ~]$ chmod 750 .ssh ; chmod 640 .ssh/authorized_keys
[root@centos-81 ~]# mkdir /data/jenkins ; chown -R jenkins.jenkins /data/jenkins
[root@centos-81 ~]# yum install -y git # 一定需要安装git
添加私钥
【系统管理】—>【密钥管理】—>【全局凭据】—>【添加凭据】
- 添加节点
【系统管理】—>【节点管理】—>【新建节点】
- 配置节点信息,使用SSH方式连接,需要确保jenkisn工作目录有权限
- 查看Agent日志
测试Agent
pipeline { agent { label "VM && golang" } stages { stage('Preparing') { steps { sh "hostname" } } } }
1.1.2. 使用JAVA WEB方式
添加节点
- 节点列表—>点击节点名称—>查看添加节点方式
- 配置agent ``` [root@centos-82 data]# useradd jenkins [root@centos-82 data]# mkdir /data/jenkins [root@centos-82 data]# mkdir -p /opt/release/jenkins/jenkins-agent-2.277.3 [root@centos-82 data]# chown -R jenkins.jenkins /data/jenkins/ /opt/release/jenkins/ [root@centos-82 data]# yum install -y git java-1.8.0-openjdk-devel
[jenkins@centos-82 ~]$ cd /opt/release/jenkins/jenkins-agent-2.277.3/ [jenkins@centos-82 jenkins-agent-2.277.3]$ ll total 1472 -rw-r—r— 1 jenkins jenkins 1506923 Apr 26 00:05 agent.jar [jenkins@centos-82 jenkins-agent-2.277.3]$ nohup java -jar agent.jar -jnlpUrl http://jenkins.ddn.com/computer/centos-82/jenkins-agent.jnlp -secret @secret-file -workDir “/data/jenkins” >/dev/null 2>&1 &
- 测试agent
pipeline { agent { label “VM && java” } stages { stage(‘Preparing’) { steps { sh “java -version” } } } }
<a name="77cffdce"></a>
## 1.2. 使用本地docker构建
这种方式连接本地的docker socket,启动容器完成构建过程,基本不怎么使用,了解即可。需要安装以下插件:
- Docker Plugin
- Docker Pipeline
```groovy
pipeline {
agent {
docker {
image 'golang:1.16.3'
// 针对golang编译,为了加快速度,可以将gopath挂载到容器内部,而且需要使用root用户运行
args '-v /opt/release/golang/path:/go -v /opt/release/binary:/tmp/binary -u 0'
}
}
stages {
stage('Preparing') {
steps {
sh "printenv"
}
}
stage('Build') {
steps {
sh "make build"
// 此处只是做一个简单的模拟,把编译好的二进制拷贝出来,实际上可以推送到制品库或者用于下一个阶段的构建
sh "cp webserver /tmp/binary/"
}
}
}
}
1.3. 使用容器作为动态agent
在之前的方式中,使用虚拟机作为固定的Agent,这个Agent在构建之前就存在,并且构建之后还存在。由于容器特性,我们可以在需要agent的时候使用docker启动一个,在运行结束之后就销毁,注意和3.4.2.中使用本地docker构建的区别:
- 本地docker构建时,是Agent调用本地的docker,启动一个容器,这个容器只是执行任务,不承担agent的功能
- 容器作为动态Agent时,当触发构建请求时,Jenkins Master使用容器启动一个Agent,再由这个Agent去执行任务
1.3.1. 安装和配置docker
- docker 安装和 /etc/daemon.json 配置可以参考 01-容器的介绍
- 签发SSL证书,用于客户端远程连接Docker Service的验证;证书签发可以参考以下章节06-1-密码学基础 ``` [root@centos-82 ~]# mkdir docker-ssl && cd docker-ssl
签署CA证书
[root@centos-82 docker-ssl]# openssl genrsa -out ca-key.pem 4096 [root@centos-82 docker-ssl]# openssl req -new -out ca-req.csr -key ca-key.pem [root@centos-82 docker-ssl]# openssl x509 -req -in ca-req.csr -out ca-cert.pem -signkey ca-key.pem -days 3650 [root@centos-82 ssl]# ll ca-* -rw-r—r— 1 root root 1911 Apr 30 05:58 ca-cert.pem -rw-r—r— 1 root root 3243 Apr 30 05:53 ca-key.pem -rw-r—r— 1 root root 1704 Apr 30 05:57 ca-req.csr
签服务端多域名证书
[root@centos-82 docker-ssl]# openssl genrsa -out server-key.pem 2048 [root@centos-82 docker-ssl]# openssl req -new -key server-key.pem -config server.conf -out server.csr [root@centos-82 docker-ssl]# openssl x509 -req -in server.csr -extfile docker-server.conf -extensions v3_req -out server.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -days 365
签署客户端(jenkins master)证书
[root@centos-82 docker-ssl]# openssl genrsa -out client-key.pem 2048 [root@centos-82 docker-ssl]# openssl req -new -out client-req.csr -key client-key.pem [root@centos-82 docker-ssl]# openssl x509 -req -in client.csr -extfile client.conf -extensions v3_req -out client.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -days 365
- 配置docker service
[root@centos-82 docker-ssl]# mkdir /etc/docker/ssl [root@centos-82 docker-ssl]# cp ca-cert.pem server.pem server-key.pem /etc/docker/ssl/
[root@centos-82 ~]# vim /usr/lib/systemd/system/docker.service
修改启动命令即可
ExecStart=/usr/bin/dockerd -H tcp://10.4.7.82:4243 -H fd:// —containerd=/run/containerd/containerd.sock —tlsverify —tlscacert /etc/docker/ssl/ca-cert.pem —tlskey /etc/docker/ssl/server-key.pem —tlscert /etc/docker/ssl/server.pem [root@centos-82 ~]# systemctl daemon-reload [root@centos-82 ~]# systemctl restart docker
- 测试客户端证书
在 Jenkins master 上测试
[root@centos-80 ~]# docker -H 10.4.7.82:4243 ps Error response from daemon: Client sent an HTTP request to an HTTPS server.
拷贝证书到jenkins master上
[root@centos-82 docker-ssl]# scp ca-cert.pem client-cert.pem client-key.pem 10.4.7.80:/root/ [root@centos-80 ~]# docker -H 10.4.7.82:4243 —tlsverify —tlscacert ca-cert.pem —tlscert client-cert.pem —tlskey client-key.pem ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
如果确实在jenkins master 上使用docker client 远程连接 jenkins slave,可以重命名以下证书
[root@centos-80 ~]# mkdir .docker [root@centos-80 ~]# mv ca-cert.pem .docker/ca.pem [root@centos-80 ~]# mv client-cert.pem .docker/cert.pem [root@centos-80 ~]# mv client-key.pem .docker/key.pem [root@centos-80 ~]# docker -H 10.4.7.82:4243 —tlsverify ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
证书文件: [docker-ssl.tar.gz](https://www.yuque.com/attachments/yuque/0/2021/gz/378176/1622285138871-9725e578-20d9-4be9-af39-b8e97433975d.gz?_lake_card=%7B%22src%22%3A%22https%3A%2F%2Fwww.yuque.com%2Fattachments%2Fyuque%2F0%2F2021%2Fgz%2F378176%2F1622285138871-9725e578-20d9-4be9-af39-b8e97433975d.gz%22%2C%22name%22%3A%22docker-ssl.tar.gz%22%2C%22size%22%3A13007%2C%22type%22%3A%22application%2Fx-gzip%22%2C%22ext%22%3A%22gz%22%2C%22status%22%3A%22done%22%2C%22uid%22%3A%221619799898843-0%22%2C%22progress%22%3A%7B%22percent%22%3A99%7D%2C%22percent%22%3A0%2C%22refSrc%22%3A%22https%3A%2F%2Fwww.yuque.com%2Fattachments%2Fyuque%2F0%2F2021%2Fgz%2F378176%2F1619799898829-a7b28efa-05a3-4124-bb55-ce3ac2cc87e6.gz%22%2C%22id%22%3A%22fVik5%22%2C%22card%22%3A%22file%22%7D)
<a name="yE9iQ"></a>
### 1.3.2. 修改Java启动参数
添加Java运行参数 `-Djdk.tls.client.protocols=TLSv1.2` ,会报错,参考页面:[error](https://developer.mongodb.com/community/forums/t/sslhandshakeexception-should-not-be-presented-in-certificate-request/12493)<br />![2021-04-30_23-31-01.png](https://cdn.nlark.com/yuque/0/2021/png/378176/1619796838534-bf8beb5f-bff1-45d0-a9f3-4e32547a61fd.png#height=489&id=S4Zdv&margin=%5Bobject%20Object%5D&name=2021-04-30_23-31-01.png&originHeight=489&originWidth=799&originalType=binary&size=80422&status=done&style=stroke&width=799)<br />不同安装方式修改方式不同,以yum安装的Jenkins为例:
[root@centos-80 ~]# vim /etc/sysconfig/jenkins # 修改JENKINS_JAVA_OPTIONS JENKINS_JAVA_OPTIONS=”-Djava.awt.headless=true -Djdk.tls.client.protocols=TLSv1.2” [root@centos-80 ~]# systemctl restart jenkins [root@centos-80 ~]# systemctl status jenkins.service # 可以查看Java启动参数
<a name="nYRhE"></a>
### 1.3.3. 配置密钥
【系统管理】-->【Manger Credentials】-->【全局凭据】-->【添加凭据】<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/378176/1619711473117-6ddb5b4a-aa95-4ce9-9eab-590f17d10095.png#height=751&id=CpyYW&margin=%5Bobject%20Object%5D&name=image.png&originHeight=830&originWidth=1239&originalType=binary&size=89760&status=done&style=stroke&width=1121)
<a name="G36zO"></a>
### 1.3.4. 配置docker动态agent
【系统管理】-->【节点管理】-->【Configure Clouds】-->【Add a new cloud】--> 选择 docker<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/378176/1619939490690-3d9218ae-3840-448d-86eb-c1fc6f6555ef.png#height=562&id=coUQX&margin=%5Bobject%20Object%5D&name=image.png&originHeight=562&originWidth=1004&originalType=binary&size=54895&status=done&style=stroke&width=1004)<br />添加docker容器模板-->【Add Docker Template】<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/378176/1619940715685-b279beb7-a80d-4d87-aa72-b5f2e8c6fc67.png#height=325&id=QAcm2&margin=%5Bobject%20Object%5D&name=image.png&originHeight=325&originWidth=794&originalType=binary&size=29403&status=done&style=stroke&width=794)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/378176/1619939796608-1a7ece46-6e8c-414b-8662-edf821651771.png#height=392&id=pN0QO&margin=%5Bobject%20Object%5D&name=image.png&originHeight=392&originWidth=981&originalType=binary&size=57847&status=done&style=stroke&width=981)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/378176/1619939932702-15031259-5ac0-44e2-be94-e60c6b71b19a.png#height=260&id=OGMiK&margin=%5Bobject%20Object%5D&name=image.png&originHeight=260&originWidth=544&originalType=binary&size=29448&status=done&style=stroke&width=544)
- 配置容器 -->点击【Container settings】
![image.png](https://cdn.nlark.com/yuque/0/2021/png/378176/1619940413888-e2e03c39-44c4-453c-aee2-4fa8f3a60681.png#height=451&id=kNC6z&margin=%5Bobject%20Object%5D&name=image.png&originHeight=451&originWidth=785&originalType=binary&size=47313&status=done&style=stroke&width=785)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/378176/1619940564367-034b6ecd-533d-468c-a074-f7a49f924ec9.png#height=470&id=hwJVp&margin=%5Bobject%20Object%5D&name=image.png&originHeight=470&originWidth=721&originalType=binary&size=33572&status=done&style=stroke&width=721)
<a name="oFoKg"></a>
### 1.3.5. 测试docker动态agent
根据上述的 Agent 模板再配置一个 golang v1.15.11 版本,用于测试<br />测试项目:[go-simple](https://gitee.com/linux_duduniao/go-simple.git) tag: v0.0.1
- 配置docker 仓库登陆用户密码
![image.png](https://cdn.nlark.com/yuque/0/2021/png/378176/1619948092263-5dd67a4d-ecab-4e9a-a441-ad405bbab1e5.png#height=434&id=eoYVp&margin=%5Bobject%20Object%5D&name=image.png&originHeight=434&originWidth=678&originalType=binary&size=15260&status=done&style=stroke&width=678)
- makefile 文件
```makefile
IMAGE ?= linuxduduniao/server
TAG ?= latest
build:
go build -o server cmd/main.go && \
docker build -t linuxduduniao/server:$(TAG) .
push:
docker push $(IMAGE):$(TAG)
clean:
rm -f server ; \
docker image rm linuxduduniao/server:$(TAG) || true
test:
echo "test ..."
deploy:
echo "deploy ..."
dockerfile文件
FROM ubuntu:18.04 COPY server /opt/apps/ RUN chmod 755 /opt/apps/server CMD ['/opt/apps/server --http_server_port 80'] USER root EXPOSE 80
jenkinsfile
pipeline { agent { label "docker && dockerclient && go1.16" } environment { GOOS='linux' GOARCH='amd64' GOPROXY='https://goproxy.cn,direct' CGO_ENABLED=0 GO111MODULE='on' } options { timeout(time:10,unit:'MINUTES') } parameters { string(name:"image", defaultValue:"linuxduduniao/server", description:"image name") string(name:"tag", defaultValue:"latest", description:"image tag") string(name:"registry", defaultValue:"https://index.docker.io/v1/", description:"docker registry url") string(name:"credential", defaultValue:"dockerhub-push", description:"docker credential id") } stages { stage('Preparing') { steps { sh "printenv" echo "${params.image}:${params.tag}" echo "${params.registry} ${params.credential}" } } stage('Build') { steps { sh "make build IMAGE=${params.image} TAG=${params.tag}" } } stage('Testing') { steps { sh "make test IMAGE=${params.image} TAG=${params.tag}" } } stage('Push') { steps { script { docker.withRegistry("${params.registry}", "${params.credential}") { sh "make push IMAGE=${params.image} TAG=${params.tag}" } } } post { always { sh "make clean IMAGE=${params.image} TAG=${params.tag}" } } } } }
1.4. 使用Pod作为动态agent
在Kubernetes环境下,使用Pod作为动态Agent是一种很常见的运用方式。通常在devops集群中配置若干个节点作为CI/CD专用,也可以专门为CI/CD搭建一套轻量级的k3s集群。这里使用Kubernetes集群演示,先安装 Kubernetes 插件 !
1.4.1. 准备k8s集群
``` [root@duduniao ~]# kubectl get node NAME STATUS ROLES AGE VERSION centos-7-51 Ready master 148d v1.18.12 centos-7-52 Ready master 148d v1.18.12 centos-7-53 Ready master 148d v1.18.12 centos-7-54 Ready worker 148d v1.18.12 centos-7-55 Ready worker 148d v1.18.12 centos-7-56 Ready worker 138d v1.18.12
添加label
[root@duduniao ~]# kubectl label nodes centos-7-55 jenkins=true [root@duduniao ~]# kubectl label nodes centos-7-56 jenkins=true
kuberentes 插件支持的认证方式如下:
- Username/password
- Secret File (kubeconfig file)
- Secret text (Token-based authentication) (OpenShift)
- Google Service Account from private key (GKE authentication)
- X.509 Client Certificate
此处配置配置 kubeconfig 文件:
[root@duduniao ~]# kubectl create namespace jenkins [root@duduniao ~]# cat /tmp/jenkins.yaml apiVersion: v1 kind: ServiceAccount metadata: labels: k8s-app: jenkins-agent name: jenkins-admin
namespace: jenkins
apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: jenkins-admin-role namespace: jenkins labels: k8s-app: jenkins-agent roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects:
kind: ServiceAccount name: jenkins-admin namespace: jenkins [root@duduniao ~]# kubectl apply -f /tmp/jenkins.yaml
``` # 生成kubeconfig文件 [root@centos-7-51 ~]# kubectl config set-cluster jenkins-cluster --server=https://10.4.7.59:6443 --certificate-authority /etc/kubernetes/pki/ca.crt --embed-certs --kubeconfig=/tmp/kubeconfig [root@centos-7-51 ~]# kubectl config set-credentials jenkins-admin --token=eyJhbGciOiJSUzI1NiIsImtpZCI6InlsYXNzWkZ4OU5kMXhqcGgxZkIzdkJqbjBid05oVHp1bjF4TWRKRURkM2cifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJqZW5raW5zIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImplbmtpbnMtYWRtaW4tdG9rZW4tY2Q4bmsiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiamVua2lucy1hZG1pbiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjY5ZjVhMTUzLTIyMzAtNDY2MC04ZDIyLTk0ZmYxZDFkODU5NyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpqZW5raW5zOmplbmtpbnMtYWRtaW4ifQ.gSRbsAly7iFrPo-guZChrPJKXc5xl9Vu-gnmnSxzJqCBkT5IXBxi0N3VkiODDFPvpHnfLbAU6Mx5q07_z1LW4z4b-YBbQVWnScp0qTxXCn5PIxACmeDnnnLLvlKvmDGfG_jnf4mht3NnQhku9vg_JlTkQlxTRTyg4zHU68JQLpH_09HQOomAcCyrEFyXooSt4YckurW5suixy_0yE4U7NrxRhghDWaXafUMySjEpG0zWuB3Q7LqqOVmbTYn7xP4vkhkR_j1JDk-bmRKI0uKTit8Tte-1dfqd5SXi2hqHaEa3QN4L7heL5gZUzOquqY4E6Ud60RuItPXmPsmPmU1i0w --kubeconfig=/tmp/kubeconfig [root@centos-7-51 ~]# kubectl config set-context jenkins@kubernetes --cluster=jenkins-cluster --user=jenkins-admin --namespace=jenkins --kubeconfig=/tmp/kubeconfig [root@centos-7-51 ~]# kubectl config use-context jenkins@kubernetes --kubeconfig=/tmp/kubeconfig
1.4.2. 配置凭据
使用kubeconfig 配置Kubernetes密钥
【系统管理】—>【manager credentails】—>…—>【添加凭据】,上传上一步生成的kubeconfig文件
1.4.3. 配置动态Agent
【系统管理】—>【节点管理】—>【Configure Clouds】—>【Add a new cloud】—>【Kubernetes】
Pod模板暂时可以不用配置,因为用户可以在自己的Jenkinsfile中定义Pod模板替换掉全局模板。
1.4.4. 使用pipeline定义Pod模板
使用Kubernetes Plugin生成动态模板时,Plugin 会启动一个名为jnlp的jenkins agent容器,并且连接Jenkins Master,如果没有特殊必要性,不要修改该容器。
用户根据需求在pipeline中定义Pod模板,这种方式会和默认的模板合并生成最终的 Yaml文件。
Kubernetes 插件支持 全局 yaml, podTemplate, containerTemplate 等语法,一般用的多些是使用全局的yaml文件,根据需要添加容器和配置。
相关文档可以参考: Kubernetes Plugin
测试项目:go-simple tag: v0.0.2
pipeline {
agent {
kubernetes {
// 选择使用哪个机器,如果只有一个kubernetes,可以忽略
cloud 'kubernetes-10.4.7.59'
// 配置默认的 container, 如果该镜像不存在git命令,会使用jenkins镜像拉取代码
defaultContainer 'golang'
// 定义Pod 的清单文件
yaml """\
apiVersion: v1
kind: Pod
metadata:
labels:
jenkins-agent: "true"
env: golang-1.15.11
spec:
# privileged:true
# 使用root是为了拉取依赖并写入gopath
runAsUser: root
volumes:
- name: go-path
hostPath:
path: /opt/release/golang/path
type: DirectoryOrCreate
- name: docker-socket
hostPath:
path: /var/run/docker.sock
type: Socket
# 配置内网域名解析,因为实验环境下,没有内网DNS
hostAliases:
- ip: "10.4.7.80"
hostnames:
- jenkins.ddn.com
containers:
- name: golang
image: golang:1.15.11
command:
- cat
tty: true
resources:
limits:
memory: "2048Mi"
cpu: "1000m"
requests:
memory: "256Mi"
cpu: "500m"
volumeMounts:
- name: go-path
mountPath: /go
- name: docker-client
image: docker:20.10
command:
- cat
tty: true
resources:
limits:
memory: "2048Mi"
cpu: "500m"
requests:
memory: "256Mi"
cpu: "100m"
volumeMounts:
- name: docker-socket
mountPath: /var/run/docker.sock
""".stripIndent()
}
}
environment {
GOOS='linux'
GOARCH='amd64'
GOPROXY='https://goproxy.cn,direct'
CGO_ENABLED=0
GO111MODULE='on'
}
options {
timeout(time:10,unit:'MINUTES')
}
parameters {
string(name:"image", defaultValue:"linuxduduniao/server", description:"image name")
string(name:"tag", defaultValue:"latest", description:"image tag")
string(name:"registry", defaultValue:"https://index.docker.io/v1/", description:"docker registry url")
string(name:"credential", defaultValue:"dockerhub-push", description:"docker credential id")
}
stages {
stage('Preparing') {
steps {
sh "printenv"
echo "${params.image}:${params.tag}"
echo "${params.registry} ${params.credential}"
}
}
stage('Build') {
steps {
// sh "make build IMAGE=${params.image} TAG=${params.tag}"
sh "go build -o server cmd/main.go"
}
}
stage('Testing') {
steps {
// sh "make test IMAGE=${params.image} TAG=${params.tag}"
echo "testing ..."
}
}
stage('image') {
environment {
// 获取凭证用于登陆 docker hub 仓库
DOCKER_HUB_CRED = credentials("${params.credential}")
DOCKER_REGISTRY = "${params.registry}"
}
steps {
container('docker-client') {
// 注意下面的这种写法,目的是为了使用系统环境变量,而不是将敏感字符直接传递给shell
sh('docker login -u $DOCKER_HUB_CRED_USR -p $DOCKER_HUB_CRED_PSW $DOCKER_REGISTRY')
sh "docker build -t ${params.image}:${params.tag} . "
sh "docker push ${params.image}:${params.tag}"
}
}
post {
always {
container('docker-client') {
sh "docker image rm ${params.image}:${params.tag} . || true"
sh "docker logout ${params.registry} || true"
}
}
}
}
}
}
这里面需要考虑的问题是,用户(开发者)具备自定义Pipeline的权限,可以自己配置启动用户和启动模式,可能存在一定的安全风险。
另外用户可以自己定义资源需求,不过这部分可以使用名称空间配额解决。
针对这些问题,可以提供通用的Jenkins模板,要求所有用户将编译、打包过程写入Makefile中,Jenkins只需要运行约定的指令即可完成编译、打包、上传镜像、测试等一系列步骤。而用户需要传递git仓库地址、分支(tag)信息、其它都采用默认值。这种方式灵活性较低,但是可以将权限收紧。
2. 对接代码仓库
2.1. 对接gitee
gitee 是国内网络能流畅访问的代码仓库网站,以此来替代github演示jenkins对接代码仓库并触发构建的流程。这种构建方式是由代码仓库事件触发Jenkins的API,并且传递必要的参数信息,然后Jenkins执行pipeline。由于Jenkins域名 jenkins.ddn.com 无法被gitee解析,因此无法演示!
Gitee 插件的官方文档地址:https://gitee.com/oschina/Gitee-Jenkins-Plugin
2.2. 对接gitlab
pass
3. 对接测试平台
pass
4. 对接部署平台
4.1. 在虚拟机部署
在传统虚拟机部署,最常用的就是Ansible,因此以Ansible作为部署工具,进行学习和演示!为了方便,制作一个包含Ansible的docker镜像,并进行部署操作!
4.1.1. Jenkins 结合Ansible
其实为了更好的控制版本,其实可以选择使用Ansible镜像来执行部署操作,没必要在宿主机安装Ansible。但是在学习初期可以在虚拟机使用yum安装下Ansible。
4.1.1.1. 安装Ansible
[root@centos-81 ~]# cat /etc/yum.repos.d/ansible.repo
[ansible]
name=ansible repo
baseurl=https://releases.ansible.com/ansible/rpm/release/epel-7-x86_64
enable=1
gpgcheck=0
[root@centos-81 ~]# yum repolist
[root@centos-81 ~]# yum install -y ansible-2.9.20-1.el7.ans # 安装指定版本,不同版本下语法可能有略微差异
[root@centos-81 ~]# ansible --version
ansible 2.9.20
config file = /etc/ansible/ansible.cfg
configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/site-packages/ansible
executable location = /usr/bin/ansible
python version = 2.7.5 (default, Apr 11 2018, 07:36:10) [GCC 4.8.5 20150623 (Red Hat 4.8.5-28)]
4.1.1.2. 配置Ansible插件
安装Ansible插件,色彩显示插件 AnsiColor
可以在全局工具配置中添加不同Ansible版本的,指定其二进制文件目录即可。
4.1.2. 使用Ansible部署应用
这边演示生产环境中的一种使用方式,由运维编写Ansible Role和Playbook,并用git仓库进行管理。用户通过前端程序调用Jenkins流水线,Jenkins拉取Ansilbe任务仓库,切换到指定分支,通过Python脚本解析请求参数 中的主机信息和变量信息,然后渲染成Ansible需要的inventory和group_vars信息,最后调用ansible模块执行,并通过 AnsiColor 插件彩色显示!
4.1.2.1. 编写Ansible任务代码
安装Git Parammeter 插件,动态获取仓库的Tag和分支,该插件需要配合 Git 插件一起使用!
仓库地址: https://gitee.com/linux_duduniao/ansible-jobs
4.1.2.2. 使用虚拟机部署
使用虚拟机部署或者容器部署都可以,区别不大。
- 配置ansible远程目标主机的密钥
- 目标Agent上安装Python3 并且安装程序依赖 requirements.txt
编写Pipeline流水线
pipeline { agent { label "ansible && VM" } options { timeout(time:20, ,unit:'MINUTES') // ansible 色彩显示 ansiColor('xterm') } post { always { cleanWs() } } parameters { // 使用的ansible Job 仓库分支 // 需要和git仓库联动获取当前分支或者tag信息 gitParameter(name: 'branch_or_tag', branchFilter: '.*', defaultValue: 'origin/master', description: '选择Branch或者Tag', listSize: '10', tagFilter: '*', type: 'PT_BRANCH_TAG') string(name:"project", description:"指定具体使用哪个job", trim: true) string(name:"action", defaultValue:"install", description:"指定动作,如install/update/unstall/upgrade") // 一般可以是从CMDB接口获取,或者上层服务传递过来,为了简化演示,这次只传递简单的机器IP地址 // 实际生成环境中可能信息更多,如远程用户信息,跳板机信息等等 // 通过Python脚本解析参数,并执行对应的Ansible // {"hosts":{"node-1":{"ip":"192.168.1.100","user":"root","port":22},"node-2":{"ip":"192.168.1.102","user":"root","port":22}},"groups":{"monitor":["node-1","node-2"]}} string(name:"hosts", description:"部署的机器信息") // key:value 格式的json: {"install_dir": "/opt/release"} string(name:"vars", defaultValue:"", description:"Ansible 变量信息") } stages { stage('print params') { steps { echo "branch_or_tag: ${params.branch_or_tag}" echo "project: ${params.project}" echo "hosts: ${params.hosts}" echo "vars: ${params.vars}" echo "action: ${params.action}" } } stage('checkout') { steps { checkout([$class: 'GitSCM', branches: [[name: "${params.branch_or_tag}"]], userRemoteConfigs: [[credentialsId: 'jenkins-deploy-gitee', url: 'git@gitee.com:linux_duduniao/ansible-jobs.git']]]) } } stage('install requirement env') { steps { sh "which python3 || yum install -y python3" sh "which pip3 || yum install -y python3-pip" sh "pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple" } } stage('init ansible inventory') { steps { writeFile encoding: 'utf-8', file: 'hosts.json', text: "${params.hosts}" writeFile encoding: 'utf-8', file: 'vars.json', text: "${params.vars}" sh "python3 init.py" } } stage('run ansible playbook') { steps { ansiblePlaybook colorized: true, credentialsId: 'ansible-deploy-key', disableHostKeyChecking: true, playbook: "projects/${params.project}/playbooks/${params.action}.yaml" } } } }
执行Pipeline
4.2. 在Kubernetes部署
在Kubernetes平台部署有两种方式,一种是使用Kubectl工具进行部署,第二个是使用helm进行部署。讨论两种典型场景(实际场景可能会略有不同):
- 持续集成:
该场景中,核心流程如下:拉取代码—>单元测试—>代码编译—>制作镜像—>推送测试仓库—>部署到测试环境—>自动化测试—>同步镜像到生产仓库
- 部署场景:
该场景中,核心流程如下:解析部署参数—>拉取Helm—>部署到生产集群(灰度发布、蓝绿部署等等)
在我们的学习中,需要考虑如何持续集成场景即可,完成从源代码到K8S中部署,仅以下流程:
拉取代码—>测试和编译—>制作镜像—>部署到K8S集群,重点关注如何使用helm部署到k8s集群:
4.2.1. 制作部署镜像
该镜像用于部署 Helm 包
FROM alpine:3.13
ADD kubectl helm /bin/
4.2.2. 添加k8s集群的凭证
在本章节的 Pod 作为动态Agent 的凭证仅用于jenkins名称空间,无法完成部署的需求,因此需要使用具备集群权限的kubeconfig文件,并上传至Jenkins中
4.2.3. 编写pipeline
pipeline {
agent {
kubernetes {
// 选择使用哪个机器,如果只有一个kubernetes,可以忽略
cloud 'kubernetes-10.4.7.59'
// 配置默认的 container, 如果该镜像不存在git命令,会使用jenkins镜像拉取代码
defaultContainer 'golang'
// 定义Pod 的清单文件
yaml """\
apiVersion: v1
kind: Pod
metadata:
labels:
jenkins-agent: "true"
env: golang-1.15.11
spec:
# privileged:true
# 使用root是为了拉取依赖并写入gopath
runAsUser: root
volumes:
- name: go-path
hostPath:
path: /opt/release/golang/path
type: DirectoryOrCreate
- name: docker-socket
hostPath:
path: /var/run/docker.sock
type: Socket
# 配置内网域名解析,因为实验环境下,没有内网DNS
hostAliases:
- ip: "10.4.7.80"
hostnames:
- jenkins.ddn.com
containers:
- name: golang
image: golang:1.15.11
command:
- cat
tty: true
resources:
limits:
memory: "2048Mi"
cpu: "1000m"
requests:
memory: "256Mi"
cpu: "500m"
volumeMounts:
- name: go-path
mountPath: /go
- name: docker-client
image: docker:20.10
command:
- cat
tty: true
resources:
limits:
memory: "2048Mi"
cpu: "500m"
requests:
memory: "256Mi"
cpu: "100m"
volumeMounts:
- name: docker-socket
mountPath: /var/run/docker.sock
- name: helm
image: linuxduduniao/helm:v0.0.1
command:
- cat
tty: true
""".stripIndent()
}
}
environment {
GOOS='linux'
GOARCH='amd64'
GOPROXY='https://goproxy.cn,direct'
CGO_ENABLED=0
GO111MODULE='on'
}
options {
timeout(time:10,unit:'MINUTES')
}
parameters {
// 打包参数
string(name:"image", defaultValue:"linuxduduniao/server", description:"image name")
string(name:"tag", defaultValue:"latest", description:"image tag")
string(name:"registry", defaultValue:"https://index.docker.io/v1/", description:"docker registry url")
string(name:"credential", defaultValue:"dockerhub-push", description:"docker credential id")
// 部署参数, values用于指定额外的参数
string(name:"namespace", defaultValue:"apps", description:"deploy application namespace")
string(name:"replicas", defaultValue:"1", description:"application replicas")
text(name: 'values', defaultValue:"",description: "values.yaml content base64")
}
stages {
stage('Preparing') {
steps {
sh "printenv"
echo "${params.image}:${params.tag}"
echo "${params.registry} ${params.credential}"
}
}
stage('Build') {
steps {
// sh "make build IMAGE=${params.image} TAG=${params.tag}"
sh "go build -o server cmd/main.go"
}
}
stage('Testing') {
steps {
// sh "make test IMAGE=${params.image} TAG=${params.tag}"
echo "testing ..."
}
}
stage('image') {
environment {
// 获取凭证用于登陆 docker hub 仓库
DOCKER_HUB_CRED = credentials("${params.credential}")
DOCKER_REGISTRY = "${params.registry}"
}
steps {
container('docker-client') {
// 注意下面的这种写法,目的是为了使用系统环境变量,而不是将敏感字符直接传递给shell
sh('docker login -u $DOCKER_HUB_CRED_USR -p $DOCKER_HUB_CRED_PSW $DOCKER_REGISTRY')
sh "docker build -t ${params.image}:${params.tag} . "
sh "docker push ${params.image}:${params.tag}"
}
}
post {
always {
container('docker-client') {
sh "docker image rm ${params.image}:${params.tag} . || true"
sh "docker logout ${params.registry} || true"
}
}
}
}
stage('deploy to kubernetes') {
environment {
KUBECONFIG = credentials('kubeconfig-local-deploy')
}
steps {
container('helm') {
writeFile encoding: 'Base64', file: 'values.yaml', text: "${params.values}"
// 暂时不清楚为啥指定 -n 参数无法生效,于是将namespace写入 values.yaml中
sh "helm template --set replicaCount=${params.replicas} \
--set image.repository=${params.image} --set image.tag=${params.tag} \
--set namespace=${params.namespace} \
-f values.yaml deploy/chart/go-simple | kubectl apply -f -"
}
}
}
}
}