Skip to content
Daniel Schmidt's Blog
Go back

Terraform Policy: Adding guard rails to Terraform

Content

TL;DR

Terraform Policy is the new Policy as Code solution that allows you to express Policies for Terraform in HCL and integrates deeply with Terraform and HCP Terraform.

Who can use Terraform Policy?

Terraform Policy is part of HCP Terraform, where it is currently available in public beta, and is not part of Terraform itself. See the official blog post and docs for more information. You have to at least be on the Standard tier of IBM HCP Terraform to use Terraform Policy.

Writing Policies

Let’s start by writing a simple policy to forbid aws_redshift_clusters from being publicly accessible:

# redshift.policy.hcl
policy {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 4.0.0, < 7.0.0"
    }
  }
}

resource_policy "aws_redshift_cluster" "public_access_check" {
  enforcement_level = "mandatory"
  enforce {
    condition = attrs.publicly_accessible == false
    error_message = "Redshift cluster is publicly accessible"
  }
}

When this policy is added to a workspace / stack it will run as part of plan and apply and enforce that no aws_redshift_cluster has publicly_accessible set to true. Terraform Policy does not evaluate the configuration, it evaluates the planned / applied values, so if we have an optional, computed attribute with a default value that goes against our policies we will still receive an error.

Policy Types

There are three policy types we currently support, each governing its respective domain:

Provider and Module policies can run at init time to ensure no dangerous / unapproved provider / module can be downloaded. This works only if the provider configuration is not inspected, so if no attrs are accessed within the policy. If you have multiple enforce blocks, the ones that can will run at init time, the others will run at plan time.

All policy types evaluate during plan / apply again. We strive to give feedback about the policy results as early as possible in the process. There might be times during plan when we have unknown values that hinder us from evaluating a policy at plan time. All policies are re-run during apply, when all values are known, so at that point we can be sure that the final state is compliant with all policies.

In general policies match the type label (first string after the policy type) to find which providers / modules / resources they apply to. We allow a glob-like pattern, e.g. resource_policy "aws_s3_*" "..." which will match all resources of this type to help when there is a general attribute structure for a certain family of resources.

Accessing Data

Terraform Policy works off the data that Terraform has in each phase:

To access the configured values for a provider config / module declaration / resource instance one can use attrs. Resource policies also allow for prior_attrs access if you want to check what has been applied before (only available when create is not part of the policy’s operations).

There is also meta accessible for all policies, please see the docs for more information.

Blocking a provider download at init time

When your provider policy is not using attrs with an enforce block, this enforce block runs at init time and can block the download of unauthorized providers, e.g. this policy forces the official aws provider to be the only one and to be version 5 and higher.

# providers.policy.hcl
provider_policy "aws" "standard_configuration" {
  enforcement_level = "mandatory"

  enforce {
    condition     = meta.source == "hashicorp/aws"
    error_message = "Only the official HashiCorp AWS provider is permitted."
  }

  enforce {
    condition     = core::semverconstraint(meta.version, ">= 5.0.0")
    error_message = "The AWS provider version must be at least 5.0.0."
  }
}

If we had a third enforce block using attrs, only that one would not run during init. You can see the core::semverconstraint is a local function call. For all functions see the docs.

Accessing Live Data

Sometimes you need data provided by a data source to make the proper assumptions in a policy. A very common example would be to retrieve a current AMI image:

# instances.policy.hcl
locals {
  amazon_linux_ami = core::getdatasource("aws_ami", {
    most_recent = true
    owners      = ["amazon"]
    filter = {
      name   = "name"
      values = ["amzn2-ami-hvm-*-x86_64-gp2"]
    }
  })
}

resource_policy "aws_instance" "approved_ami" {
  enforce {
    condition     = attrs.ami == local.amazon_linux_ami.id
    error_message = "Instance must use the approved Amazon Linux 2 AMI: ${local.amazon_linux_ami.id}"
  }
}

Fun fact: it does not matter if you use a global local or a local within a policy block, they are re-evaluated for each aws_instance we find. We do cache the core::getdatasource call though, as long as you don’t use attrs in there the real API is only hit once.

If you just need to make a plain HTTP GET request you can also use core::gethttprequest, the value is not cached though and will re-run for every policy evaluation.

Accessing other resources’ data

It is quite a common occurrence that what you want to enforce might span multiple connected resources, for example ensuring an aws_s3_bucket_server_side_encryption_configuration is attached to each aws_s3_bucket.

# s3.policy.hcl
resource_policy "aws_s3_bucket" "require_encryption_config" {
  locals {
    ssec = core::getresources(
        "aws_s3_bucket_server_side_encryption_configuration",
        {
            bucket = attrs.id
        }
    )
  }

  enforce {
    condition     =
      core::length(local.ssec) > 0 || core::try(attrs.tags["Environment"], "") == "Development"
    error_message =
     "All S3 buckets need a aws_s3_bucket_server_side_encryption_configuration (except for development buckets)"
  }
}

core::getresources takes a type name and a filter map. It will return a list of all resources of this type that match the values in the filter. If there are unknowns in the filter or in the filtered resources we will defer the policy to after apply when everything is guaranteed to be known.

Other Features

There are a bunch of other more specialized features like operation-based policies and filters (e.g. used in combination with a wildcard for the policy type like "aws_s3_*"). There is also a testing language for Terraform policy that allows you to verify your policy meets your expectations.

If you are interested in a deeper dive into any of those topics, leave me a comment.

How to migrate from Sentinel / OPA?

The go-to solution for this is the terraform-policy skill to be used in your favorite agent. It is a battle-tested solution that can migrate policies and provide testing for these policies as well.

How do I enforce my policies?

The same way you would enforce Sentinel or OPA policies in HCP Terraform: Create Policy sets and add them to your workspace / stack. You can also make use of the pre-written policies in HCP Terraofrm.

Please leave a comment

Do you have feedback around Terraform Policy or do you want to hear about a specific topic around Terraform / Software Development / Language Design / Infrastructure as Code? Please let me know in the comments below.


Share this post:

Related Posts

Daniel Schmidt
Daniel Schmidt

Terraform Core Engineer at HashiCorp. Designing and implementing the Terraform language and runtime.



Loading newsletter signup...



Next Post
What's new in Terraform 1.15?