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:
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.
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:
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:
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.
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
.
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.
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.
Note that implying not-empty is effectively equivalent to = true
:
!=
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.
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.
Numeric comparison
<, <=, >, >=
Numeric types can be compared to determine whether they are less than, greater than, or equal to another number.
String operations
STARTS WITH
STARTS WITH
Checks if a value starts with a specific string value.
ENDS WITH
ENDS WITH
Checks if a value ends with a specific string value.
MATCHES
MATCHES
Checks if a value matches another string value, where *
matches any character.
MATCHES ANY
MATCHES ANY
Checks if a value matches at least one of many string values, where *
matches any character.
MATCHES REGEX
MATCHES REGEX
Checks if a value matches a regular expression pattern.
MATCHES ANY REGEX
MATCHES ANY REGEX
Checks if a value matches at least one of many regular expression patterns.
MATCHES VERSION
MATCHES VERSION
Checks if a value matches a semantic versioning constraint.
Really follows the Terraform operators to configure version constraints.
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, ingress.cidr_blocks
is allowed to be empty:
Implicit declaration of EVERY
EVERY
In this example, ingress.cidr_blocks
is not allowed to be empty:
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
.
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.
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.
The following guardrail uses HAS
to simplify the requirements above:
The indented expressions that follow HAS
still support the full operation syntax:
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
.
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:
All operators can be used with context answers. Here we use MATCHES
apply the guardrail to all production environments.
Context answers accommodate both single- and multi-select with the same guardrail syntax.
The two statements of each of the following guardrails are equivalent:
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:
However, this guardrail only applies if every value in "environment" is "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.
This works in REQUIRE
statements as well, where the requirement will only apply if the context answer for "environment" exists.
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.
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