52

I have a domain example.com. I have a S3 bucket named example.com setup with an index.html file that works. Now I like to create two subfolders called old and new, each containing a separate version of a single page application. Requesting https://example.com/old (I like to omit the index.html when entering the request in address bar for browser) would open the index.html file in the old subfolder and requesting https://example.com/new would open the index.html. What is the best way of doing these redirects? Should I set something up in Route 53 example.com/old -> example.com/old/index.html or is there a better way of doing it?

g3blv
  • 3,877
  • 7
  • 38
  • 51

9 Answers9

80

No need for a lambda function adding expense and complexity to your project.

The following answer is quoted from https://stevepapa.com/

https://stevepapa.com/my-great-new-post/ would be expected to work the same way as: https://stevepapa.com/my-great-new-post/index.html

There’s a clever little way to get these flowing through to the Cloudfront distribution, and it involves changing the source origin from the one that Cloudfront presents to you by default.

When selecting the origin source Cloudfront will show you a list of S3 buckets. editing origin

Instead of setting the source from the bucket shown in the dropdown list, you’ll need to grab the static web hosting endpoint for that resource from its S3 settings page and pop it in manually. where the static hosting endpoint url is

Using the static source for the Cloudfront distribution origin means any request to that distribution will be using the S3’s root object lookup, and your 404 responses should disappear as the references flow through.

Important

After doing this:

Otherwise, the changes you made won't go live immediately.

Onema
  • 7,331
  • 12
  • 66
  • 102
Marc Guiselin
  • 3,442
  • 2
  • 25
  • 37
  • 1
    Worked for me!!! Thousands of thanks!!!! You saved my day of time because of thi s article. – VsMaX Sep 15 '18 at 15:59
  • 1
    Worked for me as well. – djuth Oct 23 '18 at 13:25
  • Worked great, this should be marked as answer, so I don't have to read the other stuff on top – Dan Parker Mar 14 '19 at 22:25
  • This is the best, simplest answer. – Thomas Amar Oct 18 '19 at 01:49
  • 1
    Would there be a way to achieve the same for `https://stevepapa.com/my-great-new-post` without the trailing `/`? – Leonardo Romanello Apr 12 '20 at 16:56
  • This does’t work for me. When I enter the endpoint URL, it puts it back to the bucket name upon bluring the input field. – Michael Schmid Aug 13 '20 at 08:25
  • @LeonardoRomanello - THis worked without any trailing `/` (it specifically routed `https://example.com/blog` to `https://example.com/blog/` automatically) – Daniel Apt Oct 17 '20 at 15:18
  • 8
    A downside to this approach is that the objects in the S3 bucket must be publicly accessible, otherwise, CloudFront will show a 403 forbidden error. – Charles Fulton Nov 12 '20 at 14:26
  • Using the REST API endpoint is preferred over using the website endpoint as otherwise you have two endpoints with different permissions. These might be indexed twice in web searches and the non-SSL endpoint might be vulnerable to potential attacks. See [the AWS docs](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html#private-content-restricting-access-to-s3-overview) for more info. Therefore I'd highly recommend [jkingok answer](https://stackoverflow.com/a/50458087/4185785) which uses the Lambda@Edge approach. – pat-s Jan 06 '21 at 16:37
  • I was browsing for this solution for days! Thanks! – Hoon Jan 10 '21 at 22:22
  • That is *so much* better than the official AWS documentation! – baduker Mar 13 '21 at 18:46
  • 1
    Love you so much! – bjovanov Nov 20 '21 at 19:14
  • this is still legit! – thedanotto Dec 25 '21 at 18:38
  • Manish brought me here... I owe you a beer/coffeee. Thanks mate – thapakazi Jun 27 '22 at 19:52
21

So I had this problem last night too.

The issue is as follows: S3 when configured as a website bucket is forgiving and has the index document setting, set to index.html and this gets applied at the root, ie, example.com actually gets redirected to example.com/index.html, and it also gets applied at the subfolder level, so example.com/new or example.com/new/ should both redirect to example.com/new/index.html, where there would be an object in the bucket. (If not, you'd get a NoSuchKey error instead.)

However you then "upgrade" yourself to CloudFront, likely for HTTPS, and this feature goes away. CloudFront instead makes explicit API calls to S3 and therefore doesn't trigger the index document concession. It does work for the root, but not for subfolders.

The RoutingRules solution doesn't look clean to me because by specifying KeyPrefixEquals rather than key exactly equals (which doesn't exist) I think you'd get unintended matches.

I instead have implemented a Lambda@Edge rule that rewrites the request that CloudFront makes to S3 to have a proper key value in it.

Start with the Lambda docs and the A/B testing example here: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-examples.html#lambda-examples-general-examples

Change the code to:

'use strict';

exports.handler = (event, context, callback) => {
    /*
     * Expand S3 request to have index.html if it ends in /
     */
    const request = event.Records[0].cf.request;
    if ((request.uri !== "/") /* Not the root object, which redirects properly */
        && (request.uri.endsWith("/") /* Folder with slash */
            || (request.uri.lastIndexOf(".") < request.uri.lastIndexOf("/")) /* Most likely a folder, it has no extension (heuristic) */
            )) {
        if (request.uri.endsWith("/"))
            request.uri = request.uri.concat("index.html");
        else
            request.uri = request.uri.concat("/index.html");
    }
    callback(null, request);
};

And publish it to your CloudFront distribution.

jkingok
  • 391
  • 3
  • 7
  • 1
    I had the exact same issue as OP, and this worked! thank you – yaliceme May 31 '18 at 12:54
  • 1
    Here's an AWS blog post with a similar solution https://aws.amazon.com/blogs/compute/implementing-default-directory-indexes-in-amazon-s3-backed-amazon-cloudfront-origins-using-lambdaedge/ – Nogwater Mar 30 '19 at 15:49
10

There is even easier way to accomplish this with an HTML redirect file

  1. Create a plain file named my-great-new-post (don't worry there won't be a name conflict with the folder in the same bucket)

  2. Write a meta-redirect code in that file (I pasted the code below)

  3. upload file to root bucket (where my-great-new-post folder lays)

  4. modify metadata of the new file and make Content-Type:text/html

Here lays the content of the file:

<!DOCTYPE html>
    <html>
    <head>
    <meta http-equiv="refresh" content="0; url=/my-great-new-post/index.html">
    </head>
    <body>
    </body>
    </html>
Ali Aksakarya
  • 471
  • 4
  • 11
  • 3
    After a years (read: years) of messing with S3 redirect rules, I'm done. This works best. "Best" meaning: I do this every few months over years and I appreciate that it "just works". – 101010 Feb 28 '20 at 17:05
  • Agreed. The documentation on redirect rules is really not great and there are no examples at all. – Askdesigners Jul 01 '20 at 14:00
  • Thank you for this simple solution. I appreciate not having to learn what AWS Lambda functions are - let alone how to write them. – michaelosthege Nov 08 '20 at 18:05
  • This was the only option that worked for me! Thank you very much for the great workaround – piyush sachdeva May 19 '21 at 03:01
8

If you're using CloudFront, you can use CloudFront functions to create a simple redirection.

I modified @jkingok's solution.

  1. Go to CloudFront, and click on Functions.
  2. Click on Create function, enter a name and optional description
  3. In the development section, enter the code snippet below and publish from the publish tab.
    function handler(event) {
      var request = event.request;
      if (request.uri !== "/" && (request.uri.endsWith("/") || request.uri.lastIndexOf(".") < request.uri.lastIndexOf("/"))) {
      if (request.uri.endsWith("/")) {
         request.uri = request.uri.concat("index.html");
      } else {
         request.uri = request.uri.concat("/index.html");
      }
     }
      return request;
   }

Once your function is completed, you can use the function by going to the "Behaviors" tab of your distribution, select the path pattern you want to modify, then under "Function associations", for Viewer Request, select "CloudFront function" as the function type and then select the function you created in the dropdown list.

Screenshot of Function Associations

Once you save the Behaviors, you can test your website.

NOTE: This solution redirects every URL without extension to "URL/index.html", you can modify the behaviour of the function to what works for you.

tlogbon
  • 1,212
  • 13
  • 12
  • Unfortunately, this only works for the root path. Answer from Marc works – Abdul Munim Apr 05 '22 at 18:57
  • 2
    I currently use this and it works for all paths, not sure what you mean by this working for just root path. The only challenge experience so far is that the relative paths fail if the original URL doesn't end with /. – tlogbon Apr 07 '22 at 16:55
  • Works great for me! Thanks a heap - helped me hit a big deadline! – bhu Boue vidya Jul 29 '22 at 00:05
  • Thanks for this elegant cross-cutting solution. I found that I could retrieve the security best practices for S3 if I implemented the redirection using this approach. Here is an AWS dev guide with a similar solution https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/example-function-add-index.html – Lavande Dec 14 '22 at 00:42
1

When you enable and configure static hosting with S3 you need to access the site via the bucket website endpoint. You can find this URL in the bucket properties in the Static website hosting section.

S3 Static website hosting settings

The URL of the website endpoint will look like this:

http://example-bucket.s3-website-eu-west-1.amazonaws.com/example-folder/

However (confusingly) objects stored in S3 are also accessible via a different URL, this url does not honour the index rules on subfolders. This URL looks like this:

https://example-bucket.s3-eu-west-1.amazonaws.com/example-folder/
0
  1. Configure your Bucket to deliver a static website
  2. Create a CloudFront Distribution: set your bucket as the Origin and leave the OriginPath empty (default: /)
  3. Create Route53 RecordSet which links to your CloudFront Distribution

You can find a helpful walkthrough here

Question: What should happen if your customer enters example.com (without old/new)?

Edit: 2. is optional. You could also link your Route53 RecordSet to your static website but CloudFront enables you to serve your wesbite with https (with help of AWS Certificate Manager).

MaiKaY
  • 4,422
  • 20
  • 28
  • Thanks. That is the guide that I have followed and that is the setup I have in CloudFront but that doesn't work for `example.com/old` or `example.com/new`. It results in access denied while `example.com/old/index.html` responds with the contents of the `index.html` in the `old` folder. Requesting `example.com` should keep responding with the `index.html` under `example.com`. – g3blv Mar 03 '18 at 15:49
0

If you are using CDK to create a CloudFrontWebDistribution with an S3 source, then your first guess is probably to do this:

                OriginConfigs = new[] {
                    new SourceConfiguration {
                        S3OriginSource = new S3OriginConfig
                        {
                            S3BucketSource = bucket
                        }
                        Behaviors = new[] { new Behavior { IsDefaultBehavior = true } }
                    }
                }

However, to configure cloudfront to use the website-bucket-url (that does have the behavior to resolve a directory to index.html), you need to use:

                OriginConfigs = new[] {
                    new SourceConfiguration {
                        CustomOriginSource = new CustomOriginConfig
                        {
                            DomainName = bucket.BucketWebsiteDomainName,
                            OriginProtocolPolicy = OriginProtocolPolicy.HTTP_ONLY
                        },
                        Behaviors = new[] { new Behavior { IsDefaultBehavior = true } }
                    }
                }

You need to specify the protocol as HTTP_ONLY because website buckets do not support HTTPS. The default for a CustomOriginSource is HTTPS_ONLY.

0

There following option is also available:

  1. Upload test.html to S3 bucket
  2. Rename test.html to test

Now you can access it with /test

It works because Content-Type is set to text/html in file's metadata.

enter image description here

Anton Matiash
  • 172
  • 2
  • 6
-1

You can try setting Redirection rules, Here is an untested rule.

<RoutingRules>
  <RoutingRule>
    <Condition>
      <KeyPrefixEquals>old</KeyPrefixEquals>
    </Condition>
    <Redirect>
      <ReplaceKeyWith>old/index.html</ReplaceKeyWith>
    </Redirect>
  </RoutingRule>
  <RoutingRule>
    <Condition>
      <KeyPrefixEquals>new</KeyPrefixEquals>
    </Condition>
    <Redirect>
      <ReplaceKeyWith>new/index.html</ReplaceKeyWith>
    </Redirect>
  </RoutingRule>
</RoutingRules>