diff --git a/examples/aws-router/sst.config.ts b/examples/aws-router/sst.config.ts index 5db6f2d75c..6dac06097d 100644 --- a/examples/aws-router/sst.config.ts +++ b/examples/aws-router/sst.config.ts @@ -22,9 +22,12 @@ export default $config({ access: "public", }); const router = new sst.aws.Router("MyRouter", { + // Use the AWS-managed CachingDisabled policy so this distribution is + // compatible with the CloudFront Free Tier and safe for the API route. + cachePolicy: sst.aws.cloudfront.cachePolicy.cachingDisabled, routes: { "/api/*": api.url, - "/*": $interpolate`https://${bucket.domain}`, + "/*": { bucket }, }, }); diff --git a/platform/src/components/aws/cdn.ts b/platform/src/components/aws/cdn.ts index 75ceec2f0a..3341f501df 100644 --- a/platform/src/components/aws/cdn.ts +++ b/platform/src/components/aws/cdn.ts @@ -174,6 +174,12 @@ export interface CdnArgs { * The default cache behavior for this distribution. */ defaultCacheBehavior: cloudfront.DistributionArgs["defaultCacheBehavior"]; + /** + * The cache policy to use for the default cache behavior. + * + * When set, this overrides the cache policy configured on `defaultCacheBehavior`. + */ + cachePolicy?: Input; /** * An ordered list of cache behaviors for this distribution. Listed in order of precedence. The first cache behavior will have precedence 0. */ @@ -385,7 +391,27 @@ export class Cdn extends Component { enabled: true, origins: args.origins, originGroups: args.originGroups, - defaultCacheBehavior: args.defaultCacheBehavior, + defaultCacheBehavior: all([ + args.defaultCacheBehavior, + args.cachePolicy, + ]).apply(([behavior, cachePolicy]) => { + const effectiveCachePolicy = + cachePolicy ?? behavior.cachePolicyId; + if (!effectiveCachePolicy) return behavior; + + const { + forwardedValues: _forwardedValues, + minTtl: _minTtl, + defaultTtl: _defaultTtl, + maxTtl: _maxTtl, + ...rest + } = behavior; + + return { + ...rest, + cachePolicyId: effectiveCachePolicy, + }; + }), orderedCacheBehaviors: args.orderedCacheBehaviors, defaultRootObject: args.defaultRootObject, customErrorResponses: args.customErrorResponses, diff --git a/platform/src/components/aws/cloudfront.ts b/platform/src/components/aws/cloudfront.ts new file mode 100644 index 0000000000..6dbb69f689 --- /dev/null +++ b/platform/src/components/aws/cloudfront.ts @@ -0,0 +1,19 @@ +/** + * AWS-managed CloudFront policies and configuration values. + */ +export const cloudfront = { + cachePolicy: { + /** + * Disables caching. This policy is useful for dynamic content and APIs. + * + * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html + */ + cachingDisabled: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", + /** + * Optimizes cache efficiency by minimizing values included in the cache key. + * + * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html + */ + cachingOptimized: "658327ea-f89d-4fab-a63d-7e88639e58f6", + }, +} as const; diff --git a/platform/src/components/aws/index.ts b/platform/src/components/aws/index.ts index 78bd610e70..2ee242e006 100644 --- a/platform/src/components/aws/index.ts +++ b/platform/src/components/aws/index.ts @@ -10,6 +10,7 @@ export * from "./auth.js"; export * from "./bucket.js"; export * from "./bus.js"; export * from "./cluster.js"; +export * from "./cloudfront.js"; export * from "./cognito-identity-pool.js"; export * from "./cognito-user-pool.js"; export * from "./cron.js"; diff --git a/platform/src/components/aws/router.ts b/platform/src/components/aws/router.ts index bfd5805f01..1795d86e41 100644 --- a/platform/src/components/aws/router.ts +++ b/platform/src/components/aws/router.ts @@ -1043,6 +1043,15 @@ export interface RouterArgs { } >; + /** + * The CloudFront cache policy to use by default for routes that do not specify + * their own policy. + * + * By default, SST creates a cache policy for server routes and uses CloudFront's + * managed CachingOptimized policy for bucket routes. + */ + cachePolicy?: Input; + /** * Configure Lambda function URL protection through CloudFront Origin Access Control. * @@ -1874,6 +1883,8 @@ async function handler(event) { } function createCachePolicy() { + if (args.cachePolicy) return undefined; + defaultCachePolicy = defaultCachePolicy ?? new cloudfront.CachePolicy( @@ -1962,9 +1973,11 @@ async function handler(event) { "PUT", ], cachedMethods: ["GET", "HEAD"], - defaultTtl: 0, compress: true, - cachePolicyId: route.cachePolicy ?? createCachePolicy().id, + cachePolicyId: + route.cachePolicy ?? + args.cachePolicy ?? + createCachePolicy()!.id, // CloudFront's Managed-AllViewerExceptHostHeader policy originRequestPolicyId: "b689b0a8-53d0-40ab-baf2-68738e2966ac", @@ -2061,7 +2074,7 @@ async function handler(event) { const kvStoreArn = createRequestKvStore(); const requestFunction = createRequestFunction(); const responseFunction = createResponseFunction(); - const cachePolicyId = createCachePolicy().id; + const cachePolicyId = args.cachePolicy ?? createCachePolicy()!.id; const edgeFunction = createLambdaEdgeFunction(); const distribution = createDistribution(); diff --git a/platform/src/components/aws/static-site.ts b/platform/src/components/aws/static-site.ts index d7aa009a3e..6805eac948 100644 --- a/platform/src/components/aws/static-site.ts +++ b/platform/src/components/aws/static-site.ts @@ -393,6 +393,12 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { * ``` */ domain?: CdnArgs["domain"]; + /** + * The CloudFront cache policy to use for the default cache behavior. + * + * By default, CloudFront's managed CachingOptimized policy is used. + */ + cachePolicy?: Input; /** * @deprecated The `router` prop is now the recommended way to serve your site * through a `Router` component. @@ -1199,7 +1205,8 @@ async function handler(event) { cachedMethods: ["GET", "HEAD"], compress: true, // CloudFront's managed CachingOptimized policy - cachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6", + cachePolicyId: + args.cachePolicy ?? "658327ea-f89d-4fab-a63d-7e88639e58f6", functionAssociations: all([ createRequestFunction(), createResponseFunction(), diff --git a/platform/test/components/cdn.test.ts b/platform/test/components/cdn.test.ts new file mode 100644 index 0000000000..c21bb4d859 --- /dev/null +++ b/platform/test/components/cdn.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it, vi } from "vitest"; +import * as pulumi from "@pulumi/pulumi"; + +vi.mock( + "../../src/components/aws/providers/distribution-deployment-waiter.js", + () => ({ + DistributionDeploymentWaiter: class { + isDone = pulumi.output(true); + }, + }), +); + +// @ts-ignore +global.$app = { name: "app", stage: "test" }; +global.$util = pulumi; + +pulumi.runtime.setMocks( + { + newResource: (args: pulumi.runtime.MockResourceArgs) => ({ + id: `${args.inputs.name}_id`, + state: { + ...args.inputs, + domainName: `${args.inputs.name}.cloudfront.net`, + hostedZoneId: "Z2FDTNDATAQYW2", + aliases: args.inputs.aliases ?? [], + etag: "etag", + }, + }), + call: (args: pulumi.runtime.MockCallArgs) => args.inputs, + }, + "project", + "stack", + false, +); + +describe("Cdn", () => { + it("overrides the default behavior cache policy", async () => { + const { Cdn } = await import("../../src/components/aws/cdn"); + const cdn = new Cdn("TestCdn", { + origins: [ + { + originId: "default", + domainName: "example.com", + customOriginConfig: { + httpPort: 80, + httpsPort: 443, + originProtocolPolicy: "https-only", + originSslProtocols: ["TLSv1.2"], + }, + }, + ], + cachePolicy: "managed-policy", + defaultCacheBehavior: { + targetOriginId: "default", + viewerProtocolPolicy: "redirect-to-https", + allowedMethods: ["GET", "HEAD"], + cachedMethods: ["GET", "HEAD"], + forwardedValues: { + queryString: true, + cookies: { + forward: "none", + }, + }, + defaultTtl: 60, + cachePolicyId: "sst-policy", + }, + }); + + await new Promise((resolve, reject) => { + cdn.nodes.distribution.apply((distribution) => { + pulumi.output(distribution.defaultCacheBehavior).apply((behavior) => { + try { + expect(behavior).toMatchObject({ + targetOriginId: "default", + cachePolicyId: "managed-policy", + }); + expect(behavior.forwardedValues).toBeUndefined(); + expect(behavior.defaultTtl).toBeUndefined(); + resolve(); + } catch (error) { + reject(error); + } + }); + }); + }); + }); +});