In this tutorial, we will automate the tagging of new EC2 instances using CloudFormation templates. In the previous tutorial, we learnt how to tag AWS EC2 instances when they are created. We created an event rule in EventBridge to trigger a Lambda function to tag new EC2 instances with the name of the user that created it. That tutorial had step by step instructions on how to accomplish this via the AWS management console. This tutorial covers all the steps using CloudFormation templates for easy build and tear down and re-use in the future.
Steps
- Create a python file with the Lambda function to tag EC2 instances when they are created
- Upload the file to a S3 bucket
- Create the VPC stack
- Create the CloudTrail stack
- Create the Lambda deployment and EventBridge rules stack
- Create EC2 stack
- Validate the tags on the EC2 instances
- Clean up resources. Delete all the stacks above
Step 1: Create a python file with the Lambda function to tag EC2 instances when they are created
import json
import boto3
print('loading tagEC2 lambda function')
ec2 = boto3.client('ec2')
def lambda_handler(event, context):
#print(event)
#user name; please note that this may differ based on the structure of your IAM user
userName = event['detail']['userIdentity']['sessionContext']['sessionIssuer']['userName']
#instance id
instanceId = event['detail']['responseElements']['instancesSet']['items'][0]['instanceId']
try:
ec2.create_tags(
Resources=[
instanceId,
],
Tags=[
{
'Key': 'CreatedBy',
'Value': userName
},
]
)
print("completed executing tagEC2 lambda function")
except Exception as e:
print(e)
raise e
return
Step 2: Upload the file to a S3 bucket
To upload the newly created Lambda function to the S3 bucket, first convert it to a zip file as follows:
zip maghilda_tagResourceCreation.zip maghilda_tagResourceCreation.py
Step 3: Create the VPC stack
AWSTemplateFormatVersion: 2010-09-09
Description: Set up VPC template for test environment with one public subnet in one availabily zone.
Parameters:
EnvironmentName:
Description: An environment name that is prefixed to resource names
Type: String
Default: test
VpcCIDR:
Description: Please enter the IP range (CIDR notation) for this VPC
Type: String
Default: 10.0.0.0/16
PublicSubnetCIDR:
Description: Please enter the IP range (CIDR notation) for the public subnet in the one Availability Zone
Type: String
Default: 10.0.10.0/24
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Ref EnvironmentName
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Ref EnvironmentName
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref VPC
PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [ 0, !GetAZs '' ]
CidrBlock: !Ref PublicSubnetCIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Public Subnet (AZ)
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Public Routes
DefaultPublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet
NoIngressSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: "no-ingress-sg"
GroupDescription: "Security group with no ingress rule"
VpcId: !Ref VPC
Outputs:
VPC:
Description: A reference to the created VPC
Value: !Ref VPC
PublicSubnet:
Description: A reference to the public subnet in the Availability Zone
Value: !Ref PublicSubnet
NoIngressSecurityGroup:
Description: Security group with no ingress rule
Value: !Ref NoIngressSecurityGroup
Deploy the stack via the AWS CLI
aws cloudformation create-stack --stack-name testvpc --template-body file://vpc-template.yml
Step 4: Create the CloudTrail stack
EventBridge requires CloudTrail Trail to exist as it uses the CloudTrail API. To set up CloudTrail trail with S3, please review my post here.
Step 5: Create the Lambda deployment and EventBridge rules stack
AWSTemplateFormatVersion: 2010-09-09
Description: Set up EventBridge Trigger and Lambda function to tag EC2 resources
Resources:
# BEGIN MANAGED POLICY
ManagedPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: CloudWatchLogs #similar to AWSLambdaBasicExecutionRole
Effect: Allow
Action:
- logs:PutLogEvents
- logs:CreateLogGroup
- logs:CreateLogStream
Resource: "arn:aws:logs:*:*:*"
- Sid: CreateTags
Effect: Allow
Action: ec2:CreateTags
Resource: "*"
# BEGIN LAMBDA IAM RESOURCES
EventBridgeLambdaRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- !Ref ManagedPolicy
# Deploy Lambda function
myLambdaFunction:
Type: AWS::Lambda::Function
Properties:
Runtime: python3.11
FunctionName: maghilda_tagResourceCreation
Role: !GetAtt EventBridgeLambdaRole.Arn
Handler: maghilda_tagResourceCreation.lambda_handler #filename.handler
Code:
S3Bucket: m-lambdafunctions #s3 bucket name
S3Key: maghilda_tagResourceCreation.zip #file uploaded to the s3 bucket
Description: tagEC2 lambda function
TracingConfig:
Mode: Active
#Provide Lambda permissions to execute Events
LambdaPerms:
Type: AWS::Lambda::Permission
Properties:
Action: 'lambda:InvokeFunction'
FunctionName: !Ref myLambdaFunction
Principal: events.amazonaws.com
SourceAccount: !Ref AWS::AccountId
SourceArn: !GetAtt EventBridgeTrigger.Arn
#Set up EventBridge Rule
EventBridgeTrigger:
Type: AWS::Events::Rule
Properties:
Description: 'EventTrigger on EC2'
State: 'ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS'
EventPattern:
source:
- 'aws.ec2'
detail-type:
- 'AWS API Call via CloudTrail'
detail: #the service generating the event determines the content of this field.
eventSource:
- 'ec2.amazonaws.com'
eventName:
- 'RunInstances'
Targets:
- Arn: !GetAtt myLambdaFunction.Arn
Id: 'LambdaFunction'
Outputs :
LambdaFunctionName:
Value: !Ref myLambdaFunction
LambdaRoleArn:
Value: !GetAtt EventBridgeLambdaRole.Arn
EventRuleArn:
Value: !GetAtt EventBridgeTrigger.Arn
Deploy the stack via the AWS CLI
aws cloudformation create-stack --stack-name testlambda --capabilities CAPABILITY_NAMED_IAM --template-body file://TagEC2Lambda-template.yml
Step 6: Create EC2 stack
AWSTemplateFormatVersion: 2010-09-09
Description: Create EC2 instance. The security group allows only SSH traffic
Parameters:
InstanceType:
Description: Instance Type to use
Type: String
Default: t2.micro
AMI:
Description: AMI to use; Amazon Linux 2023 AMI
Type: String
Default: 'ami-0e731c8a588258d0d'
Key:
Description: Name of an existing EC2 KeyPair to enable SSH access to the instance
Type: AWS::EC2::KeyPair::KeyName
Default: 'app-key-pair' #change to use your key-pair name
SSHLocation:
Description: The IP address range that can be used to SSH to the EC2 instances
Type: String
Default: 18.206.107.24/29 #using EC2 Instance Connect IP address range to connect to an instance
VPC:
Description: The ID of the VPC for the security group
Type: String
Default: 'vpc-0ac7ad98b5f0b664b'
SubnetID:
Description: The ID of the subnet to launch the instance into.
Type: String
Default: 'subnet-0f7be13cfe8e7f030'
Resources:
MyEC2Instance:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref AMI
KeyName: !Ref Key
InstanceType: !Ref InstanceType
NetworkInterfaces:
- AssociatePublicIpAddress: "true"
DeviceIndex: "0"
GroupSet: [!Ref SSHSecurityGroup]
SubnetId: !Ref SubnetID
SSHSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: "ssh-ingress-sg"
GroupDescription: "Enable SSH access via port 22"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: !Ref SSHLocation
VpcId: !Ref VPC
Outputs:
InstanceID:
Description: InstanceId of the newly created EC2 instance
Value: !Ref MyEC2Instance
AZ:
Description: Availability Zone of the newly created EC2 instance
Value: !GetAtt [MyEC2Instance, AvailabilityZone]
PublicDNS:
Description: Public DNSName of the newly created EC2 instance
Value: !GetAtt [MyEC2Instance, PublicDnsName]
PublicIP:
Description: Public IP address of the newly created EC2 instance
Value: !GetAtt [MyEC2Instance, PublicIp]
SSHSecurityGroup:
Description: Security group with SSH access only
Value: !Ref SSHSecurityGroup
Deploy the stack via the AWS CLI
aws cloudformation create-stack --stack-name testEC2 --template-body file://EC2-template.yml
You should have the four CloudFormation Stacks on CloudFormation as follows:
Step 7: Validate the tags on the EC2 instances
Step 8: Clean up resources. Delete all the stacks above
Delete Lambda function deployment and EventBridge Rules
aws cloudformation delete-stack --stack-name testlambda
Delete the EC2 instances
aws cloudformation delete-stack --stack-name testEC2
Delete the VPC and associated subnet and security groups
aws cloudformation delete-stack --stack-name testvpc
Delete the CloudTrail trail
aws cloudformation delete-stack --stack-name testcreatetrail
This is the end of the tutorial. Hope it was helpful to you.
References
- https://docs.aws.amazon.com/codebuild/latest/userguide/cloudformation-vpc-template.html
- https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getavailabilityzones.html
- https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-ec2-sg.html
- https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-connect-prerequisites.html
- https://docs.aws.amazon.com/vpc/latest/userguide/aws-ip-ranges.html
- https://ip-ranges.amazonaws.com/ip-ranges.json
- https://github.com/awslabs/aws-cloudformation-templates/blob/master/aws/services/EC2/EC2InstanceWithSecurityGroupSample.yaml
- https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-events.html
- https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-rule.html
- https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-events-structure.html