AWS-NodeJS-入门指南-二-

236 阅读1小时+

AWS NodeJS 入门指南(二)

原文:Beginning Amazon Web Services With Node.js

协议:CC BY-NC-SA 4.0

四、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