使用 gitea + drone + registry 实现持续交付

使用自动化流程来执行测试、构建、发布代码,已成为当今主流。流行的解决方案有 github/gitlab + jenkins/travis + nexus3/docker hub。上述服务,要么服务器部署在国外,使用起来要忍受缓慢而不稳定的网络。要么是公有服务,不能很好满足私密要求。又或者部署起来需要消耗很多硬件资源。

docker run -d --name jenkins jenkins
docker run -d --name nexus sonatype/nexus3
docker run -d --name gitlab --hostname gitlab.example.com gitlab/gitlab-ce:latest
docker stats --all --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"

即使只是启动,不接受服务,等待启动完成后也可以发现它们占用了很多资源。

NAME                CPU %               MEM USAGE / LIMIT     MEM %
jenkins             0.09%               852.9MiB / 7.671GiB   10.86%
nexus               0.94%               948.7MiB / 7.671GiB   12.08%
gitlab              45.71%              1.989GiB / 7.671GiB   25.93%

当然上面说到的问题,根源在于钱没有充够。但是,对于小微初创企业或者个人来说,每一份资源都很宝贵,用几千兆的内存交付几十兆的服务,实在是得不偿失。如果我们希望以最小的代价实现持续交付,gitea + drone + registry 才是最佳方案。即使是使用了一段时间之后,消耗的资源也比老牌的服务少很多,并且运维成本相当甚至更小。

NAME                CPU %               MEM USAGE / LIMIT     MEM %
drone               0.01%               12.26MiB / 7.671GiB   0.16%
registry            0.00%               5.066MiB / 7.671GiB   0.06%
runner              0.00%               5MiB / 7.671GiB       0.06%
gitea               0.93%               119.1MiB / 7.671GiB   1.52%
nginx               0.00%               2.836MiB / 7.671GiB   0.04%

tony_ma

工作流

为了将代码打包成 docker image,并部署到目标主机,需要经历一系列步骤:

  1. 推送代码或者 git tag 到 gitea
  2. gitea 通过 webhook 通知 drone 代码仓库有更新
  3. drone 拉取项目代码,根据代码中的 .drone.yml 将任务分派给 drone runner 执行
  4. runner 执行 .drone.yml 中的步骤,编译出二进制执行文件,与运行资源一并打包成镜像
  5. 推送镜像到 registry
  6. ssh 到目标服务器,从 registry 拉取镜像运行

网络拓扑

docker 大法好,为了隔离环境(配置坏了错了重头来过),我们所有的服务都跑在 docker 容器里。同时,registry 要求必须通过 https 访问,并且几个服务需要向外提供网页服务,所以我们使用 nginx 绑定宿主主机的 80、443 端口。

如果发现 docker 网络连接比较慢,可以考虑更换 docker 源。

通过 docker network create cicd 创建一个名为 cicd 的虚拟网络,将 nginx 容器接收到的请求转发到其他容器。如果某个容器需要提供其他例如 ssh 服务,则单独绑定宿主接口即可。如果某个服务是直接部署到宿主主机,可以通过 ip addr show docker0 查询宿主的 ip。

一个部署完成的持续集成网络,在部署了一个 blog 服务和一个 blog_dev 服务后,整个网络拓扑如下(根据你的实际情况调整):

$ docker network ls
NAME                DRIVER              SCOPE
blog                bridge              local
cicd                bridge              local
dev                 bridge              local
nginx               bridge              local

$ docker ps -a --format 'table {{.Names}}\t{{.Networks}}\t{{.Ports}}'
NAMES               NETWORKS            PORTS
nginx               nginx               0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
blog                blog,nginx          8080/tcp
mysql_blog          blog                33060/tcp, 0.0.0.0:5001->3306/tcp
blog_dev            dev,nginx           8080/tcp
mysql_dev           dev                 33060/tcp, 0.0.0.0:5000->3306/tcp
registry            cicd,nginx          5000/tcp
drone               cicd,nginx          80/tcp, 443/tcp
runner              cicd                3000/tcp
gitea               cicd,nginx          3000/tcp, 0.0.0.0:9000->22/tcp

nginx

首先我们需要准备 ssl 证书和配置文件,创建一个 nginx 文件夹,目录结构如下:

mkdir -p nginx/certs
mkdir -p nginx/conf
tree nginx

# 目录结构应该如下
nginx
├── certs
└── conf

将 ssl 证书复制到 nginx/certs 文件夹内,将以下内容复制到 nginx/conf/my.conf 文件中。

server {
    listen 443 ssl;
    server_name git.nullsfootprints.com;
    ssl_certificate /certs/git.nullsfootprints.com.crt;
    ssl_certificate_key /certs/git.nullsfootprints.com.key;
    location / {
        proxy_pass http://gitea:3000;
    }
}

server {
    listen 443 ssl;
    server_name drone.nullsfootprints.com;
    ssl_certificate /certs/drone.nullsfootprints.com.crt;
    ssl_certificate_key /certs/drone.nullsfootprints.com.key;
    location / {
        proxy_pass http://drone;
    }
}

server {
    listen 443 ssl;
    server_name registry.nullsfootprints.com;
    ssl_certificate /certs/registry.nullsfootprints.com.crt;
    ssl_certificate_key /certs/registry.nullsfootprints.com.key;
    location ~  {
        client_max_body_size 0;
        proxy_pass_header Server;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Scheme $scheme;
        proxy_pass https://registry:5000;
    }
}

上述配置文件设置了 gitea/drone/registry 的转发规则,其中 registry 是必须的,应该根据你的实际情况修改。一切准备好后,应该有如下的文件结构:

# tree nginx

nginx
├── certs
│   ├── drone.nullsfootprints.com.crt
│   ├── drone.nullsfootprints.com.key
│   ├── git.nullsfootprints.com.crt
│   ├── git.nullsfootprints.com.key
│   ├── registry.nullsfootprints.com.crt
│   ├── registry.nullsfootprints.com.key
│   ├── www.nullsfootprints.com.crt
│   └── www.nullsfootprints.com.key
└── conf
    └── my.conf

最后使用配置和证书,启动一个 nginx 容器,绑定宿主的 80\443 端口,加入 cicd 虚拟网络。

docker run -d \
-v /home/ubuntu/nginx/conf:/etc/nginx/conf.d \
-v /home/ubuntu/nginx/certs:/certs \
-p 80:80 \
-p 443:443 \
--network nginx \
--name nginx \
--restart always \
nginx

使用 docker logs nginx -f 查看容器日志,如果没有发现错误,即表示正确启动。

gitea

gitea 负责托管代码,支持通过 ssh 或者 http 访问,所以我们绑定宿主的 9000 端口用于 ssh 访问,https 访问由 nginx 转发即可。

mkdir gitea

docker run -d \
-v /home/ubuntu/gitea:/data \
-p 9000:22 \
--network cicd \
--name gitea \
--restart always \
gitea/gitea

# 加入 nginx 网络使得 nginx 容器可以将请求转发至 gitea
docker network connect nginx gitea

请注意,我们启动 nginx 的配置文件中,转发位置写的是 proxy_pass http://gitea:3000;,这是因为两个容器都加入了同一个名为 cicd 的虚拟网络中,同一个网络中的容器可以通过彼此的名字访问,详细说明可以参考docker 官方文档

gitea 容器启动之后,根据你在 nginx 中的配置,打开 gitea 前端页面,点击任何按钮,会自动重定向到初始化页面。

gitea install

如果希望自己用或者采用邀请制,在初始配置页面,“服务器和第三方服务设置”部分,勾选 “禁止用户自助注册”,然后在“管理员账号设置”中创建管理员账号即可。

如果想要通过 ssh 克隆仓库,一般设置里面 SSH 服务域名填上外部访问的域名,例如 git.nullsfootprints.com,SSH 服务端口填上创建容器时候绑定的 9000 端口。

需要注意的是,虽然我们的 gitea 不允许用户自主注册,但是如果仓库是公开的,则外部是可以看得到的。这个是 gitea 和 gitlab 的区别,gitea 的定位更接近 github。如果希望进行权限控制,则可以将仓库的可见性设置为私有。

当 gitea 初始化完成后,为了让 drone 有权限访问我们的代码仓库,需要在 gitea 中创建 OAuth2 应用。 在 https://您的域名.com/user/settings/applications 填入应用名和回调地址即可获得 ClientID 和 Client Secret,用于后续启动 drone 的时候使用。回调地址是部署的 drone 地址 + /login格式。

gitea oauth2

最后创建需要持续集成的代码仓库即可。

drone

drone 是我们的主角,负责自动化执行任务,可以分为两部分

  1. provider 负责接收 gitea 发送的 webhook 请求,拉取git代码
  2. runner 负责实际执行任务

provider

provider 支持各大主流的 git 服务,drone 推荐使用 gitea,以便能够有更好兼容性和额外的特性。如果你希望使用现有的或者其他 git 服务,可以参考官方文档

我们的 git 服务是 gitea,启动 provider镜像需要提供几个参数:

  1. DRONE_GITEA_SERVER gitea 服务器地址
  2. DRONE_GITEA_CLIENT_ID 前文在 gitea 中创建的 oauth2 client id
  3. DRONE_GITEA_CLIENT_SECRET 前文在 gitea 中创建的 oauth2 client secret
  4. DRONE_RPC_SECRET provider 与 runner 通讯时用到的密钥,随机生成即可
  5. DRONE_SERVER_HOST provider 所在的域名或者 ip+port
  6. DRONE_SERVER_PROTO provider 所用的协议,http 或者 https

运行以下命令即可启动镜像,需要将环境变量换成你实际的数据。我们没有绑定宿主主机的端口,是因为已经在 nginx 容器中做转发。

docker run -d \
--volume=/var/lib/drone:/data \
--env=DRONE_GITEA_SERVER=https://git.nullsfootprints.com \
--env=DRONE_GITEA_CLIENT_ID=dce2b1aa-8ec3-491f-871c-921a273ecb47 \
--env=DRONE_GITEA_CLIENT_SECRET=yzHtjxCxDfx0gOZuSdcoZy2yvsu4YHx_XMgcCqLxJ7c= \
--env=DRONE_RPC_SECRET=b166487c2eb8cf7239589ce078a9e95e \
--env=DRONE_SERVER_HOST=drone.nullsfootprints.com \
--env=DRONE_SERVER_PROTO=https \
--env=DRONE_GIT_ALWAYS_AUTH=true \
--network=cicd \
--name=drone \
--restart=always \
drone/drone:1

# 加入 nginx 网络使得 nginx 容器可以将请求转发至 drone
docker network connect nginx drone

启动后,打开 drone 的前端页面,可以看到 gitea 中现有的仓库已经同步过来。点击 Activate 填入参数,drone 会自动到 gitea 配置 webhook。

runner

根据官方教程,runner 支持的类型有 5 种,分别用于执行不同类型的任务:

  1. Docker Runner
  2. Kubernetes Runner
  3. Exec Runner
  4. SSH Runner
  5. Digital Runner

通常情况下,我们使用第一种即可满足需求。理论上,runner 可以部署在任何主机上,也可以部署多个。 runner 只与 provider 通讯,与外界没有任何联系。

同样地,启动 runner 容器也需要提供几个参数:

  1. DRONE_RPC_PROTO 连接 provider 用的协议,可以为 http 或者 https
  2. DRONE_RPC_HOST provider 所在的域名或者 ip+port
  3. DRONE_RPC_SECRET provider 与 runner 通讯时用到的密钥,要与启动 provider 时提供的一致
docker run -d \
-v /var/run/docker.sock:/var/run/docker.sock \
-e DRONE_RPC_PROTO=https \
-e DRONE_RPC_HOST=drone.nullsfootprints.com \
-e DRONE_RPC_SECRET=b1623b89ce96487ccf7582e078a9e95e \
-e DRONE_RUNNER_CAPACITY=2 \
-e DRONE_RUNNER_NAME=$HOSTNAME \
--name runner \
--restart always \
drone/drone-runner-docker:1

启动后执行 docker logs runner -f 查看容器日志,看到 successfully pinged the remote server 表示成功连接 provider。

registry

register 是 docker 官方提供的储存和分发镜像的镜像,外部必须通过通过 https 才能正常访问,请提前备好 ssl 证书,并将证书配置到容器。

因为是私有服务,我们还希望能够限制访问。权限的实现方式有好几种,详细说明可以查看官方文档

docker run -d \
-v /usr/local/registry:/var/lib/registry \
-v /home/ubuntu/registry/certs:/certs \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.nullsfootprints.com.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/registry.nullsfootprints.com.key \
-e REGISTRY_AUTH=htpasswd \
-e REGISTRY_AUTH_HTPASSWD_REALM=Registry_Realm \
-e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
-e REGISTRY_STORAGE_DELETE_ENABLED=true \
--name registry \
--network cicd \
--restart=always \
registry:2

# 加入 nginx 网络使得 nginx 容器可以将请求转发至 registry
docker network connect nginx registry

我们使用最简单的 htpasswd 方式,上述命令默认会在容器的 /auth/htpasswd 文件生成用户名为 docker 的账号。因为不同主机生成的账号密码不同通用,我们通过容器内置的 htpasswd 命令设置自定义的账号密码,例如设置账号为 iamnull,密码为Aa123456

docker exec -it registry sh -c 'htpasswd -Bbn iamnull Aa123456 > /auth/htpasswd && cat /auth/htpasswd'

在另一台主机上使用 docker login https://registry.nullsfootprints.com,输入账号密码,看到结果为 Login Succeeded 说明 registry 已经启动并且正确配置。

启动完成后执行 curl --user 你的registry用户名:你的registry密码 您的registry域名/v2/_catalog,看到请求成功即表示正确启动。

$ curl --user username:password https://registry.nullsfootprints.com/v2/_catalog

{"repositories":[]}

项目

我们知道,git 的常用工作流,最简单的有 3 条分支:

  1. feature/* 或者 bugfix/* 系列的分支用于日常开发工作
  2. 当 1 中功能完成或者漏洞修复后,合并到 dev 分支,发布后交由测试和产品人员确认验收
  3. 当 2 中功能或者修复被验收之后,合并到 master 分支,打上 tag,发布到正式环境供用户使用

drone 根据项目根目录的 .drone.yml 文件来执行持续交付的任务,关于这个文件的细节,可以参考官方文档

需要注意的是,设置 trigger 时 branch 与 tag 事件不能同时使用。

Note that you cannot use branch triggers with tag events. A tag is not associated with the source branch from which it was created.

各位可以去 Github 拉取 demo项目,测试部署好的环境。运行前需要将 .drome.yml 中的配置项修改为你真实存在的数据。drone 支持将敏感数据预先设置成 Secret,项目中也用到这一特性保存敏感数据,各位需要将数据填入 drone 中,才能正常运行,详情可以参考官网文档

如果设置没有问题,每一次 master/dev 分支推送代码,都会触发一次集成任务。

drone result

如何删除 registry 中的镜像

有两种方法可以删除已经推送到 registry 中的镜像。第一种方式是使用 registry 镜像提供的 rest api,详情可以参考官方文档

第二种方法是使用现有的开源库 deckschrubber,可以根据参数删除太旧的镜像。

后续

按照文章所述一步步部署好环境,是可以正常开始ci/cd旅途,如果对文章有疑问,可以评论或者联系我

评论

退出登录