< Summary - Envilder IaC (CDK)

Information
Class: src/iac/lib/stacks/staticWebsiteStack.ts
Assembly: Default
File(s): src/iac/lib/stacks/staticWebsiteStack.ts
Tag: 151_24479375065
Line coverage
94%
Covered lines: 33
Uncovered lines: 2
Coverable lines: 35
Total lines: 235
Line coverage: 94.2%
Branch coverage
65%
Covered branches: 17
Total branches: 26
Branch coverage: 65.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

src/iac/lib/stacks/staticWebsiteStack.ts

#LineLine coverage
 1import { join } from "node:path";
 2import { CfnOutput, Duration, RemovalPolicy } from "aws-cdk-lib";
 3import {
 4  Certificate,
 5  type ICertificate,
 6} from "aws-cdk-lib/aws-certificatemanager";
 7import {
 8  Distribution,
 9  type ErrorResponse,
 10  FunctionCode,
 11  FunctionEventType,
 12  Function as LambdaFunction,
 13  OriginAccessIdentity,
 14  ViewerProtocolPolicy,
 15} from "aws-cdk-lib/aws-cloudfront";
 16import { S3BucketOrigin } from "aws-cdk-lib/aws-cloudfront-origins";
 17import { ARecord, HostedZone, RecordTarget } from "aws-cdk-lib/aws-route53";
 18import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets";
 19import {
 20  BlockPublicAccess,
 21  Bucket,
 22  BucketAccessControl,
 23  BucketEncryption,
 24  HttpMethods,
 25} from "aws-cdk-lib/aws-s3";
 26import { BucketDeployment, Source } from "aws-cdk-lib/aws-s3-deployment";
 27import type { Construct } from "constructs";
 28import {
 29  CustomStack,
 30  type CustomStackProps,
 31  type DomainConfig,
 32} from "./customStack";
 33
 34export interface StaticWebsiteStackProps extends CustomStackProps {
 35  domains: DomainConfig[];
 36  distFolderPath: string;
 37}
 38
 39export class StaticWebsiteStack extends CustomStack {
 40  constructor(scope: Construct, props: StaticWebsiteStackProps) {
 141    super(scope, props);
 42
 143    if (!props.domains || props.domains.length === 0) {
 044      throw new Error("At least one domain configuration is required");
 45    }
 46
 147    const primaryDomain = props.domains[0];
 48    const primaryFullDomainName =
 149      primaryDomain.subdomain && primaryDomain.subdomain.length > 0
 50        ? [primaryDomain.subdomain, primaryDomain.domainName]
 51            .join(".")
 52            .toLowerCase()
 53        : primaryDomain.domainName.toLowerCase();
 54
 155    const allDomainNames = props.domains.map((domain) =>
 156      domain.subdomain && domain.subdomain.length > 0
 57        ? `${domain.subdomain}.${domain.domainName}`.toLowerCase()
 58        : domain.domainName.toLowerCase(),
 59    );
 60
 161    const certificateMap = new Map<string, ICertificate>();
 162    for (const domain of props.domains) {
 163      if (!certificateMap.has(domain.certificateId)) {
 164        const certificateArn = `arn:aws:acm:us-east-1:${props.env?.account}:certificate/${domain.certificateId}`;
 165        certificateMap.set(
 66          domain.certificateId,
 67          Certificate.fromCertificateArn(
 68            this,
 69            `certificate-${domain.certificateId}`,
 70            certificateArn,
 71          ),
 72        );
 73      }
 74    }
 75
 176    const primaryCertificate = certificateMap.get(primaryDomain.certificateId);
 177    if (!primaryCertificate) {
 078      throw new Error(
 79        `Certificate not found for ${primaryDomain.certificateId}`,
 80      );
 81    }
 82
 183    const loggingBucket = new Bucket(this, "logging-bucket", {
 84      accessControl: BucketAccessControl.LOG_DELIVERY_WRITE,
 85      publicReadAccess: false,
 86      versioned: false,
 87      removalPolicy: RemovalPolicy.DESTROY,
 88      bucketName: `${primaryFullDomainName}-logs`,
 89      autoDeleteObjects: true,
 90      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
 91      encryption: BucketEncryption.S3_MANAGED,
 92      enforceSSL: true,
 93      lifecycleRules: [
 94        {
 95          id: "DeleteOldLogs",
 96          expiration: Duration.days(90),
 97          enabled: true,
 98        },
 99      ],
 100    });
 101
 1102    const bucketWebsite = new Bucket(this, "static-website-bucket", {
 103      accessControl: BucketAccessControl.PRIVATE,
 104      publicReadAccess: false,
 105      versioned: false,
 106      removalPolicy: RemovalPolicy.DESTROY,
 107      bucketName: primaryFullDomainName,
 108      autoDeleteObjects: true,
 109      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
 110      encryption: BucketEncryption.S3_MANAGED,
 111      cors: [
 112        {
 113          allowedMethods: [HttpMethods.GET, HttpMethods.HEAD],
 114          allowedOrigins: ["*"],
 115          allowedHeaders: ["*"],
 116        },
 117      ],
 118      enforceSSL: true,
 119      serverAccessLogsBucket: loggingBucket,
 120      serverAccessLogsPrefix: "s3-access-logs/",
 121    });
 122
 1123    const originAccessIdentity = new OriginAccessIdentity(
 124      this,
 125      "originAccessIdentity",
 126      {
 127        comment: `Setup access from CloudFront to the bucket ${primaryFullDomainName} (read)`,
 128      },
 129    );
 130
 1131    bucketWebsite.grantRead(originAccessIdentity);
 132
 1133    const errorResponses: ErrorResponse[] = [];
 134
 1135    const errorResponse403: ErrorResponse = {
 136      httpStatus: 403,
 137      responseHttpStatus: 200,
 138      responsePagePath: "/index.html",
 139      ttl: Duration.seconds(10),
 140    };
 141
 1142    const errorResponse404: ErrorResponse = {
 143      httpStatus: 404,
 144      responseHttpStatus: 200,
 145      responsePagePath: "/index.html",
 146      ttl: Duration.seconds(10),
 147    };
 148
 1149    errorResponses.push(errorResponse403, errorResponse404);
 150
 1151    const distribution = new Distribution(this, "distribution", {
 152      domainNames: allDomainNames,
 153      defaultBehavior: {
 154        origin: S3BucketOrigin.withOriginAccessIdentity(bucketWebsite, {
 155          originAccessIdentity: originAccessIdentity,
 156        }),
 157        viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
 158        functionAssociations: [
 159          {
 160            eventType: FunctionEventType.VIEWER_REQUEST,
 161            function: new LambdaFunction(
 162              this,
 163              `${primaryFullDomainName}-url-rewrite`.toLowerCase(),
 164              {
 165                code: FunctionCode.fromFile({
 166                  filePath: join(__dirname, "cloudfront-url-rewrite.js"),
 167                }),
 168              },
 169            ),
 170          },
 171        ],
 172      },
 173      defaultRootObject: "index.html",
 174      certificate: primaryCertificate,
 175      errorResponses: errorResponses,
 176      enableLogging: true,
 177      logBucket: loggingBucket,
 178      logFilePrefix: "cloudfront-logs/",
 179    });
 180
 1181    new BucketDeployment(this, "deploy-static-website", {
 182      sources: [Source.asset(props.distFolderPath)],
 183      destinationBucket: bucketWebsite,
 184      distribution,
 185      distributionPaths: ["/*"],
 186    });
 187
 1188    const aliasRecords: ARecord[] = [];
 1189    for (const [index, domainConfig] of props.domains.entries()) {
 190      const fullDomainName =
 1191        domainConfig.subdomain && domainConfig.subdomain.length > 0
 192          ? `${domainConfig.subdomain}.${domainConfig.domainName}`.toLowerCase()
 193          : domainConfig.domainName.toLowerCase();
 194
 195      const zoneLogicalId =
 1196        index === 0
 197          ? "publicHostedZone-0"
 198          : `hostedZone-${fullDomainName.replace(/[.-]/g, "")}`;
 199
 1200      const zoneFromAttributes = HostedZone.fromHostedZoneAttributes(
 201        this,
 202        zoneLogicalId,
 203        {
 204          zoneName: domainConfig.domainName,
 205          hostedZoneId: domainConfig.hostedZoneId,
 206        },
 207      );
 208
 209      const recordLogicalId =
 1210        index === 0
 211          ? "webDomainRecord-0"
 212          : `webDomainRecord-${fullDomainName.replace(/[.-]/g, "")}`;
 213
 1214      const aliasRecord = new ARecord(this, recordLogicalId, {
 215        zone: zoneFromAttributes,
 216        recordName: fullDomainName,
 217        target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)),
 218      });
 219
 1220      aliasRecords.push(aliasRecord);
 221    }
 222
 1223    new CfnOutput(this, "CloudFrontDistributionDomainName", {
 224      value: distribution.distributionDomainName,
 225      description: "CloudFront distribution domain",
 226      exportName: `${this.getCloudFormationRepoName()}-${props.envName}-CdnDomainName`,
 227    });
 228
 1229    new CfnOutput(this, "DnsRecordName", {
 230      value: aliasRecords[0].domainName || allDomainNames[0],
 231      description: "The DNS record name (primary)",
 232      exportName: `${this.getCloudFormationRepoName()}-${props.envName}-AliasRecord`,
 233    });
 234  }
 235}

Methods/Properties

(anonymous_0)
(anonymous_1)