Writing your own Guardrails
Writing Guardrails with our policy language, called "Really"
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
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
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.
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 =
!=
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
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]
CONTAINS
CONTAINS
Checks if a list of values includes a specific value.
This is different to IN
because the list is on the left-hand side here, so the constraint is on the list rather than the value contained.
GUARDRAIL "Ensure security group ingress CIDR includes proxy IP"
WHEN aws_security_group
REQUIRE ingress.cidr_blocks CONTAINS "203.0.113.0/24"
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
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
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
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
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
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
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
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
LENGTH OF
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
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
EVERY
In this example, both ingress
and cidr_blocks
are allowed to be empty because EVERY
is mentioned explicitly. This guardrail would pass for every aws_security_group
that either has no ingress
or no cidr_blocks
otherwise.
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
EVERY
In this example, ingress
is not allowed to be empty, so for every aws_security_group
there must be at least one ingress
. This guardrail will fail for every aws_security_group
that either has no ingress
, or otherwise a from_port
that is "80"
.
GUARDRAIL "Do not allow unsecured ingress ports in security groups"
WHEN aws_security_group
REQUIRE ingress.from_port != "80"
SOME
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
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
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
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"
This can be used to control iteration when an operation applies to a list-like type, such as CONTAINS
.
Consider the guardrail below. The expression SOME ingress.cidr_blocks
would iterate ingress
as well as cidr_blocks
due to the leaf-unrolling behavior of SOME
. This effectively produces a string on the left-hand side, given that cidr_blocks
is a list of strings. The guardrail is therefore invalid because CONTAINS
expects a list-like type on the left-hand side, not a string.
GUARDRAIL "Invalid!"
WHEN aws_security_group
REQUIRE SOME ingress.cidr_blocks CONTAINS "203.0.113.0/24"
The use of HAS
separates the iteration to ensure that at least one ingress
has a cidr_blocks
list that includes "203.0.113.0/24"
.
GUARDRAIL "Ensure ingress CIDR includes proxy IP"
WHEN aws_security_group
REQUIRE SOME ingress HAS cidr_blocks CONTAINS "203.0.113.0/24"
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
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
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
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