Using New Sentinel Features in Terraform Cloud

Roger Berlind
HashiCorp Solutions Engineering Blog
22 min readMay 29, 2020

One of the most important features of Terraform Cloud (TFC) and Terraform Enterprise (TFE) (the self-hosted implementation of Terraform Cloud) is Sentinel, which lets you implement governance policies as code. Sentinel policies are checked between the plan and apply stages of runs in TFC and TFE.

How Sentinel fits into Terraform Cloud runs

HashiCorp recently released two new Sentinel features that improve the reusability of Sentinel functions and dramatically reduce the length and complexity of Sentinel policies written for Terraform Cloud and Terraform Enterprise.

In this blog post, I’ll discuss these new features and walk through some new third-generation example policies and functions that use them.

I gave a demo of the policies and functions discussed in this post in a HashiCorp SE Hangout on June 3, 2020. You can view the recording here.

Two New Sentinel Features

Previously, if you wanted to use a Sentinel function in a Sentinel policy, you had to paste the entire function into the policy. If you used a function in 10 policies, you ended up with 10 copies of it. If you wanted to change it, you had to change all 10 copies. Obviously, changing a function that was used in many policies was not fun!

The first new feature, Sentinel Modules, improves the reusability of Sentinel functions by allowing them to be written in one file and used by policies defined in other files. They were released in the Sentinel 0.15.0 runtime on March 5, 2020. They can be used with the Sentinel CLI (versions 0.15.0 and higher), with Terraform Cloud (TFC), and with Terraform Enterprise (TFE) (versions v202004–1 and higher).

When using Sentinel modules with the Sentinel CLI, you list them in your Sentinel CLI configuration or test case files. When using them in Terraform Cloud or Terraform Enterprise, you list them in your Policy Set configuration files (sentinel.hcl). Policies can then use the modules by adding a single import statement similar to those used to reference the standard Sentinel imports and the Terraform Sentinel imports.

This brings us to the second new feature, the Terraform Sentinel v2 Imports that expose data from Terraform plans to Sentinel policies written for use with Terraform Cloud and Terraform Enterprise. There are 3 of them:

The fact that these are described as “v2” imports naturally implies that they are an improvement on existing v1 imports. In particular, the v2 imports are aligned more closely with native Terraform 0.12 data structures. This makes the v2 imports easier to use than the v1 imports.

Additionally, the fact that resource instances are stored in a single flat map across all Terraform modules and resource types makes it easy to use the Sentinel filter expression to find all resource instances of a specific type or a sub-collection of them. In contrast, with the v1 imports, you have to use 3 Sentinel for loops in order to find all resource instances of a specific type.

However, there is a catch: the new v2 imports can only be used with Terraform 0.12. If you are still using Terraform 0.11, you still need to use the v1 imports.

I’ll cover some important aspects of the new v2 imports below when I discuss how the new third-generation common functions use them. You’ll find complete documentation for all the Terraform Sentinel imports here. Note that there is only a single version of the tfrun import.

You can read more about why HashiCorp released the v2 imports here.

The Evolution of Terraform Sentinel Policies

If you explore the governance directory of the hashicorp/terraform-guides GitHub repository managed by the HashiCorp solutions engineering team, you’ll see that it has 3 sub-directories: first-generation, second-generation, and third-generation; these reflect the evolution of the example policies in that repository.

The first-generation policies were written in late 2018 based on sample policies in the original Sentinel documentation. They used the Terraform Sentinel v1 imports. Unfortunately, those sample policies and the first-generation policies had several short-comings including the following:

  • Most of the policies did not print violation messages for resources that violated them.
  • They stopped evaluating conditions as soon as a single resource instance violated them. So, even if they had printed violation messages, they would not have reported all violations.
  • They failed when resources that were being destroyed violated conditions even though destroying them actually made their workspaces compliant with the policies.
  • They did most of their processing in Sentinel rules instead of in Sentinel functions. This lead to overly verbose Sentinel output which was not really helpful to users whose Terraform plans had caused violations.
  • If they used Sentinel functions at all, they did not use common functions.

The second-generation policies, which were written in the second half of 2019 and still used the Terraform Sentinel v1 imports, addressed these issues by doing the following:

  • They offloaded most processing from rules into some common parameterized functions. This reduced the effort needed to write new policies since the common functions could just be copied into them.
  • Those common functions were written in a way that caused all violations of all rules to be reported.
  • They printed out the full address of each resource instance that did violate a policy in the same format that is used in plan and apply logs, namely module.<A>.module.<B>.<type>.<name>[<index>].
  • They used a single main rule to evaluate the boolean values (true or false) returned by the functions. This suppressed most of Sentinel’s default, overly verbose output.
  • They skipped resources that were being destroyed but not recreated.
  • They tested whether resource attributes were computed to avoid errors.

The second-generation policies were also all accompanied by Sentinel CLI test cases and mocks so that users could test them with the Sentinel CLI.

While the second-generation policies were a BIG improvement on the first-generation policies, they were often quite long (mostly over 100 lines) because they had to include the full text of all the common functions they used either directly or indirectly. Also, some of the functions were themselves long due to inefficiencies in the Terraform Sentinel v1 imports. Another issue was that the common filter functions could not be applied to nested attributes of resources and data sources.

This finally brings us to the new third-generation policies that I wrote in the spring of 2020. These use the new Terraform Sentinel v2 imports and call a new library of Sentinel functions that reside in Sentinel modules. The advantages of the third-generation policies and functions are:

  • Their use of the v2 imports and the Sentinel filter expression makes it easier to restrict policies to specific operations performed by Terraform against specific resources or data sources. This makes it easier to ignore resources that are being destroyed.
  • Since the common functions are defined in Sentinel modules, their implementations do not need to be pasted into the policies that call them. In fact, they don’t even have to live in the same directory or even the same repository as the policies. This is a HUGE improvement over the second-generation common functions!
  • A related benefit of using functions from modules is that most of the policies do not have any for loops or if/else conditionals. This makes it easier for users to understand the policies and to write their own policies that copy them. Users just need to call the appropriate find and filter functions.
  • The common function evaluate_attribute, which is in the tfplan-functions.sentinel and tfstate-functions.sentinel modules, can evaluate the value of any attribute of any resource or data source even if it is deeply nested inside the resource. This reduces the need to write custom functions to handle nested attributes.

Like the second-generation policies, the third-generation policies all include test cases, but only for Terraform 0.12.

Some Prototypical Third-Generation Policies

Let’s review a few prototypical third-generation Sentinel policies that illustrate how easy it is to write them using the common functions.

A Policy that Restricts EC2 Instance Types

I’ll start with the restrict-ec2-instance-type.sentinel policy that restricts the allowed sizes of AWS EC2 instances. While I picked this one, I could just as easily have picked very similar policies that restrict the sizes of Azure VMs, GCP compute instances, or VMware VMs. I also could have picked policies that restrict other attributes of other resources or data sources.

# This policy uses the Sentinel tfplan/v2 import to require that
# all EC2 instances have instance types from an allowed list
# Import common-functions/tfplan-functions/tfplan-functions.sentinel
# with alias "plan"
import "tfplan-functions" as plan
# Allowed EC2 Instance Types
# Include "null" to allow missing or computed values
allowed_types = ["t2.small", "t2.medium", "t2.large"]
# Get all EC2 instances
allEC2Instances = plan.find_resources("aws_instance")
# Filter to EC2 instances with violations
# Warnings will be printed for all violations since the last
# parameter is true
violatingEC2Instances = plan.filter_attribute_not_in_list(
allEC2Instances, "instance_type", allowed_types, true)
# Main rule
main = rule {
length(violatingEC2Instances["messages"]) is 0
}

This policy is only 24 lines long even with all the comments. The second-generation policy equivalent to this one was 102 lines long. So, the third-generation version is 24% as long. Ignoring the comments, the third-generation policy only has 7 lines of code.

While both policies invoke two functions to find all EC2 instances and validate whether they use allowed instance types, the third-generation policy uses functions declared in a Sentinel module, tfplan-functions.sentinel, and is able to simply import both functions with a single line. In contrast, the second-generation policy had to include the actual functions.

Let’s discuss what the policy does in a bit more detail:

  1. It imports the tfplan-functions Sentinel module and assigns it the alias plan.
  2. It defines an allowed_types list with allowed EC2 instance types.
  3. It calls the find_resources function of the tfplan-functions module to assign all instances of the aws_instance resource that are being created or modified to the collection allEC2Instances.
  4. It then calls the filter_attribute_not_in_list function of the tfplan-functions module to filter the EC2 instances that do not have allowed instance types into a second collection, violatingEC2Instances. The call to this function includes the list of EC2 instances, allEC2Instances, the attribute being restricted, "instance_type", the allowed_types list, and a boolean variable set to true that tells the function to print all violations it finds.
  5. The main rule checks that the length of the messages map returned by the filter function is 0. If so, the policy will pass. If there were any violation messages, the length will be greater than 0 and the policy will fail.

Here is some output for 3 EC2 instances that do not use an allowed instance type. Note that the full address of each resource is included in each message.

aws_instance.ubuntu[0] has instance_type with value t2.xlarge that is not in the allowed list: [t2.small, t2.medium, t2.large]
aws_instance.ubuntu[1] has instance_type with value t2.xlarge that is not in the allowed list: [t2.small, t2.medium, t2.large]
module.nested.aws_instance.ubuntu has instance_type with value t2.xlarge that is not in the allowed list: [t2.small, t2.medium, t2.large]

This third-generation policy does not include any Sentinel for loops or if/else conditionals. This makes the policy very easy to understand even by people who do not know the Sentinel language and might never have written any computer code. Since it’s easy to understand, it’s also easy to create new policies by copying it and making various changes.

A Policy that Limits VMware VM Resources

Next, let’s examine the restrict-vm-cpu-and-memory.sentinel policy that restricts the CPU and memory resources that a VMware virtual machine can use. Since this policy applies two conditions to the resources it restricts, it is a bit longer, but not by much (especially if we ignore all the comments).

# This policy uses the Sentinel tfplan/v2 import to require that
# all VMware VMs respect CPU and memory limits
# Import common-functions/tfplan-functions/tfplan-functions.sentinel
# with alias "plan"
import "tfplan-functions" as plan
# CPU and Memory (MB) Limits
maxCPUs = 4
maxMemory = 8192
# Get all VMs
allVMs = plan.find_resources("vsphere_virtual_machine")
# Filter to VMs with high CPU
# Warnings will be printed for all violations since the last
# parameter is true
highCPUVMs = plan.filter_attribute_greater_than_value(allVMs,
"num_cpus", maxCPUs, true)
# Filter to VMs with high memory
# Warnings will be printed for all violations since the last
# parameter is true
highMemoryVMs = plan.filter_attribute_greater_than_value(allVMs,
"memory", maxMemory, true)
# Main rule
validated = length(highCPUVMs["messages"]) is 0 and
length(highMemoryVMs["messages"]) is 0
main = rule {
validated
}

This policy is only 32 lines long. The equivalent second-generation policy was 101 lines long. So, the third-generation version is 32% as long. Ignoring comments, the third-generation policy only has 10 lines of code.

This policy does the following things:

  1. It imports the same tfplan-functions Sentinel module as our first example.
  2. It defines two variables, maxCPUs and maxMemory, to limit the CPU and memory resources a VMware VM can have.
  3. It calls the find_resources function of the tfplan-functions module to assign all instances of the vsphere_virtual_machine resource that are being created or modified to the collection allVMs.
  4. It calls the filter_attribute_greater_than_value function of the tfplan-functions module twice, once to filter VMs that have too many CPUs into the highCPUVMs collection and a second time to filter VMs that use too much memory into the highMemoryVMs collection. While filtering the VMs, it prints any violations since the fourth parameter was set to true.
  5. It sets the boolean variable validated to an expression that will be true if there were any violation messages in the hightCPUVMs or highMemoryVMs collections and false if neither collection had any violations.
  6. The main rule returns the value of validated.

Here is some output from this policy for a VM that has too many CPUs and too much memory:

vsphere_virtual_machine.vm has num_cpus with value 8 that is greater than 4
vsphere_virtual_machine.vm has memory with value 16384 that is greater than 8192

Note that this policy calls a different filter function than the first example did. The tfplan-functions and tfstate-functions modules both include 13 distinct filter functions to handle a wide variety of filter requirements.

A Policy that Restricts VMware Disk Sizes

Let’s review a policy, restrict-vm-disk-size.sentinel, that is a bit more complicated. It limits the size of disks used by VMware VMs.

# This policy uses the Sentinel tfplan/v2 import to require that
# all VMware VMs obey a disk limit
# Import common-functions/tfplan-functions/tfplan-functions.sentinel
# with alias "plan"
import "tfplan-functions" as plan
# Disk Size Limit (in GB)
maxDiskSize = 100
# Get all VMs
allVMs = plan.find_resources("vsphere_virtual_machine")
# Validate VM Disks
disksValidated = true
for allVMs as address, vm {
# Find the disks of the current VM
disks = plan.find_blocks(vm, "disk")
# Filter to violating disks of the current VM
# Warnings will not be printed for violations since the last
# parameter is false
violatingDisks = plan.filter_attribute_greater_than_value(disks,
"size", maxDiskSize, false)
# Print warnings if any violating disks
if length(violatingDisks["messages"]) > 0 {
disksValidated = false
print(address, "has at least one disk with size greater than",
maxDiskSize)
plan.print_violations(violatingDisks["messages"], "Disk")
} // end if
} // end for VMs# Main rule
main = rule {
disksValidated
}

Rather than describing everything this policy does, we’ll just focus on the differences.

  1. After calling the find_resources function of the tfplan-functions module to assign all instances of the vsphere_virtual_machine resource that are being created or modified to the allVMs collection, the policy uses a for loop to iterate over these VMs.
  2. Inside the for loop, the policy calls the find_blocks function of the tfplan-functions module to find all instances of the disk block within the current VM.
  3. It then calls the filter_attribute_greater_than_value function to filter the disks that are larger than the value of the maxDiskSize variable. However, since the fourth parameter is set to false, the function does not print any violation messages.
  4. If the length of the messages map returned by the filter function is positive, it sets the boolean variable disksValidated to false, prints out one violation message that includes the full address of the VM, and then calls the print_violations function of the tfplan-functions module to print the violation messages for those disks that were too large, prefacing each of those messages with “Disk”.

Here is some output from this policy for a VM that has two violating disks:

vsphere_virtual_machine.vm has at least one disk with size greater than 100
Disk 0 has size with value 120 that is greater than 100
Disk 1 has size with value 160 that is greater than 100

This example explains why the filter functions all include the fourth parameter prtmsg which can be set to true or false: in some cases, the violation messages would not be clear if printed by the filter functions themselves. Delaying their printing allows the policy to first print a message that includes the address of the actual resource that generated violations and add an extra string before each of the violation messages generated by the filter function.

If I had set prtmsg to true in the call to the filter function, the policy would have printed this:

0 has size with value 120 that is greater than 100
1 has size with value 160 that is greater than 100

Those messages by themselves would not have helped the person who violated the policy know what to fix.

While this policy does use a for loop, I could have avoided that by moving the middle section of the policy into a new Sentinel function. I did not do so because I did not think the function would have been used by other policies.

A Policy that Evaluates a Nested Attribute

To conclude this section, I want to refer you to the restrict-publishers-of-current-vms.sentinel policy that evaluates a nested attribute of a resource. Specifically, it uses the tfstate-functions Sentinel module to restrict the publishers that existing Azure VMs can use.

# This policy uses the Sentinel tfplan/v2 import to require that
# all existing Azure VMs have publishers from a specified list
# Import common-functions/tfstate-functions/tfstate-
# functions.sentinel with alias "state"
import "tfstate-functions" as state
# List of allowed publishers
# Include "null" to allow missing or computed values
allowed_publishers = ["RedHat", "Canonical"]
# Get all Azure VMs
allAzureVMs = state.find_resources("azurerm_virtual_machine")
# Filter to Azure VMs with violations
# Warnings will be printed for all violations since the last
# parameter is true
violatingAzureVMs = state.filter_attribute_not_in_list(allAzureVMs,
"storage_image_reference.0.publisher", allowed_publishers, true)
# Main rule
main = rule {
length(violatingAzureVMs["messages"]) is 0
}

The publisher is given by the storage_image_reference.0.publisher attribute which refers to the publisher attribute of the first instance of the storage_image_reference block within an azurerm_virtual_machine resource. The policy uses that attribute in this line:

violatingAzureVMs = state.filter_attribute_not_in_list(allAzureVMs,  "storage_image_reference.0.publisher", allowed_publishers, true)

The policy is able to handle the nested attribute because the filter function it uses and all the other filter functions call the evaluate_attribute function, which (as mentioned above) can evaluate any attribute of any resource or data source, no matter how deeply nested the attribute is.

Summary of the Policy Examples

Having examined the four third-generation Sentinel policies that use the tfplan-functions Sentinel module, I think you’ll agree with the following observations:

  • The policies are short and easy to understand.
  • The first two and the fourth do not include any complex programming logic like for loops and if/else conditionals. Instead, they only set variables, call common functions, and evaluate the main rule. I estimate that 90% of Sentinel policies can be written in this way. Of course, the third policy does use a for loop to process the disks of the VMs; but this policy is also quite easy to understand.
  • The policies print very clear violation messages. While I have not shown Sentinel’s default output above, each policy will only print a single line starting with TRUE if the policy passes and FALSE if it fails.
  • The common-functions are both flexible and powerful, allowing many different filters to be applied and the evaluation of both top-level and nested attributes.

The Third-Generation Common Functions

Having discussed some examples of third-generation Sentinel policies, I’ll now review the common functions that they use. I’ll focus mostly on the functions in the tfplan-functions Sentinel module that use the tfplan/v2 import. The tfstate-functions module has the same functions, but they use the tfstate/v2 import. The similarity between the functions of the two modules makes it easy to create pairs of very similar policies, the first of which uses the tfplan/v2 import to restrict new and changing resources while the second uses the tfstate/v2 import to restrict existing resources.

There are also tfconfig-functions and tfrun-functions modules that respectively use the tfconfig/v2 and the tfrun imports. See the cloud-agnostic directory of the terraform-guides repository for example policies that use these modules.

Finally, the aws-functions module includes some functions used to determine the roles assumed by the AWS provider and to find AWS resources that use standard AWS tags. These are used by the restrict-assumed-roles.sentinel, restrict-assumed-roles-by-workspace.sentinel, and enforce-mandatory-tags.sentinel policies.

All of the third-generation common functions are documented in individual Markdown files in the docs directory under the directory that contains the module itself.

The Find Functions

The tfplan-functions and tfstate-functions Sentinel modules have functions that use the Sentinel filter expression to find resources, data sources or blocks that belong to a single resource or data source:

  • The find_resources and find_datasources functions find resources or data sources of a specific type. Note that the tfplan versions of these functions only find resources that are being created or changed and data sources that are being created, changed, or read.
  • The find_resourcesfind_resources_by_provider and find_datasources_by_provider functions find resources or data sources for a specific provider. Note that the tfplan versions of these functions only find resources that are being created or changed and data sources that are being created, changed, or read.
  • The find_resourcesfind_resources_being_destroyed and find_datasources_being_destroyed functions find resources or data sources that are being destroyed but not re-created.
  • The find_blocks function finds all blocks of a specific type in a single resource.

With the exception of the find_blocks function, these functions use the resource_changes collection of the tfplan/v2 import or the resources collection of the tfstate/v2 import. These functions focus on the type and/or providerattributes of the collections along with their mode attribute. The first identifies the type of resource, the second indicates which provider is provisioning it, and the third indicates whether the resource is a managed resource (with value managed) or a data source (with value data). The find functions in the tfplan-functions module also use the actions list of the change map of members of the resource_changes collection to determine whether they are being created, modified, read, or destroyed.

Here is the code of the find_resources function of the tfplan-functions module:

find_resources = func(type) {
resources = filter tfplan.resource_changes as address, rc {
rc.type is type and
rc.mode is "managed" and
(rc.change.actions contains "create" or
rc.change.actions contains "update")
}
return resources
}

You can see that it finds all (managed) resources (but not data sources) of the specified type from the resource_changes collection of the tfplan/v2 import which are being created or updated.

Here is the code of the find_datasources function of the tfstate-functions module:

find_datasources = func(type) {
datasources = filter tfstate.resources as address, d {
d.type is type and
d.mode is "data"
}
return datasources
}

You can see that it finds all data sources of the given type from the resources collection of the tfstate/v2 import. Note that this collection does not include the change map that the resource_changes collection of the tfplan/v2 import includes since the tfstate/v2 import focuses on existing resources without consideration of planned changes.

The find_blocks functions find all blocks of a certain type under a single resource or data source. We saw this used in the restrict-vm-disk-size.sentinel policy above.

The Filter Functions

The tfplan-functions and tfstate-functions Sentinel modules have filter functions that filter a collection of resources, data sources, or blocks to a sub-collection that violates some condition. In the rest of this section, when we say “resources”, we are including data sources which are really just read-only resources.

The filter functions all accept a collection of resource changes (for tfplan/v2), resources (for tfstate/v2), or blocks, an attribute, a value or a list of values, and a boolean, prtmsg, which can be true or false and indicates whether the filter function should print violation messages. The filter functions return a map consisting of 2 items:

  • resources: a map consisting of resource changes (for tfplan/v2), resources (for tfstate/v2), or blocks that violate a condition.
  • messages: a map of violation messages associated with the resource changes, resources, or blocks.

The resources and messages collections are both indexed by the address of the resources, so they will have the same order and length. The filter functions all call the evaluate_attribute function to evaluate attributes of resources even if these are nested deep within the resources. After calling a filter function and assigning the results to a variable like violatingResources, you can test if there are any violations with this condition: length(violatingResources["messages"]) is 0. Alternatively, you could check whether length(violatingResources["resources"]) is 0 since the two maps returned by the filter functions always have the same length.

The evaluate_attribute Function

We’ve already mentioned that the filter functions call the evaluate_attribute function that can evaluate the value of any attribute of any resource, data source, or block even if it is deeply nested. It does this by calling itself recursively. The declaration of this function is:

evaluate_attribute = func(r, attribute)

When calling the evaluate_attribute function against a nested attribute, you should set r to a resource derived from the tfplan.resource_changes or the tfstate.resources collections or from a block discovered by the find_blocks function. You should set attribute to a string representing a top-level or nested attribute of the resource or block. Use a simple string for a top-level attribute but use a string delimited by . for a nested attribute.

We already saw above that the publisher of an Azure VM was specified with “storage_image_reference.0.publisher” and explained that this represents the publisher attribute of the first instance of the storage_image_reference block. (Indices of lists start with 0.) Note that this way of referring to nested attributes is unique to the third-generation common functions. If you were referring to the same attribute using the tfplan/v2 import directly, you would use “storage_image_reference[0].publisher” (without quotes).

If the objectr passed to the evaluate_attribute function represents a block, then attribute should be specified relative to that block.

The evaluate_attribute function works its way from the first segment of the attribute string passed to it to the last segment by calling itself recursively.

Note that the tfplan-functions implementation of the evaluate_attribute function automatically detects if the collection r passed to it contains a map called change with a key called after and then uses r.change.after instead of r. This allows the filter functions of the tfplan-functions module to pass it r instead of r.change.after so that the filter functions can process blocks and so that policies do not have to append change.afterto the collection of resources they pass to the filter functions.

The tfstate-functions implementation of the evaluate_attribute function does something similar, replacing r with r.values when r contains a map called values that contains a key called module_address.

Finally, if the evaluate_attribute function returns the undefined value, the filter functions convert it to null.

Other Functions

The filter functions also all call the to_string function which converts any Sentinel object to a string. It is used to build the messages in the messages collection that the filter functions return.

Finally, theprint_violations function can be called by policies after they call one of the filter functions to print the violation messages it returned. This would only be called if the prtmsg argument had been set to false when calling the filter function. We saw this done in the restrict-vm-disk-size.sentinel policy above and explained that this is desirable when processing blocks of resources since your policy can then print some other message that gives the address of the resource with block-level violations before printing them. Another policy which calls this function is restrict-ingress-sg-rule-cidr-blocks.sentinel which restricts ingress blocks allowed on AWS security groups and security group rules.

Some Other Third Generation Sentinel Policies

The governance/third-generation directory of the terraform-guides repository has many other example Sentinel policies including the following:

  • Policies that restrict the size of Azure and GCP VMs.
  • Policies that require mandatory labels to be on various resources.
  • A policy that requires AWS S3 buckets to be private and be encrypted by a KMS key.
  • A policy that restricts the owners of AWS AMIs.
  • Two policies that restrict which IAM roles the AWS provider can assume.
  • A policy that prevents AWS security groups and security group rules from allowing ingress from the 0.0.0.0/0 CIDR.
  • A policy that requires all modules loaded from the root module to come from a TFC/TFE Private Module Registry (PMR) and that prevents resources being created in the root module itself.
  • A policy that prevents providers from being created in non-root modules.
  • Policies that enforce allow and prohibit lists for resources, data sources, providers, and provisioners.
  • A policy that prevents destruction of prohibited resource types.
  • Three policies that prevent provisioning in workspaces that violate certain Cost Estimation limits.

Testing the Third Generation Policies

You can easily test all the third-generation policies yourself as follows:

  1. Download the Sentinel CLI from the Sentinel Downloads page. (Be sure to use Sentinel 0.15.2 or higher.)
  2. Unzip the zip file and place the sentinel binary in your path.
  3. Fork the terraform-guides repository and clone your fork to your local machine.
  4. Navigate to any of the cloud directories (aws, azure, gcp, or vmware) or to the cloud-agnostic directory.
  5. Run sentinel test to test all policies for that cloud.
  6. To test a single policy, run sentinel test -run=<partial_policy_name> where <partial_policy_name> is enough of the policy name to distinguish it from others in the same directory.
  7. To see the violation messages of the policies add the -verbose flag to the above commands.

Using the Third Generation Policies in TFC or TFE

You can easily use the third generation policies in your Terraform Cloud (TFC) organizations by creating policy sets in them that point to the cloud directories of the governance/third-generation directory of your own fork of the terraform-guides repository. Simply follow these instructions, specifying the correct VCS Repo, VCS Branch, and Policies Path. For instance, to create a policy set that includes the GCP policies, set the Policies Path to governance/third-generation/gcp.

Here is the sentinel.hcl file for the gcp directory:

module "tfplan-functions" {
source = "../common-functions/tfplan-functions/tfplan-functions.sentinel"
}
module "tfstate-functions" {
source = "../common-functions/tfstate-functions/tfstate-functions.sentinel"
}
module "tfconfig-functions" {
source = "../common-functions/tfconfig-functions/tfconfig-functions.sentinel"
}
policy "enforce-mandatory-labels" {
source = "./enforce-mandatory-labels.sentinel"
enforcement_level = "advisory"
}
policy "restrict-gce-machine-type" {
source = "./restrict-gce-machine-type.sentinel"
enforcement_level = "advisory"
}

However, you’ll probably want to set enforcement levels of the policies to soft-mandatory or hard-mandatory in your own fork of the repository.

It is also possible to set the source attribute of a policy or module to a policy or module in a different directory or even to a policy or module in a different repository. For instance, you could refer to

https://raw.githubusercontent.com/hashicorp/terraform-guides/master/governance/third-generation/aws/restrict-ec2-instance-type.sentinel

from a sentinel.hcl file in a different repository. Using the URL of the raw content of the policy is required; using a URL like

https://github.com/hashicorp/terraform-guides/blob/master/governance/third-generation/aws/restrict-ec2-instance-type.sentinel

will give a hard error when the policy is checked.

The GCP policy set configuration file is able to refer to the Sentinel modules in the common-functions directory that is outside the gcp directory by using ... This was made possible in Terraform Cloud on May 28, 2020, but it won’t be supported in TFE until the end of June. So, if you are using TFE, you’ll need to create a repository like this one in which the “common-functions” directory has been placed in the same VCS directory as the sentinel.hcl file. The directory does not have to be the top-level directory within the VCS repository.

Conclusion

In this blog post, I discussed two new Sentinel features: Sentinel Modules and the new Terraform Sentinel v2 imports. I then reviewed the evolution of Sentinel policies, discussed four prototypical third-generation Sentinel policies, reviewed the third-generation common functions, and described how you can test these policies with the Sentinel CLI and use them in your Terraform Cloud or Terraform Enterprise organizations.

To learn more, you can view the webinar I delivered on June 3, 2020 here.

--

--

Roger Berlind
HashiCorp Solutions Engineering Blog

Roger is a Sr. Solutions Engineer at HashiCorp with over 20 years of experience explaining complex technologies like cloud, containers, and APM to customers.