前段时间微前端实践:single-spa+vite 的方式对项目进行了整合,也用 使用 Gitlab CI/CD 自动打包和部署微前端
基础概念
- jobs
- 定义在
pipeline中的单个任务
- 定义在
stage- 用于组合
job, 官方提供了一些默认值.prebuildtestdeploy.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- 设置
job运行的环境, 可以根据不同的环境来选择
- 设置
artifacts- 用于存储
job生成的文件或数据, 可以通过expire字段定义其过期时间
- 用于存储
cache- 用于缓存会复用文件或者文件夹, 比如包依赖, 打包工具之类的, 用以加速构建速度
实现思路
- 思路可以有很多种,这里就说下我试过的
- 最后的结果主应用和微应用会整合为一个文件,也就是只有一个 nginx,一个端口,一个镜像文件
git sub-modules
- 使用 git sub-modules 打包微前端是一种可行方案, 通过
git sub-modules由主应用绑定微应用 - 但是由于是单向的,所以只适合从主应用作为入口点来完成整个流程,这在测试和开发阶段是不太方便的,所以最后没有采用
仓库之间相互触发
- 微应用和主应用程序都可以作为入口点, 相互调用 pipeline 以完成整个构建任务
- 不管从哪个入口进入,进入的时候都应该记录当下环境信息或者其他业务逻辑相关的信息,比如版本信息可能从 commit 或者 tag 中提取, 这些信息后续部署的
job以及其他应用都可能会用到 - 任何上传了
artifacts的job都应该记录其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, 然后再根据子应用情况来决定是否重新打包替换文件)- => 根据子应用构建
job的id来收集子应用artifacts - => 根据环境和版本信息生成并存储
docker镜像的tag, 存储tag是因为在k8s部署的时候需要设置image的地址
- => 根据子应用构建
- => 主应用构建,(如果主应用没有变化,那可以以 tag 或者分支为凭据下载之前构建的
- => 从
build job的artifacts获取docker镜像的信息, 然后构建并推送docker镜像 - => 从
build job的artifacts里获取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
在多个仓库之间的交互方式
- 通过 GitLab API 在脚本中互相调用
pipeline(Trigger pipelines by using the API) web hook, 可以通过web hook监听pushmerge之类的事件来调用pipeline
如何在 job 或者 pipeline 之间分享变量
- 这里的变量指的是一些动态变量, 比如环境信息可能从
commit信息中提取, 或者从外部获取来的, 这些信息可能后面的job也会用到, 那就需要将这些变量传递下去
artifacts
-
可以将变量存储在
artifacts中, gitlab 不会默认提供artifacts访问能力, 需要通过need或者dependencies配置允许保留上一个job的artifacts -
needs/dependencies这两个配置都可以用来定义 job 的执行顺序以及提供artifacts的访问能力,dependencies主要用于定义artifacts的依赖关系, 表明当前任务的运行依赖于某些任务的artifactsneeds用于定义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),