AWS NodeJS 入门指南(三)
七、监控应用
我们已经谈了很多我们设计系统的主要原则——可伸缩性和弹性——在这个过程中,你已经了解了这些概念。迄今为止,我们几乎无法将这些原则付诸实践。虽然您已经看到可以通过各种 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 图层不会出现。
图 7-1。
The OpsWorks Monitoring view
将显示四个类别的层度量,每个都有自己的图表:CPU 系统、使用的内存、负载和进程。默认情况下,会加载过去 24 小时的指标。您可以快速查看是否有任何 CPU 系统(利用率)、内存使用或负载达到最大值的重大事件,或者活动进程中是否有峰值。前三个图表标题实际上都是一个下拉菜单,可用于从该类别中选择另一个指标。图 7-2 显示已用内存下拉列表。
图 7-2。
An OpsWorks Monitoring metric drop-down
您也可以将日期范围从 24 小时更改为另一个范围。这些似乎是非常有用的指标,但是没有办法更详细地查看它们。您可能认为单击其中一个图表会将其展开,但它会将您带到其他地方:到层中实例的监视视图。记下内存使用指标,因为我们将在 CloudWatch 中查看它。现在,让我们转到 CloudWatch 控制面板,在那里查看这些指标。
打开服务下拉列表,并选择 CloudWatch。在控制台的顶部,您会看到一个标题为“指标摘要”的标题(参见图 7-3 )。在此标题下,单击浏览指标按钮。
图 7-3。
CloudWatch Metric Summary
在 Metrics 视图中,您可以看到您的 AWS 帐户的所有指标,先按服务细分,然后再按该服务中的类别细分(参见图 7-4 )。您在此处看到的指标数量取决于您的帐户上已经创建了哪些资源。
图 7-4。
CloudWatch Metrics by Category
让我们来看看我们的应用的指标。假设我们想看看我们的应用层使用了多少内存。在 OpsWorks 度量标题下,单击层度量。您应该会看到一个按名称及其对应的 LayerId 字母顺序排列的指标列表,如图 7-5 所示。因为我们只有一个分配了 EC2 实例的 OpsWorks 层,所以每个指标出现一次,并对应于 Node.js 应用层。
图 7-5。
OpsWorks Layer Metrics
向下滚动到名为 memory_used 的指标。单击它旁边的复选框。在指标列表下方,会神奇地出现一个图表,以 5 分钟的间隔显示过去 12 小时的指标(参见图 7-6 )。
图 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 )。
图 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 )。
图 7-8。
SNS dashboard
将出现一个模式弹出窗口,提示您输入主题名称和可选的显示名称。在这两个字段中,输入 PhotoalbumsAlarms,因为该主题将仅由我们的应用堆栈的 CloudWatch 警报触发(参见图 7-9 )。单击创建主题。
图 7-9。
Create SNS topic
您将立即转到主题详细信息视图,在这里您可以查看主题的基本信息以及主题的订阅。现在,我们只需要为这个主题创建一个订阅:给应用的唯一管理员发送一封电子邮件。继续并点击“创建订阅”按钮。另一个模态将会出现。展开主题下拉列表并选择电子邮件。在端点字段中输入您的电子邮件地址,然后单击订阅。
该窗口将显示一条消息,通知您必须确认电子邮件地址。单击关闭返回主题详细信息视图。您应该会收到一封主题为“AWS 通知-订阅确认”的电子邮件。在电子邮件正文中,应该有一个指向订阅确认 URL 的链接。点击它会引导你进入一个类似于图 7-10 所示的页面。
图 7-10。
An SNS subscription confirmed
如果您返回到主题详细信息视图并单击刷新,您将看到已经为您的电子邮件生成了一个订阅 ID(ARN),就像您在订阅确认页面上看到的一样。现在你已经准备好使用这个 SNS 话题了!我们可以通过手动发布到这个主题来运行一个简单的测试。在主题细节视图的顶部,您会看到一个发布按钮。点击这个,另一个模态就会出现。填写一个主题和一些测试内容的消息,如图 7-11 ,点击发布消息按钮。然后,再次检查你的电子邮件。你应该会看到一条来自< no-reply@sns.amazonaws.com >的主题信息。
图 7-11。
Publishing an SNS topic
创建云监控警报
现在,我们已经为警报创建了一个 SNS 主题,我们准备创建我们的第一个 CloudWatch 警报。不幸的是,对于读者来说,每一个 CloudWatch 指标都有自己的讨论主题,需要对这个主题进行一些阐述。这一次,让我们使用 CloudFront 的指标来创建一个警报。如果您还记得,通过 Web 对我们的应用的所有请求都将通过我们的 CloudFront 发行版。例如,知道是否有许多请求正在被发出可能是有用的。
使用服务菜单,返回到 CloudWatch 仪表盘。这一次,看看左边的导航。在 Metrics 标题下,您将看到您拥有 CloudWatch 指标的所有服务的列表。单击 CloudFront,这将用 CloudFront 分布的指标填充主视图(参见图 7-12 )。
图 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 )。单击该按钮开始该过程。
图 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%之类的内容。如果你愿意,你甚至可以进一步描述。
图 7-14。
Defining an alarm
在描述的下面,您会找到警报的实际参数。“无论何时”字段已经设置为您选择的度量。在 is:行中,将比较运算符设置为>,并在相邻的字段中输入 25。
查看右栏,如果现在启用了报警,报警预览会显示报警的当前状态。从图表中可以看出,当越过红线时,警报就会响起。目前,没有任何请求,所以在我的图表上甚至没有一条蓝线。这将导致警报处于不足数据状态。您会注意到在这一列的底部有用于周期和统计的字段。将周期设置为 5 分钟,这将是根据警报阈值评估指标的频率。统计是我们衡量的价值。在这种情况下,平均就可以了。在某些情况下,您可能想测量指标的最大值或最小值。
回到左侧,Actions 标题表示 CloudWatch 对警报响起的自动响应。您将看到您的选择是添加通知或自动扩容操作。对于我们的设置,自动扩容操作不是一个可行的选项。有了一个更加微观管理的应用堆栈,您可以配置额外的 EC2 实例来自动启动或关闭以响应警报。因为我们使用 OpsWorks 来管理我们的实例,所以我们将在那里配置我们的自动伸缩。有了这个 CloudWatch 警报,我们所要做的就是生成一个通知。单击+通知创建您的第一个通知。将通知配置为每当此警报:至状态为警报时设置,并将通知发送至:照片相册警报。图 7-14 为完整视图。
警报状态
现在,当警报处于警报状态时,您将收到通知。您还可以创建一个通知,在警报处于正常状态时发出。再次点击+通知,并在该警报:状态正常时设置该字段。然后,单击创建提醒。
您应该会在页面顶部看到一条成功消息,表明您的警报已经创建。如果你看左边的导航,你会看到报警标题实际上给你一个当前报警状态的总结(见图 7-15 )。因为您刚刚创建了警报,所以它的状态为“不足 _ 数据”。过一会儿,它应该会变为正常。
图 7-15。
CloudWatch alarm states
一会儿,我们会发现这个警报的一个问题,因为我们永远不知道什么样的机器人,脚本或随机流量会在您的域中运行。尽管如此,它将很好地提醒我们所拥有的工具。如果你想触发警报,你所要做的就是向不存在的路径发出几个请求,比如http://www.[ yourdomain ].com/helloworld。几分钟后,您应该会收到一封电子邮件,指示该警报处于警报状态。
如果你再等一会儿,闹钟可能很快就会响。那么我们该如何处理这些信息呢?拥有如此复杂的基础设施的挑战之一是熟悉各种故障点。
特别是这个警报是基于 CloudFront 度量的,所以我们应该做的第一件事是检查 CloudFront,看看问题是什么。继续前进,导航到 CloudFront 仪表板。在 CloudFront 导航中,单击热门对象。您可能还记得,这提供了一份关于对您的 CloudFront 发行版的流行 URL 请求的报告(参见图 7-16 )。
图 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 )。单击该按钮,您将看到基于负载的实例的空白视图。
图 7-17。
Load-based instances in the Navigation menu
在屏幕中间,您应该会看到以下消息:No load-based instances。添加基于负荷的实例。
单击“添加基于负荷的实例”。您会认出在第二章中添加第一个实例时出现的视图。默认主机名应该没问题。为了节约起见,将大小更改为 t1.micro。您应该选择不同的可用区域,例如 us-east-1b(参见图 7-18 )。如果您还记得的话,最佳实践是将您的实例分布在不同的可用性区域,以防 AWS 中断。这意味着我们应该跨可用性区域分布 24/7 个实例,但我们暂时将就一下。
图 7-18。
Adding a new instance to the application layer
但是到目前为止,这与创建一个 24/7 的实例没有任何区别。接下来,在创建实例之前,单击“高级”查看其他设置。高级设置应类似于图 7-19 中的设置。因为我们是从基于负荷的实例视图中创建实例的,所以预先选择了基于负荷的扩容类型。我们不必更改这些值中的任何一个,但是如果您从 general instances 视图中创建基于负载的实例,您必须确保在 advanced 视图中更改扩容类型。继续,然后单击“添加实例”继续。
图 7-19。
Load-based instance advanced configuration
OpsWorks 自动扩容规则
当您返回到“实例”视图时,您应该会在黄色警告框中看到以下消息:基于载荷的自动扩容已禁用-编辑。
您看到此警告是因为,虽然我们创建了基于负载的实例,但我们没有启用基于负载的伸缩,也没有定义实例联机的规则。单击“编辑”按钮即可。
首先:将基于负载的自动扩容启用开关切换到 Yes。接下来,让我们定义一些自动扩容规则。请记住,就您的应用而言,这些值基本上是任意的,您应该根据测试和操作历史来确定自己的扩容阈值。现在,我们将设计一个简单的规则集,如图 7-20 所示,之后我将讨论其行为。
图 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 )。单击“添加实例”创建第二个基于负荷的实例。
图 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 )。单击“添加实例”进入“计划创建”视图。
图 7-22。
Adding a time-based instance
在 Schedule Creation 视图中,您可以为应用层中每个基于时间的实例手动设置时间表。界面相当简单:您选择一个小时的时间段(UTC ),在此期间您的实例将处于在线状态。
继续点击 12 和 13 之间的方框,如图 7-23 所示。保存更改时,您会看到一个简短的活动指示器。
图 7-23。
Time-based instance scheduling
现在,您的实例将从 UTC 时间每天下午 12 点至 1 点运行。假设您还希望它在 UTC 时间周五晚上 6-8 点运行。单击“Fri”选项卡,仅选择星期五的时间表。您将看到您的每日小时数被自动选择,您只需选择您想要在周五运行的额外小时数。选择 18–19 和 19–20 街区,将星期五晚上添加到您的选择中(参见图 7-24 )。
图 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 所示。
图 7-25。
ELB Monitoring view
您会注意到右手边有一个创建警报按钮。正如您可能已经猜到的,这允许您为所选的负载平衡器创建一个 CloudWatch 警报。
当你点击这个按钮时,你会看到一个模态视图,它只是 CloudWatch 界面的精简版。向您的社交网络主题发送通知。在“无论何时”字段中,选择“普通”和“健康”主机。Is 应该> = 3,这是您的 24/7 实例和基于负载的实例的总和。至少可以设置为 1 个连续 1 分钟的时间段(见图 7-26 )。
图 7-26。
Create ELB alarm
在此视图中创建闹钟时,您可以给闹钟命名,但不能设置描述。自动生成的名称对于电子邮件通知来说可能不够清楚。如果你愿意,可以把闹钟的名字改成像相册这样的东西——所有在线实例。如果一切正常,单击创建警报。
同样,这是一个不完善的方法。当您有一个基于负载的实例、一个基于时间的实例和一个 24/7 实例在线时,此警报将会响起。创建警报后,模式窗口将确认操作成功(参见图 7-27 )。在此模式中,CloudWatch 仪表板中有一个警报链接,您可以从中访问警报详细信息的完整视图。
图 7-27。
ELB alarm confirmation
接下来,让我们为不健康的主机创建警报。请注意,如果您的应用由于代码中的异常而崩溃,它将自动重启。单个不正常的实例可能无法保证响应。再次单击“创建提醒”按钮,并将通知发送到同一个 SNS 主题。这一次,只要不健康主机的平均值> = 1,警报就会响起。为了降低警报的敏感度,您可以设置它在连续两个一分钟的时间段过去时触发。正如您在右图中看到的,现在应该没有任何不健康的主机。将您的闹钟命名为 Photoalbums - 1 或更多不健康的主机,如图 7-28 所示,然后再次点击创建闹钟。
图 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 按钮位于页面的更下方。
图 7-29。
RDS monitoring
您将在这里看到许多有用的指标,大部分是原始数据,而不是百分比。这意味着,如果您扩展您的数据库,您可能需要相应地调整警报,就像我们在本章开始时看到的度量标准一样。
根据您阅读这本书花了多长时间,您可能已经有了一些数据库的操作历史。查看图 7-30 中的 CPU 利用率,您可以通过单击微型 CPU 利用率图来访问该图。
图 7-30。
RDS instance CPU utilization
如您所见,在绝大多数时间里,我的 CPU 利用率保持在 40%以下。有一个事件,它突然飙升到 100%。虽然 AWS 已经自动解决了该事件,但我们应该在将来收到该事件的通知。单击创建警报按钮。
对于此警报,我们希望在一分钟内收到 CPU 利用率> 60%的通知(参见图 7-31 )。我们再次在过于敏感而无用的警报和过于宽容而无法检测到事故的警报之间游走。60%的 CPU 使用率远远超出正常范围,我们将立即得到通知。单击创建提醒以完成提醒并返回 RDS。
图 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 日志创建另一个策略。
图 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 文件一样。
图 7-33。
Enabling custom cookbooks in OpsWorks
单击右下角的保存。接下来,我们必须配置实例,使其在启动过程中的特定时刻运行特定的配方。我们不在实例级别这样做!相反,我们必须转向层,在层中为所有附加到 Node.js 应用层的实例完成配置。在 Layers 视图中,您会注意到 Node.js App Server 下有一个编辑菜谱的链接,如图 7-34 所示。单击此处查看和编辑应用层的配方。
图 7-34。
Layers view revisited
在 recipes 视图的顶部,您会看到自动安装在实例中的内置 Chef 食谱。每个配方都以 cookbook::recipe 的格式列出,并显示在其执行阶段。这些是不可编辑的,因为它们由 AWS 控制。在 Custom Chef Recipes 标题下,您可以添加要在每个阶段的内置配方之后执行的自定义配方。我们必须在设置阶段添加两个配方。在设置右侧的文本框中,输入以下内容:
logs::config, logs::install
然后点击右边的+按钮。您的视图应该类似于图 7-35 。确认后,点击右下角的保存。
图 7-35。
Custom chef recipes
因为菜谱是在设置阶段运行的,所以我们必须在运行的实例上触发它。安装程序将在将来启动的任何实例上自动运行。如果您现在只有一个实例在运行,我们可以很容易地在它上面运行 Setup 命令。导航到 Instances 视图,并单击正在运行的实例的名称。在右上角,单击运行命令。在设置标题下,从命令下拉菜单中选择设置(参见图 7-36 )。然后点击右下角的设置。
图 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 所示。
图 7-37。
CloudWatch log groups
CloudWatch 日志有自己的层次结构。单个日志语句被称为一个事件。日志事件存储在一系列流中,这些流在顶层组织成组。在组级别,您可以控制日志保留策略,设置日志事件过期的时间段。在大多数情况下,您可能不想为您的堆栈保留完整的日志历史。如果在日志组视图中单击永不过期,您可以将保留期更改为一天到十年。
目前,让我们将日志保留时间减少到三天。单击永不过期,将显示编辑保留模式视图。从下拉列表中选择 3 天。您将看到一条消息,如图 7-38 所示,确认旧数据将被删除。单击确定进行确认。
图 7-38。
Changing CloudWatch log retention
另一个有趣的特性叫做度量过滤器。您可以创建指标过滤器来自动扫描日志中的特定术语或模式,并使用它们来创建自定义的 CloudWatch 指标。一旦您的日志生成了可量化的指标,您就可以创建一个 CloudWatch 警报,在指标超过某个阈值时通知您。
这是一个强大的功能,尽管没有得到应有的重视。我们可以自动扫描日志中特定类型的错误,并在检测到错误时触发警报。这个特性对于应用级别的日志记录非常有用:当错误发生时,我们可以得到通知,并快速做出响应。
在我们设置应用级日志记录之前,请单击相册日志组。在这个组中,您应该看到应用层中每个 EC2 实例的日志流:nodejs-app1、nodejs-app2 等。在启用自定义配方后,您将只能看到运行设置命令的实例。如果堆栈中有基于负载和基于时间的实例,它们会在启动时自动创建日志流。
将系统级日志存储在特定于实例的日志流中是有意义的。如果启动实例时出现问题,您可能希望查看特定于该实例的日志。对于应用日志来说,情况并非如此。当应用在多个实例上运行时,负载平衡器将决定哪个实例处理用户的请求。如果其中一个用户遇到错误,您无法确定错误发生在哪个实例上。最简单的解决方案是为我们的应用级日志记录创建一个单独的日志流,这将合并我们的应用中生成的所有日志事件。
应用日志流
虽然我们可以很容易地创建带时间戳的日志流,但是在这个例子中,我们只保留一个主日志流。我们将使用三天日志保留策略来防止我们的日志因历史数据过多而变得臃肿。
在“日志流”视图中,点按顶部的“创建日志流”按钮。将出现一个模式窗口,要求您命名日志流。输入名称 application,如图 7-39 所示,点击创建日志流。
图 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对象被硬编码到“相册”日志组和“应用”日志流中。如果您想保存精心制作和组织良好的日志,您可以很容易地将这些动态日志发布到多个不同的日志流甚至日志组。例如,您可以为正常的应用活动保留一个日志流,为严重错误保留另一个日志流。
接下来,我们将添加一个从简单字符串创建日志事件的公共方法。该函数简单地创建一个具有属性message和timestamp的对象,并将它们添加到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声明,使logEvent和putlogs成为公共的。
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 的内容。
图 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 。如果您试图创建一个过滤器来捕获现有的日志数据,页面上有一个方便的工具来测试您的过滤器模式。单击分配指标继续。
图 7-41。
Create metric filter
在下一个视图中,您可以命名您的过滤器以及它过滤的指标。如果您有很多度量过滤器,那么过滤器名称对于管理度量过滤器非常有用,而度量名称是 CloudWatch 中实际计数的值的名称。您可以保持过滤器名称不变,但一定要将指标名称设置为 UncaughtErrors 之类的名称,如图 7-42 所示。然后,单击创建过滤器。
图 7-42。
Filter and metric names
您将在 Filters 视图中仅看到您的新过滤器,以及一条成功消息(参见图 7-43 )。在这里,您可以编辑或删除过滤器,或者更重要的是,根据指标创建警报。
图 7-43。
Photoalbums filters
单击“创建警报”为该指标创建新的 CloudWatch 警报。这个警报的想法是,任何时候抛出一个异常,警报就会响起。因此,我们希望每当 UncaughtErrors > = 1 时就发出警报。将闹钟命名为 Photoalbums-unccatched Error,使闹钟向 PhotoalbumsAlarm 列表发送通知,如图 7-44 所示。
图 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 连接到主机。
图 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.key和photoalbums.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 )。
图 8-2。
IAM Full Access policy
单击附加策略以附加它并返回到用户详细信息视图。当您返回到 user detail 视图时,您将看到我们的用户现在在用户和组级别上都有策略,如图 8-3 所示。虽然您不能在这里编辑组策略,但是您可以轻松地编辑或删除用户策略。
图 8-3。
IAM user and group policies
如果您有兴趣,可以单击“显示策略”来查看策略声明。它应该类似于下面的代码。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iam:*",
"Resource": "*"
}
]
}
既然用户有了正确的策略,我们必须生成一个访问密钥。在安全凭据标题下,单击管理访问密钥。将出现一个带有“创建访问键”按钮的模式窗口。单击它,模式将会更新,提示您下载凭证的副本。这将是你这样做的唯一机会(见图 8-4 )。单击下载凭据以存储本地副本。
图 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 )。
图 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。
图 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 )。
图 8-7。
Origin behavior settings
对所有路线重复此过程。完成后,请记住原点的顺序很重要。重新排列它们,使基于会话的行为位于顶部,如图 8-8 所示。
图 8-8。
Beahviors ordered
像往常一样,我们在 CloudFront 中所做的更改将需要几分钟来传播。同时,我们必须改变 CloudFront 中一个更加模糊的设置。当我们添加 ELB 作为这个 CloudFront 发行版的来源时,我们只允许 CloudFront 通过 HTTP 与 ELB 通信。这意味着 HTTPS 对 CloudFront 的请求将变成对 ELB 的 HTTP 请求。我们希望确保从我们在世界各地的 CloudFront edge 位置到美国东部数据中心的流量是加密的,因此该流量应该通过 HTTPS。
选择 Origins 选项卡,您将在 Origin Protocol Policy 列中看到该问题(参见图 8-9 )。
图 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 显示了完成的设置。
图 8-10。
Adding an HTTPS ELB listener
单击保存返回到上一视图。点击保存,会看到端口已经打开的确认,如图 8-11 所示。
图 8-11。
Confirmation that HTTPS listener was created
启用 Cookie 粘性
在 ELB 上还有一个设置我们必须改变。我们知道我们的应用将开始使用基于会话的 cookies。但是由于我们的应用运行在几个实例上,不能保证一旦用户登录,他或她会被两次定向到同一个实例。这将有效地阻止会话完全可用。幸运的是,有一个名为粘性的 ELB 特性,允许我们将用户的会话绑定到特定的实例,并转发相关的 cookie。这意味着,一旦用户在实例上启动了会话,该会话中的所有未来请求都将绑定到该实例。
返回负载平衡器的描述选项卡,找到端口配置设置(参见图 8-12 )。
图 8-12。
ELB Port Configuration
单击端口 443 配置旁边的编辑。在出现的模式视图中,选择“启用应用生成的 Cookie 粘性”。在 Cookie 名称字段中,输入 connect.sid(见图 8-13 )并点击保存。
图 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 ,点击创建。
图 8-14。
New security group
接下来,我们必须让我们的应用堆栈使用这个新的安全组。导航到 OpsWorks 并选择您的应用堆栈。我们不能让我们的应用层没有任何安全组,所以首先,我们必须添加自定义的安全组,然后删除默认的安全组。从导航菜单中选择层,然后单击应用层旁边的安全性。点击右上角的编辑按钮。从下拉列表中选择您的自定义安全组(参见图 8-15 )并点击保存。
图 8-15。
Custom security group
从导航菜单转到堆栈。转到堆栈设置,然后单击编辑。在底部,您将看到 OpsWorks 安全组的切换。如图 8-16 所示,当您将其设置为“否”并单击“保存”时,您的实例将不再使用自动生成的安全组。
图 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.session和req.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 在这些变化中发挥了不小的作用。如果这本书给了你,读者,与这些变化一起成长的信心,那么你已经学到了书中最重要的一课。