import * as cdk from "aws-cdk-lib"; import * as acm from "aws-cdk-lib/aws-certificatemanager"; import * as cloudfront from "aws-cdk-lib/aws-cloudfront"; import * as origins from "aws-cdk-lib/aws-cloudfront-origins"; import * as route53 from "aws-cdk-lib/aws-route53"; import * as targets from "aws-cdk-lib/aws-route53-targets"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as s3deploy from "aws-cdk-lib/aws-s3-deployment"; import { Construct } from "constructs"; export interface WebStackProps extends cdk.StackProps { certificateArn?: string; } export class WebStack extends cdk.Stack { public readonly distributionUrl: string; public readonly bucketName: string; constructor(scope: Construct, id: string, props?: WebStackProps) { super(scope, id, props); const bucketName = `syncrow-web-${this.account}-${this.region}`; const webBucket = new s3.Bucket(this, "SyncrowWebBucket", { bucketName, websiteIndexDocument: "index.html", websiteErrorDocument: "index.html", publicReadAccess: true, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS, removalPolicy: cdk.RemovalPolicy.DESTROY, autoDeleteObjects: true, }); // Use existing wildcard certificate in us-east-1 (required for CloudFront) const webCertificate = props?.certificateArn ? acm.Certificate.fromCertificateArn( this, "WildcardCertificate", props.certificateArn ) : acm.Certificate.fromCertificateArn( this, "WildcardCertificate", "arn:aws:acm:us-east-1:482311766496:certificate/b3ea57be-9bf0-4c66-8b01-9672ef1e8530" ); // Get the hosted zone const hostedZone = route53.HostedZone.fromLookup(this, "SyncrowZone", { domainName: "syncrow.me", }); const distribution = new cloudfront.Distribution( this, "SyncrowWebDistribution", { defaultBehavior: { origin: new origins.S3Origin(webBucket), viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, }, domainNames: ["app.syncrow.me"], certificate: webCertificate, defaultRootObject: "index.html", errorResponses: [ { httpStatus: 404, responseHttpStatus: 200, responsePagePath: "/index.html", }, { httpStatus: 403, responseHttpStatus: 200, responsePagePath: "/index.html", }, ], } ); // Create Route 53 record for app.syncrow.me new route53.ARecord(this, "WebAliasRecord", { zone: hostedZone, recordName: "app", target: route53.RecordTarget.fromAlias( new targets.CloudFrontTarget(distribution) ), }); new s3deploy.BucketDeployment(this, "SyncrowWebDeployment", { sources: [s3deploy.Source.asset("./build/web")], destinationBucket: webBucket, distribution, distributionPaths: ["/*"], }); this.distributionUrl = "https://app.syncrow.me"; this.bucketName = bucketName; new cdk.CfnOutput(this, "WebsiteUrl", { value: this.distributionUrl, description: "Web Application URL", exportName: `${this.stackName}-WebsiteUrl`, }); new cdk.CfnOutput(this, "CloudFrontUrl", { value: `https://${distribution.distributionDomainName}`, description: "CloudFront Distribution URL", exportName: `${this.stackName}-CloudFrontUrl`, }); new cdk.CfnOutput(this, "BucketName", { value: this.bucketName, description: "S3 Bucket Name", exportName: `${this.stackName}-BucketName`, }); new cdk.CfnOutput(this, "WildcardCertificateArn", { value: webCertificate.certificateArn, description: "Wildcard SSL Certificate ARN (us-east-1)", exportName: `${this.stackName}-WildcardCertificateArn`, }); } }