服务器迁徙大作战(三):基于Drone搭建持续集成环境

4,702 阅读6分钟

继之前完成了服务器硬件搭建、内公网打通后,开始着手将之前基于 github 的流水线迁移到内网环境中。

持续集成/持续部署(Continuous Integration/Continuous Deployment,简称 CI/CD)是一种软件开发实践,旨在通过自动化构建、测试和部署流程来加快软件交付速度、降低错误率,并增强团队协作效率。以下是 CI/CD 的主要概念和流程:

  1. 持续集成(Continuous Integration,CI)
    • 指开发人员频繁将代码集成到共享存储库中,并通过自动化构建和测试流程来验证代码的质量。
    • CI 系统会自动触发构建、运行单元测试、静态代码分析等操作,以确保新代码的质量。
    • 目的是尽早发现和解决代码集成问题,以减少后续集成时的冲突和错误。
  2. 持续部署(Continuous Deployment,CD)
    • 指自动化部署已通过测试的代码到生产环境,以实现快速、可靠的软件交付。
    • CD 流程包括自动化构建部署流程、自动化测试、环境配置管理等步骤,以确保部署的软件质量和稳定性。
    • 目的是缩短软件交付周期,降低部署风险,提高部署的可重复性和一致性。

目前业内开源的不在少数,诸如 CircleCI、GitLab、Travis CI、鼎鼎大名的 Jenkins、以及后起之秀 Drone,基于这几种方案,简单做了下对比。


JenkinsDroneGitLabCircleCITravis CI
收费开源免费开源离线部署免费
提供商业收费版本
免费的自托管版本以及付费的托管服务提供免费套餐和付费套餐提供免费套餐和付费套餐
资源消耗云平台作业云平台作业
配置文件jenkins file.drone.yml.gitlab-ci.yml未实践未实践
私有化部署支持支持支持不支持不支持
开发语言JavaGoRuby On Rails未知未知
插件生态插件丰富插件有限,但够用有限未实践未实践
学习曲线☆☆☆☆☆☆☆☆☆☆☆未实践未实践

Jenkins 主要是因为运行慢,耗资源太高,因此踢出了方案选择,其余的几款云平台的方案,担心后续的收费问题也被踢出了备选,最后选择了Go语言的 Drone。资源消耗小,配置文件简单,关键是支持完整的离线部署,插件虽然有限,但基本够用,在不得已的情况下也能自己开发插件。

本次实践最后需要达到的实际效果(之前已经搭建了简易版的 K8S 环境,所以打算直接把 Drone 丢进了集群中。):

Drone 介绍

Dron 是一个现代化的持续集成平台,它使用强大的云原生pipeline引擎自动化构建、测试和发布工作流。Drone 与多个源代码管理系统无缝集成,包括 GitHub、GitLab、Gitee 和 Gitea 等;它的每个构建都在一个隔离的 Docker 容器中运行;另外它也支持插件,可以使用你熟知的语言轻松的扩展它们。

环境搭建

Drone 提供了集成多种 Serve 类型,原计划是直接采用 Github,但由于国内访问 Github 速度堪忧(服务器上未处理代理),故最后采用了 Gitee。

准备 OAuth

创建一个Gitee OAuth application。Consumer Key和 Consumer Secret用于授权访问Gitee资源(图粘贴自官网)。

备注:应用回调地址必须以 login 结尾,否则登录授权后无法重定向到首页。
image.png

创建共享密钥

创建一个共享密钥来验证跑步者和中央无人机服务器之间的通信,随便定义即可,也可以用 openssl 生成共享机密。

$ openssl rand -hex 16
bea26a2221fd8090ea38720fc445eca6

Drone 服务部署

本文以 K8S 集群部署为例,官网 中有 Docker 的部署方式,可以 移步参考

# 配置信息
apiVersion: v1
kind: ConfigMap
metadata:
  name: drone-config
  namespace: drone
data:
  DRONE_GITEE_CLIENT_ID: '上述截图中的 Client ID'
  DRONE_GITEE_CLIENT_SECRET: '上述截图中的 Client Secret'
  DRONE_GITEE_SERVER: 'https://gitee.com' 
  DRONE_GITEE_API_SERVER: 'https://gitee.com/api/v5'
  DRONE_SERVER_PROTO: 'http'
  DRONE_SERVER_HOST: 'http://192.168.3.26:5080' # Drone 最后部署的地址
  DRONE_RPC_SECRET: '上步骤中生成的共享密钥'
  DRONE_NAMESPACE_DEFAULT: 'drone'
  DRONE_USER_CREATE: 'username:你的Gitee用户名,admin:true' ## 非常重要,配置哪个gitee 用户具备超级管理权限

---
# 定义Drone 专用的PV
apiVersion: v1
kind: PersistentVolume
metadata:
  name: drone-pv-10gi 
  labels:
    type: local
spec:
  storageClassName: manual-drone
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce # 卷可以被一个节点以读写方式挂载
  hostPath:
    path: "/mnt/data"
---

# pvc
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  # pvc名称
  name: drone-pvc
  namespace: drone
  labels:
    type: local
spec:
  # 读写权限
  accessModes:
    - ReadWriteOnce
  # 使用的存储类
  storageClassName: manual-drone
  # 定义容量
  resources:
    requests:
      storage: 10Gi
---

# service 部署
apiVersion: v1
kind: Service
metadata:
  name: drone-service
  namespace: drone
spec:
  selector:
    app: drone
  type: LoadBalancer ## 因为是单节点, 因此直接采用了 LoadBalancer
  ports:
    - name: http
      protocol: TCP
      port: 5080
      targetPort: 80
    - name: https
      protocol: TCP
      port: 5443
      targetPort: 443

---
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: drone-deployment
  namespace: drone
spec:
  replicas: 1
  selector:
    matchLabels:
      app: drone
  template:
    metadata:
      labels:
        app: drone
    spec:
      containers:
        - name: drone
          image: drone/drone:2
          volumeMounts:
            - name: drone-data
              mountPath: /data
          envFrom:
            - configMapRef:
                name: drone-config
          ports:
            - containerPort: 80
            - containerPort: 443
          resources:
            limits:
              cpu: 3000m
              memory: 2048Mi
            requests:
              cpu: 150m
              memory: 512Mi
      volumes:
        - name: drone-data
          persistentVolumeClaim:
            claimName: drone-pvc

直接执行部署

kubectl apply -f deployment.yaml

查看 pod情况, Drone 已经在运行中。
image.png

Drone Runner 部署

个人推荐 Docker 形式的 Runner,还是以 K8S 方式部署。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: drone-runner
  namespace: drone
  labels:
    app.kubernetes.io/name: drone-runner
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: drone-runner
  template:
    metadata:
      labels:
        app.kubernetes.io/name: drone-runner
    spec:
      containers:
        - name: drone-runner
          image: drone/drone-runner-kube:latest
          ports:
            - containerPort: 3000
          env:
            - name: DRONE_RPC_HOST
              #value: drone-service.drone.svc.cluster.local  # Drone Server地址
              value: http://192.168.3.26:5080  # Drone Server地址
            - name: DRONE_RPC_PROTO
              value: http
            - name: DRONE_RPC_SECRET
              valueFrom:
                configMapKeyRef:
                  name: drone-config
                  key: DRONE_RPC_SECRET # Drone Server部署时候填写的secret共享密
            - name: DRONE_NAMESPACE_DEFAULT
              valueFrom:
                configMapKeyRef:
                  name: drone-config
                  key: DRONE_NAMESPACE_DEFAULT # Drone Server部署时候填写的secret共享密


---
apiVersion: v1
kind: Service
metadata:
  name: drone-runner-service
  namespace: drone
spec:
  type: LoadBalancer
  selector:
    app: drone-runner
  ports:
    - protocol: TCP
      port: 5300
      targetPort: 3000

直接执行部署

kubectl apply -f runner.yaml

查看 pod情况, Runner 已经在运行中。
image.png

等 Drone Server 和 Runner 完成启动后访问机器Ip+端口, 就看到类似的页面。
image.png

流水线配置

服务完成启动成功后就能配置流水线了,本文以一个 NodeJs 的服务为例,描述如何编写流水线的每个步骤(构建镜像、上传镜像、更新服务、飞书群通知)。
image.png
image.png

DockerFile准备

DockerFile的定义相对简单,可以自行编写,此处是一个简单的示例:

#!/bin/bash
FROM node:16.18.0-alpine
MAINTAINER Marvin
ENV TIME_ZONE Asia/Shanghai
RUN mkdir -p /app  ## 执行构建产物的拷贝
COPY ./war/ /app/war
WORKDIR /app
EXPOSE 10001
CMD ["node", "/app/war/index.js" ]

.drone.yml准备

.drone.yml 是 Drone 的规范文件,采用 yml 格式,用来描述流水线步骤。

构建产物

因为是 node 的服务,避免每次构建都从远程下载依赖,所以在流水线中增加了 node_modules 的缓存能力,流水线中通过 volumes 映射宿主文件地址。

kind: pipeline
type: kubernetes
name: default

volumes:
  - name: node_modules
    host:
      path: /home/marvin/cicd/drone/node/node_modules ## 宿主机中的地址,地址可以自己定义
  - name: cache
    host:
      path: /home/marvin/cicd/drone/node/pnpm_cache

steps:
- name: build
  image: node:18.20.3-alpine
  pull: if-not-exists
  volumes:
    - name: cache
      path: /drone/src/.pnpm-store
  commands: ## 此处自己定义项目工程的构建命令
  - npm config set registry https://registry.npmmirror.com/ ## 设置淘宝镜像
  - npm install -g pnpm
  - pnpm config set registry https://registry.npmmirror.com/
  - pnpm config set store-dir .pnpm-store
  - pnpm i
  - pnpm run build:min ## 构建命令,定义在 package.json, 有开发的基础的应该都能理解
  when: ## 定义在什么情况下触发构建任务
    event:
    - push 
    - tag
    branch: master #定义在哪个分支触发构建任务

镜像构建

此步骤主要用于将上个步骤中构建的产物打包成镜像并且上传到自己的镜像仓库,此步骤中采用了 Drone 的插件 Docker, 可以移步查看其它的插件

- name: image
  depends_on: [build] ## 定义这个步骤依赖哪些步骤
  image: plugins/docker
  privileged: true
  pull: if-not-exists
  settings:
    registry: registry.cn-hangzhou.aliyuncs.com
    repo: registry.cn-hangzhou.aliyuncs.com/XXXXXXxxx # 需要上传的镜像仓库地址
    use_cache: true
    username:  # 我使用了阿里云的镜像仓库
      from_secret: aliyun_docker_username
    password:
      from_secret: aliyun_docker_password
    tags:
      - ${DRONE_COMMIT_SHA:0:7} # 直接截取了commit的7位hash值作为镜像的tag
  when:
    event:
      - push
      - tag
    branch: master

服务更新

在上个步骤中完成了镜像打包并上传到了镜像仓库,之后就是要将集群中的服务升级到最新的版本(CD 的环节)此步骤中采用了 Drone 的插件 dron8s, 可以移步查看其它的插件

- name: deploy
  image: ghcr.io/bh90210/dron8s:latest
  pull: if-not-exists
  depends_on: [build-notice]
  settings:
    yaml: ./deploy/deploy.yaml ## k8s 更新服务的ymal 文件
    image_tag: ${DRONE_COMMIT_SHA:0:7}
    kubeconfig:
      from_secret: kubeconfig # kube 的配置文件
  when:
    event:
      - push
      - tag
    branch: master

以下是示例的deploy.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
    labels:
        app: gpt-user-server
    name: gpt-user-server
    namespace: gpt
spec:
    replicas: 1
    selector:
        matchLabels:
            app: gpt-user-server
    template:
        metadata:
            labels:
                app: gpt-user-server
        spec:
            containers:
                  image: registry.cn-hangzhou.aliyuncs.com/xxxxxxxx:{{.image_tag}} ## 镜像地址,用双大括号代表变量
                  imagePullPolicy: IfNotPresent
                  name: gpt-user-server
                  ports:
                      - containerPort: 80
            imagePullSecrets:
                - name: aliyun-register-pwd

---
apiVersion: v1
kind: Service
metadata:
    labels:
        app: gpt-user-server
    name: gpt-user-server-service
    namespace: gpt
spec:
    type: NodePort
    ports:
        - name: gpt-user-server-port
          port: 80
          protocol: TCP
          targetPort: 80
    selector:
        app: gpt-user-server

飞书通知

按照一般的流程,打包成功、服务更新成功,都需要通知对应的人员,通知的方式非常多,诸如钉钉、飞书、邮件,笔者计划采用飞书群通知。

Drone 没有提供飞书通知类似的插件,应该我自己采用 Go 语言魔改了一个,感兴趣的移步仓库地址

- name: deploy-notice
  depends_on: [deploy]
  image: registry.cn-hangzhou.aliyuncs.com/xxxxxxxxxxx/drone-plugin-feishu:4.0
  pull: if-not-exists
  when:
    status:
      - success
      - failure
  settings:
    messagetype: DEPLOY
    dockergroup:
      from_secret: docker_image_group
    webhook:
      from_secret: feishu_notice_webhook   ## 通知的飞书的 webhook 具体如何获取可以参考飞书的文档
    secret:
      from_secret: feishu_notice_secret ## 飞书应用的密钥
    debug: true

仓库激活

以上准备完毕后进入后台管理界面,首先同步仓库,找到需要配置流水线的仓库,进入详情页点击激活仓库按钮。完成激活后在设置tab 打开 Trusted 开关。

  • 同步仓库

image.png

  • 激活仓库(背后的逻辑:Drone 调用了 Gitee 的 OpenApi 给对应的仓库绑定了webhook)。

image.png

  • 开启仓库为信任的(如果不开启这个配置,可能会导致任务执行过程中 代码clone 失败)。

image.png

总结

上述完成后基本就完成了大部分的配置工作,仓库本地修改代码后 push 远程 master 分支后即可会触发构建任务。
image.png
image.png

Drone 内部的调用也非常简洁,Drone Server 接收到 Gitee 的 WebHook 调用请求后会分配构建任务给可用的 Runner,默认 Runner 会拉取代码(当然开发也可以自定义这个步骤),代码克隆完毕后开始执行开发者通过 .drone.yml 定义的步骤,以下以笔者使用的流水线为例:

备注

基本的 CICD 流程虽然实现了,但其中还是有很多弄得优化细节,感兴趣的同学可以在评论区一起讨论。

  1. 镜像构建时如何做缓存,构建的镜像如何裁剪?
  2. 服务升级后如何自动删除之前的镜像?
  3. 其他好玩的插件也可以推荐推荐。