OPA Series Part 1: Open Policy Agent and Terraform

Author
Ryan Fee Sep 22, 2020 • 10 Min Read
Share:

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.

This is part one of a three blog series on OPA usage with Terraform. This first blog will cover the basics of how to use OPA to evaluate and enforce policy on your Terraform plans. The next two blogs will cover more details around how to evaluate the plan JSON and write the actual OPA policies.

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

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

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 all there is to it! The actual OPA code can get much more complex based on your use and you can manipulate the plan based on the tooling you are using, but these are the basics to creating OPA checks against your Terraform plan.

In the next article in the series, we take a detailed look at writing OPA policies for Terraform and Scalr, including explanations of the more commonly used OPA language elements.

OPA Series Part 2: OPA Logic and Structure for Scalr

Resources

Ready to get started?

Scalr is free for up to 5 users