All Articles

使用Drone进行CI支持

1. 前言

Drone是一个Golang技术栈的CI解决方案,功能和Jenkins之类的CI工具类似。

优点:

  • Golang编写,分发及搭建非常容易(镜像体积很小)
  • Golang编写,运行时占用资源极小(特指和Java栈的一系列工具比较)
  • 构建运行时以镜像优先,保证在不同平台的构建结果一致
  • 优秀的设计,支持插件化提供强大的功能支持
  • 现代化的UI,开箱即用,甩开Jenkins之类有历史包袱的工具几条街

缺点:

  • 较年轻,文档工作需要强化
  • 插件的数量和质量,以及插件的文档支持需要强化
  • 功能尚不能说强大,和Jenkins之类的老牌CI相比差距较大

简单选择:

  • Drone:
    • 小型团队,对新技术适应能力较强的团队
    • 对功能的丰富性要求并不是很高,但要求性能强大功能稳定
  • Jenkins:
    • 对功能的丰富性要求较高,要求任何情况任何需求都能快速应对
    • 运维人员对Jenkins非常熟悉可以很快上手使用

相关资源:

2. 概念

2.1 pipeline

  • pipeline是drone的核心概念
  • pipeline一般负责完成一个任务:构建、发布、部署,其内部会有多个行为共同组成一个pipeline
  • pipeline在drone里是进行分类的,按类型有多种种类,常用的有:

2.2 step

  • pipeline里的每一个操作被称为step
  • 组织多个step可以完成一个特定的pipeline,e.g
    • 构建pipeline:clone、安装依赖、单元测试、打包、制作镜像、推送镜像

2.3 workspace

  • 构建运行的工作空间,在不同的环境下其位置会有所不同
  • Posix系统(含docker容器),工作空间在:/tmp/drone-${RANDOM}/drone/src
  • OSX系统,工作空间在:/private/var/folders/...
  • Win系统,工作空间在:C:\windows\drone-%RANDOM%\drone\src
  • 有一点要强调下,如果在bash脚本中访问当前pipeline的~$HOME,获得的地址就是workspace的根目录,而不是当前runner的用户home
  • 此外,workspace是临时的,会在pipeline创建的时候产生,并在完成的时候被销毁

2.4 trigger

  • trigger决定当前的pipeline是否要触发
  • trigger有很多类型:
    • 根据分支:e.g 是否dev分支
    • 根据事件:e.g 是否tag事件(注意,tag和分支trigger不能同时存在,因为tag是不区分分支的)
    • 根据reference:e.g refs/tags/**
    • 根据代码库:e.g octocat/hello-world
    • 根据runner实例:e.g drone.instance1.com
    • 根据状态触发:e.g failure

2.5 condition

  • condition决定当前的step是否要触发
  • condition有很多类型:
    • 根据分支:e.g 是否dev分支
    • 根据事件:e.g 是否tag事件(注意,tag和分支condition不能同时存在,因为tag是不区分分支的)
    • 根据reference:e.g refs/tags/**
    • 根据代码库:e.g octocat/hello-world
    • 根据runner实例:e.g drone.instance1.com
    • 根据状态触发:e.g failure

3. 架构 & Multiple Pipeline & Parallelism

3.1 架构

Drone这个CI其实很简单,也说不上什么架构。一般Drone会有一个单点Server,在git工具上注册好webhook接收事件,然后启动多个Runner来处理Server上产生的任务。这些Runner可以在同一台主机上,也可以分散在多台不同的主机上,官方文档的指引中要求不要将runner和server放在同一台主机上。

3.2 Multiple Pipeline

官方说明:

Drone supports configuring and orchestrating multiple pipelines. This is useful when you need to fan-out and distribute your build tasks across multiple machines to reduce build times, or to execute your build tasks across multiple platforms (e.g. amd64 and arm64).

用途1:

在处理大型项目任务的时候,将任务拆散,分散到不同的物理机上进行并行处理,加速整个构建进程。如果是不同主机的话,需要用到2.4提到的pipeline trigger,将不同的pipeline设定好自己的运行目标主机。保证同一个repo在不同主机上触发的时候,只有当前主机(runner)应该执行的pipeline得到执行。

用途2:

确定pipeline之间的先后顺序,通过在pipeline一级定义depends_on来申明相互之间的依赖关系。这里特别需要注意的是:pipeline之间是不共享workspace的。所以每一步都需要单独做类似于依赖安装之类的工作,这需要特别小心。

---
kind: pipeline
# ...

---
kind: pipeline
name: p3

steps:
  - name: notify
  # ...

depends_on:
  - p1
  - p2

3.3 Parallelism

这部分内容与 Multiple Pipeline 的关系,和之前讲过的 trigger 与 condition 的关系一样。Multiple Pipeline 是pipeline级别的,而Parallelism则是step级别的,本质上仍旧是并行运行。

kind: pipeline
# ...

steps:
# ...

- name: s3
  commands:
    - # ...
  depends_on:
    - s1
    - s2

4. 使用

4.1 Server:安装 & 运行

4.1.1 OAuth2准备

需要在git repo软件上申请一个OAuth应用。这里我使用的是Gitea,就拿它来做演示:

点击:右上角头像 > 设置(下拉菜单) > 应用(页面中间),打开的页面是管理用户级别的Access TokensOAuth2应用的。

找到:创建新的 OAuth2 应用程序这一栏,输入:

  • 应用名称:这名字后面用不到,填可理解的内容即可
  • 重定向 URI:http://${drone_host}:${drone_ip}/login

点击创建应用

在打开的页面上显示了OAuth2应用的相关信息,这里需要记下:

  • 客户端ID
  • 客户端秘钥

4.1.2 启动drone

Golang应用程序,使用上最容易的方式仍旧是镜像:drone/drone

drone服务器启动时的环境变量文档在:Drone Documentation > Installation > Reference

$ docker run -it -d \
    --name drone-server \
    -p 18980:80 \
    --log-driver=json-file \
    --log-opt max-size=512m \
    -v /tmp/drone/data:/data \ # sqlite db file
    -e DRONE_LOGS_DEBUG=true \
    -e DRONE_LOGS_COLOR=true \
    -e DRONE_LOGS_PRETTY=true \
    -e DRONE_LOGS_TRACE=true \
    -e DRONE_AGENTS_ENABLED=true \                                              # drone server running with no agents, start runners manually
    -e DRONE_GITEA_SKIP_VERIFY=true \                                           # gitea: use http protocol
    -e DRONE_GITEA_SERVER=http://host.docker.internal:13000 \                   # gitea: server address with protocol
    -e DRONE_GITEA_CLIENT_ID=fd023edb-7976-4d50-a92f-b16612683240 \             # gitea: oauth client id
    -e DRONE_GITEA_CLIENT_SECRET=S4Q6PktE3dKNHPUzZrTdyNJsTThwal4doUWf6jf4eRA= \ # gitea: oauth client secret 
    -e DRONE_RPC_SECRET=d9856af41ffe31f5e8025be020e981be \                      # drone: runner shall connect server with this secret
    -e DRONE_SERVER_HOST=host.docker.internal:18980 \                           # drone: server address without protocol
    -e DRONE_SERVER_PROTO=http \                                                # drone: server with http protocol
    drone/drone:1.6.3

4.1.3 OAuth2授权

访问drone的地址:http://host.docker.internal:18980,会跳转到gitea进行授权,授权完成后会跳转回drone。这时应该就已经以刚才制作OAuth2应用的用户身份登录drone了。

这里说明下,因为gitea和drone都运行在容器内,他们之间相互访问是通过容器名来进行的,而host主机访问这两者都是通过loopback地址,两者之间存在差异,会导致OAuth的过程失败。因此这里统一使用docker MAC desktop版本的host.docker.internal域名来贯通内部和外部。但需要额外修改host主机的/etc/hosts配置,把这个host解析为loopback地址。

4.1.4 UI同步 & 激活代码库

刚完成授权的账户是没有任何代码库的,打开drone首页发现是空列表(即便当前通过OAuth2授权的账号在gitea内有代码库):

点击右上角的SYNC就会开始和gitea同步账户的代码库数据,完成后即可获得所有当前账号在gitea内所拥有的代码库列表:

这时候账号下的drone项目都是未激活状态,需要点击面板上项目右边的ACTIVATE进行激活:

点击SAVE保存即可。经过这几个操作,drone即激活了对这几个代码库的webhook事件监听。

至此为止drone本体的安装基本上完成了,后续还需要配置下runner。

4.1.5 API秘钥

点击右上角的用户头像,然后点击User settings会打开用户的设置页面。在这个页面上能找到用户的token。如果有需要使用drone API的话(或者是cli命令行工具),这个token是必须的。

4.2 Runner:安装 & 运行

4.2.1 安装

服务器设置完成后,如果没有单独安装和运行runner,服务器上所有触发的事件都会是Pending状态,而且没有任何日志。

关于Pending的debug,可以看下官方的一篇帖子:Builds are Stuck in Pending Status

Runner的安装见官方文档:Drone Documentation > Installation > Runners。Runner也分不同的类型,一般常用的是:

Runner的类型和pipeline的类型并不要求一致,runner只是pipeline的运行者,不管是什么类型的pipeline,都可以在任何类型的runner上运作。举例来说,一个docker类型的pipeline,在exec runner上就会在主机上启动docker容器并运行,而docker runner则是直接在docker容器内运行。

4.2.2 运行

Exec Runner
Exec Runner需要手动在~/.drone-runner-exec/config创建一个配置文件:

DRONE_DEBUG=true
DRONE_LOG_FILE=/tmp/drone-runner-exec.txt
DRONE_LOG_FILE_MAX_SIZE=50
DRONE_RPC_DUMP_HTTP=true
DRONE_RPC_DUMP_HTTP_BODY=true
DRONE_RPC_PROTO=http
DRONE_RPC_HOST=host.docker.internal:18980
DRONE_RPC_SECRET=d9856af41ffe31f5e8025be020e981be
DRONE_RUNNER_PATH=$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

更多的配置项可以查阅官方文档:Exec Runner > Installation > Configuration Reference

配置文件里最重要的是DRONE_RPC_HOST``DRONE_RPC_SECRET这两项。修改完成后记得重启runner:

$ drone-runner-exec service stop && drone-runner-exec service start

Runner在执行pipeline操作时候的用户即是runner启动时的用户,这需要非常小心,否则会搞错用户导致权限错误。此外,在2.3 workspace的时候也提到过了,在runner执行pipeline的时候,~指向的是workspace的根目录,而不是用户的home,这点要非常小心。

举例来说,存放在$HOME/.docker下的凭证在exec runner运行的时候是不可访问的(~会定位到workspace而不是$HOME),这会导致私有registry的访问失败。同样的,pipeline运行时的PATH也不同于用户自身的PATH,需要在配置文件中设置好DRONE_RUNNER_PATH

Docker Runner
这部分就非常简单了,无非就是一个docker容器,直接使用对应的官方镜像即可:drone/drone-runner-docker

相关配置项可以查阅官方文档:Docker Runner > Installation > Configuration Reference

4.3 UI秘钥设置

在执行pipeline的时候不可避免需要访问一些密码秘钥之类的信息,而构建过程中,理论上runner只能访问代码库里的内容。这就造成了问题,因为代码库并不是安全的存放秘钥的地方。针对这样的问题,drone有对应的解决方案。

在drone ui仓库的设置中,有一处:

设置完成后,就会保存在drone系统中:

后续在pipeline可以如下的形式进行访问:

kind: pipeline
name: default

steps:
- name: build
  image: alpine
  environment:
    USERNAME:
      from_secret: docker_username
    PASSWORD:
      from_secret: docker_password

更多关于secret的设置及访问,可以参见官方文档:Drone Documentation > Configuration > Secrets。在这方面,drone支持得非常全面,甚至外部的加密设施也可以支持访问。

4.4 私有Registry

在pipeline应用的过程中,比较常见的问题是对于私有registry的访问。毕竟现在的CI基本上都围绕着镜像打转,抓取镜像进行部署或者构建镜像向上推送都是常见需求。

官方对此也有解决方案:How to pull private images with 1.0

4.5 部署 & 多点事件触发

上述的内容基本上把CI的最基础的应用都讲到了,如果只是简单的提交代码 > 触发webhook > drone pipeline这个流程的话,是没有问题的。

那么接下来就可以考虑下稍微复杂点的需求:

  • 项目的构建和部署需要分开
  • 构建定义为一个pipeline
  • 部署定义为一个pipeline
  • 部署pipeline应该由构建pipeline异步触发
  • 部署pipeline应该和构建pipeline解耦,两者可能发生在完全隔离的两个物理机的runner上

然后我简单说下结论:在当前的版本(drone/drone:1.6.3),这个需求做不到,如果是按官方的API和功能的话。而使用一些非官方的hacking思路,则可以做到基本上一致的效果

根据一开始说的,如果单独只是一个构建pipeline,很简单就能做到。而如果要实现刚才列出的需求,就需要在构建pipeline完成并退出之前触发一个额外的事件,只要能触发额外的事件,再配合上:trigger & condition,保证代码库的pipeline启动的时候,只有部署pipeline进入工作,且只有正确的runner实例进入工作,那一切就通了。

那么,能不能手动触发一个部署呢?研究了下官方的资料,发现正常途径基本上做不到,只有一些非正常的思路才可以做到。下面说下思路。

4.5.1 drone api | drone cli

首先想到的是看看官方的API中有没有可用的命令(cli实际上就是官方API的封装):API文档CLI文档

发现官方有一个命令:

$ drone build promote --help
  NAME:
     drone build promote - promote a build
  
  USAGE:
     drone build promote [command options] <repo/name> <build> <environment>
  
  OPTIONS:
     --param value, -p value  custom parameters to be injected into the job environment. Format: KEY=value
     --format value           format output (default: "...")

结合.drone.yml使用:

kind: pipeline
type: exec
name: test

platform:
  os: darwin
  arch: amd64

steps:
  - name: test
    commands:
      - echo "tested"
    when:
      event:
        - tag

---
kind: pipeline
type: exec
name: build

platform:
  os: darwin
  arch: amd64

steps:
  - name: build
    commands:
      - echo "built"
    when:
      event:
        - tag

depends_on:
  - test

---
kind: pipeline
type: exec
name: notify

platform:
  os: darwin
  arch: amd64

steps:
  - name: notify
    commands:
      - drone build promote fullstack/gateway 100 production
    environment:
      DRONE_SERVER: http://host.docker.internal:18980
      DRONE_TOKEN: K5i8g6PMlLM3tWaPDRagigPkBrl1YKGu
    when:
      event:
        - tag

depends_on:
  - build

---
kind: pipeline
type: exec
name: deploy

platform:
  os: darwin
  arch: amd64

steps:
  - name: deploy
    commands:
      - echo "deployed"
    when:
      event:
        - promote
      target:
        - production

测试后发现,如果build参数给的是已经存在的构建的话,则会重复之前的构建流程;而如果给的是一个很大的不存在该build的值(比如上面举例的100),则构建在notify这一步失败,并报错:

+ drone build promote fullstack/gateway 100 production
client error 404: {"message":"sql: no rows in result set"}

查找了下相关资料,并没有找到官方文档,仅在论坛中找到几篇讨论:

根据官方人员的解释:

When you deploy, you are essentially “promoting” a build, which means you need to provide the build number you are promoting, and the environment you are promoting to. For example:

drone deploy octocat/hello-world 42 production

In the above example, you are promoting build 42 to production. This will execute a deployment event in the system with environment name production.

也就是说你只能对已经存在的build进行触发让这个已经测试构建完成的build进行部署,而不能从头创建一个新的build。也就是说,上述的工具能大致满足上面提出的那串需求,但本质上还是不同的:

  • 测试构建等必须是一个单独的行为,需要先行做完,并形成一个完成的build
  • 需要手动触发(因为你需要确认build号,所以这个行为必然是手动的,不可能自动化)这个完成的build,让yaml中配置为promote的event触发,进行部署

4.5.2 plugin

官方有一个插件叫:Downstream Build。乍看之下貌似是满足需求的,但我实际扒了下源码,发现这个插件的本质其实就是封装了下官方的API。所以实际上和4.5.1一样,不行。

4.5.3 webhook plugin

后续的思路是尝试使用webhook插件,在构建pipeline完成之后用webhook触发一个构建:Webhook

文档工作总是半吊子,参数有几个根本不知道具体应该填什么,只能随便尝试下:

$ docker run --rm \
          -e PLUGIN_URLS=http://host.docker.internal:18980/hook \
          -e PLUGIN_USERNAME=root \
          -e PLUGIN_PASSWORD=Abcd1234_ \
          -e PLUGIN_DEBUG=true \
          -e PLUGIN_SKIP_VERIFY=true \
          -e DRONE_REPO_OWNER=fullstack \
          -e DRONE_REPO_NAME=gateway \
          -e DRONE_COMMIT_SHA=2087d78f48 \
          -e DRONE_COMMIT_BRANCH=master \
          -e DRONE_BUILD_EVENT=deployment \
          plugins/webhook:latest
Webhook 1
  URL: http://host.docker.internal:18980/hook
  METHOD: POST
  HEADERS: map[Authorization:[Basic cm9vdDpBYmNkMTIzNF8=] Content-Type:[application/json]]
  REQUEST BODY: {"repo":{"owner":"fullstack","name":"gateway"},"build":{"tag":"","event":"deployment","number":0,"commit":"2087d78f48","ref":"refs/heads/master","branch":"master","author":"","message":"","status":"success","link":"","started":0,"created":0}}

  RESPONSE STATUS: 200 OK
  RESPONSE BODY:

返回结果是200,正常,但结果本身为。查看drone的UI发现实际上没有任何build被触发。这就很无奈了,这里的问题可能出在:

  • 操作者:给的参数不正确
  • webhook插件:没有发送正确的webhook请求
  • drone服务器:忽略了不正确的webhook请求

无论如何,后续如果要继续的话就需要看源码了,插件的以及服务器的源码。

4.5.4 webhook fake

到这里就是非正常思路了。

因为gitea可以正常触发drone的webhook,思路就是使用curl仿造gitea的请求,发送到drone。其实drone官方也有对webhook的描述:How to use Global Webhooks,但实在是没什么正式的支持,API之类的易用性的工具都没有,只有这篇文章。

gitea官方有webhook相关的文档:Webhooks

gitea的钩子可以在这里找到,点击下面的链接可以查看请求细节:

当然这也非常麻烦,而且gitea发送出去的webhook请求里有非常多repo详细信息,如果要做到自动化的话,这块的获取也是很麻烦的一件事情。

4.5.5 waiting hack

另一个非正常思路就是定义一个pipeline,仅在指定的runner上运行,然后命令中执行脚本,查询私有registry,看目标仓库(镜像)的目标tag是否存在,存在的话就进行部署。而在其他的runner上进行构建和镜像推送操作。

这样就能做到一开始提的需求,虽然恶心了点。

EOF

Published 2019/12/23

Some tech & personal blog posts