前言
很久之前,有个云服务器,上面挂着自己的 Blog
,每次发布代码的时候,都需要手动到服务器上 pull
代码,很是麻烦。
于是,折腾了一下 Gitee
的 Hook
,实现了自动部署的功能。
一些答复
为什么不用比较成熟的工具?比如:Jenkins、CI/CD。
1、资源问题,本身就一台小服务器,没那么多资源搞这个。
2、自己搞个简单的,免费,香(就是搞事情)。
是否会停服?
会有短暂停服,如果不像停服,需要改造一下策略。
是否支持多副本?
不支持,因为作者的需求比较简单,未实现多副本的方案。
环境
- Docker
- PHP
- Nginx(需要支持Lua模块)
- Lua
示例代码
具体流程图
具体实现
项目结构改造
这里的目的是将项目结构都改造成一致,方便后续脚本的统一。
.
├── src // 项目目录
└── version.txt // 项目版本
宿主机安装Git
因为需要拉取云端代码,需要 git
。
配置自动化项目
这个项目可以放到代码仓库里。
本文将项目放在了 /src
目录下。
具体结构和释义如下:
/src
├── dockers
│ ├── docker-compose.yml // 使用Docker compose编排容器
│ ├── mysql // 存放mysql数据的目录
│ ├── redis // 存放redis数据的目录
│ ├── lua
│ │ └── hook.lua // 这是Nginx接收到webhook之后,执行的lua脚本
│ ├── projects
│ │ ├── api.demo.com
│ │ │ ├── Dockerfile // 生成镜像和容器的Dockerfile
│ │ │ └── publish.sh // 发布脚本,会拉取代码,根据Dockerfile创建镜像和容器
│ │ └── admin.demo.com
│ │ ├── Dockerfile
│ │ └── publish.sh
│ └── nginx
│ ├── nginx.conf // 重写了nginx.conf,用于支持后续的项目配置,这个会在一开始替换 /etc/nginx/nginx.conf
│ ├── webhook.conf // webhook的nginx conf,里面会指定需要执行的lua脚本
│ └── api.demo.com.d
│ └── api.demo.com.conf // api.demo.com的nginx conf
├── api.demo.com // 具体的项目目录
│ ├── src // 代码目录
│ └── version.txt // 项目版本,构建镜像时会使用
└── admin.demo.com // 具体的项目目录
├── src // 代码目录
└── version.txt // 项目版本,构建镜像时会使用
宿主机安装Nginx
因为需要构建容器,所以直接在宿主机上安装了 Nginx
,用于转发请求。
需要安装 lua
模块。
安装好之后,将自动化配置中的 nginx.conf
替换默认的 nginx.conf
。
宿主机安装Docker和Docker compose
这个毋庸置疑了。
安装好之后,在 /src/dockers
目录下,运行 docker compose up -d
。
配置Gitee
在项目的 管理→WebHooks
下添加WebHook。
编写Nginx Lua脚本
为了能够适应多个项目,这里会通过路由来辨别不同的项目。
大致结构是:http://webhook.demo.com/项目别名
。
同时,每个项目都有回调时都有自己的密钥。
所以我们需要先定义项目和域名的映射表,项目和密钥的映射表。
-- 需要修改域名和对应别名
local mapping = {
api = "api.demo.com",
admin = "admin.demo.com"
}
-- 每个项目的密钥
local password = {
api = "E491B741843DA372DE61FCF8EFBB5252",
admin = "E491B741843DA372DE61FCF8EFBB5252"
}
接下来是判断请求和项目、密钥是否匹配。
可以先参考一下 Gitee
的请求头信息,如果是其它平台,则需要自行研究。
Request URL: <http://webhook.demo.com/api>
Request Method: POST
Content-Type: application/json
User-Agent: git-oschina-hook
X-Gitee-Token: E491B741843DA372DE61FCF8EFBB5252
X-Gitee-Timestamp: 1591321794260
X-Gitee-Ping: false
X-Gitee-Event: Push Hook
X-Git-Oschina-Event: Push Hook
可以看出,需要判断路由对应的项目是否存在,密钥是否存在。
local project = ngx.var.request_uri
project = string.gsub(project, "/", "")
if (not mapping[project]) then
ngx.say('error project')
ngx.log(ngx.ERR, 'webhook: error project')
return 0
end
if (not password[project]) then
ngx.say('error project')
ngx.log(ngx.ERR, 'webhook: error project')
return 0
end
当项目和密钥都存在之后,就需要判断请求体信息是否正确。
- 是否是 master 分支变更
- 密钥是否正确
请求体大致结构如下(原文太长了,这里精简了一下):
{
"ref": "refs/heads/master",
"before": "5f6477bb191fd11fb98988d96c84c1070a76e771",
"after": "7826fb922fb2583705441ec6f77d4ccde24b3d14",
"total_commits_count": 1,
"commits_more_than_ten": false,
"created": false,
"deleted": false,
"compare": "",
"commits": [],
"head_commit": {},
"repository": {},
"project": {},
"user_id": 0,
"user_name": "",
"user": {},
"pusher": {},
"sender": {},
"enterprise": null,
"hook_name": "push_hooks",
"hook_id": 373558,
"hook_url": "",
"password": "E491B741843DA372DE61FCF8EFBB5252",
"timestamp": "1591321794260",
"sign": ""
}
从上面的请求体可以看出,只需要判断是否存在 refs/heads/master
和密钥。
具体实现如下:
-- 请求头没问题之后,判断一下请求体是不是master分支
ngx.req.read_body()
local requestBody = ngx.req.get_body_data()
if (not requestBody) then
ngx.say('error requestBody')
ngx.log(ngx.ERR, 'webhook: error requestBody')
return 0
end
if (not string.find(requestBody, 'refs/heads/master')) then
ngx.say('error branch')
ngx.log(ngx.ERR, 'webhook: error branch')
return 0
end
-- 请求体中也有密钥,做第二次校验
if (not string.find(requestBody, password[project])) then
ngx.say('error password')
ngx.log(ngx.ERR, 'webhook: error password')
return 0
end
接下来就是执行 Shell
脚本,创建容器了。
目前,项目都放在了 /src
目录下,同时每个项目都拥有自己的发布脚本,地址是 /src/docker/project/项目/publish.sh
。
-- 查找项目的版本
-- 目前项目都放在 src 目录下,具体结构如下:
-- /src/域名/项目代码
local filePath = "/src/" .. mapping[project] .. "/version.txt"
local file = io.open(filePath, "r")
local version = "v1"
if (file) then
io.input(file)
tmpVersion = io.read()
if (not tmpVersion) then
version = tmpVersion
end
end
-- 执行Shell脚本,创建容器
os.execute("nohup bash /src/docker/project/" .. mapping[project] .. "/publish.sh " .. version .. " > /var/log/nohup/nohup.log 2>&1 &")
最后,给以正确的响应。
ngx.say('success')
具体脚本如下:
-- 需要修改域名和对应别名
local mapping = {
api = "api.demo.com",
admin = "admin.demo.com"
}
-- 每个项目的密钥
local password = {
api = "E491B741843DA372DE61FCF8EFBB5252",
admin = "E491B741843DA372DE61FCF8EFBB5252"
}
-- 请求的结构是 <http://webhook.demo.com/项目别名>
-- 获取项目别名和对应的密钥,进行匹配
-- 请求头如下:
-- Request URL: <http://webhook.demo.com/api>
-- Request Method: POST
-- Content-Type: application/json
-- User-Agent: git-oschina-hook
-- X-Gitee-Token: E491B741843DA372DE61FCF8EFBB5252
-- X-Gitee-Timestamp: 1591321794260
-- X-Gitee-Ping: false
-- X-Gitee-Event: Push Hook
-- X-Git-Oschina-Event: Push Hook
local project = ngx.var.request_uri
project = string.gsub(project, "/", "")
if (not mapping[project]) then
ngx.say('error project')
ngx.log(ngx.ERR, 'webhook: error project')
return 0
end
if (not password[project]) then
ngx.say('error project')
ngx.log(ngx.ERR, 'webhook: error project')
return 0
end
-- 请求头没问题之后,判断一下请求体是不是master分支
ngx.req.read_body()
local requestBody = ngx.req.get_body_data()
if (not requestBody) then
ngx.say('error requestBody')
ngx.log(ngx.ERR, 'webhook: error requestBody')
return 0
end
if (not string.find(requestBody, 'refs/heads/master')) then
ngx.say('error branch')
ngx.log(ngx.ERR, 'webhook: error branch')
return 0
end
-- 请求体中也有密钥,做第二次校验
if (not string.find(requestBody, password[project])) then
ngx.say('error password')
ngx.log(ngx.ERR, 'webhook: error password')
return 0
end
-- 查找项目的版本
-- 目前项目都放在 src 目录下,具体结构如下:
-- /src/域名/项目代码
local filePath = "/src/" .. mapping[project] .. "/version.txt"
local file = io.open(filePath, "r")
local version = "v1"
if (file) then
io.input(file)
tmpVersion = io.read()
if (not tmpVersion) then
version = tmpVersion
end
end
-- 执行Shell脚本,创建容器
os.execute("nohup bash /src/docker/project/" .. mapping[project] .. "/publish.sh " .. version .. " > /var/log/nohup/nohup.log 2>&1 &")
ngx.say('success')
编写Dockerfile
位置:/src/dockers/project/你的项目/Dockerfile
。
这里会直接将项目的代码拷贝到容器中,并且执行 composer install --no-dev
。
FROM imjcw/php-fpm:7.3.18-fpm-alpine
COPY src /var/www/html/
WORKDIR /var/www/html
RUN set -e; \
apk update; \
apk add git; \
cd /var/www/html; \
rm -rf .git; \
composer install --no-dev; \
apk del git; \
rm -rf /var/cache/apk/*
编写publish.sh
这里面指定了容器开放的端口,是预留给 Nginx
使用的。
大致流程是:
- 进入项目
- 拉取代码
- 构建镜像
- 停止之前的容器
- 删除之前的容器
- 构建新的容器
- 删除不用的容器和镜像
#!/bin/bash
PROJECT="api.demo.com"
VERSION=""
if [ -n "$1" ]; then
VERSION="$1"
else
VERSION="v1"
fi
PROJECT_DIR="/src/${PROJECT}"
if [ ! -d "${PROJECT_DIR}" ]; then
echo "项目目录不存在"
exit -2
fi
# pull code
cd ${PROJECT_DIR}
git pull origin master
# build image
docker build -t imjcw/${PROJECT}:${VERSION} ./
# stop container
docker stop ${PROJECT}
docker rm ${PROJECT}
# run container
docker run -d --name ${PROJECT} --link mysql --link redis --net dockers_default -p 9000:9000 imjcw/${PROJECT}:${VERSION}
# rm <none> images
if test ! -z $(docker images -f "dangling=true" -q); then
docker rmi $(docker images -f "dangling=true" -q)
fi
最后
为了能够实现,特地学了一下 LUA
,感觉还不错。