aws-solutions-constructs icon indicating copy to clipboard operation
aws-solutions-constructs copied to clipboard

aws-cloudfront-s3 - support for s3 websites

Open kmturley opened this issue 5 years ago • 6 comments

When using your CloudFrontToS3 construct it creates an S3 bucket and CloudFront very easily. https://docs.aws.amazon.com/solutions/latest/constructs/aws-cloudfront-s3.html

This configuration works really well for hosting individual files. However when you want to host a static site on s3 the it requires a lot of manual overrides.

S3 Bucket props for switching on website hosting seems simple:

const construct = new CloudFrontToS3(this, 'my-website', {
  bucketProps: {
    websiteErrorDocument: '/404/index.html',
    websiteIndexDocument: 'index.html'
  }
});

But then when deployed, the url defined in the CloudFront Origin is the S3 bucket REST url not the Website url:

REST API endpoints use this format: DOC-EXAMPLE-BUCKET.s3.amazonaws.com

Website endpoints use this format: DOC-EXAMPLE-BUCKET.s3-website-us-east-1.amazonaws.com

Then there are further requirements for s3 bucket website hosting:

  • Objects in the bucket must be publicly accessible.
  • Objects in the bucket can't be encrypted by AWS Key Management Service (AWS KMS).
  • The bucket policy must allow access to s3:GetObject.
  • If the bucket policy grants public read access, then the AWS account that owns the bucket must also own the object.
  • The requested objects must exist in the bucket.
  • Amazon S3 Block Public Access must be disabled on the bucket.
  • If Requester Pays is enabled, then the request must include the request-payer parameter.
  • If you're using a Referrer header to restrict access from CloudFront to your S3 origin, then review the custom header.

https://aws.amazon.com/premiumsupport/knowledge-center/s3-website-cloudfront-error-403/

All this adds up to hours of work per developer to find the correct configuration overrides, to get static hosting working with CloudFront.

Use Case

It's a common use-case to host static sites on S3, not just hosting static files. So let's make it even easier to use CDK to deploy a static site, and also give developers an AWS opinionated way to securely deploy static sites!

Proposed Solution

Set a type, to automatically configure the settings between file hosting and website hosting:

const construct = new CloudFrontToS3(this, 'my-website', {
  type: 'website'
});

Other

Related issues others have faced trying to implement these settings themselves: https://stackoverflow.com/questions/34060394/cloudfront-s3-website-the-specified-key-does-not-exist-when-an-implicit-ind/64397720

kmturley avatar Oct 17 '20 01:10 kmturley

I am also keen to see this, especially in a production context where the deployment of new versions require zero downtime: eg. Blue-Green etc.

A construct showing the way in this regard would be a key resource!

sholtomaud avatar Oct 19 '20 01:10 sholtomaud

Looks like there is a recommended approach for static sites using CloudFront and private S3 buckets (set using Origin Access Identities): https://aws.amazon.com/blogs/compute/implementing-default-directory-indexes-in-amazon-s3-backed-amazon-cloudfront-origins-using-lambdaedge/

It involves adding a Lambda function to redirect requests (instead of using s3 website redirects).

However when I try adding the Lambda function it means you have to override the default originConfigs (S3 bucket and security headers Lambda). This removes helpful features which are already supported.

This is as far as I got before things started breaking:

import { CloudFrontToS3 } from '@aws-solutions-constructs/aws-cloudfront-s3';
import * as lambda from '@aws-cdk/aws-lambda';
import { LambdaEdgeEventType } from '@aws-cdk/aws-cloudfront';

const lambdaFunction = new lambda.Function(this, 'Function', {
  runtime: lambda.Runtime.NODEJS_12_X,
  code: lambda.Code.fromAsset('./lambdas'),
  handler: 'index.handler',
});
const construct = new CloudFrontToS3(this, 'my-website', {
  cloudFrontDistributionProps: {
    originConfigs: {
      behaviors: [
        {
          isDefaultBehavior: true,
          lambdaFunctionAssociations: [
            {
              eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
              lambdaFunction: lambdaFunction.latestVersion
            }
          ]
        }
      ]
    }
  },
});

I wonder if the security header lambda could be extended to offer the redirect logic needed? since it's already created and supported. It's only five lines of code:

exports.handler = (event, context, callback) => {
    var request = event.Records[0].cf.request;
    var olduri = request.uri;
    var newuri = olduri.replace(/\/$/, '\/index.html');
    request.uri = newuri;
    return callback(null, request);
};

kmturley avatar Oct 21 '20 00:10 kmturley

Look like there was a changed made in the latest release:

v1.67.0 public readonly cloudFrontWebDistribution: cloudfront.CloudFrontWebDistribution; https://github.com/awslabs/aws-solutions-constructs/blob/6af666f367c90a665534907bac9b4604bc134bdd/source/patterns/%40aws-solutions-constructs/aws-cloudfront-s3/lib/index.ts#L52

v1.68.0 public readonly cloudFrontWebDistribution: cloudfront.Distribution; https://github.com/awslabs/aws-solutions-constructs/blob/master/source/patterns/%40aws-solutions-constructs/aws-cloudfront-s3/lib/index.ts#L52

This allows you to use the new method addBehavior() after the stack has been created:

import { CloudFrontToS3 } from '@aws-solutions-constructs/aws-cloudfront-s3';
import { IBucket } from '@aws-cdk/aws-s3';
import * as lambda from '@aws-cdk/aws-lambda';
import { LambdaEdgeEventType } from '@aws-cdk/aws-cloudfront';
import { S3Origin } from '@aws-cdk/aws-cloudfront-origins';

const construct = new CloudFrontToS3(this as any, 'my-website');
const lambdaFunction = new lambda.Function(this, 'Function', {
  runtime: lambda.Runtime.NODEJS_12_X,
  code: lambda.Code.fromAsset('./lambdas'),
  handler: 'redirect.handler',
});
construct.cloudFrontWebDistribution.addBehavior('/*', new S3Origin(construct.s3Bucket as IBucket), {
  edgeLambdas: [
    {
      functionVersion: lambdaFunction.currentVersion,
      eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
    },
  ],
});

/lambdas/redirect.js

exports.handler = (event, context, callback) => {
  var request = event.Records[0].cf.request;
  var olduri = request.uri;
  var newuri = olduri.replace(/\/$/, '\/index.html');
  request.uri = newuri;
  return callback(null, request);
};

kmturley avatar Oct 21 '20 23:10 kmturley

kmturley@ Are you still facing any issue with the updates rolled out in v1.67.0+ ?

hnishar avatar Nov 03 '20 16:11 hnishar

@hnishar In 1.68+ It's now working as expected, I used the addBehavior method to extend the functionality and support static websites with redirects from '/' to '/index.html'. As mentioned in the blog post: https://aws.amazon.com/blogs/compute/implementing-default-directory-indexes-in-amazon-s3-backed-amazon-cloudfront-origins-using-lambdaedge/

However I think this feature is so common, it should be supported by either this construct, or a new construct specifically for static websites.

This example also shows the number of steps we have to repeat to setup a static site: https://github.com/aws-samples/aws-cdk-examples/blob/master/typescript/static-site/static-site.ts

kmturley avatar Nov 03 '20 17:11 kmturley

I'd like to strongly agree with @kmturley

it's very surprising that isn't hanlded by a key like website.

aisflat439 avatar Feb 16 '22 19:02 aisflat439