4

前言

上篇文章《通过 GitLab Runner 实现 CI/CD 工作流(上)》我们讲解了 GitLab Runner 的部署和配置优化,这次我们来进行一次实战演练,对一个完整的前后端分离的项目进行讲解。这个项目分为前端和后端两个项目,他们有独自的代码仓库,对于不同仓库的代码提交,会触发对应的项目的代码编译、镜像构建、镜像发布与项目部署。

架构

我们先来设计下项目的整体架构:

项目架构

项目需要运行四个容器,分别是反向代理、前端项目、后端项目和数据库,他们都在同一个 bridge 类型的 docker 网络下,只有反向代理容器暴露端口提供对外访问,其他容器均为 bridge 网络内部通信。

反向代理 Nginx 根据请求的 URL 将请求分发到前端项目或后端项目,前端项目将最终编译生成的静态资源打包在 Nginx 容器内提供访问,后端项目是使用 Go 编写的 API 服务,将编译好的二进制文件打包进 alpine 容器运行,MySQL 容器提供数据库访问。

CI / CD 流程

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

Xavier
448 声望28 粉丝

最近的关注重心: