Using Terraform and AWS CloudFormation to Enforce Your AWS Tags
Tony Chan
Posted on August 27, 2020
Once you have adopted an AWS tagging strategy, you’ll need to make sure that all your existing AWS resources and any new ones you create abide by it. Consistency is the key - if you don’t proactively enforce your AWS tagging strategy, you’ll always be playing catch up and chasing down team members to make sure they add the right tags to their resources.
While you can apply AWS tags to your resources manually using the AWS CLI or AWS Tag Editor, you’ll probably find this cumbersome and error-prone at scale. A better approach is to automatically apply AWS tags to your resources and use rules to enforce their consistent usage.
Depending on the tool you use to maintain your infrastructure on AWS, your method of proactively enforcing AWS tags on new resources may vary. In this guide, I’ll highlight two tools: Terraform and AWS CloudFormation. You’ll see how to use each to create and update AWS cost allocation tags on your resources and then enforce the proper use of specific tags for new resources. By proactively enforcing your AWS tagging strategy, you’ll minimize your time spent auditing and correcting improper AWS tags and force developers to learn best AWS tagging best practices for your environment.
Using Terraform for AWS Tags
The first infrastructure management tool I’ll cover is Terraform. Terraform works across a variety of cloud hosting providers to help you provision and maintain your AWS resources. With Terraform, you can define your servers, databases, and networks in code and apply your changes programmatically to your AWS account.
If you’re new to Terraform, they have a well-documented Getting Started guide and several AWS template examples on GitHub. In this section, I’ll show you some snippets from a demo Terraform project and module that is available on GitHub. You'll learn the following in this Terraform AWS tags:
- Tag a New AWS EC2 Instance with Terraform
- Using Terraform to Update Existing AWS Tags
- Enforce AWS Tags with Terraform
Tag a New AWS EC2 Instance with Terraform
If you want to deploy an EC2 instance with AWS Tags using Terraform, your configuration might include something like this:
resource "aws_instance" "cart" {
connection {
type = "ssh"
user = "ubuntu"
host = self.public_ip
private_key = file(var.private_key_path)
}
instance_type = "t2.micro"
ami = var.aws_amis[var.aws_region]
key_name = aws_key_pair.auth.key_name
vpc_security_group_ids = [aws_security_group.default.id]
subnet_id = aws_subnet.default.id
provisioner "remote-exec" {
inline = [
"sudo apt-get -y update",
"sudo apt-get -y install nginx",
"sudo service nginx start",
]
}
tags = {
contact = "j-mark"
env = "dev"
service = "cart"
}
}
The above example includes three AWS cost allocation tags: contact
, env
, and service
with values described as strings. When you apply this configuration. Terraform will connect to AWS and deploy an EC2 instance having the AWS tags you specified.
Using Terraform to Update Existing AWS Tags
Terraform makes it easy to update already existing resources with AWS tags in reversible and consistent ways. If you’re using AWS tags to keep track of a resource’s contact (e.g.: j-mark
in the above example), you’re likely to need to update the AWS tag when the team member leaves or changes roles.
To update the AWS tags on your resource, simply update the corresponding tags in your Terraform configuration. The new tags will overwrite any previous tags assigned to the resource, including tags added outside of Terraform.
For example, to change the contact
cost allocation tag on the EC2 instance above, you might update the tags
block above with the following:
tags = {
contact = "l-duke"
env = "dev"
service = "cart"
}
When you apply this configuration, the AWS tags will be automatically updated in the AWS console:
If you keep your Terraform configuration files in version control - which is probably a good idea - you will be able to see how tags have changed over time. You can also review changes using the same code review process that your application code goes through to help you catch mistakes in the execution of your tagging strategy.
Enforce AWS Tags with Terraform
As your infrastructure grows, a code review process likely won’t be enough to prevent improper AWS tagging. Fortunately, you can enforce AWS tag names and values using variables and custom validation rules in Terraform.
In the examples above, the tags
list was hard-coded into the EC2 instance definition. A more scalable pattern would be to break your EC2 instance template into its own module and use a tags
variable. You can then write a custom validation rule to check that the tags comply with your strategy.
For example, if you want to check that:
- The user specifies at least one tag
- The
contact
tag is eitherj-mark
orl-duke
- The
env
tag is set - The
service
tag is eithercart
orsearch
You might create a module with a variable specified like this:
variable "tags" {
description = "The tags for this resource."
validation {
condition = length(var.tags) > 0 && contains(["j-mark", "l-duke"], var.tags.contact) && var.tags.env != null && contains(["cart", "search", "cart:search"], var.tags.service)
error_message = "Invalid resource tags applied."
}
}
Now when you run terraform plan
with a missing or invalid tag, you’ll get an error:
Error: Invalid value for variable
...
Invalid resource tags applied.
Your rules can be as complex as Terraform’s Configuration Language allows, so functions like regex()
, substr()
, and distinct()
are all available. That said, there are some caveats to this approach.
First, custom variable validation is an experimental feature in Terraform. Experimental features are subject to change, meaning that you might need to pay attention to Terraform update mores closely. To enable variable_validation
, add the following to your terraform
block:
terraform {
experiments = [variable_validation]
}
Second, Terraform’s variable validation only happens during the terraform plan
phase of your infrastructure’s lifecycle. It can’t prevent users from accidentally changing your tags directly in the AWS console, and it’s only as good as the validation rules you write. If you start using a new resource but forget to add validation rules, you might end up with lots of resources that don’t adhere to your tagging strategy.
Another option for paid Terraform Cloud customers is Sentinel , which allows you to create custom policies for your resources. I won’t cover this method here, but Terraform has created an example policy to show you how to enforce mandatory AWS tags.
Using AWS CloudFormation for Tags
Similar to Terraform, AWS CloudFormation lets you provision AWS resources based on configuration files. Unlike Terraform, CloudFormation is part of Amazon’s offerings, so it won’t necessarily help you if you want to use another infrastructure provider. The approach to tagging your resources in CloudFormation is similar to that used by Terraform, but as you’ll see, the configuration format is different.
If you’re new to AWS CloudFormation, Amazon’s official walkthrough will help you get started deploying some basic templates. In this section, I’ll show you some snippets from a demo AWS CloudFormation template which is also available on GitHub. You'll learn the following in this Terraform AWS tags section:
- AWS CloudFormation Template to Deploy Tags
- Using CloudFormation to Update AWS Tags
- CloudFormation Template to Enforce AWS Tags
AWS CloudFormation Template to Deploy Tags
AWS CloudFormation is designed to make it easy to create AWS resources with a single template file. Using a CloudFormation template, every resource that can be deployed with an AWS tag.
For example, to create a new EC2 instance with the same three AWS tags used in the Terraform example above, add an array of Tags
to the resource’s Properties
block:
"Resources" : {
"WebServerInstance": {
"Type": "AWS::EC2::Instance",
"Metadata" : {...},
"Properties": {
"Tags" : [
{
"Key" : "contact",
"Value" : "j-mark"
},
{
"Key" : "env",
"Value" : "dev"
},
{
"Key" : "service",
"Value" : "cart"
}
],
...
}
},
...
},
Using AWS CLI, you can deploy this CloudFormation template as a new stack. This will ensure your template is valid and create the specified resources with their tags on AWS:
aws cloudformation create-stack --template-body file://path/to/your/template.json --stack-name=<YOUR_STACK_NAME>
If you have lots of similar resources in your template, you can deploy AWS tags to all the resources in the stack at once using the --tags
flag with the create-stack
or update-stack
commands:
# Creating a stack with tags
aws cloudformation create-stack --template-body file://path/to/your/template.json --stack-name=<YOUR_STACK_NAME> --tags="Key=env,Value=dev"
# Updating a stack with tags
aws cloudformation update-stack --template-body file://path/to/your/template.json --stack-name=<YOUR_STACK_NAME> --tags="Key=env,Value=dev"
Using CloudFormation to Update AWS Tags
If you want to change the contact on your EC2 instance created above, simply change the Tags
section of your template file and use the [update-stack](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudformation/update-stack.html)
command to deploy your changes.
"Tags" : [
{
"Key" : "contact",
"Value" : "l-duke"
},
...
],
AWS CloudFormation behaves the same way that Terraform does when you update tags outside your template file. Any tags set manually will be overridden by the update-stack
command, so be sure that everyone on your team deploy's tags through CloudFormation.
CloudFormation Template to Enforce AWS Tags
AWS provides Organization Tag Policies and Config Managed Rules to help you find improperly tagged resources, but neither of these tools prevents you from creating resources with missing or invalid tags. One way to proactively enforce your tagging strategy is by using the CloudFormation linter.
cfn-lint
is a command-line tool that will make sure your AWS CloudFormation template is correctly formatted. It checks the formatting of your JSON or YAML file, proper typing of your inputs, and a few hundred other best practices. While the presence of specific tags isn’t checked by default, you can write a custom rule to do so in Python.
For example, if you want to ensure that your CloudFormation web servers follow the same rules as the Terraform example above and have:
- At least one AWS tag
- The
contact
tag set to eitherj-mark
orl-duke
- The
env
tag set - The
service
tag set tocart
orsearch
You can create a new rule called TagsRequired.py
:
from cfnlint.rules import CloudFormationLintRule
from cfnlint.rules import RuleMatch
class TagsRequired(CloudFormationLintRule):
id = 'E9000'
shortdesc = 'Tags are properly set'
description = 'Check all Tag rules for WebServerInstaces'
def match(self, cfn):
matches = []
approved_contacts = ['j-mark', 'l-duke']
valid_services = ['cart', 'search']
web_servers = [x for x in cfn.search_deep_keys('WebServerInstance') if x[0] == 'Resources']
for web_server in web_servers:
tags = web_server[-1]['Properties']['Tags']
if not tags:
message = "All resources must have at least one tag"
matches.append(RuleMatch(web_server, message.format()))
if not next((x for x in tags if x.get('Key') == 'env'), None):
message = "All resources must have an 'env' tag"
matches.append(RuleMatch(web_server, message.format()))
for tag in tags:
if tag.get('Key') == 'contact' and tag.get('Value') not in approved_contacts:
message = "The contact must be an approved contact"
matches.append(RuleMatch(web_server, message.format()))
if tag.get('Key') == 'service' and tag.get('Value') not in valid_services:
message = "The service must be a valid service"
matches.append(RuleMatch(web_server, message.format()))
return matches
When you run cfn-lint
, include your custom rule:
cfn-lint template.json -a ./path/to/custom/rules
If your CloudFormation template is missing any tags, you’ll see an error:
E9000 Missing Tag contact at Resources/WebServerInstance/Properties/Tags
template.json:169:9
Using linting to validate your AWS CloudFormation rules is a great way to enforce your AWS tags proactively. If you’re storing your CloudFormation templates in version control, you can run cfn-lint
using pre-commit hooks or by making it part of your continuous integration workflow.
Because these rules are written in Python, they can be as complex as you need them to be, but they have drawbacks as well. Like Terraform’s custom variable validation, linting rules won’t tell you about existing problems in resources that aren’t managed by CloudFormation, so they work best when combined with a reactive tag audit and adjustment strategy.
Conclusion
Properly tagged resources will help you predict and control your costs, but your tagging strategy can’t just be reactive. Having proactively enforced patterns will require an up-front investment, but will save you time and money in the long-run.
Once you’ve adopted a tagging strategy and proactive enforcement method, the last piece of the puzzle is catching up when you fall behind. In the final part of this guide, you’ll see how to audit and find mistagged resources to ensure your tagging strategy continues to succeed in the future.
If you’re interested in getting help with your tagging, reach out to our CTO at francois@cloudforecast.io to receive a free tagging compliance report.
This was originally posted on our blog on 8/27/2020: https://www.cloudforecast.io/blog/aws-tagging-best-practices-guide-part-2/
Posted on August 27, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.