Open Policy Agent (OPA) is a declarative policy language that can be used across your cloud ecosystem to ensure controlled deployments. It has increased in popularity with the Terraform community as a way to check Terraform plans and ensure DevOps teams are deploying according to organizational standards.
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.
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.
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.
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.
profile := allowed_iam_profiles[_]
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.