# Add CloudFront CDN to Existing Deployment

# Prerequisites

Before starting, gather the following information:

  1. AWS Account ID: Locate this in the deployment organization's settings page.
  2. Deployment ID: Found in the deployment's settings page.
  3. S3 Bucket: Ensure that a media S3 bucket has been created.

Log in to the production SSO account, and assume the ProdAutoPilotSupportLevelTwo role in the correct AWS account.

# Locating The Correct Stack

  1. Open the CloudFormation service in the AWS console.
  2. Use the Deployment ID to filter and locate the main stack.
  3. To simplify the search, untoggle the "View nested" switch.

Once the main stack is identified, proceed to update it.

# Editing The Template

The recommended method for editing the stack's template is through the "Infrastructure Composer" since you can edit it through your browser. Once in the editor, follow these steps:

  1. Navigate to the Template tab within the editor.
  2. Ensure the template remains in JSON format.

Proceed to update the template.

# Gather Required Values From CloudFormation

Before adding the CloudFront resources, you'll need to reference the following values from the existing CloudFormation template. These values will be automatically populated using CloudFormation references:

  1. Deployment ID: This is automatically extracted from the stack name. The template uses Fn::Select and Fn::Split to extract it from the stack name (e.g., "jrc-XXXXX" → "XXXXX"). You'll reference this as shown in the template below.

  2. Organization ID: Check if the OrganizationId parameter exists in the Parameters section. If it doesn't exist, you may need to add it or use the deployment tags to identify the organization.

  3. Preview Domain (Alternate Domain Name): Use the TemporaryDomain parameter, which contains the private preview domain for the deployment.

# Add CloudFront Certificate Parameter

First, add a new parameter specifically for CloudFront. Locate the Parameters section in the template and add this parameter:

{
	"CloudFrontCertificateArn": {
		"Description": "SSL Certificate for CloudFront Distribution (Optional - leave empty to use CloudFront default certificate)",
		"Type": "String",
		"Default": "",
		"AllowedPattern": "^(|arn:aws:acm:[^:]+:\\d+:certificate\\/[a-z0-9-]{36})$",
		"ConstraintDescription": "must be empty or contain a certificate arn"
	}
}

# Add CloudFront Condition

Next, add a condition to check if the CloudFront certificate exists. Locate the Conditions section in the template and add this condition:

{
	"HasCloudFrontCertificate": {
		"Fn::Not": [
			{
				"Fn::Equals": [
					"",
					{
						"Ref": "CloudFrontCertificateArn"
					}
				]
			}
		]
	}
}

# Add CloudFront Distribution Resource

Locate the end of the Resources section in the template and merge the following JSON with the existing resources:

{
	"MediaCloudFrontOriginAccessControl": {
		"Type": "AWS::CloudFront::OriginAccessControl",
		"Properties": {
			"OriginAccessControlConfig": {
				"Name": {
					"Fn::Sub": "OAC-${AWS::StackName}"
				},
				"OriginAccessControlOriginType": "s3",
				"SigningBehavior": "always",
				"SigningProtocol": "sigv4"
			}
		}
	},
	"MediaCloudFrontDistribution": {
		"Type": "AWS::CloudFront::Distribution",
		"Properties": {
			"DistributionConfig": {
				"Comment": {
					"Fn::Sub": [
						"deployment=${DeploymentId}",
						{
							"DeploymentId": {
								"Fn::Select": [
									1,
									{
										"Fn::Split": [
											"jrc-",
											{
												"Ref": "AWS::StackName"
											}
										]
									}
								]
							}
						}
					]
				},
				"Enabled": true,
				"PriceClass": "PriceClass_100",
				"HttpVersion": "http2and3",
				"IPV6Enabled": false,
				"Aliases": {
					"Fn::If": [
						"HasCloudFrontCertificate",
						[
							{
								"Ref": "TemporaryDomain"
							}
						],
						{
							"Ref": "AWS::NoValue"
						}
					]
				},
				"ViewerCertificate": {
					"Fn::If": [
						"HasCloudFrontCertificate",
						{
							"AcmCertificateArn": {
								"Ref": "CloudFrontCertificateArn"
							},
							"SslSupportMethod": "sni-only",
							"MinimumProtocolVersion": "TLSv1.3_2025"
						},
						{
							"CloudFrontDefaultCertificate": true
						}
					]
				},
				"Logging": {
					"Bucket": "",
					"IncludeCookies": false
				},
				"Origins": [
					{
						"Id": "S3-MediaBucket",
						"DomainName": {
							"Fn::GetAtt": [
								"MediaS3Bucket",
								"RegionalDomainName"
							]
						},
						"S3OriginConfig": {
							"OriginAccessIdentity": ""
						},
						"OriginAccessControlId": {
							"Ref": "MediaCloudFrontOriginAccessControl"
						}
					}
				],
				"DefaultCacheBehavior": {
					"TargetOriginId": "S3-MediaBucket",
					"ViewerProtocolPolicy": "redirect-to-https",
					"AllowedMethods": [
						"GET",
						"HEAD",
						"OPTIONS"
					],
					"CachedMethods": [
						"GET",
						"HEAD"
					],
					"Compress": true,
					"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
					"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
				},
				"CacheBehaviors": [
					{
						"PathPattern": "*.js",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": true,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.css",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": true,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.png",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": true,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.jpg",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": true,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.jpeg",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": true,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.svg",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": true,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.woff",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": true,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.woff2",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": true,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.gif",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": true,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.ttf",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": true,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.ico",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": true,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.webp",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": true,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.mp4",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": false,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.webm",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": false,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.mp3",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": false,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.wav",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": false,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.pdf",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": true,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					},
					{
						"PathPattern": "*.zip",
						"TargetOriginId": "S3-MediaBucket",
						"ViewerProtocolPolicy": "redirect-to-https",
						"AllowedMethods": [
							"GET",
							"HEAD",
							"OPTIONS"
						],
						"CachedMethods": [
							"GET",
							"HEAD"
						],
						"Compress": false,
						"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
						"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf"
					}
				]
			}
		}
	}
}

# Update S3 Bucket Policy for CloudFront Access

Navigate to the MediaS3BucketPolicy resource in the template and replace the existing policy with the updated version below that includes CloudFront access:

{
	"MediaS3BucketPolicy": {
		"Type": "AWS::S3::BucketPolicy",
		"Properties": {
			"Bucket": {
				"Ref": "MediaS3Bucket"
			},
			"PolicyDocument": {
				"Version": "2012-10-17",
				"Statement": [
					{
						"Sid": "AllowReadFromWebNodesNginx",
						"Effect": "Allow",
						"Principal": "*",
						"Action": [
							"s3:GetObject"
						],
						"Resource": [
							{
								"Fn::Sub": "arn:aws:s3:::${MediaS3Bucket}/*"
							}
						],
						"Condition": {
							"IpAddress": {
								"aws:SourceIp": {
									"Fn::GetAtt": [
										"LayerJrcJump",
										"Outputs.PublicIp"
									]
								}
							}
						}
					},
					{
						"Sid": "AllowCloudFrontServicePrincipal",
						"Effect": "Allow",
						"Principal": {
							"Service": "cloudfront.amazonaws.com"
						},
						"Action": "s3:GetObject",
						"Resource": {
							"Fn::Sub": "arn:aws:s3:::${MediaS3Bucket}/*"
						},
						"Condition": {
							"StringEquals": {
								"AWS:SourceArn": {
									"Fn::Sub": "arn:aws:cloudfront::${AWS::AccountId}:distribution/${MediaCloudFrontDistribution}"
								}
							}
						}
					}
				]
			}
		}
	}
}

# Adding Output Values

Navigate to the Outputs section of the template and merge the following JSON with the existing outputs:

{
	"CloudFrontDistributionId": {
		"Value": {
			"Ref": "MediaCloudFrontDistribution"
		}
	},
	"CloudFrontDistributionDomainName": {
		"Value": {
			"Fn::GetAtt": [
				"MediaCloudFrontDistribution",
				"DomainName"
			]
		}
	}
}

# Add Metadata To Display Info In AutoPilot

Navigate to the Metadata section of the template and make the following updates:

# Add CloudFront Certificate Input (Optional)

If you want to make the CloudFront certificate parameter manageable through the AutoPilot UI, add this to the JetRails::Gui::Input section:

{
	"CloudFrontCertificateArn": {
		"Component": "Select",
		"FriendlyName": "CloudFront SSL Certificate (Optional)",
		"DataLoader": "certificates",
		"UseDomainFrom": "PrimaryDomain",
		"HideOnCreation": true
	}
}

Also, add the parameter to an appropriate input group in JetRails::Gui::InputGroup. For example, in the ApplicationSettings group:

{
	"ApplicationSettings": {
		"FriendlyName": "Application Settings",
		"Inputs": [
			"MagentoVersion",
			"CertificateArn",
			"CloudFrontCertificateArn",
			"AllowCloudflareWebTraffic",
			"FullPageCacheApplication"
		]
	}
}

# Add CloudFront Output Values

For the JetRails::Gui::Output section merge the following:

{
	"CloudFrontDistributionId": {
		"Component": "ListItem",
		"FriendlyName": "Distribution ID"
	},
	"CloudFrontDistributionDomainName": {
		"Component": "ListItem",
		"FriendlyName": "Distribution Domain Name"
	}
}

For the JetRails::Gui::OutputGroup section merge the following:

{
	"CloudFrontCDN": {
		"FriendlyName": "CloudFront CDN",
		"Variant": "LabelValueTable",
		"Outputs": [
			"CloudFrontDistributionId",
			"CloudFrontDistributionDomainName"
		]
	}
}

# Applying the Changes

  1. Validate the updated template.
  2. Ensure the new CloudFrontCertificateArn parameter is set to empty string "" (this is the default and recommended configuration).
  3. Leave the existing TemporaryCertificateArn parameter unchanged (it's required for the Application Load Balancer).
  4. Update the stack with the new template.
  5. Use the wizard to apply the changes.

Once the update is complete, the CloudFront distribution ID and domain name will be outputted in the AutoPilot interface in the Overview tab.

# Verify Changes

# Check CloudFront Distribution Status

  1. Navigate to the CloudFront service in the AWS console.
  2. Locate your distribution using the Distribution ID from the outputs.
  3. Wait for the status to change from "In Progress" to "Deployed".

# Test CloudFront Access

Once the distribution is deployed, you can test access to files through CloudFront. First, get your CloudFront distribution domain name from the stack outputs (e.g., d1234567890abc.cloudfront.net).

Upload a test file to your S3 bucket:

date > test.txt
aws s3 cp ./test.txt s3://BUCKET_NAME/test.txt

Then access the file through CloudFront using the distribution domain name:

# Replace with your actual CloudFront domain from outputs
curl https://d1234567890abc.cloudfront.net/test.txt

You should see the contents of your test file returned.

# Verify Cache Behaviors

Test that the cache behaviors are working correctly by accessing files with different extensions:

# Replace d1234567890abc.cloudfront.net with your actual CloudFront domain
CLOUDFRONT_DOMAIN="d1234567890abc.cloudfront.net"

# Test JavaScript file caching
curl -I https://$CLOUDFRONT_DOMAIN/sample.js

# Test CSS file caching
curl -I https://$CLOUDFRONT_DOMAIN/sample.css

# Test image file caching
curl -I https://$CLOUDFRONT_DOMAIN/sample.png

Check the response headers for X-Cache which should show Hit from cloudfront for cached content after the first request.

# Clean Up Test Files

Finally, remove your test files:

aws s3 rm s3://BUCKET_NAME/test.txt

# Troubleshooting

# Distribution Stuck in "In Progress"

CloudFront distributions can take up to 30 minutes to deploy. If it takes longer, check the CloudFormation stack events for any errors.

# 403 Forbidden Errors

If you receive 403 errors when accessing files:

  1. Verify the S3 bucket policy includes the CloudFront service principal statement.
  2. Check that the Origin Access Control is properly configured.
  3. Ensure the files exist in the S3 bucket.

# SSL Certificate Issues

If you encounter SSL certificate errors:

  1. Verify the SSL certificate ARN is correct.
  2. Ensure the certificate is in the us-east-1 region (CloudFront requirement).
  3. Confirm the certificate covers the alternate domain name (wildcard certificate).

# Cache Not Working

If files are not being cached:

  1. Check the cache behaviors match your file extensions.
  2. Verify the cache policy ID is correct.
  3. Review CloudFront logs (if enabled) for cache hit/miss information.

# Creating CloudFront Invalidations

After deploying CloudFront, you may need to invalidate cached content when files are updated. CloudFront invalidations allow you to remove files from CloudFront edge caches before they expire naturally.

# What Are Invalidations?

CloudFront caches content at edge locations to improve performance. When you update a file in your S3 bucket, CloudFront continues serving the cached version until it expires. Invalidations force CloudFront to fetch the latest version from the origin (S3 bucket).

# Common Use Cases

  • Updating website assets (CSS, JavaScript, images)
  • Removing outdated content
  • Fixing incorrect files that were uploaded
  • Immediate deployment of critical updates

To create invalidations from the Jump Host using the AWS CLI, you need to add CloudFront permissions to the existing InstanceProfileRole.

Navigate to the InstanceProfileRole resource in your CloudFormation template (typically around line 1062 where other policies are defined). Add the following policy to the Policies array:

{
	"PolicyName": "AllowCloudFrontInvalidation",
	"PolicyDocument": {
		"Version": "2012-10-17",
		"Statement": [
			{
				"Effect": "Allow",
				"Action": [
					"cloudfront:CreateInvalidation",
					"cloudfront:GetInvalidation",
					"cloudfront:ListInvalidations"
				],
				"Resource": {
					"Fn::Sub": "arn:aws:cloudfront::${AWS::AccountId}:distribution/${MediaCloudFrontDistribution}"
				}
			}
		]
	}
}

# Creating Invalidations via AWS CLI

Once the IAM permissions are configured, you can create invalidations using the AWS CLI:

# Basic Invalidation

Invalidate specific paths:

aws cloudfront create-invalidation \
  --distribution-id <ENTER-DISTRIBUTION-ID-HERE> \
  --paths "/media/*"

# Invalidate Multiple Paths

aws cloudfront create-invalidation \
  --distribution-id <ENTER-DISTRIBUTION-ID-HERE> \
  --paths "/media/*" "/static/css/*" "/images/*.jpg"

# Invalidate Everything (Use Carefully)

aws cloudfront create-invalidation \
  --distribution-id <ENTER-DISTRIBUTION-ID-HERE> \
  --paths "/*"

# Check Invalidation Status

aws cloudfront get-invalidation \
  --distribution-id <ENTER-DISTRIBUTION-ID-HERE> \
  --id INVALIDATION_ID

# List All Invalidations

aws cloudfront list-invalidations \
  --distribution-id <ENTER-DISTRIBUTION-ID-HERE>

# Getting the Distribution ID

The Distribution ID is available in multiple places:

  1. CloudFormation Outputs: Check the CloudFrontDistributionId output
  2. AutoPilot Interface: Overview tab under "CloudFront CDN"
  3. AWS Console: CloudFront service page

You can retrieve it via AWS CLI:

aws cloudformation describe-stacks \
  --stack-name YOUR_STACK_NAME \
  --query 'Stacks[0].Outputs[?OutputKey==`CloudFrontDistributionId`].OutputValue' \
  --output text

# Automation Example

Create a script to automate invalidations after deployments:

#!/bin/bash

DISTRIBUTION_ID="<ENTER-HERE-DISTRIBUTION-ID>"
PATHS="/media/* /static/css/* /static/js/*"

echo "Creating CloudFront invalidation..."
INVALIDATION_ID=$(aws cloudfront create-invalidation \
  --distribution-id $DISTRIBUTION_ID \
  --paths $PATHS \
  --query 'Invalidation.Id' \
  --output text)

echo "Invalidation created: $INVALIDATION_ID"
echo "Checking status..."

aws cloudfront wait invalidation-completed \
  --distribution-id $DISTRIBUTION_ID \
  --id $INVALIDATION_ID

echo "Invalidation completed!"

# Troubleshooting Invalidations

# Access Denied Error

If you receive an access denied error:

  1. Verify the IAM policy is attached to your user/role
  2. Check the distribution ID is correct
  3. Ensure the policy ARN matches your distribution
  4. Confirm your AWS credentials are configured correctly

# Invalidation Taking Too Long

Invalidations typically complete in 10-15 minutes but can take longer:

  1. Check the invalidation status using get-invalidation
  2. CloudFront processes invalidations across all edge locations
  3. Multiple concurrent invalidations may take longer
  4. Wait at least 15 minutes before considering it stuck

# Cost Concerns

If invalidation costs are a concern:

  1. Use file versioning instead of invalidations (e.g., app.v2.js)
  2. Reduce cache TTL for frequently changing content
  3. Use specific paths instead of wildcards
  4. Batch multiple changes into fewer invalidation requests

# Optional: Using a Custom Domain

By default, this guide configures CloudFront to use AWS's managed certificate and the CloudFront domain (e.g., d1234567890abc.cloudfront.net). If you need to use a custom domain name (e.g., cdn.example.com or your preview domain), follow these additional steps.

# Prerequisites for Custom Domain

  1. Valid ACM Certificate: You must have a valid SSL certificate in AWS Certificate Manager in the us-east-1 region.
  2. DNS Access: Ability to create DNS records for your domain.

# Step 1: Obtain or Create an ACM Certificate

You have two options:

# Option A: Request a New Certificate from ACM (Recommended)

  1. Open AWS Certificate Manager in the us-east-1 region
  2. Click Request a certificate
  3. Choose Request a public certificate
  4. Enter your domain name:
    • For a specific subdomain: cdn.example.com
    • For a wildcard: *.example.com or *.jetrailsdev.com
  5. Choose validation method (DNS or Email)
  6. Complete the validation process
  7. Copy the certificate ARN once it's issued

# Option B: Import an Existing Certificate

If you already have a certificate, you can import it, but you must include the complete certificate chain:

aws acm import-certificate \
  --certificate fileb://certificate.crt \
  --private-key fileb://private-key.pem \
  --certificate-chain fileb://certificate-chain.pem \
  --region us-east-1

# Step 2: Update CloudFormation Parameters

Update the CloudFrontCertificateArn parameter in your CloudFormation template with the certificate ARN:

"CloudFrontCertificateArn": "arn:aws:acm:us-east-1:ACCOUNT_ID:certificate/CERTIFICATE_ID"

# Step 3: Update the CloudFormation Stack

  1. Validate the updated template
  2. Update the stack with the new parameter value
  3. Wait for CloudFront distribution to update (15-30 minutes)

# Step 4: Configure DNS

Once the CloudFront distribution is deployed with your custom certificate:

  1. Get the CloudFront distribution domain name from the outputs (e.g., d1234567890abc.cloudfront.net)
  2. Create a CNAME record in your DNS:

    Type: CNAME
    Name: cdn (or your subdomain)
    Value: d1234567890abc.cloudfront.net
    TTL: 300

# Step 5: Verify Custom Domain

Test that your custom domain works:

curl -I https://cdn.example.com/test.jpg

You should see the X-Cache header indicating CloudFront is serving the content.

# What Changes with Custom Domain

When you configure a custom domain:

  • Alternate Domain Names: The TemporaryDomain parameter value will be used as an alias
  • SSL Certificate: Your custom ACM certificate will be used
  • Security Policy: TLS 1.3 (TLSv1.3_2025) will be enforced
  • Access: You can access content via both:
    • https://your-domain.com/path/to/file.jpg
    • https://d1234567890abc.cloudfront.net/path/to/file.jpg

# Troubleshooting Custom Domains

# Certificate Not Trusted Error

If you see "The certificate that is attached to your distribution was not issued by a trusted Certificate Authority":

  1. Check certificate status: Ensure it shows "Issued" in ACM (not "Pending validation")
  2. Verify certificate chain: For imported certificates, make sure you included all intermediate certificates
  3. Use ACM-issued certificate: The safest option is to request a certificate directly from ACM

# Domain Not Resolving

If your custom domain doesn't work:

  1. Verify the CNAME record is correct
  2. Wait for DNS propagation (can take up to 48 hours, usually much faster)
  3. Check that the domain matches the certificate (wildcards must match the pattern)
  4. Ensure the certificate covers the exact domain or uses a wildcard that matches

# Mixed Content Warnings

If you see mixed content warnings:

  1. Ensure all resources are loaded over HTTPS
  2. Update your application to use the CloudFront domain
  3. Check that the CloudFront origin (S3) is accessed via HTTPS

# Reverting to Default Certificate

If you need to remove the custom domain and go back to CloudFront's default:

  1. Set CloudFrontCertificateArn parameter to empty string: ""
  2. Update the CloudFormation stack
  3. CloudFront will automatically revert to using its default certificate and domain

This is useful if you encounter certificate issues or no longer need the custom domain.