Créer des contraintes Terraform

Avant de commencer

Framework de contrainte

gcloud beta terraform vet utilise les règles du framework de contrainte, constitué de contraintes et de modèles de contrainte. La différence entre les deux est la suivante :

  • Un modèle de contrainte est semblable à une déclaration de fonction : il définit une règle dans Rego et accepte éventuellement les paramètres d'entrée.
  • Une contrainte est un fichier qui référence un modèle de contrainte et définit les paramètres d'entrée à lui transmettre et les ressources couvertes par la règle.

Cela vous permet d'éviter la redondance. Vous pouvez écrire un modèle de contrainte avec une règle générique, puis écrire un nombre illimité de contraintes fournissant des paramètres d'entrée différents ou des règles de correspondance des ressources différentes.

Créer un modèle de contrainte

Pour créer un modèle de contrainte, procédez comme suit :

  1. Collecter des exemples de données
  2. Écrire dans Rego
  3. Tester votre Rego
  4. Configurer le squelette d'un modèle de contrainte
  5. Intégrer votre Rego
  6. Configurer une contrainte

Collecter des exemples de données

Pour écrire un modèle de contrainte, vous devez disposer d'exemples de données à utiliser. Les contraintes basées sur Terraform fonctionnent sur les données de changement de ressources, qui proviennent de la clé resource_changes du plan Terraform JSON.

Par exemple, votre JSON pourrait ressembler à ceci:

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

Écrire dans Rego

Une fois que vous disposez d'exemples de données, vous pouvez écrire la logique de votre modèle de contrainte dans Rego. Votre Rego doit être associé à une règle violations. La modification de la ressource en cours d'examen est disponible sous le nom input.review. Les paramètres de contrainte sont disponibles sous la forme input.parameters. Par exemple, pour exiger que les ressources google_compute_address aient un address_type autorisé, écrivez:

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

Attribuer un nom à votre modèle de contrainte

L'exemple précédent utilise le nom TFComputeAddressAddressTypeAllowlistConstraintV1. Il s'agit d'un identifiant unique pour chaque modèle de contrainte. Nous vous recommandons de suivre ces consignes sur l'attribution de noms :

  • Format général : TF{resource}{feature}Constraint{version}. Utilisez CamelCase. (En d'autres termes, utilisez une majuscule pour chaque nouveau mot.)
  • Pour les contraintes de ressource unique, suivez les conventions du fournisseur Terraform pour la dénomination de produits. Par exemple, pour google_tags_tag, le nom du produit est tags, même si le nom de l'API est resourcemanager.
  • Si un modèle s'applique à plusieurs types de ressources, omettez la partie "ressource" et n'incluez que la caractéristique (exemple: "TFAddressTypeAllowlistConstraintV1").
  • Le numéro de version ne suit pas le format semver. C'est un nombre unique. Ainsi, chaque version d'un modèle est unique.

Nous vous recommandons d'utiliser un nom pour votre fichier Rego qui correspond au nom du modèle de contrainte, mais en utilisant snake_case. En d'autres termes, convertissez le nom en mots distincts en minuscules avec _. Pour l'exemple précédent, le nom de fichier recommandé est tf_compute_address_address_type_allowlist_constraint_v1.rego.

Tester votre Rego

Vous pouvez tester votre Rego manuellement avec Rego Playground. Veillez à utiliser des données non sensibles.

Nous vous recommandons d'écrire des tests automatisés. Placez les exemples de données collectés dans validator/test/fixtures/<constraint filename>/resource_changes/data.json et référencez-les dans votre fichier de test comme suit :

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

Placez votre Rego et votre test dans le dossier validator de votre bibliothèque de règles.

Configurer le squelette d'un modèle de contrainte

Une fois que vous disposez d'une règle Rego opérationnelle et testée, vous devez l'empaqueter en tant que modèle de contrainte. Le framework des contraintes utilise les définitions de ressources personnalisées Kubernetes en tant que conteneur pour la règle Rego.

Le modèle de contrainte définit également les paramètres qui sont autorisés en tant qu'entrées de contraintes, à l'aide du schéma OpenAPI V3.

Utilisez le même nom pour le squelette que celui que vous avez utilisé pour votre Rego. En particulier :

  • Utilisez le même nom de fichier que pour votre Rego. Exemple : tf_compute_address_address_type_allowlist_constraint_v1.yaml
  • spec.crd.spec.names.kind doit contenir le nom du modèle
  • metadata.name doit contenir le nom du modèle, mais en minuscules.

Placez le squelette de modèle de contrainte dans policies/templates.

Pour l'exemple ci-dessus:

# 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

Intégrer votre Rego

À ce stade, après l'exemple précédent, la disposition de votre répertoire ressemble à ceci:

| 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 vous avez cloné le Dépôt de bibliothèques de règles fourni par Google, vous pouvez exécuter make build pour mettre à jour automatiquement vos modèles de contraintes dans policies/templates avec le Rego défini dans le fichier validator.

Configurer une contrainte

Les contraintes contiennent trois informations que gcloud beta terraform vet doit appliquer et signaler correctement les violations:

  • severity : low, medium ou high.
  • match: paramètres permettant de déterminer si une contrainte s'applique à une ressource particulière. Les paramètres de correspondance suivants sont acceptés :
    • addresses: liste des adresses de ressources à inclure à l'aide de la mise en correspondance de style glob
    • excludedAddresses (facultatif) : liste d'adresses de ressources à exclure à l'aide de la mise en correspondance de style glob.
  • parameters: valeurs des paramètres d'entrée du modèle de contrainte.

Assurez-vous que kind contient le nom du modèle de contrainte. Nous vous recommandons de définir metadata.name sur un slug descriptif.

Par exemple, pour n'autoriser que les types d'adresses INTERNAL à l'aide de l'exemple de modèle de contrainte précédent, écrivez:

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

Exemples de correspondance :

Outil de mise en correspondance des adresses Description
`module.**` Toutes les ressources dans n'importe quel module
`module.my_module.**` Tout dans le module "my_module"
`**.google_compute_global_forwarding_rule.*` Toutes les ressources google_compute_global_forwarding_rule dans un module
`module.my_module.google_compute_global_forwarding_rule.*` Toutes les ressources google_compute_global_forwarding_rule dans "my_module"

Si une adresse de ressource correspond aux valeurs de addresses et excludedAddresses, elle est exclue.

Limites

Les données du plan Terraform fournissent la meilleure représentation disponible de l'état réel après application. Toutefois, dans de nombreux cas, l'état après application peut ne pas être connu, car il est calculé côté serveur.

La création de chemins d'accès ancêtre CAI fait partie du processus de validation des stratégies. Ce processus utilise le projet par défaut fourni pour contourner les ID de projet inconnus. Dans le cas où aucun projet par défaut n'est fourni, le chemin d'accès ancêtre est défini par défaut sur organizations/unknown.

Vous pouvez interdire les ancêtres inconnus en ajoutant la contrainte suivante :

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: {}

Ressources compatibles

Vous pouvez créer des contraintes de modification de ressource pour toute ressource Terraform provenant de n'importe quel fournisseur Terraform.