Shared CloudFront Distribution Cache Policy with SST

Alvin JohanssonAlvin Johansson
4 min read

If you're sitting in a growing team and working with SST it's highly likely that you're working with personal staging environments. You might even use PR-deployments to test your sites before merging (smart of you). You might also have run into the problem of having too many CloudFront Distribution Cache Policies. The maximum number of cache policies per account is 20.

The error occurs because every deployment on a unique stage deploys an SST server response cache policy.

By default, the cache policy is configured to cache all responses from the server rendering Lambda based on the query-key only. If you're using cookie or header based authentication, you'll need to override the * cache policy to cache based on those values as well. - from the SST documentation

One of the possible remedies here is to create a shared cache policy for your main development and production environment. You'd then re-use the policy for your personal stages and PR-deployments. Let's look at how to set this up.

The Plan

You're going to need at least two stacks, one for your site and one for the policy. When deploying the main distribution for the stage (as in dev and not pr-1) we'd deploy the policy first together with the id of it as a value in SSM Parameter Store. We'd then fetch the id of the policy during deployment and use it for the CloudFront Distribution in the new stack. For every other stage in the account we'd only have to deploy our site stack. Let's look at some code.

Distribution Policy Stack

import {
  CachePolicy,
  CacheQueryStringBehavior,
} from "aws-cdk-lib/aws-cloudfront";
import { StringParameter } from "aws-cdk-lib/aws-ssm";
import { Duration } from "aws-cdk-lib/core";
import { StackContext } from "sst/constructs";

// the ssm param name we will share with the other stack
export const cacheParamName = "/cool-site/cache-policy-id";

export default function DistCachePolicy({ stack }: StackContext) {
  // the cache policy, configure it to your liking
  const serverCachePolicy = new CachePolicy(stack, "ServerCache", {
    queryStringBehavior: CacheQueryStringBehavior.all(),
    headerBehavior: CacheHeaderBehavior.none(),
    cookieBehavior: CacheCookieBehavior.none(),
    defaultTtl: Duration.days(0),
    maxTtl: Duration.days(365),
    minTtl: Duration.days(0),
  });

  // the ssm param
  new StringParameter(stack, "CachePolicyIdParameter", {
    parameterName: cacheParamName,
    stringValue: serverCachePolicy.cachePolicyId,
  });

  stack.addOutputs({
    CachePolicyId: serverCachePolicy.cachePolicyId,
    ParameterName: cacheParamName,
  });
}

Notice here how we set up the cache policy as well as a parameter. With the name of the parameter exported as a const. This constant will be imported and used in the Site stack.

Site Stack

The site stack fetches the id of the cache policy and uses that as the server cache policy when deploying the CloudFront Distribution.

import { CachePolicy } from "aws-cdk-lib/aws-cloudfront";
import { StringParameter } from "aws-cdk-lib/aws-ssm";
import { RemixSite, StackContext } from "sst/constructs";
// the cache param we exported in the dist stack
import { cacheParamName } from "./DistCachePolicy";

export default function Site({ stack }: StackContext) {
  // read the cache policy id from SSM
  const cachePolicyId = StringParameter.valueForStringParameter(
    stack,
    cacheParamName
  );

  const serverCachePolicy = CachePolicy.fromCachePolicyId(
    stack,
    "CachePolicy",
    cachePolicyId
  );

  // works with any high-level site construct which extends SsrSite
  const site = new RemixSite(stack, "site", {
    path: "./apps/web",
    cdk: {
      serverCachePolicy,
    },
  });

  stack.addOutputs({
    DocumentationSiteUrl: site.url,
  });
}

In this example I deploy a site based on the Remix SST construct but any high-level construct based on the SsrSite construct will work (Astro, Remix, Nextjs, SolidStart, SvelteKit).

The SST Config

We should also take a look at the sst.config.ts for how we then deploy these stacks.

import { SSTConfig } from "sst";
import Site from "./stacks/Site";
import DistCachePolicy from "./stacks/DistCachePolicy";

export default {
  config(_input) {
    return {
      name: "cool-site",
      region: "eu-north-1",
    };
  },
  stacks(app) {
    // only deploy cache policy in prod and dev, reuse them in PRs
    if (app.stage === "prod" || app.stage === "dev") {
      app.stack(DistCachePolicy);
    }
    app.stack(Site);
  },
} satisfies SSTConfig;

We set the DistCache stack within an if statement that checks if the stage is either dev or prod as those are the only stages we want to create policies for.

Deployment

To deploy and start using this shared cache we need to first deploy the dev stage.

sst deploy --stage dev

After that is ready we should be good to deploy our personal stage.

sst dev

Now we can deploy upward to a 100 individual distributions using this same cache policy. Hope this helped you out!


If you enjoyed this post you could follow me on ๐• at @Paliago. I mostly engage with the serverless community and post pictures of my pets.


Elva is a serverless-first consulting company that can help you transform or begin your AWS journey for the future

79
Subscribe to my newsletter

Read articles from Alvin Johansson directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alvin Johansson
Alvin Johansson