If you've ever deployed a website to S3 and CloudFront, you know the frustration: you push your changes, wait a minute, refresh the page, and... still see the old version. đ
The problem is CloudFront cachingâit's great for performance, but it means users can see stale content after you deploy updates . By default, CloudFront serves cached content until it expires (up to 24 hours!) .
The solution? Automate CloudFront cache invalidation from your CI/CD pipeline. Let's break down why and how.
Why Automate Invalidation?
Doing invalidations manually through the AWS Console is:
â Error-prone (forgetting to do it)
â Slow (breaks your deployment flow)
â Not scalable (as your team grows)
Automating invalidation ensures users always see the latest content immediately after deployment .
The Core Command
Whether you use GitHub Actions, Jenkins, or AWS CodePipeline, the underlying AWS CLI command is the same:
bash
aws cloudfront create-invalidation \
--distribution-id EDFDVBD6EXAMPLE \
--paths "/"
The --paths parameter supports wildcardsâ/ invalidates everything, while /index.html or /assets/* invalidates specific paths .
CI/CD Integration Patterns
- GitHub Actions This is probably the most popular approach. In your workflow YAML, add a step to create the invalidation after your deployment:
yaml
- name: Invalidate CloudFront run: | aws cloudfront create-invalidation \ --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \ --paths "/*" env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 Store your distribution ID and AWS credentials as GitHub Secrets .
- AWS CodePipeline with Lambda CodePipeline can trigger a Lambda function after S3 sync, which then creates the invalidation :
python
import boto3
def lambda_handler(event, context):
cloudfront = boto3.client('cloudfront')
response = cloudfront.create_invalidation(
DistributionId='YOUR_DISTRIBUTION_ID',
InvalidationBatch={
'Paths': {
'Quantity': 1,
'Items': ['/*']
},
'CallerReference': str(event.get('timestamp', 'manual'))
}
)
return response
- Terraform The aws_cloudfront_distribution resource doesn't natively support invalidation. However, you can use local-exec provisioner as a workaround :
hcl
resource "aws_cloudfront_distribution" "s3_distribution" {
# ... configuration ...
provisioner "local-exec" {
command = "aws cloudfront create-invalidation --distribution-id ${self.id} --paths '/*'"
}
}
â ď¸ Note: This runs after resource creation, but for every terraform apply, consider using a null_resource with triggers for more control.
- Jenkins Pipeline Similar to GitHub Actionsâjust call the AWS CLI from your Jenkinsfile:
groovy
stage('Invalidate Cache') {
steps {
sh '''
aws cloudfront create-invalidation \
--distribution-id ${CLOUDFRONT_DIST_ID} \
--paths "/*"
'''
}
}
- Package.json Scripts For smaller projects, you can chain commands in npm scripts :
json
{
"scripts": {
"build.prod": "ng build --prod --aot",
"aws.deploy": "aws s3 sync dist/ s3://www.mywebsite.com --delete",
"aws.invalidate": "aws cloudfront create-invalidation --distribution-id [ID] --paths /",
"deploy": "npm run build.prod && npm run aws.deploy && npm run aws.invalidate"
}
}
IAM Permissions
Your CI user needs at minimum:
json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["cloudfront:CreateInvalidation"],
"Resource": "*"
}]
}
If you need to wait for completion (e.g., for testing), add cloudfront:GetInvalidation .
Best Practices & Considerations
Don't invalidate everything every time. If only index.html and main.js change, invalidate just those paths to be more efficient .
Cache invalidation costs money. The first 1,000 paths per month are free; after that, it's charged . Invalidate with intention.
Consider using versioned filenames (e.g., main.a1b2c3.js) so you can keep caching forever and invalidate only the entry point. This is how modern frameworks roll.
Wait for completion? Some plugins (like ember-cli-deploy-cloudfront) offer a waitForInvalidation flag, but this can take several minutesâonly use it when absolutely necessary .
Real-World Example
A typical CI/CD flow looks like this :
Developer pushes code to GitHub
GitHub Actions/AWS CodePipeline triggers build
Build assets are uploaded to S3
CloudFront invalidation runs automatically
Users see fresh content instantly
No manual steps. No stale cache. đ
Summary
CloudFront invalidations are a necessary evil when deploying sites with a CDN. The good news is that automating them is straightforward with any CI/CD toolâGitHub Actions, AWS CodePipeline, Jenkins, or Terraform.
Choose the approach that fits your stack, add the invalidation step to your pipeline, and never think about stale cache again.
What's your preferred way to handle CloudFront invalidations? Drop a comment below!
Top comments (1)
Automating CloudFront invalidations from CI is exactly where âstale after deployâ stops being a console chore. Path-scoped clears beat a blanket /* every time, and versioned asset filenames shrink what you even need to invalidate to the HTML or data entry points.
Two day-two pains still show up once you have more than one distribution or environment: keeping the path list honest with what the build actually changed, and waiting (or not waiting) for invalidation completion before traffic shifts. Cost and propagation delay push teams toward intentional paths instead of habit wildcards.
We have been building CDNCA (AGPL-3.0) so CDN nodes and a plugin marketplace live on one control plane; marketplace features roll out release by release. Do you invalidate only the changed entry points after each deploy, or still clear a broad prefix when editors push content outside the CI path?