» Defining Policies

Sentinel Policies for Terraform are defined using the Sentinel policy language. A policy can include imports which enable a policy to access reusable libraries, external data and functions. Terraform Enterprise provides three imports to define policy rules for the configuration, state and plan.

  • tfplan - This provides access to a Terraform plan, the file created as a result of terraform plan. The plan represents the changes that Terraform needs to make to infrastructure to reach the desired state represented by the configuration.
  • tfconfig - This provides access to a Terraform configuration, the set of "tf" files that are used to describe the desired infrastructure state.
  • tfstate - This provides access to the Terraform state, the file used by Terraform to map real world resources to your configuration.

» Useful Idioms for Terraform Sentinel Policies

Terraform's internal data formats are complex, which means basic Sentinel policies for Terraform are more verbose than basic policies that use simpler data sources.

This will improve in future versions of Terraform and Sentinel; in the meantime, be aware of the following idioms as you start writing policies for Terraform.

» To Find Resources, Iterate over Modules

The most basic Sentinel task for Terraform is to enforce a rule on all resources of a given type. Before you can do that, you need to get a collection of all the relevant resources.

The easiest way to do that is to copy a function like the following into any policies that examine every resource in a configuration:

# Get an array of all resources of the given type (or an empty array).
get_resources = func(type) {
    if length(tfplan.module_paths else []) > 0 { # always true in the real tfplan import
        return get_resources_all_modules(type)
    } else { # fallback for tests
        return get_resources_root_only(type)
    }
}

get_resources_root_only = func(type) {
    resources = []
    named_and_counted_resources = tfplan.resources[type] else {}
    # Get resource bodies out of nested resource maps, from:
    # {"name": {"0": {"applied": {...}, "diff": {...} }, "1": {...}}, "name": {...}}
    # to:
    # [{"applied": {...}, "diff": {...}}, {"applied": {...}, "diff": {...}}, ...]
    for named_and_counted_resources as _, instances {
        for instances as _, body {
            append(resources, body)
        }
    }
    return resources
}

get_resources_all_modules = func(type) {
    resources = []
    for tfplan.module_paths as path {
        named_and_counted_resources = tfplan.module(path).resources[type] else {}
        # Get resource bodies out of nested resource maps, from:
        # {"name": {"0": {"applied": {...}, "diff": {...} }, "1": {...}}, "name": {...}}
        # to:
        # [{"applied": {...}, "diff": {...}}, {"applied": {...}, "diff": {...}}, ...]
        for named_and_counted_resources as _, instances {
            for instances as _, body {
                append(resources, body)
            }
        }
    }
    return resources
}

Later, use the function to get a collection of resources:

aws_instances = get_resources("aws_instance")

This example function handles several things that are tricky about finding resources:

  • It checks every module for resources (including the root module) by looping over the module_paths namespace. The top-level resources namespace is more convenient, but it only reveals resources from the root module.
  • It unwraps the import's nested data structures, leaving only an array of resource bodies. The value of tfplan.module(path).resources[type] is a series of nested maps keyed by resource name and by count, but the name and count are almost never relevant to a policy. Removing them early makes the rest of the policy more readable.
  • It uses else expressions to recover from undefined values, for modules that don't have any resources of that type.
  • It falls back to the resources namespace if the real tfplan import isn't available, to support testing. Since current versions of Sentinel don't allow you to mock tfplan's module() function, it isn't possible to test Sentinel code that accesses non-root modules. However, you can still test the rest of the policy by mocking resource data under the resources namespace.

» To Test Resources, Use all/any Expressions

Once you have a collection of resources, you usually want to test some property of each resource in the collection — for example, the planned final value of a particular resource attribute.

The most concise tool for this is Sentinel's all and any expressions. For example:

# Allowed Types
allowed_types = [
    "t2.small",
    "t2.medium",
    "t2.large",
]

# Rule to restrict instance types
instance_type_allowed = rule {
    all get_resources("aws_instance") as r {
        r.applied.instance_type in allowed_types
    }
}