From 01c3930fddb373311cf3cecb6b3069f928d521d2 Mon Sep 17 00:00:00 2001 From: aknysh Date: Tue, 17 Jan 2023 16:09:43 -0500 Subject: [PATCH 1/9] Update versions --- LICENSE | 2 +- README.md | 2 +- examples/existing-ips/context.tf | 170 +++++++++++++++++++++++++------ 3 files changed, 142 insertions(+), 32 deletions(-) diff --git a/LICENSE b/LICENSE index c844c705..10eabceb 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2017-2020 Cloud Posse, LLC + Copyright 2017-2023 Cloud Posse, LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 9ae280a1..bf0f76a5 100644 --- a/README.md +++ b/README.md @@ -516,7 +516,7 @@ In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow. ## Copyright -Copyright © 2017-2022 [Cloud Posse, LLC](https://cpco.io/copyright) +Copyright © 2017-2023 [Cloud Posse, LLC](https://cpco.io/copyright) diff --git a/examples/existing-ips/context.tf b/examples/existing-ips/context.tf index e5734b7c..5e0ef885 100644 --- a/examples/existing-ips/context.tf +++ b/examples/existing-ips/context.tf @@ -8,6 +8,8 @@ # Cloud Posse's standard configuration inputs suitable for passing # to Cloud Posse modules. # +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# # Modules should access the whole context as `module.this.context` # to get the input variables with nulls for defaults, # for example `context = module.this.context`, @@ -18,13 +20,13 @@ # will be null, and `module.this.delimiter` will be `-` (hyphen). # - module "this" { source = "cloudposse/label/null" - version = "0.22.0" // requires Terraform >= 0.12.26 + version = "0.25.0" # requires Terraform >= 0.13.0 enabled = var.enabled namespace = var.namespace + tenant = var.tenant environment = var.environment stage = var.stage name = var.name @@ -35,6 +37,10 @@ module "this" { label_order = var.label_order regex_replace_chars = var.regex_replace_chars id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags context = var.context } @@ -42,23 +48,11 @@ module "this" { # Copy contents of cloudposse/terraform-null-label/variables.tf here variable "context" { - type = object({ - enabled = bool - namespace = string - environment = string - stage = string - name = string - delimiter = string - attributes = list(string) - tags = map(string) - additional_tag_map = map(string) - regex_replace_chars = string - label_order = list(string) - id_length_limit = number - }) + type = any default = { enabled = true namespace = null + tenant = null environment = null stage = null name = null @@ -69,6 +63,17 @@ variable "context" { regex_replace_chars = null label_order = [] id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] } description = <<-EOT Single object for setting entire context at once. @@ -77,6 +82,16 @@ variable "context" { Individual variable settings (non-null) override settings in context object, except for attributes, tags, and additional_tag_map, which are merged. EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } } variable "enabled" { @@ -88,32 +103,42 @@ variable "enabled" { variable "namespace" { type = string default = null - description = "Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp'" + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" } variable "environment" { type = string default = null - description = "Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT'" + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" } variable "stage" { type = string default = null - description = "Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release'" + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" } variable "name" { type = string default = null - description = "Solution name, e.g. 'app' or 'jenkins'" + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT } variable "delimiter" { type = string default = null description = <<-EOT - Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`. + Delimiter to be used between ID elements. Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. EOT } @@ -121,36 +146,64 @@ variable "delimiter" { variable "attributes" { type = list(string) default = [] - description = "Additional attributes (e.g. `1`)" + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT } variable "tags" { type = map(string) default = {} - description = "Additional tags (e.g. `map('BusinessUnit','XYZ')`" + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT } variable "additional_tag_map" { type = map(string) default = {} - description = "Additional tags for appending to tags_as_list_of_maps. Not added to `tags`." + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT } variable "label_order" { type = list(string) default = null description = <<-EOT - The naming order of the id output and Name tag. + The order in which the labels (ID elements) appear in the `id`. Defaults to ["namespace", "environment", "stage", "name", "attributes"]. - You can omit any of the 5 elements, but at least one must be present. - EOT + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT } variable "regex_replace_chars" { type = string default = null description = <<-EOT - Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`. + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. EOT } @@ -159,11 +212,68 @@ variable "id_length_limit" { type = number default = null description = <<-EOT - Limit `id` to this many characters. + Limit `id` to this many characters (minimum 6). Set to `0` for unlimited length. - Set to `null` for default, which is `0`. + Set to `null` for keep the existing setting, which defaults to `0`. Does not affect `id_full`. EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT } #### End of copy of cloudposse/terraform-null-label/variables.tf From 7b7d33a0b1286695e40c901e9283300de1336571 Mon Sep 17 00:00:00 2001 From: aknysh Date: Tue, 17 Jan 2023 16:17:50 -0500 Subject: [PATCH 2/9] Updates --- examples/complete/main.tf | 5 ++--- examples/existing-ips/main.tf | 8 +++++--- moved.tf | 2 +- outputs-deprecated.tf | 1 - 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 7fabd4e0..b60362d7 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -4,10 +4,9 @@ provider "aws" { module "vpc" { source = "cloudposse/vpc/aws" - version = "1.1.0" - - cidr_block = "172.16.0.0/16" + version = "2.0.0" + ipv4_primary_cidr_block = "172.16.0.0/16" assign_generated_ipv6_cidr_block = true ipv6_egress_only_internet_gateway_enabled = true diff --git a/examples/existing-ips/main.tf b/examples/existing-ips/main.tf index d3735b2d..22123ac4 100644 --- a/examples/existing-ips/main.tf +++ b/examples/existing-ips/main.tf @@ -4,16 +4,18 @@ provider "aws" { module "vpc" { source = "cloudposse/vpc/aws" - version = "1.1.0" + version = "2.0.0" - cidr_block = "172.16.0.0/16" + ipv4_primary_cidr_block = "172.16.0.0/16" + assign_generated_ipv6_cidr_block = false # disable IPv6 context = module.this.context } resource "aws_eip" "nat_ips" { count = length(var.availability_zones) - vpc = true + + vpc = true depends_on = [ module.vpc diff --git a/moved.tf b/moved.tf index b235b64c..121e29a8 100644 --- a/moved.tf +++ b/moved.tf @@ -13,4 +13,4 @@ moved { moved { from = aws_route_table_association.public_default to = aws_route_table_association.public -} \ No newline at end of file +} diff --git a/outputs-deprecated.tf b/outputs-deprecated.tf index 8f332556..edde41ae 100644 --- a/outputs-deprecated.tf +++ b/outputs-deprecated.tf @@ -2,4 +2,3 @@ output "nat_gateway_public_ips" { description = "DEPRECATED: use `nat_ips` instead. Public IPv4 IP addresses in use by NAT." value = local.need_nat_eip_data ? var.nat_elastic_ips : aws_eip.default.*.public_ip } - From f0ae0d7b82654ad94d82a6a895df6b9e1183591f Mon Sep 17 00:00:00 2001 From: aknysh Date: Tue, 17 Jan 2023 16:28:32 -0500 Subject: [PATCH 3/9] Updates --- README.md | 2 +- docs/terraform.md | 2 +- variables.tf | 6 ++---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bf0f76a5..7988a3e1 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ Available targets: | [public\_open\_network\_acl\_enabled](#input\_public\_open\_network\_acl\_enabled) | If `true`, a single network ACL be created and it will be associated with every public subnet, and a rule
will be created allowing all ingress and all egress. You can add additional rules to this network ACL
using the `aws_network_acl_rule` resource.
If `false`, you will need to manage the network ACL outside of this module. | `bool` | `true` | no | | [public\_route\_table\_enabled](#input\_public\_route\_table\_enabled) | If `true`, network route table(s) will be created as determined by `public_route_table_per_subnet_enabled` and
appropriate routes will be added to destinations this module knows about.
If `false`, you will need to create your own route table(s) and route(s).
Ignored if `public_route_table_ids` is non-empty. | `bool` | `true` | no | | [public\_route\_table\_ids](#input\_public\_route\_table\_ids) | List optionally containing the ID of a single route table shared by all public subnets
or exactly one route table ID for each public subnet.
If provided, it overrides `public_route_table_per_subnet_enabled`.
If omitted and `public_route_table_enabled` is `true`,
one or more network route tables will be created for the public subnets,
according to the setting of `public_route_table_per_subnet_enabled`. | `list(string)` | `[]` | no | -| [public\_route\_table\_per\_subnet\_enabled](#input\_public\_route\_table\_per\_subnet\_enabled) | If `true` (and `public_route_table_enabled` is `true), a separate network route table will be created for and associated with each public subnet.
If `false` (and `public\_route\_table\_enabled` is `true), a single network route table will be created and it will be associated with every public subnet.
If not set, it will be set to the value of `public_dns64_nat64_enabled`. | `bool` | `null` | no | +| [public\_route\_table\_per\_subnet\_enabled](#input\_public\_route\_table\_per\_subnet\_enabled) | If `true` (and `public_route_table_enabled` is `true`), a separate network route table will be created for and associated with each public subnet.
If `false` (and `public_route_table_enabled` is `true`), a single network route table will be created and it will be associated with every public subnet.
If not set, it will be set to the value of `public_dns64_nat64_enabled`. | `bool` | `null` | no | | [public\_subnets\_additional\_tags](#input\_public\_subnets\_additional\_tags) | Additional tags to be added to public subnets | `map(string)` | `{}` | no | | [public\_subnets\_enabled](#input\_public\_subnets\_enabled) | If false, do not create public subnets.
Since NAT gateways and instances must be created in public subnets, these will also not be created when `false`. | `bool` | `true` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | diff --git a/docs/terraform.md b/docs/terraform.md index d927d277..a88aa00b 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -127,7 +127,7 @@ | [public\_open\_network\_acl\_enabled](#input\_public\_open\_network\_acl\_enabled) | If `true`, a single network ACL be created and it will be associated with every public subnet, and a rule
will be created allowing all ingress and all egress. You can add additional rules to this network ACL
using the `aws_network_acl_rule` resource.
If `false`, you will need to manage the network ACL outside of this module. | `bool` | `true` | no | | [public\_route\_table\_enabled](#input\_public\_route\_table\_enabled) | If `true`, network route table(s) will be created as determined by `public_route_table_per_subnet_enabled` and
appropriate routes will be added to destinations this module knows about.
If `false`, you will need to create your own route table(s) and route(s).
Ignored if `public_route_table_ids` is non-empty. | `bool` | `true` | no | | [public\_route\_table\_ids](#input\_public\_route\_table\_ids) | List optionally containing the ID of a single route table shared by all public subnets
or exactly one route table ID for each public subnet.
If provided, it overrides `public_route_table_per_subnet_enabled`.
If omitted and `public_route_table_enabled` is `true`,
one or more network route tables will be created for the public subnets,
according to the setting of `public_route_table_per_subnet_enabled`. | `list(string)` | `[]` | no | -| [public\_route\_table\_per\_subnet\_enabled](#input\_public\_route\_table\_per\_subnet\_enabled) | If `true` (and `public_route_table_enabled` is `true), a separate network route table will be created for and associated with each public subnet.
If `false` (and `public\_route\_table\_enabled` is `true), a single network route table will be created and it will be associated with every public subnet.
If not set, it will be set to the value of `public_dns64_nat64_enabled`. | `bool` | `null` | no | +| [public\_route\_table\_per\_subnet\_enabled](#input\_public\_route\_table\_per\_subnet\_enabled) | If `true` (and `public_route_table_enabled` is `true`), a separate network route table will be created for and associated with each public subnet.
If `false` (and `public_route_table_enabled` is `true`), a single network route table will be created and it will be associated with every public subnet.
If not set, it will be set to the value of `public_dns64_nat64_enabled`. | `bool` | `null` | no | | [public\_subnets\_additional\_tags](#input\_public\_subnets\_additional\_tags) | Additional tags to be added to public subnets | `map(string)` | `{}` | no | | [public\_subnets\_enabled](#input\_public\_subnets\_enabled) | If false, do not create public subnets.
Since NAT gateways and instances must be created in public subnets, these will also not be created when `false`. | `bool` | `true` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | diff --git a/variables.tf b/variables.tf index 088ac6ba..170d8b33 100644 --- a/variables.tf +++ b/variables.tf @@ -74,7 +74,6 @@ variable "private_label" { default = "private" } - variable "public_label" { type = string description = "The string to use in IDs and elsewhere to identify resources for the public subnets and distinguish them from resources for the private subnets" @@ -149,7 +148,6 @@ variable "ipv6_cidrs" { condition = length(var.ipv6_cidrs) < 2 error_message = "Only 1 ipv6_cidrs object can be provided. Lists of CIDRs are passed via the `public` and `private` attributes of the single object." } - } variable "availability_zones" { @@ -364,8 +362,8 @@ variable "public_route_table_enabled" { variable "public_route_table_per_subnet_enabled" { type = bool description = <<-EOT - If `true` (and `public_route_table_enabled` is `true), a separate network route table will be created for and associated with each public subnet. - If `false` (and `public_route_table_enabled` is `true), a single network route table will be created and it will be associated with every public subnet. + If `true` (and `public_route_table_enabled` is `true`), a separate network route table will be created for and associated with each public subnet. + If `false` (and `public_route_table_enabled` is `true`), a single network route table will be created and it will be associated with every public subnet. If not set, it will be set to the value of `public_dns64_nat64_enabled`. EOT default = null From 4addf1bd497d21f832687dda78b53ee6e140e2f6 Mon Sep 17 00:00:00 2001 From: aknysh Date: Tue, 17 Jan 2023 16:41:51 -0500 Subject: [PATCH 4/9] Updates --- variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variables.tf b/variables.tf index 170d8b33..f96f5bd4 100644 --- a/variables.tf +++ b/variables.tf @@ -33,7 +33,7 @@ variable "max_subnet_count" { type = number description = <<-EOT Sets the maximum number of each type (public or private) of subnet to deploy. - 0 will reserve a CIDR for every Availability Zone (excluding Local Zones) in the region, and + `0` will reserve a CIDR for every Availability Zone (excluding Local Zones) in the region, and deploy a subnet in each availability zone specified in `availability_zones` or `availability_zone_ids`, or every zone if none are specified. We recommend setting this equal to the maximum number of AZs you anticipate using, to avoid causing subnets to be destroyed and recreated with smaller IPv4 CIDRs when AWS adds an availability zone. From 83cf9ef94b2759d1c7982684239d996a66a8dab1 Mon Sep 17 00:00:00 2001 From: aknysh Date: Tue, 17 Jan 2023 22:44:45 -0500 Subject: [PATCH 5/9] Updates --- main.tf | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/main.tf b/main.tf index d3095be0..2c2ba02f 100644 --- a/main.tf +++ b/main.tf @@ -55,7 +55,6 @@ locals { subnet_az_count = local.e ? length(local.subnet_availability_zones) : 0 - subnet_count = ((local.public_enabled ? 1 : 0) + (local.private_enabled ? 1 : 0)) * local.subnet_az_count # Lookup the abbreviations for the availability zones we are using az_abbreviation_map_map = { @@ -63,6 +62,7 @@ locals { fixed = "to_fixed" full = "identity" } + az_abbreviation_map = module.utils.region_az_alt_code_maps[local.az_abbreviation_map_map[var.availability_zone_attribute_style]] subnet_az_abbreviations = [for az in local.subnet_availability_zones : local.az_abbreviation_map[az]] @@ -141,6 +141,7 @@ locals { ) public_route_table_enabled = local.public_enabled && var.public_route_table_enabled + # Use `coalesce` to pick the highest priority value (null means go to next test) public_route_table_count = coalesce( # Do not bother with route tables if not creating subnets @@ -155,6 +156,7 @@ locals { var.public_route_table_per_subnet_enabled == false ? 1 : null, local.public_dns64_enabled ? local.subnet_az_count : 1 ) + create_public_route_tables = local.public_route_table_enabled && length(var.public_route_table_ids) == 0 public_route_table_ids = local.create_public_route_tables ? aws_route_table.public.*.id : var.public_route_table_ids @@ -216,14 +218,16 @@ data "aws_vpc" "default" { } data "aws_eip" "nat" { - count = local.need_nat_eip_data ? length(var.nat_elastic_ips) : 0 + count = local.need_nat_eip_data ? length(var.nat_elastic_ips) : 0 + public_ip = element(var.nat_elastic_ips, count.index) } resource "aws_eip" "default" { count = local.need_nat_eips ? local.nat_count : 0 - vpc = true + + vpc = true tags = merge( module.nat_label.tags, From 6c4757c126036e06eba43d9b550f225cc3418faf Mon Sep 17 00:00:00 2001 From: aknysh Date: Tue, 24 Jan 2023 19:02:42 -0500 Subject: [PATCH 6/9] Updates --- README.md | 14 +++++- docs/terraform.md | 14 +++++- examples/complete/main.tf | 3 ++ examples/complete/outputs.tf | 50 +++++++++++++++++++++ examples/complete/variables.tf | 27 +++++++++++ examples/existing-ips/main.tf | 3 ++ examples/existing-ips/outputs.tf | 50 +++++++++++++++++++++ examples/existing-ips/variables.tf | 27 +++++++++++ main.tf | 62 ++++++++++++++++++++++++-- outputs.tf | 54 +++++++++++++++++++++- test/src/examples_complete_test.go | 14 ++---- test/src/examples_existing_ips_test.go | 4 +- test/src/go.mod | 12 ++--- test/src/go.sum | 26 ++++++----- test/src/utils.go | 14 ++++++ variables.tf | 27 +++++++++++ 16 files changed, 365 insertions(+), 36 deletions(-) create mode 100644 test/src/utils.go diff --git a/README.md b/README.md index 7988a3e1..a76252ae 100644 --- a/README.md +++ b/README.md @@ -358,7 +358,7 @@ Available targets: | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [map\_public\_ip\_on\_launch](#input\_map\_public\_ip\_on\_launch) | If `true`, instances launched into a public subnet will be assigned a public IPv4 address | `bool` | `true` | no | | [max\_nats](#input\_max\_nats) | Upper limit on number of NAT Gateways/Instances to create.
Set to 1 or 2 for cost savings at the expense of availability. | `number` | `999` | no | -| [max\_subnet\_count](#input\_max\_subnet\_count) | Sets the maximum number of each type (public or private) of subnet to deploy.
0 will reserve a CIDR for every Availability Zone (excluding Local Zones) in the region, and
deploy a subnet in each availability zone specified in `availability_zones` or `availability_zone_ids`,
or every zone if none are specified. We recommend setting this equal to the maximum number of AZs you anticipate using,
to avoid causing subnets to be destroyed and recreated with smaller IPv4 CIDRs when AWS adds an availability zone.
Due to Terraform limitations, you can not set `max_subnet_count` from a computed value, you have to set it
from an explicit constant. For most cases, `3` is a good choice. | `number` | `0` | no | +| [max\_subnet\_count](#input\_max\_subnet\_count) | Sets the maximum number of each type (public or private) of subnet to deploy.
`0` will reserve a CIDR for every Availability Zone (excluding Local Zones) in the region, and
deploy a subnet in each availability zone specified in `availability_zones` or `availability_zone_ids`,
or every zone if none are specified. We recommend setting this equal to the maximum number of AZs you anticipate using,
to avoid causing subnets to be destroyed and recreated with smaller IPv4 CIDRs when AWS adds an availability zone.
Due to Terraform limitations, you can not set `max_subnet_count` from a computed value, you have to set it
from an explicit constant. For most cases, `3` is a good choice. | `number` | `0` | no | | [metadata\_http\_endpoint\_enabled](#input\_metadata\_http\_endpoint\_enabled) | Whether the metadata service is available on the created NAT instances | `bool` | `true` | no | | [metadata\_http\_put\_response\_hop\_limit](#input\_metadata\_http\_put\_response\_hop\_limit) | The desired HTTP PUT response hop limit (between 1 and 64) for instance metadata requests on the created NAT instances | `number` | `1` | no | | [metadata\_http\_tokens\_required](#input\_metadata\_http\_tokens\_required) | Whether or not the metadata service requires session tokens, also referred to as Instance Metadata Service Version 2, on the created NAT instances | `bool` | `true` | no | @@ -398,6 +398,8 @@ Available targets: | [subnet\_delete\_timeout](#input\_subnet\_delete\_timeout) | Time to wait for a subnet to be deleted, specified as a Go Duration, e.g. `5m` | `string` | `"20m"` | no | | [subnet\_type\_tag\_key](#input\_subnet\_type\_tag\_key) | DEPRECATED: Use `public_subnets_additional_tags` and `private_subnets_additional_tags` instead
Key for subnet type tag to provide information about the type of subnets, e.g. `cpco.io/subnet/type: private` or `cpco.io/subnet/type: public` | `string` | `null` | no | | [subnet\_type\_tag\_value\_format](#input\_subnet\_type\_tag\_value\_format) | DEPRECATED: Use `public_subnets_additional_tags` and `private_subnets_additional_tags` instead.
The value of the `subnet_type_tag_key` will be set to `format(var.subnet_type_tag_value_format, )`
where `` is either `public` or `private`. | `string` | `"%s"` | no | +| [subnets\_per\_az\_count](#input\_subnets\_per\_az\_count) | The number of subnet of each type (public or private) to provision per Availability Zone. | `number` | `1` | no | +| [subnets\_per\_az\_names](#input\_subnets\_per\_az\_names) | The subnet names of each type (public or private) to provision per Availability Zone.
This variable is optional.
If a list of names is provided, the list items will be used as keys in the outputs `named_private_subnets_map`, `named_public_subnets_map`,
`named_private_route_table_ids_map` and `named_public_route_table_ids_map` | `list(string)` |
[
"common"
]
| no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [vpc\_id](#input\_vpc\_id) | VPC ID where subnets will be created (e.g. `vpc-aceb2723`) | `string` | n/a | yes | @@ -408,6 +410,16 @@ Available targets: |------|-------------| | [availability\_zone\_ids](#output\_availability\_zone\_ids) | List of Availability Zones IDs where subnets were created, when available | | [availability\_zones](#output\_availability\_zones) | List of Availability Zones where subnets were created | +| [az\_private\_route\_table\_ids\_map](#output\_az\_private\_route\_table\_ids\_map) | Map of AZ names to list of private route table IDs in the AZs | +| [az\_private\_subnets\_map](#output\_az\_private\_subnets\_map) | Map of AZ names to list of private subnet IDs in the AZs | +| [az\_public\_route\_table\_ids\_map](#output\_az\_public\_route\_table\_ids\_map) | Map of AZ names to list of public route table IDs in the AZs | +| [az\_public\_subnets\_map](#output\_az\_public\_subnets\_map) | Map of AZ names to list of public subnet IDs in the AZs | +| [named\_private\_route\_table\_ids\_map](#output\_named\_private\_route\_table\_ids\_map) | Map of subnet names (specified in `subnets_per_az_names` variable) to lists of private route table IDs | +| [named\_private\_subnets\_map](#output\_named\_private\_subnets\_map) | Map of subnet names (specified in `subnets_per_az_names` variable) to lists of private subnet IDs | +| [named\_private\_subnets\_stats\_map](#output\_named\_private\_subnets\_stats\_map) | Map of subnet names (specified in `subnets_per_az_names` variable) to lists of objects with each object having three items: AZ, private subnet ID, private route table ID | +| [named\_public\_route\_table\_ids\_map](#output\_named\_public\_route\_table\_ids\_map) | Map of subnet names (specified in `subnets_per_az_names` variable) to lists of public route table IDs | +| [named\_public\_subnets\_map](#output\_named\_public\_subnets\_map) | Map of subnet names (specified in `subnets_per_az_names` variable) to lists of public subnet IDs | +| [named\_public\_subnets\_stats\_map](#output\_named\_public\_subnets\_stats\_map) | Map of subnet names (specified in `subnets_per_az_names` variable) to lists of objects with each object having three items: AZ, public subnet ID, public route table ID | | [nat\_eip\_allocation\_ids](#output\_nat\_eip\_allocation\_ids) | Elastic IP allocations in use by NAT | | [nat\_gateway\_ids](#output\_nat\_gateway\_ids) | IDs of the NAT Gateways created | | [nat\_gateway\_public\_ips](#output\_nat\_gateway\_public\_ips) | DEPRECATED: use `nat_ips` instead. Public IPv4 IP addresses in use by NAT. | diff --git a/docs/terraform.md b/docs/terraform.md index a88aa00b..3d409d09 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -99,7 +99,7 @@ | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [map\_public\_ip\_on\_launch](#input\_map\_public\_ip\_on\_launch) | If `true`, instances launched into a public subnet will be assigned a public IPv4 address | `bool` | `true` | no | | [max\_nats](#input\_max\_nats) | Upper limit on number of NAT Gateways/Instances to create.
Set to 1 or 2 for cost savings at the expense of availability. | `number` | `999` | no | -| [max\_subnet\_count](#input\_max\_subnet\_count) | Sets the maximum number of each type (public or private) of subnet to deploy.
0 will reserve a CIDR for every Availability Zone (excluding Local Zones) in the region, and
deploy a subnet in each availability zone specified in `availability_zones` or `availability_zone_ids`,
or every zone if none are specified. We recommend setting this equal to the maximum number of AZs you anticipate using,
to avoid causing subnets to be destroyed and recreated with smaller IPv4 CIDRs when AWS adds an availability zone.
Due to Terraform limitations, you can not set `max_subnet_count` from a computed value, you have to set it
from an explicit constant. For most cases, `3` is a good choice. | `number` | `0` | no | +| [max\_subnet\_count](#input\_max\_subnet\_count) | Sets the maximum number of each type (public or private) of subnet to deploy.
`0` will reserve a CIDR for every Availability Zone (excluding Local Zones) in the region, and
deploy a subnet in each availability zone specified in `availability_zones` or `availability_zone_ids`,
or every zone if none are specified. We recommend setting this equal to the maximum number of AZs you anticipate using,
to avoid causing subnets to be destroyed and recreated with smaller IPv4 CIDRs when AWS adds an availability zone.
Due to Terraform limitations, you can not set `max_subnet_count` from a computed value, you have to set it
from an explicit constant. For most cases, `3` is a good choice. | `number` | `0` | no | | [metadata\_http\_endpoint\_enabled](#input\_metadata\_http\_endpoint\_enabled) | Whether the metadata service is available on the created NAT instances | `bool` | `true` | no | | [metadata\_http\_put\_response\_hop\_limit](#input\_metadata\_http\_put\_response\_hop\_limit) | The desired HTTP PUT response hop limit (between 1 and 64) for instance metadata requests on the created NAT instances | `number` | `1` | no | | [metadata\_http\_tokens\_required](#input\_metadata\_http\_tokens\_required) | Whether or not the metadata service requires session tokens, also referred to as Instance Metadata Service Version 2, on the created NAT instances | `bool` | `true` | no | @@ -139,6 +139,8 @@ | [subnet\_delete\_timeout](#input\_subnet\_delete\_timeout) | Time to wait for a subnet to be deleted, specified as a Go Duration, e.g. `5m` | `string` | `"20m"` | no | | [subnet\_type\_tag\_key](#input\_subnet\_type\_tag\_key) | DEPRECATED: Use `public_subnets_additional_tags` and `private_subnets_additional_tags` instead
Key for subnet type tag to provide information about the type of subnets, e.g. `cpco.io/subnet/type: private` or `cpco.io/subnet/type: public` | `string` | `null` | no | | [subnet\_type\_tag\_value\_format](#input\_subnet\_type\_tag\_value\_format) | DEPRECATED: Use `public_subnets_additional_tags` and `private_subnets_additional_tags` instead.
The value of the `subnet_type_tag_key` will be set to `format(var.subnet_type_tag_value_format, )`
where `` is either `public` or `private`. | `string` | `"%s"` | no | +| [subnets\_per\_az\_count](#input\_subnets\_per\_az\_count) | The number of subnet of each type (public or private) to provision per Availability Zone. | `number` | `1` | no | +| [subnets\_per\_az\_names](#input\_subnets\_per\_az\_names) | The subnet names of each type (public or private) to provision per Availability Zone.
This variable is optional.
If a list of names is provided, the list items will be used as keys in the outputs `named_private_subnets_map`, `named_public_subnets_map`,
`named_private_route_table_ids_map` and `named_public_route_table_ids_map` | `list(string)` |
[
"common"
]
| no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [vpc\_id](#input\_vpc\_id) | VPC ID where subnets will be created (e.g. `vpc-aceb2723`) | `string` | n/a | yes | @@ -149,6 +151,16 @@ |------|-------------| | [availability\_zone\_ids](#output\_availability\_zone\_ids) | List of Availability Zones IDs where subnets were created, when available | | [availability\_zones](#output\_availability\_zones) | List of Availability Zones where subnets were created | +| [az\_private\_route\_table\_ids\_map](#output\_az\_private\_route\_table\_ids\_map) | Map of AZ names to list of private route table IDs in the AZs | +| [az\_private\_subnets\_map](#output\_az\_private\_subnets\_map) | Map of AZ names to list of private subnet IDs in the AZs | +| [az\_public\_route\_table\_ids\_map](#output\_az\_public\_route\_table\_ids\_map) | Map of AZ names to list of public route table IDs in the AZs | +| [az\_public\_subnets\_map](#output\_az\_public\_subnets\_map) | Map of AZ names to list of public subnet IDs in the AZs | +| [named\_private\_route\_table\_ids\_map](#output\_named\_private\_route\_table\_ids\_map) | Map of subnet names (specified in `subnets_per_az_names` variable) to lists of private route table IDs | +| [named\_private\_subnets\_map](#output\_named\_private\_subnets\_map) | Map of subnet names (specified in `subnets_per_az_names` variable) to lists of private subnet IDs | +| [named\_private\_subnets\_stats\_map](#output\_named\_private\_subnets\_stats\_map) | Map of subnet names (specified in `subnets_per_az_names` variable) to lists of objects with each object having three items: AZ, private subnet ID, private route table ID | +| [named\_public\_route\_table\_ids\_map](#output\_named\_public\_route\_table\_ids\_map) | Map of subnet names (specified in `subnets_per_az_names` variable) to lists of public route table IDs | +| [named\_public\_subnets\_map](#output\_named\_public\_subnets\_map) | Map of subnet names (specified in `subnets_per_az_names` variable) to lists of public subnet IDs | +| [named\_public\_subnets\_stats\_map](#output\_named\_public\_subnets\_stats\_map) | Map of subnet names (specified in `subnets_per_az_names` variable) to lists of objects with each object having three items: AZ, public subnet ID, public route table ID | | [nat\_eip\_allocation\_ids](#output\_nat\_eip\_allocation\_ids) | Elastic IP allocations in use by NAT | | [nat\_gateway\_ids](#output\_nat\_gateway\_ids) | IDs of the NAT Gateways created | | [nat\_gateway\_public\_ips](#output\_nat\_gateway\_public\_ips) | DEPRECATED: use `nat_ips` instead. Public IPv4 IP addresses in use by NAT. | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index b60362d7..47c1f5af 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -31,5 +31,8 @@ module "subnets" { subnet_type_tag_key = "cpco.io/subnet/type" + subnets_per_az_count = var.subnets_per_az_count + subnets_per_az_names = var.subnets_per_az_names + context = module.this.context } diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf index 49b3a2ab..7af81bbf 100644 --- a/examples/complete/outputs.tf +++ b/examples/complete/outputs.tf @@ -32,3 +32,53 @@ output "private_route_table_ids" { description = "IDs of the created private route tables" value = module.subnets.private_route_table_ids } + +output "az_private_subnets_map" { + description = "Map of AZ names to list of private subnet IDs in the AZs" + value = module.subnets.az_private_subnets_map +} + +output "az_public_subnets_map" { + description = "Map of AZ names to list of public subnet IDs in the AZs" + value = module.subnets.az_public_subnets_map +} + +output "az_private_route_table_ids_map" { + description = "Map of AZ names to list of private route table IDs in the AZs" + value = module.subnets.az_private_route_table_ids_map +} + +output "az_public_route_table_ids_map" { + description = "Map of AZ names to list of public route table IDs in the AZs" + value = module.subnets.az_public_route_table_ids_map +} + +output "named_private_subnets_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of private subnet IDs" + value = module.subnets.named_private_subnets_map +} + +output "named_public_subnets_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of public subnet IDs" + value = module.subnets.named_public_subnets_map +} + +output "named_private_route_table_ids_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of private route table IDs" + value = module.subnets.named_private_route_table_ids_map +} + +output "named_public_route_table_ids_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of public route table IDs" + value = module.subnets.named_public_route_table_ids_map +} + +output "named_private_subnets_stats_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of objects with each object having three items: AZ, private subnet ID, private route table ID" + value = module.subnets.named_private_subnets_stats_map +} + +output "named_public_subnets_stats_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of objects with each object having three items: AZ, public subnet ID, public route table ID" + value = module.subnets.named_public_subnets_stats_map +} diff --git a/examples/complete/variables.tf b/examples/complete/variables.tf index 42ff5965..2ca43f5d 100644 --- a/examples/complete/variables.tf +++ b/examples/complete/variables.tf @@ -7,3 +7,30 @@ variable "availability_zones" { type = list(string) description = "List of Availability Zones where subnets will be created" } + +variable "subnets_per_az_count" { + type = number + description = <<-EOT + The number of subnet of each type (public or private) to provision per Availability Zone. + EOT + default = 1 + + validation { + condition = var.subnets_per_az_count > 0 + # Validation error messages must be on a single line, among other restrictions. + # See https://github.com/hashicorp/terraform/issues/24123 + error_message = "The `subnets_per_az` value must be greater than 0." + } +} + +variable "subnets_per_az_names" { + type = list(string) + + description = <<-EOT + The subnet names of each type (public or private) to provision per Availability Zone. + This variable is optional. + If a list of names is provided, the list items will be used as keys in the outputs `named_private_subnets_map`, `named_public_subnets_map`, + `named_private_route_table_ids_map` and `named_public_route_table_ids_map` + EOT + default = ["common"] +} diff --git a/examples/existing-ips/main.tf b/examples/existing-ips/main.tf index 22123ac4..3f9a0954 100644 --- a/examples/existing-ips/main.tf +++ b/examples/existing-ips/main.tf @@ -33,5 +33,8 @@ module "subnets" { nat_gateway_enabled = true nat_instance_enabled = false + subnets_per_az_count = var.subnets_per_az_count + subnets_per_az_names = var.subnets_per_az_names + context = module.this.context } diff --git a/examples/existing-ips/outputs.tf b/examples/existing-ips/outputs.tf index 5b4aae9c..a38081e0 100644 --- a/examples/existing-ips/outputs.tf +++ b/examples/existing-ips/outputs.tf @@ -7,3 +7,53 @@ output "nat_ips" { description = "IP Addresses in use by NAT" value = module.subnets.nat_ips } + +output "az_private_subnets_map" { + description = "Map of AZ names to list of private subnet IDs in the AZs" + value = module.subnets.az_private_subnets_map +} + +output "az_public_subnets_map" { + description = "Map of AZ names to list of public subnet IDs in the AZs" + value = module.subnets.az_public_subnets_map +} + +output "az_private_route_table_ids_map" { + description = "Map of AZ names to list of private route table IDs in the AZs" + value = module.subnets.az_private_route_table_ids_map +} + +output "az_public_route_table_ids_map" { + description = "Map of AZ names to list of public route table IDs in the AZs" + value = module.subnets.az_public_route_table_ids_map +} + +output "named_private_subnets_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of private subnet IDs" + value = module.subnets.named_private_subnets_map +} + +output "named_public_subnets_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of public subnet IDs" + value = module.subnets.named_public_subnets_map +} + +output "named_private_route_table_ids_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of private route table IDs" + value = module.subnets.named_private_route_table_ids_map +} + +output "named_public_route_table_ids_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of public route table IDs" + value = module.subnets.named_public_route_table_ids_map +} + +output "named_private_subnets_stats_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of objects with each object having three items: AZ, private subnet ID, private route table ID" + value = module.subnets.named_private_subnets_stats_map +} + +output "named_public_subnets_stats_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of objects with each object having three items: AZ, public subnet ID, public route table ID" + value = module.subnets.named_public_subnets_stats_map +} diff --git a/examples/existing-ips/variables.tf b/examples/existing-ips/variables.tf index 42ff5965..2ca43f5d 100644 --- a/examples/existing-ips/variables.tf +++ b/examples/existing-ips/variables.tf @@ -7,3 +7,30 @@ variable "availability_zones" { type = list(string) description = "List of Availability Zones where subnets will be created" } + +variable "subnets_per_az_count" { + type = number + description = <<-EOT + The number of subnet of each type (public or private) to provision per Availability Zone. + EOT + default = 1 + + validation { + condition = var.subnets_per_az_count > 0 + # Validation error messages must be on a single line, among other restrictions. + # See https://github.com/hashicorp/terraform/issues/24123 + error_message = "The `subnets_per_az` value must be greater than 0." + } +} + +variable "subnets_per_az_names" { + type = list(string) + + description = <<-EOT + The subnet names of each type (public or private) to provision per Availability Zone. + This variable is optional. + If a list of names is provided, the list items will be used as keys in the outputs `named_private_subnets_map`, `named_public_subnets_map`, + `named_private_route_table_ids_map` and `named_public_route_table_ids_map` + EOT + default = ["common"] +} diff --git a/main.tf b/main.tf index 2c2ba02f..1a7ab471 100644 --- a/main.tf +++ b/main.tf @@ -47,13 +47,16 @@ locals { subnet_possible_availability_zones = local.az_option_map[local.subnet_availability_zone_option] # Adjust list according to `max_subnet_count` - subnet_availability_zones = ( + vpc_availability_zones = ( var.max_subnet_count == 0 || var.max_subnet_count >= length(local.subnet_possible_availability_zones) ) ? ( local.subnet_possible_availability_zones ) : slice(local.subnet_possible_availability_zones, 0, var.max_subnet_count) + # Copy the AZs taking into account the `subnets_per_az` var + subnet_availability_zones = flatten([for z in local.vpc_availability_zones : [for net in range(0, var.subnets_per_az_count) : z]]) + subnet_az_count = local.e ? length(local.subnet_availability_zones) : 0 # Lookup the abbreviations for the availability zones we are using @@ -75,7 +78,7 @@ locals { # Figure out how many CIDRs to reserve. By default, we often reserve more CIDRs than we need so that # with future growth, we can add subnets without affecting existing subnets. existing_az_count = local.e ? length(data.aws_availability_zones.default[0].names) : 0 - base_cidr_reservations = var.max_subnet_count == 0 ? local.existing_az_count : var.max_subnet_count + base_cidr_reservations = (var.max_subnet_count == 0 ? local.existing_az_count : var.max_subnet_count) * var.subnets_per_az_count private_cidr_reservations = (local.private_enabled ? 1 : 0) * local.base_cidr_reservations public_cidr_reservations = (local.public_enabled ? 1 : 0) * local.base_cidr_reservations cidr_reservations = local.private_cidr_reservations + local.public_cidr_reservations @@ -103,6 +106,7 @@ locals { ipv4_private_subnet_cidrs = local.compute_ipv4_cidrs ? [ for net in range(0, local.private_cidr_reservations) : cidrsubnet(local.base_ipv4_cidr_block, local.required_ipv4_subnet_bits, net) ] : local.supplied_ipv4_private_subnet_cidrs + ipv4_public_subnet_cidrs = local.compute_ipv4_cidrs ? [ for net in range(local.private_cidr_reservations, local.cidr_reservations) : cidrsubnet(local.base_ipv4_cidr_block, local.required_ipv4_subnet_bits, net) ] : local.supplied_ipv4_public_subnet_cidrs @@ -110,6 +114,7 @@ locals { ipv6_private_subnet_cidrs = local.compute_ipv6_cidrs ? [ for net in range(0, local.private_cidr_reservations) : cidrsubnet(local.base_ipv6_cidr_block, local.required_ipv6_subnet_bits, net) ] : local.supplied_ipv6_private_subnet_cidrs + ipv6_public_subnet_cidrs = local.compute_ipv6_cidrs ? [ for net in range(local.private_cidr_reservations, local.cidr_reservations) : cidrsubnet(local.base_ipv6_cidr_block, local.required_ipv6_subnet_bits, net) ] : local.supplied_ipv6_public_subnet_cidrs @@ -199,6 +204,58 @@ locals { need_nat_ami_id = local.nat_instance_enabled && length(var.nat_instance_ami_id) == 0 nat_instance_ami_id = local.need_nat_ami_id ? data.aws_ami.nat_instance[0].id : try(var.nat_instance_ami_id[0], "") + # Locals for outputs + az_private_subnets_map = { for z in local.vpc_availability_zones : z => ( + [for s in aws_subnet.private : s.id if s.availability_zone == z]) + } + + az_public_subnets_map = { for z in local.vpc_availability_zones : z => ( + [for s in aws_subnet.public : s.id if s.availability_zone == z]) + } + + az_private_route_table_ids_map = { for k, v in local.az_private_subnets_map : k => ( + [for t in aws_route_table_association.private : t.route_table_id if contains(v, t.subnet_id)]) + } + + az_public_route_table_ids_map = { for k, v in local.az_public_subnets_map : k => ( + [for t in aws_route_table_association.public : t.route_table_id if contains(v, t.subnet_id)]) + } + + named_private_subnets_map = { for i, s in var.subnets_per_az_names : s => ( + compact([for k, v in local.az_private_subnets_map : try(v[i], "")])) + } + + named_public_subnets_map = { for i, s in var.subnets_per_az_names : s => ( + compact([for k, v in local.az_public_subnets_map : try(v[i], "")])) + } + + named_private_route_table_ids_map = { for i, s in var.subnets_per_az_names : s => ( + compact([for k, v in local.az_private_route_table_ids_map : try(v[i], "")])) + } + + named_public_route_table_ids_map = { for i, s in var.subnets_per_az_names : s => ( + compact([for k, v in local.az_public_route_table_ids_map : try(v[i], "")])) + } + + named_private_subnets_stats_map = { for i, s in var.subnets_per_az_names : s => ( + [ + for k, v in local.az_private_route_table_ids_map : { + az = k + route_table_id = try(v[i], "") + subnet_id = try(local.az_private_subnets_map[k][i], "") + } + ]) + } + + named_public_subnets_stats_map = { for i, s in var.subnets_per_az_names : s => ( + [ + for k, v in local.az_public_route_table_ids_map : { + az = k + route_table_id = try(v[i], "") + subnet_id = try(local.az_public_subnets_map[k][i], "") + } + ]) + } } data "aws_availability_zones" "default" { @@ -223,7 +280,6 @@ data "aws_eip" "nat" { public_ip = element(var.nat_elastic_ips, count.index) } - resource "aws_eip" "default" { count = local.need_nat_eips ? local.nat_count : 0 diff --git a/outputs.tf b/outputs.tf index f864626d..4ddaabd7 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,12 +1,12 @@ output "availability_zones" { description = "List of Availability Zones where subnets were created" - value = local.subnet_availability_zones + value = local.vpc_availability_zones } output "availability_zone_ids" { description = "List of Availability Zones IDs where subnets were created, when available" value = local.use_az_ids ? var.availability_zone_ids : [ - for az in local.subnet_availability_zones : local.az_name_map[az] + for az in local.vpc_availability_zones : local.az_name_map[az] ] } @@ -87,3 +87,53 @@ output "nat_eip_allocation_ids" { description = "Elastic IP allocations in use by NAT" value = local.nat_eip_allocations } + +output "az_private_subnets_map" { + description = "Map of AZ names to list of private subnet IDs in the AZs" + value = local.az_private_subnets_map +} + +output "az_public_subnets_map" { + description = "Map of AZ names to list of public subnet IDs in the AZs" + value = local.az_public_subnets_map +} + +output "az_private_route_table_ids_map" { + description = "Map of AZ names to list of private route table IDs in the AZs" + value = local.az_private_route_table_ids_map +} + +output "az_public_route_table_ids_map" { + description = "Map of AZ names to list of public route table IDs in the AZs" + value = local.az_public_route_table_ids_map +} + +output "named_private_subnets_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of private subnet IDs" + value = local.named_private_subnets_map +} + +output "named_public_subnets_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of public subnet IDs" + value = local.named_public_subnets_map +} + +output "named_private_route_table_ids_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of private route table IDs" + value = local.named_private_route_table_ids_map +} + +output "named_public_route_table_ids_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of public route table IDs" + value = local.named_public_route_table_ids_map +} + +output "named_private_subnets_stats_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of objects with each object having three items: AZ, private subnet ID, private route table ID" + value = local.named_private_subnets_stats_map +} + +output "named_public_subnets_stats_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of objects with each object having three items: AZ, public subnet ID, public route table ID" + value = local.named_public_subnets_stats_map +} diff --git a/test/src/examples_complete_test.go b/test/src/examples_complete_test.go index b61f8ed4..0599a96c 100644 --- a/test/src/examples_complete_test.go +++ b/test/src/examples_complete_test.go @@ -2,8 +2,7 @@ package test import ( "github.com/gruntwork-io/terratest/modules/random" - test_structure "github.com/gruntwork-io/terratest/modules/test-structure" - "os" + teststructure "github.com/gruntwork-io/terratest/modules/test-structure" "strings" "testing" @@ -11,11 +10,6 @@ import ( "github.com/stretchr/testify/assert" ) -func cleanup(t *testing.T, terraformOptions *terraform.Options, tempTestFolder string) { - terraform.Destroy(t, terraformOptions) - os.RemoveAll(tempTestFolder) -} - // Test the Terraform module in examples/complete using Terratest. func TestExamplesComplete(t *testing.T) { t.Parallel() @@ -26,7 +20,7 @@ func TestExamplesComplete(t *testing.T) { terraformFolderRelativeToRoot := "examples/complete" varFiles := []string{"fixtures.us-east-2.tfvars"} - tempTestFolder := test_structure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot) + tempTestFolder := teststructure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot) terraformOptions := &terraform.Options{ // The path to where our Terraform code is located @@ -69,7 +63,7 @@ func TestExamplesCompleteDisabled(t *testing.T) { terraformFolderRelativeToRoot := "examples/complete" varFiles := []string{"fixtures.us-east-2.tfvars"} - tempTestFolder := test_structure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot) + tempTestFolder := teststructure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot) terraformOptions := &terraform.Options{ // The path to where our Terraform code is located @@ -79,7 +73,7 @@ func TestExamplesCompleteDisabled(t *testing.T) { VarFiles: varFiles, Vars: map[string]interface{}{ "attributes": attributes, - "enabled": "false", + "enabled": false, }, } diff --git a/test/src/examples_existing_ips_test.go b/test/src/examples_existing_ips_test.go index 0f31c2a6..d0059736 100644 --- a/test/src/examples_existing_ips_test.go +++ b/test/src/examples_existing_ips_test.go @@ -2,7 +2,7 @@ package test import ( "github.com/gruntwork-io/terratest/modules/random" - test_structure "github.com/gruntwork-io/terratest/modules/test-structure" + teststructure "github.com/gruntwork-io/terratest/modules/test-structure" "strings" "testing" @@ -20,7 +20,7 @@ func TestExamplesExistingIps(t *testing.T) { terraformFolderRelativeToRoot := "examples/existing-ips" varFiles := []string{"fixtures.us-east-2.tfvars"} - tempTestFolder := test_structure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot) + tempTestFolder := teststructure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot) terraformOptions := &terraform.Options{ // The path to where our Terraform code is located diff --git a/test/src/go.mod b/test/src/go.mod index 44cafbcd..91a7b418 100644 --- a/test/src/go.mod +++ b/test/src/go.mod @@ -1,10 +1,10 @@ module github.com/cloudposse/terraform-aws-dynamic-subnets -go 1.17 +go 1.19 require ( - github.com/gruntwork-io/terratest v0.39.0 - github.com/stretchr/testify v1.7.0 + github.com/gruntwork-io/terratest v0.41.9 + github.com/stretchr/testify v1.8.1 ) require ( @@ -32,7 +32,7 @@ require ( github.com/gruntwork-io/go-commons v0.8.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-getter v1.5.11 // indirect + github.com/hashicorp/go-getter v1.6.1 // indirect github.com/hashicorp/go-multierror v1.1.0 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-version v1.3.0 // indirect @@ -64,7 +64,7 @@ require ( golang.org/x/mod v0.4.2 // indirect golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c // indirect - golang.org/x/sys v0.0.0-20210603125802-9665404d3644 // indirect + golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e // indirect golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect golang.org/x/text v0.3.6 // indirect golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect @@ -77,7 +77,7 @@ require ( google.golang.org/protobuf v1.26.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.20.6 // indirect k8s.io/apimachinery v0.20.6 // indirect k8s.io/client-go v0.20.6 // indirect diff --git a/test/src/go.sum b/test/src/go.sum index 417452b3..d6ce7fe6 100644 --- a/test/src/go.sum +++ b/test/src/go.sum @@ -177,8 +177,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -213,14 +213,14 @@ github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3i github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gruntwork-io/go-commons v0.8.0 h1:k/yypwrPqSeYHevLlEDmvmgQzcyTwrlZGRaxEM6G0ro= github.com/gruntwork-io/go-commons v0.8.0/go.mod h1:gtp0yTtIBExIZp7vyIV9I0XQkVwiQZze678hvDXof78= -github.com/gruntwork-io/terratest v0.39.0 h1:Lq7aNCoFxhhmdQIyuBFBf8N87aCnypmNBFYgvsdIfCQ= -github.com/gruntwork-io/terratest v0.39.0/go.mod h1:CjHsEgP1Pe987X5N8K5qEqCuLtu1bqERGIAF8bTj1s0= +github.com/gruntwork-io/terratest v0.41.9 h1:jyygu23iLcEFjGQhlvRx4R0EJVqOoriP+Ire4U9cZA0= +github.com/gruntwork-io/terratest v0.41.9/go.mod h1:qH1xkPTTGx30XkMHw8jAVIbzqheSjIa5IyiTwSV2vKI= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-getter v1.5.11 h1:wioTuNmaBU3IE9vdFtFMcmZWj0QzLc6DYaP6sNe5onY= -github.com/hashicorp/go-getter v1.5.11/go.mod h1:9i48BP6wpWweI/0/+FBjqLrp9S8XtwUGjiu0QkWHEaY= +github.com/hashicorp/go-getter v1.6.1 h1:NASsgP4q6tL94WH6nJxKWj8As2H/2kop/bB1d8JMyRY= +github.com/hashicorp/go-getter v1.6.1/go.mod h1:IZCrswsZPeWv9IkVnLElzRU/gz/QPi6pZHn4tv6vbwA= github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= @@ -330,13 +330,17 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tmccombs/hcl2json v0.3.3 h1:+DLNYqpWE0CsOQiEZu+OZm5ZBImake3wtITYxQ8uLFQ= github.com/tmccombs/hcl2json v0.3.3/go.mod h1:Y2chtz2x9bAeRTvSibVRVgbLJhLJXKlUeIvjeVdnm4w= github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ= @@ -524,8 +528,8 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644 h1:CA1DEQ4NdKphKeL70tvsWNdT5oFh1lOjihRcEDROi0I= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e h1:w36l2Uw3dRan1K3TyXriXvY+6T56GNmlKGcqiQUJDfM= +golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -725,8 +729,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/test/src/utils.go b/test/src/utils.go new file mode 100644 index 00000000..72b58dbf --- /dev/null +++ b/test/src/utils.go @@ -0,0 +1,14 @@ +package test + +import ( + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func cleanup(t *testing.T, terraformOptions *terraform.Options, tempTestFolder string) { + terraform.Destroy(t, terraformOptions) + err := os.RemoveAll(tempTestFolder) + assert.NoError(t, err) +} diff --git a/variables.tf b/variables.tf index f96f5bd4..5620c70b 100644 --- a/variables.tf +++ b/variables.tf @@ -471,3 +471,30 @@ variable "nat_instance_root_block_device_encrypted" { default = true } locals { nat_instance_root_block_device_encrypted = var.root_block_device_encrypted == null ? var.nat_instance_root_block_device_encrypted : var.root_block_device_encrypted } + +variable "subnets_per_az_count" { + type = number + description = <<-EOT + The number of subnet of each type (public or private) to provision per Availability Zone. + EOT + default = 1 + + validation { + condition = var.subnets_per_az_count > 0 + # Validation error messages must be on a single line, among other restrictions. + # See https://github.com/hashicorp/terraform/issues/24123 + error_message = "The `subnets_per_az` value must be greater than 0." + } +} + +variable "subnets_per_az_names" { + type = list(string) + + description = <<-EOT + The subnet names of each type (public or private) to provision per Availability Zone. + This variable is optional. + If a list of names is provided, the list items will be used as keys in the outputs `named_private_subnets_map`, `named_public_subnets_map`, + `named_private_route_table_ids_map` and `named_public_route_table_ids_map` + EOT + default = ["common"] +} From c220673b0194aff3118976c973c8d7e4dede3dff Mon Sep 17 00:00:00 2001 From: aknysh Date: Tue, 24 Jan 2023 19:37:04 -0500 Subject: [PATCH 7/9] Multiple subnets per AZ --- README.md | 4 +- docs/terraform.md | 4 +- examples/complete/main.tf | 24 +- examples/multiple-subnets-per-az/context.tf | 279 ++++++++++++++++++ .../fixtures.us-east-2.tfvars | 13 + examples/multiple-subnets-per-az/main.tf | 36 +++ examples/multiple-subnets-per-az/outputs.tf | 84 ++++++ examples/multiple-subnets-per-az/variables.tf | 36 +++ examples/multiple-subnets-per-az/versions.tf | 10 + .../examples_multiple_subnets_per_az_test.go | 85 ++++++ variables-deprecated.tf | 4 +- 11 files changed, 561 insertions(+), 18 deletions(-) create mode 100644 examples/multiple-subnets-per-az/context.tf create mode 100644 examples/multiple-subnets-per-az/fixtures.us-east-2.tfvars create mode 100644 examples/multiple-subnets-per-az/main.tf create mode 100644 examples/multiple-subnets-per-az/outputs.tf create mode 100644 examples/multiple-subnets-per-az/variables.tf create mode 100644 examples/multiple-subnets-per-az/versions.tf create mode 100644 test/src/examples_multiple_subnets_per_az_test.go diff --git a/README.md b/README.md index a76252ae..b8f6e0c2 100644 --- a/README.md +++ b/README.md @@ -330,8 +330,8 @@ Available targets: | [availability\_zone\_attribute\_style](#input\_availability\_zone\_attribute\_style) | The style of Availability Zone code to use in tags and names. One of `full`, `short`, or `fixed`.
When using `availability_zone_ids`, IDs will first be translated into AZ names. | `string` | `"short"` | no | | [availability\_zone\_ids](#input\_availability\_zone\_ids) | List of Availability Zones IDs where subnets will be created. Overrides `availability_zones`.
Useful in some regions when using only some AZs and you want to use the same ones across multiple accounts. | `list(string)` | `[]` | no | | [availability\_zones](#input\_availability\_zones) | List of Availability Zones (AZs) where subnets will be created. Ignored when `availability_zone_ids` is set.
The order of zones in the list ***must be stable*** or else Terraform will continually make changes.
If no AZs are specified, then `max_subnet_count` AZs will be selected in alphabetical order.
If `max_subnet_count > 0` and `length(var.availability_zones) > max_subnet_count`, the list
will be truncated. We recommend setting `availability_zones` and `max_subnet_count` explicitly as constant
(not computed) values for predictability, consistency, and stability. | `list(string)` | `[]` | no | -| [aws\_route\_create\_timeout](#input\_aws\_route\_create\_timeout) | DEPRECTATED: Use `route_create_timeout` instead.
Time to wait for AWS route creation, specified as a Go Duration, e.g. `2m` | `string` | `null` | no | -| [aws\_route\_delete\_timeout](#input\_aws\_route\_delete\_timeout) | DEPRECTATED: Use `route_delete_timeout` instead.
Time to wait for AWS route deletion, specified as a Go Duration, e.g. `2m` | `string` | `null` | no | +| [aws\_route\_create\_timeout](#input\_aws\_route\_create\_timeout) | DEPRECATED: Use `route_create_timeout` instead.
Time to wait for AWS route creation, specified as a Go Duration, e.g. `2m` | `string` | `null` | no | +| [aws\_route\_delete\_timeout](#input\_aws\_route\_delete\_timeout) | DEPRECATED: Use `route_delete_timeout` instead.
Time to wait for AWS route deletion, specified as a Go Duration, e.g. `2m` | `string` | `null` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | diff --git a/docs/terraform.md b/docs/terraform.md index 3d409d09..ce3592da 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -71,8 +71,8 @@ | [availability\_zone\_attribute\_style](#input\_availability\_zone\_attribute\_style) | The style of Availability Zone code to use in tags and names. One of `full`, `short`, or `fixed`.
When using `availability_zone_ids`, IDs will first be translated into AZ names. | `string` | `"short"` | no | | [availability\_zone\_ids](#input\_availability\_zone\_ids) | List of Availability Zones IDs where subnets will be created. Overrides `availability_zones`.
Useful in some regions when using only some AZs and you want to use the same ones across multiple accounts. | `list(string)` | `[]` | no | | [availability\_zones](#input\_availability\_zones) | List of Availability Zones (AZs) where subnets will be created. Ignored when `availability_zone_ids` is set.
The order of zones in the list ***must be stable*** or else Terraform will continually make changes.
If no AZs are specified, then `max_subnet_count` AZs will be selected in alphabetical order.
If `max_subnet_count > 0` and `length(var.availability_zones) > max_subnet_count`, the list
will be truncated. We recommend setting `availability_zones` and `max_subnet_count` explicitly as constant
(not computed) values for predictability, consistency, and stability. | `list(string)` | `[]` | no | -| [aws\_route\_create\_timeout](#input\_aws\_route\_create\_timeout) | DEPRECTATED: Use `route_create_timeout` instead.
Time to wait for AWS route creation, specified as a Go Duration, e.g. `2m` | `string` | `null` | no | -| [aws\_route\_delete\_timeout](#input\_aws\_route\_delete\_timeout) | DEPRECTATED: Use `route_delete_timeout` instead.
Time to wait for AWS route deletion, specified as a Go Duration, e.g. `2m` | `string` | `null` | no | +| [aws\_route\_create\_timeout](#input\_aws\_route\_create\_timeout) | DEPRECATED: Use `route_create_timeout` instead.
Time to wait for AWS route creation, specified as a Go Duration, e.g. `2m` | `string` | `null` | no | +| [aws\_route\_delete\_timeout](#input\_aws\_route\_delete\_timeout) | DEPRECATED: Use `route_delete_timeout` instead.
Time to wait for AWS route deletion, specified as a Go Duration, e.g. `2m` | `string` | `null` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 47c1f5af..b1a934ee 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -16,18 +16,18 @@ module "vpc" { module "subnets" { source = "../../" - availability_zones = var.availability_zones - vpc_id = module.vpc.vpc_id - igw_id = [module.vpc.igw_id] - ipv4_enabled = true - ipv6_enabled = true - ipv6_egress_only_igw_id = [module.vpc.ipv6_egress_only_igw_id] - ipv4_cidr_block = [module.vpc.vpc_cidr_block] - ipv6_cidr_block = [module.vpc.vpc_ipv6_cidr_block] - nat_gateway_enabled = false - nat_instance_enabled = false - aws_route_create_timeout = "5m" - aws_route_delete_timeout = "10m" + availability_zones = var.availability_zones + vpc_id = module.vpc.vpc_id + igw_id = [module.vpc.igw_id] + ipv4_enabled = true + ipv6_enabled = true + ipv6_egress_only_igw_id = [module.vpc.ipv6_egress_only_igw_id] + ipv4_cidr_block = [module.vpc.vpc_cidr_block] + ipv6_cidr_block = [module.vpc.vpc_ipv6_cidr_block] + nat_gateway_enabled = false + nat_instance_enabled = false + route_create_timeout = "5m" + route_delete_timeout = "10m" subnet_type_tag_key = "cpco.io/subnet/type" diff --git a/examples/multiple-subnets-per-az/context.tf b/examples/multiple-subnets-per-az/context.tf new file mode 100644 index 00000000..5e0ef885 --- /dev/null +++ b/examples/multiple-subnets-per-az/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/examples/multiple-subnets-per-az/fixtures.us-east-2.tfvars b/examples/multiple-subnets-per-az/fixtures.us-east-2.tfvars new file mode 100644 index 00000000..ee27538c --- /dev/null +++ b/examples/multiple-subnets-per-az/fixtures.us-east-2.tfvars @@ -0,0 +1,13 @@ +region = "us-east-2" + +availability_zones = ["us-east-2a", "us-east-2b"] + +namespace = "eg" + +stage = "test" + +name = "multiple-subnets-per-az-test" + +subnets_per_az_count = 3 + +subnets_per_az_names = ["frontend", "backend", "db"] diff --git a/examples/multiple-subnets-per-az/main.tf b/examples/multiple-subnets-per-az/main.tf new file mode 100644 index 00000000..e467756c --- /dev/null +++ b/examples/multiple-subnets-per-az/main.tf @@ -0,0 +1,36 @@ +provider "aws" { + region = var.region +} + +module "vpc" { + source = "cloudposse/vpc/aws" + version = "2.0.0" + + ipv4_primary_cidr_block = "172.16.0.0/16" + + context = module.this.context +} + +module "subnets" { + source = "../../" + + availability_zones = var.availability_zones + vpc_id = module.vpc.vpc_id + igw_id = [module.vpc.igw_id] + ipv4_enabled = true + ipv6_enabled = false + ipv6_egress_only_igw_id = [module.vpc.ipv6_egress_only_igw_id] + ipv4_cidr_block = [module.vpc.vpc_cidr_block] + ipv6_cidr_block = [module.vpc.vpc_ipv6_cidr_block] + nat_gateway_enabled = false + nat_instance_enabled = false + route_create_timeout = "5m" + route_delete_timeout = "10m" + + subnet_type_tag_key = "cpco.io/subnet/type" + + subnets_per_az_count = var.subnets_per_az_count + subnets_per_az_names = var.subnets_per_az_names + + context = module.this.context +} diff --git a/examples/multiple-subnets-per-az/outputs.tf b/examples/multiple-subnets-per-az/outputs.tf new file mode 100644 index 00000000..7af81bbf --- /dev/null +++ b/examples/multiple-subnets-per-az/outputs.tf @@ -0,0 +1,84 @@ +output "public_subnet_cidrs" { + description = "IPv4 CIDRs assigned to the created public subnets" + value = module.subnets.public_subnet_cidrs +} + +output "private_subnet_cidrs" { + description = "IPv4 CIDRs assigned to the created private subnets" + value = module.subnets.private_subnet_cidrs +} + +output "public_subnet_ipv6_cidrs" { + description = "IPv6 CIDRs assigned to the created public subnets" + value = module.subnets.public_subnet_ipv6_cidrs +} + +output "private_subnet_ipv6_cidrs" { + description = "IPv6 CIDRs assigned to the created private subnets" + value = module.subnets.private_subnet_ipv6_cidrs +} + +output "vpc_ipv6_cidr" { + description = "Default IPv6 CIDR of the VPC" + value = module.vpc.vpc_ipv6_cidr_block +} + +output "public_route_table_ids" { + description = "IDs of the created public route tables" + value = module.subnets.public_route_table_ids +} + +output "private_route_table_ids" { + description = "IDs of the created private route tables" + value = module.subnets.private_route_table_ids +} + +output "az_private_subnets_map" { + description = "Map of AZ names to list of private subnet IDs in the AZs" + value = module.subnets.az_private_subnets_map +} + +output "az_public_subnets_map" { + description = "Map of AZ names to list of public subnet IDs in the AZs" + value = module.subnets.az_public_subnets_map +} + +output "az_private_route_table_ids_map" { + description = "Map of AZ names to list of private route table IDs in the AZs" + value = module.subnets.az_private_route_table_ids_map +} + +output "az_public_route_table_ids_map" { + description = "Map of AZ names to list of public route table IDs in the AZs" + value = module.subnets.az_public_route_table_ids_map +} + +output "named_private_subnets_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of private subnet IDs" + value = module.subnets.named_private_subnets_map +} + +output "named_public_subnets_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of public subnet IDs" + value = module.subnets.named_public_subnets_map +} + +output "named_private_route_table_ids_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of private route table IDs" + value = module.subnets.named_private_route_table_ids_map +} + +output "named_public_route_table_ids_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of public route table IDs" + value = module.subnets.named_public_route_table_ids_map +} + +output "named_private_subnets_stats_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of objects with each object having three items: AZ, private subnet ID, private route table ID" + value = module.subnets.named_private_subnets_stats_map +} + +output "named_public_subnets_stats_map" { + description = "Map of subnet names (specified in `subnets_per_az_names` variable) to lists of objects with each object having three items: AZ, public subnet ID, public route table ID" + value = module.subnets.named_public_subnets_stats_map +} diff --git a/examples/multiple-subnets-per-az/variables.tf b/examples/multiple-subnets-per-az/variables.tf new file mode 100644 index 00000000..2ca43f5d --- /dev/null +++ b/examples/multiple-subnets-per-az/variables.tf @@ -0,0 +1,36 @@ +variable "region" { + type = string + description = "AWS region" +} + +variable "availability_zones" { + type = list(string) + description = "List of Availability Zones where subnets will be created" +} + +variable "subnets_per_az_count" { + type = number + description = <<-EOT + The number of subnet of each type (public or private) to provision per Availability Zone. + EOT + default = 1 + + validation { + condition = var.subnets_per_az_count > 0 + # Validation error messages must be on a single line, among other restrictions. + # See https://github.com/hashicorp/terraform/issues/24123 + error_message = "The `subnets_per_az` value must be greater than 0." + } +} + +variable "subnets_per_az_names" { + type = list(string) + + description = <<-EOT + The subnet names of each type (public or private) to provision per Availability Zone. + This variable is optional. + If a list of names is provided, the list items will be used as keys in the outputs `named_private_subnets_map`, `named_public_subnets_map`, + `named_private_route_table_ids_map` and `named_public_route_table_ids_map` + EOT + default = ["common"] +} diff --git a/examples/multiple-subnets-per-az/versions.tf b/examples/multiple-subnets-per-az/versions.tf new file mode 100644 index 00000000..9e386405 --- /dev/null +++ b/examples/multiple-subnets-per-az/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 3.71.0" + } + } +} diff --git a/test/src/examples_multiple_subnets_per_az_test.go b/test/src/examples_multiple_subnets_per_az_test.go new file mode 100644 index 00000000..06d78d23 --- /dev/null +++ b/test/src/examples_multiple_subnets_per_az_test.go @@ -0,0 +1,85 @@ +package test + +import ( + "github.com/gruntwork-io/terratest/modules/random" + teststructure "github.com/gruntwork-io/terratest/modules/test-structure" + "strings" + "testing" + + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" +) + +func TestExamplesMultipleSubnetsPerAZ(t *testing.T) { + t.Parallel() + randID := strings.ToLower(random.UniqueId()) + attributes := []string{randID} + + rootFolder := "../../" + terraformFolderRelativeToRoot := "examples/multiple-subnets-per-az" + varFiles := []string{"fixtures.us-east-2.tfvars"} + + tempTestFolder := teststructure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot) + + terraformOptions := &terraform.Options{ + // The path to where our Terraform code is located + TerraformDir: tempTestFolder, + Upgrade: true, + // Variables to pass to our Terraform code using -var-file options + VarFiles: varFiles, + Vars: map[string]interface{}{ + "attributes": attributes, + }, + } + + // At the end of the test, run `terraform destroy` to clean up any resources that were created + defer cleanup(t, terraformOptions, tempTestFolder) + + // This will run `terraform init` and `terraform apply` and fail the test if there are any errors + terraform.InitAndApply(t, terraformOptions) + + // Run `terraform output` to get the value of an output variable + privateSubnetCidrs := terraform.OutputList(t, terraformOptions, "private_subnet_cidrs") + expectedPrivateSubnetCidrs := []string{"172.16.0.0/19", "172.16.32.0/19"} + // Verify we're getting back the outputs we expect + assert.Equal(t, expectedPrivateSubnetCidrs, privateSubnetCidrs) + + // Run `terraform output` to get the value of an output variable + publicSubnetCidrs := terraform.OutputList(t, terraformOptions, "public_subnet_cidrs") + expectedPublicSubnetCidrs := []string{"172.16.96.0/19", "172.16.128.0/19"} + // Verify we're getting back the outputs we expect + assert.Equal(t, expectedPublicSubnetCidrs, publicSubnetCidrs) +} + +func TestExamplesMultipleSubnetsPerAZDisabled(t *testing.T) { + t.Parallel() + randID := strings.ToLower(random.UniqueId()) + attributes := []string{randID} + + rootFolder := "../../" + terraformFolderRelativeToRoot := "examples/multiple-subnets-per-az" + varFiles := []string{"fixtures.us-east-2.tfvars"} + + tempTestFolder := teststructure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot) + + terraformOptions := &terraform.Options{ + // The path to where our Terraform code is located + TerraformDir: tempTestFolder, + Upgrade: true, + // Variables to pass to our Terraform code using -var-file options + VarFiles: varFiles, + Vars: map[string]interface{}{ + "attributes": attributes, + "enabled": false, + }, + } + + // At the end of the test, run `terraform destroy` to clean up any resources that were created + defer cleanup(t, terraformOptions, tempTestFolder) + + // This will run `terraform init` and `terraform apply` and fail the test if there are any errors + results := terraform.InitAndApply(t, terraformOptions) + + // Should complete successfully without creating or changing any resources + assert.Contains(t, results, "Resources: 0 added, 0 changed, 0 destroyed.") +} diff --git a/variables-deprecated.tf b/variables-deprecated.tf index 8749dba8..201522d3 100644 --- a/variables-deprecated.tf +++ b/variables-deprecated.tf @@ -1,7 +1,7 @@ variable "aws_route_create_timeout" { type = string description = <<-EOT - DEPRECTATED: Use `route_create_timeout` instead. + DEPRECATED: Use `route_create_timeout` instead. Time to wait for AWS route creation, specified as a Go Duration, e.g. `2m` EOT default = null @@ -10,7 +10,7 @@ variable "aws_route_create_timeout" { variable "aws_route_delete_timeout" { type = string description = <<-EOT - DEPRECTATED: Use `route_delete_timeout` instead. + DEPRECATED: Use `route_delete_timeout` instead. Time to wait for AWS route deletion, specified as a Go Duration, e.g. `2m` EOT default = null From 8582bc2ba9df6edad2ab61e9e2068bbcecc7c781 Mon Sep 17 00:00:00 2001 From: aknysh Date: Tue, 24 Jan 2023 20:15:05 -0500 Subject: [PATCH 8/9] Multiple subnets per AZ --- .../fixtures.us-east-2.tfvars | 2 +- .../examples_multiple_subnets_per_az_test.go | 20 +++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/examples/multiple-subnets-per-az/fixtures.us-east-2.tfvars b/examples/multiple-subnets-per-az/fixtures.us-east-2.tfvars index ee27538c..98f00da8 100644 --- a/examples/multiple-subnets-per-az/fixtures.us-east-2.tfvars +++ b/examples/multiple-subnets-per-az/fixtures.us-east-2.tfvars @@ -10,4 +10,4 @@ name = "multiple-subnets-per-az-test" subnets_per_az_count = 3 -subnets_per_az_names = ["frontend", "backend", "db"] +subnets_per_az_names = ["services", "backend", "db"] diff --git a/test/src/examples_multiple_subnets_per_az_test.go b/test/src/examples_multiple_subnets_per_az_test.go index 06d78d23..d0a0c089 100644 --- a/test/src/examples_multiple_subnets_per_az_test.go +++ b/test/src/examples_multiple_subnets_per_az_test.go @@ -40,15 +40,31 @@ func TestExamplesMultipleSubnetsPerAZ(t *testing.T) { // Run `terraform output` to get the value of an output variable privateSubnetCidrs := terraform.OutputList(t, terraformOptions, "private_subnet_cidrs") - expectedPrivateSubnetCidrs := []string{"172.16.0.0/19", "172.16.32.0/19"} + expectedPrivateSubnetCidrs := []string{"172.16.0.0/21", "172.16.8.0/21", "172.16.16.0/21", "172.16.24.0/21", "172.16.32.0/21", "172.16.40.0/21"} // Verify we're getting back the outputs we expect assert.Equal(t, expectedPrivateSubnetCidrs, privateSubnetCidrs) // Run `terraform output` to get the value of an output variable publicSubnetCidrs := terraform.OutputList(t, terraformOptions, "public_subnet_cidrs") - expectedPublicSubnetCidrs := []string{"172.16.96.0/19", "172.16.128.0/19"} + expectedPublicSubnetCidrs := []string{"172.16.72.0/21", "172.16.80.0/21", "172.16.88.0/21", "172.16.96.0/21", "172.16.104.0/21", "172.16.112.0/21"} // Verify we're getting back the outputs we expect assert.Equal(t, expectedPublicSubnetCidrs, publicSubnetCidrs) + + // Run `terraform output` to get the value of an output variable + namedPrivateSubnetsStatsMap := terraform.OutputMapOfObjects(t, terraformOptions, "named_private_subnets_stats_map") + // Verify we're getting back the outputs we expect + assert.Equal(t, len(namedPrivateSubnetsStatsMap), 3) + assert.Equal(t, len(namedPrivateSubnetsStatsMap["backend"].([]any)), 2) + assert.Equal(t, len(namedPrivateSubnetsStatsMap["services"].([]any)), 2) + assert.Equal(t, len(namedPrivateSubnetsStatsMap["db"].([]any)), 2) + + // Run `terraform output` to get the value of an output variable + namedPublicSubnetsStatsMap := terraform.OutputMapOfObjects(t, terraformOptions, "named_public_subnets_stats_map") + // Verify we're getting back the outputs we expect + assert.Equal(t, len(namedPublicSubnetsStatsMap), 3) + assert.Equal(t, len(namedPublicSubnetsStatsMap["backend"].([]any)), 2) + assert.Equal(t, len(namedPublicSubnetsStatsMap["services"].([]any)), 2) + assert.Equal(t, len(namedPublicSubnetsStatsMap["db"].([]any)), 2) } func TestExamplesMultipleSubnetsPerAZDisabled(t *testing.T) { From d8bb41a4b66f9f75129ae29de83c7398809d5079 Mon Sep 17 00:00:00 2001 From: aknysh Date: Tue, 24 Jan 2023 20:23:28 -0500 Subject: [PATCH 9/9] Multiple subnets per AZ --- test/src/examples_multiple_subnets_per_az_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/src/examples_multiple_subnets_per_az_test.go b/test/src/examples_multiple_subnets_per_az_test.go index d0a0c089..1f9fda42 100644 --- a/test/src/examples_multiple_subnets_per_az_test.go +++ b/test/src/examples_multiple_subnets_per_az_test.go @@ -54,17 +54,17 @@ func TestExamplesMultipleSubnetsPerAZ(t *testing.T) { namedPrivateSubnetsStatsMap := terraform.OutputMapOfObjects(t, terraformOptions, "named_private_subnets_stats_map") // Verify we're getting back the outputs we expect assert.Equal(t, len(namedPrivateSubnetsStatsMap), 3) - assert.Equal(t, len(namedPrivateSubnetsStatsMap["backend"].([]any)), 2) - assert.Equal(t, len(namedPrivateSubnetsStatsMap["services"].([]any)), 2) - assert.Equal(t, len(namedPrivateSubnetsStatsMap["db"].([]any)), 2) + assert.Equal(t, len(namedPrivateSubnetsStatsMap["backend"].([]map[string]any)), 2) + assert.Equal(t, len(namedPrivateSubnetsStatsMap["services"].([]map[string]any)), 2) + assert.Equal(t, len(namedPrivateSubnetsStatsMap["db"].([]map[string]any)), 2) // Run `terraform output` to get the value of an output variable namedPublicSubnetsStatsMap := terraform.OutputMapOfObjects(t, terraformOptions, "named_public_subnets_stats_map") // Verify we're getting back the outputs we expect assert.Equal(t, len(namedPublicSubnetsStatsMap), 3) - assert.Equal(t, len(namedPublicSubnetsStatsMap["backend"].([]any)), 2) - assert.Equal(t, len(namedPublicSubnetsStatsMap["services"].([]any)), 2) - assert.Equal(t, len(namedPublicSubnetsStatsMap["db"].([]any)), 2) + assert.Equal(t, len(namedPublicSubnetsStatsMap["backend"].([]map[string]any)), 2) + assert.Equal(t, len(namedPublicSubnetsStatsMap["services"].([]map[string]any)), 2) + assert.Equal(t, len(namedPublicSubnetsStatsMap["db"].([]map[string]any)), 2) } func TestExamplesMultipleSubnetsPerAZDisabled(t *testing.T) {