When people first see CloudFormation YAML, it looks like programming code.
But it's actually more like giving AWS a construction blueprint:
"Please create these AWS resources for me."
In this case, the blueprint says:
Parameters:
DistributionId:
Type: String
This means:
"Before deploying, ask for a CloudFront Distribution ID."
So instead of hardcoding:
E123ABC
you can reuse the same template for:
just by supplying different parameter values.
LambdaExecutionRole:
Type: AWS::IAM::Role
Lambda is not automatically allowed to do AWS actions.
AWS security works like:
"Everything is denied unless permission is explicitly granted."
So Lambda needs an IAM Role.
Think of the role like an employee badge.
Without the badge:
ACCESS DENIED
AssumeRolePolicyDocument:
This part answers:
"Who may use this role?"
Principal:
Service:
- lambda.amazonaws.com
Meaning:
"The Lambda service is allowed to use this role."
Without this, Lambda cannot even start running.
Action:
- cloudfront:CreateInvalidation
This part answers:
"What may Lambda do?"
Meaning:
"Lambda is allowed to clear CloudFront cache."
Type: AWS::Lambda::Function
Now CloudFormation creates the actual Lambda.
Runtime: python3.12
Meaning:
"Run this Lambda using Python."
Handler: index.lambda_handler
This means:
index → filename
lambda_handler → function name
So Lambda expects:
def lambda_handler(event, context):
inside:
index.py
Environment:
Variables:
DISTRIBUTION_ID: !Ref DistributionId
This is where CloudFormation connects the parameter to Lambda.
!Ref DistributionId means:
"Take the parameter value and inject it into Lambda."
So if deployment provides:
E123ABC456
then inside Python:
os.environ["DISTRIBUTION_ID"]
returns:
E123ABC456
Code:
ZipFile: |
Normally Lambda code is uploaded as a ZIP file.
But for tiny utility Lambdas, CloudFormation lets you directly write the Python code inside YAML.
import boto3
boto3 is AWS SDK for Python.
Think of it as:
"Python library for talking to AWS services."
cloudfront = boto3.client("cloudfront")
Creates a helper object for CloudFront API calls.
distribution_id = os.environ["DISTRIBUTION_ID"]
Reads the value CloudFormation injected earlier.
cloudfront.create_invalidation(...)
This is the actual:
"Clear CloudFront cache"
operation.
"/*"?"Items": ["/*"]
Means:
"Invalidate all cached files."
Equivalent to:
/index.html
/app.js
/styles.css
everything
str(context.aws_request_id)
CloudFront requires every invalidation request to be unique.
Lambda already gets a unique request ID for every execution, so it uses that.
When CloudFormation creates resources, it remembers them.
Think of CloudFormation keeping a notebook:
"I created this IAM Role"
"I created this Lambda"
"I attached this policy"
This tracking is why CloudFormation can later:
Suppose original policy was:
Action:
- cloudfront:CreateInvalidation
Later you change it to:
Action:
- cloudfront:CreateInvalidation
- s3:ListBucket
CloudFormation compares:
Old YAML vs New YAML
and realizes:
"The IAM policy changed."
So it automatically updates the role policy in AWS.
You do not manually edit IAM Console.
CloudFormation becomes the source of truth.
Suppose old template had:
- s3:ListBucket
but new template removes it.
During stack update:
CloudFormation updates the policy
and that permission disappears from IAM.
AWS resources are continuously adjusted to match the YAML.
Usually:
No
Simple permission updates are typically done in-place.
The role itself usually stays the same.
CloudFormation deletes the resources it created.
That means:
This template uses:
Policies:
inside the role.
That creates an inline policy.
Inline policy means:
"The policy lives inside the role."
So when the role is deleted:
the policy disappears too
because it was part of the role itself.
Suppose CloudFormation created:
cloudfront:CreateInvalidation
Then somebody manually adds:
s3:DeleteBucket
through IAM Console.
Now AWS no longer matches the YAML.
This is called:
configuration drift
Later, if CloudFormation updates the stack again, it may overwrite those manual changes to make AWS match the template again.
That's why teams usually treat CloudFormation as:
"The single source of truth."
Your YAML is basically:
Desired AWS infrastructure
CloudFormation reads it and says:
Create this Lambda
Create this IAM Role
Attach these permissions
Pass this Distribution ID
Keep AWS matching this template
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
DistributionId:
Type: String
Resources:
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: CloudFrontInvalidationPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- cloudfront:CreateInvalidation
Resource: "*"
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: "*"
InvalidateCloudFrontLambda:
Type: AWS::Lambda::Function
Properties:
FunctionName: invalidate-cloudfront
Runtime: python3.12
Handler: index.lambda_handler
Role: !GetAtt LambdaExecutionRole.Arn
Timeout: 30
Environment:
Variables:
DISTRIBUTION_ID: !Ref DistributionId
Code:
ZipFile: |
import os
import boto3
cloudfront = boto3.client("cloudfront")
def lambda_handler(event, context):
distribution_id = os.environ["DISTRIBUTION_ID"]
response = cloudfront.create_invalidation(
DistributionId=distribution_id,
InvalidationBatch={
"Paths": {
"Quantity": 1,
"Items": ["/*"]
},
"CallerReference": str(context.aws_request_id)
}
)
return {
"statusCode": 200,
"body": "CloudFront invalidation created"
}