Skip to content

Commit

Permalink
feat: Possibility to use custom domain name with Cloudfront
Browse files Browse the repository at this point in the history
  • Loading branch information
JeromeGuyon authored and bigadsoleiman committed Mar 15, 2024
1 parent 97cc5de commit 9ccf06e
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 89 deletions.
28 changes: 24 additions & 4 deletions cli/magic-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,22 +223,42 @@ async function processCreateOptions(options: any): Promise<void> {
"Do you want to deploy a private website? I.e only accessible in VPC",
initial: options.privateWebsite || false,
},
{
type: "confirm",
name: "customPublicDomain",
message:
"Do you want to provide a custom domain name and corresponding certificate arn for the public website ?",
initial: options.customPublicDomain || false,
skip(): boolean {
return (this as any).state.answers.privateWebsite ;
},
},
{
type: "input",
name: "certificate",
message: "ACM certificate ARN",
message(): string {
if ((this as any).state.answers.customPublicDomain) {
return "ACM certificate ARN with custom domain for public website. Note that the certificate must resides in us-east-1";
}
return "ACM certificate ARN";
},
initial: options.certificate,
skip(): boolean {
return !(this as any).state.answers.privateWebsite;
return !(this as any).state.answers.privateWebsite && !(this as any).state.answers.customPublicDomain;
},
},
{
type: "input",
name: "domain",
message: "Domain for private website",
message(): string {
if ((this as any).state.answers.customPublicDomain) {
return "Custom Domain for public website";
}
return "Domain for private website";
},
initial: options.domain,
skip(): boolean {
return !(this as any).state.answers.privateWebsite;
return !(this as any).state.answers.privateWebsite && !(this as any).state.answers.customPublicDomain;
},
},
{
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default defineConfig({
{
text: 'Documentation',
items: [
{ text: 'Custom Public Domain', link: '/documentation/custom-public-domain' },
{ text: 'Private Chatbot', link: '/documentation/private-chatbot' },
{ text: 'Model Requirements', link: '/documentation/model-requirements' },
{ text: 'Self-hosted models', link: '/documentation/self-hosted-models' },
Expand Down
26 changes: 26 additions & 0 deletions docs/documentation/custom-public-domain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Custom public domain

Allows to specify a custom domain name via the 'npm run config' CLI setup.
- Utilises a AWS Public Certificate
- Utilises a Amazon Route 53 Public Hosted Zone and Domain


### Prerequisites:
1. AWS Certificate issued and validated (https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html) in us-east-1 for your chosen domain. (i.e. chatbot.example.org)
2. A Route 53 Public Hosted Zone (i.e. for example.org)

### During 'npm run config'
```shellsession
$ ✔ Do you want to deploy a private website? I.e only accessible in VPC (y/N) ·
false
$ ✔ Do you want to provide a custom domain name and corresponding certificate arn for the public website ? (y/N) ·
true
$ ✔ ACM certificate ARN with custom domain for public website. Note that the certificate must resides in us-east-1 ·
arn:aws:acm:us-east-1:1234567890:certificate/12345678-1234-1234-1234-12345678
$ ✔ Custom Domain for public website ·
chatbot.example.org

```

### After Deployment:
1. In your Route53 Hosted Zone, add an "A Record" that points to the Cloudfront Alias (https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-cloudfront-distribution.html)
171 changes: 86 additions & 85 deletions lib/user-interface/public-website.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
import * as apigwv2 from "@aws-cdk/aws-apigatewayv2-alpha";
import * as cognitoIdentityPool from "@aws-cdk/aws-cognito-identitypool-alpha";
import * as cdk from "aws-cdk-lib";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as cf from "aws-cdk-lib/aws-cloudfront";
import * as iam from "aws-cdk-lib/aws-iam";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3deploy from "aws-cdk-lib/aws-s3-deployment";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as route53 from "aws-cdk-lib/aws-route53";
import { IpTarget } from "aws-cdk-lib/aws-elasticloadbalancingv2-targets";
import * as acm from "aws-cdk-lib/aws-certificatemanager";
import { Construct } from "constructs";
import {
ExecSyncOptionsWithBufferEncoding,
execSync,
} from "node:child_process";
import * as path from "node:path";
import { Shared } from "../shared";
import { SystemConfig } from "../shared/types";
import { Utils } from "../shared/utils";
import { ChatBotApi } from "../chatbot-api";
import { NagSuppressions } from "cdk-nag";

Expand All @@ -41,82 +28,96 @@ export class PublicWebsite extends Construct {

constructor(scope: Construct, id: string, props: PublicWebsiteProps) {
super(scope, id);

/////////////////////////////////////
///// CLOUDFRONT IMPLEMENTATION /////
/////////////////////////////////////

const originAccessIdentity = new cf.OriginAccessIdentity(this, "S3OAI");
props.websiteBucket.grantRead(originAccessIdentity);
props.chatbotFilesBucket.grantRead(originAccessIdentity);
const distributionLogsBucket = new s3.Bucket(
this,
"DistributionLogsBucket",
{
objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
enforceSSL: true,
}


const distributionLogsBucket = new s3.Bucket(
this,
"DistributionLogsBucket",
{
objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
enforceSSL: true,
}
);

const distribution = new cf.CloudFrontWebDistribution(
this,
"Distribution",
{
viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
priceClass: cf.PriceClass.PRICE_CLASS_ALL,
httpVersion: cf.HttpVersion.HTTP2_AND_3,
loggingConfig: {
bucket: distributionLogsBucket,
},
originConfigs: [
{
behaviors: [{ isDefaultBehavior: true }],
s3OriginSource: {
s3BucketSource: props.websiteBucket,
originAccessIdentity,
},
},
{
behaviors: [
{
pathPattern: "/chabot/files/*",
allowedMethods: cf.CloudFrontAllowedMethods.ALL,
viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
defaultTtl: cdk.Duration.seconds(0),
forwardedValues: {
queryString: true,
headers: [
"Referer",
"Origin",
"Authorization",
"Content-Type",
"x-forwarded-user",
"Access-Control-Request-Headers",
"Access-Control-Request-Method",
],
},
},
],
s3OriginSource: {
s3BucketSource: props.chatbotFilesBucket,
originAccessIdentity,
},
},
],
errorConfigurations: [
{
errorCode: 404,
errorCachingMinTtl: 0,
responseCode: 200,
responsePagePath: "/index.html",
},
],
}
const distribution = new cf.CloudFrontWebDistribution(
this,
"Distribution",
{
// CUSTOM DOMAIN FOR PUBLIC WEBSITE
// REQUIRES:
// 1. ACM Certificate ARN in us-east-1 and Domain of website to be input during 'npm run config':
// "privateWebsite" : false,
// "certificate" : "arn:aws:acm:us-east-1:1234567890:certificate/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX",
// "domain" : "sub.example.com"
// 2. After the deployment, in your Route53 Hosted Zone, add an "A Record" that points to the Cloudfront Alias (https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-cloudfront-distribution.html)
...(props.config.certificate && props.config.domain && {
viewerCertificate: cf.ViewerCertificate.fromAcmCertificate(
acm.Certificate.fromCertificateArn(this,'CloudfrontAcm', props.config.certificate),
{
aliases: [props.config.domain]
})
}),
viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
priceClass: cf.PriceClass.PRICE_CLASS_ALL,
httpVersion: cf.HttpVersion.HTTP2_AND_3,
loggingConfig: {
bucket: distributionLogsBucket,
},
originConfigs: [
{
behaviors: [{ isDefaultBehavior: true }],
s3OriginSource: {
s3BucketSource: props.websiteBucket,
originAccessIdentity,
},
},
{
behaviors: [
{
pathPattern: "/chabot/files/*",
allowedMethods: cf.CloudFrontAllowedMethods.ALL,
viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
defaultTtl: cdk.Duration.seconds(0),
forwardedValues: {
queryString: true,
headers: [
"Referer",
"Origin",
"Authorization",
"Content-Type",
"x-forwarded-user",
"Access-Control-Request-Headers",
"Access-Control-Request-Method",
],
},
},
],
s3OriginSource: {
s3BucketSource: props.chatbotFilesBucket,
originAccessIdentity,
},
},
],
errorConfigurations: [
{
errorCode: 404,
errorCachingMinTtl: 0,
responseCode: 200,
responsePagePath: "/index.html",
},
],
}
);

this.distribution = distribution;
Expand All @@ -127,7 +128,7 @@ export class PublicWebsite extends Construct {
new cdk.CfnOutput(this, "UserInterfaceDomainName", {
value: `https://${distribution.distributionDomainName}`,
});

NagSuppressions.addResourceSuppressions(
distributionLogsBucket,
[
Expand All @@ -137,11 +138,11 @@ export class PublicWebsite extends Construct {
},
]
);

NagSuppressions.addResourceSuppressions(props.websiteBucket, [
{ id: "AwsSolutions-S5", reason: "OAI is configured for read." },
]);

NagSuppressions.addResourceSuppressions(distribution, [
{ id: "AwsSolutions-CFR1", reason: "No geo restrictions" },
{
Expand All @@ -151,5 +152,5 @@ export class PublicWebsite extends Construct {
{ id: "AwsSolutions-CFR4", reason: "TLS 1.2 is the default." },
]);
}

}

0 comments on commit 9ccf06e

Please sign in to comment.