AWS-NodeJS-入门指南-全-

AWS NodeJS 入门指南(全)

原文:Beginning Amazon Web Services With Node.js

协议:CC BY-NC-SA 4.0

一、Amazon Web Services 入门

欢迎使用 Node.js 开始 Amazon Web Services!在本书的整个过程中,您将学习如何优化 Node.js 应用,以便在 Amazon Web Services (AWS)上部署。通过使用 AWS 托管您的应用,您可以利用一系列通常称为“云”的特性。您将了解 AWS 的一些核心特性,了解如何设计您的应用堆栈,并将您的应用集成到 AWS 环境中。虽然您可以轻松地将代码上传到服务器并结束工作,但是学习如何将 AWS 的各种功能整合到您的项目中,将允许您充分利用云的优势。但是这些好处是什么呢?

了解云

首先也是最重要的是可伸缩性,即快速部署额外资源来支持您的应用的能力。在 AWS 等云托管提供商激增之前,如果应用的需求超过了硬件资源,部署额外的服务器是一项昂贵而费力的任务,通常超出了初创公司和中小型企业的能力。有了 AWS 上托管的应用,您可以按需分配资源,启动新的服务器并保持应用在线。根据 AWS 白皮书《云架构:最佳实践》, 1

Traditionally, applications are built for fixed, rigid and preconfigured infrastructures. Companies never need to configure and install servers every day. Therefore, most software architectures cannot solve the rapid deployment or reduction of hardware. Because the supply time and upfront investment for acquiring new resources are too high, software architects have never invested time and resources to optimize hardware utilization. This is acceptable if the hardware running the application is not fully utilized. The concept of "flexibility" in architecture is ignored, because the idea of having new resources in a few minutes is impossible.

响应应用需求的能力称为弹性。如果不是战略行动,用 100 台服务器替换一台服务器是没有用的。当需要手动启动和配置额外的服务器以响应需求时,这样做的成本导致许多企业/机构过度分配资源。这些额外的服务器将会一直运行,并且需要相应的维护,而不是为了流量峰值而增加额外的服务器。凭借 AWS 的灵活性,可以检测到流量峰值,并自动部署额外的资源。当需求恢复正常时,应用可以自动缩减到正常状态。使用您需要的东西,并为您使用的东西付费——这个简单的概念彻底改变了 web 应用开发。这节省了时间、金钱和精力,并降低了企业级软件的准入门槛。

正如您所看到的,可伸缩性和弹性是您的应用的重要属性。这些好处也意味着以不同的方式思考你作为软件开发人员和架构师的角色。从在固定硬件环境中开发转向云计算环境意味着我们现在除了软件开发人员之外,还是云架构师。这构成了开发人员必须考虑 web 应用的方式的重大变化。对于我们中的许多人来说,不再需要系统管理员或数据库管理员来维护基础设施。(他们在亚马逊、IBM、谷歌、Rackspace 等公司工作。,现在。)相反,虚拟硬件管理/云架构现在属于我们的领域。我们不仅从最佳编码实践和组织的角度考虑我们的应用,我们还必须考虑如何利用我们作为开发人员可用的大量资源。这意味着我们必须熟悉 AWS 的特性,并了解如何设计、配置和维护虚拟主机环境。

随着您学习如何成为一名云架构师,您将了解到许多 AWS 特有的优秀特性,以及弹性和可伸缩性的一般原理。AWS 的许多功能被组织成一系列重叠的服务。它们中的许多都有多余的特性,允许我们在设计系统时做出一些创造性的决定。所有这些服务都运行在位于亚马逊全球许多数据中心的虚拟化硬件环境中。我们将在接下来的章节中探讨其中的一些服务。

您将熟悉 Amazon Web Services 的基础知识。我已经讨论了使用 AWS 的一些一般原则和优点。稍后,我将更详细地讨论一些核心服务,以及我们与它们交互的不同方式。在我们开始之前,从同一页开始是很重要的。

本书中的方法

本书假设您至少已经是一名 Node.js 开发初学者,希望扩展您的技能,包括设计和开发一个具有可伸缩性和弹性的 Node.js 应用。您应该对 web 应用开发中的主要概念有一个基本的了解。您应该知道什么是 RESTful web 服务,熟悉 Git 或 SVN,并且手边有一个代码编辑器。

设计和开发应用是一个创造性的过程。这意味着必须做出许多高度主观的决定。首先,我们将使用 Amazon RDS(关系数据库服务)在 AWS 上托管 MySQL 数据库。许多 Node.js 开发人员更喜欢 MongoDB 而不是 MySQL。这是非常好的!然而,示例应用和后续说明主要关注 MySQL,它适合我们应用的需要。如果您想使用这本书来部署一个使用 MongoDB 的应用,您必须能够相应地重写数据库连接和查询。在这两种情况下,您都需要有关于数据库语言的基础知识。这只是一路上要做的许多创造性决定中的一个。在下一个项目中,您可能不同意某些观点或需要不同的方法,但您最终会更好地在下一个项目中做出这些决定,并且您将准备好作为开发人员和架构师与 AWS 服务一起工作。

为了在我们的应用中获得云计算的好处,您将了解可以集成到我们的应用中的各种 AWS 服务。这种集成将通过两种方式进行:通过 AWS 控制台中多个服务的配置和定制,以及通过 AWS SDK 在我们的应用代码库中以编程方式进行。在这种情况下,我们将使用 JavaScript AWS SDK,它旨在用于 Node.js 应用。然而,有针对各种服务器端语言的 SDK,书中的许多经验甚至对使用不同语言的开发人员有用。将 AWS 服务集成到具有类似功能的 PHP 应用中不会有太大的不同。

实际上,您可以在 AWS 管理控制台(AWS 控制台)中执行的每个任务都可以通过编程来执行,反之亦然。AWS 控制台通过提供对提示和文档的访问,以及为抽象概念提供可视化参考,增加了流程的清晰度。当你思考 AWS 能做的一切时,使用控制台会有很大帮助。你可能会问,“我怎么知道什么时候使用控制台,什么时候使用 SDK?”撇开学习曲线不谈,这是一个非常主观的话题。您可能会及时决定哪些规则最适合您的工作流程,但我们可以遵循一些基本规则。

Note

还有第三种与 AWS 服务交互的方法:AWS 命令行界面(CLI)。在最后一章之前,我们不会使用 CLI。

首先,例行任务肯定应该在 SDK 中执行。如果您的应用需要将每日日志或报告存储在 S3 存储桶中,您可能希望通过编程来完成。涉及文件传输的常规任务尤其适合 SDK。在我们的应用中,用户上传的图像将存储在一个 S3 桶中,供应用使用。当您了解如何做到这一点时,我将清楚地说明为什么使用 AWS 控制台是一个坏主意。现在,要知道在这个用例中,SDK 是一个非常省时的工具。这同样适用于事件驱动的任务,除了 CloudWatch 可以检测到它们的地方(比如服务器离线)。例如,如果您的应用必须在用户注册您的应用时生成电子邮件,您希望您的代码在它发生时立即触发它。我们将在第六章和第七章中更详细地探讨这些类型的事件。

然而,为了清楚起见,我们将在 AWS 控制台上执行许多一次性任务。与调试只需运行一次的代码相比,使用 AWS 控制台的 GUI 来诊断错误或完全避免错误要容易得多。例如,虽然我们可以通过编程方式创建一个警报,在应用对请求的响应缓慢时通知我们,但为了清晰起见,我们将在 AWS 控制台中这样做。当你学习的时候,你会发现自己用 AWS SDK 制造了可笑的错误。糟糕,我不是有意在无限循环中创建服务器实例。如果您在完成课程后感到雄心勃勃,您可能会编写本书中许多 AWS 控制台任务的脚本。我称之为额外学分。

在向您介绍我们将使用的 AWS 产品之前,重要的是要重申,使用 AWS 实现相同目标的方法不止一种。本书的范围仅限于一组产品,这些产品可以很好地协同工作,以实现我们的应用可伸缩性和弹性的目标,以及我们需要的功能集和合理的预算。然而,许多 AWS 服务有一些冗余。例如,我们将使用 OpsWorks 来管理我们的应用堆栈,但这不是唯一的服务。许多 AWS 开发人员更喜欢使用 Elastic Beanstalk 而不是 OpsWorks,因为它提供了简单性,还有其他原因。久而久之,AWS 服务已经变得越来越有凝聚力,所以我希望学习如何使用一些产品会让你走上了解更多、更轻松的道路。

许多 AWS 产品都使用 EC2 实例。EC2 或弹性计算云是 AWS 的基础。实例实际上是运行您选择的操作系统的虚拟服务器,托管在亚马逊的众多数据中心之一。该实例不固定在单个硬件上;它是在一台机器上运行的软件过程;如果那台机器崩溃或损坏,这个过程将在另一台机器上继续。在最坏的情况下,AWS 数据中心的硬件故障会中断您的应用,但是内置在云中的冗余将防止您的数据丢失。如果您的应用没有响应,您可以在 http://status.aws.amazon.com/ 检查所有 AWS 服务的状态。

当您创建和运行 EC2 实例时,您是在一个或多个 AWS 数据中心租用计算资源,这些数据中心是按地理区域组织的。这些资源的价格基于使用的能力和小时数。EC2 使用分层定价结构,由此 EC2 硬件规格(时钟速度、RAM 等)。)是根据它们的相对大小来命名的。当您创建一个实例时,资源被分配给您的实例,并且您的计费开始。不管你实际使用了多少你租用的资源。您需要为您的价格等级付费。您可以花很多钱保留最大的可用实例,但这将是一种浪费,就像前面描述的传统部署方法一样。相反,我们将使用其他 AWS 服务,根据我们的需要来扩大和缩小我们的 EC2 实例,以获得最大的性价比。由于我们现在也是云架构师和开发人员,我们将尽最大努力做到不吝啬。为您使用的东西付费;花钱买什么用什么。

虽然您可以手动创建和配置 EC2 实例,但是使用管理工具(如 OpsWorks)可以大大简化流程,并且可以大大降低设置复杂系统时的人为错误风险。我们将使用 OpsWorks 来管理我们的应用层、部署和应用的许多其他重要方面。OpsWorks 将是您配置我们项目的主要界面,也是您需要掌握的第一件事。在第二章中,将向您介绍 OpsWorks 的核心特性,并配置您的应用以部署到 EC2 实例。在本章结束时,您的应用将通过 OpsWorks 部署到 EC2 实例中。

在第三章中,您将使用 Amazon RDS 向您的应用添加一个可扩展的 MySQL 数据库。您还将了解如何向您的应用添加额外的服务器实例,以及如何设置负载平衡器来管理流量。在第四章和第五章中,你将学习如何在 CloudFront 中建立 CDN,以及如何处理文件传输和缓存。您还将了解路由 53 的 DNS 配置。在第六章的和第七章的中,您将学习如何使用 SES(简单电子邮件服务)从您的应用发送电子邮件,您将学习如何使用 CloudWatch 来监控您的应用。最后,在第八章中,您将通过将关键 API 端点限制为 HTTPS 连接来为您的用户保护您的应用。

尽管我们构建的应用相对简单,但使用云计算却不简单。除了 Node.js 的知识之外,完成这本书还需要一些工具和服务。

要求

当然,您需要 Node.js 和 MySQL 开发的所有工具:IDE、本地 MySQL 数据库和 Git 或 SVNclient(除非您更喜欢命令行)。此外,您还需要一个域名注册商、SSL 证书提供商,当然还有 AWS 的账户。

AWS 帐户

你首先需要的是你自己的 AWS 账户。您可以通过进入 http://aws.amazon.com/ 并点击注册按钮来创建一个。您需要提供帐单信息来完成注册过程。您现在应该完成此过程;没有激活服务的账户是免费的。随着您继续学习课程并激活更多服务,您将开始积累一些支出。注册后,您可以在这里随时查看您的支出: https://console.aws.amazon.com/billing/home (见图 1-1 )。

A978-1-4842-0653-9_1_Fig1_HTML.jpg

图 1-1。

The Billing and Cost Management dashboard

在上图中,您可以看到主计费和成本管理仪表板(注意,在此图中整个屏幕都是可见的,但情况并非总是如此)。您当前的月账单以粗体大字体突出显示,下方是一个进度条,范围从零到上个月的账单。这是给你一个每月费用的可靠预测,尽管它们可能会根据使用情况的变化而波动。不过,不要让我的账单吓到你,我的账户上运行的不仅仅是示例应用。

还有一个用于启用警报和通知的标注。您可以将 AWS 配置为在达到特定成本指标时向您发出警报。这样做的效用是不言自明的。要是电力公司提供这个功能就好了!

右边是一个圆形图和你的账单明细。如您所见,EC2 可能是您最大的开销,其次是 RDS 或您使用的任何数据库服务。您可以预期您的数据库服务账单会随着您的应用所做的查询量的增加而增加,因此也会随着您的用户群的扩大而增加。

你只为你使用的东西付费,但是如果你过度使用权力,你会在你的银行账户中感觉到。要完成这本书里的课程,你无疑会产生一笔不小的费用。确切的费用将取决于你完成这本书的速度,以及你是否让你的应用 24/7 运行。这里还有许多其他与报告和计费相关的工具和选项,不在这些课程的范围之内。我们将在本章的后面返回 AWS 控制台,开始配置帐户以用作生产环境。

Tip

为了省钱,你可以在不上课的时候关闭很多资源。

域注册

你需要自己的领域来完成所有的课程。这并不重要,只要你有能力改变域名服务器。如果你还没有域名,你可以在任何一个注册机构注册一个,比如 GoDaddy ( www.godaddy.com )。那不是对 GoDaddy 的认可;注册商大多都一样。预计这一成本约为每年 13 美元。你现在也可以直接在 AWS Route 53 仪表板上注册你的域名,如果你想把你所有的移动部件放在一个地方,这是很方便的。

SSL 证书

这个话题在第八章会有详细介绍,所以你现在不需要这个。但是,您必须向证书颁发机构提供有效的 SSL 证书。这至少每年要花费 9 美元。

代码库

接下来,当您在 AWS OpsWorks 中设置应用时,您会发现您需要选择一种部署应用的方式。你会有很多选择。最简单也可以说是最安全的部署方式是通过 GitHub ( https://github.com )账户,或者其他具有类似功能的 Git 账户托管服务。我们特别想要一个支持自动部署或者可以通过 SSH 连接被 AWS 访问的。下载示例项目后,您会希望将该项目添加到您自己的存储库中,您可以根据需要对其进行修改,并配置您自己的部署过程。

下载示例项目

您不会通过复制冗长的、一步一步的代码示例来开始编码课程。相反,您可以从这里下载示例项目开始:( www.apress.com/9781484206546 )。然后,我们将回顾预打包的代码,并在接下来的课程中反复修改。在执行此操作之前,请确保安装了最新版本的 Node.js(或至少 0.10.26 版)。下载示例项目的 zip 文件,或者将分支拖到您的机器上,并在代码编辑器中打开目录。

局部环境

您应该已经在本地计算机上安装了 Node.js,并且能够在命令行界面中运行它。开始时,您还需要一个本地 MySQL 数据库。最简单的方法就是用 MAMP ( www.mamp.info/en/ )或者 XAMPP ( www.apachefriends.org/index.html )。我只是假设您可以自己安装它,而不需要一步一步的指导(提示:转到网站并单击下载)。与 PHP 不同,你不需要 MAMP/XAMPP 来运行你的应用,但这是建立和访问本地 MySQL 数据库最简单的方法。你可能还想安装 MySQL Workbench ( www.mysql.com/products/workbench/ ),但是我会在后面详细讨论这个问题。

我们的应用的前端是一个 RESTful JSON API,供 web 或移动客户端使用。我们将在整个课程中与 API 进行交互,但是我们还没有客户端。因为我们将向 API 发出GETPOST HTTP 请求,所以在开发过程中,您需要的不仅仅是一个 web 浏览器来正确测试应用。幸运的是,在所有操作系统上都有许多 REST 客户端可用,这使得与像我们这样的 API 接口变得更加容易。有一个很好的 Google Chrome 扩展叫做 Advanced Rest Client,它应该可以很好地完成这项工作。你可以在 https://chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo 找到。

ExpressJS

您可能希望熟悉 ExpressJS,这是 Node.js 的一个流行的(可能是最流行的)web 应用框架。使用 ExpressJS 将完成大量路由和解析 HTTP 请求的繁重工作,并简化通过模板引擎、raw text/HTML 或 JSON 发送响应的过程。此外,ExpressJS 接受一些方便的全局配置,并允许您创建或安装中间件。这允许您通过公共函数传递对服务器的所有 HTTP 请求,这使得集成身份验证、字段验证和错误处理等功能变得容易。

该示例项目针对最新的主要版本 Express 4 进行了优化。如果你更熟悉 Express 2 或者 Express 3,那么这个版本有显著的变化。你应该复习 http://expressjs.com/guide/migrating-4.html 来提高速度。

现在让我们让您熟悉一下这个示例项目。已经为您创建了一个简单的 RESTful web 服务的一些基本功能。请记住,这本书的目标不是教你如何编码,也不是要让功能具有开创性。我希望,通过学习一个简单的应用,你会对如何将 AWS 集成到你自己的工作中有一个清晰的认识。

示例项目

虽然这在技术上不是 Node.js 指南,但我们将使用一个示例项目来将本书中的课程放在上下文中。样本项目将提供一个真实世界的应用来开发,而我们与 AWS 合作。一开始,我们将从一个基本的应用开始——它需要做大量的工作才能在现场使用。我们将在整本书中做一些这样的工作。

概观

示例项目是一个非常简单的基于照片的社交媒体应用的代码基础。功能很简单:用户可以注册一个帐户并登录。他们可以创建相册对象,相册对象由标题和照片对象集合组成。用户可以上传带标题的照片到相册,一次一张(但我们还没有存储和提供文件)。用户可以看到所有其他用户的列表,并获得特定用户的所有相册。用户通过 JSON 格式的 RESTful API 与应用进行交互,记录存储在 MySQL 数据库中。

随着课程的进行,我们将为应用添加更多功能。它将使用适当的安全用户认证方案。用户注册帐户时会收到欢迎电子邮件。我们将图像文件存储在 CDN 中,以便轻松快速地存储。应用日志将被生成并存储在 AWS 中,它将利用一系列的 AWS 服务。

像这样的应用有很多使用案例。从社交网络到新闻阅读器应用,无数平台的核心都拥有类似的功能。只需做一点工作,您就可以通过在用户之间创建一种关系来添加好友或关注的概念,以及用户创建和删除他们的方法。您可以给照片添加标签、地理位置数据和注释。

我们的应用输出是 JSON 格式的,但是 web 模板也可以很容易地由应用生成。请记住,虽然应用在这一点上很简单,但它会很快变得更复杂。示例项目的目的不是让您对 Node.js 编码技能感到眼花缭乱,而是提供一个简单、清晰的代码库,该代码库易于理解,并且可以扩展到更复杂的应用中。

Note

项目的大部分组织是使用express命令创建的,它会自动生成您的项目。如果您在课程结束后开始自己的项目,那么重新创建项目的组织将会很容易。

源代码组织

让我们从在代码编辑器中打开项目开始,看看内容。您应该在项目目录中看到以下内容:

/lib

/public

/routes

/setup

/views

server.js

package.json

在代码编辑器中打开package.json。您将看到一些 JSON 格式的项目信息和配置选项。您将看到 dependencies 属性,类似于以下内容:

"dependencies": {

"express": "∼4.8.6",

"body-parser": "∼1.6.6",

"cookie-parser": "∼1.3.2",

"morgan": "∼1.2.3",

"serve-favicon": "∼2.0.1",

"debug": "∼1.0.4",

"jade": "∼1.5.0",

... // additional dependencies truncated

}

这些是运行应用所需的其他 npm 模块。当您使用 ExpressJS app generator 命令时,它们中的许多会自动添加到依赖项列表中。您必须在本地安装这些程序,才能在本地运行应用。打开您的命令行界面(例如,OS X 的终端),并导航到此目录。在命令行中键入以下内容:

npm install

该命令将在同一目录中查找一个package.json文件,并尝试安装依赖属性中的所有模块。在几分钟的时间内,您应该会看到所有的软件包都下载并安装完毕。

让我们浏览一下项目中的其他一些文件和目录。总的来说,这个项目的结构遵循模型视图控制器(MVC)设计模式。如果您不知道这是什么,它只是将您的代码库组织成三种类型的类之间的逻辑分离的一种方式。在 MVC 中,这三种类型是模型,或对象定义和操作;应用的视图或输出;以及控制器,它包含所有的逻辑,根据应用的输入决定向模型发送什么信息/从模型中检索什么信息。当然,这是对概念的简化,但是当我们浏览它时,它会让您了解它是如何工作的。

首先,我们将探索lib目录,其中包含globals.js/model。正如前面所描述的,model目录包含了我们将要使用的各种对象的所有类定义。因为应用输出将主要是 JSON,或者由 JSON 填充的模板,所以模型本身将比使用 PHP 抽象得多,例如,如果使用 PHP,您可能会设计一个类,使用从数据库检索的属性来实例化对象。

model目录中的文件主要从控制器接收与数据库交互的命令,并将抽象对象发送回控制器作为响应。因此,示例应用中对象的属性是动态创建的。对于这种性质的应用,对象的生命周期非常短,因此使用灵活的数据模型通常是有意义的。如果您愿意,您可以创建自己的类,实例化它们,并填充它们,而不是遵循示例项目中的方法。如果这样做有助于更有效地维护您的代码库,那么这样做并没有错。

MVC 模式的控制器在/routes目录中。每个文件对应于用户请求中的一个目录路径(例如,/ photos /1/ users /adam),并处理对该路径内的 URL 的所有请求。它将从相应的模型中检索所需的任何数据,并发送响应。

那么风景在哪里呢?在响应是 JSON 的情况下,视图就是 JSON 数据本身。不过作为 ExpressJS4 app,原生支持 jade 模板引擎( http://jade-lang.com/ )。如果您要开发 web 模板,这些文件将位于/views目录中。在这种情况下,当考虑 MVC 时,视图并不总是在模板文件中可访问的。无论内容类型是text/html还是application/json,视图总是 HTTP 响应。

接下来,public目录存储所有的静态资产,比如前端模板使用的样式表、图像和 JavaScript 文件。这个目录是由 ExpressJS 自动生成的。在第四章中,你将学习如何使用 AWS CloudFront 更有效地为用户提供这些文件。

你已经了解了server.js。如果查看这个文件,您会看到请求路由是如何映射到/routes目录中的文件的。您还将在这里看到一系列中间件和 ExpressJS 配置。目前,这些都不太重要。

你也应该知道/lib/globals.js。这个文件只是存储全局配置和常用值的一个方便的地方,便于参考,不会污染全局名称空间。开发人员对于如何在他们的代码中处理这种类型的特性有各种不同的想法,所以这仅仅是许多方法中的一种。

示例项目还包括一个名为/setup/photoalbums.sql的文件。这是您可以导入到本地 MySQL 数据库的数据库模式。如果您打算在本地环境中测试示例应用,那么您需要将它导入到一个名为 photoalbums 的本地数据库中。稍后,您还需要将这个文件导入到 Amazon RDS 数据库中。

配置和启动

您可以通过以下两种方式之一启动应用:

  • 在命令行上键入node app.js
  • 在命令行上键入npm start

如果应用编译成功,您应该可以在http://localhost:8081查看欢迎页面(假设您使用了 8081 端口)。如果没有,您应该会在命令行界面上看到一个错误。如果是这样,很有可能是您的某个依赖项没有正确安装。如果是这种情况,尝试再次运行npm install

另一个可能的错误是端口 8081 在您的机器上不可用。当我们将应用部署到云中时,我们将使用端口 80。如果您必须改变这一点,打开/lib/globals并将applicationPort属性更改为不同的值,比如 8081。如果您的应用试图打开一个不可用的端口,错误将如下所示:

events.js:72

throw er; // Unhandled 'error' event

^

Error: listen EACCES

at errnoException (net.js:904:11)

at Server._listen2 (net.js:1023:19)

at listen (net.js:1064:10)

at Server.listen (net.js:1138:5)

如果这些建议都不能解决您的问题,您将不得不阅读命令行界面中的错误,并尝试自己解决它。只是提醒一下:任何时候你修改代码,你都必须重新编译你的应用。如果你习惯于使用 PHP 或前端 JavaScript 并刷新你的浏览器来测试一个补丁,这可能需要一些时间来养成定期重新编译你的应用的习惯。

使用示例应用

祝贺您让示例应用正常运行。除了安装几个 npm 包之外,您不需要在您的机器上做任何进一步的配置。从现在开始,示例项目源代码就是您的了!您现在应该将它登记到一个存储库中,这样如果您迷路了,您就有一个好的快照可以返回。

在余下的课程中,我们将在浏览器中的源代码、命令行界面和 AWS 控制台之间来回切换。让我们更深入地研究样本代码,以便更好地理解它是如何工作的。首先,我们来看看server.js。在文件的开头,您会看到所有包含的 npm 模块。从第 8 行开始,您将看到我们添加到项目中的所有源文件:

var routes = require('./routes/index');

var users = require('./routes/users');

var photos = require('./routes/photos');

var albums = require('./routes/albums');

var globals = require('./lib/globals');

再往下,在第 27 行左右,您会看到它们的使用位置:

app.use('/', routes);

app.use('/users', users);

app.use('/photos', photos);

app.use('/album', albums);

现在,我们将探索我们在应用中注册的每条不同路线的功能。

回家路线

第一行将 HTTP 请求定向到我们的应用到位于/routes/index.js的文件的根路径。我们可以在这里稍作停留,因为这条路线仅用于显示欢迎页面,所以你知道应用运行正常。在文件的顶部,您会看到

var express = require('express');

var router = express.Router();

我们构建应用的方式是,我们需要在每个控制器(路由)中包含expressexpress.Router(),以便与发送到所述控制器的 HTTP 请求进行交互。这意味着这些路由文件中的每一个都将在顶部实例化名为expressrouter的变量。

Note

完全有可能以一种方式组织你的代码,使你的控制器不必要求express,但是我们在这一点上遵循 ExpressJS 模板。

index.js中只登记了一条路线:

/* GET home page. */

router.get('/', function(req, res) {

res.render('index', { title: 'Photoalbums' });

});

如您所见,我们没有对请求做任何事情,只是发送了一个响应。不读取参数,不与模型交互,等等。我们简单地用来自views文件夹的索引模板和标题“Photoalbums”来响应。我们应用的其余部分使用 JSON 进行响应,但是对于这个登录页面,我们使用一个由 jade 模板生成的 HTML 响应。我不会详细讨论这个问题。需要知道的重要一点是,您可以使用 ExpressJS 响应对象发送各种不同的响应类型。更多信息请点击: http://expressjs.com/4x/api.html#response

/users/的请求被路由到/routes/users.js,依此类推。为了在我们的应用中实现,所有的路径或控制器文件都必须在server.js中注册。所有的控制器都是以同样的方式设计的,所以我们将从用户开始,作为一个很好的例子来说明一切是如何协同工作的。在server.js中,我们看这条线

app.use('/users', users);

并且知道按照要求去/routes/users.js看看会发生什么。

用户路线

目前,我们只有框架功能。这款应用的功能刚刚够基本的互动。随着课程的进行,用户会变得比现在更加健壮。为用户提供的现成功能包括

  • 注册账户(POST /users/register)
    • 参数:usernameemailpassword
  • 登录账户(POST /users/login)
    • 参数:usernamepassword
  • 账户注销(POST /users/logout)
  • 查看所有用户(GET /users/)
  • 按用户查看所有相册(GET /users/user/:user)
    • 参数:user

同样,这个示例代码只是为了帮助您入门。只需一点点努力,您就可以为用户帐户添加尽可能多的功能。您可以向用户数据库表中添加其他字段,如个人资料图像、个人简历或用户网站。然后,您可以添加一个更新方法,允许用户首先注册一个帐户,然后提交更新来完成他/她的配置文件。你可以要求用户在注册后确认他们的名字。您可以允许他们重置用户名或密码。记住这一点,让我们看看代码。在文件的顶部,您会看到一组不同的包含文件,如下所示:

var express = require('express');

var router = express.Router();

var model = require('./../lib/model/model-users');

因为我们在用户控制器中,所以我们知道我们会想要频繁地访问用户模型。我们将继续在这里包括用户模型文件。事实上,你可以期望每个控制器在顶部包含它的模型。然而,一些路线将需要包括多个模型类,尤其是当功能变得更加强大时。为了一致起见,我们可以将默认的模型变量命名为model,并将任何附加的模型命名为modelPhotomodelAlbum等。如果我们只需要访问另一个模型一次呢?我们将根据具体情况确定任何其他型号的可变范围。

让我们来看看用户控制器中的一个典型的router方法。

/* GET users listing. */

router.get('/', function(req, res) {

model.getAllUsers(function(err, obj){

if(err){

res.status(500).send({error: 'An unknown server error has occurred!'});

} else {

res.send(obj);

}

});

});

前面的代码是我们在 ExpressJS 控制器类中路由和处理 HTTP 请求的典型方式。从第一行可以看到,GET/users/路径的请求将在这里处理。如果有任何参数需要验证,我们会在方法开始时进行验证。然而,这只是检索所有用户列表的一种方式;没有用户输入。随着应用用户群的增长,我们可能希望允许用户通过该请求传递参数,以支持所有用户的分页列表。现在,我们可以保持简单,忽略任何用户输入。

我们立即用model.getAllUsers()从模型中检索数据。在这种情况下,没有参数被传递给模型。当我们收到从模型对象返回的消息时,我们检查它是一个错误对象还是我们想要使用的数据。

在某些情况下,我们会希望将模型对象的错误返回给用户。这看起来会像下面这样:

model.getAllUsers(function(err, obj){

if(err){

res.send(err);

}

});

然而,我们将在大多数情况下避免这样做。模型返回的一些错误可能是 MySQL 数据库查询错误。向用户公开关于数据库表的信息不是一个好的安全实践,MySQL 错误信息也不可能对我们的任何最终用户非常有用。对于控制器来说,检查从模型收到的错误并向向我们的 API 发出请求的客户机发送合适的消息会更合适。接下来,我们来看看模型,看看getAllUsers是做什么的。

与 routes 文件一样,让我们从顶部开始。

var mysql = require('mysql');

var globals = require('./../globals');

var connection = mysql.createConnection(globals.database);

当然,所有模型类中都需要mysql模块。我们将使用这个模块连接到数据库并执行所有查询。

Note

https://github.com/felixge/node-mysql 可获得mysql模块的完整文档。

如前所述,globals是一个存储公共变量而不污染global名称空间的对象,这里使用它是为了方便。MySQL 数据库连接被初始化为connection,使用存储在globals中的数据库配置。让我们看看第一种方法,我们已经知道了,getAllUsers()

function getAllUsers(callback){

connection.query('SELECT username, userID FROM users', function(err, rows, fields){

if(err){

callback(err);

} else {

callback(null, rows);

}

});

}

这个方法是模型获取函数所能得到的最简单的方法。没有要验证的参数,没有要转义的用户输入,也没有任何其他中间函数。我们简单地从数据库中检索所有用户,并将他们返回给routes/user中的回调函数。现在,让我们滚动到文件的最底部,注意exports赋值。

exports.getAllUsers = getAllUsers;

exports.getUser = getUser;

exports.createUser = createUser;

exports.loginUser = loginUser;

exports.logoutUser = logoutUser;

这几行非常重要,因为它们是你在这个文件中公开一个方法的手段;否则,所有方法都是私有的,或者对其他对象不可访问。如果您使用这个示例应用构建额外的功能,很容易忘记这一步。

回顾——事情的顺序

回到/routes/user一会儿,您应该看到请求是如何被处理的。以下是客户如何从我们的应用中检索用户列表的快速回顾:

server.js is listening for all HTTP requests at the designated port.   The client makes an HTTP GET request to /users/.   Server.js forwards the request to the controller at /routes/users.   The controller at /routes/users notes that the request is for “/” relative to “/users” and passes the request to the corresponding listener method.   The GET/” listener in the controller calls model.getAllUsers().   The model method getAllUsers() queries the database, processes the results, and returns the data set to the controller.   The controller populates the response object with data.   server.js sends an HTTP response with the data requested by the user.

这些都有意义吗?如果您已经熟悉 ExpressJS,它可能没有告诉您任何新的东西。如果你是第一次学习这个,不要担心;在接下来的课程中,我们将花大量时间来学习这些概念。最好的学习方法是试着写下你自己的路线,看看怎样才能让它们发挥作用。永远不要低估试错的力量!

示例-使用参数

让我们回到/routes/users来看另一个概念。到目前为止,我们已经看到了在没有任何参数或客户端输入的情况下处理一个基本请求。当你不涉及任何变量时,失败的可能性很小。一旦我们开始接受来自客户的特定请求,故障点就开始迅速增加。我们可以以/users/login路线为例来看。

/* POST user login. */

router.post('/login', function(req, res) {

if(req.param('username') && req.param('password') ){

var params = {

username: req.param('username').toLowerCase(),

password: req.param('password')

};

model.loginUser(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid login'});

} else {

res.send(obj);

}

});

} else {

res.status(400).send({error: 'Invalid login'});

}

});

这个路由接受 HTTP POST请求,并期望客户机已经发送了用户名和密码。然而,我们不能假定两个参数都包括在内。因此,在我们尝试使用这些参数之前,我们必须检查它们是否存在。如果参数已经包括在内,我们继续。否则,将发送无效登录错误来响应请求。

我们可以使用控制器来验证输入和格式化参数以传递给模型。在这种情况下,在我们将用户的登录凭证发送到模型之前,我们还希望实施一条规则:用户名应该不区分大小写。用请求参数实例化params对象,当params.username被设置时,用户名被转换成小写。然后我们可以看看这个模型,看看接下来会发生什么。

function loginUser(params, callback){

connection.query('SELECT username, password, userID FROM users WHERE username=' + connection.escape(params.username), function(err, rows, fields) {

if(err){

callback(err);

} else if(rows.length > 0){

var response = {

username: rows[0].username,

userID: rows[0].userID

}

callback(null, response);

} else {

var error = new Error("Invalid login");

callback(error);

}

});

}

现在,如果用户名有效,我们将从数据库中选择用户。在未来,我们将解密用户的密码来验证用户。目前,我们会自动返回一个成功响应,其中包括经过身份验证的用户的用户名和用户 ID。这里重要的一课是,在将客户机输入发送到模型之前,控制器用于净化和验证客户机输入。以这种方式分离关注点并封装功能是一个好主意。模型期望将一个具有必要属性的对象发送给它的方法,控制器负责构造该对象或拒绝格式错误的请求。如果您遵循这种模式,您将能够在不同的场景中安全地重用模型类中的公共方法,并且从长远来看,您的应用将更容易维护和调试。

尝试一下

是时候启动应用,看看它的运行情况了。如果应用没有运行,打开命令行界面,导航到应用目录,然后键入node server.js。打开 REST 客户端,输入以下 URL:

http://localhost:8081/users/register

如果你在不同的端口上运行应用,记得更换 8081。将您的 HTTP 方法设置为POST。添加名为usernamepasswordemailPOST参数。您可以在这些参数中输入任何想要的值。发送请求!

您应该会收到以下成功消息:

{

"message": "Registration successful!"

}

接下来,您应该能够使用刚刚注册的用户名登录。删除email参数,并将 URL 改为

http://localhost:8081/users/login

您应该会收到以下用户名和用户 ID 作为响应:

{

"username": "adam",

"userID": 1

}

您现在可以在浏览器中向http://localhost:8081/users/发出一个GET请求。您应该会在响应中看到您创建的用户。您可以创建其他用户,他们也会出现在此响应中。在这里,您可以使用登录响应中的用户 ID 为您的用户创建相册和照片。我将很快更详细地讨论这些。现在,让我们在/users/user/:user向用户细节 API 端点发出一个快速的GET请求,用您刚刚注册的用户名替换:user。您应该会看到类似以下的响应:

{

"username":"adam",

"userID":1,

"albums":[]

}

目前,这里没有太多新的信息。但是一旦你开始为这个用户创建相册,你将在这里得到关于他们的信息。接下来我们来看看专辑。

相册

每个用户可以拥有无限数量的相册。相册对象非常简单,由标题和用户 ID 组成。任何数量的照片也可以与相册相关联。

相册具有以下功能:

  • 创建新相册(POST /albums/upload)
    • 参数:userIDtitle
  • 按 ID 获取相册,包括相册中包含的所有照片(GET /albums/id/:albumID)
    • 参数:albumID
  • 删除相册(POST /albums/delete)
    • 参数:albumID

让我们打开/routes/albums.js仔细看看。在顶部声明的变量应该已经很熟悉了。在这种情况下,您可以看到model变量被设置为/lib/model/model-albums.js。第一条路线将允许我们创建一个相册。使用注册时收到的用户 ID,将 RESTful 客户机指向http://localhost:8081/albums/upload,将方法设置为POST,并为用户 ID 和标题添加字段。你可以把任何你想要的名字放在标题里。我们现在就用“你好世界”吧。您应该会收到以下响应:

{

"id":7,

"title":"Hello World"

}

In /routes/albums.js, find the route:

/* POST create album. */

router.post('/upload', function(req, res) {

if(req.param('title') && req.param('userID')){

var params = {

userID : req.param('userID'),

title : req.param('title')

}

model.createAlbum(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid album data'});

} else {

res.send(obj);

}

});

} else {

res.status(400).send({error: 'Invalid album data'});

}

});

首先,验证所需的参数,并用我们传递给它的参数构造一个名为params的对象。虽然您可以将请求参数直接传递给模型,但是使用中间变量是一个好习惯,主要是为了可读性。让我们转到/lib/model/model-albums.js的模型文件,看看在createAlbum()发生了什么。

function createAlbum(params, callback){

var query = 'INSERT INTO albums SET ? ';

connection.query(query, params, function(err, rows, fields){

if(err){

callback(err);

} else {

var response = {

id : rows.insertId,

title : params.title

};

callback(null, response);

}

});

}

我们在这里传递的所有参数都被插入到 albums 表的新行中。然后,我们用自动递增的 ID 和标题创建一个响应对象。请注意,ID 可以作为rows.insertId访问。当在 MySQL 数据库中创建一行时,rows参数是一个单独的对象。您会发现SELECT查询的行为略有不同。

接下来的路线允许你通过 ID 获取相册。

/* GET album by ID */

router.get('/id/:albumID', function(req, res) {

if(req.param('albumID')){

var params = {

albumID : req.param('albumID')

}

model.getAlbumByID(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid album ID'});

} else {

res.send(obj);

}

});

} else {

res.status(400).send({error: 'Invalid album ID'});

}

});

这很简单。如果请求中包含相册 ID,控制器将从模型中检索相册并发送相册数据作为响应。您可以使用刚刚创建的相册中的相册 ID。现在,前往/lib/model/model-albums.js并找到getAlbumByID()方法。

function getAlbumByID(params, callback){

var query = 'SELECT * FROM albums WHERE albumID=' + connection.escape(params.albumID);

connection.query(query, function(err, rows, fields){

if(rows.length > 0){

getPhotosForAlbum(rows[0], function(err, obj){

if(err){

callback(err);

} else {

callback(null, obj);

}

});

} else {

callback(null, []);

}

});

}

首先,从数据库中检索相册及其所有字段。在所有数据库查询中,rows是从数据库查询中填充的对象。在使用SELECT查询的情况下,rows总是一个数组。如果你找到了你要找的东西,rows的长度将为 1 或更长。在本例中,我们通过唯一标识符albumID选择一行。但是这一次,我们不在这里停下来,返回我们找到的数据。相反,我们调用getPhotosForAlbum()并将我们的结果传递给它,然后最终将数据发送回控制器。向下滚动到getPhotosForAlbum()查看那里发生了什么。

function getPhotosForAlbum(album, callback){

var modelPhotos = require('./model-photos');

modelPhotos.getPhotosByAlbumID(album, function(err, obj){

if(err){

callback(err);

} else {

album.photos = obj;

callback(null, album);

}

});

}

如果我们通过 ID 发送一个相册,终端用户期望我们提供与该相册相关的所有数据似乎是合理的。在这种情况下,我们需要获取与相册相关的所有照片。

Note

我们知道客户端有用户的信息,因为这是他/她检索专辑 ID 的方式。在功能更全面的应用中,您可能还想在这个响应中包含一些用户信息。

首先,我们在/lib/model/model-photos.js实例化一个对照片模型的引用。我们给相册一个photos属性,设置为我们从照片模型中获取的照片数组(即使它是空的)。导航到/lib/model/model-photos.js,找到getPhotosByAlbumID()完成路线。

function getPhotosByAlbumID(params, callback){

var query = 'SELECT * FROM photos WHERE published=1 AND albumID=' + connection.escape(params.albumID);

connection.query(query, function(err, rows, fields){

if(err){

callback(err);

} else {

if(rows.length > 0){

callback(null, rows);

} else {

callback(null, []);

}

}

});

}

这个方法只是检索所有带有我们传递给它的相册 ID 的照片。请注意,照片的值published必须设置为 1。对于相册和照片,我们将使用published=1来表示该对象可供公众消费,使用published=0来表示该对象被隐藏。这使我们能够提供删除功能,而不会实际破坏数据库中的数据。接下来我们可以看看删除,从/routes/album.js开始。

/* POST delete album. */

router.post('/delete', function(req, res) {

if(req.param('albumID')){

var params = {

albumID : req.param('albumID')

}

model.deleteAlbum(params, function(err, obj){

if(err){

res.status(400).send({error: 'Album not found'});

} else {

res.send(obj);

}

});

} else {

res.status(400).send({error: 'Invalid album ID'});

}

});

到目前为止,您已经多次看到了这种模式。所以,我们将立即前进到model.deleteAlbum()

function deleteAlbum(params, callback){

var query = 'UPDATE albums SET published=0 WHERE albumID=' + connection.escape(params.albumID);

connection.query(query, function(err, rows, fields){

if(err){

callback(err);

} else {

callback(null, {message: 'Album deleted successfully'});

}

});

}

如你所见,我们实际上并没有删除专辑。我们正在取消发布,这意味着它对所有用户都是不可见的。这是防止用户永久意外删除的好方法,它降低了恶意使用我们的应用的风险。如果有人的密码被盗或破解,他的所有内容被删除,我们可以恢复它没有太多的麻烦。还要注意,我们的回调没有返回任何数据,只是一条确认删除成功的消息。我们的应用非常简单,目前对于用户删除某些内容后会看到什么没有预期。目前这还可以,但是在你自己的应用中,你可能要考虑你的用户会期待什么样的响应。最后,我们将回顾照片。

照片

照片是个人照片/图像上传的对象。目前,应用中使用的照片对象不包括实际文件。此时,照片对象只不过是一个 ID 和一个标题。文件上传和 URL 生成是我们将专门为 AWS 定制的功能。我们将在后面的课程中构建此功能。

照片具有以下功能:

  • 创建新照片(POST /photos/upload)
    • 参数:albumIDcaptionuserID
  • 通过 ID ( GET /photos/id/:id)获取照片
    • 参数:id
  • 删除一张照片(POST /photos/delete)
    • 参数:id

你会注意到这些方法实际上与专辑中的方法相同。让我们回顾一下关于照片的几点,从上传一张“照片”开始。您应该从之前的 API 查询中获得一个相册 ID 和用户 ID。(我们假设两者都等于 1。)让我们继续创建一个新的照片对象,使用这些 id 作为参数。在您的 REST 客户端中,使用以下参数向http://localhost:8081/photos/upload发出一个POST请求:

userID: 1

albumID: 1

caption: "My First Photo"

响应只包含您刚刚创建的照片的 ID。

{

"id": 5

}

看看你刚才查询的方法,在/routes/photos里。

/* POST create photo. */

router.post('/upload', function(req, res) {

if(req.param('albumID') && req.param('userID')){

var params = {

userID  : req.param('userID'),

albumID : req.param('albumID')

}

if(req.param('caption')){

params.caption = req.param('caption');

}

model.createPhoto(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid photo data'});

} else {

res.send(obj);

}

});

} else {

res.status(400).send({error: 'Invalid photo data'});

}

});

值得注意的一个区别是caption参数是可选的。如果标题存在,我们会将它包含在传递给model.createPhoto()的参数对象中。这显示了构造一个中介对象的价值,而不仅仅是将请求参数直接传递给模型。如果请求中省略了一个可选字段,我们只需让数据库应用默认值。继续向/photos/upload发出另一个请求,并删除caption参数。您应该会从 API 收到相同的响应。

现在,让我们花点时间检查一下我们的相册,以确保我们的照片在那里。向http://localhost:8081/albums/id/1发出GET请求。请注意,没有标题的照片有一个空的标题字符串。响应应该如下所示:

{

"albumID":1,

"userID":1,

"title":"Hello World",

"photos":[

{

"photoID":4,

"userID":1,

"albumID":1,

"caption":"My First Photo"

},{

"photoID":5,

"userID":1,

"albumID":1,

"caption":""

}

]

}

再花点时间浏览照片路线和模型的其余代码。不应该有什么惊喜。

使用示例应用开发

到目前为止,您已经看到示例应用只有基本的功能。如果您是一名经验丰富的 MySQL 开发人员,您现在应该已经注意到没有关联表,这限制了我们在对象之间建立多对多关系的能力。为了提供简单的示例,此功能已被省略。其他更重要的功能—上传文件、身份验证等。—不完整。随着您对 AWS 的了解越来越多,您将会充实这些特性。当您学习示例应用的课程时,请记住我们的目标是开发一个利用 AWS 服务的应用。还有很多其他的书籍教授 RESTful web 服务、Node.js 和 MySQL 的优点。记住这一点,让我们开始吧!

下一步将是我们在 AWS 中的第一个任务。按照前面的步骤,您应该已经注册了一个 AWS 帐户。我们将在 AWS 控制台中做的第一件事是学习如何使用 IAM(身份和访问管理)来管理应用基础架构中的权限和安全性。IAM 是 Amazon 针对需要管理多种权限的问题的解决方案。当管理权限时,我们不仅仅是说让其他用户访问我们的 AWS 基础设施,还包括 AWS 服务之间的访问。例如,您可能有一个 EC2 实例,您希望它能够连接到 RDS 数据库,但不希望它具有访问 OpsWorks 的 send 命令的权限。

对安全凭证过于慷慨是不好的做法。想象一下,你正在管理一个新闻网站。您可能希望为不同类型的用户提供不同的权限。读者只能阅读文章和发表评论。编辑将被允许发布和编辑文章,并删除用户评论。管理员将能够禁止用户,创建新的编辑,并执行站点级管理任务。您不希望授予所有用户管理权限,并相信他们会相应地采取行动。即使只是让你的编辑成为管理员,你也可能有一天醒来发现你自己的管理权力被剥夺了。

我们将同样谨慎地管理我们的 AWS 基础设施。AWS 控制台中的用户应该只拥有完成工作所需的权限。类似地,每个服务器实例都有一个角色要扮演,并且应该仅限于该角色。当您很好地掌握了 IAM 之后,您就可以部署您的应用,而不必将密码存储在等待被黑客攻击的配置文件中。

身份和访问管理

在这种类型的应用中管理权限和凭证有很多挑战。

首先,我们只希望一组选定的用户能够管理我们的基础设施。例如,只有某些团队成员能够重启服务器。也许一组不同的用户应该拥有邮件服务器的管理权限,而另一组不同的用户应该拥有数据库管理权限。

此外,还有管理每台服务器安全性的问题。组织和限制对一系列服务器的安全密钥的访问本身就是一门艺术。通常,我们还必须在应用的源代码中包含数据库访问凭证。我们不仅要担心限制对存储凭证的源代码的访问,还要确保开发环境凭证不会意外地部署到生产环境中,反之亦然。

除了所有这些问题之外,我们还有人员流动的问题。工程师和管理员要么辞职,要么被解雇,我们必须审查我们所有的安全措施,以确保他们不能接触任何敏感的东西。如果某个拥有大量安全访问权限的人意外退出,我们将不得不手忙脚乱地锁定我们所有的凭据。安全漏洞,尽管可能是短暂的,却是有保证的。每次我们必须重置凭证时,所有错误配置的风险都会重新引入系统。现在应该很清楚,即使像我们这样简单的应用,在控制内部安全方面也存在许多故障点和巨大的人为错误风险。

AWS 通过将所有用户安全、数据库和服务器安全以及 API 访问统一到一个名为身份和访问管理(IAM)的系统中来解决这些问题。现在,我们假设没有其他 AWS 用户需要我们关注。然而,我们确实希望确保我们在 OpsWorks 中创建的 EC2 实例能够与其他 AWS 服务正确交互。

IAM 仪表板

让我们登录 AWS 控制台并导航到 IAM。当您成功登录 AWS 时,您会看到一个由三列组成的 AWS 产品列表。您可以在第二列的部署和管理标题下找到 IAM。单击它,您应该会看到类似图 1-2 的内容(为了清晰起见,一些用户界面元素被裁剪掉了)。

A978-1-4842-0653-9_1_Fig2_HTML.jpg

图 1-2。

IAM dashboard

在 IAM 仪表板的左侧,您将找到 IAM 的导航(这是 AWS 控制台的标准界面范例)。在右侧,您将看到您已经创建的所有 IAM 资源的列表。页面上还有许多其他的教程和咨询 UI 元素。您可以在闲暇时随意探索这些内容,以熟悉仪表盘。作为 AWS 中的第一个正式任务,您必须在 IAM 中创建一个用户。

IAM 用户

用户是一个管理帐户,它有一个登录名(用户名/密码)以及其他安全凭据(如果需要)。现在,让我们创建一个单一的用户帐户来管理我们的应用。在导航中选择 Users 链接,您应该会看到一个空的表视图。单击页面顶部的“创建新用户”按钮。这将带您进入图 1-3 所示的屏幕。

A978-1-4842-0653-9_1_Fig3_HTML.jpg

图 1-3。

Create up to five IAM Users

在这个屏幕上,您最多可以创建五个新用户名,但我们只需要一个。在第一个输入字段中,键入用户名 photoadmin。有一个自动选中的复选框,表示将为每个用户生成一个访问密钥。如果我们使用这个用户在我们的应用中进行 AWS API 查询,我们将需要一个访问键。然而,我们的意图是,这个用户将有权访问 AWS 控制台的必要部分,而与应用本身的功能无关。因此,我们将在单击“创建”之前取消选中该框。

用户名现在应该出现在 users 表中。您会注意到,我们的用户不属于任何组,也没有访问密钥。单击用户的行会将我们带到该用户的详细视图。您应该会看到类似于图 1-4 的内容。

A978-1-4842-0653-9_1_Fig4_HTML.jpg

图 1-4。

The user detail view

这一页上有很多信息。首先,是用户 ARN,或亚马逊资源名称。ARN 本质上是任何种类的所有 AWS 资源的全局标识符。任何时间任何资源,例如用户、EC2 实例、数据库实例等。,则会自动生成一个 ARN。稍后您可能会发现它们很有用,但是我们现在不需要对这个 ARN 做任何事情。您会注意到“组”和“权限”标题下的粗体文本。我们的用户没有组,也没有策略!

策略可能是 IAM 中最重要的概念。策略本质上是对用户、组、角色或其他资源的一个或多个权限的声明。简而言之,它们是为 AWS 中的所有实体配置权限的通用系统。策略可以包含非常细粒度的权限,或者对 AWS 产品中的所有属性进行全面的“允许”。相反,您也可以明确拒绝对服务或资源的访问。出于我们的目的,我们希望用户拥有管理我们的应用使用的所有 AWS 产品的完全权限。该用户是我们的超级管理员。

IAM 组

这里的诱惑是开始向该用户添加新的策略。等等!我们确定这是个好主意吗?我们真的知道这是唯一拥有这种力量的使用者吗?从长远来看,这个项目可能会有不止一个超级管理员。或许创建一个小组并在那里管理我们的政策会更有意义。

在导航中选择 Groups,您将看到一个类似于我们前面看到的 users 表的屏幕。单击创建新组。我们应该怎么称呼这个团体?我们知道,我们希望这些管理员能够访问我们的应用使用的所有产品。我们给了这个群体巨大的力量,因为现在,我们只是为自己创造了一个用户。让我们称这个组为 PhotoAdmins。单击下一步,您将进入附加策略视图。

IAM 管理的策略

如果您以前使用过 AWS,您可能会注意到 IAM 策略生成工具已经发生了变化。现在有两种类型的 IAM 策略:托管策略和内联策略。受管策略是 AWS 管理的策略中的一个或多个权限。例如,AmazonEC2FullAccess 策略包括对 EC2 服务的完全访问,以及弹性负载平衡和 CloudWatch 等相关服务。内联策略是当您有特定策略需求时可以创建的自定义权限,我们稍后将对此进行讨论。

创建组时,系统会提示您附加托管策略。如图 1-5 所示的 Attach Policy 视图将很快成为一个好朋友。该工具的唯一目的是简化为用户、组和角色选择正确权限的复杂过程(稍后将详细介绍)。

A978-1-4842-0653-9_1_Fig5_HTML.jpg

图 1-5。

Selecting a managed IAM policy in the Attach Policy view

虽然我们可以选择其中一个管理策略,然后就此结束,但这并不有趣,不是吗?相反,让我们创建该组,然后手动向其中添加内联策略。单击下一步进入查看视图,如图 1-6 所示,您将再次看到您的组名和策略。单击创建组完成。

A978-1-4842-0653-9_1_Fig6_HTML.jpg

图 1-6。

Review Group

您将返回到组列表视图。选择 PhotoAdmins 组并展开 Inline Policies 面板。单击链接创建新的内联策略。在下一个视图中,您可以从以下选项中进行选择:

  • 策略生成器:该选项将启动一个向导,允许您向组中添加一系列单独的策略语句。这是一种配置需要访问某些(但不是全部)服务的组的简便方法。
  • 习俗政策:这是阻力最大的道路。所有策略都以 JSON 格式读写。定制策略选项允许您手动输入策略的 JSON。例如,如果您想要配置一个策略来授予特定 AWS 资源的权限,那么您可以在这里使用策略 JSON 中的 ARNs。在某种程度上,您可以直接使用 IAM 策略声明 JSON。亚马逊在这里也提供了独立的 JSON 生成器: http://awspolicygen.s3.amazonaws.com/policygen.html

让我们继续下去,确保我们的 PhotoAdmins 组拥有它需要的所有权力(仅此而已!)并选择策略生成器选项。单击选择。

IAM 权限编辑器

下一个屏幕是权限编辑器。这里,我们将添加用户需要的每个单独的权限。除了返回 IAM 创建其他策略之外,我们将为他们提供完成其余课程的能力。参见图 1-7 。

A978-1-4842-0653-9_1_Fig7_HTML.jpg

图 1-7。

Generating individual IAM policy statements

我们配置权限的第一个选项是 Effect。虽然我们可以选择“拒绝”来禁止对特定服务的访问,但由于我们的组目前没有权限,这将是逆向操作。我们将选择“允许”,因为我们将允许访问特定服务。

从下拉列表中选择 AWS 服务。首先,我们要选择 AWS OpsWorks。接下来,我们可以只选择该组拥有权限的特定操作。此下拉列表中填充了可以在此 AWS 服务中执行的所有可能的操作。AWS 服务下拉列表中的每个选项都将重新填充操作列表。AWS 总共有数百个(如果不是数千个)操作。现在,选择所有动作。如果我们只想授予特定资源权限,我们可以在 Amazon 资源名称(ARN)输入中输入它的 ARN。我们暂时把它保留为*或全部。单击添加语句。您应该立即看到您的权限声明出现在该屏幕上,如图 1-8 所示。

A978-1-4842-0653-9_1_Fig8_HTML.jpg

图 1-8。

Permissions statement listing

让我们添加我们需要的其余权限。对于下面的每个服务,创建一个允许该服务的所有操作的语句。

  • 亚马逊云前线
  • 亚马逊云观测
  • 亚马逊云观察日志
  • 亚马逊 EC2
  • 亚马逊 RDS
  • 亚马逊路线 53
  • 亚马逊路由 53 个域
  • 亚马逊 S3
  • 亚马逊的 SES

您应该会在页面上看到所有这些权限。然后单击下一步。您将看到一个自动生成的策略名称。您可以让它保持原样,除非您为了方便使用而想要一个特定的名称。看哪!您的策略 JSON 出现在策略文档文本区域。点击“应用策略”,您的内联策略将被添加到组中,如图 1-9 所示。

A978-1-4842-0653-9_1_Fig9_HTML.jpg

图 1-9。

IAM Group Inline Policies view

接下来,我们希望将我们的用户添加到组中。导航到“用户”选项卡,并从表格中选择 photoadmin。在组标题下,单击将用户添加到组。选中我们的 PhotoAdmins 组旁边的框,然后单击添加到组。您将看到该组现在出现在用户详细视图中。很快用户就可以登录并开始工作了。

向下滚动一点,您会看到用户还没有密码。让我们给他一个默认密码。点击右下角的管理密码。在“管理密码”页面上,我们现在为他分配一个自定义密码。选择分配自定义密码旁边的单选按钮,并在框中键入单词 photo。在这些字段的正下方有一个复选框,要求用户在登录时更改密码。显然,照片不是一个安全的密码。让我们选中该框,然后单击“应用”。

如果您不信任您的用户自己选择安全密码,您可以导航到“密码策略”选项卡,并选择用户设置密码时要强制执行的一些规则,如最小长度或要求一个数字。

回到 IAM 仪表板。在页面顶部,您应该会看到文本“IAM 用户登录链接”这是您的用户将用来登录其帐户的 URL。让我们测试一下我们的进度。

将 URL 复制到剪贴板,并通过单击浏览器窗口右上角的您的姓名并选择“注销”来注销 AWS 控制台。将 URL 粘贴回地址栏,您将看到一个典型的登录屏幕。输入用户名 photoalbum 和密码 photo。如果您强制用户在登录时重置密码,系统会要求您现在重置。然后进入 AWS 控制台。您应该仍然可以在仪表板上看到所有 AWS 服务,但是您将无法执行您现在被限制执行的任何操作。

摘要

现在,您已经设置了本地环境,并完成了示例项目。现在,您应该对应用的用途有了一个清晰的概念,并为开发应用的后续步骤做好了准备。通过在身份和访问管理中创建用户、组和策略,您已经迈出了使用 AWS 的第一步,并了解了 Amazon 资源名称,这是 AWS 的全局标识符。现在,您已经准备好开始构建您的应用了。在下一章中,您将了解 AWS OpsWorks,并开始第一次在 AWS 上设置应用!

Footnotes 1

亚马逊网络服务,“云架构:最佳实践”, http://aws.amazon.com/es/whitepapers/architecting-for-the-aws-cloud-best-practices/ ,2010 年 5 月 21 日。

二、使用 AWS OpsWorks

完成第一章后,你现在应该对我们正在构建的应用有了很好的理解。您还应该已经注册了 AWS 帐户,并在 IAM 中设置了管理用户。如果您跳过了介绍性章节,那么关于身份和访问管理(IAM)的教程非常重要。您必须熟悉 IAM 的主要概念,才能使用 AWS 做几乎任何事情。

接下来,我们将开始使用 OpsWorks 应用部署服务将我们的基本应用部署到 AWS 的过程。在本章中,您将在 OpsWorks 中创建一个应用实例,并首次将其部署到 Web 上。在本章中,我们不会给代码库添加任何功能。不管怎样,我们已经迈出了在云中托管应用的第一步!

了解 OpsWorks

有趣的是,AWS OpsWorks 并不是由 AWS 从头开始构建的。2012 年,亚马逊收购了一家名为 Peritor 的公司,该公司提供类似功能的第三方企业部署服务。这两种产品的技术基础被称为 Chef ( www.chef.io/ ),这是一个用于以编程方式配置、自动化和简化服务器部署的框架。虽然您启动的每个 EC2 实例都有预设的配置,但是使用 Chef,您只需编写很少的代码就可以轻松地对环境做出自己的更改。使用 Chef 的一个优点是,您不必了解 AWS 虚拟服务器包的所有细微差别。相反,Chef 为您提供了一个用于配置常见服务器端软件的 API,如 nginx、Apache、PHP,甚至 Node.js。在本书的后面,您将直接使用 Chef 来了解这有多简单。

OpsWorks 旨在简化应用环境的定制和管理,为应用堆栈中不同类型的资源提供图形用户界面(以及 API)。我将很快更详细地回顾这些资源。任何尝试过像 OpsWorks 那样手动配置和部署应用的人都会告诉你,这可以节省大量时间,并大大降低出错的风险。没有涉足系统管理的普通软件开发人员会发现,作为云架构师,他/她现在拥有超人的能力。

随着我们继续学习本课并讨论该服务的具体功能,使用 OpsWorks 的好处应该是不言而喻的。也就是说,在易用性方面,定制总是要付出代价的。在 2013 年 2 月一篇关于 AWS OpsWorks 发布的博客文章( www.allthingsdistributed.com/2013/02/awt-opsworks.html )中,亚马逊首席技术官沃纳·威格尔提供了图 2-1 所示的便捷图表,这是 AWS 的风格。

A978-1-4842-0653-9_2_Fig1_HTML.gif

图 2-1。

The plane of Convenience vs. Control in AWS application deployment

此图描绘了便利性与控制性的一维平面,显示了 OpsWorks 与其他应用部署选项的相对位置。如您所见,OpsWorks 介于管理应用堆栈的 DIY 方法和更简单、更少定制的弹性 Beanstalk 之间。如果这让你停下来重新评估 OpsWorks 是否真的是最适合你的想法,不要担心。有些人认为 OpsWorks 是弹性豆茎的下一代翻版。你与 AWS 合作的时间越长,你就越会看到它们推出新的功能和服务之间更好的协同作用。有时,这些改进的速度可能会令人无法抗拒:每隔几周,您就会收到 AWS 的一封电子邮件,通知您可能会在您的应用中使用六个新功能。因此,不要太担心星盘。在 AWS 世界中,两年是一段很长的时间,重要的是,对于 AWS 提供的服务,有一个广泛的抽象范围,它产生了一系列的功能和限制,这取决于服务是如何设计的。

此外,当我们用 OpsWorks 部署我们的应用时,我们在应用中使用了许多其他的 AWS 服务。最终产品将真正是其各部分的总和。在课程结束时,通过一些努力,你可能会明白如何将 OpsWorks 替换为 Elastic Beanstalk 或 CloudFormation。图 2-1 仅仅说明了当你戴上云架构师的帽子时,需要进行成本效益分析。

稍后,我们将开始探索 OpsWorks 控制面板。我们将回顾它的各种特性,同时了解对 OpsWorks 至关重要的各种其他 AWS 服务。我们还将一瞥基础技术厨师如何仍然与像我们这样的 AWS 用户相关。当您在 OpsWorks 中分配资源时,您的 AWS 帐户将开始产生使用费。在自问是否需要十个 m 3.2 x 大型 EC2 实例来托管 Photoalbums 应用时,请记住这一点。但是在我们开始在 OpsWorks 中分配资源之前,我们应该对这些资源多了解一点。

分配资源

当您分配 EC2 实例、RDS (Rational Database Service)数据库实例和许多其他资源时,您做出的最重要的决定是什么和在哪里。什么是不言自明的。AWS 对这些资源有自己的定价等级,您可以在官方文档中找到。使用和定价之间的关系因服务而异,它们被单独记录。

例如,EC2 实例是根据功率(内存、CPU 时钟速度和物理处理器)×小时速率×小时数来预留的。为了回顾 EC2 实例类型的规范,这里提供了一个分类: http://aws.amazon.com/ec2/instance-types/ 。在确定实例类型时,您将使用您的判断,将您的技术需求与您的财务资源进行比较,交叉引用实例类型与此处的定价: http://aws.amazon.com/ec2/pricing/ 。虽然传统的托管服务(还有一些云托管平台)会对你预定的资源按月收费,但按小时收费才是 AWS 如此有用的原因。还是那句话,用什么付什么。这就是资源。在哪里是一个单独的问题,如果你从传统的主机转移,这是一个新的概念。

Caution

一旦您提供了您的账单信息,AWS 将允许您根据需要提供昂贵的资源。如果您是 AWS 新手,请小心请求过多的资源。每小时 1.50 美元听起来并不多,除非你不小心让一个实例在线三个月。

区域和可用性区域

许多人认为云是某种模糊的、没有位置的全球实体。当然,仍然有真实的服务器和数据中心——数据并不是真的漂浮在对流层中。AWS 在全球各地运行数据中心,通常在地理上靠近主要的人口中心。在大多数情况下,有几个数据中心服务于相同的地理区域或地区。这些地区的名称有美国东部(北弗吉尼亚)、美国西部(俄勒冈州)和亚太地区(东京)。虽然地区的名称通常描述了西欧等大陆地区,但括号中的位置描述了数据中心所在的更具体的区域(参见图 2-2 )。请记住,AWS 在一个地区的其他地方维护额外的支持基础架构,并且并非所有服务在所有地区都可用。你可以在 http://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/ 找到更详细的信息。

A978-1-4842-0653-9_2_Fig2_HTML.jpg

图 2-2。

AWS region data centers mapped (not pictured: GovCloud region)

地理在构建 web 应用时非常重要。无论你的代码有多快,或者你运行着多少台服务器,你的数据仍然要通过网络与用户交换。自拨号上网时代以来,随着技术的发展,用户的期望也发生了巨大的变化。你的最佳选择是将你的应用放在离你的预期用户群最近的地方。

Note

我将在第四章和第五章中与 S3 和 CloudFront 讨论在全球范围内发布你的内容的其他方式。

如前所述,许多地区有不止一个物理数据中心。如果某种服务中断暂时使一个数据中心无法运行,其他数据中心会保持该区域在线。这些数据中心在 AWS 服务中被抽象为可用性区域。在每个可用性区域内都有 EC2 实例和我们提供的其他资源。您可以将可用性区域视为包含运行 AWS 服务的硬件的数据中心。这个概念如图 2-3 所示。

A978-1-4842-0653-9_2_Fig3_HTML.gif

图 2-3。

Illustration of a region containing four distinct availability zones

现在您已经熟悉了区域和可用性区域的概念,您可以开始考虑不同的场景了。本质上,我们仍然在现实世界中使用物理服务器。他们会崩溃;他们可以失去权力;他们可以有一个随机部分失败,等等。有无数的故障点,如果您的所有 EC2 实例都在一个可用性区域中,并且该区域断电了,该怎么办?您的应用将脱机。事实上,可以肯定的是,在某个时刻,您的某个可用性区域将会出现孤立的中断,因此我们要为此做好准备。

人们可以写一本关于云架构应急计划的书。就本书的范围而言,我们将保持简单。我们的应用将托管在单个区域中,实例位于多个可用性区域中。我们将使用其他 AWS 服务在其他地区更快地分发我们的内容。如果我们想要额外的冗余,我们也可以将我们的应用克隆到不同的地区。只要我们的数据库在所有地区保持同步,我们就可以随意复制我们的应用。我将在整本书中讨论这些种类的优化。

其他 IAM 角色

在我们进入 OpsWorks 仪表板之前,我们将在身份和访问管理中创建一个新角色。在第一章中,您在 PhotoAdmins IAM 组中创建了一个管理级别的用户,该用户应该能够在 OpsWorks 中执行任何任务。我还讨论了在我们的代码中存储凭证的安全风险,以防开发人员离开项目或装有应用本地副本的机器被盗或丢失。

为了避免这些风险,我们将使用 IAM 角色来管理与其他服务的认证。我们实际上需要两个新的 IAM 角色:一个用于托管我们的应用的 EC2 实例,另一个允许整个应用栈代表我们行事。通过为我们的实例创建一个角色,通常称为实例角色,我们可以通过 AWS API 以编程方式访问其他 AWS 服务,而无需在源代码中存储安全凭证。我们将首先创建这个角色。

实例角色

返回 IAM 控制面板,单击左侧导航栏中的“角色”。找到页面顶部的“创建新角色”按钮,然后单击它。在下一个屏幕上,将提示您输入角色的名称,如图 2-4 所示。

A978-1-4842-0653-9_2_Fig4_HTML.jpg

图 2-4。

Set Role Name

我们希望我们的角色有一个逻辑名称,所以我们将使用 AWS-ops works-photoalbums-ec2-role,photo albums 应用中 EC2 实例的角色,它由 AWS OpsWorks 初始化。您可以使用任何有意义的约定,但我们使用的是[服务]-[应用]-[角色]的格式。单击下一步继续。同样,在为此角色创建 IAM 策略时,我们有许多选项可供选择。现在,我们将创建一个没有策略的基本角色。随着我们继续阅读本书的其余部分,我们将为这个角色添加更多的策略。首先会提示您选择角色类型。在服务角色选项框中,您会在列表顶部看到 Amazon EC2。单击选择按钮前进。

我们将再次能够选择策略(参见图 2-5 )。我们稍后将附加一个策略,因此请单击“下一步”继续查看视图。

A978-1-4842-0653-9_2_Fig5_HTML.jpg

图 2-5。

Selecting no policies for the aws-opsworks-photoalbums-ec2-role

在下一个视图中,您有机会在创建角色之前检查它。如果一切正常,单击创建角色。我们现在有了一个角色,可以分配给运行我们应用的 EC2 实例。然而,我们仍然需要整个应用堆栈本身的角色。

服务角色

作为一个 OpsWorks 应用,我们的堆栈将需要权限来执行常规任务,比如重启实例、向 AWS 控制台报告指标等。作为用户,您可以在 OpsWorks 仪表板周围单击,并手动执行各种任务。我们希望确保我们创建的应用能够自己执行这些操作。我们将通过为整个应用堆栈创建一个角色来确保这一点。这个角色通常被称为服务角色。一旦创建了这个角色,我们就可以进入 OpsWorks 仪表板来创建我们的应用了。

返回 IAM 仪表板中的“角色”选项卡,再次单击“创建新角色”。我们再次需要为我们的新角色取一个名字。按照我们之前的命名约定,我们将把这个命名为 AWS-ops works-photo albums-service-role。命名约定也是[服务]-[应用]-[角色]。继续下一步将把您带到选择角色类型视图,在这里 AWS 服务角色已经打开并在您面前列出。将此列表向下滚动到末尾,在那里您会找到 AWS OpsWorks,然后单击 Select。在附加策略视图中,将只有一个可能的策略可供选择:AWSOpsWorksRole。这是 AWS 为 OpsWorks 服务角色提供的默认托管策略。选择该框,然后单击下一步。再次查看策略,并通过单击“创建角色”完成该过程。

您将返回到 Roles 视图,在这里您将看到您的新角色已经创建。选择您的新角色,进入角色详细信息视图。在权限标题下,您会看到 AWSOpsWorksRole 是唯一的策略(参见图 2-6 )。单击 Show Policy,将出现一个模态视图。

A978-1-4842-0653-9_2_Fig6_HTML.jpg

图 2-6。

Policies attached to the aws-opsworks-photoalbums-service-role

在模态视图中,您将有机会查看策略文档的原始 JSON。JSON 应该类似于清单 2-1 ,如下所示:

Listing 2-1. The OpsWorks Service Role Policy

{

"Version": "2012-10-17",

"Statement": [

{

"Effect": "Allow",

"Action": [

"cloudwatch:GetMetricStatistics",

"ec2:DescribeAccountAttributes",

"ec2:DescribeAvailabilityZones",

"ec2:DescribeInstances",

"ec2:DescribeKeyPairs",

"ec2:DescribeSecurityGroups",

"ec2:DescribeSubnets",

"ec2:DescribeVpcs",

"elasticloadbalancing:DescribeInstanceHealth",

"elasticloadbalancing:DescribeLoadBalancers",

"iam:GetRolePolicy",

"iam:ListInstanceProfiles",

"iam:ListRoles",

"iam:ListUsers",

"iam:PassRole",

"opsworks:*",

"rds:*"

],

"Resource": [

"*"

]

}

]

}

您会注意到在Statement数组中有一个 JSON 对象,它具有属性EffectActionResourceEffect应该是不言自明的:我们在这个语句中特别启用了权限。接下来,Action是一组 AWS 服务和动作。这些可能以后会更有意义,但是你可以通过观察它们来了解它们是什么。您可以从第一个操作中看到,对于默认的 OpsWorks 角色,我们只为GetMetricStatistics启用了 CloudWatch API 权限,但是我们为 EC2 和 Elastic Load Balancing 启用了各种读取权限,并为 OpsWorks 和 RDS 启用了所有操作。很快您就会看到,这些服务可以在 OpsWorks 中直接初始化和配置,因此堆栈拥有这些权限非常重要。

您还会注意到这里有一个非常重要的动作,名为iam:PassRole。该权限将允许您的应用将其角色传递给它所管理的 EC2 实例,使它们能够代表服务角色执行任务。这很重要,你很快就会发现原因。

最后,Resource数组只包含“*”,即所有资源。为了简单起见,我们提供 IAM 角色用于所有资源,即使它们是专门为我们的应用命名的。将来,您可以返回到这些角色的策略文档,并将它们限制为应用的资源。您将了解到在策略文档中定位特定资源的能力是一个强大的特性。

不幸的是,默认的 OpsWorks 服务角色对于我们的目的来说不够强大。如前所述,我们只有 EC2 的读取权限。然而,我们希望能够在 OpsWorks 中创建资源,因此我们需要附加一个额外的策略。从“AWS-ops works-photo albums-service-role”的详细视图中,单击“Attach Policy”以添加另一个托管策略。从策略列表中选择 AmazonEC2FullAccess,然后单击附加策略。您将返回到角色详细信息视图,并且应该看到现在有两个托管策略附加到该角色。

为了遵循最佳实践,Amazon 建议撤销根帐户访问密钥,并为根帐户启用多因素身份验证。您可以随意遵循这些指南,但是您至少应该学会如何通过用户帐户而不是根帐户来管理您的架构。然而,当在 OpsWorks 中创建我们的第一个应用堆栈时,我们必须拥有 IAM 管理员访问权限,在第一章中,我们没有将此权限授予我们的 PhotoAdmins 组。因此,创建我们的应用堆栈将是我们作为根用户的最后一个动作。虽然您现在可能拥有 AWS 帐户的 root 访问权限,但最好养成在任何 AWS 帐户(即您的雇主或客户的帐户)上作为用户工作的习惯。

到目前为止,您已经在 IAM 中做了很多工作,并且了解了很多我们还没有开始直接使用的技术。这可能有点令人困惑,因为我们已经快速地学习了一些非常抽象的概念。概括地说,这是迄今为止您使用 IAM 和 OpsWorks 所取得的成果:

  • 您使用 root 帐户登录 AWS,该帐户在 IAM 中使用过,接下来将用于创建您的第一个应用堆栈。
  • 您已经创建了一个 PhotoAdmins 组并定义了它的策略,授予该组管理大量 AWS 服务的权限,我们稍后将使用这些服务。
  • 您向 PhotoAdmins 组添加了一个用户,以后您可以使用该用户在 AWS 控制台中登录并使用您的应用。
  • 您已经为应用堆栈中的 EC2 实例创建了实例角色。
  • 您已经为您的应用堆栈本身创建了一个服务角色。

在我们继续学习 OpsWorks 仪表板之前,我们还需要进行一个步骤来了解 SSH 密钥。请注意,这一步是完全可选的。对于示例应用,您不需要学习这一课,但是学习起来可能会很方便。如果你现在不想学这个,跳过下面的部分。

QUICK DETOUR: SSH KEYS

我们之前了解到,运行 Amazon Linux 实例的好处之一是严格的默认安全设置。开箱即用,您的实例只能通过 SSH 连接—FTP、SFTP 和其他常见的连接方法被禁用。要启用它们,您必须在 AWS 中打开正确的端口,并在命令行上安装软件,这本身就是一个单独的教程。要设置这些方法,您首先需要能够通过 SSH 连接到您的实例。幸运的是,Amazon 使得为实例生成密钥变得很容易。

如果我们必须为应用中的每个实例存储一个单独的 SSH 密钥,这不是很烦人吗?除了丢失和混淆键的风险之外,在栈中添加和删除实例将是一个更加劳动密集型的过程。Amazon 让这变得简单了,它允许我们在 AWS 控制台中创建一个 SSH 密钥,并将其设置为栈中所有实例的默认密钥。理论上,您可以对所有堆栈使用一个主密钥,但是每个堆栈一个密钥似乎更有意义。现在让我们继续创建我们的密钥。首先,我们必须前往 EC2 仪表板。为此,您可以单击左上角的橙色框图标,或者打开它旁边的服务菜单,然后在“计算和网络”部分找到 EC2,如下图所示。有时,如果您不确定在哪个类别中可以找到您需要的服务,返回 AWS 控制台会更容易。

A978-1-4842-0653-9_2_Figa_HTML.jpg

EC2 仪表盘上有很多东西,比我现在需要详细查看的还要多。我们以后会在这里多呆些时间。在左侧导航中,您应该会看到一系列可折叠的部分,默认情况下它们是展开的。在“网络与安全”下,单击“密钥对”,如下图所示:

A978-1-4842-0653-9_2_Figb_HTML.jpg

在这里,您将看到为 EC2 实例生成的所有 SSH 密钥的列表。这些密钥的加密是 1024 位 SSH-2 RSA。每个区域有 5,000 个键的限制,但大多数用户似乎不太可能接近这个上限。除了在控制台中创建您的密钥对之外,您还可以自己生成它们并将其导入 AWS 然而,这超出了本书的范围。

注意:如果删除密钥对,不会影响已经使用它们的实例。如果您有私钥的副本,您仍然可以连接到您的实例。但是,您将无法使用已删除的密钥对预配新实例。

到目前为止,AWS 中的一些 UI 设计选择应该开始变得熟悉了。与 IAM 类似,Create Key Pair 按钮位于主要内容区域的左上角。这只是打开一个模态窗口,您可以在其中命名密钥对,而不是开始一个多步骤的过程。让我们将其命名为 aws-opsworks-photoalbums-key,遵循与我们之前创建的 IAM 角色类似的命名约定。

当您单击 Create 时,将会生成密钥,并且一个名为aws-opsworks-photoalbums-key.pem的文件应该会自动下载到您的机器上。扩展名.pem是“隐私增强邮件”的缩写,这个文件被称为私钥。

AWS 将生成并存储相应的公钥,形成一个密钥对。公钥的副本将被保存到使用该密钥对提供的所有实例中,并且您必须提供匹配的私钥,以便使用 Amazon 提供的 Java 插件在命令行或浏览器中与您的实例建立安全连接。我们还没有任何实例,所以我们现在不会使用私钥来连接任何东西。将密钥保存在安全的地方,最好备份在某个地方。(提示:您可以将一个副本存储在一个私有的 S3 桶中,我们稍后将设置这个桶。)

OpsWorks 环境

当我们在 OpsWorks 中设置第一个应用时,了解我们的工具是什么非常重要。幸运的是,Amazon 为 OpsWorks 环境提供了一个很好的例子,因为它与一个 PHP 应用有关。你可以在图 2-7 中找到。

A978-1-4842-0653-9_2_Fig7_HTML.gif

图 2-7。

The components of an OpsWorks stack

如您所见,AWS 中应用的所有组件都分组在 OpsWorks 堆栈中。堆栈是组成应用部署的所有组件的集合。您可能以前听说过应用堆栈这个术语;我一直在频繁使用它。OpsWorks 堆栈就是 AWS 环境中的应用堆栈。与传统的宿主环境不同,OpsWorks 中的堆栈组织分配给应用的所有资源。

在图 2-7 中,您可以看到包含在 OpsWorks 堆栈中的 PHP 应用服务器层。根据它的复杂性,我们的应用可以有很多层。对于像我们这样的基本 web 应用,只有几层:应用托管服务器(EC2 实例)、负载平衡器和数据库。

您还会注意到,应用与应用服务器实例是截然不同的。这代表了我们的应用的实际源代码,堆栈将负责部署它。OpsWorks 从代码库中检索源代码(通过互联网),并将源代码部署到我们的实例中。根据在堆栈、层和应用源代码级别定义的要求,OpsWorks 将确保正确配置实例,安装依赖项,并在实例上上传和运行应用。我将很快讨论这些组件中关注点的分离。

OpsWorks 仪表板

我们走了一些大的弯路,但最终是时候在 OpsWorks 中创建您的应用了。在 AWS 控制台中,在服务列表(中间列)中找到 OpsWorks 并单击它。由于巨大的“欢迎使用 OpsWorks”标题,您现在应该可以清楚地看到 OpsWorks 仪表板。您会注意到一个蓝色的大按钮,上面写着添加第一个堆栈。

大量

想象一下,我们在一个传统的共享主机提供商上托管我们的应用。您可以使用您的帐户登录管理控制台来管理您的资源。您可能在一个选项卡中有一些 web 应用,然后在另一个选项卡中有一些数据库,甚至所有应用都托管在同一个数据库的不同表中。在这种模式下,管理很快就会变得混乱。你受到你自己、你的同事和/或你的前任的支配,希望遵循合乎逻辑的命名惯例。甚至一些云托管平台仍然是这样,这大大增加了人为错误的风险,在这位作者看来。

每个堆栈由一组独立的资源组成,这意味着影响一个应用的问题仅限于该应用。如果你的一个应用崩溃了,被黑了,或者耗尽了资源,变得没有反应,其他栈不会受到影响。因为我们将在多个可用性区域中创建实例,所以导致我们的应用宕机的唯一外部因素是主要的 AWS 服务崩溃。听起来是部署应用的好方法,对吗?

在 OpsWorks 中,您可以创建任意多的堆栈,它会将项目的所有资源整齐地组织到每个堆栈中。这对于创建开发和登台环境特别有用,因为每个堆栈上都有一个克隆按钮!您还可以一键启动和停止所有服务,并同时在所有实例上运行命令。如此简单却又如此强大!让我们把相册放到网上吧。继续并单击添加第一个堆栈。下一页发生了很多事情,如图 2-8 所示。

A978-1-4842-0653-9_2_Fig8_HTML.jpg

图 2-8。

Creating an application stack in OpsWorks

第一个领域很简单,输入相册,然后进入下一个领域。现在我们必须选择一个地区。有可能辨别你的目标受众的地理位置吗?亚马逊的主要数据中心在弗吉尼亚州北部,因此该地区默认为美国东部。除非你有理由改变这一点,否则我们可以让它保持原样。

下一个字段询问您是否要选择 VPC 或虚拟私有云( http://aws.amazon.com/vpc/ )。如果您想要将应用部署到专用网络,您可以将应用分配到此处的 VPC。在我们的例子中,我们正在构建一个供公众在网上消费的应用,所以我们不会选择 VPC。VPC 可以包含公共子网和私有子网,但这可能是续集的主题!

Note

如果要使用 VPC,必须在创建 OpsWorks 应用之前创建它。

我已经谈到了可用性区域,所以您知道您创建的每个实例都将在特定的可用性区域中运行。当您分配新实例时,默认可用性区域只是预选的可用性区域。这是一个纯粹为了方便而设计的字段。如果您正在管理大量实例,并且知道您希望它们位于特定的可用性区域中,那么在这里选择它可以省去以后在大量实例上更改区域的麻烦。但是,您当然可以在以后更改实例的可用性区域。让我们在这里保留默认值。

Amazon Linux

您将会看到默认操作系统下拉菜单为您提供了一些选择。除非你正在构建一个明确需要 Ubuntu 的应用,否则你很可能会选择最新版本的 Amazon Linux。截至发稿时,最新的是亚马逊 Linux 2014.09。

Amazon Linux 是基于 Red Hat Enterprise Linux 的 Linux 版本,由 Amazon 管理。它是专门为部署到 EC2 实例而设计的,值得注意的一个主要特性是 Amazon Linux 是为 AWS 环境中的最大安全性而定制的。默认情况下,远程访问 Amazon Linux 实例的唯一方式是通过 SSH。可以通过 AWS 控制台打开其他方法。与部署你从网上下载的 Linux 版本相比,Amazon Linux 的安全配置让你放心,因为它已经通过了专家的安全缺陷检查,而没有太多的限制,以免损害你的软件。

这带来了另一点,即通过利用云,我们试图避免陷入操作系统配置的地狱。我们不想部署定制版的 Ubuntu 我们希望启动一个实例,并知道它已经准备好以启动的速度运行我们的软件。如果我们不能信任运行在我们众多服务器上的操作系统,那么我们又回到了起点。亚马逊已经写了很多关于他们构建 Linux 的文章,你可以在这里找到全部: http://aws.amazon.com/amazon-linux-ami/

亚马逊机器图片

您会注意到,除了 Amazon Linux 和 Ubuntu,您还可以选择使用自定义 AMI。此选项允许您创建运行特定软件包的实例,或者,如果您愿意,可以将实例锁定到特定版本的操作系统。亚马逊支持这一功能,但似乎不太情愿。它建议您应该使用 Amazon Linux,然后使用 Chef 定制您的安装,如果需要的话。这是最佳实践,但是您可以选择做您想做的任何事情。如果您使用 Chef 而不是自定义 AMI,那么您将受益于 Amazon 对您的操作系统的持续支持,同时允许您进行自定义选择。选择 Amazon Linux 并继续。

实例与 EBS

啊哦!又一个陌生的问题。我们的默认根设备类型应该是实例存储还是 EBS 支持的?这一切意味着什么?基于到目前为止对 EC2 实例的讨论,您现在应该认识到这些实例是短暂的。当您停止一个实例时(无论是否选择),存储在该实例上的所有数据都将丢失。因此,依赖 EC2 实例来实现数据持久性并不是一个好主意。即使 EC2 实例提供了永久的数据持久性,我们会想要使用它们吗?AWS 应该使基础设施更容易管理,管理跨实例存储的唯一数据可能会变得复杂。

亚马逊对这个问题的解决方案叫做弹性块存储。借助 EBS,您可以为持久数据存储提供可扩展的磁盘驱动器。您一次只能将一个 EBS 实例附加到一个 EC2 实例,但是您可以获取我们的 EBS 的快照,并使用它来实例化一个新的 EBS。

这在我们的应用中有用吗?最终,我们将接受文件上传并存储,供用户通过网络访问。您肯定知道您需要持久的磁盘存储,并且我们希望在多个 EC2 实例上运行我们的应用。显然,这不是解决方案,因为存在实例过时的风险。很高兴知道这个功能的存在;它只是不适合我们的用例。

我们知道我们将需要一些持久的数据存储,并且我们知道 EBS 不能用于多个实例。因此,我们现在可以选择实例存储,因为我们知道只能在 EC2 实例上存储临时数据。这意味着文件上传、日志文件等。,将不得不存储在其他地方,我们将很快更详细地讨论这一点。

选择实例存储作为默认根设备类型后,您必须选择一个 IAM 角色。这是我们之前创建的堆栈级服务角色。从下拉列表中选择 AWS-ops works-相册-服务-角色。接下来,我们可以选择设置默认的 SSH 密钥。如果您经常需要在命令行上连接到实例,这是一个很方便的字段。它通过假设堆栈中的所有 EC2 实例应该使用相同的密钥来简化密钥管理,这是一种比为每个实例生成一个新的密钥对好得多的方法。我们的应用不需要这个,所以您可以选择不选择默认的 SSH 密钥。提醒一下,SSH 密钥在本章前面已经详细讨论过了。如果您按照这些步骤创建了一个密钥对,现在就从下拉列表中选择它。

接下来,我们必须选择默认的 IAM 实例配置文件。您还记得,我们之前创建了两个 IAM 角色:一个服务角色和一个实例角色。这是我们选择实例角色的地方,AWS-ops works-photo albums-ec2-role。您会注意到,在此屏幕中,您可以选择动态创建一个新角色,这将使 OpsWorks 自动为您生成一个角色。

说到命名约定,下一个选项可能是令人困惑的主机名主题。下拉菜单中有很多选项,如“烘焙食品”、“欧洲城市”、“野猫”等。当您在 OpsWorks 中创建 EC2 实例时,您不希望它们仅仅被命名为 instance-1、instance-2 等。主机名主题选项只是用于实例的主题名称,如“Photoalbums - london-1”或“Photoalbums - paris-2”等。你可以选择任何让你开心的事情,但是请注意列表中的第一个选项,图层依赖。这将根据实例所属的 OpsWorks 层来命名实例,我们稍后将回顾这个概念。我倾向于选择这个选项,因为这意味着您的实例将被命名为“Photoalbums nodejs-app1”、“Photoalbums nodejs-app2”等。在接下来的课程中,我们将使用层相关主题,但是如果你认为用猫命名服务器会很有趣的话,你可以随意选择另一个选项。

最后是堆栈颜色,这没有任何技术意义。这是用于 OpsWorks 仪表板中堆栈的配色方案,纯属个人喜好。让我们选择最右边的红色。

堆栈选项—摘要

现在我们终于准备好点击右下角的添加堆栈。我们选择的选项出现在清单 2-2 中。再次检查您的选择,然后单击按钮。

Listing 2-2. Summary of Stack Creation Options

Name: Photoalbums

Region: US East

VPC: No VPC

Default Availability Zone: us-east-1a

Default operating system: Amazon Linux 2014.09

Default root device type: Instance store

IAM role: aws-opsworks-photoalbums-service-role

Default SSH key: aws-opsworks-photoalbums-key (optional)

Default IAM instance profile: aws-opsworks-photoalbums-ec2-role

Hostname theme: Layer Dependent

Stack color: red

恭喜你!您已经创建了第一个堆栈。我们现在处于 stack detail 视图中,该视图应列出设置应用的下四个主要步骤:

  • 添加你的第一层。
  • 添加您的第一个实例。
  • 添加你的第一个应用。
  • 在线查看您的申请。

所以,是的,在这一课中我们还有很长的路要走。请记住,本章的目标只是通过 OpsWorks 部署在 Web 上看到“Hello World”。下一步是了解图层。

层是应用的主要软件成分,以及与之配套的硬件。让我们稍微分解一下。当您在第一章中设置本地环境时,您必须完成两个主要任务:运行 Node.js 环境和运行数据库。这是我们应用的两个层次。在 OpsWorks 中,每一层都需要分配资源。

第一层,我们的 Node.js 应用,没有运行代码的机器是没有用的——它只是页面上的文字!因此,我们的 Node.js 应用层需要分配 EC2 实例。Layers 视图允许您将这些移动的部分联系在一起:源代码、环境和运行这两者的实例。

第二层,数据库层,有点不同。我们将使用 Amazon RDS 托管我们的数据库,我们将在后面的章节中设置它。就目前而言,理解这种看待问题的方式就足够了。

每个堆栈必须至少有一层,其中有两种类型:OpsWorks 层和 service 层。OpsWorks 层只是分配给它的 EC2 实例的蓝图。OpsWorks 为您提供了许多预设的 OpsWorks 层类型,这些层类型本身被分类为负载平衡器、应用服务器、数据库或其他。对于某些 OpsWorks 层子类型(尤其是 App Server),您可能需要基于同一蓝图的多个实例。其他类型,如负载平衡器,通常只用于单个实例。

除了 OpsWorks 层,AWS 还提供了第二种类型,我们称之为服务层。服务层允许您将其他 AWS 服务作为层添加到 OpsWorks 堆栈中。目前,仅支持一种服务层类型:RDS。我希望将来会推出更多的服务。

创建 OpsWorks 图层

是时候创建我们的第一层了。在堆栈详细信息页面的第一个标题下,单击添加层。就像我们创建堆栈时一样,我们会看到许多选项,如图 2-9 所示。

A978-1-4842-0653-9_2_Fig9_HTML.jpg

图 2-9。

Add Layer view

您可以看到主要的层类型是通过顶部的选项卡来区分的。我们还没有准备好添加 RDS(服务)层,因此我们将停留在 OpsWorks 选项卡中。我们的第一个选择是层类型。这是组织不同 OpsWorks 图层类型的地方。打开下拉菜单,你会看到一个列表,如图 2-10 所示。

A978-1-4842-0653-9_2_Fig10_HTML.jpg

图 2-10。

OpsWorks layer types

默认情况下,应用服务器标题下的 Rails 应用服务器是选中的。我们将选择 Node.js 应用服务器。突然间,我们的很多选择都消失了。嗯,那就简单了!下一个字段是 Node.js 版本。这使您可以选择使用不推荐的 Node.js 版本,以防您正在部署一个尚未在 Node.js 的最新版本中测试的应用。在撰写本文时,我们将坚持使用最新的 0.10.29 版本。

最后一个选项是弹性负载平衡器。我们将在下一章添加一个 ELB 到我们的堆栈中,所以我们暂时跳过这个。将此字段留空,然后单击添加层按钮。就这么简单!我们已经得到了我们的第一层,你应该被引导到层屏幕,如图 2-11 所示。

A978-1-4842-0653-9_2_Fig11_HTML.jpg

图 2-11。

Layers view

现在我们的堆栈中只有一层,那就是应用服务器层。运行我们的应用的所有 EC2 实例都将是这一层的一部分。如果你需要复习这是如何工作的,再看一下图 2-7 。

例子

如您所见,在 OpsWorks 仪表板中已经有许多新的地方可供探索。接下来,让我们继续将我们的第一个实例添加到层中。单击屏幕右侧的添加实例按钮。下一个视图有一些介绍性的文字,以及图 2-12 中的界面。

A978-1-4842-0653-9_2_Fig12_HTML.jpg

图 2-12。

Add an instance

你会注意到只有几个设置可供选择。大多数配置都基于我们在创建堆栈时选择的默认值。例如,Hostname 字段根据我们之前选择的主机名主题预先填充了一个唯一的名称。您可以将其保留为 nodejs-app1,除非您有自己的命名约定。

可能最大的决定(没有双关语)是大小。您将在这里看到一个长长的选项列表,包括当前一代和上一代的尺寸,以及各种优化选项。亚马逊定期发布新一代实例,其规格和价位与上一代不同。你可以在这里对照这个列表: http://aws.amazon.com/ec2/instance-types/#Instance_Types 。为了不让这个页面变得过于臃肿,前代的规格被移到这里单独的一页: http://aws.amazon.com/ec2/previous-generation/

在生产环境中,当创建应用服务器层的第一个实例时,您必须考虑很多因素。我将在后面详细讨论这一点。现在,选择列表中最小和最便宜的选项 ??.micro 是有意义的,一直到最底部。

接下来,我们为此实例选择可用性区域。同样,我们在创建堆栈时选择的默认值也被选中。您可以更改可用区域,但现在让它保持为 us-east-1a。我们稍后将在其他区域中创建实例,因此我们希望在默认区域中至少保留一个实例。继续并点击高级,这样您就可以看到额外的选项。

首先是扩容类型。这是我们稍后将深入研究的主题,但是现在我们希望我们的实例 24/7 在线,所以我们将选择该选项。您将在堆栈创建过程中认识到的其他字段。让我们不去管这些默认值,但是如果您想在实例级修改它们,最好知道它们在这里。点击 Add Instance,您将进入 Instances detail 视图,这是一个您非常熟悉的屏幕(见图 2-13 )。

A978-1-4842-0653-9_2_Fig13_HTML.jpg

图 2-13。

Instances view

在此视图中,您可以看到层中所有实例的状态。顶部的圆形图显示了处于各种状态的实例的数量和百分比:联机、启动、关闭、停止或错误。我们目前有一个实例已经停止,所以这里没有太多可操作的信息。在右上方,您会注意到一个按钮,上面写着 Start All Instances,这正是它所说的。您还可以单独启动或停止实例,例如,如果您想要手动扩展其中一个实例。或者,如果您想要手动扩展许多实例,您可以一次停止一个实例,扩展它们,然后重新启动它们,这样您的应用就不会出现任何停机。您还可以在此屏幕上向您的层添加更多实例。

在顶部黑色的 AWS 导航栏下面,您会注意到 OpsWorks 附加了一个灰色的导航栏。在左侧,您可以导航到当前堆栈,紧挨着它的是一个下拉菜单,可以导航到完全不同的堆栈或创建一个新的堆栈。单击导航按钮,您应该会看到类似于图 2-14 所示的下拉菜单。

A978-1-4842-0653-9_2_Fig14_HTML.jpg

图 2-14。

OpsWorks stack navigation menu

您将看到一些我们已经参观过的视图,以及一些不熟悉的视图。选择应用,我们将最终在 OpsWorks 中创建应用。

应用

Apps 视图顶部的介绍性文本解释了这个概念,正如我所希望的那样:

An application represents the code stored in the repository that you want to install on the application server instance. When you deploy the application, OpsWorks downloads the code from the repository to the specified server instance.

当我们创建应用时,我们将 OpsWorks 配置为获取示例应用的副本,并将其部署到应用服务器层中的每个实例,然后启动这些实例。您应该会看到一个蓝色消息框,通知您没有应用,但可以添加应用。点击这个,我们就可以开始创建这个应用了。

首先是名称字段,您可以在其中输入相册的名称。下一个字段是 Type,它应该默认为 Node.js,所以我们可以不去管它。下一部分的标题是数据源,在这里您可以选择想要使用哪种类型的数据库。您可以选择 RDS,这是一个服务层。如果您希望在 OpsWorks 中创建 MySQL 数据库层,可以选择 ops works;如果您托管的是静态应用,则可以选择 None。我们将选择 RDS 并在稍后创建一个 RDS 实例(参见图 2-15 )。

A978-1-4842-0653-9_2_Fig15_HTML.jpg

图 2-15。

Add an app to your OpsWorks stack

应用源

我们已经到了一个重要的决策点:我们如何将我们的源代码部署到我们的服务器上?这是一个重要的决定,不是对你的应用的功能,而是对你的开发团队的工作流程。我们将逐一查看这些选项。您可以选择最适合自己的方法,在以后的课程中,我将假设您可以自己管理部署。图 2-16 显示了一个示例配置。

A978-1-4842-0653-9_2_Fig16_HTML.jpg

图 2-16。

Application Source configuration in OpsWorks app

如果您打开 Repository type 菜单,您将会在三个标题下看到五种类型:源代码控制、Bundle 和其他。如果从源代码管理部署,则提供 URL 和凭据,这将允许 OpsWorks 连接到您的存储库并从指定的分支下载源代码。如果您选择 Bundle,OpsWorks 将从指定位置检索一个 zip 文件,将其解压缩,然后运行应用。最后一个选项是其他,是针对更高级的用户。您可以使用 Chef recipes 来处理部署过程,而不是由 OpsWorks 检索您的代码。

如果您熟悉 GitHub,最简单的方法是从您自己的 GitHub 库进行部署。如果您使用另一个允许您通过 SSH 连接的 Git 存储库服务,过程基本上是相同的。我将在下面描述不同的方法,你可以选择适合你的方法并继续下去。

从 Git 部署

您将需要生成一个 SSH 密钥,以便 OpsWorks 可以连接到您的存储库。如果你需要帮助,这里有一个方便的指南: https://help.github.com/articles/generating-ssh-keys/

Note

生成 SSH 密钥时,不要设置密码。OpsWorks 不支持带密码的 SSH 密钥,您将无法使用您的密钥进行部署。

生成密钥后,我们可以填写字段。首先是资源库 URL,您可以在 GitHub 的屏幕右侧找到它(参见图 2-17 )。如果您使用 Beanstalk 或其他存储库服务,您会在类似的侧栏中找到 SSH 克隆 URL。

A978-1-4842-0653-9_2_Fig17_HTML.jpg

图 2-17。

SSH clone URL in GitHub

将 SSH 克隆 URL 复制到剪贴板,并粘贴到 OpsWorks 中的存储库 URL 字段。接下来,我们需要您生成的私钥。它应该被命名为“github_rsa”之类的东西,并位于您计算机上的./ssh中。在纯文本编辑器中打开文件。它应该如下所示:

-----BEGIN RSA PRIVATE KEY-----

Proc-Type: 4,ENCRYPTED

DEK-Info: AES-128-CBC,3048941ED91AFBCE12E396E516EC35D4

0gkTkCilHDYOgommrpNVlmZjtKxrD4smsFOVgvhweaNv0G8aTMQcjYb461TqwdsJ

{{A BUNCH OF RANDOM CHARACTERS}}

iLOdRv+4XFKhN3ZKyJ9VwV0yxrV6hSR0FOwFzGtXAD8OJctMcyAwGctJJmNQmRe2

-----END RSA PRIVATE KEY-----

将文件内容复制到剪贴板上,并粘贴到存储库 SSH 密钥字段中。最后,您可以选择一个特定的分支/修订来部署。默认情况下使用 HEAD,所以我们不去管它。例如,如果您正在创建一个新的堆栈作为您的开发环境,您应该在这里输入您的开发分支名称。

从 Subversion 部署

与 Git 一样,您将为 OpsWorks 提供连接到您的存储库和下载源代码副本的凭证。使用 Subversion,您必须通过 HTTP 连接,提供您的帐户用户名和密码。与 OpsWorks 一样,您可以指定要部署的特定版本。但是,请注意,这次您必须在存储库 URL 中包含代码的完整路径。如果要从特定分支进行部署,请确保在存储库 URL 中包含目录路径。

从 HTTP 归档部署

如果您没有使用代码存储库,或者出于某种原因,您不想将目录从 repo 部署到 OpsWorks,还有一些其他选项。首先,您可以从 Web 上任何地方托管的归档进行部署。如果您选择 HTTP 存档,您只需要应用的 zip 存档的 URL。OpsWorks 将下载您的归档文件,提取它,并将其部署到您的实例中。如果您的归档受密码保护,您可以选择提供用户名和密码。如果你的 zip 文件可以在网上公开访问,显然这意味着你的源代码可以被任何人访问,这可能不是一个好主意。

从 S3 档案库部署

就像部署 HTTP 归档文件一样,您也可以从亚马逊 S3(简单存储服务)上托管的归档文件进行部署。要设置这一点,我们必须绕道,但如果可以的话,整合资源并使用 HTTP 上的 S3 是明智的。当您选择 S3 时,您会看到您需要三条信息:您的存档的 URL、访问密钥和秘密。这意味着我们必须创建一个 S3 时段和一个 IAM 用户来访问该时段。我们将快速浏览这些步骤,并在本书的后面花更多的时间和 S3 在一起。

首先,让我们创建我们的 IAM 用户,允许访问 S3 存储桶。我们可以先创建一个组,然后像以前一样添加用户。然而,权限很简单,如果有必要,我们可以重用这个用户,所以让我们保持简单,只创建一个用户。最好是在一个新的标签中进行这种迂回。单击(或右键单击或按住 Control 键单击,具体取决于您的操作系统)左上角的橙色框,返回 AWS 主屏幕,然后选择 IAM。单击左侧导航栏中的用户,然后单击用户页面顶部的创建新用户。我们将这个用户称为 photoalbums-s3。当您进入下一个屏幕时,请确保单击下载凭据,以保存用户访问密钥/密码的副本。然后单击关闭。

现在,您应该会在用户列表中看到您的用户。单击名称进入用户详细信息视图。你以前来过这里!接下来,我们必须为用户生成一个策略,允许用户拥有对 S3 的完全访问权限。在权限标题下,单击附加用户策略。我们再次处于策略生成器中。在“选择策略模板”标题下,向下滚动并选择“亚马逊 S3 完全访问”。您将有机会在生成策略之前对其进行检查(参见图 2-18 )。

A978-1-4842-0653-9_2_Fig18_HTML.jpg

图 2-18。

S3 Full Access Permissions policy

接下来,我们必须创建我们的 S3 桶。回到 AWS 主页,点击 S3。单击左上角的创建存储桶。将出现一个模态弹出窗口,提示您命名该桶。输入 photoalbums-source-[您的姓名首字母]作为您的存储桶名称,并选择您所在地区的美国标准(参见图 2-19 )。

A978-1-4842-0653-9_2_Fig19_HTML.jpg

图 2-19。

Creating an S3 bucket

选择名称和区域后,单击创建。您将返回到 S3 的主视图,您的存储桶将出现在左侧。点击它,你会看到你的桶的内容,目前是空的。单击此视图左上角的上传。创建一个你的源代码的档案,并把它拖到上传对话框中。您不需要在这里设置任何其他选项;只需点击开始上传。

您将再次返回文件列表,上传进度将出现在屏幕右侧。完成后,在文件列表中点击您的.zip。在右上角,您会看到一个分段控制,当前选择了“无”。点击属性,您的档案的属性会出现在右侧,如图 2-20 所示。找到链接,并将完整的 URL 复制到剪贴板。

A978-1-4842-0653-9_2_Fig20_HTML.jpg

图 2-20。

S3 object properties

回到 OpsWorks,将 URL 粘贴到 Repository URL 字段中。访问密钥 ID 和秘密访问密钥是您创建的 IAM 用户的凭证。如果您下载了凭据,请将其打开并复制到这些字段中。如果您能够遵循所有这些步骤,那么您应该能够从 S3 进行部署。若要将更新部署到您的代码,您必须每次都覆盖 S3 存储桶中的 zip,或者上传新的 zip 并在您的应用设置中更改路径。如您所见,这并不理想。一些存储库服务还允许您从他们的服务器部署到 S3,因此也可以从您的存储库部署 zip 到 S3。无论如何,这种方法比生成一个 SSH 密钥并直接从 Git 部署要耗费更多的人力。

创建您的应用

配置完应用源后,您将看到以下部分:环境变量、添加域和 SSL 设置。您将在后续课程中学习这些内容。如果其他一切看起来都很好,继续点击右下角的添加应用。你应该会返回到 Apps 视图,如图 2-21 所示,在这里你可以看到你的应用已经被创建了。

A978-1-4842-0653-9_2_Fig21_HTML.jpg

图 2-21。

The Apps view

部署您的应用

嗯,除了部署您的应用之外,没有太多事情要做了!在我们这样做之前,我们必须启动一个实例。单击左上角的导航并选择实例以返回实例视图。在右上角,单击启动所有实例。您将看到实例旁边的 Status 字段更改为 requested,然后是 pending,booting,running_setup,最后是 online。整个过程需要几分钟。

现在我们有了一个在线实例,我们可以进行部署了。在导航下,选择应用以返回到应用视图。您会在相册行右侧的“操作”栏中看到“部署”按钮。点击它,您现在应该会发现自己在部署应用视图中,如图 2-22 所示。

A978-1-4842-0653-9_2_Fig22_HTML.jpg

图 2-22。

Deploy App view

您将看到除了 Deploy 之外,在这个视图中还有其他命令可用。您可以取消部署您的应用、回滚到以前的版本、启动、停止或重新启动应用。现在,我们将使用 Deploy 命令。下面的注释字段是您自己关于部署的内部注释。

下面的实例标题通知您将在其中一个实例上进行部署。您只能部署到已经启动的实例。这是因为停止的实例实际上并不存在。您无需为资源付费,因此 AWS 数据中心不会提供任何资源。除了启动实例之外,不能与已停止的实例进行交互。如果单击“高级”,可以看到正在部署的实例列表。

如果出于某种原因,您想只部署到特定的实例,您可以在这里选择/取消选择它们。其中一种情况是,您的一个实例出错(崩溃),您需要部署一个补丁来解决这个问题。您部署到当前在线的实例,同时尝试将有问题的实例恢复在线。一旦上线,您可以再次运行部署,但是只能在刚刚从错误中恢复的实例上运行。您可能从未遇到过这种用例,但很高兴知道 OpsWorks 可以处理它!

点击【部署】,将进入部署视图,如图 2-23 所示。

A978-1-4842-0653-9_2_Fig23_HTML.jpg

图 2-23。

Deployment view

当部署运行时,您将看到一些活动指示器在旋转。完成后,顶部的状态将变为 successful,并且您的实例旁边会出现一个绿色复选标记。您会注意到这里有几个有趣的特性。在 SSH 列下,您可以选择通过 SSH 直接连接到任何正在运行的实例。你可能永远都不需要这么做,但拥有它是件好事。在日志列中,可以单击显示来查看实例的部署日志。

这些日志一开始可能有点让人不知所措;它们是部署应用时执行的所有厨师食谱的输出。它们通常应该是这样的:

[2014-10-26T18:48:44+00:00] INFO: Chef-client pid: 3167

如果您看到除了INFOWARN之外的任何日志类型,您可能想要进一步调查。如果你喜欢冒险,你可以通过日志来了解发生了什么。几个主要事件大致如下:

  • 运行opsworks_custom_cookbooks::load::execute命令。
  • 基于你的应用的配置和语言的附加食谱列表被执行。
  • 您的代码是通过 SSH 复制和验证的。
  • 您的代码被部署到/srv/www/photoalbums,Node.js 环境被配置。
  • 检测到您的package.json文件,并且安装了node_modules
  • 您的应用已启动(或重新启动)。

这是一种简化,但是它应该让您了解在幕后发生了什么来使您的代码在云中运行。现在是真相大白的时候了。打开 OpsWorks 导航菜单并选择“实例”。在 Public IP 列中,您应该看到您的单个实例的 IP 地址。点击它,您应该会看到我们的欢迎屏幕,如图 2-24 所示。

A978-1-4842-0653-9_2_Fig24_HTML.jpg

图 2-24。

Welcome to Photoalbums!

你能相信吗?我们终于在云中运行了我们的应用!因为我们没有配置负载平衡器或添加域,所以我们只是在单个实例上查看应用。这不是我们对 OpsWorks 的预期用途——我们不希望我们的用户直接访问 EC2 实例。我们将很快解决这个问题。

摘要

恭喜你,你有了一个驻留在网络上的应用,在云中!遗憾的是,在我们至少配置好数据库之前,它基本上还是没用的。你已经在这一章中讲述了很多,从探索 AWS 架构的主要概念到从头开始创建我们的应用堆栈和部署我们的代码。在下一章中,我们将使用 Amazon RDS 托管我们的数据库,我们的应用将第一次变得可用。通过学习如何使用 OpsWorks,您还学习了很多关于 AWS 的范例和术语,尤其是关于身份和访问管理以及 EC2 的知识。这是云架构和开发道路上的一大步。

三、OpsWorks 第二部分:数据库和扩容

现在我们有了一个正常工作的应用堆栈,我们可以开始真正地构建功能了。首先,我们必须将应用连接到数据库,这样我们就可以开始存储和检索内容。其次,我们将在堆栈中添加一个负载平衡器。最后,我们将为我们的应用设置一些自动伸缩行为,以便应用可以自动伸缩以满足需求的增长,具有弹性。

您应该还记得上一章中的内容,OpsWorks 堆栈中包含一个或多个层。在第二章中,我们创建了一个单层:应用服务器。我们将在本章中添加额外的层,这将完善设置 OpsWorks 的主要任务。我们将通过配置和附加一个高可用性的托管 MySQL 数据库来创建数据库层。我们还将在堆栈上附加一个负载平衡器,在多个 EC2 实例之间分配 web 流量。虽然我们将多次返回 OpsWorks 仪表板,但我们将与其他 AWS 服务一起构建我们应用的核心组件。

关系数据库服务

RDS 是 AWS 数据库服务产品之一,也是我们在这个项目中的选择。RDS 支持 MySQL,性价比高,可以作为服务层集成到 OpsWorks 中。与 EC2 类似,RDS 实例可以进行伸缩、克隆和性能问题监控。

敏锐的观察者会想,为什么我们不能创建一个新的 OpsWorks 层,并在一些 EC2 实例上运行 MySQL。OpsWorks 里不是有 MySQL 层类型吗?是的,有,你可以很容易地做到这一点。然而,RDS 不仅仅具有原始的计算能力。RDS 有一些很棒的特性,我们将在本章中进一步探讨。

如果您在 OpsWorks 的 MySQL 层中创建了一个 EC2 实例,那么您只是安装了必要的软件来在该实例上运行 MySQL 数据库。仍然由您来执行所有管理任务,最重要的是备份您的数据。您还可以根据需要安装软件更新。如果 MySQL 崩溃,或者实例出错,您将不得不手动恢复。

这些听起来都不吸引人,不是吗?幸运的是,RDS 可以为您自动完成所有这些任务!如果我们不得不做这么多的维护工作,我们就不会充分利用云或者利用我在过去两章中提到的优势。当您使用 RDS 实例时,您不必担心所有这些维护工作。对应用性能最重要的是错误恢复。当您创建 RDS 数据库时,您以与 EC2 实例相同的方式提供资源。您的数据库托管在特定区域内的特定可用性区域中。如果数据库层出现故障,而您正尝试手动恢复,这可能会导致整个应用长时间停机。RDS 不仅最大限度地减少了停机时间,还允许您在主实例停机时保持备份实例随时可用,这是一种称为多 AZ 部署的特性。

多 AZ 部署

当您的应用依赖于单个 RDS 数据库实例时,这自然会在您的应用中产生一个故障点:如果该可用性区域(或实例)出现故障,可能会导致您的堆栈出现全局故障。为了缓解这个问题,您可以使用多 AZ 部署。当您启用多 AZ 部署时,RDS 会自动在另一个可用性区域中提供备用数据库(参见图 3-1 ),并通过同步过程将数据复制到备用实例。每次在数据库上执行写或提交操作时,在事务完成之前,也会在备用数据库上执行该操作。缺点是,与单个可用性区域部署相比,这会导致延迟略有增加。如果您的应用要求每秒进行数千次数据库写入,这种微小的延迟差异可能会变得很明显。请记住,这可能会在您的应用中构成有形的性能成本。多 AZ 部署的成本也更高,因为您实际上使用的资源是其他方式的两倍。

A978-1-4842-0653-9_3_Fig1_HTML.gif

图 3-1。

Multi-AZ deployments

显然,多 AZ 部署需要进行成本效益分析。一方面,您不必担心通过手动启动数据库的备份副本来应对停机。任何不得不在周五晚上 11 点上班的工程师都知道这一点的价值。拥有一个完全最新的备份副本来运行您的应用并在几分钟内可用,这是一个技术奇迹,但一般人对此并不欣赏。另一方面,使用 Multi-AZ 会损失一点点写入速度,而且成本更高。如果 Multi-AZ 不值得花费,AWS 承诺 RDS 实例每月 99.95%的正常运行时间。如果您选择使用单个实例,您的应用将面临中断的风险。

然而,在我们这样的应用中,我们甚至可能不会注意到延迟的差异。我们的数据库模式很简单,查询也很简单。用户不太可能一次对每个用户执行大量的写操作,所以在我们的例子中,这似乎不是一个主要问题。事实是,虽然多 AZ 的好处可能是普遍的,但多 AZ 部署的性能成本取决于应用的性质以及预计的用户群规模和行为。

读取副本

正如刚才所讨论的,我们预计我们的数据库操作是读取密集型的。如果我们数据库上的大量读取操作(用 MySQL 术语来说,任何SELECT查询)成为我们数据库的瓶颈,这将对整个应用产生连锁反应。幸运的是,有一种方法可以将部分工作转移到另一个数据库实例,即读取副本。您可以创建这种类型的 RDS 实例(最多五个),指定一个主数据库并为您的读取副本选择一个区域和可用性。从术语“副本”可以推断,这些实例应该与原始数据库拥有相同的资源。

在图 3-2 中,您可以看到读取副本是如何工作的。数据库读取可以路由到读取副本,读取副本以较低的查询量从主实例读取。任何写操作都会绕过读复制副本,转到应用堆栈的原始可用性区域中的主实例。使用读取副本可以将大量工作从主数据库实例转移到副本。如果主数据库只需要处理少量的读操作和所有的写操作,会有很大的好处。

A978-1-4842-0653-9_3_Fig2_HTML.gif

图 3-2。

Cross-Region Read Replica behavior. The green lines represent write operations, and the black lines represent read operations

正如您可能从图 3-2 中推测的那样,RDS 实际上支持在其他区域创建读取副本,具有一个称为跨区域读取副本的特性。灾难恢复场景有一个好处。如果由于某种原因,整个 AWS 区域发生了中断,您必须在另一个区域快速重新部署整个堆栈。为此,您可以将堆栈克隆到不同的区域,并将先前存在的读取副本之一提升为新的主数据库。否则,从区域性中断中恢复是可能的,但是这个特性只是使它变得更容易。

如果您只想将堆栈移动到不同的区域,跨区域读取副本在非灾难场景中也很有用。假设我们的应用在美国很失败,但在德国却很受欢迎。有理由将堆栈移动到离我们的用户群更近的地理区域。

由于 OpsWorks 旨在让您在单个区域部署堆栈,并使用其他服务来提高您在其他区域的性能,因此我们不会从跨区域读取副本中获得太多好处,除非我们计划应对区域性灾难。您将很快了解到有助于我们提高性能的其他服务,但不管怎样,了解一下这项功能还是有好处的。

PRICING

有了这些额外的功能,你可能会立即怀疑 RDS 的可负担性。的确,它比 EC2 定价要简单一些,你可以在这里看到: http://aws.amazon.com/rds/pricing/ 。主要区别在于:除了为处理器和内存(也就是实例的计算能力)付费之外,还需要为数据存储和数据传输付费。数据存储是指存储在实例中的原始数据量,数据传输是从数据库到应用层的输入/输出。与其他服务类似,这是一种按需付费的模式,但这里有很大的偏差空间。

调配 IOPS 存储

尽管我们已经选择了实例大小,但我们没有向 Amazon 提供我们想要执行多少输入/输出操作的信息,I/O 速度和容量是衡量数据库性能的关键指标。如果我们的流量在一个小时内从五个用户激增到五千个用户,每秒钟进行一次数据库查询,我们将依赖 AWS 来动态地自动提供额外的 I/O 容量。

如果您考虑一下查询数据库时会发生什么,这是有意义的。当突然出现超过数据库 I/O 容量的请求时,它们会被放入一个队列中,而 RDS 则试图跟上流量。与此同时,RDS 试图为您的实例增加 I/O 资源,这完全发生在 AWS 数据中心的幕后(也称为云中)。但是,分配额外的容量很可能需要一秒钟以上的时间,请求留在队列中的每一秒钟都是用户等待所请求数据的额外一秒钟。当流量减少时,资源会被取消分配,并且会对您使用的资源进行收费。

如果我们要管理性能预期,这似乎不是生产中的最佳想法。幸运的是,您可以使用调配的 IOPS 存储预留 I/O 资源(每秒输入/输出操作数)。当您使用调配的 IOPS 存储时,您会按存储卷和每秒 I/O 操作数来保留 IOPS 资源。虽然这比随用随付更昂贵(如果您不使用您提供的资源),但提前预留资源可以保证在高流量期间保持一致的速度和性能。

让我们想象一下与之前相同的场景,只是使用了调配的 IOPS 存储。这一次,您保留了 10,000 IOPS 和 10GB 的存储空间。同样,您的流量从 5000 个用户激增到 5000 个用户,每秒钟进行一次数据库查询。在这种情况下,我们已经有足够的资源来处理双倍的 I/O 流量。当然,如果我们的流量翻倍,我们将再次陷入困境,但我们也可以为这种可能性做好准备。

数据库安全组

在创建数据库之前,我们必须创建一个 DB 安全组。如果我们的数据库在虚拟私有云中,公共访问将受到限制。因为我们没有使用 VPC,所以我们希望将数据库访问限制在应用堆栈中的 EC2 实例。因为我们也想在本地工作,我们将允许从我们的 IP 地址访问数据库。虽然您会从我们在身份和访问管理方面所做的工作中认识到这些概念,但我们实际上将在 RDS 中配置我们的安全组。

首先返回您的登录 URL,以您在第一章中创建的 photoadmin IAM 用户身份登录。从 AWS 控制台主页(或菜单)中,选择 RDS。您应该会看到类似于图 3-3 中的视图,其中显示了 RDS 仪表板最左边的两列。和往常一样,右边有一个专栏,其中有这里没有显示的其他链接和资源。

A978-1-4842-0653-9_3_Fig3_HTML.jpg

图 3-3。

The RDS dashboard

在左侧列中,单击安全组。在右侧,您会看到一个蓝色的大按钮,上面写着 Create DB Security Group。第一种观点很简单。您为您的安全组提供一个名称和描述(参见图 3-4 )。

A978-1-4842-0653-9_3_Fig4_HTML.jpg

图 3-4。

Create DB Security Group view in the RDS dashboard

您可以将该组命名为 photoalbums-rds-group。描述可以是以后对你有用的任何东西。然后,单击是,创建。您将返回到 RDS 安全组视图,并且您应该看到您的组出现在表格中,如图 3-5 所示。

A978-1-4842-0653-9_3_Fig5_HTML.jpg

图 3-5。

Your RDS security groups

您会注意到状态栏中的红色文本“无授权”这意味着尽管该安全组已经创建并可以分配给 RDS 实例,但该组目前不提供对任何实例的访问。这是一个方便的警告,说明你在这里的工作还没有完成。

为您的安全组选择表行,您将能够创建一个新的连接类型,以便在您的安全组中进行授权。这两种类型是 CIDR/IP 和 EC2 安全组。我们将分别创建一个。

当您授权一个 CIDR/IP 时,您将列出一个特定的 IP 地址来连接到您的数据库。这对于开发来说是理想的,因为我们可以列出自己的 IP 地址来连接 RDS 实例。默认情况下,您将在字段中看到您当前的 IP 地址。如果你使用代理/防火墙,你必须禁用它,或者如果你在公司网络上,与网络管理员合作。如果您没有代理或在防火墙后面,请保留 CIDR/IP 地址不变,然后点按“授权”。否则,确定正确的 IP 并相应地更改值。请记住,如果您有一个动态 IP,您将不得不在每次 IP 地址刷新时重复这个过程。

您将在您的安全组旁边看到一个活动指示器,并且会出现一个新表,其中包含您的安全组的授权连接(参见图 3-6 )。接下来,我们将创建一个 EC2 安全组类型的新连接。就像 RDS 一样,EC2 也有自己的安全组。事实上,我们的应用服务器层中的所有实例都是它们自己的安全组的一部分。

A978-1-4842-0653-9_3_Fig6_HTML.jpg

图 3-6。

DB Security Group, authorizing connections

从连接类型下拉列表中选择 EC2 安全组。您会注意到,除了您自己的帐户之外,您还可以选择另一个 AWS 帐户。在撰写本文时,Amazon 正在推出跨帐户连接,从而允许另一个 AWS 帐户上的 EC2 实例连接到您的数据库。保持此帐户处于选中状态,并在 EC2 安全组下拉列表中选择 AWS-OpsWorks-nodejs-AppServer,然后单击授权。现在,您应该在安全组的连接表中看到第二行。

创建 RDS 数据库

现在您有了一个 RDS 安全组,它将允许来自您的本地机器、您在 OpsWorks 中的应用服务器层的连接,而不允许来自其他地方的连接!事不宜迟,让我们继续创建我们的数据库层。在 RDS 主页屏幕上,您将看到一个蓝色的大按钮,邀请您启动一个 DB 实例。单击该按钮将带您进入多步实例设置。如图 3-7 所示,第一步是选择数据库引擎,在撰写本文时,它包括四个选项:MySQL、PostgreSQL、Oracle 和 SQL Server。这是显而易见的,因为我已经讨论过在我们的应用中使用 MySQL。单击选择并继续。

A978-1-4842-0653-9_3_Fig7_HTML.jpg

图 3-7。

RDS database engine selection

第 2 步很有趣,因为这是 AWS 就如何配置生产环境和开发环境给出明确建议的罕见场景之一。如图 3-8 所示,步骤 2 的标题是“生产?”对我们来说,答案是肯定的!

A978-1-4842-0653-9_3_Fig8_HTML.jpg

图 3-8。

Choosing to use an RDS instance in a production environment

如您所见,Amazon 强烈建议在生产环境中使用多 AZ 部署和调配的 IOPS 存储功能。我们会听从他们的建议,选择“是”,然后单击“下一步”。在第 3 步中,我们将配置我们的数据库细节(参见图 3-9 )。

A978-1-4842-0653-9_3_Fig9_HTML.jpg

图 3-9。

RDS database details

实例规格

License Model 字段可以保留为 general-public-license,也可以保留 DB 引擎版本。如果你需要一个特定的 MySQL 版本,你可以在这里选择。接下来,在 DB Instance Class 字段中选择实例的大小。出于我们的目的,db.t1.micro 应该可以工作。让我们保留多 AZ 部署和调配的 IOPS (SSD)。您可以现在选择磁性存储以节省资金,以后再切换到调配的 IOPS,但转换将需要不确定的时间。在这种情况下,AWS 控制台将显示一条消息,通知您在应用更改时,多 AZ 备份实例将正常工作。我艰难地认识到情况并不总是这样。

如果您听取了我的建议并选择了“已调配 IOPS ”,您必须选择您希望分配多少存储和多少 IOPS。当您选择“调配的 IOPS”作为存储类型时,将选择 100GB 存储和 1000 调配的 IOPS。亚马逊推荐 IOPS 与存储的比率在 3:1 到 10:1 之间。我们可以暂时不考虑这个设置。随着时间的推移,我们将为我们的应用建立一个操作历史,并根据我们对收集的指标的分析来扩大或缩小这些资源。

Note

在现实世界的应用中,在 AWS 提供的最小实例中使用所有这些 RDS 特性有点傻。我们选择 db.t1.micro 只是为了经济高效的实践。

设置

如果您以前曾经建立过数据库,这些设置会很熟悉。您将为您的数据库设置唯一的标识符、用户名和密码。在数据库实例标识符中,输入 photoalbums。为了便于讨论,我们将主用户名命名为 admin。你实际上可以设置任何你想要的用户名和密码,只要你记住它!设置好凭据后,单击下一步。

高级设置

步骤 4 将我们带到高级设置,您可以在图 3-10 中看到。在网络安全中,您将无法选择 VPC 或可用性区域。选择 photoalbums-rds-group 作为您的数据库安全组。在数据库选项中,您可以命名您的数据库相册。同样,数据库端口、参数组和选项组也不必更改。

A978-1-4842-0653-9_3_Fig10_HTML.jpg

图 3-10。

RDS advanced settings

下一小节的标题是“备份”,从中可以为实例选择自动备份计划。RDS 将自动对您的实例进行每日备份(快照),您可以指定进行这些备份的窗口以及保留备份的时间长度。首先,选择一个备份保留期,默认为 7 天。这是亚马逊将保留备份的天数,从 0 到 35。如果选择 35,您将能够将您的实例恢复到过去 35 天拍摄的任何每日快照。万一发生灾难,有这些备份是很方便的。但是,如果您需要更多的长期备份,您将不得不手动生成它们(或者使用 AWS API 以编程方式)。

您还可以为您的数据库选择一个备份窗口。您可能希望在特定时间创建备份,例如在预期流量激增之前或之后。如果您没有偏好,它们将默认为深夜,此时流量预计会减少。没有首选项也可以,您同样可以将维护设置保留为默认值。单击启动数据库实例,这将带您到一个中间屏幕。单击以返回 RDS 仪表板,在这里您将看到您的数据库首先处于创建状态,然后是修改状态,然后是备份状态,最后是可用状态。

数据库导入

创建 RDS 实例时,让我们准备将本地数据库模式导入到 RDS 实例中。在第一章的中,您看到在/setup/photoalbums.sql有一个 MySQL 数据库与示例项目打包在一起。我们将使用 MySQL Workbench 连接到 RDS 实例,并将该文件导入数据库。如果您喜欢使用另一个接口来连接 MySQL 数据库,那很好。我们使用 MySQL Workbench 只是因为它很简单。

为了唤起您的记忆,我们创建了一个 RDS 安全组,该组只允许访问我们指定的 EC2 实例和您的个人 IP 地址。(如果您在多个地点工作,您必须向安全组添加更多 IP。)我们在该安全组中创建了一个 RDS 实例,我们将确保可以连接到该实例,此时我们将知道一切都按计划进行。打开 MySQL Workbench,或者你正在使用的任何 MySQL 客户端。在左上角,您应该会看到一个+按钮,单击它可以创建一个新的连接。您应该会看到如图 3-11 所示的窗口。将连接命名为相册 RDS。连接方法可以保持为 TCP/IP。我们将不得不参考 RDS 来填写剩余的字段。

A978-1-4842-0653-9_3_Fig11_HTML.jpg

图 3-11。

Setting up a new connection in MySQL Workbench

成功创建实例后,RDS 将生成您的主机名。让我们回到控制台找到它。在 RDS dashboard 中,您应该在表视图中看到您的实例,其状态为绿色的 available。点击该行显示该实例的详细信息,如图 3-12 所示。

A978-1-4842-0653-9_3_Fig12_HTML.jpg

图 3-12。

RDS instance details

在正上方,您应该会看到端点。这是您可以在端口 3306 上连接到实例的 URL。只接受来自你的 IP 地址的连接。复制这个 URL 并将其粘贴到 MySQL Workbench 的 Hostname 字段中(粘贴时记得去掉:3306)。输入您之前选择的用户名和密码,然后单击确定。

MySQL Workbench 将自动打开到数据库的连接。我们现在准备从示例项目中运行 SQL 导出脚本。在文件菜单下,选择运行 SQL 脚本并导航到.sql文件。将出现一个窗口,显示 SQL 脚本的内容,让您选择更改默认模式名或默认字符集。在默认模式名称下拉列表中,选择相册。将字符集留空,然后单击运行。窗口应该将其副标题更改为 Output,输出应该如下所示:

Preparing...

Importing photoalbums.sql...

Finished executing script

Operation completed successfully

单击关闭按钮,并在左侧导航的 SCHEMAS 标题下找到相册数据库。按住 Control 键点按(或在 PC 上右键点按)相册,然后选择“全部刷新”。过一会儿,您应该能够展开 photoalbums 下的表格,并看到您已经在本地使用过的表格:相册、照片和用户(参见图 3-13 )。我们很快就让它运行起来了,不是吗?现在,如果我们的应用也能连接到它就好了!

A978-1-4842-0653-9_3_Fig13_HTML.jpg

图 3-13。

The tables created on your RDS instance

OpsWorks RDS 层

您应该记得,我在前面的第二章中讨论过 RDS 层。现在我们有了一个 RDS 实例,我们将把它注册到应用中。在 AWS 控制台中,返回到 OpsWorks 仪表板。选择您的堆栈,并从导航菜单中选择层。

您应该在列表中只看到一个层:Node.js App Server。下面,点击+层按钮。我们回到添加层视图,这一次,我们必须单击 RDS 选项卡来创建一个 RDS 层。该设置与您看到的应用服务器层略有不同,如图 3-14 所示。

A978-1-4842-0653-9_3_Fig14_HTML.jpg

图 3-14。

Adding the RDS layer

相册实例被自动检测到,因为它在同一区域。你所要做的就是输入你的用户名和密码,然后点击注册堆栈。您将返回到 Layers 视图,现在您将看到在 App Server 层下面的 RDS 层,如图 3-15 所示。但是上面说没有 app 连接!

A978-1-4842-0653-9_3_Fig15_HTML.jpg

图 3-15。

Layers in our stack, with RDS layer added

为了将我们的应用连接到 RDS 层,我们必须将数据库设置为应用服务器的数据源。首先,单击连接应用,这将带您进入堆栈中的应用列表。在相册的操作列中,单击编辑,这将允许您编辑应用的一些设置。

第二个标题是数据源。您会注意到,目前数据源类型被设置为 None。单击 RDS 旁边的单选按钮;选择相册(mysql)作为数据库实例;并输入 photoalbums 作为您的数据库名称。它看起来应该与图 3-16 一模一样。暂时不要离开这个屏幕;我们在这里还有一件事要做。

A978-1-4842-0653-9_3_Fig16_HTML.jpg

图 3-16。

RDS selected as data source for the app

环境和环境变量

如前所述,我们的目标之一是将数据库凭证移出源代码。在第一章的中,我讨论了如果团队成员离开或他/她的电脑被盗,将这些凭证存储在源文件中会带来怎样的安全风险。还有另一个需要考虑的原因,那就是交换我们堆栈中的数据库。到目前为止,您已经看到从头开始创建 RDS 实例是多么容易。将来,您将能够从现有的数据库实例中创建新的数据库实例,这将使您能够在出现严重故障时运行堆栈中的备份数据库。这还将使克隆整个堆栈和维护用于生产和开发的独立堆栈变得非常容易,这是一种强烈推荐的做法。通过遵循这些后续步骤,您将能够维护在所有这些场景下工作的代码,而不必部署代码更改或管理凭据。

到目前为止,我主要讨论了两个环境:本地环境和生产环境。现实情况是,您的工作流可能包括在本地环境中编写和测试代码,然后在开发堆栈上部署和测试,最后部署到生产环境中。这涉及到要管理的三组凭证!如果我们在存储库的不同分支中保存不同的凭证,管理起来会很麻烦,并且很容易出现开发人员错误。相反,我们将使用环境变量来告诉我们的应用它在哪里运行。

如果你已经精通 Node.js,你会知道有一个名为process.env的全局对象,它存储了 Node.js 中关于用户环境的一些信息,你可以在这里找到关于这个的信息: http://nodejs.org/api/process.html#process_process_env 。文档中的属性似乎都没有那么有用。还好我们可以给他们加!

在编辑应用时,向下滚动到环境变量标题,如图 3-17 所示,您会看到您可以在这里添加自己的键值属性。

A978-1-4842-0653-9_3_Fig17_HTML.jpg

图 3-17。

App Environment Variables

添加一个名为ENVIRONMENT的变量,并赋予其值production。如果/当你创建一个开发栈,你将改变这个值。继续并点击右下角的保存。我们现在已经设置了应用的数据源和环境变量ENVIRONMENT

这个环境变量和process.env有什么联系?这是 Chef 的底层技术在幕后工作以使事情变得更简单的微妙方式之一,仅在 Chef 11.10 中可用。当您的应用部署到应用服务器层中的实例时,您设置的环境变量(最多 20 个)将包含在实例的部署脚本中。因为我们的应用是 Node.js,所以在部署过程中会将环境变量添加到process.env中。如果我们运行一个 PHP 应用,环境变量将可以通过getenv($variableName)访问。无论如何,应用设置中的这个接口为我们提供了一个简单的方法来动态设置所有实例中的环境变量,而无需进入服务器配置,也不会破坏我们应用的弹性。

接下来,在耽搁了很久之后,我们不得不回到我们的源代码。目前,源代码被硬编码到本地数据库中。如前所述,我们必须支持三种不同的环境。首先,打开您的代码编辑器到/lib/globals.js。找到database属性,它有本地主机的凭证。我们不想返回一个静态对象,而是想把database变成一个函数(嘿,因为它是 JavaScript,所以并不难!).因为这个文件中还没有其他内容,所以您会希望用清单 3-1 替换整个文件,但是要准备好您的本地凭证,因为我们不会完全丢弃它们。

Listing 3-1. /lib/globals.js

module.exports = {

applicationPort  : 80,

database : function(){

if(process.env.ENVIRONMENT){

var opsworks = require('./../opsworks');

var opsWorksDB = opsworks.db;

var rdsConnection = {

host : opsWorksDB.host,

port : opsWorksDB.port,

database : opsWorksDB.database,

user : opsWorksDB.username,

password : opsWorksDB.password

};

return rdsConnection;

} else {

var local = require('./../config/local');

var localConnection = local.db;

return localConnection;

}

}

}

好了,这里发生了很多事情,我们来分解一下。首先,我们不是返回静态对象,而是检查process.env.ENVIRONMENT是否存在。如果是,我们不关心它是什么,我们将尝试从 OpsWorks 加载数据库凭证,因此出现了行var opsworks = require('./../opsworks');

您会注意到我们的项目中没有这样的文件。在部署期间运行的Configure stack 命令动态编译我们项目根目录中的一个opsworks.js文件。当我们将 RDS 层连接到 Photoalbums 应用时,它会自动开始在该文件中存储数据库的凭据。这意味着,即使我们更改了该层使用的数据库实例,我们也可以通过在堆栈上运行Configure命令来更新凭证,稍后我们将回到这个问题。在opsworks.js文件中,数据库凭证存储在一个名为db的公共变量中。事实上,整个文件看起来有点像清单 3-2 。

Listing 3-2. Sample opsworks.js file

exports.db = {"adapter":"mysql",

"database":"ops_db",

"password":"AsdFGh3k",

"port":3306,

"reconnect":true,

"username":"opsuser",

"data_source_provider":"rds",

"host":"opsinstance.ccdvt3hwog1a.us-east-1.rds.amazonaws.com"

}

exports.memcached = {"port":11211,

"host":null}

在理想情况下,db变量的语法应该与我们在mysql Node.js 模块中需要的语法相匹配。可悲的是,这两者略有不同。为此,我们实例化了一个名为rdsConnection的变量,并将值重新映射到它。然后,返回rdsConnection变量。

在没有设置process.env.ENVIRONMENT的情况下,我们希望保留我们的本地数据库凭证。在您的项目中,创建一个名为config的目录和一个名为local.js的空文件。使用您的本地数据库凭据,将以下内容粘贴到其中:

module.exports = {

db : {

host : 'localhost',

port  : 8889,

database : 'photoalbums',

user : 'root',

password  : 'root'j

}

}

我希望这有意义。我们将本地凭证移动到一个单独的文件中,只有在没有找到process.env.ENVIRONMENT的情况下,我们才会从globals.js中读取这个文件。现在,因为我们把globals.database改成了globals.database(),我们将不得不在几个地方修复它。在server.js中,导航到文件的底部,并在这一行中进行更改:

var connection  = mysql.createConnection(globals.database());

保存更改,然后我们必须更改所有三个模型文件顶部的声明:model-usersmodel-photosmodel-albums。在所有三个文件中,将第 3 行改为:

var connection = mysql.createConnection(globals.database());

为了强调这一点,我们所做的只是在database后面加上一个“()”。

现在,我们将对凭据管理进行最后的收尾工作。我们将让我们的代码库忽略我们的本地凭证,这样它们就不会被推送到 repo,不会与其他开发人员共享,也不会被部署到应用服务器。如果您使用 Git,打开文件.gitignore并添加下面一行:

/config/local.js

保存此更改。如果您使用的是 SVN,您同样希望忽略该文件。将所有这些更改提交到您的存储库中,并返回到 OpsWorks。在保存了最后的更改后,您应该会返回到应用的详细视图。您应该会在右上角看到一个蓝色按钮,上面写着部署应用。点击按钮,进入部署应用视图。这里你唯一需要改变的是添加对你有用的注释(见图 3-18 )。

A978-1-4842-0653-9_3_Fig18_HTML.jpg

图 3-18。

The Deploy App view

单击右下角的 Deploy,等待部署完成。如果几分钟后失败,请点按“日志”栏中的“显示”,看看是否能发现错误。如果您的主机名旁边有一个绿色的复选标记,那么您应该有一个应用现在连接到您的数据库。单击 nodejs-app1,向下滚动到 Network Security,找到您的实例的公共 IP。点击它,你应该回到你的 Hello World 屏幕。现在将/users/添加到 URL 中。

如果你看到一对空括号,恭喜你!您请求了所有用户,但收到的都是零。这意味着您的应用服务器能够连接到 RDS 实例,查询数据库,并返回结果。如果您看到的是一条错误消息,那么很遗憾,一定有什么地方出错了。检查打字错误,如果你没有发现,试着追溯你的步骤。

堆栈命令:备份场景

我简要讨论了堆栈命令的概念,这是一个 OpsWorks 工具,用于在堆栈中的所有实例上运行命令。这些命令本质上抽象了 Chef 的底层技术,让我们可以控制所有实例,如果我们必须单独管理实例,这将更加困难。我们将使用一个数据库场景来更深入地探讨这个问题。

RDS 快照

假设您的数据库处于错误状态,并且您无法让它重新联机。随着时间的推移,您的用户无法访问您的应用。您的多 AZ 备份没有按预期工作,从应用服务器到 RDS 层的每个查询都返回一个错误。您和您的团队一致认为,最好的做法是部署一个备份数据库,让您的应用重新上线。在传统的堆栈中,完成这一任务所涉及的管理任务可能是一场噩梦,会导致应用长时间停机。幸运的是,我们可以通过使用 OpsWorks 节省大量的时间和精力。

我们要做的第一件事是获取数据库的快照,我们将用它来生成新的实例。如果实例中的实际数据没有问题,我们现在就可以拍摄快照;否则,您最好使用 RDS 生成的自动快照。导航到 RDS 仪表板并选择 DB Instances 选项卡。从表中选择您的实例,单击“实例操作”按钮以显示“操作”菜单,然后选择“拍摄数据库快照”。

系统将提示您命名快照。自动快照前面有 rds:前缀。将快照命名为 photo albums-Snapshot-[YYYYMMDD],然后单击是,拍摄快照(参见图 3-19 )。

A978-1-4842-0653-9_3_Fig19_HTML.jpg

图 3-19。

Taking an RDS DB snapshot

您将进入快照视图,在该视图中,您的快照将处于正在创建状态。您还可以在此视图中看到一些自动备份。在我们继续本课之前,您需要等待快照完成。

快照完成后,我们将创建一个新实例。从列表中选择快照,然后单击顶部的恢复快照。您将看到一个名为 Restore DB Instance 的视图,尽管您会认出最初创建 RDS 实例时的界面。因为我们只是运行一个练习,所以您可以使用另一个 db.t1.micro 实例并禁用多 AZ 部署。您可以将实例命名为 photoalbums-failover,然后单击启动数据库实例(参见图 3-20 )。

A978-1-4842-0653-9_3_Fig20_HTML.jpg

图 3-20。

Restoring a DB instance from a snapshot

同样,我们需要等待一段时间来创建实例。当状态变为可用时,我们可以开始将其部署到堆栈中。

创建新的 RDS 层

返回 OpsWorks,访问相册堆栈。打开导航菜单并选择层。单击 RDS photoalbums 图层查看该图层的详细信息。在我们将备份数据库添加到堆栈之前,我们将取消注册这一层。在右上角,单击“取消注册”按钮。系统将提示您确认是否要取消注册该层。单击红色的取消注册按钮后,您将被带到 Resources 视图,此时该视图为空。

在 Resources 标题下,有一个按类型筛选资源的子菜单:卷、弹性 IP 和 RDS。单击 RDS,您将在您的资源中看到一个空的 RDS 实例表。在右上角,单击注册 RDS 数据库实例。这似乎与我们之前创建图层的方式相反,但我们只是在 OpsWorks 的指导下通过不同的工作流程完成相同的流程。之前,我们创建了一个 RDS 层,然后添加了一个 RDS 资源。这一次,我们将添加一个 RDS 资源,然后自动生成一个层。

您将在一个表中看到您的两个 RDS 实例。选择 photoalbums-failover,在表格下方的连接细节面板中输入您的数据库凭证,如图 3-21 所示。这些应该与原始数据库的凭据相同,因为该实例是从该实例的快照创建的。然后,单击向堆栈注册。

A978-1-4842-0653-9_3_Fig21_HTML.jpg

图 3-21。

Adding an RDS resource to the stack

将新数据库层连接到应用

再次打开导航菜单,并选择层。您应该会看到一个新的 RDS 层已添加到您的堆栈中,这一次名为 RDS: photoalbums-failover。您会再次注意到,您有一个未连接到任何应用的 RDS 图层。单击连接到应用,这将再次将您带到应用表。在相册旁边的行中,单击编辑。

同样,您必须为您的应用设置数据源。将数据源类型从 None 更改为 RDS。当您这样做时,您应该会看到 photoalbums-failover(mysql)出现在一个下拉列表中,并且有一个数据库名称的空字段。在数据库名称字段中键入 photoalbums,然后单击右下角的保存。

运行堆栈命令

我们从数据库的快照中创建了一个新的 RDS 实例,一个新的 OpsWorks 层,并将其连接到我们的应用。您还记得,在部署的配置阶段,RDS 层的凭证被复制到我们的 EC2 实例中。为了更新我们的凭证,我们必须在实例上再次运行Configure命令,然后部署以将新凭证推送到实例。

打开导航菜单,并选择堆栈。您将返回到堆栈的详细视图。在右上角,您将看到一个标题为“运行命令”的按钮,您可以单击它。在“运行命令”视图中,您可以从命令列表中进行选择,向命令添加注释,并选择在哪些实例上运行命令。从命令下拉菜单中选择配置,如图 3-22 所示。在“注释”字段中,键入给自己的消息,如“已配置的 RDS 故障转移实例凭据”在视图的右下角,单击配置以运行命令。

A978-1-4842-0653-9_3_Fig22_HTML.jpg

图 3-22。

Running the Configure command

您将看到一个标题为运行命令 configure 的视图,在该视图中,当命令正在执行时,您可以在 EC2 实例旁边看到一个活动指示器。一旦命令完成,应用源代码根目录中的opsworks.js文件将会使用新的凭证进行更新。接下来,运行deploy命令将代码更改部署到实例中,并重启应用。

你可以想象这个工作流程有多有用。我们能够在几分钟内将应用移动到备份数据库,而无需修改任何代码。当然,这样做也意味着您的应用要在您要换出的数据库上运行几分钟,因此,如果您的数据库包含时间敏感的信息,这在理想情况下不是您需要使用的过程。

看待该特性的另一种方式是,它为您提供了两种执行数据库维护的方式。如果您必须增加数据库存储、转换存储类型或进行一些其他重大配置更改,您的应用可能会出现一些中断。一方面,您可以对现有数据库进行更改(总是先拍摄快照),并让 AWS 使用多 AZ 特性在维护过程中自动使用故障转移数据库。

或者,您可以创建一个快照,并使用您想要的配置更改从快照创建一个新的数据库。然后,您必须将数据库实例添加到 OpsWorks 堆栈中,然后运行Configuredeploy命令。

既然本教程已经完成,让我们回到原来的数据库。在 OpsWorks 中,导航到图层并单击 RDS 图层的标题。在右上角,单击“取消注册”,并确认从堆栈中分离数据库。再次从层屏幕,点击+层。选择 RDS 选项卡,并从下面的 RDS 实例列表中选择相册。重新输入数据库凭据,然后单击向堆栈注册。当您返回“层”屏幕时,再次点按“连接应用”,然后点按应用服务器旁边的“编辑”。向下滚动到数据源,并将数据源类型更改为 RDS。选择原始数据库实例和数据库名称。单击保存。

从导航菜单中选择堆栈,然后在右上角单击运行命令。将命令更改为 Configure,然后单击右下角的按钮。最后,导航到应用,然后单击相册旁边的部署。然后单击右下角的 Deploy,让部署运行。现在,我们连接回了原始数据库,如果需要,您可以删除故障转移。

弹性负载平衡(ELB)

此时,我们的应用运行在一个 EC2 实例上。我们知道,虽然我们的实例可以扩展到更大的规模,但这可能不足以处理应用的传入流量。此外,更改 EC2 实例的大小会导致服务中断,这是我们希望避免的。

如果您曾经广泛使用 Node.js,您会知道许多服务器端错误会使您的应用崩溃,这需要重新启动应用。虽然 OpsWorks 会自动重启你的应用,但一个错误就让你的整个应用离线,哪怕只有几秒钟,都是一种耻辱。

这两个问题的解决方案是在多个实例上运行我们的应用。用 AWS 术语来说,这可以通过负载平衡器或弹性负载平衡(ELB)来实现。借助 ELB,流量可以在多个 EC2 实例之间和多个可用性区域之间自动重定向。如果一个实例崩溃,或者处于所谓的不健康状态,ELB 将停止向该实例发送流量,直到它恢复健康。这种自动化的便利性不能被夸大。如果一个实例变得不健康,您不必担心手动将其从堆栈中删除,以免流量被路由到处于错误状态的服务器。

在图 3-23 中,您可以看到负载平衡器将如何在我们的应用堆栈中工作。流量将被路由到 ELB,然后它将流量定向到各个实例,并将响应转发回请求者。

A978-1-4842-0653-9_3_Fig23_HTML.gif

图 3-23。

An ELB routing traffic to three EC2 instances Note

ELB 还可以用来调节应用两层之间的内部流量,而不仅仅是 Web 和应用服务器之间的流量。

此外,使用 ELB 为我们提供了另一种评估应用健康状况的有用方法。我将在稍后讨论监控,但目前,我们可以对云中的服务器健康问题进行概念性分析。如果您的应用堆栈中有 10 台服务器,那么随时监控所有实例的健康状况将是一项相当艰巨的任务。如果一个实例离线,是否会立即引起重大关注?如果你在一百台服务器上运行这个应用会怎么样?如您所见,随着移动部件的增多,需要分析的信息也越来越多。我们可以监视 ELB 的各种指标,而不是单独监视每个实例。

创建负载平衡器

ELB 在 AWS 中没有自己的控制面板。相反,它们在 EC2 的一个子部分中进行管理。返回 AWS 控制台并导航到 EC2。在左侧导航中的网络和安全标题下,单击负载平衡器。然后,在屏幕顶部,单击创建负载平衡器。创建负载平衡器向导将在一个模态窗口中打开,如图 3-24 所示。

A978-1-4842-0653-9_3_Fig24_HTML.jpg

图 3-24。

Create Load Balancer wizard

定义负载平衡器

在向导的第一个屏幕上,您可以将负载平衡器命名为 photoalbums-elb。“创建 LB 内部”下拉列表可以保留为 EC2-Classic。只有在虚拟专用云中创建 ELB 时,才需要更改此字段。接下来,您必须打开端口,负载平衡器可以通过这些端口处理流量。默认情况下,端口 80 在选择 HTTP 协议的情况下打开。如果您还记得,这也是您的应用正在侦听的端口。您可以离开并继续,然后单击“继续”继续。

配置运行状况检查

之前,您第一次听到健康和不健康这两个术语来描述实例。这些都不是经验性的术语,事实上,要由我们来定义。在下一步中,我们将确定 EC2 实例在什么条件下可以被认为是健康的,什么时候可以被认为是不健康的。ELB 将对分配给它的每个实例执行常规运行状况检查。

我们将为这些健康检查定义参数,这将包括 ELB 将从每个实例请求的相对路径。您可以在图 3-25 中看到健康检查参数。

A978-1-4842-0653-9_3_Fig25_HTML.jpg

图 3-25。

Configuring an ELB health check

首先,我们配置 URL 请求,或 ping。Ping 协议应该是 HTTP,Ping 端口应该是 80,和我们的 app 监听的一样。最后,Ping 路径是在每个实例上发送请求的路径。将此更改为/,这将请求我们应用的 Hello World 页面。/index.html的默认值将从我们的应用返回一个 404 错误。

高级细节是运行状况检查的更主观的参数。首先,我们有响应超时。每次对 EC2 实例执行 pinged 操作时,负载平衡器都会测量从实例接收响应所需的时间。如果实例响应缓慢或完全没有响应,它将无法通过一次运行状况检查。默认响应超时是五秒。对于我们的 Hello World 页面,预计它的生成时间不到五秒钟似乎是合理的。事实上,我们可能会进一步减少,但 5 是安全的。

接下来,我们必须确定运行状况检查的执行频率,或者运行状况检查的时间间隔。默认值为 30 秒,这意味着当实例在线时,运行状况检查将每 30 秒执行一次。

一个健康的实例可能偶尔无法通过一次健康检查。由于多种因素,它可能会响应缓慢。我们没有必要因为一个缓慢的响应就从池中删除一个实例。相反,我们希望设置一个不健康的阈值,在这个点上,实例实际上被认为处于不健康状态。默认情况下,该阈值为 2,健康阈值为 10。这些阈值如此不同的原因是为了谨慎起见。一旦一个实例变得不健康,我们不想过早地认为它是健康的。如果负载平衡器过早地认为实例是健康的,就会有更多用户收到错误响应或根本没有响应的风险。

最后,我们的健康检查规则是这样的:

  • 确定实例不正常最多需要 70 秒。
    • 五秒响应超时,每 30 秒两次
  • 确定实例再次正常运行需要 350 秒。
    • 五秒响应超时,每 30 秒,十次

当然,这些只是默认值。如果您想更积极地响应某个实例的缓慢性能,您可以改为每十秒运行一次健康检查。您还可以在五次而不是十次成功检查后,决定将实例安全地放回池中。决定您的策略是一门独立的科学,由您的应用的性质、您的应用堆栈的架构以及您的应用的操作历史决定,这意味着随着时间的推移评估您的堆栈的性能可以帮助您做出更好的决策。

单击继续进入下一个屏幕,在这里您将向负载平衡器添加实例。

添加 EC2 实例

现在,我们只添加我们之前创建的单个 EC2 实例。选择实例,并确保启用了“启用跨区域负载平衡”。如果启用,您的负载平衡器可以将流量路由到多个可用性区域中的实例。我们已经讨论了跨可用性区域分配资源的优势,因此我们肯定希望跨不同可用性区域中的 EC2 实例平衡流量。单击继续并继续添加标签步骤。

添加标签

到目前为止,您可能已经意识到,您可以在 AWS 中非常快速地积累大量资源,其中一些是由 AWS 自动命名的,另一些则遵循您自己的命名约定。很难跟踪所有的移动部件。AWS 帮助缓解这个问题的一个方法是通过使用标签。标签只是可以在 AWS 中为许多资源创建的键值对。

Note

可标记资源的完整列表可在 http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#tag-restrictions 获得。

每个资源,无论是 EC2 还是 RDS 实例,等等。,最多可以有十个标签。这可以帮助您组织您的资源,并使您的账单更易于组织。例如,如果您在 AWS 帐户上为多个客户端托管基础设施,那么您可以按照客户端名称或客户端项目来标记您的资产,从而为每个客户端生成计费摘要。

也许您正在运行一个生产和开发堆栈,并且您想要确定为长期开发周期生成额外开发环境的成本。您可以用“环境”=“生产”来标记您的生产资源,用“环境”=“开发”来标记您的开发资源,以预测额外开发环境的成本。

这只是几个例子。虽然 AWS 计费管理超出了本书的范围,但是最好解释一下标签是什么,而不是完全忽略它们。为了熟悉它们,让我们给我们的 ELB 添加两个标签。在第一个 tag 字段中,键入 Environment 作为键,Production 作为值,如图 3-26 所示。然后单击 Create Tag 并添加值为 Photoalbums 的密钥堆栈。单击继续进入查看步骤。

A978-1-4842-0653-9_3_Fig26_HTML.jpg

图 3-26。

Add ELB tags

回顾

Amazon 方便地允许我们在创建负载平衡器之前回顾所有的选择(见图 3-27 )。花一点时间检查所有的东西,确保它看起来是正确的。如果你犯了一个错误,有一个编辑按钮,可以带你回到每个单独的步骤来解决问题。完成审阅后,单击创建。几秒钟后,您将看到一条确认消息,表明您的负载平衡器已创建,当您关闭它时,您将在一个表中看到您的负载平衡器。

A978-1-4842-0653-9_3_Fig27_HTML.jpg

图 3-27。

Load balancer details

当您选择负载平衡器时,列表下方会出现一个选项卡式详细视图,如图 3-27 所示。“描述”选项卡提供了负载平衡器的概述。在前面和中间,您会看到一个 Status 字段,它会快速告诉您有多少实例在使用中。它应该显示为“服务中的 1 个实例中的 1 个”如果您的实例变得不健康,这将从 1 变为 0,要点是服务中是健康的委婉说法。您还可以重新访问您对负载平衡器所做的任何定制。您可以添加/编辑标签,打开/关闭端口,以及更改运行状况检查的配置。

您还可以在“实例”选项卡中管理连接到负载平衡器的实例。但是,我们不会在这里这样做。如果我们要在 OpsWorks 堆栈中使用负载平衡器,我们不希望在 EC2 仪表板中手动添加/删除负载平衡器的实例。相反,我们希望像处理数据库一样处理负载平衡:在 OpsWorks 中添加一个 ELB 层。

OpsWorks ELB 层

返回 OpsWorks 并打开相册堆栈。使用导航菜单,转到 Layers 视图并选择 Node.js 应用服务器层。在层子导航中,选择网络。在顶部,您应该会看到一个弹性负载平衡标题,在它下面,您会看到没有添加 ELB。单击添加 ELB 将 ELB 附加到应用服务器图层。

页面将刷新,弹性负载平衡器字段现在将显示一个下拉列表,您可以从中选择 photoalbums-elb。当您这样做时,将出现一个警告,通知您 OpsWorks 配置现在将取代在 EC2 仪表板中所做的更改。这意味着如果您返回 EC2 并添加更多实例,这些更改将被忽略,因为 OpsWorks 命令将优先。单击保存提交您的更改,这将再次重新加载页面。

打开导航菜单并返回到层视图。你会看到一个 ELB 层已经自动生成,现在如图 3-28 所示。

A978-1-4842-0653-9_3_Fig28_HTML.jpg

图 3-28。

ELB, App Server, and RDS layers

点击 ELB 层(ELB:相册-elb),你会看到该层的详细视图(见图 3-29 )。首先,您将在 DNS 名称旁边看到一个公共 URL。如果你点击这个,你将被带到我们应用的 Hello World 页面!您还会看到一条警告,提示您的所有实例都在一个可用性区域中。如前所述,最佳实践是在其他可用性区域中复制资源。在这种情况下,这是一个有争议的问题,因为我们现在在应用服务器层只有一个实例。

A978-1-4842-0653-9_3_Fig29_HTML.jpg

图 3-29。

ELB layer view in OpsWorks

在页面的底部,您将看到一个表,其中列出了向负载平衡器注册的所有实例,这些实例按可用性区域组织,每个实例的右侧都有状态指示器。因为我们在一个可用性区域中只有一个实例(在本例中是 us-east-1a),所以这里没有太多信息。但是,一旦我们添加了更多的实例和可用性区域,这一部分将为您提供应用服务器状态的鸟瞰图,以及评估性能问题的起点。

Note

在过去的几分钟里,我们已经看到 EC2 实例被描述为健康的、服务中的和正在服务中的。这些都是同一个意思的变体。

到目前为止,我们已经通过应用服务器层中 EC2 实例的静态 IP 地址访问了我们的应用。这只是我们应用发展过程中的一个临时步骤,因为我们永远不希望用户在使用我们的应用时能够控制他们连接到哪个实例。接下来,我们将向堆栈中添加一个实例,并通过负载平衡器与两个实例进行交互。

添加新实例

打开导航菜单,单击实例返回实例视图。单击左下角的+Instance 按钮打开实例创建对话框。您可以创建一个新实例或将现有实例添加到该层。后一种情况不允许您在 AWS 帐户中添加任何实例。相反,您只能添加已经属于 OpsWorks 层的现有实例。如果您愿意,您可以在多个层之间共享计算资源,并在一个共享实例上托管两个或更多层。因为我们的堆栈只有一个应用服务器层,所以我们不会探索这个选项,但知道它在那里是很好的。

将新实例大小更改为 t1.micro(或任何较小的大小),并选择不同于您为第一个实例选择的可用性区域(参见图 3-30 )。然后单击添加实例。

A978-1-4842-0653-9_3_Fig30_HTML.jpg

图 3-30。

Adding a new instance to a layer

现在你会在实例表中看到你的新实例,如图 3-31 所示。它不会自动启动,也不会分配 IP 地址,因为公共 IP 仅在实例联机时保留。

A978-1-4842-0653-9_3_Fig31_HTML.jpg

图 3-31。

App Server Instances overview

既然应用中有多个实例,这个视图开始变得更加有用。您可以看到左上角的圆圈实际上是一个圆形图,描述了处于五种可能状态之一的实例的百分比。您还可以快速链接到活动实例的公共 IP 以及 ELB。您可以启动或删除停止的实例,并通过 SSH 停止或连接到在线实例。

如果您回想一下关于 RDS 的课程,您可能会认为在将源代码部署到新实例之前必须运行Configure命令。事实并非如此。因为在连接 RDS 实例之后,您已经在堆栈上运行了Configure命令,所以将部署到新实例的凭证是最新的。你所要做的就是点击开始。

Note

有时,AWS 不允许在特定的可用性区域中分配新的实例。如果 us-east-1b 已满,并且您遇到此错误,只需选择 us-east-1c 或 us-east-1d 即可。

您的实例将在该视图中自动运行多个状态,从 requested 开始,到 online 结束。单击相册-elb 导航回负载平衡器图层。关于您的实例位于单个可用性区域的警告应该已经消失了,现在您应该在底部看到第二列,列出了您在 us-east-1b 中的实例。单击 DNS 名称旁边的 URL,您将看到 Hello World 页面在一个新窗口中打开。您现在直接连接到您的负载平衡器,它将请求路由到您在单独的可用性区域中的两个实例!

既然我们已经走了这么远,让我们继续前进,踢踢轮胎。您应该能够通过向您的负载平衡器 URL 上的注册路径提交一个POST请求来注册一个新用户。打开您的 REST 客户端,粘贴 ELB DNS 名称,添加/users/register。添加用户名、密码和电子邮件POST参数并发送请求。您应该会收到以下响应:

{"message":"Registration successful!"}

您的用户似乎已经成功注册,并且在 RDS 实例上创建了记录。如果你还记得第一章中关于示例应用的一些细节,你会记得在应用中有一个列出注册用户的 API。在您的 REST 客户端中,更改为一个GET请求,并从 URL 中删除register,或者在您的浏览器中打开该 URL。您应该会收到注册用户的 JSON 列表,如下所示:

[{"username":"admin","userID":1}]

摘要

在这一章里,我已经讲了很多内容。您了解了 Amazon RDS 的一些重要特性,创建了一个实例,并从该实例创建了一个 OpsWorks 层。然后,我们将 OpsWorks 配置为在我们的应用服务器实例上打包 RDS 凭证,并修改了访问这些凭证的源代码,而不是使用存储在我们的存储库中的硬编码凭证。我们还运行了几个场景,在这些场景中,我们可以使用 RDS 和 OpsWorks 的功能来妥善处理数据库故障或快速部署备份数据库。

您还了解了弹性负载平衡,这是亚马逊的负载平衡服务。我们创建了第一个负载平衡器,并配置了健康检查来识别不健康的 EC2 实例,并调节健康实例的流量。我们使用这个负载平衡器在 OpsWorks 中生成一个 ELB 层,并在 ELB 的多个可用性区域中添加实例。最后,我们通过在 ELB 的公共 URL 注册一个用户来测试我们的进展。

在某些方面,这是最困难的一章,因为我们终于创建了一个真正基于云的应用,不依赖于单一资源。当然,虽然我们在两个实例上运行我们的应用,但这并不意味着它有很强的容错能力,也不意味着它可以供公众使用。在接下来的几章中,您将了解如何赋予这种基础架构弹性,通过扩展我们的资源来满足需求,从而响应繁忙的流量。您还将了解更多关于我们的应用堆栈中的各种指标和故障点的信息,并了解如何使用其他 AWS 服务来提供缓存和加速内容分发,以减少我们的应用服务器和 RDS 层的工作负载。

四、CloudFront 和 DNS 管理

到目前为止,我们已经使用了很多 AWS 服务:EC2、RDS、ELB、IAM 和 OpsWorks。在这一章中,我们将在我们的清单中增加两个服务:CloudFront 和 Route 53。在这些课程中,我们将实施亚马逊的全球内容缓存工具,并在顶级网络域发布我们的应用。到本章结束时,该应用将几乎为黄金时间做好准备。

在前面的章节中,我已经讨论了地理上的接近对应用性能的影响,如果我们用毫秒来衡量的话。为了针对全球用户优化我们的应用,理论上我们可以在每个 AWS 区域运行我们整个应用堆栈的副本。然而,这将非常昂贵,并且会分散我们的资源。如果我们没有无限的预算,我们按地区划分资源越多,我们在每个地区的资源就越少。在这种情况下,我们的应用将失去一些弹性和可伸缩性。

幸运的是,我们可以使用 CloudFront 来加速我们在全球的内容分发。CloudFront 服务在全球各地的数据中心(称为边缘位置)存储我们网络内容的副本。当从 CloudFront 请求一个 URL 时,用户的请求被定向到离请求的地理起点最近的边缘位置。这并不意味着 CloudFront 只能提供静态内容。对 CloudFront 的请求可以传递到我们的应用,并在必要的情况下接收未缓存的内容,例如当用户登录并接收身份验证令牌时。

正如您可能已经注意到的,对于分配给 EC2 实例的公共 IP,AWS 中的 IP 和 URL 是动态的,如果我们希望我们的应用栈在我们选择的域中是活动的,这可能会带来挑战。您不希望将您的域指向您的一个实例的 IP 地址,也不希望将请求直接路由到 ELB 实例。相反,我们将使用名为 Route 53 的服务,它是 AWS 的 DNS 管理器。

使用 Route 53,您可以创建一个与您的域相对应的托管区域,并将各种子域映射到不同的 AWS 服务。您将收到 AWS 名称服务器,您可以指向您的域,然后在 Route 53 中配置您的 DNS 记录。

我们将结合使用这些服务,使我们的应用在我们选择的领域全球可用。Route 53 将把对我们域名的请求发送到 CloudFront,CloudFront 将在离我们用户最近的边缘位置提供我们的应用内容。在图 4-1 中,您可以看到这些服务是如何与我们的应用堆栈一起工作的。

A978-1-4842-0653-9_4_Fig1_HTML.gif

图 4-1。

The request route, from Route 53 to EC2 instances in the application stack

要完成本章,您必须已经注册了一个域,或者在课程中准备好这样做。为了完成这一课,我们将把cloudyeyes.net称为域——一个我已经注册的任意域。

内容推送服务

在 web 应用中实现缓存有许多不同的方法,CloudFront 只是其中一种。使用 CloudFront 将允许我们缓存和提供独立于应用 EC2 实例的静态资产,以及来自应用的缓存响应数据。其结果是一种缓存机制,可以极大地减少流量对应用的影响。

在本章中,我们将设置 CloudFront 作为应用服务器层请求的通道。要做到这一点,我们必须定义规则,通过这些规则,对 CloudFront 的 URL 请求被处理并路由到我们的应用堆栈。在第五章中,我们将设置一个 S3 桶来存储我们所有的图像和其他静态资产。然后,我们将使用 CloudFront 来服务这些资产,只需几个步骤就能有效地创建企业级内容交付网络(CDN)。

CloudFront 允许你在应用中使用 CDN 1 的功能,完全独立于你的应用层。虽然 CDN 通常只用于图像和其他资产,但我们将使用 CloudFront 向用户交付整个应用,并使用我们定义的缓存规则。

Note

在 CloudFront 术语中,发出 HTTP 请求的最终用户被称为查看者。

创建发行版

CloudFront 规则被组织成所谓的分布。首先,我们将为我们的应用创建一个发行版,然后我们将基于各种 URL 模式定义规则。重新登录 AWS 控制台,并在主仪表板上的存储和内容交付标题下选择 CloudFront。像往常一样,创建按钮位于主内容区域的左上角(见图 4-2 )。单击创建发行版开始创建您的发行版。

A978-1-4842-0653-9_4_Fig2_HTML.jpg

图 4-2。

The top of the CloudFront dashboard, with the Create Distribution button

AWS 将发行版创建呈现为一个两步过程。实际上,这些步骤是完全不平衡的。您首先选择一种交付方法,然后在第二步中经历一个漫长的配置过程。

当您选择配送方式时,您可以选择网上或 RTMP。RTMP 是一种 Adobe 流媒体协议,可用于音频或视频流。其他内容都在 web distribution 下,因此您可以单击 Web Distribution 标题下的 Get Started 继续。

配置分发

接下来,您将配置分布,并定义默认的源和缓存行为。完成这个过程的唯一方法就是一个一个地检查设置。如果您迷路或犯了错误,您随时可以在以后编辑配置。

原点设置

您将通过定义原点设置开始配置,其中我们为我们的分布配置原点(参见图 4-3 )。用 CloudFront 的术语来说,origin 就是 CloudFront 提供的内容的来源(origin)。CloudFront 发行版可以提供来自多个来源的内容,我们将充分利用这一特性。

A978-1-4842-0653-9_4_Fig3_HTML.jpg

图 4-3。

Origin Settings in a CloudFront web distribution

当您单击 Origin Domain Name 字段时,将出现一个下拉列表,列出您的 AWS 帐户上的所有 S3 存储桶和 HTTP 服务器。在我们的例子中,唯一有效的 HTTP 服务器是我们的弹性负载平衡器,但是高级用户也可以将 CloudFront 用于托管在单个 EC2 实例上的网站。选择我们的负载平衡器 photoalbums-elb-[id]。us-east-1.elb.amazonaws.com,从下拉菜单。Origin ID 只是一个显示在 CloudFront 中的字符串标识符,它将自动生成类似 elb-相册-elb-[id]的内容。让它保持原样,除非你想给它起个聪明的名字。

将原始协议策略设置为匹配查看器,这意味着如果查看器尝试 HTTPS 连接,CloudFront 将接受它们。我们目前没有使用 HTTPS,但以后会,我们不希望 CloudFront 干扰设置。或者,如果您只想允许 HTTP 连接,您可以在 CloudFront 级别控制它。最后,您可以更改默认的 HTTP:80 和 HTTPS:443 端口。您应该还记得,我们的应用正在侦听端口 80,所以我们肯定不想更改这个值。

默认缓存行为设置

当您创建一个发行版时,您基于 URL 路径模式定义缓存规则,我将更详细地讨论这一点。这些规则被称为行为。当我们定义行为时,我们也建立了一个将请求与我们的行为规则进行比较的顺序。因此,请求会按顺序与每个行为进行比较,直到它与某个行为匹配。如果请求与我们声明的任何行为都不匹配,那么 CloudFront 需要一个默认行为来应用于请求。因此,当我们创建 CloudFront 发行版时,我们将定义默认行为(参见图 4-4 ),任何其他行为都可以在事后定义。

A978-1-4842-0653-9_4_Fig4_HTML.jpg

图 4-4。

Distribution Default Cache Behavior Settings

行为之间的主要区别是路径模式,即匹配查看者请求的 URL 路径语法。对于您识别的每个路径模式,您可以配置一组唯一的缓存规则。您可以在表 4-1 中找到一些路径模式示例列表。

表 4-1。

A Few Examples of Valid Path Patterns

| 路径模式 | 说明 | | --- | --- | | `/users/login` | 仅适用于`/users/login`的规则 | | `/users/*` | 用于`/users/`目录中所有其他请求的规则 | | `*.jpg` | 所有`.jpg`文件的规则 | | 默认(*) | 默认规则 |

你会注意到表 4-1 中的第一个路径模式是最具体的,而最后一个模式是最通用的。这是有意的,因为我们将以同样的方式实现缓存行为。从列表顶部开始,按降序检查模式。因此,*的默认值必须是最后一个规则。

这也意味着默认路径模式是不可协商的。例如,如果您的默认值是某个值,比如/users/*,那么请求可能与您定义的任何路径模式都不匹配。这样一来,CloudFront 就不知道如何满足请求,也就无法为用户生成响应。因此,您将看到您无法更改路径模式,因此我们将继续查看器协议策略。在行为级别,您可以确定查看者是只能通过 HTTP 和 HTTPS 或 HTTPS 访问内容,还是将 HTTP 请求重定向到 HTTPS。现在,我们可以保留默认设置,HTTP 和 HTTPS。

接下来,我们用允许的 HTTP 方法设置选择我们想要允许的 HTTP 方法。可能有些行为或整个应用是只读的。在这些情况下,您将只允许 GET,HEAD 或 GET,HEAD 选项。在我们的例子中,我们必须决定是允许所有方法,然后将它们限制在只读路由中,还是默认为只读,并在特定路由中启用POST。让我们采用后一种方法,因为很少有端点不是只读的。因此,我们将选择 GET,HEAD。

下一个字段是缓存的 HTTP 方法。您可能会注意到,无论您在前面的字段中允许什么 HTTP 方法,您都不能选择缓存PUTPOSTPATCHDELETE。这对你来说应该有些意义。例如,如果带有用户登录信息的 HTTP POST请求为该特定用户生成了一个身份验证令牌,我们不希望响应被 CloudFront 缓存。如果是这样的话,就会有将缓存的错误信息返回给下一个用户的风险。在任何情况下,该字段都会根据您之前的选择进行更新,所以我们现在不能更改那里的值。

接下来,当 CloudFront 收到请求时,您必须决定是否启用转发头。如果您选择 None,那么您的缓存将会显著提高——CloudFront 在决定是提供响应的缓存副本还是从应用服务器请求新副本时,将会忽略消息头。

如果出于某种原因,您需要解析应用中的 HTTP 请求头,您可以选择 All 或 Whitelist。当您选择白名单时,您可以手动选择与您的应用相关的单个请求头,这将在下一节中详细介绍,但是如果您不感兴趣,可以跳过它。

SCENARIO: WHITE-LISTING HEADERS

为了便于讨论,让我们假设我们想要捕获将访问者链接到我们的应用的网页。我们通常可以在Referrer HTTP 头中找到这些信息。如果我们没有从 CloudFront 转发标头,我们就无法访问这些信息,所以我们希望将那个特定的标头列入白名单。

从 Forward Headers 下拉列表中选择 Whitelist,将会出现一个类似下图的界面,带有一个滚动的请求头列表。您会注意到,在标题列表上方有一个文本框,除了从预设中进行选择之外,您还可以在其中输入一个自定义标题,以防您想要在应用中使用自己的请求标题(请参见下图)。

A978-1-4842-0653-9_4_Figa_HTML.jpg

向下滚动标题列表以查找推荐人,然后单击添加> >。当您完成创建这个 CloudFront 发行版时,Referrer头在所有通过 CloudFront 运行的请求中都将是动态的,而其他头将是静态的。

在浏览该列表时,您可能会注意到一些不熟悉的标题:

  • CloudFront-Forwarded-Proto
  • CloudFront-Is-Mobile-Viewer
  • CloudFront-Is-Desktop-Viewer
  • CloudFront-Is-Tablet-Viewer
  • CloudFront-Viewer-Country

这些是 CloudFront 根据自己的内部逻辑添加到请求中的头。例如,前面的三个标题可以帮助您确定查看者是在移动设备、平板电脑还是桌面设备上。AWS 维护自己的内部设备列表,并检查User-Agent HTTP 头,将其与设备列表进行比较,并相应地生成这些头。如果您正在考虑使用自己的设备列表来识别移动用户,CloudFront 可以帮您完成这项工作!你可以在 AWS 的博客中找到更多关于这些标题的文档: http://aws.amazon.com/blogs/aws/enhanced-cloudfront-customization/

现在回到主要的课程…

我们将选择 None,不向我们的应用转发任何 HTTP 头。接下来的两个字段,对象缓存和最小 TTL,也是耦合的。在这里,我们必须决定是以编程方式管理缓存响应的到期时间,还是希望在 CloudFront 中这样做。在前一种情况下,我们将选择使用原始缓存头,我们将不得不用 ExpressJS 手动设置我们的头。如果您希望将响应缓存 60 秒,那么在将响应发送回用户之前,您应该添加以下行:

res.set('Cache-Control', 'public, max-age=60'); // cache for up to 60 seconds

但是,您也可以使用 CloudFront 来设置这个头,方法是为您的对象缓存选择 Customize,然后将您的最小 TTL 设置为 60。CloudFront 中默认的max-age是 24 小时,供参考。我们将选择后者,并将我们的自定义 TTL 设置为 60 秒。

接下来,我们有一些额外的属性可以从 CloudFront 转发到我们的应用服务器。首先是转发 Cookies。同样,我们的选项是 None(为了更好的缓存)、Whitelist 和 All。我们现在将选择 None,因为我们目前没有在我们的应用中使用 cookies。

向前查询字符串是一个简单的是/否选择。我们可以选择否,因为这是我们默认的行为;然而,我们保证在我们定义的其他行为中需要查询字符串。重要的是,任何解析查询字符串的请求都有相应的 CloudFront 行为来转发它们。

SCENARIO: FORWARDING QUERY STRINGS

假设我们没有转发查询字符串,/user路由接受一个名为idGET参数,并基于这个参数返回关于用户的信息。第一个用户登录到我们的应用并发送一个对/user?id=1的请求。她收到了她请求的信息,CloudFront 为/user创建了一个缓存对象。然后第二个用户登录并发送一个/user?id=2请求。CloudFront 忽略查询字符串,为/user寻找缓存对象。它找到为/user?id=1缓存的响应,并发送它以响应第二个用户的请求。

看到发生了什么?如果查询字符串影响了输出,并且我们没有转发它们,那么/user?id=1/user?id=2被缓存为同一个对象,错误的响应被发送给我们的用户。第二个用户得到了错误的数据,因为我们在应该转发查询字符串的时候没有转发!

因为我们还没有设置用户认证,所以我们不需要保存 cookies。暂时将转发 Cookies 设置为无。将来,我们将不得不允许对某些行为使用 cookies。Smooth Streaming 同样可以设置为 No。这是由 Microsoft 创建的一个 HTTP 协议,它可以针对客户端的带宽实时优化流媒体。这在我们的应用中没有多大用处,不是吗?

本节的最后一部分是使用签名的 URL 来限制查看者访问的选项。您也可以将其设置为 No。您可以启用此功能,以便通过 CloudFront 提供私人内容。如果您要这样做,您必须自己管理已签名的 URL,这本身就是一项重要的任务。

分发设置

既然您已经为您的发行版配置了默认行为,那么是时候配置发行版本身了(参见图 4-5 )。首先,我们必须选择一个价格等级,它通常会影响服务的价格和性能。

A978-1-4842-0653-9_4_Fig5_HTML.jpg

图 4-5。

CloudFront Distribution Settings

价格等级

与 EC2 或 RDS 不同,我们不选择具有内置计算能力的实例。相反,定价基于传输的数据量和激活的区域。对于我们启用了 CloudFront 的每个地区,我们支付数据传输入(从 CloudFront 到分发源)和出(到互联网)的速率,以及每个地区的 HTTP 和 HTTPS 请求的数量。

更令人困惑的是,随着数据量的增加,每个地区的数据传输(到互联网)价格会降低。价格层是企业级的,因此第一个价格层包括每月高达 10TB 的容量。对于按请求数量定价,费率为每 10,000。您可以在 http://aws.amazon.com/cloudfront/pricing/ 找到当前汇率的完整明细。

用一个简单的例子来总结定价,如果您只为美国和欧洲启用了 CloudFront,您的定价公式如下:

  • 美国数据输入+欧洲数据输入+美国数据输出+欧洲数据输出+(美国 HTTP 请求/10,000) +(美国 HTTPS 请求/10,000) +(欧洲 HTTP 请求/10,000) +(欧洲 HTTPS 请求/10,000)

尽管定价复杂,但您的 CloudFront 账单可能比 EC2 或 RDS 账单低一个数量级。如果您只期望美国和欧洲的用户,您可以将您的 CloudFront 发行版限制在这些地区。您还可以将发行范围限制在美国、欧洲和亚洲。但是我们将选择使用所有边缘位置。

备用域名

在这个字段中,您最多可以输入 100 个 CNAMEs,通过这些 CNAMEs 可以访问您的 CloudFront 发行版。这是您将在应用中使用域的第一个地方。您将在此处输入,包括www但不包括http://

SSL 证书

如果我们有 SSL 证书,我们将在这里配置它。我们现在将跳过这一部分,选择默认的 CloudFront 证书。

默认根对象

当查看者仅在浏览器中输入域时,您可以使用此字段指定索引文件的路径。在我们的例子中,您会记得我们已经在 ExpressJS 中设置了这个相同的函数。当用户请求“/”路径时,我们的 Hello World 页面被提供。例如,如果我们使用 CloudFront 来提供一个普通的 HTML 文件,我们会将index.html设置为默认的根对象。因此,我们不必使用该功能。

接下来的几个字段与为 CloudFront 生成日志有关。我们将在后面的课程中进一步研究日志。我们暂时将日志设置为关闭。还有一个评论字段,纯粹是内部使用。如果需要的话,你可以在这里给你自己/你的团队留言。最后,分布状态是整个分布的开/关开关。将该值设置为 Enabled,并检查您的选择,以确保一切配置正确。

分发设置—摘要

Origin Settings

Origin Domain Name photoalbums-elb-[id].us-east-1.elb.amazonaws.com

Origin ID ELB-photoalbums-elb-[id]

Origin Protocol Policy Match Viewer

HTTP Port 80

HTTPS Port 443

Default Cache Behavior Settings

Viewer Protocol Policy HTTP & HTTPS

Allowed HTTP Methods GET, HEAD

Forward Headers None

Object Caching Use Origin Cache Headers

Forward Cookies None

Forward Query String No

Smooth Streaming No

Restrict Viewer Access No

Distribution Settings

Price Class Use All Edge Locations

Alternate Domain Nameswww.[yourdomain].com

SSL Certificate Default CloudFront Certificate

Default Root Object (blank)

Logging Off

Comment (blank)

Distribution State Enabled

最后,单击创建发行版。您将返回到 CloudFront 发行版视图,您的发行版将出现在表中,状态为“进行中”。创建您的发行版需要几分钟时间。但是,在创建发行版时,您仍然可以访问它。

分布明细视图

单击分发的 ID 以进入分发详细信息视图。它看起来应该如图 4-6 所示。

A978-1-4842-0653-9_4_Fig6_HTML.jpg

图 4-6。

Distribution detail view

您会注意到,CloudFront 的组织与我们使用的其他服务略有不同。在左边的列中,您将看到辅助导航主要用于访问指标和报告。这与 OpsWorks、IAM 和 EC2 仪表板中建立的模式有些冲突,在这些仪表板中,左侧的导航允许您深入到服务的子部分。

在 CloudFront 中,可以在主内容区域的选项卡式视图中访问发行版子部分。您从常规选项卡(图 4-7 )开始,在这里您可以看到刚刚创建的分布设置。请注意分发状态,它可以告诉您对分发的最新更改是已经传播(已部署),还是仍在生效(正在进行)。“起源”标签列出了该分布的所有起源。“行为”选项卡允许您根据请求的 URL 路径定义查看器请求的行为。错误页允许您向查看者显示自定义错误页和 HTTP 状态代码。“限制”选项卡允许您创建访问内容的地理限制,并且您可以使用“失效”选项卡手动清除 CloudFront 缓存。

A978-1-4842-0653-9_4_Fig7_HTML.jpg

图 4-7。

Distribution origins

起源

点击原点选项卡,如图 4-7 所示。在 Origins 表中,您会发现在创建这个分布时创建了一个单一的原点。你现在唯一的来源是相册负载平衡器。这意味着对 CloudFront 实例的请求只能转发给负载均衡器,而不能转发给其他源。将来,我们将在这里添加第二个原点。虽然一个来源可以包含在多个分配中,但是您必须为每个分配中的来源创建一个记录。

Note

你现在可能意识到这里有很大的潜力。您可以创建多个 OpsWorks 堆栈,每个堆栈都有自己的负载平衡器,并在单个域下提供来自多个应用堆栈的内容!

行为

下一个选项卡如图 4-8 所示,显示了该分布的行为。当您创建发行版时,您也创建了默认行为。每个分布必须总是至少有一个行为。您会注意到,如果您选择默认行为,您可以编辑它,但不能删除它。

A978-1-4842-0653-9_4_Fig8_HTML.jpg

图 4-8。

Behaviors

如果您回忆一下我们为默认行为选择的设置,您会记得不允许使用POST HTTP 方法。然而,我们确实需要POST来进行用户注册、登录/注销,以及创建照片和相册。因此,我们将根据具体情况确定和创建其他行为。我们立即面临一个选择:我们是否应该为每条允许POST的路线创建一个行为?或者我们应该让群体行为变得更加宽容而不那么专一?先说/users/。让我们重温一下我们在/users/*路径中定义的路线。

GET /users/

POST /users/login

POST /users/logout

POST /users/register

GET /users/user/:user

我们知道我们必须允许/users/login/users/register/users/logout使用POST。那么,我们似乎可以考虑用/users/*的路径模式将这些逻辑地组合在一个行为中。但是当我们设计我们的行为时,还有其他规则要考虑。我们必须接受带有这些路径的查询字符串吗?/users/只有两条GET路由,都不接受GET参数。饼干呢?我们还没有建立我们的身份验证,但可以肯定地说,我们可能需要它们。

看起来这里的路线有足够的共同点来证明创建一个行为是正确的。点按左上方的“创建行为”按钮。您将看到一个看起来很熟悉的创建行为视图。我们看到这个视图嵌套在更大的 Create Distribution 视图中。

在 Path Pattern 字段中,输入/users/*,以捕获/users/路径中的所有请求。我们要明确的是,这不仅仅包括像/users/login这样的 URLs 它将拦截任何文件类型的请求。如果您的查看器请求/users/profile.jpg/users/profile.txt,它仍然会被这个行为处理,除非列表中更早的行为先捕捉到它。

将允许的 HTTP 方法更改为 GET、HEAD、OPTIONS、PUT、POST、PATCH、DELETE。然后,将转发 Cookies 更改为全部。您可以将其余设置保留为默认值。确保一切看起来像图 4-9 中的选择,然后点击创建。

A978-1-4842-0653-9_4_Fig9_HTML.jpg

图 4-9。

Create behavior for /users/*

接下来,我们将对/albums/条路线进行同样的处理。相册功能非常有限,包括以下内容:

GET /albums/id/:albumID

POST /albums/upload

POST /albums/delete

好像和/users/挺像的。同样,不需要转发查询字符串,但是我们希望稍后转发 cookies。再次点按“创建行为”,并在“路径模式”栏中输入/albums/*。将允许的 HTTP 方法更改为 GET、HEAD、OPTIONS、PUT、POST、PATCH、DELETE。然后,将转发 Cookies 更改为全部。您可以将其余设置保留为默认值。确保一切正常,然后单击创建。

/photos/路线同样与/users/非常相似。/photos/定义的路线如下:

GET /photos/id/:photoID

POST / photos /upload

POST / photos /delete

让我们用同样的规则创建这个行为。再次单击“创建行为”,并在“路径模式”字段中输入/photos/*。将允许的 HTTP 方法更改为 GET、HEAD、OPTIONS、PUT、POST、PATCH、DELETE。然后,将转发 Cookies 更改为全部。您可以将其余设置保留为默认值。确保一切正常,然后单击创建。

如果您现在查看行为表,您会注意到您可以方便地在表中看到来源、查看器协议策略和转发的查询字符串值,这只是为了更容易地管理您的行为。

再看一下行为表,如图 4-10 所示。您将会看到,尽管默认行为是首先创建的,但在表格中,其余行为会按照创建的顺序出现在它的上方。这是因为当 CloudFront 接收到一个 HTTP 请求时,将按照从列表顶部开始递减的顺序检查请求中的每个行为。“优先级”字段从 1 开始向上编号,表示行为的比较顺序。默认行为是最通用的,应该是请求所要比较的最后一个行为。目前,我们的其他行为之间没有重叠,因此它们的顺序并不太重要。如果有的话,您会希望更具体的路径在顶部,优先级更高,而更一般的路径在底部。通过选择行为,单击上移或下移,然后保存,可以更改行为优先级。

A978-1-4842-0653-9_4_Fig10_HTML.jpg

图 4-10。

Updated Behaviors table

查询字符串的行为

让我们继续添加一个允许查询字符串的行为。但是首先,我们必须在应用中实际创建路线。在我们添加行为之前,我们将最终返回到代码编辑器,并为 Photoalbums 应用添加一些新功能。我们没有花太多时间讨论示例应用的更大目标,因为它是一个公认的基本应用。但是我们可以尝试添加一些对这个应用和一般的 web 应用开发有用的小功能。

查询字符串(或GET参数)的一个常见用途是通过一个或多个参数搜索或过滤内容。为照片添加一个基本的搜索功能会很有用,这样用户就可以在照片中搜索标题字段中的匹配文本。打开/routes/photos.js,找到/id/:id路线和/upload路线之间的空间。将清单 4-1 中的代码粘贴到这里。

Listing 4-1. The /photos/search Route

/* GET photo search */

router.get('/search', function(req, res) {

if(req.param('query')){

var params = {

query : req.param('query')

}

model.getPhotosSearch(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid photo search'});

} else {

res.send(obj);

}

});

} else {

res.status(400).send({error: 'No search term found'});

}

});

正如您所看到的,这个路由只是在查询参数中接受一个字符串,并将其传递给model.getPhotosSearch。如果缺少查询,则会返回一个错误。控制器逻辑的结构类似于我们创建的其他路由的结构。我们不直接将GET参数传递到模型中。相反,我们构造了一个params对象,在其上我们可以执行任何我们需要的额外操作。例如,如果我们想从搜索查询中过滤掉脏话,我们可以很容易地用这个模式插入这个功能。

接下来,我们必须在模型中添加getPhotosSearch函数。导航至/lib/models/model-photos.js。在getPhotosByAlbumID函数下面,粘贴清单 4-2 中的代码。

Listing 4-2. getPhotosSearch Function in model-photos.js

function getPhotosSearch(params, callback){

var query = 'SELECT photoID, caption, albumID, userID FROM photos WHERE caption LIKE "%' +

params.query + '%"';

connection.query(query, function(err, rows, fields){

if(err){

callback(err);

} else {

if(rows.length > 0){

callback(null, rows);

} else {

callback(null, []);

}

}

});

}

该函数也遵循与我们创建的其他模型函数相同的模式。我们选择几个照片字段,并使用 SQL 操作符LIKE根据caption的值过滤照片。我们运行查询并将结果返回给控制器。我们必须确保通过在文件底部添加下面一行来公开这个函数:

exports.getPhotosSearch = getPhotosSearch;

部署代码更改

我们已经准备好将我们的代码更改推送到我们的应用堆栈中。如果本地数据库还在运行,您也可以先在本地进行测试。将您的更改提交到代码库中。在 AWS 控制台中,导航回 OpsWorks。点按相册堆栈。打开导航菜单,然后单击应用。单击部署按钮返回到部署应用视图。在“注释”栏中,为“添加的照片搜索方法”添加注释,然后点按“部署”。

添加新行为

导航回 CloudFront 并单击您的发行版。打开行为选项卡,然后再次单击创建分布。这一次,我们为一个特定的路径创建一个行为,所以我们将把它直接输入到路径模式:/photos/search。这一次,我们将允许的 HTTP 方法设置为默认的 GET,HEAD。同样,Forward Cookies 可以设置为 None,因为这是一个不需要身份验证的公共方法。将转发查询字符串设置为是,然后单击创建。

当您返回到“行为”表时,您会看到新行为位于列表的第四位,优先级为 4。这是行不通的,因为请求在到达这里之前会被/photos/*行为捕获。选择新行为并点按“上移”,然后点按“存储”。你的行为表现在应该如图 4-11 所示。

A978-1-4842-0653-9_4_Fig11_HTML.jpg

图 4-11。

Behaviors table with new behavior

随着代码的部署和行为的到位,我们应该准备好测试了。我们第一次测试托管在 AWS 上的代码时,我们直接访问了 EC2 实例。接下来的测试,我们在负载平衡器的 URL 上运行我们的请求。这一次,我们在实例和用户之间添加了另一个中间层,我们将使用 CloudFront URL。

返回“常规”选项卡,找到域名。应该是像[unique-identifier].cloudfront.net这样的格式。在浏览器中打开这个 URL,您应该会看到 Hello World 页面。现在,让我们创建一些照片,以确保我们的行为正常工作。在我们上传照片之前,我们需要一个userID和一个albumID。如果您之前创建了一个用户,您可以在/user/:username获得他/她的 ID(但是最有可能的是userID将是 1)。如果还没有,那么打开 REST 客户机,用下面的参数向/users/register发出一个POST请求:usernameemailpassword

当你有了你的userID,用userIDtitle的参数制作一个POST/albums/upload,创建一个相册。您应该会收到对您请求的响应中的albumID。接下来,我们可以创建照片对象(是的,仍然没有文件上传附加到它们)。用你的userIDalbumID和标题“Hello World”向/photos/upload发出一个POST请求。如果您得到一个 ID 作为响应,那么您的照片就创建成功了(当然,您也可以从状态代码 200 中看出这一点)。为了测试搜索,我们需要一些照片。再次发出标题为“你好,芝加哥”的请求,然后发出标题为“再见,纽约”的第三个请求

到目前为止,我们有三张照片,这足以测试一些搜索。在 REST 客户机或浏览器中,请求路径/photos/search?query=Hello。您应该会看到两个照片条目:“Hello World”和“Hello Chicago”将您的请求更改为/photos/search?query=Hello%20World,您应该只会看到一个条目。

贮藏

现在是时候看看 CloudFront 的缓存了。向/photos/upload发出另一个POST请求,这次标题为“Hello London”然后,向/photos/search?query=Hello发出另一个GET请求。响应应该快如闪电,但是如果你仔细看 JSON,你将看不到你最近的照片,“你好,伦敦。”这是因为在第一次请求之后,CloudFront 现在为地址/photos/search?query=Hello存储了一个缓存对象。它将继续对该 URL 的所有请求发送相同的响应,直到对象过期。但是什么时候到期呢?

存储在缓存中的每个对象都链接到一个行为,其到期时间由相应行为中几个字段的值决定,如图 4-12 所示。

A978-1-4842-0653-9_4_Fig12_HTML.jpg

图 4-12。

Important CloudFront behavior fields when determining object caching

决定一个对象在缓存中停留多长时间有两个基本因素:最小 TTL 和 HTTP 请求头。如图 4-12 所示,/photos/search的行为采用使用原始缓存头来确定对象的缓存。通过这种设置,我们必须以编程方式控制响应的缓存。如果我们将设置改为 Customize,我们可以指定一个对象在缓存中停留的秒数,在这种情况下,CloudFront 将覆盖我们的应用发送的任何Cache-Control:max-age头。然而,CloudFront 的默认过期时间是 24 小时,如果我们不努力,它不会在更短的时间内缓存一个对象。

FORWARDED QUERY STRINGS

重要的是要注意其他设置如何影响缓存。因为我们正在转发查询字符串,这意味着它们被纳入对象在 CloudFront 中的存储方式。这意味着/photos/search?query=hello/photos/search?query=goodbye作为不同的对象存储在缓存中。如果不是,两次搜索可能会产生相同的结果。

测试这一点的一个简单方法是通过向请求添加一个唯一的查询字符串来强制刷新结果。例如,/photos/search?query=hello&time=4815162332/photos/search?query=hello&time=4211138不同,它将迫使 CloudFront 为原点检索一个新的响应。

您应该会在搜索请求中看到如下代码所示的标题:

Request Header

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

Accept-Encoding: gzip,deflate,sdch

Accept-Language: en-US,en;q=0.8

Cache-Control: max-age=0

Connection: keep-alive

Host: d23xpp2aiwzqtf.cloudfront.net

If-None-Match: W/"fe-4203691681"

Response Header

Connection: keep-alive

Date: Mon, 24 Nov 2014 23:51:35 GMT

ETag: W/"180-2935506378"

X-Powered-By: Express

Age: 1196 X-Cache: Hit from cloudfront

Via: 1.1 f519cbbbbf1657343dde8ed4d32a9966.cloudfront.net (CloudFront)

X-Amz-Cf-Id: l1UWAownAiSOIHoV0XGuw5dHo3Rt_9P0Cx5eqCL-Dqus4BijxF-oWg==

正如您所看到的,请求中的Cache-Control头被设置为max-age=0,但是这个头明显地没有出现在响应中。虽然您可能认为这意味着 CloudFront 必须刷新对象,但实际上并非如此。如前所述,默认的最小 TTL 为 0 实际上意味着 24 小时,并且在没有源覆盖该指令的情况下,最小 TTL 优先。在我们当前的场景中,浏览器只会缓存响应 0 秒,但是 CloudFront 会将对象保留在缓存中 24 小时。

对于我们的行为,我们面临着一个古老的问题:我们的反应要缓存多久。一方面,缓存我们的结果将减少我们的应用堆栈的工作负载,因为请求可以完全由 CloudFront 处理,而不会加重应用层或数据库的负担。另一方面,用户或客户对企业应用的期望通常意味着我们必须提供近乎即时的结果。我们的代码更改只需要几分钟就可以完成和部署;然而,首先,我们必须从 CloudFront 中移除缓存的对象。

无效

不幸的是,我们不能简单地部署我们的代码并再次测试它。在 CloudFront 的缓存中已经有关于这些 URL 的对象,所以 CloudFront 将继续使用旧的响应头提供响应。这让我们想到了 CloudFront 发行版的另一个特性:失效。

失效本质上是一个从 CloudFront edge 缓存中删除对象的命令。您不能简单地清除浏览器中的缓存,因为失效必须全局发送到 CloudFront 缓存。因为 CloudFront 将您的内容缓存在全球各地的数据中心,所以需要一些时间来撤销它。

乍一看,您可能会想,“为什么我不能在内容更新时以编程方式动态地使我的缓存无效,而在其他情况下对我的响应使用最大缓存?”理论上,这个概念是有意义的:在 CloudFront 中存储所有响应的缓存副本,只有当响应的内容发生变化时才生成新的副本。虽然这听起来不错,但不幸的是,CloudFront 的失效远不是瞬间的。虽然您可以使用 AWS SDK 以编程方式使您的缓存失效(实际上,我们在控制台中所做的一切都可以以编程方式完成),但是您很快就会发现,在您创建失效和操作完成之间有很大的延迟。

在 CloudFront 中,再次打开您的发行版并单击 Invalidations 选项卡。您将看到一个空的无效表。点击顶部的创建失效按钮,将打开一个模态文本区,如图 4-13 所示。添加两条要使其无效的路径,用换行符隔开,如下所示:

A978-1-4842-0653-9_4_Fig13_HTML.jpg

图 4-13。

Creating a CloudFront invalidation

/albums/id/1

/photos/search?query=Hello

继续并点击无效按钮。您将看到您的失效出现在表格中,状态为“进行中”。更改需要几分钟才能生效,此时状态将变为“已完成”。您还会注意到,每个失效都有一个惟一的 ID 和一个时间戳。现在我们可以修改代码了,一旦代码部署完毕,我们就可以开始测试了。请注意,我们只是在推送修复之前使缓存无效,以节省时间,因为没有其他人在使用该应用。在生产设置中,您应该在使缓存失效之前部署您的更改。

控制缓存

让我们做两个改变,反映两个不同的场景。首先,我们希望照片搜索结果是即时的。让我们指示 CloudFront 永远不要缓存结果。当然,随着用户群的扩大,这将是一个问题,但至少我们知道如何去做。其次,我们将使用 Expires 请求头让请求路由在特定的时间间隔到期。

在第一个场景中,我们将配置请求头,这样浏览器和 CloudFront 都不会试图缓存响应。在代码编辑器中,导航到/routes/photos.js。找到/search rout 的处理程序,并将下面一行粘贴到函数的开头:

res.header('Cache-Control', 'no-cache, no-store');

这应该很简单,因为我们只是使用 ExpressJS 语法将一个键值对传递给响应头。它属于函数的开头,以避免复制粘贴。概括地说,该函数应该类似于清单 4-3 。

Listing 4-3. /photos/search with Cache-Control Header

router.get('/search', function(req, res) {

res.header('Cache-Control', 'no-cache, no-store');

if(req.param('query')){

var params = {

query : req.param('query')

}

model.getPhotosSearch(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid photo search'});

} else {

res.send(obj);

}

});

} else {

res.status(400).send({error: 'No search term found'});

}

});

接下来,我们将添加第二个用例。假设按 ID 请求相册时的响应可以被 CloudFront 缓存十秒。这将使响应保持最新,而不会像完全禁用缓存那样加重服务器的负担。打开/routes/albums.js并找到/id/:id路线的处理程序。在函数的顶部,添加以下两行:。

res.header("Cache-Control", "public, max-age=10");

res.header("Expires", new Date(Date.now() + 10).toUTCString());

您的处理程序应该类似于清单 4-4 。

Listing 4-4. /albums/id/:albumID with Cache-Control Header

router.get('/id/:albumID', function(req, res) {

res.header("Cache-Control", "public, max-age=10");

res.header("Expires", new Date(Date.now() + 10000).toUTCString());

if(req.param('albumID')){

var params = {

albumID : req.param('albumID')

}

model.getAlbumByID(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid album ID'});

} else {

res.send(obj);

}

});

} else {

res.status(400).send({error: 'Invalid album ID'});

}

});

响应接受当前日期,并在其上加上十秒(以毫秒为单位),然后设置标头。需要注意的一点是,这十秒钟并不准确,因为在发送响应之前,从模型中检索数据仍然需要时间。如果你想让它尽可能接近完美,那就在回调中为model.getAlbumByID设置头。

你可能想知道为什么标题被设置在顶部。还有其他可能的响应,例如当 URL 中缺少相册 ID 时,或者当没有为所提供的 ID 找到相册时,会发送 400 错误,后者可能是由某种数据库错误引起的。CloudFront 可能会缓存用户看到的错误响应,从而导致这个错误错误地显示给其他用户。在这种情况下,CloudFront 可能会因为不必要地延长客户端的错误而适得其反。因此,最好让所有响应都包含 Expires 标头。您可以在每次从这个路由发送响应时设置标题,但是这只会使列表看起来更混乱。毕竟,如果一个代码样本太杂乱,那么它又有什么用呢?

无论如何,继续将您的更改提交到您的代码库中。在 AWS 控制台中,导航回 OpsWorks。点按相册堆栈。打开导航菜单,然后点按“应用”。单击部署按钮返回到部署应用视图。在“注释”字段中,添加“添加的缓存控制头”的注释,然后单击“部署”。给 OpsWorks 几分钟时间来推进你的代码。

测试 CloudFront 缓存

部署完成后,让我们先测试相册路径。在您的浏览器中向/albums/id/1发出GET请求(确保清除您的本地缓存)或 REST 客户端。看一下响应标题。它们看起来应该类似于清单 4-5 。

Listing 4-5. New and Improved, Ten-Second Cache Response Headers

Response Header

Cache-Control:public, max-age=10

Connection:keep-alive

Content-Length:631

Content-Type:application/json; charset=utf-8

Date:Wed, 26 Nov 2014 01:29:02 GMT

ETag:W/"277-3646801943"

Expires:Wed, 26 Nov 2014 01:29:02 GMT

Via:1.1 a2c541774483a4b9c153c3cb7c7a7753.cloudfront.net (CloudFront)

X-Amz-Cf-Id:pcxqj03svkFItzzQ3KWi4OK5jJf4eGXs91PCQLjv2liWf9f7iP-KaQ==

X-Cache:Miss from cloudfront

X-Powered-By:Express

响应标头的重要部分以粗体显示。首先,您可以看到我们添加的 Cache-Control 头逐字出现。Expires 标头应该在当前时间后大约十秒钟出现,以适应时区差异。你还会看到一个我们没有添加的头,X-Cache。第一次,这可能是“cloudfront 小姐。”快速连续地发出几个请求,您将看到“来自 cloudfront 的点击”这个头通知您 CloudFront 是提供了一个缓存的响应(命中)还是必须从源位置检索一个新的响应(未命中)。

但是,您的浏览器或 REST 客户端也可能符合缓存头,缓存 X-Cache 头,因此您可能看不到预期的结果。如果是这种情况,您必须使用 cURL 进行测试。打开您的命令行界面(终端),并键入以下命令:

curl –I http://[cloudfront-id].cloudfront.net/albums/id/1

如果您从 CloudFront 收到一个带有 miss 的响应头,请多运行几次该命令。在第二次或第三次请求时,您应该会收到一个命中结果。

您还会注意到 X-Amz-Cf-Id 头。您可能已经推断出这是请求的 CloudFront ID。如果在 CloudFront 中启用日志记录,这是 CloudFront 收到的每个请求的惟一 ID。如果您在调试 CloudFront 问题时需要向 AWS 寻求支持,它可能会询问您遇到问题的 X- Amz-Cf-Id请求。

接下来,让我们测试一下照片搜索的无缓存解决方案。为了演示这一点,我们将进行搜索,上传另一张照片,然后再次运行搜索。首先,通过向/photos/search?query=New%20York发出请求来搜索带有“纽约”字样的照片。您应该会看到类似于清单 4-6 的内容。

Listing 4-6. No-Cache Response Headers and Body

Response Header

Cache-Control:no-cache, no-store

Connection:keep-alive

Content-Length:67

Content-Type:application/json; charset=utf-8

Date:Wed, 26 Nov 2014 04:23:44 GMT

ETag:W/"43-3955827999"

Via:1.1 b05dafe95c8baade280459c121e622be.cloudfront.net (CloudFront)

X-Amz-Cf-Id:zhNlH4MXzU9G7Mrb5tVgBq8qtMLlW3XONjZsmEZOmQ5MhXmCqdJxAg==

X-Cache:Miss from cloudfront

X-Powered-By:Express

Response Body

[{"photoID":3,"caption":"Goodbye New York","albumID":1,"userID":1}]

重要的标题再次被加粗。你可以在顶部看到我们的缓存控制头。我们的第一次搜索在 X 缓存头中得到一个Miss from cloudfront。这是有意义的,因为这应该是我们在缓存中使对象无效后运行的第一次搜索。现在让我们创建另一张照片并再次搜索,以确保得到我们期望的结果。

制作一个新的POST/photos/upload,使用与之前相同的相册和用户 id,标题为“Hello New York”当您得到 200 响应时,再次运行搜索查询。你的回应应该看起来像清单 4-7 ,新照片几乎立即出现在下一次搜索中。

Listing 4-7. Search Results Showing Up Instantly in the Next Request

Response Header

Cache-Control:no-cache, no-store

Connection:keep-alive

Content-Length:132

Content-Type:application/json; charset=utf-8

Date:Wed, 26 Nov 2014 04:35:58 GMT

ETag:W/"84-3303063004"

Via:1.1 2b0986af7f8d32d3d4b4cf9330702abf.cloudfront.net (CloudFront)

X-Amz-Cf-Id:KTpgTxO9XBebAzuS0MSP1f2EkrcRGfqijMFz3Fc6xGqI93TPXsnldw==

X-Cache:RefreshHit from cloudfront

X-Powered-By:Express

Response Body

[

{

"photoID":3,

"caption":"Goodbye New York",

"albumID":1,

"userID":1

},

{

"photoID":10,

"caption":"Hello New York",

"albumID":1,"userID":1

}

]

这一次,X 缓存头的值是RefreshHit from cloudfront。这意味着 CloudFront 意识到它需要刷新请求,它也确实这么做了。这正是我们想要发生的!

由于浏览器的行为,这两种场景之间的差异可能会令人困惑,因为 CloudFront 和浏览器都响应相同的 HTTP 响应头。对于/albums/id/1请求,CloudFront 和浏览器都响应头指令来缓存响应,所以浏览器通常会缓存整个响应,包括响应头。您可以通过关注 X-Amz-Cf-Id 头并观察它的变化来验证这一点。

/photos/search响应的情况下,浏览器服从 Cache-Control: no-cache,no-store 头,因此总是向 CloudFront 发出新的请求,CloudFront 又通过总是将请求转发给源来响应头。

虽然还有许多其他可能的场景,但是我们已经讨论了两个主要的缓存策略,您将从您的源以编程方式生成这两个策略。如果您发现自己不得不适应一些不寻常的场景,AWS 在 http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Expiration.html 提供了一个所有可能的源响应头用例的表格。

缓存统计

我们已经运行了一些测试,以查看 CloudFront 何时为我们的内容提供服务,以及源何时提供服务。幸运的是,很容易得到我们对象行为的统计分类。在左侧导航中,在 Reports & Analysis 标题下,您将看到许多描绘 CloudFront 性能的报告。首先,单击 Cache Statistics,这将显示一系列图表。在顶部,您会看到一系列字段,您可以使用这些字段来过滤页面上的所有图表。选择开始日期和结束日期,包括您在本章中处理的日期范围。将 Web 分发更改为您的分发,然后单击更新。

第一个图表“请求总数”不言自明。第二个图表是按结果类型划分的查看者请求百分比,如图 4-14 所示。在这里,您可以看到您的请求按命中、未命中和错误分类。有趣的是,图表中没有显示刷新次数。尽管如此,命中请求的百分比是一个很好的观察指标。这基本上告诉了你什么时候 CloudFront 正在保存你的应用,因为每次点击都是一个由 CloudFront 处理的请求。正如你在图 4-14 中看到的,当新的缓存规则被应用时,点击量急剧下降。

A978-1-4842-0653-9_4_Fig14_HTML.jpg

图 4-14。

Percentage of Viewer Requests by Result Type

请按照自己的进度随意查看其他图表。稍后,当讨论监控应用堆栈健康状况的方法时,我们将回到 CloudFront 报告。接下来,单击 CloudFront 导航中的热门对象。您将再次看到一个顶部带有过滤工具的视图。选择与您学习本章的天数相对应的日期范围,然后单击更新。您应该会看到类似于图 4-15 中的表格。

A978-1-4842-0653-9_4_Fig15_HTML.jpg

图 4-15。

Popular objects in CloudFront distribution

这是一个方便的细分,因为您可以确定用户在哪些地方点击和未点击,以及应用生成了多少可能由 CloudFront 生成的输出。

如果您正在寻找优化代码或提高 CloudFront 效率的起点,您还可以看到哪些请求收到得最多。同样重要的是,您可以看到哪些对象响应 4XX 或 5XX 错误,这是非常宝贵的诊断信息。

下一节,使用报告,也充满了有用的指标来衡量您的应用的流量。您也可以随意探索,但这并不是本课程的重点。我会在第七章讨论监控和报警,所以你现在不用复习。

GEO-RESTRICTIONS

另一个在企业层面可能有帮助的功能是地理限制,即阻止往来于世界特定地区的流量。虽然您以前可能不得不在软件级别执行这种阻止,或者依赖网络管理员来执行,但有了 CloudFront,您可以非常轻松地使用这一特性。

让我们假设我们只希望我们的应用在美国可用。返回到 CloudFront 仪表板,再次选择您的发行版。单击限制选项卡,这将带您到限制表。这里的用户界面有点滑稽。表中唯一的实际限制是地理限制。点按表格上方的“编辑”按钮。您将面临一个单一的设置:启用地理限制。嗯嗯……让我们选择是。

有两种启用地理限制的方法:白名单和黑名单。简而言之,你可以选择允许哪些国家(白名单),也可以选择不允许哪些国家(黑名单)。因为我们只希望允许一个国家,所以将美国列入白名单比将其他国家列入黑名单更符合逻辑。选择美国-美国并单击添加> >(见下图),然后单击是,编辑。

A978-1-4842-0653-9_4_Figb_HTML.jpg

您将返回到分布视图中的限制选项卡,在这里您将看到地理限制的状态为已启用,类型设置为白名单。同样,当更改生效时,您的发行版将处于“正在进行”状态。

Note

如果你想测试地理限制,你可以将你当前所在的国家列入黑名单,并尝试通过 CloudFront URL 访问你的应用。

既然我已经介绍了 CloudFront 的基本特性和用例,那么是时候继续讨论在您的应用托管在域名之前我们必须配置的最终服务了。返回 AWS 控制台并选择路线 53。

53 号公路

我们如何将我们的域指向应用?如果您以前使用过 DNS,您可能会注意到我们可以指向我们的域的几个位置:我们的一个实例的公共 IP、我们的负载平衡器的 URL 或者我们的 CloudFront 实例的 URL。这些方法都有一些限制。

首先也是最重要的,我们要坚持第一章中提出的原则:可伸缩性和弹性。显然,我们的 EC2 实例的 IP 地址是不可靠的,如果我们添加新的实例,但是我们的域只指向一个实例,我们就不能适当地伸缩。DNS 更改可能需要 72 小时才能传播。现在,我们可以对我们的配置进行任意数量的更改,并在几秒或几分钟内看到结果。无论出现什么问题,我们都希望保持这种能力。如果我们出于某种原因必须启动应用堆栈的克隆,负载平衡器地址将会改变。我们也不能肯定地说我们的 CloudFront URL 永远不会改变。有可能,我们要做好准备!

为了获得最佳结果,我们将使用路由 53 来配置我们的 DNS。我们将配置我们的域指向 AWS 名称服务器,然后我们可以添加我们的 CNAMES、A 记录、MX 记录等。在 53 号公路。我们可以将wwwapp子域用于不同的目的,或者我们可以让wwwdev子域指向各自的生产和开发应用堆栈。在这一课中,我们将简单地了解服务,并在www.[ your-domain ].com设置我们的应用堆栈。我们开始吧!

我们假设您已经预订了想要使用的域名,并且知道如何使用域名注册机构的门户网站。如果您还没有域名,实际上您可以在 53 号公路上注册一个,方法是单击左侧导航中的注册域名,然后按照步骤进行操作。然而,我们的重点将是与现有的领域。

在 Route 53 仪表板上,您将在顶部看到您的资源:0 个托管区域、0 个运行状况检查和 0 个域。一个域的记录集收集在称为托管区域的实体下。这与域名不同,域名只是您从 Route 53 注册的域名。

单击左上角的托管区域,然后单击顶部的创建托管区域。这一次,创建工具将出现在屏幕右侧的一个容器中,而不是将您带到一个新的视图或呈现一个模态弹出窗口(看,每个 AWS 服务都有一个微妙的独特界面)。在域名字段中,输入您的域名,不要包含 http://或 www。输入标记托管区域的注释,并将类型字段设置为公共托管区域(参见图 4-16 )。单击视图底部的创建。

A978-1-4842-0653-9_4_Fig16_HTML.jpg

图 4-16。

Create Hosted Zone in Route 53

您的托管区域将很快出现在表格中(您可能需要单击右上角的刷新按钮)。选择它并单击转到记录集。

如图 4-17 所示,记录集视图是一个双面板布局,左侧是您的托管区域记录,右侧是详细信息/编辑视图。一旦创建了托管区域,AWS 将自动生成四个 AWS 名称服务器地址,作为 NS(名称服务器)类型记录的值。回到您的域名注册机构,将您的域名服务器依次更改为这些地址。如您所知,DNS 更改传播可能需要 72 小时。你还会看到一个 SOA,权威的开始,记录。你很可能不需要接触它,但它是域名注册的必要组成部分。

A978-1-4842-0653-9_4_Fig17_HTML.jpg

图 4-17。

Record sets

我们将假设您已经准备好继续前进,同时您的域更改正在传播。下一个任务是将请求从您的域路由到 CloudFront。为此,我们将创建一个 A 记录并将其指向我们的www子域。

在屏幕顶部,单击创建记录集。您将看到屏幕右侧的面板会更新您需要的界面元素,以便创建一个记录集。在名称字段中,输入要为其创建记录的子域。在字段中输入 www。Type 字段应设置为–IP v4 地址,因此只有在情况并非如此时才更改该字段的值。

下一个名为“别名”的字段有许多附属选项。术语“别名”是“AWS 资源别名”的简写您实际上是在选择是要直接链接到您创建的 AWS 资源,还是要手动配置它。如果选择“否”,则可以设置 TTL 并直接在值字段中输入 IP 地址。选择 Yes,您将看到界面发生了变化。现在,系统会提示您输入别名目标的名称。如果您单击该字段,将出现一个下拉列表,列出您所有符合条件的 AWS 资源,如图 4-18 所示。您应该可以看到您的负载平衡器和您的 CloudFront 发行版。

A978-1-4842-0653-9_4_Fig18_HTML.jpg

图 4-18。

Selecting your record set alias target

从下拉列表中选择您的 CloudFront 发行版,您会看到字段旁边出现一个黄色的警告图标。此图标提醒您在 CloudFront 发行版中设置备用域。幸运的是,我们之前做过,所以我们不需要担心。

接下来,是我们的子域的路由策略。我们将把它设置为简单,但是最好知道您可以用这个策略做什么。通过利用路由策略,您可以为同一个子域创建多个记录,这些记录协同工作以将用户路由到最佳的 AWS 资源。虽然这方面的教程超出了本课的范围,但我将讨论路由策略可能有用的几种情况。

设想一个场景,您希望在每个 AWS 区域中建立一个完整的应用堆栈,而不是使用 CloudFront。首先,您将克隆您的应用堆栈,并为不同的区域配置不同的堆栈。然后,您将前往 Route 53 并使用路由策略创建一个 www 记录集:Geolocation。您将选择默认,并创建您的记录集。然后,您可以为每个洲创建一个 www 记录,这样总共有八个 www 记录。然后,您可以将每个堆栈指向离该大陆最近的负载平衡器(可能会有一些重复,比如南极洲就没有任何 AWS 数据中心)。

在另一个场景中,假设您只想在主堆栈性能不佳时准备好一个备份应用堆栈来处理请求。首先,您将在 OpsWorks 中克隆您的应用堆栈。然后,您将使用路由策略创建两个记录集:故障转移。一个记录集是故障转移记录类型:主要,另一个是故障转移记录类型:次要。当然,您需要一些指标来确定请求被路由到备份堆栈的点。您将在您的子域上创建一个健康检查,并确定用于确定健康状况的参数。然后,当您的应用堆栈运行缓慢或遭遇中断时,Route 53 会自动将流量路由到备份堆栈。

这只是几个例子,但是您可以看到使用 Route 53 保存 DNS 记录的效用。继续操作并单击 Create,使用简单的路由策略完成记录集的构建。您的记录集应该立即出现,类型列为 A(别名),如图 4-19 所示。一旦您给了您的 DNS 更改传播时间,您应该最终能够访问您的域中的 Hello World 页面!

A978-1-4842-0653-9_4_Fig19_HTML.jpg

图 4-19。

ALIAS record created

摘要

在这一章中,我们达到了使我们的应用在万维网上可访问的重要里程碑。这并不容易,但是到目前为止,我们在发布网络应用方面已经取得了巨大的进步。通过使用 CloudFront,我们通过缓存和加速内容交付优化了我们的应用性能。然而,有几个主要部分不见了。

首先,我们有一个照片分享应用,不接受任何照片上传!我们将在下一章中添加这一功能,但重要的是我们首先要设置 CloudFront,因为我们在编写代码时会考虑到 CloudFront。

记住我们试图坚持的基本原则也很重要:可伸缩性和弹性。虽然我们拥有一些难以置信的资源,但我们还没有真正实现这些。您将在后面的课程中处理应用健康和监控问题,但重要的是要记住,路的尽头并不是让我们的应用上线。不管真正的目标是什么,都要保持我们的应用在线。

Footnotes 1

有关 CDN 和性能的有用讨论,请参见 www.webperformancetoday.com/2013/06/12/11-faqs-content-delivery-networks-cdn-web-performance/

五、简单的存储服务和内容交付

既然我们的应用已经在 Web 上运行,是时候构建一些核心功能了:图片上传。人们会认为我们可以从一开始就这样做,但事实并非如此。虽然我们已经使用了许多 AWS 服务,但只编写了很少的代码,这个功能是一个例外。我们正在基于我们的 AWS 架构构建我们的照片上传和查看功能。为此,我们将首次尝试使用 AWS SDK 以编程方式直接与 AWS 服务进行交互。

我们将在控制台中做一些工作,然后快速将 SDK 添加到我们的应用包中,并开始编码。我们将不得不创建一个 S3(简单存储服务) 1 桶,这个 AWS 服务旨在为静态资产提供文件存储。我们还必须配置 S3 存储桶和相应的 IAM 策略。

如果我们的应用开始运行,我们预计会有成千上万的文件上传和下载。如果我们将这些文件存储在 EC2 实例上,将会产生大量的问题。首先,我们的介质所消耗的巨大磁盘空间要求我们扩大实例存储容量。事实上,每个实例都需要每个映像的副本,因此这种冗余会造成大量资源浪费。其次,如果我们的实例负责向用户发送图像,纯粹是在检索和发送所有内容所使用的内存中,这可能会导致严重的瓶颈。

第三,这将产生一个主要的同步问题。存储在 EC2 实例上的所有数据都是短暂的,持久数据存储在我们的 RDS 数据库中。如果一个用户上传一张照片到一个实例,就会产生一种情况,即图像必须被复制到所有其他正在运行的实例。简而言之,使用实例来上传文件是个坏主意。

在这一章中,我们将通过使用 S3 存储静态内容来避开这些问题。因为 S3 提供了无限文件存储的高可用性和冗余性,所以我们可以将所有静态内容保存在一个地方。我们将修改我们的应用,以便它上传图像到 S3 桶。有了 AWS SDK,我们将能够以编程方式上传文件,设置它们的权限,并从公共 URL 访问它们。然后,我们将使用 CloudFront 将图像分发给我们的用户。

在应用中使用 S3

使用 S3 进行静态内容存储将省去我们前面描述的许多麻烦,并显著减轻应用的整体重量。此外,如果我们不得不更换实例、堆栈或数据库,我们可以依靠 S3 保持独立于我们的应用堆栈的服务。虽然 S3 存储桶是在特定区域创建的,但它们在该区域内是冗余的,从而最大限度地降低了可用性区域中断带来的问题风险。

当然,由于 S3 存储桶是在特定地区创建的,因此图像有可能存储在远离用户的地方。这就是 CloudFront 可以帮助我们的地方,它将我们的文件副本存储在边缘位置。为了最大化 CloudFront 的有效性,我们将在我们的文件命名中使用版本控制,并让我们的图像在 CloudFront 中有较长的生命周期。这两种服务将协同工作,作为通常所说的内容交付网络,或 CDN。

注意图 5-1 。这是我们之前看到的系统图的更新。这一次,它已被更新,以反映 S3 桶将发挥的新作用。虽然 CloudFront 收到的许多请求将被路由到负载均衡器,但对媒体的请求将完全绕过应用堆栈,转而到达 S3 存储桶。同样,我们的应用堆栈中的 EC2 实例将能够直接连接到 S3 存储桶,向其传输文件。

A978-1-4842-0653-9_5_Fig1_HTML.gif

图 5-1。

Our system, updated for this lesson

创建 S3 存储桶

首先,我们将在 AWS 控制台中创建一个 S3 存储桶。登录并导航到 S3。如果你以前没有来过这里,它有点类似于 Route 53 界面:左边是铲斗列表,右边是详细视图。点按左上角的“创建存储桶”按钮。将出现一个模态视图,提示您命名存储桶并选择一个区域。给你的桶起个名字,比如相册-cdn。

Note

S3 标识符是唯一的,因此您不能将您的存储桶命名为与前面的文本完全相同。尝试一些类似的也能反映你喜好的东西。

从地区下拉列表中,选择美国标准(地区名称不符合我们习惯的美国东、美国西的惯例),如图 5-2 所示。

A978-1-4842-0653-9_5_Fig2_HTML.jpg

图 5-2。

Selecting a bucket name and region

现在,您可以选择立即创建存储桶或为存储桶设置日志记录。对于生产应用,您可能会发现日志记录很有用,所以现在让我们启用它。单击设置日志记录➤.在下一个视图中,选择您的存储桶作为存储日志的目标存储桶(参见图 5-3 )。保持其他字段不变,然后单击“创建”。

A978-1-4842-0653-9_5_Fig3_HTML.jpg

图 5-3。

Setting up logging for your bucket

S3 桶本身很容易交互。每个存储桶都包含自己的目录结构。您可以根据需要创建任意多的目录,并根据需要对它们进行深度嵌套。就文件管理而言,在控制台中与 S3 存储桶进行交互有一些限制。您不能将文件从一个目录移动到另一个目录;你必须下载并重新上传到新的目录。除非先删除目录或存储桶的内容,否则不能删除目录或存储桶。相反,AWS 建议您在开始设计系统时就确定文件的生命周期。例如,如果您想在 30 天后删除日志文件,最好现在就确定并相应地配置存储桶的行为。但这本身就是一个完整的话题。

现在我们已经创建了我们的 bucket,我们需要一个地方来存储用户上传的照片。让我们简单地将这些文件存储在一个/uploads目录中。单击您的存储桶以访问存储桶详细信息视图。和以前一样,您将在左侧看到一个列表(这次是目录和文件的列表),在右侧看到属性/其他详细信息。

在左上角,点按“创建文件夹”按钮。一个未命名的文件夹将出现在列表中,其名称可以编辑,就像在 Finder for Mac 或 Windows Explorer for Windows 中一样。继续输入上传的名字。你现在应该在你的桶中看到两个目录:logsuploads(见图 5-4 )。

A978-1-4842-0653-9_5_Fig4_HTML.jpg

图 5-4。

Bucket contents list

在 IAM 中启用 S3 访问

到目前为止,我们需要转向身份和访问管理来配置我们的应用,以便将资产上传到我们的 S3 存储桶,这应该不足为奇。在第一章中,有一个可选课程,您可能已经创建了一个 IAM 用户,该用户具有从 S3 存储桶进行读/写的权限。我们再次需要类似的功能,尽管这里有不止一种可能的方法。

存储凭据

主要的问题不是我们是否可以通过 AWS SDK 让我们的实例访问 S3 桶,而是当我们这样做时,我们如何管理凭证。根据 Amazon 的说法,在代码中使用凭证时,有一个最佳实践的层次结构。在 JavaScript SDK 文档( http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/node-configuring.html )中,亚马逊按如下顺序提供了他们的建议:

Loaded from IAM roles for Amazon EC2 (if running on EC2),   Loaded from the shared credentials file ( ∼/.aws/credentials ),   Loaded from environment variables,   Loaded from a JSON file on disk,   Hardcoded in your application

前三个建议是特定于 AWS 环境的,而后两个是更常见的、与平台无关的技术。虽然我们希望遵循亚马逊的建议,但有一个问题是,这将削弱我们在当地环境中的开发能力。这不是亚马逊独有的缺陷,许多平台即服务(PaaS)提供商都会遇到。

第一个建议是“从 Amazon EC2 的 IAM 角色加载”,实现起来并不难。作为首选和最简单的建议,我们将很快实施这一策略。然而,我们希望在本地开发环境中保留尽可能多的功能。没有办法在您的本地环境中模拟 IAM EC2 角色,因此我们必须采用不同的方法来实现本地开发。但是,我们可以创建一个 IAM 用户,并在本地环境中通过 AWS SDK 使用其凭证。为此,我们也将支持第四项建议。

实施 IAM 角色

让我们首先实现 IAM 角色方法。首先,让我们回到 OpsWorks 来唤起你的记忆。导航至您的应用堆栈,并点击堆栈设置(参见图 5-5 )。

A978-1-4842-0653-9_5_Fig5_HTML.jpg

图 5-5。

Access application stack settings via the Stack Settings button at the top right

在 Stack Settings 视图中,您将看到我们在开始时做出的许多配置决策(参见图 5-6 )。您会发现一个名为 Default IAM instance profile 的设置,它应该设置为一个以 AWS-ops works-photo albums-ec2-role 结尾的长标识符。当我们在前面设置这个值时,我们创建了 IAM 角色,它被部署到应用堆栈中的每个 EC2 实例。这么多工作已经完成了!

A978-1-4842-0653-9_5_Fig6_HTML.jpg

图 5-6。

Stack Settings

导航到 IAM,并从左侧导航栏中选择角色。在角色列表中,选择 AWS-ops works-photo albums-ec2-role。在 Permissions 标题下,您将看到我们尚未为此角色创建任何策略。我们根本不需要。我们现在必须授予该角色对 S3 存储桶的读/写权限。为了清楚起见,我们将启用对 S3 的全局访问,但将来我们可能希望限制对特定 S3 存储桶的访问。正如你所看到的,我们再次面临对我们的实践和组织方法做出高度主观的决定。与其说是科学,不如说是艺术。

单击附加策略以转至策略选择视图。在策略表中向下滚动,直到找到 AmazonS3FullAccess 并选中它,如图 5-7 所示。

A978-1-4842-0653-9_5_Fig7_HTML.jpg

图 5-7。

Policy Template—S3 Full Access

单击右下角的应用策略。您将返回到用户详细信息视图,在该视图中,您将看到您的策略被列出(参见图 5-8 )。

A978-1-4842-0653-9_5_Fig8_HTML.jpg

图 5-8。

EC2 Service Role Permissions

就这样!现在,当我们使用 AWS SDK 时,它将自动检测 EC2 服务角色的权限,并使用这些凭证。您可以理解为什么 Amazon 推荐这种方法:凭证不会暴露给任何人,人为错误的风险可以忽略不计。

Note

稍后,我们可能希望我们的应用不仅仅拥有 S3 权限。我们将简单地返回到这个 IAM 角色,并附加额外的策略来扩展权限。

使用 IAM 用户凭据

对于我们的第二种方法,我们将创建可用于本地开发的凭证,但我们不会签入到我们的存储库中或部署到生产中。

回顾前面的 IAM 课程,我讨论过您总是可以选择创建 IAM 用户并配置其权限,或者创建组权限并将用户添加到组中。同样,我们可以选择任何一种方法。至此,我已经介绍了如何创建 IAM 用户、组和角色,因此几乎没有必要重复每一项。如果要创建 IAM 组,请为其提供适当的策略,并在该组中创建一个用户。随意这样做,锻炼你已经学到的东西。为了简洁起见,我们将简单地创建一个 IAM 用户,并将策略应用于用户本身。

在 IAM 左侧导航栏中选择“用户”,然后单击“创建新用户”。当提示输入名称时,输入 photoalbums-stack。确保选中了生成访问密钥的框,然后单击创建。在下一个屏幕上,单击下载凭据。文件下载时,保密;保管好它。然后,单击关闭返回到用户列表。

单击您的用户进入用户详细信息视图。我们有一个用户和它的凭证,现在我们只需要给它必要的权限。我们将遵循与为 IAM 角色创建策略时完全相同的步骤。在权限标题下,单击附加策略。我们回到了托管策略选择视图。向下滚动,直到找到 AmazonS3FullAccess。选择它旁边的框,然后单击“Attach Policy”按钮。您将返回到用户默认视图,在这里您将看到您的策略已被添加。

我们已经完成了这两个 IAM 方法,我们将很快在代码中使用它们。在此之前,我们必须在奥普斯沃斯做短暂停留。

添加 OpsWorks 环境变量

在第三章中,我们通过将应用层连接到数据库层,将数据库凭证从源代码中移出,放入由 OpsWorks 生成的文件中。不幸的是,我们不能用 S3 桶做同样的事情。但是,您可能还记得,我们在代码中使用了 OpsWorks 环境变量来确定应该在哪里查找这些数据库凭证。我们将使用类似的方法连接到我们的 S3 桶。我们不必将 IAM 凭证存储在环境变量中,但是我们应该为 S3 存储区名称这样做。这样做的原因很简单:这将使更改我们的应用中的 S3 桶变得容易,并且使创建新的应用堆栈变得容易。如果我们必须创建一个开发堆栈,或者出于任何原因创建一个副本,我们也可以创建一个新的 S3 桶,并在 OpsWorks 中轻松地交换名称。

在 AWS 控制台中导航到 OpsWorks 并选择您的应用堆栈。使用导航菜单,选择应用。当您看到应用列表视图时,单击相册应用旁边的编辑(参见图 5-9 )。

A978-1-4842-0653-9_5_Fig9_HTML.jpg

图 5-9。

Return to OpsWorks Apps list view

在应用编辑视图中,向下滚动到环境变量标题(参见图 5-10 )。您应该会看到已经创建的名为“环境”的变量添加一个变量,其键为 S3BUCKET,值等于您之前创建的 S3 存储桶的名称。然后点击右下角的保存。

A978-1-4842-0653-9_5_Fig10_HTML.jpg

图 5-10。

New Environment Variables

当我们重新部署我们的应用时,环境变量将是可访问的。但是在开始编码之前,我们还有一个任务,那就是更改实例的默认服务器配置。

使用 AWS SDK 进行开发

到目前为止,我们的 AWS 课程完全依赖于 AWS 控制台。这并不意味着你必须使用它,但它只是更容易学习和更容易教。事实上,我们到目前为止所做的大部分工作也可以使用 AWS SDK 以编程方式实现。在某些情况下,在 AWS 控制台中工作要快得多。在其他情况下,特别是对于您希望自动化的任务,使用 SDK 并编写您想要的行为会更有意义。

AWS SDK 有多种语言和平台,包括 JavaScript,这将是我们的选择。你可以在这里找到 AWS SDK 工具的完整列表: http://aws.amazon.com/tools/ 。安装 AWS SDK 很容易。

更新相关性

我们必须将 AWS SDK 添加到我们的应用中,以及 multer 中间件包。对于以前用过 ExpressJS 的人来说,ExpressJS 第 4 版有点不一样。几个中间件依赖项已经被移除,我们必须根据我们需要支持的功能将它们分别添加到我们的包中。我们将使用 multer 来接受文件上传并将它们写入一个临时目录。

在代码编辑器中,打开根目录中的package.json。在依赖项列表的开始,您将添加 AWS SDK Node 模块和 multer,因此您的依赖项 JSON 应该类似于清单 5-1 。

Listing 5-1. package.json Dependencies

"dependencies": {

"aws-sdk": "2.0.*",

"multer": "⁰.1.3",

"express": "∼4.8.6",

"body-parser": "∼1.6.6",

"cookie-parser": "∼1.3.2",

"mysql": "2.0.*",

"morgan": "∼1.2.3",

"serve-favicon": "∼2.0.1",

"debug": "∼1.0.4",

"jade": "∼1.5.0"

}

接下来,我们必须在本地重新安装我们的应用,以安装新的软件包。在命令行界面中,导航到项目目录并键入以下内容:

npm install

aws-sdkmulterNode 模块及其各自的依赖项应该开始下载,这将向控制台打印诸如清单 5-2 之类的内容。

Listing 5-2. AWS SDK Installing

npm http GET https://registry.npmjs.org/aws-sdk

npm http 200 https://registry.npmjs.org/aws-sdk

npm http GET https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.0.29.tgz

npm http 200 https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.0.29.tgz

npm http GET https://registry.npmjs.org/xml2js/0.2.6

npm http GET https://registry.npmjs.org/xmlbuilder/0.4.2

npm http 200 https://registry.npmjs.org/xml2js/0.2.6

npm http GET https://registry.npmjs.org/xml2js/-/xml2js-0.2.6.tgz

npm http 200 https://registry.npmjs.org/xmlbuilder/0.4.2

npm http GET https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz

npm http 200 https://registry.npmjs.org/xml2js/-/xml2js-0.2.6.tgz

npm http 200 https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz

npm http GET https://registry.npmjs.org/sax/0.4.2

npm http 200 https://registry.npmjs.org/sax/0.4.2

npm http GET https://registry.npmjs.org/sax/-/sax-0.4.2.tgz

npm http 200 https://registry.npmjs.org/sax/-/sax-0.4.2.tgz

aws-sdk@2.0.29 node_modules/aws-sdk

一旦安装完成,您就可以开始在代码中使用aws-sdk模块。下次部署代码时,它也会自动安装在 EC2 实例上。让我们开始编写上传代码吧!

首先,我们必须配置 express app 实例来使用 multer。打开/server.js,在顶部的变量声明中添加下面一行,如下所示:

var multer = require('multer');

“header”(不是真的,而是精神上的)现在看起来应该类似于清单 5-3 。

Listing 5-3. The server.js “Header”

var express = require('express');

var path = require('path');

var favicon = require('serve-favicon');

var logger = require('morgan');

var cookieParser = require('cookie-parser');

var bodyParser = require('body-parser');

var multer = require('multer');

var debug = require('debug')('photoalbums');

var routes = require('./routes/index');

var users = require('./routes/users');

var photos = require('./routes/photos');

var albums = require('./routes/albums');

var globals = require('./lib/globals');

var mysql = require('mysql');

var app = express();

接下来,我们将告诉express实例使用 multer 作为中间件,并将文件上传的目的地作为参数传入。在代码中再往下一点,您会看到一系列配置expressapp.use()语句。在app.use(bodyParser{...})之后,增加以下内容:

app.use(multer({dest: './tmp/'}));

您的app.use()块现在应该如清单 5-4 所示。

Listing 5-4. server.js Express App Configuration

app.use(logger('dev'));

app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: false }));

app.use(multer({dest: './tmp/'}));

app.use(cookieParser());

app.use(express.static(path.join(__dirname, 'public')));

就像我们在这里完成的那样,将您的更改保存到文件中。接下来,我们希望设置我们的应用来使用我们创建的 OpsWorks 环境变量。我们将使用类似的方法来访问数据库凭证。如果代码在生产环境中运行,我们将使用环境变量。否则,我们将默认使用凭据的本地副本。

访问环境变量

在本地凭证的情况下,这意味着凭证仍然会向开发人员公开。但是因为 IAM 凭证和 bucket 名称都存储在环境变量中,所以您可以很容易地为本地开发创建一个单独的 bucket 和 IAM 用户,限制该用户对单个 dev bucket 的访问。如果您对练习这种级别的安全性感兴趣,您可以自己尝试一下。

打开/lib/globals.js。我们将增加一个与database()几乎相同的功能,叫做awsVariables()。正如您所想象的,我们将再次检查ENVIRONMENT变量,如果它在那里,就使用我们创建的新变量。如果没有定义,我们将加载一个本地配置。您希望您的globals文件看起来像清单 5-5 。

Listing 5-5. Complete /lib/globals.js

module.exports = {

applicationPort  : 80,

database : function(){

if(process.env.ENVIRONMENT){

var opsworks = require('./../opsworks');

var opsWorksDB = opsworks.db;

var rdsConnection = {

host : opsWorksDB.host,

port : opsWorksDB.port,

database : opsWorksDB.database,

user : opsWorksDB.username,

password : opsWorksDB.password

};

return rdsConnection;

} else {

var local = require('./../config/local');

var localConnection = local.db;

return localConnection;

}

},

awsVariables : function(){

if(process.env.ENVIRONMENT){

var variables = {

bucket : process.env.S3BUCKET

}

return variables;

} else {

var local = require('./../config/local');

return local.awsVariables;

}

}

}

接下来,我们必须更新本地配置文件。打开/config/local.js,你将在其中添加一个awsVariables对象。这个对象的属性应该映射到/lib/globals.js中的属性,因此您的代码应该类似于清单 5-6 ,使用我们为 IAM 用户生成的密钥/秘密。虽然由您来决定如何管理这些凭证,但是请记住,如果您不愿意,您不需要将它们提交给存储库,或者您可以使用空字符串作为键和密码值来提交文件。

Listing 5-6. Local Config File

module.exports = {

db :  {

host : 'localhost',

port : 3306,

database : 'photoalbums',

user  : 'root',

password : 'root'

},

awsVariables : {

bucket : 'photoalbums-cdn',

key : 'AKIAINJDCDGH3TBMN7AA',

secret : '8RJHMIGriShsKjgs;8W3B8gIRXC/v0QXDhcVH2RwMAw'

}

}

处理文件上传

现在我们已经完成了所有的配置,可以开始编写上传代码了。幸运的是,我们不必对模型进行任何更改,尽管我们必须改变数据库模式。我们将在/routes/photos.js做出所有的改变。

这个文件中剩下的主要任务是重要的。当用户进行POST/photos/upload操作时,我们希望采取以下操作:

User input is validated (user ID, album ID, and image are required).   Image is written to /tmp folder.   Image is uploaded from /tmp folder to S3 bucket.   Image is deleted from /tmp folder.   Final image URL is generated.   Entry is created in database, including URL.   User receives success message.

在这个过程中,有许多事情可能会出错,我们希望对此有所计划:用户可能会包含无效的输入;在 EC2 实例上读取/写入映像可能有问题;或者可能无法上传到 S3 或写入数据库。坏消息是,在复杂的 Node.js 应用中正确的错误处理看起来有点混乱。好消息是,我们可以用相对较少的代码完成所有这些步骤。

首先,我们必须允许路由访问globals以及fs模块。尽管文件系统模块内置在 Node.js 中,但您必须声明它才能直接访问它。路由的顶部现在看起来如下所示:

var express = require('express');

var router = express.Router();

var model = require('./../lib/model/model-photos');

var globals = require('./../lib/globals');

var fs = require('fs');

接下来,/upload的路线需要完全重写。将其替换为清单 5-7 。

Listing 5-7. New POST/upload Route

router.post('/upload', function(req, res) {

if(req.param('albumID') && req.param('userID') && req.files.photo){

var params = {

userID : req.param('userID'),

albumID : req.param('albumID')

}

if(req.param('caption')){

params.caption = req.param('caption');

}

fs.exists(req.files.photo.path, function(exists) {

if(exists) {

params.filePath = req.files.photo.path;

var timestamp = Date.now();

params.newFilename = params.userID + '/' + params.filePath.replace('tmp/', timestamp);

uploadPhoto(params, function(err, fileObject){

if(err){

res.status(400).send({error: 'Invalid photo data'});

} else {

params.url = fileObject.url;

delete params.filePath;

delete params.newFilename;

model.createPhoto(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid photo data'});

} else {

res.send(obj);

}

});

}

});

} else {

res.status(400).send({error: 'Invalid photo data'});

}

});

} else {

res.status(400).send({error: 'Invalid photo data'});

}

});

这里发生了很多事情,让我们一步一步来。首先,验证表单数据。除了需要albumIDuserID之外,我们现在还需要提交一个名字为photo的文件。大部分代码都包装在这个条件中,如果失败,就会发送一个 HTTP 400 错误来响应请求。

像我们经常做的那样,我们接下来基于请求参数构造一个params对象。所需的albumIDuserID都包括在内,如果找到了一个标题,也包括在内。标题是可选的,我们从不通过这种方式直接访问它们。因为我们使用 multer,当一个文件被包含在POST中时,它会被自动写入到/tmp文件夹中(我们在server.js中指定)。存储在/tmp文件夹中的副本不保留其原始名称,而是分配一个随机标识符,以减轻对重复文件名的担忧。不难想象,两个从同一部智能手机上传照片的用户会有相同的图片名称。请求中包含的任何文件都会自动分配一个 path 属性,指向它们在服务器上的位置。这给我们省了不少麻烦!

接下来,我们开始使用fs模块。首先,我们使用fs.exists()来检查文件是否确实位于我们期望的路径,可以通过req.files.photo.path访问。如果在这里找不到,将向用户发送一个错误,我们的路线将被停止。如果找到了文件,那么我们将文件的路径添加到我们的params对象中。我们还创建了一个名为newFilenameparams属性,这将是文件上传到 S3 时的最终文件名。因为我们的应用同时在几个实例上运行,即使使用随机文件名,仍然有文件名冲突的可能。为了减轻这种情况,我们在文件名前添加了一个时间戳,使文件名更加独特。此外,我们还在路径中包含一个带有用户 ID 的目录。使用这些技术产生文件名冲突的可能性微乎其微。

现在我们的params对象已经准备好了,我们把它发送给uploadPhoto()方法,我们还没有检查它。如果成功,我们的图像将被写入 S3,我们的 params 对象将被分配一个url属性。最后,我们删除不再需要的params属性,并将完成的对象发送给model.createPhoto()函数。如果操作成功,我们向用户返回一个带有照片 ID 的 HTTP 200 状态。

/routes/photos.js中,向下滚动到路线的末端,但在底部的module.exports声明之前。我们将在这里添加私有函数,仅在该文件中使用。首先,我们将添加uploadPhoto()函数,如清单 5-8 所示。

Listing 5-8. uploadPhoto() Function

function uploadPhoto(params, callback){

fs.readFile(params.filePath, function (err, imgData) {

if(err){

callback(err);

} else {

var contentType = 'image/jpeg';

var uploadPath = 'uploads/' + params.newFilename;

var uploadData = {

Bucket: globals.awsVariables().bucket,

Key: uploadPath,

Body: imgData,

ACL:'public-read',

ContentType: contentType

}

putS3Object(uploadData, function(err, data){

if(err){

callback(err);

} else {

fs.unlink(params.filePath, function (err) {

if (err){

callback(err);

} else {

callback(null, {url: uploadPath});

}

});

}

});

}

});

}

首先,这个函数从/tmp目录中读取文件。然后使用来自params对象的文件名设置上传路径。使用 AWS SDK 所需的键值,构造了一个名为uploadData的对象。我们构建这个对象是为了准备将图像上传到 S3,在这一点上它将被称为一个对象。

Bucket键使用在我们的 globals 中声明的 bucket,它最终在 OpsWorks 环境变量中设置。Key就是 S3 桶中的路径。Body包含我们用fs.readFile(). ACL检索的图像数据,代表访问控制列表,代表对象在 S3 上创建时的权限。最后是ContentType,硬编码为'image/jpeg'

作为一个额外的练习,你可以动态地设置ContentType,通过用fs读取它并把它传递给params对象中的这个函数。

接下来,我们将uploadData对象传递给putS3Object()。上传完成后,使用fs.unlink()将图像从/tmp目录中移除。最后,在回调中返回 S3 对象路径。您会记得这个相对路径是传递给model.createPhoto()的路径,从那里它被写入数据库。

我们将在uploadPhoto()下面添加最后一个函数putS3Object()。这个函数(参见清单 5-9 )使用 AWS SDK 简单地处理到 S3 的上传。在/routes/photos.js中增加以下功能:

Listing 5-9. putS3Object() Function

function putS3Object(uploadData, callback){

var aws = require('aws-sdk');

if(globals.awsVariables().key){

aws.config.update({ accessKeyId: globals.awsVariables().key, secretAccessKey: globals.awsVariables().secret });

}

var s3 = new aws.S3();

s3.putObject(uploadData, function(err, data) {

if(err){

callback(err);

} else {

callback(null, data);

}

});

}

让我们一行一行地分解它。首先,加载aws-sdk。然后,我们检查globals.awsVariables().key是否被定义。您应该还记得,它只是在本地定义的,用于我们使用 IAM 用户凭证的用例。如果你不想使用这种方法,你可以完全删除这个if语句。但是如果您使用 IAM 用户获得 S3 权限,那么必须将密钥和秘密传递给aws.config.update()。如果我们转而依赖实例的 IAM 角色,那么 AWS SDK 会自动获取凭证,我们永远也不需要调用aws.config.update()

然后,我们就简单的叫s3.putObject()。如前所述,S3 存储桶的内容被含糊地称为对象,不管是什么类型。我们已经在这个函数之前构造了必要的参数,所以它很简单。

为了弄清楚这一切是如何工作的,让我们快速看一下model.createPhoto()。打开/lib/model/model-photos.js。在文件的顶部附近,您应该可以看到清单 5-10 中的代码。

Listing 5-10. Model createPhoto() Function

function createPhoto(params, callback){

var query = 'INSERT INTO photos SET ? ';

connection.query(query, params, function(err, rows, fields){

if(err){

callback(err);

} else {

var response = {

id : rows.insertId

};

callback(null, response);

}

});

}

我们没有对此功能进行任何更改。因为它根据params对象参数的内容设置值,所以对控制器和数据库的任何更改都会自动反映在这里。可以看到,返回的值只是照片的 ID。

但是,如果您查看模型中的其他方法,您会看到我们选择了特定的字段来输出给用户。我们必须对其他 SQL 语句进行一些修改。毕竟,拥有一个实际上不显示任何照片的相册 web 应用是很可笑的。无论如何,它可能会筹集到 5000 万美元的风险投资。

先找函数getPhotoByID()。将url添加到查询变量中,因此该函数现在如清单 5-11 所示。

Listing 5-11. Model getPhotoByID() Function

function getPhotoByID(params, callback){

var query = 'SELECT photoID, caption, url, albumID, userID FROM photos WHERE published=1 AND photoID=' + connection.escape(params.photoID);

connection.query(query, function(err, rows, fields){

if(err){

callback(err);

} else {

if(rows.length > 0){

callback(null, rows);

} else {

callback(null, []);

}

}

});

}

同样,我们希望在通过相册 ID 选择照片时包含 URL。同样,只更新 SQL 查询(参见清单 5-12 )。

Listing 5-12. Model getPhotosByAlbumID() Function

function getPhotosByAlbumID(params, callback){

var query = 'SELECT photoID, caption, url, albumID, userID FROM photos WHERE published=1 AND albumID=' + connection.escape(params.albumID);

connection.query(query, function(err, rows, fields){

if(err){

callback(err);

} else {

if(rows.length > 0){

callback(null, rows);

} else {

callback(null, []);

}

}

});

}

最后,我们希望包含通过搜索检索到的照片的 URL(参见清单 5-13 )。

Listing 5-13. Model getPhotosSearch() Function

function getPhotosSearch(params, callback){

var query = 'SELECT photoID, caption, url, albumID, userID FROM photos WHERE caption LIKE "%' + params.query + '%"';

connection.query(query, function(err, rows, fields){

if(err){

callback(err);

} else {

if(rows.length > 0){

callback(null, rows);

} else {

callback(null, []);

}

}

});

}

现在我们完成了编码!将您的更改提交到您的存储库中。返回 OpsWorks 并部署您的应用。你现在可以不需要指导就能做到。这次部署过程有很多工作要做。它将您的 OpsWorks 环境变量添加到每个实例中,更新 IAM 角色,并运行您的 Chef JSON。当您的代码从存储库中检索出来时,OpsWorks 会找到您的package.json文件中列出的新依赖项,并自动将它们与您的应用的其余部分一起安装。同时,在本课结束之前,我们还有一些任务要完成。

更新数据库模式

您可能已经注意到,我们的数据库模式不再反映我们需要的值。我们必须对照片表格进行快速更改。在您的 MySQL 客户端(理想情况下是 MySQL Workbench)中,连接到 RDS 实例。展开左侧导航中的相册数据库并展开表格以显示照片(参见图 5-11 )。

A978-1-4842-0653-9_5_Fig11_HTML.jpg

图 5-11。

RDS instance tables

Control -点击表格,从工具提示菜单中选择 Alter Table。您应该会看到表模式出现在中间的列中。添加一个 Varchar(250)类型的 url 列,如图 5-12 所示。如果您更喜欢执行原始 SQL 查询,这里是:

A978-1-4842-0653-9_5_Fig12_HTML.jpg

图 5-12。

The photos table schema

ALTER TABLE photoalbums.photos ADD url VARCHAR(250) NOT NULL;

如果要使本地环境保持最新,请确保对本地数据库进行相同的更改。

与 CloudFront 集成

在第四章中,我们创建了一个 CloudFront 实例来服务我们的应用。在此过程中,我们将应用堆栈的负载平衡器注册为对 CloudFront 实例的请求源。接下来,我们将对 S3 桶做同样的事情。S3 桶将是 CloudFront 实例的第二个来源,并自动将我们的资产副本存储在 CloudFront edge 位置。

创建云锋 S3 起源

使用服务菜单,在 AWS 控制台中导航到 CloudFront。从 CloudFront 登录页面的发行版列表中选择您的发行版。单击原点选项卡并创建原点。您将再次进入“创建源”视图,在该视图中,您将选择您的 S3 存储桶作为源域名。开始编辑字段时,会出现一个下拉列表。当您选择您的存储桶时,将自动生成一个起点 ID。你可以让它保持原样。

目前,我们不打算让我们的应用中的照片保密。然而,我们很可能希望在以后的某一天使用户内容成为私有的(或者至少是受限制的)。作为支持该功能的第一步,我们必须限制对 bucket 的直接访问。当我们将 Restrict Bucket Access 值设置为 Yes 时,S3 URL 将不再是公共的,用户将只能访问 CloudFront URLs 上的内容。

因为我们限制了 bucket 的访问,所以我们必须创建一个 CloudFront 访问身份,该身份具有访问 S3 bucket 的权限。虽然这听起来像是您在 IAM 中管理的东西,但是这些身份完全是在 CloudFront 中管理的。选择“创建新身份”以生成新的 CloudFront origin 访问身份。在 Comment 字段中,您可以输入一个字符串来标识您正在创建的身份,类似于 access-identity-photo albums-cloudfront。

接下来,将询问您是否要授予对 Bucket 的读取权限。这是一种更新 S3 存储桶策略的便捷方法,因此您不必手动执行此操作。选择是,更新存储桶策略。如果一切看起来如图 5-13 所示,点击创建继续。

A978-1-4842-0653-9_5_Fig13_HTML.jpg

图 5-13。

Creating a CloudFront Origin for the S3 bucket

云锋 S3 行为

既然我们已经为我们的 S3 桶创建了一个 CloudFront 源,我们必须创建将请求路由到该源的行为。选择行为选项卡。当我们在第四章中实验缓存行为时,你应该认识到这个观点。您会注意到,到目前为止,我们所有的行为都源于同一个地方:应用堆栈中的负载平衡器。但是如前所述,让我们的应用为用户提供图片上传服务是一种资源浪费。这一次,我们将创建一个源于 S3 桶的行为,从等式中完全删除应用堆栈。

单击左上角的“创建行为”。同样,您必须为行为输入一个路径模式。在“路径模式”字段中,输入/uploads/*以捕获对上传文件夹的所有请求。在“原点”字段中,展开下拉列表并选择您刚刚创建的原点,该原点对应于 S3 桶。

在我们到达对象缓存之前,不要管其他字段。因为我们不会从我们的应用栈发送定制的源头,所以我们希望 CloudFront 控制这些资产的 TTL。选择自定义,并在最小 TTL 字段中输入 43200(秒),持续 12 小时。从我们的 S3 桶中检索到的资产将在至少 12 小时后在 CloudFront 中刷新,如果您愿意,当然可以将其更改为任何其他值。

浏览其余选项,我们不会转发查询字符串或 cookies,也不会更改任何其他值。检查您的选择,确保它们与图 5-14 匹配,然后点击创建。

A978-1-4842-0653-9_5_Fig14_HTML.jpg

图 5-14。

Create CloudFront behavior for /uploads/*

您会注意到新行为出现在行为列表的底部,除了默认(*),它总是在底部。没有必要将此行为移到列表的顶部,因为它与我们现有的行为没有冲突。同样,CloudFront 中的变化需要一些时间来传播。您可以留意分发列表中的状态字段,等待您的状态从正在进行更改为已部署。

现在是真相大白的时候了!找到您想要用来创建照片的 jpeg 图像文件。我们假设你已经创建了一个用户和相册。如果您还没有,现在就提出这些请求,您应该得到 1 的userID和 1 的albumID。现在做一个POST请求到www.[ your domain ].com/photos/upload。在表单数据中包含以下参数:

albumID: 1

userID: 1

caption: 'my first real photo'

文件密钥应该是 photo,并且一定要将Content-Type设置为application/x-www-form-urlencoded。如果请求成功,您仍然会收到响应中的照片 ID。

{"id":21}

您的响应标题应该类似于清单 5-14 。您会注意到,X-Cache标题显示了来自 CloudFront 的一个未命中。响应不应该来自 CloudFront,否则您无疑会看到错误的数据。

Listing 5-14. Photo Upload Response Headers

Content-Type : application/json; charset=utf-8

Content-Length : 9

Connection : keep-alive

Date : Mon, 15 Dec 2014 21:02:57 GMT

X-Powered-By : Express

X-Cache : Miss from cloudfront

Via : 1.1 eadedd3fe9e82c51cc035044b3a5f3fa.cloudfront.net (CloudFront)

X-Amz-Cf-Id : yRWUsgyOTSh4xw5NcKfX-ne2-N7EU9yUIQYot9J82xcF1elqiRgBnw==

接下来,让我们验证我们上传到应用的数据存储正确,并且现在可以访问。打开您的浏览器到www.[ your domain ].com/photos/id/21(用您收到的 ID 替换21)。您应该会看到类似如下的 JSON:

[

{

"photoID":21,

"caption":"my first real photo",

"url":"uploads/1/141867737733318085bdee7f0a1577a57200e59c65306.jpg",

"albumID":1,

"userID":1

}

]

有图片的网址!接下来,只要 CloudFront 行为改变完成,您就应该能够访问您的域中的映像。复制 URL,将其添加到您的域中,并尝试在您的浏览器中查看。你应该看看你的形象!

收尾

恭喜你!现在您的应用中有了一个 CDN!这是应用的一个重大突破。这将是一个很好的时机,可以回顾并应用您所学到的一些经验来改进功能。一些小的改变可以大大改进我们的应用。

我们的 web app API 中的 URL 都是相对的。虽然这可能没问题,但在这种情况下,许多开发人员更喜欢绝对 URL。我们应该能够支持这两者。当前的设置也是本地开发的一个问题,因为我们仍然在上传文件到 S3,即使我们在本地数据库上运行应用。因此,虽然有一个可通过网络访问的版本uploads/1/141867737733318085bdee7f0a1577a57200e59c65306.jpg,但在http://localhost:8081/uploads/1/141867737733318085bdee7f0a1577a57200e59c65306.jpg却找不到该图像。因此,我们的第一个任务是将我们的图像转换为绝对 URL,并使 URL 正确,即使在本地环境中也是如此。

我们还可以改进 CloudFront 缓存图像的方式。默认情况下,/uploads/*路径中缓存的 URL 会在 CloudFront 中缓存 24 小时。然而,我们可以肯定的是,这些图像根本不会改变。我们不支持任何图像修饰或裁剪,即使我们支持,我们也会使用版本化文件命名。现在,24 小时没什么大不了的。但是,如果我们服务于成千上万的用户,为什么不从 CloudFront 中获益呢?因此,另一个收尾工作将是简单地将图像存储在 CloudFront 中,时间远远超过 24 小时。

绝对 URL

第一个任务虽然更难,但仍然很简单。如果您认为我们可以使用环境变量来存储域,那么您就错了!这不仅将允许我们在本地开发时访问我们的 S3 映像,还将使克隆我们的开发堆栈、更改域等变得容易。

首先,返回 OpsWorks 并选择您的堆栈。导航到堆栈中的应用视图,然后单击应用旁边的列中的编辑。向下滚动到环境变量标题,为关键域添加值http://www.[ your domain ].com(参见图 5-15 )。然后,单击页面底部的保存。

A978-1-4842-0653-9_5_Fig15_HTML.jpg

图 5-15。

Adding another Environment Variable to OpsWorks

既然已经设置好了,我们可以修改代码,然后开始新的部署。回到你的代码编辑器,我们将一个接一个地进行修改。我们要做的第一件事是将新变量添加到全局变量中。打开/libs/globals,我们将在其中添加域变量。awsVariables()函数看起来应该如清单 5-15 所示。

Listing 5-15. Adding Another Environment Variable to the Code Base

awsVariables : function(){

if(process.env.ENVIRONMENT){

var variables = {

bucket : process.env.S3BUCKET,

domain : process.env.DOMAIN

}

return variables;

} else {

var local = require('./../config/local');

return local.awsVariables;

}

}

我们还将在全球化方面更进一步。虽然我们现在只需要将照片的相对 URL 转换成绝对 URL,但是可以想象,我们可能在其他地方也需要这个功能。让我们给全局变量添加一个名为absoluteURL()的函数,这样我们就可以轻松地重用我们的代码。在awsVariables :function(){}之后,添加一个逗号,后面是清单 5-16 中的代码。

Listing 5-16. absoluteURL() Helper Function

absoluteURL : function(path){

if(this.awsVariables().domain){

return this.awsVariables().domain + '/' + path;

}

return path;

}

这个功能很简单,但是以后可能会省去我们很多复制粘贴的工作。它接受一个相对路径作为参数,并在域前面加上一个正斜杠(如果定义了域的话)。如果没有,它会无声地失败,不会崩溃。

我们还需要本地文件中的域变量。前往/config/local.js,将您的域名添加到那里的awsVariables对象中。

目前,我们没有对数据库中的数据进行任何查询后操作。我们只需检索我们需要的对象和属性,并将它们发送回各自的控制器。不幸的是,聚会已经结束了。我们不打算将域写入数据库,而是在数据库数据从模型返回之前将其添加到响应中。此外,可以想象,这将是我们对从数据库中检索的数据执行的许多查询后操作中的第一个。因此,我们可以向模型添加一个私有函数,从这个函数中我们可以组织对数据的修改。

打开/lib/model/model-photos.js并滚动到底部。在deletePhotoByID()方法之后,添加清单 5-17 中的代码。

Listing 5-17. formatPhotoData() Helper Function

function formatPhotoData(rows){

for(var i = 0; i < rows.length; i++){

var photo = rows[i];

if(photo.url){

photo.url = globals.absoluteURL(photo.url);

}

}

return rows;

}

这个函数很简单,遍历从数据库中检索到的照片,并对那些具有url属性的照片调用我们的absoluteURL()辅助函数。如果我们需要对所有照片进行任何其他查询后操作,我们可以稍后将它们添加到这个循环中。

接下来,我们必须确保每个检索照片的方法都使用这个函数,这意味着我们必须在三个地方进行更改。向上滚动到功能getPhotoByID(),找到如下一行:

callback(null, rows);

用以下内容替换:

callback(null, formatPhotoData(rows));

该函数现在应该如下所示:

function getPhotoByID(params, callback){

var query = 'SELECT photoID, caption, url, albumID, userID FROM photos WHERE published=1 AND photoID=' + connection.escape(params.photoID);

connection.query(query, function(err, rows, fields){

if(err){

callback(err);

} else {

if(rows.length > 0){

callback(null, formatPhotoData(rows));

} else {

callback(null, []);

}

}

});

}

最后,我们必须对getPhotosByAlbumID()getPhotosSearch()进行相同的更改。进行这些更改,然后将您的代码提交到存储库中。回到 OpsWorks 并部署您的应用。等待几分钟让部署完成,完成后,刷新浏览器中的/photos/id/21路径。现在,您应该可以看到图像的绝对路径,如下所示:

[

{

"photoID":27,

"caption":"test",

"url":" http://www.cloudyeyes.net/uploads/1/1418498633152f1fe581691cc3aa20577958626077976

.jpg",

"albumID":1,

"userID":1

}

]

增强型图像缓存

对于本章的最后一个任务,您将增加 CloudFront 中的/uploads/*路径的 TTL。在 AWS 控制台中返回 CloudFront,并从发行版列表中选择您的发行版。然后,单击“原点”选项卡。选择/uploads/*路径模式,如图 5-16 ,点击编辑。

A978-1-4842-0653-9_5_Fig16_HTML.jpg

图 5-16。

Edit CloudFront behavior

找到对象缓存字段,并将值从使用原始缓存头更改为自定义。使用原始缓存头没有任何好处。因为对/uploads/*的请求甚至从未到达我们的应用堆栈,所以我们不能像对其他路径那样以编程方式控制缓存行为。通过切换到 Customize,我们可以输入 CloudFront edge 位置中的缓存对象的最小 TTL,或者说生命周期。选择自定义后,在最小 TTL 字段中输入数字 604800。这将存储缓存的对象一周(秒)。我们可以很容易地把它设置为两周,或者一年!不过,就目前而言,一周时间似乎足够了。最后,单击 Yes,Edit,您的更改将开始生效。您可以通过观察发行状态来检查这一点,发行状态现在将被设置为 InProgress。记住:CloudFront 中的变化传播需要几分钟时间。

摘要

下一章,对我们的应用进行了一些大的修改。然而,我们只需编写很少的代码就可以为我们的软件添加一些强大的工具。我希望您已经习惯了使用 AWS 控制台对我们的基础设施进行大规模更改的想法。如此随意地行使这种权力可能会让人有些伤脑筋。考虑到这一点,我们仅仅触及了 AWS SDK 的皮毛。在接下来的几章中,我们将继续使用 SDK 来添加更多的特性。

Footnotes 1

也参见 S3 文献中的 http://aws.amazon.com/documentation/s3/

六、简单电子邮件服务

虽然我们的应用已经取得了很大的进步,但在它准备好投入使用之前,我们还需要构建一些功能。当然,最明显的是用户账户缺乏安全性。甚至不需要登录就可以上传到别人的相册。在这种情况下,安全确实是一种幻觉。不过不用担心,我会在第八章的大结局中谈到这个话题。

事实上,在真正投入生产之前,我们的应用还缺少相当多的部分。如果这是一本严格意义上的编程书籍,我们仍然需要浏览关于构建管理门户、不当内容标记系统和社交网络功能的教程。不幸的是,在这些课程中没有时间构建一个完整的企业应用。我们的重点仍然是在 AWS 上构建一个可伸缩的弹性应用,其成本在于关注集成到 AWS 服务中的功能,而不是那些您可以从另一本 Node.js 编程书籍中的年长和明智的开发人员那里学到的功能。

也就是说,我们仍然可以通过 AWS 和一些优秀的老式编程之间的协同作用来构建一些功能。因为我们的应用完全作为 web 服务进行交互,所以感觉有点单调,不是吗?上传了这么多旅行照片,得到的回复都是 JSON 这个,JSON 那个。通往我内心的路是我的收件箱,是时候让我们的应用发送一些邮件了!

简单电子邮件服务简介

不管内容如何,任何包含用户生成内容的 web 应用都可以从某种通知系统中受益,无论是电子邮件还是移动推送通知。亚马逊提供了一些不同的服务来支持通知,但我们将专注于亚马逊简单电子邮件服务(SES)。Amazon SES 旨在允许您以编程方式生成任何类型和数量的电子邮件通知。从大规模营销活动到密码重置电子邮件,您可以使用 Amazon SES 发送任意数量的静态和动态内容。

如果您以前曾经用任何语言构建过服务器端应用,那么您很有可能曾经生成过电子邮件。如果您使用 PHP,您可能已经花了一些时间仔细格式化mail()函数的参数。这种方法虽然乏味,但在小范围内效果很好。但是想象一下,如果您的应用有成千上万的用户。突然之间,仅仅生成电子邮件就有了资源管理方面的问题。

Amazon 为这个问题提供了一个解决方案,它允许您将发送邮件的工作负载从应用堆栈中分离出来。相反,我们将使用 AWS SDK 向 Amazon SES 发送命令,它将代表我们发送邮件。这将提供显著的资源节约,并且我们的应用堆栈将保持没有邮件服务器的负担。我们也免去了配置自己的邮件服务器的麻烦。我们也不必看着我们辛辛苦苦生成的电子邮件直接进入垃圾邮件文件夹。

让我们欣慰的是,亚马逊 SES 有一个免费层。在撰写本文时,你每月可以免费发送 62,000 封电子邮件。之后,你要为每一千封邮件支付 0.10 美元。除了发送邮件的费用,您的邮件包含在从 EC2 费率表中转出的数据中,并且您还需要为每 GB 附件支付 0.12 美元。总之,这非常合理,并且您的 SES 成本可能会比 RDS 或 EC2 账单低一个数量级。

探索 SES 仪表板

我们将向我们的应用添加一些电子邮件,这将在我们的代码中以编程方式生成。在此之前,我们必须在 AWS 控制台中执行一些任务。让我们从配置 SES 开始。在 AWS 控制台中,在页面右侧的“应用服务”列中找到 SES,然后单击它。我们将在 SES 控制面板中开始这一过程。正如你在屏幕上看到的(以及图 6-1 ),这个仪表盘上发生了很多事情。

A978-1-4842-0653-9_6_Fig1_HTML.jpg

图 6-1。

SES dashboard

与许多其他 AWS 仪表板一样,视图的左栏是二级导航。在主要内容区域,您会立即看到一条警告,提示您的帐户拥有“沙盒”访问权限。默认情况下,所有 AWS 帐户都是在沙盒模式下创建的,这限制了 AWS 客户向公众发送大量电子邮件的能力。这只是一种反垃圾邮件的预防措施,因为请求生产访问很容易。稍后我会更详细地讨论这一点,但是现在,请注意,此时您不能向任何人发送电子邮件。

在警告的正下方,您可以看到 Amazon SES 发送限制的快速快照。您会注意到,当前的发送配额是每 24 小时 200 封电子邮件。在您向 SES 请求生产访问权限之前,此配额一直有效。明确地说,这意味着 200 个接受者。如果你发送 20 封电子邮件,每封有 10 个收件人,你将达到你的配额。

在您的亚马逊 SES 发送限制下方,您会看到一个标题为“您的亚马逊 SES 指标”的标题。在此部分,您可以查看 SES 发送的消息的结果,以实际数量或比率(百分比)查看。如果你以前用过电子邮件营销软件,你会认识这些术语:投递、退回、投诉、拒绝。如果您仅使用 SES 向订阅和注册用户发送通知,这些指标可能对您没有价值。但是如果你计划用电子邮件营销来恐吓你的用户,这些可以是有用的标准。

SES 验证

当我们处于沙盒模式时,对于我们可以向谁发送电子邮件有很大的限制。我们用作发件人或收件人的任何地址都必须经过验证。在图 6-2 中,注意左侧导航中的验证发送者。

A978-1-4842-0653-9_6_Fig2_HTML.jpg

图 6-2。

SES Verified Senders

您可以在两个级别验证 SES 地址:个人电子邮件地址级别和域级别。对于已验证的电子邮件地址,您需要手动验证每个电子邮件地址。然后,您可以向经过验证的地址发送电子邮件,也可以从该地址接收电子邮件。如果在域级别进行验证,则可以从域中的任何地址发送电子邮件。例如,在一种情况下,您可能希望验证从support@yourdomain.com发送电子邮件的域,而在另一种情况下,您可能希望验证从donotreploy@yourdomain.com发送电子邮件的域。验证域只允许您从有问题的域发送电子邮件。例如,你不能验证 gmail.com 并被允许向数百万 Gmail 用户发送电子邮件。

出于开发目的,从我们的应用的 web 域向注册用户发送电子邮件是理想的。虽然我们正在开发,注册用户还必须在 ses 中验证电子邮件地址。稍后,我们将请求 SES 的生产访问权限,使我们能够向所有用户发送电子邮件。但是我们不必这样做来完成开发和测试。

电子邮件地址验证

让我们首先验证一个电子邮件地址——您自己的个人电子邮件。在图 6-2 所示的已验证发件人标题下,点击电子邮件地址。您将看到一个经过验证的地址表,现在已经没有了。单击页面顶部的验证新电子邮件地址。页面上方会出现一个模态窗口,如图 6-3 所示。输入您的电子邮件地址,然后单击验证此电子邮件地址。

A978-1-4842-0653-9_6_Fig3_HTML.jpg

图 6-3。

Verifying an e-mail address

几分钟后,模式窗口将通知您验证电子邮件已经发送。当您退出模式并返回主视图时,您的地址将出现在表格中,状态为待验证(参见图 6-4 )。

A978-1-4842-0653-9_6_Fig4_HTML.jpg

图 6-4。

SES verified e-mail addresses

在您的收件箱中,查看主题为“亚马逊 SES 地址验证请求”的电子邮件,地址在 region[您当前所在的地区]。您将看到一个冗长的验证 URL,您应该单击它来确认地址。如果您在 24 小时内没有点击该链接,它将过期,并且您的电子邮件地址的验证状态将变为失败。该链接会将您带到 AWS 页面,祝贺您验证了您的电子邮件地址。庆祝即将开始!

当您刷新电子邮件地址列表时,您的地址状态现在应该得到验证。让我们做一个快速测试。选择您的地址,然后单击发送测试电子邮件。将出现一个模式窗口,允许您填充电子邮件的“收件人”、“主题”和“正文”字段(参见图 6-5 )。您可以单击“更多选项”来添加其他电子邮件标题,如“密件抄送:。让你自己也成为收件人:填写一条消息,然后点击发送测试邮件。

A978-1-4842-0653-9_6_Fig5_HTML.jpg

图 6-5。

SES Send Test Email

过一会儿,您应该会收到电子邮件。现在让我们试着给别人发一封电子邮件。从列表中选择您的电子邮件,然后再次单击发送测试电子邮件。这一次,在 To:字段中,输入属于朋友的不同地址或您自己的地址,然后再次单击 Send Test Email。这一次,您会遇到一个错误,如图 6-6 所示,因为收件人也必须是经过验证的电子邮件地址。

A978-1-4842-0653-9_6_Fig6_HTML.jpg

图 6-6。

SES Send Test Email again

就这么简单。然而,出于开发的目的,我们必须能够发送邮件,而不仅仅是与自己往来。让我们继续验证我们的域,这样我们就可以开始从应用发送邮件了。

域验证

选择左侧导航栏中“已验证的发件人”标题下的“域”。单击顶部的验证新域按钮。将再次出现一个模态窗口,提示您输入域名。输入域名并点击生成 DKIM 设置旁边的复选框(见图 6-7 ),然后点击验证该域。

A978-1-4842-0653-9_6_Fig7_HTML.jpg

图 6-7。

Verifying a domain

一会儿,一个新的模态视图将出现,如图 6-8 所示。

A978-1-4842-0653-9_6_Fig8_HTML.jpg

图 6-8。

Verify domain DNS records

为了完成域验证,必须在您的主机上创建这些 DNS 记录。此外,理想情况下应创建 DKIM 记录。DKIM,或域密钥识别邮件,本质上是一种加密方法,用于验证声称从一个域发送的邮件实际上是源自所述域。通过在域级别启用 DKIM,我们降低了应用消息出现在垃圾邮件文件夹中的风险。

如果您在别处管理您的 DNS,您必须创建 TXT 和 CNAME 记录,如模态视图所示。但是,您会注意到右下角的“使用 53 号公路”按钮。因为我们使用 Route 53 来管理我们的 DNS,所以我们可以自动完成配置。点击此按钮继续。

另一个模态视图将会出现,而不是被重定向到 53 号公路,准备执行一些 53 号公路的任务。您将看到域验证和 DKIM 记录集的表格,每个表格上方都有复选框,用于切换这些记录的创建(参见图 6-9 )。

A978-1-4842-0653-9_6_Fig9_HTML.jpg

图 6-9。

Creating DNS records in Route 53

单击创建记录集以自动创建记录集。过一会儿,您将返回到“已验证的发件人:域”表,如图 6-10 所示,该表将列出您的域,并显示待验证状态。

A978-1-4842-0653-9_6_Fig10_HTML.jpg

图 6-10。

Verified domains

作为健全性检查,让我们验证记录是否被正确创建。打开 AWS 服务菜单,导航至 53 号公路。选择托管区域,并从出现的表格中选择您的域名。在顶部,单击转到记录集以查看与您的域相关联的 DNS 记录。果然,您应该看到三个 CNAME 记录和一个 TXT 记录,它们的名称和值与我们在 SES 中生成的内容相对应(参见图 6-11 )。由于这实际上是一个 DNS 更改,可能需要 72 小时来验证您的域。有趣的是,它比使用 53 号公路花费的时间要少得多。在任何情况下,我们的域名都不会被立即验证。

A978-1-4842-0653-9_6_Fig11_HTML.jpg

图 6-11。

Route 53 record sets for SES

使用 IAM 管理 SES 权限

同时,我们可以完成 AWS 的设置。到目前为止,您可能已经得出结论,我们将使用 AWS SDK 从应用堆栈中的 EC2 实例生成 SES 邮件。上次我们使用 AWS SDK 来控制另一个服务时,我们必须在 IAM 中管理权限,以便允许我们的实例运行命令。我们将再次经历同样的过程。

导航到 IAM 仪表板。从导航中选择角色,您将看到我们创建的 IAM 角色列表。在列表中找到 AWS-ops works-photo albums-ec2-role 并单击它。在权限标题下,您应该看到我们在上一章中创建的策略,该策略授予此角色上传到 S3 存储桶的权限。策略的名称描述了它的效用。我们将为新权限添加一个内联策略,而不是修改现有策略。单击创建角色策略再次开始策略生成过程。选择策略生成器标题后,单击选择。在编辑权限视图中,从 AWS 服务下拉列表中选择 Amazon SES,并从操作下拉列表中选择所有操作(*)(参见图 6-12 )。

A978-1-4842-0653-9_6_Fig12_HTML.jpg

图 6-12。

Amazon SES full permissions policy

单击 Add Statement,然后单击 Next Step,这将显示策略的原始 JSON 以及自动生成的名称。该策略应该类似于清单 6-1 。您可能还想将策略名称字段中的名称更改为类似 amazonsefullcaccess-AWS-ops works-photo albums-ec2-role 的名称。单击右下角的应用策略。

Listing 6-1. SES Full-Access IAM Policy

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "Stmt1424445811000",

"Effect": "Allow",

"Action": [

"ses:*"

],

"Resource": "*"

}

]

}

当您返回到角色的详细视图时,权限/策略应该如图 6-13 所示。

A978-1-4842-0653-9_6_Fig13_HTML.jpg

图 6-13。

EC2 Instance role policies

我们还希望能够在本地开发环境中使用 SES 功能。这意味着我们还必须给予 photoalbums-s3 用户相同的权限,我们在本地使用该用户的凭证。从 IAM 导航中选择用户,然后选择相册堆栈用户(或您用于本地开发的任何用户)。在用户详细信息视图中向下滚动,直到看到权限标题。单击链接为此用户创建内联策略。

同样,您将向下滚动策略模板列表,直到找到 Amazon SES 完全访问权限。您将允许该服务的所有权限,单击“添加语句”,然后单击“下一步”。您可以再次查看您选择的策略。策略文档应该就像我们之前在清单 6-1 中看到的一样。

单击应用策略返回到用户详细信息视图。该用户现在应该有两个策略:一个用于 S3 访问的托管策略和一个用于 SES 权限的内联策略(参见图 6-14 )。

A978-1-4842-0653-9_6_Fig14_HTML.jpg

图 6-14。

IAM user policies

将 SES 与 AWS SDK 一起使用

现在我们已经准备好开始将 SES 集成到我们的应用中了!目前,让我们添加一些非常标准的功能。当新用户注册时,我们会向她发送一封注册确认电子邮件。我们不会强迫她在邮件里激活账号;这将只是一个受欢迎的信息。

在深入讨论之前,我们必须再次确定如何使我们的地址动态化,也就是说,避免将它们硬编码到应用中。毕竟,我们希望我们的代码在开发环境中像在生产环境中一样工作良好。发送注册邮件的电子邮件地址应该类似于donotreply@yourdomain.com。总是假设发件人应该是“不知道”可能是一个合理的决定我们还将域存储在 OpsWorks 环境变量中—格式错误。这里我们可以采取一些方法。

Store specific e-mail addresses, such as contact@yourdomain.com in OpsWorks Environment Variables, and access them programmatically.   Use the existing DOMAIN environment variable and programmatically trim http://www off the beginning, to make it usable in constructing e-mail addresses dynamically.   Make a new environment variable for the mail domain and use that.

所有这三种方法(可能还有一些我没有想到的方法)都非常有效。为了简单起见,我们将使用第二种方法,这将省去我们创建新环境变量的麻烦。但是,此时您应该对使用前面的任何方法在这个上下文中动态构造电子邮件地址的能力充满信心。

全球

在代码编辑器中打开/lib/globals.js并滚动到末尾。在absoluteURL()功能后,粘贴以下内容:

rootDomain : function(){

return this.awsVariables().domain.replace('``http://www

}

这个简单的函数会将 http://www.yourdomain.com 转换成简单的yourdomain.com。我们称之为rootDomain,而不仅仅是mailDomain,因为我们可能在以后的某个时候需要根域。为了清楚起见,您的globals文件现在应该如下面的清单 6-2 所示:

Listing 6-2. Updated Globals

module.exports = {

applicationPort : 80,

database  : function(){

if(process.env.ENVIRONMENT){

var opsworks = require('./../opsworks');

var opsWorksDB = opsworks.db;

var rdsConnection = {

host  : opsWorksDB.host,

port : opsWorksDB.port,

database : opsWorksDB.database,

user : opsWorksDB.username,

password : opsWorksDB.password

};

return rdsConnection;

} else {

var local = require('./../config/local');

var localConnection = local.db;

return localConnection;

}

},

awsVariables : function(){

if(process.env.ENVIRONMENT){

var variables = {

bucket : process.env.S3BUCKET,

domain : process.env.DOMAIN

}

return variables;

} else {

var local = require('./../config/local');

return local.awsVariables;

}

},

absoluteURL : function(path){

if(this.awsVariables().domain){

return this.awsVariables().domain + '/' + path;

}

return path;

},

rootDomain : function(){

return this.awsVariables().domain.replace('``http://www

}

}

Mail.js

虽然我们可以将其余大部分代码放在 users route 中,但是用一个单独的类来处理我们所有的 SES 事务可能更有组织意义。毕竟,我们可能希望在应用的其他地方为其他目的生成电子邮件。可以肯定地说,电子邮件通信应该有自己的代理类。

/lib目录下创建一个名为mail.js的新文件。当然,如果你愿意,你可以给它起个名字,比如ses.js。首先,我们必须包含这个文件的依赖项。我们需要访问我们的globals.js,以及 AWS SDK。在文件的顶部,粘贴以下几行:

var aws = require('aws-sdk');

var globals = require('./globals');

目前,我们仅添加注册电子邮件。但是,我们应该假设将来会添加其他邮件功能来构建这个文件。因此,我们将编写两个函数:一个构造注册电子邮件的内容,另一个发送 SES 邮件。

首先,将sendEmail()函数添加到mail.js(参见清单 6-3 )。这个函数将简单地发送 SES 邮件和传递给它的参数。它将仍然是一个私人功能。

Listing 6-3. sendEmail Function

function sendEmail(params, callback){

if(globals.awsVariables().key){

aws.config.update({ accessKeyId: globals.awsVariables().key, secretAccessKey: globals.awsVariables().secret });

}

var ses = new aws.SES({region:'us-east-1'});

var recipient = params.username + '<' + unescape(params.email) + '>';

var sesParams = {

Source: params.messageSource,

Destination: {

ToAddresses: [recipient],

BccAddresses: params.bccAddress

},

Message: {

Subject: {

Data: params.subject,

Charset: 'UTF-8'

},

Body: {

Text: {

Data: params.messageText,

Charset: 'UTF-8'

},

Html: {

Data: params.messageHTML,

Charset: 'UTF-8'

}

}

},

ReplyToAddresses: [emailSender()]

}

ses.sendEmail(sesParams, function(err, data){

callback(err, data);

});

}

您会注意到一个与我们的 S3 上传功能相似的模式。如果找到 IAM 键,意味着我们在本地环境中,那么调用aws.config.update()来使用我们的本地凭证。然后,我们从 SDK 初始化 SES 的一个实例。要使用 SES,您还必须设置区域。在我们的例子中,是'us-east-1'。该函数的其余部分是用在params对象中发送的值填充 SES 参数。最后,ses.sendEmail()发送电子邮件。

接下来,我们将创建sendRegistrationConfirmation()函数。该函数将构造传递给sendEmail的参数。为了向我们的应用添加其他电子邮件,我们将仅仅复制sendRegistrationConfirmation功能。添加清单 6-4 到mail.js中的代码。

Listing 6-4. sendRegistrationConfirmation Function

function sendRegistrationConfirmation(params, callback){

var emailParams = {

username : params.username,

email : params.email

};

emailParams.messageSource = emailSender();

emailParams.bccAddress = [];

emailParams.subject = 'Registration Confirmation';

emailParams.messageText = 'You have successfully registered for Photoalbums. Your username is ' + emailParams.username + '.';

emailParams.messageHTML = 'You have successfully registered for Photoalbums. Your username is <strong>' + emailParams.username + '</strong>.';

sendEmail(emailParams, callback);

}

如您所见,我们以纯文本和 HTML 格式生成电子邮件主题和消息。您会注意到messageSource被设置为emailSender()。因为我们可能会向用户发送多个系统电子邮件,所以可重用函数是最小化代码重复的好方法。emailSender()功能也应该添加到mail.js中。

function emailSender(){

return 'donotreply@' + globals.rootDomain();

}

在前面的代码中,我们使用源自 OpsWorks 环境变量的根域来构造发件人的电子邮件地址。

最后,我们必须使sendRegistrationConfirmation()成为一个公共方法。在文件末尾,添加以下行:

exports.sendRegistrationConfirmation = sendRegistrationConfirmation;

用户注册途径

接下来,我们将这个新功能集成到我们的应用中。目前,我们只在用户路线中增加了邮件功能。打开/routes/users.js。在文件的顶部,将mail包含在文件中:

var mail   = require('./../lib/mail');

找到您将添加mail.sendRegistrationConfirmation()功能的/register路线。在我们成功地在数据库中创建用户帐户之前,我们不想发送注册电子邮件。路线应该如下所示(列表 6-5 ):

Listing 6-5. New User Registration Code

router.post('/register', function(req, res) {

if(req.param('username') && req.param('password') && req.param('email')){

var email = unescape(req.param('email'));

var emailMatch = email.match(/\S+@\S+\.\S+/);

if (emailMatch !== null) {

var params = {

username: req.param('username').toLowerCase(),

password: req.param('password'),

email: req.param('email').toLowerCase()

};

model.createUser(params, function(err, obj){

if(err){

res.status(400).send({error: 'Unable to register'});

} else {

mail.sendRegistrationConfirmation({username: req.param('username'), email: req.param('email')}, function(errMail, objMail){

if(errMail){

res.status(400).send(errMail);

} else {

res.send(obj);

}

});

}

});

} else {

res.status(400).send({error: 'Invalid email'});

}

} else {

res.status(400).send({error: 'Missing required field'});

}

});

Note

在生产环境中,您可能希望在注册过程中检查重复的电子邮件地址。

部署和测试

这些就是我们需要的所有代码更改!将您的代码提交到您的存储库中;导航到 OpsWorks 并选择您的应用堆栈。从下拉列表中选择应用。当您在列表中看到您的应用时,单击部署。给应用几分钟的部署时间。

当您的部署完成后,我们可以测试新功能。通过向http://www.[ yourdomain ].com/users/register发出POST请求来注册新用户。确保包括以下参数:usernameemailpassword。使用您喜欢的任何用户名和密码,并确保使用 SES 验证的电子邮件作为电子邮件。发送请求,您应该会看到以下响应:

{  message: "Registration successful!" }

这是个好迹象。继续检查你的电子邮件。您应该会看到一条类似图 6-15 的消息。

A978-1-4842-0653-9_6_Fig15_HTML.jpg

图 6-15。

E-mail sent by SES

让我们回到 SES,看看我们的指标。在 AWS 菜单中,选择 SES。在仪表板中,您现在应该可以看到您的测试结果,如图 6-16 所示。如您所见,您已经发送了 200 封邮件中的 1 封,并且您的电子邮件有 100%的送达率。

A978-1-4842-0653-9_6_Fig16a_HTML.jpg A978-1-4842-0653-9_6_Fig16b_HTML.jpg

图 6-16。

SES sending limits and metrics

这就是全部了!您已经将 SES 集成到您的应用中,应该能够在本课的基础上进行推断,在其他用例中从您的应用中生成电子邮件。但是在我们结束这一章之前,我们将快速浏览一下 AWS 资源的组织。

AWS 资源组

到了第六章,你已经在你的应用中使用了一些服务。随着基础架构的增长,可能很难保持跟踪。有几种方法可以组织我们的资源,使它们更容易找到。我们将很快重新访问一些 AWS 服务,用我们的应用名标记它们,并从这些标记创建一个资源组。

正如我在第三章中讨论的,标签没有技术用途;它们的存在纯粹是为了我们自己的方便。虽然它们可以帮助我们组织我们的资源,但它们对分析您的 AWS 账单尤其有用,这个主题超出了本书的范围。

为了便于组织,我们可以给相册资源一组通用的标签。然后,我们将基于这些公共标签创建一个资源组,这将使整合我们系统的移动部分变得更加容易。

标记资源

让我们从标记我们的数据库开始。从 AWS 控制台,导航到 RDS。单击左侧导航栏中的 Instances,这将在右侧视图中显示您的实例。在视图底部,单击“标签”按钮。该视图将作为实例的详细页面重新加载。在底部,您会看到标签标题下方的表格,如图 6-17 所示。

A978-1-4842-0653-9_6_Fig17_HTML.jpg

图 6-17。

RDS instance tags

当前分配给 RDS 实例的唯一标签是 workload-type: production。让我们添加一个标签来表明这个数据库是 Photoalbums 项目的一部分。单击添加/编辑标签。在标签数据库实例视图中(见图 6-18 ,点击添加另一个标签。输入 project 作为关键字,photoalbums 作为值。单击保存标签。

A978-1-4842-0653-9_6_Fig18_HTML.jpg

图 6-18。

Adding tags to the database

这个很简单。不幸的是,默认情况下,我们不能像我们希望的那样在应用堆栈中标记实例(但是我们可以使用 Chef 脚本)。现在,让我们手动标记 EC2 实例。前往 EC2 仪表板。点击左侧导航栏中的标签,将显示 EC2 标签列表(参见图 6-19 )。您会注意到有些标签是为您的实例自动生成的。

A978-1-4842-0653-9_6_Fig19_HTML.jpg

图 6-19。

EC2 tags

单击页面顶部的管理标签。现在您将看到一个视图,您可以在其中多选实例并添加新标签。选择实例并将 project: photoalbums 标签添加到实例中,然后单击“添加标签”。您的实例现在已被标记。

记住:您的负载平衡器也在这里!从左侧菜单中选择负载平衡器。选择您的负载平衡器,然后单击标记选项卡。如图 6-20 所示,它已经有了一些标签(我们在第三章中创建的)!如果同样的标签被自动应用到我们在 OpsWorks 中创建的所有资源上,那不是很好吗?我们可以梦想…

A978-1-4842-0653-9_6_Fig20_HTML.jpg

图 6-20。

ELB tags

不幸的是,我们必须添加自己的标签。单击 Add/Edit Tags,当模式出现时,通过单击 Create Tag,输入标记,然后单击 Save,添加与前面相同的标记键/值对。

这真是一次回忆之旅。我们还可以标记哪些资源?S3 水桶应该是目前为止最后一个了。前往 S3;选择您的存储桶;在右侧视图中,展开 Tags 部分。添加您的标签,如图 6-21 所示,点击保存。

A978-1-4842-0653-9_6_Fig21_HTML.jpg

图 6-21。

S3 tags

创建和查看资源组

接下来,让我们用新的标记创建一个资源组。您会注意到导航栏中有一个名为 AWS 的菜单,我们从未使用过。展开菜单,如图 6-22 所示,点击创建资源组。

A978-1-4842-0653-9_6_Fig22_HTML.jpg

图 6-22。

AWS menu

在 Create a resource group 视图中,我们可以配置资源组。在这个视图中,我们将通过标签过滤我们的资源。首先,将资源组命名为 Photoalbums Resources。首先,我们将向资源组添加两个标记。在“标签”下拉列表中,选择“项目”,然后在附带的文本字段中,选择“相册”。

在“区域”字段中,您可以选择该组中包含哪些地理区域。我们所有的资源都在美国东部,所以你只能选择美国东部。将资源类型字段留空以包含所有资源类型(参见图 6-23 )。完成后,单击保存。

A978-1-4842-0653-9_6_Fig23_HTML.jpg

图 6-23。

Editing a resource group

现在我们有了一种快速获取资源的新方法。资源组视图可随时从 AWS 的主导航中访问,它将您快速链接到项目中的所有资源(参见图 6-24 )。

A978-1-4842-0653-9_6_Fig24_HTML.jpg

图 6-24。

Viewing the Resource Group

事实上,你可能永远也不会用到它。我们已经养成了快速从一项服务切换到另一项服务的习惯,这是有好处的。但是现在你知道你可以标记资源,事实上,AWS 会自动为你标记一些资源。您可以使用这些标签来查看您连接的资源的详细信息,也可以使用这些标签来管理您的账单。

如果您想要维护多个应用环境/应用堆栈,您可以为 photoalbums dev 添加另一个标记值,并且您可以创建一个所有堆栈共有的标记,从而有效地创建跨所有 AWS 服务的不同资源视图。

摘要

我们的应用现在可以给我们的用户发送电子邮件了!这是一个快速的教训,它应该打开许多大门。我们还走了一条捷径来组织我们越来越多的 AWS 资源,当您在 AWS 中构建一个复杂的系统时,这非常有用。在下一章中,我们将最终扩展我们的应用并响应需求,就像我们最初计划的那样。

Footnotes 1

更多关于 DKIM 的精彩世界,请访问官方网站 www.dkim.org

七、监控应用

我们已经谈了很多我们设计系统的主要原则——可伸缩性和弹性——在这个过程中,你已经了解了这些概念。迄今为止,我们几乎无法将这些原则付诸实践。虽然您已经看到可以通过各种 AWS 服务快速增加资源分配,但我们还没有明智地这样做。当然,您总是可以在尽可能大的服务器上运行您的应用,但是这忽略了一点。弹性再次意味着能够扩展我们的基础设施以响应需求或其他事件,我们将这些统称为事件。在本章中,您将学习如何应用这一原则,首先识别事故,然后做出反应。

我们如何知道我们需要扩展?应对事故的第一个也是最重要的障碍是评估我们基础设施的健康状况。虽然我们不需要知道当前有多少用户登录,但是我们需要能够评估对我们的应用有一定影响的特定指标。例如,如果我们想知道 EC2 实例的大小是否足够,我们必须测量诸如 CPU 利用率和内存之类的东西,以便确定当前实例的状态。如果我们的应用运行在单个实例上,并且它的 CPU 利用率为 100%,那么将会出现严重的性能问题,我们可以称之为事件。

一旦我们发现一个事件,我们必须制定一个反应。在前面的场景中,最明显的反应是向我们的应用堆栈添加另一个实例。在本章中,我们将针对这种情况和其他可能发生的情况制定计划,并自动对事件做出响应。您将学习如何使用基于负载和基于时间的 EC2 实例来部署额外的资源,以响应和预测高需求。当然,有些事件需要人工干预来解决,或者其解决方案超出了初学者手册的范围。在这些情况下,我们将在发生重大事件时设置通知。

云监控

亚马逊已经将他们所有的监控指标整合到一个名为 CloudWatch ( http://aws.amazon.com/cloudwatch/ )的伞形服务下。您可以在任何其他 AWS 服务中查看的任何指标也可以在 CloudWatch 中收集和跟踪。通常,在 CloudWatch 中更容易详细查看指标。需要注意的是,AWS 指标仅在两周内有效。

让我们先来看看 OpsWorks 中的指标,并将它们与 CloudWatch 进行比较。登录 AWS 控制台并导航到 OpsWorks。选择相册堆栈。然后打开导航下拉菜单,并单击监控。默认情况下,您将看到分配了 EC2 实例的应用层的监控视图(参见图 7-1 )。在此视图中,RDS 和 ELB 图层不会出现。

A978-1-4842-0653-9_7_Fig1_HTML.jpg

图 7-1。

The OpsWorks Monitoring view

将显示四个类别的层度量,每个都有自己的图表:CPU 系统、使用的内存、负载和进程。默认情况下,会加载过去 24 小时的指标。您可以快速查看是否有任何 CPU 系统(利用率)、内存使用或负载达到最大值的重大事件,或者活动进程中是否有峰值。前三个图表标题实际上都是一个下拉菜单,可用于从该类别中选择另一个指标。图 7-2 显示已用内存下拉列表。

A978-1-4842-0653-9_7_Fig2_HTML.jpg

图 7-2。

An OpsWorks Monitoring metric drop-down

您也可以将日期范围从 24 小时更改为另一个范围。这些似乎是非常有用的指标,但是没有办法更详细地查看它们。您可能认为单击其中一个图表会将其展开,但它会将您带到其他地方:到层中实例的监视视图。记下内存使用指标,因为我们将在 CloudWatch 中查看它。现在,让我们转到 CloudWatch 控制面板,在那里查看这些指标。

打开服务下拉列表,并选择 CloudWatch。在控制台的顶部,您会看到一个标题为“指标摘要”的标题(参见图 7-3 )。在此标题下,单击浏览指标按钮。

A978-1-4842-0653-9_7_Fig3_HTML.jpg

图 7-3。

CloudWatch Metric Summary

在 Metrics 视图中,您可以看到您的 AWS 帐户的所有指标,先按服务细分,然后再按该服务中的类别细分(参见图 7-4 )。您在此处看到的指标数量取决于您的帐户上已经创建了哪些资源。

A978-1-4842-0653-9_7_Fig4_HTML.jpg

图 7-4。

CloudWatch Metrics by Category

让我们来看看我们的应用的指标。假设我们想看看我们的应用层使用了多少内存。在 OpsWorks 度量标题下,单击层度量。您应该会看到一个按名称及其对应的 LayerId 字母顺序排列的指标列表,如图 7-5 所示。因为我们只有一个分配了 EC2 实例的 OpsWorks 层,所以每个指标出现一次,并对应于 Node.js 应用层。

A978-1-4842-0653-9_7_Fig5_HTML.jpg

图 7-5。

OpsWorks Layer Metrics

向下滚动到名为 memory_used 的指标。单击它旁边的复选框。在指标列表下方,会神奇地出现一个图表,以 5 分钟的间隔显示过去 12 小时的指标(参见图 7-6 )。

A978-1-4842-0653-9_7_Fig6_HTML.jpg

图 7-6。

OpsWorks Layer Metrics in CloudWatch

这与我们刚才在 OpsWorks 中看到的指标相同,只是更加详细。默认情况下,数据点的跨度和间隔是不同的,但是我们可以很容易地将图表更改为 24 小时和 1 分钟的平均值,以匹配 OpsWorks 中的图表。单击 5 分钟展开间隔下拉列表,并更改为 1 分钟。使用图表右侧的时间范围过滤器(图中未显示),将 From 字段更改为 24。然后单击更新图表。您现在应该会看到与在 OpsWorks 中看到的相同的数据集。您也可以将鼠标悬停在图表中的直线上,查看每个点的更多详细信息。

CloudWatch 中另一个有用的特性是可以查看多个指标。在我们的例子中,与总的可用内存相比,使用的内存量是一个更有价值的指标。在度量列表中,选择 memory_total 度量。您的 memory_used 图应该变成橙色,memory_total 指标将显示为蓝色。这看起来不太对吧?如果 memory_used 线高于 memory_total 线,则必须翻转坐标轴。将鼠标放在其中一行上,找到 Y 轴属性。然后,单击“切换”来更改轴。现在您应该对这两个指标都有了一个视图,向您显示在过去的 24 小时内您的可用内存使用了多少(参见图 7-7 )。

A978-1-4842-0653-9_7_Fig7_HTML.jpg

图 7-7。

Memory used vs. memory total

正如你在图 7-7 中看到的,我有 600MB 的总可用内存,我在过去 24 小时的使用量大部分都在 400MB 到 500MB 之间徘徊。请记住,这不仅仅是 Node.js 应用的内存使用情况;它包括在实例上运行的所有软件,包括操作系统。下一个问题是如何处理这些信息。

CloudWatch 警报

当您的指标超过特定阈值时,您可以配置一个 CloudWatch 警报,向您或您的团队发送通知。您可以使用 CloudWatch 中可用的任何指标在您的帐户上创建多达 5,000 个警报,在撰写本文时,这些警报每月每个警报的费用为 0.10 美元。创建这些警报的主要目的是量化应用堆栈中发生的事件,从而导致手动或自动响应。

创建有用的警报可能不像听起来那么容易。如果您的应用出现问题,您完全有可能创建警报,然后完全忽略正在发生的事件。例如,假设您正在创建警报,以通过 HTTP 监控应用的输出(稍后将详细介绍)。您可以创建一个警报,当 HTTP 响应代码 500 返回给用户时触发该警报,但是由于代码中的一些不可预见的错误,您的应用只是挂起,并且请求超时。您的应用将没有响应,而您永远也不会知道!

创建警报时,选择一个指标和一个比较运算符(大于、小于、大于或等于等)。).您不能直接比较两个指标来创建警报。例如,您不能创建当已用内存> =总内存时发出的警报。您必须将警报配置为在已用内存> = 600,000 时响起。不幸的是,这个警报并没有那么有用,除非您打算将实例保持在特定的比例。

乍一看,你可能会想,当我们使用所有 600MB 时,警报不会响起吗?那么,我们不能添加新的实例,并在警报停止时关闭它们吗?当您向您的层添加另一个实例时,也会有与该实例相关联的大量内存开销,因此这可能不是最实用的警报。只要您有更多的实例(因此有更多的内存)在线,内存占用可能会错误地保持警报状态为活动状态,而实际上事件不再发生。

报警有三种可能的状态:正常、报警和数据不足。当报警条件为false时,出现正常状态。如果你的闹钟被设计成在 memory_used == 600,000(据我们所知,并没有那么有用)时响起,它将处于 OK 状态,直到满足这个条件,此时它将切换到 alarm。INSUFFICIENT _ DATA 状态意味着没有足够的数据来确定报警是处于正常状态还是报警状态。当第一次创建警报时,如果没有收集足够的数据,或者由于某种原因,度量数据当前不可用,您可能会看到此警报。如果您长时间看到这种状态,这意味着您的警报有问题,您应该调查为什么它没有按预期工作。

警报周期

当然,AWS 基础设施和您的应用都容易出现问题,就像任何其他技术一样。如果您的应用慢了几秒钟,或者单个用户经历了延迟,您不一定希望所有的警报都响起。为此,您必须定义检查报警状态的时间间隔以及构成报警状态的连续周期数。

假设我们要根据应用层的 CPU 利用率设置一个警报。如果 CPU 利用率大于 50%,我们希望得到通知。这是一个相当重要的指标——如果我们的 CPU 利用率太高,这意味着我们的实例超负荷工作,我们的应用将变得无响应。因此,我们应该将闹铃的间隔或周期设置为一分钟,这是一种相当常规的健康检查。也就是说,如果 CPU 利用率出现孤立的峰值,我们不一定希望警报响起。

CPU 利用率是您的应用正在经历过度需求事件的一个很好的指标,因此通过向应用堆栈添加更多实例来对此做出响应是有意义的。然而,我们现在知道 EC2 实例不会立即启动。如果你的闹钟前一分钟响了,下一分钟又停了,然后又响了,你可能会让你的额外实例不停地启动和关闭。这将构成巨大的资源浪费,并且不断地得到假警报将是令人恼火的。因此,假设三个连续的警报周期构成一个事件,那么我们将配置我们的警报,如果我们的 CPU 利用率在三分钟内大于 50%,就触发警报。

简单通知服务(SNS)

在我们开始创建 CloudWatch 警报之前,我们必须快速转向简单的通知服务。这项服务允许您将 CloudWatch 警报与各种通知方法联系起来,包括 HTTP、电子邮件、移动推送通知和 SMS。像许多其他服务一样,这项服务本身就值得一本书。我们只会将 SNS 用于其最简单的用途:当 CloudWatch 警报响起时,向一组用户发送电子邮件。

从 AWS 服务菜单中,选择简单通知服务。您会注意到这里的几个关键术语,主要是主题和订阅。就像其他 AWS 服务一样,一个主题有一个唯一的 ARN(Amazon Resource Name——AWS 生态系统中的全局 ID ),它本身就是一个资源——通知事件的目标。我们将为所有与相册基础设施相关的警报创建一个主题。然后,我们将为每个管理员创建订阅,当在此主题下生成通知时,将向他们发送电子邮件。虽然订阅也可以是 URL、SMS 收件人或其他端点,但在我们这个简单的例子中,您可以将订阅视为用户的电子邮件地址。

让我们首先为相册管理员创建一个主题。在 SNS 仪表板的中央,您应该会看到一个标记为“创建新主题”的按钮(参见图 7-8 )。

A978-1-4842-0653-9_7_Fig8_HTML.jpg

图 7-8。

SNS dashboard

将出现一个模式弹出窗口,提示您输入主题名称和可选的显示名称。在这两个字段中,输入 PhotoalbumsAlarms,因为该主题将仅由我们的应用堆栈的 CloudWatch 警报触发(参见图 7-9 )。单击创建主题。

A978-1-4842-0653-9_7_Fig9_HTML.jpg

图 7-9。

Create SNS topic

您将立即转到主题详细信息视图,在这里您可以查看主题的基本信息以及主题的订阅。现在,我们只需要为这个主题创建一个订阅:给应用的唯一管理员发送一封电子邮件。继续并点击“创建订阅”按钮。另一个模态将会出现。展开主题下拉列表并选择电子邮件。在端点字段中输入您的电子邮件地址,然后单击订阅。

该窗口将显示一条消息,通知您必须确认电子邮件地址。单击关闭返回主题详细信息视图。您应该会收到一封主题为“AWS 通知-订阅确认”的电子邮件。在电子邮件正文中,应该有一个指向订阅确认 URL 的链接。点击它会引导你进入一个类似于图 7-10 所示的页面。

A978-1-4842-0653-9_7_Fig10_HTML.jpg

图 7-10。

An SNS subscription confirmed

如果您返回到主题详细信息视图并单击刷新,您将看到已经为您的电子邮件生成了一个订阅 ID(ARN),就像您在订阅确认页面上看到的一样。现在你已经准备好使用这个 SNS 话题了!我们可以通过手动发布到这个主题来运行一个简单的测试。在主题细节视图的顶部,您会看到一个发布按钮。点击这个,另一个模态就会出现。填写一个主题和一些测试内容的消息,如图 7-11 ,点击发布消息按钮。然后,再次检查你的电子邮件。你应该会看到一条来自< no-reply@sns.amazonaws.com >的主题信息。

A978-1-4842-0653-9_7_Fig11_HTML.jpg

图 7-11。

Publishing an SNS topic

创建云监控警报

现在,我们已经为警报创建了一个 SNS 主题,我们准备创建我们的第一个 CloudWatch 警报。不幸的是,对于读者来说,每一个 CloudWatch 指标都有自己的讨论主题,需要对这个主题进行一些阐述。这一次,让我们使用 CloudFront 的指标来创建一个警报。如果您还记得,通过 Web 对我们的应用的所有请求都将通过我们的 CloudFront 发行版。例如,知道是否有许多请求正在被发出可能是有用的。

使用服务菜单,返回到 CloudWatch 仪表盘。这一次,看看左边的导航。在 Metrics 标题下,您将看到您拥有 CloudWatch 指标的所有服务的列表。单击 CloudFront,这将用 CloudFront 分布的指标填充主视图(参见图 7-12 )。

A978-1-4842-0653-9_7_Fig12_HTML.jpg

图 7-12。

ELB metrics in CloudWatch

其中一些指标可能非常有用。列表中的前两个,4xxErrorRate 和 5xxErrorRate 特别有趣。这些指标跟踪由 400–500 个错误代码响应的 HTTP 请求的百分比。4xxErrorRate 通常与 404 或资源未找到错误相关联。由于用户错误——人们输入了错误的 URL,或者客户端出现了请求格式不正确的问题,预计会出现少量的 404 响应。但是,如果 4xx 错误在我们发送的回复中占了很大比例,那么就有一个值得调查的问题了。虽然没有一个神奇的数字,但我们同意 25%导致 4XX 错误的请求将构成一个事件。

同样,5XX 错误构成内部服务器错误。一个用户也许能够用一个糟糕的构造请求引发一个 404,但是在我们的应用中,一个用户不应该引发一个 500 错误。我们应该容忍比 400 更低的 500 错误代码阈值。如果我们要为此指标创建一个 CloudWatch 警报,我们可能会在比率大于 0%时触发警报。

Note

TotalErrorRate 是接收任何非 200 HTTP 响应的请求的百分比,它是前两个度量的组合百分比。

Requests 指标统计了对您的 CloudFront 发行版的原始请求数。理论上你可以用它来识别分布式拒绝服务攻击, 1 或者跟踪流量的激增。

对于后一点,这不是检测非恶意流量激增的最佳方式。您最好通过查看实例的 CPU 和内存来识别流量激增。首先,并非所有的请求都是平等的。一千个用户上传照片不应该被解释为等同于一千个用户请求图像——对系统的影响是完全不同的。此外,许多到达 CloudFront 的请求,比如对图像的请求,根本不会到达应用堆栈,因此会影响性能。

让我们首先创建一个简单的警报,它将在我刚才讨论的条件下触发,即当 4XX 错误率大于 25%时。为您的分配选择 4xxErrorRate 行。图表视图将出现在下面,图表上可能有也可能没有点,这取决于您如何使用您的应用。在右下角,你会看到一个写着“创建警报”的按钮(见图 7-13 )。单击该按钮开始该过程。

A978-1-4842-0653-9_7_Fig13_HTML.jpg

图 7-13。

Graph view tools and Create Alarm button (cropped to show time-range interface)

定义警报

您现在应该处于警报创建过程的第二步,已经为您的警报选择了指标。该视图中有三个标题:报警阈值、报警预览和动作(参见图 7-14 查看完整视图)。在警报阈值下,我们命名并定义警报。在“名称”字段中,输入 photo albums CloudFront http 400>25%。警报的名称将出现在您的电子邮件通知中,因此您希望它是描述性的。描述字段允许您输入警报的更长形式的描述,因此您可以输入诸如相册 CloudFront 分发请求的平均百分比已超过 25%之类的内容。如果你愿意,你甚至可以进一步描述。

A978-1-4842-0653-9_7_Fig14_HTML.jpg

图 7-14。

Defining an alarm

在描述的下面,您会找到警报的实际参数。“无论何时”字段已经设置为您选择的度量。在 is:行中,将比较运算符设置为>,并在相邻的字段中输入 25。

查看右栏,如果现在启用了报警,报警预览会显示报警的当前状态。从图表中可以看出,当越过红线时,警报就会响起。目前,没有任何请求,所以在我的图表上甚至没有一条蓝线。这将导致警报处于不足数据状态。您会注意到在这一列的底部有用于周期和统计的字段。将周期设置为 5 分钟,这将是根据警报阈值评估指标的频率。统计是我们衡量的价值。在这种情况下,平均就可以了。在某些情况下,您可能想测量指标的最大值或最小值。

回到左侧,Actions 标题表示 CloudWatch 对警报响起的自动响应。您将看到您的选择是添加通知或自动扩容操作。对于我们的设置,自动扩容操作不是一个可行的选项。有了一个更加微观管理的应用堆栈,您可以配置额外的 EC2 实例来自动启动或关闭以响应警报。因为我们使用 OpsWorks 来管理我们的实例,所以我们将在那里配置我们的自动伸缩。有了这个 CloudWatch 警报,我们所要做的就是生成一个通知。单击+通知创建您的第一个通知。将通知配置为每当此警报:至状态为警报时设置,并将通知发送至:照片相册警报。图 7-14 为完整视图。

警报状态

现在,当警报处于警报状态时,您将收到通知。您还可以创建一个通知,在警报处于正常状态时发出。再次点击+通知,并在该警报:状态正常时设置该字段。然后,单击创建提醒。

您应该会在页面顶部看到一条成功消息,表明您的警报已经创建。如果你看左边的导航,你会看到报警标题实际上给你一个当前报警状态的总结(见图 7-15 )。因为您刚刚创建了警报,所以它的状态为“不足 _ 数据”。过一会儿,它应该会变为正常。

A978-1-4842-0653-9_7_Fig15_HTML.jpg

图 7-15。

CloudWatch alarm states

一会儿,我们会发现这个警报的一个问题,因为我们永远不知道什么样的机器人,脚本或随机流量会在您的域中运行。尽管如此,它将很好地提醒我们所拥有的工具。如果你想触发警报,你所要做的就是向不存在的路径发出几个请求,比如http://www.[ yourdomain ].com/helloworld。几分钟后,您应该会收到一封电子邮件,指示该警报处于警报状态。

如果你再等一会儿,闹钟可能很快就会响。那么我们该如何处理这些信息呢?拥有如此复杂的基础设施的挑战之一是熟悉各种故障点。

特别是这个警报是基于 CloudFront 度量的,所以我们应该做的第一件事是检查 CloudFront,看看问题是什么。继续前进,导航到 CloudFront 仪表板。在 CloudFront 导航中,单击热门对象。您可能还记得,这提供了一份关于对您的 CloudFront 发行版的流行 URL 请求的报告(参见图 7-16 )。

A978-1-4842-0653-9_7_Fig16_HTML.jpg

图 7-16。

CloudFront Popular Objects

如图 7-16 所示,我的发行版中最受欢迎的对象是/robots.txt文件,它不见了。搜索引擎!他们在这个域上寻找一个robots.txt文件,并得到一个 404 响应。您会遇到同样的问题,您可以通过在您的 Express 应用中添加一个静态路径到一个robots.txt文件来解决这个问题。关键是你知道如何创建一个警报,调查问题,并可以确定一个响应。

Note

在弹性负载平衡级别也有类似的指标。您可以监视应用堆栈中的负载平衡器,以获得相同的响应,而不是从 CloudFront 计算 400 和 500。

将 OpsWorks 与 CloudWatch 配合使用

回想一下,当您将实例添加到 OpsWorks 应用层时,有三种类型的实例:24/7、基于负载和基于时间。目前,我们有一个全天候运行的实例。接下来,让我们向应用添加一些基于负载的实例。这样,我们将设计我们的应用来有效地处理增加的需求。在本节的后面,我们还将研究基于时间的实例。

对于一个 web 应用来说,流量的波动是正常的,我们不想手动响应流量的起伏。我们更愿意灵活地使用我们的资源,这样它们就可以根据需求自动扩展。但这种策略只会带来更多的问题。

假设您的应用堆栈通常可以处理运行在 t1.micro EC2 实例上的 500 个用户。您通常预计流量会增加到 1,000 个用户,所以您希望在这种情况下有可用的资源(这些完全是虚构的数字)。然而,通常情况下,您可能有基础设施预算,因此您必须有效地管理您的资源。我们不能只是扔 100 台服务器来解决这个问题。

在这些约束条件下,我们将配置 OpsWorks 来自动检测应用层实例中的事件,并继续添加新的服务器,直到事件得到解决,此时我们的额外实例将自动关闭。

NOTES ON DETERMINING SCALING BEHAVIOR

稍后,我们将开始使用指标和计划来扩展我们的基础架构。在一个完美的世界里,你可以从这一课中得到确切的数字。不幸的是,它比那更抽象一点。你将不得不用你自己的方法来决定最佳策略。您可以用一定数量的用户测试您的应用性能,检查指标,并由此推断您需要的资源,尽管这种方法可能不准确。例如,如果五个用户正常使用应用使您的实例达到 5%的 CPU 利用率,二十个用户达到 10%的 CPU 利用率,您可以通过这种方式进行测试来预测曲线。

在为您的决策提供信息方面,没有什么比现实世界的运营历史更好的了。有些人更喜欢对他们的应用软件进行试运行或封闭测试。其他公司在发布时部署了多余的资源,然后小心翼翼地缩减到更保守的部署。所有这些都是一门艺术。根据我的经验,两个不同的应用可能会以完全不同的标准体验速度变慢。我们还将关注基于时间的扩展,这是基于您的应用的独特流量模式。因此,我无法告诉您何时触发扩容操作,但可以向您展示您将使用的工具。在这个场景中,我们将使用一些实例,这些实例代表了您实际上可以使用大量功能强大的实例做些什么。

基于负载的实例

面对现实吧,一个 t1.micro 实例是不够的!我们需要添加一些实例来响应需求。前往 OpsWorks,选择您的应用堆栈。打开导航下拉菜单,在实例标题下,您将看到一个基于负载的实例的链接(参见图 7-17 )。单击该按钮,您将看到基于负载的实例的空白视图。

A978-1-4842-0653-9_7_Fig17_HTML.jpg

图 7-17。

Load-based instances in the Navigation menu

在屏幕中间,您应该会看到以下消息:No load-based instances。添加基于负荷的实例。

单击“添加基于负荷的实例”。您会认出在第二章中添加第一个实例时出现的视图。默认主机名应该没问题。为了节约起见,将大小更改为 t1.micro。您应该选择不同的可用区域,例如 us-east-1b(参见图 7-18 )。如果您还记得的话,最佳实践是将您的实例分布在不同的可用性区域,以防 AWS 中断。这意味着我们应该跨可用性区域分布 24/7 个实例,但我们暂时将就一下。

A978-1-4842-0653-9_7_Fig18_HTML.jpg

图 7-18。

Adding a new instance to the application layer

但是到目前为止,这与创建一个 24/7 的实例没有任何区别。接下来,在创建实例之前,单击“高级”查看其他设置。高级设置应类似于图 7-19 中的设置。因为我们是从基于负荷的实例视图中创建实例的,所以预先选择了基于负荷的扩容类型。我们不必更改这些值中的任何一个,但是如果您从 general instances 视图中创建基于负载的实例,您必须确保在 advanced 视图中更改扩容类型。继续,然后单击“添加实例”继续。

A978-1-4842-0653-9_7_Fig19_HTML.jpg

图 7-19。

Load-based instance advanced configuration

OpsWorks 自动扩容规则

当您返回到“实例”视图时,您应该会在黄色警告框中看到以下消息:基于载荷的自动扩容已禁用-编辑。

您看到此警告是因为,虽然我们创建了基于负载的实例,但我们没有启用基于负载的伸缩,也没有定义实例联机的规则。单击“编辑”按钮即可。

首先:将基于负载的自动扩容启用开关切换到 Yes。接下来,让我们定义一些自动扩容规则。请记住,就您的应用而言,这些值基本上是任意的,您应该根据测试和操作历史来确定自己的扩容阈值。现在,我们将设计一个简单的规则集,如图 7-20 所示,之后我将讨论其行为。

A978-1-4842-0653-9_7_Fig20_HTML.jpg

图 7-20。

Load-based auto-scaling rules

当您使用 OpsWorks 自动伸缩时,您将使用一个简化的界面,该界面使用我们之前使用的底层 CloudWatch 指标。然而,在使用 OpsWorks 和使用 CloudWatch 进行扩容之间存在一些差异。在 OpsWorks 中设置自动扩容规则时,可以在不同的规则集中分别定义放大和缩小规则。放大或缩小时,可以设置每个扩容操作要添加/移除的实例数。您的扩展操作不是为每个指标创建警报和自动扩展操作,而是基于每个时间间隔对最多三个指标的评估:平均 CPU (%)、平均内存(%)或平均负载。您不必在扩容规则中使用所有这三种元素。如果您使用所有三个指标,当任何指标超过您定义的阈值时,将发生扩容操作。

对于 CloudWatch 警报,我们为指标设置了一个周期,以及触发警报之前的连续周期数。在 OpsWorks 中,为了清楚起见,我们为扩容设置了一个单一的时间框架,我们称之为阈值超出时间。这是一个或多个阈值必须被超过才能应用比例规则的时间量。阈值超过时间触发扩容操作,之后是一个时间间隔,在此期间度量被忽略。我们将这个间隔称为忽略度量间隔。这个时间间隔的原因是,它为新实例提供了一个宽限期,以减少堆栈中现有实例的工作负载。让我们根据图 7-20 中显示的值,分解我们可以预期的行为。

自动扩容场景 1

考虑以下场景:

A single 24/7 instance is online.   Average CPU utilization reaches 51% and remains there for five minutes.   Threshold exceed time for Up rule is met.   A single load-based instance is started.   Metrics are ignored for five minutes.   Metrics are checked again. Average CPU utilization is now reduced to 23%.   Ten minutes pass.   Average CPU utilization is still below 30%.   Threshold exceed time for Down rule is met.   Load-based instance is stopped.

基于负载的实例运行时间:15 分钟

根据我们定义的自动扩展规则和创建的实例,仍有有限的资源可以部署到我们的应用堆栈,这有助于我们控制成本,但我们的能力是有限的。考虑使用完全相同配置的另一个场景。

自动扩容场景 2

考虑以下场景:

A single 24/7 instance is online.   Average CPU utilization reaches 75% and remains there for five minutes.   Threshold exceed time for Up rule is met.   A single load-based instance is started.   Metrics are ignored for five minutes.   Metric checks are resumed. Average CPU utilization is at 63% and remains there for five minutes.   No additional load-based instances are available.   Average CPU utilization remains at 63% for an additional five minutes.   No additional load-based instances are available.   And so on and so on...

基于负载的实例运行时:不确定

如您所见,尽管我们同时拥有 24/7 和基于负载的资源,但我们并没有准备好处理应用的需求。在这种情况下,我们的基于负载的实例也可以是 24/7 实例,如果它始终在线以满足我们的基线流量要求。此外,我们资源的缺乏可能会开始影响应用的性能。

这个问题有几种可能的解决方法。最简单的解决方案是添加额外的 24/7 实例。如果我们认为这是暂时的需求激增,那么添加一个额外的基于负载的实例会更划算。让我们继续这样做,然后在另一个场景中回顾该行为。

记得单击保存以创建您的扩容规则。扩容规则应该不再可编辑。然后,单击表格下方的+实例按钮。再次选择 t1.micro 实例大小。选择不同的可用区域,例如 us-east-1c(参见图 7-21 )。单击“添加实例”创建第二个基于负荷的实例。

A978-1-4842-0653-9_7_Fig21_HTML.jpg

图 7-21。

Adding a second load-based instance

您将在扩容规则下看到如下摘要:2 个实例中的 0 个正在运行–显示> >。

使用我们当前的扩展规则,让我们再来看看在有第二个基于负载的实例可用的情况下,我们的实例将如何进行不同的扩展。

自动扩容场景 3

考虑第三种情况:

A single 24/7 instance is online.   Average CPU utilization reaches 75% and remains there for five minutes.   Threshold exceed time for Up rule is met.   A single load-based instance is started.   Metrics are ignored for five minutes.   Metric checks are resumed. Average CPU utilization is at 63% and remains there for five minutes.   Threshold exceed time for Up rule is met.   A second load-based instance is started.   Metrics are ignored for five minutes.   Metric checks are resumed. Average CPU utilization is at 25% and remains there for ten minutes.   Threshold exceed time for Down rule is met.   The second load-based instance is stopped.   Metrics are ignored for ten minutes.   Metric checks are resumed. Average CPU utilization is at 23% and remains there for ten minutes.   Threshold exceed time for Down rule is met.   The first load-based instance is stopped.   A single 24/7 instance is online.

基于负载的实例运行时间:60 分钟

因为我们已经定义了自动扩展规则,当我们向上扩展时添加一个实例,当我们向下扩展时删除一个实例,所以我们能够根据需要进行扩展,从而非常有效地利用资源。在这个场景中,我们基于负载的实例运行了 60 分钟:第一次运行了 45 分钟,第二次运行了 15 分钟。你会注意到,我们放大和缩小时间的差异让我们在谨慎方面犯了错误。虽然我们在向上扩展后仅等待 5 分钟,然后再记录要再次扩展的指标,但我们在开始记录要向下扩展的指标之前会等待 10 分钟,并要求在停止实例之前达到阈值 10 分钟。

假设我们再添加 100 个实例,并继续使用相同的扩容规则。如果需求激增,我们的应用堆栈会每 10 分钟自动让一个新实例上线,而我们的实例超过了纵向扩展阈值,每 15-20 分钟就会让实例离线。面对极端的需求,这种反应可能不够迅速。您可以将阈值超过时间减少到 1 分钟,并将忽略度量间隔减少到 1 分钟。

自动扩容场景 4

最后,考虑这个场景:

A single 24/7 instance is online.   Average CPU utilization reaches 75% and remains there for one minute.   Threshold exceed time for Up rule is met.   A single load-based instance is started.   Metrics are ignored for one minute.   Metric checks are resumed. Average CPU utilization is at 63% and remains there for one minute.   Threshold exceed time for Up rule is met.   A second load-based instance is started.   And so on...

使用更积极的纵向扩展规则,每两分钟就可以让一个实例上线。您可以保持缩减规则不变,因此一旦实例联机,我们会等待一段时间以确保事件结束,然后再减少资源。

基于时间的实例

您可以在 OpsWorks 中创建最后一种类型的实例:基于时间的实例。如果您知道在大多数情况下,您的应用在特定时间会有较高的流量,您可以创建在高流量窗口自动启动和停止的实例。同样,如果您知道通常流量很少的时候,您可以使用基于时间的实例,否则您可能会使用 24/7 实例。

结合使用所有这三种实例类型是运行应用的最有效方式,并且是在 OpsWorks 中实现比手动实现容易得多的策略。您对基线资源使用 24/7 实例,对较高的流量时间使用基于时间的实例,同时使用基于负载的实例来响应需求的增加。如果您在基于时间的实例计划脱机时遇到流量激增,那么基于负载的实例仍然可以响应。

让我们开始向应用添加基于时间的实例。使用 OpsWorks 中的导航菜单,选择实例标题下的基于时间的实例。您将看到一条消息,就像我们在基于负载的实例视图中看到的一样:没有基于时间的实例。添加基于时间的实例。

单击添加基于时间的实例以创建单个基于时间的实例。和前面一样,在您喜欢的任何可用性区域中选择一个 t1.micro 实例。当您点击高级时,您将看到扩容类型被设置为基于时间(参见图 7-22 )。单击“添加实例”进入“计划创建”视图。

A978-1-4842-0653-9_7_Fig22_HTML.jpg

图 7-22。

Adding a time-based instance

在 Schedule Creation 视图中,您可以为应用层中每个基于时间的实例手动设置时间表。界面相当简单:您选择一个小时的时间段(UTC ),在此期间您的实例将处于在线状态。

继续点击 12 和 13 之间的方框,如图 7-23 所示。保存更改时,您会看到一个简短的活动指示器。

A978-1-4842-0653-9_7_Fig23_HTML.jpg

图 7-23。

Time-based instance scheduling

现在,您的实例将从 UTC 时间每天下午 12 点至 1 点运行。假设您还希望它在 UTC 时间周五晚上 6-8 点运行。单击“Fri”选项卡,仅选择星期五的时间表。您将看到您的每日小时数被自动选择,您只需选择您想要在周五运行的额外小时数。选择 18–19 和 19–20 街区,将星期五晚上添加到您的选择中(参见图 7-24 )。

A978-1-4842-0653-9_7_Fig24_HTML.jpg

图 7-24。

Time-based instance scheduling—single-day view

通过 OpsWorks 使用警报

您应该能够让您的应用自动伸缩,但您可能希望在发生这种情况时得到通知。当您的所有实例都在线时,您特别希望得到通知。正如我所讨论的,如果所有基于负载的实例都在线,这并不能保证当前的事件已经解决。我们希望在达到或接近最大容量时发出警报,通知我们。从那里,您可以监视情况并手动添加新的实例。

不幸的是,目前没有 CloudWatch 指标来跟踪您当前的 OpsWorks 实例数。但是,在 ELB 级别有两个非常有用的指标:健康主机和不健康主机。这些指标告诉我们连接到负载平衡器的处于健康状态和不健康状态的实例的数量。你会记得我们很久以前就定义了健康和不健康!

我们可以创造性地使用这些指标来通知我们问题。例如,当所有实例都在线时,当健康主机==最大实例数时发出的警报就会响起。当不健康的主机> 1 时,可能会发出另一个警报。这样,当我们的所有实例都在线时,或者当我们的一个实例处于错误状态时,我们就会收到警报。

这种方法可能不是在所有情况下都有效。例如,如果您有 100 个实例在运行,您可能不需要对一个不正常的实例做出响应。此外,您还需要考虑正在使用的基于时间的实例的数量,因为这将影响应该发出警报的健康主机的数量。

ELB 监控

要创建您的 ELB 警报,您不必通过 CloudWatch 仪表盘,您也可以从 ELB 视图本身来完成。可以通过 EC2 仪表板访问 ELB 实例。从服务菜单中选择 EC2。在左侧导航中,选择网络和安全标题下的负载平衡器。

如果您只有一个负载平衡器,它将被自动选择。如果没有,现在就选择它。然后,导航到“监控”选项卡。在这个选项卡中,您可以看到许多绘制在图表上的 CloudWatch 指标,如图 7-25 所示。

A978-1-4842-0653-9_7_Fig25_HTML.jpg

图 7-25。

ELB Monitoring view

您会注意到右手边有一个创建警报按钮。正如您可能已经猜到的,这允许您为所选的负载平衡器创建一个 CloudWatch 警报。

当你点击这个按钮时,你会看到一个模态视图,它只是 CloudWatch 界面的精简版。向您的社交网络主题发送通知。在“无论何时”字段中,选择“普通”和“健康”主机。Is 应该> = 3,这是您的 24/7 实例和基于负载的实例的总和。至少可以设置为 1 个连续 1 分钟的时间段(见图 7-26 )。

A978-1-4842-0653-9_7_Fig26_HTML.jpg

图 7-26。

Create ELB alarm

在此视图中创建闹钟时,您可以给闹钟命名,但不能设置描述。自动生成的名称对于电子邮件通知来说可能不够清楚。如果你愿意,可以把闹钟的名字改成像相册这样的东西——所有在线实例。如果一切正常,单击创建警报。

同样,这是一个不完善的方法。当您有一个基于负载的实例、一个基于时间的实例和一个 24/7 实例在线时,此警报将会响起。创建警报后,模式窗口将确认操作成功(参见图 7-27 )。在此模式中,CloudWatch 仪表板中有一个警报链接,您可以从中访问警报详细信息的完整视图。

A978-1-4842-0653-9_7_Fig27_HTML.jpg

图 7-27。

ELB alarm confirmation

接下来,让我们为不健康的主机创建警报。请注意,如果您的应用由于代码中的异常而崩溃,它将自动重启。单个不正常的实例可能无法保证响应。再次单击“创建提醒”按钮,并将通知发送到同一个 SNS 主题。这一次,只要不健康主机的平均值> = 1,警报就会响起。为了降低警报的敏感度,您可以设置它在连续两个一分钟的时间段过去时触发。正如您在右图中看到的,现在应该没有任何不健康的主机。将您的闹钟命名为 Photoalbums - 1 或更多不健康的主机,如图 7-28 所示,然后再次点击创建闹钟。

A978-1-4842-0653-9_7_Fig28_HTML.jpg

图 7-28。

Creating a second ELB alarm

如果你看看其他的 ELB 指标,你会发现它们大部分都很有说服力。您可能希望设置额外的通知来监视您的应用的健康状况。例如,平均延迟度量跟踪实例返回对请求的响应需要多长时间。您可以为此指标设置警报,以监控应用性能的下降。

Note

请记住,当您在没有完整的 CloudWatch UI 的情况下创建内嵌警报时,当警报返回正常状态时,您将不会收到通知。

自动扩容摘要

您已经看到了手动向应用添加实例是多么容易,现在您可以根据需求或计划自动扩展应用。我们已经创建了一些警报,帮助您监控您的应用并对事件做出响应。ELB 级别的警报展示了你可以创造性地使用警报的许多方法中的几个。

您可以随意创建更多的警报来帮助您进行监控。创建警报时,挑战在于保持良好的信噪比。您不希望一系列警报被忽略——最好只有在您必须对应用中的问题保持警惕时才触发警报。

RDS 警报

如果你还记得第三章中的 RDS 课程,你会知道我们的 RDS 数据库中内置了许多冗余和故障保险。我们使用调配的 IOPS 为我们的实例保留增加的 I/O 容量,并在必要时使用多 AZ 部署将请求重新路由到备份实例。出于备份目的,我们还会定期自动拍摄数据库快照。

有了所有这些工具,RDS 实例在很大程度上应该可以自我维护。尽管如此,我们不想被任何问题弄得措手不及。我们可以轻松地增加数据库的磁盘空间,或者从具有更大容量的快照创建一个新实例。我们可以在 OpsWorks 中轻松地交换数据库凭证,因此如果我们需要快速部署备份数据库,我们已经学会了如何这样做。简而言之,我们有应对重大危机的手段。

让我们继续创建一些警报来通知我们 RDS 实例的任何事件。这些事件可能不需要作出重大反应,但它们应该引起进一步的调查。

导航到 RDS 仪表板,在那里应该会自动选择您的实例。单击顶部的显示监控。与 ELB 一样,嵌入式监控视图允许您访问所选实例的 CloudWatch 指标(参见图 7-29 ),只是这一次,Create Alarm 按钮位于页面的更下方。

A978-1-4842-0653-9_7_Fig29_HTML.jpg

图 7-29。

RDS monitoring

您将在这里看到许多有用的指标,大部分是原始数据,而不是百分比。这意味着,如果您扩展您的数据库,您可能需要相应地调整警报,就像我们在本章开始时看到的度量标准一样。

根据您阅读这本书花了多长时间,您可能已经有了一些数据库的操作历史。查看图 7-30 中的 CPU 利用率,您可以通过单击微型 CPU 利用率图来访问该图。

A978-1-4842-0653-9_7_Fig30_HTML.jpg

图 7-30。

RDS instance CPU utilization

如您所见,在绝大多数时间里,我的 CPU 利用率保持在 40%以下。有一个事件,它突然飙升到 100%。虽然 AWS 已经自动解决了该事件,但我们应该在将来收到该事件的通知。单击创建警报按钮。

对于此警报,我们希望在一分钟内收到 CPU 利用率> 60%的通知(参见图 7-31 )。我们再次在过于敏感而无用的警报和过于宽容而无法检测到事故的警报之间游走。60%的 CPU 使用率远远超出正常范围,我们将立即得到通知。单击创建提醒以完成提醒并返回 RDS。

A978-1-4842-0653-9_7_Fig31_HTML.jpg

图 7-31。

RDS CPU utilization alarm

您可能希望在这里为许多其他指标创建警报。例如,如果读 IOPS 或写 IOPS 指标偏离了它们的正常模式,这将构成一个事件。我们不必逐个地运行每个指标,因为我们知道我们必须分析模式的指标,并在模式严重偏离时创建警报。

CloudWatch 日志

熟悉 Node.js 中的本地开发后,您可能已经习惯了将日志直接打印到终端窗口中。不幸的是,有了 OpsWorks 应用堆栈,我们就失去了这种能力。相反,我们必须利用另一个名为 CloudWatch Logs 的特性。

CloudWatch 日志允许您在 AWS 控制台中对系统日志进行分组、存储和监控。虽然默认情况下没有启用它们,但只需几个步骤就可以让它们启动并运行起来。首先,我们将安装和配置 CloudWatch 日志,以存储由 OpsWorks 生成的一些系统级日志。一旦完成,我们将在 CloudWatch 中设置一些应用级别的日志记录。最后,我们将根据这些日志设置一个警报。

EC2 实例角色

在我们深入探讨这个问题之前,我们的应用堆栈中的实例将需要对 CloudWatch 日志的完全权限。如您所知,我们将不得不在身份和访问管理上再做一次停留。从服务菜单中选择 IAM。单击左侧导航栏中的角色,并从列表中选择 AWS-ops works-photo albums-ec2-role。同样,这是分配给应用堆栈中每个实例的角色。在 role detail 视图中,您应该看到您创建的允许实例访问 S3 和 SES 的策略(参见图 7-32 )。单击“附加策略”为 CloudWatch 日志创建另一个策略。

A978-1-4842-0653-9_7_Fig32_HTML.jpg

图 7-32。

Instance Role Policies

选择 CloudWatchLogsFullAccess 托管策略,然后单击附加策略。如果您查看策略文档,它将类似于清单 7-1 。

Listing 7-1. CloudWatch Logs Full-Access Policy

{

"Version": "2012-10-17",

"Statement": [

{

"Action": [

"logs:*"

],

"Effect": "Allow",

"Resource": "*"

}

]

}

您的角色已更新,但在实例重新启动之前,这些更改不会在实例上生效。现在让我们回到 OpsWorks 并设置日志记录!

在 OpsWorks 中使用 Chef

我们提到了 OpsWorks 背后的核心技术 Chef 的力量。现在我们终于要用上它了!别担心,我们不会写任何食谱,我们只是让它们为我们服务。

要将我们的系统日志写入 CloudWatch 日志,我们必须为应用堆栈中的每个实例安装一个 Chef cookbook。在每个食谱中有一个或多个配置脚本,称为厨师食谱。如果我们必须在每个实例上手动安装脚本,我们将面临一个巨大的任务。尤其是现在我们使用基于时间和负载的实例,在每个实例上手动安装脚本将非常不方便、耗时且难以维护。

当您在 OpsWorks 层中启动一个新实例时,流程中有许多阶段,每个阶段都会自动执行一些命令。事实上,服务器端包和 AWS 软件的安装是在 Chef 中管理的。在为每个阶段运行 AWS 配方之后,有机会运行定制配方。

写我们自己的食谱是一个完全不同的话题。幸运的是,Amazon 已经很慷慨地为我们提供了一个样本食谱,用于将 OpsWorks 日志发送到 CloudWatch。我们将在堆栈中实现示例食谱,并在实例的设置阶段执行食谱中的特定食谱。

安装烹饪书和食谱

要安装 cookbook,我们必须在 OpsWorks 中的堆栈级别进行更改。默认情况下,定制 cookbooks 是禁用的,所以当创建实例时,没有机会添加我们自己的定制。

导航到 OpsWorks 并从导航菜单中选择 Stack。然后,点按右上角的堆栈设置。在页面中间,您会看到一个标题,上面写着配置管理。我们要做的第一件事是更改全局配置,以允许安装定制的 Chef cookbooks。将“使用定制厨师烹饪书”切换到“是”位置。系统会立即提示您选择食谱的位置。虽然您可以随意添加任意数量的食谱,但是它们应该被打包到一个食谱中,存储在存储库或 zip 中。

我们将使用的定制食谱可以从这里的一个公共网址获得: https://s3.amazonaws.com/aws-cloudwatch/downloads/CloudWatchLogs-Cookbooks.zip 。选择 Http Archive 作为您的存储库类型(参见图 7-33 )。尽管它存储在 S3,因为它是一个公共文件,你可以通过 HTTP 访问它,就像网上的其他 zip 文件一样。

A978-1-4842-0653-9_7_Fig33_HTML.jpg

图 7-33。

Enabling custom cookbooks in OpsWorks

单击右下角的保存。接下来,我们必须配置实例,使其在启动过程中的特定时刻运行特定的配方。我们不在实例级别这样做!相反,我们必须转向层,在层中为所有附加到 Node.js 应用层的实例完成配置。在 Layers 视图中,您会注意到 Node.js App Server 下有一个编辑菜谱的链接,如图 7-34 所示。单击此处查看和编辑应用层的配方。

A978-1-4842-0653-9_7_Fig34_HTML.jpg

图 7-34。

Layers view revisited

在 recipes 视图的顶部,您会看到自动安装在实例中的内置 Chef 食谱。每个配方都以 cookbook::recipe 的格式列出,并显示在其执行阶段。这些是不可编辑的,因为它们由 AWS 控制。在 Custom Chef Recipes 标题下,您可以添加要在每个阶段的内置配方之后执行的自定义配方。我们必须在设置阶段添加两个配方。在设置右侧的文本框中,输入以下内容:

logs::config, logs::install

然后点击右边的+按钮。您的视图应该类似于图 7-35 。确认后,点击右下角的保存。

A978-1-4842-0653-9_7_Fig35_HTML.jpg

图 7-35。

Custom chef recipes

因为菜谱是在设置阶段运行的,所以我们必须在运行的实例上触发它。安装程序将在将来启动的任何实例上自动运行。如果您现在只有一个实例在运行,我们可以很容易地在它上面运行 Setup 命令。导航到 Instances 视图,并单击正在运行的实例的名称。在右上角,单击运行命令。在设置标题下,从命令下拉菜单中选择设置(参见图 7-36 )。然后点击右下角的设置。

A978-1-4842-0653-9_7_Fig36_HTML.jpg

图 7-36。

Run Setup command

您的实例完成安装命令需要一些时间。当实例建立时,我们正在运行的菜谱会自动将存储在/var/log/aws/opsworks/opsworks-agent.log的日志上传到 CloudWatch 日志。

CHANGING THE RECIPE

如果你喜欢冒险,你可以尝试上传不同的系统日志到 CloudWatch,而不是默认选择的日志。您的实例上没有可用的日志主列表。您可以尝试使用 SSH 连接到您的实例,查找您感兴趣的日志,然后修改食谱,选择不同的日志文件,从您的 S3 存储桶或存储库中上传和部署食谱。

CloudWatch 日志

一分钟后,你的日志就会出现在 CloudWatch 中。我们去看看。转到 CloudWatch 控制面板,并从左侧导航中选择 Logs。你很快就会看到一个名为“相册”的日志组,如图 7-37 所示。

A978-1-4842-0653-9_7_Fig37_HTML.jpg

图 7-37。

CloudWatch log groups

CloudWatch 日志有自己的层次结构。单个日志语句被称为一个事件。日志事件存储在一系列流中,这些流在顶层组织成组。在组级别,您可以控制日志保留策略,设置日志事件过期的时间段。在大多数情况下,您可能不想为您的堆栈保留完整的日志历史。如果在日志组视图中单击永不过期,您可以将保留期更改为一天到十年。

目前,让我们将日志保留时间减少到三天。单击永不过期,将显示编辑保留模式视图。从下拉列表中选择 3 天。您将看到一条消息,如图 7-38 所示,确认旧数据将被删除。单击确定进行确认。

A978-1-4842-0653-9_7_Fig38_HTML.jpg

图 7-38。

Changing CloudWatch log retention

另一个有趣的特性叫做度量过滤器。您可以创建指标过滤器来自动扫描日志中的特定术语或模式,并使用它们来创建自定义的 CloudWatch 指标。一旦您的日志生成了可量化的指标,您就可以创建一个 CloudWatch 警报,在指标超过某个阈值时通知您。

这是一个强大的功能,尽管没有得到应有的重视。我们可以自动扫描日志中特定类型的错误,并在检测到错误时触发警报。这个特性对于应用级别的日志记录非常有用:当错误发生时,我们可以得到通知,并快速做出响应。

在我们设置应用级日志记录之前,请单击相册日志组。在这个组中,您应该看到应用层中每个 EC2 实例的日志流:nodejs-app1、nodejs-app2 等。在启用自定义配方后,您将只能看到运行设置命令的实例。如果堆栈中有基于负载和基于时间的实例,它们会在启动时自动创建日志流。

将系统级日志存储在特定于实例的日志流中是有意义的。如果启动实例时出现问题,您可能希望查看特定于该实例的日志。对于应用日志来说,情况并非如此。当应用在多个实例上运行时,负载平衡器将决定哪个实例处理用户的请求。如果其中一个用户遇到错误,您无法确定错误发生在哪个实例上。最简单的解决方案是为我们的应用级日志记录创建一个单独的日志流,这将合并我们的应用中生成的所有日志事件。

应用日志流

虽然我们可以很容易地创建带时间戳的日志流,但是在这个例子中,我们只保留一个主日志流。我们将使用三天日志保留策略来防止我们的日志因历史数据过多而变得臃肿。

在“日志流”视图中,点按顶部的“创建日志流”按钮。将出现一个模式窗口,要求您命名日志流。输入名称 application,如图 7-39 所示,点击创建日志流。

A978-1-4842-0653-9_7_Fig39_HTML.jpg

图 7-39。

Creating an application log stream

您会注意到,虽然其他日志流在最后接收时间列中有时间戳,但是我们的新日志流没有时间戳。为了用数据填充我们的日志流,我们将使用 AWS SDK 以编程方式发布事件。在下面的例子中,我们将手动构造发送到日志的字符串。也就是说,这仅仅是功能的一个例子。完全可以将 CloudWatch 日志与其他日志库整合,比如 log4js、logger、winston 等。为了最大限度地减少对第三方模块的依赖,并保持对 AWS 工具的关注,我们选择了更简单的方法。但是,使用现有的日志库将简化日志数据的格式化和标准化过程。如果您选择了日志库,我强烈建议您基于下面的例子将它与 CloudWatch 日志集成在一起。

自定义 CloudWatch 日志记录类(cwlogs)

首先,让我们创建一个新类来抽象我们对 AWS SDK 的使用。在/lib目录中,创建一个名为cwlogs.js的新文件。在文件的顶部,我们必须添加 AWS SDK。我们还将存储一个对象,用于存储日志事件。见清单 7-2 。

Listing 7-2. /lib/cwlogs.js

var aws= require('aws-sdk');

var cloudwatchlogs = new aws.CloudWatchLogs({region:'us-east-1'});

var logParams = {

logGroupName: 'Photoalbums',

logStreamName: 'application',

logEvents: []

};

logParams对象被硬编码到“相册”日志组和“应用”日志流中。如果您想保存精心制作和组织良好的日志,您可以很容易地将这些动态日志发布到多个不同的日志流甚至日志组。例如,您可以为正常的应用活动保留一个日志流,为严重错误保留另一个日志流。

接下来,我们将添加一个从简单字符串创建日志事件的公共方法。该函数简单地创建一个具有属性messagetimestamp的对象,并将它们添加到logParams.logEvents数组中。添加清单 7-3 到/lib/cwlogs.js中的代码。

Listing 7-3. logEvent Function

// store event in logs to be sent to CloudWatch Logs

function logEvent(message){

var eventTimestamp = Math.floor(new Date());

var newEvent = {

message: message,

timestamp: eventTimestamp

}

logParams.logEvents.push(newEvent);

}

在创建时将每一条日志消息都上传到 CloudWatch 是一种资源浪费。相反,我们将在应用运行时聚集日志,并定期上传日志。你上传日志的频率完全由你决定。因为我们将日志事件分开并上传,所以我们有一个单独的发布方法来放置日志。将清单 7-4 添加到文件中。

Listing 7-4. putLogs Function

function putLogs(){

if(logParams.logEvents.length > 0){

getSequenceToken(function(err, token){

if(token){

logParams.sequenceToken = token;

}

cloudwatchlogs.putLogEvents(logParams, function(err, data) {

if (err){

} else {

logParams.sequenceToken = data.nextSequenceToken;

logParams.logEvents = [];

}

});

});

}

}

当您使用 AWS API 时,现有的日志流有一个序列标记。为了将日志事件发布到流中,必须首先检索该流的下一个序列标记。这个过程是在一个私有函数中执行的,我们很快就会回顾这个函数。如果令牌存在,cloudwatchlogs.putLogEvents()使用logParams对象将日志上传到流。如果成功,清空logParams.logEvents数组,并销毁内存中的日志事件。如果还没有日志事件被上传到流中,那么这个流就没有序列标记,当您使用putLogEvents时,您也不必包含一个序列标记。

为了获得下一个序列令牌,我们必须使用方法describeLogStreams来检索特定日志组的所有日志流。清单 7-5 在getNextSequenceToken函数中展示了这一点。添加这个函数和exports声明,使logEventputlogs成为公共的。

Listing 7-5. getNextSequenceToken Function

function getSequenceToken(callback) {

cloudwatchlogs.describeLogStreams({logGroupName:logParams.logGroupName}, function(err, data){

if (err){

callback(err);

} else {

for(var i = 0; i < data.logStreams.length; i++){

var logStream = data.logStreams[i];

if(logStream.logStreamName == logParams.logStreamName){

callback(null, logStream.uploadSequenceToken);

break;

}

}

}

});

}

exports.logEvent = logEvent;

exports.putLogs = putLogs;

集成 cwlogs

现在是时候开始向代码库添加一些基本的日志记录了!我们只看一个简单的例子。假设您想要在 web 服务中记录特定路线的工作流,将一些值打印到控制台。打开/routes/users.js,我们将在GET /users/user/:user路线中添加一些日志。首先,在文件的顶部包含cwlogs类,如下所示:

var cwlogs  = require('./../lib/cwlogs');

找到router.get(/user/:user)函数,我们将在其中添加几个日志语句。用清单 7-6 替换该功能。

Listing 7-6. getUser with Logging Enabled

router.get('/user/:user', function(req, res) {

var params= {

username: req.param('user')

}

var eventMessage = 'GET /users/user/' + params.username;

cwlogs.logEvent(eventMessage);

model.getUser(params, function(err, obj){

if(err){

res.status(500).send({error: 'An unknown server error has occurred!'});

} else {

var eventMessage = 'getUser ' + params.username + ' ' + JSON.stringify(obj);

cwlogs.logEvent(eventMessage);

res.send(obj);

}

});

cwlogs.putLogs();

});

我们在这里只记录几件事:请求的方法和路径以及从model.getUser()检索的对象。因为您可以将任何字符串传递给cwlogs,所以它足够灵活,您可以决定什么有效。在路线的终点,cwlogs.putLogs()将日志上传到 CloudWatch。应该只有两个条目,但是您可以轻松地添加更多条目,包括用户模型中的一些条目。

是时候点火了!将这些更改提交到您的存储库中,然后返回 OpsWorks 并选择您的堆栈。从导航菜单中选择部署,然后单击右上角的部署应用。如果愿意,添加一些部署注释,然后单击右下角的 Deploy。一旦您的部署完成,继续向/users/user/[ your username ]发出GET请求。

您应该会得到与之前相同的 JSON 响应,但是这一次,一些数据被添加到了您的 CloudWatch 日志中。返回到 CloudWatch,并从导航中选择日志。当您单击相册日志组时,您应该会看到应用日志流现在在“上次摄取时间”列中有一个值。这是个好兆头!单击应用查看日志流。您应该会看到类似图 7-40 的内容。

A978-1-4842-0653-9_7_Fig40_HTML.jpg

图 7-40。

Viewing the CloudWatch Log Stream

万岁!应用日志曾经如此激动人心吗?

异常处理

让我们进入下一个逻辑步骤,即将异常上传到 CloudWatch 日志。一旦我们这样做了,我们就可以创建一个度量过滤器并生成一个警报,通知我们应用中出现了一个异常。

让我们不要试图破坏应用,而是让一个错误发生。将以下代码添加到/routes/users.js:

router.get('/error', function(req, res) {

throw new Error("[ERROR] This is an intentional error");

});

这纯粹是为了测试的目的,你应该尽快删除它。接下来,我们将把cwlogs添加到 Express 中间件。打开/server.js,再次将cwlogs包含在顶部。

var cwlogs = require('./lib/cwlogs');

然后,定位如下所示的中间件功能:

app.use(function(err, req, res, next) {

});

对于这个函数出现的任何错误,我们都会将错误消息发送到 CloudWatch。将函数更改为类似于清单 7-7 。

Listing 7-7. server.js error-handling Middleware

app.use(function(err, req, res, next) {

cwlogs.logEvent(err.message);

cwlogs.putLogs();

res.status(err.status || 500);

res.send({

message: err.message,

error: {}

});

});

Note

您可能有这个中间件的开发和生产版本。如果要使用此功能,可以使用 OpsWorks 环境变量来覆盖 Express app 环境变量。

继续提交并部署此更改。现在,让我们创建一个度量过滤器来检测错误。返回到 CloudWatch 日志,然后单击相册日志组旁边的 0 过滤器。除了一个添加度量过滤器的按钮之外,您在这个页面上应该看不到什么。点击这个。

在“定义日志”度量过滤器视图中,您可以创建一个文本模式来匹配您的度量过滤器。我们将创建一个简单的例子,它计算任何带有文本“[ERROR]”的日志事件在过滤模式字段中,输入文本“[错误]”(包括双引号),如图 7-41 。如果您试图创建一个过滤器来捕获现有的日志数据,页面上有一个方便的工具来测试您的过滤器模式。单击分配指标继续。

A978-1-4842-0653-9_7_Fig41_HTML.jpg

图 7-41。

Create metric filter

在下一个视图中,您可以命名您的过滤器以及它过滤的指标。如果您有很多度量过滤器,那么过滤器名称对于管理度量过滤器非常有用,而度量名称是 CloudWatch 中实际计数的值的名称。您可以保持过滤器名称不变,但一定要将指标名称设置为 UncaughtErrors 之类的名称,如图 7-42 所示。然后,单击创建过滤器。

A978-1-4842-0653-9_7_Fig42_HTML.jpg

图 7-42。

Filter and metric names

您将在 Filters 视图中仅看到您的新过滤器,以及一条成功消息(参见图 7-43 )。在这里,您可以编辑或删除过滤器,或者更重要的是,根据指标创建警报。

A978-1-4842-0653-9_7_Fig43_HTML.jpg

图 7-43。

Photoalbums filters

单击“创建警报”为该指标创建新的 CloudWatch 警报。这个警报的想法是,任何时候抛出一个异常,警报就会响起。因此,我们希望每当 UncaughtErrors > = 1 时就发出警报。将闹钟命名为 Photoalbums-unccatched Error,使闹钟向 PhotoalbumsAlarm 列表发送通知,如图 7-44 所示。

A978-1-4842-0653-9_7_Fig44_HTML.jpg

图 7-44。

Metric filter alarm

点击创建报警,然后制作一个GET/users/error。如果您检查日志流,您应该会看到错误,并且您还应该会收到一个电子邮件通知,告知发生了错误。虽然您不会收到错误的电子邮件副本,但您确切地知道在哪里可以看到错误打印出来的内容!

摘要

从这里开始,你可以走很多不同的路。您可以更改错误日志记录来记录错误堆栈(提示:err.stack 而不是 err.message),或者您可以修改 cwlogs 来使用您喜欢的日志记录库。您还可以为不同类型的信息创建多个日志流,这取决于您希望如何组织。

在本章开始时,您对如何监控、维护和扩展我们的应用堆栈知之甚少。最终,您将拥有根据我们的规则自动扩展的服务器、监控各种故障点的警报,以及保存在 CloudWatch 中的多实例应用日志。呈现这一章的挑战之一是在有如此多的可能性的时候,保持核心课程的轨道!

在下一章,也是最后一章,我将把重点放在安全性上,最后,将认证添加到应用中。你可能已经注意到,任何人都可以直接走进去开始上传内容。像其他课程一样,我们的安全措施将是编码和使 AWS 服务工作的结合。

Footnotes 1

关于 DDoS 攻击类型及对策的研究,请参见“分布式拒绝服务(DDoS)洪泛攻击防御机制综述”, http://d-scholarship.pitt.edu/19225/1/FinalVersion.pdf

八、保护应用

在最后一章中,我们将在应用中实现一些安全措施。事实上,在“安全”的幌子下,我们将执行许多不同的任务。首先,我们将为我们的域提供一个 SSL 证书,然后我们可以将用户和应用之间的敏感交互限制到 HTTPS。随后,我们可以为应用的用户实现安全登录,并将密码存储在数据库的加密字段中。

虽然这看起来有很多任务要完成,但还是有一些好消息。到目前为止,我们在安全方面一直很努力。让我们快速回顾一下我们已经采取的一些安全措施。

  • 访问凭据仅存储在 OpsWorks 中,而不存储在源代码中。
  • 我们的应用中的各种角色和用户只拥有使用他们需要的服务的权限,尽管您可以更进一步,将 IAM 权限限制在特定的 ARN id 上(这是前面提到的一个建议)。
  • 我们的数据库只接受来自 OpsWorks 实例和我们本地 IP 地址的连接。

我们已经在最佳实践方面非常努力了,这真的没有那么难。现在,我们只需确保用户可以安全地连接到我们的应用,并确保他们的凭证得到适当的加密。

使用 HTTPS

你可能已经注意到了浏览器角落地址栏旁边的小挂锁图标(见图 8-1 ),并想知道这是什么意思。这个挂锁已经成为一个通用的标志,表明您通过 HTTPS 而不是未加密的 HTTP 连接到主机。

A978-1-4842-0653-9_8_Fig1_HTML.jpg

图 8-1。

The padlock in the address bar indicates an HTTPS connection

您甚至可以单击挂锁来查看用于验证主机连接的 SSL 证书的更多详细信息。我们将在我们的应用中添加这一层安全性,允许用户通过 HTTPS 进行连接,并使用可信证书对连接进行签名。

当我们使用 HTTPS 协议与我们的应用进行通信时,我们使用的是一种加密传输方法,这种方法由被称为证书颁发机构的可信第三方进行验证。加密与 HTTPS 的通信本身就是一门科学。

我们将使用 HTTPS 来加密用户(客户端)和 CloudFront 之间以及 CloudFront 和负载均衡器之间的流量。这意味着数据在客户端和负载平衡器之间被安全地加密。应用将开始使用会话 cookies,通过负载平衡器将用户的会话连接到特定的实例。我们还将限制对实例的 HTTP 请求,以便只接受来自 ELB 的请求。因为用户只能通过负载均衡器连接来创建会话,所以用户会话被劫持的可能性最多只是理论上的。

SECURITY IS A SHARED RESPONSIBILITY

有时,会在 SSL 协议中发现漏洞,AWS 会快速部署所需的补丁并通过电子邮件通知其客户。因此,您可以放心,当发现漏洞时,AWS 将立即采取行动消除威胁。

作为 AWS 客户,您的责任主要在于遵守其推荐的最佳安全实践。到目前为止,我们已经做到了这一点,将 IAM 角色和用户限制在他们需要的权限内,并将访问密钥和凭证暴露的风险降至最低。

换句话说,AWS 负责云的安全,而您负责云中的安全。当然,SSL 是 AWS 代表您维护的许多活动部件之一。如需了解更多信息,请查看位于 http://aws.amazon.com/compliance/shared-responsibility-model/ 的 AWS 共享安全模型。

值得注意的是,EC2 实例和负载平衡器之间的通信没有获得额外的加密层。增加这一额外的加密层被称为后端认证,这超出了初学者书籍的范围。本书中的课程将为您提供一个安全的应用,但是如果您有特定的安全遵从标准要遵守,您应该彻底检查这些协议。

从我们在 AWS 控制台花费的时间来看,应该很清楚 HTTPS 是各种 AWS 服务支持的传输方法。然而,我们已经把这个特性放到了一边,直到最后,所以我们可以全面地实现这个协议。因为我们从自己的域提供内容,所以我们必须将该域的有效 SSL 证书上传到 AWS。请记住,对我们应用的请求首先通过 CloudFront 和我们的 ELB,然后被这些服务修改。支持 SSL 将要求我们重新配置这两种服务。

当然,存储和验证 SSL 证书属于身份和访问管理的管辖范围。不过,不要在 IAM 中寻找 SSL certificate 选项卡,因为没有这个选项卡!一旦我们有了有效的证书,我们就必须使用 AWS 命令行界面(CLI)将它上传到 IAM。

SSL 证书生成

首先,我们必须为我们的域生成并签署一个 SSL 证书。这是一个多步骤的过程,在其中的一些时间里,你只能靠自己。我尽了最大努力将课程保存在 AWS 控制台中,在那里很容易看到正在做什么,并避免依赖第三方工具。不幸的是,要完成这一部分,我们必须使用许多命令行工具。现在让我们开始安装软件的旅程。

在此过程中,您必须从证书颁发机构获得一个签名证书。您必须完成证书颁发机构要求的步骤,我可以提供您必须采取的步骤的一般描述。我们假设您有能力验证您的应用所使用的域,因为当您获得证书签名时,这在某种程度上是必需的。在接下来的几个步骤中,您必须同时使用 web 浏览器和命令行界面。打开终端或其等效物开始。

安装 OpenSSL

第一步是在您的机器上安装 OpenSSL。OpenSSL 是一个开源加密工具,我们将使用它来生成私钥和证书签名请求(CSR)。您的机器可能已经安装了它。通过在命令行中键入openssl来找出答案。如果这会打开一个 OpenSSL 提示符,而不是抛出一个错误,那么您就可以开始了。

如果没有安装 OpenSSL,可以在这里下载 Mac/Linux 的: www.openssl.org/source 。在 Windows 上,你可以在这里下载一个二进制: www.openssl.org/related/binaries.html

Note

如果你在安装 OpenSSL 时遇到问题,试试 wiki: http://wiki.openssl.org/index.php/Compilation_and_Installation

创建密钥和 CSR

完成安装后,您必须生成一个私钥和相应的证书签名请求,即 CSR。在命令行界面中,输入以下命令:

openssl req -new -newkey rsa:2048 -nodes -keyout photoalbums.key -out photoalbums.csr

代替photoalbums.keyphotoalbums.csr,您可以使用与我们正在获取证书的域相关的文件名。在 CLI 中,系统会提示您填写一些问题。您将填写每一项并按下 Return 键继续。

Country Name (2 letter code) [AU]:

State or Province Name (full name) [Some-State]:

Locality Name (e.g., city) []:

Organization Name (e.g., company) [Internet Widgits Pty Ltd]:

Organizational Unit Name (e.g., section) []:

Common Name (e.g., server FQDN or YOUR name) []:

Email Address []:

大多数这些字段都是不言自明的。继续输入您的国家、州/省、地区和组织信息。在“常用名”字段中,输入您的域名,不要输入 www。一定要用你的邮箱地址。

完成这些后,系统会提示您输入可选字段。您应该按下 Return 键,不输入任何信息。

Please enter the following 'extra' attributes to be sent with your certificate request

A challenge password []:

An optional company name []:

现在在您工作的目录中应该有一个.key.csr。接下来,我们必须向证书颁发机构请求一个证书。

申请证书

我们必须向证书颁发机构提交我们的密钥和证书签名请求。您使用哪个提供商完全由您决定。亚马逊不做任何推荐,而是将用户导向这里的部分列表: www.dmoz.org/Computers/Security/Public_Key_Infrastructure/PKIX/Tools_and_Services/Third_Party_Certificate_Authorities/

使用出售由证书颁发机构签名的证书的供应商通常更容易。根据您用来注册域名的公司,它也可能提供证书服务。或者,NameCheap 是一个信誉良好的厂商,你可以通过他们获得一个由 Comodo 签名的证书,大约 9 美元/年。使用 NameCheap 大约需要十分钟。如果只是想要一个有效的证书进行开发,这可能是阻力最小的路径。

在 SSL 证书供应商处,您将首先选择一个证书包/价格。一旦你支付了费用,你就可以开始申请证书和验证域名了。您将被要求提供 CSR。为此,您必须在纯文本编辑器中打开.csr。内容应该如下所示:

-----BEGIN CERTIFICATE REQUEST-----

FYvKlArPvGZYWNmCMeNDjwa3pxtHWVu6CeXsXUsU4Axwaqtc60VMofEoQCqfwCi+

CDLLoSnwMQIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAAzFDJs+FNdUgvNmdsBO

5qeETlUtIHyw9rDLSwF/wvMWS/86uSyuq3wR7GiDPIYSjs5dIWqmAleyroKRaMZd

FzAVBgNVBAMTDmNsb3VkeWV5ZXMubmV0MRwwGgYJKoZIhvcNAQkBFg1hZGFtQGNy

5qeETlUtIHyw9rDLSwF/wvMWS/86uSyuq3wR7GiDPIYSjs5dIWqmAleyroKRaMZd

PyrafU/eidGboCv83NYMSUUyJ0xDVCbIe4EoJUnpOmzu7e07vZDbB5cZDCaJWpuo

l5tf9361gLcJrKxwiHuPffipf9vv4q0M1jwdNgKtUNGSq11FdiYqlfXR87iSTMEI

nNuScyAUWgX3yXjeGhCszUIfNMbGEHL3oOKsWvpYP/Kj+ESr5DDrNujHol9n3CQz

CDLLoSnwMQIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAAzFDJs+FNdUgvNmdsBO

5qeETlUtIHyw9rDLSwF/wvMWS/86uSyuq3wR7GiDPIYSjs5dIWqmAleyroKRaMZd

PyrafU/eidGboCv83NYMSUUyJ0xDVCbIe4EoJUnpOmzu7e07vZDbB5cZDCaJWpuo

l5tf9361gLcJrKxwiHuPffipf9vv4q0M1jwdNgKtUNGSq11FdiYqlfXR87iSTMEI

5qeETlUtIHyw9rDLSwF/wvMWS/86uSyuq3wR7GiDPIYSjs5dIWqmAleyroKRaMZd

PyrafU/eidGboCv83NYMSUUyJ0xDVCbIe4EoJUnpOmzu7e07vZDbB5cZDCaJWpuo

l5tf9361gLcJrKxwiHuPffipf9vv4q0M1jwdNgKtUNGSq11FdiYqlfXR87iSTMEI

SevmFhb6EkqLe1sEeDODqKj/FcDZYYjISNEe6ftwPGdBEivRXJpHIH/11wQRQuSw

7ws=

-----END CERTIFICATE REQUEST-----

选择文件的全部内容,并复制粘贴到出现提示的字段中。还可能会要求您选择 web 服务器类型。如果可以的话,选择 Apache+OpenSSL。如果证书有效,域名将自动从您生成 CSR 时输入的通用名称中提取。然后,证书颁发机构将尝试验证该域。为此,它可以向注册为域管理员的电子邮件地址发送一封带有验证码的电子邮件。作为参考,您可以通过在命令行中键入whois yourdomain.com并在响应中找到管理员电子邮件来找到它。您也可以在申请证书的域中选择一个电子邮件地址。在任何情况下,期待一个验证过程的发生,很可能是通过电子邮件。

当您的证书请求被批准时,您将从证书颁发机构收到几个扩展名为.crt的文件。它们应该有逻辑地命名,否则会给你贴上标签。其中一个文件是根 CA 证书;另一个是你的 SSL 证书,应该命名为yourdomain _com.crt之类的东西。除此之外,您还将拥有一个或多个中级 CA 证书。

在我们将这些用于 AWS 服务之前,我们必须解决一个兼容性问题。AWS 只接受 X.509 PEM 格式的证书。我们可以使用 OpenSSL 将我们的证书转换成 PEM。在 CLI 中执行以下命令,然后按回车键:

openssl rsa -in photoalbums.key -text > aws-private.pem

这是在您的机器上生成的私钥,它将被上传到 AWS。接下来,让我们转换公钥,它是由证书颁发机构提供的。输入以下命令,用证书的实际文件名替换yourdomain_com:

openssl x509 -inform PEM -in yourdomain_com.crt > aws-public.pem

现在您有了自己的公钥和私钥。我们需要的最后一个文件是证书链。不需要太多的细节(我不是这方面的权威),有两种类型的认证机构:root 和 intermediate。每个证书由可信机构颁发给一个机构,形成了从我们的域通过中间 ca 到根证书机构的信任链。证书链是通过将来自中间证书颁发机构的证书按顺序拼接在一起而生成的,反映了这种信任链。我们必须创建 X.509 PEM 格式的证书链,就像公钥和私钥一样。

当您收到证书时,它们应该附有一个有序列表,如下所示,但命名约定取决于您使用的证书颁发机构:

  • 根 CA 证书— AddTrustExternalCARoot.crt
  • 中级 CA 证书— CANameAddTrustCA.crt
  • 中级 CA 证书— CANameDomainValidationSecureServerCA.crt
  • 您的 PositiveSSL 证书— yourdomain_com.crt

当我们创建证书链时,我们将记下中间 CA 证书的顺序并颠倒它们。在命令行上,输入以下内容:

(openssl x509 -inform PEM -in CANameDomainValidationSecureServerCA.crt; openssl x509 -inform PEM -in CANameAddTrustCA.crt) >> aws-certchain.pem

明确一下,第二个中间 CA 证书是第一个,第一个是第二个。它们被组合成一个名为aws-certchain.pem的单个.pem文件。如果您打开该文件,它看起来会像下面这样(只是长得多):

-----BEGIN CERTIFICATE-----

MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB

hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G

A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV

bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB

0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap

lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf

+AZxAeKCINT+b72x

-----END CERTIFICATE-----

-----BEGIN CERTIFICATE-----

MIIFdDCCBFygAwIBAgIQJ2buVutJ846r13Ci/ITeIjANBgkqhkiG9w0BAQwFADBv

MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk

ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF

VQQDEyJDT01PRE8gUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkq

hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkehUktIKVrGsDSTdxc9EZ3SZKzejfSNw

B5a6SE2Q8pTIqXOi6wZ7I53eovNNVZ96YUWYGGjHXkBrI/V5eu+MtWuLt29G9Hvx

PUsE2JOAWVrgQSQdso8VYFhH2+9uRv0V9dlfmrPb2LjkQLPNlzmuhbsdjrzch5vR

pu/xO28QOG8=

-----END CERTIFICATE-----

现在我们准备好使用证书了!

AWS 命令行界面(CLI)

如前所述,IAM 仪表板中没有 SSL 视图。相反,我们必须使用 AWS 命令行界面将我们的证书上传到 AWS。为此,我们必须在您的计算机上安装和配置 AWS CLI 工具。在您的操作系统上安装 AWS CLI 的说明可在此处获得: http://docs.aws.amazon.com/cli/latest/userguide/installing.html

按照亚马逊提供的步骤操作,应该没有问题。现在,您可以通过命令行界面执行我们在控制台中执行的任何命令。在我们开始使用它之前,我们必须确保正确配置了权限。

配置权限

我们可以使用我们的 photoadmin IAM 用户,并生成一个访问密钥,用于命令行界面。在 AWS 控制台中导航到 IAM,并从左侧导航栏中选择用户。

在第一章中,我们让 photoadmin 用户成为 PhotoAdmins 组的成员。用户没有任何独特的策略,而是从 IAM 组继承权限。我们希望使用这个用户将 SSL 证书上传到 IAM,但是给整个组完全的 IAM 访问权限似乎有点极端。我们可以在用户级别临时授予该用户额外的权限。

从用户列表中选择 photoadmin。在用户详细信息视图中,单击附加策略。您将再次选择一个托管策略。向下滚动(或通过在策略类型过滤器字段中键入 iam 来过滤列表)直到找到 IAMFullAccess 并单击复选框(参见图 8-2 )。

A978-1-4842-0653-9_8_Fig2_HTML.jpg

图 8-2。

IAM Full Access policy

单击附加策略以附加它并返回到用户详细信息视图。当您返回到 user detail 视图时,您将看到我们的用户现在在用户和组级别上都有策略,如图 8-3 所示。虽然您不能在这里编辑组策略,但是您可以轻松地编辑或删除用户策略。

A978-1-4842-0653-9_8_Fig3_HTML.jpg

图 8-3。

IAM user and group policies

如果您有兴趣,可以单击“显示策略”来查看策略声明。它应该类似于下面的代码。

{

"Version": "2012-10-17",

"Statement": [

{

"Effect": "Allow",

"Action": "iam:*",

"Resource": "*"

}

]

}

既然用户有了正确的策略,我们必须生成一个访问密钥。在安全凭据标题下,单击管理访问密钥。将出现一个带有“创建访问键”按钮的模式窗口。单击它,模式将会更新,提示您下载凭证的副本。这将是你这样做的唯一机会(见图 8-4 )。单击下载凭据以存储本地副本。

A978-1-4842-0653-9_8_Fig4_HTML.jpg

图 8-4。

Download IAM credentials

接下来,我们必须配置 CLI 以使用我们刚刚创建的凭据。在命令行上,键入aws configure。您将收到如下提示:

AWS Access Key ID [****************4ZAA]:

粘贴刚才保存的凭据中的访问密钥,然后按 Return 键。然后会提示您输入密码。

AWS Secret Access Key [****************FpdG]:

您将输入默认的 AWS 区域。输入us-east-1并按回车键。

Default region name [us-east-1]:

最后,系统会提示您设置默认输出格式。留空并按回车键。

Default output format [None]:

上传 SSL 证书

所有这些配置都导致了一个命令的执行。在一行中,我们将上传证书,并使它们在 CloudFront 中可访问。将证书路径设置为 CloudFront 很重要,因为在这个阶段,对我们的应用的所有请求都要通过 CloudFront。它是我们应用的看门人。确保您的.pem文件与您正在工作的文件在同一个目录中,并输入以下命令:

aws iam upload-server-certificate --certificate-body file://aws-public.pem --private-key file://aws-private.pem --certificate-chain file://aws-certchain.pem --server-certificate-name yourdomain _com --path /cloudfront/ www.yourdomain.com/

这里有一些细微的差别。重要的是,您的–path参数被设置为/cloudfront/ www. yourdomain .com/ ,使用您的域,并在末尾包括尾随斜杠。–server-certificate-name值(在命令中显示为yourdomain_com)应该只是您的域。file://路径是相对的,但是如果你必须使用绝对路径,使用file:///

当您按下 Return 键时,您将得到如下所示的错误或 JSON 响应:

{

"ServerCertificateMetadata": {

"ServerCertificateId": "ASCAJLNQBYPFYEYN5BQNU",

"ServerCertificateName": "yourdomain_com",

"Expiration": "2016-01-05T00:03:54Z",

"Path": "/cloudfront/www.yourdomain.com/

"Arn": "arn:aws:iam::061246224738:server-certificate/cloudfront/``www.yourdomain.com/yourdomain_com

"UploadDate": "2015-01-05T01:04:36.593Z"

}

}

启用 HTTPSin CloudFront

祝贺是理所当然的——这不是一项简单的任务。现在我们有了有效的 SSL 证书,我们必须检查我们的基础设施并启用它。我们必须做的第一件事是在 CloudFront。您已经完成了命令行。在 AWS 控制台中导航到 CloudFront 并选择您的发行版。在常规选项卡中,单击编辑。

我们必须在分发级别启用自定义 SSL 证书。在 SSL 证书头的旁边,您会看到默认的 CloudFront 证书被选中。切换到自定义 SSL 证书,并从下拉列表中选择您的证书(参见图 8-5 )。

A978-1-4842-0653-9_8_Fig5_HTML.jpg

图 8-5。

CloudFront custom SSL certificate

当您选择自定义 SSL 证书时,您必须确定是支持所有客户端还是仅支持那些支持服务器名称指示的客户端。为了支持所有客户端,您必须请求访问,并且您将支付比仅支持 SNI 的客户端高得多的费用。除非您的应用有支持所有客户端的特殊原因,否则您不太可能需要承担这笔额外的费用。这个主题超出了本书的范围,所以请选择后者,除非您有支持所有客户端的特殊原因。

点击右下方的是,编辑。正如我们所知,传播对 CloudFront 分发设置的更改需要几分钟时间。您可以关注您的发行版的 status 字段,等待它从 InProgress 变为 Deployed。完成后,在浏览器中访问你的 Hello World 页面,确保在 URL 前添加https://。你现在应该会看到地址旁边那个令人欣慰的小挂锁图标(参见图 8-1 )。

虽然现在可以通过 HTTPS 连接到我们的域,但这是可选的。你可以在浏览器的地址栏中输入你的 URL,只用http而不是https就可以看到这一点。挂锁会消失!目前,对于我们应用的某些部分来说,这可能是可以的。但是,如果用户要与我们的应用进行交互,我们需要为一些路径建立安全会话。如果你回想一下 CloudFront 上的课程,你会记得我们的发行版没有将 cookies 转发给负载均衡器。

在您的分发中,选择“行为”选项卡。您将看到,如图 8-6 所示,所有行为的查看器协议策略都设置为 HTTP 和 HTTPS。

A978-1-4842-0653-9_8_Fig6_HTML.jpg

图 8-6。

CloudFront behaviors—Viewer Protocol Policy

对于需要 HTTPS 连接和会话 cookie 的请求,我们必须支持一些额外的行为。这些是我们需要限制的路线:

  • /users/login
  • /users/register
  • /users/logout
  • /albums/upload
  • /albums/delete
  • /photos/upload
  • /photos/delete

如果不用创造七种新行为就好了。看到什么模式了吗?我认为我们可以通过以下五条途径来实现我们的目标:

  • /users/login
  • /users/register
  • /users/logout
  • /*/upload
  • /*/delete

用相同的规则创建所有五种行为。原点应该是 ELB,查看器协议策略应该设置为仅 HTTPS。允许的 HTTP 方法应该是 GET、HEAD、OPTIONS、PUT、POST、PATCH、DELETE。转发头和转发 Cookies 应设置为 All(见图 8-7 )。

A978-1-4842-0653-9_8_Fig7_HTML.jpg

图 8-7。

Origin behavior settings

对所有路线重复此过程。完成后,请记住原点的顺序很重要。重新排列它们,使基于会话的行为位于顶部,如图 8-8 所示。

A978-1-4842-0653-9_8_Fig8_HTML.jpg

图 8-8。

Beahviors ordered

像往常一样,我们在 CloudFront 中所做的更改将需要几分钟来传播。同时,我们必须改变 CloudFront 中一个更加模糊的设置。当我们添加 ELB 作为这个 CloudFront 发行版的来源时,我们只允许 CloudFront 通过 HTTP 与 ELB 通信。这意味着 HTTPS 对 CloudFront 的请求将变成对 ELB 的 HTTP 请求。我们希望确保从我们在世界各地的 CloudFront edge 位置到美国东部数据中心的流量是加密的,因此该流量应该通过 HTTPS。

选择 Origins 选项卡,您将在 Origin Protocol Policy 列中看到该问题(参见图 8-9 )。

A978-1-4842-0653-9_8_Fig9_HTML.jpg

图 8-9。

CloudFront Origins

选择您的 ELB,然后单击编辑。在原始设置视图中,将原始协议策略从仅 HTTP 更改为匹配查看器,然后单击是,编辑。从现在开始,对 CloudFront 的 HTTP 请求将通过 HTTP 转发到 ELB,HTTPS 的请求也将通过 HTTPS 转发。传播这些更改也需要几分钟时间。

在 ELB 扶持 HTTPS

请记住,云锋边缘位置分散在全球各地,数据必须从云锋传输到不同地区的 ELB。为了适当地保护我们的流量,我们必须确保到达 CloudFront 的 HTTPS 请求可以被安全地转发到 ELB。这意味着我们的 SSL 证书也必须安装在 ELB 上,并且它需要配置为通过 HTTPS 接受请求。

正如您刚才看到的,HTTPS 请求在端口 443 上被侦听。CloudFront 现在在这个端口上将请求转发给 ELB。当 CloudFront 试图到达端口 443 上的源(ELB)时,它发现一个关闭的端口。CloudFront 认为它根本无法到达原点。

监听 HTTPS 连接

如果请求要通过,我们必须打开 ELB 上的端口。按照 AWS 的说法,我们的 ELB 必须在端口 443 上添加一个监听器。默认情况下,ELB 配置为仅侦听端口 80 上未加密的 HTTP 请求。

导航到 EC2,并从左侧导航栏中选择负载平衡器。选择 ELB 实例后,打开 Listeners 选项卡。您会看到只有端口 80 是打开的。单击“编辑”将 HTTPS 添加到您的 ELB 听众中。将出现一个模式窗口,您可以在其中配置侦听器。

使用负载平衡器端口 443 为 HTTPS(安全 HTTP)添加负载平衡器协议。实例协议应该是 HTTP,实例端口应该是 80。在 SSL 证书列中,单击更改。选择选择现有证书并选择您的证书。图 8-10 显示了完成的设置。

A978-1-4842-0653-9_8_Fig10_HTML.jpg

图 8-10。

Adding an HTTPS ELB listener

单击保存返回到上一视图。点击保存,会看到端口已经打开的确认,如图 8-11 所示。

A978-1-4842-0653-9_8_Fig11_HTML.jpg

图 8-11。

Confirmation that HTTPS listener was created

在 ELB 上还有一个设置我们必须改变。我们知道我们的应用将开始使用基于会话的 cookies。但是由于我们的应用运行在几个实例上,不能保证一旦用户登录,他或她会被两次定向到同一个实例。这将有效地阻止会话完全可用。幸运的是,有一个名为粘性的 ELB 特性,允许我们将用户的会话绑定到特定的实例,并转发相关的 cookie。这意味着,一旦用户在实例上启动了会话,该会话中的所有未来请求都将绑定到该实例。

返回负载平衡器的描述选项卡,找到端口配置设置(参见图 8-12 )。

A978-1-4842-0653-9_8_Fig12_HTML.jpg

图 8-12。

ELB Port Configuration

单击端口 443 配置旁边的编辑。在出现的模式视图中,选择“启用应用生成的 Cookie 粘性”。在 Cookie 名称字段中,输入 connect.sid(见图 8-13 )并点击保存。

A978-1-4842-0653-9_8_Fig13_HTML.jpg

图 8-13。

Enabling cookie stickiness

修改安全组

为了完成我们的配置,我们还需要采取一项额外的安全措施。我们知道 HTTPS 连接是在客户端和 CloudFront 之间以及 CloudFront 和负载均衡器之间协商的。仍然可以直接连接到实例,因为它们不在 VPC 中。虽然恶意用户不太可能发现我们的一个实例的公共 IP,但是我们仍然可以采取额外的措施,通过改变我们的安全组来防止他们直接连接到我们的实例。

在 EC2 中,选择左侧导航中的安全组链接。您将看到为 OpsWorks 自动生成的安全组列表。这些是应用于在各自的 OpsWorks 层中创建的实例的安全规则。要更改这些设置,您可以向堆栈分配一个自定义安全组,或者更改自动生成的安全组。尽管需要额外的努力,我们将创建我们自己的自定义安全组。

在安全组列表中,找到 AWS-OpsWorks-nodejsApp。打开上面的“动作”菜单,然后单击“复制到新文件”。将您的安全组命名为 Photoalbums-nodejs-App,并给它一个有用的描述,如 Photoalbums App 的安全组,限制 HTTP 和 HTTPS 连接。您不需要选择 VPC。

接下来,您必须更改入站 HTTP 和 HTTPS 连接的安全规则。找到 HTTP 行,并将源更改为自定义 IP。在 IP 字段中,输入 amazon-elb/amazon-elb-sg。找到 HTTPS,做同样的改变。完成后(参见图 8-14 ,点击创建。

A978-1-4842-0653-9_8_Fig14_HTML.jpg

图 8-14。

New security group

接下来,我们必须让我们的应用堆栈使用这个新的安全组。导航到 OpsWorks 并选择您的应用堆栈。我们不能让我们的应用层没有任何安全组,所以首先,我们必须添加自定义的安全组,然后删除默认的安全组。从导航菜单中选择层,然后单击应用层旁边的安全性。点击右上角的编辑按钮。从下拉列表中选择您的自定义安全组(参见图 8-15 )并点击保存。

A978-1-4842-0653-9_8_Fig15_HTML.jpg

图 8-15。

Custom security group

从导航菜单转到堆栈。转到堆栈设置,然后单击编辑。在底部,您将看到 OpsWorks 安全组的切换。如图 8-16 所示,当您将其设置为“否”并单击“保存”时,您的实例将不再使用自动生成的安全组。

A978-1-4842-0653-9_8_Fig16_HTML.jpg

图 8-16。

Use OpsWorks security groups

就这样!如果您尝试通过实例的公共 IP 地址连接到实例,请求将会失败。您可以直接连接到您的负载平衡器,但是单个实例与 web 流量隔离。从现在开始,我们只需要修改代码。

应用安全

是时候在我们的应用中构建身份验证了。在执行以下步骤时,请将这些安全技术视为建议。您可能已经有了自己的身份验证策略。无论如何,凭你的经验去做吧。虽然我们将实现许多加密模块中的一个,但您应该能够轻松地将这个模块换成另一个模块。同样,您可以根据自己的需要在应用中实现更严格或更宽松的安全性。这里的目标是演示一种技术,然后按照步骤使我们的应用符合我们在 AWS 中所做的更改。

我们需要在应用中实现两种安全模式。首先,我们必须开始存储密码,而且它们绝对必须在我们的数据库中加密。时不时你会听说一个数据库被泄露,里面有一堆以明文形式存储的密码,任何人都可以窃取。我们不想以那种方式制造新闻。

其次,我们不希望任何用户能够为其他人创建和删除相册或照片。现在,我们允许任何人代表任何其他用户创建和删除内容,只要他们将正确的用户 ID 作为参数传递。这显然是一个糟糕的想法。我们将开始使用安全会话,并将用户 ID 存储在会话 cookies 中。如果没有有效的用户 id 会话,删除内容的尝试将被忽略。

添加会话和加密模块

我们将不得不向我们的应用添加两个额外的模块:easycrypto 和 express-session。Easycrypto 是许多加密和解密密码字符串的库之一。如果您喜欢不同的库,请随意使用它。Express-session 是专门为 ExpressJS 应用构建的会话中间件——就像它听起来那样。

在您的package.json中,将以下内容添加到依赖对象:

"easycrypto": "0.1.1",

"express-session": "¹.7.6",

确保您没有任何尾随逗号,因为您的依赖项应该类似于以下内容:

{

...

"debug": "∼1.0.4",

"easycrypto": "0.1.1",

"express-session": "¹.7.6",

"jade": "∼1.5.0"

}

在命令行上,导航到您的工作目录并键入命令:npm install。您的依赖项应该会像以前一样自动安装。

添加密码加密

接下来,我们将添加密码加密/解密。该代码将完全在/lib/model/model-user.js内发生。打开该文件并在顶部添加一个名为encryptionKey的变量。将该值设置为您喜欢的任意随机字符串。

var encryptionKey   = '80smoviereferencegoeshere';

在文件的底部,我们将添加两个私有方法,用于生成散列密码和解密散列密码。添加清单 8-1 中的代码。

Listing 8-1. Encrypt/Decrypt Hash Password

function generatePasswordHash(password){

var easycrypto = require('easycrypto').getInstance();

var encrypted = easycrypto.encrypt(password, encryptionKey);

return encrypted;

}

function decryptPasswordHash(passwordHash) {

var easycrypto = require('easycrypto').getInstance();

var decryptedPass = easycrypto.decrypt(passwordHash, encryptionKey);

return decryptedPass;

}

当用户注册时,他/她会向我们的路由传递一个密码参数。我们想得到这个密码,散列它,并把它存储在数据库中。当用户稍后登录时,我们会将他/她提供的密码与存储在数据库中的密码进行比较。在我们现有的功能中,我们首先要改变的是保存到数据库中的密码。我们将首先在输入上调用generatePasswordHash(),而不是直接保存密码参数。用清单 8-2 替换createUser功能。

Listing 8-2. createUser

function createUser(params, callback){

var newUser = {

username: params.username,

password: generatePasswordHash(params.password),

email: params.email

}

var query = 'INSERT INTO users SET ? ';

connection.query(query, newUser, function(err, rows, fields) {

if (err) {

if(err.errno == 1062){

var error = new Error("This username has already been taken.");

callback(error);

} else {

callback(err);

}

} else {

callback(null, {message:'Registration successful!'});

}

});

}

现在,用清单 8-3 替换loginUser()。启用此功能后,使用纯文本密码的用户将无法再登录。

Listing 8-3. loginUser

function loginUser(params, callback){

connection.query('SELECT username, password, userID FROM users WHERE username=' + connection.escape(params.username), function(err, rows, fields) {

if(err){

callback(err);

} else if(rows.length > 0){

var decryptedPass = decryptPasswordHash(rows[0].password);

if(decryptedPass == params.password){

var response = {

username: rows[0].username,

userID: rows[0].userID

}

callback(null, response);

} else {

var error = new Error("Invalid login");

callback(error);

}

} else {

var error = new Error("Invalid login");

callback(error);

}

});

}

使用安全会话

这要稍微复杂一点,但它确实取决于您的应用的目标。如果你想将你的应用的某些部分限制为已验证的用户,那么实现起来会简单一些。在我们的例子中,所有的路由都混合了受限和非受限的 API 端点。因此,我们将在单个路由级别手动保护我们的应用。

我们将首先配置我们的应用来使用快速会话中间件。在server.js中,在包含express之后立即添加 express-session 中间件。

var express = require('express');

var expressSession = require('express-session');

var path = require('path');

然后,在其他app.use语句之前,添加以下内容:

app.use(expressSession({secret: 'ssshhhhh'}));

继续用你自己的秘密密钥替换秘密的值— ssshhhhh肯定不是最好的选择。对于快速会话,您可以修改更多的设置,但我们将保持不变。这是启用会话所需的全部内容;现在,我们必须在会话中获取和设置值。首先,当用户登录时,我们必须在他们的会话 cookies 中设置一个标识用户的值。为了简单起见,我们将使用用户 ID,尽管您可能会考虑诸如电子邮件或多个值之类的东西。打开/routes/users.js,定位/login路线。在model.loginUser()的回调中,设置会话的userID,如清单 8-4 所示。

Listing 8-4. /users/login

router.post('/login', function(req, res) {

if(req.param('username') && req.param('password') ){

var params = {

username: req.param('username').toLowerCase(),

password: req.param('password')

};

model.loginUser(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid login'});

} else {

req.session.userID = obj.userID;

res.send(obj);

}

});

} else {

res.status(400).send({error: 'Invalid login'});

}

});

同样,/logout路线也可以简化。它所要做的就是销毁当前会话(参见清单 8-5 )。

Listing 8-5. /users/logout

router.post('/logout', function(req, res) {

if(req.session){

req.session.destroy();

}

res.send({message: 'User logged out successfully'});

});

现在让我们让我们的关键路线需要会话 cookies,而不是POST参数。打开/routes/photos.js。定位/upload路线。我们将在对req.sessionreq.session.userID的条件检查中包含我们所有的功能。此外,我们将向模型发送req.session.userID,而不是req.param('userID')。用清单 8-6 中的代码替换您的代码。

Listing 8-6. /photos/upload

router.post('/upload', function(req, res) {

if(req.session && req.session.userID){

if(req.param('albumID') && req.files.photo){

var params = {

userID : req.session.userID,

albumID : req.param('albumID')

}

if(req.param('caption')){

params.caption = req.param('caption');

}

fs.exists(req.files.photo.path, function(exists) {

if(exists) {

params.filePath = req.files.photo.path;

var timestamp = Date.now();

params.newFilename = params.userID + '/' + params.filePath.replace('tmp/', timestamp);

uploadPhoto(params, function(err, fileObject){

if(err){

res.status(400).send({error: 'Invalid photo data'});

}

params.url = fileObject.url;

delete params.filePath;

delete params.newFilename;

model.createPhoto(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid photo data'});

} else {

res.send(obj);

}

});

});

} else {

res.status(400).send({error: 'Invalid photo data'});

}

});

} else {

res.status(400).send({error: 'Invalid photo data'});

}

} else {

res.status(401).send({error: 'You must be logged in to upload photos'});

}

});

我们还需要在会话检查中包装/photos/delete。用清单 8-7 中的代码替换它。

Listing 8-7. /photos/delete

router.post('/delete', function(req, res) {

if(req.session && req.session.userID){

if(req.param('id')){

var params = {

photoID : req.param('id'),

userID : req.session.userID

}

model.deletePhoto(params, function(err, obj){

if(err){

res.status(400).send({error: 'Photo not found'});

} else {

res.send(obj);

}

});

} else {

res.status(400).send({error: 'Invalid photo ID'});

}

} else {

res.status(401).send({error: 'Unauthorized to create album'});

}

});

我们也需要在/routes/albums.js中做这个改变。打开文件并用新函数覆盖/upload/delete(参见清单 8-8 )。

Listing 8-8. /albums routes

router.post('/upload', function(req, res) {

if(req.session && req.session.userID){

if(req.param('title')){

var params = {

userID : req.session.userID,

title : req.param('title')

}

model.createAlbum(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid album data'});

} else {

res.send(obj);

}

});

} else {

res.status(400).send({error: 'Invalid album data'});

}

} else {

res.status(401).send({error: 'Unauthorized to create album'});

}

});

router.post('/delete', function(req, res) {

if(req.session && req.session.userID){

if(req.param('albumID')){

var params = {

albumID : req.param('albumID'),

userID : req.session.userID

}

model.deleteAlbum(params, function(err, obj){

if(err){

res.status(400).send({error: 'Album not found'});

} else {

res.send(obj);

}

});

} else {

res.status(400).send({error: 'Invalid album ID'});

}

} else {

res.status(401).send({error: 'Unauthorized to create album'});

}

});

我们还必须对我们的模型进行一些修改。以前,任何用户都可以删除任何人的相册或照片。混乱会接踵而至!我们必须确保用户只删除他们拥有的内容。

/lib/models/model-photos.js中,用清单 8-9 替换deletePhotobyID()方法。

Listing 8-9. Photos deletePhotoByID Method

function deletePhotoByID(params, callback){

var query = 'UPDATE photos SET published=0 WHERE photoID=' + connection.escape(params.photoID)  + ' AND userID=' + params.userID;

connection.query(query, function(err, rows, fields){

if(rows.length > 0){

callback(null, rows);

} else {

if(rows.changedRows > 0){

callback(null, {message: 'Photo deleted successfully'});

} else {

var deleteError = new Error('Unable to delete photo');

callback(deleteError);

}

}

});

}

您会注意到 SQL 查询发生了变化,限制用户更新他们自己的内容。此外,还有一个手动构造并返回给回调的新错误。如果一个UPDATE查询没有改变任何行,那么处理程序中的 rows 对象将拥有一个等于0的属性changedRows。虽然这本身不是一个错误,但是我们的应用应该将它视为一个错误。这意味着用户试图删除一张不存在或不属于他/她的照片。

我们需要将同样的逻辑应用到专辑中。打开/lib/models/model-albums.js并用清单 8-10 替换deleteAlbum()方法。

Listing 8-10. Albums deleteAlbum Method

function deleteAlbum(params, callback){

var query = 'UPDATE albums SET published=0 WHERE albumID=' + connection.escape(params.albumID) + ' AND userID=' + params.userID;

connection.query(query, function(err, rows, fields){

if(err){

callback(err);

} else {

if(rows.changedRows > 0){

callback(null, {message: 'Album deleted successfully'});

} else {

var deleteError = new Error('Unable to delete album');

callback(deleteError);

}

}

});

}

这就是我们要做的所有代码更改。将更改提交到您的代码库中。部署到您的 OpsWorks 实例,并等待该过程完成。完成后,您可以开始测试。您以前的用户现在将无效。应用将尝试解密它们,从而导致不匹配。最简单的测试方法是注册一个新用户,登录,然后创建一个相册。您应该能够通过向域或负载平衡器本身发出 HTTPS POST请求来成功实现这些操作。通过 HTTP 登录到域的尝试将被拒绝。

结论

最后一课到此结束!我们很快完成了各种任务。如果你回头看看我们在第一章中的位置,你可能会对自己印象深刻。

在创建这些经验教训的过程中,人们一直在争论该在哪里划线,尤其是在源代码方面。现在,我们有一个具有许多企业应用主要特征的应用。它安全、冗余、可扩展,并使用强大的缓存和通知。但是构建一个真正商业上可行的 web 应用并不是一件简单的任务,我们的仍然有很多缺点。只看用户,我们忽略了密码重置、用户搜索等等。目标与其说是教你如何编写应用,不如说是教你为 AWS 编写应用。希望从这里开始,你对自己在这个软件或你写的任何其他软件中推断这些想法的能力感到自信。

另一个主要挑战是与 AWS 保持同步(更不用说 Express 了!).亚马逊定期推出新功能。他们偶尔会收购一家初创公司,并在六个月后公布他们在 AWS 平台上开发的技术。掌握 AWS 中的新特性需要持续的警惕,但这也令人兴奋。事实上,在本书写作期间,一些特性发生了变化,需要一些修改。

您已经看到了我们所拥有的工具的威力,以及我们是如何快速地构建出十年前普通开发人员无法实现的东西。事实上,web 开发人员的角色在短时间内发生了巨大的变化,AWS 在这些变化中发挥了不小的作用。如果这本书给了你,读者,与这些变化一起成长的信心,那么你已经学到了书中最重要的一课。

posted @ 2024-08-13 14:06  绝不原创的飞龙  阅读(87)  评论(0)    收藏  举报