如何使用 Gitlab 实现代码托管自由?

前言

版本控制

  当我们谈到代码托管平台,我们不得不先谈一谈“版本控制”。什么是“版本控制”?版本控制是一种记录一个或若干内容变化,以便将来查阅特定版本修订情况的系统。在我们日常的编写代码过程或者工作中,版本控制显得尤为重要。有了它你就可以将选定的文件回溯到之前的状态,甚至可以将整个项目代码都回退到过去某个时间点的状态,你可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致怪异问题出现的原因,又是谁在何时报告了某个功能缺陷等等。使用版本控控制系统通常还意味着,就算你胡乱处理项目中的文件,你也照样可以轻松回复到原先的养殖,而且额外增加的工作量却是微乎其微。

  其实除了代码之外,任何文件都可以加入版本控制。比如像最新的 Office 365 中,如果将文件放在与 Onedrive 同步的文件夹中,那么 Office 365 不但会时不时为你自动保存文件,而且会把每一次的保存都记录下来,同时在你关闭文档时记录为一个版本。

  回过头来看版本控制系统,它主要可以分为本地版本控制系统、集中化的版本控制系统和分布式版本控制系统。

本地版本控制系统

  举个例子,当我在写论文或者做 PPT 的时候,我习惯在文件名的最后加上完成年月日以示区分。这样做的唯一好处就是简单便捷,但是特别容易犯错,尤其是需要和之前的版本进行交叉修改的时候。为了解决这种问题,很久以前就有许多中本地版本控制系统被开发出来,其中大多数都是采用某种简单的数据库来记录文件的历次更新差异。其中最流行的一种叫做 RCS,现今许多计算机系统上还看得到它的踪影。RCS 的工作原理是在硬盘上保留补丁集(补丁是指文件修订前后的变化);通过应用所有的补丁,可以重新计算出各个版本的文件内容。我们常用的网盘的增量同步就是采用了这种方式,网盘同步工具会在同步的主目录建立一个文件来描述本地文件的修订情况,在联网之后与远程服务器的信息进行对比就能发现本地文件与远程文件是否一致。如果两个文件的 MD5 值相同则可以说明文件无须操作,如果不同则根据时间戳来判断哪个文件最新,并将最新的文件更新给另一方。

local-version

集中化的版本控制系统

  以上的本地版本控制系统也有一定的局限性,比如无法让不同系统上的开发者协同工作。于是就有了集中化的版本控制系统(CVCS)。比较有名的像 Subversion(SVN),CVS 等等,都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人脉通常通过客户端连接到这台服务器,取出最新的文件或者提交更新。在 Git 没有被发明出来之前,这是主要的版本控制系统的标准,而且主要以 SVN 使用者最多。

  这样一来,的确在某种程度上提供了多人开发协同的功能,但是如果中央服务器发生了故障,那么谁也无法提交更新或者协同工作。如果磁盘发生损坏且未进行实时备份,毫无疑问所有或者一部分数据都会丢失。

central-version

分布式版本控制系统

  于是分布式版本控制系统(DVCS)应运而生,其中比较知名的有 Git、Mercurial、Bazaar 等等。在这类系统中,客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来,包括完整的历史记录、这样一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。因为每一次的克隆操作,实际上都是一次对代码仓库的完整备份。

  关于 Git 的诞生有个小故事一直被人津津乐道。Linux 内核开源项目有着众多的参与者,但是早期(1991~2002 年间)绝大多数的 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上。从 2002 年开始,整个项目组开始启用一个专门的分布式版本控制系统 BitKeeper 来管理和维护代码。到了 2005 年,开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了 Linux 内核开源社区免费使用 BitKeeper 的权利。于是 Linux 的缔造者 Linus 决定闭关开发一款自己的版本控制系统。一个星期后 Git 就诞生了。Git 的目标是速度、简单的设计、对非线性开发模式的强力支持、完全分布式、有能力高效管理类似 Linux 内核一样的超大规模项目,因此它的速度非空,极其适合管理大项目。

  Git 诞生以前几乎是 SVN 占据了开源届版本控制系统的江山,但之后随着 Git 的诞生与日臻完善,版本控制系统的天下早已被 Git 收入囊中。就连原来以 SVN 作为版本控制系统的 SourceForge 和以 Mercurial 作为版本控制系统的 Bitbucket 都相继支持 Git。

distributed-version

Git 私有代码托管平台解决方案

  目前公有代码托管平台国外主要有 GithubGitlabSourceForgeBitbucket 等,国内主要有 GiteeCoding阿里云 Code 等。其中,Github、SourceForge、Bitbucket、Gitee 都提供商业化的 Git 私有代码托管平台解决方案,只有 Gitlab 提供开源的 Git 私有代码托管平台解决方案,其他平台暂不了解是否有相应的解决方案。

  除了 Gitlab 这家开源的解决方案之外,其实还有 gogsGitea 等。Gitlab CE 版本提供了与 Gitlab EE 几乎一样的功能,并包含了非常丰富的特性,比如 Pages(静态页面托管)、Oauth2(第三方应用登录验证)、CI(持续集成)、CD(持续部署)等。相比之下,gogs 或者 Gitea 则偏向于更加基础、精悍的 Git 私有代码托管平台解决方案。从 Gitea 提供的 Gitea compared to other Git hosting options 一文可以看出,Gitlab CE、Gitlab EE 与 Github EE 事实上是特性最为丰富的,Gitea 除了不支持 Pages、内置容器 Registry、内置 CI/CD、提交人身份验证等特性之外,还是支持大部分特性的。考虑到未来可能出现的更加复杂的应用场景需求,比如 JupyterLab 的 Oauth2 登录验证等,这里我们采用了 Gitlab CE 的解决方案。

  随着 Docker 的广泛使用和发展,Gitlab CE 官方也提供了 Docker 化的部署方式,但是相比较而言,由 sameersbn 提供的 sameersbn/docker-gitlab 更加适合实践。主要的理由有以下两点:

平台搭建

  由于这里采用了 Docker 化的方式进行部署 Gitlab CE,所以预先需要安装 Docker 环境 和 docker-compose 工具,这里不对此进行赘述。

准备

# 为 Gitlab 创建一个目录用于放置应用配置和数据
mkdir ~/web/gitlab

# 下载 docker-compose.yml 文件
wget -c https://raw.githubusercontent.com/sameersbn/docker-gitlab/master/docker-compose.yml

修改配置

镜像版本的设置

  编辑 docker-compose.yml 配置文件。配置文件包含了对于使用的各个容器镜像的定义,主要有三个:redis、postgresql、gitlab。由于缓存数据库与应用本身没有直接的影响,只是为了加快应用的访问响应速度,所以 redis 镜像一般是使用默认的最新镜像即可,可以使用具体的最新版本号,也可以直接使用 latest 标签。数据库这里采用的 postgresql 数据库,一般来说使用 sameersbn 构建的版本即可。由于 Gitlab 在 13.7.0 版本之后将 postgresql 最低版本的要求升级到了 12,所以现在一般 sammersbn/postgresql 镜像的版本标签都是 12-20200524,以前 postgresql 11 的时候对应的标签就是 11-20200524。gitlab 镜像的版本号主要是跟着 Gitlab 的本身的版本升级而变化,但是由于 Gitlab 的版本更新比较频繁(Gitlab 素来有“版本帝”的称号),所以 sameersbn/gitlab 的版本更新可能不会包含每一个版本,但是已编译的版本标签是符合 Gitlab 官方的版本升级路线图的。

数据持久化

  这里为了使容器的数据能够持久化,一般来说会把挂载一个本地目录来对应容器的数据目录。当然 Docker 本身提供了 volume 的创建和管理,sameersbn 推荐的也是 volume 的方式挂载,但是考虑到实际的中心存储服务器来存储所有数据,仍然使用最原始的本地目录对应挂载。在下面的配置文件中,三个容器对应的数据目录都做了这项变动。

随机秘钥

  由于 Gitlab 应用本身需要有三个随机字符串来分别完成数据库记录生成、密码加密、二次验证生成,所以这里建议使用 uuid 命令生成三个长随机字符串替代 GITLAB_SECRETS_DB_KEY_BASE 、GITLAB_SECRETS_SECRET_KEY_BASE、GITLAB_SECRETS_OTP_KEY_BASE 三个参数,如下所示。

时区设置

  这里使用的时区默认是 sameersbn 所在的时区,如果需要改成中国,则 TZ 为 Asia/Shanghai ,GITLAB_TIMEZONE 为 Beijing。注意,中国时区的表示并不是 Asia/Beijing,而是 Asia/Shanghai。只用一个单词表示时区时,则使用 Beijing (北京时间)。

端口和 SSL 设置

  在 gitlab 容器中实际上是有 Nginx 服务的,所以官方提供了自带的 SSL 加载方式。但是考虑到搭建 Gitlab 的服务器可能还要用于其他服务,所以这里的 80 和 22 端口将会修改成本地其他端口,而在宿主机上使用统一的 Nginx + SSL 来支持 SSL 化。虽然这样不需要把 SSL 证书挂载到容器内,但还是需要告诉容器启动 HTTPS (GITLAB_HTTPS)和实际使用的端口 (GITLAB_PORT 和 GITLAB_SSH_PORT),否则在 UI 上显示的将是 HTTP 和 容器内部的端口。

自动备份设置

  一般来说采用默认的“每天凌晨1点”策略自动备份即可,可以根据实际需求修改为不同的时间点或每周或每月。备份的压缩包会保存在 gitlab-data 目录下的 backups 目录里。

Pages 设置

  Pages 功能是 Gitlab CE 提供的一个非常实用的内部静态页面托管方案,不像 Github Pages 是完全公开的, Gitlab Pages 也可以根据 Gitlab 本身的用户验证来限制页面的访问权限。这里上面下载的配置文件中默认不包含 Pages 的配置,需要进行添加如下以 GITLAB_PAGES_ 开头的配置节。当开启 Pages 功能后,需要设定好对应的域名(建议二级域名,非主域名),并将该域名对应的通配符解析到 Gitlab 服务器。解析好后,普通项目的访问 URL 是 https://{username}.pages.example.com/{project},比如项目 zhonger/zhonger 的 Pages 访问 URL 为 https://zhonger.pages.example.com/zhonger。如果项目名与前面的域名完全对应,那么就可以直接使用域名访问,无须带子目录,比如项目 zhonger/zhonger.pages.example.com 的 Pages 使用 https://zhonger.pages.example.com 访问即可。

相对路径配置

  如果想要把 Gitlab 服务与其他服务共用一个域名对外提供服务,那么就需要设置相对路径 GITLAB_RELATIVE_URL_ROOT。比如想要使用 https://example.com/git 的 URL 访问 Gitlab 服务,那么就需要将相对路径的配置内容设为 /git。这里笔者没有这个需求,所以置空即可。注意,当使用相对路径配置时升级前最好将相对路径置空,待正常升级后再重新设置相对路径编译生效。编译过程可能比较长,大约十分钟左右。

初始 root 密码配置

  GITLAB_ROOT_PASSWORD 配置节为初始管理员用户(root)密码,如果置空则为 Gitlab 官方常用默认密码 5iveL!fe

version: '2.3'

services:
  redis:
    restart: always
    image: redis:6.2
    command:
    - --loglevel warning
    volumes:
    - ./redis-data:/data

  postgresql:
    restart: always
    image: sameersbn/postgresql:12-20200524
    volumes:
    - ./postgresql-data:/var/lib/postgresql
    environment:
    - DB_USER=gitlab
    - DB_PASS=password
    - DB_NAME=gitlabhq_production
    - DB_EXTENSION=pg_trgm,btree_gist

  gitlab:
    restart: always
    image: sameersbn/gitlab:14.1.1
    depends_on:
    - redis
    - postgresql
    ports:
    - "10080:80"
    - "10022:22"
    volumes:
    - ./gitlab-data:/home/git/data
    healthcheck:
      test: ["CMD", "/usr/local/sbin/healthcheck"]
      interval: 5m
      timeout: 10s
      retries: 3
      start_period: 5m
    environment:
    - DEBUG=false

    - DB_ADAPTER=postgresql
    - DB_HOST=postgresql
    - DB_PORT=5432
    - DB_USER=gitlab
    - DB_PASS=password
    - DB_NAME=gitlabhq_production

    - REDIS_HOST=redis
    - REDIS_PORT=6379

    - TZ=Asia/Shanghai
    - GITLAB_TIMEZONE=Beijing

    - GITLAB_HTTPS=true
    - SSL_SELF_SIGNED=false

    - GITLAB_HOST=localhost
    - GITLAB_PORT=10080
    - GITLAB_SSH_PORT=10022
    - GITLAB_RELATIVE_URL_ROOT=
    - GITLAB_SECRETS_DB_KEY_BASE=fd6d127c-f4ce-11eb-8163-1e008a0e6985
    - GITLAB_SECRETS_SECRET_KEY_BASE=03221924-f4cf-11eb-a75e-1e008a0e6985
    - GITLAB_SECRETS_OTP_KEY_BASE=07d5caec-f4cf-11eb-ab3b-1e008a0e6985

    - GITLAB_ROOT_PASSWORD=root@root
    - GITLAB_ROOT_EMAIL=root@example.com

    - GITLAB_NOTIFY_ON_BROKEN_BUILDS=true
    - GITLAB_NOTIFY_PUSHER=false

    - GITLAB_EMAIL=notifications@example.com
    - GITLAB_EMAIL_REPLY_TO=noreply@example.com
    - GITLAB_INCOMING_EMAIL_ADDRESS=reply@example.com

    - GITLAB_BACKUP_SCHEDULE=daily
    - GITLAB_BACKUP_TIME=01:00

    - GITLAB_SHARED_DIR=/home/git/data/shared
    - GITLAB_PAGES_ENABLED=true
    - GITLAB_PAGES_DOMAIN=pages.example.com
    - GITLAB_PAGES_DIR=$GITLAB_SHARED_DIR/pages
    - GITLAB_PAGES_PORT=80
    - GITLAB_PAGES_HTTPS=true

    .....

启动与测试

  在 ~/web/gitlab/ 目录使用以下命令拉取并启动所有容器实例。如果已经在宿主机的 Nginx 上预先配置好 Gitlab 访问域名 git.example.com 以及 SSL 证书,则可以使用 https://git.example.com 直接访问启动好的 Gitlab。

docker-compose up -d

Gitlab Runner

  Gitlab CE 提供的 Pages 功能必须与 Gitlab Runner 一起联合使用,否则源代码无法编译成静态页面,从而无法正常提供 Pages 功能。虽然 Gitlab Runner 有好几种方式,但是为了避免对于服务器的环境的破坏和支持更多的源码编译环境,这里建议采用 docker 方式启动 Gitlab runner。

启动

  使用以下命令拉取并启动 gitlab-runner 的最新镜像。

docker run -d --name gitlab-runner --restart always \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -n gitlab-runner \
  gitlab/gitlab-runner:latest

配置

# 进入 gitlab-runner 容器
docker exec -ti gitlab-runner register
# 根据提示输入内容
# 输入 Gitlab 服务的 URL,比如 https://git.example.com
# 输入 Token,可以在 Gitlab 的管理员配置的 Runner 里面看到
# 输入描述,根据实际自行设定,可以是主机别名
# 输入与该 runner 绑定的标签 tag,可以输入一个或空格间隔多个,也可以置空(建议)
# 选择 runner executor 为 docker
# 输入默认的编译环境镜像,比如 python:alpine

平台运维

升级

升级路线规划

  前面已经提到过 Gitlab 的升级实际上是要遵循 Gitlab 官方提供的路线图的,即按版本逐步迭代升级,具体可以查看 Upgrade-paths。截止到文章撰写之时,Gitlab 的版本升级主要路线为:8.11.Z -> 8.12.0 -> 8.17.7 -> 9.5.10 -> 10.8.7 -> 11.11.8 -> 12.0.12 -> 12.1.17 -> 12.10.14 -> 13.0.14 -> 13.1.11 -> latest 13.12.Z -> latest 14.0.Z -> 14.1.Z -> latest 14.Y.Z。这里的 Z 指的是该主要版本的最后一个次要版本,Y 指的是最新的主要版本。Gitlab 的版本可以通过 Version-specific upgrading instructionsReleases 来确认。当然,在文档中我们也可以看到一些具体的版本升级路线的示例。如果我们当前的版本符合某一条升级路线,那么可以直接按照示例中的路线逐步升级。如果不符合则需要自行根据主要路线进行确认。

  这里我们以当前版本 13.2.6 想要升级到 13 主要版本的最后一个次要版本 13.12.4 为例进行尝试。首先查阅一下 sameersbn/docker-gitlab 的版本号,发现如下:

  根据以上的升级主要路线可知,从 13.2.6 版本到 13.12.4 其实一步到位升级也是可行的。但是实际上 13.2.6 版本对应的 postgresql 的版本还在 11,而从 13.7.0 版本之后开始升级到 12。根据 Gitlab 遵循的版本号命名规则,地位代表主要版本,第二位代表次要版本,第三位代表修补版本,并且每个次要版本的最后一个修补版本可以认为是稳定版本。在同一个主要版本内,从一个稳定次要版本升级到稳定次要版本被认为是稳定可行的。所以可以设定升级路线为:13.2.6 -> 13.7.4 -> 13.12.4。

边备份边升级

  Gitlab 的备份还原需要是在与备份对应的版本上做的。如果因为升级失败而需要使用旧版本的备份还原,需要移除失败版本所用的文件夹及文件,并启动一个对应版本的新实例然后进行备份还原。

# 首先对 13.2.6 版本进行备份
docker-compose run --rm gitlab app:rake gitlab:backup:create # 将会进行手动备份

# 修改 docker-compose.yml 中的 gitlab 镜像版本为 13.7.4,
# 同时修改 postgresql 的镜像版本为 12-20200524
# 销毁旧版本的实例集群并启动新的实例集群
docker-compose down && docker-compose up -d
# 等待镜像完成数据库迁移和前端样式库生成,并自动重启各项服务
# 查询应用实例的状态
docker logs gitlab_gitlab_1
# 访问界面,登录验证,确认实例升级成功

# 对 13.7.4 版本进行备份
docker-compose run --rm gitlab app:rake gitlab:backup:create # 将会进行手动备份

# 修改 dockerc-compose.yml 中的 gitlab 镜像版本为 13.12.4,
# 同时修改 redis 的镜像版本为 6.2,之前 redis 的镜像版本为 5.0.9
# 销毁旧版本的实例集群并启动新的实例集群
docker-compose down && docker-compose up -d
# 等待镜像完成数据库迁移和前端样式库生成,并自动重启各项服务
# 查询应用实例的状态
docker logs gitlab_gitlab_1
# 访问界面,登录验证,确认实例升级成功

  如果在升级之后发现升级失败或新版本中某些组件有问题想要回滚,就需要利用刚才升级过程中提前备份好的数据还原备份了。

# 复制刚才备份的所有数据文件到当前工作目录
cp gitlab/backups/*.tar ./
# 销毁所有实例
docker-compose down
# 移除本地持久化文件夹及文件
sudo rm -rf gitlab postgresql redis

# 修改 docker-compose.yml 中的配置到想要回滚的备份数据对应的版本
# 此处注意三个镜像版本的对应修改
# 启动实例集群
docker-compose up -d
# 复制备份数据到新实例的备份文件夹
cp ./*.tar gitlab/backups
# 执行恢复数据操作
docker-compose run --rm gitlab app:rake gitlab:backup:restore # 将会看到可用备份列表

参考资料