Kubernetes 基于Jenkins 的 CI/CD

2字数 5288阅读 2713

1. 安装Jenkins

新建kube-ops 命名空间

$ kubectl create namespace kube-ops

新建Deployment文件(jenkins2.yaml)

---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: jenkins2
  namespace: kube-ops
spec:
  template:
    metadata:
      labels:
        app: jenkins2
    spec:
      terminationGracePeriodSeconds: 10
      serviceAccount: jenkins2
      containers:
      - name: jenkins
        image: jenkins/jenkins:lts
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
          name: web
          protocol: TCP
        - containerPort: 50000
          name: agent
          protocol: TCP
        resources:
          limits:
            cpu: 1000m
            memory: 1Gi
          requests:
            cpu: 500m
            memory: 512Mi
        livenessProbe:
          httpGet:
            path: /login
            port: 8080
          initialDelaySeconds: 60
          timeoutSeconds: 5
          failureThreshold: 12 
        readinessProbe:
          httpGet:
            path: /login
            port: 8080
          initialDelaySeconds: 60
          timeoutSeconds: 5
          failureThreshold: 12
        volumeMounts:
        - name: jenkinshome
          subPath: jenkins2
          mountPath: /var/jenkins_home
        env:
        - name: LIMITS_MEMORY
          valueFrom:
            resourceFieldRef:
              resource: limits.memory
              divisor: 1Mi
        - name: JAVA_OPTS
          value: -Xmx$(LIMITS_MEMORY)m -XshowSettings:vm -Dhudson.slaves.NodeProvisioner.initialDelay=0 -Dhudson.slaves.NodeProvisioner.MARGIN=50 -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85 -Duser.timezone=Asia/Shanghai
      securityContext:
        fsGroup: 1000
      volumes:
      - name: jenkinshome
        persistentVolumeClaim:
          claimName: opspvc

---
apiVersion: v1
kind: Service
metadata:
  name: jenkins2
  namespace: kube-ops
  labels:
    app: jenkins2
spec:
  selector:
    app: jenkins2
  type: NodePort
  ports:
  - name: web
    port: 8080
    targetPort: web
    nodePort: 30002
  - name: agent
    port: 50000
    targetPort: agent

2. 创建PVC对象

我们将容器的 /var/jenkins_home 目录挂载到了一个名为 opspvc 的 PVC 对象上面,所以我们同样还得提前创建一个对应的 PVC 对象,我们可以使用 StorageClass 对象来自动创建。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: opspv
spec:
  capacity:
    storage: 20Gi
  accessModes:
  - ReadWriteMany
  persistentVolumeReclaimPolicy: Delete
  nfs:
    server: 192.168.16.239
    path: /data/k8s

---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: opspvc
  namespace: kube-ops
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 20Gi

创建需要用到的 PVC 对象:

$ kubectl  create -f  opspvc.yaml

我们这里还需要使用到一个拥有相关权限的 serviceAccount:jenkins2,我们这里只是给 jenkins 赋予了一些必要的权限,当然如果你对 serviceAccount 的权限不是很熟悉的话,我们给这个 sa 绑定一个 cluster-admin 的集群角色权限也是可以的,当然这样具有一定的安全风险:(rbac.yaml)

apiVersion: v1
kind: ServiceAccount
metadata:
  name: jenkins2
  namespace: kube-ops

---

kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: jenkins2
  namespace: kube-ops
rules:
  - apiGroups: ["extensions", "apps"]
    resources: ["deployments"]
    verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["create","delete","get","list","patch","update","watch"]
  - apiGroups: [""]
    resources: ["pods/exec"]
    verbs: ["create","delete","get","list","patch","update","watch"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get","list","watch"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  name: jenkins2
  namespace: kube-ops
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: jenkins2
subjects:
  - kind: ServiceAccount
    name: jenkins2
    namespace: kube-ops

因为默认的镜像使用的是 jenkins 这个用户,而我们通过 PVC 挂载到 nfs 服务器的共享数据目录下面却是 root 用户的,所以没有权限访问该目录,要解决该问题,也很简单,我只需要在 nfs 共享数据目录下面把我们的目录权限重新分配下即可:

$ chown -R 1000 /data/k8s/jenkins2

直接创建 Jenkins 服务

$ kubectl create -f opspvc.yaml -f   rbac.yaml -f jenkins2.yaml

访问Jenkins:

$ kubectl  get  svc -n kube-ops
NAME       TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)                          AGE
jenkins2   NodePort   10.111.112.3   <none>        8080:30005/TCP,50000:30340/TCP   3h

访问 30005端口


unmin.club

unmin.club

3. 动态Jenkins Slave

从图上可以看到 Jenkins Master 和 Jenkins Slave 以 Pod 形式运行在 Kubernetes 集群的 Node 上,Master 运行在其中一个节点,并且将其配置数据存储到一个 Volume 上去,Slave 运行在各个节点上,并且它不是一直处于运行状态,它会按照需求动态的创建并自动删除。

这种方式的工作流程大致为:当 Jenkins Master 接受到 Build 请求时,会根据配置的 Label 动态创建一个运行在 Pod 中的 Jenkins Slave 并注册到 Master 上,当运行完 Job 后,这个 Slave 会被注销并且这个 Pod 也会自动删除,恢复到最初状态。

那么我们使用这种方式带来了哪些好处呢?

服务高可用,当 Jenkins Master 出现故障时,Kubernetes 会自动创建一个新的 Jenkins Master 容器,并且将 Volume 分配给新创建的容器,保证数据不丢失,从而达到集群服务高可用。
动态伸缩,合理使用资源,每次运行 Job 时,会自动创建一个 Jenkins Slave,Job 完成后,Slave 自动注销并删除容器,资源自动释放,而且 Kubernetes 会根据每个资源的使用情况,动态分配 Slave 到空闲的节点上创建,降低出现因某节点资源利用率高,还排队等待在该节点的情况。
扩展性好,当 Kubernetes 集群的资源严重不足而导致 Job 排队等待时,可以很容易的添加一个 Kubernetes Node 到集群中,从而实现扩展。

安装kubernetes plugin,让他能够动态的生成 Slave 的 Pod。

unmin.club

安装完毕后,点击 Manage Jenkins —> Configure System —> (拖到最下方)Add a new cloud —> 选择 Kubernetes,然后填写 Kubernetes 和 Jenkins 配置信息。
unmin.club

注意 namespace,我们这里填 kube-ops,然后点击Test Connection,如果出现 Connection test successful 的提示信息证明 Jenkins 已经可以和 Kubernetes 系统正常通信了,然后下方的 Jenkins URL 地址:http://jenkins2.kube-ops.svc.cluster.local:8080,这里的格式为:服务名.namespace.svc.cluster.local:8080,根据上面创建的jenkins 的服务名填写,我这里是之前创建的名为jenkins,如果是用上面我们创建的就应该是jenkins2
第3步. 配置 Pod Template,其实就是配置 Jenkins Slave 运行的 Pod 模板,命名空间我们同样是用 kube-ops,Labels 这里也非常重要,对于后面执行 Job 的时候需要用到该值,然后我们这里使用的是 cnych/jenkins:jnlp 这个镜像,这个镜像是在官方的 jnlp 镜像基础上定制的,加入了 kubectl 等一些实用的工具。
unmin.club

另外需要注意我们这里需要在下面挂载两个主机目录,一个是 /var/run/docker.sock,该文件是用于 Pod 中的容器能够共享宿主机的 Docker,这就是大家说的 docker in docker 的方式,Docker 二进制文件我们已经打包到上面的镜像中了,另外一个目录下 /root/.kube 目录,我们将这个目录挂载到容器的 /home/jenkins/.kube 目录下面这是为了让我们能够在 Pod 的容器中能够使用 kubectl 工具来访问我们的 Kubernetes 集群,方便我们后面在 Slave Pod 部署 Kubernetes 应用。
unmin.club

另外还有几个参数需要注意,如下图中的Time in minutes to retain slave when idle,这个参数表示的意思是当处于空闲状态的时候保留 Slave Pod 多长时间,这个参数最好我们保存默认就行了,如果你设置过大的话,Job 任务执行完成后,对应的 Slave Pod 就不会立即被销毁删除。
unmin.club

测试
Kubernetes 插件的配置工作完成了,接下来我们就来添加一个 Job 任务,看是否能够在 Slave Pod 中执行,任务执行完成后看 Pod 是否会被销毁。

在 Jenkins 首页点击create new jobs,创建一个测试的任务,输入任务名称,然后我们选择 Freestyle project 类型的任务:


unmin.club

注意在下面的 Label Expression 这里要填入haimaxy-jnlp,就是前面我们配置的 Slave Pod 中的 Label,这两个地方必须保持一致


unmin.club

然后往下拉,在 Build 区域选择Execute shell
unmin.club

然后输入我们测试命令

echo "测试 Kubernetes 动态生成 jenkins slave"
echo "==============docker in docker==========="
docker info

echo "=============kubectl============="
kubectl get pods

unmin.club

保存后我们直接在页面点击做成的 Build now 触发构建即可,然后观察 Kubernetes 集群中 Pod 的变化

$ kubectl get pods -n kube-ops
NAME                       READY     STATUS              RESTARTS   AGE
jenkins2-7c85b6f4bd-rfqgv   1/1       Running             3          1d
jnlp-hfmvd          

可以看到在我们点击立刻构建的时候可以看到一个新的 Pod:jnlp-hfmvd 被创建了,这就是我们的 Jenkins Slave。任务执行完成后我们可以看到任务信息,比如我们这里是 花费了 5.2s 时间在 jnlp-hfmvd 这个 Slave上面


unmin.club

到这里证明我们的任务已经构建完成,然后这个时候我们再去集群查看我们的 Pod 列表,发现 kube-ops 这个 namespace 下面已经没有之前的 Slave 这个 Pod 了。

$ kubectl get pods -n kube-ops
NAME                       READY     STATUS    RESTARTS   AGE
jenkins2-7c85b6f4bd-rfqgv   1/1       Running   3          1d

到这里我们就完成了使用 Kubernetes 动态生成 Jenkins Slave 的方法

4. Jenkins Pipeline部署kubernetes应用

Jenkins Pipeline 介绍
要实现在 Jenkins 中的构建工作,可以有多种方式,我们这里采用比较常用的 Pipeline 这种方式。Pipeline,简单来说,就是一套运行在 Jenkins 上的工作流框架,将原来独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂流程编排和可视化的工作。

Jenkins Pipeline 有几个核心概念:

  • Node:节点,一个 Node 就是一个 Jenkins 节点,Master 或者 Agent,是执行 Step 的具体运行环境,比如我们之前动态运行的 Jenkins Slave 就是一个 Node 节点
  • Stage:阶段,一个 Pipeline 可以划分为若干个 Stage,每个 Stage 代表一组操作,比如:Build、Test、Deploy,Stage 是一个逻辑分组的概念,可以跨多个 Node
  • Step:步骤,Step 是最基本的操作单元,可以是打印一句话,也可以是构建一个 Docker 镜像,由各类 Jenkins 插件提供,比如命令:sh 'make',就相当于我们平时 shell 终端中执行 make 命令一样。

那么我们如何创建 Jenkins Pipline 呢?

  • Pipeline 脚本是由 Groovy 语言实现的,但是我们没必要单独去学习 Groovy,当然你会的话最好
  • Pipeline 支持两种语法:Declarative(声明式)和 Scripted Pipeline(脚本式)语法
  • Pipeline 也有两种创建方法:可以直接在 Jenkins 的 Web UI 界面中输入脚本;也可以通过创建一个 Jenkinsfile 脚本文件放入项目源码库中
  • 一般我们都推荐在 Jenkins 中直接从源代码控制(SCMD)中直接载入 Jenkinsfile Pipeline 这种方法

在 Slave 中构建任务

我们在添加 Slave Pod 的时候,记的添加的 label 吗?没错,我们就需要用到这个 label,我们重新编辑上面创建的 Pipeline 脚本,给 node 添加一个 label 属性,如下:

node('haimaxy-jnlp') {
    stage('Clone') {
      echo "1.Clone Stage"
    }
    stage('Test') {
      echo "2.Test Stage"
    }
    stage('Build') {
      echo "3.Build Stage"
    }
    stage('Deploy') {
      echo "4. Deploy Stage"
    }
}

构建之前查看下 kubernetes 集群中的 Pod:

$ kubectl get pods -n kube-ops
NAME                       READY     STATUS    RESTARTS   AGE
jenkins-7c85b6f4bd-rfqgv   1/1       Running   4          6d
jnlp-0hrrz                 1/1       Running   0          23s

我们发现多了一个名叫jnlp-0hrrz的 Pod 正在运行,隔一会儿这
个 Pod 就消失了:这也证明我们的 Job 构建完成了,同样回到 Jenkins 的 Web UI 界面中查看 Console Output,可以看到如下的信息:


unmin.club

unmin.club

5. 部署 Kubernetes 应用

要部署 Kubernetes 应用,我们就得对我们之前部署应用的流程要非常熟悉才行,我们之前的流程是怎样的:

  • 编写代码
  • 测试
  • 编写 Dockerfile
  • 构建打包 Docker 镜像
  • 推送 Docker 镜像到仓库
  • 编写 Kubernetes YAML 文件
  • 更改 YAML 文件中 Docker 镜像 TAG
  • 利用 kubectl 工具部署应用

现在我们就需要把上面这些流程放入 Jenkins 中来自动帮我们完成(当然编码除外),从测试到更新 YAML 文件属于 CI 流程,后面部署属于 CD 的流程。如果按照我们上面的示例,我们现在要来编写一个 Pipeline 的脚本,应该怎么编写呢?

node('haimaxy-jnlp') {
    stage('Clone') {
      echo "1.Clone Stage"
    }
    stage('Test') {
      echo "2.Test Stage"
    }
    stage('Build') {
      echo "3.Build Docker Image Stage"
    }
    stage('Push') {
      echo "4.Push Docker Image Stage"
    }
    stage('YAML') {
      echo "5. Change YAML File Stage"
    }
    stage('Deploy') {
      echo "6. Deploy Stage"
    }
}

我们来将一个简单 golang 程序,部署到 kubernetes 环境中,代码链接:(http://gitlab.unmin.club:82/root/jenkins-demo)。如果按照之前的示例,我们是不是应该像这样来编写 Pipeline 脚本:

  • 第一步,clone 代码,这个没得说吧
  • 第二步,进行测试,如果测试通过了才继续下面的任务
  • 第三步,由于 Dockerfile 基本上都是放入源码中进行管理的,所* 以我们这里就是直接构建 Docker 镜像了
  • 第四步,镜像打包完成,就应该推送到镜像仓库中吧
  • 第五步,镜像推送完成,是不是需要更改 YAML 文件中的镜像 TAG 为这次镜像的 TAG
  • 第六步,万事俱备,只差最后一步,使用 kubectl 命令行工具进行部署了

到这里我们的整个 CI/CD 的流程是不是就都完成了。

接下来我们就来对每一步具体要做的事情进行详细描述就行了
第一步,Clone 代码

stage('Clone') {
    echo "1.Clone Stage"
    git url: "http://gitlab.unmin.club:82/root/jenkins-demo.git"
}

第二步,测试
由于我们这里比较简单,忽略该步骤即可
第三步,构建镜像

stage('Build') {
    echo "3.Build Docker Image Stage"
    sh "docker build -t zam2017/jenkins-demo:${build_tag} ."
}

我们平时构建的时候是不是都是直接使用docker build命令进行构建就行了,那么这个地方呢?我们上节课给大家提供的 Slave Pod 的镜像里面是不是采用的 Docker In Docker 的方式,也就是说我们也可以直接在 Slave 中使用 docker build 命令,所以我们这里直接使用 sh 直接执行 docker build 命令即可。
但是镜像的 tag 呢?如果我们使用镜像 tag,则每次都是 latest 的 tag,这对于以后的排查或者回滚之类的工作会带来很大麻烦,我们这里采用和git commit的记录为镜像的 tag,这里有一个好处就是镜像的 tag 可以和 git 提交记录对应起来,也方便日后对应查看。但是由于这个 tag 不只是我们这一个 stage 需要使用,下一个推送镜像是不是也需要,所以这里我们把这个 tag 编写成一个公共的参数,把它放在 Clone 这个 stage 中,这样一来我们前两个 stage 就变成了下面这个样子:

stage('Clone') {
    echo "1.Clone Stage"
    git url: "http://gitlab.unmin.club:82/root/jenkins-demo.git"
    script {
        build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
    }
}
stage('Build') {
    echo "3.Build Docker Image Stage"
    sh "docker build -t zam2017/jenkins-demo:${build_tag} ."
}
第四步,推送镜像:

镜像构建完成了,现在我们就需要将此处构建的镜像推送到镜像仓库中去,当然如果你有私有镜像仓库也可以,我们这里还没有自己搭建私有的仓库,所以直接使用 docker hub 即可。
我们需要提前注册一个 docker hub 的帐号,记住用户名和密码,我们这里需要使用。正常来说我们在本地推送 docker 镜像的时候,是不是需要使用docker login命令,然后输入用户名和密码,认证通过后,就可以使用docker push命令来推送本地的镜像到 docker hub 上面去了

stage('Push') {
    echo "4.Push Docker Image Stage"
    sh "docker login -u zam2017 -p xxxxx"
    sh "docker push zam2017/jenkins-demo:${build_tag}"
}

在 Jenkins 的 Web UI 界面中来完成这个任务的话,我们这里的 Pipeline 是可以这样写的,但是我们是不是推荐使用 Jenkinsfile 的形式放入源码中进行版本管理,这样的话我们直接把 docker 仓库的用户名和密码暴露给别人这样很显然是非常非常不安全的,更何况我们这里使用的是 github 的公共代码仓库,所有人都可以直接看到我们的源码,所以我们应该用一种方式来隐藏用户名和密码这种私密信息,幸运的是 Jenkins 为我们提供了解决方法。

在首页点击 Credentials -> Stores scoped to Jenkins 下面的 Jenkins -> Global credentials (unrestricted) -> 左侧的 Add Credentials:添加一个 Username with password 类型的认证信息,如下:

unmin.club

输入 docker hub 的用户名和密码,ID 部分我们输入dockerHub,注意,这个值非常重要,在后面 Pipeline 的脚本中我们需要使用到这个 ID 值。

有了上面的 docker hub 的用户名和密码的认证信息,现在我们可以在 Pipeline 中使用这里的用户名和密码了:

stage('Push') {
    echo "4.Push Docker Image Stage"
    withCredentials([usernamePassword(credentialsId: 'dockerHub', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) {
        sh "docker login -u ${dockerHubUser} -p ${dockerHubPassword}"
        sh "docker push zam2017/jenkins-demo:${build_tag}"
    }
}

我们这里在 stage 中使用了一个新的函数withCredentials,其中有一个 credentialsId 值就是我们刚刚创建的 ID 值,而对应的用户名变量就是 ID 值加上 User,密码变量就是 ID 值加上 Password,然后我们就可以在脚本中直接使用这里两个变量值来直接替换掉之前的登录 docker hub 的用户名和密码,现在是不是就很安全了。

第五步,更改 YAML

上面我们已经完成了镜像的打包、推送的工作,接下来我们是不是应该更新 Kubernetes 系统中应用的镜像版本了,当然为了方便维护,我们都是用 YAML 文件的形式来编写应用部署规则,比如我们这里的 YAML 文件:(k8s.yaml)

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: jenkins-demo
spec:
  template:
    metadata:
      labels:
        app: jenkins-demo
    spec:
      containers:
      - image: zam2017/jenkins-demo:<BUILD_TAG>
        imagePullPolicy: IfNotPresent
        name: jenkins-demo

我们使用一个 Deployment 资源对象来管理 Pod,该 Pod 使用的就是我们上面推送的镜像,唯一不同的地方是 Docker 镜像的 tag 不是我们平常见的具体的 tag,而是一个 的标识,实际上如果我们将这个标识替换成上面的 Docker 镜像的 tag,是不是就是最终我们本次构建需要使用到的镜像?怎么替换呢?其实也很简单,我们使用一个sed命令就可以实现了:

stage('YAML') {
    echo "5. Change YAML File Stage"
    sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
}
第六步,部署
stage('Deploy') {
    echo "6. Deploy Stage"
    sh "kubectl apply -f k8s.yaml"
}

到这里基本流程已经配置完毕了,可以构建测试一下。

人工确认

理论上来说我们上面的6个步骤其实已经完成了,但是一般在我们的实际项目实践过程中,可能还需要一些人工干预的步骤,这是为什么呢?比如我们提交了一次代码,测试也通过了,镜像也打包上传了,但是这个版本并不一定就是要立刻上线到生产环境的,对吧,我们可能需要将该版本先发布到测试环境、QA 环境、或者预览环境之类的,总之直接就发布到线上环境去还是挺少见的,所以我们需要增加人工确认的环节,一般都是在 CD 的环节才需要人工干预,比如我们这里的最后两步,我们就可以在前面加上确认,比如:

stage('YAML') {
    echo "5. Change YAML File Stage"
    def userInput = input(
        id: 'userInput',
        message: 'Choose a deploy environment',
        parameters: [
            [
                $class: 'ChoiceParameterDefinition',
                choices: "Dev\nQA\nProd",
                name: 'Env'
            ]
        ]
    )
    echo "This is a deploy step to ${userInput.Env}"
    sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
}

我们这里使用了 input 关键字,里面使用一个 Choice 的列表来让用户进行选择,然后在我们选择了部署环境后,我们当然也可以针对不同的环境再做一些操作,比如可以给不同环境的 YAML 文件部署到不同的 namespace 下面去,增加不同的标签等等操作:

stage('Deploy') {
    echo "6. Deploy Stage"
    if (userInput.Env == "Dev") {
      // deploy dev stuff
    } else if (userInput.Env == "QA"){
      // deploy qa stuff
    } else {
      // deploy prod stuff
    }
    sh "kubectl apply -f k8s.yaml"
}

由于这一步也属于部署的范畴,所以我们可以将最后两步都合并成一步,我们最终的 Pipeline 脚本如下:

node('haimaxy-jnlp') {
    stage('Clone') {
        echo "1.Clone Stage"
        git url: "http://gitlab.unmin.club:82/root/jenkins-demo.git"
        script {
            build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
        }
    }
    stage('Test') {
      echo "2.Test Stage"
    }
    stage('Build') {
        echo "3.Build Docker Image Stage"
        sh "docker build -t zam2017/jenkins-demo:${build_tag} ."
    }
    stage('Push') {
        echo "4.Push Docker Image Stage"
        withCredentials([usernamePassword(credentialsId: 'dockerHub', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) {
            sh "docker login -u ${dockerHubUser} -p ${dockerHubPassword}"
            sh "docker push zam2017/jenkins-demo:${build_tag}"
        }
    }
    stage('Deploy') {
        echo "5. Deploy Stage"
        def userInput = input(
            id: 'userInput',
            message: 'Choose a deploy environment',
            parameters: [
                [
                    $class: 'ChoiceParameterDefinition',
                    choices: "Dev\nQA\nProd",
                    name: 'Env'
                ]
            ]
        )
        echo "This is a deploy step to ${userInput}"
        sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
        if (userInput == "Dev") {
            // deploy dev stuff
        } else if (userInput == "QA"){
            // deploy qa stuff
        } else {
            // deploy prod stuff
        }
        sh "kubectl apply -f k8s.yaml"
    }
}

现在我们在 Jenkins Web UI 中重新配置 jenkins-demo 这个任务,将上面的脚本粘贴到 Script 区域,重新保存,然后点击左侧的 Build Now,触发构建,然后过一会儿我们就可以看到 Stage View 界面出现了暂停的情况:


unmin.club

unmin.club

选择PROD,可以看到console控制台打印信息


unmin.club

到服务器查看是否部署成功
[root@master app]# kubectl  get pod 
NAME                                      READY     STATUS             RESTARTS   AGE
jenkins-demo-67446f58cd-mvpj8             0/1       CrashLoopBackOff   5          3m
nfs-client-provisioner-6b688c9495-dc9gb   1/1       Running            6          10d
nfs-web-0                                 1/1       Running            5          10d
nfs-web-1                                 1/1       Running            2          10d
nfs-web-2                                 1/1       Running            0          10d
nginx-deployment1-6f98c47948-hqgf9        1/1       Running            0          3d
static-web-master                         1/1       Running            11         99d
test-pod                                  0/1       Completed          0          10d
web-0                                     0/1       Pending            0          10d
[root@master app]# kubectl   logs -f  jenkins-demo-67446f58cd-mvpj8 
Hello, Kubernetes!I'm from Jenkins CI/CD!

我们可以看到我们的应用已经正确的部署到了 Kubernetes 的集群环境中.

6 .Jenkinsfile

在实际的工作实践中,我们更多的是将 Pipeline 脚本写入到 Jenkinsfile 文件中,然后和代码一起提交到代码仓库中进行版本管理。现在我们将上面的 Pipeline 脚本拷贝到一个 Jenkinsfile 中,将该文件放入上面的 git 仓库中,但是要注意的是,现在既然我们已经在 git 仓库中了,是不是就不需要 git clone 这一步骤了,所以我们需要将第一步 Clone 操作中的 git clone 这一步去掉,可以参考:http://www.unmin.club:82/root/jenkins-demo

然后我们更改上面的 jenkins-demo 这个任务,点击 Configure -> 最下方的 Pipeline 区域 -> 将之前的 Pipeline Script 更改成 Pipeline Script from SCM,然后根据我们的实际情况填写上对应的仓库配置,要注意 Jenkinsfile 脚本路径:


unmin.club

构建测试


unmin.club

Jenkins BlueOcean

上面我们使用了 Jenkins Pipeline 来自动化部署一个 Kubernetes 应用,在实际的项目中,往往一个代码仓库都会有很多分支的,比如开发、测试、线上这些分支都是分开的,一般情况下开发或者测试的分支我们希望提交代码后就直接进行 CI/CD 操作,而线上的话最好增加一个人工干预的步骤。Jenkins 也是支持对代码仓库有多分支的流程。

现在我们新建一个dev分支,依然使用Jenkinsfile 的方式配置。参考代码

node('haimaxy-jnlp') {
    stage('Prepare') {
        echo "1.Prepare Stage"
        checkout scm
        script {
            build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
            if (env.BRANCH_NAME != 'master') {
                build_tag = "${env.BRANCH_NAME}-${build_tag}"
            }
        }
    }
    stage('Test') {
      echo "2.Test Stage"
    }
    stage('Build') {
        echo "3.Build Docker Image Stage"
        sh "docker build -t zam2017/jenkins-demo:${build_tag} ."
    }
    stage('Push') {
        echo "4.Push Docker Image Stage"
        withCredentials([usernamePassword(credentialsId: 'dockerHub', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) {
            sh "docker login -u ${dockerHubUser} -p ${dockerHubPassword}"
            sh "docker push zam2017/jenkins-demo:${build_tag}"
        }
    }
    stage('Deploy') {
        echo "5. Deploy Stage"
        if (env.BRANCH_NAME == 'master') {
            input "确认要部署线上环境吗?"
        }
        sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
        sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml"
        sh "kubectl apply -f k8s.yaml --record"
    }
}

在第一步中我们增加了checkout scm命令,用来检出代码仓库中当前分支的代码,为了避免各个环境的镜像 tag 产生冲突,我们为非 master 分支的代码构建的镜像增加了一个分支的前缀,在第五步中如果是 master 分支的话我们才增加一个确认部署的流程,其他分支都自动部署,并且还需要替换 k8s.yaml 文件中的环境变量的值。更改完成后,提交 dev 分支到 github 仓库中。

BlueOcean

我们这里使用 BlueOcean 这种方式来完成此处 CI/CD 的工作,BlueOcean 是 Jenkins 团队从用户体验角度出发,专为 Jenkins Pipeline 重新设计的一套 UI 界面,仍然兼容以前的 fressstyle 类型的 job,BlueOcean 具有以下的一些特性:

  • 连续交付(CD)Pipeline 的复杂可视化,允许快速直观的了解 Pipeline 的状态
  • 可以通过 Pipeline 编辑器直观的创建 Pipeline
  • 需要干预或者出现问题时快速定位,BlueOcean 显示了 Pipeline 需要注意的地方,便于异常处理和提高生产力
  • 用于分支和拉取请求的本地集成可以在 GitHub 或者 Bitbucket 中与其他人进行代码协作时最大限度提高开发人员的生产力。
    BlueOcean 可以安装在现有的 Jenkins 环境中,也可以使用 Docker 镜像的方式直接运行,我们这里直接在现有的 Jenkins 环境中安装 BlueOcean 插件:登录 Jenkins Web UI -> 点击左侧的 Manage Jenkins -> Manage Plugins -> Available -> 搜索查找 BlueOcean -> 点击下载安装并重启
unmin.club

点击开始创建一个新的 Pipeline


unmin.club

Blue Ocean 会自动扫描仓库中的每个分支,会为根文件夹中包含Jenkinsfile的每个分支创建一个 Pipeline,比如我们这里有 master 和 dev 两个分支,并且两个分支下面都有 Jenkinsfile 文件,所以创建完成后会生成两个 Pipeline:


unmin.club

我们可以看到有两个任务在运行了,我们可以把 master 分支的任务停止掉,我们只运行 dev 分支即可,然后我们点击 dev 这个 pipeline 就可以进入本次构建的各步骤详细页面:


unmin.club

可以看到本次构建的 Docker 镜像的 Tag 为dev-d429515,是符合我们在jenkinsfile文件中的定义,非master分支是需要有分支前缀的


unmin.club

查看dockerHub 仓库镜像是否已上传
unmin.club

现在我们本地克隆代码,修改后提交到gitlab仓库,查看jenkins是否基于我们定义的非master分支代码自动触发构建。

[root@master app]# git clone http://gitlab.unmin.club:82/root/jenkins-demo.git
正克隆到 'jenkins-demo'...
remote: Counting objects: 110, done.
remote: Compressing objects: 100% (59/59), done.
remote: Total 110 (delta 65), reused 93 (delta 50)
接收对象中: 100% (110/110), 9.72 KiB | 0 bytes/s, done.
处理 delta 中: 100% (65/65), done.
[root@master app]# cd jenkins-demo/
[root@master jenkins-demo]# git status
# 位于分支 master
无文件要提交,干净的工作区
[root@master jenkins-demo]# git checkout dev 
分支 dev 设置为跟踪来自 origin 的远程分支 dev。
切换到一个新分支 'dev'
[root@master jenkins-demo]# vim main.go 
[root@master jenkins-demo]# git add .
[root@master jenkins-demo]# git commit -m " test "
[dev aca25d0]  test
 1 file changed, 1 insertion(+), 1 deletion(-)
[root@master jenkins-demo]# git push origin dev
Username for 'http://gitlab.unmin.club:82': root
Password for 'http://root@gitlab.unmin.club:82': 
Counting objects: 5, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 275 bytes | 0 bytes/s, done.
Total 3 (delta 2), reused 0 (delta 0)

remote: 
remote: To create a merge request for dev, visit:
remote:   http://gitlab.unmin.club/root/jenkins-demo/merge_requests/new?merge_request%5Bsource_branch%5D=dev
remote: 
To http://gitlab.unmin.club:82/root/jenkins-demo.git
   d429515..aca25d0  dev -> dev

可以看到jenkins已经触发构建,版本号也是我们gitlab的最新提交的版本号


unmin.club

构建完成后,我们查看kubernetes是否为我们创建了响应的Pod

[root@master jenkins-demo]# kubectl  get  pod 
NAME                                      READY     STATUS             RESTARTS   AGE
jenkins-demo-676769b5bf-q75dg             0/1       CrashLoopBackOff   285        23h
nfs-client-provisioner-6b688c9495-dc9gb   1/1       Running            6          17d
nfs-web-0                                 1/1       Running            5          17d
nfs-web-1                                 1/1       Running            2          17d
nfs-web-2                                 1/1       Running            0          17d
static-web-master                         1/1       Running            11         106d
test-pod                                  0/1       Completed          0          17d
web-0                                     0/1       Pending            0          17d
[root@master jenkins-demo]# kubectl logs  -f jenkins-demo-676769b5bf-q75dg  
Hello, Kubernetes!I'm  dev    from Jenkins CI!
BRANCH: dev

好的 ,如上所示dev的验证是符合我们的预期。现在我们把dev分支的代码合并到master,验证jenkins是否和我们定义的规则相同。

[root@master jenkins-demo]# git status
# 位于分支 dev
无文件要提交,干净的工作区
[root@master jenkins-demo]# git checkout master
切换到分支 'master'
[root@master jenkins-demo]# git merge  dev
自动合并 main.go
Merge made by the 'recursive' strategy.
 main.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
[root@master jenkins-demo]# git push origin master
Username for 'http://gitlab.unmin.club:82': root
Password for 'http://root@gitlab.unmin.club:82': 
Counting objects: 1, done.
Writing objects: 100% (1/1), 217 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To http://gitlab.unmin.club:82/root/jenkins-demo.git
   992361a..852f3da  master -> master

jenkins上master分支代码已经触发自动构建了,可以查看到此处推送的镜像 TAG 为852f3da ,没有分支的前缀,镜像推送完成后,进入 Deploy 阶段的时候我们可以看到出现了一个暂停的操作,让我们选择是否需要部署到线上,我们前面在Jenkinsfile定义的如果是 master 分支的话,在部署的阶段需要我们人工确认:


unmin.club

unmin.club

现在我们点击Proceed按钮发布到生产环境,查看服务器Pod是否为我们合并到master分支上的代码

[root@master jenkins-demo]# kubectl  get pod
NAME                                      READY     STATUS      RESTARTS   AGE
jenkins-demo-6865cbf6bf-ckf2x             0/1       Completed   0          16s
nfs-client-provisioner-6b688c9495-dc9gb   1/1       Running     6          17d
nfs-web-0                                 1/1       Running     5          17d
nfs-web-1                                 1/1       Running     2          17d
nfs-web-2                                 1/1       Running     0          17d
static-web-master                         1/1       Running     11         106d
test-pod                                  0/1       Completed   0          17d
web-0                                     0/1       Pending     0          17d
[root@master jenkins-demo]# kubectl  logs -f jenkins-demo-6865cbf6bf-ckf2x   
Hello, Kubernetes!I'm   test dev    from Jenkins CI!
BRANCH: master

可以看到打印出来的信息是 master,证明部署是没有问题的。

到这里我们就实现了多分支代码仓库的完整的 CI/CD 流程。

参考:https://www.qikqiak.com/

推荐阅读更多精彩内容