Jenkins on Kubernetes中的Pipeline语法以及自定义Slave的使用方式

Posted 嗡汤圆

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jenkins on Kubernetes中的Pipeline语法以及自定义Slave的使用方式相关的知识,希望对你有一定的参考价值。

从单体应用,到Spring Cloud,再到Kubernetes,我们的平台正在一点一点追赶别人的脚步。但是由于网络隔离等外部因素影响,CI/CD一直是实施比较困难的环节。最近稍微空闲一些,准备一鼓作气做一次整体代码迁移,打通代码仓库和应用部署环境。在技术选型方面,jenkins的入门门槛最低,同时由于我们已经将应用部署在Kubernetes了,因此选择了集群方式的Jenkins部署。主要部署参考文档: 初试 Jenkins 使用 Kubernetes Plugin 完成持续构建与发布
下面我主要对Jenkins部署过程中的关键点、Jenkins使用过程中Pipeline和slave的定制方面分享一下心得。

1 Jenkins在Kubernetes上的部署

首先最好单独新建一个CICD namespace以便将Jenkins和生产应用独立开。
当Jenkins以集群方式部署时候,我们将Master以Deployment的方式部署在Kubernetes中。要点如下:

1.1 建立ServiceAccount

建立ServiceAccount以分配权限

apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app: jenkins
  name: jenkins-admin
  namespace: cicd
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: jenkins-admin
  labels:
    app: jenkins
subjects:
  - kind: ServiceAccount
    name: jenkins-admin
    namespace: cicd
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

1.2 配置Volume

由于Jenkins Master需要保存一些配置和插件,因此需要一个可以持久化存储的Volume并挂载在容器内,我们采用比较容易部署的nfs方式。

具体方法参考: CentOS 6.8 NFS 文件共享设置的方法

  1. 找一台磁盘较大的服务器作为文件服务器,并安装nfs服务(nfs-utils, rpcbind)
  2. 启动nfs服务
  3. 在集群节点上安装(nfs-utils)以便任意节点均支持nfs挂载
    然后,我们在Jenkins的Deployment声明文件中挂载nfs即可 (即yaml中的volumes和volumeMounts)
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: jenkins
  namespace: cicd
  labels:
    app: jenkins
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: jenkins
    spec:
      serviceAccountName: jenkins-admin
      containers:
      - name: jenkins
        image: jenkins/jenkins
        imagePullPolicy: Always
        volumeMounts:
        - name: jenkins-home
          mountPath: /var/jenkins_home
          readOnly: false
        - name: maven-repository
          mountPath: /opt/maven/repository
          readOnly: false
        - name: docker
          mountPath: /usr/bin/docker
        - name: docker-sock
          mountPath: /var/run/docker.sock
        ports:
        - containerPort: 8080
        - containerPort: 50000
      volumes:
      - name: jenkins-home
        nfs:
          path: /path/of/your/jenkins_home
          server: ip-of-your-nfs-server
      - name: maven-repository
        nfs:
          path: /path/of/your/jenkins_repository
          server: ip-of-your-nfs-server
      - name: docker
        hostPath:
          path: /usr/bin/docker
      - name: docker-sock
        hostPath:
          path: /var/run/docker.sock

1.3 将Jenkins暴露出来

内部服务我们声明一个jenkins的ClusterIp,对外用户访问则简单点用NodePort形式暴露即可,这里我们使用32003端口。

apiVersion: v1
kind: Service
metadata:
  labels:
    app: jenkins
  name: jenkins
  namespace: cicd
  annotations:
    prometheus.io/scrape: 'true'
spec:
  ports:
  - name: jenkins-web
    port: 8080
    targetPort: 8080
  - name: jenkins-agent
    port: 50000
    targetPort: 50000
  selector:
    app: jenkins
---
apiVersion: v1
kind: Service
metadata:
  name: jenkins-exposed
  namespace: cicd
  labels:
    app: jenkins
spec:
  type: NodePort
  ports:
  - port: 8080
    nodePort: 32003
  selector:
    app: jenkins

1.4 初始化Jenkins

部署上去后,我们打开 nodeip:32003就可以看到jenkins的初始化界面了,按照正常操作,安装必要插件即可。
安装完毕后,我们进入系统配置,在cloud中添加kubernetes,由于Jenkins就运行在kubernetes中,因此Kubernetes地址直接填写内部api-server地址 https://kubernetes.default 即可。这里填写default是因为我们的Jenkins运行在CICD命名空间中,而api-server在default命名空间中。

2 Pipeline的使用

首先先把kubernetes, pipeline等插件安装上,参考的仍然是文章开头的那个博客。

2.1 pipeline雏形

pipeline语法参见: pipeline
我们新建一个任务,项目类型中选择Pipeline即可进入配置页面,最下边的Pipeline选项卡就是填写脚本的地方。
我们一般根据项目需要将构建过程分为几个阶段(Stage),每个阶段都有不同的步骤(Step)。从实践中可以知道,step是可以简化不写的,在最终log时候,每步操作命令都会变成step。形式如下

stage('stage-1') 
	sh 'echo "step 1.1"'
	sh 'echo "step 1.2"'

stage('stage-2')
  sh 'echo "step 2.1"'

比如我们的项目分为 checkout, maven build, docker build, docker push, deploy 5个阶段,我们一次写上5个stage即可。

2.2 自定义Slave

如果不自行声明,默认Jenkins创建的slave Pod 仅包含一个名为jnlp的容器,这个容器支持基本的echo, cp, mv, ls, svn, git的操作,可以用来检出代码。
注意: 这里必须包含一个名字叫jnlp的容器,如果用户自定义容器不叫jnlp,则Jenkins创建的Pod会包含两个容器,一个是用户自定义容器,一个是Jnlp。

但是,如果我们需要进行maven操作, docker操作,则基本的jnlp容器就不知吃了,这个时候,我们可以在Slave Pod中添加各个功能对应的基础容器接口。容器可以自己基于基础镜像编写Dockerfile来构建,也可以在Dockerhub上搜索可以直接使用的镜像。

容器Slave的自定义方式,使用podTemplate创建Slave Pod。 如果对Kubernetes熟悉,这里的声明方式和Pod的yaml声明结构几乎一致。参考: kubernetes-plugin
比如我这里需要svn检出、maven构建、docker构建操作,我们就引入maven和docker基础镜像即可。总体pipeline结构变为

podTemplate()
  stage()
  
  stage()

然后根据我们的实际情况填写参数:

def label = "pipeline-worker-$PROJECT"
podTemplate(label: label, cloud: 'my-kubernetes-cloud', 
        containers: [
        containerTemplate(name: 'jnlp',
        ttyEnabled: true,
        image: 'jenkins/jnlp-slave:alpine', imagePullPolicy: 'Always'),
        containerTemplate(name: 'jnlp-mvn',
        ttyEnabled: true,
        command: 'cat',
        image: 'jenkins/jnlp-slave-maven:v1.5', imagePullPolicy: 'Always'),
        containerTemplate(name: 'jnlp-docker',
        ttyEnabled: true,
        command: 'cat',
        image: 'jenkins/jnlp-slave-docker:v1.6', imagePullPolicy: 'Always')
    ],
    volumes: [
        hostPathVolume(hostPath: '/var/run/docker.sock', mountPath:'/var/run/docker.sock'),
        hostPathVolume(hostPath: '/etc/docker/daemon.json', mountPath:'/etc/docker/daemon.json')
    ]
    ) 
  stage()
  stage()

这里各个容器的image我们隐藏了私服的具体细节,用户根据实际需要选择即可。另外podTemplate的参数中cloud就是我们在初始化Kubernetes时候填写的kubernetes cloud名称。
由于我们用了docker,采用docker in docker方式运行(即在容器中运行docker命令)执行我们构建步骤中的docker build和docker push,因此我们将容器中的docker和宿主机的docker的sock以及daemon文件进行连接,通过hostPathVolume的方式挂载在容器中即可

2.3 代码生成器

对于svn检出、docker login私服的操作,如果我们直接把密码写在pipeline中很不安全,我们可以借助Jenkins的Pipeline Syntax帮助我们生成安全的检出代码和docker操作代码,这样我们的用户名口令就会保存在Jenkins的Credentials中。

1) 检出SVN的代码

  1. 根据提示操作并单机Pipeline Syntax,选择checkout: Check out from version control
  2. 填写svn地址,添加Credentials,并选择好Credentials
  3. 点击Generate Pipeline Script
  4. 将生成的代码拷贝回pipeline对应的stage中即可
checkout([$class: 'SubversionSCM', additionalCredentials: [], excludedCommitMessages: '', excludedRegions: '', excludedRevprop: '', excludedUsers: '', filterChangelog: false, ignoreDirPropChanges: false, includedRegions: '', locations: [[cancelProcessOnExternalsFail: true, credentialsId: '588162ba-56c0-4c9d-8cf6-e7c12d437a44', depthOption: 'infinity', ignoreExternalsOption: true, local: '.', remote: 'svn://xx.xxx.xx.xx/your/svn/repo/path']], quietOperation: true, workspaceUpdater: [$class: 'UpdateUpdater']])

2) 用Credentials隐藏pipeline中的用户名和口令

比如docker登录私服的时候我们需要docker login -U xxx -P xxx,这时候我们就可以通过Cerdentials隐藏用户名密码

  1. Pipeline Syntax 中选择 withCredentials: Bind credentials to variables
  2. 选择Add, Username and password(separated)
  3. 填入用户名和密码
  4. 点击Generate Pipeline Script
  5. 将需要用户名口令的操作写在生成的代码中,并使用变量 $USERNAME$PASSWORD代替原来的明文即可,如下演示了如何进行 docker login操作。
withCredentials([usernamePassword(credentialsId: 'd23f551a-1e55-4bea-a9fb-5098a9ac291d', passwordVariable: 'PASSWORD', usernameVariable: 'USERNAME')]) 
                            sh "docker login $DOCKER_URL -u $USERNAME -p $PASSWORD"
                        

2.4 不同阶段切换不同的功能容器

上文中我们引入了 jnlp, jnlp-mvn, jnlp-docker三个容器共同组成了Slave Pod, 现在就可以在不同的操作中使用不同的容器进行构建了。用container()包住stage内部的代码即可

stage('svn checkout') 
  container('jnlp') 
    // do svn check out
  

stage('maven build') 
  container('jnlp-mvn') 
    // do maven build stuff
  

stage('docker build') 
  container('jnlp-docker') 
    // do docker build, docker push stuff
  

2.5 pipeline外部变量

在默认pipeline中,点击“build”,pipeline就会开始工作,但是缺少必要的外部交互,比如在build前需要用户进行一些参数输入的话,就需要外部参数的支持。
在pipeline的设置页面中勾选"参数化构建过程",就可以添加外部变量了,可以选择:文本、选项、布尔值等等不同形式的变量类型。对应的在脚本中使用"$"引用即可。
此时 “build"按钮会变为"Build With Parameters”。点击后会先让用户设置参数,然后才开始构建。

效果

构建结构会分阶段展示出来

以上是关于Jenkins on Kubernetes中的Pipeline语法以及自定义Slave的使用方式的主要内容,如果未能解决你的问题,请参考以下文章

从 kubernetes 插件 / jenkins 中的 gcr 拉取 docker 图像的问题

配置 Jenkins 连接 Kubernetes 集群

kubernetes 基于jenkins spinnaker的ci/cd实践一增加制品镜像扫描

kubernetes 基于jenkins spinnaker的ci/cd实践一增加制品镜像扫描

Kubernetes和Jenkins——基于Kubernetes构建Jenkins持续集成平台

kubernetes 基于jenkins spinnaker的ci/cd实践一增加制品镜像扫描