First Look into… HashiCorp Sentinel and Policy-as-Code (Part 2)

This post is the follow-up to the introduction to Policy-As-Code and to Sentinel. In this post, we’ll look at how to write the actual Sentinel policies.

In the previous post, we reviewed how we can use policy-as-code with Terraform to enforce specific policies. The example we used was to force the operator to deploy VMware Cloud on AWS SDDC resources in a specific region.

Let’s talk about the actual code to make this happen. Again, we’re focusing on Sentinel and Terraform together.

Terraform Config, State and Plan

Sentinel can look at your Terraform config, state and plan. In other words, policies can be applied based on:

  • What is currently deployed (the state)
  • What the intent of the target infrastructure is (the config)
  • What the actual simulated infrastructure would be, once the changes are pushed (the plan)

Sentinel Config Files

Sentinel requires a policy set and policies:

  • a configuration file called sentinel.hcl that describes the multiple policies that are attached
  • one or more policies files (one per policy). These files must match the name of the policy from the configuration file and carry the .sentinel suffix.

Let’s walk through a couple of examples.

Anatomy of a Sentinel Configuration File

It’s pretty short and pretty self-explanatory.

module "tfplan-functions" {
    source = "https://raw.githubusercontent.com/hashicorp/terraform-guides/master/governance/third-generation/common-functions/tfplan-functions/tfplan-functions.sentinel"
}

module "tfstate-functions" {
    source = "https://raw.githubusercontent.com/hashicorp/terraform-guides/master/governance/third-generation/common-functions/tfstate-functions/tfstate-functions.sentinel"
}

module "tfconfig-functions" {
    source = "https://raw.githubusercontent.com/hashicorp/terraform-guides/master/governance/third-generation/common-functions/tfconfig-functions/tfconfig-functions.sentinel"
}

policy "terraform-vmc-location" {
  source            = "./terraform-vmc-location.sentinel"
  enforcement_level = "hard-mandatory"
}

policy "terraform-vmc-region" {
  source            = "./terraform-vmc-region.sentinel"
  enforcement_level = "soft-mandatory"
}

The Sentinel Configuration File above leverages a module called “tfplan-functions” that has already a set of pre-defined functions that we will be leveraging further down this post. These functions will make our policies far simpler to write.

We are specifying two policies – terraform-vmc-location and terraform-vmc-region. We will look at terraform-vmc-region in more details later on. As described above, we are applying different enforcement levels to the policies – if Terraforms fails the policy check ‘terraform-vmc-region’, we can override it and apply it regardless but if it fails ‘terraform-vmc-location’, the apply will be cancelled.

Anatomy of a Sentinel Policy – VMware Storage DRS

Let’s walk through an actual Sentinel policy line-by-line. This one is for VMware users to ensure that “storage DRS is enabled at the cluster-level”.

import "tfplan"

get_datastore_clusters = func() {
  datastore_clusters = []
  for tfplan.module_paths as path {
    datastore_clusters += values(tfplan.module(path).resources.vsphere_datastore_cluster) else []
  }
  return datastore_clusters
}

datastore_clusters = get_datastore_clusters()

# Require Storage DRS be enabled (true)
# other option is false
require_storage_drs = rule {
  all datastore_clusters as _, instances {
    all instances as index, r {
      r.applied.sdrs_enabled is true
    }
  }
}

main = rule {
  (require_storage_drs) else true
}

“What on Earth is this language?!” was my first reaction when I started with Sentinel. It doesn’t look like anything I’ve used and for me, the learning curve was quite high but eventually, I found a way to bypass some of the complexity (read further below for an explanation).

First, we’re starting with the following line:

import "tfplan"

The command above will import the Terraform plan output. Now Sentinel can look at what we expect the infrastructure to be, if Terraform does update the infrastructure.

get_datastore_clusters = func() {
  datastore_clusters = []
  for tfplan.module_paths as path {
    datastore_clusters += values(tfplan.module(path).resources.vsphere_datastore_cluster) else []
  }
  return datastore_clusters
}

datastore_clusters = get_datastore_clusters()

get_datastore_clusters is a function. Within the function, we first create an empty list called datastore_clusters and we iterate (using for) through a list of all the modules within the TF plan (tfplan.modules_paths) and we add the values of all the resources vsphere_datastore_cluster found within TF plan to the datastore_clusters list through concatenation with “+=“.

We execute the function get_datastore_clusters() and return its value as “datastore_clusters”.

In other words, we now have a list of all the clusters.

# Require Storage DRS be enabled (true)
# other option is false
require_storage_drs = rule {
  all datastore_clusters as _, instances {
    all instances as index, r {
      r.applied.sdrs_enabled is true
    }
  }
}

Now that we have a list of the “datastore _clusters”, we want to go through it. This gets a bit more complicated here. We use something called quantifiers. What we do here is go through the list of clusters and check if all of them have Storage DRS enabled. If one of them is not enabled, the output of require_storage_drs will be false.

main = rule {
  (require_storage_drs) else true
}

Sentinel expects a main rule to be always present. The value of this rule is the result of the entire policy.

If the result of main is true, the policy passes. If the value is anything else, the policy fails.

Here, the main rule above returns the result of require_storage_drs so a ‘false’ would be returned if any of the datastore cluster has Storage DRS disabled and the policy would fail.

The policy above works but… it’s a bit complicated. We just want to check if an attribute of a resource has a specific value. It would be great to simplify the overall syntax.

Anatomy of a Sentinel Policy – VMware Cloud Region

Let’s walk through an easier use case and I’ll explain how we can significantly reduce the complexity of the Sentinel policies.

In our case, let’s imagine you’re a British company and, because of Brexit (the least I say about Brexit, the better), you can only deploy an SDDC in London.

# Rule to specify the allowed regions in which the user can deploy a VMC SDDC. This is to enforce deployments only to the allowed regions for compliance reasons.

# Import common-functions/tfplan-functions/tfplan-functions.sentinel
# with alias "plan"

import "tfplan-functions" as plan

allowed_regions = ["EU_WEST_2"]

print(allowed_regions)
# Get all SDDCs

allSDDC = plan.find_resources("vmc_sddc")

print(allSDDC)
# Filter to SDDC with wrong regions
# Warnings will be printed for all violations.

wrongSDDCregions = plan.filter_attribute_not_in_list(allSDDC, "region", allowed_regions, true)

print(wrongSDDCregions)

main = rule {
 length(wrongSDDCregions["messages"]) is 0
}

The first thing we’re doing is using an already written function called tfplan-functions.sentinel.

# Import common-functions/tfplan-functions/tfplan-functions.sentinel
# with alias "plan"

That’s one of the advantages of defining things as code – we can reuse them! Roger at HashiCorp has created a lot of handy functions that we can reuse throughout our code.

To use these functions, we only need the “import” line above and the following line in the sentinel.hcl file:

module "tfplan-functions" {
    source = "https://raw.githubusercontent.com/hashicorp/terraform-guides/master/governance/third-generation/common-functions/tfplan-functions/tfplan-functions.sentinel"
}

The tfplan-functions module provides lots of handy functions you can use through your code, such as find_resources and filter_attribute_not_in_list :

allSDDC = plan.find_resources("vmc_sddc")

print(allSDDC)

The above simply looks through all the resources of the “vmc_sddc” type. The print statement above is purely for troubleshooting and logging and it will appear in Terraform Cloud when the policy check is happening.

Similarly, the filter_attribute_not_in_list checks in all my SDDCs to see if the value of the attribute “region” is present in the list of allowed_regions.

allowed_regions = ["EU_WEST_2"]

wrongSDDCregions = plan.filter_attribute_not_in_list(allSDDC, "region", allowed_regions, true)

print(wrongSDDCregions)

main = rule {
 length(wrongSDDCregions["messages"]) is 0
}

The filter functions return a map consisting of 2 items:

  • “resources”: a map consisting of resource changes that violate a condition
  • “messages”: a map of violation messages associated with the resources

If the list of messages is empty (ie the length is 0), it means that there’s no violation and the policy check is a success.

main = rule {
 length(wrongSDDCregions["messages"]) is 0
}

If you exclude the descriptions and the print statements, it’s just 5 lines of code to define a business policy. It’s then quite easy to implement it.

Here is the full demo again:

Additional Resources and Materials

  • As far as I can tell, Roger Berlind at HashiCorp is the expert on Sentinel. He’s got lots of great stuff on his Medium page.
  • The official docs on Terraform and Sentinel can be found here and here.

Thanks for reading.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s