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: ["/*"],
}
);
}
}
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