
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.
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 uses the same format Scalr creates the JSON with, which reads "tfplan" as the input. Scalr does this so it can read objects from the plan as well as the general run, which it imports as "tfrun". So you'll probably need to add "tfplan" to the start 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 make sure you save the output so you can convert it into JSON later.
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
tfplanin it before continuing as mentioned at the top of the blog post.
Now that the plan is converted to JSON, you can write OPA code to check it before you run an apply. Since OPA is policy as code, you can write any policy you want, as long as the values you refer to exist in the Terraform plan JSON file.
In this example we'll start simple and check that only approved AWS resources are being created. The example Terraform plan creates an AWS instance, which isn't an approved resource 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 import the tfplan. Then you 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"
]
}OPA flagged that the Terraform plan violates the policy because it creates an AWS instance resource.
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
