Enforcing AWS (Amazon Web Services) Tagging with OPA and Atlantis

Written by
Published on
September 21, 2024
Written by
This is some text inside of a div block.
This is some text inside of a div block.

Introduction

Putting tags on your resources is often discussed, but implementing a tagging strategy can be challenging. The strategy’s primary focus should include why you would like to have tags in the first place and then give them appropriate keys and values. You could create a tagging strategy for cost allocation and financial management, data security and access control or operational use cases like workload lifecycles and patching. Or all of them all at once. Tags on all applicable resources will help you map the cloud infrastructure and get a better overview of what exists in your cloud.

Atlantis is a well-known (and used) open-source tool for reviewing and applying code. It can also establish guidelines and policies within your GitHub organization. Open Policy Agent (OPA) is an open-source policy engine that helps you enforce policies throughout your organization.

The problem statement

As of today, AWS offers a solution called Tag Policies that can help you standardize tags on all available resources. This means you can create a template with rulesets of how tags should be written with specific keys and values. While this is a great start, Tag Policies does not stop the creation of a resource with no tags at all. So, even if you have a template with allowed tags, resources can still be created without any. This results in an unintended loophole for many developers since tags are unfortunately forgotten many times while creating new resources.

Since the AWS Tag Policies do not fulfil every aspect of a tagging strategy, we had to look elsewhere to clog that loophole of creating resources with no tags at all.

The solution

With OPA and Atlantis, you can build a tagging strategy that also enforces tags on newly created resources. OPA policies are written in a language called Rego, which has its own documentation and examples for inspiration.

Firstly, you must create a new folder in the Atlantis repository called “policies”. This is where you can create multiple policies you want to enforce in your organization, like tagging. Your policy could have any name if it ends with .rego.

There are many ways to write tagging policies, but in this post, we will share the code that worked for us:

package com.example.opa.tagging

import rego.v1

mandatory_tags := {"Repository", "Service", "Owner"}

deny contains msg if {
 # Loop over all resources with changes
 some x in input.resource_changes

 # We only care about the resource with tags object
 not is_null(x.change.after.tags)

 # Collect the union of tags and tags_all
 t1 := object.get(x, ["change", "after", "tags"], {})
 t2 := object.get(x, ["change", "after", "tags_all"], {})
 t3 := object.union_n([t1, t2])

 # Get the missing tags
 missing := [tag | is_null(object.get(t3, mandatory_tags[i], null)); tag := mandatory_tags[i]]

 # Only continue if missing tags array is empty
 count(missing) != 0

 msg = sprintf("Resource: `%s` doesn't have the required tags, `%s`", [x.address, missing])
}

Package and import:

  • Package defines the package name
  • Import is introduced for when OPA v1.0 will be released. This release will introduce breaking changes to the Rego language, so adding the import rego.v1 is a way to get one step ahead.

Deny rule:

  • deny contains message if {: The policy will deny the apply with a deny message if any of your code violates this policy, i.e. not containing any of the mandatory tags.

Logic of the rule:

  • some x in input.resource_changes: iterates over all the resources in input.resource_changes, where x represents each resource change.
  • not is_null(x.change.after.tags): This checks if the resource has any tags field after the change. If the tags field is null (i.e., the resource does not support tags), this rule will not apply to that resource, and it will be skipped.
  • t1:=object.get(x, [“change”, “after”, “tags”], {}): Retrieves the tags object from the resource’s “after” state (i.e., the state after the change).
  • t2 := object.get(x, ["change", "after", "tags_all"]): Retrieves all tags associated with the resource.
  • t3 := object.union_n([t1, t2]): Combines the tags from both tags and tags_all into a single set of tags.
  • missing := [tag| is_null(object.get(t3, mandatory_tags[i], null)); tag := mandatory_tags[i]]: This line checks which of the mandatory tags are missing from t3. If a tag from mandatory_tags is not found in t3, it is added to the missing list.
  • count(missing) !=0: If the missing list is not empty (meaning one or more mandatory tags are missing), the denial condition is satisfied.

Deny Message:

  • msg = sprintf("Resource: %s doesn't have the required tags,%s", [x.address, missing]): If the condition for denial is met, this message is generated, indicating which resource is missing the required tags and listing the missing tags.

For testing and writing your Rego code, Rego has a testing environment called The Rego Playground, which will give you error messages about your code if it’s not working. You can also test your policy locally by creating a test file in the same folder as your policies, which is excellent for debugging. This is an example of a test file we used to test our policy:

package com.example.opa.tagging_test

import data.com.example.opa.tagging as t
import rego.v1

test_deny_null_tags_denied if {
 t.deny with input as {"resource_changes": [{
  "address": "module.s3_bucket.aws_s3_bucket.acme",
  "change": {"after": {"tags": null}},
 }]}
}

test_deny_missing_tags_denied if {
 t.deny with input as {"resource_changes": [{
  "address": "module.s3_bucket.aws_s3_bucket.acme",
  "change": {"after": {"tags": {"Repository": "acme"}}},
 }]}
}

test_deny_combined_missing_tags_denied if {
 t.deny with input as {"resource_changes": [{
  "address": "module.s3_bucket.aws_s3_bucket.acme",
  "change": {"after": {"tags": {"Repository": "acme"}, "tags_all": {"Service": "acme", "Owner": "acme"}}},
 }]}
}

We chose to test our tagging policy by simply trying to create a new S3 bucket with the wrong mandatory tags: “Repository”: “acme”, “Service”: “acme”, and “Owner”: “acme”.

To see if the policy is correct, run the command: opa test . -v, in the same directory containing the files. The output should look something like this:

policies/tagging_test.rego:
data.com.example.opa.tagging_test.test_deny_null_tags_denied: PASS(4.475ms)
data.com.example.opa.tagging_test.test_deny_missing_tags_denied: PASS(652.166µs)
data.com.example.opa.tagging_test.test_deny_combined_missing_tags_denied: PASS(1.735458ms)
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -PASS: 3/3

Conclusion

Taking advantage of OPA and Atlantis when implementing a tagging strategy will overcome the obstacle of forgetting to add tags to new resources when you scale in AWS. While the AWS Tag Policy is a great tool, the major loophole it has is that resources can still be created without any tags at all, even though you’ve spent time implementing a tagging template. OPA and Atlantis will ensure that this will not happen again, so you and other developers can work on the development and enhancement to make a great cloud environment instead.

References
1. https://www.runatlantis.io/
2. openpolicyagent.org
3. https://www.conftest.dev/

Newsletter

Subscribe to receive the latest blog posts to your inbox every week.

By subscribing you agree to with our Terms of Services.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.