homeposts rss services talks gpg

Enforce terraform managed AWS tags

2022-10-19

OPA

You may or may not have heard of Open Policy Agent. Basically it’s a tool for crafting policies. It’s generalized enough that you can use it for most anything, such as ensuring that any resources created via terraform have required tags.

Rego

OPA uses Rego1, which is in the familiy of logic programming languages. Of those, Prolog is probably the one with most recognition.

If you’ve never dabbled in logic languages I highly recommend going through one of swi prolog intros or pragprog has a decent intro to gnu prolog in seven languages in seven weeks. Unfortunately prolog has several dialects that are kinda-sorta the same, but differ enough to be a thing. Much like lisp.

Rego’s syntax is not the same as prolog. But in line with logic programming, your statements aren’t evaluated imperatively. You define the problem and let the language work out the details.

example

NOTE: The boiler plate from this example was copied straight out of the rego documentation2 which is excellent overall.

You’ll notice below that there are three functions named match; a conditional selection of which to execute. So our match functions will be evaluated based on whether there is a tags object or a key named foo or bar in tags.

So the hand-wavy tl’dr;

  • changes walks the tree and as a side effect assigns all the resource keys in the object resource_changes to array c
  • match checks for the conditions as outlined above
  • the order this is all invoked in is dictated by the language itself

Pretty straightforward when you look at the policy itself.

#./policy/tags.rego
package terraform.tags

deny[msg] {
	match(changes[c].change.after)
	msg := sprintf("fail: %v is missing required tags.", [changes[c].address])
}

match (i) {
	not i.tags
}

match(i) {
	not i.tags.foo
}

match(i) {
	not i.tags.bar
}

changes := { c |
	some path, value
	walk(input, [path, value])
	reverse_index(path, 1) == "resource_changes"
	c = value[_]
}

reverse_index(path, idx) = value {
	value := path[count(path) - idx]
}

And the deny policy above is invoked by the cli itself.

  • generate a terraform plan3
  • run opa telling it to
    • look in the policy directory for rego files
    • evoke the deny policy in the terraform.tags package
~  terragrunt plan -out=./terragrunt.plan && terragrunt show -json ./terragrunt.plan > terragrunt.json
~  opa exec --decision terraform/tags/deny --bundle policy terragrunt.json 2> >(jq)
{
  "result": [
    {
      "path": "terragrunt.json",
      "result": [
        "fail: module.rds.module.db_instance.aws_db_instance.this[0] is missing required tags.",
        "fail: module.rds.module.db_parameter_group.aws_db_parameter_group.this[0] is missing required tags.",
        "fail: module.security-group.aws_security_group.default[0] is missing required tags."
      ]
    }
  ]
}

CI

Now you could wire that up in your right into your CI process using the CLI if that tickles your fancy. However, there are tools out there3 that support opa policies by default. It’s worth looking into.


  1. Or web assembly interestingly enough. ↩︎

  2. https://www.openpolicyagent.org/docs/latest/terraform/ ↩︎

  3. I’m using terragrunt in this circumstance, but the process would be identical for terraform ↩︎ ↩︎