如何为Cloudfront上的静态托管网站的子目录设置默认根对象?

时间:2015-06-24 03:29:39

标签: amazon-web-services amazon-s3 amazon-cloudfront

如何在Cloudfront上的静态托管网站上为子目录设置默认根对象?具体来说,只要用户要求www.example.com/subdir/index.html,我就会www.example.com/subdir提供服务。注意,这是用于提供在S3存储桶中保存的静态网站。此外,我想使用原始访问标识将对S3存储桶的访问限制为仅限Cloudfront。

现在,我知道Cloudfront的工作方式与S3和亚马逊的工作方式不同specifically

  

CloudFront默认根对象的行为与   Amazon S3索引文档的行为。配置Amazon S3时   作为网站并指定索引文档,Amazon S3返回   索引文档,即使用户请求了一个子目录   桶。 (索引文件的副本必须出现在每个   子目录。)有关配置Amazon S3的更多信息   作为网站和索引文档的存储桶,请参阅主机   亚马逊简单存储服务中Amazon S3上的网站   开发者指南。

因此,尽管Cloudfront允许我们指定默认的根对象,但这仅适用于www.example.com而不适用于www.example.com/subdir。为了解决这个难题,我们可以将原始域名更改为指向S3给出的网站端点。这很好用,可以统一指定根对象。不幸的是,这似乎与origin access identities不相容。具体而言,上述链接指出:

  

更改为编辑模式:

     

Web分布 - 单击“起源”选项卡,单击要编辑的原点,然后单击“编辑”。您只能创建原始访问权限   原产地类型为S3原产地的原产地标识。

基本上,为了设置正确的默认根对象,我们使用S3网站端点而不是网站存储桶本身。这与使用原始访问标识不兼容。因此,我的问题归结为

  1. 是否可以为Cloudfront上的静态托管网站的所有子目录指定默认根对象?

  2. 是否可以为从Cloudfront提供的内容设置原始访问标识,其中源是S3网站端点而不是S3存储桶?

12 个答案:

答案 0 :(得分:183)

IS 是一种方法。不要通过在下拉列表中选择它(www.example.com.s3.amazonaws.com)将其指向您的桶,而是将其指向您的桶的静态域(例如www.example.com.s3- website-us -west-2.amazonaws.com):

enter image description here

感谢This AWS Forum thread

答案 1 :(得分:8)

激活S3托管意味着您必须向全世界打开存储桶。就我而言,我需要将存储桶保密,并使用原始访问标识功能来限制对Cloudfront的访问。像@Juissi建议的那样,Lambda函数可以修复重定向:

'use strict';

/**
 * Redirects URLs to default document. Examples:
 *
 * /blog            -> /blog/index.html
 * /blog/july/      -> /blog/july/index.html
 * /blog/header.png -> /blog/header.png
 *
 */

let defaultDocument = 'index.html';

exports.handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;

    if(request.uri != "/") {
        let paths = request.uri.split('/');
        let lastPath = paths[paths.length - 1];
        let isFile = lastPath.split('.').length > 1;

        if(!isFile) {
            if(lastPath != "") {
                request.uri += "/";
            }

            request.uri += defaultDocument;
        }

        console.log(request.uri);
    }

    callback(null, request);
};

发布功能后,请转到AWS控制台中的cloudfront分发。转到Behaviors,然后选择Origin Request下的Lambda Function Associations,最后将ARN粘贴到新功能中。

答案 2 :(得分:4)

还有另一种方法可以在子目录中提供默认文件,例如example.com/subdir/。实际上,您可以(以编程方式)存储桶中密钥为subdir/的文件。此文件将显示在S3管理控制台中,但它实际存在,CloudFront将为其提供服务。

答案 3 :(得分:3)

该问题的解决方法是使用lambda @ edge重写请求。只需要为CloudFront分发的查看者请求事件设置lambda,并重写以' /'结尾的所有内容。 AND不等于' /'使用默认根文档,例如的index.html。

答案 4 :(得分:2)

更新:看起来我错了!请参阅JBaczuk的答案,该答案应该是此主题的接受答案。

不幸的是,你的两个问题的答案都是否定的。

<强> 1。是否可以为Cloudfront上的静态托管网站的所有子目录指定默认根对象?

没有。正如AWS CloudFront docs ...

中所述
  

...如果定义默认根对象,则对分发的子目录的最终用户请求不会返回默认根对象。例如,假设index.html是您的默认根对象,CloudFront会在您的CloudFront分配下收到安装目录的最终用户请求:

     

http://d111111abcdef8.cloudfront.net/install/

     

即使安装目录中出现index.html的副本,CloudFront也不会返回默认的根对象。

     

...

     

CloudFront默认根对象的行为与Amazon S3索引文档的行为不同。将Amazon S3存储桶配置为网站并指定索引文档时,即使用户请求存储桶中的子目录,Amazon S3也会返回索引文档。 (索引文档的副本必须出现在每个子目录中。)

<强> 2。是否可以为从Cloudfront提供的内容设置原始访问标识,其中源是S3网站端点而不是S3存储桶?

不直接。您对CloudFront的起源选项是S3存储桶或您自己的服务器。

这是第二种选择确实开辟了一些有趣的可能性。这可能会破坏您尝试做的目的,但您可以设置自己的服务器,其唯一的工作是成为CloudFront原始服务器。

http://d111111abcdef8.cloudfront.net/install/的请求进入时,CloudFront会将此请求转发到您的原始服务器,并要求/install。您可以根据需要配置源服务器,包括在这种情况下提供index.html

或者您可以编写一个小型网络应用程序,只需接听此电话并直接从S3获取。

但我意识到,设置自己的服务器并担心缩放它可能会破坏你首先尝试做的目的。

答案 5 :(得分:2)

Johan GorterJeremie 表示 index.html 可以存储为键为 subdir/ 的对象。 我通过awscli的{​​{1}}

验证了这种方法是有效的,并且是另一种简单的方法来做到这一点
s3api copy-object

答案 6 :(得分:2)

我完全同意这是一个荒谬的问题! CloudFront 知道将 index.html 服务为 Default Root Object 并且他们仍然说它不适用于子目录 (source) 的事实非常奇怪!

<块引用>

CloudFront 默认根对象的行为与 Amazon S3 索引文档的行为不同。当您将 Amazon S3 存储桶配置为网站并指定索引文档时,即使用户请求存储桶中的子目录,Amazon S3 也会返回索引文档。

我个人认为,AWS 已经做到了这一点,因此 CloudFront 仅成为 CDN(加载资产,其中没有任何逻辑)并且对您网站中的路径的每个请求都应该从“服务器”(例如 EC2 Node/Php 服务器,或 Lambda 函数。)

这种限制的存在是为了增强安全性,还是为了让事物分开(即逻辑和存储分离),还是为了赚更多钱(强制人们拥有专用服务器,即使是静态内容),还有待商榷。


无论如何,我在这里总结了可能的解决方案解决方法,以及它们的优缺点。

1) S3 可以是公共的 - 使用自定义源。

这是最简单的一个,最初由@JBaczuk answer 以及 this github gist 发布。由于 S3 已经支持通过静态网站托管在子目录中提供 index.html,您需要做的就是:

  1. 转到 S3,启用静态网站托管
  2. http://<bucket-name>.s3-website-us-west-2.amazonaws.com的形式抓取网址
  3. 在 CloudFront 中创建一个新源并将其作为自定义源(以及非 S3 源)输入,以便 CloudFront 在获取内容时将其视为外部网站。

优点:

  1. 非常容易设置。
  2. 它支持 /about//about/about/index.html,并正确地将最后两个重定向到第一个。

缺点:

  1. 如果您在 S3 存储桶中的文件不在 S3 的根目录中(例如在 /artifacts/* 中,那么转到 www.domain.com/about(没有尾随 /)会将您重定向到www.domain.com/artifacts/about 这是您根本不想要的东西!如果您从 CloudFront 提供服务,并且文件的路径(从根)不提供,则 S3 中的 /about/about/ 重定向基本上会中断t 匹配。

  2. 安全性和功能性:您不能将 S3 设为私有。这是因为 CloudFront 的 Origin Access Identity 将不受支持,很明显,因为 CloudFront 被指示将此 Origin 作为随机网站。这意味着用户可能会直接从 S3 获取文件,由于安全/WAF 问题,这可能不是您所需要的,如果您的 JS/html 仅依赖于您的域的路径,则网站实际运行.

  3. [可能是一个问题] CloudFront 和 S3 之间的通信不是推荐的优化内容的方式。

  4. [也许吗?] 有人抱怨分发版中的多个 Origin 无法顺利运行(即希望 /blog 去某个地方)

  5. [也许?] 有人抱怨它没有按预期保留原始查询参数。

2) 官方解决方案 - 使用 Lambda 函数。

它是 the official solution(尽管文档是 2017 年的)。还有一个 ready-to-launch 3rd-party Application (JavaScript source in github) 和示例 Python Lambda 函数 (this answer)。

从技术上讲,通过这样做,您可以创建一个迷你服务器(他们称之为无服务器!),它只向 S3 提供 CloudFront 的源请求(因此,它基本上位于 CloudFront 和 S3 之间。)

优点:

  1. 嘿,这是官方解决方案,所以可能持续时间更长,并且是最优化的。
  2. 您可以根据需要自定义 Lambda 函数并对其进行控制。您可以在其中支持进一步重定向。
  3. 如果实现正确,(如 the 3rd party JS one,我认为不是官方的)它同时支持 /about//about(后者的重定向没有尾随 {{ 1}} 到前者)。

缺点:

  1. 还有一件事需要设置。
  2. 有一只眼睛是另一回事,所以它不会坏。
  3. 检查出现问题时还有一件事。
  4. 还要维护一件事——例如自 2021 年 1 月(现在是 2021 年 4 月)以来,这里的第三方open PRs
  5. 第 3 方 JS 解决方案不保留查询参数。所以 / 是 301 重定向到 /about?foo=bar 而不是 /about/。您需要对该 lambda 函数进行更改才能使其正常工作。
  6. 第 3 方 JS 解决方案将 /about/?foo=bar 保留为规范版本。如果您希望 /about/ 成为规范版本(即其他格式通过 301 重定向到它),您必须对脚本进行更改。
  7. [minor]only works in us-east-1(自 2020 年以来在 Github 中存在未解决的问题,2021 年 4 月仍处于未解决状态且实际存在问题)。
  8. [minor] 它有其自身的成本,尽管考虑到 CloudFront 的缓存,但应该不会很大。

3) 在 S3 中创建虚假的“文件夹文件” - 使用手动脚本。

这是前两者之间的解决方案——它支持 OAI(私有 S3)并且不需要服务器。不过有点恶心!

您在这里所做的是,您运行一个脚本,该脚本为 /about 的每个子目录在 S3 中创建一个名为(具有 /about/index.html of)key 的对象并复制该 HTML 文件(将内容和 /about) 放入此对象中。

可以使用 AWS CLI 在 this Reddit answerthis answer 中找到示例脚本。

优点:

  1. 安全:支持 S3 Private 和 CloudFront OAI。
  2. 没有额外的实时片段:脚本运行预上传到 S3(或一次性),然后系统保持完整,只有 S3 和 CF 两个片段。

缺点:

  1. [需要确认]我相信它支持 content-type 但不支持带有尾随 /about/about/
  2. 从技术上讲,您存储了两个不同的文件。如果有大量 HTML 文件,可能看起来很混乱,并使您的部署成本高昂。
  3. 您的脚本必须手动查找所有子目录并在 S3 中从中创建一个虚拟对象。这有可能在未来打破。

附注。其他技巧)

在自定义错误上使用 Javascript 的肮脏技巧

虽然它看起来不像真的,但 this answer 值得称赞,IMO!

您让拒绝访问(404 变为 403)通过,然后捕获它们,并通过 JS 手动将它们重定向到正确的位置。

优点

  1. 同样,易于设置。

缺点

  1. 它依赖于客户端的 JavaScript。
  2. 它会干扰 SEO - 特别是在爬虫不运行 JS 的情况下。
  3. 它会干扰用户的浏览器历史记录。 (即后退按钮)并且可能可以通过 HTML5 / 改进(并变得更复杂!)。

答案 7 :(得分:1)

我知道这是一个老问题,但我自己也在努力解决这个问题。最终,我的目标不是在目录中设置默认文件,而是更多地使得文件的最终结果是在其末尾没有.html的情况下提供

我最终从文件名中删除了.html,并以编程方式/手动将mime类型设置为text/html。它不是传统的方式,但它似乎确实有效,并且在不牺牲cloudformation的好处的情况下满足了我对漂亮网址的要求。设置mime类型很烦人,但在我看来支付这个好处的代价很小

答案 8 :(得分:1)

有一个"official" guide published on AWS blog建议设置由您的CloudFront发行版触发的Lambda @ Edge函数:

  

当然,期望用户总是在每个URL的末尾键入index.html(或者甚至知道它应该在该URL中)是一种糟糕的用户体验。到目前为止,还没有一种通过CloudFront向用户提供这些更简单的URL(相当于Apache Web Server配置中的DirectoryIndex指令)的简便方法。如果您仍然希望能够使用OAI限制对S3原点的访问,则不会。但是,随着Lambda @ Edge的发布,您可以使用在CloudFront边缘节点上运行的JavaScript函数来查找这些模式,并从S3来源请求适当的对象密钥。

     

解决方案

     

在此示例中,您使用CloudFront边缘的计算能力来检查来自客户端的请求。然后重新编写请求,以便CloudFront为以“ /”结尾的任何请求URI请求默认索引对象(在这种情况下为index.html)。

     

对Web服务器发出请求时,客户端会在请求中指定要获取的对象。您可以使用此URI并对其应用正则表达式,以便在CloudFront从源请求该对象之前将这些URI解析为默认索引对象。使用以下代码:

'use strict';
exports.handler = (event, context, callback) => {

    // Extract the request from the CloudFront event that is sent to Lambda@Edge
    var request = event.Records[0].cf.request;

    // Extract the URI from the request
    var olduri = request.uri;

    // Match any '/' that occurs at the end of a URI. Replace it with a default index
    var newuri = olduri.replace(/\/$/, '\/index.html');

    // Log the URI as received by CloudFront and the new URI to be used to fetch from origin
    console.log("Old URI: " + olduri);
    console.log("New URI: " + newuri);

    // Replace the received URI with the URI that includes the index page
    request.uri = newuri;

    // Return to CloudFront
    return callback(null, request);

};

请按照上面链接的指南查看进行此设置所需的所有步骤,包括S3存储桶,CloudFront发行版和Lambda@Edge函数创建。

答案 9 :(得分:1)

使用lambda @ edge的另一种替代方法是使用CloudFront的错误页面。设置Custom Error Response以将所有403发送到特定文件。然后将javascript添加到该文件,以将index.html附加到以/结尾的网址。示例代码:

if ((window.location.href.endsWith("/") && !window.location.href.endsWith(".com/"))) {
    window.location.href = window.location.href + "index.html";
}
else {
    document.write("<Your 403 error message here>");
}

答案 10 :(得分:0)

@ johan-gorter上面指出,CloudFront提供以/结尾的密钥文件 经过调查,看来该选项有效,并且可以以编程方式在S3中创建这种类型的文件。因此,我写了一个小lambda,它在S3上创建文件时触发,后缀为index.html或index.htm

它的作用是将对象dir/subdir/index.html复制到对象dir/subdir/

import json
import boto3

s3_client = boto3.client("s3")

def lambda_handler(event, context):

    for f in event['Records']:

        bucket_name = f['s3']['bucket']['name']
        key_name = f['s3']['object']['key']
        source_object = {'Bucket': bucket_name, 'Key': key_name}

        file_key_name = False

        if key_name[-10:].lower() == "index.html" and key_name.lower() != "index.html":
            file_key_name = key_name[0:-10]
        elif key_name[-9:].lower() == "index.htm" and key_name.lower() != "index.htm":
            file_key_name = key_name[0:-9]
        
        if file_key_name:
            s3_client.copy_object(CopySource=source_object, Bucket=bucket_name, Key=file_key_name)

答案 11 :(得分:0)

可以使用发布的cloudfront functions,这里是sample code

注意:如果您使用的是 static website hosting,那么您不需要任何函数!