יצירת אילוצים של Terraform

לפני שמתחילים

Constraint Framework

בשדה gcloud beta terraform vet פועלת מדיניות של Constraint Framework, שמכילה אילוצים וגם תבניות אילוצים. ההבדל בין שניהם:

  • תבנית אילוצים דומה להצהרת פונקציה. היא מגדירה כלל ב-Rego ומאפשרת להשתמש בפרמטרים של הקלט.
  • אילוץ הוא קובץ שמפנה לתבנית אילוצים, ובו מוגדרים הפרמטרים של הקלט שצריך להעביר לתבנית ואת המשאבים שהמדיניות חלה עליהם.

כך אפשר להימנע מחזרה על הגדרות. תוכלו לכתוב מדיניות כללית בתבנית אילוצים, ולאחר מכן לכתוב כמה אילוצים שתצטרכו – עם פרמטרים שונים של קלט ועם כללים שונים להתאמת המשאבים.

יצירה של תבנית אילוצים

כדי ליצור תבנית אילוצים:

  1. איסוף נתונים לדוגמה
  2. כתיבה בשפת Rego
  3. בדיקת ה-Rego
  4. הגדרת שלד של תבנית אילוצים
  5. הצבת ה-Rego בתוך שורה
  6. הגדרת אילוץ

איסוף נתונים לדוגמה

כדי לכתוב תבנית אילוצים יש צורך בנתונים לדוגמה שאפשר להסתמך עליהם. אילוצים המבוססים על Terraform מסתמכים על resource change data, שנובע מהמפתח resource_changes ב-JSON של תוכנית Terraform.

לדוגמה, קובץ ה-JSON יכול להיראות כך:

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

כתיבה בשפת Rego

אחרי שהנתונים לדוגמה מתקבלים, אפשר לכתוב את הלוגיקה של תבנית האילוצים ב-Rego. ב-Rego חייב להיות כלל violations. השינוי של המשאב שנמצא בבדיקה זמין בתור input.review. הפרמטרים של האילוצים זמינים בתור input.parameters. לדוגמה, כדי לדרוש שלמשאבים של google_compute_address יהיה address_type מורשה, צריך לכתוב:

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

בחירת שם לתבנית האילוצים

בדוגמה הקודמת השתמשנו בשם TFComputeAddressAddressTypeAllowlistConstraintV1. זהו מזהה ייחודי של כל תבנית אילוצים. מומלץ לבחור שמות לפי ההנחיות הבאות:

  • פורמט כללי: TF{resource}{feature}Constraint{version}. צריך להשתמש ב-CamelCase (כלומר, להשתמש באות גדולה בכל מילה חדשה).
  • באילוצים ממשאב יחיד צריך לבחור שם למוצרים בהתאם למוסכמות של ספק ה-Terraform. לדוגמה: לגבי google_tags_tag, שם המוצר הוא tags על אף ששם ה-API הוא resourcemanager.
  • אם התבנית חלה על מספר סוגי משאבים, צריך להשמיט את החלק של המשאב ולכלול רק את המאפיין (לדוגמה: "TFAddressTypeAllowlistConstraintV1")‏.
  • מספר הגרסה מורכב ממספר אחד בלבד, ולא לפי תבנית semver. כך למעשה כל גרסה של תבנית הופכת לתבנית ייחודית.

מומלץ לבחור שם לקובץ ה-Rego שמתאים לשם של תבנית האילוצים, אבל עם שימוש ב-snake_case. במילים אחרות, צריך להמיר את השם למילים נפרדות באותיות קטנות באמצעות _. בדוגמה הקודמת, שם הקובץ המומלץ הוא tf-compute-address-address-type-allowlist-constraint-v1.rego.

בדיקת ה-Rego

תוכלו לבדוק את קוד ה-Rego באופן ידני בעזרת Rego Playground. הקפידו להשתמש בנתונים שלא מכילים מידע אישי רגיש.

אנחנו ממליצים לנסח בדיקות אוטומטיות. עליכם להכניס את נתוני הדגימה שאספתם אל validator/test/fixtures/<constraint filename>/resource_changes/data.json ולציין אותם בקובץ הבדיקה, באופן הבא:

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

צריך להעביר את ה-Rego ואת הבדיקה לתיקייה validator שבספריית המדיניות שלכם.

הגדרת שלד של תבנית אילוצים

אחרי שיצרתם כלל Rego שנבדק ופועל, עליכם לארוז אותו כתבנית אילוצים. המדיניות Constraint Framework משתמשת ב-Kubernetes Custom Resource Definitions בתור הקונטיינר של ה-Rego במדיניות.

תבנית האילוצים גם מגדירה באמצעות הסכימה OpenAPI V3 באילו פרמטרים אפשר להשתמש כמקורות קלט מאילוצים.

שם השלד צריך להיות זהה לשם שהשתמשתם ב-Rego. הקפידו במיוחד על הדברים הבאים:

  • השתמשו באותו שם קובץ שבחרתם ל-Rego. לדוגמה: tf-compute-address-address-type-allowlist-constraint-v1.yaml
  • השדה spec.crd.spec.names.kind חייב להכיל את שם התבנית.
  • השדה metadata.name חייב להכיל את שם התבנית, אבל צריך להשתמש באותיות קטנות.

עליכם להציב את השלד של תבנית האילוצים ב-policies/templates.

לדוגמה:

# 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

הצבת ה-Rego בתוך שורה

עכשיו, לאחר הדוגמה הקודמת, הפריסה של הספריות נראית כך:

| 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

אם שכפלתם את מאגר ספריית המדיניות של Google, תוכלו להריץ את make build כדי לעדכן אוטומטית את תבניות האילוצים ב-policies/templates לפי ה-Rego שהוגדר ב-validator.

הגדרת אילוץ

האילוצים מכילים שלושה פרטים שנדרשים ל-gcloud beta terraform vet כדי לאכוף הפרות ולדווח עליהן כמו שצריך:

  • severity:‏ low,‏ medium או high
  • match:‏ פרמטרים שנועדו לקבוע אם האילוץ חל על משאב מסוים. אפשר להשתמש בפרמטרים הבאים להתאמה:
    • addresses: רשימה של כתובות משאבים שצריך לכלול באמצעות התאמה של glob-style.
    • excludedAddresses: (אופציונלי) רשימה של כתובות משאבים שצריך להחריג באמצעות התאמה של glob-style.
  • parameters: ערכים לפרמטרים בקלט של תבנית האילוצים.

הקפידו שהשדה kind יכלול את השם של תבנית האילוצים. מומלץ להגדיר שהשדה metadata.name יהיה שורה עם תיאור.

לדוגמה, כדי לאפשר רק כתובות מסוג INTERNAL באמצעות תבנית האילוצים מהדוגמה הקודמת, עליכם לכתוב:

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

דוגמאות להתאמות:

כלי להתאמת כתובות תיאור
module.** כל המשאבים בכל מודול
module.my_module.** כל מה שנמצא במודול my_module
**.google_compute_global_forwarding_rule.* כל המשאבים של oogle_compute_global_forwarding_rule שנמצאים בכל מודול
module.my_module.google_compute_global_forwarding_rule.* כל המשאבים של google_compute_global_forwarding_rule שנמצאים ב-'my_module'

אם כתובת של משאב מסוים תהיה תואמת לערכים ברשימות addresses ו-excludedAddresses, היא תוחרג.

מגבלות

הנתונים של תוכנית Terraform מציגים הכי טוב את הייצוג הזמין של המצב בפועל אחרי ההחלה, אבל במקרים רבים ייתכן שהמצב לאחר ההחלה לא יהיה ידוע כי הוא מחושב בצד של השרת.

בניית נתיבים לישויות אב של CAI היא חלק מתהליך תיקוף המדיניות. לצורך כך משתמשים בפרויקט ברירת המחדל שסופק, כדי לעקוף מזהי פרויקטים לא ידועים. אם לא סופק פרויקט ברירת מחדל, ברירת המחדל של הנתיב לישויות האב היא organizations/unknown.

אפשר למנוע שימוש בישויות אב עם מזהים לא ידועים על ידי הוספת האילוץ הבא:

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

משאבים נתמכים

אפשר ליצור אילוצים לגבי שינוי משאבים לכל משאב ב-Terraform ודרך כל ספק של Terraform.