实战 web 应用 Docker 镜像解耦交付

简介: 实战 web 应用 Docker 镜像解耦交付

把大象放进冰箱需要几步?把一个 web 应用塞进集装箱呢?

随着几次浏览器大战的硝烟散尽和 Flash 的背影远去,当下的 web 应用开发经过十余年的发展,在工程化、测试、持续集成等方面都已经汇入了软件开发的快车道。

然而虽然新概念、新特性层出不穷,细分领域愈加专业化,但其究极奥义始终未变 -- 不管你怎么折腾,生成出来的交付物仍是 HTML/CSS/JS 老三样等静态资源,加上若干动态请求 的形式。从直接把文件拖放到 FTP 软件中手动上传的刀耕火种时代,到如今 Docker 镜像成为一种常见的部署格式 ,研发团队和运维团队的交互也在发生变化。

本文将在个人经验的基础上,尝试以一个前端项目为案例,浅谈其面向部署时的一些固有问题,以及与 Docker 相关的部分实践。

拥抱 Docker 时的麻烦

在此之前,要部署一个前端项目,运维人员需要做什么呢?

  • 安装完整的 node 环境并保持其更新
  • 阅读前端项目中 README 中的相关说明并更改相关文件中的设置项
  • 用 npm 安装一些全局依赖项
  • 保证 npm run build 流程的正确运行
  • 和前端开发同事协作解决由于打包机器不同可能带来的问题

等搞定这么一全套的“份外”工作,才能得到打包后的目标文件并开始部署;这不但是多么痛的一种领悟,也是工作流层面一系列莫大的耦合。

"All problems in computer science can be solved by another level of indirection." -- David John Wheeler

面对代码封装中出现的耦合类问题,即便不了解 SOLID 原则、DRY 原则等等,以上这句 “啥都不叫事,抽象就对了!” 也算得上应该谨记的万金油了,是解耦的根本所在。

Docker 镜像就作为这样一种优良的抽象层,为研发团队和运维团队更好地解耦提供了可能。

然而在实际开发和部署中,囿于旧有经验和认知水平,可能会存在一些新问题:

利用不同的环境变量分别编译

严格来说这不算遇到 Docker 后才有的问题,可以说绝大部分前端项目一直都是默认这么做的。

根据 BUILD_ENV 环境变量,分别对开发、测试、预发、生产环境等区分编译不同的 API 的访问前缀 -- 比如对 GET /api/shops 数据接口的访问地址被分别编译成 http://test.com/api/shopshttps://api.stage.com/shops 等,虽然在传统的物理主机/虚拟主机工作流中这是无可指摘的标准做法;但在 Docker 语境中,这会导致分多次生成几个不同的镜像,从理论上难以保证“所测试的就是所部署的”这一理念。

此外,无法控制团队中的开发人员会利用这一特性添加什么其它的变量,甚至因为线上 bug 在本地难以重现而加以滥用作出特殊处理的也并不鲜见,这些都会对项目部署造成未知的干扰。

所以对于环境变量,或许我们应该稍稍反思并保证最小化使用,从而探索更适于 Docker 的新经验。

在镜像外独立构建等

无论对于分发还是部署,镜像越小越好,这是面对 Docker 时的一条普遍共识。对于构建过程中常见的优化方式有:

  • 选用 alpine 版本的基础镜像
  • && 操作符来实现链式的 RUN 等指令以减少分层
  • 在容器中使用 nginx 而非 node 来伺服静态文件(服务器软件本身至少能减少 70M+)

另外,编译过程中的依赖文件 也是没有必要包含在最终镜像中的,一般的处理如:

  • 在 Dockerfile 中编译然后用指令语句删除一些文件
  • 分为可复用的依赖镜像和最终打包镜像
  • 利用 Docker 的多阶段构建,在一个 Dockerfile 中解决问题;后面会有介绍

比较糟糕的一种做法可能是,每次让运维人员利用类似 npm run build && docker build ...  的命令,在服务器上构建项目再打包到 Docker 镜像中。这样做既增加了运维团队的负担,使其和传统模式一样深陷在环境依赖和繁复流程中;又无法保证其手动调整项目配置项等代码后整体的正确性;且 npm 打包环境异于开发者,有较高的不确定性。

构建参数

--build-arg 本身是个很方便的属性,能在 docker build 时传入必要的参数。但和项目中的环境变量类似,如果应用不当也会造成不同环境下镜像不一致的问题。因此交由运维人员或者自动化执行的 docker build 命令最好没有构建参数。

SASS 依赖

不同于其它依赖项,npm 安装 node-sass 包时,会从 github.com 上下载 .node 文件等。由于网络环境的问题,这个下载时间通常会很长,甚至导致超时失败。这往往成为了运维人员一个意料之外的痛点。

一般的解决办法是在 Dockerfile 中用 ENV 指令指定淘宝源:


ENV SASS_BINARY_SITE https://npm.taobao.org/mirrors/node-sass/

而有些项目的构建环境更加极端,出于安全等考虑无法访问外网,其它依赖从公司内部的私有 npm 源上获取。这时针对 node-sass 问题,处理起来就要更特殊一些:

  • 访问 github.com/sass/node-s… .node 文件
  • npm i node-sass --sass_binary_path=<下载的.node文件> 语句整合进 Dockerfile

让镜像更易于交付

汇总之前分析的种种细节,来相对完整地看看如何配置镜像:

Dockerfile 多阶段构建

Docker 多阶段构建 是 17.05 版本开始后才有的一个特性。多阶段构建允许我们将多个 FROM 语句放在同一个 Dockerfile 中。

每条 FROM 指令都可以使用各自不同的基础镜像。每个 FROM 语句也都标记了 Docker 构建过程中一个新阶段的开始。我们可以拷贝一个阶段的产出物到另一个阶段,也可以抛弃不需要的部分。

这是个非常有用的特性,能避免最终镜像中存在编译过程中的依赖文件,也就是镜像会变得更小了 。


# stage 0
FROM node:10-alpine as build-stage
WORKDIR /app
COPY package.json ./
ENV SASS_BINARY_SITE https://npm.taobao.org/mirrors/node-sass/
RUN npm install --registry=https://registry.npm.taobao.org
COPY . .
RUN npm run build-prod --silent 
# stage 1 (nginx)
FROM nginx:1.17-alpine
COPY config/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 8081
CMD ["nginx", "-g", "daemon off;"]

注意我们通过 –from= 引用了 构建阶段 stage 0,并从构建阶段的工作目录拷贝了项目代码。

用数据卷覆盖镜像内配置

既然说了 npm 项目构建阶段用环境变量写入 API 请求地址等行为破坏 Docker 镜像的一致性,那到底如何请求到正确的端点呢?总要有个类似变量的东西传进去呀 ?!

但由于一来浏览器中无法用 process 感知环境,二来 Nginx 又不似 Node.js 应用一样可以直接传入参数;我们只好稍费周章,想办法 写入一些 Nginx 可以伺服的文件作为变量来源

采用的技术正是 Docker 中的数据卷(volume),也就是在 docker run 时加载指定的目录或文件,用以在容器内创建或覆盖某些路径。单以写入  API 请求地址的需求为例,具体做法如下:

  • 在服务器上创建一个 endpoint.json 文件,内容为:


{
  "ENDPOINT": "http://api.app.com:5678"
}
  • 在 ``docker run时加入参数-v`:


docker run -p 48081:8081 -v <JSON文件绝对地址>:/usr/share/nginx/html/endpoint.json:ro -d <镜像名>

这样就在容器中的项目根目录下楔入了一个我们可以随意配置的文件。

项目局部的异步改造

配置文件很轻松的就解决了,那么有了 endpoint.json 配置文件,如何在 runtime 将其应用于每一次异步请求呢?思路似乎也颇为简单:

  1. 项目启动时先异步读取配置文件中的 ENDPOINT 属性
  2. 将读取到的属性放入项目中 fetch/ajax 框架的构造函数中,完成统一注入

注:某些构建糟糕的项目可能要多费些事了,需要将原本分散写在各处的请求前缀收敛为由统一的 fetch/ajax 框架处理

但或许麻烦就来自于异步请求这里 -- 由于一些状态管理工具的 store 里也存在异步请求,甚至 router 等处也会引用到 store,就很有可能造成 其异步调用早于 fetch/ajax 框架的构造函数 执行,,从而造成一些请求的失败;我们要做的就是对这些部分改为延迟加载。

以 vue 项目为例,在 main.js 中:

  • 删除原有的 import 语句:


// import router from './router';
// import store from './store';
  • 改为延迟加载:


const init = async () => {
  const store = await import('./store');
  const router = await import('./router');
  return new Vue({
    i18n,
    router: router.default,
    store: store.default,
    render: h => h(App),
  }).$mount('#app');
};
  • 保证顺序的初始化:


fetch('/endpoint.json').then(res => res.json()).then(cfg => {
  window.API_ENDPOINT = cfg.ENDPOINT;
  init();
});

以及,在 fetch 框架中的引用:


const FetchWrapper = function(option) {
  const r = new QuickFetch(mergeWith({
    endpoint: window.API_ENDPOINT,
    baseURL: '/api'
  }, option));
  
  ...
}

总结

面向以 Docker 镜像为交付物的前端开发,代码层面所需的调整其实不是很多,主要是观念上是否敢于从传统舒适的工作模式稍微跳脱出来。

另外在团队中多换位思考,让开发链条中处于下游的运维小伙伴更乐于对接你的工作,共同提升开发部署效率和质量,也是很重要的。

参考资料


相关文章
|
12天前
|
应用服务中间件 Linux nginx
Docker镜像-手动制作yum版nginx镜像
这篇文章介绍了如何手动制作一个基于CentOS 7.6的Docker镜像,其中包括下载指定版本的CentOS镜像,创建容器,配置阿里云软件源,安装并配置nginx,自定义nginx日志格式和web页面,最后提交镜像并基于该镜像启动新容器的详细步骤。
69 21
Docker镜像-手动制作yum版nginx镜像
|
12天前
|
应用服务中间件 nginx Docker
Docker镜像-基于DockerFile制作编译版nginx镜像
这篇文章介绍了如何基于Dockerfile制作一个编译版的nginx镜像,并提供了详细的步骤和命令。
89 17
Docker镜像-基于DockerFile制作编译版nginx镜像
|
11天前
|
Docker 容器
Docker自建仓库之Harbor高可用部署实战篇
关于如何部署Harbor高可用性的实战教程,涵盖了从单机部署到镜像仓库同步的详细步骤。
48 15
Docker自建仓库之Harbor高可用部署实战篇
|
11天前
|
算法 Linux 调度
Docker的资源限制实战篇
本文详细介绍了如何利用Docker对容器的资源进行限制,包括内存和CPU的使用。文章首先概述了资源限制的重要性及其在Linux系统中的实现原理,并强调了不当设置可能导致的风险。接着,通过一系列实战案例展示了如何具体设置容器的内存限制,包括硬性限制、动态调整以及软限制等。最后,文章还提供了限制容器CPU访问的具体方法和示例,如指定容器使用的CPU核心数和基于`--cpu-shares`参数对CPU资源进行分配。通过这些实践,读者可以更好地理解和掌握Docker资源管理技巧。
40 14
Docker的资源限制实战篇
|
11天前
|
存储 数据管理 应用服务中间件
Docker的数据管理实战篇
关于Docker数据管理实战的教程,涵盖了Docker数据卷的使用、特点、场景以及数据卷容器的概念和应用。
42 13
Docker的数据管理实战篇
|
12天前
|
应用服务中间件 Linux nginx
Docker镜像管理篇
关于Docker镜像管理的教程,涵盖了Docker镜像的基本概念、管理命令以及如何制作Docker镜像等内容。
60 7
Docker镜像管理篇
|
12天前
|
Ubuntu Linux Docker
Ubuntu 18.04 安装Docker实战案例
关于如何在Ubuntu 18.04系统上安装Docker的实战案例,包括安装步骤、配置镜像加速以及下载和运行Docker镜像的过程。
81 3
Ubuntu 18.04 安装Docker实战案例
|
12天前
|
存储 Linux Docker
CentOS 7.6安装Docker实战案例及存储引擎和服务进程简介
关于如何在CentOS 7.6上安装Docker、介绍Docker存储引擎以及服务进程关系的实战案例。
49 3
CentOS 7.6安装Docker实战案例及存储引擎和服务进程简介
|
12天前
|
应用服务中间件 Linux nginx
Docker镜像-基于DockerFile制作yum版nginx镜像
本文介绍了如何使用Dockerfile制作一个基于CentOS 7.6.1810的yum版nginx镜像,并提供了详细的步骤和命令。
55 20
|
11天前
|
存储 测试技术 数据安全/隐私保护
Docker自建仓库之Harbor部署实战
关于如何部署和使用Harbor作为Docker企业级私有镜像仓库的详细教程。
32 12