Custom Domains & SSL Certificates
Overview
Section titled “Overview”Astro AWS supports custom domains with SSL certificates for your CloudFront distributions. This guide walks you through setting up a custom domain with SSL certificate provisioning and DNS record configuration using AWS Route53 and AWS Certificate Manager (ACM).
When you configure a custom domain, you’ll:
- Create an SSL certificate in ACM in the
us-east-1region (required for CloudFront) - Configure your CloudFront distribution to use the custom domain and certificate
- Set up Route53 DNS records (A and AAAA) to point your domain to CloudFront
Prerequisites
Section titled “Prerequisites”Before setting up a custom domain, ensure you have:
- A registered domain name - Your domain must be registered with a domain registrar
- Route53 hosted zone - A hosted zone must exist in Route53 for your domain
- Domain ownership - You must have access to manage DNS records for your domain
Creating a Route53 Hosted Zone
Section titled “Creating a Route53 Hosted Zone”If you don’t already have a hosted zone for your domain:
- Go to the AWS Route53 console
- Click “Hosted zones” → “Create hosted zone”
- Enter your domain name (e.g.,
example.com) - Choose “Public hosted zone” (for internet-facing domains)
- Click “Create hosted zone”
Note: If your domain is registered with a different registrar, you’ll need to update your domain’s nameservers to point to the Route53 nameservers shown in the hosted zone.
Understanding Cross-Region Certificates
Section titled “Understanding Cross-Region Certificates”CloudFront requires SSL certificates to be in the us-east-1 region, regardless of where your CDK stack is deployed. This is an AWS requirement for CloudFront distributions.
When creating certificates for CloudFront, you have two options:
- Create the certificate in a separate stack in
us-east-1- Recommended for most cases - Use DNS-validated certificates - Automatically handles DNS validation records
This guide shows you how to create certificates in the correct region and configure them with your CloudFront distribution.
Basic Custom Domain Setup
Section titled “Basic Custom Domain Setup”Here’s a minimal example of setting up a custom domain. Since CloudFront requires certificates in us-east-1, we’ll create the certificate in a separate stack:
import { Stack } from "aws-cdk-lib"import type { StackProps } from "aws-cdk-lib"import { Construct } from "constructs"import { Certificate, CertificateValidation,} from "aws-cdk-lib/aws-certificatemanager"import { HostedZone } from "aws-cdk-lib/aws-route53"
export class CertificateStack extends Stack { public readonly certificate: Certificate
public constructor(scope: Construct, id: string, props: StackProps) { super(scope, id, { ...props, env: { ...props.env, region: "us-east-1" } })
const hostedZone = HostedZone.fromLookup(this, "HostedZone", { domainName: "example.com", })
this.certificate = new Certificate(this, "Certificate", { domainName: "example.com", subjectAlternativeNames: ["www.example.com"], validation: CertificateValidation.fromDns(hostedZone), }) }}
// lib/astro-site-stack.tsimport { Stack } from "aws-cdk-lib"import type { StackProps } from "aws-cdk-lib"import { Construct } from "constructs"import { AstroAWS } from "@astro-aws/constructs"import { CertificateStack } from "./certificate-stack"
export class AstroSiteStack extends Stack { public constructor(scope: Construct, id: string, props: StackProps) { super(scope, id, props)
// Create certificate stack in us-east-1 const certificateStack = new CertificateStack(this, "CertificateStack", { env: { region: "us-east-1" }, })
const domainNames = ["example.com", "www.example.com"]
new AstroAWS(this, "AstroAWS", { websiteDir: "../my-astro-project", cdk: { cloudfrontDistribution: { certificate: certificateStack.certificate, domainNames, }, }, }) }}Setting Up DNS Records
Section titled “Setting Up DNS Records”After configuring the certificate and CloudFront distribution, you need to set up DNS records to point your domain to CloudFront. You can automatically create these records using CDK:
import { Stack } from "aws-cdk-lib"import type { StackProps } from "aws-cdk-lib"import { Construct } from "constructs"import { Certificate, CertificateValidation,} from "aws-cdk-lib/aws-certificatemanager"import { HostedZone } from "aws-cdk-lib/aws-route53"
export class CertificateStack extends Stack { public readonly certificate: Certificate
public constructor(scope: Construct, id: string, props: StackProps) { super(scope, id, { ...props, env: { ...props.env, region: "us-east-1" } })
const hostedZone = HostedZone.fromLookup(this, "HostedZone", { domainName: "example.com", })
this.certificate = new Certificate(this, "Certificate", { domainName: "example.com", subjectAlternativeNames: ["www.example.com"], validation: CertificateValidation.fromDns(hostedZone), }) }}
// lib/astro-site-stack.tsimport { Stack } from "aws-cdk-lib"import type { StackProps } from "aws-cdk-lib"import { Construct } from "constructs"import { AstroAWS } from "@astro-aws/constructs"import { HostedZone, ARecord, AaaaRecord, RecordTarget,} from "aws-cdk-lib/aws-route53"import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets"import { CertificateStack } from "./certificate-stack"
export class AstroSiteStack extends Stack { public constructor(scope: Construct, id: string, props: StackProps) { super(scope, id, props)
// Look up your Route53 hosted zone const hostedZone = HostedZone.fromLookup(this, "HostedZone", { domainName: "example.com", })
// Create certificate stack in us-east-1 const certificateStack = new CertificateStack(this, "CertificateStack", { env: { region: "us-east-1" }, })
const domainNames = ["example.com", "www.example.com"]
const astroAws = new AstroAWS(this, "AstroAWS", { websiteDir: "../my-astro-project", cdk: { cloudfrontDistribution: { certificate: certificateStack.certificate, domainNames, }, }, })
// Create DNS records for each domain domainNames.forEach((domainName) => { // A record for IPv4 new ARecord(this, `ARecord-${domainName}`, { recordName: domainName, target: RecordTarget.fromAlias( new CloudFrontTarget(astroAws.cdk.cloudfrontDistribution), ), zone: hostedZone, })
// AAAA record for IPv6 new AaaaRecord(this, `AaaaRecord-${domainName}`, { recordName: domainName, target: RecordTarget.fromAlias( new CloudFrontTarget(astroAws.cdk.cloudfrontDistribution), ), zone: hostedZone, }) }) }}Certificate Validation
Section titled “Certificate Validation”ACM certificates require DNS validation to prove domain ownership. When using CertificateValidation.fromDns(hostedZone), CDK automatically creates the DNS validation records in your Route53 hosted zone.
The certificate validation process:
- CDK requests the certificate with DNS validation
- CDK automatically creates CNAME records in your Route53 hosted zone
- AWS validates the certificate (typically within a few minutes)
- The certificate becomes available for use
Note: If you’re not using CertificateValidation.fromDns(), you’ll need to manually add validation records:
- Go to AWS Certificate Manager console (in
us-east-1region) - Select your certificate
- Click “Create record in Route53” for each validation record, or manually copy the CNAME records
- Add these records to your Route53 hosted zone
Multiple Domains and Subdomains
Section titled “Multiple Domains and Subdomains”You can configure multiple domains or subdomains using the subjectAlternativeNames parameter:
// In CertificateStack constructorthis.certificate = new Certificate(this, "Certificate", { domainName: "example.com", subjectAlternativeNames: [ "www.example.com", "api.example.com", "blog.example.com", ], validation: CertificateValidation.fromDns(hostedZone),})
// In AstroSiteStack constructorconst domainNames = [ "example.com", "www.example.com", "api.example.com", "blog.example.com",]All domains will be included in a single certificate (SAN certificate), and DNS records will be created for each domain.
Complete Example
Section titled “Complete Example”Here’s a complete example that sets up a custom domain with SSL certificate and DNS records:
import { Stack } from "aws-cdk-lib"import type { StackProps } from "aws-cdk-lib"import { Construct } from "constructs"import { Certificate, CertificateValidation,} from "aws-cdk-lib/aws-certificatemanager"import { HostedZone } from "aws-cdk-lib/aws-route53"
export class CertificateStack extends Stack { public readonly certificate: Certificate
public constructor(scope: Construct, id: string, props: StackProps) { super(scope, id, { ...props, env: { ...props.env, region: "us-east-1" } })
const hostedZone = HostedZone.fromLookup(this, "HostedZone", { domainName: "example.com", })
this.certificate = new Certificate(this, "Certificate", { domainName: "example.com", subjectAlternativeNames: ["www.example.com"], validation: CertificateValidation.fromDns(hostedZone), }) }}
// lib/astro-site-stack.tsimport { Stack } from "aws-cdk-lib"import type { StackProps } from "aws-cdk-lib"import { Construct } from "constructs"import { AstroAWS } from "@astro-aws/constructs"import { HostedZone, ARecord, AaaaRecord, RecordTarget,} from "aws-cdk-lib/aws-route53"import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets"import { ViewerProtocolPolicy } from "aws-cdk-lib/aws-cloudfront"import { CertificateStack } from "./certificate-stack"
export class AstroSiteStack extends Stack { public constructor(scope: Construct, id: string, props: StackProps) { super(scope, id, props)
const hostedZoneName = "example.com" const aliases = ["", "www"] // Empty string for root domain
// Look up Route53 hosted zone const hostedZone = HostedZone.fromLookup(this, "HostedZone", { domainName: hostedZoneName, })
// Build full domain names const domainNames = aliases .map((alias) => [alias, hostedZoneName].filter(Boolean).join(".")) .filter(Boolean) as [string, ...string[]]
// Create certificate stack in us-east-1 const certificateStack = new CertificateStack(this, "CertificateStack", { env: { region: "us-east-1" }, })
// Create Astro AWS construct with custom domain const astroAws = new AstroAWS(this, "AstroAWS", { websiteDir: "../my-astro-project", cdk: { cloudfrontDistribution: { certificate: certificateStack.certificate, domainNames, defaultBehavior: { viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, }, }, }, })
// Create DNS records for each domain domainNames.forEach((domainName) => { new ARecord(this, `ARecord-${domainName}`, { recordName: domainName, target: RecordTarget.fromAlias( new CloudFrontTarget(astroAws.cdk.cloudfrontDistribution), ), zone: hostedZone, })
new AaaaRecord(this, `AaaaRecord-${domainName}`, { recordName: domainName, target: RecordTarget.fromAlias( new CloudFrontTarget(astroAws.cdk.cloudfrontDistribution), ), zone: hostedZone, }) }) }}Troubleshooting
Section titled “Troubleshooting”Certificate Validation Fails
Section titled “Certificate Validation Fails”Problem: Certificate remains in “Pending validation” status.
Solutions:
- Verify DNS validation records are correctly added to Route53
- Check that the validation records match exactly what ACM provides
- Ensure your Route53 hosted zone is properly configured
- Wait a few minutes for DNS propagation
CloudFront Distribution Shows “Invalid Certificate”
Section titled “CloudFront Distribution Shows “Invalid Certificate””Problem: CloudFront distribution fails to deploy with certificate errors.
Solutions:
- Ensure the certificate is in
us-east-1region (required for CloudFront) - Verify the certificate is in “Issued” status before deploying
- Check that all domain names match between certificate and CloudFront configuration
- Ensure the certificate includes all domains you’re using
DNS Records Not Resolving
Section titled “DNS Records Not Resolving”Problem: Domain doesn’t resolve to CloudFront distribution.
Solutions:
- Verify A and AAAA records are created in Route53
- Check that the records point to the correct CloudFront distribution
- Ensure your domain’s nameservers point to Route53
- Wait for DNS propagation (can take up to 48 hours, but usually much faster)
- Use
digornslookupto verify DNS resolution
Certificate Takes Too Long to Issue
Section titled “Certificate Takes Too Long to Issue”Problem: Certificate validation takes longer than expected.
Solutions:
- DNS validation typically completes within minutes after adding validation records
- Check that validation records are correctly formatted
- Verify Route53 hosted zone is accessible
- Ensure there are no typos in domain names
Multiple Domains Not Working
Section titled “Multiple Domains Not Working”Problem: Some domains work but others don’t.
Solutions:
- Verify all domains are included in the certificate’s
alternateNames - Check that DNS records exist for all domains
- Ensure all domains are listed in CloudFront
domainNames - Verify each domain has proper validation records
Best Practices
Section titled “Best Practices”- Always use HTTPS: Configure
ViewerProtocolPolicy.REDIRECT_TO_HTTPSto ensure all traffic uses SSL - Include www and root domain: Set up both
example.comandwww.example.comfor better user experience - Use Route53 for DNS: Route53 integrates seamlessly with ACM and CloudFront
- Monitor certificate expiration: ACM certificates auto-renew, but monitor expiration dates
- Test DNS propagation: Use tools like
digor online DNS checkers to verify DNS changes - Keep certificate in us-east-1: Always use
us-east-1for CloudFront certificates, even if your stack is in another region
Next Steps
Section titled “Next Steps”- Learn about configuring deployment for more CloudFront customization options
- Review security headers and CSP to enhance your site’s security
- Check out performance optimization to improve your site’s speed