持续集成和构建的工具有很多,除了著名的 Jenkins,Travis,CircleCI,还有最近比较热门的 Github Action 和 Gitlab CI/CD。但是这些工具面对私人项目不是要收费就是占用大量服务器资源,作为个人开发者的私人项目如果想要使用并不友好。那么开源免费的 Drone CI 是个不错选择,它不但非常轻量,而且十分强大。并可以结合私有代码仓库自动编译、构建服务,几行脚本即可实现自动化部署。本文讲述 Drone CI 的具体实践,结合Gitea,怎么在 VPS 里从零开始搭建一个基于 Gitea + Drone CI 的持续集成系统。
Gitea 是一个开源社区驱动的轻量级代码托管解决方案,后端采用 Go 编写,采用 MIT 许可证
Gitea的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。采用Go作为后端语言,只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux, macOS 和 Windows 以及各种架构,除了x86,amd64,还包括 ARM 和 PowerPC.
Gitea 功能特性
Drone 是一款基于 Docker 的 CI/CD 工具,所有编译、测试、发布的流程都在 Docker 容器中进行.
开发者只需在项目中包含 .drone.yml 文件,将代码推送到 git 仓库,Drone 就能够自动化的进行编译、测试、发布。
1、首先到各大公有云厂商提供的云平台上购买对应的机器,配置可以选择1核2g,或者2核2g,不需要购买太大的配置。
2、机器开通完成后,部署docker环境,可以选择手动部署,或者使用Ansible脚本部署,本次使用Ansible部署,部署脚本如下:(docker-install.yaml)
---
- name: Remove Docker system
yum:
name:
- docker-client
- docker-client-latest
- docker-common
- docker-latest
- docker-latest-logrotate
- docker-logrotate
- docker-selinux
- docker-engine-selinux
- docker-engine
state: absent
tags:
- cicd
- docker_remove
- name: Remove Docker files
shell: |
rm -rf /etc/systemd/system/docker.service.d
rm -rf /var/lib/docker
rm -rf /var/run/docke
rm -rf /etc/docker
tags:
- cicd
- docker_remove
- name: Install Docker yum
yum:
name:
- yum-utils
- device-mapper-persistent-data
- lvm2
state: present
tags:
- cicd
- docker_install
- name: Install yum manager
shell: |
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
tags:
- cicd
- docker_install
- name: Install Docker
yum:
name: docker-ce
state: present
tags:
- cicd
- docker_install
- name: Configure Docker for files
file:
path: "{{ item }}"
state: directory
with_items:
- /etc/docker
- /etc/systemd/system/docker.service.d
tags:
- cicd
- docker_install
- name: Configure Docker for config
template:
src: "{{ item.name }}"
dest: "{{ item.dest }}"
loop:
- { name: "daemon.json.j2", dest: "/etc/docker/daemon.json" }
- { name: "docker.service.j2", dest: "/usr/lib/systemd/system/docker.service" }
tags:
- cicd
- docker_install
- name: Started Docker
systemd:
name: docker
enabled: yes
state: started
tags:
- cicd
- docker_install
- name: Install Docker-Compose
environment:
DOCKER_COMPOSE_VERSION: 1.25.0-rc2
shell: |
curl -L https://get.daocloud.io/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
tags:
- cicd
- docker_install
- name: Install Docker Swarm
shell:
docker swarm init --advertise-addr {{ groups['pankuibo'][0] }}
tags:
- cicd
- docker_install
3、安装docker环境,由于本次Ansible为剧本形式,所以执行如下命令来安装docker:
ansible-playbook --inventory-file='./../inventory/inventory.ini' ./deploy.yml -e target='test' --tags='docker_remove,docker_install,docker_compose' --forks=5 --user='root'
这里说明一下: 1、由于定义了不同的tags,来执行不同的操作 2、部署文件、主机文件、角色文件单独分开,更加灵活方便
Gitea 在其 Docker Hub 组织内提供自动更新的 Docker 镜像。可以始终使用最新的稳定标签或使用其他服务来更新 Docker 镜像,安装的配置文件如下(docker-compose-gitea.yaml):
version: "3.8"
services:
gitea:
image: gitea/gitea:1.16.5
environment:
- USER_UID=1000
- USER_GID=1000
- DB_TYPE=mysql
- DB_HOST=localhost:3306
- DB_NAME=gitea
- DB_USER=gitea
- SSH_PORT=2224
volumes:
- /data/gitea:/data
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2224:2224"
networks:
- "default"
deploy:
mode: replicated
replicas: 1
labels:
- "traefik.enable=true"
- "traefik.docker.network=default"
- "traefik.http.services.gitea_gitea.loadbalancer.server.port=3000"
# http 80
- "traefik.http.routers.gitea.rule=Host(`gitea.localhost.com`)"
- "traefik.http.routers.gitea.entrypoints=web"
placement:
constraints: [node.role == manager]
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
networks:
default:
external:
name: traefik_default
由于依赖于数据库,所以需要先安装Mysql服务到环境中,使用Mysql的安装配置文件如下(docker-compose-mysql.yaml)
version: "3.8"
services:
mysql:
image: mysql:5.7.37
environment:
- MYSQL_ROOT_PASSWORD=PWD
command: --default-authentication-plugin=mysql_native_password
volumes:
- /data/mysql:/var/lib/mysql
- /etc/localtime:/etc/localtime:ro
ports:
- "3306:3306"
networks:
- "default"
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
networks:
default:
external:
name: traefik_default
要基于 docker-compose 启动gitea,请执行 docker-compose up -d,以在后台启动 Gitea。使用 docker-compose ps 将显示 Gitea 是否正确启动。可以使用 docker-compose logs 查看日志。
要停止gitea,请执行 docker-compose down。这将停止并杀死容器。这些卷将仍然存在。
本次使用如下命令来安装Gitea,目前环境中使用Docker Swarm集群,所以使用如下命令安装即可,关于Docker Swarm的使用说明可以参照Docker Swarm使用说明
1、docker stack deploy -c docker-compose-mysql.yaml mysql
2、docker stack deploy -c docker-compose-gitea.yaml gitea
以上设置中 Gitea 的端口号为 3000,因此本地环境浏览器进入localhost:3000即可访问页面,建议配置域名和 Nginx 或 Caddy 反向代理访问。本次使用的代理组件是traefik代理,更多关于traefik的使用说明请参考traefik使用说明
要将 Gitea 与 MySQL 数据库结合使用,请将这些更改应用于上面创建的 docker-compose-gitea.yaml 文件
version: "3.8"
services:
gitea:
image: gitea/gitea:1.16.5
environment:
+ - DB_TYPE=mysql
+ - DB_HOST=localhost:3306
+ - DB_NAME=gitea
+ - DB_USER=gitea
创建一个Gitea的 OAuth2 应用程序,“客户端ID”和“客户端密钥”用于授权访问Gitea的资源。 重定向 URI配置必须按照下面示例的格式和路径,并且必须是真实存在的
创建一个新的共享密钥,用于授权Runners和Drone Server之间进行通信。
可以使用openssl命令生成一个共享密钥:
openssl rand -hex 16
61379d57490fe37822267e7984acc934
Drone Server 以轻量级的Docker镜像的形式发布,镜像是自包含的,没有任何外部依赖。
docker pull drone/drone
Drone 服务器使用环境变量进行配置。本文引用了配置选项的子集,定义如下。有关配置选项的完整列表,请参阅配置
DRONE_GITEA_CLIENT_ID 必需的字符串值提供您的 Gitea oauth 客户端 ID
DRONE_GITEA_CLIENT_SECRET 必需的字符串值提供您的 Gitea oauth 客户端密码
DRONE_GITEA_SERVER 必需的字符串值提供您的 Gitea 服务器地址。例如https://gitea.company.com,请注意,http(s)否则您将看到来自 Gitea 的“不支持的协议方案”错误
DRONE_RPC_SECRET 必需的字符串值提供在上一步中生成的共享密钥。这用于验证服务器和运行器之间的 rpc 连接。必须为服务器和运行器提供相同的秘密值
DRONE_SERVER_HOST 必需的字符串值提供您的外部主机名或 IP 地址。如果使用 IP 地址,您可以包括端口。例如drone.company.com
DRONE_SERVER_PROTO 必需的字符串值提供您的外部协议方案。此值应设置为 http 或 https。如果您配置 ssl 或 acme,此字段默认为 https
DRONE_DATABASE_DATASOURCE
DRONE_DATABASE_DATASOURCE=root:password@tcp(1.2.3.4:3306)/drone?parseTime=true
可选的字符串值。配置数据库连接字符串。默认值为嵌入的 sqlite 数据库文件的路径
DRONE_DATABASE_DRIVER 可选字符串值。配置数据库驱动程序名称。默认值为 sqlite3 驱动程序。替代驱动程序是 postgres 和 mysql
DRONE_GITEA_SKIP_VERIFY 布尔值在建立与远程 Gitea 服务器的连接时禁用 tls 验证。默认值为假
DRONE_RUNNER_CAPACITY 可选数字值。限制运行器可以执行的并发管道的数量。这并不限制可以在单个远程实例上执行的并发管道的数量
DRONE_USER_CREATE
$ openssl rand -hex 16
55f24eb3d61ef6ac5e83d550178638dc
DRONE_USER_CREATE=username:octocat,machine:false,admin:true,token:55f24eb3d61ef6ac5e83d550178638dc
在启动时创建的可选用户帐户。这应该用于使用管理帐户为系统播种。它可以是真实账户(即真实的 GitHub 用户),也可以是机器账户
DRONE_USER_FILTER 可选的以逗号分隔的帐户列表。注册仅限于此列表中的用户,或属于此列表中组织成员的用户
一旦Drone服务已启动并运行,可以安装runners来执行构建流水线(pipeline).
Drone runners 轮询服务器以查找要执行的工作任务,这里提供了几种不同的runners针对不同用户场景和运行时环境进行了优化,可以根据情况安装一个或多个,一种或多种。
1、Docker Runner
2、kubernetes Runner
3、Exec Runner
4、SSH Runner
5、Digital Ocean Runner
6、Macstadium Runner
Docker runner 是一个守护进程,它在一个短生命周期容器中执行流水线(pipeline)任务。可以安装一个单独的 Docker runner,或者在多台机器上安装来创建一个构建集群。
Docker runner 是一个通用的 runner,针对可以在无状态容器中运行测试和编译代码的项目进行了优化。
Docker runner 不太适合不能在容器内运行测试或编译代码的项目,包括以 Docker 不支持的操作系统或体系结构为目标的项目,如macOS
安装的配置文件如下(docker-compose-drone.yaml):
version: "3.8"
services:
drone:
image: drone/drone:2.0.0 #不要用latest,latest并非稳定版本
ports:
- "7000:80"
networks:
- "drone"
volumes:
- /data/drone/:/var/lib/drone/:rw
- /var/run/docker.sock:/var/run/docker.sock:rw
environment:
#- "DB_PASSWD_FILE=/run/secrets/db_passwd"
- DRONE_DEBUG=true
- DRONE_DATABASE_DATASOURCE=drone:123456@tcp(localhost:3306)/drone?parseTime=true #mysql配置,要与上边mysql容器中的配置一致
- DRONE_DATABASE_DRIVER=mysql
- DRONE_GITEA_SKIP_VERIFY=false
- DRONE_GITEA_CLIENT_ID=xxxxxx
- DRONE_GITEA_CLIENT_SECRET=xxxxxx
- DRONE_GITEA_SERVER=http://localhost:3000/
- DRONE_TLS_AUTOCERT=false
- DRONE_RUNNER_CAPACITY=2
- DRONE_RPC_SECRET=48f11fe546a25099cde4a05ce35a4815 #RPC秘钥
- DRONE_SERVER_PROTO=http #这个配置决定了你激活时仓库中的webhook地址的proto
- DRONE_SERVER_HOST=localhost:7000
- DRONE_USER_CREATE=username:root,admin:true #管理员账号,是你想要作为管理员的Gitea用户名
- DRONE_USER_FILTER=root
- DRONE_DATADOG_ENABLE=false
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
drone-runner:
image: drone/drone-runner-docker:1.6.3
networks:
- "drone"
depends_on:
- drone
volumes:
- /var/run/docker.sock:/var/run/docker.sock:rw
environment:
- DRONE_RPC_HOST=localhost:7000
- DRONE_RPC_SECRET=48f11fe546a25099cde4a05ce35a4815
- DRONE_RPC_PROTO=http
- DRONE_RUNNER_CAPACITY=4
- DRONE_RUNNER_NAME=runner
- DRONE_RUNNER_LABELS=machine1:runner1
- DRONE_DEBUG=true
- DRONE_LOGS_DEBUG=true
- DRONE_LOGS_PRETTY=true
- DRONE_LOGS_NOCOLOR=false
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
networks:
drone:
external: true
name: traefik_default
如果是使用docker-compose方式启动,只需要在docker-compose-drone.yaml的目录下输入docker-compose up -d
即可
本次通过以下命令可以启动Drone服务,容器通过环境变量配置,如果想要查看完整的配置参数,请查看配置参考(https://docs.drone.io/server/reference)
docker stack deploy -c docker-compose-drone.yaml drone
以上设置中 Server 的端口号为 7000,因此本地环境浏览器进入localhost:7000即可访问管理页面,建议配置域名和 Nginx 或 Caddy 反向代理访问。本次使用的代理组件是traefik代理,更多关于traefik的使用说明请参考traefik使用说明
Drone 的登录账号默认是绑定 Gitea 账号的,因此只要登录了 Gitea,Drone 也会自动登录。
在打开并登录 Drone 后,你的 Repositories 应该是空的,因为没有同步 Gitea 的代码仓库到 Drone CI 里,只要在首页里的右上角点击SYNC按钮,Drone 便会自动开始同步 Gitea 的代码仓库。同步完成后需要激活仓库,配置完成后,会自动到对应的私有仓库中创建Webhook构建钩子。
如果 Steps 需要挂载宿主机的文件夹,需要在 Drone 对应项目中的 SETTINGS 里的 Project settings 里需要勾选Trusted,这意味着开启容器的特权模式去挂载宿主机的文件夹。开启这个设置用户的权限必须是 admin ,其他用户没有权限开启。
在项目代码的根目录新建一个.drone.yml文件,一旦代码上传到代码仓库( github, gitlab, gitea 等),git 仓库会通过 Drone 预先埋好的 Webhoot 钩子发送事件请求给 Drone,Drone 接收到事件请求后会找到仓库项目根目录中的.drone.yml文件进行解析并根据文件的描述执行任务。
Drone CI 构建的每个 step 都会根据镜像产生一个 Docker 容器,并在容器里运行指定任务。
首先每个 Pipline 都有的头描述部分:
kind: pipeline # Pipeline 的类型,其他的还有 secret and signature。
type: docker # 定义了执行任务的类型,这里会使用 Docker 执行。
name: web # 定义 Pipline 的名字,一个 .drone.yml 可以有多个不同名字的 Pipeline。
然后是描述任务的每个步骤,steps 属性后描述此步骤的 name (名字) 和 image (镜像),每一步都会用到一个镜像,任务进行时会根据提供的镜像名字拉取镜像并生成一个临时 Docker 容器运行任务指令,步骤完成后自动删除。
steps:
- name: build-imaeg # 步骤名
image: docker # 步骤需要用到的镜像
下面是一个 vue 前端程序打包成 Docker 镜像并部署到服务器的例子。文中介绍的范例主要想覆盖常见的坑,对于新手可能会比较复杂,如果看不懂,没关系,可以直接跳过这一节,自己尝试动手安装 Drone CI 后回头再细品。
kind: pipeline
type: docker #在docker runner中运行
name: web
#定义setups,每个setup有属于自己的name,最后会显示在Drone CI管理页面的侧边栏
steps:
- name: restore-cache # 把之前缓存好的数据取出
image: drillster/drone-volume-cache
settings:
restore: true
mount: # 缓存挂载的文件夹
- ./.npm-cache
- ./node_modules
volumes:
- name: cache
path: /cache
- name: compile #编译
image: node:12
commands:
- yarn config set registry https://registry.npm.taobao.org -g
- yarn config set cache ./.npm-cache --global
- yarn install
- yarn run build
- name: build image #打成docker镜像
image: docker
failure: ignore
volumes:
- name: sock
path: /var/run/docker.sock
commands:
- docker build -t localhost:v1.0 -f Dockerfile .
- docker image prune -f --filter "dangling=true" # 清理无用镜像
- name: rebuild-cache # 把依赖和 npm 缓存放到缓存里
image: drillster/drone-volume-cache
settings:
rebuild: true
mount:
- ./.npm-cache
- ./node_modules
volumes:
- name: cache
path: /cache
- name: deploy #部署到服务器上
image: docker
failure: ignore
volumes:
- name: sock
path: /var/run/docker.sock
commands:
- docker service ls|grep test || export SERVICE=down #先检查服务是否存在,存在更新,不存在创建
- |
if [ "$SERVICE" != "down" ]
then
docker service update --image test:v1.0 test_test
else
docker stack deploy -c deploy.yaml autocd-web
fi
# 循环检测服务是否启动成功
- |
while true
do
docker service ps test_test|awk '{print $6}'|awk 'NR==2'|grep 'Running' || export SERVICE=down
if [ "$SERVICE" == "down" ]
then
echo -e "\033[5;35;40m 正在启动中请稍后 ... \033[0m"
export SERVICE=up
continue
else
docker service logs -n 200 test_test
sleep 3
break
fi
done
# 挂载宿主机文件到docker容器中
volumes:
- name: sock
host:
path: /var/run/docker.sock
- name: cache
host:
path: /tmp/cache
# 创建触发器,绑定分支及事件及上一次成功时才运行
trigger:
branch:
- master
event:
- pull_request
- push
status:
- success
- failure
node:
machine1:runner1
# 设置基础镜像,如果本地没有该镜像,会从Docker.io服务器pull镜像
# 这里会直接调用宿主机的密钥登录私有仓库。
FROM nginx:1.19.2-alpine
# 编译项目,使用npm安装程序的所有依赖,利用taobao的npm安装,并打包编译成静态文件
# 这两步在drone里已经完成
# 复制所有静态文件到 /usr/share/nginx/html下。
# 拷贝配置文件到nginx配置目录中
COPY dist/ /usr/share/nginx/html/
ADD nginx.conf /etc/nginx/nginx.conf
ADD default.conf /etc/nginx/conf.d/default.conf
# 暴露container的端口
EXPOSE 80
# 运行命令
CMD ["nginx", "-g", "daemon off;"]
上面的范例有5个Steps
简单整理一下每一步(详细的上面注释都有解释)
1、clone克隆私有仓库代码(默认自动添加);
2、restore-cache 步骤会把之前缓存的文件从宿主机中取出;
3、compile 步骤时 yarn或npm 跳过已经安装过的依赖;
4、build 步骤会时根据仓库中的 dockerfile 打成本地镜像包,由于不需要推送到docker私有镜像仓库即并没有使用plugins/docker插件;
5、rebuild-cache 步骤把缓存通过挂载文件放到宿主机中;
6、deploy 步骤使用 将应用部署到容器中;
因为一次构建每一个 steps 都会新生成一个容器并在容器里运行构建,沙盒环境里没有缓存数据。通过restore-cache和rebuild-cache这两个 steps 建立宿主机与容器的缓存,把 vue 的依赖 node_modules 目录和 yran 缓存通过 volumes 映射到宿主机上,在下一次构建并安装依赖时 yarn 会自动跳过没有变化的依赖包,从而加快构建速度。
实际在构建过程中,Drone CI会默认在所有setup最前面添加一个克隆代码的setup(clone), 使用自建的 Gitea 服务内网拉取可以极致地加快构建速度,等代码克隆完成后才会开始执行预定义的一些setup,如果中途报错,即会直接报错退出整个pipeline流水线流程。
在 docker-compose-drone.yaml 文件中定义 Runner 的DRONE_RUNNER_LABELS环境变量可以为 Runner 加上标签,在定义 .drone.yml 时通过这个标签让 pipeline 路由到不同的 Runner 执行任务。
例如我有两个不同的机器放在不同的地方,在这两台机器上运行 Runner 并使用DRONE_RUNNER_LABELS环境变量分别定义这两个 Runner 的标签,例如在第一个 Runner 里DRONE_RUNNER_LABELS=nodeA:runnerA,另一个 Runner 里DRONE_RUNNER_LABELS=nodeB:runnerB,那么在.drone.yml文件中我们可以定义
kind: pipeline
type: docker
name: default
steps:
- name: build
image: golang
commands:
- go build
- go test
node:
nodeA: runnerA
那么这个任务就只会在标签是nodeA:runnerA的 Runner 里运行。
如果想要在两个节点中运行,可以把这两个标签都加上,例如:
node:
nodeA: runnerA
nodeB: runnerB
因为 Runner 会主动心跳连接 Server 并在 Server 上注册自己,不需要固定的网络地址而且足够轻量, 因此这个 Runner 节点可以是你的 PC 机、笔记本,甚至是树莓派。