#
Add CloudFront CDN to Existing Deployment
#
Prerequisites
Before starting, gather the following information:
- AWS Account ID: Locate this in the deployment organization's settings page.
- Deployment ID: Found in the deployment's settings page.
- S3 Bucket: Ensure that a media S3 bucket has been created.
warning
For All-In-One (AIO) type deployments, you must first follow the Add S3 Bucket to Existing Deployment guide before proceeding with CloudFront setup.
info
Default Configuration: By default, this guide configures CloudFront to use AWS's default SSL certificate and CloudFront domain (e.g., d1234567890abc.cloudfront.net). This is the recommended approach as it:
- Works immediately without certificate configuration
- Avoids SSL certificate issues
- Provides full CloudFront CDN functionality
- Is free and fully managed by AWS
If you need to use a custom domain name, see the
Log in to the production SSO account, and assume the ProdAutoPilotSupportLevelTwo role in the correct AWS account.
#
Locating The Correct Stack
- Open the CloudFormation service in the AWS console.
- Use the Deployment ID to filter and locate the main stack.
- 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:
- Navigate to the Template tab within the editor.
- Ensure the template remains in JSON format.
danger
Do not convert the template to YAML. AutoPilot requires JSON format, and converting from YAML can introduce discrepancies that prevent the deployment page from loading correctly.
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:
Deployment ID: This is automatically extracted from the stack name. The template uses
Fn::SelectandFn::Splitto extract it from the stack name (e.g., "jrc-XXXXX" → "XXXXX"). You'll reference this as shown in the template below.Organization ID: Check if the
OrganizationIdparameter exists in theParameterssection. If it doesn't exist, you may need to add it or use the deployment tags to identify the organization.Preview Domain (Alternate Domain Name): Use the
TemporaryDomainparameter, which contains the private preview domain for the deployment.
tip
All of these values are already parameters or can be derived within the CloudFormation template using intrinsic functions, so you don't need to manually copy and paste values. The template snippets below show the correct reference syntax.
info
About SSL Certificates: The existing TemporaryCertificateArn parameter is used by the Application Load Balancer and cannot be modified for CloudFront purposes. We'll create a new dedicated CloudFrontCertificateArn parameter that is optional and independent.
#
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"
}
}
note
We're creating a separate parameter for CloudFront because the existing TemporaryCertificateArn parameter is required by the Application Load Balancer and cannot be empty.
#
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"
}
]
}
}
}
}
note
Important Configuration Details:
- Name: The distribution name is automatically generated from the CloudFormation stack name using
Fn::Sub. - Description: Set to
deployment={DeploymentId}whereDeploymentIdis extracted from the stack name by splitting on "jrc-" and taking the second element. - Price Class:
PriceClass_100covers only North America and Europe. - Domain Configuration (Default):
- By default, uses CloudFront's default
*.cloudfront.netdomain - No alternate domain names configured
- Uses CloudFront's default SSL certificate
- By default, uses CloudFront's default
- HTTP Versions:
http2and3enables both HTTP/2 and HTTP/3. - IPv6: Set to
falseto disable IPv6. - Standard Logging: Left empty to disable logging.
- Cookie Logging: Set to
falseviaIncludeCookies. - Tags: Only includes the Deployment tag. The Organization tag has been omitted as
OrganizationIdis not defined in the standard template. If your deployment requires organization tracking, add theOrganizationIdparameter to your template first.
The cache behaviors are configured for common media file extensions including: *.js, *.css, *.png, *.jpg, *.jpeg, *.svg, *.woff*, *.gif, *.ttf, *.ico, *.webp, *.mp4, *.webm, *.mp3, *.wav, *.pdf, and *.zip.
Conditional Certificate Logic: The template includes conditional logic that automatically detects if CloudFrontCertificateArn is populated. By keeping this parameter empty (default), CloudFront uses its managed certificate. See the
Note: The existing TemporaryCertificateArn parameter is used by the Application Load Balancer and must remain populated. The new CloudFrontCertificateArn parameter is independent and optional.
#
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}"
}
}
}
}
]
}
}
}
}
note
This updated policy adds a new statement that allows CloudFront to access the S3 bucket using Origin Access Control (OAC). The condition ensures only your specific CloudFront distribution can access the bucket.
For AIO deployments: The policy uses LayerJrcJump.Outputs.PublicIp as shown above (this matches your template).
For Cluster deployments: If you're working with a cluster deployment, change LayerJrcJump.Outputs.PublicIp to LayerJrcNetwork.Outputs.ElasticIpPrimary in the first statement.
#
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
- Validate the updated template.
- Ensure the new
CloudFrontCertificateArnparameter is set to empty string""(this is the default and recommended configuration). - Leave the existing
TemporaryCertificateArnparameter unchanged (it's required for the Application Load Balancer). - Update the stack with the new template.
- Use the wizard to apply the changes.
warning
CloudFront distribution creation and updates can take 15-30 minutes to complete. Be patient and monitor the CloudFormation stack progress.
Once the update is complete, the CloudFront distribution ID and domain name will be outputted in the AutoPilot interface in the Overview tab.
success
Expected Result: The CloudFront distribution will be created with:
- Distribution ID (e.g.,
E1234ABCD5678) - CloudFront domain name (e.g.,
d1234567890abc.cloudfront.net)
You can access your S3 content through: https://d1234567890abc.cloudfront.net/path/to/file.jpg
#
Verify Changes
#
Check CloudFront Distribution Status
- Navigate to the CloudFront service in the AWS console.
- Locate your distribution using the Distribution ID from the outputs.
- 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.
tip
The CloudFront domain name is available in:
- CloudFormation stack outputs (
CloudFrontDistributionDomainName) - AutoPilot interface Overview tab
- AWS CloudFront console
#
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:
- Verify the S3 bucket policy includes the CloudFront service principal statement.
- Check that the Origin Access Control is properly configured.
- Ensure the files exist in the S3 bucket.
#
SSL Certificate Issues
If you encounter SSL certificate errors:
- Verify the SSL certificate ARN is correct.
- Ensure the certificate is in the
us-east-1region (CloudFront requirement). - Confirm the certificate covers the alternate domain name (wildcard certificate).
#
Cache Not Working
If files are not being cached:
- Check the cache behaviors match your file extensions.
- Verify the cache policy ID is correct.
- 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}"
}
}
]
}
}
note
Permissions Breakdown:
cloudfront:CreateInvalidation- Create new invalidationscloudfront:GetInvalidation- Check invalidation statuscloudfront:ListInvalidations- List all invalidations for the distribution
The policy is scoped to only the specific CloudFront distribution created by this stack.
tip
This policy should be inserted in the InstanceProfileRole resource, specifically in the Policies array, after the existing policies like AllowSnsTopicPublish and before the closing bracket. What this does:
- Grants permission to create invalidations on the specific CloudFront distribution
- Allows checking invalidation status (GetInvalidation)
- Allows listing invalidations for monitoring purposes
- Scoped specifically to your 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:
- CloudFormation Outputs: Check the
CloudFrontDistributionIdoutput - AutoPilot Interface: Overview tab under "CloudFront CDN"
- 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:
- Verify the IAM policy is attached to your user/role
- Check the distribution ID is correct
- Ensure the policy ARN matches your distribution
- Confirm your AWS credentials are configured correctly
#
Invalidation Taking Too Long
Invalidations typically complete in 10-15 minutes but can take longer:
- Check the invalidation status using
get-invalidation - CloudFront processes invalidations across all edge locations
- Multiple concurrent invalidations may take longer
- Wait at least 15 minutes before considering it stuck
#
Cost Concerns
If invalidation costs are a concern:
- Use file versioning instead of invalidations (e.g.,
app.v2.js) - Reduce cache TTL for frequently changing content
- Use specific paths instead of wildcards
- 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
- Valid ACM Certificate: You must have a valid SSL certificate in AWS Certificate Manager in the
us-east-1region. - 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)
- Open AWS Certificate Manager in the
us-east-1region - Click Request a certificate
- Choose Request a public certificate
- Enter your domain name:
- For a specific subdomain:
cdn.example.com - For a wildcard:
*.example.comor*.jetrailsdev.com
- For a specific subdomain:
- Choose validation method (DNS or Email)
- Complete the validation process
- 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
warning
Important for Imported Certificates:
- You must include the complete certificate chain (intermediate + root CA certificates)
- Self-signed certificates will not work with CloudFront
- The certificate must be from a trusted Certificate Authority
- Missing intermediate certificates will cause the error: "The certificate that is attached to your distribution was not issued by a trusted Certificate Authority"
#
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"
warning
Important: Do not modify the TemporaryCertificateArn parameter - that's used by the Application Load Balancer. The CloudFrontCertificateArn is a separate parameter specifically for CloudFront.
#
Step 3: Update the CloudFormation Stack
- Validate the updated template
- Update the stack with the new parameter value
- Wait for CloudFront distribution to update (15-30 minutes)
#
Step 4: Configure DNS
Once the CloudFront distribution is deployed with your custom certificate:
- Get the CloudFront distribution domain name from the outputs (e.g.,
d1234567890abc.cloudfront.net) 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
TemporaryDomainparameter 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.jpghttps://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":
- Check certificate status: Ensure it shows "Issued" in ACM (not "Pending validation")
- Verify certificate chain: For imported certificates, make sure you included all intermediate certificates
- 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:
- Verify the CNAME record is correct
- Wait for DNS propagation (can take up to 48 hours, usually much faster)
- Check that the domain matches the certificate (wildcards must match the pattern)
- Ensure the certificate covers the exact domain or uses a wildcard that matches
#
Mixed Content Warnings
If you see mixed content warnings:
- Ensure all resources are loaded over HTTPS
- Update your application to use the CloudFront domain
- 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:
- Set
CloudFrontCertificateArnparameter to empty string:"" - Update the CloudFormation stack
- 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.
note
Remember: The TemporaryCertificateArn parameter should remain unchanged as it's used by the Application Load Balancer.