Skip to content

Instantly share code, notes, and snippets.

@SeanHood
Last active February 3, 2021 09:36
Show Gist options
  • Save SeanHood/4c3fc2800091293de0e13a083101bea8 to your computer and use it in GitHub Desktop.
Save SeanHood/4c3fc2800091293de0e13a083101bea8 to your computer and use it in GitHub Desktop.
Terraform and OPA/Conftest/Rego, what a pain in the arse it is

Terraform and OPA/Conftest/Rego, what a pain in the arse it is

So this has taken me 24+ hours to figure out. All docs, references to using OPA with Terraform are about testing the plan. I want some static analysis on raw .tf files. An OPA based linter if you will.

First thing I ran into was iterating over an object, then being able to use it's key. In Python I'd do: for k,v in dict. In Rego, we do hash[key], then just go and use key wherever. ¯\_(ツ)_/¯ Maybe this will make more sense the more I use Rego.

Second thing, I think I was being caught out by was: "Universal Quantification". So in my policy where I have not key == computed, I had used key != computed which, maybe this will make sense in the future, but right now. Mind blown.

Anyway, the thing you've been waiting for. Some examples.

So if we take our main.tf and module.rego and run them like so with the handy tool called conftest.

$ conftest test main.tf -p policy/module.rego
FAIL - main.tf - main - Module name: foo_app_uk_prod, does not match computed configuration: foo_app_uk_stage

1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions

We can see that we have one failure. This policy/linter is trying to ensure the module name matches the configuration passed to it.

Looks like we've messed up when copy and pasting. Our app config, let's update line 7 to say prod and run conftest again:

$ conftest test main.tf -p policy/module.rego

1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions

Much better.

module "foo_app_uk_prod" {
source = "./modules/foo"
module = "foo"
project = "app"
region = "uk"
env = "stage"
}
module "foo_database_uk_prod" {
source = "./modules/foo"
module = "foo"
project = "database"
region = "uk"
env = "prod"
}
package main
deny[msg] {
m := input.module[key]
# Based on the attributes, we compute what the expected module should be named
computed_module_name := sprintf("%s_%s_%s_%s", [m.module, m.project, m.region, m.env])
not key == computed_module_name
msg := sprintf("Module name: %v, does not match computed configuration: %v", [key, computed_module_name])
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment