TONG-H

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

2.6k11Frontendmicro_frontendCI/CD2025-01-232025-02-23

前段时间微前端实践: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,避免主应用以及其他子应用重新打包
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
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 资源
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
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
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
#!/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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 (https://docs.gitlab.com/ee/ci/yaml/index.html#dependencies)
    1
    2
    3
    4
    5
    6
    7
    8
    # 保存
    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 之间共享变量

1
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

1
2
3
4
5
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),

QA

  • error: This job is stuck because the project doesn’t have any runners online assigned to it.
    • job are assigned by tag, or enable the runner to run without tags
  • error: could not read Username for ‘https://gitlab.com‘: No such device or address on Gitlab CI
    • make sure Allow access to this project with a CI_JOB_TOKEN switch is enabled