Part one in this series provided an overview of developing and testing OPA policies. This article provides a more detailed guide to writing OPA policies for Terraform for use with Scalr. It provides commonly used OPA expressions and explains the specific implementation of OPA used by Scalr.
Most of the examples herein are based on the Terraform plan data, tfplan. A detailed explanation of this data is provided in part three of this series of articles.
In part four, we are providing a series of simple templates that implement a number of common policy requirements
NOTE: This is not a comprehensive guide and does not cover every possible part of the OPA language. However it should provide enough information to enable development of the majority of policies for Terraform and Scalr.
How OPA Works
The basic concept of OPA is as follows.
“Evaluate expressions against some input data and generate some output”
In Scalr’s use of OPA the input is the tfplan and tfrun data, and the required output Scalr is looking for is an array of strings generated by rules named “deny”. e.g.
You can read more about OPA inputs here. There is an overview of the tfplan and tfrun data in the Scalr documentation, and the next article in this series will provide a detailed description of this data and its use with OPA.
If an OPA policy produces any output then Scalr considers the policy check to have been violated.
To be clear, OPA itself does not decide whether a policy has been violated or not. OPA simply evaluates the policy and returns any output from the rules. It is up to the calling system to decide how to interpret that output, and in Scalr any output from a rule called “deny” means the policy has been violated.
This means that all policies used in Scalr must contain at least one “deny” rule as shown in this pseudo example.
The input data (JSON):
This simple example is a good guide to how OPA works. When a policy is run/evaluated each rule called “deny” is processed. Any rule where all expressions are TRUE will return the text in the header. If we evaluate this policy the output is as follows.
Only the first “deny” rule produced output because it is the only rule where every expression has been evaluated to TRUE, i.e. there is no message in the array for “french”. Processing of a rule terminates as soon as an expression result is FALSE or UNDEFINED.
Note that the output text is assigned to the header of the rule within the body using sprintf(). The reason text can be hardcoded into the header [“Hello world”] but this approach allows for dynamic reason text to be generated.
We will now step through a complete example that uses Terraform Plan input data (tfplan) and evaluates that all instances are using one of the allowed IAM Instance Profiles.
The input data (abbreviated) looks like this.
Within tfplan is an array of resource_changes. There would be an element in the array for each instance the plan and change.after.iam_instance_profile is the planned value.
The complete policy is as follows.
Arrays and Loops
As seen above the “tfplan” data will usually contain arrays, especially if multiple resources exist in the Terraform configuration. Arrays can also be declared in the OPA document.
For example in the tfplan data the resource changes are an array (one element for each resource), as denoted by the [.
You might also declare arrays in the policy, for example to provide a list of allowed values to be checked against.
OPA has a construct you can use to process all elements of an array where the start of a loop is implied and all the array index handling is done under the covers by OPA. This is done using the [_] array index.
This means iterate through allowed_iam_profiles assigning each value to variable profile in turn, and then process all the following expressions in the rule for each value of profile.
Note that this construct will process ALL elements in the array. If any iteration hits a FALSE/UNDEFINED expression the next iteration will start. However if none of the loop iterations yields TRUE for all following expressions rule not return any output.
In this example the policy needs to iterate through all the aws_instances in the tfplan data to check the instance types. This extract from the data looks like this.
The OPA Policy
Only 2 elements of the array are printed due to the call to array_contains() only matching “t2” and “t3”
The tfrun data can contain multidimensional arrays.
In this example the tfplan data has an array of “resource_changes” and within that there are instances that have arrays of “ebs_block_devices”.
Multiple [_] array indexes can be specified in the same expression or rule which will cause loops within loops for each array in the structure.
Comprehensions are used to build arrays from a series of expressions and look like this.
Of necessity the expressions are usually some form of iteration.
This example looks for instance types starting with “t3” and adds them to the array. As with rules all expressions must be TRUE for a value to be added to the array.
This article explained OPA in a level of detail that will allow you to start writing policies for Terraform.
In the next article in the series, we take a deep dive into the Terraform plan JSON and how each section of the data can be used within OPA.
OPA Series Part 3: How to analyze the JSON plan