Drone CI 入坑指北
Drone CI 概念图
Drone CI 概念图

入坑 Drone CI

如果你不了解 Drone CI 不妨看看下面这个使用场景:

我有一个日记本,之前使用 Typecho 这个 CMS 来写日记,但是书写体验不太好(没有自动保存功能,写完提交时页面卡住等等问题),丢了两次日志后,我打算切换到 Jekyll 来写,它们文档的格式都使用 markdown,所以迁移起来很方便。

由于我之前使用了 Gitea 作为个人的代码仓库。所以我的日记被我放到了 Gitea 上。

jekyll 需要将写好的 markdown 文档来编译出 html 文件,再由 nginx 提供网页服务。那么问题来了:在仓库中只有 markdown 文档,除非每次写完日记手动执行一下 bundle exec jekyll build 然后同步到 nginx 服务器的指定目录下,否则看日记时只能看 markdown ,而且还要先把仓库中所有的日记下载下来才能看。这太不优雅了!

这时引入一个 CI/CD 流程就可以解决这个问题。从上面这个场景可以很明显地看出:Drone CI 就是帮助我们完成编译与部署过程的。

这类技术有一些名词,例如 CI/CD 以及 DevOps 等,它们的详细定义,请参考维基百科,这里就不过多赘述了。

Drone CI Alternate

除了 Drone CI 外,类似的实现还有许多,例如出名的 Github CI,Circle CI,Argon CI 等等,但我的需求是开源、本地部署、能够适配现有的代码仓库以及简单好用。

值得一提的是 Gitea 有自己的 CI,支持 Gitea Runners,可以兼容 Github CI,也就是说可以把现有的 Github CI 的 pipeline 直接抄来用。但是由于其处于 Alpha 阶段,还不稳定,所以我并没有使用它。

Deploy

Drone CI 由两大部分组成:

  • Drone Server
    • Server 负责与代码仓库通信,获得代码更新状态,并给 runners 下达指令。
  • Drone Runners
    • Runners 负责编译、部署等等过程。
    • 根据需求的不同,有不同的 Runner。例如需要使用 docker 可以使用 docker runner, 需要不同的处理器架构,也需要不同的 runner。

也就是说 Runners 可以有多个,而 Server 只有一个。这一点从单词的单、复数形式也可看出。本文中将说明 docker runner 的使用。

至于详细的部署流程请参考官方文档,对于 Gitea,部署完后,如果你想测试部署是否成功,则可以在仓库的 设置 -> Web钩子 -> 点开指定的钩子-> 最下方的测试推送。就可以了解 Drone Server 与 Gitea 的连接状态。

Drone CI Pipeline

Pipeline 是编译与部署的流程图,Runners 将根据 Pipeline 中的内容执行对应的步骤。你需要在需要使用 CI 的仓库中添加 .drone.yml 文件,并在该文件内编写 pipeline。

由于docker提供了环境隔离,便于测试、部署,你甚至可以把 runner 和 server 部署在同一台服务器上。下文中将以 docker pipeline 为主进行说明,其它类型的 pipeline 也大同小异。如果你还不了解 docker 请先学习 docker 再继续阅读。

docker pipeline 的执行将由如下几步组成:

  1. clone 当前仓库的所有内容到一个临时Volume中,它将被挂载到每一步的容器中
  2. 拉取当前步骤需要的容器镜像
  3. 运行容器,并执行pipline中的指令
  4. 以此 2和3 的方式执行一个或多个步骤
  5. 结束运行,停止并删除容器

以上步骤均在 Runner 中执行,Runner 将使用宿主机的docker 环境,所以会修改运行环境,因此不建议在生产服务器上运行 Runner,而是使用专用的服务器。

值得一提的是:如果你的服务器性能不错,或某些步骤对服务器压力不大,且步骤间没有依赖关系,但耗时较长。你可以将多个上述任务并行,以缩短构建时间。

第一个 Pipeline

文中将以构建自己的 debian 镜像为例子来写一个 pipeline:

由于网络原因,如果你经常使用 docker 镜像进行测试,你会发现更新、下载软件的速度十分缓慢,因此我希望在官方提供的镜像上更换更新源,所以我写了下面这个dockerfile:

FROM debian:12

RUN rm -rf /etc/apt/sources.list.d/debian.sources \
    && touch /etc/apt/sources.list \
    && echo "deb http://mirrors.bfsu.edu.cn/debian/ bookworm main contrib non-free non-free-firmware" >> /etc/apt/sources.list \
    && echo "deb http://mirrors.bfsu.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list \
    && echo "deb http://mirrors.bfsu.edu.cn/debian/ bookworm-backports main contrib non-free non-free-firmware" >> /etc/apt/sources.list \
    && echo "deb http://mirrors.bfsu.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list \
    && apt -y update \
    && apt -y install apt-transport-https ca-certificates \
    && sed -i 's/http:/https:/g' /etc/apt/sources.list

RUN apt -y update \
    && apt -y upgrade

上面的文件保存在仓库的 debian/Dockerfile 。下面我需要使用 pipeline 来实现 docker image 的自动构建:

---
kind: pipeline
type: docker
name: debian-builder

steps:
  - name: build-debian
    image: plugins/docker
    settings:
      build_args:
        - HTTP_PROXY=http://192.168.1.10:10801
        - HTTPS_PROXY=http://192.168.1.10:10801
      registry: example.registry.com
      repo: example.registry.com/user/debian
      username:
        from_secret: docker_username
      password:
        from_secret: docker_password
      tags:
        - latest
        - 12
        - bookworm
      dockerfile: ./debian/Dockerfile

可以看到这怎么和前面说的步骤不一样,这个pipeline中并没有什么 linux 命令,它需要干什么呢?其实对于这种常见的操作(制作docker镜像、同步、release),你不需要手动编写 pipeline,在插件市场中可以找到对应的插件,你只需要填写对应的参数即可。

下面是一些需要说明的参数

step

首先这个 pipeline 只有一个 step,这一 step 包含了制作镜像并 push 到镜像服务器 example.registry.com

proxy

我使用了 build_args 这将会让构建过程的网络通信通过代理服务器,如果你的网络连接受限,不妨加入代理服务器,这个参数也可以用于其它 step 中。

secret

对于密码肯定不能直接写在 .drone.yml 中。这会让你的密码暴漏在所有能访问代码仓库的人。 Drone 提供了两种解决方案:

  • 使用 Drone 自己的密码管理功能
  • 使用 HashiCorp Vault

这里就不讲 Vault 的集成了,直接使用 Drone 的密码管理:

由于我需要推送到私有源,所以需要用户名和密码,对于这些插件来说,你需要打开 Drone 的管理界面,点开对应的项目,在 Settings 有一个 secret 选项用来存这类信息,例如用户名、密码、token等等。

create secret
创建 secret

secret 以键值对的形式存储,Drone 对值基本不做限制,根据需要,值可以是:

  • 一个数字、字母密钥
  • 一个包含密钥的 json

键则是:

  • 在 pipeline 中使用的名字

对于这个示例 pipeline 我有两个密钥,分别是 docker_usernamedocker_password 他们的值对应我在私有源的用户名和密码。

这样我的密码就被安全地存好了,不会被别人看到。即使后续需要修改键值对,也不会直接看到原来的密码。

左侧下方的 Organization 中也有一个 secret,与上面那个不同的是,上方的 secret 仅限于该项目使用,下方组织(团队)里的 secret,则可以在多个项目中共用。如果你有 Vault 那么也是同样的道理,相对与这个简单的密码管理来说, Vault 功能要多得多。

以上就是一个基础的 pipeline 文件,它根据 Dockerfile,构建镜像、标记 tag 并使用用户名和密码完成认证,上传到私有源中。

Docker in Docker

在完成对 pipeline 的初步了解后,首先需要知道这个插件本身就是一个容器,你给它设置好环境变量,它会根据这些变量工作。现在,再猜想一下上方的插件是怎么做到构建镜像的。

很明显,在容器(插件)内部,我们还要依赖一个docker来构建镜像。如果只是一个没有 docker 的 linux 镜像是没办法完成上述操作的。

实现

Docker in Docker 简称 dind,指的是在容器中使用 docker 的概念。我以简单通俗的方式来说一下实现这个目标两种方法:

Nested

嵌套方式,容器以特权模式运行,在容器内部虚拟出来一个docker容器,类似于嵌套虚拟化

  • 优点:
    • 内部容器完全隔离
    • 不污染宿主机环境
  • 缺点:
    • 运行速度慢,不建议用于编译任务
    • 无法挂载主机资源
    • 无法使用 docker volume
    • 需要特权模式,安全性存在问题

docker.sock

直接将主机的 docker.sock 传递到容器内,容器使用它来创建新的容器,新容器和这个容器之间是平级的关系,你可以在宿主机上看到新容器。

执行任务的仍是宿主及的 docker。实际上这不叫 docker in docker 而是 docker outside of docker。但是它与 dind 的目标是一样的。

  • 优点:
    • 相对于嵌套方式,有更高的性能
    • 可以使用宿主机上的资源
  • 缺点:
    • 污染宿主机环境
    • 共享 sock 使安全性降低
    • 错误的挂载和操作会造成系统损坏

Plan B

由上可见,这两个方式各有各的问题我更倾向于使用 docker.sock 的方法。

除此之外,要实现相同的目标,其实可以使用一个虚拟机 runner 或 shell runner,直接在系统上运行docker,这样既不会污染环境,也不会降低速度。但是,你需要更多的设备并且能够对虚拟机、物理及进行生命周期的管理。使用起来更快捷、安全,但管理麻烦且需要更多设备。

本文以 docker runner 为主,这个内容就不在这里进行讨论了。

Nested DinD Pipeline

为了说明更多内容,我们来看看嵌套的 dind 的 pipeline 是什么样子的:

---
kind: pipeline
name: default

steps:
- name: test
  image: docker:dind
  volumes:
  - name: dockersock
    path: /var/run
  commands:
  - sleep 5 # give docker enough time to start
  - docker ps -a

services:
- name: docker
  image: docker:dind
  privileged: true
  volumes:
  - name: dockersock
    path: /var/run

volumes:
- name: dockersock
  temp: {}

请在创建一个空的仓库,运行上面这个 pipeline。注意:这个 pipeline 中使用了特权容器,你需要在 drone 的设置中的 Project Settings 打开 Trust 选项,信任这个仓库,这样 drone 才会运行 privilege 容器。

services

这里多出的 services 字段在 Drone 中作为为 step 中的步骤提供服务的容器,每一个 service 都作为一个单独的容器启动,来为 steps 中的步骤提供服务。它的使用场景主要用于测试步骤:如果你的程序需要使用 redis 数据库,或其它容器提供的服务,在这里可以很方便地运行。本处也是使用 service 的例子。

这里使用 privileged: true 来启动特权容器,为 step 提供 dind 环境。

volumes

volumes 是卷组,在 Drone 中有两种卷:

  • 共享主机文件的卷组(类似于 docker bind volume)用于访问宿主机的文件
  • 临时卷(类似于 docker named volume)临时卷只在执行 Pipeline 时存在,执行完后会销毁

这里有一个 dockersock 卷组,它是一个临时卷,它用来共享 service 中的 dind 容器下的 /var/run 来达到通过 sock 控制 dind 中的 docker 的目的,从而在 dind service 容器中创建容器。

那么我们 step 中的容器和这个 service 容器是平级的关系,你可以在宿主机上看到这两个容器。step 容器通过操作 service 的 sock 来在 service 容器中创建容器,创建的容器属于嵌套的方式,在宿主机上看不到。

该 pipeline 的 docker ps 输出将会是空的,原因是 service 容器中没有任何容器。

commands

commands 用于执行指令,你可以根据需要把用到的指令写在这里,这里的指令不像 Dockerfile 中的 RUN 每一各run会多一层。所以放心地写吧,需要多条命令就换行,不用使用 && 了。

image

这里是容器要使用的镜像,这也是我喜欢 docker runner 的一点。编译环境不用每次都搭建,只需要搭建一次,做成镜像,或者将该镜像的构建放入 CI/CD 工作流中。对于一些程序,直接使用对应的公共镜像就可以。

Drone CI plugins/docker

Drone 的开发团队在 dockerhub 注册了用户名为 plugins 的用户,所以 image 才是 plugins/docker 不得不说这个想法非常牛,这使得在 .drone.yml 的可读性非常好。

docker 插件使用的是 docker outside of docker,即直接使用 docker.sock。

docker 插件有一个特性:为了安全考虑,禁用了 volume,如果你试图在这个地方挂载 volume 那么这就会造成 docker.sock 无法访问。

但是这给我们带来了很多不变,例如最终容器需要前一个步骤(例如编译)得到的结果,但是你又没办法共享卷组,下面是这类问题的解决方法。

使用外部服务

构建一个简单的暂存文件的服务,例如使用 sftp,http(s) 这种基础的协议来传输文件,将某一步的结果上传至暂存服务器,再在打包最终镜像时下载下来。

Multi-stage

充分利用 Dockerfile 的功能来实现编译、构建镜像,具体参考 Multi-stage builds。在非必要的情况下,我更推荐这种方法。这会使得 pipeline 更加简单、清晰。

多任务并行

如果需要多个 steps 那么来构建多个 docker 镜像,那么这些 steps 会依次执行,构建完一个再构建下一个。对于构建一些不需要编译操作的 docker 镜像我喜欢让多个构建过程并行。

可以在 .drone.yml 中书写多个 pipeline 来实现这个功能即:

---
kind: pipeline
type: docker
name: alpine-builder

steps:
  - name: build
    ...
    ...
---
kind: pipeline
type: docker
name: debian-builder

steps:
  - name: build
    ...
    ...

这样就在执行时就会实现并行构建,对于其它需要并行操作的 steps 也可以使用相同方法。

Triggers

Drone 对于所有操作,都是默认执行的,例如:对于上面这些 pipeline,只要仓库有变化,例如发生 release、commit、pull request 等等,也包含定时任务,Drone 会执行 pipeline 流程。

如果你想做一个当发生 pull request 时触发的 CI/CD 流程,你可以在 pipeline 的下方加入:

trigger:
  event:
  - pull_request
  action:
  - opened

通过 triggers 可以在同一个 .drone.yml 文件中包含多个 pipeline(类似于多任务并行的写法),但是使用 triggers 限制 pipeline 的执行,对不同行为做不同的操作。

还有一个比较有用的功能就是 cron job, 在 Drone 的 web 页面,点击对应的仓库 settings -> Cron job 即可简单得创建定时任务,填上任务名字,执行分支和间隔,这样你可以实现诸如:Nightly 版本、每月更新等等,但是请注意,不要和 .drone.yml 内的 trigger 产生冲突。

更详细的 triggers 请参考官方文档

小结

对于构建一个自定义的镜像,和写一些基础的 pipeline 需要掌握的内容差不多就是这样。

还记得文章开头的 jekyll 使用的问题吗?这是我的解决方法:

  • 我的服务器每日拉取 ruby 镜像,构建出包含 jekyll 的镜像,并推送镜像到 Gitea
  • 当 jekyll 仓库更新,触发 CI 工作流
    • 拉取每日构建的镜像
    • 使用 jekyll 将 markdown 转换为 html
  • CD 部份很简单,使用 rsync 上传到 nginx 服务器,增量更新

我总是使用最新的 Gemfile 和 Gemfile.lock 避免因为 ruby 版本升级造成版本不匹配而不可编译问题。这样就构建出了一个完整的 CI/CD 工作流,我只负责写日记就行。不过,上面这个流程还有值得优化的地方:例如使用 jekyll 的增量编译。

通过使用 CI/CD ,我的仓库里只有 markdown 日记,几个 html 页面,没有 Gemfile 和 Gemfile.lock 等文件。代码仓库只会因为有了新的日志而发生变化。

当然,CI/CD 的目的不仅仅在于使用 jekyll 写日记,这只是本文为说明工作流程的例子而已。你还可以通过各种操作、插件,实现代码仓库的更新,例如使用:git-push 插件对代码库进行更新,ansible 插件实现集群部署。如果插件满足不了你的需求,你也可以自己写 pipeline。

CI/CD 只是 Devops 中的一部分,本文也只是大致说明了 Drone CI 的使用。还有更多的内容需要去学习,请参考其它的文档。不妨写一个 pipeline 练练手吧!

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
隐藏
变装