沪ICP备2021032517号-1

基于 Jenkins、Gitlab、Harbor和 Kubernetes 的 CI/CD

  |   0 评论   |   0 浏览

流程图。来源于这篇文章


image.png

流程说明

本篇文章没有使用Helm管理yaml资源,替换成流水线中使用kubectl更新yaml文件

  1. 开发人员提交代码到 Gitlab 代码仓库
  2. 通过 Gitlab 配置的 Jenkins Webhook 触发 Pipeline 自动构建
  3. Jenkins 触发构建构建任务,根据 Pipeline 脚本定义分步骤构建
  4. 先进行代码静态分析,单元测试
  5. 然后进行 Maven 构建(Java 项目)
  6. 根据构建结果构建 Docker 镜像
  7. 推送 Docker 镜像到 Harbor 仓库
  8. 流水线执行 kubectl 命令更新yaml文件
  9. 查看服务是否更新成功。

以上步骤的实现需要用到的主要系统和工具有:

gitkraken git客户端,负责向gitlab提交 和更新代码

gitlab 收到git客户端推送来的代码后触发jenkins执行流水线任务

jenkins gitlab触发jenkins后运行流水线、根据jenkinsfile执行Maven打包、构建镜像并推送到Harbor

Harbor 负责存储jenkins构建的image、当Kubernetes创建容器时从该仓库获取image

kubectl 负责在Kubernetes上部署和和更新服务

Kubernetes 负责运行POD

以上各系统的部署在本博客均有文章详细给出,这里不做描述

本篇文章会详细描述配置各系统之间的连接过程。在实现这以流程前确保这些系统已部署完毕。


项目是solo博客系统

先将项目clone到本地,然后添加以下文件

Dockerfile文件,内容如下

FROM openjdk:8-alpine

COPY target/solo /app/

WORKDIR /app

ENV  TZ=Asia/Shanghai

EXPOSE 8080

ENTRYPOINT [ "java", "-cp", "WEB-INF/lib/*:WEB-INF/classes", "org.b3log.solo.Starter" ]

下面是流水线文件

其中

def imageTag = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() 自动生成版本号

替换成了手动输入

def imageTag = "v1.2" #这里的版本号每次发布是需要修改,且和需要和后面yaml文件的imaged值对应

Jenkinsfile

def label = "slave-${UUID.randomUUID().toString()}"
podTemplate(label: label, containers: [
  containerTemplate(name: 'maven', image: 'maven:3.6-alpine', command: 'cat', ttyEnabled: true),
  containerTemplate(name: 'docker', image: 'docker', command: 'cat', ttyEnabled: true),
  containerTemplate(name: 'kubectl', image: 'cnych/kubectl', command: 'cat', ttyEnabled: true),
  containerTemplate(name: 'helm', image: 'cnych/helm', command: 'cat', ttyEnabled: true)
], volumes: [
  hostPathVolume(mountPath: '/root/.m2', hostPath: '/var/run/m2'),
  hostPathVolume(mountPath: '/home/jenkins/.kube', hostPath: '/root/.kube'),
  hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
  hostPathVolume(mountPath: '/etc/docker', hostPath: '/etc/docker'),
  hostPathVolume(mountPath: '/etc/hosts', hostPath: '/etc/hosts')
]) {
node(label) {
  def myRepo = checkout scm
  def gitCommit = myRepo.GIT_COMMIT
  def gitBranch = myRepo.GIT_BRANCH

  def imageTag = "v1.2"
  def dockerRegistryUrl = "hbr.hf.cn"
  def imageEndpoint = "test/solo"
  def image = "${dockerRegistryUrl}/${imageEndpoint}"

  stage('代码编译打包') {
    try {
      container('maven') {
        echo "2. 代码编译打包阶段"
        sh "mvn clean package -Dmaven.test.skip=true"
      }
    } catch (exc) {
      println "构建失败 - ${currentBuild.fullDisplayName}"
      throw(exc)
    }
  }
  stage('构建 Docker 镜像') {
    withCredentials([[$class: 'UsernamePasswordMultiBinding',
      credentialsId: 'dockerhbr',
      usernameVariable: 'DOCKER_HUB_USER',
      passwordVariable: 'DOCKER_HUB_PASSWORD']]) {
        container('docker') {
          echo "3. 构建 Docker 镜像阶段"
          sh """
            docker login https://${dockerRegistryUrl} -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD}
            docker build -t ${image}:${imageTag} .
            docker push ${image}:${imageTag}
            docker rmi -f ${image}:${imageTag}
            #sleep 3600
            """
        }
    }
  }
  stage('运行 Kubectl') {
      container('kubectl') {
        echo "创建对象"
        sh """
           #kubectl delete -f deployment.yaml
           kubectl apply -f deployment.yaml
           #kubectl create -f service.yaml
           #kubectl create -f ingress.yaml
           kubectl get pods --all-namespaces
           """
      }
    }
  }
}  

连接数据库部分为了账号密码安全问题使用secret环境变量方式

先根据代码中配置文件连接mysql的变量值创建连接mysql的环境变量

配置文件中关于mysql部分的定义

#### MySQL runtime ####
runtimeDatabase=${RUNTIME_DB}
jdbc.username=${JDBC_USERNAME}
jdbc.password=${JDBC_PASSWORD}
jdbc.driver=${JDBC_DRIVER}
jdbc.URL=${JDBC_URL}

创建环境变量

echo 'jdbc:mysql://192.168.19.126:3306/solo?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC' > JDBC_URL

echo 'solo' > JDBC_USERNAME

echo 'solo' > JDBC_PASSWORD

echo 'com.mysql.cj.jdbc.Driver' > JDBC_DRIVER

echo 'mysql' > RUNTIME_DB

根据自己情况做调整

创建Secret

kubectl create secret generic mysqlsecret --from-file=RUNTIME_DB --from-file=JDBC_USERNAME --from-file=JDBC_PASSWORD --from-file=JDBC_DRIVER --from-file=JDBC_URL --namespace=dev

deployment.yaml

apiVersion: apps/v1
#apiVersion: v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    run: solo
  name: solo
  namespace: dev
spec:
  replicas: 1
  selector:
    matchLabels:
      run: solo
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        run: solo
    spec:
      containers:
      - image: hbr.hf.cn/test/solo:v1.2
        name: solo
        env:
        - name: JDBC_USERNAME
          valueFrom:
            secretKeyRef:
              name: mysqlsecret
              key: JDBC_USERNAME
        - name: JDBC.PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysqlsecret
              key: JDBC_PASSWORD
        - name: JDBC_DRIVER
          valueFrom:
            secretKeyRef:
              name: mysqlsecret
              key: JDBC_DRIVER
        - name: RUNTIME_DB
          valueFrom:
            secretKeyRef:
              name: mysqlsecret
              key: RUNTIME_DB
        - name: JDBC_URL
          valueFrom:
            secretKeyRef:
              name: mysqlsecret
              key: JDBC_URL
        resources:
          requests:
            memory: "2Gi"
            cpu: "500m"
          limits:
            memory: "4Gi"
            cpu: "800m"
status: {}

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: solo
  namespace: dev
spec:
  ports:
    - protocol: TCP
      name: web
      port: 80
      targetPort: 8080
  selector:
    app: solo

Ingress.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: solo
  namespace: dev
  annotations:
    kubernetes.io/ingress.class: traefik
spec:
  rules:
  - host: solo.cn
    http:
      paths:
      - path: /
        backend:
          serviceName: solo
          servicePort: web

将以上文件到项目中的根目录并push到gitlab

image.png


jenkins在Kubernetes部署及连接Kubernetes参考本博客jenkins分类部分文章

Gitlab 配置 Webhook 触发jenkins自动构建

jenkins设置

系统管理---全局安全设置---勾选匿名用户可以做任何事

系统管理---全局安全设置---取消勾选跨站请求伪造保护

image.png

流水线配置

新建一个流水线项目 名称为 app

image.png

Jenkins_URL就是ip或者域名,域名的话gitlab能够访问这个域名(确认是否要在hosts添加映射)

http://jenkins.ui/job/app/build?token=test

gitlab设置

其中项目名字略有偏差,方法流程都是一样的,根据自己实际情况填写

先设置允许外发请求

image.png

注意:

gitlab在创建群组时 群组名称不能以中文没命名,否则后面配置完Web钩子,测试时会报500错误。如下图蓝色标注部分不能是中文。

image.png

点击项目---设置---集成 最下面的 SSL verification 不要勾选

image.png

添加后点击测试

如果测试出现了 Hook executed successfully: HTTP 201则证明 Webhook 配置成功了

image.png

image.png

到jenkins页面也可以看到任务会在gitlab点击测试后自动构建

jenkins官网关于流水线的教程
https://jenkins.io/zh/doc/pipeline/tour/hello-world/

出现的问题1:

image.png

第三阶段失败,查看输出信息如下:

[Pipeline] { (运行 Kubectl)
[Pipeline] container
[Pipeline] {
[Pipeline] echo
查看 K8S 集群 Pod 列表
[Pipeline] sh
+ kubectl get pods
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:default" cannot list resource "pods" in API group "" in the namespace "default"
[Pipeline] }
[Pipeline] // container
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] }
[Pipeline] // podTemplate
[Pipeline] End of Pipeline
ERROR: script returned exit code 1
Finished: FAILURE

错误原因分析

Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:default" cannot list resource "pods" in API group "" in the namespace "default"

来自服务器的错误(禁止):禁止使用pod:用户“ system:serviceaccount:default:default”无法在名称空间“ default”的API组“”中列出资源“ pods”

好像权限方面的问题

解决方案

这里文章中提供的解决方案是

kubectl create clusterrolebinding kube-system-default-admin \
  --clusterrole=cluster-admin \
  --serviceaccount=kube-system:default

重启POD后发现没有上述报错

连接数据库部分为了账号密码安全问题使用secret环境变量方式

先根据代码中配置文件连接mysql的变量值创建连接mysql的环境变量

配置文件中关于mysql部分的定义

#### MySQL runtime ####
runtimeDatabase=${RUNTIME_DB}
jdbc.username=${JDBC_USERNAME}
jdbc.password=${JDBC_PASSWORD}
jdbc.driver=${JDBC_DRIVER}
jdbc.URL=${JDBC_URL}

创建环境变量

echo 'com.mysql.cj.jdbc.Driversolojdbc:mysql://192.168.19.126:3306/solo?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTCr' > JDBC_URL

echo 'solo' > JDBC_USERNAME

echo 'solo' > JDBC_PASSWORD

echo 'com.mysql.cj.jdbc.Driver' > JDBC_DRIVER

echo 'mysql' > RUNTIME_DB

根据自己情况做调整

创建Secret

kubectl create secret generic mysqlsecret --from-file=RUNTIME_DB --from-file=JDBC_USERNAME --from-file=JDBC_PASSWORD --from-file=JDBC_DRIVER --from-file=JDBC_URL --namespace=dev

jenkins流水线中构建image并推送到Harbor

连接Harbor的前提条件

1. 由于Harbor附带的registry为v2版本,默认使用https方式,且默认情况下,https仅支持域名访问

2. 客户端需要能政正常访问harbor的域名,hosts要有ip和域名的映射关系

3. 客户端需要存在这个目录和文件 /etc/docker/certs.d/域名/ca.crt

上面是连接v2版本的Harbor的方式,缺一不可。所以在流水线的docker image构建推送环节必须考虑上述因素

下面是流水线文件

cat Jenkinsfile

def label = "slave-${UUID.randomUUID().toString()}"
podTemplate(label: label, containers: [
  containerTemplate(name: 'maven', image: 'maven:3.6-alpine', command: 'cat', ttyEnabled: true),
  containerTemplate(name: 'docker', image: 'docker', command: '', ttyEnabled: true),
  containerTemplate(name: 'kubectl', image: 'cnych/kubectl', command: 'cat', ttyEnabled: true),
  containerTemplate(name: 'helm', image: 'cnych/helm', command: 'cat', ttyEnabled: true)
], volumes: [
  hostPathVolume(mountPath: '/root/.m2', hostPath: '/var/run/m2'),
  hostPathVolume(mountPath: '/home/jenkins/.kube', hostPath: '/root/.kube'),
  hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
  hostPathVolume(mountPath: '/etc/docker', hostPath: '/etc/docker'),
  hostPathVolume(mountPath: '/etc/hosts', hostPath: '/etc/hosts')
]) {
node(label) {
  def myRepo = checkout scm
  def gitCommit = myRepo.GIT_COMMIT
  def gitBranch = myRepo.GIT_BRANCH

  def imageTag = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
  def dockerRegistryUrl = "hbr.zifuy.cn"
  def imageEndpoint = "test/tale"
  def image = "${dockerRegistryUrl}/${imageEndpoint}"

  stage('代码编译打包') {
    try {
      container('maven') {
        echo "2. 代码编译打包阶段"
        sh "mvn clean package -Dmaven.test.skip=true"
      }
    } catch (exc) {
      println "构建失败 - ${currentBuild.fullDisplayName}"
      throw(exc)
    }
  }
  stage('构建 Docker 镜像') {
    withCredentials([[$class: 'UsernamePasswordMultiBinding',
      credentialsId: 'dockerhbr',
      usernameVariable: 'DOCKER_HUB_USER',
      passwordVariable: 'DOCKER_HUB_PASSWORD']]) {
        container('docker') {
          echo "3. 构建 Docker 镜像阶段"
          sh """
            docker login https://${dockerRegistryUrl} -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD}
            docker build -t ${image}:${imageTag} .
            docker push ${image}:${imageTag}
            """
        }
    }
  }
    stage('运行 Kubectl') {
      container('kubectl') {
        echo "查看 K8S 集群 Pod 列表"
        sh "kubectl get pods"
      }
    }
    stage('运行 Helm') {
      container('helm') {
        echo "查看 Helm Release 列表"
        sh "helm list"
      }
    }
  }
}  

上面jenkinsfile文件的解释

volumes模块定义了挂载宿主机目录到容器目录的选项

/var/run/docker.sock 该文件是用于 Pod 中的容器能够共享宿主机的 Docker

/root/.kube 目录,我们将这个目录挂载到容器的 /root/.kube 目录下面这是为了让我们能够在 Pod 的容器中能够使用 kubectl 工具来访问我们的 Kubernetes 集群

比原教程多加了如下两个挂载目录,原因是Harbor V2版本后必须使用https进行操作,https默认只支持域名方式访问,如果要到达构建的image成功推送到Harbor必须满足Harbor客户端的要求,所以挂载了宿主机的如下目录

/etc/docker 该目录下有certs.d/域名/ca.crt 是连接harbor认证文件所在路径

/etc/hosts 该文件有Harbor的域名和ip映射关系

这里是官网的手册

既然是流水线就会有分阶段执行,在jenkins流水线配置界面提供两种执行jenkins流水线脚本的方法

使用Pipeline script 时直接粘贴脚本内容

使用Pipeline script from SCM 时脚本内容写到代码项目根目录的jenkinsfile文件中

image.png

我这里选择Pipeline script from SCM方式

具体项目的流水线的执行无外乎分为以下几个阶段

Clone 代码 -> 代码静态分析 -> 单元测试 -> Maven 打包 -> Docker 镜像构建/推送 -> Helm 更新服务。

要构建 Docker 镜像,就需要提供镜像的名称和 tag,要推送到 Harbor 仓库,就需要提供登录的用户名和密码,所以我们这里使用到了 withCredentials方法,在dockerfile里面可以提供一个 credentialsIdockerhub的认证信息,如下:

stage('构建 Docker 镜像') {

  withCredentials([[$class: 'UsernamePasswordMultiBinding',

    credentialsId: 'dockerhbr',

    usernameVariable: 'DOCKER_HUB_USER',

    passwordVariable: 'DOCKER_HUB_PASSWORD']]) {

      container('docker') {
        echo "3. 构建 Docker 镜像阶段"
        sh """
          docker login ${dockerRegistryUrl} -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD}
          docker build -t ${image}:${imageTag} .
          docker push ${image}:${imageTag}
          """
      }
  }
}

其中 ${image} 和 ${imageTag} 我们可以在上面定义成全局变量

def imageTag = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()

def dockerRegistryUrl = "registry.qikqiak.com"

def imageEndpoint = "course/polling-app-server"

def image = "${dockerRegistryUrl}/${imageEndpoint}"

docker 的用户名和密码信息则需要通过 凭据来进行添加,进入 jenkins 首页 -> 左侧菜单 凭据 -> 添加凭据,选择用户名和密码类型的,其中 ID 一定要和上面的 credentialsI的值保持一致:

image.png

出现的问题1:

image.png

第三阶段失败,查看输出信息如下:

[Pipeline] { (运行 Kubectl)
[Pipeline] container
[Pipeline] {
[Pipeline] echo
查看 K8S 集群 Pod 列表
[Pipeline] sh
+ kubectl get pods
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:default" cannot list resource "pods" in API group "" in the namespace "default"
[Pipeline] }
[Pipeline] // container
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] }
[Pipeline] // podTemplate
[Pipeline] End of Pipeline
ERROR: script returned exit code 1
Finished: FAILURE

错误原因分析

Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:default" cannot list resource "pods" in API group "" in the namespace "default"

来自服务器的错误(禁止):禁止使用pod:用户“ system:serviceaccount:default:default”无法在名称空间“ default”的API组“”中列出资源“ pods”

好像权限方面的问题

解决方案

这里文章中提供的解决方案是

kubectl create clusterrolebinding kube-system-default-admin \
  --clusterrole=cluster-admin \
  --serviceaccount=kube-system:default

重启POD后发现没有上述报错

Deployment中的密码使用Secret

连接数据库部分为了账号密码安全问题使用secret环境变量方式

先根据代码中配置文件连接mysql的变量值创建连接mysql的环境变量

配置文件中关于mysql部分的定义

#### MySQL runtime ####
runtimeDatabase=${RUNTIME_DB}
jdbc.username=${JDBC_USERNAME}
jdbc.password=${JDBC_PASSWORD}
jdbc.driver=${JDBC_DRIVER}
jdbc.URL=${JDBC_URL}

创建环境变量

echo 'com.mysql.cj.jdbc.Driversolojdbc:mysql://192.168.19.126:3306/solo?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTCr' > JDBC_URL

echo 'solo' > JDBC_USERNAME

echo 'solo' > JDBC_PASSWORD

echo 'com.mysql.cj.jdbc.Driver' > JDBC_DRIVER

echo 'mysql' > RUNTIME_DB

根据自己情况做调整

创建Secret

kubectl create secret generic mysqlsecret --from-file=RUNTIME_DB --from-file=JDBC_USERNAME --from-file=JDBC_PASSWO

jenkins ci 提示docker权限的问题

将jenkins用户加入root组

/etc/sudoers
%wheel	ALL=(ALL)	ALL  #取消前面的注释
jenkins ALL=(ALL) NOPASSWD: ALL

修改 /etc/passwd 将用户UID修改为0

jenkins:x:0:990:Jenkins Automation Server:/var/lib/jenkins:/bin/false

jenkins ci 过程 docker push 提示

denied: requested access to the resource is denied

检查以上权限设置harbor仓库地址是否正确

jenkins ci Helm提示 unknown flag: --ca-file

unknown flag: --ca-file

image.png

解决 需要加 sudo

image.png


标题:基于 Jenkins、Gitlab、Harbor和 Kubernetes 的 CI/CD
作者:zifuy
地址:https://www.zifuy.cn/articles/2019/09/09/1568019952351.html