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 expression
foo else false is false // false
foo else true is true // true
// the new defined expressions
foo is defined // false
foo 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.sentinel
import "helpers" // our helpers module
import "tfplan/v2" as tfplan // tfplan import
param id // id of the policy
param resource_type // the type of resource
param valid_actions // allowed actions
param attr // the attribute to check
param allowed_value // the allowed value for the attribute
// Filter resources by type
all_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 condition
violations = 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.sentinel
import "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 all
the rules evaluated. Note that some rules may be missing if
short-circuit logic was taken.
Note that for collection types and long strings, output may be
truncated; re-run "sentinel apply" with the -json flag to see the
full 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
More blog posts like this one
5 ways to improve DevEx and security for infrastructure provisioning
Still using manual scripting and provisioning processes? Learn how to accelerate provisioning using five best practices for Infrastructure Lifecycle Management.
Fix the developers vs. security conflict by shifting further left
Resolve the friction between dev and security teams with platform-led workflows that make cloud security seamless and scalable.
HashiCorp at AWS re:Invent: Your blueprint to cloud success
If you’re attending AWS re:Invent in Las Vegas, Dec. 2 - Dec. 6th, visit us for breakout sessions, expert talks, and product demos to learn how to take a unified approach to Infrastructure and Security Lifecycle Management.