DevEx improvements in HashiCorp Sentinel

Recent releases of Sentinel have targeted improvements to the developer experience.

Improving the developer experience for writing policies and configuration has been the focus of recent releases of HashiCorp Sentinel. This blog covers the most notable features of these releases including static imports, named functions, defined checks, and per-policy parameter values. If you are new to Sentinel, be sure to read our Sentinel documentation and try out the Sentinel Playground.

»Improved configuration syntax

Previously, the terms "import" and "plugin" were often used interchangeably, and "module" had a different meaning. However, the way you accessed these different import types (standard import, plugin, or module) within policy was the same.

Starting with Sentinel 0.19, we have improved the import configuration syntax, which makes it simpler to work with Sentinel. Our introduction of a standardized naming convention ensures consistent import configuration using the HCL syntax already employed by Terraform.

We’ve also added support to the import block to allow overriding the default configuration for the standard imports and plugins that are used within a policy. The improved configuration syntax for Sentinel makes it easier to define different types of imports in a consistent and repeatable way. The new import configuration syntax for plugins and modules looks like this:

import "plugin" "time" {	config = {		timezone = "Australia/Brisbane"	}} import "module" "reporter" {	source = "./reporter.sentinel"}

»Support for static JSON data

Making the policy evaluation process more dynamic has several benefits, such as reducing the number of policies that need to be written and simplifying policy logic for easier contribution to policy libraries by teams. Importing arbitrary structured data into policies is a commonly requested feature from customers looking to enhance their policy-evaluation process.

Starting with version 0.19 of Sentinel, a new static import feature has been added that allows structured data to be imported into policies. This feature currently supports JSON documents, which is a popular data format used in many programming languages. The Sentinel team plans to support more data formats in the future. The new import configuration syntax for static imports looks like this:

import "static" "animals" {    source = "./animals.json"    format = "json"}

»Named functions

The introduction of named functions in Sentinel 0.20 has significant impact to the policy authoring experience. Named functions provide a way for the author to define a function that cannot be reassigned or reused. For instance, anonymous functions can be re-assigned, causing policies to fail if an attempted call is made later. This provides some extra safety for policy authors to be certain that critical functions will not change after definition. Here is an example of a named function:

func sum(a, b) {	a + b}

»Simplified expressions for unknown values

Sentinel allows values to be undefined, however there has historically been no way for policy authors to determine if a value is undefined. Additionally, policy authors must use the else expression to recover from undefined values and provide an alternative value. As part of the Sentinel 0.21 release, there are now two new helpers to determine if a value has been defined. This drastically improves readability of policies, as seen in this example:

foo = undefined // using the else expressionfoo else false is false // falsefoo else true is true // true // the new defined expressionsfoo is defined // falsefoo is not defined // true

»Per-policy parameter values

Parameters help facilitate policy reuse and allow values to be removed from the policy itself. Previously, parameter values could be supplied only once within a configuration, with that value being shared across policies. With the introduction of per-policy parameter values in Sentinel 0.21, parameter values can be supplied once per-policy, with the policy value taking precedence over a globally supplied value. Providing a parameter value to a single policy within configuration is shown here:

policy "restrict-s3" {	source = "./deny-resource.sentinel"	params = {		resource_kind = "aws_s3_bucket"	}}

»Bringing it all together

The example below brings all of the above features together to showcase what they enable for policy authors. In this example, we are going to create a policy that utilizes exemptions to determine its result. Here are a few considerations:

  • Make the policy reusable to allow for different inputs
  • Use static data to manage exemptions

First, let's create a modular policy for finding violations:

// main.sentinelimport "helpers" 			// our helpers moduleimport "tfplan/v2" as tfplan	// tfplan import param id			// id of the policyparam resource_type	// the type of resourceparam valid_actions	// allowed actionsparam attr			// the attribute to checkparam allowed_value	// the allowed value for the attribute // Filter resources by typeall_resources = filter tfplan.resource_changes as _, rc {		rc.type is resource_type and			rc.mode is "managed" and			rc.change.actions in valid_actions}// Filter resources that violate a given conditionviolations = filter all_resources as _, r {		r.change.after[attr] != allowed_value} result = rule when not helpers.exempt(id) {	violations is empty} main = rule {	result}

This policy is heavily parameterized, giving it greater reusability. It will filter all resources based on resource type and its action via the resource_type and valid_actions parameters. It will then find all violations through filtering the resources and asserting the provided attribute against the allowed value. The result rule is then evaluated based on the value returned from helpers.exempt(id), ensuring that no violations are present.

Now that we have a working policy, let's take a look at the helpers module for finding exemptions in static data:

// helpers.sentinelimport "exemptions"	// static import func exempt(id) {	if exemptions[id] is defined {		return exemptions[id]	} else {		return false	}}

This simple module has a single named function, exempt, which returns the value of the id within the exemptions static import, or false if it isn't defined. Our exemption static data will look like this.

{	"ec2_instance_size": false}

Finally, our configuration will contain the following:

import "module" "helpers" {	source = "./helpers.sentinel"} import "static" "exemptions" {	source = "./exemptions.json"	format = "json"} policy "ec2_instance_size" {	source = "./main.sentinel"	params = {		id = "ec2_instance_size",		resource_type = "aws_instance",		attr = "instance_type",		allowed_value = "t3.micro",		valid_actions = [			["no-op"],			["create"],			["update"],		]	}}

If we were to run this policy against valid HashiCorp Terraform plan data with no violations, we should expect an output similar to what’s shown here:

No module changes to install No policy changes to install Execution trace. The information below will show the values of allthe rules evaluated. Note that some rules may be missing ifshort-circuit logic was taken. Note that for collection types and long strings, output may betruncated; re-run "sentinel apply" with the -json flag to see thefull contents of these values. Pass - ec2_instance_size.sentinel ec2_instance_size.sentinel:25:1 - Rule "main"  Value:	true ec2_instance_size.sentinel:21:1 - Rule "result"  Value:	true

»Get started

The latest release of HashiCorp Sentinel includes several new features that build on previous investments in the policy authoring workflow. You can start exploring these new capabilities now by downloading the latest version of the Sentinel CLI from the Sentinel download page.

For more information on the Sentinel language and specification, visit the Sentinel documentation page. If you would like to engage with the community to discuss information related to Sentinel use cases and best practices, visit the HashiCorp Community Forum.

If you would like to experiment with Sentinel in a safe development environment, you can do so by visiting the Sentinel Playground, which provides the ability to evaluate and share example Sentinel policies and mock data. You can also get hands-on with tutorials for Sentinel’s integrations with Terraform Cloud, Vault Enterprise, and Nomad Enterprise.

Sign up for the latest HashiCorp news

By submitting this form, you acknowledge and agree that HashiCorp will process your personal information in accordance with the Privacy Policy.