getlift / lift

Expanding Serverless Framework beyond functions using the AWS CDK
MIT License
913 stars 113 forks source link

Add user defined 'Cache-Control' header to CloudFront responses for assets #278

Open duswie opened 1 year ago

duswie commented 1 year ago

Start from the Use-case

It's best practise to serve static assets with a efficient cache policy: https://web.dev/uses-long-cache-ttl/

This is especially relevant when using an asset build system with cache busting (dynamic asset names). In this case I just want to set one Cache-Control header for all assets.

Example Config

constructs:
  website:
    type: server-side-website
    assetsCachePolicyHeader: max-age=31536000

Implementation Idea

A) CloudFront function: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/example-function-add-cache-control-header.html

or

B) CloudFront Response Header Policy (AWS::CloudFront::ResponseHeadersPolicy) https://docs.aws.amazon.com/de_de/AWSCloudFormation/latest/UserGuide/aws-resource-cloudfront-responseheaderspolicy.html

Not sure which one performs better, but I think the response header policy is the 'cleaner' way.

I look forward to your feedback and would implement the feature if desired.

jaulz commented 11 months ago

I think it would be great if we could even extend it using ResponseHeaderPolicy and also pass options to define the strict transport security etc.

duswie commented 9 months ago

Thanks for your Feedback! My current solution for custom response headers is as follows and work fine for me. When I find time, I will try to implement it natively.

resources:
    Resources:
        ResponseHeadersPolicyBuildAssets:
             Type: AWS::CloudFront::ResponseHeadersPolicy
             Properties:
                 ResponseHeadersPolicyConfig:
                     Name: AddCacheControlHeader
                     CustomHeadersConfig:
                         Items:
                             -   Header: Cache-Control 
                                 Override: false
                                 Value: max-age=31536000 
constructs:
    website:
        #...
        assets:
            '/build/*': public/build
        extensions:
            distribution:
                Properties:
                    DistributionConfig:
                        CacheBehaviors:
                            0:
                                ResponseHeadersPolicyId: !Ref ResponseHeadersPolicyBuildAssets
esimonetti commented 9 months ago

@duswie thanks for that. Is this the actual content of a working .yml file?

I am asking because the 0: should probably be -, and once that's changed, there are a few properties that are required that don't work for me (throw errors). Examples: TargetOriginId, ViewerProtocolPolicy etc. Disclaimer: I am using static-website and not server-side-website

Here is my related discussion about caching of static assets: https://github.com/getlift/lift/discussions/370

An alternative could be to extend the cloudfront function but unfortunately, I could not figure out how to do that programmatically.

Cheers

duswie commented 9 months ago

yes, im using it like that, but I only posted the relevant parts. 0: is used cause it has to be an object in order to get merged with the exiting CacheBehavior with index 0 generated by lift. Otherwise they all get overwritten.

It's not tested with the static-website construct. You might check the ejected CloudFormation template for the right indices.

esimonetti commented 9 months ago

Hey @duswie I am not sure what I am doing wrong on static-website. Anyway an alternative I found was to modify/overriding the previous response Cloudfront function:

function handler(event) {
    var response = event.response;

    response.headers = Object.assign({}, {
        "x-frame-options": {
            "value": "SAMEORIGIN"
        },
        "x-content-type-options": {
            "value": "nosniff"
        },
        "x-xss-protection": {
            "value": "1; mode=block"
        },
        "strict-transport-security": {
            "value": "max-age=63072000"
        }
    }, response.headers);

    var request = event.request ? event.request : null;
    var uri = request.uri ? request.uri : '';

    // if it is not / and does not end with .html add long term caching!
    if (uri != '' && !uri.endsWith('/') && !uri.endsWith('.html')) {
        Object.assign(response.headers, {
            "cache-control": { "value": "public, max-age=63072000" },
            "enrico-was-here": { "value": "yay" }
        });
    }

    return response;
}

This had the wanted behaviour. For sure not the most elegant way, but it did the job for now :)