[CI/CD] - 利用Gitea Drone CI实现Hexo的自动交付

Drone CI是一个基于容器的轻量级的本地持续交付平台,可以将下面的流程自动化

            graph LR
            Commit --> Build --> Unit-Tests --> Integraion-Tests --> Review --> Staging --> Production
          

安装

官方推荐Gitea和Drone分别在不同的实例里,不推荐将Gitea和Drone通过docker-compose安装在一个实例里

这里需要先介绍以下Drone的工作模式,Drone提供Web UI界面,并将需要执行的工作分配给drone runner(也可以说是 drone agent),drone runner可以不和Server安装在同一个服务器,你可以在自己的电脑上安装Runnner

  • Server: 用于验证,配置repository、用户、密钥、接受webhooks,分发任务
  • Runnner: 用于接受构建任务,并实际执行

例如:将Server安装在树莓派上,将Runnner安装在电脑上

drone runner有许多不同种类用来最优化执行不同的运行环境。分别有

数据库

Drone需要数据库来持续化储存,默认Drone使用sqlite(不需要额外配置)

Drone推荐使用Postgres而不是MySQL,因为Drone专门为Postgres进行了优化 - 官网

下面先给出Postgres的配置,将在最后给出完整的配置

docker-compose.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
postgres:
image: postgres:latest
restart: unless-stopped
container_name: drone-postgres
env_file:
- ./postgres.env
environment:
- POSTGRES_DB={数据库名}
- POSTGRES_USER={数据库用户名}
volumes:
- ./postgres/:/var/lib/postgresql/data:rw
networks:
- default

postgres.env:

1
POSTGRES_PASSWORD={数据库密码}

Drone Server

我们是基于Gitea进行配置的,基于不同的版本控制服务其环境变量的配置部分也不同,其中Gitea需要配置以下部分

更多环境变量配置参见Configuration Reference

Gitea OAuth

我们还需要现在Gitea里面配置OAuth2

验证回调连接必须完全和你的Drone服务器链接一致(包括scheme和host)

记下Client ID和Client Secret下面需要用到

RPC Secret

还需要设置RPC连接密码,Runner会使用这个密码来连接Server,可以使用openssl生成密码

1
2
> openssl rand -hex 16
bea26a2221fd8090ea38720fc445eca6

Drone Server的docker-compose.yml

docker-compose.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
drone-server:
image: drone/drone:latest
container_name: drone
env_file:
- ./drone.env
- ./drone-rpc.env
environment:
# 使用的数据库
- DRONE_DATABASE_DRIVER=postgres
# Drone的域名
- DRONE_SERVER_HOST={你的域名:www.drone.com}
# 此项用于生成链接等,并不实际影响访问效果
- DRONE_SERVER_PROTO=https
# 是否使用Lets Encrypt进行https
# 我们使用nginx反向代理实现https,这里设置false
- DRONE_TLS_AUTOCERT=false
# 是否跳过GITEA的验证
- DRONE_GITEA_SKIP_VERIFY=false
# GITEA的服务器
- DRONE_GITEA_SERVER={https://你的git服务器}
# GIT的操作每次都需要先验证
- DRONE_GIT_ALWAYS_AUTH=false
# jwilder/nginx-proxy配置
- VIRTUAL_HOST={你的域名:www.drone.com}
# jrcs/letsencrypt-nginx-proxy-companion配置
- LETSENCRYPT_HOST={你的域名:www.drone.com}
- LETSENCRYPT_EMAIL={你的邮箱:[email protected]}
volumes:
- ./drone:/var/lib/drone/:rw
- /var/run/docker.sock:/var/run/docker.sock
networks:
- nginx-proxy
- default
depends_on:
- postgres
restart: unless-stopped

# 由于我们的nginx-proxy在另外一个docker-compose里面
# 这里引入nginx-proxy的网络并给drone-server
networks:
nginx-proxy:
external:
name: nginx-proxy

drone.env:

1
2
3
4
5
DRONE_USER_CREATE=username:{你的用户名},machine:false,admin:true,token:{你的密码}
DRONE_GITEA_CLIENT_ID={Gitea的Client ID}
DRONE_GITEA_CLIENT_SECRET={Gitea的Client Secret}
DRONE_DATABASE_DATASOURCE=postgres://{数据库用户名}:{数据库密码}@postgres:5432/{数据库名}?sslmode=disable
DRONE_DATABASE_SECRET={用于数据库数据加密储存:自行设置}

drone-rpc.env:

1
DRONE_RPC_SECRET={RPC Secret}

Drone Runner

当我们完成Server的安装以后,我们就需要Runner来执行pipelines
Runner可以安装在LinuxWindows上,但是都需要使用Docker进行运行,至少需要配置以下3项:

更多环境变量配置参见Configuration Reference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
drone-agent:
image: drone/drone-runner-docker:1
container_name: drone-agent
networks:
- default
volumes:
- /var/run/docker.sock:/var/run/docker.sock:rw
env_file:
- ./drone-rpc.env
environment:
- DRONE_RPC_HOST=drone-server
# 上方使用的此docker-compose网络下drone-server的ip进行连接
# 所以这里填http而不是https,我们https利用nginx反向代理实现
- DRONE_RPC_PROTO=http
# Limits the number of concurrent pipelines that a runner can execute. The runner executes 2 concurrent pipelines by default.
- DRONE_RUNNER_CAPACITY=2
- DRONE_RUNNER_NAME={Runner自定义名字}
depends_on:
- drone-server
restart: unless-stopped

部署

完整配置文件

Drone的docker-compose-drone.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
---
version: "3"
services:
postgres:
image: postgres:latest
restart: unless-stopped
container_name: drone-postgres
env_file:
- ./postgres.env
environment:
- POSTGRES_DB={数据库名}
- POSTGRES_USER={数据库用户名}
volumes:
- ./postgres/:/var/lib/postgresql/data:rw
networks:
- default

drone-server:
image: drone/drone:latest
container_name: drone
env_file:
- ./drone.env
- ~/docker/drone/drone-rpc.env
environment:
# 使用的数据库
- DRONE_DATABASE_DRIVER=postgres
# Drone的域名
- DRONE_SERVER_HOST={你的域名:www.drone.com}
# 此项用于生成链接等,并不实际影响访问效果
- DRONE_SERVER_PROTO=https
# 是否使用Lets Encrypt进行https
# 我们使用nginx反向代理实现https,这里设置false
- DRONE_TLS_AUTOCERT=false
# 是否跳过GITEA的验证
- DRONE_GITEA_SKIP_VERIFY=false
# GITEA的服务器
- DRONE_GITEA_SERVER={https://你的git服务器网址}
# GIT的操作每次都需要先验证
- DRONE_GIT_ALWAYS_AUTH=false
# jwilder/nginx-proxy配置
- VIRTUAL_HOST={你的域名:www.drone.com}
# jrcs/letsencrypt-nginx-proxy-companion配置
- LETSENCRYPT_HOST={你的域名:www.drone.com}
- LETSENCRYPT_EMAIL={你的邮箱:[email protected]}
volumes:
- ./drone:/var/lib/drone/:rw
- /var/run/docker.sock:/var/run/docker.sock
networks:
- nginx-proxy
- default
depends_on:
- postgres
restart: unless-stopped

drone-agent:
image: drone/drone-runner-docker:1
container_name: drone-agent
networks:
- default
volumes:
- /var/run/docker.sock:/var/run/docker.sock:rw
env_file:
- ~/docker/drone/drone-rpc.env
environment:
- DRONE_RPC_HOST=drone-server
# 上方使用的此docker-compose网络下drone-server的ip进行连接
# 所以这里填http而不是https,我们https利用nginx反向代理实现
- DRONE_RPC_PROTO=http
# Limits the number of concurrent pipelines that a runner can execute. The runner executes 2 concurrent pipelines by default.
- DRONE_RUNNER_CAPACITY=2
- DRONE_RUNNER_NAME={Runner自定义名字}
depends_on:
- drone-server
restart: unless-stopped

networks:
nginx-proxy:
external:
name: nginx-proxy

postgres.env:

1
POSTGRES_PASSWORD={数据库密码}

drone.env:

1
2
3
4
5
DRONE_USER_CREATE=username:{你的用户名},machine:false,admin:true,token:{你的密码}
DRONE_GITEA_CLIENT_ID={Gitea的Client ID}
DRONE_GITEA_CLIENT_SECRET={Gitea的Client Secret}
DRONE_DATABASE_DATASOURCE=postgres://{数据库用户名}:{数据库密码}@postgres:5432/{数据库名}?sslmode=disable
DRONE_DATABASE_SECRET={用于数据库数据加密储存:自行设置}

drone-rpc.env:

1
DRONE_RPC_SECRET={RPC Secret}

nginx-proxy的docker-compose-nginx-proxy.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
---
version: "3"
services:
nginx-proxy:
image: jwilder/nginx-proxy:alpine
container_name: nginx-proxy
labels:
com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
# conf
- ./nginx-proxy/conf.d/:/etc/nginx/conf.d/
- ./nginx-proxy/conf/proxy.conf:/etc/nginx/proxy.conf
- ./nginx-proxy/conf/nginx.conf:/etc/nginx/nginx.conf
# certs
- ./nginx-proxy/certs/:/etc/nginx/certs/:ro
# settings per site
- ./nginx-proxy/vhost.d/:/etc/nginx/vhost.d/
# dhparam
- ./nginx-proxy/dhparam/:/etc/nginx/dhparam/:rw
# certs html
- ./nginx-proxy/html/:/usr/share/nginx/html
networks:
- nginx-proxy
restart: unless-stopped

letsencrypt-nginx-proxy-companion:
image: jrcs/letsencrypt-nginx-proxy-companion
container_name: nginx-letsencrypt
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./nginx-proxy/conf.d/:/etc/nginx/conf.d/
- ./nginx-proxy/certs/:/etc/nginx/certs:rw
- ./nginx-proxy/vhost.d/:/etc/nginx/vhost.d
- ./nginx-proxy/html/:/usr/share/nginx/html
restart: unless-stopped
depends_on:
- nginx-proxy

networks:
nginx-proxy:
external:
name: nginx-proxy

请自行注意文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Drone
├── drone
├── drone.env
├── drone-rpc.env
├── postgres [error opening dir]
└── postgres.env

nginx-proxy
├── certs
├── conf
├── conf.d
├── dhparam
├── html
└── vhost.d

然后运行

1
2
> docker-compose -p drone -f docker-compose-drone.yml up -d
> docker-compose -p nginx-proxy -f docker-compose-nginx-proxy.yml up -d

即可启动Drone

运行后的配置

运行成功后,访问Drone的域名会自动跳转到Gitea进行验证,需要注意的是检测链接里面的回调链接的scheme(https还是http)需要和Gitea里面OAuth设置的一致,否则会验证失败。

验证成功后就会自动进入Drone的主界面,在网页上Drone本身不需要进行配置,进入则代表部署成功

Pipelines

Pipelines帮助我们自动化程序交付过程,比如:编译,测试,发布到生产环境
Pipelines通过源代码库的变动触发从而执行,一次提交后将触发Drone的webhook运行对于的pipeline
同时还有其他触发器,比如:定时触发器

Pipelines通过在Git项目根目录下放置.drone.yml进行配置,使用yaml语法
Drone支持多种Pipelines,每个都对不同的使用场景和运行环境进行了优化:

Docker Pipelines

这里只对Docker Pipelines进行说明,可以先看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
---
kind: pipeline
type: docker
name: default

steps:
- name: greeting
image: golang:1.12
commands:
- go build
- go test

- name: frontend
image: node
commands:
- npm install
- npm test

kindtype定义一个Docker pipeline

1
2
3
---
kind: pipeline
type: docker

Triggers

当我们push代码到库、或拉取、或创建一个tag,我们的版本控制服务将自动发起一个webhook给Drone然后触发Triggers运行,我们可以使用Triggers来限制pipeline的执行,总共有以下几种方式

  • Branch
  • Event
  • Reference
  • Repository
  • Status
  • Target
  • Cron
By Branch
1
2
3
trigger:
branch:
- master

可以使用includeexclude语法

1
2
3
4
5
6
trigger:
branch:
include:
- master
exclude:
- feature/*
By Event

可以通过不同事件来触发trigger

1
2
3
4
5
6
7
8
9
trigger:
event:
- cron
- custom
- push
- pull_request
- tag
- promote
- rollback

同样支持includeexclude语法

Platform

利用platform来配置支持的操作系统、系统构架,默认为Linux amd64

1
2
3
4
5
6
7
kind: pipeline
type: docker
name: default

platform:
os: linux
arch: amd64

Workspace

Drone会自动创建一个临时的volume作为workspace,在这里将clone我们的源码库,同时workspace将作为我们每一个步骤的工作目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kind: pipeline
type: docker
name: default

workspace:
path: /drone/src

steps:
- name: backend
image: golang:latest
commands:
- go get
- go test

- name: frontend
image: node:latest
commands:
- npm install
- npm run tests

等价于

1
2
3
> docker volume create my-named-volume
> docker run --volume=my-named-volume:/drone/src golang
> docker run --volume=my-named-volume:/drone/src node

Steps

steps部分定义一系列的指令,这些指令将作为Entrypoint在Docker容器里面的项目根目录(workspace)下执行
如果任何一个指令返回一个不是0的代码,则pipeline将失败并退出,同时整个pipeline状态将被标记为失败,剩下的流程将被跳过。

需要注意的是每一个step都是在不同容器里面执行的

其他Docker相关配置

Hexo自动交付

至于为什么要设置hexo的自动交付,因为设备很多,每个设备环境可能多少有些不同,管理起来非常的麻烦。而且随着blog的字数越来越多,每一次编译都要非常的久,在8GB内存的笔记本上经常直接爆内存,导致失败,所以干脆都转移到服务器上进行generate,然后deploy,每次只需要git push,后面的事情都是自动的,省时省力。

由于使用了next主题所以需要先pull主题,推荐自己创建一个next主题的库,然后在本地库新增一个远程分支,每次推送到你自己的远程分支里面,这样就可以和next官方的分支分可互不影响,也不会影响更新,同时配置文件也都同步。

流程:需要去Drone的网站激活你的blog库,同时我们使用drillster/drone-rsync插件进行同步数据,所以需要设置添加secret rsync_key为你的私钥。

.drone.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
kind: pipeline
type: docker
name: default

steps:
- name: clone-theme
image: alpine/git
commands:
- git clone {你自己的next库的git链接} themes/next

- name: build
image: node:latest
commands:
- npm install -g hexo
- npm install
- npm run build

- name: deploy
image: drillster/drone-rsync
environment:
RSYNC_KEY:
from_secret: rsync_key
settings:
user: {用户名}
hosts:
- {部署服务器IP}
port: 22
source: ./public
target: {你的web目录}
delete: true
when:
branch: master

trigger:
event:
- push

这样设置好了后,每次git push后就会自动按照环境、然后generate,最后deploy了,所有的过程都可以在Drone后台看到。

添加缓存

当你上面成功后你会发现,虽然git都是clone的自己的服务器的,但是npm install还是得从网上托啊,不说浪费资源的问题啥的了,每次托也慢,所以有必要加上缓存。

drillster/drone-volume-cache插件需要配置Volume,所以你的库必须被标记为信任的!在不信任的环境里是不应该启用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- name: restore-cache
image: drillster/drone-volume-cache
volumes:
- name: cache
path: /cache
settings:
restore: true
mount:
- ./node_modules

- name: rebuild-cache
image: drillster/drone-volume-cache
volumes:
- name: cache
path: /cache
settings:
rebuild: true
mount:
- ./node_modules

volumes:
- name: cache
host:
path: /tmp/cache
  • restore:标志让插件把host的文件复制到build环境里面,所以这需要在pipeline最开始声明
  • rebuild:标志让插件把build环境的文件复制到host里面,所以这需要在pipeline最后声明

于是最后.drone.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
kind: pipeline
type: docker
name: default

steps:
- name: restore-cache
image: drillster/drone-volume-cache
volumes:
- name: cache
path: /cache
settings:
restore: true
mount:
- ./node_modules

- name: clone-theme
image: alpine/git
commands:
- git clone {你自己的next库的git链接} themes/next

- name: build
image: node:latest
commands:
- npm install -g hexo
- npm install
- npm audit fix
- npm run build

- name: deploy
image: drillster/drone-rsync
environment:
RSYNC_KEY:
from_secret: rsync_key
settings:
user: {用户名}
hosts:
- {部署服务器IP}
port: 22
source: ./public
target: {你的web目录}
delete: true
when:
branch: master

- name: rebuild-cache
image: drillster/drone-volume-cache
volumes:
- name: cache
path: /cache
settings:
rebuild: true
mount:
- ./node_modules

trigger:
event:
- push

更新时间

通过git clone会把文件修改时间都变为现在的时间,所以我们需要找个方法解决,比较好的方法是使用git的commit的时间替代修改时间

我们选择git-restore-mtime来修复更新时间,放在generate之前就行了

1
2
3
4
5
6
7
- name: git-restore-time
image: ubuntu:18.04
commands:
- apt-get update
- apt-get install -y bash python git
- apt-get install -y git-restore-mtime
- /usr/lib/git-core/git-restore-mtime --commit-time --work-tree . --git-dir ./.git

参考