前端部署十四: 构建功能分支测试环境

Preview and Environments

关于 Preview,我在前几篇文章提到过几次,即每一个功能分支都配有对应的测试环境

PS: 如果不了解 Preview 的话,可以看看我在 cra-deploy 的一个 PR #21

项目研发的从开发到上线,一般可以可以划分为三个环境

  1. local:本地环境,把项目 git clone 到自己的工作笔记本或者开发机中,在 localhost:8080 类似的地址进行调试与开发。此时环境的面向对象主要是开发者。
  2. dev:测试环境,本地业务迭代开发结束并交付给测试进行功能测试的环境,在 dev.shanyue.tech 类似的二级域名进行测试。此时环境的面向对象主要是测试人员。
  3. prod:生产环境,线上供用户使用的环境,在 shanyue.tech 类似的地址。此时环境的面向对象主要是用户。

这里我们增加一个功能分支测试环境,对应于 feature 分支。每个 feature 分支都会有一个环境,一个特殊的测试环境。如对功能 feature-A 的开发在 feature-A.cra.dev.shanyue.tech 进行测试。

本篇文章将实践对 cra-deployfeature-preview 分支部署在 feature-preview.cra.shanyue.tech 中作为示例。

那实现多分支环境部署?

基于 docker/compose 进行部署

回忆之前关于部署的章节内容,我们可以根据以下 docker-compose.yaml 进行部署,并配置为 cra.shanyue.tech

version: "3"
services:
  domain:
    build:
      context: .
      dockerfile: router.Dockerfile
    labels:
      # 为 cra 配置我们的自定义域名
      - "traefik.http.routers.cra.rule=Host(`cra.shanyue.tech`)"
      # 设置 https,此时我们的 certresolver 为 le,与上篇文章配置保持一致
      - traefik.http.routers.cra.tls=true
      - traefik.http.routers.cra.tls.certresolver=le
 
networks:
  default:
    external:
      name: traefik_default

则仔细一思索,不难得出 docker-compose 的解决方案。

  1. 对不同分支根据分支名配置不同的 service
  2. 对每个 service 根据分支名配置响应的 labels

回忆之前关于 CI 的章节内容,我们在构建服务器中,可通过环境变量获取到当前仓库的当前分支,我们基于分支名称进行功能分支环境部署。

假设 COMMIT_REF_NAME 为指向当前分支名称的环境变量,如此

version: "3"
services:
  cra-preview-${COMMIT_REF_NAME}:
    build:
      context: .
      dockerfile: router.Dockerfile
    labels:
       # 配置域名: Preview
       - "traefik.http.routers.cra-preview-${COMMIT_REF_NAME}.rule=Host(`${COMMIT_REF_NAME}.cra.shanyue.tech`)"
       - traefik.http.routers.cra-preview-${COMMIT_REF_NAME}.tls=true
       - traefik.http.routers.cra-preview-${COMMIT_REF_NAME}.tls.certresolver=le
 
# 一定要与 traefik 在同一网络下
networks:
  default:
    external:
      name: traefik_default

大功告成,但还有一点问题: 在 Service Name 上无法使用环境变量

$ docker-compose -f preview.docker-compose.yaml up
WARNING: The COMMIT_REF_NAME variable is not set. Defaulting to a blank string.
ERROR: The Compose file './preview.docker-compose.yaml' is invalid because:
Invalid service name 'cra-app-${COMMIT_REF_NAME}' - only [a-zA-Z0-9\._\-] characters are allowed

docker-compose.yaml 中的环境变量替换

docker-compose.yaml 中不支持将 Service 作为环境变量,因此 docker-compose up 启动容器失败。

我们可以写一段脚本将文件中的环境变量进行替换,但完全没有这个必要,因为有一个内置于操作系统的命令 envsubst 专职于文件内容的环境变量替换

PS: 如果系统中无自带 envsubst 命令,可使用第三方 envsubst 进行替代。

注意,以下命令中的 COMMIT_REF_NAME 环境变量为当前分支名称,可通过 git 命令获取。而在 CI 当中,可直接通过 CI 相关环境变量获得,无需通过命令。

$ cat preview.docker-compose.yaml | COMMIT_REF_NAME=$(git rev-parse --abbrev-ref HEAD) envsubst
version: "3"
services:
  cra-preview-feature-preview:
    build:
      context: .
      dockerfile: router.Dockerfile
    labels:
       # 配置域名: Preview
       - "traefik.http.routers.cra-preview-master.rule=Host(`cra.master.shanyue.tech`)"
       - traefik.http.routers.cra-preview-master.tls=true
       - traefik.http.routers.cra-preview-master.tls.certresolver=le
 
# 一定要与 traefik 在同一网络下
networks:
  default:
    external:
      name: traefik_default
 
# 将代理文件进行环境变量替换后,再次输出为 temp.docker-compose.yaml 配置文件
$ cat preview.docker-compose.yaml | COMMIT_REF_NAME=$(git rev-parse --abbrev-ref HEAD) envsubst > temp.docker-compose.yaml
 
# 根据配置文件启动容器服务
$ docker-compose -f temp.docker-compose.yaml up --build

environment

在我们实施了 Preview/Production 后,我们希望可以看到在 PR 的评论或者其它地方可以看到我们的部署地址。这就是 Environtment

在 CI 中配置 environment 为期望的部署地址,则可以在每次部署成功后,便可以看到其地址。

environment:
  name: review/$COMMIT_REF_NAME
  url: http://$COMMIT_REF_NAME.cra.shanyue.tech

而在 Github 中,你甚至可以看到每个 Environment 的部署历史。

Deployment History

基于 CICD 的多分支部署

在 CICD 中可根据环境变量获取当前分支名,详情可参考上一篇文章: CI 中的环境变量

在 Gitlab CI 中可以通过环境变量 CI_COMMIT_REF_SLUG 获取,该环境变量还会做相应的分支名替换,如 feature/Afeature-a 的转化。

在 Github Actions 中可以通过环境变量 GITHUB_REF_NAME/GITHUB_HEAD_REF 获取。

CI_COMMIT_REF_SLUG: $CI_COMMIT_REF_NAME lowercased, shortened to 63 bytes, and with everything except 0-9 and a-z replaced with -. No leading / trailing -. Use in URLs, host names and domain names.

Github Actions Default Environment Variables

以下是一个基于 github actions 的多分支部署的简单示例:

PS: 该 CI 配置位于 cra-deploy/preview.yaml

# 为了试验,此处作为单独的 Workflow,在实际工作中可 Install -> Lint、Test -> Preview 串行检验
name: Preview
 
# 执行 CI 的时机: 当 git push 到 feature-* 分支时
on:
  push:
    branches:    
      - feature-*
 
# 执行所有的 jobs
jobs:
  preview:
    # 该 Job 在自建的 Runner 中执行
    runs-on: self-hosted
    environment:
      # 获取 CICD 中的变量: Context
      # https://docs.github.com/en/actions/learn-github-actions/expressions
      name: preview/${{ github.ref_name }}
      url: https://${{ github.ref_name	}}.cra.shanyue.tech
    steps:
      # 切出代码,使用该 Action 将可以拉取最新代码
      - uses: actions/checkout@v2
      - name: Preview
        run: |
          cat preview.docker-compose.yaml | envsubst > docker-compose.yaml
          docker-compose up --build -d cra-preview-${COMMIT_REF_NAME}
        env:
          COMMIT_REF_NAME: ${{ github.ref_name }}

以下是一个基于 gitlab CI 的多分支部署的简单示例:

deploy-for-feature:
  stage: deploy
  only:
    refs:
      - /^feature-.*$/
  script:
    # 在 CI 中可直接修改为 docker-compose.yaml,因在 CI 中都是一次性操作
    - cat preview.docker-compose.yaml | envsubst > docker-compose.yaml
    - docker-compose up --build -d
  # 部署环境展示,可在 Pull Request 或者 Merge Request 中直接查看
  environment:
    name: review/$CI_COMMIT_REF_NAME
    url: http://$CI_COMMIT_REF_SLUG.cra.shanyue.tech

自动 Stop Preview

当新建了一个功能分支,并将它 push 到仓库后,CI 将在测试环境部署服务器将会自动启动一个容器。即便该分支已被合并,然而该分支对应的功能分支测试地址仍然存在,其对应的容器也仍然存在。

而当业务迭代越来越频繁,功能分支越来越多时,将会有数十个容器在服务器中启动,这将造成极大的服务器资源浪费。

当然,我们可以将已经合并到主分支的功能分支所对应的容器进行手动停止,但是不够智能。

我们可以通过 CI 做这件事情: 当 PR 被合并后,自动将该功能分支所对应的 Docker 容器进行关停

PS: 该 CI 配置位于 cra-deploy/stop-preview.yaml

PS2: Stop Preview 所对应的 Action

# 为了避免服务器资源浪费,每次当 PR 被合并或者关闭时,自动停止对应的 Preview 容器
name: Stop Preview
 
on:
  pull_request:
    types:
      # 当 feature 分支关闭时,
      - closed
 
jobs:
  stop-preview:
    runs-on: self-hosted
    steps:
      # - name: stop preview
      #   # 根据 Label 找到对应的容器,并停止服务,因为无需代码,所以用不到 checkout
      #   run: docker ps -f label="com.docker.compose.service=cra-preview-${COMMIT_REF_NAME}" -q | xargs docker stop
 
      - uses: actions/checkout@v2
      - name: stop preview
        run: |
          cat preview.docker-compose.yaml | envsubst > docker-compose.yaml
          docker-compose stop
        env:
          COMMIT_REF_NAME: ${{ github.head_ref }}

基于 k8s 的多分支部署

PS: 本段内容需要对 k8s 有基本概念的了解,比如 DeploymentPodService。可在下一篇章进行了解。

  • Deployment: 对 cra-deploy 项目的部署视作一个 Deployment
  • Service: 对 cra-deploy 提供可对集群或外网的访问地址

如此一来

  1. 根据分支名作为镜像的 Tag 构建镜像。如 cra-deploy-app:feature-A
  2. 根据带有 Tag 的镜像,对每个功能分支进行单独的 Deployment。如 cra-deployment-feature-A
  3. 根据 Deployment 配置相对应的 Service。如 cra-service-feature-A
  4. 根据 Ingress 对外暴露服务并对不同的 Service 提供不同的域名。如 feature-A.cra.shanyue.tech

配置文件路径位于 k8s-preview-app.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: cra-deployment-${COMMIT_REF}
spec:
  selector:
    matchLabels:
      app: cra
  replicas: 3
  template:
    metadata:
      labels:
        app: cra
    spec:
      containers:
      - name: cra-deploy
        image: cra-deploy-app:${COMMIT_REF}
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
 
---
 
apiVersion: v1
kind: Service
metadata:
  name: cra-service-${COMMIT_REF}
spec:
  selector:
    app: cra
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

deploy 命令的封装

但是无论基于那种方式的部署,我们总是可以在给它封装一层来简化操作,一来方便运维管理,一来方便开发者直接接入。如把部署抽象为一个命令,我们这里暂时把这个命令命名为 deploydeploy 这个命令可能基于 kubectl/heml 也有可能基于 docker-compose

该命令最核心 API 如下:

$ deploy service-name --host :host

假设要部署一个应用 shanyue-feature-A,设置它的域名为 feature-A.dev.shanyue.tech,则这个部署前端的命令为:

$ deploy shanyue-feature-A --host feature-A.dev.shanyue.tech

现在只剩下了一个问题:找到当前分支。

作业

  • 初阶:你们公司中有没有对功能分支进行单独的测试环境,是如何实现的
  • 中阶:完成 Preview 的实现

小结

随着 CICD 的发展、对快速迭代以及代码质量提出了更高的要求,基于分支的分支测试环境则成为了刚需。

对���该环境的搭建,思路也很清晰

  1. 借用现有的 CICD 服务,如 github actions 或者 gitlab CI 获取当前分支信息
  2. 借用 Docker 快速部署前端或者后端,根据分支信息启动不同的服务,根据 Docker 启动服务并配置标签
  3. 根据容器的标签与当前 Git 分支对前端后端设置不同的域名

另外,这个基于容器的思路不仅仅使用于前端,同样也适用于后端。而现实的业务中复杂多样,如又分为已下几种,这需要在项目的使用场景中灵活处理。

  • feature-A 的前端分支对应 feature-A 的后端分支环境
  • feature-A 的前端分支对应 develop 的后端分支环境
  • feature-A 的前端分支对应 master 的后端分支环境