From a4eb4d24fd6432bd444c6d5c4b6a17efd41719fb Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Thu, 26 Mar 2026 12:31:40 +0100 Subject: [PATCH] Compute VM module refactor (#3805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add ad for compute-vm refactor * Exclue nic_type from validated fields, add split of main.tf and template.tf * boot disk * fix examples and fixtures * attached disks * fix further examples and module-level tests * remove extra file * fix mig examples * finish refactoring variables * align fast and other modules * refactor(compute-vm): align examples and ADR with the newly implemented interface This commit addresses the remaining references of the `instance_type` and `confidential_compute` parameters in the testing environment and updates the ADR. * feat(compute-vm): add network_performance_config to instance and templates This change implements the usage of the `network_performance_tier` variable we added earlier into the actual Terraform resources. --------- Co-authored-by: Wiktor Niesiobędzki --- CONTRIBUTING.md | 38 +- .../20260323-compute-vm-refactoring.md | 510 ++++++++++++++++++ fast/addons/2-networking-test/main.tf | 14 +- fast/addons/2-networking-test/outputs.tf | 2 +- fast/extras/0-cicd-gitlab/README.md | 12 +- .../self-hosted-agents/main.tf | 18 +- fast/stages/2-networking/factory-nva.tf | 20 +- fast/stages/3-gke-dev/README.md | 12 +- .../bindplane/README.md | 8 +- .../cloud-config-container/coredns/README.md | 8 +- .../envoy-sni-dyn-fwd-proxy/README.md | 8 +- .../envoy-traffic-director/README.md | 10 +- .../cloud-config-container/mysql/README.md | 10 +- .../nginx-tls/README.md | 8 +- .../cloud-config-container/nginx/README.md | 8 +- .../simple-nva/README.md | 18 +- modules/compute-mig/README.md | 66 +-- modules/compute-vm/README.md | 468 ++++++++-------- modules/compute-vm/disks.tf | 127 +++++ modules/compute-vm/instance.tf | 311 +++++++++++ modules/compute-vm/main.tf | 397 +------------- modules/compute-vm/resource-policies.tf | 32 +- modules/compute-vm/tags.tf | 12 +- modules/compute-vm/template.tf | 262 +++++---- modules/compute-vm/test.tfvars | 9 - modules/compute-vm/variables.tf | 352 ++++++------ modules/net-lb-app-ext-regional/README.md | 18 +- modules/net-lb-app-ext/README.md | 6 +- .../README.md | 6 +- .../instances.tf | 14 +- modules/net-lb-ext/README.md | 7 +- modules/net-lb-int/README.md | 8 +- .../recipe-ilb-next-hop/gateways.tf | 20 +- modules/net-lb-int/recipe-ilb-next-hop/vms.tf | 24 +- modules/project-factory/README.md | 2 +- modules/project/README.md | 6 +- modules/spanner-instance/README.md | 2 +- modules/vpc-sc/README.md | 2 +- tests/examples/test_plan.py | 2 +- tests/fixtures/compute-mig.tf | 2 +- tests/fixtures/compute-vm-group-bc.tf | 4 +- .../context-template-regional.tfvars | 8 +- .../compute_vm/context-template-regional.yaml | 2 +- .../compute_vm/context-template.tfvars | 8 +- .../modules/compute_vm/context-template.yaml | 2 +- tests/modules/compute_vm/context-vm.tfvars | 8 +- tests/modules/compute_vm/context-vm.yaml | 2 +- tests/modules/compute_vm/examples/cmek.yaml | 2 +- .../modules/compute_vm/examples/defaults.yaml | 2 +- .../disk-hyperdisk-cust-performance.yaml | 2 +- .../compute_vm/examples/disk-options.yaml | 2 +- .../examples/disks-example-template.yaml | 2 +- .../examples/independent-boot-disk.yaml | 2 +- .../compute_vm/examples/sa-custom.yaml | 2 +- .../compute_vm/examples/sa-default.yaml | 2 +- .../compute_vm/examples/sa-managed.yaml | 2 +- .../modules/compute_vm/examples/sa-none.yaml | 2 +- .../examples/snapshot-schedule-create.yaml | 1 - .../compute_vm/examples/sole-tenancy.yaml | 2 +- .../compute_vm/examples/tag-bindings.yaml | 2 +- .../vpn-single-tunnel-custom-ciphers.yaml | 2 +- .../examples/vpn-single-tunnel.yaml | 2 +- tools/format_tftest.py | 147 +++++ tools/tfdoc.py | 21 +- 64 files changed, 1971 insertions(+), 1119 deletions(-) create mode 100644 adrs/modules/20260323-compute-vm-refactoring.md create mode 100644 modules/compute-vm/disks.tf create mode 100644 modules/compute-vm/instance.tf delete mode 100644 modules/compute-vm/test.tfvars create mode 100755 tools/format_tftest.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 74ac583d2..ad18e9039 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -327,8 +327,10 @@ module "simple-vm-example" { zone = "europe-west1-b" name = "test" boot_disk = { - initialize_params = { + source = { image = "projects/debian-cloud/global/images/family/cos-97-lts" + } + initialize_params = { type = "pd-balanced" size = 10 } @@ -336,7 +338,7 @@ module "simple-vm-example" { } ``` -Where this results in objects with too many attributes, we usually split attributes between required and optional by adding a second level, as in this example where VM `attached_disks[].options` contains less used attributes and can be set to null if not needed. +Where this results in objects with too many attributes, we usually group attributes into logical blocks, as in this example where VM `attached_disks` separates initialization parameters and source parameters. ```hcl module "simple-vm-example" { @@ -344,13 +346,17 @@ module "simple-vm-example" { project_id = var.project_id zone = "europe-west1-b" name = "test" - attached_disks = [ - { name="data", size=10, source=null, source_type=null, options=null } - ] + attached_disks = { + data = { + initialize_params = { + # size = 10 (this is the default) + } + } + } } ``` -Whenever options are not passed like in the example above, we typically infer their values from a defaults variable which can be customized when using defaults across several items. In the following example instead of specifying regional PD options for both disks, we set their options to `null` and change the defaults used for all disks. +When defining multiple items, we use maps instead of lists to ensure stable keys in the Terraform state. In the following example we define multiple attached disks using a map. ```hcl module "simple-vm-example" { @@ -358,16 +364,18 @@ module "simple-vm-example" { project_id = var.project_id zone = "europe-west1-b" name = "test" - attached_disk_defaults = { - auto_delete = false - mode = "READ_WRITE" - replica_zone = "europe-west1-c" - type = "pd-balanced" + attached_disks = { + data1 = { + initialize_params = { + # size = 10 (this is the default) + } + } + data2 = { + initialize_params = { + size = 20 + } + } } - attached_disks = [ - { name="data1", size=10, source=null, source_type=null, options=null }, - { name="data2", size=10, source=null, source_type=null, options=null } - ] } ``` diff --git a/adrs/modules/20260323-compute-vm-refactoring.md b/adrs/modules/20260323-compute-vm-refactoring.md new file mode 100644 index 000000000..cc951ede9 --- /dev/null +++ b/adrs/modules/20260323-compute-vm-refactoring.md @@ -0,0 +1,510 @@ +# Refactor compute-vm module variables and add new resource attributes + +**authors:** [Ludo](https://github.com/ludoo) [Wiktor](https://github.com/wiktorn) \ +**date:** Mar 23, 2026 + + +- [Status](#status) +- [Context](#context) +- [Decision](#decision) + - [1. List vs. Map for Interfaces and Disks](#1-list-vs-map-for-interfaces-and-disks) + - [2. Disk Refactoring Strategy](#2-disk-refactoring-strategy) + - [Disambiguating Disk "Names"](#disambiguating-disk-names) + - [Unifying `boot_disk` and `attached_disks` Structures](#unifying-boot_disk-and-attached_disks-structures) + - [Polymorphic `source` Object](#polymorphic-source-object) + - [Example Usage: Polymorphic Disks](#example-usage-polymorphic-disks) + - [1. Boot Disk Examples](#1-boot-disk-examples) + - [2. Attached Disks Examples](#2-attached-disks-examples) + - [3. Feature and Options Grouping Strategy](#3-feature-and-options-grouping-strategy) + - [Proposed Groupings and Type Definitions](#proposed-groupings-and-type-definitions) + - [1. `machine_type` / Machine Configuration](#1-machine_type-machine-configuration) + - [2. `scheduling_config` (Replaces parts of `options`)](#2-scheduling_config-replaces-parts-of-options) + - [3. `confidential_compute` (Updating to support SEV-SNP)](#3-confidential_compute-updating-to-support-sev-snp) + - [4. `shielded_config`](#4-shielded_config) + - [5. `network_interfaces` Enhancements](#5-network_interfaces-enhancements) + - [6. `network_performance_tier` (NEW)](#6-network_performance_tier-new) + - [7. `lifecycle_config` (Replaces residual `options`)](#7-lifecycle_config-replaces-residual-options) + - [4. Instance Groups and Policies](#4-instance-groups-and-policies) + - [Instance Groups (`group`)](#instance-groups-group) + - [Resource Policies (Snapshots and Schedules)](#resource-policies-snapshots-and-schedules) + - [5. Templates (`create_template`) Strategy](#5-templates-create_template-strategy) + - [Key Refactoring Points for Templates](#key-refactoring-points-for-templates) +- [TODO](#todo) + + +## Status + +Draft + +## Context + +The `compute-vm` module currently uses variable schemas that diverge from the modern standards adopted by newer Cloud Foundation Fabric modules. The current design of `boot_disk` and `attached_disks` uses different schemas, lacks polymorphic source structures, and utilizes lists instead of maps, causing `for_each` stability issues. Furthermore, several modern `google_compute_instance` attributes (e.g., `queue_count`, `network_performance_config`, advanced scheduling, SEV-SNP) are missing. + +## Decision + +### 1. List vs. Map for Interfaces and Disks + +- **Network Interfaces:** The order of network interfaces is critical in GCP VMs (e.g., `nic0` is the primary interface, `nic1` is secondary, etc.). Terraform's `google_compute_instance` resource processes the `network_interface` blocks in the order they are defined. Using a `map` would lose this explicit ordering (since map keys are sorted alphabetically in Terraform), making it impossible to guarantee which interface becomes `nic0`. Therefore, `network_interfaces` **must remain a list**. +- **Attached Disks:** While disks also have an implicit order when attached, their identity is more strongly tied to their `device_name` or `name` rather than their strict numerical index. The current approach of using a list and generating keys based on index (`"disk-${i}"`) causes issues with `for_each` loops (like in `google_compute_disk_resource_policy_attachment`) when dynamic values or conditional creations are involved, leading to the "value of count cannot be computed" or "invalid for_each argument" errors. Switching `attached_disks` to a `map(object({...}))` where the key is the logical name or `device_name` solves these `for_each` stability issues and aligns with modern Fabric patterns. + +### 2. Disk Refactoring Strategy + +#### Disambiguating Disk "Names" + +Disks in GCP and Terraform have several identifiers which often cause confusion. We will explicitly disambiguate them as follows: + +1. **Map Key (The Identifier):** In the new `attached_disks` map, the key itself will act as the primary logical identifier for the disk within the module's Terraform state. +2. **Device Name (`device_name`):** This is the name exposed to the Guest OS (e.g., visible in `/dev/disk/by-id/google-`). + - *Rule:* We will default the `device_name` to the **Map Key**. Users can override it explicitly if needed, but the map key provides a safe, predictable default. +3. **Resource Name (`name`):** This is the actual name of the `google_compute_disk` resource created in the GCP API. + - *Rule:* To ensure uniqueness across a project, we will default the resource name to `${var.name}-${each.key}` (the VM name hyphenated with the Map Key). Users can provide an explicit `name` attribute to override this (e.g., when attaching an existing disk or requiring a specific naming convention). + +#### Unifying `boot_disk` and `attached_disks` Structures + +Currently, `boot_disk` uses an `initialize_params` block (mirroring Terraform's native syntax), while `attached_disks` uses an `options` block and keeps `size` at the top level. We will align them to use a consistent schema: + +- **Adopt `initialize_params`:** Both `boot_disk` and `attached_disks` will use an `initialize_params` block for creation-specific attributes (size, type, image, architecture, provisioned iops/throughput). This clearly separates attributes used for *creating* a disk from attributes used for *attaching* a disk. +- **Top-level attributes:** Attributes relevant to the attachment or lifecycle (e.g., `source`, `auto_delete`, `mode`) will live at the top level of the disk object. +- **Source Type Handling:** For `attached_disks`, we will keep a mechanism to distinguish between creating from an image/snapshot vs. attaching an existing disk (e.g., keeping `source_type` or inferring it from the presence of `initialize_params` vs `source`). + +#### Polymorphic `source` Object + +To eliminate the confusing `source` (string) and `source_type` (string) variables, we will use a polymorphic `source` object. This pattern ensures mutual exclusivity and clearly defines the origin of the disk. + +**Type Definition:** + +```hcl +source = optional(object({ + attach = optional(string) + disk = optional(string) + image = optional(string) + snapshot = optional(string) +})) +``` + +**Validation:** +A validation rule will ensure that if `source` is provided, exactly one of its attributes is non-null. If `source` is omitted entirely (for attached disks), it implies creating a blank disk. + +**Updated Variable Structures:** + +```hcl +variable "boot_disk" { + type = object({ + architecture = optional(string) + auto_delete = optional(bool, true) + snapshot_schedule = optional(list(string)) + initialize_params = optional(object({ + size = optional(number, 10) + type = optional(string, "pd-balanced") + hyperdisk = optional(object({ + provisioned_iops = optional(number) + provisioned_throughput = optional(number) # in MiB/s + storage_pool = optional(string) + }), {}) + }), {}) + source = optional(object({ + attach = optional(string) + disk = optional(string) + image = optional(string) + snapshot = optional(string) + }), { image = "projects/debian-cloud/global/images/family/debian-11" }) + use_independent_disk = optional(object({ + name = optional(string) + })) + }) +} + +variable "attached_disks" { + type = map(object({ + auto_delete = optional(bool, true) # applies only to vm templates + device_name = optional(string) + mode = optional(string, "READ_WRITE") + name = optional(string) + initialize_params = optional(object({ + replica_zone = optional(string) + size = optional(number, 10) + type = optional(string, "pd-balanced") + hyperdisk = optional(object({ + provisioned_iops = optional(number) + provisioned_throughput = optional(number) # in MiB/s + storage_pool = optional(string) + }), {}) + }), {}) + snapshot_schedule = optional(list(string)) + source = optional(object({ + attach = optional(string) + image = optional(string) + snapshot = optional(string) + }), {}) + })) +} +``` + +#### Example Usage: Polymorphic Disks + +Here is how the proposed `boot_disk` and `attached_disks` variables look in practice using the polymorphic `source` object. + +##### 1. Boot Disk Examples + +```hcl +# Default boot disk (from image) +boot_disk = { + auto_delete = true + source = { + image = "projects/debian-cloud/global/images/family/debian-11" + } + initialize_params = { + size = 20 + type = "pd-ssd" + } +} + +# Booting from an existing attached disk +boot_disk = { + auto_delete = false + source = { + attach = "projects/my-project/zones/europe-west1-b/disks/my-existing-boot-disk" + } + # initialize_params are omitted/ignored when attaching +} +``` + +##### 2. Attached Disks Examples + +```hcl +attached_disks = { + # 1. Create a blank disk + # The map key ("data-disk") is the primary identifier. + data-disk = { + auto_delete = false + mode = "READ_WRITE" + initialize_params = { + size = 100 + type = "pd-balanced" + } + # source is omitted entirely + } + + # 2. Create a disk from a snapshot + restored-data = { + source = { + snapshot = "projects/my-project/global/snapshots/my-snapshot" + } + initialize_params = { + size = 500 + type = "pd-ssd" + } + } + + # 3. Attach an existing disk (overriding defaults) + existing-backup = { + device_name = "backup-mount" # Explicitly set device name for OS + mode = "READ_ONLY" + source = { + attach = "projects/my-project/zones/europe-west1-b/disks/my-existing-disk" + } + } +} +``` + +### 3. Feature and Options Grouping Strategy + +Currently, `compute-vm` has a mix of top-level boolean toggles (`confidential_compute`, `can_ip_forward`, `enable_display`) and a catch-all `options` variable that houses `advanced_machine_features`, scheduling/spot configurations, and operational toggles. + +To align with modern Fabric patterns, we will decompose these into logical `*_config` objects and structured variables. **Crucially, almost all string field that maps to an API enum (e.g., `provisioning_model`, `maintenance_interval`) will include strict Terraform validation rules.** `nic_type` will be an exception from this rule as the valid values depend on machine\_type and there might be new types introduced in the future. + +#### Proposed Groupings and Type Definitions + +##### 1. `machine_type` / Machine Configuration + +Currently named `instance_type`, we will rename it to `machine_type` to align with GCP console and gcloud terminology. `min_cpu_platform` remains top-level. `advanced_machine_features` will be extracted from `options` into its own top-level block or kept flat. + +##### 2. `scheduling_config` (Replaces parts of `options`) + +The `google_compute_instance.scheduling` block in Terraform handles spot instances, maintenance, and run durations. We will extract these from the current `options` variable into a dedicated `scheduling_config` object and add the missing modern attributes. + +```hcl +variable "scheduling_config" { + description = "Scheduling configuration for the instance." + type = object({ + automatic_restart = optional(bool) # Defaults to !spot + maintenance_interval = optional(string) # NEW + min_node_cpus = optional(number) # NEW + on_host_maintenance = optional(string) # Defaults to MIGRATE or TERMINATE based on GPU/Spot + provisioning_model = optional(string) # "SPOT" or "STANDARD" + termination_action = optional(string) + local_ssd_recovery_timeout = optional(object({ # NEW + nanos = optional(number) + seconds = number + })) + max_run_duration = optional(object({ + nanos = optional(number) + seconds = number + })) + node_affinities = optional(map(object({ + values = list(string) + in = optional(bool, true) + })), {}) + }) + default = {} +} +``` + +*Example Usage:* + +```hcl +scheduling_config = { + provisioning_model = "SPOT" + termination_action = "STOP" + maintenance_interval = "PERIODIC" + node_affinities = { + "compute.googleapis.com/node-group-name" = { + values = ["my-node-group"] + } + } +} +``` + +##### 3. `confidential_compute` (Updating to support SEV-SNP) + +Currently a boolean. Since the Terraform block (`confidential_instance_config`) effectively only needs to know the type (SEV or SEV_SNP) when enabled, we will change this to a simple string to avoid a single-field object. + +```hcl +variable "confidential_compute" { + description = "Confidential Compute configuration. Set to 'SEV' or 'SEV_SNP' to enable." + type = string + default = null # If null, feature is disabled + validation { + condition = var.confidential_compute == null || contains(["SEV", "SEV_SNP"], coalesce(var.confidential_compute, "-")) + error_message = "Allowed values are 'SEV' or 'SEV_SNP'." + } +} +``` + +*Example Usage:* + +```hcl +confidential_compute = "SEV_SNP" +``` + +##### 4. `shielded_config` + +Remains an object, but we ensure its type signature uses strict `optional()` defaults mirroring current behavior. + +```hcl +variable "shielded_config" { + description = "Shielded VM configuration of the instances." + type = object({ + enable_secure_boot = optional(bool, true) + enable_vtpm = optional(bool, true) + enable_integrity_monitoring = optional(bool, true) + }) + default = null +} +``` + +*Example Usage:* + +```hcl +shielded_config = { + enable_secure_boot = true + enable_vtpm = false +} +``` + +##### 5. `network_interfaces` Enhancements + +We will add the missing modern attributes directly to the existing list of objects: + +- `queue_count` +- `internal_ipv6_prefix_length` + +```hcl +variable "network_interfaces" { + description = "Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed." + type = list(object({ + network = string + subnetwork = string + alias_ips = optional(map(string), {}) + nat = optional(bool, false) + network_tier = optional(string) + nic_type = optional(string) + stack_type = optional(string) + queue_count = optional(number) # NEW + internal_ipv6_prefix_length = optional(number) # NEW + addresses = optional(object({ + internal = optional(string) + external = optional(string) + }), null) + })) +} +``` + +*Example Usage:* + +```hcl +network_interfaces = [{ + network = "my-vpc" + subnetwork = "my-subnet" + queue_count = 4 + internal_ipv6_prefix_length = 96 +}] +``` + +##### 6. `network_performance_tier` (NEW) + +Since the `network_performance_config` block only contains a single field (`total_egress_bandwidth_tier`), we will implement it as a flat string variable to avoid unnecessary complex objects. + +```hcl +variable "network_performance_tier" { + description = "Network performance total egress bandwidth tier." + type = string + default = null + validation { + condition = var.network_performance_tier == null || contains(["DEFAULT", "TIER_1"], coalesce(var.network_performance_tier, "-")) + error_message = "Allowed values are 'DEFAULT' or 'TIER_1'." + } +} +``` + +*Example Usage:* + +```hcl +network_performance_tier = "TIER_1" +``` + +##### 7. `lifecycle_config` (Replaces residual `options`) + +Operational toggles will be grouped into a `lifecycle_config` object. `key_revocation_action_type` dictates whether the VM stops when its CMEK is revoked, which fits well within lifecycle management. + +```hcl +variable "lifecycle_config" { + description = "Instance lifecycle and operational configurations." + type = object({ + allow_stopping_for_update = optional(bool, true) + deletion_protection = optional(bool, false) + key_revocation_action_type = optional(string, "NONE") + graceful_shutdown = optional(object({ + enabled = optional(bool, false) + max_duration_secs = optional(number) + })) + }) + default = {} +} +``` + +*Example Usage:* + +```hcl +lifecycle_config = { + deletion_protection = true + allow_stopping_for_update = false + key_revocation_action_type = "STOP" + graceful_shutdown = { + enabled = true + max_duration_secs = 60 + } +} +``` + +### 4. Instance Groups and Policies + +#### Instance Groups (`group`) + +Currently, the module can only *create* an unmanaged instance group and add the VM to it. We will expand this to support adding the VM to an *existing* unmanaged instance group using the `google_compute_instance_group_membership` resource. + +To manage this cleanly, we will update the `group` variable to support both modes: + +```hcl +variable "group" { + description = "Instance group configuration. Set 'named_ports' to create a new unmanaged instance group, or provide an existing group self_link/id in 'membership' to join one." + type = object({ + named_ports = optional(map(number)) + membership = optional(string) # ID of an existing unmanaged group to join + }) + default = null +} +``` + +*Note: If `named_ports` is provided, a new group is created. If `membership` is provided, the VM joins the specified existing group. They are mutually exclusive.* + +#### Resource Policies (Snapshots and Schedules) + +The module currently supports creating `snapshot_schedules` and an `instance_schedule`. + +- **Snapshot Schedules:** The existing `snapshot_schedules` variable is already well-structured using modern optionals. We will retain this structure. The primary refactoring here will be updating the attachment logic (`google_compute_disk_resource_policy_attachment`) to iterate over the new `attached_disks` map instead of the old list. +- **Instance Schedule:** The `instance_schedule` variable is also well-structured using strict optionals and will be retained. +- **Placement Policies:** The existing `resource_policies` list variable already allows attaching externally created placement policies (Collocated/Spread) or other custom policies. We will keep this as-is for flexibility, as placement policies are typically shared across multiple standalone VMs. + +### 5. Templates (`create_template`) Strategy + +Currently, `create_template` is an object `type = object({ regional = optional(bool, false) })` that defaults to `null`. It creates either a `google_compute_instance_template` or `google_compute_region_instance_template` depending on the `regional` flag. + +While this pattern is somewhat unusual in the Fabric codebase, we will keep the `create_template` variable structure but ensure it is strictly integrated with the new disk schemas. + +#### Key Refactoring Points for Templates + +1. **Disk Schema Alignment:** The `template.tf` file currently maps the old `options` block to the template's `disk` block. This mapping will be updated to reflect the new `initialize_params` and polymorphic `source` blocks. + - *Constraint:* Templates do not allow specifying `source_image` alongside `disk_name` or `disk_size_gb` in the same way standalone instances do (some fields are mutually exclusive). + - *Solution Map:* + - `source.image` -> `source_image` + - `source.snapshot` -> `source_snapshot` + - `source.attach` -> `source` (attaching an existing disk) + - `source == null` -> creates a blank disk +2. **Attribute Parity:** All newly refactored attributes (`network_performance_tier`, `scheduling_config`, updated `confidential_compute`, and network interface enhancements) will be mapped directly into the respective blocks within both regional and global template resources. +3. **Tags and Labels:** No architectural change here, but we will ensure that `tag_bindings_immutable` continues to map correctly to `resource_manager_tags`. + +## TODO + +Example tests will be adapted and run as part of each task iteration. + +- [x] **Task 1:** Update `variables.tf` to implement the new disk structures (`boot_disk` and `attached_disks`), polymorphic `source`, and disambiguate disk names. +- [ ] **Task 2:** Refactor `variables.tf` for feature grouping: rename `instance_type` to `machine_type`, add `scheduling_config`, `lifecycle_config`, `network_performance_tier`, and update `confidential_compute`. +- [ ] **Task 3:** Add new attributes to `network_interfaces` (`queue_count`, `internal_ipv6_prefix_length`). +- [ ] **Task 4:** Split `template.tf` into `template-zonal.tf` and `template-regional.tf`, extract `instance.tf` from `main.tf` to allow easy comparison of feature coverage. +- [ ] **Task 5:** Expand the `group` variable to support the `membership` attribute. +- [ ] **Task 6:** Update `instance.tf` and `outputs.tf` to consume the new variables (standalone VM implementation). +- [ ] **Task 7:** Update `tags.tf` and `resource-policies.tf` to work with the new `attached_disks` map instead of a list. +- [ ] **Task 8:** Update `template-zonal.tf` and `template-regional.tf` to align with the new disk schemas and map the new feature attributes. +- [ ] **Task 9:** Run integration tests and regenerate documentation (`python3 tools/tfdoc.py` and YAML test files updates). +- [ ] **Task 10:** Assess if disk-level encryption key overrides make sense, and if so implement them. + +## Addendum: Missing Disk Attributes + +Based on a review of the latest `terraform-provider-google` documentation for `google_compute_disk`, `google_compute_region_disk`, and `google_compute_instance` disk attachments, the following attributes are currently missing from the proposed disk type definitions and should be considered for inclusion: + +### 1. Metadata and Organization + +* **`description`** `(string)`: An optional description of the disk resource. +- **`labels`** `(map(string))`: Key/value pairs to label the disk. +- **`params`** / **`resource_manager_tags`** `(map(string))`: Resource manager tags to be bound to the disk. +- **`licenses`** `(list(string))`: Applicable license URIs to apply to the disk. + +### 2. Encryption and Security + +* **`disk_encryption_key`** `(object)`: Used to encrypt the disk with a customer-supplied (CSEK) or customer-managed (CMEK) key. +- **`source_image_encryption_key`** `(object)`: Required to decrypt the source image if it is protected by a CSEK/CMEK. +- **`source_snapshot_encryption_key`** `(object)`: Required to decrypt the source snapshot if it is protected by a CSEK/CMEK. +- **`enable_confidential_compute`** `(bool)`: Whether the disk uses confidential compute mode (supported on certain Hyperdisk SKUs). +- **`disk_encryption_key_raw`** / **`kms_key_self_link`**: Required on the `attached_disk` block of `google_compute_instance` to mount an existing encrypted disk. + +### 3. Advanced Disk Features & Hyperdisk + +* **`access_mode`** `(string)`: Specifically for Hyperdisks (e.g., `READ_WRITE_SINGLE`, `READ_WRITE_MANY`, `READ_ONLY_SINGLE`). +- **`multi_writer`** `(bool)`: Indicates whether a persistent disk can be read/write attached to more than one instance. +- **`physical_block_size_bytes`** `(number)`: Allows specifying physical block size (usually `4096` or `16384`). +- **`guest_os_features`** `(list(object))`: Features to enable on the guest OS (e.g., `UEFI_COMPATIBLE`, `SECURE_BOOT`, `MULTI_IP_SUBNET`). +- **`async_primary_disk`** `(object)`: Primary disk configuration for asynchronous disk replication. + +### 4. Source Creation Options + +* **`source_disk`** `(string)`: Allows creating a new disk by cloning an existing `google_compute_disk` (supported by both zonal and regional disks). +- **`source_instant_snapshot`** `(string)`: Allows creating a disk from a Google Compute instant snapshot. +- **`source_storage_object`** `(string)`: Allows creating a disk directly from a GCS URI tarball/vmdk. +- **`erase_windows_vss_signature`** `(bool)`: Specifies whether the disk restored from a source snapshot should erase the Windows-specific VSS signature. +- **Note on Regional Disks:** `google_compute_region_disk` does not support initialization directly from an `image`. The `source.image` attribute will only work for zonal disks. + +### 5. Disk Lifecycle + +* **`create_snapshot_before_destroy`** `(bool)`: If `true`, creates a snapshot of the disk before Terraform destroys it. +- **`create_snapshot_before_destroy_prefix`** `(string)`: A custom prefix for the snapshot name created prior to destruction. diff --git a/fast/addons/2-networking-test/main.tf b/fast/addons/2-networking-test/main.tf index 416bf1ff0..f5270e351 100644 --- a/fast/addons/2-networking-test/main.tf +++ b/fast/addons/2-networking-test/main.tf @@ -50,14 +50,14 @@ module "service-accounts" { } module "instances" { - source = "../../../modules/compute-vm" - for_each = { for k in local.instances : k.name => k } - project_id = each.value.project_id - zone = each.value.zone - name = each.key - instance_type = each.value.type + source = "../../../modules/compute-vm" + for_each = { for k in local.instances : k.name => k } + project_id = each.value.project_id + zone = each.value.zone + name = each.key + machine_type = each.value.type boot_disk = { - initialize_params = { + source = { image = each.value.image } } diff --git a/fast/addons/2-networking-test/outputs.tf b/fast/addons/2-networking-test/outputs.tf index a62486655..69b880c93 100644 --- a/fast/addons/2-networking-test/outputs.tf +++ b/fast/addons/2-networking-test/outputs.tf @@ -25,7 +25,7 @@ output "instance_ssh" { description = "Instance SSH commands." value = { for k, v in module.instances : k => ( - "gcloud compute ssh ${k} --project ${v.instance.project} --zone ${v.instance.zone}" + "gcloud compute ssh ${k} --project ${nonsensitive(v.instance.project)} --zone ${nonsensitive(v.instance.zone)}" ) } } diff --git a/fast/extras/0-cicd-gitlab/README.md b/fast/extras/0-cicd-gitlab/README.md index 9df02e2d2..1808eccf9 100644 --- a/fast/extras/0-cicd-gitlab/README.md +++ b/fast/extras/0-cicd-gitlab/README.md @@ -70,7 +70,7 @@ group with source ref: ```hcl modules_config = { project_name = "modules" - key_config = { + key_config = { create_key = true create_secrets = true } @@ -86,7 +86,7 @@ repository: ```hcl modules_config = { project_name = "modules" - key_config = { + key_config = { create_key = true create_secrets = true } @@ -106,11 +106,11 @@ deploy key in the modules project, and as secrets in the stage repositories: ```hcl modules_config = { project_name = "modules" - key_config = { + key_config = { create_key = true create_secrets = true } - group = "shared" + group = "shared" key_config = { create_key = true create_secrets = true @@ -127,11 +127,11 @@ and new repositories need to be created and their corresponding secret set: ```hcl modules_config = { project_name = "modules" - key_config = { + key_config = { create_key = true create_secrets = true } - group = "shared" + group = "shared" key_config = { create_secrets = true keypair_path = "~/modules-repository-key" diff --git a/fast/project-templates/devops-azure-wif/self-hosted-agents/main.tf b/fast/project-templates/devops-azure-wif/self-hosted-agents/main.tf index 31a674779..3a79c1c09 100644 --- a/fast/project-templates/devops-azure-wif/self-hosted-agents/main.tf +++ b/fast/project-templates/devops-azure-wif/self-hosted-agents/main.tf @@ -68,17 +68,19 @@ module "secret" { } module "instance" { - source = "../../../../modules/compute-vm" - count = local.create_instance ? 1 : 0 - project_id = var.project_id - zone = "${var.location}-${var.instance_config.zone}" - name = "${var.name}-agent" - instance_type = "e2-micro" + source = "../../../../modules/compute-vm" + count = local.create_instance ? 1 : 0 + project_id = var.project_id + zone = "${var.location}-${var.instance_config.zone}" + name = "${var.name}-agent" + machine_type = "e2-micro" boot_disk = { auto_delete = false - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-117-lts" - size = 10 + } + initialize_params = { + size = 10 } } network_interfaces = [{ diff --git a/fast/stages/2-networking/factory-nva.tf b/fast/stages/2-networking/factory-nva.tf index 7d68bf3ec..64468d153 100644 --- a/fast/stages/2-networking/factory-nva.tf +++ b/fast/stages/2-networking/factory-nva.tf @@ -47,7 +47,7 @@ locals { nva_def.auto_instance_config.image, "projects/debian-cloud/global/images/family/debian-12" ) - instance_type = try( + machine_type = try( nva_def.auto_instance_config.instance_type, "e2-standard-4" ) metadata = coalesce( @@ -60,7 +60,7 @@ locals { } ) attachments = try(nva_def.auto_instance_config.nics, []) - confidential_compute = try(nva_def.auto_instance_config.confidential_compute, false) + confidential_compute = try(nva_def.auto_instance_config.confidential_compute, null) encryption = try(nva_def.auto_instance_config.encryption, null) options = try(nva_def.auto_instance_config.options, null) shielded_config = try(nva_def.auto_instance_config.shielded_config, null) @@ -117,7 +117,7 @@ module "nva-instance" { project_id = each.value.project_id name = "nva-${each.key}" zone = each.value.zone - instance_type = each.value.instance_type + machine_type = each.value.machine_type tags = each.value.tags can_ip_forward = true network_interfaces = [for k, v in each.value.attachments : @@ -129,14 +129,18 @@ module "nva-instance" { } ] boot_disk = { + source = { + image = each.value.image + } initialize_params = { - image = each.value.image - google-logging-enabled = true - type = "pd-ssd" - size = 10 # TODO: make configurable? + type = "pd-ssd" + size = 10 # TODO: make configurable? } } - metadata = each.value.metadata + metadata = merge( + each.value.metadata, + { google-logging-enabled = true } + ) encryption = each.value.encryption shielded_config = each.value.shielded_config confidential_compute = each.value.confidential_compute diff --git a/fast/stages/3-gke-dev/README.md b/fast/stages/3-gke-dev/README.md index 7e7b34764..e131daaf9 100644 --- a/fast/stages/3-gke-dev/README.md +++ b/fast/stages/3-gke-dev/README.md @@ -115,19 +115,19 @@ clusters = { } private_nodes = true } - enable_features = { + enable_features = { binary_authorization = true groups_for_rbac = "gke-security-groups@example.com" intranode_visibility = true rbac_binding_config = { - enable_insecure_binding_system_unauthenticated: false - enable_insecure_binding_system_authenticated: false + enable_insecure_binding_system_unauthenticated : false + enable_insecure_binding_system_authenticated : false } - shielded_nodes = true + shielded_nodes = true upgrade_notifications = { event_types = ["SECURITY_BULLETIN_EVENT", "UPGRADE_AVAILABLE_EVENT", "UPGRADE_INFO_EVENT", "UPGRADE_EVENT"] } - workload_identity = true + workload_identity = true } vpc_config = { subnetwork = "projects/ldj-dev-net-spoke-0/regions/europe-west8/subnetworks/gke" @@ -141,7 +141,7 @@ clusters = { nodepools = { test-00 = { 00 = { - node_count = { initial = 1 } + node_count = { initial = 1 } node_config = { sandbox_config_gvisor = true } diff --git a/modules/cloud-config-container/bindplane/README.md b/modules/cloud-config-container/bindplane/README.md index 055b22f22..62af89ed2 100644 --- a/modules/cloud-config-container/bindplane/README.md +++ b/modules/cloud-config-container/bindplane/README.md @@ -62,10 +62,12 @@ module "bindplane" { google-logging-enabled = true } boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" - type = "pd-ssd" - size = 10 + } + initialize_params = { + type = "pd-ssd" + size = 10 } } tags = ["http-server", "ssh"] diff --git a/modules/cloud-config-container/coredns/README.md b/modules/cloud-config-container/coredns/README.md index c4f63a90b..2717b0735 100644 --- a/modules/cloud-config-container/coredns/README.md +++ b/modules/cloud-config-container/coredns/README.md @@ -41,10 +41,12 @@ module "vm" { google-logging-enabled = true } boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" - type = "pd-ssd" - size = 10 + } + initialize_params = { + type = "pd-ssd" + size = 10 } } tags = ["dns", "ssh"] diff --git a/modules/cloud-config-container/envoy-sni-dyn-fwd-proxy/README.md b/modules/cloud-config-container/envoy-sni-dyn-fwd-proxy/README.md index 76d1b19d4..4814da81f 100644 --- a/modules/cloud-config-container/envoy-sni-dyn-fwd-proxy/README.md +++ b/modules/cloud-config-container/envoy-sni-dyn-fwd-proxy/README.md @@ -32,10 +32,12 @@ module "vm-envoy-sni-dyn-fwd-proxy" { google-logging-enabled = true } boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" - type = "pd-ssd" - size = 10 + } + initialize_params = { + type = "pd-ssd" + size = 10 } } tags = ["https-server", "ssh"] diff --git a/modules/cloud-config-container/envoy-traffic-director/README.md b/modules/cloud-config-container/envoy-traffic-director/README.md index caa0ec5ec..1c4c076c1 100644 --- a/modules/cloud-config-container/envoy-traffic-director/README.md +++ b/modules/cloud-config-container/envoy-traffic-director/README.md @@ -29,9 +29,13 @@ module "vm" { google-logging-enabled = true } boot_disk = { - image = "projects/cos-cloud/global/images/family/cos-stable" - type = "pd-ssd" - size = 10 + source = { + image = "projects/cos-cloud/global/images/family/cos-stable" + } + initialize_params = { + type = "pd-ssd" + size = 10 + } } tags = ["http-server", "ssh"] } diff --git a/modules/cloud-config-container/mysql/README.md b/modules/cloud-config-container/mysql/README.md index 31045804d..827e5cc3b 100644 --- a/modules/cloud-config-container/mysql/README.md +++ b/modules/cloud-config-container/mysql/README.md @@ -44,9 +44,13 @@ module "vm" { google-logging-enabled = true } boot_disk = { - image = "projects/cos-cloud/global/images/family/cos-stable" - type = "pd-ssd" - size = 10 + source = { + image = "projects/cos-cloud/global/images/family/cos-stable" + } + initialize_params = { + type = "pd-ssd" + size = 10 + } } tags = ["mysql", "ssh"] } diff --git a/modules/cloud-config-container/nginx-tls/README.md b/modules/cloud-config-container/nginx-tls/README.md index 15f2ffe0c..d9e343e30 100644 --- a/modules/cloud-config-container/nginx-tls/README.md +++ b/modules/cloud-config-container/nginx-tls/README.md @@ -25,10 +25,12 @@ module "vm-nginx-tls" { google-logging-enabled = true } boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" - type = "pd-ssd" - size = 10 + } + initialize_params = { + type = "pd-ssd" + size = 10 } } tags = ["http-server", "https-server", "ssh"] diff --git a/modules/cloud-config-container/nginx/README.md b/modules/cloud-config-container/nginx/README.md index dad3d55d9..5d8b45958 100644 --- a/modules/cloud-config-container/nginx/README.md +++ b/modules/cloud-config-container/nginx/README.md @@ -41,10 +41,12 @@ module "vm-nginx-tls" { google-logging-enabled = true } boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" - type = "pd-ssd" - size = 10 + } + initialize_params = { + type = "pd-ssd" + size = 10 } } tags = ["http-server", "ssh"] diff --git a/modules/cloud-config-container/simple-nva/README.md b/modules/cloud-config-container/simple-nva/README.md index c3be2d561..06501a167 100644 --- a/modules/cloud-config-container/simple-nva/README.md +++ b/modules/cloud-config-container/simple-nva/README.md @@ -62,10 +62,12 @@ module "vm" { google-logging-enabled = true } boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" - type = "pd-ssd" - size = 10 + } + initialize_params = { + type = "pd-ssd" + size = 10 } } tags = ["nva", "ssh"] @@ -134,9 +136,13 @@ module "vm" { google-logging-enabled = true } boot_disk = { - image = "projects/cos-cloud/global/images/family/cos-stable" - type = "pd-ssd" - size = 10 + source = { + image = "projects/cos-cloud/global/images/family/cos-stable" + } + initialize_params = { + type = "pd-ssd" + size = 10 + } } tags = ["nva", "ssh"] } diff --git a/modules/compute-mig/README.md b/modules/compute-mig/README.md index 83545289f..68f86f3b9 100644 --- a/modules/compute-mig/README.md +++ b/modules/compute-mig/README.md @@ -45,7 +45,7 @@ module "nginx-template" { addresses = null }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } @@ -88,7 +88,7 @@ module "nginx-template" { addresses = null }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } @@ -139,7 +139,7 @@ module "nginx-template" { addresses = null }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } @@ -191,7 +191,7 @@ module "nginx-template" { addresses = null }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } @@ -242,7 +242,7 @@ module "nginx-template" { addresses = null }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } @@ -287,27 +287,28 @@ module "cos-nginx" { } module "nginx-template" { - source = "./fabric/modules/compute-vm" - project_id = var.project_id - name = "nginx-template" - zone = "${var.region}-b" - tags = ["http-server", "ssh"] - instance_type = "e2-small" + source = "./fabric/modules/compute-vm" + project_id = var.project_id + name = "nginx-template" + zone = "${var.region}-b" + tags = ["http-server", "ssh"] + machine_type = "e2-small" network_interfaces = [{ network = var.vpc.self_link subnetwork = var.subnet.self_link }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } - attached_disks = [{ - source_type = "attach" - name = "data-1" - size = 10 - source = google_compute_disk.test-disk.name - }] + attached_disks = { + data-1 = { + source = { + attach = google_compute_disk.test-disk.name + } + } + } create_template = {} metadata = { user-data = module.cos-nginx.cloud_config @@ -338,27 +339,28 @@ module "cos-nginx" { } module "nginx-template" { - source = "./fabric/modules/compute-vm" - project_id = var.project_id - name = "nginx-template" - zone = "${var.region}-b" - tags = ["http-server", "ssh"] - instance_type = "e2-small" + source = "./fabric/modules/compute-vm" + project_id = var.project_id + name = "nginx-template" + zone = "${var.region}-b" + tags = ["http-server", "ssh"] + machine_type = "e2-small" network_interfaces = [{ network = var.vpc.self_link subnetwork = var.subnet.self_link }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } - attached_disks = [{ - source_type = "attach" - name = "data-1" - size = 10 - source = google_compute_disk.test-disk.name - }] + attached_disks = { + data-1 = { + source = { + attach = google_compute_disk.test-disk.name + } + } + } create_template = {} metadata = { user-data = module.cos-nginx.cloud_config @@ -413,7 +415,7 @@ module "nginx-template" { addresses = null }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } diff --git a/modules/compute-vm/README.md b/modules/compute-vm/README.md index e13553c18..cca2c3472 100644 --- a/modules/compute-vm/README.md +++ b/modules/compute-vm/README.md @@ -151,12 +151,12 @@ module "vm-managed-sa-example2" { #### Disk sources -Attached disks can be created and optionally initialized from a pre-existing source, or attached to VMs when pre-existing. The `source` and `source_type` attributes of the `attached_disks` variable allows several modes of operation: +Attached disks can be created and optionally initialized from a pre-existing source, or attached to VMs when pre-existing. The `source` attribute of the `attached_disks` variable allows several modes of operation: -- `source_type = "image"` can be used with zonal disks in instances and templates, set `source` to the image name or self link -- `source_type = "snapshot"` can be used with instances only, set `source` to the snapshot name or self link -- `source_type = "attach"` can be used for both instances and templates to attach an existing disk, set source to the name (for zonal disks) or self link (for regional disks) of the existing disk to attach; no disk will be created -- `source_type = null` can be used where an empty disk is needed, `source` becomes irrelevant and can be left null +- `source.image` can be used with zonal disks in instances and templates, set to the image name or self link +- `source.snapshot` can be used with instances only, set to the snapshot name or self link +- `source.attach` can be used for both instances and templates to attach an existing disk, set to the name (for zonal disks) or self link (for regional disks) of the existing disk to attach; no disk will be created +- `source = null` can be used where an empty disk is needed This is an example of attaching a pre-existing regional PD to a new instance: @@ -170,15 +170,16 @@ module "vm-disks-example" { network = var.vpc.self_link subnetwork = var.subnet.self_link }] - attached_disks = [{ - name = "repd-1" - size = 10 - source_type = "attach" - source = "regions/${var.region}/disks/repd-test-1" - options = { - replica_zone = "${var.region}-c" + attached_disks = { + repd-1 = { + initialize_params = { + replica_zone = "${var.region}-c" + } + source = { + attach = "regions/${var.region}/disks/repd-test-1" + } } - }] + } service_account = { auto_create = true } @@ -198,15 +199,17 @@ module "vm-disks-example" { network = var.vpc.self_link subnetwork = var.subnet.self_link }] - attached_disks = [{ - name = "repd" - size = 10 - source_type = "attach" - source = "https://www.googleapis.com/compute/v1/projects/${var.project_id}/regions/${var.region}/disks/repd-test-1" - options = { - replica_zone = "${var.region}-c" + attached_disks = { + repd = { + auto_delete = false + initialize_params = { + replica_zone = "${var.region}-c" + } + source = { + attach = "https://www.googleapis.com/compute/v1/projects/${var.project_id}/regions/${var.region}/disks/repd-test-1" + } } - }] + } service_account = { auto_create = true } @@ -217,7 +220,7 @@ module "vm-disks-example" { #### Disk types and options -The `attached_disks` variable exposes an `option` attribute that can be used to fine tune the configuration of each disk. The following example shows a VM with multiple disks +The `attached_disks` variable exposes an `initialize_params` attribute that can be used to fine tune the configuration of each disk. The following example shows a VM with multiple disks ```hcl module "vm-disk-options-example" { @@ -229,27 +232,26 @@ module "vm-disk-options-example" { network = var.vpc.self_link subnetwork = var.subnet.self_link }] - attached_disks = [ - { - name = "data1" - size = "10" - source_type = "image" - source = "image-1" - options = { + attached_disks = { + data1 = { + initialize_params = { replica_zone = "${var.region}-c" } - }, - { - name = "data2" - size = "20" - source_type = "snapshot" - source = "snapshot-2" - options = { - type = "pd-ssd" - mode = "READ_ONLY" + source = { + image = "image-1" } } - ] + data2 = { + mode = "READ_ONLY" + initialize_params = { + size = 20 + type = "pd-ssd" + } + source = { + snapshot = "snapshot-2" + } + } + } service_account = { auto_create = true } @@ -261,47 +263,51 @@ For hyperdisks there are additional options available to configure performance. ```hcl module "vm-disk-options-example" { - source = "./fabric/modules/compute-vm" - project_id = var.project_id - zone = "${var.region}-b" - name = "test" - instance_type = "n4-standard-2" + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "${var.region}-b" + name = "test" + machine_type = "n4-standard-2" network_interfaces = [{ network = var.vpc.self_link subnetwork = var.subnet.self_link }] boot_disk = { initialize_params = { - image = "projects/debian-cloud/global/images/family/debian-12" - provisioned_iops = 3000 - provisioned_throughput = 140 - type = "hyperdisk-balanced" - } - } - - attached_disks = [ - { - name = "data1" - size = "10" - options = { + type = "hyperdisk-balanced" + hyperdisk = { provisioned_iops = 3000 provisioned_throughput = 140 - type = "hyperdisk-balanced" } - }, - { - name = "data2" - size = "10" - source_type = "image" - source = "projects/debian-cloud/global/images/family/debian-12" - options = { - provisioned_iops = 5000 - provisioned_throughput = 500 - type = "hyperdisk-balanced" + } + source = { + image = "projects/debian-cloud/global/images/family/debian-12" + } + } + attached_disks = { + data1 = { + initialize_params = { + type = "hyperdisk-balanced" + hyperdisk = { + provisioned_iops = 3000 + provisioned_throughput = 140 + } } - }, - - ] + } + data2 = { + mode = "READ_ONLY" + initialize_params = { + type = "hyperdisk-balanced" + hyperdisk = { + provisioned_iops = 5000 + provisioned_throughput = 500 + } + } + source = { + image = "projects/debian-cloud/global/images/family/debian-12" + } + } + } service_account = { auto_create = true } @@ -316,24 +322,22 @@ You can use storage pool for better management of storage capacity. ```hcl # hyperdisk - with storage pool resource "google_compute_storage_pool" "default" { - project = var.project_id - name = "storage-pool-basic" - + project = var.project_id + name = "storage-pool-basic" pool_provisioned_capacity_gb = "20480" pool_provisioned_iops = "10000" pool_provisioned_throughput = 1024 storage_pool_type = "hyperdisk-balanced" zone = "${var.region}-c" - - deletion_protection = false + deletion_protection = false } module "vm-disk-options-example" { - source = "./fabric/modules/compute-vm" - project_id = var.project_id - zone = "${var.region}-c" - name = "test" - instance_type = "c4d-standard-2" + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "${var.region}-c" + name = "test" + machine_type = "c4d-standard-2" network_interfaces = [ { network = var.vpc.self_link @@ -341,40 +345,41 @@ module "vm-disk-options-example" { } ] boot_disk = { - use_independent_disk = true + use_independent_disk = {} initialize_params = { - image = "projects/debian-cloud/global/images/family/debian-12" - provisioned_iops = 3000 - provisioned_throughput = 140 - storage_pool = google_compute_storage_pool.default.id - type = "hyperdisk-balanced" + type = "hyperdisk-balanced" + hyperdisk = { + provisioned_iops = 3000 + provisioned_throughput = 140 + storage_pool = google_compute_storage_pool.default.id + } + } + source = { + image = "projects/debian-cloud/global/images/family/debian-12" } } - - attached_disks = [ - { - name = "data1" - size = "10" - options = { - # provisioned_iops = 3000 - # provisioned_throughput = 140 - storage_pool = google_compute_storage_pool.default.id - type = "hyperdisk-balanced" + attached_disks = { + data1 = { + initialize_params = { + type = "hyperdisk-balanced" + hyperdisk = { + storage_pool = google_compute_storage_pool.default.id + } } - }, - { - name = "data2" - size = "10" - source_type = "image" - source = "projects/debian-cloud/global/images/family/debian-12" - options = { - provisioned_iops = 5000 - provisioned_throughput = 500 - type = "hyperdisk-balanced" + } + data2 = { + initialize_params = { + type = "hyperdisk-balanced" + hyperdisk = { + provisioned_iops = 5000 + provisioned_throughput = 500 + } } - }, - - ] + source = { + image = "projects/debian-cloud/global/images/family/debian-12" + } + } + } service_account = { auto_create = true } @@ -390,50 +395,51 @@ For hyperdisks there are additional options available to configure performance. ```hcl module "vm-arm" { - source = "./fabric/modules/compute-vm" - project_id = var.project_id - zone = "${var.region}-c" - name = "test" - instance_type = "c4a-standard-1" + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "${var.region}-c" + name = "test" + machine_type = "c4a-standard-1" network_interfaces = [{ network = var.vpc.self_link subnetwork = var.subnet.self_link }] boot_disk = { + architecture = "ARM64" initialize_params = { - image = "projects/debian-cloud/global/images/family/debian-12-arm64" - architecture = "ARM64" - provisioned_iops = 3000 - provisioned_throughput = 140 - type = "hyperdisk-balanced" - } - } - - attached_disks = [ - { - name = "data1" - size = "10" - options = { - architecture = "ARM64" + type = "hyperdisk-balanced" + hyperdisk = { provisioned_iops = 3000 provisioned_throughput = 140 - type = "hyperdisk-balanced" } - }, - { - name = "data2" - size = "10" - source_type = "image" - source = "projects/debian-cloud/global/images/family/debian-12-arm64" - options = { - architecture = "ARM64" - provisioned_iops = 5000 - provisioned_throughput = 500 - type = "hyperdisk-balanced" + } + source = { + image = "projects/debian-cloud/global/images/family/debian-12-arm64" + } + } + attached_disks = { + data1 = { + initialize_params = { + type = "hyperdisk-balanced" + hyperdisk = { + provisioned_iops = 3000 + provisioned_throughput = 140 + } } - }, - - ] + } + data2 = { + initialize_params = { + type = "hyperdisk-balanced" + hyperdisk = { + provisioned_iops = 5000 + provisioned_throughput = 500 + } + } + source = { + image = "projects/debian-cloud/global/images/family/debian-12-arm64" + } + } + } service_account = { auto_create = true } @@ -445,7 +451,7 @@ module "vm-arm" { #### Boot disk as an independent resource -To create the boot disk as an independent resources instead of as part of the instance creation flow, set `boot_disk.use_independent_disk` to `true` and optionally configure `boot_disk.initialize_params`. +To create the boot disk as an independent resources instead of as part of the instance creation flow, set `boot_disk.use_independent_disk` to a non-null object (e.g. `{}`) and optionally configure `boot_disk.initialize_params`. This will create the boot disk as its own resource and attach it to the instance, allowing to recreate the instance from Terraform while preserving the boot disk. @@ -456,8 +462,7 @@ module "simple-vm-example" { zone = "${var.region}-b" name = "test" boot_disk = { - initialize_params = {} - use_independent_disk = true + use_independent_disk = {} } network_interfaces = [{ network = var.vpc.self_link @@ -536,7 +541,6 @@ resource "google_compute_image" "cos-gvnic" { project = var.project_id name = "my-image" source_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-89-16108-534-18" - guest_os_features { type = "GVNIC" } @@ -558,8 +562,10 @@ module "vm-with-gvnic" { name = "test" boot_disk = { initialize_params = { + type = "pd-ssd" + } + source = { image = google_compute_image.cos-gvnic.self_link - type = "pd-ssd" } } network_interfaces = [{ @@ -674,8 +680,8 @@ module "spot-vm-example" { project_id = var.project_id zone = "${var.region}-b" name = "test" - options = { - spot = true + scheduling_config = { + provisioning_model = "SPOT" termination_action = "STOP" } network_interfaces = [{ @@ -696,10 +702,10 @@ module "vm-confidential-example" { project_id = var.project_id zone = "${var.region}-b" name = "confidential-vm" - confidential_compute = true - instance_type = "n2d-standard-2" + confidential_compute = "SEV" + machine_type = "n2d-standard-2" boot_disk = { - initialize_params = { + source = { image = "projects/debian-cloud/global/images/family/debian-12" } } @@ -714,11 +720,11 @@ module "template-confidential-example" { project_id = var.project_id zone = "${var.region}-b" name = "confidential-template" - confidential_compute = true + confidential_compute = "SEV" create_template = {} - instance_type = "n2d-standard-2" + machine_type = "n2d-standard-2" boot_disk = { - initialize_params = { + source = { image = "projects/debian-cloud/global/images/family/debian-12" } } @@ -790,10 +796,9 @@ module "kms-vm-example" { network = module.vpc.self_link subnetwork = module.vpc.subnet_self_links["${var.region}/production"] }] - attached_disks = [{ - name = "attached-disk" - size = 10 - }] + attached_disks = { + attached-disk = {} + } service_account = { auto_create = true } @@ -819,10 +824,9 @@ module "autokey-vm-example" { network = "projects/myhost/global/networks/dev-spoke-0" subnetwork = "projects/myhost/regions/europe-west8/subnetworks/gce" }] - attached_disks = [{ - name = "attached-disk" - size = 10 - }] + attached_disks = { + attached-disk = {} + } service_account = { auto_create = true } @@ -839,7 +843,7 @@ module "autokey-vm-example" { ### Advanced machine features -Advanced machine features can be configured via the `options.advanced_machine_features` variable. +Advanced machine features can be configured via the `machine_features_config` variable. ```hcl module "simple-vm-example" { @@ -851,12 +855,10 @@ module "simple-vm-example" { network = var.vpc.self_link subnetwork = var.subnet.self_link }] - options = { - advanced_machine_features = { - enable_nested_virtualization = true - enable_turbo_mode = true - threads_per_core = 2 - } + machine_features_config = { + enable_nested_virtualization = true + enable_turbo_mode = true + threads_per_core = 2 } } # tftest modules=1 resources=1 @@ -879,13 +881,13 @@ module "cos-test" { subnetwork = var.subnet.self_link }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } - attached_disks = [ - { size = 10 } - ] + attached_disks = { + disk-0 = {} + } service_account = { email = module.iam-service-account.email } @@ -909,13 +911,15 @@ module "cos-test" { subnetwork = var.subnet.self_link }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } - attached_disks = [ - { size = 10 } - ] + attached_disks = { + disk-0 = { + auto_delete = true + } + } service_account = { email = module.iam-service-account.email } @@ -945,7 +949,7 @@ module "instance-group" { subnetwork = var.subnet.self_link }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } @@ -976,7 +980,7 @@ module "instance" { subnetwork = var.subnet.self_link }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } @@ -1019,7 +1023,7 @@ module "instance" { subnetwork = var.subnet.self_link }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } @@ -1047,21 +1051,19 @@ module "instance" { subnetwork = var.subnet.self_link }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } snapshot_schedule = ["boot"] } - attached_disks = [ - { - name = "disk-1" - size = 10 - options = { + attached_disks = { + disk-1 = { + initialize_params = { replica_zone = "${var.region}-c" } snapshot_schedule = ["data"] } - ] + } snapshot_schedules = { boot = { schedule = { @@ -1141,16 +1143,16 @@ You can add node affinities (and anti-affinity) configurations to allocate the V ```hcl module "sole-tenancy" { - source = "./fabric/modules/compute-vm" - project_id = var.project_id - zone = "${var.region}-b" - instance_type = "n1-standard-1" - name = "test" + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "${var.region}-b" + machine_type = "n1-standard-1" + name = "test" network_interfaces = [{ network = var.vpc.self_link subnetwork = var.subnet.self_link }] - options = { + scheduling_config = { node_affinities = { workload = { values = ["frontend"] @@ -1169,43 +1171,45 @@ module "sole-tenancy" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L323) | Instance name. | string | ✓ | | -| [network_interfaces](variables.tf#L335) | Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed. | list(object({…})) | ✓ | | -| [project_id](variables.tf#L430) | Project id. | string | ✓ | | -| [zone](variables.tf#L550) | Compute zone. | string | ✓ | | -| [attached_disk_defaults](variables.tf#L17) | Defaults for attached disks options. | object({…}) | | {…} | -| [attached_disks](variables.tf#L37) | Additional disks, if options is null defaults will be used in its place. Source type is one of 'image' (zonal disks in vms and template), 'snapshot' (vm), 'existing', and null. | list(object({…})) | | [] | -| [boot_disk](variables.tf#L92) | Boot disk properties. Initialize params are ignored when source is set. | object({…}) | | {…} | -| [can_ip_forward](variables.tf#L135) | Enable IP forwarding. | bool | | false | -| [confidential_compute](variables.tf#L141) | Enable Confidential Compute for these instances. | bool | | false | -| [context](variables.tf#L147) | Context-specific interpolations. | object({…}) | | {} | -| [create_template](variables.tf#L164) | Create instance template instead of instances. Defaults to a global template. | object({…}) | | null | -| [description](variables.tf#L173) | Description of a Compute Instance. | string | | "Managed by the compute-vm Terraform module." | -| [enable_display](variables.tf#L179) | Enable virtual display on the instances. | bool | | false | -| [encryption](variables.tf#L185) | Encryption options. Only one of kms_key_self_link and disk_encryption_key_raw may be set. If needed, you can specify to encrypt or not the boot disk. | object({…}) | | null | -| [gpu](variables.tf#L195) | GPU information. Based on https://cloud.google.com/compute/docs/gpus. | object({…}) | | null | -| [group](variables.tf#L230) | Define this variable to create an instance group for instances. Disabled for template use. | object({…}) | | null | -| [hostname](variables.tf#L238) | Instance FQDN name. | string | | null | -| [iam](variables.tf#L244) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [instance_schedule](variables.tf#L250) | Assign or create and assign an instance schedule policy. Either resource policy id or create_config must be specified if not null. Set active to null to dtach a policy from vm before destroying. | object({…}) | | null | -| [instance_type](variables.tf#L274) | Instance type. | string | | "e2-micro" | -| [kms_autokeys](variables.tf#L280) | KMS Autokey key handles. If location is not specified it will be inferred from the zone. Key handle names will be added to the kms_keys context with an `autokeys/` prefix. | map(object({…})) | | {} | -| [labels](variables.tf#L298) | Instance labels. | map(string) | | {} | -| [metadata](variables.tf#L304) | Instance metadata. | map(string) | | {} | -| [metadata_startup_script](variables.tf#L310) | Instance startup script. Will trigger recreation on change, even after importing. | string | | null | -| [min_cpu_platform](variables.tf#L317) | Minimum CPU platform. | string | | null | -| [network_attached_interfaces](variables.tf#L328) | Network interfaces using network attachments. | list(string) | | [] | -| [network_tag_bindings](variables.tf#L356) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance only for networking purposes, and modifiable without impacting the main resource lifecycle. | map(string) | | {} | -| [options](variables.tf#L363) | Instance options. | object({…}) | | {…} | -| [project_number](variables.tf#L435) | Project number. Used in tag bindings to avoid a permadiff. | string | | null | -| [resource_policies](variables.tf#L441) | Resource policies to attach to the instance or template. | list(string) | | null | -| [scratch_disks](variables.tf#L448) | Scratch disks configuration. | object({…}) | | {…} | -| [service_account](variables.tf#L460) | Service account email and scopes. If email is null, the default Compute service account will be used unless auto_create is true, in which case a service account will be created. Set the variable to null to avoid attaching a service account. | object({…}) | | {} | -| [shielded_config](variables.tf#L470) | Shielded VM configuration of the instances. | object({…}) | | null | -| [snapshot_schedules](variables.tf#L480) | Snapshot schedule resource policies that can be attached to disks. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L523) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance and zonal disks, and modifiable without impacting the main resource lifecycle. | map(string) | | {} | -| [tag_bindings_immutable](variables.tf#L530) | Immutable resource manager tag bindings, in tagKeys/id => tagValues/id format. These are set on the instance or instance template at creation time, and trigger recreation if changed. | map(string) | | null | -| [tags](variables.tf#L544) | Instance network tags for firewall rule targets. | list(string) | | [] | +| [name](variables.tf#L353) | Instance name. | string | ✓ | | +| [network_interfaces](variables.tf#L365) | Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed. | list(object({…})) | ✓ | | +| [project_id](variables.tf#L405) | Project id. | string | ✓ | | +| [zone](variables.tf#L562) | Compute zone. | string | ✓ | | +| [attached_disks](variables.tf#L17) | Additional disks. Source type is one of 'image' (zonal disks in vms and template), 'snapshot' (vm), 'existing', and null. | map(object({…})) | | {} | +| [boot_disk](variables.tf#L56) | Boot disk properties. | object({…}) | | {} | +| [can_ip_forward](variables.tf#L113) | Enable IP forwarding. | bool | | false | +| [confidential_compute](variables.tf#L119) | Confidential Compute configuration. Set to 'SEV' or 'SEV_SNP' to enable. | string | | null | +| [context](variables.tf#L129) | Context-specific interpolations. | object({…}) | | {} | +| [create_template](variables.tf#L146) | Create instance template instead of instances. Defaults to a global template. | object({…}) | | null | +| [description](variables.tf#L155) | Description of a Compute Instance. | string | | "Managed by the compute-vm Terraform module." | +| [enable_display](variables.tf#L161) | Enable virtual display on the instances. | bool | | false | +| [encryption](variables.tf#L167) | Encryption options. Only one of kms_key_self_link and disk_encryption_key_raw may be set. If needed, you can specify to encrypt or not the boot disk. | object({…}) | | null | +| [gpu](variables.tf#L178) | GPU information. Based on https://cloud.google.com/compute/docs/gpus. | object({…}) | | null | +| [group](variables.tf#L213) | Instance group configuration. Set 'named_ports' to create a new unmanaged instance group, or provide an existing group self_link/id in 'membership' to join one. | object({…}) | | null | +| [hostname](variables.tf#L222) | Instance FQDN name. | string | | null | +| [iam](variables.tf#L228) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [instance_schedule](variables.tf#L234) | Assign or create and assign an instance schedule policy. Set active to null to detach a policy from vm before destroying. | object({…}) | | null | +| [kms_autokeys](variables.tf#L258) | KMS Autokey key handles. If location is not specified it will be inferred from the zone. Key handle names will be added to the kms_keys context with an `autokeys/` prefix. | map(object({…})) | | {} | +| [labels](variables.tf#L276) | Instance labels. | map(string) | | {} | +| [lifecycle_config](variables.tf#L282) | Instance lifecycle and operational configurations. | object({…}) | | {} | +| [machine_features_config](variables.tf#L304) | Machine-level configuration. | object({…}) | | {} | +| [machine_type](variables.tf#L328) | Machine type. | string | | "e2-micro" | +| [metadata](variables.tf#L334) | Instance metadata. | map(string) | | {} | +| [metadata_startup_script](variables.tf#L340) | Instance startup script. Will trigger recreation on change, even after importing. | string | | null | +| [min_cpu_platform](variables.tf#L347) | Minimum CPU platform. | string | | null | +| [network_attached_interfaces](variables.tf#L358) | Network interfaces using network attachments. | list(string) | | [] | +| [network_performance_tier](variables.tf#L388) | Network performance total egress bandwidth tier. | string | | null | +| [network_tag_bindings](variables.tf#L398) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance only for networking purposes, and modifiable without impacting the main resource lifecycle. | map(string) | | {} | +| [project_number](variables.tf#L410) | Project number. Used in tag bindings to avoid a permadiff. | string | | null | +| [resource_policies](variables.tf#L416) | Resource policies to attach to the instance or template. | list(string) | | null | +| [scheduling_config](variables.tf#L423) | Scheduling configuration for the instance. | object({…}) | | {} | +| [scratch_disks](variables.tf#L458) | Scratch disks configuration. | object({…}) | | {…} | +| [service_account](variables.tf#L471) | Service account email and scopes. If email is null, the default Compute service account will be used unless auto_create is true, in which case a service account will be created. Set the variable to null to avoid attaching a service account. | object({…}) | | {} | +| [shielded_config](variables.tf#L482) | Shielded VM configuration of the instances. | object({…}) | | null | +| [snapshot_schedules](variables.tf#L492) | Snapshot schedule resource policies that can be attached to disks. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L535) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance and zonal disks, and modifiable without impacting the main resource lifecycle. | map(string) | | {} | +| [tag_bindings_immutable](variables.tf#L542) | Immutable resource manager tag bindings, in tagKeys/id => tagValues/id format. These are set on the instance or instance template at creation time, and trigger recreation if changed. | map(string) | | null | +| [tags](variables.tf#L556) | Instance network tags for firewall rule targets. | list(string) | | [] | ## Outputs diff --git a/modules/compute-vm/disks.tf b/modules/compute-vm/disks.tf new file mode 100644 index 000000000..05ddb840a --- /dev/null +++ b/modules/compute-vm/disks.tf @@ -0,0 +1,127 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + attached_disks_regional = { + for k, v in var.attached_disks : k => v + if v.initialize_params.replica_zone != null + } + attached_disks_zonal = { + for k, v in var.attached_disks : k => v + if v.initialize_params.replica_zone == null + } +} + +resource "google_compute_disk" "boot" { + count = ( + !local.is_template && var.boot_disk.use_independent_disk != null ? 1 : 0 + ) + project = local.project_id + zone = local.zone + # by default, GCP creates boot disks with the same name as the instance + # the deviation here is kept for backwards compatibility + name = coalesce( + var.boot_disk.use_independent_disk.name, "${var.name}-boot" + ) + image = var.boot_disk.source.image + architecture = var.boot_disk.architecture + type = var.boot_disk.initialize_params.type + size = var.boot_disk.initialize_params.size + provisioned_iops = var.boot_disk.initialize_params.hyperdisk.provisioned_iops + provisioned_throughput = var.boot_disk.initialize_params.hyperdisk.provisioned_throughput + storage_pool = var.boot_disk.initialize_params.hyperdisk.storage_pool + labels = merge(var.labels, { + disk_name = "boot" + disk_type = var.boot_disk.initialize_params.type + }) + dynamic "disk_encryption_key" { + for_each = var.encryption != null ? [""] : [] + content { + raw_key = var.encryption.disk_encryption_key_raw + kms_key_self_link = lookup( + local.ctx_kms_keys, + var.encryption.kms_key_self_link, + var.encryption.kms_key_self_link + ) + } + } +} + +resource "google_compute_disk" "disks" { + for_each = local.is_template ? {} : { + for k, v in local.attached_disks_zonal : + k => v if v.source.attach == null + } + project = local.project_id + zone = local.zone + name = coalesce(each.value.name, "${var.name}-${each.key}") + type = each.value.initialize_params.type + size = each.value.initialize_params.size + architecture = var.boot_disk.architecture + image = each.value.source.image + provisioned_iops = each.value.initialize_params.hyperdisk.provisioned_iops + provisioned_throughput = each.value.initialize_params.hyperdisk.provisioned_throughput + snapshot = each.value.source.snapshot + storage_pool = each.value.initialize_params.hyperdisk.storage_pool + labels = merge(var.labels, { + disk_name = coalesce(each.value.name, each.key) + disk_type = each.value.initialize_params.type + }) + dynamic "disk_encryption_key" { + for_each = var.encryption != null ? [""] : [] + content { + raw_key = var.encryption.disk_encryption_key_raw + kms_key_self_link = lookup( + local.ctx_kms_keys, + var.encryption.kms_key_self_link, + var.encryption.kms_key_self_link + ) + } + } +} + +resource "google_compute_region_disk" "disks" { + for_each = local.is_template ? {} : { + for k, v in local.attached_disks_regional : + k => v if v.source.attach == null + } + project = local.project_id + region = local.region + replica_zones = [local.zone, each.value.initialize_params.replica_zone] + name = coalesce(each.value.name, "${var.name}-${each.key}") + type = each.value.initialize_params.type + size = each.value.initialize_params.size + # image = each.value.source.image + snapshot = each.value.source.snapshot + labels = merge(var.labels, { + disk_name = coalesce(each.value.name, each.key) + disk_type = each.value.initialize_params.type + }) + dynamic "disk_encryption_key" { + for_each = var.encryption != null ? [""] : [] + content { + raw_key = var.encryption.disk_encryption_key_raw + # TODO: check if self link works here + kms_key_name = lookup( + local.ctx_kms_keys, + var.encryption.kms_key_self_link, + var.encryption.kms_key_self_link + ) + + } + } +} + diff --git a/modules/compute-vm/instance.tf b/modules/compute-vm/instance.tf new file mode 100644 index 000000000..35eefe6ef --- /dev/null +++ b/modules/compute-vm/instance.tf @@ -0,0 +1,311 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +resource "google_compute_instance" "default" { + provider = google-beta + count = local.is_template ? 0 : 1 + project = local.project_id + zone = local.zone + name = var.name + hostname = var.hostname + description = var.description + tags = var.tags + machine_type = var.machine_type + min_cpu_platform = var.min_cpu_platform + can_ip_forward = var.can_ip_forward + allow_stopping_for_update = var.lifecycle_config.allow_stopping_for_update + deletion_protection = var.lifecycle_config.deletion_protection + key_revocation_action_type = var.lifecycle_config.key_revocation_action_type + enable_display = var.enable_display + labels = var.labels + metadata = var.metadata + metadata_startup_script = var.metadata_startup_script + resource_policies = ( + var.resource_policies == null && var.instance_schedule == null + ? null + : concat( + coalesce(var.resource_policies, []), + coalesce(local.ischedule, []) + ) + ) + + dynamic "advanced_machine_features" { + for_each = local.advanced_mf ? [""] : [] + content { + enable_nested_virtualization = var.machine_features_config.enable_nested_virtualization + enable_uefi_networking = var.machine_features_config.enable_uefi_networking + performance_monitoring_unit = var.machine_features_config.performance_monitoring_unit + threads_per_core = var.machine_features_config.threads_per_core + turbo_mode = ( + var.machine_features_config.enable_turbo_mode == true ? "ALL_CORE_MAX" : null + ) + visible_core_count = var.machine_features_config.visible_core_count + } + } + + dynamic "attached_disk" { + for_each = local.attached_disks_zonal + iterator = disk + content { + device_name = coalesce( + disk.value.device_name, disk.value.name, disk.key + ) + force_attach = disk.value.force_attach + mode = disk.value.mode + source = ( + disk.value.source.attach != null + ? disk.value.source.attach + : google_compute_disk.disks[disk.key].name + ) + } + } + + dynamic "attached_disk" { + for_each = local.attached_disks_regional + iterator = disk + content { + device_name = coalesce( + disk.value.device_name, disk.value.name, disk.key + ) + force_attach = disk.value.force_attach + mode = disk.value.mode + source = ( + disk.value.source.attach != null + ? disk.value.source.attach + : google_compute_region_disk.disks[disk.key].id + ) + } + } + + boot_disk { + auto_delete = ( + var.boot_disk.use_independent_disk != null + ? false + : var.boot_disk.auto_delete + ) + force_attach = var.boot_disk.force_attach + source = ( + var.boot_disk.use_independent_disk != null + ? google_compute_disk.boot[0].id + : try(coalesce( + var.boot_disk.source.snapshot, + var.boot_disk.source.attach + ), null) + ) + disk_encryption_key_raw = ( + var.encryption != null ? + try( + local.ctx_kms_keys[var.encryption.disk_encryption_key_raw], + var.encryption.disk_encryption_key_raw + ) + : null + ) + kms_key_self_link = ( + var.encryption != null + ? try( + local.ctx_kms_keys[var.encryption.kms_key_self_link], + var.encryption.kms_key_self_link + ) + : null + ) + dynamic "initialize_params" { + for_each = ( + var.boot_disk.initialize_params == null + || + var.boot_disk.use_independent_disk != null + || ( + var.boot_disk.source.snapshot != null && + var.boot_disk.source.attach != null + ) + ? [] + : [""] + ) + content { + architecture = var.boot_disk.architecture + image = var.boot_disk.source.image + size = var.boot_disk.initialize_params.size + type = var.boot_disk.initialize_params.type + resource_manager_tags = var.tag_bindings_immutable + provisioned_iops = var.boot_disk.initialize_params.hyperdisk.provisioned_iops + provisioned_throughput = var.boot_disk.initialize_params.hyperdisk.provisioned_throughput + storage_pool = var.boot_disk.initialize_params.hyperdisk.storage_pool + } + } + } + + dynamic "confidential_instance_config" { + for_each = var.confidential_compute != null ? [""] : [] + content { + enable_confidential_compute = true + } + } + + dynamic "network_interface" { + for_each = var.network_interfaces + iterator = config + content { + network = lookup( + local.ctx.networks, config.value.network, config.value.network + ) + subnetwork = lookup( + local.ctx.subnets, config.value.subnetwork, config.value.subnetwork + ) + network_ip = try( + local.ctx.addresses[config.value.addresses.internal], + config.value.addresses.internal, + null + ) + nic_type = config.value.nic_type + stack_type = config.value.stack_type + queue_count = config.value.queue_count + internal_ipv6_prefix_length = config.value.internal_ipv6_prefix_length + dynamic "access_config" { + for_each = config.value.nat || config.value.network_tier != null ? [""] : [] + content { + nat_ip = try( + local.ctx.addresses[config.value.addresses.external], + config.value.addresses.external, + null + ) + network_tier = try(config.value.network_tier, null) + } + } + dynamic "alias_ip_range" { + for_each = config.value.alias_ips + iterator = config_alias + content { + subnetwork_range_name = config_alias.key + ip_cidr_range = config_alias.value + } + } + } + } + + dynamic "network_interface" { + for_each = var.network_attached_interfaces + content { + network_attachment = network_interface.value + } + } + + dynamic "network_performance_config" { + for_each = var.network_performance_tier != null ? [""] : [] + content { + total_egress_bandwidth_tier = var.network_performance_tier + } + } + + scheduling { + automatic_restart = coalesce( + var.scheduling_config.automatic_restart, var.scheduling_config.provisioning_model != "SPOT" + ) + instance_termination_action = local.termination_action + on_host_maintenance = local.on_host_maintenance + preemptible = var.scheduling_config.provisioning_model == "SPOT" + provisioning_model = coalesce(var.scheduling_config.provisioning_model, "STANDARD") + min_node_cpus = var.scheduling_config.min_node_cpus + maintenance_interval = var.scheduling_config.maintenance_interval + + dynamic "max_run_duration" { + for_each = var.scheduling_config.max_run_duration == null ? [] : [""] + content { + nanos = var.scheduling_config.max_run_duration.nanos + seconds = var.scheduling_config.max_run_duration.seconds + } + } + + dynamic "local_ssd_recovery_timeout" { + for_each = var.scheduling_config.local_ssd_recovery_timeout == null ? [] : [""] + content { + nanos = var.scheduling_config.local_ssd_recovery_timeout.nanos + seconds = var.scheduling_config.local_ssd_recovery_timeout.seconds + } + } + + dynamic "node_affinities" { + for_each = var.scheduling_config.node_affinities + iterator = affinity + content { + key = affinity.key + operator = affinity.value.in ? "IN" : "NOT_IN" + values = affinity.value.values + } + } + + dynamic "graceful_shutdown" { + for_each = var.lifecycle_config.graceful_shutdown != null ? [""] : [] + content { + enabled = var.lifecycle_config.graceful_shutdown.enabled + dynamic "max_duration" { + for_each = ( + var.lifecycle_config.graceful_shutdown.enabled == true && + var.lifecycle_config.graceful_shutdown.max_duration_secs != null + ? [""] + : [] + ) + content { + seconds = var.lifecycle_config.graceful_shutdown.max_duration_secs + nanos = 0 + } + } + } + } + + } + + dynamic "scratch_disk" { + for_each = [ + for i in range(0, var.scratch_disks.count) : var.scratch_disks.interface + ] + iterator = config + content { + interface = config.value + } + } + + dynamic "service_account" { + for_each = var.service_account == null ? [] : [""] + content { + email = local.service_account.email + scopes = local.service_account.scopes + } + } + + dynamic "shielded_instance_config" { + for_each = var.shielded_config != null ? [var.shielded_config] : [] + iterator = config + content { + enable_secure_boot = config.value.enable_secure_boot + enable_vtpm = config.value.enable_vtpm + enable_integrity_monitoring = config.value.enable_integrity_monitoring + } + } + + dynamic "params" { + for_each = var.tag_bindings_immutable == null ? [] : [""] + content { + resource_manager_tags = var.tag_bindings_immutable + } + } + + dynamic "guest_accelerator" { + for_each = local.gpu ? [var.gpu] : [] + content { + type = guest_accelerator.value.type + count = guest_accelerator.value.count + } + } +} diff --git a/modules/compute-vm/main.tf b/modules/compute-vm/main.tf index e8b5d7fbd..05d14fd09 100644 --- a/modules/compute-vm/main.tf +++ b/modules/compute-vm/main.tf @@ -15,22 +15,10 @@ */ locals { - _region = join("-", slice(split("-", local.zone), 0, 2)) - advanced_mf = var.options.advanced_machine_features - attached_disks = { - for i, disk in var.attached_disks : - coalesce(disk.name, disk.device_name, "disk-${i}") => merge(disk, { - options = disk.options == null ? var.attached_disk_defaults : disk.options - }) - } - attached_disks_regional = { - for k, v in local.attached_disks : - k => v if try(v.options.replica_zone, null) != null - } - attached_disks_zonal = { - for k, v in local.attached_disks : - k => v if try(v.options.replica_zone, null) == null - } + _region = join("-", slice(split("-", local.zone), 0, 2)) + advanced_mf = anytrue([ + for k, v in var.machine_features_config : v != null + ]) ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv @@ -42,10 +30,13 @@ locals { }) ctx_p = "$" gpu = var.gpu != null - on_host_maintenance = ( - var.options.spot || var.confidential_compute || local.gpu - ? "TERMINATE" - : "MIGRATE" + on_host_maintenance = coalesce( + var.scheduling_config.on_host_maintenance, + ( + var.scheduling_config.provisioning_model == "SPOT" || + var.confidential_compute != null || + local.gpu + ) ? "TERMINATE" : "MIGRATE" ) project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) region = lookup(local.ctx.locations, local._region, local._region) @@ -75,7 +66,9 @@ locals { ) } termination_action = ( - var.options.spot || var.options.max_run_duration != null ? coalesce(var.options.termination_action, "STOP") : null + var.scheduling_config.provisioning_model == "SPOT" || var.scheduling_config.max_run_duration != null + ? coalesce(var.scheduling_config.termination_action, "STOP") + : null ) zone = lookup(local.ctx.locations, var.zone, var.zone) } @@ -92,366 +85,6 @@ resource "google_kms_key_handle" "default" { resource_type_selector = each.value.resource_type_selector } -resource "google_compute_disk" "boot" { - count = !local.template_create && var.boot_disk.use_independent_disk ? 1 : 0 - project = local.project_id - zone = local.zone - # by default, GCP creates boot disks with the same name as instance, the deviation here is kept for backwards - # compatibility - name = coalesce(var.boot_disk.name, "${var.name}-boot") - type = var.boot_disk.initialize_params.type - size = var.boot_disk.initialize_params.size - architecture = var.boot_disk.initialize_params.architecture - image = var.boot_disk.initialize_params.image - provisioned_iops = var.boot_disk.initialize_params.provisioned_iops - provisioned_throughput = var.boot_disk.initialize_params.provisioned_throughput - storage_pool = var.boot_disk.initialize_params.storage_pool - labels = merge(var.labels, { - disk_name = "boot" - disk_type = var.boot_disk.initialize_params.type - }) - dynamic "disk_encryption_key" { - for_each = var.encryption != null ? [""] : [] - content { - raw_key = var.encryption.disk_encryption_key_raw - kms_key_self_link = lookup( - local.ctx_kms_keys, - var.encryption.kms_key_self_link, - var.encryption.kms_key_self_link - ) - } - } -} - -resource "google_compute_disk" "disks" { - for_each = local.template_create ? {} : { - for k, v in local.attached_disks_zonal : - k => v if v.source_type != "attach" - } - project = local.project_id - zone = local.zone - name = "${var.name}-${each.key}" - type = each.value.options.type - size = each.value.size - architecture = each.value.options.architecture - image = each.value.source_type == "image" ? each.value.source : null - provisioned_iops = each.value.options.provisioned_iops - provisioned_throughput = each.value.options.provisioned_throughput - snapshot = each.value.source_type == "snapshot" ? each.value.source : null - storage_pool = each.value.options.storage_pool - labels = merge(var.labels, { - disk_name = each.value.name - disk_type = each.value.options.type - }) - dynamic "disk_encryption_key" { - for_each = var.encryption != null ? [""] : [] - content { - raw_key = var.encryption.disk_encryption_key_raw - kms_key_self_link = lookup( - local.ctx_kms_keys, - var.encryption.kms_key_self_link, - var.encryption.kms_key_self_link - ) - } - } -} - -resource "google_compute_region_disk" "disks" { - provider = google-beta - for_each = local.template_create ? {} : { - for k, v in local.attached_disks_regional : - k => v if v.source_type != "attach" - } - project = local.project_id - region = local.region - replica_zones = [local.zone, each.value.options.replica_zone] - name = "${var.name}-${each.key}" - type = each.value.options.type - size = each.value.size - # image = each.value.source_type == "image" ? each.value.source : null - snapshot = each.value.source_type == "snapshot" ? each.value.source : null - labels = merge(var.labels, { - disk_name = each.value.name - disk_type = each.value.options.type - }) - dynamic "disk_encryption_key" { - for_each = var.encryption != null ? [""] : [] - content { - raw_key = var.encryption.disk_encryption_key_raw - # TODO: check if self link works here - kms_key_name = lookup( - local.ctx_kms_keys, - var.encryption.kms_key_self_link, - var.encryption.kms_key_self_link - ) - - } - } -} - -resource "google_compute_instance" "default" { - provider = google-beta - count = local.template_create ? 0 : 1 - project = local.project_id - zone = local.zone - name = var.name - hostname = var.hostname - description = var.description - tags = var.tags - machine_type = var.instance_type - min_cpu_platform = var.min_cpu_platform - can_ip_forward = var.can_ip_forward - allow_stopping_for_update = var.options.allow_stopping_for_update - deletion_protection = var.options.deletion_protection - key_revocation_action_type = var.options.key_revocation_action_type - enable_display = var.enable_display - labels = var.labels - metadata = var.metadata - metadata_startup_script = var.metadata_startup_script - resource_policies = ( - var.resource_policies == null && var.instance_schedule == null - ? null - : concat( - coalesce(var.resource_policies, []), - coalesce(local.ischedule, []) - ) - ) - - dynamic "advanced_machine_features" { - for_each = local.advanced_mf != null ? [""] : [] - content { - enable_nested_virtualization = local.advanced_mf.enable_nested_virtualization - enable_uefi_networking = local.advanced_mf.enable_uefi_networking - performance_monitoring_unit = local.advanced_mf.performance_monitoring_unit - threads_per_core = local.advanced_mf.threads_per_core - turbo_mode = ( - local.advanced_mf.enable_turbo_mode ? "ALL_CORE_MAX" : null - ) - visible_core_count = local.advanced_mf.visible_core_count - } - } - - dynamic "attached_disk" { - for_each = local.attached_disks_zonal - iterator = config - content { - device_name = ( - config.value.device_name != null - ? config.value.device_name - : config.value.name - ) - mode = config.value.options.mode - source = ( - config.value.source_type == "attach" - ? config.value.source - : google_compute_disk.disks[config.key].name - ) - } - } - - dynamic "attached_disk" { - for_each = local.attached_disks_regional - iterator = config - content { - device_name = coalesce( - config.value.device_name, config.value.name, config.key - ) - mode = config.value.options.mode - source = ( - config.value.source_type == "attach" - ? config.value.source - : google_compute_region_disk.disks[config.key].id - ) - } - } - - boot_disk { - auto_delete = ( - var.boot_disk.use_independent_disk - ? false - : var.boot_disk.auto_delete - ) - source = ( - var.boot_disk.use_independent_disk - ? google_compute_disk.boot[0].id - : var.boot_disk.source - ) - disk_encryption_key_raw = ( - var.encryption != null ? - try( - local.ctx_kms_keys[var.encryption.disk_encryption_key_raw], - var.encryption.disk_encryption_key_raw - ) - : null - ) - kms_key_self_link = ( - var.encryption != null - ? try( - local.ctx_kms_keys[var.encryption.kms_key_self_link], - var.encryption.kms_key_self_link - ) - : null - ) - dynamic "initialize_params" { - for_each = ( - var.boot_disk.initialize_params == null - || - var.boot_disk.use_independent_disk - || - var.boot_disk.source != null - ? [] - : [""] - ) - content { - architecture = var.boot_disk.initialize_params.architecture - image = var.boot_disk.initialize_params.image - size = var.boot_disk.initialize_params.size - type = var.boot_disk.initialize_params.type - resource_manager_tags = var.tag_bindings_immutable - provisioned_iops = var.boot_disk.initialize_params.provisioned_iops - provisioned_throughput = var.boot_disk.initialize_params.provisioned_throughput - storage_pool = var.boot_disk.initialize_params.storage_pool - } - } - } - - dynamic "confidential_instance_config" { - for_each = var.confidential_compute ? [""] : [] - content { - enable_confidential_compute = true - } - } - - dynamic "network_interface" { - for_each = var.network_interfaces - iterator = config - content { - network = lookup( - local.ctx.networks, config.value.network, config.value.network - ) - subnetwork = lookup( - local.ctx.subnets, config.value.subnetwork, config.value.subnetwork - ) - network_ip = try( - local.ctx.addresses[config.value.addresses.internal], - config.value.addresses.internal, - null - ) - nic_type = config.value.nic_type - stack_type = config.value.stack_type - dynamic "access_config" { - for_each = config.value.nat || config.value.network_tier != null ? [""] : [] - content { - nat_ip = try( - local.ctx.addresses[config.value.addresses.external], - config.value.addresses.external, - null - ) - network_tier = try(config.value.network_tier, null) - } - } - dynamic "alias_ip_range" { - for_each = config.value.alias_ips - iterator = config_alias - content { - subnetwork_range_name = config_alias.key - ip_cidr_range = config_alias.value - } - } - } - } - - dynamic "network_interface" { - for_each = var.network_attached_interfaces - content { - network_attachment = network_interface.value - } - } - - scheduling { - automatic_restart = !var.options.spot - instance_termination_action = local.termination_action - on_host_maintenance = local.on_host_maintenance - preemptible = var.options.spot - provisioning_model = var.options.spot ? "SPOT" : "STANDARD" - dynamic "max_run_duration" { - for_each = var.options.max_run_duration == null ? [] : [""] - content { - nanos = var.options.max_run_duration.nanos - seconds = var.options.max_run_duration.seconds - } - } - - dynamic "node_affinities" { - for_each = var.options.node_affinities - iterator = affinity - content { - key = affinity.key - operator = affinity.value.in ? "IN" : "NOT_IN" - values = affinity.value.values - } - } - - dynamic "graceful_shutdown" { - for_each = var.options.graceful_shutdown != null ? [""] : [] - content { - enabled = var.options.graceful_shutdown.enabled - dynamic "max_duration" { - for_each = var.options.graceful_shutdown.enabled == true && var.options.graceful_shutdown.max_duration_secs != null ? [""] : [] - content { - seconds = var.options.graceful_shutdown.max_duration_secs - nanos = 0 - } - } - } - } - - } - - dynamic "scratch_disk" { - for_each = [ - for i in range(0, var.scratch_disks.count) : var.scratch_disks.interface - ] - iterator = config - content { - interface = config.value - } - } - - dynamic "service_account" { - for_each = var.service_account == null ? [] : [""] - content { - email = local.service_account.email - scopes = local.service_account.scopes - } - } - - dynamic "shielded_instance_config" { - for_each = var.shielded_config != null ? [var.shielded_config] : [] - iterator = config - content { - enable_secure_boot = config.value.enable_secure_boot - enable_vtpm = config.value.enable_vtpm - enable_integrity_monitoring = config.value.enable_integrity_monitoring - } - } - - dynamic "params" { - for_each = var.tag_bindings_immutable == null ? [] : [""] - content { - resource_manager_tags = var.tag_bindings_immutable - } - } - - dynamic "guest_accelerator" { - for_each = local.gpu ? [var.gpu] : [] - content { - type = guest_accelerator.value.type - count = guest_accelerator.value.count - } - } -} - resource "google_compute_instance_iam_binding" "default" { project = local.project_id for_each = var.iam @@ -465,7 +98,7 @@ resource "google_compute_instance_iam_binding" "default" { } resource "google_compute_instance_group" "unmanaged" { - count = var.group != null && !local.template_create ? 1 : 0 + count = var.group != null && !local.is_template ? 1 : 0 project = local.project_id network = ( length(var.network_interfaces) > 0 diff --git a/modules/compute-vm/resource-policies.tf b/modules/compute-vm/resource-policies.tf index 26a841525..873bbc243 100644 --- a/modules/compute-vm/resource-policies.tf +++ b/modules/compute-vm/resource-policies.tf @@ -21,23 +21,21 @@ locals { google_compute_resource_policy.schedule[0].id ] disk_zonal_schedule_attachments = flatten([ - for disk_key, disk_data in local.attached_disks_zonal : - disk_data.snapshot_schedule != null ? [ - for schedule in disk_data.snapshot_schedule : { - disk_key = disk_key - source_type = disk_data.source_type - source = disk_data.source + for k, v in local.attached_disks_zonal : + v.snapshot_schedule != null ? [ + for schedule in v.snapshot_schedule : { + disk_key = k + source = v.source snapshot_schedule = schedule } ] : [] ]) disk_regional_schedule_attachments = flatten([ - for disk_key, disk_data in try(local.attached_disks_regional, []) : - disk_data.snapshot_schedule != null ? [ - for schedule in disk_data.snapshot_schedule : { - disk_key = disk_key - source_type = disk_data.source_type - source = disk_data.source + for k, v in try(local.attached_disks_regional, []) : + v.snapshot_schedule != null ? [ + for schedule in v.snapshot_schedule : { + disk_key = k + source = v.source snapshot_schedule = schedule } ] : [] @@ -140,7 +138,7 @@ resource "google_compute_disk_resource_policy_attachment" "boot" { ) # if independent disk is used for boot disk it will have a different name compared to when created implicitly disk = ( - !local.template_create && var.boot_disk.use_independent_disk + !local.is_template && var.boot_disk.use_independent_disk != null ? google_compute_disk.boot[0].name : var.name ) @@ -160,8 +158,8 @@ resource "google_compute_disk_resource_policy_attachment" "attached" { each.value.snapshot_schedule ) disk = ( - each.value.source_type == "attach" - ? each.value.source + each.value.source.attach != null + ? each.value.source.attach : google_compute_disk.disks[each.value.disk_key].name ) depends_on = [ @@ -182,8 +180,8 @@ resource "google_compute_region_disk_resource_policy_attachment" "attached" { each.value.snapshot_schedule ) disk = ( - each.value.source_type == "attach" - ? each.value.source + each.value.source.attach != null + ? each.value.source.attach : google_compute_region_disk.disks[each.value.disk_key].name ) depends_on = [ diff --git a/modules/compute-vm/tags.tf b/modules/compute-vm/tags.tf index d5a6a4fb7..e99f13586 100644 --- a/modules/compute-vm/tags.tf +++ b/modules/compute-vm/tags.tf @@ -53,7 +53,7 @@ locals { # use a different resource to avoid overlapping key issues resource "google_tags_location_tag_binding" "network" { - for_each = local.template_create ? {} : var.network_tag_bindings + for_each = local.is_template ? {} : var.network_tag_bindings parent = ( "${local.tag_parent_base}/zones/${local.zone}/instances/${google_compute_instance.default[0].instance_id}" ) @@ -62,7 +62,7 @@ resource "google_tags_location_tag_binding" "network" { } resource "google_tags_location_tag_binding" "instance" { - for_each = local.template_create ? {} : var.tag_bindings + for_each = local.is_template ? {} : var.tag_bindings parent = ( "${local.tag_parent_base}/zones/${local.zone}/instances/${google_compute_instance.default[0].instance_id}" ) @@ -72,7 +72,7 @@ resource "google_tags_location_tag_binding" "instance" { resource "google_tags_location_tag_binding" "boot_disks" { for_each = ( - local.template_create ? {} : { for v in local.boot_disk_tags : v.key => v } + local.is_template ? {} : { for v in local.boot_disk_tags : v.key => v } ) parent = ( "${local.tag_parent_base}/zones/${local.zone}/disks/${each.value.disk_id}" @@ -83,7 +83,7 @@ resource "google_tags_location_tag_binding" "boot_disks" { resource "google_tags_location_tag_binding" "disks" { for_each = ( - local.template_create ? {} : { for v in local.disk_tags : v.key => v } + local.is_template ? {} : { for v in local.disk_tags : v.key => v } ) parent = ( "${local.tag_parent_base}/zones/${local.zone}/disks/${each.value.disk_id}" @@ -94,7 +94,7 @@ resource "google_tags_location_tag_binding" "disks" { resource "google_tags_location_tag_binding" "disks_regional" { for_each = ( - local.template_create ? {} : { for v in local.region_disk_tags : v.key => v } + local.is_template ? {} : { for v in local.region_disk_tags : v.key => v } ) parent = ( "${local.tag_parent_base}/regions/${local.region}/disks/${each.value.disk_id}" @@ -106,7 +106,7 @@ resource "google_tags_location_tag_binding" "disks_regional" { # TODO: enable once the template id is available # resource "google_tags_location_tag_binding" "template" { -# for_each = local.template_create ? var.tag_bindings : {} +# for_each = local.is_template ? var.tag_bindings : {} # parent = ( # "${local.tag_parent_base}/regions/${local.region}/instanceTemplates/${google_compute_instance_template.default[0].instance_id}" # ) diff --git a/modules/compute-vm/template.tf b/modules/compute-vm/template.tf index 692ea2e5f..b19255700 100644 --- a/modules/compute-vm/template.tf +++ b/modules/compute-vm/template.tf @@ -15,26 +15,26 @@ */ locals { - template_create = var.create_template != null + is_template = var.create_template != null template_regional = try(var.create_template.regional, null) == true } resource "google_compute_instance_template" "default" { provider = google-beta - count = local.template_create && !local.template_regional ? 1 : 0 + count = local.is_template && !local.template_regional ? 1 : 0 project = local.project_id region = local.region name_prefix = "${var.name}-" description = var.description tags = var.tags - machine_type = var.instance_type + machine_type = var.machine_type min_cpu_platform = var.min_cpu_platform can_ip_forward = var.can_ip_forward metadata = var.metadata metadata_startup_script = var.metadata_startup_script labels = var.labels resource_manager_tags = var.tag_bindings_immutable - key_revocation_action_type = var.options.key_revocation_action_type + key_revocation_action_type = var.lifecycle_config.key_revocation_action_type resource_policies = ( var.resource_policies == null && var.instance_schedule == null ? null @@ -44,29 +44,29 @@ resource "google_compute_instance_template" "default" { ) ) dynamic "advanced_machine_features" { - for_each = local.advanced_mf != null ? [""] : [] + for_each = local.advanced_mf ? [""] : [] content { - enable_nested_virtualization = local.advanced_mf.enable_nested_virtualization - enable_uefi_networking = local.advanced_mf.enable_uefi_networking - performance_monitoring_unit = local.advanced_mf.performance_monitoring_unit - threads_per_core = local.advanced_mf.threads_per_core + enable_nested_virtualization = var.machine_features_config.enable_nested_virtualization + enable_uefi_networking = var.machine_features_config.enable_uefi_networking + performance_monitoring_unit = var.machine_features_config.performance_monitoring_unit + threads_per_core = var.machine_features_config.threads_per_core turbo_mode = ( - local.advanced_mf.enable_turbo_mode ? "ALL_CORE_MAX" : null + var.machine_features_config.enable_turbo_mode == true ? "ALL_CORE_MAX" : null ) - visible_core_count = local.advanced_mf.visible_core_count + visible_core_count = var.machine_features_config.visible_core_count } } disk { - architecture = var.boot_disk.initialize_params.architecture - auto_delete = var.boot_disk.auto_delete boot = true + architecture = var.boot_disk.architecture + auto_delete = var.boot_disk.auto_delete disk_size_gb = var.boot_disk.initialize_params.size disk_type = var.boot_disk.initialize_params.type - provisioned_iops = var.boot_disk.initialize_params.provisioned_iops - provisioned_throughput = var.boot_disk.initialize_params.provisioned_throughput + source_image = var.boot_disk.source.image + provisioned_iops = var.boot_disk.initialize_params.hyperdisk.provisioned_iops + provisioned_throughput = var.boot_disk.initialize_params.hyperdisk.provisioned_throughput resource_manager_tags = var.tag_bindings_immutable - source_image = var.boot_disk.initialize_params.image dynamic "disk_encryption_key" { for_each = var.encryption != null ? [""] : [] @@ -81,7 +81,7 @@ resource "google_compute_instance_template" "default" { } dynamic "confidential_instance_config" { - for_each = var.confidential_compute ? [""] : [] + for_each = var.confidential_compute != null ? [""] : [] content { enable_confidential_compute = true } @@ -95,36 +95,39 @@ resource "google_compute_instance_template" "default" { } } dynamic "disk" { - for_each = local.attached_disks - iterator = config + for_each = var.attached_disks content { - architecture = config.value.options.architecture - auto_delete = config.value.options.auto_delete - device_name = coalesce( - config.value.device_name, config.value.name, config.key + architecture = var.boot_disk.architecture + auto_delete = disk.value.mode == "READ_ONLY" ? null : disk.value.auto_delete + device_name = coalesce(disk.value.device_name, disk.value.name, disk.key) + disk_name = ( + disk.value.source.attach == null + ? coalesce(disk.value.name, disk.key) + : null ) + mode = disk.value.mode + resource_manager_tags = var.tag_bindings_immutable + source_image = disk.value.source.image + source = disk.value.source.attach + type = "PERSISTENT" # Cannot use `source` with any of the fields in # [disk_size_gb disk_name disk_type source_image labels] disk_type = ( - config.value.source_type != "attach" ? config.value.options.type : null + disk.value.source.attach == null + ? disk.value.initialize_params.type + : null ) disk_size_gb = ( - config.value.source_type != "attach" ? config.value.size : null + disk.value.source.attach == null + ? disk.value.initialize_params.size + : null ) - mode = config.value.options.mode - provisioned_iops = config.value.options.provisioned_iops - provisioned_throughput = config.value.options.provisioned_throughput - source_image = ( - config.value.source_type == "image" ? config.value.source : null + provisioned_iops = ( + disk.value.initialize_params.hyperdisk.provisioned_iops ) - source = ( - config.value.source_type == "attach" ? config.value.source : null + provisioned_throughput = ( + disk.value.initialize_params.hyperdisk.provisioned_throughput ) - disk_name = ( - config.value.source_type != "attach" ? config.value.name : null - ) - resource_manager_tags = var.tag_bindings_immutable - type = "PERSISTENT" dynamic "disk_encryption_key" { for_each = var.encryption != null ? [""] : [] content { @@ -153,8 +156,10 @@ resource "google_compute_instance_template" "default" { config.value.addresses.internal, null ) - nic_type = config.value.nic_type - stack_type = config.value.stack_type + nic_type = config.value.nic_type + stack_type = config.value.stack_type + queue_count = config.value.queue_count + internal_ipv6_prefix_length = config.value.internal_ipv6_prefix_length dynamic "access_config" { for_each = config.value.nat || config.value.network_tier != null ? [""] : [] content { @@ -184,22 +189,42 @@ resource "google_compute_instance_template" "default" { } } + dynamic "network_performance_config" { + for_each = var.network_performance_tier != null ? [""] : [] + content { + total_egress_bandwidth_tier = var.network_performance_tier + } + } + scheduling { - automatic_restart = !var.options.spot + automatic_restart = coalesce( + var.scheduling_config.automatic_restart, var.scheduling_config.provisioning_model != "SPOT" + ) instance_termination_action = local.termination_action on_host_maintenance = local.on_host_maintenance - preemptible = var.options.spot - provisioning_model = var.options.spot ? "SPOT" : "STANDARD" + preemptible = var.scheduling_config.provisioning_model == "SPOT" + provisioning_model = coalesce(var.scheduling_config.provisioning_model, "STANDARD") + min_node_cpus = var.scheduling_config.min_node_cpus + maintenance_interval = var.scheduling_config.maintenance_interval + dynamic "max_run_duration" { - for_each = var.options.max_run_duration == null ? [] : [""] + for_each = var.scheduling_config.max_run_duration == null ? [] : [""] content { - nanos = var.options.max_run_duration.nanos - seconds = var.options.max_run_duration.seconds + nanos = var.scheduling_config.max_run_duration.nanos + seconds = var.scheduling_config.max_run_duration.seconds + } + } + + dynamic "local_ssd_recovery_timeout" { + for_each = var.scheduling_config.local_ssd_recovery_timeout == null ? [] : [""] + content { + nanos = var.scheduling_config.local_ssd_recovery_timeout.nanos + seconds = var.scheduling_config.local_ssd_recovery_timeout.seconds } } dynamic "node_affinities" { - for_each = var.options.node_affinities + for_each = var.scheduling_config.node_affinities iterator = affinity content { key = affinity.key @@ -209,13 +234,18 @@ resource "google_compute_instance_template" "default" { } dynamic "graceful_shutdown" { - for_each = var.options.graceful_shutdown != null ? [""] : [] + for_each = var.lifecycle_config.graceful_shutdown != null ? [""] : [] content { - enabled = var.options.graceful_shutdown.enabled + enabled = var.lifecycle_config.graceful_shutdown.enabled dynamic "max_duration" { - for_each = var.options.graceful_shutdown.enabled == true && var.options.graceful_shutdown.max_duration_secs != null ? [""] : [] + for_each = ( + var.lifecycle_config.graceful_shutdown.enabled == true && + var.lifecycle_config.graceful_shutdown.max_duration_secs != null + ? [""] + : [] + ) content { - seconds = var.options.graceful_shutdown.max_duration_secs + seconds = var.lifecycle_config.graceful_shutdown.max_duration_secs nanos = 0 } } @@ -248,20 +278,20 @@ resource "google_compute_instance_template" "default" { resource "google_compute_region_instance_template" "default" { provider = google-beta - count = local.template_create && local.template_regional ? 1 : 0 + count = local.is_template && local.template_regional ? 1 : 0 project = local.project_id region = local.region name_prefix = "${var.name}-" description = var.description tags = var.tags - machine_type = var.instance_type + machine_type = var.machine_type min_cpu_platform = var.min_cpu_platform can_ip_forward = var.can_ip_forward metadata = var.metadata metadata_startup_script = var.metadata_startup_script labels = var.labels resource_manager_tags = var.tag_bindings_immutable - key_revocation_action_type = var.options.key_revocation_action_type + key_revocation_action_type = var.lifecycle_config.key_revocation_action_type resource_policies = ( var.resource_policies == null && var.instance_schedule == null ? null @@ -271,35 +301,36 @@ resource "google_compute_region_instance_template" "default" { ) ) dynamic "advanced_machine_features" { - for_each = local.advanced_mf != null ? [""] : [] + for_each = local.advanced_mf ? [""] : [] content { - enable_nested_virtualization = local.advanced_mf.enable_nested_virtualization - enable_uefi_networking = local.advanced_mf.enable_uefi_networking - performance_monitoring_unit = local.advanced_mf.performance_monitoring_unit - threads_per_core = local.advanced_mf.threads_per_core + enable_nested_virtualization = var.machine_features_config.enable_nested_virtualization + enable_uefi_networking = var.machine_features_config.enable_uefi_networking + performance_monitoring_unit = var.machine_features_config.performance_monitoring_unit + threads_per_core = var.machine_features_config.threads_per_core turbo_mode = ( - local.advanced_mf.enable_turbo_mode ? "ALL_CORE_MAX" : null + var.machine_features_config.enable_turbo_mode == true ? "ALL_CORE_MAX" : null ) - visible_core_count = local.advanced_mf.visible_core_count + visible_core_count = var.machine_features_config.visible_core_count } } disk { - architecture = var.boot_disk.initialize_params.architecture - auto_delete = var.boot_disk.auto_delete boot = true + architecture = var.boot_disk.architecture + auto_delete = var.boot_disk.auto_delete disk_size_gb = var.boot_disk.initialize_params.size disk_type = var.boot_disk.initialize_params.type - provisioned_iops = var.boot_disk.initialize_params.provisioned_iops - provisioned_throughput = var.boot_disk.initialize_params.provisioned_throughput + source_image = var.boot_disk.source.image + provisioned_iops = var.boot_disk.initialize_params.hyperdisk.provisioned_iops + provisioned_throughput = var.boot_disk.initialize_params.hyperdisk.provisioned_throughput resource_manager_tags = var.tag_bindings_immutable - source_image = var.boot_disk.initialize_params.image dynamic "disk_encryption_key" { for_each = var.encryption != null ? [""] : [] content { - kms_key_self_link = try( - local.ctx_kms_keys[var.encryption.kms_key_self_link], + kms_key_self_link = lookup( + local.ctx_kms_keys, + var.encryption.kms_key_self_link, var.encryption.kms_key_self_link ) } @@ -307,7 +338,7 @@ resource "google_compute_region_instance_template" "default" { } dynamic "confidential_instance_config" { - for_each = var.confidential_compute ? [""] : [] + for_each = var.confidential_compute != null ? [""] : [] content { enable_confidential_compute = true } @@ -320,42 +351,47 @@ resource "google_compute_region_instance_template" "default" { count = guest_accelerator.value.count } } + dynamic "disk" { - for_each = local.attached_disks - iterator = config + for_each = var.attached_disks content { - architecture = config.value.options.architecture - auto_delete = config.value.options.auto_delete - device_name = coalesce( - config.value.device_name, config.value.name, config.key + architecture = var.boot_disk.architecture + auto_delete = disk.value.mode == "READ_ONLY" ? null : disk.value.auto_delete + device_name = coalesce(disk.value.device_name, disk.value.name, disk.key) + disk_name = ( + disk.value.source.attach == null + ? coalesce(disk.value.name, disk.key) + : null ) + mode = disk.value.mode + resource_manager_tags = var.tag_bindings_immutable + source_image = disk.value.source.image + source = disk.value.source.attach + type = "PERSISTENT" # Cannot use `source` with any of the fields in # [disk_size_gb disk_name disk_type source_image labels] disk_type = ( - config.value.source_type != "attach" ? config.value.options.type : null + disk.value.source.attach == null + ? disk.value.initialize_params.type + : null ) disk_size_gb = ( - config.value.source_type != "attach" ? config.value.size : null + disk.value.source.attach == null + ? disk.value.initialize_params.size + : null ) - mode = config.value.options.mode - provisioned_iops = config.value.options.provisioned_iops - provisioned_throughput = config.value.options.provisioned_throughput - source_image = ( - config.value.source_type == "image" ? config.value.source : null + provisioned_iops = ( + disk.value.initialize_params.hyperdisk.provisioned_iops ) - source = ( - config.value.source_type == "attach" ? config.value.source : null + provisioned_throughput = ( + disk.value.initialize_params.hyperdisk.provisioned_throughput ) - disk_name = ( - config.value.source_type != "attach" ? config.value.name : null - ) - resource_manager_tags = var.tag_bindings_immutable - type = "PERSISTENT" dynamic "disk_encryption_key" { for_each = var.encryption != null ? [""] : [] content { - kms_key_self_link = try( - local.ctx_kms_keys[var.encryption.kms_key_self_link], + kms_key_self_link = lookup( + local.ctx_kms_keys, + var.encryption.kms_key_self_link, var.encryption.kms_key_self_link ) } @@ -378,8 +414,10 @@ resource "google_compute_region_instance_template" "default" { config.value.addresses.internal, null ) - nic_type = config.value.nic_type - stack_type = config.value.stack_type + nic_type = config.value.nic_type + stack_type = config.value.stack_type + queue_count = config.value.queue_count + internal_ipv6_prefix_length = config.value.internal_ipv6_prefix_length dynamic "access_config" { for_each = config.value.nat || config.value.network_tier != null ? [""] : [] content { @@ -403,21 +441,34 @@ resource "google_compute_region_instance_template" "default" { } scheduling { - automatic_restart = !var.options.spot + automatic_restart = coalesce( + var.scheduling_config.automatic_restart, var.scheduling_config.provisioning_model != "SPOT" + ) instance_termination_action = local.termination_action on_host_maintenance = local.on_host_maintenance - preemptible = var.options.spot - provisioning_model = var.options.spot ? "SPOT" : "STANDARD" + preemptible = var.scheduling_config.provisioning_model == "SPOT" + provisioning_model = coalesce(var.scheduling_config.provisioning_model, "STANDARD") + min_node_cpus = var.scheduling_config.min_node_cpus + maintenance_interval = var.scheduling_config.maintenance_interval + dynamic "max_run_duration" { - for_each = var.options.max_run_duration == null ? [] : [""] + for_each = var.scheduling_config.max_run_duration == null ? [] : [""] content { - nanos = var.options.max_run_duration.nanos - seconds = var.options.max_run_duration.seconds + nanos = var.scheduling_config.max_run_duration.nanos + seconds = var.scheduling_config.max_run_duration.seconds + } + } + + dynamic "local_ssd_recovery_timeout" { + for_each = var.scheduling_config.local_ssd_recovery_timeout == null ? [] : [""] + content { + nanos = var.scheduling_config.local_ssd_recovery_timeout.nanos + seconds = var.scheduling_config.local_ssd_recovery_timeout.seconds } } dynamic "node_affinities" { - for_each = var.options.node_affinities + for_each = var.scheduling_config.node_affinities iterator = affinity content { key = affinity.key @@ -427,13 +478,18 @@ resource "google_compute_region_instance_template" "default" { } dynamic "graceful_shutdown" { - for_each = var.options.graceful_shutdown != null ? [""] : [] + for_each = var.lifecycle_config.graceful_shutdown != null ? [""] : [] content { - enabled = var.options.graceful_shutdown.enabled + enabled = var.lifecycle_config.graceful_shutdown.enabled dynamic "max_duration" { - for_each = var.options.graceful_shutdown.enabled == true && var.options.graceful_shutdown.max_duration_secs != null ? [""] : [] + for_each = ( + var.lifecycle_config.graceful_shutdown.enabled == true && + var.lifecycle_config.graceful_shutdown.max_duration_secs != null + ? [""] + : [] + ) content { - seconds = var.options.graceful_shutdown.max_duration_secs + seconds = var.lifecycle_config.graceful_shutdown.max_duration_secs nanos = 0 } } diff --git a/modules/compute-vm/test.tfvars b/modules/compute-vm/test.tfvars deleted file mode 100644 index 5c60eab21..000000000 --- a/modules/compute-vm/test.tfvars +++ /dev/null @@ -1,9 +0,0 @@ -project_id = "tf-playground-svpc-gce" -zone = "europe-west8-b" -name = "test-sa" -instance_type = "e2-small" -network_interfaces = [{ - network = "https://www.googleapis.com/compute/v1/projects/ldj-dev-net-spoke-0/global/networks/dev-spoke-0" - subnetwork = "https://www.googleapis.com/compute/v1/projects/ldj-dev-net-spoke-0/regions/europe-west8/subnetworks/gce" -}] -# service_account = null diff --git a/modules/compute-vm/variables.tf b/modules/compute-vm/variables.tf index b701af052..feb416365 100644 --- a/modules/compute-vm/variables.tf +++ b/modules/compute-vm/variables.tf @@ -14,110 +14,88 @@ * limitations under the License. */ -variable "attached_disk_defaults" { - description = "Defaults for attached disks options." - type = object({ - auto_delete = optional(bool, false) - mode = string - replica_zone = string - type = string - }) - default = { - auto_delete = true - mode = "READ_WRITE" - replica_zone = null - type = "pd-balanced" - } - validation { - condition = var.attached_disk_defaults.mode == "READ_WRITE" || !var.attached_disk_defaults.auto_delete - error_message = "auto_delete can only be specified on READ_WRITE disks." - } -} - variable "attached_disks" { - description = "Additional disks, if options is null defaults will be used in its place. Source type is one of 'image' (zonal disks in vms and template), 'snapshot' (vm), 'existing', and null." - type = list(object({ - name = optional(string) - device_name = optional(string) - # TODO: size can be null when source_type is attach - size = string - snapshot_schedule = optional(list(string)) - source = optional(string) - source_type = optional(string) - options = optional( - object({ - architecture = optional(string) - auto_delete = optional(bool, false) # applies only to vm templates - mode = optional(string, "READ_WRITE") + description = "Additional disks. Source type is one of 'image' (zonal disks in vms and template), 'snapshot' (vm), 'existing', and null." + type = map(object({ + auto_delete = optional(bool, true) # applies only to vm templates + device_name = optional(string) + force_attach = optional(bool) + # auto_delete can only be specified for READ_WRITE, force null otherwise + mode = optional(string, "READ_WRITE") + name = optional(string) + initialize_params = optional(object({ + replica_zone = optional(string) + size = optional(number, 10) + type = optional(string, "pd-balanced") + hyperdisk = optional(object({ provisioned_iops = optional(number) provisioned_throughput = optional(number) # in MiB/s - replica_zone = optional(string) storage_pool = optional(string) - type = optional(string, "pd-balanced") - }), - { - auto_delete = true - mode = "READ_WRITE" - replica_zone = null - type = "pd-balanced" - } - ) + }), {}) + }), {}) + snapshot_schedule = optional(list(string)) + source = optional(object({ + attach = optional(string) + # disk = optional(string) + image = optional(string) # not supported yet for repd + # instant_snapshot = optional(string) + snapshot = optional(string) + }), {}) })) - default = [] + nullable = false + default = {} validation { - condition = length([ - for d in var.attached_disks : d if( - d.source_type == null - || - contains(["image", "snapshot", "attach"], coalesce(d.source_type, "1")) - ) - ]) == length(var.attached_disks) - error_message = "Source type must be one of 'image', 'snapshot', 'attach', null." - } - validation { - condition = length([ - for d in var.attached_disks : d if d.options == null || - d.options.mode == "READ_WRITE" || !d.options.auto_delete - ]) == length(var.attached_disks) - error_message = "auto_delete can only be specified on READ_WRITE disks." - } - validation { - condition = alltrue([for d in var.attached_disks : - (d.options.architecture == null || contains(["ARM64", "X86_64"], d.options.architecture)) + condition = alltrue([ + for k, v in var.attached_disks : + contains(["READ_WRITE", "READ_ONLY"], v.mode) ]) - error_message = "Architecture can be null, 'X86_64' or 'ARM64'." + error_message = "Allowed values for 'mode' are 'READ_WRITE', 'READ_ONLY'." } } variable "boot_disk" { - description = "Boot disk properties. Initialize params are ignored when source is set." + description = "Boot disk properties." type = object({ - name = optional(string) + architecture = optional(string) auto_delete = optional(bool, true) + force_attach = optional(bool) snapshot_schedule = optional(list(string)) - source = optional(string) initialize_params = optional(object({ - architecture = optional(string) - image = optional(string, "projects/debian-cloud/global/images/family/debian-11") - provisioned_iops = optional(number) - provisioned_throughput = optional(number) # in MiB/s - size = optional(number, 10) - storage_pool = optional(string) - type = optional(string, "pd-balanced") + size = optional(number, 10) + type = optional(string, "pd-balanced") + hyperdisk = optional(object({ + provisioned_iops = optional(number) + provisioned_throughput = optional(number) # in MiB/s + storage_pool = optional(string) + }), {}) }), {}) - use_independent_disk = optional(bool, false) + source = optional(object({ + attach = optional(string) + disk = optional(string) + image = optional(string) + # instant_snapshot = optional(string) + snapshot = optional(string) + }), { image = "debian-cloud/debian-13" }) + use_independent_disk = optional(object({ + name = optional(string) + })) }) - default = { - initialize_params = {} - } + default = {} nullable = false validation { - condition = var.boot_disk.source != null || var.boot_disk.initialize_params != null - error_message = "You can only have one of boot disk source or initialize params." + condition = ( + var.boot_disk.initialize_params == null || + ( + var.boot_disk.source.attach == null && + var.boot_disk.source.snapshot == null && + var.boot_disk.source.disk == null + ) + ) + error_message = "Initialize params cannot be used when attaching an existing disk or creating from a snapshot." } validation { condition = ( - var.boot_disk.use_independent_disk != true + var.boot_disk.use_independent_disk == null || var.boot_disk.initialize_params != null ) @@ -125,8 +103,8 @@ variable "boot_disk" { } validation { condition = ( - var.boot_disk.initialize_params.architecture == null || - contains(["ARM64", "X86_64"], var.boot_disk.initialize_params.architecture) + var.boot_disk.architecture == null || + contains(["ARM64", "X86_64"], var.boot_disk.architecture) ) error_message = "Architecture can be null, 'X86_64' or 'ARM64'." } @@ -139,9 +117,13 @@ variable "can_ip_forward" { } variable "confidential_compute" { - description = "Enable Confidential Compute for these instances." - type = bool - default = false + description = "Confidential Compute configuration. Set to 'SEV' or 'SEV_SNP' to enable." + type = string + default = null + validation { + condition = var.confidential_compute == null || contains(["SEV", "SEV_SNP"], var.confidential_compute) + error_message = "Allowed values are 'SEV' or 'SEV_SNP'." + } } variable "context" { @@ -184,6 +166,7 @@ variable "enable_display" { variable "encryption" { description = "Encryption options. Only one of kms_key_self_link and disk_encryption_key_raw may be set. If needed, you can specify to encrypt or not the boot disk." + # TODO: Add validation to enforce exclusivity of kms_key_self_link and disk_encryption_key_raw type = object({ encrypt_boot = optional(bool, false) disk_encryption_key_raw = optional(string) @@ -228,9 +211,10 @@ variable "gpu" { } variable "group" { - description = "Define this variable to create an instance group for instances. Disabled for template use." + description = "Instance group configuration. Set 'named_ports' to create a new unmanaged instance group, or provide an existing group self_link/id in 'membership' to join one." type = object({ - named_ports = map(number) + membership = optional(string) # ID of an existing unmanaged group to join + named_ports = optional(map(number), {}) }) default = null } @@ -248,7 +232,7 @@ variable "iam" { } variable "instance_schedule" { - description = "Assign or create and assign an instance schedule policy. Either resource policy id or create_config must be specified if not null. Set active to null to dtach a policy from vm before destroying." + description = "Assign or create and assign an instance schedule policy. Set active to null to detach a policy from vm before destroying." type = object({ active = optional(bool, true) description = optional(string) @@ -271,12 +255,6 @@ variable "instance_schedule" { } } -variable "instance_type" { - description = "Instance type." - type = string - default = "e2-micro" -} - variable "kms_autokeys" { description = "KMS Autokey key handles. If location is not specified it will be inferred from the zone. Key handle names will be added to the kms_keys context with an `autokeys/` prefix." type = map(object({ @@ -301,6 +279,58 @@ variable "labels" { default = {} } +variable "lifecycle_config" { + description = "Instance lifecycle and operational configurations." + type = object({ + allow_stopping_for_update = optional(bool, true) + deletion_protection = optional(bool, false) + key_revocation_action_type = optional(string, "NONE") + graceful_shutdown = optional(object({ + enabled = optional(bool, false) + max_duration_secs = optional(number) + })) + }) + default = {} + validation { + condition = ( + var.lifecycle_config.key_revocation_action_type == null || contains( + ["NONE", "STOP"], var.lifecycle_config.key_revocation_action_type + ) + ) + error_message = "Allowed values for key_revocation_action_type are 'NONE' or 'STOP'." + } +} + +variable "machine_features_config" { + description = "Machine-level configuration." + type = object({ + enable_nested_virtualization = optional(bool) + enable_turbo_mode = optional(bool) + enable_uefi_networking = optional(bool) + performance_monitoring_unit = optional(string) + threads_per_core = optional(number) + visible_core_count = optional(number) + }) + nullable = false + default = {} + validation { + condition = ( + try(var.machine_features_config.performance_monitoring_unit, null) == null || + contains( + ["ARCHITECTURAL", "ENHANCED", "STANDARD"], + coalesce(try(var.machine_features_config.performance_monitoring_unit, null), "-") + ) + ) + error_message = "Allowed values for performance_monitoring_unit are ARCHITECTURAL', 'ENHANCED', 'STANDARD' and null." + } +} + +variable "machine_type" { + description = "Machine type." + type = string + default = "e2-micro" +} + variable "metadata" { description = "Instance metadata." type = map(string) @@ -335,17 +365,19 @@ variable "network_attached_interfaces" { variable "network_interfaces" { description = "Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed." type = list(object({ - network = string - subnetwork = string - alias_ips = optional(map(string), {}) - nat = optional(bool, false) - nic_type = optional(string) - stack_type = optional(string) + network = string + subnetwork = string + alias_ips = optional(map(string), {}) + nat = optional(bool, false) + network_tier = optional(string) + nic_type = optional(string) + stack_type = optional(string) + queue_count = optional(number) # NEW + internal_ipv6_prefix_length = optional(number) # NEW addresses = optional(object({ internal = optional(string) external = optional(string) }), null) - network_tier = optional(string) })) validation { condition = alltrue([for v in var.network_interfaces : contains(["STANDARD", "PREMIUM"], coalesce(v.network_tier, "PREMIUM"))]) @@ -353,6 +385,16 @@ variable "network_interfaces" { } } +variable "network_performance_tier" { + description = "Network performance total egress bandwidth tier." + type = string + default = null + validation { + condition = var.network_performance_tier == null || contains(["DEFAULT", "TIER_1"], coalesce(var.network_performance_tier, "-")) + error_message = "Allowed values are 'DEFAULT' or 'TIER_1'." + } +} + variable "network_tag_bindings" { description = "Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance only for networking purposes, and modifiable without impacting the main resource lifecycle." type = map(string) @@ -360,73 +402,6 @@ variable "network_tag_bindings" { default = {} } -variable "options" { - description = "Instance options." - type = object({ - advanced_machine_features = optional(object({ - enable_nested_virtualization = optional(bool) - enable_turbo_mode = optional(bool) - enable_uefi_networking = optional(bool) - performance_monitoring_unit = optional(string) - threads_per_core = optional(number) - visible_core_count = optional(number) - })) - allow_stopping_for_update = optional(bool, true) - deletion_protection = optional(bool, false) - key_revocation_action_type = optional(string) - graceful_shutdown = optional(object({ - enabled = optional(bool, false) - max_duration_secs = optional(number) - })) - max_run_duration = optional(object({ - nanos = optional(number) - seconds = number - })) - node_affinities = optional(map(object({ - values = list(string) - in = optional(bool, true) - })), {}) - spot = optional(bool, false) - termination_action = optional(string) - }) - default = { - allow_stopping_for_update = true - deletion_protection = false - spot = false - termination_action = null - key_revocation_action_type = "NONE" - } - validation { - condition = ( - var.options.termination_action == null - || - contains(["STOP", "DELETE"], coalesce(var.options.termination_action, "1")) - ) - error_message = "Allowed values for options.termination_action are 'STOP', 'DELETE' and null." - } - validation { - condition = ( - try(var.options.advanced_machine_features.performance_monitoring_unit, null) == null - || - contains(["ARCHITECTURAL", "ENHANCED", "STANDARD"], coalesce( - try( - var.options.advanced_machine_features.performance_monitoring_unit, null - ), "-" - ) - ) - ) - error_message = "Allowed values for options.advanced_machine_features.performance_monitoring_unit are ARCHITECTURAL', 'ENHANCED', 'STANDARD' and null." - } - validation { - condition = ( - var.options.key_revocation_action_type == null - || - contains(["NONE", "STOP"], var.options.key_revocation_action_type) - ) - error_message = "Allowed values for options.key_revocation_action_type are 'NONE' or 'STOP'." - } -} - variable "project_id" { description = "Project id." type = string @@ -445,6 +420,41 @@ variable "resource_policies" { default = null } +variable "scheduling_config" { + description = "Scheduling configuration for the instance." + type = object({ + automatic_restart = optional(bool) # Defaults to !spot + maintenance_interval = optional(string) # NEW + min_node_cpus = optional(number) # NEW + on_host_maintenance = optional(string) # Defaults to MIGRATE or TERMINATE based on GPU/Spot + provisioning_model = optional(string) # "SPOT" or "STANDARD" + termination_action = optional(string) + local_ssd_recovery_timeout = optional(object({ # NEW + nanos = optional(number) + seconds = number + })) + max_run_duration = optional(object({ + nanos = optional(number) + seconds = number + })) + node_affinities = optional(map(object({ + values = list(string) + in = optional(bool, true) + })), {}) + }) + nullable = false + default = {} + validation { + condition = ( + var.scheduling_config.termination_action == null || contains( + ["STOP", "DELETE"], coalesce(var.scheduling_config.termination_action, "1" + ) + ) + ) + error_message = "Allowed values for termination_action are 'STOP', 'DELETE' and null." + } +} + variable "scratch_disks" { description = "Scratch disks configuration." type = object({ @@ -457,6 +467,7 @@ variable "scratch_disks" { } } + variable "service_account" { description = "Service account email and scopes. If email is null, the default Compute service account will be used unless auto_create is true, in which case a service account will be created. Set the variable to null to avoid attaching a service account." type = object({ @@ -467,6 +478,7 @@ variable "service_account" { default = {} } + variable "shielded_config" { description = "Shielded VM configuration of the instances." type = object({ diff --git a/modules/net-lb-app-ext-regional/README.md b/modules/net-lb-app-ext-regional/README.md index 9a52e620f..9f2a6b9d1 100644 --- a/modules/net-lb-app-ext-regional/README.md +++ b/modules/net-lb-app-ext-regional/README.md @@ -339,18 +339,20 @@ This example shows how to use the module with a manage instance group as backend ```hcl module "win-template" { - source = "./fabric/modules/compute-vm" - project_id = var.project_id - zone = "${var.region}-a" - name = "win-template" - instance_type = "n2d-standard-2" + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "${var.region}-a" + name = "win-template" + machine_type = "n2d-standard-2" create_template = { regional = false } boot_disk = { - initialize_params = { + source = { image = "projects/windows-cloud/global/images/windows-server-2019-dc-v20221214" - size = 70 + } + initialize_params = { + size = 70 } } network_interfaces = [{ @@ -522,7 +524,7 @@ module "ralb-0" { endpoints = { e-0 = { ip_address = "192.0.2.5" - port = 443 + port = 443 } } } diff --git a/modules/net-lb-app-ext/README.md b/modules/net-lb-app-ext/README.md index 9f7332211..ba28d6096 100644 --- a/modules/net-lb-app-ext/README.md +++ b/modules/net-lb-app-ext/README.md @@ -311,12 +311,14 @@ module "win-template" { project_id = var.project_id zone = "${var.region}-a" name = "win-template" - instance_type = "n2d-standard-2" + machine_type = "n2d-standard-2" create_template = {} boot_disk = { initialize_params = { + size = 70 + } + source = { image = "projects/windows-cloud/global/images/windows-server-2019-dc-v20221214" - size = 70 } } network_interfaces = [{ diff --git a/modules/net-lb-app-int-cross-region/recipe-cross-reg-int-app-lb-vm-dns/README.md b/modules/net-lb-app-int-cross-region/recipe-cross-reg-int-app-lb-vm-dns/README.md index e6cbb3489..075c91b30 100644 --- a/modules/net-lb-app-int-cross-region/recipe-cross-reg-int-app-lb-vm-dns/README.md +++ b/modules/net-lb-app-int-cross-region/recipe-cross-reg-int-app-lb-vm-dns/README.md @@ -107,7 +107,7 @@ vpc_config = { instances_config = { # both attributes are optional machine_type = "e2-small" - zones = ["b", "c"] + zones = ["b", "c"] } } # tftest modules=5 resources=15 @@ -131,8 +131,8 @@ vpc_config = { "projects/my-project/global/networks/test", "projects/my-other-project/global/networks/test" ] - domain = "foo.example." - hostname = "lb-test" + domain = "foo.example." + hostname = "lb-test" } } # tftest modules=5 resources=15 diff --git a/modules/net-lb-app-int-cross-region/recipe-cross-reg-int-app-lb-vm-dns/instances.tf b/modules/net-lb-app-int-cross-region/recipe-cross-reg-int-app-lb-vm-dns/instances.tf index 32bf10095..e0993fff3 100644 --- a/modules/net-lb-app-int-cross-region/recipe-cross-reg-int-app-lb-vm-dns/instances.tf +++ b/modules/net-lb-app-int-cross-region/recipe-cross-reg-int-app-lb-vm-dns/instances.tf @@ -46,14 +46,14 @@ module "instance-sa" { } module "instances" { - source = "../../compute-vm" - for_each = local.instances - project_id = var.project_id - zone = each.value.zone - name = each.key - instance_type = var.instances_config.machine_type + source = "../../compute-vm" + for_each = local.instances + project_id = var.project_id + zone = each.value.zone + name = each.key + machine_type = var.instances_config.machine_type boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } diff --git a/modules/net-lb-ext/README.md b/modules/net-lb-ext/README.md index 998cf65a2..c8dcfccc5 100644 --- a/modules/net-lb-ext/README.md +++ b/modules/net-lb-ext/README.md @@ -159,9 +159,11 @@ module "instance-group" { }] boot_disk = { initialize_params = { + type = "pd-ssd" + size = 10 + } + source = { image = "projects/cos-cloud/global/images/family/cos-stable" - type = "pd-ssd" - size = 10 } } tags = ["http-server", "ssh"] @@ -196,6 +198,7 @@ module "nlb" { ``` ## Deploying changes to load balancer configurations + For deploying changes to load balancer configuration please refer to [net-lb-app-ext README.md](../net-lb-app-ext/README.md#deploying-changes-to-load-balancer-configurations) ## Variables diff --git a/modules/net-lb-int/README.md b/modules/net-lb-int/README.md index aacd6e57a..c4345fd76 100644 --- a/modules/net-lb-int/README.md +++ b/modules/net-lb-int/README.md @@ -334,10 +334,12 @@ module "instance-group" { addresses = null }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" - type = "pd-ssd" - size = 10 + } + initialize_params = { + type = "pd-ssd" + size = 10 } } tags = ["http-server", "ssh"] diff --git a/modules/net-lb-int/recipe-ilb-next-hop/gateways.tf b/modules/net-lb-int/recipe-ilb-next-hop/gateways.tf index 2e99956e2..be89bacf6 100644 --- a/modules/net-lb-int/recipe-ilb-next-hop/gateways.tf +++ b/modules/net-lb-int/recipe-ilb-next-hop/gateways.tf @@ -15,17 +15,19 @@ */ module "gw" { - source = "../../../modules/compute-vm" - for_each = local.zones - project_id = module.project.project_id - zone = each.value - name = "${var.prefix}-gw-${each.key}" - instance_type = "f1-micro" + source = "../../../modules/compute-vm" + for_each = local.zones + project_id = module.project.project_id + zone = each.value + name = "${var.prefix}-gw-${each.key}" + machine_type = "f1-micro" boot_disk = { + source = { + image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2004-lts" + } initialize_params = { - image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2004-lts", - type = "pd-ssd", - size = 10 + type = "pd-ssd", + size = 10 } } network_interfaces = [ diff --git a/modules/net-lb-int/recipe-ilb-next-hop/vms.tf b/modules/net-lb-int/recipe-ilb-next-hop/vms.tf index 259eacb09..c4ca452fd 100644 --- a/modules/net-lb-int/recipe-ilb-next-hop/vms.tf +++ b/modules/net-lb-int/recipe-ilb-next-hop/vms.tf @@ -23,12 +23,12 @@ END } module "vm-left" { - source = "../../../modules/compute-vm" - for_each = local.zones - project_id = module.project.project_id - zone = each.value - name = "${var.prefix}-vm-left-${each.key}" - instance_type = "f1-micro" + source = "../../../modules/compute-vm" + for_each = local.zones + project_id = module.project.project_id + zone = each.value + name = "${var.prefix}-vm-left-${each.key}" + machine_type = "f1-micro" network_interfaces = [ { network = module.vpc-left.self_link @@ -45,12 +45,12 @@ module "vm-left" { } module "vm-right" { - source = "../../../modules/compute-vm" - for_each = local.zones - project_id = module.project.project_id - zone = each.value - name = "${var.prefix}-vm-right-${each.key}" - instance_type = "f1-micro" + source = "../../../modules/compute-vm" + for_each = local.zones + project_id = module.project.project_id + zone = each.value + name = "${var.prefix}-vm-right-${each.key}" + machine_type = "f1-micro" network_interfaces = [ { network = module.vpc-right.self_link diff --git a/modules/project-factory/README.md b/modules/project-factory/README.md index a5a9526d5..efadb58d5 100644 --- a/modules/project-factory/README.md +++ b/modules/project-factory/README.md @@ -375,7 +375,7 @@ context = { "test/prod" = "folders/1234567890" } iam_principals = { - mysa = "serviceAccount:test@test-project.iam.gserviceaccount.com" + mysa = "serviceAccount:test@test-project.iam.gserviceaccount.com" } project_ids = { vpc-host = "test-vpc-host" diff --git a/modules/project/README.md b/modules/project/README.md index 0384e2d3a..b18e20d3a 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -934,7 +934,7 @@ For composer v3: ``` module "project" { - source = "./fabric/modules/project" + source = "./fabric/modules/project" billing_account = var.billing_account_id name = "project" prefix = var.prefix @@ -952,7 +952,7 @@ For composer v2: ``` module "project" { - source = "./fabric/modules/project" + source = "./fabric/modules/project" billing_account = var.billing_account_id name = "project" prefix = var.prefix @@ -1569,7 +1569,7 @@ Note that entitlements defined via `pam_entitlements` take precedence over those ```hcl module "project" { - source = "./fabric/modules/project" + source = "./fabric/modules/project" billing_account = var.billing_account_id name = "project" parent = var.folder_id diff --git a/modules/spanner-instance/README.md b/modules/spanner-instance/README.md index ea0f0a147..e568f9ce1 100644 --- a/modules/spanner-instance/README.md +++ b/modules/spanner-instance/README.md @@ -93,7 +93,7 @@ module "spanner_instance" { source = "./fabric/modules/spanner-instance" project_id = var.project_id instance = { - name = "my-instance" + name = "my-instance" } instance_create = false databases = { diff --git a/modules/vpc-sc/README.md b/modules/vpc-sc/README.md index c5a1b75a3..921467a1f 100644 --- a/modules/vpc-sc/README.md +++ b/modules/vpc-sc/README.md @@ -210,7 +210,7 @@ The caller must have `cloudasset.assets.searchAllResources` permission to perfor module "vpc-sc" { source = "./fabric/modules/vpc-sc" project_id_search_scope = var.org_id - access_policy = "12345678" + access_policy = "12345678" ingress_policies = { i1 = { from = { diff --git a/tests/examples/test_plan.py b/tests/examples/test_plan.py index 8ce2bd247..fc96d08c1 100644 --- a/tests/examples/test_plan.py +++ b/tests/examples/test_plan.py @@ -105,7 +105,7 @@ def _test_terraform_example(plan_validator, example): result = subprocess.run( f'{binary} fmt -check -diff -no-color main.tf'.split(), cwd=tmp_path, stdout=subprocess.PIPE, encoding='utf-8') - assert result.returncode == 0, f'terraform example code in README.md not formatted correctly\n{result.stdout}' + assert result.returncode == 0, f'[{example.name}] terraform example code in README.md not formatted correctly\n{result.stdout}' def _test_yaml_example(example): diff --git a/tests/fixtures/compute-mig.tf b/tests/fixtures/compute-mig.tf index 1c1279eb6..a18d17bd2 100644 --- a/tests/fixtures/compute-mig.tf +++ b/tests/fixtures/compute-mig.tf @@ -25,7 +25,7 @@ module "_instance-template" { addresses = null }] boot_disk = { - initialize_params = { + source = { image = "projects/cos-cloud/global/images/family/cos-stable" } } diff --git a/tests/fixtures/compute-vm-group-bc.tf b/tests/fixtures/compute-vm-group-bc.tf index 59183f939..107ec7149 100644 --- a/tests/fixtures/compute-vm-group-bc.tf +++ b/tests/fixtures/compute-vm-group-bc.tf @@ -23,7 +23,7 @@ module "compute-vm-group-b" { subnetwork = var.subnet.self_link }] boot_disk = { - initialize_params = { + source = { image = "cos-cloud/cos-stable" } } @@ -40,7 +40,7 @@ module "compute-vm-group-c" { subnetwork = var.subnet.self_link }] boot_disk = { - initialize_params = { + source = { image = "cos-cloud/cos-stable" } } diff --git a/tests/modules/compute_vm/context-template-regional.tfvars b/tests/modules/compute_vm/context-template-regional.tfvars index fc11cce4f..21c4d7d90 100644 --- a/tests/modules/compute_vm/context-template-regional.tfvars +++ b/tests/modules/compute_vm/context-template-regional.tfvars @@ -1,8 +1,6 @@ -attached_disks = [{ - name = "data-0" - size = 10 - } -] +attached_disks = { + data-0 = {} +} context = { addresses = { ext-test-0 = "35.10.10.10" diff --git a/tests/modules/compute_vm/context-template-regional.yaml b/tests/modules/compute_vm/context-template-regional.yaml index 0204429ba..21b742922 100644 --- a/tests/modules/compute_vm/context-template-regional.yaml +++ b/tests/modules/compute_vm/context-template-regional.yaml @@ -39,7 +39,7 @@ values: resource_manager_tags: null resource_policies: null source: null - source_image: projects/debian-cloud/global/images/family/debian-11 + source_image: debian-cloud/debian-13 source_image_encryption_key: [] source_snapshot: null source_snapshot_encryption_key: [] diff --git a/tests/modules/compute_vm/context-template.tfvars b/tests/modules/compute_vm/context-template.tfvars index 3ee07484a..507bf8746 100644 --- a/tests/modules/compute_vm/context-template.tfvars +++ b/tests/modules/compute_vm/context-template.tfvars @@ -1,8 +1,6 @@ -attached_disks = [{ - name = "data-0" - size = 10 - } -] +attached_disks = { + data-0 = {} +} context = { addresses = { ext-test-0 = "35.10.10.10" diff --git a/tests/modules/compute_vm/context-template.yaml b/tests/modules/compute_vm/context-template.yaml index e3b1d5553..caa19c60b 100644 --- a/tests/modules/compute_vm/context-template.yaml +++ b/tests/modules/compute_vm/context-template.yaml @@ -39,7 +39,7 @@ values: resource_manager_tags: null resource_policies: null source: null - source_image: projects/debian-cloud/global/images/family/debian-11 + source_image: debian-cloud/debian-13 source_image_encryption_key: [] source_snapshot: null source_snapshot_encryption_key: [] diff --git a/tests/modules/compute_vm/context-vm.tfvars b/tests/modules/compute_vm/context-vm.tfvars index 91295c4d8..9c8e4e8ab 100644 --- a/tests/modules/compute_vm/context-vm.tfvars +++ b/tests/modules/compute_vm/context-vm.tfvars @@ -1,8 +1,6 @@ -attached_disks = [{ - name = "data-0" - size = 10 - } -] +attached_disks = { + data-0 = {} +} context = { addresses = { ext-test-0 = "35.10.10.10" diff --git a/tests/modules/compute_vm/context-vm.yaml b/tests/modules/compute_vm/context-vm.yaml index 1656963d8..b9e319ecd 100644 --- a/tests/modules/compute_vm/context-vm.yaml +++ b/tests/modules/compute_vm/context-vm.yaml @@ -69,7 +69,7 @@ values: force_attach: null initialize_params: - enable_confidential_compute: null - image: projects/debian-cloud/global/images/family/debian-11 + image: debian-cloud/debian-13 resource_manager_tags: null size: 10 source_image_encryption_key: [] diff --git a/tests/modules/compute_vm/examples/cmek.yaml b/tests/modules/compute_vm/examples/cmek.yaml index 27b8238b5..5c24f1763 100644 --- a/tests/modules/compute_vm/examples/cmek.yaml +++ b/tests/modules/compute_vm/examples/cmek.yaml @@ -39,7 +39,7 @@ values: - auto_delete: true disk_encryption_key_raw: null initialize_params: - - image: projects/debian-cloud/global/images/family/debian-11 + - image: debian-cloud/debian-13 resource_manager_tags: size: 10 type: pd-balanced diff --git a/tests/modules/compute_vm/examples/defaults.yaml b/tests/modules/compute_vm/examples/defaults.yaml index 48bdb4767..65a08a130 100644 --- a/tests/modules/compute_vm/examples/defaults.yaml +++ b/tests/modules/compute_vm/examples/defaults.yaml @@ -21,7 +21,7 @@ values: - auto_delete: true disk_encryption_key_raw: null initialize_params: - - image: projects/debian-cloud/global/images/family/debian-11 + - image: debian-cloud/debian-13 resource_manager_tags: null size: 10 type: pd-balanced diff --git a/tests/modules/compute_vm/examples/disk-hyperdisk-cust-performance.yaml b/tests/modules/compute_vm/examples/disk-hyperdisk-cust-performance.yaml index 61070ef54..764812d6a 100644 --- a/tests/modules/compute_vm/examples/disk-hyperdisk-cust-performance.yaml +++ b/tests/modules/compute_vm/examples/disk-hyperdisk-cust-performance.yaml @@ -99,7 +99,7 @@ values: disk_encryption_key_rsa: null disk_encryption_service_account: null force_attach: null - mode: READ_WRITE + mode: READ_ONLY source: test-data2 boot_disk: - auto_delete: true diff --git a/tests/modules/compute_vm/examples/disk-options.yaml b/tests/modules/compute_vm/examples/disk-options.yaml index 95e301f5c..0f5f6969b 100644 --- a/tests/modules/compute_vm/examples/disk-options.yaml +++ b/tests/modules/compute_vm/examples/disk-options.yaml @@ -33,7 +33,7 @@ values: - auto_delete: true disk_encryption_key_raw: null initialize_params: - - image: projects/debian-cloud/global/images/family/debian-11 + - image: debian-cloud/debian-13 resource_manager_tags: size: 10 type: pd-balanced diff --git a/tests/modules/compute_vm/examples/disks-example-template.yaml b/tests/modules/compute_vm/examples/disks-example-template.yaml index 75a2e8a70..50ff12488 100644 --- a/tests/modules/compute_vm/examples/disks-example-template.yaml +++ b/tests/modules/compute_vm/examples/disks-example-template.yaml @@ -29,7 +29,7 @@ values: resource_manager_tags: null resource_policies: null source: null - source_image: projects/debian-cloud/global/images/family/debian-11 + source_image: debian-cloud/debian-13 source_image_encryption_key: [] source_snapshot: null source_snapshot_encryption_key: [] diff --git a/tests/modules/compute_vm/examples/independent-boot-disk.yaml b/tests/modules/compute_vm/examples/independent-boot-disk.yaml index 50aa91072..6b1af3504 100644 --- a/tests/modules/compute_vm/examples/independent-boot-disk.yaml +++ b/tests/modules/compute_vm/examples/independent-boot-disk.yaml @@ -16,7 +16,7 @@ values: module.simple-vm-example.google_compute_disk.boot[0]: description: null disk_encryption_key: [] - image: projects/debian-cloud/global/images/family/debian-11 + image: debian-cloud/debian-13 labels: disk_name: boot disk_type: pd-balanced diff --git a/tests/modules/compute_vm/examples/sa-custom.yaml b/tests/modules/compute_vm/examples/sa-custom.yaml index 540f91527..7661dee68 100644 --- a/tests/modules/compute_vm/examples/sa-custom.yaml +++ b/tests/modules/compute_vm/examples/sa-custom.yaml @@ -21,7 +21,7 @@ values: - auto_delete: true disk_encryption_key_raw: null initialize_params: - - image: projects/debian-cloud/global/images/family/debian-11 + - image: debian-cloud/debian-13 resource_manager_tags: null size: 10 type: pd-balanced diff --git a/tests/modules/compute_vm/examples/sa-default.yaml b/tests/modules/compute_vm/examples/sa-default.yaml index d8d9562a6..0335014a7 100644 --- a/tests/modules/compute_vm/examples/sa-default.yaml +++ b/tests/modules/compute_vm/examples/sa-default.yaml @@ -21,7 +21,7 @@ values: - auto_delete: true disk_encryption_key_raw: null initialize_params: - - image: projects/debian-cloud/global/images/family/debian-11 + - image: debian-cloud/debian-13 resource_manager_tags: null size: 10 type: pd-balanced diff --git a/tests/modules/compute_vm/examples/sa-managed.yaml b/tests/modules/compute_vm/examples/sa-managed.yaml index 1d3025869..fb481ec60 100644 --- a/tests/modules/compute_vm/examples/sa-managed.yaml +++ b/tests/modules/compute_vm/examples/sa-managed.yaml @@ -21,7 +21,7 @@ values: - auto_delete: true disk_encryption_key_raw: null initialize_params: - - image: projects/debian-cloud/global/images/family/debian-11 + - image: debian-cloud/debian-13 resource_manager_tags: null size: 10 type: pd-balanced diff --git a/tests/modules/compute_vm/examples/sa-none.yaml b/tests/modules/compute_vm/examples/sa-none.yaml index 075afee3a..56c39e04c 100644 --- a/tests/modules/compute_vm/examples/sa-none.yaml +++ b/tests/modules/compute_vm/examples/sa-none.yaml @@ -21,7 +21,7 @@ values: - auto_delete: true disk_encryption_key_raw: null initialize_params: - - image: projects/debian-cloud/global/images/family/debian-11 + - image: debian-cloud/debian-13 resource_manager_tags: null size: 10 type: pd-balanced diff --git a/tests/modules/compute_vm/examples/snapshot-schedule-create.yaml b/tests/modules/compute_vm/examples/snapshot-schedule-create.yaml index f710f087b..a5a864739 100644 --- a/tests/modules/compute_vm/examples/snapshot-schedule-create.yaml +++ b/tests/modules/compute_vm/examples/snapshot-schedule-create.yaml @@ -114,7 +114,6 @@ values: disk_name: disk-1 disk_type: pd-balanced goog-terraform-provisioned: 'true' - interface: null labels: disk_name: disk-1 disk_type: pd-balanced diff --git a/tests/modules/compute_vm/examples/sole-tenancy.yaml b/tests/modules/compute_vm/examples/sole-tenancy.yaml index 887d51eb1..eeb4c9cc0 100644 --- a/tests/modules/compute_vm/examples/sole-tenancy.yaml +++ b/tests/modules/compute_vm/examples/sole-tenancy.yaml @@ -22,7 +22,7 @@ values: disk_encryption_key_raw: null initialize_params: - enable_confidential_compute: null - image: projects/debian-cloud/global/images/family/debian-11 + image: debian-cloud/debian-13 resource_manager_tags: null size: 10 type: pd-balanced diff --git a/tests/modules/compute_vm/examples/tag-bindings.yaml b/tests/modules/compute_vm/examples/tag-bindings.yaml index 4f5b848b7..d397301bc 100644 --- a/tests/modules/compute_vm/examples/tag-bindings.yaml +++ b/tests/modules/compute_vm/examples/tag-bindings.yaml @@ -21,7 +21,7 @@ values: - auto_delete: true disk_encryption_key_raw: null initialize_params: - - image: projects/debian-cloud/global/images/family/debian-11 + - image: debian-cloud/debian-13 resource_manager_tags: tagKeys/1234567890: tagValues/7890123456 size: 10 diff --git a/tests/modules/net_vpn_dynamic/examples/vpn-single-tunnel-custom-ciphers.yaml b/tests/modules/net_vpn_dynamic/examples/vpn-single-tunnel-custom-ciphers.yaml index 20ace6358..0cba5b68c 100644 --- a/tests/modules/net_vpn_dynamic/examples/vpn-single-tunnel-custom-ciphers.yaml +++ b/tests/modules/net_vpn_dynamic/examples/vpn-single-tunnel-custom-ciphers.yaml @@ -25,7 +25,7 @@ values: force_attach: null initialize_params: - enable_confidential_compute: null - image: projects/debian-cloud/global/images/family/debian-11 + image: debian-cloud/debian-13 resource_manager_tags: null size: 10 source_image_encryption_key: [] diff --git a/tests/modules/net_vpn_dynamic/examples/vpn-single-tunnel.yaml b/tests/modules/net_vpn_dynamic/examples/vpn-single-tunnel.yaml index a0a5f26aa..cbdef8dac 100644 --- a/tests/modules/net_vpn_dynamic/examples/vpn-single-tunnel.yaml +++ b/tests/modules/net_vpn_dynamic/examples/vpn-single-tunnel.yaml @@ -25,7 +25,7 @@ values: force_attach: null initialize_params: - enable_confidential_compute: null - image: projects/debian-cloud/global/images/family/debian-11 + image: debian-cloud/debian-13 resource_manager_tags: null size: 10 source_image_encryption_key: [] diff --git a/tools/format_tftest.py b/tools/format_tftest.py new file mode 100755 index 000000000..abd741889 --- /dev/null +++ b/tools/format_tftest.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 + +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Format Terraform code blocks with tftest directives in README files.''' + +import os +import subprocess +import sys +from pathlib import Path + +import click +import marko + +# Add fabric root to sys.path to import from tests +FABRIC_ROOT = Path(__file__).resolve().parents[1] +sys.path.append(str(FABRIC_ROOT)) + +try: + from tests.examples.utils import get_tftest_directive +except ImportError: + print('Error: Could not import tests.examples.utils', file=sys.stderr) + sys.exit(1) + + +def find_readme_files(paths): + '''Find all README.md files in the given paths.''' + files_to_check = [] + for path in paths: + if os.path.isfile(path) and os.path.basename(path) == 'README.md': + files_to_check.append(path) + elif os.path.isdir(path): + for root, _, files in os.walk(path): + if 'README.md' in files: + files_to_check.append(os.path.join(root, 'README.md')) + return files_to_check + + +def find_examples(content): + '''Find all Terraform examples with tftest directives in the markdown content.''' + doc = marko.parse(content) + examples = [] + last_header = None + index = 0 + for child in doc.children: + if isinstance(child, marko.block.Heading): + last_header = child.children[0].children + index = 0 + continue + if not isinstance(child, marko.block.FencedCode): + continue + index += 1 + if child.lang not in ('hcl', 'tfvars'): + continue + code = child.children[0].children + directive = get_tftest_directive(code) + # identical logic to pytest tests filtering + if directive is None: + continue + if 'skip' in directive.args: + continue + example_id = f'{last_header}:{index}' + examples.append((example_id, child.lang, code)) + return examples + + +def format_example(code): + '''Format a single Terraform example using terraform fmt.''' + try: + proc = subprocess.run(['terraform', 'fmt', '-'], input=code, text=True, + capture_output=True, check=True) + return proc.stdout, None + except subprocess.CalledProcessError as e: + return code, e.stderr + + +def replace_examples(content, formatted_examples): + '''Replace the original examples with the formatted ones in the markdown content.''' + new_content = content + for lang, original_code, formatted_code in formatted_examples: + if original_code != formatted_code: + old_block = f'```{lang}\n{original_code}```' + new_block = f'```{lang}\n{formatted_code}```' + new_content = new_content.replace(old_block, new_block) + return new_content + + +@click.command() +@click.argument('paths', type=click.Path(exists=True), nargs=-1) +@click.option('--check', is_flag=True, + help='Check if files need formatting without changing them.') +def main(paths, check): + '''Format Terraform code blocks with tftest directives in README files. + + PATHS can be specific README.md files or directories to search recursively. + If no paths are provided, searches the current directory recursively. + ''' + if not paths: + paths = ('.',) + files_to_check = find_readme_files(paths) + has_changes = False + for file_path in files_to_check: + try: + with open(file_path, 'r') as f: + content = f.read() + examples = find_examples(content) + formatted_examples = [] + file_changed = False + file_output = [] + for example_id, lang, code in examples: + formatted_code, error = format_example(code) + if error: + file_output.append(f' ❌ {example_id}') + formatted_examples.append((lang, code, code)) + else: + if formatted_code != code: + file_output.append(f' ✅ {example_id}') + file_changed = True + has_changes = True + formatted_examples.append((lang, code, formatted_code)) + if file_output: + print(f'{file_path}:') + for line in file_output: + print(line) + if file_changed and not check: + new_content = replace_examples(content, formatted_examples) + with open(file_path, 'w') as f: + f.write(new_content) + except Exception as e: + print(f'Error processing {file_path}: {e}', file=sys.stderr) + if check and has_changes: + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/tools/tfdoc.py b/tools/tfdoc.py index 5fc60f252..dc3e06ed5 100755 --- a/tools/tfdoc.py +++ b/tools/tfdoc.py @@ -177,6 +177,25 @@ def _parse(body, enum=VAR_ENUM, re=VAR_RE, template=VAR_TEMPLATE): item[context].append(data) +def _extract_title(element): + 'Extract and format text from marko elements.' + if isinstance(element, str): + return element + if hasattr(element, 'children'): + if isinstance(element.children, str): + if element.get_type() == 'CodeSpan': + return f'`{element.children}`' + return element.children + elif isinstance(element.children, list): + inner = ''.join(_extract_title(c) for c in element.children) + if element.get_type() == 'StrongEmphasis': + return f'**{inner}**' + elif element.get_type() == 'Emphasis': + return f'*{inner}*' + return inner + return '' + + def create_toc(readme, skip=['contents']): 'Create a Markdown table of contents a for README.' doc = marko.parse(readme) @@ -184,7 +203,7 @@ def create_toc(readme, skip=['contents']): headings = [x for x in doc.children if x.get_type() == 'Heading'] skip = skip or [] for h in headings[1:]: - title = h.children[0].children + title = _extract_title(h) slug = title.lower().strip() slug = re.sub(r'[^\w\s-]', '', slug) slug = re.sub(r'[-\s]+', '-', slug)