使用GitLabCI+Docker部署博客

背景

起初本站点源码方式在 GitHub 上,后来希望通过 CI/CD 的方式能够时博客能够自动化部署。

同时又想要使用 Docker 这种容器化方案,使我的服务器能够更加方便部署其他应用。

经过调研 GitHub 虽然可以通过 Actions 的形式将代码进行发布,但一般都是发布到 GitHub Pages 上。

如果想要使用 Docker 并且能够自动发布的话,需要在脚本中配置连接到我的服务器上,这些敏感信息放在公开仓库中是不合适的,而 GitHub 由于评论的原因无法变成私有仓库。 于是就计划将代码仓库从 GitHub 迁移到 GitLab 私有仓库中。

当然最主要的原因是 Gitlab 可以使用自己的机器作为 Runner 运行,更方便部署完整的 CI/CD 流程。

部署教程

整个 Docker + GitLab Runner 安装流程我是参考的以下两篇文章:

使用 docker 快速部署 hexo 博客

使用 Docker 部署 GitLab-CI-Runner

一些“坑”

在部署的过程中遇到了一些问题。

GitLab 私有 Runner

GitLab 使用的是 SAAS 版本,默认的 Runner 是由对方提供的共享 Runner,而使用这个是需要花钱的,所以才有上面文章中需要部署私有 Runner。

但即使注册了私有 Runner,在运行 CI 时发现还是默认使用的公共的 Runner。

首先需要在 CI 文件中将所有的任务都指定 tags,即私有 Runner 的 tag:

1
2
3
4
build:
stage: build
tags:
- docker-node

同时确认仓库 settings -> CI/CD -> Runners 中 Specific Runner 已经部署成功且 Shared runners 处于关闭状态。

runner.png

Docker 镜像无法构建

由于使用了 Docker 容器作为 Runner,在构建应用时采用的是 DinD 方案,而按照上面的文章无法正常构建代码,会报以下错误:

Cannot connect to the Docker daemon at tcp://127.0.0.1:2375/. Is the docker daemon running?

当发现类似无法连接到 Docker 服务的类似的错误的时候,需要修改配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
build:
stage: build
tags:
- docker-node
image: docker:20-dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
CUST_APP_IMAGE_NAME: xxx
services:
- name: docker:20-dind
command: ["--tls=false"]
before_script:
- echo ${CUST_DOCKER_PASS} | docker login ${CUST_DOCKER_HOST} -u ${CUST_DOCKER_USER} --password-stdin
script:
# - npm run build
- docker build -t $CUST_APP_IMAGE_NAME .
- docker push $CUST_APP_IMAGE_NAME

具体原因可以参考这篇文章:https://gitlab.com/gitlab-org/gitlab-runner/-/issues/27300

免费容器镜像服务

方法一:可以是用阿里云的服务:选择个人示例即可

另外该仓库还可以自动关联 GitHub 或者 GitLab 仓库,当满足设定的条件时(tag 或者 branch 变动)自动构建镜像。使用这个能力,我们可以快速为项目构建基础镜像。

方法二:Gitlab 自带的 GitLab Container Registry,docker login registry.gitlab.com

该方式可以直接在 Gitlab 后台管理镜像,也是不错的选择。

关于 CI/CD 触发条件

起初我打算将 CI/CD 流程设定为只要 master 分支上有提交代码就自动部署。但在实际操作的过程中发现了部署的镜像无法生效,即使查看所有的构建日志发现一切都是正常的。

原因是使用 branch 来发布镜像势必导致镜像的 tag 是固定的,即使可以通过覆盖的方式更新镜像仓库中的容器。但 docker 在运行的时候是将镜像拉到本地再运行的,如果本地存在同名的镜像时,则会使用旧版本的镜像。

我们可以通过 docker images查看本地镜像,使用docker rmi 删除本地镜像。

也可以在启动容器前强制拉取一遍最新的镜像:docker pull xxx,或者使用--pull always参数。推荐使用第一种方式提前先拉取镜像

还有一种最保险的方式是通过 GitLab Tags 的形式发布代码,这样虽然稍微麻烦了一些,但可以使版本管理更加清晰,企业中的发布流程往往采用这种方式。

后来又发现了使用 rules:changes 来区分依赖构建和应用构建的方式,可以直接在 master 分支上完成构建部署,无需手动创建 tag,详细文章可以参考这篇文章:GitlabCI配置only-except的坑

发布 runner

构建镜像使用 DinD 方案,但发布时需要运行docker run命令启动 docker 服务。

经测试发现,虽然任务能够执行成功,但在宿主机上并没有看到相应的容器进程。

简单分析一下原因,应该是因为 DinD 方案任务也是一个容器,在容器中执行docker run相当于在容器中又启动了一个容器,然而构建任务结束之后容器会被销毁,那么容器中的服务自然也就结束了。

所以发布时需要在宿主机上启动容器,而不是在容器中启动容器。为了达到这个目的,我们需要再注册一个 Runner,该 Runner 通过 shell 的形式直接在宿主机上运行。

通过官方介绍的命令进行 Runner 的注册:

1
2
3
4
5
6
7
8
9
10
11
12
# Download the binary for your system
sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

# Give it permissions to execute
sudo chmod +x /usr/local/bin/gitlab-runner

# Create a GitLab CI user
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

# Install and run as service
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

注册 Runner 时选择shell模式。

再次执行发布脚本发现无法执行 docker 相关的命令:

Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker

原因是 docker 进程使用 Unix Socket 而不是 TCP 端口。而默认情况下,Unix socket 属于 root 用户,需要 root 权限才能访问。可以将 runner 的用户添加进 docker 用户组即可有权限访问:

1
sudo gpasswd -a gitlab-runner docker

shell 模式 runner 无法拉取仓库代码

使用上一节注册的 runner 发布代码时,第一次能正常执行,然而第二次之后就无法拉取 Git 仓库代码,报如下错误:

fatal: git fetch-pack: expected shallow list

原因是 centos 自带的 git 版本过低,没有相关的命令。

解决方案一:使用git clone替代git fetch拉取代码:

Gitlab CI 默认使用 fetch 的方式拉取最新代码,该 api 在 centos 上自带的 git 中不存在,可以使用 clone 的方式替代,但缺点是效率会比原本低很多。

git-fetch.png

解决方案二:升级最新版本的 git

经过测试使用 yum 命令无法使 git 升级到最新版本,所以这里才用源码安装的方式:

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
# 拉取源码
wget -O /tmp/git-2.34.1.tar.gz https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.24.1.tar.gz

# 安装编译依赖
yum install -y curl-devel expat-devel gettext-devel openssl-devel zlib-devel gcc perl-ExtUtils-MakeMaker

# 解压
tar -zxf /tmp/git-2.34.1.tar.gz -C /tmp/

cd /tmp/git-2.34.1

# 检验相关依赖,设置安装路径
./configure --prefix=/usr/local/git

# 编译安装
make && make install

# 删除已有的 git
yum remove git

# 配置环境变量
vim /etc/profile

# GIT_HOME
GIT_HOME=/usr/local/git
export PATH=$PATH:$GIT_HOME/bin

# 刷新
source /etc/profile

安装完成后重新执行任务即可。

完整 CI/CD 流程

gitlab+dind 设计 ci_cd.jpg

.gitlab-ci.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
80
81
82
83
84
85
86
87
88
89
variables:
CUST_DOCKER_HOST: registry.cn-hangzhou.aliyuncs.com
CUST_DOCKER_REPO: repo
CUST_DOCKER_USER: user
CUST_DOCKER_PASS: password
CUST_PORT: 9000
CUST_PORT_APP: 9000

stages:
- build-dep
- build
- release

# 构建依赖
build-dep:
stage: build-dep
tags:
- docker-node
image: docker:20-dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
CUST_APP_IMAGE_NAME: $CUST_DOCKER_HOST/$CUST_DOCKER_REPO/$CI_PROJECT_NAME:$CI_COMMIT_REF_NAME
services:
- name: docker:20-dind
command: ["--tls=false"]
before_script:
- echo ${CUST_DOCKER_PASS} | docker login ${CUST_DOCKER_HOST} -u ${CUST_DOCKER_USER} --password-stdin
script:
- export CUST_APP_IMAGE_NAME=${CUST_APP_IMAGE_NAME//master/dep-latest} #将 master 转换成 dep-latest, tag 模式不变
- docker build -f ./Dockerfile-dep -t $CUST_APP_IMAGE_NAME .
- docker push $CUST_APP_IMAGE_NAME
rules:
# 自动模式
- if: $CI_COMMIT_REF_NAME == "master"
changes:
- package.json
- Dockerfile
- Dockerfile-dep
# tag模式
- if: $CI_COMMIT_REF_NAME =~ /^dep-.*$/

# 构建应用
build:
stage: build
tags:
- docker-node
image: docker:20-dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
CUST_APP_IMAGE_NAME: $CUST_DOCKER_HOST/$CUST_DOCKER_REPO/$CI_PROJECT_NAME:$CI_COMMIT_REF_NAME
services:
- name: docker:20-dind
command: ["--tls=false"]
before_script:
- echo ${CUST_DOCKER_PASS} | docker login ${CUST_DOCKER_HOST} -u ${CUST_DOCKER_USER} --password-stdin
script:
- docker build -t $CUST_APP_IMAGE_NAME .
- docker push $CUST_APP_IMAGE_NAME
rules:
# 自动模式
- if: $CI_COMMIT_REF_NAME == "master"
# tag模式
- if: $CI_COMMIT_REF_NAME =~ /^release-.*$/

# 发布
release:
stage: release
tags:
- shell-node
variables:
CUST_APP_IMAGE_NAME: $CUST_DOCKER_HOST/$CUST_DOCKER_REPO/$CI_PROJECT_NAME:$CI_COMMIT_REF_NAME
CUST_APP_NAME: $CI_PROJECT_NAMESPACE-$CI_PROJECT_NAME-$CI_ENVIRONMENT_NAME
environment:
name: release
url: https://www.wobushi.top/
script:
- docker pull $CUST_APP_IMAGE_NAME
- EXIST_CONTAINER_ID=`docker ps -af name=$CUST_APP_NAME --format "{{.ID}}"`
- if [ -n "$EXIST_CONTAINER_ID" ] ; then docker rm -f $EXIST_CONTAINER_ID; fi
- docker run -d --name $CUST_APP_NAME -p $CUST_PORT:$CUST_PORT_APP $CUST_APP_IMAGE_NAME
rules:
# 自动模式
- if: $CI_COMMIT_REF_NAME == "master"
# tag模式
- if: $CI_COMMIT_REF_NAME =~ /^release-.*$/

使用GitLabCI+Docker部署博客
https://www.wobushi.top/2021/使用GitLabCI+Docker部署博客/
作者
Pride Su
发布于
2021年12月24日
许可协议