constructor()

in packages/secure-static-site/lib/static-site.ts [125:304]


  constructor(scope: Construct, id: string, props: StaticSiteProps) {
    super(scope, id);
    const {
      path = "./",
      distFolder = "dist",
      envVars,
      buildCommand = "npm run build",
      allowedIPs,
      responseHeaders,
      enableWaf,
      disableCoreWafRuleGroup,
      disableAmazonIPWafRuleGroup,
      disableAnonymousIPWafRuleGroup,
      domainNameBase,
      domainNamePrefix,
    } = props;
    const enableWafMetrics = !!props.enableWafMetrics;

    // S3
    const serverAccessLogsBucket = new Bucket(this, "ServerAccessLogsBucket", {
      autoDeleteObjects: true,
      removalPolicy: RemovalPolicy.DESTROY,
      enforceSSL: true,
      encryption: BucketEncryption.S3_MANAGED,
      lifecycleRules: [
        { 
          transitions: [
            {
              storageClass: StorageClass.INTELLIGENT_TIERING,
              transitionAfter: Duration.days(0),
            }
          ]
        }
      ]
    });
    this.bucket = new Bucket(this, "StaticSiteBucket", {
      autoDeleteObjects: true,
      removalPolicy: RemovalPolicy.DESTROY,
      enforceSSL: true,
      encryption: BucketEncryption.S3_MANAGED,
      serverAccessLogsBucket,
      serverAccessLogsPrefix: "s3"
    });

    // WAF
    let webACL: CfnWebACL | undefined = undefined;
    if (enableWaf) {
      // allow requests that are not blocked by other rules by default
      let defaultAction: CfnWebACL.RuleActionProperty = { allow: {} };

      let ipRuleSet: CfnIPSet | undefined = undefined;
      if (allowedIPs) {
        defaultAction = { block: {} };
        ipRuleSet = new CfnIPSet(this, "IPRuleSet", {
          addresses: allowedIPs,
          ipAddressVersion: "IPV4",
          scope: "CLOUDFRONT",
        });
      }

      // create WAF rules using relevant props
      const rules: CfnWebACL.RuleProperty[] = createWafRules({
        enableWafMetrics,
        disableAmazonIPWafRuleGroup,
        disableAnonymousIPWafRuleGroup,
        disableCoreWafRuleGroup,
        allowedIPs,
        ipRuleSet,
      });

      // For CLOUDFRONT, you must create your WAFv2 resources in the US East (N. Virginia) Region, us-east-1.
      webACL = new CfnWebACL(this, "WebACL", {
        defaultAction,
        rules,
        scope: "CLOUDFRONT",
        visibilityConfig: {
          cloudWatchMetricsEnabled: enableWafMetrics,
          metricName: "DefaultMetric",
          sampledRequestsEnabled: enableWafMetrics,
        },
      });
    }

    // CloudFront
    let distributionProps: DistributionProps = {
      defaultBehavior: {
        origin: new S3Origin(this.bucket),
        viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        functionAssociations: [
          {
            function: new Function(this, "SecurityHeadersFn", {
              code: FunctionCode.fromInline(getFunctionCode(responseHeaders)),
              // explicit function name needed b/c https://github.com/aws/aws-cdk/issues/15523
              functionName: `SecureStaticSite${this.node.addr}`,
            }),
            eventType: FunctionEventType.VIEWER_RESPONSE,
          },
        ],
      },
      defaultRootObject: "index.html",
      errorResponses: [
        {
          httpStatus: 404,
          responseHttpStatus: 200,
          responsePagePath: "/index.html",
        },
      ],
      enableLogging: true,
      logBucket: serverAccessLogsBucket,
      logFilePrefix: "cloudfront",
      minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021,
    };

    // add WAF web ACL to distribution if present
    if (webACL) {
      distributionProps = { ...distributionProps, webAclId: webACL.attrArn };
    }

    // configure Route 53 only if domain name base and prefix are set
    if (domainNameBase && domainNamePrefix) {
      this.zone = HostedZone.fromLookup(this, "StaticSiteHostedZone", {
        domainName: domainNameBase,
      });
      // prefix allows multiple apps to use same base
      this.fullDomainName = `${domainNamePrefix}.${domainNameBase}`;
      // can only create certificate in CDK if using Route 53 for DNS
      const certificate = new Certificate(this, "StaticSiteCertificate", {
        domainName: this.fullDomainName,
        validation: CertificateValidation.fromDns(this.zone),
        // allow subdomains (e.g. www, test, stage, etc)
        subjectAlternativeNames: [`*.${this.fullDomainName}`],
      });
      // update CloudFront distribution with domain names and certificate
      distributionProps = {
        ...distributionProps,
        domainNames: [this.fullDomainName, `www.${this.fullDomainName}`],
        certificate,
      };
    }

    this.distribution = new Distribution(
      this,
      "StaticSiteDistribution",
      distributionProps
    );

    // hosted zone and full domain name must exist to create Route 53 records
    if (this.zone && this.fullDomainName) {
      // IPV4
      new ARecord(this, "StaticSiteARecord", {
        zone: this.zone,
        recordName: this.fullDomainName,
        target: RecordTarget.fromAlias(new CloudFrontTarget(this.distribution)),
      });
      new ARecord(this, "StaticSiteSubsiteARecord", {
        zone: this.zone,
        recordName: `*.${this.fullDomainName}`,
        target: RecordTarget.fromAlias(new CloudFrontTarget(this.distribution)),
      });
      // IPV6
      new AaaaRecord(this, "StaticSiteAaaaRecord", {
        zone: this.zone,
        recordName: this.fullDomainName,
        target: RecordTarget.fromAlias(new CloudFrontTarget(this.distribution)),
      });
      new AaaaRecord(this, "StaticSiteSubsiteAaaaRecord", {
        zone: this.zone,
        recordName: `*.${this.fullDomainName}`,
        target: RecordTarget.fromAlias(new CloudFrontTarget(this.distribution)),
      });
    }

    // TODO: create CloudFront Function to specify Content-Security-Policy
    buildStaticSite({ path, buildCommand, envVars });
    new BucketDeployment(this, "BucketDeployment", {
      destinationBucket: this.bucket,
      sources: [Source.asset(resolve(path, distFolder))],
      distribution: this.distribution,
    });
  }