19

Got a bucket called www.foo.site. In that site there's a landing page, an about page and some pages in a few bar/* folders. Each bar/* has an index.html page: bar/a/index.html, bar/b/index.html etc.

The landing page is running fine (meaning www.foo.site will load when I browse to it) but the /about/index.html page and /bar/index.html pages aren't getting served when I click on my about links etc. If I curl the URLs I get 404. I've tried setting the origin path and origin domain name separately:

First try:

domain name: www.foo.site.s3.amazonaws.com
origin path: (blank)

Second try:

domain name: s3-us-west-1.amazonaws.com
origin path: www.foo.site

Default document is index.html for both.

Neither one worked. All of the S3 pages mentioned above are directly browsable. Meaning https://s3-us-west-1.amazonaws.com/www.foo.site/bar/index.html loads the expected html.

This must be some Cloudfront setting I'm missing. Possibly something in my DNS records? Is it possible to serve html files in S3 "folders" via Cloudfront?

jcollum
  • 43,623
  • 55
  • 191
  • 321
  • 1
    Some ideas https://aws.amazon.com/blogs/compute/implementing-default-directory-indexes-in-amazon-s3-backed-amazon-cloudfront-origins-using-lambdaedge/ and https://someguyontheinter.net/blog/serving-index-pages-from-a-non-root-location-via-cloudfront/ – jarmod Jan 07 '20 at 21:41
  • That last one did the trick. The real issue was that you have to *turn off* the default document. Make an answer and I'll edit it as needed and mark it answered. – jcollum Jan 08 '20 at 02:39
  • can anyone elaborate on the answer? This is the second time I've seen the blog link to someguyontheinter.net (was also linked on Reddit for the same topic). That link now returns a 502. It's best to summarize answers in comments/responses rather than throwing up a link and saying "this works". – Chris Concannon Apr 08 '21 at 23:53
  • @ChrisConcannon what worked for me was in my comment and jarmod's answer below – jcollum Apr 08 '21 at 23:55

4 Answers4

26

Here are a couple of resources that are helpful when serving index.html from S3 implicitly via https://domain/folder/ rather than having to explicitly use https://domain/folder/index.html:

The key thing when configuring your CloudFront distribution is:

do not configure a default root object for your CloudFront distribution

If you configure index.html as the default root object then https://domain/ will correctly serve https://domain/index.html but no subfolder reference such as https://domain/folder/ will work.

It's also important to not use the dropdown in Cloudfront when connecting the CF distribution to the S3 bucket. You need to use the URL for the S3 static site instead.

jarmod
  • 71,565
  • 16
  • 115
  • 122
  • 3
    the first link is not working. And do note that if one wants to go this way, one would have to make the bucket public as Origin Access Identity wont be appicable. – CodeTripper Sep 29 '20 at 12:37
  • 2
    applying no default root object and connecting the distribution to the static site URL solved the problem. – Jonathan R Jan 18 '22 at 13:40
8

You can now use CloudFront Functions to do this instead of using Lambda@Edge. Lots of benefits to using CF Functions as mentioned here in the AWS News Blog.

You can use this CloudFormation snippet to deploy S3 / CloudFront / Functions resources.

Resources:
  DistributionBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: AES256
      BucketName: test-site-site
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
      - Key: STAGE
        Value: dev
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain

  DistributionBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref DistributionBucket
      PolicyDocument:
        Statement:
        - Action: s3:*
          Condition:
            Bool:
              aws:SecureTransport: 'false'
          Effect: Deny
          Principal:
            AWS: "*"
          Resource:
            - !GetAtt DistributionBucket.Arn
            - !Sub '${DistributionBucket.Arn}/*'
        - Action: s3:GetObject
          Effect: Allow
          Principal:
            CanonicalUser: !GetAtt DistributionOAI.S3CanonicalUserId
          Resource:
            - !Sub '${DistributionBucket.Arn}/*'
        Version: '2012-10-17'

  DistributionCertificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: !Ref DomainName
      DomainValidationOptions:
      - DomainName: !Ref DomainName
        HostedZoneId: !Ref HostedZoneId
      Tags:
      - Key: STAGE
        Value: dev
      ValidationMethod: DNS

  DistributionOAI:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: Identity for s3-origin-test-site

  Distribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Aliases:
          - !Ref DomainName
        Comment: Distribution for test-site
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
          Compress: true
          FunctionAssociations:
            - EventType: viewer-request
              FunctionARN: !GetAtt DistributionFunction.FunctionMetadata.FunctionARN
          TargetOriginId: s3-origin-test-site
          ViewerProtocolPolicy: redirect-to-https
        DefaultRootObject: index.html
        Enabled: true
        HttpVersion: http2
        IPV6Enabled: true
        Origins:
        - DomainName: !GetAtt DistributionBucket.RegionalDomainName
          Id: s3-origin-test-site
          S3OriginConfig:
            OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${DistributionOAI}'
        PriceClass: PriceClass_200
        ViewerCertificate:
          AcmCertificateArn:
            Ref: DistributionCertificate
          MinimumProtocolVersion: TLSv1.2_2021
          SslSupportMethod: sni-only
      Tags:
      - Key: STAGE
        Value: dev

  DistributionFunction:
    Type: AWS::CloudFront::Function
    Properties:
      AutoPublish: true
      FunctionCode: |
        function handler(event) {
          var request = event.request;
          var uri = request.uri;
          
          // Check whether the URI is missing a file name.
          if (uri.endsWith('/')) {
              request.uri += 'index.html';
          } 
          // Check whether the URI is missing a file extension.
          else if (!uri.includes('.')) {
              request.uri += '/index.html';
          }

          return request;
        }
      FunctionConfig:
        Comment: Redirect-Default-Index-Request
        Runtime: cloudfront-js-1.0
      Name: test-site-site-redirect-index-request

  DistributionRecordSetA:
    Type: AWS::Route53::RecordSet
    Properties:
      Name: !Sub '${DomainName}.'
      Type: A
      AliasTarget:
        DNSName: !GetAtt Distribution.DomainName
        HostedZoneId: Z2FDTNDATAQYW2
      Comment: 'Alias record for test-site'
      HostedZoneId: !Ref HostedZoneId

  DistributionRecordSetAAAA:
    Type: AWS::Route53::RecordSet
    Properties:
      Name: !Sub '${DomainName}.'
      Type: AAAA
      AliasTarget:
        DNSName: !GetAtt Distribution.DomainName
        HostedZoneId: Z2FDTNDATAQYW2
      Comment: 'Alias record for test-site'
      HostedZoneId: !Ref HostedZoneId
captainblack
  • 4,107
  • 5
  • 50
  • 60
  • 3
    This worked like a charm, and, nicely, did not cause "/index.html" to show in the browser URL. And, unlike the accepted answer, this is compatible with HTTPS over Cloudfront! Thank you! – Asker Jun 29 '23 at 12:38
2

Distilling @captainblack 's excellent answer, you can achieve your goal simply by associating the following CloudFront Function with the Viewer Request of your CloudFront distribution's cache behavior:

function handler(event) {
      var request = event.request;
      var uri = request.uri;
      
      // Check whether the URI is missing a file name.
      if (uri.endsWith('/')) {
          request.uri += 'index.html';
      } 
      // Check whether the URI is missing a file extension.
      else if (!uri.includes('.')) {
          request.uri += '/index.html';
      }

      return request;
    }

This is far better than the outdated methods in @jarmod 's answer, because this is easier to implement and, unlike those methods, this is compatible with CloudFront over HTTPS.

Another benefit of this method is that it hides the ugly /index.html string at the end of the browser's URL unless the user types that string.

Asker
  • 1,299
  • 2
  • 14
  • 31
1

CloudFront serves S3 files with keys ending by /

After investigation, it appears that one can create this type of files in S3 programatically. Therefore, I wrote a small lambda that is triggered when a file is created on S3, with a suffix index.html or index.htm

What it does is copying the object dir/subdir/index.html into the object 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)
Jeremie
  • 646
  • 8
  • 24