用Drone配置CI流程

发布于 更新于
能够完全受自己掌控的、且轻量级的CI,还是很有用处的

简介

Drone是一个非常轻量的、独立运行的CI组件。

它由Go语言编写,所以运行环境非常清爽迅捷。虽然这种行事风格让我有种很亲切的感觉,可是它并不是CNCF的成员。

它仅仅只做了CI这一件事情,因此它可以很轻易地接入Github、Gitlab、Gitea等主流代码托管服务。组件运行时全部容器化(再加上Go语言天生的编译优势)所以部署也十分简单。比起我之前尝试过的 Gitlab-CI、Github-Actions、自建简易CI服务几种方案来说,Drone在综合使用体验上来说是最好的。

它的官网文档质量很高,想要找的东西都能很快地从官方文档中找到。实际运行体验,有一些小坑,但是依靠我的运维经验都能比较顺利地解决,就是费了不少时间来调试。

它提供了一个插件中心,提供了许多常见的能力,以docker镜像的形式进行封装因此很容易使用。

宏观架构

Drone主要分为两种组件运行时,

一种是Server,一个系统中只需要有一个它就可以了,它有如下几个作用:

  • 向Runner分配CI任务
  • 任务列表管理、日志查看、设置等操作
  • 负责与代码托管平台进行事件交互(接受webhook回调)

出于第三点原因,为了能让外部的代码托管平台正确地进行webhook回调,因此这个Server组件最好放置在公网域名下(同时为了安全也要配置HTTPS),否则就会导致失去“自动”能力。

另一种是Runner,它负责执行实际的CI任务(也就是各种构建动作),是属于高负载应用,因此一个系统中根据业务规模可能需要许多个Runner来支持需求,甚至接入k8s进行动态扩容。

以我个人博客的改造过程举例,我的架构设计中需要三个Drone容器来支持我所需的CI/CD流程,如下:

  1. 一个Server,配置在线上服务器上并配有三级子域名。
  2. (至少)一个构建用Runner,可以跑在任意机器上(公司、家里、台式机、笔记本),只要连上Server即可开始干活(就像外包一样)。
  3. 一个生产部署用Runner,跑在线上服务器上并接入线上k8s环境,负责拉取已经构建好的镜像并命令k8s执行滚动更新操作。

接下来简单记录一下我的改造过程。

k8s部署Server

它的官方文档只介绍了Docker的部署方式,但想要接入线上的k8s环境也是非常容易的,自己写点yaml配置就可以了。由于它实质上只是一个简单的web服务,因此部署在Docker与k8s两种方式并没有实质上的区别。

它的Server是只能指定一种代码托管平台的,例如GitHub,而且指定之后就直接以该平台的OAuth2方式来认证登录,没有账号密码注册环节。这点让我一开始有点摸不着头脑,不过很快还是发现它的好处——我可以不用费脑筋再记一套密码了。

在众多环境变量中,特别需要注意的是DRONE_USER_FILTER这一项,它用来限制哪些用户可以注册登入Drone-Server来进行操作。在Github平台,它就是github账号用户名。这点很重要,一定要配置!

另外需要注意安全的是DRONE_GITHUB_CLIENT_SECRET这一串token,出于安全考虑可以使用k8s的secret资源来进行管理。

运行过程中还可能会提示『Invalid port configuration』,参考,为此还需要再配置DRONE_SERVER_PORT=:80这个环境变量以避免Drone自行检测它所属的端口导致运行异常。

还要指定DRONE_USER_CREATE环境变量,将你的账号设置为管理员。(Drone的账号管理都是基于用户名的,稍微有些诡异,但实际运行下来也没啥问题)

在k8s中为它创建一个Deployment,为了实现单例模式需要指定strategy: Recreate,同时需要挂载一个持久化的Volume,然后暴露containerPort: 80容器端口,再创建一个Service来将容器端口暴露到服务端口targetPort: 80上去,就可以接入线上的边缘入口服务器(nginx)了。

上线Server

经过前面步骤,Server已经作为服务在k8s集群内部启动了,接下来需要确保外部连通性。

首先需要申请一个域名证书,在腾讯云控制台上进行申请,自动DNS验证,只过了几分钟就签发成功了,效率还是很高的。

下载证书后将其放入前端nginx镜像中,并且修改nginx的反向代理配置,部署。

然后还要去腾讯云控制台配置DNS,新增一条A记录指向我们的线上nginx服务器。

稍等一会让DNS生效,然后就可以在外部网络中通过域名访问到Server的网页啦。

点击『Continue』会跳转到Github进行OAuth2授权,授权成功再跳转回来,注册登录流程就此完成。

Docker部署Runner

之前已经说过了,Runner理论上可以运行在任何环境下,只需要确保它与Server(以及代码托管平台)之间的连通性即可。所以在开发环境的场景下,一种典型的运用方式就是把Runner部署在内网的开发机上,而且以Docker形式部署,无状态运行,可以抛弃掉所有的运维负担。

不过也不能算是严格的“无状态”,毕竟运行了一次CI我们还是需要看到它的运行结果的,无论是日志(存在Server上)、IM消息(发送到钉钉/飞书机器人)、还是输出构建产物(npm、pypi、docker-registry),总归是有输出的,不过这种输出依赖的都是公共服务,是可以完全脱离本地宿主机器的依赖的。

输出:IM消息

官方提供了支持 slack (国外常见的聊天/办公协作工具)的消息机器人插件drone-slack,同时在插件库中也有支持钉钉机器人的插件drone-dingtalk-message(虽然它的代码几乎是完全从slack那个仓库里抄过来的)。

但是,有点难受的是,没有飞书机器人版本的。于是我自己手撸了一个插件drone-feishu,做得好的是在原仓库的基础上改造了代码架构以更方便定制化消息内容,做得不好的是我还在偷懒没有把它打包发布到docker-registry上去、以及暂时仅支持富文本格式消息。

输出:构建产物

对于开源项目来说,最经典的构建方式是将构建产物发布到某个公共平台(npm、pypi等)上。具体到技术上来说,就是在Runner容器内部打包,然后将产物通过网络传输出去,随后容器可以自由销毁。

另一种典型方式是构建成docker镜像,然后推送到镜像仓库(registry)上。既然需要打包镜像,那么也就需要在容器内部访问docker客户端了,因此需要借助dind技术,其实理解了它的概念之后也挺简单的。

不过要访问Docker的话,需要挂载外部的docker.sock文件进到容器内部,这样容器内部就获得了对Docker的完整控制权。这个是有风险性的,因此只能对受信任的代码仓库才使用这种方式。这一步还需要以管理员账号在 Drone Server 的网页端进行设置才可以生效。

部署镜像

既然前面用dind打包镜像的时候已经访问到Docker了,那么自然也有能力运行docker run命令了,这样也就很自然地实现了”部署”的能力。

通过同样的方式,如果我将.kube/config文件挂进容器内部,那我也可以在容器内部操作外部的kubectl,也就是在容器内部实现了k8s部署动作。(这一步需要修改config文件的访问权限,安全性请自行评估,最好的方式还是借助k8s的Role机制创建专用账户来操作)

指定运行机器

Runner在启动的时候,可以通过DRONE_LIMIT_REPOS环境变量来限定它只会执行哪些仓库的CI任务;

同时在代码仓库里也可以通过node:条件来指定运行任务的Runner 。

两种方式组合,就能得到 仓库-Runner 的稳固的绑定关系。

k8s部署Runner

参考官方文档,它的核心运作原理是依赖k8s提供的集群内认证机制,这需要用到我之前没接触过的k8sRole资源。而且这种方式也被它自己标记为『Beta』状态。

再从原理上分析,把Runner接入k8s中,获得的只是k8s的集群调度能力;而对于CI流程来说,这样的能力似乎怎么想都是多余的。

所以我先暂时不在这块深究了。直接用Docker来实现我的需求即可,这应该是最优解。