TrademarkTrademark
Features
Documentation

OPA Series Part 1: Open Policy Agent and Terraform

This first blog will cover the basics of how to use OPA to evaluate and enforce policy on your Terraform plans.
Ryan FeeJune 2, 2021
OPA Series Part 1: Open Policy Agent and Terraform
Key takeaways
  • Open Policy Agent (OPA) is a declarative policy language used to check Terraform plans and enforce organizational standards before an apply.
  • The workflow is to create a Terraform plan, convert it to JSON with terraform show -json, then run an OPA check against that JSON.
  • An OPA policy declares the terraform package, imports the plan as tfplan, and writes deny rules that flag values that exist in the plan JSON.
  • Scalr formats its plan JSON with tfplan as the input, so example JSON needs tfplan added at the top to work with the sample code.

Open Policy Agent (OPA) is a declarative policy language you can run across your cloud stack to keep deployments under control. The Terraform community has picked it up as a way to check plans and confirm that DevOps teams are deploying within an organization's standards.

This is part one of a four-part series on using OPA with Terraform. It covers the basics: how to evaluate and enforce policy on your Terraform plans. The next three posts go deeper into reading the plan JSON and writing the policies themselves.

Create the Terraform Plan & OPA Policy

The general flow to create an OPA policy and check the Terraform plan is as follows:

Create Terraform Plan -> Convert to JSON -> Run OPA Check

This example will be done against the same format that Scalr creates the JSON with, which has "tfplan" as the input that is read. The reason for this is that Scalr can read objects pertaining to the plan as well as the general run, which is imported as "tfrun". Because of this, you will likely need to add "tfplan" to the beginning of your example JSON to use the sample code below:

{
  "tfplan":{
    "format_version":"0.1",
      ...
        ..
          .

1. Create the Terraform Plan

The Terraform plan step works just like any other plan, but you need to ensure that you save the output so that it can be converted into JSON at a later stage.

terraform plan --out=FILENAME

2. Convert the plan into JSON

Next, you want to convert the plan to JSON so that it can be read by OPA.

terraform show -json FILENAME > FILENAME.json

The result being:

{
   "tfplan":{
      "format_version":"0.1",
      "terraform_version":"0.12.25",
      "planned_values":{
         "root_module":{
            "resources":[
               {
                  "address":"aws_instance.scalr",
                  "mode":"managed",
                  "type":"aws_instance",
                  "name":"scalr",
                  "provider_name":"aws",
                  "schema_version":1,
                  "values":{
                     "ami":"ami-2757f111",
                     "credit_specification":[
                        
                     ],
                     "disable_api_termination":null,
                     "ebs_optimized":null,
                     "get_password_data":false,
                     "hibernation":null,
                     "iam_instance_profile":null,
                     "instance_initiated_shutdown_behavior":null,
                     "instance_type":"t2.nano",
                     "key_name":"my_key",
                     "monitoring":null,
                     "source_dest_check":true,
                     "subnet_id":"subnet-0ebb1058ad7212345",
                     "tags":null,
                     "timeouts":null,
                     "user_data":null,
                     "user_data_base64":null,
                     "vpc_security_group_ids":[
                        "sg-0880cfdc546b123456"
                     ]
                  }
               }
            ]
         }
      },
      "resource_changes":[
         {
            "address":"aws_instance.scalr",
            "mode":"managed",
            "type":"aws_instance",
            "name":"scalr",
            "provider_name":"aws",
            "change":{
               "actions":[
                  "create"
               ],
               "before":null,
               "after":{
                  "ami":"ami-2757f555",
                  "credit_specification":[
                     
                  ],
                  "disable_api_termination":null,
                  "ebs_optimized":null,
                  "get_password_data":false,
                  "hibernation":null,
                  "iam_instance_profile":null,
                  "instance_initiated_shutdown_behavior":null,
                  "instance_type":"t2.nano",
                  "key_name":"my_key",
                  "monitoring":null,
                  "source_dest_check":true,
                  "subnet_id":"subnet-0ebb1058ad7212345",
                  "tags":null,
                  "timeouts":null,
                  "user_data":null,
                  "user_data_base64":null,
                  "vpc_security_group_ids":[
                     "sg-0880cfdc546b12345"
                  ]
               },
               "after_unknown":{
                  "arn":true,
                  "associate_public_ip_address":true,
                  "availability_zone":true,
                  "cpu_core_count":true,
                  "cpu_threads_per_core":true,
                  "credit_specification":[
                     
                  ],
                  "ebs_block_device":true,
                  "ephemeral_block_device":true,
                  "host_id":true,
                  "id":true,
                  "instance_state":true,
                  "ipv6_address_count":true,
                  "ipv6_addresses":true,
                  "metadata_options":true,
                  "network_interface":true,
                  "outpost_arn":true,
                  "password_data":true,
                  "placement_group":true,
                  "primary_network_interface_id":true,
                  "private_dns":true,
                  "private_ip":true,
                  "public_dns":true,
                  "public_ip":true,
                  "root_block_device":true,
                  "secondary_private_ips":true,
                  "security_groups":true,
                  "tenancy":true,
                  "volume_tags":true,
                  "vpc_security_group_ids":[
                     false
                  ]
               }
            }
         }
      ],
      "configuration":{
         "provider_config":{
            "aws":{
               "name":"aws",
               "expressions":{
                  "access_key":{
                     "constant_value":"123456"
                  },
                  "region":{
                     "constant_value":"us-east-1"
                  },
                  "secret_key":{
                     "constant_value":"1245"
                  }
               }
            }
         },
         "root_module":{
            "resources":[
               {
                  "address":"aws_instance.scalr",
                  "mode":"managed",
                  "type":"aws_instance",
                  "name":"scalr",
                  "provider_config_key":"aws",
                  "expressions":{
                     "ami":{
                        "constant_value":"ami-2757f123"
                     },
                     "instance_type":{
                        "constant_value":"t2.nano"
                     },
                     "key_name":{
                        "constant_value":"my_key"
                     },
                     "subnet_id":{
                        "constant_value":"subnet-0ebb1058ad7212345"
                     },
                     "vpc_security_group_ids":{
                        "constant_value":[
                           "sg-0880cfdc546b12345"
                        ]
                     }
                  },
                  "schema_version":1
               }
            ]
         }
      }
   }
}

3. Create the OPA code

Note: Please ensure the plan JSON has tfplan in it before continuing as mentioned at the top of the blog post.

Now that you have the plan converted to JSON, you can write OPA code to check the plan before you run an apply. Given that OPA is policy as code, you can write any policy you want as long as the values you are referring to exist in the Terraform plan JSON file.

In this example, we will start simple by checking to ensure that only approved AWS resources are being created. The example Terraform plan shows that an AWS instance will be created, which is not an approved resource as seen in the OPA policy below. The first step in writing an OPA policy for Terraform is to declare that the package is Terraform. Next, you need to import the tfplan. Last, write the logic to check the JSON.

OPA policy files require the .rego extension, e.g. my_policy.rego

package terraform
 
import input.tfplan as tfplan
 
 
# Allowed Terraform resources
allowed_resources = [
	"aws_security_group",
	#  "aws_instance",
	"aws_s3_bucket"
]
 
 
array_contains(arr, elem) {
	arr[_] = elem
}
 
deny[reason] {
  resource := tfplan.resource_changes[_]
  action := resource.change.actions[count(resource.change.actions) - 1]
  array_contains(["create", "update"], action)  # allow destroy action
 
  not array_contains(allowed_resources, resource.type)
 
  reason := sprintf(
    "%s: resource type %q is not allowed",
    [resource.address, resource.type]
  )
}

4. Check the Terraform plan against the OPA policy

To check the plan, run the following command:

opa eval --format pretty --data FILENAME.rego --input FILENAME.json "data.terraform"

{
   "allowed_resources":[
      "aws_security_group",
      "aws_s3_bucket"
   ],
   "deny":[
      "aws_instance.scalr: resource type \"aws_instance\" is not allowed"
   ]
}

The result being that OPA identified that the Terraform plan will violate the policy based on the creation of an AWS instance resource.

Summary

That's the whole loop. Real policies get more involved, and how you manipulate the plan depends on the tooling you run OPA through, but the steps above are the foundation for checking a Terraform plan with OPA.

The next article in the series digs into writing OPA policies for Terraform and Scalr, with explanations of the OPA language elements you'll reach for most often.

OPA Series Part 2: OPA Logic and Structure for Scalr

Resources

About the author
Ryan Feedirector of platform engineering at Scalr
Ryan Fee is the director of platform engineering at Scalr, with over 15 years of experience improving infrastructure experiences at companies large and small.