前端项目容器化之旅
发布日期:2021-05-10 07:21:49 浏览次数:24 分类:精选文章

本文共 3166 字,大约阅读时间需要 10 分钟。

本文介绍了笔者的前端容器化部署踩坑之旅,重点解决了以下几个问题:node_modules缓存、镜像体积过大以及镜像数量多且上线流程复杂。本文要求读者有一定的容器基础,掌握Docker的基本操作,了解K8s的Pod原理。如果还不了解,请先拉到文末,阅读参考资料。

最近将一个前端项目部署到公司的容器服务。事情的起因是这样的:原先我们在一台服务器上部署了一套Jenkins,用于前端项目集成。一个月前,安全部门发邮件指出我们的Jenkins插件有漏洞。于是,我们花了两天时间备份配置、为服务器申请各个项目的Deploy Key、升级Jenkins。期间同事们没法快速上线,只能手工拷贝到线上服务器。

笔者反思了一下,现有的流程有以下问题:

  • 这套集成服务没有做灾备,加上升级时还要解决一些版本兼容问题。一旦挂掉,影响生产效率。

  • 每次部署一个新项目的时候,需要申请机器、安装一系列环境依赖、为Jenkins所在的机器添加访问权限。

  • 使用Jenkins时,需要在脚本里指明目标机器地址。如果要增加新的服务器,就得重复2和3里的步骤。

  • 从长远计,我们需要更稳定、更自动化的集成服务(而且要由更专业的人维护)。公司的云平台提供了基于Kubernetes(简称K8s)搭建的容器服务,还能够持续集成。符合我们的需求。那就开始新的部署之旅吧。

    初次部署

    由于该项目前后端分离得很彻底,只需从源代码编译产生静态资源,再用Nginx关联前端的域名和静态资源即可。因此笔者基于Node和Nginx创建了两个镜像。将这两个镜像运行的容器跑在一个Pod里,并且让两个容器通过共享空间读写静态资源目录。如下图所示:

    图片被截断,以下是相关说明:

    Node镜像是在云平台的持续集成流程中构建的。因为云平台在Gitlab申请了Webhook,所以在提交代码时会自动触发构建流程。Nginx镜像则是先本地构建好再上传到云平台。

    云端构建Node镜像的大致过程:

    图片被截断,以下是相关说明:

    node.dockerfile的内容如下:

    FROM $NODE_BASE_IMAGE
    WORKDIR /site
    WORKDIR /site-build
    ADD ./ /site-build/
    RUN npm install && npm run build && mv /site-build/dist /site/ && rm -rf /site-build
    ENTRYPOINT $MOVE_DIST_TO_SHARED_FOLDER_AND__KEEP_CONTAINER_ALIVE

    最后在ENTRYPOINT里声明,启动容器时需要将编译好的文件夹拷贝到共享文件夹,并且运行一个让容器不退出的命令。(随便什么命令,不退出就对了。因为线上开启了退出自动重启功能,会导致容器不停地重启)

    本地构建Nginx镜像的nginx.dockerfile则简单许多:

    FROM $NGINX_BASE_IMAGE
    ADD ./nginx.conf /$NGINX_CONF_INCLUDE_PATH
    EXPOSE 80

    以上配置先指定基础镜像,再将本地的Nginx配置文件添加到镜像里对应的Nginx的include目录。因为Nginx镜像在本地构建,且默认开启缓存,所以只要ADD的文件不变,并且各种命令不变,构建过程就会特别快。

    完成镜像的构建和上传后,在云平台配置好容器的共享文件夹和nginx日志的挂载路径。选择这两个镜像并发布。初次部署完成了。

    但有几个很明显的问题:

  • 云平台的持续集成没有缓存。这意味着,每次构建Node镜像时都要执行一遍npm install。显然这是没有必要的。如果能够将node_modules缓存起来,在第三方依赖不变的情况下,就不必npm install了。

  • 镜像太大。两个2GB+的基础镜像。上传慢而且占用了不必要的磁盘空间。

  • 运行Node的容器必须保持存活。虽然这个容器只需要在启动时给共享空间写入dist目录就完成了使命。

  • 优化一:缓存node_modules

    解决方案是将node_modules打包到基础镜像中,只有当本地的node_modules改变时才重新构建。然后以此为基础构建第二个镜像(用于提供编译后的dist目录)。需要解决以下问题:

  • 自动检测到第三方依赖变化后自动构建新的基础镜像,并将该镜像上传到云平台。

  • 自动将新的基础镜像地址写入到node.dockerfile的FROM字段。

  • 自动提交更新后的node.dockerfile文件,交给持续集成在线上构建新镜像。

  • 因为要实现自动化,所以自然要用到git钩子。又因为只有上线前才需要更新镜像,所以采用npm version的钩子(其他钩子都不合适)。在package.json的scripts中添加version和postversion钩子,分别用于自动检测构建和推送代码。上线前只需要执行一下npm version(major/minor/patch),这样就可以自动检测第三方依赖改变并构建镜像。

    如何检测node_modules变化?

    第一个尝试的是检测package.json的变化(通过git的commit日志记录对比上次修改时间)。因为第三方依赖跟package.json的dependencies对应,只要package.json有新的提交记录,八成是dependencies改变了。但是每次执行npm version会修改package.json中的version字段,也会提交package.json,因此不准确。

    第二个想到package-lock.json。第三方依赖改变时,也会反映到package-lock.json中。观察了一下,package-lock.json也包含了version字段。作罢。

    最后想到了yarn。观察一下yarn.lock,不包含package.json中的version信息。只有当依赖改变时,yarn.lock才会变化。完美。索性弃npm投yarn了。

    这一次优化后的流程大致如下:

    原先云端构建需要平均400秒,优化后,平均210秒,节省了一半时间。

    优化二:缩小镜像体积

    原先Node镜像和Nginx镜像体积分别是2GB+,可以说很臃肿了。而真正有用的静态文件(dist目录)加起来不到10MB。所以需要替换成更小的基础镜像。

    最初使用的基础镜像是CentOS操作系统,本身体积较大。在容器时代最受欢迎的Linux版本是什么?当然是Alpine了。原因就是它足够小。小到什么程度呢?大概5MB吧。

    于是在做第一步优化时,顺便将本地的Node基础镜像换成了Alpine,构建镜像时再安装yarn(指令:RUN apk add --no-cache yarn)。

    原先本地构建出来的Node镜像(已安装node_modules)是2.53GB,替换基础镜像后缩小到668MB。

    优化三:multi-stage build

    接下来看第三个问题,其实Node容器没有必要存活。我们的网站只需要Nginx+配置文件+静态资源目录就可以了。

    这时要用到Docker的multi-stage build特性。它允许将多个构建步骤写到一个Dockerfile里面。相当于在一次镜像构建中执行多个步骤,但以最后一个步骤产生的文件和指令为准。

    那么就将云端的镜像构建分为两个阶段,第一阶段产生dist目录,第二个阶段提供nginx配置和静态资源。构建流程如下:

    最终效果:

    镜像体积降至25MB(原先两个镜像分别2GB+)。一个Pod里只用运行一个容器,并且去掉共享空间。

    持续集成构建时间平均170秒(原先平均400秒)。

    上一篇:深入解析 float,前端页面必备技能
    下一篇:Node 最古老的 npm 包 request 将被废弃

    发表评论

    最新留言

    第一次来,支持一个
    [***.219.124.196]2025年05月09日 20时28分49秒