From 601ebd028e9d77d699dc0816596d916faa0d7760 Mon Sep 17 00:00:00 2001 From: Stenio Ferreira Date: Wed, 15 Dec 2021 07:56:53 -0600 Subject: [PATCH] Added support for Stateful Managed Instance Groups (#367) * First iteration updates * All tests passing * Updated README and var descriptions * Updated README * Updated example README * Consolidated stateful vars * consolidated stateful vars * Updated README * Requested changes to try * Fixed README examples and try Co-authored-by: Ludovico Magnocavallo --- modules/compute-mig/README.md | 185 +++++++++++++++++- modules/compute-mig/main.tf | 54 +++++ modules/compute-mig/variables.tf | 32 ++- tests/modules/compute_mig/fixture/main.tf | 15 +- .../modules/compute_mig/fixture/variables.tf | 31 +++ tests/modules/compute_mig/test_plan.py | 80 +++++++- 6 files changed, 390 insertions(+), 7 deletions(-) diff --git a/modules/compute-mig/README.md b/modules/compute-mig/README.md index 4293604e3..f1651bc08 100644 --- a/modules/compute-mig/README.md +++ b/modules/compute-mig/README.md @@ -1,8 +1,10 @@ # GCE Managed Instance Group module -This module allows creating a managed instance group supporting one or more application versions via instance templates. A health check and an autoscaler can also be optionally created. +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. ## Examples @@ -266,6 +268,182 @@ module "nginx-mig" { # tftest:modules=2:resources=2 ``` +### 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. + +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). + +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 = "./modules/cloud-config-container/nginx" +} + +module "nginx-template" { + source = "./modules/compute-vm" + project_id = var.project_id + name = "nginx-template" + zone = "europe-west1-b" + tags = ["http-server", "ssh"] + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + nat = false + addresses = null + }] + boot_disk = { + image = "projects/cos-cloud/global/images/family/cos-stable" + type = "pd-ssd" + size = 10 + } + attached_disks = [{ + name = "repd-1" + size = null + source_type = "attach" + source = "regions/${var.region}/disks/repd-test-1" + options = { + mode = "READ_ONLY" + replica_zone = "${var.region}-c" + type = "PERSISTENT" + } + }] + create_template = true + metadata = { + user-data = module.cos-nginx.cloud_config + } +} + +module "nginx-mig" { + source = "./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" + } + 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 = { + persistent-disk-1 = { + delete_rule = "NEVER" + } + } + } + } +} +# tftest:modules=2:resources=3 + +``` + +### Stateful MIGs - Instance Config +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. + +```hcl +module "cos-nginx" { + source = "./modules/cloud-config-container/nginx" +} + +module "nginx-template" { + source = "./modules/compute-vm" + project_id = var.project_id + name = "nginx-template" + zone = "europe-west1-b" + tags = ["http-server", "ssh"] + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + nat = false + addresses = null + }] + boot_disk = { + image = "projects/cos-cloud/global/images/family/cos-stable" + type = "pd-ssd" + size = 10 + } + attached_disks = [{ + name = "repd-1" + size = null + source_type = "attach" + source = "regions/${var.region}/disks/repd-test-1" + options = { + mode = "READ_ONLY" + replica_zone = "${var.region}-c" + type = "PERSISTENT" + } + }] + create_template = true + metadata = { + user-data = module.cos-nginx.cloud_config + } +} + +module "nginx-mig" { + source = "./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" + } + 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 = { + # note that this needs to be the name of an existing instance within the Managed Instance Group + 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 = { + } + } + } +} +# tftest:modules=2:resources=4 + +``` + ## Variables @@ -280,6 +458,7 @@ module "nginx-mig" { | *health_check_config* | 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* | Named ports. | map(number) | | null | | *regional* | Use regional instance group. When set, `location` should be set to the region. | bool | | false | +| *stateful_config* | 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* | Optional list of URLs for target pools to which new instances in the group are added. | list(string) | | [] | | *target_size* | Group target size, leave null when using an autoscaler. | number | | null | | *update_policy* | Update policy. Type can be 'OPPORTUNISTIC' or 'PROACTIVE', action 'REPLACE' or 'restart', surge type 'fixed' or 'percent'. | object({...}) | | null | @@ -297,4 +476,4 @@ module "nginx-mig" { ## TODO -- [ ] add support for instance groups +- [✓] add support for instance groups diff --git a/modules/compute-mig/main.tf b/modules/compute-mig/main.tf index 75ab2d3dc..5b3a92425 100644 --- a/modules/compute-mig/main.tf +++ b/modules/compute-mig/main.tf @@ -84,6 +84,14 @@ resource "google_compute_instance_group_manager" "default" { 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 @@ -135,6 +143,43 @@ resource "google_compute_instance_group_manager" "default" { } } +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 @@ -206,6 +251,15 @@ resource "google_compute_region_instance_group_manager" "default" { 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 diff --git a/modules/compute-mig/variables.tf b/modules/compute-mig/variables.tf index def5a74cc..31da4aa34 100644 --- a/modules/compute-mig/variables.tf +++ b/modules/compute-mig/variables.tf @@ -65,7 +65,6 @@ variable "location" { description = "Compute zone, or region if `regional` is set to true." type = string } - variable "name" { description = "Managed group name." type = string @@ -88,6 +87,37 @@ variable "regional" { default = 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 + }) + })) + + mig_config = object({ + stateful_disks = map(object({ + #device_name is the key + delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION + })) + }) + + }) + default = null +} + variable "target_pools" { description = "Optional list of URLs for target pools to which new instances in the group are added." type = list(string) diff --git a/tests/modules/compute_mig/fixture/main.tf b/tests/modules/compute_mig/fixture/main.tf index 4d3a53c41..a18b237eb 100644 --- a/tests/modules/compute_mig/fixture/main.tf +++ b/tests/modules/compute_mig/fixture/main.tf @@ -14,6 +14,15 @@ * limitations under the License. */ +# Used in stateful disk test +resource "google_compute_disk" "default" { + name = "test-disk" + type = "pd-ssd" + zone = "europe-west1-c" + image = "debian-9-stretch-v20200805" + physical_block_size_bytes = 4096 +} + module "test" { source = "../../../../modules/compute-mig" project_id = "my-project" @@ -28,6 +37,8 @@ module "test" { health_check_config = var.health_check_config named_ports = var.named_ports regional = var.regional - update_policy = var.update_policy - versions = var.versions + stateful_config = var.stateful_config + + 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 2a02cb6c9..c025665cd 100644 --- a/tests/modules/compute_mig/fixture/variables.tf +++ b/tests/modules/compute_mig/fixture/variables.tf @@ -60,6 +60,37 @@ variable "regional" { default = 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 + }) + })) + + mig_config = object({ + stateful_disks = map(object({ + #device_name is the key + delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION + })) + }) + + }) + default = null +} + variable "update_policy" { type = object({ type = string # OPPORTUNISTIC | PROACTIVE diff --git a/tests/modules/compute_mig/test_plan.py b/tests/modules/compute_mig/test_plan.py index 81e6a313a..a6987904c 100644 --- a/tests/modules/compute_mig/test_plan.py +++ b/tests/modules/compute_mig/test_plan.py @@ -24,6 +24,7 @@ def test_defaults(plan_runner): "Test variable defaults." _, resources = plan_runner(FIXTURES_DIR) assert len(resources) == 1 + print(resources[0]['type']) mig = resources[0] assert mig['type'] == 'google_compute_instance_group_manager' assert mig['values']['target_size'] == 2 @@ -35,7 +36,6 @@ def test_defaults(plan_runner): assert mig['values']['target_size'] == 2 assert mig['values']['region'] - def test_health_check(plan_runner): "Test health check resource." health_check_config = '{type="tcp", check={port=80}, config=null, logging=false}' @@ -75,3 +75,81 @@ def test_autoscaler(plan_runner): assert len(resources) == 2 autoscaler = resources[0] assert autoscaler['type'] == 'google_compute_region_autoscaler' + +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( + FIXTURES_DIR, stateful_config=stateful_config) + assert len(resources) == 1 + statefuldisk = resources[0] + assert statefuldisk['type'] == 'google_compute_instance_group_manager' + assert statefuldisk['values']['stateful_disk'] == [{ + 'device_name': 'persistent-disk-1', + 'delete_rule': 'NEVER', + }] + +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"}' + '}' + '}' + '}' + ) + _, resources = plan_runner( + FIXTURES_DIR, stateful_config=stateful_config) + assert len(resources) == 2 + instanceconfig = resources[0] + assert instanceconfig['type'] == 'google_compute_instance_group_manager' + instanceconfig = resources[1] + assert instanceconfig['type'] == 'google_compute_per_instance_config' + + assert instanceconfig['values']['preserved_state'] == [{ + 'disk': [{ + 'device_name': 'persistent-disk-1', + 'delete_rule': 'NEVER', + 'source': 'test-disk', + 'mode': 'READ_ONLY', + }], + '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 \ No newline at end of file