How to Tag Instances Consistently (and without user interaction)

BLOG ARTICLE

Take the challenge out of remembering how to tag your instances by letting a few AWS Services do it for you.

Part 1 - getting it working

AWS provides any number of resources that recommend tagging and they offer different strategies and guidance. Two such cases in point are:

https://d1.awsstatic.com/whitepapers/aws-tagging-best-practices.pdf
https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html

Essentially it can be distilled into one very simple principle; you should be tagging all of your instances. What to actually tag them with is a debate for another time, but in this post I’ll be showing you how you can leverage a couple of services and some pretty simple code to get you on your way.

The challenge is always remembering what rules have we defined, what are the specific tag(s) and values for each instance, and then for each environment? If I’m in a non-prod environment and possibly spinning up an instance just to test something on, do I really need to tag? Especially it if it’s only going to be around for a short period of time? 

The answer to these questions is, yes. 

This post exists to share with you one way I’ve found to make that a little easier.

Overview

This simple solution can easily be extended to any number of different services with a minimal amount of knowledge. For simplicity I’ll pick one of everyone’s favourites, EC2, as the one to demonstrate with. 

In this scenario we’ll automatically tag any freshly created EC2 instance with a creator and creation_date tags. Once you see how easy this is, it will be clear that the same process can be extended for use in your organisation.

The services we’re going to use are AWS EventBridge and Lambda.

Yes, auto-tagging can be all yours by simply implementing two services.

process

Lambda

First of all let’s create a lambda to do the work; so fire up the console and create a new Lambda function

Drop in the following code for our lambda:

import json

import boto3

import logging

import time

from datetime import date

logger = logging.getLogger()

logger.setLevel(logging.INFO)

ec2_source = 'aws.ec2'

###############################################################

# default entry point

def lambda_handler(event, context):

    logger.info('Received event: [{}]'.format(json.dumps(event, indent=2, default=str)))

    

    event_source = event['source']

    file_contents = "ResourceType, ResourceId, OtherDetails\n"

    if event_source == ec2_source:

        file_contents = file_contents + ec2_tag(event,file_contents)

    logger.info('file_contents: [{}]'.format(str(file_contents)))

############################################################### 

# tag the ec2 instances

def ec2_tag(event, file_contents):

    detail = event['detail']

    myInstance = detail['responseElements']['instancesSet']['items'][0]['instanceId']

    eventInfo = extract_event_details(event)

    try:

        file_contents = file_contents + "EC2," + myInstance + ", Tagged\n"

        create_tags('ec2', myInstance, eventInfo)

        return file_contents

    except Exception as e:

        logger.error('Something went wrong: [{}]'.format(str(e)))

        

    return file_contents

    

###############################################################

# extract details from event

def extract_event_details(event):

    detail = event['detail']

    eventname = detail['eventName']

    eventtime = detail['eventTime']

    principal = detail['userIdentity']['principalId']

    userType = detail['userIdentity']['type']

    if userType == 'IAMUser':

        user = detail['userIdentity']['userName']

    else:

        user = principal.split(':')[1]

    eventInfo = {

        'creator': user, 

        'creation_date': eventtime

    }

    return eventInfo

###############################################################

def create_tags(service, instanceId, eventInfo):    

    # create the client to service the call

    client = boto3.client(service)

    

    if (service == 'ec2'):

        # this call will overwrite existing tags, you could check and decide based on the

        # following

        # response = client.describe_tags(

        # Filters=[

        # {

       #            'Name': 'resource-id',

       #     'Values': [

       #         instanceId

       #     ],

       # },

       # ],

       #     )

        client.create_tags(

            Resources=[

                instanceId,

            ],

            Tags=[

                {

                    'Key': 'creator',

                    'Value': eventInfo['creator']

                },

                {

                    'Key': 'creation_date',

                    'Value': eventInfo['creation_date']

                }

            ]

        )

        

###############################################################

IAM

Once the code is in place we need to update our Lambda execution role to have ec2:CreateTags permission to tag the newly created instance.

Click on Permissions at the top of the Lambda function page to find the execution role and select to open in the IAM Console.

You’ll need to update the policy to reflect the new permission to create tags:

{

    "Version": "2012-10-17",

    "Statement": [

        {

            "Effect": "Allow",

            "Action": "ec2:CreateTags",

            "Resource": "*"

        },

        {

            "Effect": "Allow",

            "Action": [

                "logs:CreateLogStream",

                "logs:PutLogEvents"

            ],

            "Resource": "arn:aws:logs:ap-southeast-2:111111111111:log-group:/aws/lambda/auto-tagging-lambda:*"

        },

        {

            "Effect": "Allow",

            "Action": "logs:CreateLogGroup",

            "Resource": "arn:aws:logs:ap-southeast-2:111111111111:*"

        }

    ]

Event Bridge

Now that we have the code in place, and our updated permissions, we need to set up the mechanism to fire the function. Here we use AWS EventBridge which delivers a stream of real-time data from event sources to capture our EC2 creation event and pass it along to our lambda function.

So let’s open up our AWS EventBridge console and configure it as follows:

  1. Create a new rule
  2. Define the event we want to capture


  3. Point the event to our target lambda


  4. Create the event
Testing / Validation

Now we have the event and the lambda function we want to test to see if it’s connected and firing correctly.

Once we have created a new EC2 instance we can open the CloudWatch logs associated with our lambda function and see the event has come through and inspect it.

Based on our log file we can see that everything has worked, so now we can open our EC2 instance and confirm the tag has been applied to the instance identified.

These steps should be easily extensible for anyone wishing to start tagging instances in their AWS accounts. Simply, identify the services you use, create the appropriate AWS EventBridge events, add the method(s) to the lambda and you’re on your way. One thing to be wary of is that each AWS service call the ‘createTags’ with slightly different syntax.

Eg. 

For EC2 it’s: 

if (service == 'ec2'):

         client.create_tags(

             Resources=[

                 instanceId,

             ],

            Tags=[

                 {

'Key': 'creator',

            'Value': eventInfo['creator']

},

            {

             'Key': 'creation_date',

                     'Value': eventInfo['creation_date']

}

            ]

         )

For RDS it’s

if (service == 'rds'):

         client = client.add_tags_to_resource(

             ResourceName=instanceId,

             Tags=[

                 {

                     'Key': 'creator',

                     'Value': eventInfo['creator']

},

{

                     'Key': 'creation_date',

                     'Value': eventInfo['creation_date']

                 }

             ]

         )

Summary

This approach will let you get started with automatic tagging of your resources – the next steps are to wire this into a CI/CD pipeline to ensure this code is easily updatable and extended to any additional accounts you may have.