Create Terraform constraints

Before you begin

Constraint Framework

gcloud beta terraform vet uses Constraint Framework policies, which consist of constraints and constraint templates. The difference between the two is as follows:

  • A constraint template is like a function declaration; it defines a rule and optionally takes variables as inputs.
  • A constraint is a file that references a constraint template and defines the values to use with it.

Create a constraint template

1. Collect sample data

In order to write a constraint template, you need to have sample data to operate on. Terraform-based constraints operate on resource change data, which comes from the resource_changes key of Terraform plan JSON.

For example, your JSON might look like this:

// tfplan.json
{
  "format_version": "0.2",
  "terraform_version": "1.0.10",
  "resource_changes": [
    {
      "address": "google_compute_address.internal_with_subnet_and_address",
      "mode": "managed",
      "type": "google_compute_address",
      "name": "internal_with_subnet_and_address",
      "provider_name": "registry.terraform.io/hashicorp/google",
      "change": {
        "actions": [
          "create"
        ],
        "before": null,
        "after": {
          "address": "10.0.42.42",
          "address_type": "INTERNAL",
          "description": null,
          "name": "my-internal-address",
          "network": null,
          "prefix_length": null,
          "region": "us-central1",
          "timeouts": null
        },
        "after_unknown": {
          "creation_timestamp": true,
          "id": true,
          "network_tier": true,
          "project": true,
          "purpose": true,
          "self_link": true,
          "subnetwork": true,
          "users": true
        },
        "before_sensitive": false,
        "after_sensitive": {
          "users": []
        }
      }
    }
  ],
  // other data
}

2. Write Rego

After you have sample data, you can write the logic for your constraint template in Rego. Your Rego must have a violations rule. The resource change being reviewed is available as input.review. Constraint parameters are available as input.parameters. For example, to require that google_compute_address resources have an allowed address_type, write:

# validator/tf_compute_address_address_type_allowlist_constraint_v1.rego
package templates.gcp.TFComputeAddressAddressTypeAllowlistConstraintV1

violation[{
  "msg": message,
  "details": metadata,
}] {
  resource := input.review
  resource.type == "google_compute_address"

  allowed_address_types := input.parameters.allowed_address_types
  count({resource.after.address_type} & allowed_address_types) >= 1
  message := sprintf(
    "Compute address %s has a disallowed address_type: %s",
    [resource.address, resource.after.address_type]
  )
  metadata := {"resource": resource.name}
}

Name your constraint template

The previous example uses the name TFComputeAddressAddressTypeAllowlistConstraintV1. This is a unique identifier for each constraint template. We recommend following these naming guidelines:

  • General format: TF{resource}{feature}Constraint{version}. Use CamelCase. (In other words, capitalize each new word.)
  • For single-resource constraints, follow the Terraform provider's conventions for product naming. For example, for google_tags_tag the product name is tags even theough the API name is resourcemanager.
  • If a template applies to more than one type of resource, omit the resource part and only include the feature (example: "TFAddressTypeAllowlistConstraintV1").
  • The version number does not follow semver form; it is just a single number. This effectively makes every version of a template an unique template.

We recommend using a name for your rego file that matches the constraint template name, but using snake_case. In other words, convert the name to lowercase separate words with _. For the previous example, the recommended filename is tf_compute_address_address_type_allowlist_constraint_v1.rego

3. Test your Rego

You can test your Rego manually with the Rego Playground. Make sure to use non-sensitive data.

We recommend writing automated tests. Put your collected sample data in validator/test/fixtures/<constraint filename>/resource_changes/data.json and reference it in your test file like this:

# validator/tf_compute_address_address_type_allowlist_constraint_v1_test.rego
package templates.gcp.TFComputeAddressAddressTypeAllowlistConstraintV1

import data.test.fixtures.tf_compute_address_address_type_allowlist_constraint_v1_test.resource_changes as resource_changes

test_violation_with_disallowed_address_type {
  parameters := {
    "allowed_address_types": "EXTERNAL"
  }
  violations := violation with input.review as resource_changes[_]
    with input.parameters as parameters
  count(violations) == 1
}

Place your rego and your test in the validator folder in your policy library.

4. Set up a constraint template skeleton

After you have a working and tested Rego rule, you must package it as a constraint template. Constraint Framework uses Kubernetes Custom Resource Definitions as the container for the policy Rego.

The constraint template also defines what parameters are allowed as inputs from constraints, using the OpenAPI V3 schema.

Use the same name for the skeleton as you used for your rego. In particular:

  • Use the same filename as for your rego. Example: tf_compute_address_address_type_allowlist_constraint_v1.yaml
  • spec.crd.spec.names.kind must contain the template name
  • metadata.name must contain the template name, but lower-cased

Place the constraint template skeleton in policies/templates.

For the example above:

# policies/templates/tf_compute_address_address_type_allowlist_constraint_v1.yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: tfcomputeaddressaddresstypeallowlistconstraintv1
spec:
  crd:
    spec:
      names:
        kind: TFComputeAddressAddressTypeAllowlistConstraintV1
      validation:
        openAPIV3Schema:
          properties:
            allowed_address_types:
              description: "A list of address_types allowed, for example: ['INTERNAL']"
              type: array
              items:
                type: string
  targets:
    - target: validation.resourcechange.terraform.cloud.google.com
      rego: |
            #INLINE("validator/tf_compute_address_address_type_allowlist_constraint_v1.rego")
            #ENDINLINE

5. Inline your rego

At this point, following the previous example, your directory layout looks like this:

| policy-library/
|- validator/
||- tf_compute_address_address_type_allowlist_constraint_v1.rego
||- tf_compute_address_address_type_allowlist_constraint_v1_test.rego
|- policies
||- templates
|||- tf_compute_address_address_type_allowlist_constraint_v1.yaml

If you cloned the Google-provided policy-library repository, you can run make build to automatically update your constraint templates in policies/templates with the rego defined in validator.

6. Set up a constraint

Constraints contain three pieces of information that gcloud beta terraform vet needs to properly enforce and report violations:

  • severity: low, medium, or high
  • match: parameters for determining if a constraint applies to a particular resource. The following match parameters are supported:
    • addresses: A list of resource addresses to include using glob-style matching
    • excludedAddresses: (Optional) A list of resource addresses to exclude using glob-style matching.
  • parameters: Values for the constraint template's input parameters.

Make sure that kind contains the constraint template name. We recommend setting metadata.name to a descriptive slug.

For example, to only allow INTERNAL address types using the previous example constraint template, write:

# policies/constraints/tf_compute_address_internal_only.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: TFComputeAddressAddressTypeAllowlistConstraintV1
metadata:
  name: tf_compute_address_internal_only
spec:
  severity: high
  match:
    addresses:
    - "**"
  parameters:
    allowed_address_types:
    - "INTERNAL"

Matching examples:

Address matcher Description
`module.**` All resources in any module
`module.my_module.**` Everything in module `my_module`
`**.google_compute_global_forwarding_rule.*` All google_compute_global_forwarding_rule resources in any module
`module.my_module.google_compute_global_forwarding_rule.*` All google_compute_global_forwarding_rule resources in `my_module`

If a resource address matches values in addresses and excludedAddresses, it is excluded.

Limitations

Terraform plan data gives the best available representation of actual state after apply; however, in many cases the state after apply may not be known because it is calculated on the server side.

Supported resources

You can create resource change constraints for any resources from Terraform's google and google-beta providers.