本文是帮助您将 2023 年 3 月微服务中的概念付诸实践的四篇教程之一:开始交付微服务:
- 如何部署和配置微服务(本文)
- 如何安全地管理容器中的机密
- 如何使用 GitHub Actions 自动化微服务金丝雀发布
- 如何使用 OpenTelemetry 跟踪来了解您的微服务
所有应用程序都需要配置,但配置微服务时的注意事项可能与单体应用程序不同。我们可以查看十二因素应用程序的因素 3(在环境中存储配置)以获取适用于这两种类型的应用程序的指南,但该指南可以适用于微服务应用程序。特别是,我们可以调整定义服务配置的方式,向服务提供配置,并使服务可用作可能依赖于它的其他服务的配置值。
为了让步要真正了解如何针对微服务调整因素 3(特别是配置文件、数据库和服务发现的最佳实践),请阅读我们博客上的配置微服务应用程序的最佳实践。这篇文章是将这些知识付诸实践的好方法。
注意:本教程的目的是说明一些核心概念,而不是展示在生产中部署微服务的正确方法。虽然它使用真正的“微服务”架构,但有一些重要的注意事项:
- 本教程不使用 Kubernetes 或 Nomad 等容器编排框架。这样您就可以了解微服务概念,而不必陷入某个框架的细节中。此处介绍的模式可移植到运行这些框架之一的系统。
- 这些服务经过优化是为了易于理解,而不是为了软件工程的严谨性。重点是看服务”系统中的角色及其通信模式,而不是代码的细节。有关详细信息,请参阅各个服务的自述文件。
教程概述
本教程说明了因素 3 概念如何应用于微服务应用。在四个挑战中,您将探索一些常见的微服务配置模式并使用这些模式部署和配置服务:
-
在挑战 1 和挑战 2 中,您将探索第一个模式,该模式涉及您在何处找到微服务应用的配置。共有三个典型位置:
- 应用程序代码
- 应用程序的部署脚本
- 部署脚本访问的外部源
- 在挑战 3 中,您设置了另外两种模式:通过 NGINX 作为反向代理向外界公开应用,以及使用 Consul 启用服务发现。
- 在挑战 4 中,您实现了最终模式:使用实例您的微服务作为“作业运行程序”,执行与其通常功能不同的一次性操作(在本例中模拟数据库迁移)。
本教程使用四种技术:
- messenger – 为本教程创建的具有消息存储功能的简单聊天 API
- NGINX 开源 – 消息服务和更广泛系统的入口点
- Consul – 动态服务注册表和键值存储
- RabbitMQ – 一种流行的开源消息代理,支持服务异步通信
观看此视频以大致了解本教程。这些步骤与本文并不完全一致,但有助于理解概念。
先决条件和设置
先决条件
要在您自己的环境中完成本教程,您需要:
- Linux/Unix 兼容环境t
- 基本熟悉 Linux 命令行、JavaScript 和 bash(但提供并解释了所有代码和命令,因此即使知识有限,您仍然可以取得成功)
- Docker 和 Docker Compose
-
Node.js 19.x 或更高版本
- 我们测试了 19.x 版本,但预计较新版本的 Node.js 也能正常工作。
- 有关安装 Node,js 的详细信息,请参阅消息服务存储库中的自述文件。您还可以安装 asdf 以获得与容器中使用的完全相同的 Node.js 版本。
- curl(已安装在大多数系统上)
- 教程概述中列出的四种技术:messenger(您将在下一部分中下载)、NGINX 开源、Consul 和 RabbitMQ。
设置
在您的主目录中,创建 microservices-march 目录并克隆 t他将本教程的 GitHub 存储库放入其中。 (您还可以使用不同的目录名称并相应地调整说明。)
注意:在整个教程中,省略了 Linux 命令行上的提示符,以便更轻松地将命令复制并粘贴到终端中。波形符 (~) 代表您的主目录。
mkdir ~/微服务-march
cd ~/微服务-march
git clone https://github.com/microservices-march/platform.git –branch mm23-twelve-factor-start
git clone https://github.com/microservices-march/messenger.git –branch mm23-twelve-factor-start
更改为平台存储库并启动 Docker Compose:
光盘平台
docker compose up -d –build
这会启动 RabbitMQ 和 Consul,这将在后续挑战中使用。
- -d 标志指示 Docker Compose 在容器启动后与容器分离(否则容器将保持连接到您的终端)。
- –build 标志指示 Docker Compose 在启动时重建所有映像。这可确保您正在运行的映像在任何潜在的文件更改时保持更新。
更改为 Messenger 存储库并启动 Docker Compose:
cd ../信使
docker compose up -d –build
这将启动用于信使服务的 PostgreSQL 数据库,在本教程的其余部分中我们将其称为信使数据库。
挑战 1:定义应用程序级微服务配置
在本次挑战中,您将在我们将在教程中介绍的三个位置中的第一个位置设置配置:应用程序级别。 (挑战 2 说明了第二个和第三个位置、部署脚本和外部源。)
十二要素应用程序明确排除了应用程序级配置,因为此类配置不需要在不同的部署环境之间进行更改(十二要素应用程序将其称为部署))。尽管如此,为了完整性,我们涵盖了所有三种类型 – 在开发、构建和部署服务时处理每个类别的方式是不同的。
消息服务是用 Node.js 编写的,入口点位于消息存储库的 app/index.mjs 中。文件的这一行:
app.use(express.json());
是应用程序级配置的示例。它将 Express 框架配置为将 application/json 类型的请求主体反序列化为 JavaScript 对象。
此逻辑与您的应用程序代码紧密耦合,而不是十二因素应用程序所认为的“配置”。然而,在软件中一切都取决于你的情况,不是吗?
在接下来的两节中,您将修改此行以实现应用程序级配置的两个示例。
示例 1
在此示例中,您设置消息服务接受的请求正文的最大大小。这个尺寸限制我由 express.json 函数的 limit 参数设置,如 Express API 文档中所述。在这里,您将 limit 参数添加到上面讨论的 Express 框架的 JSON 中间件的配置中。
在您喜欢的文本编辑器中,打开 app/index.mjs 并替换:
应用程序.use(express.json())
与:
app.use(express.json({ limit: “20b” }));
在应用终端(您在“设置”中使用的终端)中,切换到应用目录并启动 Messenger 服务:
光盘应用程序
npm 安装
节点索引.mjs
messenger_service 监听端口 4000
启动第二个单独的终端会话(后续指令调用客户端)并向消息服务发送 POST 请求。该错误消息表明请求已成功处理,因为请求正文低于步骤 1 中设置的 20 字节限制,但 JSON 负载的内容不正确:
curl -d ‘{ “text”: “hello” }’ -H “内容类型:application/json” -X POST http://localhost:4000/conversations
…
{ “error”: “对话必须有 2 个唯一用户” }
发送稍长的消息正文(同样在客户端)。输出比步骤 3 多得多,包括指示这次请求正文超过 20 字节的错误消息:
curl -d ‘{ “text”: “hello, world” }’ -H “Content-Type: application/json” -X POST http://localhost:4000/conversations
…
\“PayloadTooLargeError:请求实体太大”
示例 2
此示例使用 convict,这是一个库,可让您在单个文件中定义整个配置“架构”。它还说明了十二因素应用的因素 3 的两条准则:
- 将配置存储在环境变量中 – 您可以修改应用,以便使用环境变量 (JSON_BODY_LIMIT) 设置最大正文大小,而不是在应用代码中进行硬编码。
- 显然是优化您的服务配置 – 这是针对微服务的因素 3 的改编。如果您不熟悉这个概念,我们建议您花一些时间阅读我们博客上配置微服务应用的最佳实践中的相关内容。
该示例还设置了一些您将在挑战 2 中利用的“管道”:您将在该挑战中创建的信使部署脚本设置 JSON_BODY_LIMIT 环境变量,您将其插入到此处的应用程序代码中,作为说明部署脚本中指定的配置。
打开 convict 配置文件 app/config/config.mjs,并将以下内容添加为 amqpport 键后的新键:
jsonBodyLimit:{
doc:`将由解析的最大大小(包含单位)
JSON 中间件。单元解析是由
https://www.npmjs.com/package/bytes 库。
例如:“100kb”`,
格式:字符串,
默认值:空,
环境:“JSON_BODY_LIMIT”,
},
的当您在下面的步骤 3 中使用 JSON_BODY_LIMIT 环境变量在命令行上设置最大主体大小时,convict 库会负责解析它:
- 从正确的环境变量中提取值
- 检查变量的类型(字符串)
- 允许在应用程序中的 jsonBodyLimit 键下对其进行访问
在 app/index.mjs 中替换:
app.use(express.json({ limit: “20b” }));
与
app.use(express.json({ limit: config.get(“jsonBodyLimit”) }));
在应用终端(您在示例 1 的步骤 2 中启动 Messenger 服务的位置),按 Ctrl+c 停止该服务。然后再次启动它,使用 JSON_BODY_LIMIT 环境变量将最大正文大小设置为 27 字节:
^c
JSON_BODY_LIMIT=27b节点index.mjs
这是一个修改配置方法的示例,这样做对您的用例有意义 – 您已经从硬编码值(在本例中是大小limit)在应用程序代码中使用环境变量进行设置,如十二因素应用程序所建议的。
如上所述,在挑战 2 中,当您使用消息服务的部署脚本而不是在命令行上设置环境变量时,JSON_BODY_LIMIT 环境变量的使用将成为第二个配置位置的示例。
在客户端中,重复示例 1 的步骤 4 中的curl 命令(使用较大的请求正文)。由于您现在已将大小限制增加到 27 字节,因此请求正文不再超出限制,并且您会收到错误消息,指示请求已处理,但 JSON 负载的内容不正确:
curl -d ‘{ “text”: “hello, world” }’ -H “Content-Type: application/json” -X POST http://localhost:4000/conversations
{ “error”: “对话必须有 2 个唯一用户” }
如果您愿意,您可以关闭客户端。你’将在应用程序终端中发出本教程其余部分中的所有命令。
在应用终端中,按 Ctrl+c 停止 Messenger 服务(您已在上面的步骤 3 中停止并重新启动了该终端中的服务)。
^c
停止信使数据库。您可以安全地忽略显示的错误消息,因为平台存储库中定义的基础架构元素仍在使用网络。在 Messenger 存储库的根目录运行此命令。
docker 组合下来
…无法删除网络 mm_2023…
挑战 2:为服务创建部署脚本
“配置应该与代码严格分离(否则部署之间怎么会有所不同?)”
– 来自十二因素应用程序的因素 3
乍一看,您可能会将其理解为“不要将配置签入源代码管理”。在本次挑战中,您将实现微服务环境的通用模式,看起来可能违反了这条规则,但实际上尊重了这条规则,同时提供了对微服务环境至关重要的有价值的流程改进。
在此挑战中,您将创建部署脚本来模拟基础架构即代码和部署清单的功能,这些清单为微服务提供配置,修改脚本以使用外部配置源,设置机密,然后运行脚本部署服务及其基础设施。
您在 Messenger 存储库中新创建的基础结构目录中创建部署脚本。称为基础设施(或该名称的某种变体)的目录是现代微服务架构中的常见模式,用于存储以下内容:
- 基础设施即代码(例如 Terraform、AWS CloudFormation、Google Cloud Deployment Manager 和 Azure Resource Manager)
- 容器编排系统的配置(例如 Helm 图表和 Kubernetes 清单)
- 与应用程序部署相关的任何其他文件
这种模式的好处包括:
- 它将服务部署和服务特定基础设施(例如数据库)的部署的所有权分配给拥有该服务的团队。
- 团队可以确保对任何这些元素的更改都经过其开发流程(代码审查、CI 等)。
- 团队可以轻松更改服务及其支持基础架构的部署方式,而无需依赖外部团队为其工作。
如前所述,我们本教程的目的不是展示如何设置真实的系统,并且您在本次挑战中部署的脚本与真实的生产系统并不相似。相反,它们说明了在处理微服务相关的基础设施部署时通过特定于工具的配置解决的一些核心概念和问题,同时还将脚本抽象到最小程度无法使用特定的工具。
创建初始部署脚本
在应用程序终端中,在 Messenger 存储库的根目录下创建一个基础结构目录,并创建文件以包含 Messenger 服务和 Messenger 数据库的部署脚本。根据您的环境,您可能需要在 chmod 命令前添加 sudo 前缀:
mkdir 基础设施
裁谈会基础设施
触摸messenger-deploy.sh
chmod +x 信使-deploy.sh
触摸messenger-db-deploy.sh
chmod +x messages-db-deploy.sh
在您喜欢的文本编辑器中,打开messenger-deploy.sh 并添加以下内容来为Messenger 服务创建初始部署脚本:
#!/bin/bash
设置-e
JSON_BODY_LIMIT=20b
码头工人运行\
– R M \
-e JSON_BODY_LIMIT=”${JSON_BODY_LIMIT}” \
信使
此脚本目前尚未完成,但它说明了几个概念:
- 它通过包含该 con 来为环境变量分配一个值直接在部署脚本中进行配置。
- 它使用 docker run 命令上的 -e 标志在运行时将环境变量注入到容器中。
以这种方式设置环境变量的值可能看起来多余,但这意味着 – 无论此部署脚本变得多么复杂 – 您都可以快速查看脚本的最顶部并了解配置数据的结构提供给部署。
此外,虽然真正的部署脚本可能不会显式调用 docker run 命令,但此示例脚本旨在传达 Kubernetes 清单等内容正在解决的核心问题。当使用 Kubernetes 等容器编排系统时,部署会启动一个容器,并且从 Kubernetes 配置文件派生的应用程序配置可供该容器使用。因此,我们可以将此示例部署文件视为部署 sc 的最小版本ript 与特定于框架的部署文件(例如 Kubernetes 清单)起着相同的作用。
在真实的开发环境中,您可以将此文件签入源代码管理并对其进行代码审查。这使团队的其他成员有机会评论您的设置,从而有助于避免错误配置的值导致意外行为的事件。例如,在此屏幕截图中,团队成员正确地指出传入 JSON 请求正文的 20 字节限制(使用 JSON_BODY_LIMIT 设置)太低。
修改部署脚本以从外部源查询配置值
在挑战的这一部分中,您为微服务的配置设置第三个位置:在部署时查询的外部源。动态注册值并在部署时从外部源获取它们比硬编码值要好得多,硬编码值必须更新不断地修改并可能导致失败。有关讨论,请参阅我们博客上的配置微服务应用程序的最佳实践。
此时有两个基础设施组件正在后台运行,提供消息服务所需的辅助服务:
app/config/config.mjs 中消息服务的 convict 模式定义了与这些外部配置相对应的所需环境变量。在本部分中,您将设置这两个组件,通过在公共可访问位置设置变量值来提供配置,以便消息服务在部署时可以查询它们。
RabbitMQ 和信使数据库所需的连接信息已注册在 Consul Key/V 中alue (KV) 存储,这是所有服务部署时都可以访问的公共位置。 Consul KV 存储不是存储此类数据的标准位置,但为了简单起见,本教程使用它。
将基础设施/messenger-deploy.sh(在上一节的步骤 2 中创建)的内容替换为以下内容:
#!/bin/bash
设置-e
# 此配置需要新的提交才能更改
NODE_ENV=生产
端口=4000
JSON_BODY_LIMIT=100kb
# 通过从中提取信息来配置 Postgres 数据库
# 系统
POSTGRES_USER=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-application-user?raw=true)
PGPORT=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-port?raw=true)
PGHOST=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-host?raw=true)
# 通过从系统拉取 RabbitMQ 配置
AMQPHOST=$(curl -X GET http://localhost:8500/v1/kv/amqp-host?raw=true)
AMQPPORT=$(curl -X GET http://localhost:8500/v1/kv/amqp-端口?原始=真)
码头工人运行\
– R M \
-e NODE_ENV=”${NODE_ENV}” \
-e 端口=“${端口}”\
-e JSON_BODY_LIMIT=”${JSON_BODY_LIMIT}” \
-e PGUSER=”${POSTGRES_USER}” \
-e PGPORT=”${PGPORT}” \
-e PGHOST=”${PGHOST}” \
-e AMQPPORT=“${AMQPPORT}”\
-e AMQPHOST=”${AMQPHOST}” \
信使
此脚本举例说明了两种类型的配置:
- 直接在部署脚本中指定的配置 – 它设置部署环境 (NODE_ENV) 和端口 (PORT),并将 JSON_BODY_LIMIT 更改为 100 KB,这是一个比 20 字节更实际的值。
- 从外部源查询的配置 – 它从 Consul KV 存储中获取 POSTGRES_USER、PGPORT、PGHOST、AMQPHOST 和 AMQPPORT 环境变量的值。您可以通过以下两个步骤在 Consul KV 存储中设置环境变量的值。
打开messenger-db-deploy.sh并添加以下内容以创建初始部署脚本对于信使数据库:
#!/bin/bash
设置-e
端口=5432
POSTGRES_USER=postgres
码头工人运行\
-d \
– R M \
–name Messenger-db \
-v 数据库数据:/var/lib/postgresql/data/pgdata \
-e POSTGRES_USER=”${POSTGRES_USER}” \
-e POSTGRES_PASSWORD=”${POSTGRES_PASSWORD}” \
-e PGPORT=”${PORT}” \
-e PGDATA=/var/lib/postgresql/data/pgdata \
–网络mm_2023 \
postgres:15.1
# 向 Consul 注册有关数据库的详细信息
curl -X PUT http://localhost:8500/v1/kv/messenger-db-port \
-H“内容类型:application/json”\
-d“${端口}”
curl -X PUT http://localhost:8500/v1/kv/messenger-db-host \
-H“内容类型:application/json”\
-d ‘messenger-db’ # 这与上面的“–name”标志匹配
#(主机名)
curl -X PUT http://localhost:8500/v1/kv/messenger-db-application-user \
-H“内容类型:application/json”\
-d“${POSTGRES_USER}”
除了定义可在部署时由信使服务查询的配置之外,该脚本说明了与创建初始部署脚本中的信使服务初始脚本相同的两个概念):
- 它直接在部署脚本中指定某些配置,在本例中是告诉 PostgreSQL 数据库运行的端口以及默认用户的用户名。
- 它使用 -e 标志运行 Docker,以在运行时将环境变量注入到容器中。它还将正在运行的容器的名称设置为 messenger-db,该名称将成为您在设置第 2 步启动平台服务时创建的 Docker 网络中数据库的主机名。
在实际部署中,通常是平台团队(或类似团队)负责处理平台存储库中 RabbitMQ 等服务的部署和维护,就像您对信使存储库中的信使数据库所做的那样。然后,平台团队确保依赖于该基础设施的服务可以发现该基础设施的位置。为了为了本教程的目的,请自行设置 RabbitMQ 值:
curl -X PUT –silent –output /dev/null –show-error –fail \
-H“内容类型:application/json”\
-d“rabbitmq”\
http://localhost:8500/v1/kv/amqp-主机
curl -X PUT –silent –output /dev/null –show-error –fail \
-H“内容类型:application/json”\
-d“5672”\
http://localhost:8500/v1/kv/amqp-port
(您可能想知道为什么使用 amqp 来定义 RabbitMQ 变量 – 这是因为 AMQP 是 RabbitMQ 使用的协议。)
在部署脚本中设置机密
消息服务的部署脚本中只缺少一项(关键)数据 – 消息数据库的密码!
注意:机密管理不是本教程的重点,因此为了简单起见,机密是在部署文件中定义的。切勿在实际环境(开发、测试或生产)中执行此操作 – 它会产生巨大的影响安全风险。
要了解正确的秘密管理,请查看 2023 年 3 月《微服务》的第 2 单元“微服务秘密管理 101”。(剧透:秘密管理工具是存储秘密的唯一真正安全的方法)。
将基础设施/messenger-db-deploy.sh 的内容替换为以下内容,以将信使数据库的密码秘密存储在 Consul KV 存储中:
#!/bin/bash
设置-e
端口=5432
POSTGRES_USER=postgres
# 注意:切勿在实际部署中执行此操作。存储密码
# 仅在加密的秘密存储中。
POSTGRES_PASSWORD=postgres
码头工人运行\
– R M \
–name messenger-db-primary \
-d \
-v 数据库数据:/var/lib/postgresql/data/pgdata \
-e POSTGRES_USER=”${POSTGRES_USER}” \
-e POSTGRES_PASSWORD=”${POSTGRES_PASSWORD}” \
-e PGPORT=”${PORT}” \
-e PGDATA=/var/lib/postgresql/data/pgdata \
–网络mm_2023 \
postgres:15.1
echo “注册关键messenger-db-port\n”
卷曲-X PUT –silent –output /dev/null –show-error –fail http://localhost:8500/v1/kv/messenger-db-port \
-H“内容类型:application/json”\
-d“${端口}”
echo“注册密钥messenger-db-host\n”
curl -X PUT –silent –output /dev/null –show-error –fail http://localhost:8500/v1/kv/messenger-db-host \
-H“内容类型:application/json”\
-d ‘messenger-db-primary’ # 这与上面的“–name”标志匹配
# 对于我们的设置来说这意味着主机名
echo“注册密钥messenger-db-application-user\n”
curl -X PUT –silent –output /dev/null –show-error –fail http://localhost:8500/v1/kv/messenger-db-application-user \
-H“内容类型:application/json”\
-d“${POSTGRES_USER}”
echo“注册密钥messenger-db-password-never-do-this\n”
curl -X PUT –silent –output /dev/null –show-error –fail http://localhost:8500/v1/kv/messenger-db-password-never-do-this \
-H“内容类型:application/json”\
-d“${POSTGRES_PASSWORD}”
printf “\n用 Consu 注册 postgres 详细信息l\n”
将基础设施/messenger-deploy.sh 的内容替换为以下内容,以从 Consul KV 存储中获取信使数据库密码机密:
#!/bin/bash
设置-e
# 此配置需要新的提交才能更改
NODE_ENV=生产
端口=4000
JSON_BODY_LIMIT=100kb
# 通过从中提取信息来配置 Postgres 数据库
# 系统
POSTGRES_USER=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-application-user?raw=true)
PGPORT=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-port?raw=true)
PGHOST=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-host?raw=true)
# 注意:切勿在实际部署中执行此操作。存储密码
# 仅在加密的秘密存储中。
PGPASSWORD=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-password-never-do-this?raw=true)
# 通过从系统拉取 RabbitMQ 配置
AMQPHOST=$(curl -X GET http://localhost:8500/v1/kv/amqp-host?raw=true)
AMQPPORT=$(curl -X GET http://localhost:8500/v1/kv/amqp-端口?原始=真)
码头工人运行\
– R M \
-d \
-e NODE_ENV=”${NODE_ENV}” \
-e 端口=“${端口}”\
-e JSON_BODY_LIMIT=”${JSON_BODY_LIMIT}” \
-e PGUSER=”${POSTGRES_USER}” \
-e PGPORT=”${PGPORT}” \
-e PGHOST=”${PGHOST}” \
-e PGPASSWORD=”${PGPASSWORD}” \
-e AMQPPORT=“${AMQPPORT}”\
-e AMQPHOST=”${AMQPHOST}” \
–网络mm_2023 \
信使
运行部署脚本
更改为 Messenger 存储库中的应用程序目录并为 Messenger 服务构建 Docker 映像:
cd ../应用程序
docker build -t Messenger 。
验证只有属于平台服务的容器正在运行:
docker ps –format ‘{{.Names}}’
领事服务器
领事客户
兔子MQ
更改为信使存储库的根目录并部署信使数据库和信使服务:
光盘 ..
./infrastructure/messenger-db-deploy.sh
./infrastructure/messenger-deploy.sh
messenger-db-deploy.sh 脚本创建消息数据库并向系统注册适当的信息(在本例中为 Consul KV 存储)。
然后,messenger-deploy.sh 脚本启动应用程序,并从系统(同样是 Consul KV 存储)中提取由 messenger-db-deploy.sh 注册的配置。
提示:如果容器启动失败,请删除部署脚本中 docker run 命令的第二个参数(-d \ 行)并再次运行脚本。然后容器在前台启动,这意味着它的日志出现在终端中并且可能会识别问题。解决问题后,恢复 -d \ 行,以便实际容器在后台运行。
向应用程序发送简单的运行状况检查请求以验证部署是否成功:
卷曲本地主机:4000/health
卷曲:(7)11毫秒后无法连接到本地主机端口4000:连接被拒绝
哎呀,失败了!事实证明,你还缺一个关键的配置部分和信使服务不会暴露给更广泛的系统。它在 mm_2023 网络中运行良好,但该网络只能从 Docker 内部访问。
停止正在运行的容器,准备在下一个挑战中创建新映像:
docker rm $(docker stop $(docker ps -a -q –filter祖先=信使 –format=”{{.ID}}”))
挑战 3:向外界公开服务
在生产环境中,您通常不会直接公开服务。相反,您遵循常见的微服务模式,并将反向代理服务放置在主服务之前。
在此挑战中,您通过设置服务发现向外界公开信使服务:注册新服务信息并动态更新其他服务访问的该信息。为此,您可以使用以下技术:
- Consul,一个动态服务注册中心,一个nd Consul模板,一个基于Consul数据动态更新文件的工具
- NGINX 开源,作为反向代理和负载均衡器,为您的消息服务公开单个入口点,该入口点将由在容器中运行的应用程序的多个单独实例组成
要了解有关服务发现的更多信息,请参阅我们博客上的“配置微服务应用程序的最佳实践”中的“将服务作为配置提供”。
设置领事
messenger 存储库中的 app/consul/index.mjs 文件包含在启动时向 Consul 注册 Messenger 服务以及在正常关闭时取消注册所需的所有代码。它公开了一个函数,register,该函数向 Consul 的服务注册表注册任何新部署的服务。
在您喜欢的文本编辑器中,打开 app/index.mjs 并在其他导入语句后添加以下代码片段,以从 app/consul/index.mjs 导入注册函数:
进口t { 注册为 registerConsul } 来自“./consul/index.mjs”;
然后如图所示修改脚本末尾的 SERVER START 部分,以便在应用程序启动后调用 registerConsul():
/* =================
服务器启动
================== */
app.listen(端口, async () => {
console.log(`messenger_service 监听端口 ${port}`);
注册领事();
});
导出默认应用程序;
在 app/config/config.mjs 中打开 convict 架构,并在示例 2 的步骤 1 中添加的 jsonBodyLimit 键后添加以下配置值。
领事服务名称:{
doc: “Consul 中注册服务的名称。如果未指定,则服务未注册”,
格式: ”*”,
默认值:空,
环境:“CONSUL_SERVICE_NAME”,
},
领事主机:{
doc: “Consul 客户端运行的主机”,
格式:字符串,
默认值:“领事客户端”,
环境:“CONSUL_HOST”,
},
领事端口:{
doc: “Consul 客户端的端口”,格式:“端口”,
默认:8500,
环境:“CONSUL_PORT”,
},
这配置了注册新服务的名称,并定义了 Consul 客户端的主机名和端口。在下一步中,您将修改消息服务的部署脚本以包含这个新的 Consul 连接和服务注册信息。
打开基础设施/messenger-deploy.sh并将其内容替换为以下内容,以将您在上一步中设置的Consul连接和服务注册信息包含在信使服务配置中:
#!/bin/bash
设置-e
# 此配置需要新的提交才能更改
NODE_ENV=生产
端口=4000
JSON_BODY_LIMIT=100kb
CONSUL_SERVICE_NAME=”信使”
# Consul 主机和端口包含在每个主机中,因为我们
# 在我们了解 Consul 之前无法查询它们
CONSUL_HOST=”${CONSUL_HOST}”
CONSUL_PORT=”${CONSUL_PORT}”
# 通过从中提取信息来配置 Postgres 数据库
# 系统
POSTGRES_USER=$(卷曲-X GET “http://localhost:8500/v1/kv/messenger-db-application-user?raw=true”)
PGPORT=$(curl -X GET “http://localhost:8500/v1/kv/messenger-db-port?raw=true”)
PGHOST=$(curl -X GET “http://localhost:8500/v1/kv/messenger-db-host?raw=true”)
# 注意:切勿在实际部署中执行此操作。存储密码
# 仅在加密的秘密存储中。
PGPASSWORD=$(curl -X GET “http://localhost:8500/v1/kv/messenger-db-password-never-do-this?raw=true”)
# 通过从系统拉取 RabbitMQ 配置
AMQPHOST=$(curl -X GET “http://localhost:8500/v1/kv/amqp-host?raw=true”)
AMQPPORT=$(curl -X GET “http://localhost:8500/v1/kv/amqp-port?raw=true”)
码头工人运行\
– R M \
-d \
-e NODE_ENV=”${NODE_ENV}” \
-e 端口=“${端口}”\
-e JSON_BODY_LIMIT=”${JSON_BODY_LIMIT}” \
-e PGUSER=”${POSTGRES_USER}” \
-e PGPORT=”${PGPORT}” \
-e PGHOST=”${PGHOST}” \
-e PGPASSWORD=”${PGPASSWORD}” \
-e AMQPPORT=“${AMQPPORT}”\
-e AMQPHOST=”${AMQPHOST}” \
-e CONSUL_HOST=”${CONSUL_HOST}” \
-e CONSUL_PORT=“${CONSUL_PORT}” \
-e CONSUL_SERVICE_NAME=”${CONSUL_SERVICE_NAME}” \
–网络mm_2023 \
信使
主要注意事项是:
- CONSUL_SERVICE_NAME 环境变量告诉信使服务实例在向 Consul 注册时要使用什么名称。
- CONSUL_HOST 和 CONSUL_PORT 环境变量适用于在部署脚本运行位置运行的 Consul 客户端。
注意:在实际部署中,这是一个必须在团队之间达成一致的配置示例 – 负责 Consul 的团队必须在所有环境中提供 CONSUL_HOST 和 CONSUL_PORT 环境变量,因为如果没有此变量,服务将无法查询 Consul连接信息。
在应用程序终端中,切换到应用程序目录,停止所有正在运行的 Messenger 服务实例,并重建 Docker 映像以烘焙新的服务注册代码:
光盘应用程序
docker rm $(docker stop $(做cker ps -a -q –filter祖先=信使 –format=”{{.ID}}”))
docker build -t Messenger 。
在浏览器中导航到 http://localhost:8500 以查看 Consul UI 的运行情况(尽管还没有发生任何有趣的事情)。
在信使存储库的根目录中,运行部署脚本以启动信使服务的实例:
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-deploy.sh
在浏览器的 Consul UI 中,单击标题栏中的“服务”以验证单个消息服务是否正在运行。
多运行几次部署脚本以启动更多消息服务实例。在 Consul UI 中验证它们正在运行。
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-deploy.sh
设置 NGINX
下一步是添加 NGINX 开源作为反向代理和负载均衡器,以将传入流量路由到所有正在运行的信使实例。
在应用程序终端中,将目录更改为 Messenger 存储库的根目录,并创建一个名为 load-balancer 的目录和三个文件:
mkdir 负载均衡器
cd 负载均衡器
触摸 nginx.ctmpl
触摸领事模板配置.hcl
触摸 Dockerfile
Dockerfile 定义了 NGINX 和 Consul 模板运行的容器。当消息服务在其服务注册表中发生变化(服务实例启动或关闭)时,Consul 模板使用其他两个文件动态更新 NGINX 上游。
打开在步骤 1 中创建的 nginx.ctmpl 文件并添加以下 NGINX 配置片段,Consul 模板使用该片段动态更新 NGINX 上游组:
上游messenger_service {
{{-范围服务“信使”}}
服务器 {{ .地址 }}:{{ .端口 }};
{{- 结尾 }}
}
服务器 {
听8085;
服务器名称本地主机;
地点 / {
proxy_pass http://messenger_service;
add_header 上游主机 $upst令地址;
}
}
此代码段将向 Consul 注册的每个信使服务实例的 IP 地址和端口号添加到 NGINX messenger_service 上游组。 NGINX 将传入请求代理到动态定义的上游服务实例集。
打开在步骤 1 中创建的 consul-template-config.hcl 文件并添加以下配置:
领事{
地址 =“领事客户端:8500”
重试 {
启用=真
尝试次数 = 12
退避=“250ms”
}
}
模板 {
源=“/usr/templates/nginx.ctmpl”
目的地=“/etc/nginx/conf.d/default.conf”
烫发 = 0600
command =“if [ -e /var/run/nginx.pid ]; then nginx -s reload; else nginx; fi”
}
Consul 模板的此配置告诉它重新渲染源模板(上一步中创建的 NGINX 配置片段),将其放置在指定的目的地,最后运行指定的命令(这告诉 NGINX 重新加载其配置) )。
<实际上,这意味着每次在 Consul 中注册、更新或注销服务实例时都会创建一个新的 default.conf 文件。然后,NGINX 会在不停机的情况下重新加载其配置,从而确保 NGINX 拥有一组可以向其发送流量的最新、健康的服务器(消息服务实例)。
打开在步骤 1 中创建的 Dockerfile 文件并添加以下内容,这将构建 NGINX 服务。 (为了本教程的目的,您不需要了解 Dockerfile,但为了方便起见,代码已内联记录。)
来自 nginx:1.23.1
ARG CONSUL_TEMPLATE_VERSION=0.30.0
# 设置Consul位置的环境变量
# 簇。默认情况下,它尝试解析为 consul-client:8500
# 这是 Consul 作为容器运行时的行为
# 同一主机并链接到此 NGINX 容器(使用别名
# 领事,当然)。不过这个环境变量也可以
# 重写为包含如果我们想解决的话就开始吧
# 另一个地址。
ENV CONSUL_URL 领事客户端:8500
# 下载指定版本的Consul模板
添加 https://releases.hashicorp.com/consul-template/${CONSUL_TEMPLATE_VERSION}/consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip /tmp
运行 apt-get update \
&& apt-get install -y –no-install-recommends mud-init unzip \
&& 解压 /tmp/consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip -d /usr/local/bin \
&& rm -rf /tmp/consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip
复制 consul-template-config.hcl ./consul-template-config.hcl
复制 nginx.ctmpl /usr/templates/nginx.ctmpl
曝光8085
停止信号退出
CMD [“dumb-init”,“consul-template”,“-config=consul-template-config.hcl”]
构建 Docker 镜像:
docker build -t Messenger-lb 。
切换到messenger目录的根目录,创建一个名为messenger-load-balancer-deploy.sh的文件作为NGINX服务的部署文件(就像wi您在整个教程中部署的其余服务)。根据您的环境,您可能需要在 chmod 命令前添加 sudo 前缀:
光盘 ..
触摸基础设施/messenger-load-balancer-deploy.sh
chmod +x 基础设施/messenger-load-balancer-deploy.sh
打开messenger-load-balancer-deploy.sh并添加以下内容:
#!/bin/bash
设置-e
# Consul 主机和端口包含在每个主机中,因为我们
# 在我们了解 Consul 之前无法查询它们
CONSUL_HOST=”${CONSUL_HOST}”
CONSUL_PORT=”${CONSUL_PORT}”
码头工人运行\
– R M \
-d \
–name Messenger-lb \
-e CONSUL_URL=”${CONSUL_HOST}:${CONSUL_PORT}” \
-p 8085:8085 \
–网络mm_2023 \
信使-lb
现在一切就绪,部署 NGINX 服务:
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-load-balancer-deploy.sh
看看您是否可以从外部访问Messenger服务:
卷曲-X GET http://localhost:8085/health
好的
它有效! NGINX 现在在已创建的消息服务的所有实例之间进行负载平衡。您可以看出这一点,因为 X-Forwarded-For 标头显示的信使服务 IP 地址与上一节第 8 步中 Consul UI 中的 IP 地址相同。
挑战 4:使用服务作为作业运行器迁移数据库
大型应用程序通常使用带有小型工作进程的“作业运行程序”,这些工作进程可用于执行修改数据等一次性任务(例如 Sidekiq 和 Celery)。这些工具通常需要额外的支持基础设施,例如 Redis 或 RabbitMQ。在这种情况下,您可以使用消息服务本身作为“作业运行程序”来运行一次性任务。这是有道理的,因为它已经很小了,完全能够与数据库和它所依赖的其他基础设施进行交互,并且完全独立于提供流量的应用程序运行。
有这样做的好处是:
在此挑战中,您将探索如何通过更改某些数据库 c 来修改工件以填补新角色配置值并迁移信使数据库以使用新值并测试其性能。
迁移 Messenger 数据库
对于实际生产部署,您可以创建两个具有不同权限的不同用户:“应用程序用户”和“迁移器用户”。为简单起见,在本示例中,您使用默认用户作为应用程序用户,并创建具有超级用户权限的迁移器用户。在实际情况中,值得花更多时间根据每个用户的角色来决定其需要哪些特定的最低权限。
在应用程序终端中,创建一个具有超级用户权限的新 PostgreSQL 用户:
echo “使用超级用户密码’migrator_password’创建用户messenger_migrator;” | docker exec -i messenger-db-primary psql -U postgres
打开数据库部署脚本 (infrastruct/messenger-db-deploy.sh) 并替换其内容以添加新用户的凭据。
注意:让我们花点时间重申一下 – 对于实际部署,切勿将数据库凭据等机密放入部署脚本或机密管理工具以外的任何位置。有关详细信息,请参阅《微服务》2023 年 3 月的第 2 单元:微服务机密管理 101。
#!/bin/bash
设置-e
端口=5432
POSTGRES_USER=postgres
# 注意:切勿在实际部署中执行此操作。存储密码
# 仅在加密的秘密存储中。
# 因为我们在本教程中重点讨论其他概念,所以我们
# 为了方便起见,在这里设置密码。
POSTGRES_PASSWORD=postgres
# 迁移用户
POSTGRES_MIGRATOR_USER=messenger_migrator
# 注意:如上所述,切勿在实际部署中执行此操作。
POSTGRES_MIGRATOR_PASSWORD=migrator_password
码头工人运行\
– R M \
–name messenger-db-primary \
-d \
-v 数据库数据:/var/lib/postgresql/data/pgdata \
-e POSTGRES_USER=”${POSTGRES_USER}” \
-e POSTGRES_PASSWORD=”${POSTGRES_PASSWORD}” \
-e PGPORT=”${PORT}” \
-e PG数据=/var/lib/postgresql/data/pgdata \
–网络mm_2023 \
postgres:15.1
echo “注册关键messenger-db-port\n”
curl -X PUT –silent –output /dev/null –show-error –fail http://localhost:8500/v1/kv/messenger-db-port \
-H“内容类型:application/json”\
-d“${端口}”
echo“注册密钥messenger-db-host\n”
curl -X PUT –silent –output /dev/null –show-error –fail http://localhost:8500/v1/kv/messenger-db-host \
-H“内容类型:application/json”\
-d ‘messenger-db-primary’ # 这与上面的“–name”标志匹配
# 对于我们的设置来说这意味着主机名
echo“注册密钥messenger-db-application-user\n”
curl -X PUT –silent –output /dev/null –show-error –fail http://localhost:8500/v1/kv/messenger-db-application-user \
-H“内容类型:application/json”\
-d“${POSTGRES_USER}”
curl -X PUT –silent –output /dev/null –show-error –fail http://localhost:8500/v1/kv/messenger-db-password-never-do-this \
-H“内容类型:应用程序n/json” \
-d“${POSTGRES_PASSWORD}”
echo“注册密钥messenger-db-application-user\n”
curl -X PUT –silent –output /dev/null –show-error –fail http://localhost:8500/v1/kv/messenger-db-migrator-user \
-H“内容类型:application/json”\
-d“${POSTGRES_MIGRATOR_USER}”
curl -X PUT –silent –output /dev/null –show-error –fail http://localhost:8500/v1/kv/messenger-db-migrator-password-never-do-this \
-H“内容类型:application/json”\
-d“${POSTGRES_MIGRATOR_PASSWORD}”
printf “\n完成向 Consul 注册 postgres 详细信息\n”
此更改只是将迁移者用户添加到数据库部署后在 Consul 中设置的用户集中。
在基础结构目录中创建一个名为 messenger-db-migrator-deploy.sh 的新文件(同样,您可能需要在 chmod 命令前添加 sudo 前缀):
触摸基础设施/messenger-db-migrator-deploy.sh
chmod +x 基础设施/messenger-db-migrator-deploy.sh
打开messenger-db-migrator-deploy.sh并添加以下内容:
#!/bin/bash
设置-e
# 此配置需要新的提交才能更改
NODE_ENV=生产
端口=4000
JSON_BODY_LIMIT=100kb
CONSUL_SERVICE_NAME=”messenger-migrator”
# Consul 主机和端口包含在每个主机中,因为我们
# 在我们了解 Consul 之前无法查询它们
CONSUL_HOST=”${CONSUL_HOST}”
CONSUL_PORT=”${CONSUL_PORT}”
# 获取迁移器用户名和密码
POSTGRES_USER=$(curl -X GET “http://localhost:8500/v1/kv/messenger-db-migrator-user?raw=true”)
PGPORT=$(curl -X GET “http://localhost:8500/v1/kv/messenger-db-port?raw=true”)
PGHOST=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-host?raw=true)
# 注意:切勿在实际部署中执行此操作。存储密码
# 仅在加密的秘密存储中。
PGPASSWORD=$(curl -X GET “http://localhost:8500/v1/kv/messenger-db-migrator-password-never-do-this?raw=true”)
# 通过从系统拉取 RabbitMQ 配置
AMQPHOST=$(curl -X GET “http://localhost:8500/v1/kv/amqp-host?raw=true”)
AMQPPORT=$(铜rl -X GET “http://localhost:8500/v1/kv/amqp-port?raw=true”)
docker run \–rm \
-d \
–name 信使迁移器 \
-e NODE_ENV=”${NODE_ENV}” \
-e 端口=“${端口}”\
-e JSON_BODY_LIMIT=”${JSON_BODY_LIMIT}” \
-e PGUSER=”${POSTGRES_USER}” \
-e PGPORT=”${PGPORT}” \
-e PGHOST=”${PGHOST}” \
-e PGPASSWORD=”${PGPASSWORD}” \
-e AMQPPORT=“${AMQPPORT}”\
-e AMQPHOST=”${AMQPHOST}” \
-e CONSUL_HOST=”${CONSUL_HOST}” \
-e CONSUL_PORT=”${CONSUL_PORT}” \
-e CONSUL_SERVICE_NAME=”${CONSUL_SERVICE_NAME}” \
–网络mm_2023 \
信使
此脚本的最终形式与您在设置 Consul 的步骤 3 中创建的基础设施/messenger-deploy.sh 脚本非常相似。主要区别在于 CONSUL_SERVICE_NAME 是Messenger-Migrator 而不是Messenger,并且PGUSER 对应于您在上面第1 步中创建的“migrator”超级用户。
CONSUL_SERVICE_NAME 是信使迁移器,这一点很重要。如果设置为 Messenger,NGINX 会自动将此服务轮流接收 API 调用,但它并不意味着处理任何流量。
该脚本以迁移者的角色部署一个短暂的实例。这可以防止迁移中的任何问题影响主信使服务实例的流量服务。
重新部署 PostgreSQL 数据库。由于您在本教程中使用 bash 脚本,因此需要停止并重新启动数据库服务。在生产应用程序中,您通常只需运行基础架构即代码脚本,以仅添加已更改的元素。
docker stop messages-db-primary
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-db-deploy.sh
部署 PostgreSQL 数据库迁移器服务:
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-db-migrator-deploy.sh
验证实例是否按预期运行:
docker ps –format “{{.Names}}”
…
信使r-迁移器
您还可以在 Consul UI 中验证数据库迁移器服务是否已在 Consul 中正确注册为信使迁移器(同样,它不会在信使名称下注册,因为它不处理流量):
现在进行最后一步 – 运行数据库迁移脚本!这些脚本与任何真正的数据库迁移脚本都不相似,但它们确实使用Messenger-Migrator服务来运行特定于数据库的脚本。数据库迁移完成后,停止 Messenger-Migrator 服务:
docker exec -i -e PGDATABASE=postgres -e CREATE_DB_NAME=messenger messenger-migrator 节点脚本/create-db.mjs
docker exec -i messenger-migrator 节点脚本/create-schema.mjs
docker exec -i messenger-migrator 节点脚本/create-seed-data.mjs
docker stop 信使迁移器
实际测试 Messenger 服务
现在您已将 Messenger 数据库迁移为其最终格式,messenger 服务终于准备好供您观看了!为此,您需要针对 NGINX 服务运行一些基本的curl 查询(您在“设置 NGINX”中将 NGINX 配置为系统的入口点)。
以下一些命令使用 jq 库来格式化 JSON 输出。您可以根据需要安装它,也可以根据需要从命令行中省略它。
创建对话:
curl -d ‘{“participant_ids”: [1, 2]}’ -H “Content-Type: application/json” -X POST ‘http://localhost:8085/conversations’
{
“对话”:{“id”:“1”,“inserted_at”:“YYYY-MM-DDT06:41:59.000Z”}
}
向 ID 为 1 的用户发送消息到对话:
curl -d ‘{“content”: “这是第一条消息”}’ -H “用户 ID: 1” -H “内容类型: application/json” -X POST ‘http://localhost:8085/对话/1/消息’ |杰克
{
“信息”: {
“id”:“1”,
“content”: “这是第一条消息”,
“索引”:1,
“用户id”:1,
“用户名”:“詹姆斯·布兰德电话”,
“科nversation_id”: 1,
“inserted_at”: “YYYY-MM-DDT06:42:15.000Z”
}
}
回复来自其他用户(ID 2)的消息:
curl -d ‘{“content”: “这是第二条消息”}’ -H “User-Id: 2” -H “Content-Type: application/json” -X POST ‘http://localhost:8085/对话/1/消息’ |杰克
{
“信息”: {
“id”:“2”,
“content”: “这是第二条消息”,
“指数”:2,
“用户id”:2,
“用户名”: “正常的 Ropetoter”,
“对话id”:1,
“inserted_at”: “YYYY-MM-DDT06:42:25.000Z”
}
}
获取消息:
卷曲 -X GET ‘http://localhost:8085/conversations/1/messages’ |杰克
{
“消息”:[
{
“id”:“1”,
“content”: “这是第一条消息”,
“用户id”:“1”,
“channel_id”:“1”,
“索引”:“1”,
“inserted_at”: “YYYY-MM-DDT06:42:15.000Z”,
“用户名”:“詹姆斯·布兰德电话”
},
{
“id”:“2”,
“content”: “这是第二条消息”,
“用户id”:“2”,
“channel_id”: “1”,
“索引”:“2”,
“inserted_at”: “YYYY-MM-DDT06:42:25.000Z”,
“用户名”:“普通绳索者”
}
]
}
清理
您在本教程中创建了大量容器和映像!使用以下命令删除您不想保留的任何 Docker 容器和映像。
-
要删除任何正在运行的 Docker 容器:
docker rm $(docker stop $(docker ps -a -q –filter祖先=信使 –format=”{{.ID}}”))
docker rm $(docker stop messages-db-primary)
docker rm $(docker stop messages-lb) -
删除平台服务:
# 来自平台存储库
docker 组合下来 -
要删除本教程中使用的所有 Docker 映像:
docker rmi 信使
docker rmi 信使-lb
docker rmi postgres:15.1
docker rmi hashcorp/consul:1.14.4
docker rmirabbitmq:3.11.4-management-alpine
后续步骤
您可能会想“这看起来很糟糕”rk 设置一些简单的东西”——你是对的!迁移到以微服务为中心的架构需要仔细考虑如何构建和配置服务。尽管很复杂,您还是取得了一些扎实的进展:
- 您设置了一个以微服务为中心的配置,该配置易于其他团队理解。
- 您将微服务系统设置为在涉及的各种服务的扩展和使用方面都具有一定的灵活性。
要继续您的微服务教育,请查看 2023 年 3 月的微服务。第 2 单元“微服务秘密管理 101”提供了有关微服务环境中的秘密管理的深入但用户友好的概述。
发表回复