From a30c186f1fcc21e9cd2e6016485b6d797af176e8 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Tue, 1 Nov 2022 09:38:59 +0100 Subject: [PATCH] Refactor compute-mig module for Terraform 1.3 (#931) * wip: autoscaler * wip: fix autoscaler * wip: health check * wip: untested * wip: tests and examples missing * wip: examples * wip: consumers * blueprint tests * fast --- blueprints/networking/filtering-proxy/main.tf | 46 +- blueprints/networking/glb-and-armor/main.tf | 56 +- fast/stages/02-networking-nva/nva.tf | 36 +- modules/compute-mig/README.md | 248 ++++----- modules/compute-mig/autoscaler.tf | 229 ++++++++ modules/compute-mig/health-check.tf | 128 +++++ modules/compute-mig/main.tf | 519 +++++------------- modules/compute-mig/outputs.tf | 9 +- modules/compute-mig/stateful-config.tf | 91 +++ modules/compute-mig/variables.tf | 231 +++++--- tests/modules/compute_mig/fixture/main.tf | 31 +- .../modules/compute_mig/fixture/variables.tf | 117 ++-- tests/modules/compute_mig/test_plan.py | 94 ++-- 13 files changed, 1019 insertions(+), 816 deletions(-) create mode 100644 modules/compute-mig/autoscaler.tf create mode 100644 modules/compute-mig/health-check.tf create mode 100644 modules/compute-mig/stateful-config.tf diff --git a/blueprints/networking/filtering-proxy/main.tf b/blueprints/networking/filtering-proxy/main.tf index 97d6efec..ca998bf9 100644 --- a/blueprints/networking/filtering-proxy/main.tf +++ b/blueprints/networking/filtering-proxy/main.tf @@ -165,33 +165,31 @@ module "squid-vm" { } module "squid-mig" { - count = var.mig ? 1 : 0 - source = "../../../modules/compute-mig" - project_id = module.project-host.project_id - location = "${var.region}-b" - name = "squid-mig" - target_size = 1 - autoscaler_config = { - max_replicas = 10 - min_replicas = 1 - cooldown_period = 30 - cpu_utilization_target = 0.65 - load_balancing_utilization_target = null - metric = null + count = var.mig ? 1 : 0 + source = "../../../modules/compute-mig" + project_id = module.project-host.project_id + location = "${var.region}-b" + name = "squid-mig" + instance_template = module.squid-vm.template.self_link + target_size = 1 + auto_healing_policies = { + initial_delay_sec = 60 } - default_version = { - instance_template = module.squid-vm.template.self_link - name = "default" + autoscaler_config = { + max_replicas = 10 + min_replicas = 1 + cooldown_period = 30 + scaling_signals = { + cpu_utilization = { + target = 0.65 + } + } } health_check_config = { - type = "tcp" - check = { port = 3128 } - config = {} - logging = true - } - auto_healing_policies = { - health_check = module.squid-mig.0.health_check.self_link - initial_delay_sec = 60 + enable_logging = true + tcp = { + port = 3128 + } } } diff --git a/blueprints/networking/glb-and-armor/main.tf b/blueprints/networking/glb-and-armor/main.tf index 6e43bc15..83622609 100644 --- a/blueprints/networking/glb-and-armor/main.tf +++ b/blueprints/networking/glb-and-armor/main.tf @@ -153,22 +153,20 @@ module "vm_siege" { } module "mig_ew1" { - source = "../../../modules/compute-mig" - project_id = module.project.project_id - location = "europe-west1" - name = "${local.prefix}europe-west1-mig" - regional = true - default_version = { - instance_template = module.instance_template_ew1.template.self_link - name = "default" - } + source = "../../../modules/compute-mig" + project_id = module.project.project_id + location = "europe-west1" + name = "${local.prefix}europe-west1-mig" + instance_template = module.instance_template_ew1.template.self_link autoscaler_config = { - max_replicas = 5 - min_replicas = 1 - cooldown_period = 45 - cpu_utilization_target = 0.8 - load_balancing_utilization_target = null - metric = null + max_replicas = 5 + min_replicas = 1 + cooldown_period = 45 + scaling_signals = { + cpu_utilization = { + target = 0.65 + } + } } named_ports = { http = 80 @@ -179,22 +177,20 @@ module "mig_ew1" { } module "mig_ue1" { - source = "../../../modules/compute-mig" - project_id = module.project.project_id - location = "us-east1" - name = "${local.prefix}us-east1-mig" - regional = true - default_version = { - instance_template = module.instance_template_ue1.template.self_link - name = "default" - } + source = "../../../modules/compute-mig" + project_id = module.project.project_id + location = "us-east1" + name = "${local.prefix}us-east1-mig" + instance_template = module.instance_template_ue1.template.self_link autoscaler_config = { - max_replicas = 5 - min_replicas = 1 - cooldown_period = 45 - cpu_utilization_target = 0.8 - load_balancing_utilization_target = null - metric = null + max_replicas = 5 + min_replicas = 1 + cooldown_period = 45 + scaling_signals = { + cpu_utilization = { + target = 0.65 + } + } } named_ports = { http = 80 diff --git a/fast/stages/02-networking-nva/nva.tf b/fast/stages/02-networking-nva/nva.tf index 4e70d02f..d0afbd72 100644 --- a/fast/stages/02-networking-nva/nva.tf +++ b/fast/stages/02-networking-nva/nva.tf @@ -15,7 +15,7 @@ */ locals { - # routing_config should be aligned to the NVA network interfaces - i.e. + # routing_config should be aligned to the NVA network interfaces - i.e. # local.routing_config[0] sets up the first interface, and so on. routing_config = [ { @@ -94,27 +94,21 @@ module "nva-template" { } module "nva-mig" { - for_each = local.nva_locality - source = "../../../modules/compute-mig" - project_id = module.landing-project.project_id - regional = true - location = each.value.region - name = "nva-cos-${each.value.trigram}-${each.value.zone}" - target_size = 1 - # FIXME: cycle - # auto_healing_policies = { - # health_check = module.nva-mig[each.key].health_check.self_link - # initial_delay_sec = 30 - # } - health_check_config = { - type = "tcp" - check = { port = 22 } - config = {} - logging = true + for_each = local.nva_locality + source = "../../../modules/compute-mig" + project_id = module.landing-project.project_id + location = each.value.region + name = "nva-cos-${each.value.trigram}-${each.value.zone}" + instance_template = module.nva-template[each.key].template.self_link + target_size = 1 + auto_healing_policies = { + initial_delay_sec = 30 } - default_version = { - instance_template = module.nva-template[each.key].template.self_link - name = "default" + health_check_config = { + enable_logging = true + tcp = { + port = 22 + } } } diff --git a/modules/compute-mig/README.md b/modules/compute-mig/README.md index 1d421527..4e5b5a45 100644 --- a/modules/compute-mig/README.md +++ b/modules/compute-mig/README.md @@ -2,7 +2,7 @@ This module allows creating a managed instance group supporting one or more application versions via instance templates. Optionally, a health check and an autoscaler can be created, and the managed instance group can be configured to be stateful. -This module can be coupled with the [`compute-vm`](../compute-vm) module which can manage instance templates, and the [`net-ilb`](../net-ilb) module to assign the MIG to a backend wired to an Internal Load Balancer. The first use case is shown in the examples below. +This module can be coupled with the [`compute-vm`](../compute-vm) module which can manage instance templates, and the [`net-ilb`](../net-ilb) module to assign the MIG to a backend wired to an Internal Load Balancer. The first use case is shown in the examples below. Stateful disks can be created directly, as shown in the last example below. @@ -39,15 +39,12 @@ module "nginx-template" { } module "nginx-mig" { - source = "./fabric/modules/compute-mig" - project_id = "my-project" - location = "europe-west1-b" - name = "mig-test" - target_size = 2 - default_version = { - instance_template = module.nginx-template.template.self_link - name = "default" - } + source = "./fabric/modules/compute-mig" + project_id = "my-project" + location = "europe-west1-b" + name = "mig-test" + target_size = 2 + instance_template = module.nginx-template.template.self_link } # tftest modules=2 resources=2 ``` @@ -85,20 +82,18 @@ module "nginx-template" { } module "nginx-mig" { - source = "./fabric/modules/compute-mig" - project_id = "my-project" - location = "europe-west1-b" - name = "mig-test" - target_size = 3 - default_version = { - instance_template = module.nginx-template.template.self_link - name = "default" - } + source = "./fabric/modules/compute-mig" + project_id = "my-project" + location = "europe-west1-b" + name = "mig-test" + target_size = 3 + instance_template = module.nginx-template.template.self_link versions = { canary = { instance_template = module.nginx-template.template.self_link - target_type = "fixed" - target_size = 1 + target_size = { + fixed = 1 + } } } } @@ -138,24 +133,20 @@ module "nginx-template" { } module "nginx-mig" { - source = "./fabric/modules/compute-mig" - project_id = "my-project" - location = "europe-west1-b" - name = "mig-test" - target_size = 3 - default_version = { - instance_template = module.nginx-template.template.self_link - name = "default" - } + source = "./fabric/modules/compute-mig" + project_id = "my-project" + location = "europe-west1-b" + name = "mig-test" + target_size = 3 + instance_template = module.nginx-template.template.self_link auto_healing_policies = { - health_check = module.nginx-mig.health_check.self_link initial_delay_sec = 30 } health_check_config = { - type = "http" - check = { port = 80 } - config = {} - logging = true + enable_logging = true + http = { + port = 80 + } } } # tftest modules=2 resources=3 @@ -194,22 +185,21 @@ module "nginx-template" { } module "nginx-mig" { - source = "./fabric/modules/compute-mig" - project_id = "my-project" - location = "europe-west1-b" - name = "mig-test" - target_size = 3 - default_version = { - instance_template = module.nginx-template.template.self_link - name = "default" - } + source = "./fabric/modules/compute-mig" + project_id = "my-project" + location = "europe-west1-b" + name = "mig-test" + target_size = 3 + instance_template = module.nginx-template.template.self_link autoscaler_config = { - max_replicas = 3 - min_replicas = 1 - cooldown_period = 30 - cpu_utilization_target = 0.65 - load_balancing_utilization_target = null - metric = null + max_replicas = 3 + min_replicas = 1 + cooldown_period = 30 + scaling_signals = { + cpu_utilization = { + target = 0.65 + } + } } } # tftest modules=2 resources=3 @@ -246,23 +236,19 @@ module "nginx-template" { } module "nginx-mig" { - source = "./fabric/modules/compute-mig" - project_id = "my-project" - location = "europe-west1-b" - name = "mig-test" - target_size = 3 - default_version = { - instance_template = module.nginx-template.template.self_link - name = "default" - } + source = "./fabric/modules/compute-mig" + project_id = "my-project" + location = "europe-west1-b" + name = "mig-test" + target_size = 3 + instance_template = module.nginx-template.template.self_link update_policy = { - type = "PROACTIVE" minimal_action = "REPLACE" + type = "PROACTIVE" min_ready_sec = 30 - max_surge_type = "fixed" - max_surge = 1 - max_unavailable_type = null - max_unavailable = null + max_surge = { + fixed = 1 + } } } # tftest modules=2 resources=2 @@ -270,7 +256,7 @@ module "nginx-mig" { ### Stateful MIGs - MIG Config -Stateful MIGs have some limitations documented [here](https://cloud.google.com/compute/docs/instance-groups/configuring-stateful-migs#limitations). Enforcement of these requirements is the responsibility of users of this module. +Stateful MIGs have some limitations documented [here](https://cloud.google.com/compute/docs/instance-groups/configuring-stateful-migs#limitations). Enforcement of these requirements is the responsibility of users of this module. You can configure a disk defined in the instance template to be stateful for all instances in the MIG by configuring in the MIG's stateful policy, using the `stateful_disk_mig` variable. Alternatively, you can also configure stateful persistent disks individually per instance of the MIG by setting the `stateful_disk_instance` variable. A discussion on these scenarios can be found in the [docs](https://cloud.google.com/compute/docs/instance-groups/configuring-stateful-disks-in-migs). @@ -278,7 +264,6 @@ An example using only the configuration at the MIG level can be seen below. Note that when referencing the stateful disk, you use `device_name` and not `disk_name`. - ```hcl module "cos-nginx" { source = "./fabric/modules/cloud-config-container/nginx" @@ -319,40 +304,33 @@ module "nginx-template" { } module "nginx-mig" { - source = "./fabric/modules/compute-mig" - project_id = "my-project" - location = "europe-west1-b" - name = "mig-test" - target_size = 3 - default_version = { - instance_template = module.nginx-template.template.self_link - name = "default" - } + source = "./fabric/modules/compute-mig" + project_id = "my-project" + location = "europe-west1-b" + name = "mig-test" + target_size = 3 + instance_template = module.nginx-template.template.self_link autoscaler_config = { - max_replicas = 3 - min_replicas = 1 - cooldown_period = 30 - cpu_utilization_target = 0.65 - load_balancing_utilization_target = null - metric = null - } - stateful_config = { - per_instance_config = {}, - mig_config = { - stateful_disks = { - repd-1 = { - delete_rule = "NEVER" - } + max_replicas = 3 + min_replicas = 1 + cooldown_period = 30 + scaling_signals = { + cpu_utilization = { + target = 0.65 } } } + stateful_disks = { + repd-1 = null + } } # tftest modules=2 resources=3 ``` ### Stateful MIGs - Instance Config -Here is an example defining the stateful config at the instance level. + +Here is an example defining the stateful config at the instance level. Note that you will need to know the instance name in order to use this configuration. @@ -396,46 +374,36 @@ module "nginx-template" { } module "nginx-mig" { - source = "./fabric/modules/compute-mig" - project_id = "my-project" - location = "europe-west1-b" - name = "mig-test" - target_size = 3 - default_version = { - instance_template = module.nginx-template.template.self_link - name = "default" - } + source = "./fabric/modules/compute-mig" + project_id = "my-project" + location = "europe-west1-b" + name = "mig-test" + target_size = 3 + instance_template = module.nginx-template.template.self_link autoscaler_config = { - max_replicas = 3 - min_replicas = 1 - cooldown_period = 30 - cpu_utilization_target = 0.65 - load_balancing_utilization_target = null - metric = null + max_replicas = 3 + min_replicas = 1 + cooldown_period = 30 + scaling_signals = { + cpu_utilization = { + target = 0.65 + } + } } stateful_config = { - per_instance_config = { - # note that this needs to be the name of an existing instance within the Managed Instance Group - instance-1 = { - stateful_disks = { + # name needs to match a MIG instance name + instance-1 = { + minimal_action = "NONE", + most_disruptive_allowed_action = "REPLACE" + preserved_state = { + disks = { persistent-disk-1 = { source = "test-disk", - mode = "READ_ONLY", - delete_rule= "NEVER", - }, - }, + } + } metadata = { foo = "bar" - }, - update_config = { - minimal_action = "NONE", - most_disruptive_allowed_action = "REPLACE", - remove_instance_state_on_destroy = false, - }, - }, - }, - mig_config = { - stateful_disks = { + } } } } @@ -449,21 +417,25 @@ module "nginx-mig" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [default_version](variables.tf#L45) | Default application version template. Additional versions can be specified via the `versions` variable. | object({…}) | ✓ | | -| [location](variables.tf#L64) | Compute zone, or region if `regional` is set to true. | string | ✓ | | -| [name](variables.tf#L68) | Managed group name. | string | ✓ | | -| [project_id](variables.tf#L79) | Project id. | string | ✓ | | -| [auto_healing_policies](variables.tf#L17) | Auto-healing policies for this group. | object({…}) | | null | -| [autoscaler_config](variables.tf#L26) | Optional autoscaler configuration. Only one of 'cpu_utilization_target' 'load_balancing_utilization_target' or 'metric' can be not null. | object({…}) | | null | -| [health_check_config](variables.tf#L53) | Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage. | object({…}) | | null | -| [named_ports](variables.tf#L73) | Named ports. | map(number) | | null | -| [regional](variables.tf#L84) | Use regional instance group. When set, `location` should be set to the region. | bool | | false | -| [stateful_config](variables.tf#L90) | Stateful configuration can be done by individual instances or for all instances in the MIG. They key in per_instance_config is the name of the specific instance. The key of the stateful_disks is the 'device_name' field of the resource. Please note that device_name is defined at the OS mount level, unlike the disk name. | object({…}) | | null | -| [target_pools](variables.tf#L121) | Optional list of URLs for target pools to which new instances in the group are added. | list(string) | | [] | -| [target_size](variables.tf#L127) | Group target size, leave null when using an autoscaler. | number | | null | -| [update_policy](variables.tf#L133) | Update policy. Type can be 'OPPORTUNISTIC' or 'PROACTIVE', action 'REPLACE' or 'restart', surge type 'fixed' or 'percent'. | object({…}) | | null | -| [versions](variables.tf#L148) | Additional application versions, target_type is either 'fixed' or 'percent'. | map(object({…})) | | null | -| [wait_for_instances](variables.tf#L158) | Wait for all instances to be created/updated before returning. | bool | | null | +| [instance_template](variables.tf#L150) | Instance template for the default version. | string | ✓ | | +| [location](variables.tf#L155) | Compute zone or region. | string | ✓ | | +| [name](variables.tf#L160) | Managed group name. | string | ✓ | | +| [project_id](variables.tf#L171) | Project id. | string | ✓ | | +| [all_instances_config](variables.tf#L17) | Metadata and labels set to all instances in the group. | object({…}) | | null | +| [auto_healing_policies](variables.tf#L26) | Auto-healing policies for this group. | object({…}) | | null | +| [autoscaler_config](variables.tf#L35) | Optional autoscaler configuration. | object({…}) | | null | +| [default_version_name](variables.tf#L83) | Name used for the default version. | string | | "default" | +| [description](variables.tf#L89) | Optional description used for all resources managed by this module. | string | | "Terraform managed." | +| [distribution_policy](variables.tf#L95) | DIstribution policy for regional MIG. | object({…}) | | null | +| [health_check_config](variables.tf#L104) | Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage. | object({…}) | | null | +| [named_ports](variables.tf#L165) | Named ports. | map(number) | | null | +| [stateful_config](variables.tf#L183) | Stateful configuration for individual instances. | map(object({…})) | | {} | +| [stateful_disks](variables.tf#L176) | Stateful disk configuration applied at the MIG level to all instances, in device name => on permanent instance delete rule as boolean. | map(bool) | | {} | +| [target_pools](variables.tf#L202) | Optional list of URLs for target pools to which new instances in the group are added. | list(string) | | [] | +| [target_size](variables.tf#L208) | Group target size, leave null when using an autoscaler. | number | | null | +| [update_policy](variables.tf#L214) | Update policy. Minimal action and type are required. | object({…}) | | null | +| [versions](variables.tf#L235) | Additional application versions, target_size is optional. | map(object({…})) | | {} | +| [wait_for_instances](variables.tf#L248) | Wait for all instances to be created/updated before returning. | object({…}) | | null | ## Outputs diff --git a/modules/compute-mig/autoscaler.tf b/modules/compute-mig/autoscaler.tf new file mode 100644 index 00000000..b8bd0acc --- /dev/null +++ b/modules/compute-mig/autoscaler.tf @@ -0,0 +1,229 @@ +/** + * Copyright 2022 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. + */ + +# tfdoc:file:description Autoscaler resource. + +locals { + as_enabled = true + as_scaling = try(var.autoscaler_config.scaling_control, null) + as_signals = try(var.autoscaler_config.scaling_signals, null) +} + +resource "google_compute_autoscaler" "default" { + provider = google-beta + count = local.is_regional || var.autoscaler_config == null ? 0 : 1 + project = var.project_id + name = var.name + zone = var.location + description = var.description + target = google_compute_instance_group_manager.default.0.id + + autoscaling_policy { + max_replicas = var.autoscaler_config.max_replicas + min_replicas = var.autoscaler_config.min_replicas + cooldown_period = var.autoscaler_config.cooldown_period + + dynamic "scale_down_control" { + for_each = local.as_scaling.down == null ? [] : [""] + content { + time_window_sec = local.as_scaling.down.time_window_sec + dynamic "max_scaled_down_replicas" { + for_each = ( + local.as_scaling.down.max_replicas_fixed == null && + local.as_scaling.down.max_replicas_percent == null + ? [] + : [""] + ) + content { + fixed = local.as_scaling.down.max_replicas_fixed + percent = local.as_scaling.down.max_replicas_percent + } + } + } + } + + dynamic "scale_in_control" { + for_each = local.as_scaling.in == null ? [] : [""] + content { + time_window_sec = local.as_scaling.in.time_window_sec + dynamic "max_scaled_in_replicas" { + for_each = ( + local.as_scaling.in.max_replicas_fixed == null && + local.as_scaling.in.max_replicas_percent == null + ? [] + : [""] + ) + content { + fixed = local.as_scaling.in.max_replicas_fixed + percent = local.as_scaling.in.max_replicas_percent + } + } + } + } + + dynamic "cpu_utilization" { + for_each = local.as_signals.cpu_utilization == null ? [] : [""] + content { + target = local.as_signals.cpu_utilization.target + predictive_method = ( + local.as_signals.cpu_utilization.optimize_availability == true + ? "OPTIMIZE_AVAILABILITY" + : null + ) + } + } + + dynamic "load_balancing_utilization" { + for_each = local.as_signals.load_balancing_utilization == null ? [] : [""] + content { + target = local.as_signals.load_balancing_utilization.target + } + } + + dynamic "metric" { + for_each = toset( + local.as_signals.metrics == null ? [] : local.as_signals.metrics + ) + content { + name = metric.value.name + type = metric.value.type + target = metric.value.target_value + single_instance_assignment = metric.value.single_instance_assignment + filter = metric.value.time_series_filter + } + } + + dynamic "scaling_schedules" { + for_each = toset( + local.as_signals.schedules == null ? [] : local.as_signals.schedules + ) + iterator = schedule + content { + duration_sec = schedule.value.duration_sec + min_required_replicas = schedule.value.min_required_replicas + name = schedule.value.name + schedule = schedule.value.cron_schedule + description = schedule.value.description + disabled = schedule.value.disabled + time_zone = schedule.value.timezone + } + } + + } +} + +resource "google_compute_region_autoscaler" "default" { + provider = google-beta + count = local.is_regional && var.autoscaler_config != null ? 1 : 0 + project = var.project_id + name = var.name + region = var.location + description = var.description + target = google_compute_region_instance_group_manager.default.0.id + + autoscaling_policy { + max_replicas = var.autoscaler_config.max_replicas + min_replicas = var.autoscaler_config.min_replicas + cooldown_period = var.autoscaler_config.cooldown_period + + dynamic "scale_down_control" { + for_each = local.as_scaling.down == null ? [] : [""] + content { + time_window_sec = local.as_scaling.down.time_window_sec + dynamic "max_scaled_down_replicas" { + for_each = ( + local.as_scaling.down.max_replicas_fixed == null && + local.as_scaling.down.max_replicas_percent == null + ? [] + : [""] + ) + content { + fixed = local.as_scaling.down.max_replicas_fixed + percent = local.as_scaling.down.max_replicas_percent + } + } + } + } + + dynamic "scale_in_control" { + for_each = local.as_scaling.in == null ? [] : [""] + content { + time_window_sec = local.as_scaling.in.time_window_sec + dynamic "max_scaled_in_replicas" { + for_each = ( + local.as_scaling.in.max_replicas_fixed == null && + local.as_scaling.in.max_replicas_percent == null + ? [] + : [""] + ) + content { + fixed = local.as_scaling.in.max_replicas_fixed + percent = local.as_scaling.in.max_replicas_percent + } + } + } + } + + dynamic "cpu_utilization" { + for_each = local.as_signals.cpu_utilization == null ? [] : [""] + content { + target = local.as_signals.cpu_utilization.target + predictive_method = ( + local.as_signals.cpu_utilization.optimize_availability == true + ? "OPTIMIZE_AVAILABILITY" + : null + ) + } + } + + dynamic "load_balancing_utilization" { + for_each = local.as_signals.load_balancing_utilization == null ? [] : [""] + content { + target = local.as_signals.load_balancing_utilization.target + } + } + + dynamic "metric" { + for_each = toset( + local.as_signals.metrics == null ? [] : local.as_signals.metrics + ) + content { + name = metric.value.name + type = metric.value.type + target = metric.value.target_value + single_instance_assignment = metric.value.single_instance_assignment + filter = metric.value.time_series_filter + } + } + + dynamic "scaling_schedules" { + for_each = toset( + local.as_signals.schedules == null ? [] : local.as_signals.schedules + ) + iterator = schedule + content { + duration_sec = schedule.value.duration_sec + min_required_replicas = schedule.value.min_required_replicas + name = schedule.value.name + schedule = schedule.cron_schedule + description = schedule.value.description + disabled = schedule.value.disabled + time_zone = schedule.value.timezone + } + } + + } +} diff --git a/modules/compute-mig/health-check.tf b/modules/compute-mig/health-check.tf new file mode 100644 index 00000000..9a5d8d1f --- /dev/null +++ b/modules/compute-mig/health-check.tf @@ -0,0 +1,128 @@ +/** + * Copyright 2022 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. + */ + +# tfdoc:file:description Health check resource. + +locals { + hc = var.health_check_config + hc_grpc = try(local.hc.grpc, null) != null + hc_http = ( + try(local.hc.http, null) != null && + lower(try(local.hc.http.use_protocol, "")) == "http" + ) + hc_http2 = ( + try(local.hc.http, null) != null && + lower(try(local.hc.http.use_protocol, "")) == "http2" + ) + hc_https = ( + try(local.hc.http, null) != null && + lower(try(local.hc.http.use_protocol, "")) == "https" + ) + hc_ssl = try(local.hc.tcp.use_ssl, null) == true + hc_tcp = try(local.hc.tcp, null) != null && !local.hc_ssl +} + +resource "google_compute_health_check" "autohealing" { + provider = google-beta + count = local.hc != null ? 1 : 0 + project = var.project_id + name = var.name + description = local.hc.description + check_interval_sec = local.hc.check_interval_sec + healthy_threshold = local.hc.healthy_threshold + timeout_sec = local.hc.timeout_sec + unhealthy_threshold = local.hc.unhealthy_threshold + + dynamic "grpc_health_check" { + for_each = local.hc_grpc ? [""] : [] + content { + port = local.hc.grpc.port + port_name = local.hc.grpc.port_name + port_specification = local.hc.grpc.port_specification + grpc_service_name = local.hc.grpc.service_name + } + } + + dynamic "http_health_check" { + for_each = local.hc_http ? [""] : [] + content { + host = local.hc.http.host + port = local.hc.http.port + port_name = local.hc.http.port_name + port_specification = local.hc.http.port_specification + proxy_header = local.hc.http.proxy_header + request_path = local.hc.http.request_path + response = local.hc.http.response + } + } + + dynamic "http2_health_check" { + for_each = local.hc_http2 ? [""] : [] + content { + host = local.hc.http.host + port = local.hc.http.port + port_name = local.hc.http.port_name + port_specification = local.hc.http.port_specification + proxy_header = local.hc.http.proxy_header + request_path = local.hc.http.request_path + response = local.hc.http.response + } + } + + dynamic "https_health_check" { + for_each = local.hc_https ? [""] : [] + content { + host = local.hc.http.host + port = local.hc.http.port + port_name = local.hc.http.port_name + port_specification = local.hc.http.port_specification + proxy_header = local.hc.http.proxy_header + request_path = local.hc.http.request_path + response = local.hc.http.response + } + } + + dynamic "ssl_health_check" { + for_each = local.hc_ssl ? [""] : [] + content { + port = local.hc.tcp.port + port_name = local.hc.tcp.port_name + port_specification = local.hc.tcp.port_specification + proxy_header = local.hc.tcp.proxy_header + request = local.hc.tcp.request + response = local.hc.tcp.response + } + } + + dynamic "tcp_health_check" { + for_each = local.hc_tcp ? [""] : [] + content { + port = local.hc.tcp.port + port_name = local.hc.tcp.port_name + port_specification = local.hc.tcp.port_specification + proxy_header = local.hc.tcp.proxy_header + request = local.hc.tcp.request + response = local.hc.tcp.response + } + } + + dynamic "log_config" { + for_each = try(local.hc.enable_logging, null) == true ? [""] : [] + content { + enable = true + } + } +} diff --git a/modules/compute-mig/main.tf b/modules/compute-mig/main.tf index b5a71fac..92738995 100644 --- a/modules/compute-mig/main.tf +++ b/modules/compute-mig/main.tf @@ -14,105 +14,50 @@ * limitations under the License. */ -resource "google_compute_autoscaler" "default" { - provider = google-beta - count = var.regional || var.autoscaler_config == null ? 0 : 1 - project = var.project_id - name = var.name - description = "Terraform managed." - zone = var.location - target = google_compute_instance_group_manager.default.0.id - - autoscaling_policy { - max_replicas = var.autoscaler_config.max_replicas - min_replicas = var.autoscaler_config.min_replicas - cooldown_period = var.autoscaler_config.cooldown_period - - dynamic "cpu_utilization" { - for_each = ( - var.autoscaler_config.cpu_utilization_target == null ? [] : [""] - ) - content { - target = var.autoscaler_config.cpu_utilization_target - } - } - - dynamic "load_balancing_utilization" { - for_each = ( - var.autoscaler_config.load_balancing_utilization_target == null ? [] : [""] - ) - content { - target = var.autoscaler_config.load_balancing_utilization_target - } - } - - dynamic "metric" { - for_each = ( - var.autoscaler_config.metric == null - ? [] - : [var.autoscaler_config.metric] - ) - iterator = config - content { - name = config.value.name - single_instance_assignment = config.value.single_instance_assignment - target = config.value.target - type = config.value.type - filter = config.value.filter - } - } - } +locals { + health_check = ( + try(var.auto_healing_policies.health_check, null) == null + ? try(google_compute_health_check.autohealing.0.self_link, null) + : try(var.auto_healing_policies.health_check, null) + ) + instance_group_manager = ( + local.is_regional ? + google_compute_region_instance_group_manager.default : + google_compute_instance_group_manager.default + ) + is_regional = length(split("-", var.location)) == 2 } - resource "google_compute_instance_group_manager" "default" { - provider = google-beta - count = var.regional ? 0 : 1 - project = var.project_id - zone = var.location - name = var.name - base_instance_name = var.name - description = "Terraform-managed." - target_size = var.target_size - target_pools = var.target_pools - wait_for_instances = var.wait_for_instances + provider = google-beta + count = local.is_regional ? 0 : 1 + project = var.project_id + zone = var.location + name = var.name + base_instance_name = var.name + description = var.description + target_size = var.target_size + target_pools = var.target_pools + wait_for_instances = try(var.wait_for_instances.enabled, null) + wait_for_instances_status = try(var.wait_for_instances.status, null) + + dynamic "all_instances_config" { + for_each = var.all_instances_config == null ? [] : [""] + content { + labels = try(var.all_instances_config.labels, null) + metadata = try(var.all_instances_config.metadata, null) + } + } + dynamic "auto_healing_policies" { - for_each = var.auto_healing_policies == null ? [] : [var.auto_healing_policies] + for_each = var.auto_healing_policies == null ? [] : [""] iterator = config content { - health_check = config.value.health_check - initial_delay_sec = config.value.initial_delay_sec - } - } - dynamic "stateful_disk" { - for_each = try(var.stateful_config.mig_config.stateful_disks, {}) - iterator = config - content { - device_name = config.key - delete_rule = config.value.delete_rule - } - } - dynamic "update_policy" { - for_each = var.update_policy == null ? [] : [var.update_policy] - iterator = config - content { - type = config.value.type - minimal_action = config.value.minimal_action - min_ready_sec = config.value.min_ready_sec - max_surge_fixed = ( - config.value.max_surge_type == "fixed" ? config.value.max_surge : null - ) - max_surge_percent = ( - config.value.max_surge_type == "percent" ? config.value.max_surge : null - ) - max_unavailable_fixed = ( - config.value.max_unavailable_type == "fixed" ? config.value.max_unavailable : null - ) - max_unavailable_percent = ( - config.value.max_unavailable_type == "percent" ? config.value.max_unavailable : null - ) + health_check = local.health_check + initial_delay_sec = var.auto_healing_policies.initial_delay_sec } } + dynamic "named_port" { for_each = var.named_ports == null ? {} : var.named_ports iterator = config @@ -121,167 +66,88 @@ resource "google_compute_instance_group_manager" "default" { port = config.value } } - version { - instance_template = var.default_version.instance_template - name = var.default_version.name + + dynamic "stateful_disk" { + for_each = var.stateful_disks + content { + device_name = stateful_disk.key + delete_rule = stateful_disk.value + } } + + dynamic "update_policy" { + for_each = var.update_policy == null ? [] : [var.update_policy] + iterator = p + content { + minimal_action = p.value.minimal_action + type = p.value.type + max_surge_fixed = try(p.value.max_surge.fixed, null) + max_surge_percent = try(p.value.max_surge.percent, null) + max_unavailable_fixed = try(p.value.max_unavailable.fixed, null) + max_unavailable_percent = try(p.value.max_unavailable.percent, null) + min_ready_sec = p.value.min_ready_sec + most_disruptive_allowed_action = p.value.most_disruptive_action + replacement_method = p.value.replacement_method + } + } + + version { + instance_template = var.instance_template + name = var.default_version_name + } + dynamic "version" { - for_each = var.versions == null ? {} : var.versions - iterator = version + for_each = var.versions content { name = version.key instance_template = version.value.instance_template - target_size { - fixed = ( - version.value.target_type == "fixed" ? version.value.target_size : null - ) - percent = ( - version.value.target_type == "percent" ? version.value.target_size : null - ) + dynamic "target_size" { + for_each = version.value.target_size == null ? [] : [""] + content { + fixed = version.value.target_size.fixed + percent = version.value.target_size.percent + } } } } } -locals { - instance_group_manager = ( - var.regional ? - google_compute_region_instance_group_manager.default : - google_compute_instance_group_manager.default - ) -} - -resource "google_compute_per_instance_config" "default" { - for_each = try(var.stateful_config.per_instance_config, {}) - #for_each = var.stateful_config && var.stateful_config.per_instance_config == null ? {} : length(var.stateful_config.per_instance_config) - zone = var.location - # terraform error, solved with locals - #instance_group_manager = var.regional ? google_compute_region_instance_group_manager.default : google_compute_instance_group_manager.default - instance_group_manager = local.instance_group_manager[0].id - name = each.key - project = var.project_id - minimal_action = try(each.value.update_config.minimal_action, null) - most_disruptive_allowed_action = try(each.value.update_config.most_disruptive_allowed_action, null) - remove_instance_state_on_destroy = try(each.value.update_config.remove_instance_state_on_destroy, null) - preserved_state { - - metadata = each.value.metadata - - dynamic "disk" { - for_each = try(each.value.stateful_disks, {}) - #for_each = var.stateful_config.mig_config.stateful_disks == null ? {} : var.stateful_config.mig_config.stateful_disks - iterator = config - content { - device_name = config.key - source = config.value.source - mode = config.value.mode - delete_rule = config.value.delete_rule - } - } - } -} - -resource "google_compute_region_autoscaler" "default" { - provider = google-beta - count = var.regional && var.autoscaler_config != null ? 1 : 0 - project = var.project_id - name = var.name - description = "Terraform managed." - region = var.location - target = google_compute_region_instance_group_manager.default.0.id - - autoscaling_policy { - max_replicas = var.autoscaler_config.max_replicas - min_replicas = var.autoscaler_config.min_replicas - cooldown_period = var.autoscaler_config.cooldown_period - - dynamic "cpu_utilization" { - for_each = ( - var.autoscaler_config.cpu_utilization_target == null ? [] : [""] - ) - content { - target = var.autoscaler_config.cpu_utilization_target - } - } - - dynamic "load_balancing_utilization" { - for_each = ( - var.autoscaler_config.load_balancing_utilization_target == null ? [] : [""] - ) - content { - target = var.autoscaler_config.load_balancing_utilization_target - } - } - - dynamic "metric" { - for_each = ( - var.autoscaler_config.metric == null - ? [] - : [var.autoscaler_config.metric] - ) - iterator = config - content { - name = config.value.name - single_instance_assignment = config.value.single_instance_assignment - target = config.value.target - type = config.value.type - filter = config.value.filter - } - } - } -} - - resource "google_compute_region_instance_group_manager" "default" { provider = google-beta - count = var.regional ? 1 : 0 + count = local.is_regional ? 1 : 0 project = var.project_id region = var.location name = var.name base_instance_name = var.name - description = "Terraform-managed." - target_size = var.target_size - target_pools = var.target_pools - wait_for_instances = var.wait_for_instances - dynamic "auto_healing_policies" { - for_each = var.auto_healing_policies == null ? [] : [var.auto_healing_policies] - iterator = config + description = var.description + distribution_policy_target_shape = try( + var.distribution_policy.target_shape, null + ) + distribution_policy_zones = try( + var.distribution_policy.zones, null + ) + target_size = var.target_size + target_pools = var.target_pools + wait_for_instances = try(var.wait_for_instances.enabled, null) + wait_for_instances_status = try(var.wait_for_instances.status, null) + + dynamic "all_instances_config" { + for_each = var.all_instances_config == null ? [] : [""] content { - health_check = config.value.health_check - initial_delay_sec = config.value.initial_delay_sec - } - } - dynamic "stateful_disk" { - for_each = try(var.stateful_config.mig_config.stateful_disks, {}) - iterator = config - content { - device_name = config.key - delete_rule = config.value.delete_rule + labels = try(var.all_instances_config.labels, null) + metadata = try(var.all_instances_config.metadata, null) } } - dynamic "update_policy" { - for_each = var.update_policy == null ? [] : [var.update_policy] + dynamic "auto_healing_policies" { + for_each = var.auto_healing_policies == null ? [] : [""] iterator = config content { - instance_redistribution_type = config.value.instance_redistribution_type - type = config.value.type - minimal_action = config.value.minimal_action - min_ready_sec = config.value.min_ready_sec - max_surge_fixed = ( - config.value.max_surge_type == "fixed" ? config.value.max_surge : null - ) - max_surge_percent = ( - config.value.max_surge_type == "percent" ? config.value.max_surge : null - ) - max_unavailable_fixed = ( - config.value.max_unavailable_type == "fixed" ? config.value.max_unavailable : null - ) - max_unavailable_percent = ( - config.value.max_unavailable_type == "percent" ? config.value.max_unavailable : null - ) + health_check = local.health_check + initial_delay_sec = var.auto_healing_policies.initial_delay_sec } } + dynamic "named_port" { for_each = var.named_ports == null ? {} : var.named_ports iterator = config @@ -290,172 +156,49 @@ resource "google_compute_region_instance_group_manager" "default" { port = config.value } } - version { - instance_template = var.default_version.instance_template - name = var.default_version.name + + dynamic "stateful_disk" { + for_each = var.stateful_disks + content { + device_name = stateful_disk.key + delete_rule = stateful_disk.value + } } + + dynamic "update_policy" { + for_each = var.update_policy == null ? [] : [var.update_policy] + iterator = p + content { + minimal_action = p.value.minimal_action + type = p.value.type + instance_redistribution_type = p.value.regional_redistribution_type + max_surge_fixed = try(p.value.max_surge.fixed, null) + max_surge_percent = try(p.value.max_surge.percent, null) + max_unavailable_fixed = try(p.value.max_unavailable.fixed, null) + max_unavailable_percent = try(p.value.max_unavailable.percent, null) + min_ready_sec = p.value.min_ready_sec + most_disruptive_allowed_action = p.value.most_disruptive_action + replacement_method = p.value.replacement_method + } + } + + version { + instance_template = var.instance_template + name = var.default_version_name + } + dynamic "version" { - for_each = var.versions == null ? {} : var.versions - iterator = version + for_each = var.versions content { name = version.key instance_template = version.value.instance_template - target_size { - fixed = ( - version.value.target_type == "fixed" ? version.value.target_size : null - ) - percent = ( - version.value.target_type == "percent" ? version.value.target_size : null - ) + dynamic "target_size" { + for_each = version.value.target_size == null ? [] : [""] + content { + fixed = version.value.target_size.fixed + percent = version.value.target_size.percent + } } } } } - -resource "google_compute_health_check" "http" { - provider = google-beta - count = try(var.health_check_config.type, null) == "http" ? 1 : 0 - project = var.project_id - name = var.name - description = "Terraform managed." - - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - http_health_check { - host = try(var.health_check_config.check.host, null) - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request_path = try(var.health_check_config.check.request_path, null) - response = try(var.health_check_config.check.response, null) - } - - dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] - content { - enable = true - } - } -} - -resource "google_compute_health_check" "https" { - provider = google-beta - count = try(var.health_check_config.type, null) == "https" ? 1 : 0 - project = var.project_id - name = var.name - description = "Terraform managed." - - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - https_health_check { - host = try(var.health_check_config.check.host, null) - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request_path = try(var.health_check_config.check.request_path, null) - response = try(var.health_check_config.check.response, null) - } - - dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] - content { - enable = true - } - } -} - -resource "google_compute_health_check" "tcp" { - provider = google-beta - count = try(var.health_check_config.type, null) == "tcp" ? 1 : 0 - project = var.project_id - name = var.name - description = "Terraform managed." - - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - tcp_health_check { - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request = try(var.health_check_config.check.request, null) - response = try(var.health_check_config.check.response, null) - } - - dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] - content { - enable = true - } - } -} - -resource "google_compute_health_check" "ssl" { - provider = google-beta - count = try(var.health_check_config.type, null) == "ssl" ? 1 : 0 - project = var.project_id - name = var.name - description = "Terraform managed." - - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - ssl_health_check { - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request = try(var.health_check_config.check.request, null) - response = try(var.health_check_config.check.response, null) - } - - dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] - content { - enable = true - } - } -} - -resource "google_compute_health_check" "http2" { - provider = google-beta - count = try(var.health_check_config.type, null) == "http2" ? 1 : 0 - project = var.project_id - name = var.name - description = "Terraform managed." - - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - http2_health_check { - host = try(var.health_check_config.check.host, null) - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request_path = try(var.health_check_config.check.request_path, null) - response = try(var.health_check_config.check.response, null) - } - - dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] - content { - enable = true - } - } -} diff --git a/modules/compute-mig/outputs.tf b/modules/compute-mig/outputs.tf index 93de9223..a7be7d2e 100644 --- a/modules/compute-mig/outputs.tf +++ b/modules/compute-mig/outputs.tf @@ -37,13 +37,6 @@ output "health_check" { value = ( var.health_check_config == null ? null - : try( - google_compute_health_check.http.0, - google_compute_health_check.https.0, - google_compute_health_check.tcp.0, - google_compute_health_check.ssl.0, - google_compute_health_check.http2.0, - {} - ) + : google_compute_health_check.autohealing.0 ) } diff --git a/modules/compute-mig/stateful-config.tf b/modules/compute-mig/stateful-config.tf new file mode 100644 index 00000000..1e9e056e --- /dev/null +++ b/modules/compute-mig/stateful-config.tf @@ -0,0 +1,91 @@ +/** + * Copyright 2022 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. + */ + +# tfdoc:file:description Instance-level stateful configuration resources. + +resource "google_compute_per_instance_config" "default" { + for_each = local.is_regional ? {} : var.stateful_config + project = var.project_id + zone = var.location + name = each.key + instance_group_manager = try( + google_compute_instance_group_manager.default.0.id, null + ) + minimal_action = each.value.minimal_action + most_disruptive_allowed_action = each.value.most_disruptive_action + remove_instance_state_on_destroy = each.value.remove_state_on_destroy + + dynamic "preserved_state" { + for_each = each.value.preserved_state == null ? [] : [""] + content { + metadata = each.value.preserved_state.metadata + dynamic "disk" { + for_each = ( + each.value.preserved_state.disks == null + ? {} + : each.value.preserved_state.disks + ) + content { + device_name = disk.key + source = disk.value.source + delete_rule = ( + disk.value.delete_on_instance_deletion == true + ? "ON_PERMANENT_INSTANCE_DELETION" + : "NEVER" + ) + mode = disk.value.read_only == true ? "READ_ONLY" : "READ_WRITE" + } + } + } + } +} + +resource "google_compute_region_per_instance_config" "default" { + for_each = local.is_regional ? var.stateful_config : {} + project = var.project_id + region = var.location + name = each.key + region_instance_group_manager = try( + google_compute_region_instance_group_manager.default.0.id, null + ) + minimal_action = each.value.minimal_action + most_disruptive_allowed_action = each.value.most_disruptive_action + remove_instance_state_on_destroy = each.value.remove_state_on_destroy + + dynamic "preserved_state" { + for_each = each.value.preserved_state == null ? [] : [""] + content { + metadata = each.value.preserved_state.metadata + dynamic "disk" { + for_each = ( + each.value.preserved_state.disks == null + ? {} + : each.value.preserved_state.disks + ) + content { + device_name = disk.key + source = disk.value.source + delete_rule = ( + disk.value.delete_on_instance_deletion == true + ? "ON_PERMANENT_INSTANCE_DELETION" + : "NEVER" + ) + mode = disk.value.read_only == true ? "READ_ONLY" : "READ_WRITE" + } + } + } + } +} diff --git a/modules/compute-mig/variables.tf b/modules/compute-mig/variables.tf index 76f4fb21..299dacc8 100644 --- a/modules/compute-mig/variables.tf +++ b/modules/compute-mig/variables.tf @@ -14,57 +14,149 @@ * limitations under the License. */ +variable "all_instances_config" { + description = "Metadata and labels set to all instances in the group." + type = object({ + labels = optional(map(string)) + metadata = optional(map(string)) + }) + default = null +} + variable "auto_healing_policies" { description = "Auto-healing policies for this group." type = object({ - health_check = string + health_check = optional(string) initial_delay_sec = number }) default = null } variable "autoscaler_config" { - description = "Optional autoscaler configuration. Only one of 'cpu_utilization_target' 'load_balancing_utilization_target' or 'metric' can be not null." + description = "Optional autoscaler configuration." type = object({ - max_replicas = number - min_replicas = number - cooldown_period = number - cpu_utilization_target = number - load_balancing_utilization_target = number - metric = object({ - name = string - single_instance_assignment = number - target = number - type = string # GAUGE, DELTA_PER_SECOND, DELTA_PER_MINUTE - filter = string - }) + max_replicas = number + min_replicas = number + cooldown_period = optional(number) + mode = optional(string) # OFF, ONLY_UP, ON + scaling_control = optional(object({ + down = optional(object({ + max_replicas_fixed = optional(number) + max_replicas_percent = optional(number) + time_window_sec = optional(number) + })) + in = optional(object({ + max_replicas_fixed = optional(number) + max_replicas_percent = optional(number) + time_window_sec = optional(number) + })) + }), {}) + scaling_signals = optional(object({ + cpu_utilization = optional(object({ + target = number + optimize_availability = optional(bool) + })) + load_balancing_utilization = optional(object({ + target = number + })) + metrics = optional(list(object({ + name = string + type = string # GAUGE, DELTA_PER_SECOND, DELTA_PER_MINUTE + target_value = number + single_instance_assignment = optional(number) + time_series_filter = optional(string) + }))) + schedules = optional(list(object({ + duration_sec = number + name = string + min_required_replicas = number + cron_schedule = string + description = optional(bool) + timezone = optional(string) + disabled = optional(bool) + }))) + }), {}) }) default = null } -variable "default_version" { - description = "Default application version template. Additional versions can be specified via the `versions` variable." +variable "default_version_name" { + description = "Name used for the default version." + type = string + default = "default" +} + +variable "description" { + description = "Optional description used for all resources managed by this module." + type = string + default = "Terraform managed." +} + +variable "distribution_policy" { + description = "DIstribution policy for regional MIG." type = object({ - instance_template = string - name = string + target_shape = optional(string) + zones = optional(list(string)) }) + default = null } variable "health_check_config" { description = "Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage." type = object({ - type = string # http https tcp ssl http2 - check = map(any) # actual health check block attributes - config = map(number) # interval, thresholds, timeout - logging = bool + check_interval_sec = optional(number) + description = optional(string, "Terraform managed.") + enable_logging = optional(bool, false) + healthy_threshold = optional(number) + timeout_sec = optional(number) + unhealthy_threshold = optional(number) + grpc = optional(object({ + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + service_name = optional(string) + })) + http = optional(object({ + host = optional(string) + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + proxy_header = optional(string) + request_path = optional(string) + response = optional(string) + use_protocol = optional(string, "http") # http http2 https + })) + tcp = optional(object({ + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + proxy_header = optional(string) + request = optional(string) + response = optional(string) + use_ssl = optional(bool, false) + })) }) default = null + validation { + condition = ( + (try(var.health_check_config.grpc, null) == null ? 0 : 1) + + (try(var.health_check_config.http, null) == null ? 0 : 1) + + (try(var.health_check_config.tcp, null) == null ? 0 : 1) <= 1 + ) + error_message = "Only one health check type can be configured at a time." + } +} + +variable "instance_template" { + description = "Instance template for the default version." + type = string } variable "location" { - description = "Compute zone, or region if `regional` is set to true." + description = "Compute zone or region." type = string } + variable "name" { description = "Managed group name." type = string @@ -81,41 +173,30 @@ variable "project_id" { type = string } -variable "regional" { - description = "Use regional instance group. When set, `location` should be set to the region." - type = bool - default = false +variable "stateful_disks" { + description = "Stateful disk configuration applied at the MIG level to all instances, in device name => on permanent instance delete rule as boolean." + type = map(bool) + default = {} + nullable = false } variable "stateful_config" { - description = "Stateful configuration can be done by individual instances or for all instances in the MIG. They key in per_instance_config is the name of the specific instance. The key of the stateful_disks is the 'device_name' field of the resource. Please note that device_name is defined at the OS mount level, unlike the disk name." - type = object({ - per_instance_config = map(object({ - #name is the key - #name = string - stateful_disks = map(object({ - #device_name is the key - source = string - mode = string # READ_WRITE | READ_ONLY - delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION - })) - metadata = map(string) - update_config = object({ - minimal_action = string # NONE | REPLACE | RESTART | REFRESH - most_disruptive_allowed_action = string # REPLACE | RESTART | REFRESH | NONE - remove_instance_state_on_destroy = bool - }) + description = "Stateful configuration for individual instances." + type = map(object({ + minimal_action = optional(string) + most_disruptive_action = optional(string) + remove_state_on_destroy = optional(bool) + preserved_state = optional(object({ + disks = optional(map(object({ + source = string + delete_on_instance_deletion = optional(bool) + read_only = optional(bool) + }))) + metadata = optional(map(string)) })) - - mig_config = object({ - stateful_disks = map(object({ - #device_name is the key - delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION - })) - }) - - }) - default = null + })) + default = {} + nullable = false } variable "target_pools" { @@ -131,32 +212,44 @@ variable "target_size" { } variable "update_policy" { - description = "Update policy. Type can be 'OPPORTUNISTIC' or 'PROACTIVE', action 'REPLACE' or 'restart', surge type 'fixed' or 'percent'." + description = "Update policy. Minimal action and type are required." type = object({ - instance_redistribution_type = optional(string, "PROACTIVE") # NONE | PROACTIVE. The attribute is ignored if regional is set to false. - max_surge_type = string # fixed | percent - max_surge = number - max_unavailable_type = string - max_unavailable = number - minimal_action = string # REPLACE | RESTART - min_ready_sec = number - type = string # OPPORTUNISTIC | PROACTIVE + minimal_action = string + type = string + max_surge = optional(object({ + fixed = optional(number) + percent = optional(number) + })) + max_unavailable = optional(object({ + fixed = optional(number) + percent = optional(number) + })) + min_ready_sec = optional(number) + most_disruptive_action = optional(string) + regional_redistribution_type = optional(string) + replacement_method = optional(string) }) default = null } variable "versions" { - description = "Additional application versions, target_type is either 'fixed' or 'percent'." + description = "Additional application versions, target_size is optional." type = map(object({ instance_template = string - target_type = string # fixed | percent - target_size = number + target_size = optional(object({ + fixed = optional(number) + percent = optional(number) + })) })) - default = null + default = {} + nullable = false } variable "wait_for_instances" { description = "Wait for all instances to be created/updated before returning." - type = bool - default = null + type = object({ + enabled = bool + status = optional(string) + }) + default = null } diff --git a/tests/modules/compute_mig/fixture/main.tf b/tests/modules/compute_mig/fixture/main.tf index 5d87f40f..b91c0140 100644 --- a/tests/modules/compute_mig/fixture/main.tf +++ b/tests/modules/compute_mig/fixture/main.tf @@ -24,21 +24,18 @@ resource "google_compute_disk" "default" { } module "test" { - source = "../../../../modules/compute-mig" - project_id = "my-project" - location = "europe-west1" - name = "test-mig" - target_size = 2 - default_version = { - instance_template = "foo-template" - name = "foo" - } - autoscaler_config = var.autoscaler_config - health_check_config = var.health_check_config - named_ports = var.named_ports - regional = var.regional - stateful_config = var.stateful_config - - update_policy = var.update_policy - versions = var.versions + source = "../../../../modules/compute-mig" + project_id = "my-project" + name = "test-mig" + target_size = 2 + default_version_name = "foo" + instance_template = "foo-template" + location = var.location + autoscaler_config = var.autoscaler_config + health_check_config = var.health_check_config + named_ports = var.named_ports + stateful_config = var.stateful_config + stateful_disks = var.stateful_disks + update_policy = var.update_policy + versions = var.versions } diff --git a/tests/modules/compute_mig/fixture/variables.tf b/tests/modules/compute_mig/fixture/variables.tf index b9fde834..70117838 100644 --- a/tests/modules/compute_mig/fixture/variables.tf +++ b/tests/modules/compute_mig/fixture/variables.tf @@ -14,101 +14,82 @@ * limitations under the License. */ -variable "autoscaler_config" { - type = object({ - max_replicas = number - min_replicas = number - cooldown_period = number - cpu_utilization_target = number - load_balancing_utilization_target = number - metric = object({ - name = string - single_instance_assignment = number - target = number - type = string # GAUGE, DELTA_PER_SECOND, DELTA_PER_MINUTE - filter = string - }) - }) +variable "all_instances_config" { + type = any default = null } variable "auto_healing_policies" { - type = object({ - health_check = string - initial_delay_sec = number - }) + type = any + default = null +} + +variable "autoscaler_config" { + type = any + default = null +} + +variable "default_version_name" { + type = any + default = "default" +} + +variable "description" { + type = any + default = "Terraform managed." +} + +variable "distribution_policy" { + type = any default = null } variable "health_check_config" { - type = object({ - type = string # http https tcp ssl http2 - check = map(any) # actual health check block attributes - config = map(number) # interval, thresholds, timeout - logging = bool - }) + type = any default = null } +variable "location" { + type = any + default = "europe-west1-b" +} + variable "named_ports" { - type = map(number) + type = any default = null } -variable "regional" { - type = bool - default = false +variable "stateful_disks" { + type = any + default = {} } variable "stateful_config" { - description = "Stateful configuration can be done by individual instances or for all instances in the MIG. They key in per_instance_config is the name of the specific instance. The key of the stateful_disks is the 'device_name' field of the resource. Please note that device_name is defined at the OS mount level, unlike the disk name." - type = object({ - per_instance_config = map(object({ - #name is the key - #name = string - stateful_disks = map(object({ - #device_name is the key - source = string - mode = string # READ_WRITE | READ_ONLY - delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION - })) - metadata = map(string) - update_config = object({ - minimal_action = string # NONE | REPLACE | RESTART | REFRESH - most_disruptive_allowed_action = string # REPLACE | RESTART | REFRESH | NONE - remove_instance_state_on_destroy = bool - }) - })) + type = any + default = {} +} - mig_config = object({ - stateful_disks = map(object({ - #device_name is the key - delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION - })) - }) +variable "target_pools" { + type = any + default = [] +} - }) +variable "target_size" { + type = any default = null } variable "update_policy" { - type = object({ - type = string # OPPORTUNISTIC | PROACTIVE - minimal_action = string # REPLACE | RESTART - min_ready_sec = number - max_surge_type = string # fixed | percent - max_surge = number - max_unavailable_type = string - max_unavailable = number - }) + type = any default = null } variable "versions" { - type = map(object({ - instance_template = string - target_type = string # fixed | percent - target_size = number - })) + type = any + default = {} +} + +variable "wait_for_instances" { + type = any default = null } diff --git a/tests/modules/compute_mig/test_plan.py b/tests/modules/compute_mig/test_plan.py index 253e27bc..e24a7ca7 100644 --- a/tests/modules/compute_mig/test_plan.py +++ b/tests/modules/compute_mig/test_plan.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. + def test_defaults(plan_runner): "Test variable defaults." _, resources = plan_runner() @@ -21,7 +22,7 @@ def test_defaults(plan_runner): assert mig['type'] == 'google_compute_instance_group_manager' assert mig['values']['target_size'] == 2 assert mig['values']['zone'] - _, resources = plan_runner(regional='true') + _, resources = plan_runner(location='"europe-west1"') assert len(resources) == 1 mig = resources[0] assert mig['type'] == 'google_compute_region_instance_group_manager' @@ -31,7 +32,12 @@ def test_defaults(plan_runner): def test_health_check(plan_runner): "Test health check resource." - health_check_config = '{type="tcp", check={port=80}, config=null, logging=false}' + health_check_config = '''{ + enable_logging = true + tcp = { + port = 80 + } + }''' _, resources = plan_runner(health_check_config=health_check_config) assert len(resources) == 2 assert any(r['type'] == 'google_compute_health_check' for r in resources) @@ -39,20 +45,26 @@ def test_health_check(plan_runner): def test_autoscaler(plan_runner): "Test autoscaler resource." - autoscaler_config = ( - '{' - 'max_replicas=3, min_replicas=1, cooldown_period=60,' - 'cpu_utilization_target=65, load_balancing_utilization_target=null,' - 'metric=null' - '}' - ) + autoscaler_config = '''{ + colldown_period = 60 + max_replicas = 3 + min_replicas = 1 + scaling_signals = { + cpu_utilization = { + target = 65 + } + } + }''' _, resources = plan_runner(autoscaler_config=autoscaler_config) assert len(resources) == 2 autoscaler = resources[0] assert autoscaler['type'] == 'google_compute_autoscaler' assert autoscaler['values']['autoscaling_policy'] == [{ 'cooldown_period': 60, - 'cpu_utilization': [{'predictive_method': 'NONE', 'target': 65}], + 'cpu_utilization': [{ + 'predictive_method': 'NONE', + 'target': 65 + }], 'load_balancing_utilization': [], 'max_replicas': 3, 'metric': [], @@ -62,7 +74,7 @@ def test_autoscaler(plan_runner): 'scaling_schedules': [], }] _, resources = plan_runner(autoscaler_config=autoscaler_config, - regional='true') + location='"europe-west1"') assert len(resources) == 2 autoscaler = resources[0] assert autoscaler['type'] == 'google_compute_region_autoscaler' @@ -71,17 +83,10 @@ def test_autoscaler(plan_runner): def test_stateful_mig(plan_runner): "Test stateful instances - mig." - stateful_config = ( - '{' - 'per_instance_config = {},' - 'mig_config = {' - 'stateful_disks = {' - 'persistent-disk-1 = {delete_rule="NEVER"}' - '}' - '}' - '}' - ) - _, resources = plan_runner(stateful_config=stateful_config) + stateful_disks = '''{ + persistent-disk-1 = null + }''' + _, resources = plan_runner(stateful_disks=stateful_disks) assert len(resources) == 1 statefuldisk = resources[0] assert statefuldisk['type'] == 'google_compute_instance_group_manager' @@ -93,35 +98,19 @@ def test_stateful_mig(plan_runner): def test_stateful_instance(plan_runner): "Test stateful instances - instance." - stateful_config = ( - '{' - 'per_instance_config = {' - 'instance-1 = {' - 'stateful_disks = {' - 'persistent-disk-1 = {' - 'source = "test-disk",' - 'mode = "READ_ONLY",' - 'delete_rule= "NEVER",' - '},' - '},' - 'metadata = {' - 'foo = "bar"' - '},' - 'update_config = {' - 'minimal_action = "NONE",' - 'most_disruptive_allowed_action = "REPLACE",' - 'remove_instance_state_on_destroy = false,' - - '},' - '},' - '},' - 'mig_config = {' - 'stateful_disks = {' - 'persistent-disk-1 = {delete_rule="NEVER"}' - '}' - '}' - '}' - ) + stateful_config = '''{ + instance-1 = { + most_disruptive_action = "REPLACE", + preserved_state = { + disks = { + persistent-disk-1 = { + source = "test-disk" + } + } + metadata = { foo = "bar" } + } + } + }''' _, resources = plan_runner(stateful_config=stateful_config) assert len(resources) == 2 instanceconfig = resources[0] @@ -134,13 +123,12 @@ def test_stateful_instance(plan_runner): 'device_name': 'persistent-disk-1', 'delete_rule': 'NEVER', 'source': 'test-disk', - 'mode': 'READ_ONLY', + 'mode': 'READ_WRITE', }], 'metadata': { 'foo': 'bar' } }] - assert instanceconfig['values']['minimal_action'] == 'NONE' assert instanceconfig['values']['most_disruptive_allowed_action'] == 'REPLACE' assert instanceconfig['values']['remove_instance_state_on_destroy'] == False