利用 git 自动部署环境

和 @Leniy 说到利用 git 自动部署的事情, @Leniy 问了句, 怎么配, 既然有人有兴趣, 我就扯几句.

git 无疑是一个很赞的版本管理系统, 简单易用不伤手. 而如果在 VPS 上部署一个 git, 每次提交后, 直接部署到环境中, 不用 ssh 登进去顶着 XXXms 的延迟穷倒腾. 说明一下, 我只是举个栗子, 实现了”自动部署”的要求, 看官可以做的远不止这些.

首先, 环境要求:

要求客户端和服务端都有个 git 环境.

要求能 push 东西到服务端

要求能修改服务端的环境–ftp环境和ssh环境都可以接受其实, 不过安全起见, 最好让 git 的目录是 web 根目录的父目录

其次, 原理：

git 的自动部署利用的是 hooks (中文版), 还可以参考这个.

简单说就是在服务端的 hooks 的文件夹中, 有若干文件, 在你执行各种操作的时候, 会触发特定的bash, 然后git会执行这些bash.

在 GIT_DIR/hooks/ 中有各种 xxx.sample 文件, 看官若有兴趣, 可以研究下人家怎么写的.

因为是测试, 我都在本地写, 但其实一个是客户端, 一个是服务端, 两边都有操作. 看官需要稍稍分辨下是在哪端.

好, 废话不说, 介绍个完整的耍耍.

1. 初始化服务端 git

我在服务端 $HOME/ 目录下建立 repo 文件夹, 初始化一个 test.git

[yu@argcv-com ~]$ cd ~
[yu@argcv-com ~]$ mkdir repo
[yu@argcv-com ~]$ cd repo/
[yu@argcv-com repo]$ git init --bare test.git
Initialized empty Git repository in /home/yu/repo/test.git/
[yu@argcv-com repo]$

2. 初始化客户端

我在/tmp/client下建立个客户端

[yu@argcv-com ~]$ cd /tmp/
[yu@argcv-com tmp]$ mkdir client
[yu@argcv-com tmp]$ cd client/
[yu@argcv-com client]$ git init
Initialized empty Git repository in /tmp/client/.git/
[yu@argcv-com client]$ git remote add origin yu@localhost:repo/test.git
[yu@argcv-com client]$ touch README.md
[yu@argcv-com client]$ git add .
[yu@argcv-com client]$ git commit -m "init"
[master (root-commit) 00da9a9] init
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 README.md
[yu@argcv-com client]$ git push origin --all
Counting objects: 3, done.
Writing objects: 100% (3/3), 210 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
To yu@localhost:repo/test.git
* [new branch] master -> master
[yu@argcv-com client]$

3. 初始化服务端待部署的环境

服务端部署在 /tmp/deploy/ 下

[yu@argcv-com ~]$ cd /tmp/
[yu@argcv-com tmp]$ mkdir deploy
[yu@argcv-com tmp]$ cd deploy/
[yu@argcv-com deploy]$ git clone $HOME/repo/test.git .
Cloning into '.'...
done.

特别提醒下, 此时在/tmp/deploy下有隐藏文件夹.git

安全起见, 不要让你的web的/目录和它在同一层, 而应该让它成为/tmp/deploy的一个子目录

4. 服务端添加 hooks

刚才说过, 服务端的git在 $HOME/repo/test.git/ 文件夹下

添加文件 $HOME/repo/test.git/hooks/post-receive, 做出部署

注意, hooks 下有好几个触发器, 分别对应在不同阶段的时候采取的动作.

比如 update, 对应的其实是在更新前的动作, 这个 bash 可以用来做 backup, 而如果用它来写自动部署, 那么它每次部署都会慢一拍… 这会很囧的

[yu@argcv-com repo]$ pwd
/home/yu/repo
[yu@argcv-com repo]$ cd test.git/
[yu@argcv-com test.git]$ cd hooks/
[yu@argcv-com hooks]$ cat > post-receive <<'EOF'
#!/bin/sh
unset GIT_DIR
NowPath=`pwd`
echo "now path is :"$NowPath
DeployPath="/tmp/deploy"
echo "deploy path is :"$DeployPath
cd $DeployPath
echo "cd deploy path"
git add . -A && git stash # remove local changes
git pull origin master # pull data from master
# the follow line is also ok:
# git add . && git fetch origin && git reset --hard origin/master
echo "deploy done"
cd $NowPath
echo "fine"
# --- Finished
exit 0
EOF
[yu@argcv-com hooks]$ chmod +x post-receive

hook 其实就是个 bash, 不言自明, 到待部署的目录, 执行 git pull或者 fetch && reset , 完成部署. 之所以要”git add . -A”是因为你可能写一些 untracked 的文件, 而这些文件若没有被 add, 那么版本控制可能无法对它进行修改.

当然, 你也可以执行 backup 之类的动作.

注意和普通 bash 一样, 加上可执行.

和 Makefile 一样, 在每一行, 目录都是固定的, 需要解除环境变量 GIT_DIR 环境变量才能自由移动当前位置.

当然, 其实我也可以用

(cd $DeployPath && git add . -A && git stash && git pull origin master )

来替换, 如果比较简单的话.

关于数据文件.

stash, reset 等操作会清理掉所有修改.

一般认为, 我们不应该把数据文件和代码文件放在一起. 在工程设计上, 我们本来就应该把数据文件放在别的地方. 因此这个逻辑并没有太多大问题. 但实际操作中, 可能会出于兼容啊, 第三方上游代码啊, 拍脑袋啊, 配置原因啊等, 我们必须要在工程下放点啥. 那么对我们而言, 需要考虑在工程里配置下 gitignore 文件, 那么在它白名单下的文件将会被无视.

5. 测试

首先, 客户端添加文件, 加入commit, 并push

[yu@argcv-com client]$ touch newfile
[yu@argcv-com client]$ git add .
[yu@argcv-com client]$ git commit -m "add new file"
[master 68efdcf] add new file
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 newfile
[yu@argcv-com client]$ git push origin
Counting objects: 4, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 384 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
remote: now path is :/home/yu/repo/test.git
remote: deploy path is :/tmp/deploy
remote: cd deploy path
remote: From /home/yu/repo/test
remote: 6627165..5ac80ec master -> origin/master
remote: Updating 6627165..5ac80ec
remote: Fast-forward
remote: newfile | 0
remote: 1 file changed, 0 insertions(+), 0 deletions(-)
remote: create mode 100644 newfile
remote: deploy done
remote: fine
To yu@localhost:repo/test.git
b3a32f2..5ac80ec master -> master
[yu@argcv-com client]$

我们可以看到remote端提示

再查看服务端deploy的文件

[yu@argcv-com deploy]$ ls -al
total 0
drwxrwxr-x 3 yu yu 100 Apr 26 17:29 .
drwxrwxrwt 22 root root 460 Apr 26 17:29 ..
drwxrwxr-x 8 yu yu 280 Apr 26 17:29 .git
-rw-rw-r-- 1 yu yu 0 Apr 26 17:29 newfile
-rw-rw-r-- 1 yu yu 0 Apr 26 17:13 README.md

基本原理就是这样了.

6. 相关介绍

用户权限问题

在实际使用的时候, 有人反馈说会有 Permission Denied 之类的事情.

这种事情大多发生在你 Web Service 使用的用户和 git push 的用户不一致的情况下. 一般解决思路是这样的.

首先把 git 所在用户加入 web 用户组, 比如命令

sudo usermod -a -G web git

若你把目标目录放在了 /var/www/your_git 下, 那么你要检查下 /var, /var/www, /var/www, /var/www/your_git 这几个目录的权限是否开了足够权限.

一般情况下, 所有父目录对 git 用户至少要有读权限, 而 git 目录需要写权限. 对我而言, /var 是公共空间, 一般都能看到就不详述, /var/www/ 下面可能会管理若干工程, 那么一个可以考虑的方法是把它的 owner 设置为 root 用户, web 用户组. 然后我们可以通过控制同组权限的方法来给 /var/www 目录加上合适的限制. 比如在此处, 我会建议你给 /var/www 设置权限为 750 (sudo chmod 750 /var/www). FAQ: How do directory permissions in Linux work?, 然后给 /var/www/your_git 为 770 即可.

然后还要考虑是否有 SELinux 在挡道, 一般修改权限后 sudo restorecon -R /var/www 下就可以了.

你也可以尝试以 git 的身份查看下 web 目录, 命令是

sudo -u git > /var/www/your_git/test_file

这个命令会尝试以 git 的身份在 /var/www/your_git/ 的目录下写个名为 test_file 的空文件.

控制更新频率

一个比较好的实践是开多个 branch. 比如平时是 push 到远程的 master branch, 那么你可以开个名为 publish 的 branch. 前面的脚本改成使用 “origin/publish”.

那么你执行

git push origin master

远程不会有任何更新. 而你执行

git push origin master:publish

那么远程的 publish 这时候会更新, 然后 hook 会把 publish 的最新结果放到运行环境.

webhooks and CI tools

对 github, gitlab 托管的工程. 服务器自己内置了各种 hooks, 对用户而言, 出于安全考虑, 我们很难接触底层的脚本. 一般我们被推荐使用 webhooks. 我们需要在自己服务器上提供一个对外部暴露的 web 接口. 配置一下工程后, 每当有特定的 event 来临, 我们会收到一个 push event, 包含了许多相关信息, 我们可以借此执行后续操作.

此外, 我们也可以善用 travis ci 等工具, 它们其实也是基于 webhooks 的更加高层的应用. 即便是公开工程, 也允许我们设置一些 private environment variables. 然后每当部署成功后, 可以执行一些脚本.

若我们既想托管到 github, 又不想浪费之前写过的部署脚本, 我们还可以在 travis 的配置文件中写一下 deploy 脚本, 把最新的代码再 push 到我们文件.

deploy: # 只有前面测试脚本通过后才执行它
- provider: script
keep-history: true
skip-cleanup: false
script: echo $ID_RSA > $HOME/id_rsa && chmod 0600 $HOME/id_rsa && GIT_SSH_COMMAND="ssh -i $HOME/id_rsa" git push git@myserver:proj master --force
on:
branch: master # 只有 master 文件才会执行 push

比如可以用类似上面的脚本. 我们在 travis 的 global environment variable 里面写一下 ID_RSA 的内容为你的私钥, 然后直接 push 到自己的服务器即可. 然后以后你只需要往 github push 代码就好

gitlab 等也有内置 ci, 此外, 我们还可以自己搭建 ci 服务. 比如 drone. 但无论如何, 基本逻辑就是一条, 当我们 push 一下代码, 下面就交给各种 “当 foo 的时候就 bar” 的 hooks 吧, 别自找麻烦了.

7. 总结

这篇的内容本意是简单粗暴地介绍一点解耦方面的考虑. 我们如何监控一个事件的发生? 除了没日没夜发心跳包去骚扰对方的状态管理工具外, 我们还可以选择另一种方法, 让事件发生的同时, 让事件来告诉我们.

这其实是一个很常见的做法. 在程序中有 callback 在库中有 libevent. 因为当需要面对大量监听的时候, 轮询是没有未来的 :(. 而本文提到的 hooks 其实就是 git 提供的一个通用的工具, 让我们在更高抽象层上也能利用这类模型. 希望可以让各位能更高效地摆脱重复劳动.