使用 Gitlab CI/CD 自动打包和部署微前端

313 阅读5分钟

前段时间微前端实践:single-spa+vite 的方式对项目进行了整合,也用 使用 Gitlab CI/CD 自动打包和部署微前端

基础概念

  • jobs
    • 定义在 pipeline 中的单个任务
  • stage
    • 用于组合 job, 官方提供了一些默认值.pre build test deploy .post, 除了这些默认值以外, 可以通过全局关键字 stages 自定义
    • stage 是从上到下按序执行, stage 中的 job 是并行运行的, 可以通过 needs / dependencies 更改
  • pipeline
    • 是一组 job 的集合, 也代表 CI/CD 处理流程, 这些 job 可以并行/按顺序运行
    • 可以通过多种方式触发, 触发来源可通过 CI_PIPELINE_SOURCE 获取 (ci_pipeline_source)
  • runner
    • 一个应用程序, 在服务器安装以及通过 token 注册之后, 可以监听以及运行分配给它的 job
    • 可以通过安装 gitlab 实例/群组/项目级别的 runner, 以及在 job 中定义 tag 等来管理 runner 以及分配任务
  • executor
  • artifacts
    • 用于存储 job 生成的文件或数据, 可以通过 expire 字段定义其过期时间
  • cache
    • 用于缓存会复用文件或者文件夹, 比如包依赖, 打包工具之类的, 用以加速构建速度

实现思路

  • 思路可以有很多种,这里就说下我试过的
  • 最后的结果主应用和微应用会整合为一个文件,也就是只有一个 nginx,一个端口,一个镜像文件

git sub-modules

  • 使用 git sub-modules 打包微前端是一种可行方案, 通过 git sub-modules 由主应用绑定微应用
  • 但是由于是单向的,所以只适合从主应用作为入口点来完成整个流程,这在测试和开发阶段是不太方便的,所以最后没有采用

仓库之间相互触发

  • 微应用和主应用程序都可以作为入口点, 相互调用 pipeline 以完成整个构建任务
  • 不管从哪个入口进入,进入的时候都应该记录当下环境信息或者其他业务逻辑相关的信息,比如版本信息可能从 commit 或者 tag 中提取, 这些信息后续部署的 job 以及其他应用都可能会用到
  • 任何上传了 artifactsjob 都应该记录其 CI_JOB_ID, 用于后续其他应用下载其 artifacts

微应用发生了变更,以微应用作为入口触发

  • => 构建微应用并上传 artifacts(打包后的结果)
  • => 触发主应用 pipeline, 可以指定主应用的稳定版本 artifacts id,避免主应用以及其他子应用重新打包
image: node:20.16.0
variables:
  # 为了避免主应用以及其他子应用重新打包,可以指定主应用的稳定版本 artifact id,也可以不传让主应用取最新的 artifacts
  SPECIFIED_BASE_JOB_ID: 9044
cache:
  paths:
    - ./node_modules

frontend_build:
  stage: build
  tags:
    - frontend_build
  artifacts:
    name: "app1"
    paths:
      - app1 # 构建后的文件夹名称
      - shared_vars.env # 需要分享的变量
    expire_in: 5 days
  script:
    - echo "BUILD_JOB_ID=$CI_JOB_ID" > shared_vars.env
    - |- 
        if [ "$ENV" ]; then
          # 如果 env 存在,那么子应用的 pipeline 是从外部触发的
          echo "ENV from external";
        else 
          # 反之,那就从 commit 信息中提取
          if [[ $CI_COMMIT_TAG =~ ^PRO.* ]]; then 
            ENV="pro";
          else
            ENV="dev";
          fi;
        fi;
        echo "ENV=$ENV" >> shared_vars.env
    # 开始打包
    - npm i
    - npm run build:$ENV

frontend_base_trigger:
  stage: build
  dependencies:
    - frontend_build
  needs: ["frontend_build"]
  tags:
    - frontend_build
  script:
    - source shared_vars.env
    - echo "trigger base app pipeline with $ENV"
    - if [ -z "$BASE_REF" ]; then BASE_REF="master"; fi; echo $BASE_REF;
    # 通过当前 job token,主应用的 ref 以及仓库 id 为凭据调用 pipline
    # 如果不需要打包主应用,那么就通过 SPECIFIED_BASE_JOB_ID 指定稳定版本的 artifacts
    - curl -v POST
     --form token=$CI_JOB_TOKEN
     --form ref=$BASE_REF
     --form "variables[PRE_JOB_ID]=$BUILD_JOB_ID"
     --form "variables[PRE_PROJECT_ID]=$CI_PROJECT_ID"
     --form "variables[SPECIFIED_BASE_JOB_ID]=$SPECIFIED_BASE_JOB_ID"
     --form "variables[ENV]=$ENV"
     "https://gitlab.bicitech.cn/api/v4/projects/${BASE_PROJECT_ID}/trigger/pipeline"

主应用发生了变更,从主应用作为入口触发

  • 在生产环境中,以安全为考量因素,或者在开发测试阶段为了快速部署,大部分时候我们其实不需要现打包子应用,可以考虑以 tag 或者分支为凭据收集稳定版本的子应用的 artifacts
  • 需要现打包子应用
    • => 提取环境变量, 然后携带相关参数调用子应用 pipeline
      • => 打包单个子应用:等待子应用构建完成后, 再由子应用重新触发主应用 pipeline
      • => 同时打包多个子应用时:其实不太会碰到需要同时打包多个子应用的情况,因为每一个应用变更都会触发一次构建流程,那么最新的主应用 artifacts 就会更新
        • 但是如果有,可以在调用多个子应用 pipeline 后通过 scheduled pipeline 来定时检查子应用 pipeline 的构建情况

不管从哪个入口进入,最后都会到主应用来完成整个镜像打包以及部署流程

  • => 构建部署文件,在这个 job 里会生成所有需要的文件和信息,并作为 artifacts 上传
    • => 主应用构建,(如果主应用没有变化,那可以以 tag 或者分支为凭据下载之前构建的 artifacts, 然后再根据子应用情况来决定是否重新打包替换文件)
      • => 根据子应用构建 jobid 来收集子应用 artifacts
      • => 根据环境和版本信息生成并存储 docker 镜像的 tag, 存储 tag 是因为在 k8s 部署的时候需要设置 image 的地址
  • => 从 build jobartifacts 获取 docker 镜像的信息, 然后构建并推送 docker 镜像
  • => 从 build jobartifacts 里获取 docker 镜像的信息 , 更新 k8s 资源
frontend_build:
  stage: build
  tags:
    - frontend_build
  artifacts:
    name: "dist"
    paths:
      - dist/
      - shared_vars.env
    expire_in: 5 days
  before_script:
   # unzip 用于解压 artifacts
    - sudo apt update && apt install -y unzip
  script:
    - echo "$CI_PIPELINE_SOURCE-$PRE_NAME-$PRE_JOB_ID-$PRE_REF_NAME"
    - |- 
        if [ "$ENV" ]; then
          echo "ENV from external";
        else 
            if [[ $CI_COMMIT_TAG =~ ^PRO.* ]]; then 
              ENV="pro";
            else
              ENV="dev";
            fi;
        fi;
        if [ -z "$PRE_JOB_ID" ]; then
          export PRE_JOB_ID
          export PRE_PROJECT_ID
        fi;
        export ENV
        # 下载以及整合子应用
        bash ./getMicoFrontend.sh
        BUILD_IMAGE_PATH="${镜像地址}:${CI_JOB_ID}"
        declare -p ENV BUILD_JOB_ID BUILD_IMAGE_PATH SUB_BUILD_JOB_ID SUB_BUILD_PROJECT_ID > shared_vars.env
#!/bin/bash

base_project_id="your id"

download () {
  curl --location --output artifact.zip "https://gitlab.bicitech.cn/api/v4/projects/$2/jobs/$1/artifacts?job_token=$CI_JOB_TOKEN"
  if [ -f artifact.zip ]; then
    if [ "$2" == "$base_project_id" ]; then
      unzip -o "artifact.zip" -d "./"
    else
      mkdir "dist/micoFrontendApps"
      unzip -o "artifact.zip" -d "dist/micoFrontendApps"
    fi
  fi
}
if [ "$SPECIFIED_BASE_JOB_ID" ]; then 
  download $SPECIFIED_BASE_JOB_ID $base_project_id
else 
  npm i
  npm run build:${ENV}
fi

if [ "$PRE_JOB_ID" ]; then 
  download $PRE_JOB_ID $PRE_PROJECT_ID
fi

构建 docker 镜像

官方提供多种方式, 这里就只列举了两个尝试过的

  • 如果运行器的执行器也是 docker, 在 docker 中构建 docker, (using_docker_build)
  • 使用 kaniko 更简单些 (using_kaniko), kaniko 不要求 Docker daemon 以及 privileged mode, 适合一些无法运行 Docker 的场景, 比如 Kubernetes 或者 CI/CD pipelines
  frontend_image_build:
    stage: deploy
    dependencies:
      - frontend_build
    tags:
      - frontend_deploy
    image:
      name: gcr.io/kaniko-project/executor:v1.9.0-debug
      entrypoint: [""]
    script:
      - source ./shared_vars.env
      # 授权: 将用户名密码等授权信息 base64 处理之后保存在 `/kaniko/.docker/config.json`
      - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64)\"}}}" > /kaniko/.docker/config.json
      - | 
        if [ -e "${CI_PROJECT_DIR}/dist" ]; then
          # 打包镜像
          /kaniko/executor --context "${CI_PROJECT_DIR}" --dockerfile "${CI_PROJECT_DIR}/Dockerfile" --destination "${BUILD_IMAGE_PATH}"
        else
          echo "dist dir is not exist"
          exit 1
        fi

使用 k8s 部署

frontend_image_deploy:
  stage: deploy
  dependencies:
    - frontend_build
    - frontend_image_build
  tags:
    - frontend_deploy
  image: registry.cn-hangzhou.aliyuncs.com/haoshuwei24/kubectl:1.16.6
  script:
    - source ./shared_vars.env
    - mkdir -p $HOME/.kube
    - echo "$KUBE_CONFIG" > $HOME/.kube/config  # 解码并配置 Kubernetes 认证
    - sed -e "s~\${BUILD_IMAGE_PATH}~$BUILD_IMAGE_PATH~g" ./k8s.yaml > ./k8s_copy.yaml # 替换 k8s 配置中的镜像地址
    - kubectl apply -f ./k8s_copy.yaml

在多个仓库之间的交互方式

如何在 job 或者 pipeline 之间分享变量

  • 这里的变量指的是一些动态变量, 比如环境信息可能从 commit 信息中提取, 或者从外部获取来的, 这些信息可能后面的 job 也会用到, 那就需要将这些变量传递下去

artifacts

  • 可以将变量存储在 artifacts 中, gitlab 不会默认提供 artifacts 访问能力, 需要通过 need 或者 dependencies 配置允许保留上一个 jobartifacts

  • needs / dependencies 这两个配置都可以用来定义 job 的执行顺序以及提供 artifacts 的访问能力,

    • dependencies 主要用于定义 artifacts 的依赖关系, 表明当前任务的运行依赖于某些任务的 artifacts
    • needs 用于定义 job 的依赖关系, 这就会包含 artifacts 的访问权限
    • 由于同一个 stage 中的 job 是并行运行的, 所以在同一个 stage 中的 artifacts 的分享就需要使用 needs 而不是 dependencies (docs.gitlab.com/ee/ci/yaml/…)
      # 保存
      echo "ENV=$ENV" > shared_vars.env
      echo "REF=$REF" >> shared_vars.env
    # 保存多个
    declare -p VAR3 VAR4 >> shared_vars.env
      # 使用
      source shared_vars.env
    echo $ENV
    
  • artifacts 会被上传, 那么通过下载 artifacts 自然也能在 pipeline 之间共享变量

  curl --location --output artifact.zip "https://gitlab.example.cn/api/v4/projects/${CI_PROJECT_ID}/jobs/${CI_JOB_ID}/artifacts?job_token=$CI_JOB_TOKEN"

通过 pipeline api 携带

pipeline 可以通过 api 调用, api 的 variables 可以用来传递一些额外的变量, 这些变量拥有最高的优先权, 可以覆盖其他任何同名的变量, 这些变量在 job 详情页面也可以看到 pass-cicd-variables-in-the-api-call

curl --request POST \
     --form token=TOKEN \
     --form ref=main \
     --form "variables[UPLOAD_TO_S3]=true" \
     "https://gitlab.example.com/api/v4/projects/123456/trigger/pipeline"

变量相关 api

通过使用 GitLab CI/CD 项目/组级别的变量增删改相关的 api, 也可以达到在 job 或者 pipeline 动态传递变量的目的, 但是毕竟是全局变量,也可能有权限问题, 酌情使用 (project_level_variables),