之前已经用 Docker 和 Docker-compose 搭建了 Drone 的持续集成/部署环境

这次我们将 Drone 部署到 Kubernetes 上。

Drone 介绍

在官方介绍中 Drone 介绍自己是一个现代化的持续集成和持续交付平台,是面向繁忙的开发团队的自助式持续交付平台。其实就是想表达 Drone 无需过多复杂的配置,就可以轻松部署和搭建测试环境。

但尝试使用 Drone 会发现,网络上大部分关于 Drone 的教程都是面向老版本的(v0.8),而现在的版本又与这个老版本有很大出入。所以我们就用截至现在最新版本的 drone 搭建集成测试环境,并部署到 k8s 上。

Drone 主要分为两个部分:

Drone Server

Drone Server 是一个 Drone 的控制管理平台,用于查看现在正在运行中的集成测试结果、终止测试或跳转源码。

Drone 支持大部分主流代码托管平台(Github, Bitbucket, Gitlab),同时也支持自建平台(Gogs, Gitea)。

Drone Runner

Drone Runner 负责执行实际测试,官方提供了适用于各种运行环境的 Runner。今天要用的就是其中的 Kubernetes Runner。

在 Kubernetes 上部署 Drone

首先你得有个可以被外网访问的 k8s 集群,至于怎么弄,请自行解决。

部署 Drone Server

这一步操作的最终目标是将 drone/drone:1.7.0 这个 Docker 镜像部署到 k8s 、暴露出接口并绑定域名(方便 webhook 发送请求)。

为了达成这个目的我们要创建一个运行 Drone 容器的 Deployment,暴露这个 Deployment 的 Service,以及用于反向代理的 Ingress。配置文件存在 ConfigMap 中。

所以我们首先需要一个 Ingress 做一个域名解析,内容如下:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: drone-ingress
  namespace: devops
spec:
  rules:
  - host: example.com
    http:
      paths:
      - backend:
          serviceName: drone-service
          servicePort: 80
        path: /

我们的所有操作将在 devops 这个命名空间里进行,替换自己的域名,代理指向 drone-service 这个服务。

接下来创建 drone-service 服务:

apiVersion: v1
kind: Service
metadata:
  name: drone-service
  namespace: devops
spec:
  selector:
    app: drone-server
  ports:
  - name: http
    protocol: TCP
    port: 8080
    targetPort: 80

drone-service 这个服务将使用 drone-server 这个 Deployment,targetPort 将 80 端口暴露给 Ingress,port 的 8080 端口则是为连接 Drone Runner 准备的内部端口。

最后就是 drone-server,用于运行 Drone Server 容器。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: drone-server
  namespace: devops
spec:
  selector:
    matchLabels:
      app: drone-server
  replicas: 1
  template:
    metadata:
      labels:
        app: drone-server
    spec:
      nodeSelector:
        node-pool: devops
      containers:
      - image: drone/drone:1.7.0
        imagePullPolicy: Always
        name: drone-server
        volumeMounts:
        - name: drone-server-sqlite-db
          mountPath: /var/lib/drone
        envFrom:
        - configMapRef:
            name: drone-env
      volumes:
      - name: drone-server-sqlite-db
        hostPath:
          path: /rc/data/drone

nodeSelector 请根据使用的 k8s 情况自行修改,Drone Server 镜像使用最新的 1.7.0,volume 使用节点的本地卷(重启后会丢失所有数据,所以需要的话可以挂在到其他永久卷中)。

这里最关键的一点就是 envFrom 环境变量,这里面定义了 Drone Server 所需要一些重要参数。以下内容以 github 为例:

apiVersion: v1
kind: ConfigMap
metadata:
  name: drone-env
  namespace: devops
data:
  DRONE_GITHUB_SERVER: https://github.com
  DRONE_GITHUB_CLIENT_ID: <github-client-id>
  DRONE_GITHUB_CLIENT_SECRET: <github-client-secret>
  DRONE_SERVER_HOST: <example.com>
  DRONE_SERVER_PROTO: http
  DRONE_RPC_SECRET: <rpc-secret>
  DRONE_LOGS_TRACE: 'true'

其中,DRONE_SERVER_HOSTDRONE_SERVER_PROTODRONE_RPC_SECRET 最为关键。

分别是你的 Drone Server 的域名,以及与 Drone Runner 通讯的 secret,其中 secret 可以存储在 k8s secret 里,再次为了方便演示直接写入配置文件中。

Kubernetes Runner

搭建好 Drone Server 后我们要创建 Drone Runner,并连接 Drone Server。

上一个教程不同,这次我们要用到 drone/drone-runner-kube 这个官方镜像。Kubernetes Runner 不同于 Docker runner,他会创建 pod,在新建的 pod 上执行完 pipeline 后销毁掉它们。

因为 Kubernetes Runner 并不需要向外暴露自己的服务,所以我们只需要一个 Deployment。内容如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kube-runner
  namespace: devops
  labels:
    app.kubernetes.io/name: kube-runner
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: kube-runner
  template:
    metadata:
      labels:
        app.kubernetes.io/name: kube-runner
    spec:
      nodeSelector:
        node-pool: dev
      containers:
      - name: runner
        image: drone/drone-runner-kube:1.0.0-beta.2
        ports:
        - containerPort: 3000
        envFrom:
        - configMapRef:
            name: kube-runner-env

与 Drone Server 一样,我们要自行替换 nodeSelector ,尴尬的是截至今天 drone_runner_kube 最新版本是 _1.0.0-beta.2,我已我们直接使用它。

环境变量如下:

apiVersion: v1
kind: ConfigMap
metadata:
  name: kube-runner-env
  namespace: devops
data:
  DRONE_RPC_PROTO: http
  DRONE_RPC_HOST: drone-service.devops:8080
  DRONE_RPC_SECRET: <rpc-secret>
  DRONE_NAMESPACE_DEFAULT: devops
  DRONE_RUNNER_CAPACITY: '2'
  DRONE_RUNNER_NAME: kube-runner

其中, DRONE_RPC_HOST 要指向上面创建的 drone-service 地址,DRONE_RPC_SECRET 要与 drone-env 中的一致。

Kubernetes Runner 还有一个很重的东西就是 ServiceAccount,因为 Kubernetes Runner 需要创建新的 pod 执行 pipeline 然后销毁他们,所以它需要一个角色赋予他权限。

我们的 Drone 角色如下:

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: devops
  name: kube-runner
rules:
- apiGroups:
  - ""
  resources:
  - secrets
  verbs:
  - create
  - delete
- apiGroups:
  - ""
  resources:
  - pods
  - pods/log
  verbs:
  - get
  - create
  - delete
  - list
  - watch
  - update

创建好角色后就要将角色绑定到我们的 ServiceAcount:

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: kube-runner
  namespace: devops
subjects:
- kind: ServiceAccount
  name: default
  namespace: devops
roleRef:
  kind: Role
  name: kube-runner
  apiGroup: rbac.authorization.k8s.io

Monorepo

现在很多项目都会采用一种 Monorepo 的组织方式,就是一个库里面有好几个项目。这时候我们希望 Drone 只执行当前目录下的项目而不是其他的。官方为我们提供了一个解决方案: drone-tree-config。感兴趣可以看一下。

总结

Drone 给我们带来了简单快速的持续集成部署的方案,在繁忙的工作中可以不必盯着繁杂的配置项苦恼。如果你不想用 Java 系那些强大而笨重的解决方案,想去使用更符合 go 语言风格、更贴近容器化思维的解决方案,那么 Drone 是一个不错的选择。