diff --git a/packages/static-hosting/README.md b/packages/static-hosting/README.md index f105b757..9e3f34eb 100644 --- a/packages/static-hosting/README.md +++ b/packages/static-hosting/README.md @@ -3,7 +3,17 @@ ## Overview This repository defines a CDK construct for hosting a static website on AWS using S3 and CloudFront. -It can be imported and used within CDK applications. + +It can be imported and used within CDK applications. By default this construct will create a CloudFront distribution with an S3 bucket as the origin. It will also create an IAM user and group that have permission to create files in the S3 bucket. + +It has the following features that can optionally be enabled: + +- Create a DNS record in an existing hosted zone +- Store CloudFront logs in an S3 bucket +- Add a custom backend origin +- Remap static files to the the S3 or backend origin + +![static hosting diagram](docs/static_hosting.png) ## Example @@ -21,8 +31,6 @@ const HostingStackProps : StaticHostingProps = { domainName: 'domain.tld', certificateArn: 'arn:aws:acm:us-east-1:123456789:certificate/some-arn-id', createDnsRecord: false, - createPublisherGroup: true, - createPublisherUser: true, enableErrorConfig: true }; diff --git a/packages/static-hosting/docs/static_hosting.png b/packages/static-hosting/docs/static_hosting.png new file mode 100644 index 00000000..a818100c Binary files /dev/null and b/packages/static-hosting/docs/static_hosting.png differ diff --git a/packages/static-hosting/lib/static-hosting.ts b/packages/static-hosting/lib/static-hosting.ts index 756d354c..5c3eac5d 100644 --- a/packages/static-hosting/lib/static-hosting.ts +++ b/packages/static-hosting/lib/static-hosting.ts @@ -45,57 +45,206 @@ import { CSP } from "../types/csp"; import { PathRemapFunction } from "./path-remap"; export interface StaticHostingProps { - exportPrefix?: string; + /** + * Domain name for the stack. Combined with the subDomainName it is used as + * the name for the S3 origin and an alternative domain name for the + * CloudFront distribution + */ domainName: string; + + /** + * Subdomain name for the stack. Combined with the domainName it is used as + * the name for the S3 origin and an alternative domain name for the + * CloudFront distribution + */ subDomainName: string; + + /** + * An array of additional Cloudfront alternative domain names. + * + * @default undefined + */ + extraDistributionCnames?: ReadonlyArray; + + /** + * The arn of the certificate to attach to the CloudFront distribution. + * Must be created in us-east-1 + */ certificateArn: string; - createDnsRecord?: boolean; + + /** + * Custom backend host to add as a second origin to the CloudFront distribution + * + * @default undefined + */ + backendHost?: string; + + /** + * The hosted zone name to create a DNS record in. + * If not supplied a DNS record will not be created + * + * @default undefined + */ + zoneName?: string; + + /** + * Whether to create a group with permissions to publish to the S3 bucket. + * + * @default true + */ createPublisherGroup?: boolean; + + /** + * Whether to create a user with permissions to publish to the S3 bucket. + * The user will not have permissions unless the publisher group is also created + * + * @default true + */ createPublisherUser?: boolean; - extraDistributionCnames?: ReadonlyArray; + + /** + * Enable CloudFront access logs + * + * @default false + */ enableCloudFrontAccessLogging?: boolean; + + /** + * Enable S3 access logging + * + * @default false + */ enableS3AccessLogging?: boolean; - zoneName?: string; + + /** + * Enable returning the errorResponsePagePath on a 404. + * Not required when using Prerender or Feature environment Lambda@Edge functions + * + * @default false + */ enableErrorConfig?: boolean; + + /** + * Custom error response page path + * + * @default index.html + */ + errorResponsePagePath?: string; + + /** + * Create behaviours for the following file extensions to route straight to the S3 origin + * js, css, json, svg, jpg, jpeg, png, gif, ico, woff, woff2, otf + * + * @default true + */ enableStaticFileRemap?: boolean; + + /** + * Paths to remap on the default behaviour. For example you might remap deployed_sitemap.xml -> sitemap.xml + * Created a behaviour in CloudFront to handle the remap. If the paths are different + * it will also deploy a Lambda@Edge function to perform the required remap. + * + * @default undefined + */ remapPaths?: remapPath[]; - backendHost?: string; + + /** + * Functions the same as remapPaths but uses the backendHost as the origin. + * Requires a valid backendHost to be configured + * + * @see remapPaths + * @default undefined + */ remapBackendPaths?: remapPath[]; + + /** + * Override the default root object + * + * @default index.html + */ defaultRootObject?: string; + + /** + * Enforce ssl on bucket requests + * + * @default true + */ enforceSSL?: boolean; - comment?: string; /** - * Disable the use of the CSP header. Default value is false. + * Disable the use of the CSP header + * + * @default false */ disableCSP?: boolean; /** + * Adds custom CSP directives and URLs to the header. + * * AWS limits the max header size to 1kb, this is too small for complex csp headers. * The main purpose of this csp header is to provide a method of setting a report-uri. + * + * @default undefined */ csp?: CSP; /** * This will generate a csp based *purely* on the provided csp object. * Therefore disabling the automatic adding of common use-case properties. + * + * @default false */ explicitCSP?: boolean; /** * Extend the default props for S3 bucket + * + * @default undefined */ s3ExtendedProps?: BucketProps; /** - * Optional WAF ARN + * Add an external WAF via an arn + * + * @default undefined */ webAclArn?: string; + + /** + * Add response headers policies to the default behaviour + * + * @default undefined + */ responseHeadersPolicies?: ResponseHeaderMappings; + + /** + * Additional behaviours + * + * @default undefined + */ additionalBehaviors?: Record; - errorResponsePagePath?: string; + + /** + * Lambda@Edge functions to add to the default behaviour + * + * @default undefined + */ defaultBehaviorEdgeLambdas?: EdgeLambda[]; + /** + * A request policy used on the default behavior + * + * @default undefined + */ + defaultBehaviorRequestPolicy?: OriginRequestPolicy; + + /** + * A cache policy used on the default behavior + * + * @default undefined + */ + defaultBehaviorCachePolicy?: CachePolicy; + /** * After switching constructs, you need to maintain the same logical ID * for the underlying CfnDistribution if you wish to avoid the deletion @@ -105,19 +254,24 @@ export interface StaticHostingProps { * the new Distribution construct with the logical ID created by the * old construct * - * https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront-readme.html#migrating-from-the-original-cloudfrontwebdistribution-to-the-newer-distribution-construct. + * @see https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront-readme.html#migrating-from-the-original-cloudfrontwebdistribution-to-the-newer-distribution-construct. + * @default undefined */ overrideLogicalId?: string; /** - * A Request policy used on the default behavior + * A string to prefix CloudFormation outputs with + * + * @default undefined */ - defaultBehaviorRequestPolicy?: OriginRequestPolicy; + exportPrefix?: string; /** - * A Cache policy used on the default behavior + * Add a comment to the CloudFront distribution + * + * @default undefined */ - defaultBehaviorCachePolicy?: CachePolicy; + comment?: string; } interface remapPath { @@ -211,11 +365,12 @@ export class StaticHosting extends Construct { exportName: `${exportPrefix}BucketName`, }); - const publisherUser = props.createPublisherUser - ? new User(this, "PublisherUser", { - userName: `publisher-${siteName}`, - }) - : undefined; + const publisherUser = + props.createPublisherUser !== false + ? new User(this, "PublisherUser", { + userName: `publisher-${siteName}`, + }) + : undefined; if (publisherUser) { new CfnOutput(this, "PublisherUserName", { @@ -225,9 +380,10 @@ export class StaticHosting extends Construct { }); } - const publisherGroup = props.createPublisherGroup - ? new Group(this, "PublisherGroup") - : undefined; + const publisherGroup = + props.createPublisherGroup !== false + ? new Group(this, "PublisherGroup") + : undefined; if (publisherGroup) { this.bucket.grantReadWrite(publisherGroup); @@ -434,7 +590,7 @@ export class StaticHosting extends Construct { exportName: `${exportPrefix}DistributionName`, }); - if (props.createDnsRecord && props.zoneName) { + if (props.zoneName) { const zone = HostedZone.fromLookup(this, "Zone", { domainName: props.zoneName, });