65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Node.js 高效 Docker 化平台无关应用程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2018 年 6 月 29 日

CPOL

20分钟阅读

viewsIcon

16423

downloadIcon

77

如何使用 Node.js Web 应用程序高效创建 Docker 镜像

引言

Docker 一直是我一直想写的话题,但我脑海中一直没有一个足够简单且贴近现实的例子来解释 Docker 镜像的概念以及如何构建你的第一个 Docker 镜像。我已经在 Docker 环境中工作了一段时间,我个人认为,应用程序越简单,从中创建镜像就越好。出于这个目的,我选择了 Node.js 环境来创建和运行应用程序。此外,Docker 在底层基础设施、应用程序环境、网络、存储等方面提供了额外的抽象。因此,我们更容易理解 Docker 的工作原理,以及 Node.js 的工作原理——而无需学习它们如何匹配或如何集成。

这时,Node.js 闪亮登场,这个框架本身就是纯粹的美。我一直讨厌 JavaScript;至今仍然如此,但这个事实并不会影响我判断 Node.js 是否是一个好框架。长话短说,现在我们来看看如何创建一个镜像,运行一个 Node.js 服务器,以及如何在 Docker 容器中服务它,还有更多内容。

另外,对 Node.js 和 Docker 的一点介绍也非常受欢迎,因为我可能会在学习曲线上快速转向,所以如果你对 Node.js 运行时有所了解,如何在上面进行开发,以及如何开发容器和镜像,那将是非常棒的。

开发 Node.js 应用程序

我不会从零开始构建一个服务器,因此我希望你了解 Node.js 的基本知识。在本节中,我将讨论如何在考虑 Docker 化最佳实践的情况下开发 Node.js 服务器。Docker 是一个概念——除了作为一家 公司 和一个用于容器化应用程序的服务之外——而 Docker 化需要我们开发应用程序的方式以及我们想象开发过程的方式发生巨大变化。但这部分我们可以留待以后讨论。目前,我们可以先掌握环境变量的基本概念,看看如何管理我们应用程序的运行时配置。请记住,这是 Node.js 运行时部署中最重要的一部分,在我们自己的案例中,我们将利用它来从单个构建产物中打包和部署应用程序到不同的平台。再说一遍,我们将沿着这个仓库进行:https://github.com/afzaal-ahmad-zeeshan/nodejs-dockerized,并查看它是如何构建的。

Visual Studio Code 作为 IDE

我使用 Visual Studio Code 作为此项目的 IDE,你可以使用任何其他文本编辑器,但我建议你考虑使用它。除了轻量级和跨平台之外,它还有几个好处。在 Visual Studio Code 中,IDE 内置了基本的语法高亮和语言支持,并且你可以始终通过 IDE 中的扩展来扩展其功能。

总之,你可以使用任何你喜欢的文本编辑器,并安装 Node.js 工具,例如 NPM。我们将使用 NPM 来创建和管理我们将要开始的项目,然后编写代码来在 Docker 环境中创建我们简单的可打包镜像。要创建一个新项目,请在某个目录中打开终端并执行以下命令:

$ npm init

此命令的作用是,它会询问有关包名称(NPM 是 Node.js 包管理器)、版本、许可证、入口点以及其他一些元素(如包的描述和它将驻留的存储库;例如 GitHub)的信息。你可以输入所有字段,最后,查看它的说明,一切都将放入一个非常基本的 package.json 文件中,该文件包含从基本设置到你的包以及依赖于该包的包的友好配置的所有内容。

在输入命令提示符后,package.json 文件中添加了以下内容:

{
  "name": "express-nodejs",
  "version": "1.1.0",
  "description": "Sample Express based Node.js app for containerization samples and demos. ",
  "main": "./src/app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node ./src/app.js"
  },
  "keywords": [
    "express",
    "nodejs",
    "afzaalahmadzeeshan",
    "docker",
    "image"
  ],
  "author": "Afzaal Ahmad Zeeshan",
  "license": "MIT"
}

这个package.json 文件说明,该包有一个入口点(./src/app.js),以及运行程序的脚本,一些有助于搜索包的基本关键字,最后是该包发布的作者和许可证。

这不是最重要的部分,因为这部分仅帮助 NPM 和基本的 Node.js 运行时了解包以及在运行前需要解析哪些依赖项,然而,我们 Docker 化 Node.js 应用程序最重要的部分是 app.js 文件,到目前为止,它什么都没有(坦白说,它也还没有创建)。现在,这是我需要你对 Node.js 运行时有一些基本了解的部分,以及如何开发一个基本的 Node.js 应用程序。

添加 Node.js 依赖项

Node.js 应用程序的特点是它有自己的内置 HTTP 处理程序,服务器组件是建立在这些 HTTP 处理程序之上的。我们将使用 Express 服务器用于 Node.js 运行时,除此之外,我们将使用 Pug 模板框架来构建前端。选择这两者的原因是因为它们是 Node.js 中最广泛使用的服务器和模板实现。此外,我们还希望记录我们容器的使用情况,为此,我们将添加 Azure 的 Application Insights SDK,并使用它来监控我们的应用程序是如何被人们使用的。

有两种安装依赖项的方法,一种是使用 NPM 命令行界面,通过在应用程序所在的目录下的终端中发出以下命令来轻松安装包:

$ npm install package-name 

这也会更新package.json 文件中的 dependencies 元素。这在大多数情况下可能很有帮助,否则,你可以更新package.json 文件中的 dependencies 元素,并包含你想要为应用程序下载和设置的包。因此,通过这种方式,你可以将依赖项添加到package.json

"dependencies": {
    "express": "^4.16.3",
    "pug": "^2.0.3",
    "applicationinsights": "^1.0.2"
}

此元素可以附加到package.json 对象中的任何位置,完成后,只需执行以下命令:

$ npm update

这将为你下载和安装包。到目前为止,所有这些都只是一个基本的 Node.js Hello World 应用程序,本文的重点不是指导你编写一个好的 Node.js Hello World 应用程序,而是如何编写容器化的 Node.js 应用程序。因此,我们现在继续讨论开发容器所需的基本配置。

构建容器化的 Node.js 服务器

你可以查看任何 Node.js Hello World 示例,你会发现最基本的 Node.js 应用程序会使用 Express 服务器和一个调用 listen 函数的基本调用!调用大致如下:

let Express = require("express"); // provided you have the dependency
let app = new Express();

app.all("*", (req, res) => {
   // catch-all
   res.send("Hello there!");
});

let port = 1234; // 8080, 80, or whatever you want
app.listen(port, () => {
    console.log("Server is now listening on localhost:" + port);
});

这是一个最基本的 Node.js 应用程序,一个功能齐全的 Web 应用程序,它可以处理请求并提供响应——每次都提供相同的响应,但我们也可以处理这种情况。现在试着想象一下,Docker 应用程序是一个便携式程序,一旦部署在运行时,它就能按需运行。这个程序很可能会失败,原因如下:

  1. 服务器硬编码为监听 1234!可以更改,见下文。
  2. 服务器不检查环境约束,它按照需要启动。
  3. 服务器的命名和端口将在 Dockerfile 中遵循。

Dockerfile 和应用程序必须在转换中移动,有些东西应该由应用程序处理,有些必须留给 Docker 来管理和处理,网络、存储等要留给 Docker。在这里不遵循这一点。因此,在接下来的章节中,我将不构建 Node.js 应用程序,而是**展示将 Node.js 应用程序分解成块的最佳实践,并从中构建一个可移植且灵活的容器镜像**!

初始 Dockerfile

让我们从创建 Dockerfile 开始,然后我们将继续检查如何改进整个应用程序。我们最基本的 Dockerfile 将如下所示,请记住将此文件放在 Node.js 应用程序的根目录中,即package.json 所在的位置。

FROM node:9
EXPOSE 1234
COPY . .
RUN [ "npm", "update" ]
CMD [ "npm", "start" ]

这告诉 Docker 在构建镜像时执行五个步骤。让我们详细讨论这些行:

  1. FROM 指令告诉 Docker 运行时使用此特定镜像作为当前镜像的骨干。在此命令中,带有标签 9node 是父镜像,我们的镜像依赖于该运行时。简单来说,我们的镜像将安装 Node.js 版本 9 并准备使用,这是 Docker 的保证!
  2. EXPOSE 命令允许在特定端口或端口范围(**1234-1240 等**)上进行网络通信。我只允许我感兴趣的端口,即 **1234** 端口。
  3. COPY 命令将——你猜对了!——将内容从源复制到目标。在我的例子中,我在两种情况下都使用了一个句点,这意味着,从这个目录(在主机文件系统中)到当前目录(在 Docker 容器文件系统中)。
  4. RUN 命令在 Docker 镜像的构建过程中运行一个命令。想象一下,就像解析依赖项、解压内容、下载一些配置文件等。你可以使用此命令在容器内执行一个特定命令,并使其为进一步的步骤做好准备,例如应用程序的执行。结构是将主可执行文件名作为第一个参数,并将额外参数作为字符串序列传递。对于基本的 .NET Core 应用程序,我们可能需要这样做:
    RUN [ "dotnet", "MyApp.dll" ]
  5. CMD 是告诉 Docker 在运行镜像作为容器时执行此命令的命令。还有一个命令,ENTRYPOINT,它的作用大致相同,你可以在 Stack Overflow 上的这里阅读有关不同之处的信息。

这基本上总结了我们所做的一切。到目前为止,我们有一个将在端口 1234 上监听的服务器,一切都已设置妥当。让我们继续在 Microsoft Azure 上创建一个新的 Web 应用容器,并在那里进行尝试。

Azure 上的 Web 应用容器

Web 应用容器是 Microsoft Azure 提供的一种特定平台(即服务),它允许你在几分钟内从你提供的镜像运行你的容器!Web 应用容器的一些特点是:

  • 它仅支持 Linux 容器,目前不支持 Windows 容器;有关更多信息,请查看 此链接
  • 它会自动处理负载均衡和自动缩放。
  • 为你的容器提供无服务器环境!
  • 可以遵循从 Docker Hub 的 CI/CD(你可以使用 Docker 的 Webhooks 来自动升级容器中的镜像)

此平台更高级的功能是,你可以集成 Azure AD 进行身份验证(就像你可以在 App Service 中进行身份验证一样),你还可以为容器设置自定义域名。我认为这足以作为对 Web 应用容器平台的介绍。现在让我们继续自己创建一个。

访问 Azure Portal,搜索“web app for containers”,选择 Microsoft 的产品并创建一个新实例。

我将使用这个 Docker 镜像来运行容器,来源是 https://hub.docker.com/r/afzaalahmadzeeshan/express-nodejs/,你随时可以下载并尝试一下。如果你没有 Azure 帐户,请注册一个 免费试用,否则,你可以在本地运行 Docker 并在本地尝试该镜像。

图 1:使用 Docker 镜像在 Azure Portal 上创建 Web 应用容器。

正如你所见,Docker Compose 和 Kubernetes 集群部署目前也处于预览阶段,你可以稍后查看这些服务,或查看这篇帖子并 学习如何部署多容器应用程序。我们部署的容器运行得相当好。

图 2:在 Azure App Service 的容器化环境中运行的 Node.js 应用程序。

现在如果我们运行应用程序,你会发现 Azure 会为我们自动配置端口和网络。存在一些常见问题。

  • 如果我们想将平台从 Azure 转移到其他平台(如 AWS、Heroku 或任何其他平台)怎么办?
  • 如果平台不再支持 1234 端口,或者平台要求你必须启用 80 端口怎么办?
  • 如果平台要求你使用环境变量并始终监听由变量指定的端口怎么办?

第三点是 Redhat 的 OpenShift 所要求的,否则它将不接受镜像并将其作为容器运行。Azure 是智能云,知道如何处理,而其他平台可能不知道。

图 3:显示的服务 Pod 和路由的 OpenShift 配置。

你可以研究一下 路由 配置,并研究如何管理容器的基本路由。在大多数情况下,开发用于 Docker 容器化的应用程序的最佳实践是,移除硬编码和对平台的依赖,而是使用环境变量来配置应用程序的启动方式。

减少容器的硬编码

现在艰苦的工作开始了,我们需要跳出固有思维,重新设计应用程序,以便能够将其部署到任何地方。首先要做的是重新设计应用程序,使其能够处理和监听环境要求它监听的端口。使用 Docker 的好处在于,我们可以将任何外部流量路由到容器,而容器不会关心外部连接器和用户正在遵循什么路由。这是普通进程无法实现的,可能需要负载均衡器来处理。

现在让我们从 app.js 文件本身开始,我们需要从中移除的是端口的硬编码。我们可以这样做:

let port = process.env.PORT || 1234; 

但这会将我们的进程绑定到考虑环境变量中的 PORT 变量,如果为 null,则再次移动到 1234 端口。让我们在试图解决的问题中陷入僵局。因此,这就是为什么我们需要重新设想一些概念,并使用*模块化方法*。我们需要一个模块来为我们处理这个问题,并且我们可以向其中添加更多功能,使其更易于控制。其他元素也是如此,例如日志服务的连接字符串——参见下面的 App Insights 部分

我提出的想法是有一个单独的模块,或者一个导入,它定义了我的服务器将要监听的端口。以及一些其他配置,该内容结构如下:

let port = process.env.PORT || process.env.PORT_AZURE || process.env.PORT_AWS || 5000;

// OpenShift ports
if(process.env.OPENSHIFT_NODEJS_PORT) {
    port = process.env.OPENSHIFT_NODEJS_PORT;
}

// Override the ports
if(process.env.OVERRIDE_PORT) {
    port = process.env.CUSTOM_PORT;
}

// Variable name environment variable for the port
if(process.env.VARIABLE_PORT) {
    port = process.env[process.env.VARIABLE_PORT_NAME];
}

module.exports = {
    serverPort: port,
    applicationInsightsInstrumentationKey: process.env.appInsightsKey
};

正如你现在所见,该模块现在包含一个对象,该对象保存 serverPortapplicationInsightsInstrumentationKey。需要考虑的是,这些现在是完全可配置的,没有任何东西被绑定到任何地方。这些值将来自环境变量,并将控制数据流向何处以及流量来自何处。以上形式,我们现在可以创建一些环境变量,并控制我们的应用程序如何监听。我们可以以各种模式传递变量:

  • 我们可以通过 PORTPORT_AZUREPORT_AWS 传递值,或者它将指向 80。
  • 我们还可以检查 OpenShift 节点端口是否已启用,然后监听它们——我认为我在这里遗漏了 OpenShift IP 映射,但这可以在下一迭代中完成,到目前为止你应该明白这个意思了。
  • 我们也可以覆盖端口,并设置一个非常自定义的端口。如果平台要求你使用特定端口,这会很有用。
  • 最后,你还可以配置为端口本身使用一个变量名,我无法想象一个平台,但如果存在一个你必须通过特定名称获取端口的机会——例如,OpenShift 使用 OPENSHIFT_NODEJS_PORT 来捕获端口。

在正常情况下,我们必须编辑应用程序,更改端口的环境变量,然后重新打包并部署。这也意味着我们需要一个单独的镜像,以免我们的其他平台因名称不匹配而崩溃。在这些情况下,传递环境变量的名称,然后检查是否存在键将有助于我们。从而为编写应用程序提供了极大的灵活性,并允许 Docker 环境控制我们的应用程序如何在哪个端口上监听请求。因此,通过这一点,我们的 app.js 将更改为以下内容:

let serverConfigurations = require("./serverconfig");
app.listen(serverConfigurations.serverPort, () => {
    let serverStatus = `Server listening on localhost:
                        ${serverConfigurations.serverPort}.`;
    console.log(serverStatus);
});

正如你所见,我们不再试图在 app.js 中捕获端口,而是通过环境变量自己提供值。现在我们的服务器可以监听任何端口。Azure 在门户上记录以下消息,以便我们了解发生了什么:

2018_06_28_RD0003FF4BD7B3_docker.log:
db7d242cd7f4: Pull complete
Digest: sha256:17fb0bd0e2aac1161341049cfd56201f24f17a7d724917ccb965aa37dee6156a
Status: Downloaded newer image for afzaalahmadzeeshan/express-nodejs:latest

2018-06-28 12:59:01.404 INFO  - Starting container for site
2018-06-28 12:59:01.406 INFO  - docker run -d -p 51254:80 
    --name nodejs-container_0 -e WEBSITES_ENABLE_APP_SERVICE_STORAGE=false 
    -e WEBSITE_SITE_NAME=nodejs-container -e WEBSITE_AUTH_ENABLED=False 
    -e PORT=80 -e WEBSITE_ROLE_INSTANCE_ID=0 
    -e WEBSITE_INSTANCE_ID=580f93078686e0f2f3ba43ac3ad7d74b00459611341f6979290f
       ec8ac61c929f afzaalahmadzeeshan/express-nodejs:latest  

2018-06-28 12:59:01.410 INFO  - Logging is not enabled for this container.
Please use https://aka.ms/linux-diagnostics to enable logging to see container logs here.
2018-06-28 12:59:13.169 INFO  - Container nodejs-container_0 for site 
                                nodejs-container initialized successfully.

仔细研究日志,你可以看到 Azure 导致我们的代码短路并在 process.env.PORT 处停止,因为有一个变量已设置(($ docker ... -e PORT=80 ...)),这意味着我们的应用程序将开始监听该端口,然后 Azure 通过负载均衡器定向流量,并将 **51254** 端口的流量转发到容器的 **80** 端口。有意义吗? :-)

从 Dockerfile 中移除 EXPOSE

现在我们的 Docker 容器存在一个问题。并非所有平台都是 Azure,这让我们在端口和网络配置方面陷入僵局。因此,我们需要移除显式的端口暴露,并通过命令行来控制。在典型场景下,你有:

  • 选项是不暴露任何内容,也不监听任何流量。应用程序将从容器内部访问。
  • 选项是暴露端口,但不授予对应用程序的外部网络访问权限。
  • 选项是暴露端口,还将容器发布到主机到容器的端口映射。

第一个选项根本没有用,除非你要传递卷并让应用程序执行内部操作。第二个可能有用,考虑一个架构,其中还部署了负载均衡器,而最后一个是我们感兴趣的。但与其硬编码,我们可以使用命令的 --expose 参数来暴露一个动态端口给容器。一个这样做的例子是:

$ docker run -d --name mynodeapp --expose=1234 afzaalahmadzeeshan/express-nodejs:latest

这可以达到目的,我们可以移除硬编码的 EXPOSE 命令——如果我错了请纠正我——我相信这个终端命令也会覆盖 Dockerfile 中提供的 EXPOSE 指令。但同样,实际上是 -p--publish,它完成了主机和容器的实际映射;阅读 这个线程来了解更多关于此的信息。

通过环境变量暴露 App Insights 密钥

够了!现在,让我们看一个实际的例子,说明它是如何工作的,以及我们如何通过环境变量将值传递给应用程序的某个部分。我将使用 Azure 的 App Service 并从那里配置设置来在环境中暴露一些值。这次,我们将添加我们自己的设置,我想添加的设置将具有键 appInsightsKey,因为我们的应用程序将尝试在那里查找环境变量的值。让我们去配置它,我在 Azure 中创建了一个额外的 Application Insights 类型资源,我将获取 Instrumentation Key,它在 **Properties** 选项卡下可用,或者你也可以在 **Overview** 选项卡中捕获它。

图 4:Azure Portal 上的 Application Insights Overview 选项卡。

捕获密钥,然后使用 App Service 中的 App Settings 选项卡输入该值,这将为运行时创建一个新的环境变量,并允许你的应用程序从环境中捕获该值,而不是硬编码或在 Dockerfile 中提供。

图 5:Azure Portal 中 Azure App Service 的应用程序设置页面。

完成后,请重启应用程序,以便在应用程序启动时将环境变量注入到应用程序中。现在我们知道我们的应用程序将设置该变量,我们在 insights 中使用以下代码来检查应用程序如何监听该变量并配置管道以将日志推送到 App Insights。

const appInsights = require("applicationinsights");
const serverConfig = require("../serverconfig");

let enabled = true;
try {
    appInsights.setup(serverConfig.applicationInsightsInstrumentationKey)
        .setAutoDependencyCorrelation(true)
        .setAutoCollectRequests(true)
        .setAutoCollectPerformance(true)
        .setAutoCollectExceptions(true)
        .setAutoCollectDependencies(true)
        .setAutoCollectConsole(true)
        .setUseDiskRetryCaching(true)
        .start();
} catch (error) {
    console.log("Cannot start Application Insights; either pass the value to this app, 
                 or use the App Insights default environment variable.");
    enabled = false;
}

module.exports = {
    ready: enabled,
    logRequest: function(req) {
        let message = `Request was captured for path: ${req.originalUrl}.`;
        console.log(message);
        if(this.ready) {
            appInsights.defaultClient.trackRequest({ name: "normalPage", properties: 
                { type: "page", value: req.originalUrl, dateTime: new Date() }});
        }
    },
    logEvent: function(name, data) {
        if(this.ready) {
            appInsights.defaultClient.trackEvent
            ({ name: name, properties: { data: data }});
        }
    },
    logApiCall: function(apiRoute) {
        if(this.ready) {
            appInsights.defaultClient.trackRequest({ name: "apiCall", properties: 
            { type: "api", value: apiRoute, dateTime: new Date() }});
        }
    }
}

这是最基本的日志实现,它在控制台打印信息,如果启用了 AppInsights,它还会将请求记录在那里。现在如果我们再次返回并访问 Azure Portal 上的服务,我们可以轻松地看到,一旦流量开始命中,日志就可用了。

图 6:Application Insights Metric Explorer 显示当前应用程序使用情况和服务器响应时间的指标。

这样做的优点是,我们现在可以在运行时更改 AppInsights。更好的是,我们还可以在 App Settings 中使用不同的 instrumentation key 来测试服务。Azure 支持 Deployment Slots,我们可以在那里测试应用程序,这样我们就可以消除应用程序对资源的依赖,并在运行时提供资源、路径和管道。

为什么要做这一切?

Docker 是在云平台上构建和发布应用程序的新方式。Docker 也在本地环境中工作,例如在本地部署集群。Docker 的问题在于,你永远不知道你的应用程序是如何提供给客户的。它可以运行,

  • 作为一个隔离的进程,没有人可以访问。
  • 作为一个监听流量的单个进程——同样,流量由主机内部管理。
  • 作为服务中的单个节点,运行在负载均衡器后面。

部署应用程序有很多方法。问题在于,当你必须在不同的平台和环境中定义不同的依赖项时。直接的方法是部署带有不同标签的单独镜像,并按此操作。

1: afzaalahmadzeeshan/express-nodejs:azure
2: afzaalahmadzeeshan/express-nodejs:aws
3: afzaalahmadzeeshan/express-nodejs:gcp
4: afzaalahmadzeeshan/express-nodejs:production
5: afzaalahmadzeeshan/express-nodejs:testing
6: afzaalahmadzeeshan/express-nodejs:legacy

它们有效,并且它们是不同的镜像,你可以分别运行它们。但这意味着你现在必须单独管理应用程序和镜像。你将拥有不同的 Dockerfile,不同的配置需要检查,更不用说你必须进行的不同的测试了。一个很好的方法是使用通过控制反转或依赖注入学到的概念。你将参数、对象等注入到应用程序中,应用程序将继续按指示运行。

这就是本文的主要目标,解释我在将 Node.js 容器化应用程序部署到 Azure、AWS 和 OpenShift 的旅程中必须尝试的一些基本实践。我仍然需要尝试 Heroku 平台并查看它的工作原理。但目前,就这些了。

我鼓励你尝试示例和镜像,请分享你的反馈和问题,以便我改进。哦,你也欢迎直接向我的 GitHub 存储库提交改进。:-)

© . All rights reserved.