CI/CD

日常运维中常常听到 CI/CD 这个词,它其实包含整个研发生命周期的三个阶段:

  • CI,Continuous integration,持续集成
  • CD,Continuous delivery,持续交付
  • CD,Continuous deployment,持续部署

大致的流程图如下:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group

而对于 Kubernete 的 CI/CD 工具目前也有很多,比如 JenkinsGitlab CI 以及 drone 等等,日常运维中遇到最多的就是 Jenkins。

Jenkins

Jenkins 是由 Java 开发的一个可扩展的持续集成引擎,前身为 Hudson。官网地址为:

https://www.jenkins.io

由于是 Java 开发,那么 Jenkins 的运行就会依赖于 JDK 运行环境。在官方文档中有提到:

Jenkins requires Java 11 or 17 since Jenkins 2.357 and LTS 2.361.1

注意自己配置的时候选对 JDK 版本。安装 Jenkins 的方式也有很多,常见的有:

  • Docker(一般不会单独选择它)
  • Kubernetes(在 Kubernetes 环境中可以选择,好处在于高可用)
  • Linux rpm(安装简单,但是不方便定制化统一管理)
  • Java tomcat(War 或者 jar,用户自定义程度高,管理维护也方便)
  • 其它

可以根据自己的需求进行安装,但是需要注意:如果公司项目比较多,构建也很频繁,需要配置较大内存(或者多个 Agent)和较大磁盘空间(会存代码依赖包,可能很大)。

官网中也有提到关于配置方面的内容:

Minimum hardware requirements:

  • 256 MB of RAM
  • 1 GB of drive space (although 10 GB is a recommended minimum if running Jenkins as a Docker container)

Recommended hardware configuration for a small team:

  • 4 GB+ of RAM
  • 50 GB+ of drive space

The amount of memory Jenkins needs is largely dependent on many factors, which is why the RAM allotted for it can range from 200 MB for a small installation to 70+ GB for a single and massive Jenkins controller. However, you should be able to estimate the RAM required based on your project build needs.

Each build node connection will take 2-3 threads, which equals about 2 MB or more of memory. You will also need to factor in CPU overhead for Jenkins if there are a lot of users who will be accessing the Jenkins user interface.

接下来将会通过两种方式进行安装。

Jenkins Server(War / 推荐使用)

安装包可以去官网下载,本文使用的是最新稳定版 2.387.1

https://get.jenkins.io/war-stable/

同时也需要 JDK 11 以上版本,这些都可以去华为镜像站下载。

Jenkins:

https://repo.huaweicloud.com/jenkins/

JDK:

https://repo.huaweicloud.com/java/jdk/

如果想要使用 Tomcat 运行 war,可以看看官网的要求:

Jenkins requires Servlet API 4.0 (Jakarta EE 8) with javax.servlet imports.

Tomcat 9 is based on Servlet API 4.0 (Jakarta EE 8), which is the version of the servlet API required by Jenkins.


安装 JDK:

# 下载安装包
wget https://repo.huaweicloud.com/java/jdk/11.0.2+9/jdk-11.0.2_linux-x64_bin.tar.gz

# 解压安装
tar -zxf jdk-11.0.2_linux-x64_bin.tar.gz 
mkdir -p /opt/service/jdk
mv jdk-11.0.2 /opt/service/jdk/

由于这里 Java 没有配置环境变量,后续需要使用绝对路径来执行 java 命令。

这也是在 Jenkins 环境中比较推荐的,因为可能会存在多个 JDK 版本共存的情况,不建议设置环境变量。


安装 Jenkins:

# 下载安装包
wget https://repo.huaweicloud.com/jenkins/war-stable/2.387.1/jenkins.war

# 安装依赖
yum install fontconfig

# 解压安装
mkdir -p /opt/service/jenkins/{logs,data,backup,bin,server,agent}
mv jenkins.war /opt/service/jenkins/server/


配置启动脚本,相关参数可以在官方文档中看到:

https://www.jenkins.io/doc/book/installing/initial-settings/

其它 Jenkins 本身的启动参数:

https://www.jenkins.io/doc/book/managing/system-properties/#Featurescontrolledbysystemproperties-PropertiesinJenkinsCore

常用的参数包含以下:

  • 数据目录:-DHUDSON_HOME
  • 时区设置:-Dorg.apache.commons.jelly.tags.fmt.timeZone=Asia/Shanghai

脚本内容如下:

cat > /opt/service/jenkins/bin/start_server.sh << EOF
#!/bin/bash

##############################################################
# 说明:Jenkins 启动脚本
##############################################################

##############################################################
# JDK 相关
##############################################################
# 安装目录
JAVA_HOME="/opt/service/jdk/jdk-11.0.2"
# 启动命令
JAVA="\${JAVA_HOME}/bin/java"

##############################################################
# Jenkins 相关
##############################################################
# 安装目录
JENKINS_BASE_PATH="/opt/service/jenkins"
# 数据目录
JENKINS_HOME="\${JENKINS_BASE_PATH}/data"
# 日志目录
JENKINS_LOG="\${JENKINS_BASE_PATH}/logs"
# Server 目录
JENKINS_SERVER="\${JENKINS_BASE_PATH}/server"
# Agent 目录
JENKINS_AGENT="\${JENKINS_BASE_PATH}/agent"

##############################################################
# 启动参数
##############################################################
\${JAVA} -DHUDSON_HOME="\${JENKINS_HOME}" \\
        -Dorg.apache.commons.jelly.tags.fmt.timeZone=Asia/Shanghai \\
        -jar "\${JENKINS_SERVER}/jenkins.war" > \${JENKINS_LOG}/jenkins-server.log 2>&1 &
EOF


修改权限并启动:

chmod 755 /opt/service/jenkins/bin/start_server.sh
sh /opt/service/jenkins/bin/start_server.sh

启动之后在日子中会看到:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_02

这里就是第一次登录的密码,使用安装服务器的 8080 端口则可以访问到 Jenkins。

初始化配置

启动完成之后就能进行初始化配置了:

  1. 使用刚刚日志中的密码解锁 Jenkins:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_03


  1. 插件安装,可以后续自己安装,推荐的很多用不到,到时候用一个装一个:

kubesphere jenkins 用户名密码 kubernetes jenkins_Jenkins_04

有些时候因为网络的原因,这个页面面能会提示 离线,不需要管它,跳过插件安装 即可。

只需要安装中文本地插件,用于汉化即可:

kubesphere jenkins 用户名密码 kubernetes jenkins_Jenkins_05

安装如图所示:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_06


  1. 创建超级管理员用户:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_07


  1. 配置 Jenkins 访问地址:

kubesphere jenkins 用户名密码 kubernetes jenkins_JAVA_08

如果有域名就写域名,后续接口访问这些会在页面显示这个地址,如果不修改也没有关系,后面可以通过在系统设置中设置:


  1. 安装完成:

kubesphere jenkins 用户名密码 kubernetes jenkins_JAVA_09


  1. 修改 Jenkins 保存密码的方式 Manage Jenkins

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_10

全局安全设置:

kubesphere jenkins 用户名密码 kubernetes jenkins_Jenkins_11

使用 Jenkins 自己的数据库:

kubesphere jenkins 用户名密码 kubernetes jenkins_JAVA_12

一般安装配置完成使用就是这个,如果不是,那么下次登录的时候就会有问题。


  1. 中文设置:

kubesphere jenkins 用户名密码 kubernetes jenkins_JAVA_13

设置 Locale:

kubesphere jenkins 用户名密码 kubernetes jenkins_Jenkins_14

设置为 zh_CN 并忽略浏览器语言。但是 Jenkins 这个汉化比较随意,一般都不彻底。

Jenkins Agent(Jar / 推荐使用)

打开 Jenkins 的管理 Jenkins 菜单,会有这样一个提示:

kubesphere jenkins 用户名密码 kubernetes jenkins_Jenkins_15

Jenkins 不推荐直接在 Server 上进行 build,建议按照 agent 来执行构建任务。

这里推荐的也是 agent,毕竟一台机器的性能有限,如果需求很大,可以通过多个 agent 分担压力。


创建 Agent 节点:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_16


添加节点:

kubesphere jenkins 用户名密码 kubernetes jenkins_Jenkins_17


kubesphere jenkins 用户名密码 kubernetes jenkins_JAVA_18


详细信息配置:

kubesphere jenkins 用户名密码 kubernetes jenkins_Jenkins_19


完成之后可以在左边看到:

kubesphere jenkins 用户名密码 kubernetes jenkins_JAVA_20


查看节点状态:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_21

发现因为启动的时候并没有配置 Agent 通信的节点,所有默认只监听了 Web 访问端口。没有用于 Agent 连接的端口。

可以点击 配置链接 进行端口设置(实际就是在全局安全设置下面):

kubesphere jenkins 用户名密码 kubernetes jenkins_JAVA_22

这里自定义了一个 12580 端口。


再次查看节点状态,可以看到已经不一样了:

kubesphere jenkins 用户名密码 kubernetes jenkins_JAVA_23

用过 Kubernetes 的就知道,这个有点像节点加入集群的方法,可以选择第二种安装 agent 的方式。

同时还提供了 agent 的下载地址,此时只需要在一台新的机器上执行相关命令即可,由于我这里测试只有一台机器,所以还是当前机器:

cd /opt/service/jenkins/agent/
echo ae9baf90e272d7c969f4f264573374eba3ca42666806c45980a071c02b7c8013 > secret-file
curl -sO http://192.168.2.100:8080/jnlpJars/agent.jar


添加启动脚本:

cat > /opt/service/jenkins/bin/start_agent.sh << EOF
#!/bin/bash

##############################################################
# 说明:Jenkins Agent 启动脚本
##############################################################

##############################################################
# JDK 相关
##############################################################
# 安装目录
JAVA_HOME="/opt/service/jdk/jdk-11.0.2"
# 启动命令
JAVA="\${JAVA_HOME}/bin/java"

##############################################################
# Jenkins 相关
##############################################################
# 安装目录
JENKINS_BASE_PATH="/opt/service/jenkins"
# 日志目录
JENKINS_LOG="\${JENKINS_BASE_PATH}/logs"
# Agent 目录
JENKINS_AGENT="\${JENKINS_BASE_PATH}/agent"

##############################################################
# 启动参数
##############################################################
cd ${JENKINS_AGENT}
\${JAVA} -jar agent.jar \\
    -jnlpUrl http://192.168.2.100:8080/manage/computer/%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%2D01/jenkins-agent.jnlp \\
    -secret @secret-file \\
    -workDir "/devops/jenkins" > \${JENKINS_LOG}/jenkins-agent.log 2>&1 &
EOF


启动 agent:

chmod 755 /opt/service/jenkins/bin/start_agent.sh
sh /opt/service/jenkins/bin/start_agent.sh


启动完成之后刷新页面:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_24

Jenkins Server(Kubernetes)

Kubernetes 里面运行 Jenkins,最核心的就是数据持久化。

  1. 创建一个 PV:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-longhorn-jenkins
  namespace: devops
spec:
  storageClassName: longhorn
  resources:
    requests:
      storage: 1Gi
  accessModes:
    - ReadWriteOnce


  1. 创建 RBAC 授权:
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sa-jenkins
  namespace: devops
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cr-jenkins
rules:
  - apiGroups: ["extensions", "apps"]
    resources: ["deployments", "ingresses"]
    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", "events"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: crb-jenkins
  namespace: devops
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cr-jenkins
subjects:
  - kind: ServiceAccount
    name: sa-jenkins
    namespace: devops

为了安全起见,没有设置 cluster-admin 的集群角色权限,而是使用自定义的。


  1. 创建 Jenkins Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins
  namespace: devops
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: jenkins
      app.kubernetes.io/version: "2.397"
  template:
    metadata:
      labels:
        app.kubernetes.io/name: jenkins
        app.kubernetes.io/version: "2.397"
    spec:
      serviceAccount: sa-jenkins
      volumes:
        - name: v-jenkins
          persistentVolumeClaim:
            claimName: pvc-longhorn-jenkins
      initContainers:
        - name: fix-permissions
          image: busybox:1.34.1
          command: ["sh", "-c", "chown -R 1000:1000 /var/jenkins_home"]
          securityContext:
            privileged: true
          volumeMounts:
            - name: v-jenkins
              mountPath: /var/jenkins_home
      containers:
      - name: jenkins
        image: jenkins/jenkins:2.397
        env:
          - name: JAVA_OPTS
            value: -Dhudson.model.DownloadService.noSignatureCheck=true
        ports:
          - containerPort: 8080
            name: web
          - containerPort: 50000
            name: agent
        resources:
          requests:
            cpu: 1000m
            memory: 1024Mi
          limits:
            cpu: 1000m
            memory: 1024Mi
        readinessProbe:
          httpGet:
            path: /login
            port: 8080
          initialDelaySeconds: 60
        volumeMounts:
          - name: v-jenkins
            mountPath: /var/jenkins_home

由于 jenkens 会对 update-center.json 做签名校验安全检查,需要先提前关闭,否则下面更改插件源可能会失败。

通过配置环境变量 JAVA_OPTS=-Dhudson.model.DownloadService.noSignatureCheck=true 即可。


  1. 创建 Service:
apiVersion: v1
kind: Service
metadata:
  name: svc-jenkins
  namespace: devops
spec:
  selector:
    app.kubernetes.io/name: jenkins
    app.kubernetes.io/version: "2.397"
  ports:
    - name: web
      port: 8080
      targetPort: 8080
    - name: agent
      port: 50000
      targetPort: 50000


  1. 创建 Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: jenkins-ingress
  namespace: devops
spec:
  ingressClassName: nginx
  rules:
  - host: jenkins.k8s.io
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: svc-jenkins
            port: 
              number: 8080

本地配置 ingress 节点的 host 解析即可。

完成之后通过查看 Pod 日志或者进入 Pod 中查看初始化密码,初始化不走跟 war 包启动的方式一样。

Jenkins Slave(Kubernetes)

在传统的 Server / Agent 架构中,存在以下一些痛点:

  • Master 单点故障,整个 Jenkins 集群就不可用了。
  • 可以通过给不同 Agent 配置不同的打包环境用于处理特定的任务,但是不好管理,增加了维护成本。
  • 资源分配不均,忙的忙死,闲的闲死。
  • 在没有任务构建的时候,服务会一直占用着系统资源,造成资源浪费。

为了解决这些痛点,在 Kubernetes 中采用了新的架构来实现 CI/CD:

kubesphere jenkins 用户名密码 kubernetes jenkins_JAVA_25

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

该方案的好处在于:

  • 服务高可用,不再有单点故障的风险。
  • 动态伸缩,合理的利用资源。
  • 扩展性好,能够尽可能的减少多任务排队情况,提高效率。


配置方法:

  1. 安装 Kubernetes 插件:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_26

安装完成后重启 Jenkins。


设置集群:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_27


设置 Kubernetes:

kubesphere jenkins 用户名密码 kubernetes jenkins_Jenkins_28


配置 Kubernetes 信息:

kubesphere jenkins 用户名密码 kubernetes jenkins_Jenkins_29


配置 Jenkins 信息:

kubesphere jenkins 用户名密码 kubernetes jenkins_JAVA_30


配置 Pod 信息:

kubesphere jenkins 用户名密码 kubernetes jenkins_JAVA_31

注意标签不要有特殊符号,可能会识别失败。


配置容器信息:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_32

特别说明:

官方的容器其实只是一个模板,需要根据自己的实际情况,打包环境,在它的基础上生成新的容器。

对于一家公司而言,开发语言可能会有很多种,这意味着 agent 的镜像可能会有多个,Pod 也就有多个。通过对不同的 Pod 设置不同的标签,可以在执行任务的时候指定使用某个 Pod。


配置 Pod SA:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_33

Jenkins Slave(Kubernetes 测试)

创建一个自由风格的任务:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_34


执行 Shell:

kubesphere jenkins 用户名密码 kubernetes jenkins_Jenkins_35


执行构建:

kubesphere jenkins 用户名密码 kubernetes jenkins_Jenkins_36

可以看到新建了一个 agent 来执行构建。


也能看到新建的 Pod:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_37

调整时区

默认安装的 Jenkins 的时区不对,需要进行配置:

kubesphere jenkins 用户名密码 kubernetes jenkins_Jenkins_38

执行配置:

System.setProperty('org.apache.commons.jelly.tags.fmt.timeZone', 'Asia/Shanghai')

如图所示:

kubesphere jenkins 用户名密码 kubernetes jenkins_Group_39

此时时间就正常了!