In this guide we are going to walk you through the steps to incorporate conftest into your Terrahaxs workflow.
Step 1: Create your policies
Create a directory named policy
in the root of your repository and add your policies to it.
As an example, we are going to create a policy which checks your resources for proper tags. First we’ll create a main.rego
file:
# main.rego
package main
import data.tags_validation
module_address[i] = address {
changeset := input.resource_changes[i]
address := changeset.address
}
tags_pascal_case[i] = resources {
changeset := input.resource_changes[i]
tags := changeset.change.after.tags
resources := [resource | resource := module_address[i]; val := tags[key]; not tags_validation.key_val_valid_pascal_case(key, val)]
}
tags_contain_minimum_set[i] = resources {
changeset := input.resource_changes[i]
tags := changeset.change.after.tags
resources := [resource | resource := module_address[i]; not tags_validation.tags_contain_proper_keys(changeset.change.after.tags)]
}
deny[msg] {
resources := tags_contain_minimum_set[_]
resources != []
msg := sprintf("Invalid tags (missing minimum required tags) for the following resources: %v", [resources])
}
deny[msg] {
resources := tags_pascal_case[_]
resources != []
msg := sprintf("Invalid tags (not pascal case) for the following resources: %v", [resources])
}
Notice this file imports import data.tags_validation
, which we define in tags.rego
:
# tags.rego
package tags_validation
minimum_tags = {"ApplicationRole", "Owner", "Project"}
key_val_valid_pascal_case(key, val) {
is_pascal_case(key)
is_pascal_case(val)
}
is_pascal_case(string) {
re_match(`^([A-Z][a-z0-9]+)+`, string)
}
tags_contain_proper_keys(tags) {
keys := {key | tags[key]}
leftover := minimum_tags - keys
leftover == set()
}
Step 2: Test your policies
Before deploying the policies we want to validate they work as expected. We’ll create two test files - one for main.rego and one for tags.rego.
Here are the test files:
# main_test.rego
package main
test_tags_pascal_case {
deny == set() with input as {"resource_changes": [{ "address": "module.one", "change": { "after": { "tags": { "ApplicationRole": "ArtifactRepository", "Owner": "Ssi", "Project": "Artifacts", "Country": "Ng" }}}}]}
}
test_tags_pascal_case_with_wrong_value_format {
deny with input as { "resource_changes": [{ "address": "module.one", "change": { "after": { "tags": { "ApplicationRole": "artifactRepository", "Owner": "Ssi", "Project": "Artifacts", "Country": "Ng" }}}}]}
}
test_tags_pascal_case_with_wrong_key_format {
deny with input as { "resource_changes": [{ "address": "module.one", "change": { "after": { "tags": { "ApplicationRole": "ArtifactRepository", "owner": "Ssi", "Project": "Artifacts", "Country": "Ng" }}}}]}
}
test_tags_contain_minimum_set {
deny == set() with input as { "resource_changes": [{ "address": "module.one", "change": { "after": { "tags": { "ApplicationRole": "ArtifactRepository", "Owner": "Ssi", "Project": "Artifacts" }}}}]}
}
test_tags_contain_minimum_set_with_extra_tags {
deny == set() with input as { "resource_changes": [{ "address": "module.one", "change": { "after": { "tags": { "ApplicationRole": "ArtifactRepository", "Owner": "Ssi", "Project": "Artifacts", "Country": "Ng" }}}}]}
}
test_tags_contain_minimum_set_without_minimum {
deny with input as { "resource_changes": [{ "address": "module.one", "change": { "after": { "tags": { "ApplicationRole": "ArtifactRepository", "Project": "Artifacts", "Country": "Ng" }}}}]}
}
and our tags test:
# test_tags.rego
package tags_validation
test_tags_valid_pascal_case {
tags := { "ApplicationRole": "ArtifactRepository" }
val := tags[key]
key_val_valid_pascal_case(key, val)
}
test_tags_valid_pascal_case_lower_case_key {
tags := { "applicationRole": "ArtifactRepository" }
val := tags[key]
not key_val_valid_pascal_case(key, val)
}
test_tags_valid_pascal_case_lower_case_value {
tags := { "ApplicationRole": "artifactRepository" }
val := tags[key]
not key_val_valid_pascal_case(key, val)
}
test_tags_valid_pascal_case_lower_case_value_multiple_tags {
tags := { "ApplicationRole": "artifactRepository", "Project": "Artifacts" }
val := tags[key]
not key_val_valid_pascal_case(key, val)
}
test_tags_contain_proper_keys {
tags := { "ApplicationRole": "ArtifactRepository", "Project": "Artifacts", "Owner": "Ssi", "Country": "Ng" }
tags_contain_proper_keys(tags)
}
test_tags_contain_proper_keys_missing_key {
tags := { "ApplicationRole": "ArtifactRepository", "Project": "Artifacts", "Country": "Ng" }
not tags_contain_proper_keys(tags)
}
After creating these files we can now run conftest verify policy
from the root of the project. We should see an output like this:
conftest verify policy
12 tests, 12 passed, 0 warnings, 0 failures, 0 exceptions, 0 skipped
Step 3: Update your workflow to include Conftest
The third and final step is to create a custom workflow to include Conftest into your CI/CD pipeline.
Here is what an example workflow should look like:
version: 3
defaults:
workflow: myworkflow
roles:
aws: arn:aws:iam::530084066544:role/terrahaxs-worker
projects:
- dir: conftest
workspace: default
terraform_version: v1.0.11
workflows:
myworkflow:
plan:
steps:
- run: terraform fmt -check -diff .
- init
- plan:
extra_args: ["-lock=false"]
- show
- run: conftest test $SHOWFILE --policy $CLONE_DIR/policy --no-color
name: Conftest
Note that conftest runs against a json plan created by using the terraform show command. It
is important to include - show
before running your conftest command.
After updating your workflow, the next time Terrahaxs runs you will see the output of conftest.
Notice that if the conftest command doesn’t pass then a user will not have the option to apply their changes.
And that’s it!
After following these three steps you can now use conftest to enforce standards and security policies.