1. 分布式构建

jenkins 采用的是 master + slave(agent) 结构,其中master承担HTTP请求和任务配置,slave 承担具体的构建任务。在Jenkins架构如下图:
未命名绘图.png
涉及到的名词:
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用户,并添加公钥

    1. [root@centos-81 ~]# useradd jenkins
    2. [root@centos-81 ~]# su - jenkins
    3. [jenkins@centos-81 ~]$ mkdir .ssh
    4. [jenkins@centos-81 ~]$ vim .ssh/authorized_keys # 添加ssh公钥
    5. [jenkins@centos-81 ~]$ chmod 750 .ssh ; chmod 640 .ssh/authorized_keys
    6. [root@centos-81 ~]# mkdir /data/jenkins ; chown -R jenkins.jenkins /data/jenkins
    7. [root@centos-81 ~]# yum install -y git # 一定需要安装git
  • 添加私钥

【系统管理】—>【密钥管理】—>【全局凭据】—>【添加凭据】
image.png

  • 添加节点

【系统管理】—>【节点管理】—>【新建节点】
image.png

  • 配置节点信息,使用SSH方式连接,需要确保jenkisn工作目录有权限

image.png
image.png

  • 查看Agent日志

image.png

  • 测试Agent

    pipeline {
      agent {
          label "VM && golang"
      }
      stages {
          stage('Preparing') {
              steps {
                  sh "hostname"
              }
          }
      }
    }
    

    1.1.2. 使用JAVA WEB方式

  • 添加节点

image.png

  • 节点列表—>点击节点名称—>查看添加节点方式

image.png

  • 配置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去执行任务

这种动态Agent需要安装docker plugin

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文件
image.png

1.4.3. 配置动态Agent

【系统管理】—>【节点管理】—>【Configure Clouds】—>【Add a new cloud】—>【Kubernetes】
image.png
image.png
image.png
image.png
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远程目标主机的密钥

image.png

  • 目标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

image.png
image.png

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中
image.png

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 -"
                }
            }
        }
    }
}

4.2.4. 测试

image.png
image.png


05-2-3-Jenkins实践 - 图20