前言
上篇文章《通过 GitLab Runner 实现 CI/CD 工作流(上)》我们讲解了 GitLab Runner 的部署和配置优化,这次我们来进行一次实战演练,对一个完整的前后端分离的项目进行讲解。这个项目分为前端和后端两个项目,他们有独自的代码仓库,对于不同仓库的代码提交,会触发对应的项目的代码编译、镜像构建、镜像发布与项目部署。
架构
我们先来设计下项目的整体架构:
项目需要运行四个容器,分别是反向代理、前端项目、后端项目和数据库,他们都在同一个 bridge
类型的 docker 网络下,只有反向代理容器暴露端口提供对外访问,其他容器均为 bridge 网络内部通信。
反向代理 Nginx 根据请求的 URL 将请求分发到前端项目或后端项目,前端项目将最终编译生成的静态资源打包在 Nginx 容器内提供访问,后端项目是使用 Go 编写的 API 服务,将编译好的二进制文件打包进 alpine 容器运行,MySQL 容器提供数据库访问。
CI / CD 流程
当我们为项目绑定好 Runner 之后,开发者提交代码到 GitLab 项目仓库,GitLab 根据项目中的 .gitlab-ci.yml
配置文件要求触发 Runner 工作。我们在 .gitlab-ci.yml
文件中这样定义了整个流程:构建应用镜像 -> 推送到镜像仓库 -> 触发服务器的自部署脚本 -> 服务器拉取新镜像、停止并移除旧容器、启动新容器。
部署策略
可以看到,我们的部署方式为停止旧版本应用并启用新版本应用,这种部署策略叫做 重建(recreate),这种更新应用的实现方式比较简单,但有一个显著的问题是,如果是线上生产服务器的部署,可能会出现短暂的流量中断的情况。
其他的部署更新策略还有蓝绿部署、滚动更新、灰度发布(金丝雀发布)等,如果使用 Kubernetes 进行集群容器应用管理,它自带了多种更新策略,会非常方便我们进行 CD。
请阅读参考这篇文章:《Kubernetes 部署策略详解》。
项目讲解
前端项目代码
前端项目是一个使用 Vue.js 脚手架的 SPA,另外我们还需要在项目中添加三个配置文件:
-
.gitlab-ci.yml
CI 流程 -
Dockerfile
镜像构建流程 -
default.conf
前端 Nginx 容器中的配置
三者的关系是:CI 过程中会调用 Dockerfile
进行镜像构建,镜像构建过程又会调用 default.conf
将文件写进镜像。
.gitlab-ci.yml
# 两个阶段:构建并发布镜像、部署(通过执行远程脚本的方式)
stages:
- build
- deploy
build_job:
stage: build
image: docker:latest
tags:
- jczh100
only:
- dev
script:
# 构建镜像(每个 Job 默认都会将项目代码 fetch 到 Job 容器中,这一步将用到 Dockerfile)
- docker build -t ucfe:latest .
# 登录镜像仓库
- docker login -u $DOCKER_REGISTRY_USERNAME -p $DOCKER_REGISTRY_PASSWORD $DOCKER_REGISTRY_ADDR
# 给镜像打标签
- docker tag ucfe:latest ${DOCKER_REGISTRY_ADDR}/xvrzhao/ucfe:latest
# 发布到镜像仓库
- docker push ${DOCKER_REGISTRY_ADDR}/xvrzhao/ucfe:latest
deploy_job:
stage: deploy
image: alpine:latest
variables:
# 跳过 git 操作,加快流水线执行速度,本 job 不需要获取仓库代码
GIT_STRATEGY: none
tags:
- jczh100
only:
- dev
before_script:
# 更换 alpine apk 源为阿里源
- sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
# 安装 sshpass,sshpass 可以将密码传入 ssh,免去交互式输入密码
- apk add --update --no-cache openssh sshpass
script:
# sshpass 参数:
# -p 将密码传入 ssh
#
# ssh 参数:
# -T 不申请伪终端
# -o 传入 ssh 配置项,这里传入了 StrictHostKeyChecking 参数,默认值为 ask,设置为 no 代表 ssh 自动将远程主机的 hostkeys 加入到 ~/.ssh/known_hosts,并且允许远程主机的 hostkeys 发生变化
- sshpass -p $TEST_SERVER_XVR_PASSWORD ssh -T -o StrictHostKeyChecking=no xvrzhao@$TEST_SERVER_IP 'bash /home/xvrzhao/cd-scripts/ucenter-fe.sh'
注意:为了避免一些敏感数据(比如密码等)暴露在代码库中,可以在 GitLab 项目仓库的 CI 设置中添加一些变量,只有项目维护者可以管理和查看这些变量。在.gitlab-ci.yml
可以通过$
符直接使用这些变量值。
Dockerfile
# node 作为基础镜像来执行前端项目的构建
FROM node:alpine AS builder
WORKDIR /app
# 根据镜像分层原理,先拷贝依赖文件,若依赖文件没有改动,则下一步安装依赖会直接利用缓存
COPY ./package.json ./
RUN npm install --registry=https://registry.npm.taobao.org
COPY ./ ./
RUN npm run build
# 将前端项目 dist 打包进 nginx 镜像
FROM nginx:alpine
COPY --from=builder /app/dist/ /dist/
# 修改前端 Nginx 配置(这一步使用到 default.conf)
COPY ./default.conf /etc/nginx/conf.d/
default.conf
# 前端项目 dist 打包到了 nginx 镜像,该文件为 nginx server 配置
server {
listen 80;
location ^~ /ucenter {
alias /dist;
}
}
关于 Nginx 提供静态资源访问的优化,请参考这篇文章:《Nginx 优化静态文件访问》。
部署脚本
通过 .gitlab-ci.yml
可以看到,在 deploy_job
中我们通过 SSH 远程执行了所要部署的服务器上的脚本。脚本会拉取在 build_job
中推送的最新镜像,然后停止并删除目前服务器上运行的容器,启动最新镜像。
ucenter-fe.sh
脚本内容:
#!/bin/bash
# 服务器 CD 脚本
# Author: Xavier Zhao <xvrzhao@gmail.com>
# Date: 2020/05/06
echo -e -n "\n开始执行部署 ...\n\n(1/4) 拉取最新版本镜像 ucfe:latest ...\t"
res=$(docker pull registry.cn-shanghai.aliyuncs.com/xvrzhao/ucfe:latest 2>&1)
if [ $? -eq 0 ]; then
echo "完成!"
else
echo -e "错误!\n\n报错信息:$res"
exit 1
fi
echo -e -n "(2/4) 停止旧版本容器 ucenter-fe ...\t"
res=$(docker stop ucenter-fe 2>&1)
if [ $? -eq 0 ]; then
echo "完成!"
else
echo -e "错误!\n\n报错信息:$res"
exit 1
fi
echo -e -n "(3/4) 移除旧版本容器 ucenter-fe ...\t"
res=$(docker rm ucenter-fe 2>&1)
if [ $? -eq 0 ]; then
echo "完成!"
else
echo -e "错误!\n\n报错信息:$res"
exit 1
fi
echo -e -n "(4/4) 启动新版本容器 ucenter-fe ...\t"
res=$(docker run -d --name ucenter-fe --network ucenter registry.cn-shanghai.aliyuncs.com/xvrzhao/ucfe:latest 2>&1)
if [ $? -eq 0 ]; then
echo "完成!"
else
echo -e "错误!\n\n报错信息:$res"
exit 1
fi
echo -e "\n部署完成!\n"
执行效果:
$ ./ucenter-fe.sh
开始执行部署 ...
(1/4) 拉取最新版本镜像 ucfe:latest ... 完成!
(2/4) 停止旧版本容器 ucenter-fe ... 完成!
(3/4) 移除旧版本容器 ucenter-fe ... 完成!
(4/4) 启动新版本容器 ucenter-fe ... 完成!
部署完成!
或
$ ./ucenter-fe.sh
开始执行部署 ...
(1/4) 拉取最新版本镜像 ucfe:latest ... 完成!
(2/4) 停止旧版本容器 ucenter-fe ... 错误!
报错信息:Error response from daemon: No such container: ucenter-fe
后端项目代码
// TODO
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。