Crear restricciones de Terraform

Antes de empezar

Framework de restricciones

gcloud beta terraform vet usa políticas del framework de restricciones, que constan de restricciones y plantillas de restricciones. La diferencia entre ambos es la siguiente:

  • Una plantilla de restricción es como una declaración de función: define una regla en Rego y, opcionalmente, toma parámetros de entrada.
  • Una restricción es un archivo que hace referencia a una plantilla de restricción y define los parámetros de entrada que se le deben enviar y los recursos que abarca la política.

De esta forma, no tendrás que repetir la misma información. Puedes escribir una plantilla de restricción con una política genérica y, a continuación, escribir cualquier número de restricciones que proporcionen diferentes parámetros de entrada o reglas de coincidencia de recursos.

Crear una plantilla de restricción

Para crear una plantilla de restricción, sigue estos pasos:

  1. Recoge datos de muestra.
  2. Escribe Rego.
  3. Prueba tu Rego.
  4. Configura un esqueleto de plantilla de restricción.
  5. Incluye tu Rego.
  6. Configura una restricción.

Recoger datos de muestra

Para escribir una plantilla de restricción, necesitas tener datos de muestra con los que trabajar. Las restricciones basadas en Terraform operan con datos de cambio de recursos, que proceden de la clave resource_changes de JSON de plan de Terraform.

Por ejemplo, tu JSON podría tener este aspecto:

// 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
}

Escribir Rego

Una vez que tengas datos de ejemplo, puedes escribir la lógica de tu plantilla de restricción en Rego. Tu Rego debe tener una regla violations. El cambio de recurso que se está revisando está disponible en input.review. Los parámetros de restricción están disponibles como input.parameters. Por ejemplo, para requerir que los google_compute_address recursos tengan un address_type permitido, escribe lo siguiente:

# 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.change.after.address_type} & allowed_address_types) >= 1
  message := sprintf(
    "Compute address %s has a disallowed address_type: %s",
    [resource.address, resource.change.after.address_type]
  )
  metadata := {"resource": resource.name}
}

Ponle un nombre a la plantilla de restricción.

En el ejemplo anterior se usa el nombre TFComputeAddressAddressTypeAllowlistConstraintV1. Es un identificador único de cada plantilla de restricción. Te recomendamos que sigas estas directrices para elegir nombres:

  • Formato general: TF{resource}{feature}Constraint{version}. Usa CamelCase. Es decir, escribe con mayúscula inicial cada palabra nueva.
  • En el caso de las restricciones de un solo recurso, sigue las convenciones del proveedor de Terraform para asignar nombres a los productos. Por ejemplo, en el caso de google_tags_tag, el nombre del producto es tags, aunque el nombre de la API sea resourcemanager.
  • Si una plantilla se aplica a más de un tipo de recurso, omita la parte del recurso e incluya solo la función (por ejemplo, "TFAddressTypeAllowlistConstraintV1").
  • El número de versión no sigue el formato de semver, sino que es un solo número. De esta forma, cada versión de una plantilla se convierte en una plantilla única.

Te recomendamos que uses un nombre para tu archivo Rego que coincida con el nombre de la plantilla de restricción, pero en formato snake_case. Es decir, convierte el nombre a minúsculas y separa las palabras con _. En el ejemplo anterior, el nombre de archivo recomendado es tf-compute-address-address-type-allowlist-constraint-v1.rego.

Probar tu Rego

Puedes probar tu Rego manualmente con el Rego Playground. Asegúrate de usar datos no sensibles.

Te recomendamos que escribas pruebas automatizadas. Coloca los datos de muestra recogidos en validator/test/fixtures/<constraint filename>/resource_changes/data.json y haz referencia a ellos en tu archivo de prueba de esta forma:

# 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
}

Coloca tu Rego y tu prueba en la carpeta validator de tu biblioteca de políticas.

Configurar un esqueleto de plantilla de restricción

Una vez que tengas una regla Rego que funcione y hayas probado, debes empaquetarla como una plantilla de restricción. Constraint Framework usa definiciones de recursos personalizados de Kubernetes como contenedor de la política Rego.

La plantilla de restricción también define qué parámetros se permiten como entradas de las restricciones mediante el esquema OpenAPI V3.

Usa el mismo nombre para el esqueleto que usaste para tu Rego. En particular:

  • Usa el mismo nombre de archivo que en tu Rego. Ejemplo: tf-compute-address-address-type-allowlist-constraint-v1.yaml
  • spec.crd.spec.names.kind debe contener el nombre de la plantilla
  • metadata.name debe contener el nombre de la plantilla, pero en minúsculas

Coloca el esqueleto de la plantilla de restricción en policies/templates.

Por ejemplo:

# 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

Incluir tu Rego

En este punto, siguiendo el ejemplo anterior, el diseño de tu directorio sería el siguiente:

| 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

Si has clonado el repositorio de la biblioteca de políticas proporcionado por Google, puedes ejecutar make build para actualizar automáticamente tus plantillas de restricciones en policies/templates con el Rego definido en validator.

Configurar una restricción

Las restricciones contienen tres datos que gcloud beta terraform vet necesita para aplicar correctamente las restricciones e informar de las infracciones:

  • severity: low, medium o high
  • match: parámetros para determinar si una restricción se aplica a un recurso concreto. Se admiten los siguientes parámetros de coincidencia:
    • addresses: lista de direcciones de recursos que se incluirán mediante la coincidencia de estilo glob
    • excludedAddresses: (opcional) lista de direcciones de recursos que se van a excluir mediante la coincidencia de estilo glob.
  • parameters: valores de los parámetros de entrada de la plantilla de restricción.

Asegúrate de que kind contenga el nombre de la plantilla de restricción. Te recomendamos que asignes a metadata.name un slug descriptivo.

Por ejemplo, para permitir solo los tipos de dirección INTERNAL con la plantilla de restricción del ejemplo anterior, escribe lo siguiente:

# 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"

Ejemplos de coincidencias:

Coincidencia de dirección Descripción
module.** Todos los recursos de cualquier módulo
module.my_module.** Todo lo que incluye el módulo my_module
**.google_compute_global_forwarding_rule.* Todos los recursos google_compute_global_forwarding_rule de cualquier módulo
module.my_module.google_compute_global_forwarding_rule.* Todos los recursos google_compute_global_forwarding_rule de `my_module`

Si la dirección de un recurso coincide con los valores de addresses y excludedAddresses, se excluye.

Limitaciones

Los datos del plan de Terraform ofrecen la mejor representación disponible del estado real después de aplicar los cambios. Sin embargo, en muchos casos, el estado después de aplicar puede no conocerse porque se calcula del lado del servidor.

La creación de rutas de ancestros de CAI forma parte del proceso de validación de las políticas. Usa el proyecto predeterminado proporcionado para evitar los IDs de proyecto desconocidos. Si no se proporciona ningún proyecto predeterminado, la ruta de ancestro será organizations/unknown de forma predeterminada.

Para inhabilitar la ascendencia desconocida, añade la siguiente restricción:

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: GCPAlwaysViolatesConstraintV1
metadata:
  name: disallow_unknown_ancestry
  annotations:
    description: |
      Unknown ancestry is not allowed; use --project=<project> to set a
      default ancestry
spec:
  severity: high
  match:
    ancestries:
    - "organizations/unknown"
  parameters: {}

Recursos compatibles

Puedes crear restricciones de cambio de recursos para cualquier recurso de Terraform de cualquier proveedor de Terraform.