背景 起初本站点源码方式在 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 处于关闭状态。
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: - 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 经过测试使用 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 yum remove git vim /etc/profile GIT_HOME=/usr/local/gitexport PATH=$PATH :$GIT_HOME /binsource /etc/profile
安装完成后重新执行任务即可。
完整 CI/CD 流程
.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} - 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 - 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" - 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" - if: $CI_COMMIT_REF_NAME =~ /^release-.*$/