Skip to content

Commit

Permalink
Merge pull request #202 from fargito/feat/static-website
Browse files Browse the repository at this point in the history
fix(static-website): re-enable static website hosting
  • Loading branch information
fredericbarthelet committed May 13, 2022
2 parents f715c86 + a535059 commit cb162a9
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 39 deletions.
14 changes: 14 additions & 0 deletions src/constructs/aws/StaticWebsite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import { FunctionEventType } from "aws-cdk-lib/aws-cloudfront";
import type { Construct as CdkConstruct } from "constructs";
import type { AwsProvider } from "@lift/providers";
import type { BucketProps } from "aws-cdk-lib/aws-s3";
import { RemovalPolicy } from "aws-cdk-lib";
import { redirectToMainDomain } from "../../classes/cloudfrontFunctions";
import { getCfnFunctionAssociations } from "../../utils/getDefaultCfnFunctionAssociations";
import type { CommonStaticWebsiteConfiguration } from "./abstracts/StaticWebsiteAbstract";
Expand Down Expand Up @@ -55,4 +57,16 @@ export class StaticWebsite extends StaticWebsiteAbstract {
code: cloudfront.FunctionCode.fromInline(code),
});
}

getBucketProps(): BucketProps {
return {
// Enable static website hosting
websiteIndexDocument: "index.html",
websiteErrorDocument: this.errorPath(),
// public read access is required when enabling static website hosting
publicReadAccess: true,
// For a static website, the content is code that should be versioned elsewhere
removalPolicy: RemovalPolicy.DESTROY,
};
}
}
31 changes: 23 additions & 8 deletions src/constructs/aws/abstracts/StaticWebsiteAbstract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import {
ViewerProtocolPolicy,
} from "aws-cdk-lib/aws-cloudfront";
import { S3Origin } from "aws-cdk-lib/aws-cloudfront-origins";
import type { BucketProps } from "aws-cdk-lib/aws-s3";
import { Bucket } from "aws-cdk-lib/aws-s3";
import type { Construct as CdkConstruct } from "constructs";
import { Duration } from "aws-cdk-lib";
import { CfnOutput, RemovalPolicy } from "aws-cdk-lib";
import { Duration, RemovalPolicy } from "aws-cdk-lib";
import { CfnOutput } from "aws-cdk-lib";
import type { ConstructCommands } from "@lift/constructs";
import { AwsConstruct } from "@lift/constructs/abstracts";
import type { AwsProvider } from "@lift/providers";
Expand Down Expand Up @@ -87,10 +88,9 @@ export abstract class StaticWebsiteAbstract extends AwsConstruct {
);
}

const bucket = new Bucket(this, "Bucket", {
// For a static website, the content is code that should be versioned elsewhere
removalPolicy: RemovalPolicy.DESTROY,
});
const bucketProps = this.getBucketProps();

const bucket = new Bucket(this, "Bucket", bucketProps);

// Cast the domains to an array
this.domains = configuration.domain !== undefined ? flatten([configuration.domain]) : undefined;
Expand Down Expand Up @@ -266,8 +266,7 @@ export abstract class StaticWebsiteAbstract extends AwsConstruct {
return this.provider.getStackOutput(this.distributionIdOutput);
}

private errorResponse(): ErrorResponse {
// Custom error page
errorPath(): string | undefined {
if (this.configuration.errorPage !== undefined) {
let errorPath = this.configuration.errorPage;
if (errorPath.startsWith("./") || errorPath.startsWith("../")) {
Expand All @@ -281,6 +280,15 @@ export abstract class StaticWebsiteAbstract extends AwsConstruct {
errorPath = `/${errorPath}`;
}

return errorPath;
}
}

private errorResponse(): ErrorResponse {
const errorPath = this.errorPath();

// Custom error page
if (errorPath !== undefined) {
return {
httpStatus: 404,
ttl: Duration.seconds(0),
Expand Down Expand Up @@ -326,4 +334,11 @@ export abstract class StaticWebsiteAbstract extends AwsConstruct {
code: cloudfront.FunctionCode.fromInline(code),
});
}

getBucketProps(): BucketProps {
return {
// For a static website, the content is code that should be versioned elsewhere
removalPolicy: RemovalPolicy.DESTROY,
};
}
}
130 changes: 99 additions & 31 deletions test/unit/staticWebsites.test.ts → test/unit/staticWebsite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ describe("static websites", () => {
});
const bucketLogicalId = computeLogicalId("landing", "Bucket");
const bucketPolicyLogicalId = computeLogicalId("landing", "Bucket", "Policy");
const originAccessIdentityLogicalId = computeLogicalId("landing", "CDN", "Origin1", "S3Origin");
const responseFunction = computeLogicalId("landing", "ResponseFunction");
const cfDistributionLogicalId = computeLogicalId("landing", "CDN");
const cfOriginId = computeLogicalId("landing", "CDN", "Origin1");
Expand All @@ -36,15 +35,20 @@ describe("static websites", () => {
bucketLogicalId,
bucketPolicyLogicalId,
responseFunction,
originAccessIdentityLogicalId,
cfDistributionLogicalId,
]);
expect(cfTemplate.Resources[bucketLogicalId]).toMatchObject({
expect(cfTemplate.Resources[bucketLogicalId]).toStrictEqual({
Type: "AWS::S3::Bucket",
UpdateReplacePolicy: "Delete",
DeletionPolicy: "Delete",
Properties: {
WebsiteConfiguration: {
IndexDocument: "index.html",
},
},
});
expect(cfTemplate.Resources[bucketPolicyLogicalId]).toMatchObject({
expect(cfTemplate.Resources[bucketPolicyLogicalId]).toStrictEqual({
Type: "AWS::S3::BucketPolicy",
Properties: {
Bucket: {
Ref: bucketLogicalId,
Expand All @@ -55,9 +59,7 @@ describe("static websites", () => {
Action: "s3:GetObject",
Effect: "Allow",
Principal: {
CanonicalUser: {
"Fn::GetAtt": [originAccessIdentityLogicalId, "S3CanonicalUserId"],
},
AWS: "*",
},
Resource: { "Fn::Join": ["", [{ "Fn::GetAtt": [bucketLogicalId, "Arn"] }, "/*"]] },
},
Expand All @@ -66,20 +68,14 @@ describe("static websites", () => {
},
},
});
expect(cfTemplate.Resources[originAccessIdentityLogicalId]).toMatchObject({
Type: "AWS::CloudFront::CloudFrontOriginAccessIdentity",
Properties: {
CloudFrontOriginAccessIdentityConfig: {
Comment: `Identity for ${cfOriginId}`,
},
},
});
expect(cfTemplate.Resources[cfDistributionLogicalId]).toMatchObject({
expect(cfTemplate.Resources[cfDistributionLogicalId]).toStrictEqual({
Type: "AWS::CloudFront::Distribution",
Properties: {
DistributionConfig: {
Comment: "app-dev landing website CDN",
CustomErrorResponses: [
{
// The response code is forced to 200 and changed to /index.html
ErrorCachingMinTTL: 0,
ErrorCode: 404,
ResponseCode: 200,
Expand All @@ -88,6 +84,7 @@ describe("static websites", () => {
],
DefaultCacheBehavior: {
AllowedMethods: ["GET", "HEAD", "OPTIONS"],
CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6",
Compress: true,
TargetOriginId: cfOriginId,
ViewerProtocolPolicy: "redirect-to-https",
Expand All @@ -106,23 +103,24 @@ describe("static websites", () => {
IPV6Enabled: true,
Origins: [
{
DomainName: {
"Fn::GetAtt": [bucketLogicalId, "RegionalDomainName"],
CustomOriginConfig: {
OriginProtocolPolicy: "http-only",
OriginSSLProtocols: ["TLSv1.2"],
},
Id: cfOriginId,
S3OriginConfig: {
OriginAccessIdentity: {
"Fn::Join": [
"",
[
"origin-access-identity/cloudfront/",
DomainName: {
"Fn::Select": [
2,
{
"Fn::Split": [
"/",
{
Ref: originAccessIdentityLogicalId,
"Fn::GetAtt": [bucketLogicalId, "WebsiteURL"],
},
],
],
},
},
],
},
Id: cfOriginId,
},
],
},
Expand Down Expand Up @@ -154,14 +152,32 @@ describe("static websites", () => {
},
},
});
expect(cfTemplate.Resources[responseFunction]).toMatchObject({
expect(cfTemplate.Resources[responseFunction]).toStrictEqual({
Type: "AWS::CloudFront::Function",
Properties: {
AutoPublish: true,
FunctionConfig: {
Comment: "app-dev-us-east-1-landing-response",
Runtime: "cloudfront-js-1.0",
},
FunctionCode: `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);
return response;
}`,
Name: "app-dev-us-east-1-landing-response",
},
});
Expand Down Expand Up @@ -269,9 +285,10 @@ describe("static websites", () => {
}),
});
const edgeFunction = computeLogicalId("landing", "ResponseFunction");
expect(cfTemplate.Resources[edgeFunction]).toMatchObject({
expect(cfTemplate.Resources[edgeFunction]).toStrictEqual({
Type: "AWS::CloudFront::Function",
Properties: {
AutoPublish: true,
// Check that the `x-frame-options` header is not set
FunctionCode: `function handler(event) {
var response = event.response;
Expand All @@ -288,6 +305,11 @@ describe("static websites", () => {
}, response.headers);
return response;
}`,
FunctionConfig: {
Comment: "app-dev-us-east-1-landing-response",
Runtime: "cloudfront-js-1.0",
},
Name: "app-dev-us-east-1-landing-response",
},
});
});
Expand Down Expand Up @@ -386,9 +408,14 @@ describe("static websites", () => {
});

const cfDistributionLogicalId = computeLogicalId("landing", "CDN");
expect(cfTemplate.Resources[cfDistributionLogicalId]).toMatchObject({
const bucketLogicalId = computeLogicalId("landing", "Bucket");
const responseFunction = computeLogicalId("landing", "ResponseFunction");
const cfOriginId = computeLogicalId("landing", "CDN", "Origin1");
expect(cfTemplate.Resources[cfDistributionLogicalId]).toStrictEqual({
Type: "AWS::CloudFront::Distribution",
Properties: {
DistributionConfig: {
Comment: "app-dev landing website CDN",
CustomErrorResponses: [
{
// The response code is forced to 404 and changed to /error.html
Expand All @@ -398,6 +425,47 @@ describe("static websites", () => {
ResponsePagePath: "/my/custom/error.html",
},
],
DefaultCacheBehavior: {
AllowedMethods: ["GET", "HEAD", "OPTIONS"],
CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6",
Compress: true,
TargetOriginId: cfOriginId,
ViewerProtocolPolicy: "redirect-to-https",
FunctionAssociations: [
{
EventType: "viewer-response",
FunctionARN: {
"Fn::GetAtt": [responseFunction, "FunctionARN"],
},
},
],
},
DefaultRootObject: "index.html",
Enabled: true,
HttpVersion: "http2",
IPV6Enabled: true,
Origins: [
{
CustomOriginConfig: {
OriginProtocolPolicy: "http-only",
OriginSSLProtocols: ["TLSv1.2"],
},
DomainName: {
"Fn::Select": [
2,
{
"Fn::Split": [
"/",
{
"Fn::GetAtt": [bucketLogicalId, "WebsiteURL"],
},
],
},
],
},
Id: cfOriginId,
},
],
},
},
});
Expand Down

0 comments on commit cb162a9

Please sign in to comment.