Writing Guardrails with Really

Authoring Your Own Guardrails

Authoring Your Own Guardrails

Resourcely uses a domain-specific policy language called Really to describe configuration rules referred to as "guardrails". Assuming Terraform configuration, here is an example of a guardrail that requires a specific prefix on all S3 buckets:

GUARDRAIL "Ensure all S3 buckets are prefixed by org"
  WHEN aws_s3_bucket
    REQUIRE bucket STARTS WITH "resourcely-"

The first line of a guardrail is the GUARDRAIL declaration, which describes the intent of the guardrail. Underneath the declaration there are one or more conditional WHEN expressions, each with one or more indented REQUIRE statements. Requirements are relative to the entity matched by the enclosing WHEN expression. All requirements are relative to a specific WHEN, and every WHEN must have at least one requirement.

This general structure makes it easy to express the best practices of your organization without the need to write complicated programs. Consider writing guardrails as specifically as possible to more easily enable and disable specific policies without the need to edit guardrail text.

Matching entities with WHEN

While most syntax is consistent throughout a guardrail, WHEN expressions uniquely support leading references to match entities by type, such as aws_s3_bucket or module. For example, WHEN aws_s3_bucket.bucket refers to the bucket property of all aws_s3_bucket resources in your configuration.

Some policies should only apply when either creating a new resource or deleting an existing resource. Specifying either CREATING or DELETING after WHEN determines whether the requirements that follow should apply. Omitting this keyword will apply the requirements regardless of the nature of the proposed changes.

GUARDRAIL "Disallow the deletion of S3 buckets used for backups"
  WHEN DELETING aws_s3_bucket
    REQUIRE bucket NOT MATCHES "backup-*"

Wildcards

Resource types support wildcards to match more than one resource simultaneously. For example, WHEN aws* applies to all resource types that start with "aws", however limited to those that are compatible with the properties referenced in the underlying REQUIRE statements.

The following guardrail would fail for any AWS resource that supports a tags property, but which had no tags in the configuration:

GUARDRAIL "Ensure AWS resources that support tags have tags"
  WHEN aws*
    REQUIRE tags

Mixed resource types

You may reference multiple resource types in a single expression as long as all the requirements of the guardrail are compatible with the properties of every matched resource. For example, the following guardrail would require tags only on aws_s3_bucket and aws_instance resources:

GUARDRAIL "Ensure S3 buckets and EC2 instances have tags"
  WHEN aws_s3_bucket OR aws_instance
    REQUIRE tags

Modules

Really supports conditions and constraints on the Terraform modules in your configuration, specifically the properties source and version. For Resourcely to be aware of the modules resolved by Terraform, it is necessary to pass a --modules_file to the Resourcely CLI.

Guardrails that reference modules will not behave as expected without this metadata.

GUARDRAIL "Ensure S3 bucket modules use at least version 3.5, but not 4"
  WHEN module.source MATCHES "*/terraform-aws-modules/s3-bucket/aws"
    REQUIRE version MATCHES VERSION "~> 3.5"

Operations

Really supports a collection of operations to expression conditional logic and requirements. Operations can be negated using the NOT keyword before the operator.

Existence and emptiness

EXISTS

Checks if a property is not missing, i.e. not null.

GUARDRAIL "Ensure EC2 instances have a team tag"
  WHEN aws_instance
    REQUIRE tags.team EXISTS

There is however a subtle difference between "exists" and "not empty". Properties with values like "", [] and false do exist but are considered empty. This distinction can be realized by dropping the EXISTS keyword to imply non-emptiness.

GUARDRAIL "Ensure EC2 instances have at least one tag"
  WHEN aws_instance
    REQUIRE tags

The following table shows values that are considered empty for each data type in Really.

Data Type
Empty value

string

""

bool

false

int

0

float

0.0

list / set

[]

map / record

{}

Equality and membership

=

Checks if a value is equal to a another value.

GUARDRAIL "Enforce the configuration of force_destroy for all S3 buckets"
  WHEN aws_s3_bucket
    REQUIRE force_destroy = true
GUARDRAIL "Ensure that all encrypted AWS EBS volumes are configured with a KMS"
  WHEN aws_ebs_volume.encrypted = true
    REQUIRE kms_key_id

Note that implying not-empty is effectively equivalent to = true:

GUARDRAIL "Ensure that all encrypted AWS EBS volumes are configured with a KMS"
  WHEN aws_ebs_volume.encrypted
    REQUIRE kms_key_id

!= and NOT =

Checks if a value is not equal to another value.

Note that inequality passes for missing values because null is not equal to anything. For example, REQUIRE bucket != "test" would pass when there was no bucket at all. Consider also checking EXISTS where some defined value is required.

GUARDRAIL "Ensure that S3 buckets are not named 'test'"
  WHEN aws_s3_bucket
    REQUIRE bucket != "test"
GUARDRAIL "Ensure that S3 buckets are not named 'test'"
  WHEN aws_s3_bucket
    REQUIRE bucket NOT = "test"

IN

Checks if a value exists within a list of values.

The data type of the value to search for must match the data type of the values in the list.

GUARDRAIL "Ensure EC2 instance types are micro"
  WHEN aws_instance
    REQUIRE instance_type IN ["t2.micro", "t3.micro", "t3a.micro", "t4g.micro"]
GUARDRAIL "Ensure EBS volume size is a supported square number value"
  WHEN aws_ebs_volume
    REQUIRE size IN [8, 16, 32, 64]

Numeric comparison

<, <=, >, >=

Numeric types can be compared to determine whether they are less than, greater than, or equal to another number.

GUARDRAIL "Ensure EBS volume size is at least 32 GB"
  WHEN aws_ebs_volume
    REQUIRE size >= 32

String operations

STARTS WITH

Checks if a value starts with a specific string value.

GUARDRAIL "Ensure S3 bucket names start with the organization prefix"
  WHEN aws_s3_bucket
    REQUIRE bucket STARTS WITH "resourcly-"

ENDS WITH

Checks if a value ends with a specific string value.

GUARDRAIL "Ensure S3 bucket names end with '-bucket'"
  WHEN aws_s3_bucket
    REQUIRE bucket ENDS WITH "-bucket"

MATCHES

Checks if a value matches another string value, where * matches any character.

GUARDRAIL "Ensure S3 bucket names have three components"
  WHEN aws_s3_bucket
    REQUIRE bucket MATCHES "*-*-*"

MATCHES ANY

Checks if a value matches at least one of many string values, where * matches any character.

GUARDRAIL "Ensure S3 bucket names consist of an environment and name"
  WHEN aws_s3_bucket
    REQUIRE bucket MATCHES ANY ["dev-*", "staging-*", prod-*"]

MATCHES REGEX

Checks if a value matches a regular expression pattern.

GUARDRAIL "Ensure S3 bucket names consist of an environment and name"
  WHEN aws_s3_bucket
    REQUIRE bucket MATCHES REGEX "(dev|prod)-[a-z]+"

MATCHES ANY REGEX

Checks if a value matches at least one of many regular expression patterns.

GUARDRAIL "Ensure S3 bucket names consist of an environment and name"
  WHEN aws_s3_bucket
    REQUIRE bucket MATCHES ANY REGEX ["(dev|prod)-[a-z]+", "test-[0-9]+"]

MATCHES VERSION

Checks if a value matches a semantic versioning constraint.

Really follows the Terraform operators to configure version constraints.

GUARDRAIL "Ensure S3 bucket modules use at least version 3.5, but not 4"
  WHEN module.source MATCHES "*/terraform-aws-modules/s3-bucket/aws"
    REQUIRE version MATCHES VERSION "~> 3.5"

Property modifiers

In some cases, an expression applies to some characteristic of a value rather than the value itself. For example, the length of a string or the number of items in a list.

LENGTH OF

Data Type
Behavior

string

Number of characters

bool

-

int / float

-

list / set

Number of values

map / record

Number of keys

Iteration

Many use cases involve checks on list values, for example aws_instance.security_groups or aws_security_group.ingress. Really supports list operations using the keywords EVERY, SOME, and NO.

EVERY

Iterates over a list and ensures that all values of that list satisfy the operation that follows it. EVERY passes for empty lists, however when referencing a list property without an explicit iteration operator, the result is effectively the same as EVERY except that the list is then required to not be empty.

Explicit declaration of EVERY

In this example, ingress.cidr_blocks is allowed to be empty:

GUARDRAIL "Do not allow 0.0.0.0/0 in ingress"
  WHEN aws_security_group
    REQUIRE EVERY ingress.cidr_blocks != "0.0.0.0/0"

Implicit declaration of EVERY

In this example, ingress.cidr_blocks is not allowed to be empty:

GUARDRAIL "Do not allow 0.0.0.0/0 in ingress"
  WHEN aws_security_group
    REQUIRE ingress.cidr_blocks != "0.0.0.0/0"

SOME

Iterates over a list and ensures that at least one value of that list satisfies the operation that follows it. SOME does not pass for empty lists, since at least one value must satisfy the operation.

NO

Iterates over a list and ensures that none of the values of that list satisfy the operation that follows it. NO passes for empty lists, being effectively equivalent to EVERY combined with NOT.

GUARDRAIL "Do not allow port 22 to be open"
  WHEN aws_security_group
    REQUIRE NO ingress.from_port = 22

Multi-property iteration

Referencing list properties along the way inherits and applies the iteration operator that precedes the reference. For example, SOME aws_security_group.ingress.cidr_blocks != "0.0.0.0/0" requires that there is at least one ingress that has at least one value in cidr_blocks that is not equal to "0.0.0.0/0", for every aws_security_group resource.

Logical expressions

AND, OR

Operations can be separated by the keywords AND or OR to join operations together.

GUARDRAIL "Ensure S3 buckets start with a known environment name"
  WHEN aws_s3_bucket
    REQUIRE bucket STARTS WITH "dev-" OR bucket STARTS WITH "prod-"
GUARDRAIL "Ensure S3 buckets have a bucket name and not a bucket prefix"
  WHEN aws_s3_bucket
    REQUIRE bucket AND NOT bucket_prefix

HAS

When dealing with objects, the HAS keyword makes all indented expressions that follow inherit the scope of the object. This makes it easier to read and write requirements that operate on multiple properties of the same object.

GUARDRAIL "Ensure EC2 instances have all standard tags defined"
    WHEN aws_instance
        REQUIRE tags.team
        REQUIRE tags.env
        REQUIRE tags.owner
        REQUIRE tags.pii

The following guardrail uses HAS to simplify the requirements above:

GUARDRAIL "Ensure EC2 instances have all standard tags defined"
    WHEN aws_instance
        REQUIRE tags HAS
          team
          env
          owner
          pii

The indented expressions that follow HAS still support the full operation syntax:

GUARDRAIL "Ensure EC2 instances must have valid team and environment tags"
    WHEN aws_instance
        REQUIRE tags HAS
          team team = "admins" OR team ENDS WITH "-team"
          env IN ["dev", prod"]

When the left-hand side of a HAS expression is a list, the expressions that follow apply to all members of that list. This is effectively the same behavior as the implied EVERY quantifier, where the property is required to also not be empty.

The following guardrail uses EVERY to explicitly allow an empty list on the left-hand side of a HAS.

GUARDRAIL "Ensure all AWS security groups are configured to permit ingress traffic over SSL/TLS"
  WHEN aws_security_group
    REQUIRE ingress HAS
      from_port = 443
      to_port = 443
      protocol = "tcp"

Parentheses

Really generally supports the use of ( and ) to group logical expressions and avoid ambiguous grammar.

Context

Guardrails can reference context answers using the CONTEXT keyword followed by the label of the context question. The CONTEXT keyword may be used on either side of an operation within WHEN, but only on the right-hand side within REQUIRE.

This guardrail we use environment from the global context:

GUARDRAIL "Ensure S3 bucket names start with an environment"
  WHEN aws_s3_bucket
    REQUIRE bucket STARTS WITH CONTEXT environment

All operators can be used with context answers. Here we use MATCHES apply the guardrail to all production environments.

GUARDRAIL "Ensure S3 buckets in production start with the organization name"
  WHEN aws_s3_bucket AND CONTEXT environment MATCHES "prod-*"
    REQUIRE bucket STARTS WITH "resourcely-"

Context answers accommodate both single- and multi-select with the same guardrail syntax.

The two statements of each of the following guardrails are equivalent:

GUARDRAIL "Ensure bucket name starts with environment"
  WHEN aws_s3_bucket
    REQUIRE bucket STARTS WITH CONTEXT environment
  WHEN aws_s3_bucket
    REQUIRE bucket STARTS WITH SOME CONTEXT environment
GUARDRAIL "Ensure bucket names match one of the options in the bucket list"
  WHEN aws_s3_bucket
    REQUIRE bucket = CONTEXT bucket_list
  WHEN aws_s3_bucket
    REQUIRE bucket IN CONTEXT bucket_list

You can use quantifiers like EVERY, SOME and NO with CONTEXT on the left-hand side, within WHEN. The two REQUIRE statements of the following guardrail are equivalent:

GUARDRAIL "Ensure bucket names in production are prefixed with 'prod-'"
  WHEN aws_s3_bucket AND CONTEXT environment = "prod"
    REQUIRE bucket STARTS WITH "prod-"
  WHEN aws_s3_bucket AND SOME CONTEXT environment = "prod"
    REQUIRE bucket STARTS WITH "prod-"

However, this guardrail only applies if every value in "environment" is "prod":

GUARDRAIL "Ensure bucket names in production are prefixed with 'prod-'"
  WHEN aws_s3_bucket AND EVERY CONTEXT environment = "prod"
    REQUIRE bucket STARTS WITH "prod-"

OPTIONAL CONTEXT

By default, guardrails will not pass when a context answer is missing. However, this behavior might be too strict in some cases, such as when intentionally not adding context answers to all .resourcely.yaml config roots, or when using the existence of a context answer conditionally.

The OPTIONAL CONTEXT syntax disables this behavior, effectively false in WHEN and a no-op in REQUIRE.

The following guardrail will not apply the requirement when the context answer for "environment" is missing.

GUARDRAIL "Ensure S3 buckets start with 'prod' in production"
  WHEN aws_s3_bucket AND OPTIONAL CONTEXT environment = "prod"
    REQUIRE bucket STARTS WITH "prod"

This works in REQUIRE statements as well, where the requirement will only apply if the context answer for "environment" exists.

GUARDRAIL "Ensure S3 buckets start with the environment, if known"
  WHEN aws_s3_bucket
    REQUIRE bucket STARTS WITH OPTIONAL CONTEXT environment

Overrides and approvals

Really usually operates in a domain where configuration changes are proposed and requirements are either satisfied or not. This is typically somewhere in a continuous-integration pipeline running against a specific pull request. When requirements are not satisfied, the guardrail becomes a gate that requires an approval to permit the proposed changes.

OVERRIDE WITH APPROVAL

Guardrails have a default approver group, but also support the ability to override the default approver group. This group can approve an override to allow the violating config to be merged.

GUARDRAIL "Private S3 buckets require an approval from admins"
  WHEN aws_s3_bucket
    REQUIRE bucket NOT MATCHES "private-*"
  OVERRIDE WITH APPROVAL @admins

REQUIRE APPROVAL

Approvals can be also be required explicitly using REQUIRE APPROVAL. This sort of guardrail does not define requirements that a configuration change might violate. Instead, the approval itself is the requirement. For example, there is nothing wrong with a large RDS instance, but the budget team might want to approve each one.

Last updated