Skip to content

Commit 68b318a

Browse files
committed
feat: adds shared-static-website-hosting
1 parent e4eb457 commit 68b318a

File tree

6 files changed

+183
-19
lines changed

6 files changed

+183
-19
lines changed

src/index.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1 @@
1-
import { Construct } from 'constructs';
2-
3-
export class StaticWebsitePreview extends Construct {
4-
constructor(scope: Construct, id: string) {
5-
super(scope, id);
6-
}
7-
}
1+
export * from './shared-static-website-hosting';

src/request-handler/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './request-handler';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { CloudFrontRequestHandler } from 'aws-lambda';
2+
3+
export const handler: CloudFrontRequestHandler = async (event) => {
4+
const request = event.Records[0].cf.request;
5+
6+
if (request.uri.endsWith('/')) {
7+
request.uri += 'index.html';
8+
}
9+
10+
for (const [headerName, [header]] of Object.entries(request.headers)) {
11+
if (headerName === 'host') {
12+
const [subDomain] = header.value.split('.');
13+
request.uri = ['/', subDomain, request.uri].join('');
14+
}
15+
}
16+
17+
return request;
18+
};

src/shared-static-website-hosting.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { RemovalPolicy } from 'aws-cdk-lib';
2+
import { CertificateValidation, DnsValidatedCertificate } from 'aws-cdk-lib/aws-certificatemanager';
3+
import { Distribution, OriginAccessIdentity, IDistribution, LambdaEdgeEventType, CachePolicy } from 'aws-cdk-lib/aws-cloudfront';
4+
import { S3Origin } from 'aws-cdk-lib/aws-cloudfront-origins';
5+
import { Runtime } from 'aws-cdk-lib/aws-lambda';
6+
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
7+
import { ARecord, IHostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53';
8+
import { CloudFrontTarget } from 'aws-cdk-lib/aws-route53-targets';
9+
import { BlockPublicAccess, Bucket, BucketAccessControl, BucketEncryption, IBucket, ObjectOwnership } from 'aws-cdk-lib/aws-s3';
10+
import { Construct } from 'constructs';
11+
12+
const WILDCARD_RECORD_NAME = '*';
13+
14+
interface SharedStaticWebsiteHostingProps {
15+
/**
16+
* A hosted zone that is open for modification by the construct. This construct will add a wildcard
17+
* A record that points to the CloudFront distribution.
18+
*/
19+
hostedZone: IHostedZone;
20+
21+
/**
22+
* An S3 bucket where the static files will be stored.
23+
*
24+
* This construct assumes full control of the bucket.
25+
*
26+
* @default
27+
*
28+
* A new bucket will be created, if not provided.
29+
*/
30+
bucket?: IBucket;
31+
}
32+
33+
/**
34+
* Host static websites on a shared CloudFront distribution.
35+
*/
36+
export class SharedStaticWebsiteHosting extends Construct {
37+
/**
38+
* Bucket where the static files are stored.
39+
*/
40+
readonly bucket: IBucket;
41+
42+
/**
43+
* CloudFront distribution.
44+
*/
45+
readonly distribution: IDistribution;
46+
47+
readonly #hostedZone: IHostedZone;
48+
49+
constructor(scope: Construct, id: string, props: SharedStaticWebsiteHostingProps) {
50+
super(scope, id);
51+
52+
this.#hostedZone = props.hostedZone;
53+
const domainName = [WILDCARD_RECORD_NAME, this.#hostedZone.zoneName].join('.');
54+
55+
this.bucket = props.bucket ?? new Bucket(this, 'Bucket', {
56+
removalPolicy: RemovalPolicy.DESTROY,
57+
autoDeleteObjects: true,
58+
59+
publicReadAccess: false,
60+
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
61+
accessControl: BucketAccessControl.PRIVATE,
62+
objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED,
63+
encryption: BucketEncryption.S3_MANAGED,
64+
});
65+
66+
const originAccessIdentity = new OriginAccessIdentity(this, 'OriginAccessIdentity', {
67+
comment: `CloudFront OriginAccessIdentity for ${this.bucket.bucketName}`,
68+
});
69+
70+
// https://github.com/aws/aws-cdk/issues/13983
71+
this.bucket.grantRead(originAccessIdentity);
72+
73+
const certificate = new DnsValidatedCertificate(this, 'Certificate', {
74+
hostedZone: this.#hostedZone,
75+
domainName,
76+
validation: CertificateValidation.fromDns(),
77+
cleanupRoute53Records: true,
78+
79+
// CloudFront distributions requires the region to be us-east-1.
80+
region: 'us-east-1',
81+
});
82+
83+
const originRequestHandler = new NodejsFunction(this, 'OriginRequestHandler', {
84+
entry: require.resolve('./request-handler'),
85+
runtime: Runtime.NODEJS_16_X,
86+
});
87+
88+
this.distribution = new Distribution(this, 'Distribution', {
89+
defaultBehavior: {
90+
origin: new S3Origin(this.bucket, { originAccessIdentity }),
91+
edgeLambdas: [
92+
{
93+
eventType: LambdaEdgeEventType.VIEWER_REQUEST,
94+
functionVersion: originRequestHandler.currentVersion,
95+
},
96+
],
97+
},
98+
domainNames: [domainName],
99+
certificate,
100+
defaultRootObject: 'index.html',
101+
});
102+
103+
new ARecord(this, 'AWildcardRecord', {
104+
zone: this.#hostedZone,
105+
recordName: WILDCARD_RECORD_NAME,
106+
target: RecordTarget.fromAlias(new CloudFrontTarget(this.distribution)),
107+
comment: `A sub-domain for the ${SharedStaticWebsiteHosting.name} construct.`,
108+
});
109+
}
110+
}

test/integ.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { IntegTest, IntegTestCaseStack } from '@aws-cdk/integ-tests-alpha';
2+
import { App, CfnOutput } from 'aws-cdk-lib';
3+
import { HostedZone } from 'aws-cdk-lib/aws-route53';
4+
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';
5+
import { SharedStaticWebsiteHosting } from '../src';
6+
import { AWS_ACCOUNT_ID, ROOT_ZONE_NAME } from './env';
7+
8+
const app = new App({
9+
analyticsReporting: false,
10+
autoSynth: true,
11+
treeMetadata: false,
12+
});
13+
14+
const testStack = new IntegTestCaseStack(app, 'ITS', {
15+
// needed for HostedZone.fromLookup
16+
env: {
17+
account: AWS_ACCOUNT_ID,
18+
region: 'us-east-1',
19+
},
20+
});
21+
22+
const rootZone = HostedZone.fromLookup(testStack, 'RootZone', {
23+
domainName: ROOT_ZONE_NAME,
24+
});
25+
26+
const swp = new SharedStaticWebsiteHosting(testStack, 'SharedStaticWebsiteHosting', {
27+
hostedZone: rootZone,
28+
});
29+
30+
const testSubDomain = ['test', Date.now()].join('-');
31+
32+
new BucketDeployment(testStack, 'BucketDeployment', {
33+
destinationBucket: swp.bucket,
34+
distribution: swp.distribution,
35+
36+
// Adds test data
37+
prune: false,
38+
sources: [
39+
Source.data(`${testSubDomain}/index.html`, 'index'),
40+
Source.data(`${testSubDomain}/test.html`, 'test'),
41+
],
42+
});
43+
44+
new CfnOutput(swp, 'BucketName', { value: swp.bucket.bucketName });
45+
new CfnOutput(swp, 'DistributionDomainName', { value: swp.distribution.distributionDomainName });
46+
new CfnOutput(swp, 'TestCloudFrontURL', { value: `https://${swp.distribution.distributionDomainName}/${testSubDomain}/` });
47+
new CfnOutput(swp, 'TestURL', { value: `https://${testSubDomain}.${rootZone.zoneName}/` });
48+
49+
new IntegTest(app, 'IT1', {
50+
testCases: [
51+
testStack,
52+
],
53+
});

0 commit comments

Comments
 (0)