基于docker部署微前端项目的入门实践指南🐋
🚢 前言
这是一篇讲述如何基于docker
部署微前端项目的文章。本文章会先从理论角度去描述部署方式,然后再通过代码去实现部署的逻辑和细节。
本文适合以下人群阅读:
- 刚认识
docker
,想通过实践进一步学习docker
的前端 er - 想了解如何用
docker
部署微前端项目的前端 er
本文中所部署的微前端项目是micro-fe,来源于之前写的一篇讲述如何接入微前端的文章给 vue-element-admin 接入 qiankun 的微前端开发实践总结 🍀。
🚀 阅前须知:关于微前端项目的仓库模式
关于微前端项目有两种仓库管理模式:
- 多仓库模式:主应用和子应用分别放在不同的仓库里。
- 单仓库模式:主应用和子应用分别放在同一个的仓库里,但分开放在不同的目录里。
本文推荐使用多仓库模式管理模式。因为多仓库模式比单仓库模式有以下优点:
-
git
独立:- 分支管理相对简洁:不会出现多个分支分别对应主应用和子应用的开发特性。
- 提交记录不密集:因为只有一个应用的提交而不是所有应用的提交。
- 创建
Release
或label
不混乱:如果存在创建Release
或label
的操作,则需要加上前缀去分辨不同应用,不然会出现多个同名的版本。
-
CICD
流水线独立:只有对应当前应用的流水线,不会出现多条对应不同应用的流水线。且如果使用单仓库模式,则流水线的触发条件需要更加复杂,不能出现一个应用更新触发流水线导致所有应用进入CICD
环节。
在下面的部署教程中,会以多仓库模式为前提来进行分析讲述。
🚠 当基于 docker 部署单体前端项目时,我们是怎么做的
所谓学会走才能学会跑。在学习部署微前端项目之前,我们先了解一下怎么基于docker
部署单体前端项目。通常我们把打包好的前端制品文件放在服务器里的nginx
上,且对nginx
的配置文件里做路径配置,用于区分获取前后端数据的请求,如下代码所示:
server{
# 处理获取前端的请求
location / {
# 前端制品文件存放在 /usr/share/nginx/html 里
# 当nginx接收到路径名为/index.css的客户端请求时,把/usr/share/nginx/html/index.css文件返回给客户端
root /usr/share/nginx/html;
index index.html index.htm;
}
# 如果路径以/api/开头,则判断该请求是获取后端数据的请求,因此把该请求通过反响代理引往后端
# 当以普通路径前缀匹配时,最长路径会优先匹配,因此即使路径为/api/xx的请求也符合上面的路径格式,也会以此处规则进行处理
location /api/ {
# proxy_pass中定义后端的uri
# 当nginx接收到路径名为/api/login的客户端请求时,nginx会进行反向代理的过程:
# 1. 向host为http://backend的后端发出/api/login的请求,
# 2. 在接收到响应后,把响应返回给客户端
proxy_pass http://backend/;
}
}
在配置下,nginx
处理请求的流向效果如下所示:
那么,部署单体前端项目这一过程其实就是在服务器启动一个配置好路径分发和存放前端制品的nginx
容器(不一定非要nginx
,看团队的选择)。接下来通过代码来实现这一过程:
🚟 1. 提供 nginx
配置文件
在前端代码项目中,提供用于配置nginx
的配置文件nginx.conf,如下所示:
# nginx/nginx.conf
server {
listen 80;
listen [::]:80;
# 开启日志记录
access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /api/ {
proxy_pass: http://backend/
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
🚟 2. 提供用于生成镜像的Dockerfile
文件
Dockerfile
文件用于对前端项目生成镜像,服务器通过镜像生成容器去运行
# 新建构建阶段build-stage,指定node为基础镜像,该阶段用于生成前端制品文件
FROM node:16-alpine as build-stage
# 指定工作目录/app用于存放前端制品,以便在COPY,RUN以及下一个构建阶段中使用。
WORKDIR /app
# 复制package.json、package-lock.json、yarn.lock到工作目录里,COPY最后一个参数dest如果是相对路径,则会以工作目录有作为基准,复制到WORKDIR/<dest>里
COPY package*.json yarn.lock ./
# 下载依赖
RUN yarn install
COPY . .
# 生成制品
RUN yarn build
# 新建构建阶段deploy-stage,指定nginx为基础镜像,该阶段用于配置和运行nginx
FROM nginx:stable-alpine as deploy-stage
# 把build-stage阶段中的前端制品和nginx.conf配置文件复制到nginx的指定路径里
# 这样子就可以设置nginx的配置文件,而配置文件中的默认匹配路径是/usr/share/nginx/html,也就是存放前端制品的路径
COPY --from=build-stage /app/build/ /usr/share/nginx/html
COPY --from=build-stage /app/nginx/nginx.conf /etc/nginx/conf.d/default.conf
# 指定容器在运行过程时监听80端口
EXPOSE 80
# 指定在容器开启运行时,通过运行以下命令行指令开启nignx
CMD ["nginx", "-g", "daemon off;"]
对于上述的Dockerfile
需要提及以下注意点:
-
在
build-stage
的逻辑中,是先复制package.json
和yarn.lock
且安装依赖后,然后再把所有项目文件复制到工作目录。为什么不直接把所有项目文件复制到工作目录然后安装依赖呢?这里需要提及到
Dockerfile
的镜像以及缓存机制,篇幅较长,因此放在下面的 🚃 进阶- 🚌 1. 如何写好Dockerfile
章节中解释。 -
关于
Dockerfile
的EXPOSE
指令,用于指定容器在运行过程中监听特定端口号。该指令不会开放指定端口到宿主机->容器的映射表,如果要执行此操作请使用docker run -p
去手动指定。对于
EXPOSE
的描述里更多的可看此处
🚟 3. 在宿主机构建镜像且运行容器
完成上面的操作后,在宿主机依次执行以下步骤即可部署单体前端项目:
- 拉取前端项目代码:通过
git clone
复制前端项目代码 - 生成镜像:进入前端项目的根目录,运行命令行
docker build -t imageName .
去生成镜像。命令行中的最后一个参数用于指定Dockerfile
的位置。 - 生成容器且运行:运行命令行
docker run -d --name containerName imageName
,以对指定的镜像生成容器且在后台运行。
至此,一个单体前端项目的部署过程已经介绍完毕了。当然在实践中还要考虑其他细节,但主要操作流程已经在本节中讲述清楚了。
🚆 那么,如何部署微前端项目呢
一个微前端项目中包括一个主应用和数个子应用。我们可以用上节中分析的部署单体前端方式把主应用和子应用都生成对应的nginx
容器放在服务器里,在主应用nginx
容器里配置路径规则转发获取子应用资源的请求。如下图所示:
客户端发出路径前缀为/vue-app/
的请求,则是请求VueApp
子应用的资源;发出前缀为/react-app/
的请求,则是请求ReactTSApp
子应用的资源。这些请求都会由主应用nginx
容器做反向代理处理。
实现图中的多容器部署需要补充一些额外的细节。接下来我们分开主应用和子应用来列出这些额外细节:
🚊 主应用细节
1. 修改子应用的加载链接
这里的主应用以registerMicroApps
与start
对子应用进行加载和注册,在registerMicroApps
的相关代码中用以下写法:
const isProd = process.env.NODE_ENV === "production";
registerMicroApps([
{
name: "react app",
// 如果是生产环境则用特定前缀路径,当客户端发出该请求时,服务器中的主应用nginx容器收到请求会做反向代理分发到后端
entry: isProd ? `//${location.host}/react-app/` : "//localhost:3001",
container: "#app-react",
loader,
activeRule: "/app-react/index",
props: {
basepath: "/app-react/index",
},
},
{
name: "vue app",
entry: isProd ? `//${location.host}/vue-app/` : "//localhost:3002",
container: "#app-vue",
loader,
activeRule: "/app-vue/index",
props: {
basepath: "/app-vue/index",
},
},
// ...省略其余子应用
]);
2. 结合DockerNetwork
修改nginx.conf
配置文件
在展示nginx.conf代码之前,我们需要了解Docker Network
这一概念:当我们成功启动一个容器时,该容器会接入到Docker Network
实例里,如果docker run
指令中有用--network
参数指定网络,则接入到指定网络里,否则默认接入到bridge network
里。我们可以通过docker network ls
来查看当前存在哪些网络:
docker network ls
输出:
NETWORK ID NAME DRIVER SCOPE
95a79d75ba56 bridge bridge local
ebb2b72a64a9 host host local
b3fb09dbb1d0 none null local
上面三个network
是默认存在的网络,关于网络类型bridge
、none
、host
的区别可查看#network-drivers。
我们可以通过docker network inspect networkName
指令查看指定network
中有哪些接入的容器:
docker network inspect bridge
输出:
[
{
"Name": "bridge",
// 省略其他字段
// 在Containers里可以查看接入的容器
"Containers": {
"1e2d0761c5ad2154c7cfc2c9c912c094f3dda59623b1a992c1a66d9a14c4dedf": {
"Name": "master-app",
"EndpointID": "b206d19490336b0527496cf43c90b61befbd036b6c2d9909926124c0979458ea",
"MacAddress": "02:42:ac:11:00:04",
"IPv4Address": "172.17.0.4/16",
"IPv6Address": ""
},
"2c94c287b6ff3ec6561864cbad86f4fbd57ed39ec89a4bbf432f245887332c50": {
"Name": "vue-app",
"EndpointID": "3ee5696cd3cc0424c9246d1c4e5c1c5de76a4f638b4317a6df5fd7e0c0cb13ab",
"MacAddress": "02:42:ac:11:00:03",
"IPv4Address": "172.17.0.3/16",
"IPv6Address": ""
},
"e2299a334abdec2a01b0caca15cacf980dc3f9c55ea54519758789c8daa3742e": {
"Name": "react-ts-app",
"EndpointID": "ec876f84a999fb7f77cd8f7185e077bb597f0a50daa850b55729b4b9662ce50a",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
},
}
]
从上面的输出信息可知:
- master-app,即主应用
nginx
容器的 ip 为 172.17.0.4 - vue-app,即VueApp
nginx
容器的 ip 为 172.17.0.3 - react-ts-app,即ReactTSApp
nginx
容器的 ip 为 172.17.0.2
在同一网络中,容器可以通过 ip 互相访问。例如,当VueAppnginx
容器以 80 端口EXPOSE
时,主应用nginx
容器可通过 172.17.0.3 访问VueAppnginx
容器。
但在实践中,我们偏向于把需要通信的容器接入到自定义网络(通过docker network create networkName
指令来创建)。因为自定义网络对比于默认bridge
网络有一个优点:
User-defined bridges provide automatic DNS resolution between containers.
Containers on the default bridge network can only access each other by IP addresses, unless you use the --link option, which is considered legacy. On a user-defined bridge network, containers can resolve each other by name or alias.
引自Differences between user-defined bridges and the default bridge
引文中的意思是:自定义网络会对其接入的容器提供DNS
自动解析: 在默认bridge
网络中,容器可以通过 ip 访问其他容器。而在自定义网络,容器可以通过容器名或者昵称(docker run --network-alias
指定)来访问别的容器。例如:主应用nginx
容器可以通过http://vue-app
去访问VueAppnginx
容器。
采用容器名作为域名访问容器可以说是非常方便。而且因迭代更新导致镜像变化,从而生成新的子应用nginx
容器时,即使容器名字不变,但重新接入到网络时,容器 ip 可能会变化。此时如果再次通过固定 ip 访问子应用nginx
容器则会访问失败。但采取容器名访问就不用担心这种情况。
因此,所有微前端项目中的主应用nginx
容器和子应用nginx
容器,都接入到自定义网络里,就可以通过容器名作为域名访问彼此。在这前提下,我们的nginx.conf的写法如下所示:
server {
listen 80;
listen [::]:80;
access_log /var/log/nginx/host.access.log main;
error_log /var/log/nginx/host.error.log error;
location / {
try_files $uri $uri/ /index.html;
root /usr/share/nginx/html;
index index.html index.htm;
}
# 对于路径前缀为/vue-app/的请求,反向代理到名为vue-app的容器
location /vue-app/ {
proxy_pass http://vue-app/;
}
# 对于路径前缀为/react-app/的请求,反向代理到名为react-ts-app的容器
location /react-app/ {
proxy_pass http://react-ts-app/;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
至此,主应用的细节已经讲完。
🚊 子应用细节
子应用方面需要在打包过程中给引入资源的路径加上公共前缀。
对此,在使用vue-cli
脚手架创建的VueApp应用中,vue.config.js配置如下所示:
const isProd = process.env.NODE_ENV === "production";
module.exports = defineConfig({
transpileDependencies: true,
// 加上公共路径前缀
publicPath: isProd ? "/vue-app/" : undefined,
//...省略其他不变的配置
});
对此,在使用cra
脚手架创建的在ReactTSApp应用中,.rescriptsrc.js配置如下所示:
const isProd = process.env.NODE_ENV === "production";
module.exports = (
isProd ? [] : [["use-stylelint-config", ".stylelintrc.js"]]
).concat({
webpack: (config) => {
// 加上公共路径前缀
config.output.publicPath = isProd ? "/react-app/" : undefined;
//...省略其他不变的配置
},
});
至此,子应用的细节已讲完。
🚅 动手实践:基于Githb Action
来实现CD
以部署微前端项目
🚝 为什么要选择Githb Action
来实现CD
不妨思考一个问题:CD
流程要放在生产机器上执行吗?
大多数公司里,会用一台机器或者集群来负责CICD
操作,在走完CICD
流程后把制品上传到生产机器上部署。因为CICD
操作是会占用机器的资源和算力的,如果CICD
流程放在生产机器上运行,毫无疑问会降低生产机器处理外部请求的速度。因此要专门设立负责执行CICD
的机器或集群。
但对于我们个人开发而言,手上往往只有一台自费购买的机器,而且机器配置还不怎么高,都是 1 核 1G 的(都是薅首购优惠的,薅完腾讯云就薅阿里云)。那如果我们要在自己的低配置机器上做微前端CD
操作,同时跑主应用和好几个子应用的CD
流程,那机器机器会处于高负载的情况。我在自己的生产机器上跑自己的微前端项目(主应用+四个子应用)的CD
流程时,CPU
使用率达到 60%多,连在本地用SSH
访问生产机器都失败。
但我们也没有另一台的机器用于跑CICD
。在这种情况下就可以选择Github Action
。对于公共仓库和私有仓库,他都可以分配机器来运行我们的CICD
流程,我们只需要编写yml
文件来指定CICD
流程即可。我之前写过一篇关于Github Action
的入门级别文章作为前端,要学会用 Github Action 给自己的项目加上 CICD,有兴趣的可以看看。这里就不再介绍Github Action
的入门知识了,直接进入设计环节。
🚝 CD
的设计与实现
在设计CD
流程时,我们需要考虑两点:
CD
的执行时机CD
执行什么操作
下面通过本次CD
的流程图来展示上述两点:
首先准备docker.json文件,用于定义镜像名、镜像版本和容器名:
{
"name": "react-ts-app",
"version": "0.1.9"
}
为什么不取package.json中的name
和version
字段来定义呢?因为我们每次迭代更新而生成新版本的镜像时,都要更新版本,如果版本取自package.json里的version
,则会导致Dockerfile
中的RUN yarn install
指令对应的缓存失效。导致每次生成镜像时,即使应用依赖没变化,但因为package.json变化了导致要重新安装依赖。关于Dockerfile
的缓存细节会放到下面的 🚃 进阶 - 🚌 1. 如何写好Dockerfile
章节中分析。
接下来编写yml
文件对应上面的CD
流程图:
name: CD
on:
# 执行时机设计为master分支被推送时触发
push:
branches: master
jobs:
# ReadInfo Job用于读取且导出package.json里的信息,用于下一个Job作为环境变量使用
ReadInfo:
runs-on: ubuntu-latest
outputs:
info: ${{ steps.read-info.outputs.info }}
steps:
# 拉取代码
- name: Checkout repository
uses: actions/checkout@v3
# docker.json,把其内容即json字符串注入到存放输出结果的outputs.info
- name: Read Info
id: read-info
run: |
JSON=`cat ./$APP_PATH/docker.json`
JSON="${JSON//'%'/''}"
JSON="${JSON//$'\n'/''}"
JSON="${JSON//$'\r'/''}"
echo "info=$JSON" >> $GITHUB_OUTPUT
CD:
runs-on: ubuntu-latest
needs: ReadInfo
# 根据上一步ReadInfo Job中的输出结果info来注入环境变量
env:
# 容器名取自docker.json里的name
Container: ${{fromJson(needs.ReadInfo.outputs.info).name}}
# 镜像名和镜像标签取自docker.json里的name和version
Image: ${{fromJson(needs.ReadInfo.outputs.info).name}}:${{fromJson(needs.ReadInfo.outputs.info).version}}
# 镜像文件为docker.json里的name + 后缀.tar
ImageFile: ${{fromJson(needs.ReadInfo.outputs.info).name}}.tar
# 发布版本取自docker.json里的version
Release: ${{fromJson(needs.ReadInfo.outputs.info).version}}
steps:
# 拉取代码
- name: Checkout repository
uses: actions/checkout@v3
# 安装docker
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# 创建docker镜像
- name: Build Docker Image
uses: docker/build-push-action@v3
with:
# 缓存类型使用gha,该类型能很好的和github action结合实现缓存镜像,更多细节可看 https://docs.docker.com/build/building/cache/backends/gha/
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{env.Image}}
context: .
load: true
# 导出docker镜像文件
- name: Export Image as Tar
run: |
docker images
docker save ${{env.Image}} > ${{env.ImageFile}}
# 创建Release版本且上传镜像文件
- name: Create GitHub Release
id: create_release
uses: softprops/action-gh-release@v1
with:
token: ${{ secrets.PROJECT_ACCESS_TOKEN }}
tag_name: ${{ env.Release }}
name: ${{ env.Release }}
draft: false
prerelease: false
# 附上镜像文件
files: |
${{ env.ImageFile }}
# 把镜像文件上传至生产服务器
- name: Upload Image Tar to Deploy Server
# easingthemes/ssh-deploy的原理时使用rsync+ssh上传文件
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_TOKEN }}
ARGS: "-avzr --delete"
SOURCE: ${{env.ImageFile}}
REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
REMOTE_USER: ${{secrets.REMOTE_USER}}
TARGET: ${{secrets.TARGET}}
# 登陆进生产服务器进行操作
- name: Run Docker Container
uses: appleboy/ssh-action@master
env:
Network: microfe
with:
host: ${{secrets.REMOTE_HOST}}
username: ${{secrets.REMOTE_USER}}
key: ${{ secrets.DEPLOY_TOKEN }}
# 步骤:
# 1. 停止移除容器
# 2. 移除已有镜像
# 3. 进入存放镜像文件的目录且把镜像文件转为镜像
# 4. 检测若不存在microfe网络,则创建microfe网络
# 5. 根据镜像创建且运行容器,且把容器接入到microfe网络
# 6. 移除镜像文件
script: |
${{ format('docker ps -q --filter "name={0}" | grep -q . && docker rm -f {0}', env.Container) }}
${{ format('docker rmi -f $(docker images -q --filter reference="{0}")', env.Container) }}
${{ format('cd {0}' , secrets.TARGET)}}
${{ format('docker load < {0}', env.ImageFile)}}
${{ format('docker network ls -q --filter "name={0}" | grep -q . || docker network create {0}',env.Network)}}
${{ format('docker run -d --name {0} --network {1} {2}',env.Container,env.Network,env.Image)}}
${{ format('rm {0}',env.ImageFile)}}
最后运行结果如下所示:
大家也可以访问micro-fe 的 CD 执行记录来查看其中的执行细节。
🚃 进阶
🚌 1. 如何写好Dockerfile
本节中的内容是Docker
官网中总结出来的。如果在阅读以下内容后还想深入学习可点击阅读Optimizing builds with cache management。
关于Dockerfile
的分层机制
拿下面的Dockerfile
代码进行分析:
FROM node:16-alpine
WORKDIR /app
COPY . .
RUN yarn install
RUN yarn build
在Dockerfile
中,FROM
指令除了指定基础镜像外,还用于声明一个新的构建阶段,构建阶段在执行结束后都会输出一个新的镜像作为产物(下称输出镜像)。输出镜像中有多个分层,每个分层都对应一条指令。拿上述Dockerfile
代码来画图分析如下所示:
每一个分层都是基于前一个分层进行构建的。我们可以把整个输出镜像当作是一个存放分层的栈。每一个分层都可以会放到缓存里,如果下一次再次执行同样的Dockerfile
代码指令时,如果缓存中有对应该指令的分层,可直接取出使用,从而跳过构建分层的过程,减少构建输出镜像的时间。但如果指令发生变化,例如,COPY
所操作的文件发生变化,会导致缓存失效,需要重构对应的分层。如下所示:
而当一个指令发成变化导致分层重构后,该指令下面的所有指令都不能使用缓存分层只能全部重构。因为缓存中的分层是基于前一分层构建。如下所示:
如果我们在构建镜像时使用上述的Dockerfile
,在每次迭代时,由于新增修改开发代码,会让COPY . .
缓存失效从而导致对应的分层重构,继而执行yarn install
和yarn build
两个过程。但如果我们采用下面的写法:
FROM node:16-alpine as build-stage
WORKDIR /app
COPY package*.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build
这样子就可以在依赖不变的情况下,跳过yarn install
环节,从而加快构建镜像的速度。
我们可以对比在上一章Github Action CD
流水线下,执行RUN yarn install
指令时,不使用缓存和使用缓存的对比:
不使用缓存:耗时 79 秒
使用缓存:无须耗时,直接跳过
至于更多关于如何充分利用缓存的写法可阅读How can I use the cache efficiently?
关于上述Dockerfile
代码的进一步改进
对于以下Dockerfile
代码,还存在改进的空间:如果我只改用于配置nginx
规则的nginx.conf
配置文件,其他的不做修改。那么是可以通过缓存机制来跳过yarn install
和yarn build
环节的。
FROM node:16-alpine as build-stage
WORKDIR /app
COPY package*.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build
FROM nginx:stable-alpine as deploy-stage
COPY --from=build-stage /app/dist/ /usr/share/nginx/html
COPY --from=build-stage /app/nginx/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
对于上述代码,我们可以改进成以下代码:
FROM node:16-alpine as build-stage
WORKDIR /app
COPY package*.json yarn.lock ./
RUN yarn install
# COPY . .
# 把上面的COPY换成下面几行COPY指令。以明确指定复制除nginx以外的参与到yarn build的文件和目录
# 注意COPY指令在复制目录时会自动解包,只复制目录里面的子文件和子目录,因此目标对象必须是同名的目录
COPY src ./src
COPY public ./public
COPY .env .rescriptsrc.js tsconfig.json ./
RUN yarn build
FROM nginx:stable-alpine as deploy-stage
COPY --from=build-stage /app/build/ /usr/share/nginx/html
# COPY --from=build-stage /app/nginx/nginx.conf /etc/nginx/conf.d/default.conf
# 把上面的COPY换成下面的COPY指令。因为经过上面的调整,build-stage的工作目录里不存在nginx文件,需要直接从当前目录里复制过去
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
上面的写法也存在着缺点,就是需要写多行COPY
指令以明确指定每一个参与生成制品的文件目录,缺一个都会导致制品失败。
为了查看上面写法的效果,我触发了下面三种情况的CD
,并得出以下实验数据:
package.json
或者yarn.lock
发生变化: 构建镜像时间为2m53s,其中RUN yarn install
耗费85.5s,RUN yarn build
耗费25.6s,更多流水线细节可看此处package.json
和yarn.lock
没变化,开发代码发生变化: 构建镜像时间为1m20s,其中RUN yarn install
命中缓存无耗时,RUN yarn build
耗费40.2s,更多流水线细节可看此处package.json
和yarn.lock
没变化和开发代码没发生变化,nginx.conf
发生变化: 构建镜像时间为10s,其中RUN yarn install
和RUN yarn build
都命中缓存无耗时,多流水线细节可看此处
🚌 2. 如何实现回滚操作
回滚是指把服务器上的项目撤回到上一个版本,用于在上线新版本的项目后,发现紧急 bug。需要紧急手动撤回到旧版本的情景。接下来通过Github Action
实现一个简单可用的Rollback
回滚流水线。直接附上Rollback
流水线的流程图:
接下来编写yml
文件对应上面的Rollback
流程图:
name: Rollback
on:
workflow_dispatch:
inputs:
version:
description: "choose a version to deploy"
required: true
jobs:
Rollback:
runs-on: ubuntu-latest
env:
# 指定重新生成的容器名
Container: vue3-ts-app
# 指定镜像名
Image: vue3-ts-app:${{ github.event.inputs.version }}
# 指定要拉取的镜像文件
ImageFile: vue3-ts-app.tar
# 指定要从哪个发布版本拉取镜像文件
Release: vue3-ts-app/${{ github.event.inputs.version }}
steps:
- name: Echo Input
run: |
echo "Version: $VERSION"
# 从指定的发布版本里拉取镜像文件
- name: Download ImageFile
uses: dsaltares/fetch-gh-release-asset@master
with:
version: "tags/${{env.Release}}"
file: ${{env.ImageFile}}
token: ${{ secrets.GITHUB_TOKEN }}
# 把镜像文件上传至生产服务器
- name: Upload Image Tar to Deploy Server
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_TOKEN }}
ARGS: "-avzr --delete"
SOURCE: ${{env.ImageFile}}
REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
REMOTE_USER: ${{secrets.REMOTE_USER}}
TARGET: ${{secrets.TARGET}}
# 登陆进生产服务器进行操作
- name: Run Docker Container
uses: appleboy/ssh-action@master
env:
Network: microfe
with:
host: ${{secrets.REMOTE_HOST}}
username: ${{secrets.REMOTE_USER}}
key: ${{ secrets.DEPLOY_TOKEN }}
# 步骤:
# 1. 停止移除容器
# 2. 移除已有镜像
# 3. 进入存放镜像文件的目录且把镜像文件转为镜像
# 4. 检测若不存在microfe网络,则创建microfe网络
# 5. 根据镜像创建且运行容器,且把容器接入到microfe网络
# 6. 移除镜像文件
script: |
${{ format('docker ps -q --filter "name={0}" | grep -q . && docker rm -f {0}', env.Container) }}
${{ format('docker rmi -f $(docker images -q --filter reference="{0}")', env.Container) }}
${{ format('cd {0}' , secrets.TARGET)}}
${{ format('docker load < {0}', env.ImageFile)}}
${{ format('docker network ls -q --filter "name={0}" | grep -q . || docker network create {0}',env.Network)}}
${{ format('docker run -d --name {0} --network {1} {2}',env.Container,env.Network,env.Image)}}
${{ format('rm {0}',env.ImageFile)}}
最后的运行结果如下所示:
关于上图中Rollback
流水线的更多执行细节可看此处
🚞 后记
这篇文章写到这里就结束了,如果觉得有用可以点赞收藏,如果有疑问可以直接在评论留言,欢迎交流 👏👏👏。
转载自:https://juejin.cn/post/7165515526947471391