1

I have deployed my website here:

https://curlycactus.com/

if you traverse the links, all the pages work fine. But when I copy and paste the direct link for example this:

https://curlycactus.com/work/1

I get this error:

<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>40EFGXY32PKPH10V</RequestId>
<HostId>NSWqXYQGVXuN39bP9DEyqkJ8tjIDDH2xpv08l/CUwcEVUKeoRcKNnwrDm0V/eENkLczmF8935OY=</HostId>
</Error>

any ideas why this happens? Here is my CDK setup:

import * as path from "path";
import { Aws, CfnOutput, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3Deployment from "aws-cdk-lib/aws-s3-deployment";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as targets from "aws-cdk-lib/aws-route53-targets";
import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
import * as iam from "aws-cdk-lib/aws-iam";
import * as route53 from "aws-cdk-lib/aws-route53";
import * as acm from "aws-cdk-lib/aws-certificatemanager";
import { S3Origin } from "aws-cdk-lib/aws-cloudfront-origins";
import { CONSTRUCT_NAMES } from "./ConstructNames";

export interface IStaticWebsiteConstruct extends StackProps {
  domainName: string;
}

export class StaticWebsiteConstruct extends Construct {
  websiteBucket: s3.Bucket;
  deploy: s3Deployment.BucketDeployment;
  cloudFront: cloudfront.CloudFrontWebDistribution;

  constructor(parent: Stack, id: string, props: IStaticWebsiteConstruct) {
    super(parent, id);
    // create bucket which holds the website data

    const zone = route53.HostedZone.fromLookup(this, "Zone", {
      domainName: props.domainName,
    });

    const siteDomain = props.domainName;
    const cloudfrontOAI = new cloudfront.OriginAccessIdentity(
      this,
      "cloudfront-OAI",
      {
        comment: `OAI for ${id}`,
      }
    );

    this.websiteBucket = new s3.Bucket(this, CONSTRUCT_NAMES.bucket.name, {
      bucketName: CONSTRUCT_NAMES.bucket.name,
      websiteIndexDocument: "index.html",
      publicReadAccess: true,
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    // Grant access to cloudfront
    this.websiteBucket.addToResourcePolicy(
      new iam.PolicyStatement({
        actions: ["s3:GetObject"],
        resources: [this.websiteBucket.arnForObjects("*")],
        principals: [
          new iam.CanonicalUserPrincipal(
            cloudfrontOAI.cloudFrontOriginAccessIdentityS3CanonicalUserId
          ),
        ],
      })
    );

    // TLS certificate
    const certificateArn = new acm.DnsValidatedCertificate(
      this,
      "SiteCertificate",
      {
        domainName: siteDomain,
        hostedZone: zone,
        region: "us-east-1", // Cloudfront only checks this region for certificates.
      }
    ).certificateArn;

    // Specifies you want viewers to use HTTPS & TLS v1.1 to request your objects
    const viewerCertificate = cloudfront.ViewerCertificate.fromAcmCertificate(
      {
        certificateArn: certificateArn,
        env: {
          region: Aws.REGION,
          account: Aws.ACCOUNT_ID,
        },
        node: this.node,
        stack: parent,
        metricDaysToExpiry: () =>
          new cloudwatch.Metric({
            namespace: "TLS Viewer Certificate Validity",
            metricName: "TLS Viewer Certificate Expired",
          }),
      },
      {
        sslMethod: cloudfront.SSLMethod.SNI,
        securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_1_2016,
        aliases: [siteDomain],
      }
    );

    // CloudFront distribution
    const distribution = new cloudfront.CloudFrontWebDistribution(
      this,
      "SiteDistribution",
      {
        viewerCertificate,
        originConfigs: [
          {
            s3OriginSource: {
              s3BucketSource: this.websiteBucket,
              originAccessIdentity: cloudfrontOAI,
            },
            behaviors: [
              {
                isDefaultBehavior: true,
                compress: true,
                allowedMethods:
                  cloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
              },
            ],
          },
        ],
      }
    );

    // Route53 alias record for the CloudFront distribution
    new route53.ARecord(this, "SiteAliasRecord", {
      recordName: siteDomain,
      target: route53.RecordTarget.fromAlias(
        new targets.CloudFrontTarget(distribution)
      ),
      zone,
    });

    // deploy/copy the website built website to s3 bucket
    this.deploy = new s3Deployment.BucketDeployment(
      this,
      CONSTRUCT_NAMES.bucket.deployment,
      {
        sources: [
          s3Deployment.Source.asset(
            path.join(__dirname, "..", "..", "frontend", "out")
          ),
        ],
        destinationBucket: this.websiteBucket,
        // distribution: this.cloudFront,
        distribution,
        distributionPaths: ["/*"],
      }
    );
  }
}
fedonev
  • 20,327
  • 2
  • 25
  • 34
ArmenB
  • 2,125
  • 3
  • 23
  • 47
  • 2
    Does it solve your problem? https://stackoverflow.com/a/52343542/9089876 – Cheng Dicky Jan 17 '22 at 08:21
  • 1
    If you have a look at the network calls in the browser when you navigate to the work1/1 page vs when you are requesting the url directly, you'll see they are requesting completely different things from your backend. So I suspect this is a routing problem in your front end rather than anything to do with the CDK. – matt helliwell Jan 17 '22 at 11:49
  • actually when I add .html to the end of each page it works. I am using nextjs to generate a static website which generates the files in the output folder – ArmenB Jan 17 '22 at 23:48

2 Answers2

1

Your website doesn't feel like 100% (client side) static website. By that I mean every HTML page is pre-generated and everything is static on client side. If that's the case then /work/1 should not load any html page as it's not a html resource. For it to be HTML resource it should be like /work/1.html

With that being said, it looks like you're using React or some other technology which translates the routing when previous page is known. / -> /work/1

As you have CloudFront already in your stack. Just set the error pages to redirect back to home page and then it should work fine. Attaching the solution for my react app hosted on S3+CloudFront.

enter image description here

Hussain Mansoor
  • 2,934
  • 2
  • 27
  • 40
  • actually when I add .html to the end of each page it works. I am using nextjs to generate a static website which generates the files in the output folder but the links don't add a .html. Is there a CDK config that I can say append a .html to all URLs? – ArmenB Jan 17 '22 at 23:52
0

found the issue. Http servers like Apache, nginx automatically look for .html postfix files for a URL request that doesn't have a postfix. We can do the same using cloudfront lambdas apparently, these lambdas are called lambda@edge.

https://jarredkenny.com/cloudfront-pretty-urls/

bear in mind, the lambda@edge functionality works in us-east-1 only so you have to deploy two stacks unless you move your whole stack to us-east-1

ArmenB
  • 2,125
  • 3
  • 23
  • 47