将 CloudFront 设置为托管您的 Web 应用程序

Avatar of Adam Rackis
Adam Rackis

DigitalOcean 提供适用于您旅程各个阶段的云产品。立即开始使用 $200 免费积分!

在我上一篇文章中,我们介绍了如何设置一个 Web 应用程序,它从 CloudFront 提供 CSS 和 JavaScript 的块和包。我们将其集成到 Vite 中,以便当应用程序在浏览器中运行时,从应用程序根 HTML 文件请求的资产将从 CloudFront 作为 CDN 拉取。

虽然 CloudFront 的边缘缓存确实提供了优势,但从这些多个位置提供应用程序的资源并非没有成本。让我们看一下我自己 Web 应用程序的 WebPageTest 跟踪,它运行的是上一篇博文中配置的配置。

注意第 2-4 行的大连接时间。第 1 行是我们的 HTML 入口点。解析 HTML,浏览器看到位于 CDN 上的 JavaScript 和 CSS 资产的脚本和链接标签,并请求它们。这会导致建立一个新的连接,正如您所看到的,这需要时间。

这篇文章将向您展示如何解决这个问题。我们将逐步介绍如何将整个 Web 应用程序托管在 CloudFront 上,并让 CloudFront 将数据、身份验证等不可缓存的请求转发(或“代理”)到我们的基础 Web 服务器。

请注意,这比我们在上一篇文章中看到的内容要多得多,并且基于您的 Web 应用程序的确切需求,这些说明可能对您有所不同,因此您的里程可能会有所不同。我们将更改 DNS 记录,并且根据您的 Web 应用程序,您可能需要添加一些缓存标头以防止某些资产被缓存。我们将深入探讨所有这些内容!

您可能想知道我们在上一篇文章中介绍的设置是否提供任何好处,因为我们在本文中所做的事情。考虑到较长的连接时间,我们是否应该放弃 CDN,而是从 Web 服务器提供所有资产,以避免更长的等待时间?我用自己的 Web 应用程序对这种情况进行了测量,上面的 CDN 版本确实更快,但差距并不大。最初的 LCP 页面加载速度大约快了 200-300 毫秒。请记住,这仅仅是针对初始加载。一旦建立了这个连接,边缘缓存应该为所有后续的异步加载的块提供更大的价值。

设置我们的 DNS

我们的最终目标是从 CloudFront 提供我们的整个 Web 应用程序。这意味着当我们访问我们的域名时,我们希望结果来自 CloudFront,而不是它目前链接到的任何 Web 服务器。这意味着我们必须修改我们的 DNS 设置。我们将为此使用 AWS Route 53。

我使用mydemo.technology作为示例,这是我拥有的域名。我将在这里向您展示所有步骤。但到您阅读本文时,我已经从我的 Web 应用程序中删除了这个域名。因此,当我开始向您展示实际的 CNAME 记录等时,那些将不再存在。

转到 Route 53 主页,然后单击托管区域

Showing the hosted zone configuration screen in the CloudFront settings.

单击创建托管区域并输入应用程序的域名

现在,请注意下一个屏幕中列出的名称服务器。它们应该类似于以下内容。

我们还没有真正完成任何事情。我们告诉 AWS 我们希望它为我们管理这个域名,AWS 向我们提供了它将通过其路由我们的流量的名称服务器。为了实现这一点,我们需要转到我们的域名注册的地方。应该有一个地方让您输入您自己的自定义名称服务器。

请注意,我的域名是在 GoDaddy 上注册的,这体现在本文中的屏幕截图中。UI、设置和选项可能与您在注册商中看到的有所不同。

警告:我建议在进行更改之前写下原始名称服务器以及所有 DNS 记录。这样,如果出现故障,您将拥有将系统回滚到开始之前状态所需的一切。即使一切正常,您仍然需要将所有其他记录重新添加到 Route 53 中,例如 MX 记录等。

设置 CloudFront 分发

让我们创建一个 CloudFront 分发来托管我们的 Web 应用程序。我们在上一篇文章中介绍了基础知识,因此我们将直接进入主题。与上次相比,一个很大的变化是我们在源域中输入的内容。不要输入顶级域名,例如 your-app.net。您需要的是托管应用程序的基础域。如果那是 Heroku,那么输入 Heroku 提供给您的 URL。

接下来,如果您计划通过安全的 HTTPS 连接使用此站点,请确保更改默认协议

这部分至关重要。如果您的 Web 应用程序正在运行身份验证、托管数据或任何其他操作,请确保启用除 GET 以外的其他动词。如果您跳过这部分,那么用于身份验证、修改数据等的任何 POST 请求将被拒绝并失败。如果您的 Web 应用程序只做的是提供资产,而所有这些事情都由外部服务处理,那么太棒了!您拥有很棒的设置,您可以跳过此步骤。

与上次相比,我们必须对缓存密钥和源请求设置进行一些更改

我们需要创建一个缓存策略,其最小 TTL 为 0,因此我们发送回的非缓存标头将被正确尊重。您可能还想启用所有查询字符串。当多个 GraphQL 请求与不同的查询字符串一起发出时,我看到了奇怪的行为,这些查询字符串被忽略了,导致所有这些请求在 CloudFront 的角度看来都相同。

我的策略最终看起来像这样

对于源请求策略,如果需要,我们应该确保发送查询字符串和 Cookie 以使身份验证和数据查询正常工作。需要明确的是,这决定了是否从 CloudFront 发送 Cookie 和查询字符串到您的 Web 服务器(例如 Heroku 或类似服务器)。

我的看起来像这样

最后,对于响应标头策略,我们可以从列表中选择“CORS 带预检”。最后,您的前两个将具有不同的名称,具体取决于您设置它们的方式。但我的看起来像这样

让我们将我们的域名(无论它是什么)连接到这个 CloudFront 分发。不幸的是,这比您预期的要多。我们需要向 AWS 证明我们实际上拥有这个域名,因为对于亚马逊来说,我们可能没有。我们在 Route 53 中创建了一个托管区域。我们获取了它提供的名称服务器并将它们注册到 GoDaddy(或您注册域名的任何地方)。但亚马逊还不知道这一点。我们需要向亚马逊证明我们确实控制着该域名的 DNS。

首先,我们将请求 SSL 证书。

接下来,让我们请求证书链接

现在,我们将选择请求公共证书选项

我们需要提供域名

而且,在我的情况下,证书正在等待

因此,我将单击它

这证明我们拥有并控制着这个域名。在一个单独的标签中,返回 Route 53,然后打开我们的托管区域

现在我们需要创建 CNAME 记录。复制记录名称的第一部分。例如,如果 CNAME 是_xhyqtrajdkrr.mydemo.technology,那么请放入_xhyqtrajdkrr部分。对于记录值,请复制整个值。

假设您将 AWS 名称服务器注册到您的域名主机 GoDaddy 或任何其他地方,AWS 很快就能 ping 它刚刚要求您创建的 DNS 条目,查看它期望的响应,并验证您的证书。

您在开始时设置的名称服务器可能需要一些时间才能传播。理论上,这可能需要长达 72 小时,但对我来说通常会在 1 小时内更新。

您将在域名上看到成功

…以及证书

!快要完成了。现在让我们将所有这些连接到我们的 CloudFront 分发。我们可以返回到 CloudFront 设置屏幕。现在,在自定义SSL 证书下,我们应该看到我们创建的证书(以及您之前创建的任何其他证书)

然后,让我们添加应用程序的顶级域名

剩下的就是告诉 Route 53 将我们的域名路由到这个 CloudFront 分发。因此,让我们回到 Route 53 并创建另一个 DNS 记录。

我们需要为 IPv4 输入 A 记录,为 IPv6 输入 AAAA 记录。对于两者,请将记录名称保留为空,因为我们只注册我们的顶级域名,而不是其他任何内容。

选择 A 记录类型。接下来,将记录指定为别名,然后将别名映射到 CloudFront 分发。这应该会打开一个选项,让您选择您的 CloudFront 分发,并且由于我们之前在 CloudFront 中注册了域名,因此在进行选择时,您应该只看到该分发,而不是其他任何分发。

我们为 IPv6 支持所需的 AAAA 记录类型重复完全相同的步骤。

运行您的 Web 应用程序,并确保它确实有效。它应该可以正常工作!

需要测试和验证的事项

好的,虽然我们技术上已经完成了,但很有可能仍然有一些事情要做才能满足您的 Web 应用程序的确切需求。不同的应用程序有不同的需求,我到目前为止所展示的内容指导我们完成了通过 CloudFront 路由内容以获得更好性能的常见步骤。很可能您的应用程序中有一些独特的事情需要更多关注。因此,为此,我将介绍一些您在设置过程中可能遇到的几个可能的额外项目。

首先,确保您拥有的任何 POST 都已正确发送到您的源。假设 CloudFront 正确配置为将 Cookie 转发到您的源,这应该已经可以正常工作,但检查一下并无坏处。

更大的问题是所有其他发送到您的 Web 应用程序的 GET 请求。默认情况下,CloudFront 收到的任何 GET 请求(如果已缓存),都会使用缓存的响应提供给您的 Web 应用程序。这可能是灾难性的。任何使用 GET 发送到任何 REST 或 GraphQL 端点的​​数据请求都会被 CDN 缓存。如果您正在发布服务工作者,那也会被缓存,而不是正常的行为,正常的行为是在后台发送当前版本并在有更改时更新。

为了告诉 CloudFront *不要* 缓存某些内容,请确保将 "Cache-Control" 标头设置为 "no-cache"。如果您使用的是像 Express 这样的框架,您可以为您的数据访问设置中间件,就像这样

app.use("/graphql", (req, res, next) => {
  res.set("Cache-Control", "no-cache");
  next();
});
app.use(
  "/graphql",
  expressGraphql({
    schema: executableSchema,
    graphiql: true,
    rootValue: root
  })
); 

对于像服务工作者这样的东西,您可以在静态中间件之前为这些文件设置特定规则

app.get("/service-worker.js", express.static(__dirname + "/react/dist", { setHeaders: resp => resp.set("Cache-Control", "no-cache") }));
app.get("/sw-index-bundle.js", express.static(__dirname + "/react/dist", { setHeaders: resp => resp.set("Cache-Control", "no-cache") }));
app.use(express.static(__dirname + "/react/dist", { maxAge: 432000 * 1000 * 10 }));

等等。彻底测试一切,因为有很多可能出错的地方。在您进行每次更改后,请确保在 CloudFront 中运行完整的无效化并清除缓存,然后重新运行您的 Web 应用程序以测试是否有东西被正确地从缓存中排除。您可以在 CloudFront 的 **无效化** 选项卡中执行此操作。打开它并将 /* 放在值中,以清除所有内容。

一个可行的 CloudFront 实现

现在我们已经运行了所有东西,让我们在 WebPageTest 中重新运行我们的跟踪

就这样,我们不再像之前那样为我们的资产建立连接。对于我自己的 Web 应用程序,我在 LCP 中看到了 500 毫秒的实质性改进。这是一个不错的胜利!


在 CDN 上托管整个 Web 应用程序可以提供所有优点。我们获得了静态资源的边缘缓存,但没有连接成本。不幸的是,这种改进不是免费的。正确设置所有必要的代理并不完全直观,然后仍然需要设置缓存头以避免不可缓存的请求进入 CDN 的缓存。