Who’s been eating my porridge? — Tracking the origin of infrastructure changes across multiple repos.

Mike Hodgkins
Engineering at FundApps
6 min readMay 12, 2022

--

a.k.a. making Yor work with Terraform caller and child modules.

We’re on a mission at FundApps to empower our developers to take ownership of the Infrastructure their applications depend on. We’ve made some great progress here but we have a LOT of Terraform and we often hear our developers say “I don’t know where to find the Terraform that created a particular resource”. This is a challenge we can solve with metadata, in most cases AWS tags.

In this post we’ll talk about:

  • How to add git repo based metadata to AWS resources using Yor
  • How to add the same metadata to Terraform modules and their caller module

Automated tagging with Yor

Yor is an open-source tool from Bridgecrew. Yor provides functionality for custom tagging rules but the biggest benefit for us comes out of the box — git tags. No, not those git tags, AWS resource tags that contain metadata extracted from git.

Yor can be used as a cli tool, or automated with Github Actions or pre-commit. This is great for getting those tags onto all of our AWS resources, here’s one I made earlier:

resource "aws_kms_key" "example_key" {
description = "blah blah blah, I am a key"
policy = data.aws_iam_policy_document.key.json
enable_key_rotation = true
tags = {
git_commit = "COMMITHASH"
git_file = "main.tf"
git_last_modified_at = "2022-05-06 13:26:30"
git_last_modified_by = "not.my.real@email.com"
git_modifiers = "mike.hodgkins"
git_repo = "terraform-aws-kms-key"
yor_trace = "7f03727b-256d-44cd-868c-492379820ee5"
}
}

Module tagging — where things get a little trickier

As some of you might have noticed, the git_repo tag of my example above is terraform-aws-kms-key, which is the standard naming convention for a repo containing a terraform module. This means the tags yor creates are based on the metadata from that git repo. This doesn’t really help us at FundApps. We have a multitude of repos, just like this one, that contain general purpose Terraform modules. We also call our modules in many different places and often the information we’re looking for is “who called this module and where from?”. This means we also want Yor to be able to tag resources with the git metadata from our caller modules.

Keeping tag keys unique

Yor supports passing tags to a module invocation as well as directly to resources, which is a great start for us. There are a few caveats, namely:

  • Yor only supports this on remote modules. So, if you have a modules directory in the same repo as your caller module this doesn’t apply. The expectation is that you simply run Yor against both caller modules and your modules directory.
  • Yor only supports passing tags to a module using a tags parameter. So, if you use any other parameter to pass tags into your modules, you might have some tinkering to do.

A module invocation with Yor tags will look something like this:

module "remote_module" {
source = "my-remote-registry/kms-key/aws"
tags = {
yor_trace = "912066a1-31a3-4a08-911b-0b06d9eac64e"
git_repo = "example"
git_file = "keys.tf"
git_commit = "COMMITHASH"
git_modifiers = "mike.hodgkins"
git_last_modified_at = "2022-05-06 14:19:10"
git_last_modified_by = "not.my.real@email.com"
}
}

So, we know Yor can tag resources, and we know it can pass tags to modules. So, how do we get the tags from both the caller and called modules onto our resources?

Terraform merge

Terraform has a bunch of handy functions available to help us with this. The most important of which in this case is merge(). The merge() function simply merges 2 maps together. The syntax is merge(var.map1, var.map2) and this function operates in a “last write wins” fashion. This means if you have duplicate keys (such as yor_trace) in both of your maps only the second value passed var.map2["yor_trace"] would be present post-merge.

Yor merge

Yor also has functionality to magically add some terraform merge() in certain scenarios. Let’s say I have this block of Terraform:

resource "aws_kms_key" "example_key" {
description = "blah blah blah, I am a key"
policy = data.aws_iam_policy_document.key.json
enable_key_rotation = true
tags = var.tags
}

When I run Yor, it turns into this:

resource "aws_kms_key" "example_key" {
description = "blah blah blah, I am a key"
policy = data.aws_iam_policy_document.key.json
enable_key_rotation = true
tags = merge(var.tags, {
git_commit = "COMMITHASH"
git_file = "main.tf"
git_last_modified_at = "2022-05-06 13:26:30"
git_last_modified_by = "not.my.real@email.com"
git_modifiers = "mike.hodgkins"
git_repo = "terraform-aws-kms-key"
yor_trace = "7f03727b-256d-44cd-868c-492379820ee5"
})
}

Note how tags = var.tags became tags = merge(var.tags, {...

So, that’s got us halfway there, but we still have the last-write-wins problem to deal with. In the above example if that file is in a module that has been called with a set of Yor tags passed in, the only tags that will appear in our terraform plan (and therefore the AWS console) are the ones from the module source code (because that was the second map passed to the merge() function).

Remap Yor

So, we need to make the keys that we pass into the caller module distinct from the ones in the called module. We can do this with a terraform local, a for loop and a couple of nested replace functions:

locals {
remap_yor = { for k, v in var.tags : replace(replace(k, "yor_", "yor_caller_"), "git_", "git_caller_") => v }
}

This means that anything we pass into var.tags from our caller module with either the git_ or yor_ prefixes will be present in local.remap_yor with it’s key prefixes updated to git_caller_ and yor_caller_ respectively.

Putting it together

Making use of our new local is pretty easy. We simply need to change any resources that use var.tags to use local.remap_yor. So, our example resource would change from:

resource "aws_kms_key" "example_key" {
description = "blah blah blah, I am a key"
policy = data.aws_iam_policy_document.key.json
enable_key_rotation = true
tags = var.tags
}

to:

resource "aws_kms_key" "example_key" {
description = "blah blah blah, I am a key"
policy = data.aws_iam_policy_document.key.json
enable_key_rotation = true
tags = local.remap_yor
}

and when we run Yor it becomes:

resource "aws_kms_key" "example_key" {
description = "blah blah blah, I am a key"
policy = data.aws_iam_policy_document.key.json
enable_key_rotation = true
tags = merge(local.remap_yor, {
git_commit = "COMMITHASH"
git_file = "main.tf"
git_last_modified_at = "2022-05-06 13:26:30"
git_last_modified_by = "not.my.real@email.com"
git_modifiers = "mike.hodgkins"
git_repo = "terraform-aws-kms-key"
yor_trace = "7f03727b-256d-44cd-868c-492379820ee5"
})
}

Our caller module doesn’t change. When we run terraform plan the output is this:

# module.this.aws_kms_key.primary_region_key will be created
+ resource "aws_kms_key" "primary_region_key" {
+ arn = (known after apply)
+ bypass_policy_lockout_safety_check = false
+ customer_master_key_spec = "SYMMETRIC_DEFAULT"
+ description = "blah blah blah, I am a key"
+ enable_key_rotation = true
+ id = (known after apply)
+ is_enabled = true
+ key_id = (known after apply)
+ key_usage = "ENCRYPT_DECRYPT"
+ multi_region = (known after apply)
+ policy = (known after apply)
+ tags = {
+ "git_commit" = "COMMITHASH"
+ "git_caller_commit" = "COMMITHASH"
+ "git_file" = "main.tf"
+ "git_caller_file" = "keys.tf"
+ "git_last_modified_at" = "2022-05-06 13:26:30"
+ "git_caller_last_modified_at" = "2022-05-06 14:19:10"
+ "git_last_modified_by" = "not.my.real@email.com"
+ "git_caller_last_modified_by" = "not.my.real@email.com"
+ "git_modifiers" = "mike.hodgkins"
+ "git_caller_modifiers" = "mike.hodgkins"
+ "git_repo" = "terraform-aws-kms-key"
+ "git_caller_repo" = "example"
+ "yor_caller_trace" = "912066a1-31a3-4a08-911b-0b06d9eac64e"
+ "yor_trace" = "7f03727b-256d-44cd-868c-492379820ee5"
}
}

As you can see, the expected tags from both the module and it’s caller are present and correct. We now have the ability to track down the origin of module source code changes as well as changes to the caller module.

A special thanks to James Woolfenden from Bridgecrew for his help with this.

--

--

Helping FundApps devs #automate-all-the-things by automating all of their things.