CI/CD介绍

Continuous Integration (CI) / Continuous Delivery (CD)

软件交付流程
image.png
一个软件从零开始到最终交付,大概包括以下几个阶段:规划、编码、构建、测试、发布、部署和维护,基于这些阶段,我们的软件交付模型大致经历了几个阶段:

瀑布式流程

image.png
前期需求确立之后,软件开发人员花费数周和数月编写代码,把所有需求一次性开发完,然后将代码交给QA(质量保障)团队进行测试,然后将最终的发布版交给运维团队去部署。瀑布模型,简单来说,就是等一个阶段所有工作完成之后,再进入下一个阶段。这种模式的问题也很明显,产品迭代周期长,灵活性差。一个周期动辄几周几个月,适应不了当下产品需要快速迭代的场景。

敏捷开发

image.png
任务由大拆小,开发、测试协同工作,注重开发敏捷,不重视交付敏捷。

DevOps

image.png
开发、测试、运维协同工作, 持续开发+持续交付。

为什么最初的开发模式没有直接进入DevOps的时代?
原因是:沟通成本。

各角色人员去沟通协作的时候都是手动去做,交流靠嘴,靠人去指挥,很显然会出大问题。所以说不能认为DevOps就是一种交付模式,因为解决不了沟通协作成本,这种模式就不具备可落地性。

那DevOps时代如何解决角色之间的成本问题?DevOps的核心就是自动化。自动化的能力靠什么来支撑,工具和技术。

DevOps工具链
image.png
靠这些工具和技术,才实现了自动化流程,进而解决了协作成本,使得devops具备了可落地性。因此我们可以大致给devops一个定义:

devops** = 提倡开发、测试、运维协同工作来实现持续开发、持续交付的一种软件交付模式 + 基于工具和技术支撑的自动化流程的落地实践。

因此devops不是某一个具体的技术,而是一种思想+自动化能力,来使得构建、测试、发布软件能够更加地便捷、频繁和可靠的落地实践。

利用工具和技术来实现完整的DevOps平台的建设。我们主要使用的工具有:

  1. gitlab,代码仓库,企业内部使用最多的代码版本管理工具。
  2. Jenkins, 一个可扩展的持续集成引擎,用于自动化各种任务,包括构建、测试和部署软件。
  3. robotFramework, 基于Python的自动化测试框架
  4. sonarqube,代码质量管理平台
  5. maven,java包构建管理工具
  6. Kubernetes
  7. Docker

    Jenkins初体验

    k8s中部署jenkins

    注意:

  8. 首次启动很慢;

  9. 因涉及到与Kubernetes的集成,需调用到k8s集群的api,故安装时通过sa进行权限赋予;
  10. 将其部署到标签节点 jenkins=true ;
  11. ingress外部访问;
  12. 数据存储通过hostpath挂载。

jenkins/jenkins-all.yaml

  1. apiVersion: v1
  2. kind: Namespace
  3. metadata:
  4. name: jenkins
  5. ---
  6. apiVersion: v1
  7. kind: ServiceAccount
  8. metadata:
  9. name: jenkins
  10. namespace: jenkins
  11. ---
  12. apiVersion: rbac.authorization.k8s.io/v1beta1
  13. kind: ClusterRoleBinding
  14. metadata:
  15. name: jenkins-crb
  16. roleRef:
  17. apiGroup: rbac.authorization.k8s.io
  18. kind: ClusterRole
  19. name: cluster-admin
  20. subjects:
  21. - kind: ServiceAccount
  22. name: jenkins
  23. namespace: jenkins
  24. ---
  25. apiVersion: apps/v1
  26. kind: Deployment
  27. metadata:
  28. name: jenkins-master
  29. namespace: jenkins
  30. spec:
  31. replicas: 1
  32. selector:
  33. matchLabels:
  34. devops: jenkins-master
  35. template:
  36. metadata:
  37. labels:
  38. devops: jenkins-master
  39. spec:
  40. nodeSelector:
  41. jenkins: "true"
  42. serviceAccount: jenkins #Pod 需要使用的服务账号
  43. initContainers:
  44. - name: fix-permissions
  45. image: busybox
  46. command: ["sh", "-c", "chown -R 1000:1000 /var/jenkins_home"]
  47. securityContext:
  48. privileged: true
  49. volumeMounts:
  50. - name: jenkinshome
  51. mountPath: /var/jenkins_home
  52. containers:
  53. - name: jenkins
  54. image: jenkinsci/blueocean:1.23.2
  55. imagePullPolicy: IfNotPresent
  56. ports:
  57. - name: http #Jenkins Master Web 服务端口
  58. containerPort: 8080
  59. - name: slavelistener #Jenkins Master 供未来 Slave 连接的端口
  60. containerPort: 50000
  61. volumeMounts:
  62. - name: jenkinshome
  63. mountPath: /var/jenkins_home
  64. env:
  65. - name: JAVA_OPTS
  66. value: "-Xms4096m -Xmx5120m -Duser.timezone=Asia/Shanghai -Dhudson.model.DirectoryBrowserSupport.CSP="
  67. volumes:
  68. - name: jenkinshome
  69. hostPath:
  70. path: /var/jenkins_home/
  71. ---
  72. apiVersion: v1
  73. kind: Service
  74. metadata:
  75. name: jenkins
  76. namespace: jenkins
  77. spec:
  78. ports:
  79. - name: http
  80. port: 8080
  81. targetPort: 8080
  82. - name: slavelistener
  83. port: 50000
  84. targetPort: 50000
  85. type: ClusterIP
  86. selector:
  87. devops: jenkins-master
  88. ---
  89. apiVersion: extensions/v1beta1
  90. kind: Ingress
  91. metadata:
  92. name: jenkins-web
  93. namespace: jenkins
  94. spec:
  95. rules:
  96. - host: jenkins.crab.com
  97. http:
  98. paths:
  99. - backend:
  100. serviceName: jenkins
  101. servicePort: 8080
  102. path: /

#创建服务
## 为node2打标签,将jenkins-master部署在node2节点
$ kubectl label node node2 jenkins=true

部署服务
$ kubectl create -f jenkins-all.yaml

查看服务
$ kubectl -n jenkins get po

查看日志,第一次启动提示需要完成初始化设置
$ kubectl -n jenkins logs -f jenkins-master-767df9b574-lgdr5
......
*************************************************************
Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:
5396b4e1c395450f8360efd8ee641b18
This may also be found at: /var/jenkins_home/secrets/initialAdminPassword
*************************************************************

#访问服务

  1. 配置解析后,访问jenkins.ops.com,能正常显示页面后,进入jenkins目录,替换插件地址,如下操作:

    1. $ cd /var/jenkins_home/updates
    2. $ sed -i 's/http:\/\/updates.jenkins-ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' default.json
    3. $ sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' default.json
  2. 删除jenkins pod,使其加载新配置重建,通过管理员密码进入jenkins。

  3. 选择右上角admin->configure->password重新设置管理员密码,设置完后,会退出要求重新登录,使用admin/xxxxxx(新密码),登录即可。
  4. Jenkins -> manage Jenkins -> Plugin Manager -> Avaliable,搜索 chinese关键字,安装汉化插件(Localization:Chinese)。
  5. 重启jenkins,正常进行安装插件即可。

    jenkins基础使用

    演示目标

Jenkins使用涉及到代码仓库gitlab,而代码仓库又依赖postgres和redis。

部署postgres

1、准备secret文件
$ cat gitlab-secret.txt
postgres.user.root=root
postgres.pwd.root=123456
$ kubectl -n jenkins create secret generic gitlab-secret —from-env-file=gitlab-secret.txt

2、部署postgres
注意点:
- 使用secret来引用账户密码
- 使用postgres=true来指定节点
**
$ cat postgres.yaml

  1. apiVersion: v1
  2. kind: Service
  3. metadata:
  4. name: postgres
  5. labels:
  6. app: postgres
  7. namespace: jenkins
  8. spec:
  9. ports:
  10. - name: server
  11. port: 5432
  12. targetPort: 5432
  13. protocol: TCP
  14. selector:
  15. app: postgres
  16. ---
  17. apiVersion: apps/v1
  18. kind: Deployment
  19. metadata:
  20. namespace: jenkins
  21. name: postgres
  22. labels:
  23. app: postgres
  24. spec:
  25. replicas: 1
  26. selector:
  27. matchLabels:
  28. app: postgres
  29. template:
  30. metadata:
  31. labels:
  32. app: postgres
  33. spec:
  34. nodeSelector:
  35. postgres: "true"
  36. tolerations:
  37. - operator: "Exists"
  38. containers:
  39. - name: postgres
  40. image: postgres:11.4
  41. imagePullPolicy: "IfNotPresent"
  42. ports:
  43. - containerPort: 5432
  44. env:
  45. - name: POSTGRES_USER #PostgreSQL 用户名
  46. valueFrom:
  47. secretKeyRef:
  48. name: gitlab-secret
  49. key: postgres.user.root
  50. - name: POSTGRES_PASSWORD #PostgreSQL 密码
  51. valueFrom:
  52. secretKeyRef:
  53. name: gitlab-secret
  54. key: postgres.pwd.root
  55. resources:
  56. limits:
  57. cpu: 1000m
  58. memory: 2048Mi
  59. requests:
  60. cpu: 50m
  61. memory: 100Mi
  62. volumeMounts:
  63. - mountPath: /var/lib/postgresql/data
  64. name: postgredb
  65. volumes:
  66. - name: postgredb
  67. hostPath:
  68. path: /var/lib/postgres/

部署到node2节点
$ kubectl label node node2 postgres=true

创建postgres
$ kubectl create -f postgres.yaml

创建数据库gitlab,为后面部署gitlab组件使用
$ kubectl -n jenkins exec -ti postgres-7ff9b49f4c-nt8zh bash
root@postgres-7ff9b49f4c-nt8zh:/# psql
root=# create database gitlab;

部署redis

$ cat redis.yaml

  1. apiVersion: v1
  2. kind: Service
  3. metadata:
  4. name: redis
  5. labels:
  6. app: redis
  7. namespace: jenkins
  8. spec:
  9. ports:
  10. - name: server
  11. port: 6379
  12. targetPort: 6379
  13. protocol: TCP
  14. selector:
  15. app: redis
  16. ---
  17. apiVersion: apps/v1
  18. kind: Deployment
  19. metadata:
  20. namespace: jenkins
  21. name: redis
  22. labels:
  23. app: redis
  24. spec:
  25. replicas: 1
  26. selector:
  27. matchLabels:
  28. app: redis
  29. template:
  30. metadata:
  31. labels:
  32. app: redis
  33. spec:
  34. tolerations:
  35. - operator: "Exists"
  36. containers:
  37. - name: redis
  38. image: sameersbn/redis:4.0.9-2
  39. imagePullPolicy: "IfNotPresent"
  40. ports:
  41. - containerPort: 6379
  42. resources:
  43. limits:
  44. cpu: 1000m
  45. memory: 2048Mi
  46. requests:
  47. cpu: 50m
  48. memory: 100Mi

创建
$ kubectl create -f redis.yaml

部署gitlab

注意点:
- 使用ingress暴漏服务
- 添加annotation,指定nginx端上传大小限制,否则推送代码时会默认被限制1m大小,相当于给nginx设置client_max_body_size的限制大小
- 使用gitlab=true来选择节点
- 使用服务发现地址来访问postgres和redis
- 在secret中引用数据库账户和密码
- 数据库名称为gitlab

$ cat gitlab.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: gitlab
  namespace: jenkins
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
  rules:
  - host: gitlab.crab.com
    http:
      paths:
      - backend:
          serviceName: gitlab
          servicePort: 80
        path: /
---
apiVersion: v1
kind: Service
metadata:
  name: gitlab
  labels:
    app: gitlab
  namespace: jenkins
spec:
  ports:
  - name: server
    port: 80
    targetPort: 80
    protocol: TCP
  selector:
    app: gitlab
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: jenkins
  name: gitlab
  labels:
    app: gitlab
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gitlab
  template:
    metadata:
      labels:
        app: gitlab
    spec:
      nodeSelector:
        gitlab: "true"
      tolerations:
      - operator: "Exists"
      containers:
      - name: gitlab
        image:  sameersbn/gitlab:13.2.2
        imagePullPolicy: "IfNotPresent"
        env:
        - name: GITLAB_HOST
          value: "gitlab.crab.com"
        - name: GITLAB_PORT
          value: "80"
        - name: GITLAB_SECRETS_DB_KEY_BASE
          value: "long-and-random-alpha-numeric-string"
        - name: GITLAB_SECRETS_DB_KEY_BASE
          value: "long-and-random-alpha-numeric-string"
        - name: GITLAB_SECRETS_SECRET_KEY_BASE
          value: "long-and-random-alpha-numeric-string"
        - name: GITLAB_SECRETS_OTP_KEY_BASE
          value: "long-and-random-alpha-numeric-string"
        - name: DB_HOST
          value: "postgres"
        - name: DB_NAME
          value: "gitlab"
        - name: DB_USER
          valueFrom:
            secretKeyRef:
              name: gitlab-secret
              key: postgres.user.root
        - name: DB_PASS
          valueFrom:
            secretKeyRef:
              name: gitlab-secret
              key: postgres.pwd.root
        - name: REDIS_HOST
          value: "redis"
        - name: REDIS_PORT
          value: "6379"
        ports:
        - containerPort: 80
        resources:
          limits:
            cpu: 2000m
            memory: 5048Mi
          requests:
            cpu: 100m
            memory: 500Mi
        volumeMounts:
        - mountPath: /home/git/data
          name: data
      volumes:
      - name: data
        hostPath:
          path: /var/lib/gitlab/

部署到k8s-slave2节点
$ kubectl label node node2 gitlab=true

创建
$ kubectl create -f gitlab.yaml

项目代码

访问 http://gitlab.crab.com/ ,设置管理员密码,笔者配置为root/88888888

拉取远程A仓库代码到本地并推送到远程B仓库
#先在gitlab上创建project(名称为myblog)
**image.png
git clone https://gitee.com/agagin/python-demo.git
cd python-demo/
git remote rename origin old-origin
git remote add origin http://gitlab.crab.com/root/myblog.git
git push -u origin —all
git push -u origin —tags

报错说明:
在push过程中笔者出现以下报错:

error: RPC failed; result=22, HTTP code = 413
fatal: The remote end hung up unexpectedly
Writing objects: 100% (284/284), 1.51 MiB | 0 bytes/s, done.
Total 284 (delta 89), reused 284 (delta 89)
fatal: The remote end hung up unexpectedly
Everything up-to-date

报错解决:
经过排查分析,因笔者环境对于域名gitlab.crab.com走了nginx的解析,默认没配置上传文件大小限制,nginx加上如下配置后,可正常push。
client_max_body_size 100m;

配置钉钉

  • 配置机器人
  1. 新建钉钉群;
  2. 点击头像选择 > 机器人管理 > 新建机器人(通过Webhook接入自定义服务)并加入群;
  3. 提取机器人 Webhook 信息

https://oapi.dingtalk.com/robot/send?access_token=bbfee20ac6e7735689e1b7a043117cd1937ea689d694dfa90a20122ebc3f9c35

  1. 验证发送消息

    执行以下命令(保证access_token一致),机器人在群里发送消息。

    curl 'https://oapi.dingtalk.com/robot/send?access_token=bbfee20ac6e7735689e1b7a043117cd1937ea689d694dfa90a20122ebc3f9c35'    -H 'Content-Type: application/json'    -d '{"msgtype": "text", 
         "text": {
              "content": "我是卡子火啊!"
         }
       }'
    

    演示过程

    流程示意图
    jenkins-gitlab.png

1、jenkins中安装名为 ‘gitlab’ 的插件;
2、jenkins中配置Gitlab信息;

  • 登录gitlab,选择user->Settings->access tokens新建一个访问token,提取到token信息为 zDygCPzWxhRwwzaGVCXP

image.png

  • 登陆jenkins,点击 系统管理 > Manage Credentials ,继续点击后进行添加凭据

image.png

  • 在jenkins中点击 全局配置,配置gitlab信息

image.png
3、创建自由风格项目
- gitlab connection 选择为刚创建的gitlab
- 源码管理选择Git,填项项目地址
- 新建一个 Credentials 认证,使用用户名密码方式,配置gitlab的用户和密码
image.png
- 构建触发器选择 Build when a change is pushed to GitLab
image.png
- 生成一个Secret token
image.png
- 添加构建步骤,执行shell,调用钉钉api
image.png

  • 点击保存,需记录信息如下:
    URL:http://jenkins.crab.com/project/demo1
    Token:85bfe87b213a5d8ea59b673ac056f40a
    4、设置gitlab允许向本地网络发送webhook请求
    由于安全设置,后续test push events,会出现报错:Requests to the local network are not allowed,故提前配置解决。
    依次点击 Admin Aera -> Settings -> Network -> Outbound requests
    Collapse,勾选第一项即可。
    5、到gitlab配置webhook
    - 进入项目下settings->Integrations
    image.png
    - Add webhook
    6、提交代码到gitlab仓库,触发jenkins构建对应项目,然后执行shell,调用api发送钉钉消息。

    Master-Slaves(agent)模式

    上面演示的任务,默认都是在master节点执行的,多个任务都在master节点执行,对master节点的性能会造成一定影响,如何将任务分散到不同的节点,做成多slave的方式?

1. 添加slave节点(此处将jenkins-slave固定跑到node3节点上)
- 依次点击 系统管理 -> 节点管理 -> 新建节点
- 启动方式选择通过java web启动代理,代理是运行jar包,通过JNLP(是一种允许客户端启动托管在远程Web服务器上的应用程序的协议 )启动连接到master节点服务中image.png
2. 执行java命令启动agent服务

  • k8s-slave3配置保存后再点击查看配置

image.png

  • node3节点上操作

$ wget ~~[http://jenkins.crab.com/jnlpJars/agent.jar~~](http://jenkins.luffy.com/jnlpJars/agent.jar)
$ java -jar ~~[agent.jar](http://jenkins.crab.com/jnlpJars/agent.jar) -jnlpUrl [http://jenkins.crab.com/computer/k8s-slave3/slave-agent.jnlp~~](http://jenkins.crab.com/computer/k8s-slave3/slave-agent.jnlp)~~ -secret 52eaca00b6621fb217b74e48bf1217a3bae383276583003277de3d8e69a4757b -workDir “/srv/jenkins_jobs”~~

#报错说明
运行上述命令后会出现如下报错:

SEVERE: http://jenkins.crab.com/ provided port:50000 is not reachable
java.io.IOException: http://jenkins.crab.com/ provided port:50000 is not reachable

#报错解决
因域名 jenkins.crab.com 在ingress解析时只是配置代理了8080端口,故无法解析到50000端口,将域名换成 ClusterIP:8080 ,agent.jar启动时即可自动调用到ClusterIP:50000
]# kubectl get svc -n jenkins|grep jenkins

jenkins    ClusterIP   10.111.192.40    <none>        8080/TCP,50000/TCP   108m

节点设置中高级指定 Tunnel连接位置
image.png
$ wget http://10.111.192.40:8080/jnlpJars/agent.jar
$ java -jar agent.jar -jnlpUrl http://10.111.192.40:8080/computer/k8s-slave3/slave-agent.jnlp -secret 52eaca00b6621fb217b74e48bf1217a3bae383276583003277de3d8e69a4757b -workDir “/srv/jenkins_jobs” &

3. 查看Jenkins节点列表,新节点已经处于可用状态
agent.jar启动前状态
image.png
agent.jar启动后状态
image.png

4. 测试使用新节点执行任务
- 配置demo1项目
- 限制项目的运行节点 ,标签表达式选择node3(因为前面新增节点k8s-slave3配置的标签是node3,同时请确保node3节点已安装git命令)
image.png
- 立即构建
- 查看构建日志

结果:通过构建日志,可看到demo1项目的构建在node3节点上执行。

定制化容器镜像

每次部署jenkins新环境,都需要安装很多必要的插件,因此可以考虑把插件提前做到镜像中。

Dockerfile

FROM jenkinsci/blueocean:1.23.2
LABEL maintainer="inspur_lyx@hotmail.com"

## 用最新的插件列表文件替换默认插件文件
COPY plugins.txt /usr/share/jenkins/ref/

## 执行插件安装
RUN /usr/local/bin/install-plugins.sh < /usr/share/jenkins/ref/plugins.txt

plugin.txt是插件列表文件
]# tail -3 plugins.txt
workflow-durable-task-step:2.36
workflow-cps:2.83
workflow-job:2.40

plugin.txt文件获取
#基于现有jenkins环境(已安装好所需插件),根据实际环境替换用户名&密码&jenkins地址

curl -sSL  "http://admin:123456@localhost:8080/pluginManager/api/xml?depth=1&xpath=/*/*/shortName|/*/*/version&wrapper=plugins" | perl -pe 's/.*?<shortName>([\w-]+).*?<version>([^<]+)()(<\/\w+>)+/\1:\2\n/g'|sed 's/ /:/' > plugins.txt

执行构建,定制jenkins容器
$ docker build . -t harbor.od.com/jenkins:v20200414 -f Dockerfile
$ docker push harbor.od.com/jenkins:v20200414

流水线入门

image.png
官方文档:https://www.jenkins.io/zh/doc/book/pipeline/getting-started/

为什么叫做流水线,和工厂产品的生产线类似,pipeline是从源码到发布到线上环境。关于流水线,需要知道的几个点:
- 重要的功能插件,帮助Jenkins定义了一套工作流框架;
- Pipeline 的实现方式是一套 Groovy DSL( 领域专用语言 ),所有的发 布流程都可以表述为一段 Groovy 脚本;
- 将WebUI上需要定义的任务,以脚本代码的方式表述出来;
- 帮助jenkins实现持续集成CI(Continue Integration)和持续部署CD(Continue Deliver)的重要手段。

流水线基础语法

官方文档:https://www.jenkins.io/zh/doc/book/pipeline/syntax/

脚本示例

pipeline { 
    agent {label '192.168.136.12'}
    environment { 
        PROJECT = 'myblog'
    }
    stages {
        stage('Checkout') { 
            steps { 
                checkout scm 
            }
        }
        stage('Build') { 
            steps { 
                sh 'make' 
            }
        }
        stage('Test'){
            steps {
                sh 'make check'
                junit 'reports/**/*.xml' 
            }
        }
        stage('Deploy') {
            steps {
                sh 'make publish'
            }
        }
    }
    post {
        success { 
            echo 'Congratulations!'
        }
        failure { 
            echo 'Oh no!'
        }
        always { 
            echo 'I will always say Hello again!'
        }
    }
}

脚本解释

  • checkout步骤为检出代码; scm是一个特殊变量,指示checkout步骤克隆触发此Pipeline运行的特定修订 ;
  • agent:指明使用哪个agent节点来执行任务,定义于pipeline顶层或者stage内部;

        - any,可以使用任意可用的agent来执行<br />          - label,在提供了标签的 Jenkins 环境中可用的代理上执行流水线或阶段。 例如: `agent { label 'my-defined-label' }`,最常见的使用方式<br />          - none,当在 `pipeline` 块的顶部没有全局代理, 该参数将会被分配到整个流水线的运行中并且每个 `stage` 部分都需要包含他自己的 `agent` 部分。比如: `agent none`<br />          - docker, 使用给定的容器执行流水线或阶段。 在指定的节点中,通过运行容器来执行任务
    
    agent {
          docker {
              image 'maven:3-alpine'
              label 'my-defined-label'
              args  '-v /tmp:/tmp'
          }
      }
    
  • options: 允许从流水线内部配置特定于流水线的选项。

  • buildDiscarder , 为最近的流水线运行的特定数量保存组件和控制台输出。例如: options { buildDiscarder(logRotator(numToKeepStr: '10')) }
    - disableConcurrentBuilds ,不允许同时执行流水线。 可被用来防止同时访问共享资源等。 例如: options { disableConcurrentBuilds() }
    - timeout ,设置流水线运行的超时时间, 在此之后,Jenkins将中止流水线。例如: options { timeout(time: 1, unit: 'HOURS') }
    - retry,在失败时, 重新尝试整个流水线的指定次数。 For example: options { retry(3) }
    - environment: 指令制定一个 键-值对序列,该序列将被定义为所有步骤的环境变量
    - stages: 包含一系列一个或多个指令, stages 部分是流水线描述的大部分”work” 的位置。 建议 stages 至少包含一个指令用于连续交付过程的每个离散部分,比如构建,测试, 和部署。

    pipeline {
       agent any
       stages {  
           stage('Example') {
               steps {
                   echo 'Hello World'
               }
           }
       }
    }
    
  • steps: 在给定的 stage 指令中执行的定义了一系列的一个或多个[steps]。

  • post: 定义一个或多个steps ,这些阶段根据流水线或阶段的完成情况而运行post 支持以下 [post-condition]块中的其中之一: always, changed, failure, success, unstable, 和 aborted

  • always, 无论流水线或阶段的完成状态如何,都允许在 post 部分运行该步骤
    - changed, 当前流水线或阶段的完成状态与它之前的运行不同时,才允许在 post 部分运行该步骤
    - failure, 当前流水线或阶段的完成状态为”failure”,才允许在 post 部分运行该步骤, 通常web UI是红色
    - success, 当前流水线或阶段的完成状态为”success”,才允许在 post 部分运行该步骤, 通常web UI是蓝色或绿色
    - unstable, 当前流水线或阶段的完成状态为”unstable”,才允许在 post 部分运行该步骤, 通常由于测试失败,代码违规等造成。通常web UI是黄色
    - aborted, 只有当前流水线或阶段的完成状态为”aborted”,才允许在 post 部分运行该步骤, 通常由于流水线被手动的aborted。通常web UI是灰色

    创建pipeline示意

    新建任务 -> 流水线

jenkins/pipelines/p1.yaml

pipeline {
   agent {label 'node3'}
   environment {  
      PROJECT = 'myblog'
   }
   stages {
      stage('printenv') {
         steps {
            echo 'Hello World'
            sh 'printenv'
         }
      }
      stage('check') {
         steps {
            checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'gitlab-user', url: 'http://gitlab.crab.com/root/myblog.git']]])
         }
      }
      stage('build-image') {
         steps {
            sh 'docker build . -t myblog:latest -f Dockerfile'
         }
      }
      stage('send-msg') {
         steps {
            sh """
            curl 'https://oapi.dingtalk.com/robot/send?access_token=bbfee20ac6e7735689e1b7a043117cd1937ea689d694dfa90a20122ebc3f9c35' \
   -H 'Content-Type: application/json' \
   -d '{"msgtype": "text",  
        "text": {
             "content": "我shi卡子火啊"
        }
      }'
      """
         }
      }
   }
}

点击“立即构建”

Jenkinsfile

使用Jenkinsfile管理pipeline

  • 将上节 jenkins/pipelines/p1.yaml文件内容写成到文件中,命名为Jenkinsfile,提交到gitlab的myblog项目根目录。
  • 配置pipeline任务,流水线定义为Pipeline Script from SCM。

image.png

优化流水线内容

  • 优化代码检出阶段,由于目前已经配置了使用git仓库地址,且使用SCM来检测项目,因此代码检出阶段完全没有必要再去指定一次
    - 构建镜像的tag使用git的commit id
    - 增加post阶段的消息通知,丰富通知内容
    - 配置webhook,实现myblog代码推送后,触发Jenkinsfile任务执行

jenkins/pipelines/p3.yaml

pipeline {
    agent { label 'node3'}

    stages {
        stage('printenv') {
            steps {
            echo 'Hello World'
            sh 'printenv'
            }
        }
        stage('check') {
            steps {
                checkout scm
            }
        }
        stage('build-image') {
            steps {
                retry(2) { sh 'docker build . -t myblog:${GIT_COMMIT}'}
            }
        }
    }
    post {
        success { 
            echo 'Congratulations!'
            sh """
                curl 'https://oapi.dingtalk.com/robot/send?access_token=bbfee20ac6e7735689e1b7a043117cd1937ea689d694dfa90a20122ebc3f9c35' \
                    -H 'Content-Type: application/json' \
                    -d '{"msgtype": "text", 
                            "text": {
                                "content": "😄👍构建成功👍😄\n 关键字:luffy\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"
                        }
                }'
            """
        }
        failure {
            echo 'Oh no!'
            sh """
                curl 'https://oapi.dingtalk.com/robot/send?access_token=bbfee20ac6e7735689e1b7a043117cd1937ea689d694dfa90a20122ebc3f9c35' \
                    -H 'Content-Type: application/json' \
                    -d '{"msgtype": "text", 
                            "text": {
                                "content": "😖❌构建失败❌😖\n 关键字:luffy\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"
                        }
                }'
            """
        }
        always { 
            echo 'I will always say Hello again!'
        }
    }
}

使用k8s部署服务

  • 新建deploy目录,将k8s部署所需的文件(deploy.yaml)放到deploy目录中
    - 将deploy.yaml文件中镜像地址改成模板变量,在pipeline中使用新构建的镜像进行替换
    - 执行kubectl apply -f deploy应用更改,需要配置kubectl认证
    $ scp -r k8s-master:/root/.kube /root

jenkins/pipelines/p4.yaml

pipeline {
    agent { label 'node3'}

    environment {
        IMAGE_REPO = "harbor.od.com/myblog"
    }

    stages {
        stage('printenv') {
            steps {
              echo 'Hello World'
              sh 'printenv'
            }
        }
        stage('check') {
            steps {
                checkout scm
            }
        }
        stage('build-image') {
            steps {
                retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}
            }
        }
        stage('push-image') {
            steps {
                retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}
            }
        }
        stage('deploy') {
            steps {
                sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' deploy/*"
                timeout(time: 1, unit: 'MINUTES') {
                    sh "kubectl apply -f deploy/"
                }
            }
        }
    }
    post {
        success { 
            echo 'Congratulations!'
            sh """
                curl 'https://oapi.dingtalk.com/robot/send?access_token=bbfee20ac6e7735689e1b7a043117cd1937ea689d694dfa90a20122ebc3f9c35' \
                    -H 'Content-Type: application/json' \
                    -d '{"msgtype": "text", 
                            "text": {
                                "content": "😄👍构建成功👍😄\n 关键字:myblog\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"
                        }
                }'
            """
        }
        failure {
            echo 'Oh no!'
            sh """
                curl 'https://oapi.dingtalk.com/robot/send?access_token=bbfee20ac6e7735689e1b7a043117cd1937ea689d694dfa90a20122ebc3f9c35' \
                    -H 'Content-Type: application/json' \
                    -d '{"msgtype": "text", 
                            "text": {
                                "content": "😖❌构建失败❌😖\n 关键字:crab\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"
                        }
                }'
            """
        }
        always { 
            echo 'I will always say Hello again!'
        }
    }
}

使用凭据管理敏感信息

上述钉钉发消息涉及到token信息,为避免暴露,可将其以 用户名/密码 的方式配置在jenkins的凭据里,通过变量进行调用。

以下示例中,带密码的用户名凭据被分配了环境变量,凭据已在 Jenkins 中配置了凭据 ID jenkins-bitbucket-common-creds

当在 [environment]指令中设置凭据环境变量时:

environment {
    BITBUCKET_COMMON_CREDS = credentials('jenkins-bitbucket-common-creds')
}

这实际设置了下面的三个环境变量:

  • BITBUCKET_COMMON_CREDS - 包含一个以冒号分隔的用户名和密码,格式为 username:password
  • BITBUCKET_COMMON_CREDS_USR - 附加的一个仅包含用户名部分的变量。
  • BITBUCKET_COMMON_CREDS_PSW - 附加的一个仅包含密码部分的变量。
    pipeline {
     agent {
         // 此处定义 agent 的细节
     }
     environment {
         //顶层流水线块中使用的 environment 指令将适用于流水线中的所有步骤。 
         BITBUCKET_COMMON_CREDS = credentials('jenkins-bitbucket-common-creds')
     }
     stages {
         stage('Example stage 1') {
              //在一个 stage 中定义的 environment 指令只会将给定的环境变量应用于 stage 中的步骤。
             environment {
                 BITBUCKET_COMMON_CREDS = credentials('another-credential-id')
             }
             steps {
                 // 
             }
         }
         stage('Example stage 2') {
             steps {
                 // 
             }
         }
     }
    }
    

对原有Jenkinsfile做改造:
jenkins/pipelines/p5.yaml

pipeline {
    agent { label 'node3'}

    environment {
        IMAGE_REPO = "harbor.od.com/myblog"
        DINGTALK_CREDS = credentials('dingTalk')
    }

    stages {
        stage('printenv') {
            steps {
            echo 'Hello World'
            sh 'printenv'
            }
        }
        stage('check') {
            steps {
                checkout scm
            }
        }
        stage('build-image') {
            steps {
                retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}
            }
        }
        stage('push-image') {
            steps {
                retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}
            }
        }
        stage('deploy') {
            steps {
                sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' deploy/*"
                timeout(time: 1, unit: 'MINUTES') {
                    sh "kubectl apply -f deploy/"
                }
            }
        }
    }
    post {
        success { 
            echo 'Congratulations!'
            sh """
                curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
                    -H 'Content-Type: application/json' \
                    -d '{"msgtype": "text", 
                            "text": {
                                "content": "😄👍构建成功👍😄\n 关键字:luffy\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"
                        }
                }'
            """
        }
        failure {
            echo 'Oh no!'
            sh """
                curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
                    -H 'Content-Type: application/json' \
                    -d '{"msgtype": "text", 
                            "text": {
                                "content": "😖❌构建失败❌😖\n 关键字:luffy\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"
                        }
                }'
            """
        }
        always { 
            echo 'I will always say Hello again!'
        }
    }
}

多分支流水线

新建钉钉access_token凭据
image.png

新建多分支流水线项目
image.png
image.png

保存后,会自动检索项目中所有存在Jenkinsfile文件的分支和标签,若匹配我们设置的过滤正则表达式,则会添加到多分支的构建视图中。所有添加到视图中的分支和标签,会默认执行一次构建任务。因为配置了扫描间隔为1min,所以当git项目有代码变更或者新建分支时,会自动触发项目构建。

Jenkinsfile

pipeline {
    agent { label 'node3' }

    environment {
        IMAGE_REPO = "harbor.od.com/app/myblog"
        DINGTALK_CREDS = credentials('dingTalk')
        TAB_STR = "\n                    \n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"
    }

    stages {
        stage('printenv') {
            steps {
                script{
                    sh "git log --oneline -n 1 > gitlog.file"
                    env.GIT_LOG = readFile("gitlog.file").trim()
                }
                sh 'printenv'
            }
        }
        stage('checkout') {
            steps {
                checkout scm
                script{
                    env.BUILD_TASKS = env.STAGE_NAME + "√..." + env.TAB_STR
                }
            }
        }
        stage('build-image') {
            steps {
                retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}
                script{
                    env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
                }
            }
        }
        stage('push-image') {
            steps {
                retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}
                script{
                    env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
                }
            }
        }
        stage('deploy') {
            steps {
                sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' deploy/*"
                timeout(time: 1, unit: 'MINUTES') {
                    sh "kubectl apply -f deploy/"
                }
                script{
                    env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
                }
            }
        }
    }
    post {
        success { 
            echo 'Congratulations!'
            sh """
                curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
                    -H 'Content-Type: application/json' \
                    -d '{
                        "msgtype": "markdown",
                        "markdown": {
                            "title":"myblog",
                            "text": "😄👍 构建成功 👍😄  \n**项目名称**:crab  \n**Git log**: ${GIT_LOG}   \n**构建分支**: ${GIT_BRANCH}   \n**构建地址**:${RUN_DISPLAY_URL}  \n**构建任务**:${BUILD_TASKS}"
                        }
                    }'
            """ 
        }
        failure {
            echo 'Oh no!'
            sh """
                curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
                    -H 'Content-Type: application/json' \
                    -d '{
                        "msgtype": "markdown",
                        "markdown": {
                            "title":"myblog",
                            "text": "😖❌ 构建失败 ❌😖  \n**项目名称**:crab  \n**Git log**: ${GIT_LOG}   \n**构建分支**: ${GIT_BRANCH}  \n**构建地址**:${RUN_DISPLAY_URL}  \n**构建任务**:${BUILD_TASKS}"
                        }
                    }'
            """
        }
        always { 
            echo 'I will always say Hello again!'
        }
    }
}

新建develop分支,再次触发构建
$ git checkout -b develop
$ git push —set-upstream origin develop

jenkins项目自动生成develop分支构建项目
image.png

钉钉收到消息
image.png

工具集成与jenkinsfile实践

集成kubernets

插件官方文档:https://plugins.jenkins.io/kubernetes/

插件安装及配置

  1. [系统管理] -> [插件管理] -> [搜索kubernetes]->直接安装
    2. [系统管理] -> [系统配置] -> [Add a new cloud]
    3. 配置地址信息
    - Kubernetes 地址: https://kubernetes.default(或者vip地址:https://10.2.2.6:6443
    - Kubernetes 命名空间:jenkins
    - 服务证书不用写(我们在安装Jenkins的时候已经指定过serviceAccount),均使用默认
    - 连接测试,成功会提示:Connection test successful
    - Jenkins地址:http://jenkins:8080
    - Jenkins 通道 :jenkins:50000
    4. 配置Pod Template
    - 名称:jnlp-slave
    - 命名空间:jenkins
    - 标签列表:jnlp-slave,作为agent的label选择用
    - 连接 Jenkins 的超时时间(秒) :300,设置连接jenkins超时时间
    - 节点选择器:agent=true
    - 工作空间卷:选择hostpath,设置/opt/jenkins_jobs/,注意需要设置chown -R 1000:1000 /opt/jenkins_jobs/权限,否则Pod没有权限

    演示动态slave pod

    为准备运行jnlp-slave-agent的pod的节点打上label
    $ kubectl label node node1 agent=true

[项目名称] -> [左下角的具体构建] -> [回放],更改Jenkinsfile中agent信息,进行一次流水线develop分支构建
agent { label ‘jnlp-slave’}

说明:在配置Pod Template时,笔者没有指定镜像,故jenkins-slave将使用默认的镜像地址。

Pod Template容器镜像定制化

因默认的jenkins-slave镜像的环境不能完全满足需求,故需自做tools镜像,集成常用的工具,来完成常见的构建任务,以下为注意点:
- 使用alpine基础镜像,自身体积比较小
- 替换国内安装源
- 为了使用docker,安装了docker,并配置认证信息
- 为了克隆代码,安装git
- 为了后续做python的测试等任务,安装python环境
- 为了在容器中调用kubectl的命令,拷贝了kubectl的二进制文件
- 为了认证kubectl,需要在容器内部生成.kube目录及config文件

操作
$ mkdir tools;
$ cd tools;
$ cp which kubectl .
$ cp ~/.kube/config .
$ cp /root/.docker/config.json .

说明:config.json是在已登录harbor.od.com仓库后的服务器上提取,此文件记录了仓库登录信息!

tools/Dockerfile

FROM alpine
LABEL maintainer="kazihuo8@qq.com"
USER root

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
    apk update && \
    apk add  --no-cache openrc docker git curl tar gcc g++ make \
    bash shadow openjdk8 python2 python2-dev py-pip openssl-dev libffi-dev \
    libstdc++ harfbuzz nss freetype ttf-freefont && \
    mkdir -p /root/.kube && \
    usermod -a -G docker root

COPY config /root/.kube/
COPY config.json /root/.docker/

RUN rm -rf /var/cache/apk/* 
#-----------------安装 kubectl--------------------#
COPY kubectl /usr/local/bin/
RUN chmod +x /usr/local/bin/kubectl
# ------------------------------------------------#

执行镜像构建并推送到仓库
$ docker build . -t harbor.od.com/base/tools:v1
$ docker push harbor.od.com/base/tools:v1

启动临时镜像做测试
$ docker run —rm -ti harbor.od.com/base/tools:v1 bash
# / git clone http://xxxxxx.git
# / kubectl get no
# / python3
#/ docker

重新挂载docker的sock文件
$ docker run -v /var/run/docker.sock:/var/run/docker.sock —rm -ti harbor.od.com/base/tools:v1 bash

demo项目自动发布

前面笔者配置Pod Template时,未涉及到Container Template的配置,故其会使用默认的容器模板。而现在要使用自制镜像实现环境需求,故需要在Pod Template中新增Container Template(基于自建镜像tools)的配置。

未防止新增Container Template配置会覆盖掉默认的Container Template配置,而失去jenkins-slave的功能,故在Container Template中也要加上默认的容器模板的配置。

要配置Container Template的默认设置,那如何get到默认的容器模板信息呢?
1、通过jenkins项目构建触发jenkins-slave Pod的生成;
2、因jenkins-slave Pod在构建完后会自动销毁,故在构建过程中通过 $ kubectl get pod jenkins-slavexxx -o yaml 查看信息即可;
3、根据第2步查看到的默认镜像,通过 $ docker inspect jenkins/inbound-agent:4.3-4 查看运行的命令及参数。

最后,jenkins中集群配置如下:
image.png
image.png
image.png

Jenkinsfile

pipeline {
    agent { label 'jnlp-slave'}

    options {
        buildDiscarder(logRotator(numToKeepStr: '10'))
        disableConcurrentBuilds()
        timeout(time: 20, unit: 'MINUTES')
        gitLabConnection('gitlab')
    }

    environment {
        IMAGE_REPO = "harbor.od.com/app/myblog"
        DINGTALK_CREDS = credentials('dingTalk')
        TAB_STR = "\n                    \n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"
    }

    stages {
        stage('printenv') {
            steps {
                script{
                    sh "git log --oneline -n 1 > gitlog.file"
                    env.GIT_LOG = readFile("gitlog.file").trim()
                }
                sh 'printenv'
            }
        }
        stage('checkout') {
            steps {
                container('tools') {
                    checkout scm
                }
                updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
                script{
                    env.BUILD_TASKS = env.STAGE_NAME + "√..." + env.TAB_STR
                }
            }
        }
        stage('build-image') {
            steps {
                container('tools') {
                    retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}
                }
                updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
                script{
                    env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
                }
            }
        }
        stage('push-image') {
            steps {
                container('tools') {
                    retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}
                }
                updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
                script{
                    env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
                }
            }
        }
        stage('deploy') {
            steps {
                container('tools') {
                    sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' deploy/*"
                    timeout(time: 1, unit: 'MINUTES') {
                        sh "kubectl apply -f deploy/"
                    }
                }
                updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
                script{
                    env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
                }
            }
        }
    }
    post {
        success { 
            echo 'Congratulations!'
            sh """
                curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
                    -H 'Content-Type: application/json' \
                    -d '{
                        "msgtype": "markdown",
                        "markdown": {
                            "title":"myblog",
                            "text": "😄👍 构建成功 👍😄  \n**项目名称**:luffy  \n**Git log**: ${GIT_LOG}   \n**构建分支**: ${BRANCH_NAME}   \n**构建地址**:${RUN_DISPLAY_URL}  \n**构建任务**:${BUILD_TASKS}"
                        }
                    }'
            """ 
        }
        failure {
            echo 'Oh no!'
            sh """
                curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
                    -H 'Content-Type: application/json' \
                    -d '{
                        "msgtype": "markdown",
                        "markdown": {
                            "title":"myblog",
                            "text": "😖❌ 构建失败 ❌😖  \n**项目名称**:luffy  \n**Git log**: ${GIT_LOG}   \n**构建分支**: ${BRANCH_NAME}  \n**构建地址**:${RUN_DISPLAY_URL}  \n**构建任务**:${BUILD_TASKS}"
                        }
                    }'
            """
        }
        always { 
            echo 'I will always say Hello again!'
        }
    }
}

说明
此节内容应该应用到多分支流水线的项目中,因为根据jenkins项目的类型不同,对应的可用环境变量也不一样,BRANCH_NAME等变量在一般的项目类型中是没有的。

集成sonarQube实现代码扫描

Sonar可以从以下七个维度检测代码质量,而作为开发人员至少需要处理前5种代码质量问题。
1. 不遵循代码标准
sonar可以通过PMD,CheckStyle,Findbugs等等代码规则检测工具规范代码编写。
2. 潜在的缺陷
sonar可以通过PMD,CheckStyle,Findbugs等等代码规则检测工具检 测出潜在的缺陷。
3. 糟糕的复杂度分布
文件、类、方法等,如果复杂度过高将难以改变,这会使得开发人员 难以理解它们, 且如果没有自动化的单元测试,对于程序中的任何组件的改变都将可能导致需要全面的回归测试。
4. 重复
显然程序中包含大量复制粘贴的代码是质量低下的,sonar可以展示 源码中重复严重的地方。
5. 注释不足或者过多
没有注释将使代码可读性变差,特别是当不可避免地出现人员变动 时,程序的可读性将大幅下降 而过多的注释又会使得开发人员将精力过多地花费在阅读注释上,亦违背初衷。
6. 缺乏单元测试
sonar可以很方便地统计并展示单元测试覆盖率。
7. 糟糕的设计
通过sonar可以找出循环,展示包与包、类与类之间的相互依赖关系,可以检测自定义的架构规则 通过sonar可以管理第三方的jar包,可以利用LCOM4检测单个任务规则的应用情况, 检测耦合。

架构简介

image.png
1. CS架构
- sonarqube scanner
- sonarqube server
2. SonarQube Scanner 扫描仪在本地执行代码扫描任务
3. 执行完后,将分析报告被发送到SonarQube服务器进行处理
4. SonarQube服务器处理和存储分析报告导致SonarQube数据库,并显示结果在UI中

环境搭建

1. 资源文件准备
sonar/sonar.yaml
- 和gitlab共享postgres数据库
- 部署在sonar=true的节点上
- 使用ingress地址 sonar.crab.com 进行访问
- 使用initContainers进行系统参数调整

apiVersion: v1
kind: Service
metadata:
  name: sonarqube
  namespace: jenkins
  labels:
    app: sonarqube
spec:
  ports:
  - name: sonarqube
    port: 9000
    targetPort: 9000
    protocol: TCP
  selector:
    app: sonarqube
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: jenkins
  name: sonarqube
  labels:
    app: sonarqube
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sonarqube
  template:
    metadata:
      labels:
        app: sonarqube
    spec:
      nodeSelector:
        sonar: "true"
      initContainers:
      - command:
        - /sbin/sysctl
        - -w
        - vm.max_map_count=262144
        image: alpine:3.6
        imagePullPolicy: IfNotPresent
        name: elasticsearch-logging-init
        resources: {}
        securityContext:
          privileged: true
      containers:
      - name: sonarqube
        image: sonarqube:7.9-community
        ports:
        - containerPort: 9000
        env:
        - name: SONARQUBE_JDBC_USERNAME
          valueFrom:
            secretKeyRef:
              name: gitlab-secret
              key: postgres.user.root
        - name: SONARQUBE_JDBC_PASSWORD
          valueFrom:
            secretKeyRef:
              name: gitlab-secret
              key: postgres.pwd.root
        - name: SONARQUBE_JDBC_URL
          value: "jdbc:postgresql://postgres:5432/sonar"
        livenessProbe:
          httpGet:
            path: /sessions/new
            port: 9000
          initialDelaySeconds: 60
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /sessions/new
            port: 9000
          initialDelaySeconds: 60
          periodSeconds: 30
          failureThreshold: 6
        resources:
          limits:
            cpu: 2000m
            memory: 4096Mi
          requests:
            cpu: 300m
            memory: 512Mi
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: sonarqube
  namespace: jenkins
spec:
  rules:
  - host: sonar.crab.com
    http:
      paths:
      - backend:
          serviceName: sonarqube
          servicePort: 9000
        path: /
status:
  loadBalancer: {}

2. sonarqube服务端安装
#打算将sonarqube部署在哪个节点,打上label,sonar=true
$ kubectl label node node1 sonar=true**
# 创建sonar数据库
$ kubectl -n jenkins exec -ti postgres-5859dc6f58-mgqz9 bash
#/ psql
# create database sonar;
#创建sonarqube服务器
$ kubectl create -f sonar.yaml
#配置解析
10.2.2.6 sonar.crab.com
## 访问sonarqube,初始用户名密码为 admin/admin
$ curl http://sonar.crab.com

3. sonar-scanner的安装
$ cd /opt
$ wget https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.2.0.1873-linux.zip
$ unzip sonar-scanner-cli-4.2.0.1873-linux.zip

  • 配置sonarqube服务器地址

由于sonar-scanner需要将扫描结果上报给sonarqube服务器做质量分析,因此我们需要在sonar-scanner中配置sonarqube的服务器地址。

$ cat sonar-scanner/conf/sonar-scanner.properties
#----- Default SonarQube server
#sonar.host.url=http://localhost:9000
sonar.host.url=http://sonar.crab.com
#----- Default source code encoding
#sonar.sourceEncoding=UTF-8
**

  • 在项目根目录(即需要扫描的项目)中准备配置文件(新建sonar-project.properties文件,填充如下内容)

sonar.projectKey=myblog
sonar.projectName=myblog
# if you want disabled the DTD verification for a proxy problem for example, true by default
sonar.coverage.dtdVerification=false
# JUnit like test report, default value is test.xml
sonar.sources=.

  • 执行扫描

在项目的根目录下执行
$ /opt/sonar-scanner-4.0.0.1744-linux/bin/sonar-scanner -X

  • sonarqube界面查看结果

sonarqube界面新增一条扫描记录
image.png

插件安装及配置

1、集成到tools容器中
由于我们的代码拉取、构建任务均是在tools容器中进行,因此我们需要把scanner集成到我们的tools容器中,又因为scanner是一个cli客户端,因此我们直接把包解压好,拷贝到tools容器内部,配置一下PATH路径即可,注意:
- 直接在tools镜像中配置http://sonar.crab.com
- 由于tools已经集成了java环境,因此可以直接剔除scanner自带的jre
- 删掉sonar-scanner/jre目录
- 修改sonar-scanner/bin/sonar-scanner
use_embedded_jre=false

$ cd tools
$ cp -r /opt/sonar-scanner-4.0.0.1744-linux/ sonar-scanner
## sonar配置,由于我们是在Pod中使用,也可以直接配置:sonar.host.url=http://sonarqube:9000
$ cat sonar-scanner/conf/sonar-scanner.properties
#----- Default SonarQube server
sonar.host.url=http://sonar.``crab``.com
#----- Default source code encoding
#sonar.sourceEncoding=UTF-8
$ rm -rf sonar-scanner/jre
$ vi sonar-scanner/bin/sonar-scanner

use_embedded_jre=false

jenkins/tools/Dockerfile

FROM alpine
LABEL maintainer="kazihuo8@qq.com"
USER root
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
    apk update && \
    apk add  --no-cache openrc docker git curl tar gcc g++ make \
    bash shadow openjdk8 python2 python2-dev py-pip openssl-dev libffi-dev \
    libstdc++ harfbuzz nss freetype ttf-freefont && \
    mkdir -p /root/.kube && \
    usermod -a -G docker root
COPY config /root/.kube/
COPY config.json /root/.docker/
RUN rm -rf /var/cache/apk/* 

#-----------------安装 kubectl--------------------#
COPY kubectl /usr/local/bin/
RUN chmod +x /usr/local/bin/kubectl
# ------------------------------------------------#

#---------------安装 sonar-scanner-----------------#
COPY sonar-scanner /usr/lib/sonar-scanner
RUN ln -s /usr/lib/sonar-scanner/bin/sonar-scanner /usr/local/bin/sonar-scanner && chmod +x /usr/local/bin/sonar-scanner
ENV SONAR_RUNNER_HOME=/usr/lib/sonar-scanner
# ------------------------------------------------#

$ docker build . -t harbor.od.com/base/tools:v2
$ docker push harbor.od.com/base/tools:v2

2. 修改Jenkins PodTemplate
为了在新的构建任务中可以拉取v2版本的tools镜像,需要更新PodTemplate

3. 安装并配置sonar插件**
由于sonarqube的扫描的结果需要进行Quality Gates的检测,那么我们在容器中执行完代码扫描任务后,如何知道本次扫描是否通过了Quality Gates,那么就需要借助于sonarqube实现的jenkins的插件。

  • 安装插件
    插件中心搜索sonarqube,直接安装
    - 配置插件
    系统管理->系统配置-> SonarQube servers ->Add SonarQube
    - Name:sonarqube
    - Server URL:http://sonarqube:9000http://sonar.crab.com
    - Server authentication token
    ① 登录sonarqube -> My Account -> Security -> Generate Token
    ② 登录Jenkins,添加全局凭据,类型为Secret text
    - 如何在jenkinsfile中使用
    我们在 https://jenkins.io/doc/pipeline/steps/sonar/ 官方介绍中可以看到:

    Jenkinsfile集成sonarqube

    Jenkinsfile
    jenkins/pipelines/p9.yaml

    pipeline {
    agent { label 'jnlp-slave'}
    
    options {
       buildDiscarder(logRotator(numToKeepStr: '10'))
       disableConcurrentBuilds()
       timeout(time: 20, unit: 'MINUTES')
       gitLabConnection('gitlab')
    }
    
    environment {
       IMAGE_REPO = "harbor.od.com/app/myblog"
       DINGTALK_CREDS = credentials('dingTalk')
       TAB_STR = "\n                    \n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"
    }
    
    stages {
       stage('git-log') {
           steps {
               script{
                   sh "git log --oneline -n 1 > gitlog.file"
                   env.GIT_LOG = readFile("gitlog.file").trim()
               }
               sh 'printenv'
           }
       }        
       stage('checkout') {
           steps {
               container('tools') {
                   checkout scm
               }
               updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
               script{
                   env.BUILD_TASKS = env.STAGE_NAME + "√..." + env.TAB_STR
               }
           }
       }
       stage('CI'){
           failFast true
           parallel {
               stage('Unit Test') {
                   steps {
                       echo "Unit Test Stage Skip..."
                   }
               }
               stage('Code Scan') {
                   steps {
                       container('tools') {
                           withSonarQubeEnv('sonarqube') {
                               sh 'sonar-scanner -X'
                               sleep 3
                           }
                           script {
                               timeout(1) {
                                   sleep(5)
                                   def qg = waitForQualityGate('sonarqube')
                                   if (qg.status != 'OK') {
                                       error "未通过Sonarqube的代码质量阈检查,请及时修改!failure: ${qg.status}"
                                   }
                               }
                           }
                       }
                   }
               }
           }
       }
       stage('build-image') {
           steps {
               container('tools') {
                   retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}
               }
               updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
               script{
                   env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
               }
           }
       }
       stage('push-image') {
           steps {
               container('tools') {
                   retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}
               }
               updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
               script{
                   env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
               }
           }
       }
       stage('deploy') {
           steps {
               container('tools') {
                   sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' deploy/*"
                   timeout(time: 1, unit: 'MINUTES') {
                       sh "kubectl apply -f deploy/"
                   }
               }
               updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
               script{
                   env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
               }
           }
       }
    }
    post {
       success { 
           echo 'Congratulations!'
           sh """
               curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
                   -H 'Content-Type: application/json' \
                   -d '{
                       "msgtype": "markdown",
                       "markdown": {
                           "title":"myblog",
                           "text": "😄👍 构建成功 👍😄  \n**项目名称**:crab  \n**Git log**: ${GIT_LOG}   \n**构建分支**: ${BRANCH_NAME}   \n**构建地址**:${RUN_DISPLAY_URL}  \n**构建任务**:${BUILD_TASKS}"
                       }
                   }'
           """ 
       }
       failure {
           echo 'Oh no!'
           sh """
               curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
                   -H 'Content-Type: application/json' \
                   -d '{
                       "msgtype": "markdown",
                       "markdown": {
                           "title":"myblog",
                           "text": "😖❌ 构建失败 ❌😖  \n**项目名称**:crab  \n**Git log**: ${GIT_LOG}   \n**构建分支**: ${BRANCH_NAME}  \n**构建地址**:${RUN_DISPLAY_URL}  \n**构建任务**:${BUILD_TASKS}"
                       }
                   }'
           """
       }
       always { 
           echo 'I will always say Hello again!'
       }
    }
    }
    

    集成RobotFramework实现验收测试

    一个基于Python语言,用于验收测试和验收测试驱动开发(ATDD)的通用测试自动化框架,提供了一套特定的语法,并且有非常丰富的测试库 。

    robot用例简介

    robot/robot.txt ```yaml Settings Library RequestsLibrary Library SeleniumLibrary

Variables ${demo_url} http://myblog.crab.com/admin

Test Cases api [Tags] critical Create Session api ${demo_url} ${alarm_system_info} RequestsLibrary.Get Request api / log ${alarm_system_info.status_code} log ${alarm_system_info.content} should be true ${alarm_system_info.status_code} == 200

ui [Tags] critical ${chrome_options} = Evaluate sys.modules[‘selenium.webdriver’].ChromeOptions() sys, selenium.webdriver Call Method ${chrome_options} add_argument headless Call Method ${chrome_options} add_argument no-sandbox ${options}= Call Method ${chrome_options} to_capabilities Open Browser ${demo_url}/ browser=chrome desired_capabilities=${options} sleep 2s Capture Page Screenshot Page Should Contain Django close browser


**# 使用tools镜像启动容器,来验证手动使用robotframework来做验收测试**<br />$ docker run --rm -ti  harbor.od.com/base/tools:v2 bash<br />bash-5.0# apk add chromium chromium-chromedriver<br />$ cat requirements.txt<br />`robotframework`<br />`robotframework-seleniumlibrary`<br />`robotframework-databaselibrary`<br />`robotframework-requests`<br />#pip安装必要的软件包<br />$ pip install -i [http://mirrors.aliyun.com/pypi/simple/](http://mirrors.aliyun.com/pypi/simple/) --trusted-host mirrors.aliyun.com -r requirements.txt  <br />#使用robot命令做测试<br />$ robot -d artifacts/ robot.txt  #将测试结果输出到artifacts目录<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1149260/1607509588595-70a446dd-9097-45c3-a4da-100b21d7b90b.png#align=left&display=inline&height=136&margin=%5Bobject%20Object%5D&name=image.png&originHeight=238&originWidth=790&size=14734&status=done&style=none&width=450)
<a name="EoHwH"></a>
### 与tools工具镜像集成
Dockerfile
```yaml
FROM alpine
LABEL maintainer="kazihuo8@qq.com"
USER root
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
    apk update && \
    apk add  --no-cache openrc docker git curl tar gcc g++ make \
    bash shadow openjdk8 python2 python2-dev py-pip openssl-dev libffi-dev \
    libstdc++ harfbuzz nss freetype ttf-freefont chromium chromium-chromedriver && \
    mkdir -p /root/.kube && \
    usermod -a -G docker root
COPY config /root/.kube/
COPY config.json /root/.docker/
COPY requirements.txt /

RUN pip install -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com -r requirements.txt 

RUN rm -rf /var/cache/apk/* && \
    rm -rf ~/.cache/pip

#-----------------安装 kubectl--------------------#
COPY kubectl /usr/local/bin/
RUN chmod +x /usr/local/bin/kubectl
# ------------------------------------------------#

#---------------安装 sonar-scanner-----------------#
COPY sonar-scanner /usr/lib/sonar-scanner
RUN ln -s /usr/lib/sonar-scanner/bin/sonar-scanner /usr/local/bin/sonar-scanner && chmod +x /usr/local/bin/sonar-scanner
ENV SONAR_RUNNER_HOME=/usr/lib/sonar-scanner
# ------------------------------------------------#

$ docker build -t . harbor.od.com/base/tools:v3
$ docker push harbor.od.com/base/tools:v3

插件安装及配置

1. 安装robotFramework
- 插件中心搜索robotframework,直接安装
- tools集成robot命令(之前已经安装)

2. 与jenkinsfile的集成

    container('tools') {
        sh 'robot -i critical  -d artifacts/ robot.txt || echo ok'
        echo "R ${currentBuild.result}"
        step([
            $class : 'RobotPublisher',
            outputPath: 'artifacts/',
            outputFileName : "output.xml",
            disableArchiveOutput : false,
            passThreshold : 80,
            unstableThreshold: 20.0,
            onlyCritical : true,
            otherFiles : "*.png"
        ])
        echo "R ${currentBuild.result}"
        archiveArtifacts artifacts: 'artifacts/*', fingerprint: true
    }

通过Jenkinsfile实现demo项目的验收测试

1、更新Jenkins中kubernetes中的containers template所所用的tools镜像,更改为tools:v3版本。
2、将robot.txt文件提交到项目代码根目录。

jenkins/pipelines/p10.yaml

pipeline {
    agent { label 'jnlp-slave'}

    options {
        buildDiscarder(logRotator(numToKeepStr: '10'))
        disableConcurrentBuilds()
        timeout(time: 20, unit: 'MINUTES')
        gitLabConnection('gitlab')
    }

    environment {
        IMAGE_REPO = "harbor.od.com/app/myblog"
        DINGTALK_CREDS = credentials('dingTalk')
        TAB_STR = "\n                    \n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"
    }

    stages {
        stage('git-log') {
            steps {
                script{
                    sh "git log --oneline -n 1 > gitlog.file"
                    env.GIT_LOG = readFile("gitlog.file").trim()
                }
                sh 'printenv'
            }
        }        
        stage('checkout') {
            steps {
                container('tools') {
                    checkout scm
                }
                updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
                script{
                    env.BUILD_TASKS = env.STAGE_NAME + "√..." + env.TAB_STR
                }
            }
        }
        stage('CI'){
            failFast true
            parallel {
                stage('Unit Test') {
                    steps {
                        echo "Unit Test Stage Skip..."
                    }
                }
                stage('Code Scan') {
                    steps {
                        container('tools') {
                            withSonarQubeEnv('sonarqube') {
                                sh 'sonar-scanner -X'
                                sleep 3
                            }
                            script {
                                timeout(1) {
                                    sleep(5)
                                    def qg = waitForQualityGate('sonarqube')
                                    if (qg.status != 'OK') {
                                        error "未通过Sonarqube的代码质量阈检查,请及时修改!failure: ${qg.status}"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        stage('build-image') {
            steps {
                container('tools') {
                    retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}
                }
                updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
                script{
                    env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
                }
            }
        }
        stage('push-image') {
            steps {
                container('tools') {
                    retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}
                }
                updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
                script{
                    env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
                }
            }
        }
        stage('deploy') {
            steps {
                container('tools') {
                    sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' deploy/*"
                    timeout(time: 1, unit: 'MINUTES') {
                        sh "kubectl apply -f deploy/;sleep 20;"
                    }
                }
                updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
                script{
                    env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
                }
            }
        }
        stage('Accept Test') {
            steps {
                    container('tools') {
                        sh 'robot -i critical  -d artifacts/ robot.txt|| echo ok'
                        echo "R ${currentBuild.result}"
                        step([
                            $class : 'RobotPublisher',
                            outputPath: 'artifacts/',
                            outputFileName : "output.xml",
                            disableArchiveOutput : false,
                            passThreshold : 40,
                            unstableThreshold: 20.0,
                            onlyCritical : true,
                            otherFiles : "*.png"
                        ])
                        echo "R ${currentBuild.result}"
                        archiveArtifacts artifacts: 'artifacts/*', fingerprint: true
                        sh "rm -rf artifacts"
                    }
            }
        }
    }
    post {
        success { 
            echo 'Congratulations!'
            sh """
                curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
                    -H 'Content-Type: application/json' \
                    -d '{
                        "msgtype": "markdown",
                        "markdown": {
                            "title":"myblog",
                            "text": "😄👍 构建成功 👍😄  \n**项目名称**:crab  \n**Git log**: ${GIT_LOG}   \n**构建分支**: ${BRANCH_NAME}   \n**构建地址**:${RUN_DISPLAY_URL}  \n**构建任务**:${BUILD_TASKS}"
                        }
                    }'
            """ 
        }
        failure {
            echo 'Oh no!'
            sh """
                curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
                    -H 'Content-Type: application/json' \
                    -d '{
                        "msgtype": "markdown",
                        "markdown": {
                            "title":"myblog",
                            "text": "😖❌ 构建失败 ❌😖  \n**项目名称**:crab  \n**Git log**: ${GIT_LOG}   \n**构建分支**: ${BRANCH_NAME}  \n**构建地址**:${RUN_DISPLAY_URL}  \n**构建任务**:${BUILD_TASKS}"
                        }
                    }'
            """
        }
        always { 
            echo 'I will always say Hello again!'
        }
    }
}

#Jenkins构建后查看页面如下
image.png