From d2379060deae173a60b8f16506a4433d9b2c561a Mon Sep 17 00:00:00 2001 From: Nuru Date: Sat, 26 Jun 2021 16:28:28 -0700 Subject: [PATCH 01/19] Update README --- README.md | 25 +++++++++++++++++-------- docs/terraform.md | 25 +++++++++++++++++-------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d5e4a4f..69f89ec 100644 --- a/README.md +++ b/README.md @@ -224,9 +224,13 @@ Available targets: | Name | Type | |------|------| +| [aws_security_group.cbd](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_security_group_rule.cidr](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_security_group_rule.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | -| [aws_security_group.external](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/security_group) | data source | +| [aws_security_group_rule.egress](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.self](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.sg](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | ## Inputs @@ -235,32 +239,37 @@ Available targets: | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional tags for appending to tags\_as\_list\_of\_maps. Not added to `tags`. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | 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,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | +| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior.
Note that changing this value will change the security group name. | `bool` | `false` | no | +| [create\_security\_group](#input\_create\_security\_group) | Set `true` to create a new security group. If false, `existing_security_group_id` must be provided. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [description](#input\_description) | The Security Group description. | `string` | `"Managed by Terraform"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [id](#input\_id) | The external Security Group ID to which Security Group rules will be assigned.
Required to set `security_group_enabled` to `false`. | `string` | `""` | no | +| [existing\_security\_group\_id](#input\_existing\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
Required if `security_group_enabled` is `false`, ignored otherwise. | `string` | `""` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | The letter case of output label values (also used in `tags` and `id`).
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Default value: `lower`. | `string` | `null` | no | | [name](#input\_name) | Solution name, e.g. 'app' or 'jenkins' | `string` | `null` | no | | [namespace](#input\_namespace) | Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp' | `string` | `null` | no | +| [open\_egress\_enabled](#input\_open\_egress\_enabled) | A convenience. Add to the rules in `var.rules` a rule that allows all egress.
If this is false and `var.rules` does not specify any egress rules, then
no egress will be allowed. | `bool` | `false` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [rules](#input\_rules) | A list of maps of Security Group rules.
The values of map is fully complated with `aws_security_group_rule` resource.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `null` | no | -| [security\_group\_enabled](#input\_security\_group\_enabled) | Whether to create Security Group. | `bool` | `true` | no | +| [rule\_matrix](#input\_rule\_matrix) | A convenience. Apply the same list of rules to all the provided security groups and CIDRs and self.
Type is object as specified in the default, but keys are optional except for `rules`.
The `rules` list is a list of maps that are fully compatible with the `aws_security_group_rule` resource,
but any keys already at the top level will be ignored. Rules keys listed in the default are required, except for `description`.
All elements of the list must have the same set of keys and each key must have a consistent value type.
Example:
{
source\_security\_group\_ids = []
cidr\_blocks= []
ipv6\_cidr\_blocks= []
prefix\_list\_ids = []
self = true
rules = [{
type = "egress"
from\_port = 0
to\_port = 65535
protocol = "all"
description = "Allow full egress"
}] | `any` |
{
"rules": []
}
| no | +| [rules](#input\_rules) | A list of maps of Security Group rules.
The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | +| [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the account.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | -| [use\_name\_prefix](#input\_use\_name\_prefix) | Whether to create a unique name beginning with the normalized prefix. | `bool` | `false` | no | | [vpc\_id](#input\_vpc\_id) | The VPC ID where Security Group will be created. | `string` | n/a | yes | ## Outputs | Name | Description | |------|-------------| -| [arn](#output\_arn) | The Security Group ARN | -| [id](#output\_id) | The Security Group ID | -| [name](#output\_name) | The Security Group Name | +| [arn](#output\_arn) | The created Security Group ARN | +| [id](#output\_id) | The created Security Group ID | +| [name](#output\_name) | The created Security Group Name | +| [rules](#output\_rules) | Details about all the security group rules created | +| [security\_group\_details](#output\_security\_group\_details) | Details about the security group created | diff --git a/docs/terraform.md b/docs/terraform.md index 0faa366..f45d42f 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -22,9 +22,13 @@ | Name | Type | |------|------| +| [aws_security_group.cbd](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_security_group_rule.cidr](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_security_group_rule.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | -| [aws_security_group.external](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/security_group) | data source | +| [aws_security_group_rule.egress](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.self](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.sg](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | ## Inputs @@ -33,30 +37,35 @@ | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional tags for appending to tags\_as\_list\_of\_maps. Not added to `tags`. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | 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,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | +| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior.
Note that changing this value will change the security group name. | `bool` | `false` | no | +| [create\_security\_group](#input\_create\_security\_group) | Set `true` to create a new security group. If false, `existing_security_group_id` must be provided. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [description](#input\_description) | The Security Group description. | `string` | `"Managed by Terraform"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [id](#input\_id) | The external Security Group ID to which Security Group rules will be assigned.
Required to set `security_group_enabled` to `false`. | `string` | `""` | no | +| [existing\_security\_group\_id](#input\_existing\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
Required if `security_group_enabled` is `false`, ignored otherwise. | `string` | `""` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | The letter case of output label values (also used in `tags` and `id`).
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Default value: `lower`. | `string` | `null` | no | | [name](#input\_name) | Solution name, e.g. 'app' or 'jenkins' | `string` | `null` | no | | [namespace](#input\_namespace) | Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp' | `string` | `null` | no | +| [open\_egress\_enabled](#input\_open\_egress\_enabled) | A convenience. Add to the rules in `var.rules` a rule that allows all egress.
If this is false and `var.rules` does not specify any egress rules, then
no egress will be allowed. | `bool` | `false` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [rules](#input\_rules) | A list of maps of Security Group rules.
The values of map is fully complated with `aws_security_group_rule` resource.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `null` | no | -| [security\_group\_enabled](#input\_security\_group\_enabled) | Whether to create Security Group. | `bool` | `true` | no | +| [rule\_matrix](#input\_rule\_matrix) | A convenience. Apply the same list of rules to all the provided security groups and CIDRs and self.
Type is object as specified in the default, but keys are optional except for `rules`.
The `rules` list is a list of maps that are fully compatible with the `aws_security_group_rule` resource,
but any keys already at the top level will be ignored. Rules keys listed in the default are required, except for `description`.
All elements of the list must have the same set of keys and each key must have a consistent value type.
Example:
{
source\_security\_group\_ids = []
cidr\_blocks= []
ipv6\_cidr\_blocks= []
prefix\_list\_ids = []
self = true
rules = [{
type = "egress"
from\_port = 0
to\_port = 65535
protocol = "all"
description = "Allow full egress"
}] | `any` |
{
"rules": []
}
| no | +| [rules](#input\_rules) | A list of maps of Security Group rules.
The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | +| [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the account.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | -| [use\_name\_prefix](#input\_use\_name\_prefix) | Whether to create a unique name beginning with the normalized prefix. | `bool` | `false` | no | | [vpc\_id](#input\_vpc\_id) | The VPC ID where Security Group will be created. | `string` | n/a | yes | ## Outputs | Name | Description | |------|-------------| -| [arn](#output\_arn) | The Security Group ARN | -| [id](#output\_id) | The Security Group ID | -| [name](#output\_name) | The Security Group Name | +| [arn](#output\_arn) | The created Security Group ARN | +| [id](#output\_id) | The created Security Group ID | +| [name](#output\_name) | The created Security Group Name | +| [rules](#output\_rules) | Details about all the security group rules created | +| [security\_group\_details](#output\_security\_group\_details) | Details about the security group created | From 43f81506b16af886c6230a1fd6a430805beaf1ba Mon Sep 17 00:00:00 2001 From: Nuru Date: Sat, 26 Jun 2021 16:28:13 -0700 Subject: [PATCH 02/19] Overhaul --- examples/complete/main.tf | 84 ++++++++++------------ examples/complete/outputs.tf | 39 ++++++---- examples/complete/versions.tf | 4 ++ main.tf | 110 ++++++++++++++++++++++++----- outputs.tf | 22 ++++-- test/src/examples_complete_test.go | 17 +++-- variables.tf | 83 +++++++++++++++++----- 7 files changed, 254 insertions(+), 105 deletions(-) diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 90aa4b7..d46071f 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -4,9 +4,9 @@ provider "aws" { module "vpc" { source = "cloudposse/vpc/aws" - version = "v0.18.2" + version = "v0.25.0" - cidr_block = "10.0.0.0/16" + cidr_block = "10.0.0.0/24" context = module.this.context } @@ -16,30 +16,32 @@ module "vpc" { module "new_security_group" { source = "../.." - vpc_id = module.vpc.vpc_id + vpc_id = module.vpc.vpc_id + open_egress_enabled = true + rule_matrix = { + # Allow ingress on ports 22 and 80 from created security grup, existing security group, and CIDR "10.0.0.0/8" + source_security_group_ids = [aws_security_group.existing.id] + cidr_blocks = ["10.0.0.0/8"] + prefix_list_ids = null + self = true + rules = [ + { + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + description = "Allow SSH access" + }, + { + type = "ingress" + from_port = 80 + to_port = 80 + protocol = "tcp" + description = "Allow HTTP access" + }, + ] + } rules = [ - { - type = "ingress" - from_port = 22 - to_port = 22 - protocol = "tcp" - cidr_blocks = [] - ipv6_cidr_blocks = null - source_security_group_id = aws_security_group.external.id - description = "Allow SSH access from the external SG" - self = false - }, - { - type = "ingress" - from_port = 22 - to_port = 22 - protocol = "tcp" - cidr_blocks = [] - ipv6_cidr_blocks = null - source_security_group_id = null - description = "Allow SSH access from our own SG" - self = true - }, { type = "ingress" from_port = 443 @@ -51,17 +53,6 @@ module "new_security_group" { description = null self = null }, - { - type = "egress" - from_port = 0 - to_port = 65535 - protocol = "all" - cidr_blocks = ["0.0.0.0/0"] - ipv6_cidr_blocks = null - source_security_group_id = null - description = "Allow all outbound traffic" - self = null - } ] context = module.this.context @@ -69,19 +60,19 @@ module "new_security_group" { # Create rules for pre-created security group -resource "aws_security_group" "external" { - name_prefix = format("%s-%s-", module.this.id, "external") +resource "aws_security_group" "existing" { + name_prefix = format("%s-%s-", module.this.id, "existing") vpc_id = module.vpc.vpc_id tags = module.this.tags } -module "external_security_group" { +module "existing_security_group" { source = "../.." - vpc_id = module.vpc.vpc_id - id = aws_security_group.external.id - rules = var.rules - security_group_enabled = false + vpc_id = module.vpc.vpc_id + existing_security_group_id = aws_security_group.existing.id + rules = var.rules + create_security_group = false context = module.this.context } @@ -91,9 +82,10 @@ module "external_security_group" { module "disabled_security_group" { source = "../.." - vpc_id = module.vpc.vpc_id - id = aws_security_group.external.id - rules = var.rules + vpc_id = module.vpc.vpc_id + existing_security_group_id = aws_security_group.existing.id + rules = var.rules + context = module.this.context enabled = false } diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf index 1044ee0..cdff431 100644 --- a/examples/complete/outputs.tf +++ b/examples/complete/outputs.tf @@ -13,32 +13,47 @@ output "new_sg_name" { value = module.new_security_group.name } -output "external_sg_id" { - description = "The external Security Group ID" - value = module.external_security_group.id +output "new_sg_details" { + description = "Details about the security group created" + value = module.new_security_group.security_group_details } -output "external_sg_arn" { - description = "The external Security Group ARN" - value = module.external_security_group.arn +output "new_sg_rules" { + description = "Details about all the security group rules created for the existing security group" + value = module.new_security_group.rules } -output "external_sg_name" { - description = "The external Security Group Name" - value = module.external_security_group.name +output "existing_sg_id" { + description = "The existing Security Group ID" + value = module.existing_security_group.id +} + +output "existing_sg_arn" { + description = "The existing Security Group ARN" + value = module.existing_security_group.arn +} + +output "existing_sg_name" { + description = "The existing Security Group Name" + value = module.existing_security_group.name +} + +output "existing_sg_rules" { + description = "Details about all the security group rules created for the existing security group" + value = module.existing_security_group.rules } output "disabled_sg_id" { description = "The disabled Security Group ID (should be empty)" - value = module.disabled_security_group.id + value = module.disabled_security_group.id == null ? "" : module.disabled_security_group.id } output "disabled_sg_arn" { description = "The disabled Security Group ARN (should be empty)" - value = module.disabled_security_group.arn + value = module.disabled_security_group.arn == null ? "" : module.disabled_security_group.arn } output "disabled_sg_name" { description = "The disabled Security Group Name (should be empty)" - value = module.disabled_security_group.name + value = module.disabled_security_group.name == null ? "" : module.disabled_security_group.name } diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index 5b2c49b..3b49b80 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -6,5 +6,9 @@ terraform { source = "hashicorp/aws" version = ">= 2.0" } + random = { + source = "hashicorp/random" + version = ">= 3.0" + } } } diff --git a/main.tf b/main.tf index 30d9a02..4ea9e14 100644 --- a/main.tf +++ b/main.tf @@ -1,12 +1,18 @@ locals { - security_group_enabled = module.this.enabled && var.security_group_enabled - is_external = module.this.enabled && var.security_group_enabled == false - use_name = var.use_name_prefix ? null : module.this.id - use_name_prefix = var.use_name_prefix ? format("%s%s", module.this.id, module.this.delimiter) : null - id = local.is_external ? join("", data.aws_security_group.external.*.id) : join("", aws_security_group.default.*.id) - arn = local.is_external ? join("", data.aws_security_group.external.*.arn) : join("", aws_security_group.default.*.arn) - name = local.is_external ? join("", data.aws_security_group.external.*.name) : join("", aws_security_group.default.*.name) - rules = module.this.enabled && var.rules != null ? { + enabled = module.this.enabled + # Because Terraform formatting for `not` (!) changes between versions 0.13 and 0.14, use == false instead + create_security_group = local.enabled && var.create_security_group + lookup_security_group = local.enabled && var.create_security_group == false + created_security_group = local.create_security_group ? ( + var.create_before_destroy ? aws_security_group.cbd[0] : aws_security_group.default[0] + ) : null + security_group_id = local.enabled ? ( + # Use coalesce() here to hack an error message into the output + var.create_security_group ? local.created_security_group.id : coalesce(var.existing_security_group_id, + "use_existing_security_group is true, but no ID supplied ") + ) : null + + rules = local.enabled && var.rules != null ? { for indx, rule in flatten(var.rules) : format("%v-%v-%v-%v-%s", rule.type, @@ -16,22 +22,31 @@ locals { try(rule["description"], null) == null ? md5(format("Managed by Terraform #%d", indx)) : md5(rule.description) ) => rule } : {} -} -data "aws_security_group" "external" { - count = local.is_external ? 1 : 0 - id = var.id - vpc_id = var.vpc_id + rule_matrix_rule_count = try(length(var.rule_matrix.rules), 0) + rule_matrix_enabled = local.enabled && local.rule_matrix_rule_count > 0 } +# You cannot toggle `create_before_destroy` based on input, +# you have to have a completely separate resource to change it. resource "aws_security_group" "default" { - count = local.security_group_enabled && local.is_external == false ? 1 : 0 + # Because Terraform formatting for `not` (!) changes between versions 0.13 and 0.14, use == false instead + count = local.create_security_group && var.create_before_destroy == false ? 1 : 0 - name = local.use_name - name_prefix = local.use_name_prefix + name = coalesce(var.security_group_name, module.this.id) description = var.description vpc_id = var.vpc_id - tags = module.this.tags + tags = merge(module.this.tags, length(var.security_group_name) > 0 ? { Name = var.security_group_name } : {}) +} + +resource "aws_security_group" "cbd" { + # Because we use `== false` in the other resource, use `== true` for symmetry + count = local.create_security_group && var.create_before_destroy == true ? 1 : 0 + + name_prefix = coalesce(var.security_group_name, format("%s%s", module.this.id, module.this.delimiter)) + description = var.description + vpc_id = var.vpc_id + tags = merge(module.this.tags, length(var.security_group_name) > 0 ? { Name = var.security_group_name } : {}) lifecycle { create_before_destroy = true @@ -41,7 +56,7 @@ resource "aws_security_group" "default" { resource "aws_security_group_rule" "default" { for_each = local.rules - security_group_id = local.id + security_group_id = local.security_group_id type = each.value.type from_port = each.value.from_port to_port = each.value.to_port @@ -55,3 +70,62 @@ resource "aws_security_group_rule" "default" { source_security_group_id = lookup(each.value, "source_security_group_id", null) } + +resource "aws_security_group_rule" "self" { + # We use "== true" here because you cannot use `null` as a conditional + count = local.rule_matrix_enabled && try(var.rule_matrix.self, null) == true ? local.rule_matrix_rule_count : 0 + + security_group_id = local.security_group_id + type = var.rule_matrix.rules[count.index].type + from_port = var.rule_matrix.rules[count.index].from_port + to_port = var.rule_matrix.rules[count.index].to_port + protocol = var.rule_matrix.rules[count.index].protocol + description = try(var.rule_matrix.rules[count.index].description, "Managed by Terraform") + + self = var.rule_matrix.self +} + +resource "aws_security_group_rule" "sg" { + # source_security_group_ids may be unknown at plan time and there is no valid proxy for them, + # so there is no point in trying to come up with a static string key to use with for_each. + count = local.rule_matrix_enabled ? length(var.rule_matrix.source_security_group_ids) * local.rule_matrix_rule_count : 0 + + security_group_id = local.security_group_id + type = var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].type + from_port = var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].from_port + to_port = var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].to_port + protocol = var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].protocol + description = try(var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].description, "Managed by Terraform") + + source_security_group_id = var.rule_matrix.source_security_group_ids[floor(count.index / local.rule_matrix_rule_count)] +} + +resource "aws_security_group_rule" "cidr" { + count = local.rule_matrix_enabled && (try(length(var.rule_matrix.cidr_blocks), 0) + + try(length(var.rule_matrix.ipv6_cidr_blocks), 0) + + try(length(var.rule_matrix.prefix_list_ids), 0)) > 0 ? local.rule_matrix_rule_count : 0 + + security_group_id = local.security_group_id + type = "ingress" + cidr_blocks = try(length(var.rule_matrix.cidr_blocks), 0) > 0 ? var.rule_matrix.cidr_blocks : null + ipv6_cidr_blocks = try(length(var.rule_matrix.ipv6_cidr_blocks), 0) > 0 ? var.rule_matrix.ipv6_cidr_blocks : null + prefix_list_ids = try(length(var.rule_matrix.prefix_list_ids), 0) > 0 ? var.rule_matrix.prefix_list_ids : null + from_port = var.rule_matrix.rules[count.index].from_port + to_port = var.rule_matrix.rules[count.index].to_port + protocol = var.rule_matrix.rules[count.index].protocol + description = try(var.rule_matrix.rules[count.index].description, "Managed by Terraform") +} + + +resource "aws_security_group_rule" "egress" { + count = local.enabled && var.open_egress_enabled ? 1 : 0 + + security_group_id = local.security_group_id + type = "egress" + from_port = 0 + to_port = 65535 + protocol = "all" + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + description = "Allow all egress" +} diff --git a/outputs.tf b/outputs.tf index 8f9c7f2..c1152cc 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,14 +1,24 @@ output "id" { - description = "The Security Group ID" - value = try(local.id, null) + description = "The created Security Group ID" + value = try(local.created_security_group.id, null) } output "arn" { - description = "The Security Group ARN" - value = try(local.arn, null) + description = "The created Security Group ARN" + value = try(local.created_security_group.arn, null) } output "name" { - description = "The Security Group Name" - value = try(local.name, null) + description = "The created Security Group Name" + value = try(local.created_security_group.name, null) +} + +output "security_group_details" { + description = "Details about the security group created" + value = var.create_before_destroy ? aws_security_group.cbd : aws_security_group.default +} + +output "rules" { + description = "Details about all the security group rules created" + value = concat(try(values(aws_security_group_rule.default), []), aws_security_group_rule.sg, aws_security_group_rule.cidr, aws_security_group_rule.self, aws_security_group_rule.egress) } diff --git a/test/src/examples_complete_test.go b/test/src/examples_complete_test.go index b92023d..f8ba7d6 100644 --- a/test/src/examples_complete_test.go +++ b/test/src/examples_complete_test.go @@ -48,14 +48,17 @@ func TestExamplesComplete(t *testing.T) { assert.Contains(t, newSgARN, "arn:aws:ec2", "SG ID should contains substring 'arn:aws:ec2'") assert.Equal(t, "eg-ue2-test-sg-"+randID, newSgName) - // Verify that outputs are valid when `security_group_enabled=false` and `sg_id` set to external SG ID - externalSgID := terraform.Output(t, terraformOptions, "external_sg_id") - externalSgARN := terraform.Output(t, terraformOptions, "external_sg_arn") - externalSgName := terraform.Output(t, terraformOptions, "external_sg_name") +/* + Module used to output SG information for existing security groups, but no longer does + // Verify that outputs are valid when `security_group_enabled=false` and `sg_id` set to existing SG ID + existingSgID := terraform.Output(t, terraformOptions, "existing_sg_id") + existingSgARN := terraform.Output(t, terraformOptions, "existing_sg_arn") + existingSgName := terraform.Output(t, terraformOptions, "existing_sg_name") - assert.Contains(t, externalSgID, "sg-", "SG ID should contains substring 'sg-'") - assert.Contains(t, externalSgARN, "arn:aws:ec2", "SG ID should contains substring 'arn:aws:ec2'") - assert.Contains(t, externalSgName, "eg-ue2-test-sg-"+randID) + assert.Contains(t, existingSgID, "sg-", "SG ID should contains substring 'sg-'") + assert.Contains(t, existingSgARN, "arn:aws:ec2", "SG ID should contains substring 'arn:aws:ec2'") + assert.Contains(t, existingSgName, "eg-ue2-test-sg-"+randID) +*/ // Verify that outputs are empty when module is disabled disabledSgID := terraform.Output(t, terraformOptions, "disabled_sg_id") diff --git a/variables.tf b/variables.tf index 3d5572c..3d69e49 100644 --- a/variables.tf +++ b/variables.tf @@ -3,16 +3,39 @@ variable "vpc_id" { description = "The VPC ID where Security Group will be created." } -variable "security_group_enabled" { +variable "security_group_name" { + type = string + default = "" + description = <<-EOT + The name to assign to the security group. Must be unique within the account. + If not provided, will be derived from the `null-label.context` passed in. + If `create_before_destroy` is true, will be used as a name prefix. + EOT +} + +variable "create_security_group" { type = bool default = true - description = "Whether to create Security Group." + description = "Set `true` to create a new security group. If false, `existing_security_group_id` must be provided." +} + +variable "existing_security_group_id" { + type = string + default = "" + description = <<-EOT + The ID of an existing Security Group to which Security Group rules will be assigned. + Required if `security_group_enabled` is `false`, ignored otherwise. + EOT } -variable "use_name_prefix" { + +variable "create_before_destroy" { type = bool default = false - description = "Whether to create a unique name beginning with the normalized prefix." + description = <<-EOT + Set `true` to enable terraform `create_before_destroy` behavior. + Note that changing this value will change the security group name. + EOT } variable "description" { @@ -21,21 +44,49 @@ variable "description" { description = "The Security Group description." } -variable "id" { - type = string - default = "" - description = <<-EOT - The external Security Group ID to which Security Group rules will be assigned. - Required to set `security_group_enabled` to `false`. - EOT -} - variable "rules" { type = list(any) - default = null + default = [] description = <<-EOT - A list of maps of Security Group rules. - The values of map is fully complated with `aws_security_group_rule` resource. + A list of maps of Security Group rules. + The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except + for `security_group_id` which will be ignored. To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . EOT } + +variable "open_egress_enabled" { + type = bool + default = false + description = <<-EOT + A convenience. Add to the rules in `var.rules` a rule that allows all egress. + If this is false and `var.rules` does not specify any egress rules, then + no egress will be allowed. + EOT +} + +variable "rule_matrix" { + type = any + default = { rules = [] } + description = <<-EOT + A convenience. Apply the same list of rules to all the provided security groups and CIDRs and self. + Type is object as specified in the default, but keys are optional except for `rules`. + The `rules` list is a list of maps that are fully compatible with the `aws_security_group_rule` resource, + but any keys already at the top level will be ignored. Rules keys listed in the default are required, except for `description`. + All elements of the list must have the same set of keys and each key must have a consistent value type. + Example: + { + source_security_group_ids = [] + cidr_blocks= [] + ipv6_cidr_blocks= [] + prefix_list_ids = [] + self = true + rules = [{ + type = "egress" + from_port = 0 + to_port = 65535 + protocol = "all" + description = "Allow full egress" + }] + EOT +} From ed5446cf214817ee7d9c20ad786a71d0e31a64a3 Mon Sep 17 00:00:00 2001 From: Nuru Date: Sun, 27 Jun 2021 18:38:24 -0700 Subject: [PATCH 03/19] Address reviewer comments --- examples/complete/main.tf | 2 +- examples/complete/outputs.tf | 12 ++++---- main.tf | 4 +-- test/src/examples_complete_test.go | 6 ++-- variables.tf | 44 ++++++++++++++++-------------- 5 files changed, 35 insertions(+), 33 deletions(-) diff --git a/examples/complete/main.tf b/examples/complete/main.tf index d46071f..e174904 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -17,7 +17,7 @@ module "new_security_group" { source = "../.." vpc_id = module.vpc.vpc_id - open_egress_enabled = true + allow_all_egress = true rule_matrix = { # Allow ingress on ports 22 and 80 from created security grup, existing security group, and CIDR "10.0.0.0/8" source_security_group_ids = [aws_security_group.existing.id] diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf index cdff431..5b268d4 100644 --- a/examples/complete/outputs.tf +++ b/examples/complete/outputs.tf @@ -1,25 +1,25 @@ -output "new_sg_id" { +output "created_sg_id" { description = "The new one Security Group ID" value = module.new_security_group.id } -output "new_sg_arn" { +output "created_sg_arn" { description = "The new one Security Group ARN" value = module.new_security_group.arn } -output "new_sg_name" { +output "created_sg_name" { description = "The new one Security Group Name" value = module.new_security_group.name } -output "new_sg_details" { +output "created_sg_details" { description = "Details about the security group created" value = module.new_security_group.security_group_details } -output "new_sg_rules" { - description = "Details about all the security group rules created for the existing security group" +output "created_sg_rules" { + description = "Details about all the security group rules created for the created security group" value = module.new_security_group.rules } diff --git a/main.tf b/main.tf index 4ea9e14..1c9876a 100644 --- a/main.tf +++ b/main.tf @@ -9,7 +9,7 @@ locals { security_group_id = local.enabled ? ( # Use coalesce() here to hack an error message into the output var.create_security_group ? local.created_security_group.id : coalesce(var.existing_security_group_id, - "use_existing_security_group is true, but no ID supplied ") + "`create_security_group` is false, but no ID was supplied ") ) : null rules = local.enabled && var.rules != null ? { @@ -118,7 +118,7 @@ resource "aws_security_group_rule" "cidr" { resource "aws_security_group_rule" "egress" { - count = local.enabled && var.open_egress_enabled ? 1 : 0 + count = local.enabled && var.allow_all_egress ? 1 : 0 security_group_id = local.security_group_id type = "egress" diff --git a/test/src/examples_complete_test.go b/test/src/examples_complete_test.go index f8ba7d6..4948d6d 100644 --- a/test/src/examples_complete_test.go +++ b/test/src/examples_complete_test.go @@ -40,9 +40,9 @@ func TestExamplesComplete(t *testing.T) { // Run `terraform output` to get the value of an output variable // Verify that outputs are valid when `security_group_enabled=true` - newSgID := terraform.Output(t, terraformOptions, "new_sg_id") - newSgARN := terraform.Output(t, terraformOptions, "new_sg_arn") - newSgName := terraform.Output(t, terraformOptions, "new_sg_name") + newSgID := terraform.Output(t, terraformOptions, "created_sg_id") + newSgARN := terraform.Output(t, terraformOptions, "created_sg_arn") + newSgName := terraform.Output(t, terraformOptions, "created_sg_name") assert.Contains(t, newSgID, "sg-", "SG ID should contains substring 'sg-'") assert.Contains(t, newSgARN, "arn:aws:ec2", "SG ID should contains substring 'arn:aws:ec2'") diff --git a/variables.tf b/variables.tf index 3d69e49..af06d63 100644 --- a/variables.tf +++ b/variables.tf @@ -55,38 +55,40 @@ variable "rules" { EOT } -variable "open_egress_enabled" { +variable "allow_all_egress" { type = bool default = false description = <<-EOT - A convenience. Add to the rules in `var.rules` a rule that allows all egress. + A convenience that adds to the rules in `var.rules` a rule that allows all egress. If this is false and `var.rules` does not specify any egress rules, then no egress will be allowed. EOT } variable "rule_matrix" { + # rule_matrix is independent of the `rules` input. + # Only the rules specified in the `rule_matrix` object are applied to the subjects. + # Schema: + # { + # # these top level lists define all the subjects to which rule_matrix rules will be applied + # source_security_group_ids = list of source security group IDs to apply all rules to + # cidr_blocks = list of ipv4 CIDR blocks to apply all rules to + # ipv6_cidr_blocks= list of ipv6 CIDR blocks to apply all rules to + # prefix_list_ids = list of prefix list IDs to apply all rules to + # self = # set "true" to apply the rules to the created or existing security group + # + # # each rule in the rules list will be applied to every subject defined above + # rules = [{ + # type = "egress" + # from_port = 0 + # to_port = 65535 + # protocol = "all" + # description = "Allow full egress" + # }] + type = any default = { rules = [] } description = <<-EOT - A convenience. Apply the same list of rules to all the provided security groups and CIDRs and self. - Type is object as specified in the default, but keys are optional except for `rules`. - The `rules` list is a list of maps that are fully compatible with the `aws_security_group_rule` resource, - but any keys already at the top level will be ignored. Rules keys listed in the default are required, except for `description`. - All elements of the list must have the same set of keys and each key must have a consistent value type. - Example: - { - source_security_group_ids = [] - cidr_blocks= [] - ipv6_cidr_blocks= [] - prefix_list_ids = [] - self = true - rules = [{ - type = "egress" - from_port = 0 - to_port = 65535 - protocol = "all" - description = "Allow full egress" - }] + A convenient way to apply the same set of rules to a set of subjects. See README for details. EOT } From d9551c8ac9b219d2bb172ac4a477b99e4711d7b1 Mon Sep 17 00:00:00 2001 From: cloudpossebot <11232728+cloudpossebot@users.noreply.github.com> Date: Mon, 28 Jun 2021 01:39:23 +0000 Subject: [PATCH 04/19] Abandon for_each, use count instead --- README.md | 9 +++---- docs/terraform.md | 9 +++---- examples/complete/main.tf | 2 +- main.tf | 33 ++++++++++---------------- test/src/examples_complete_test.go | 5 ++++ variables.tf | 38 +++++++++++++++--------------- 6 files changed, 47 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 69f89ec..84731cd 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,7 @@ Available targets: | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional tags for appending to tags\_as\_list\_of\_maps. Not added to `tags`. | `map(string)` | `{}` | no | +| [allow\_all\_egress](#input\_allow\_all\_egress) | A convenience that adds to the rules in `var.rules` a rule that allows all egress.
If this is false and `var.rules` does not specify any egress rules, then
no egress will be allowed. | `bool` | `false` | no | | [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | 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,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | | [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior.
Note that changing this value will change the security group name. | `bool` | `false` | no | @@ -252,13 +253,13 @@ Available targets: | [label\_value\_case](#input\_label\_value\_case) | The letter case of output label values (also used in `tags` and `id`).
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Default value: `lower`. | `string` | `null` | no | | [name](#input\_name) | Solution name, e.g. 'app' or 'jenkins' | `string` | `null` | no | | [namespace](#input\_namespace) | Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp' | `string` | `null` | no | -| [open\_egress\_enabled](#input\_open\_egress\_enabled) | A convenience. Add to the rules in `var.rules` a rule that allows all egress.
If this is false and `var.rules` does not specify any egress rules, then
no egress will be allowed. | `bool` | `false` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [rule\_matrix](#input\_rule\_matrix) | A convenience. Apply the same list of rules to all the provided security groups and CIDRs and self.
Type is object as specified in the default, but keys are optional except for `rules`.
The `rules` list is a list of maps that are fully compatible with the `aws_security_group_rule` resource,
but any keys already at the top level will be ignored. Rules keys listed in the default are required, except for `description`.
All elements of the list must have the same set of keys and each key must have a consistent value type.
Example:
{
source\_security\_group\_ids = []
cidr\_blocks= []
ipv6\_cidr\_blocks= []
prefix\_list\_ids = []
self = true
rules = [{
type = "egress"
from\_port = 0
to\_port = 65535
protocol = "all"
description = "Allow full egress"
}] | `any` |
{
"rules": []
}
| no | +| [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` |
{
"rules": []
}
| no | | [rules](#input\_rules) | A list of maps of Security Group rules.
The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | | [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the account.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | +| [unstable\_output\_enabled](#input\_unstable\_output\_enabled) | Some outputs are unstable, meaning that they can show a change even when no resource changes are made.
These outputs are suppressed by default. Set `unstable_output_enabled` to enable them. | `bool` | `false` | no | | [vpc\_id](#input\_vpc\_id) | The VPC ID where Security Group will be created. | `string` | n/a | yes | ## Outputs @@ -268,8 +269,8 @@ Available targets: | [arn](#output\_arn) | The created Security Group ARN | | [id](#output\_id) | The created Security Group ID | | [name](#output\_name) | The created Security Group Name | -| [rules](#output\_rules) | Details about all the security group rules created | -| [security\_group\_details](#output\_security\_group\_details) | Details about the security group created | +| [rules](#output\_rules) | (UNSTABLE) Details about all the security group rules created | +| [security\_group\_details](#output\_security\_group\_details) | (UNSTABLE) Details about the security group created | diff --git a/docs/terraform.md b/docs/terraform.md index f45d42f..c803948 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -35,6 +35,7 @@ | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional tags for appending to tags\_as\_list\_of\_maps. Not added to `tags`. | `map(string)` | `{}` | no | +| [allow\_all\_egress](#input\_allow\_all\_egress) | A convenience that adds to the rules in `var.rules` a rule that allows all egress.
If this is false and `var.rules` does not specify any egress rules, then
no egress will be allowed. | `bool` | `false` | no | | [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | 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,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | | [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior.
Note that changing this value will change the security group name. | `bool` | `false` | no | @@ -50,13 +51,13 @@ | [label\_value\_case](#input\_label\_value\_case) | The letter case of output label values (also used in `tags` and `id`).
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Default value: `lower`. | `string` | `null` | no | | [name](#input\_name) | Solution name, e.g. 'app' or 'jenkins' | `string` | `null` | no | | [namespace](#input\_namespace) | Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp' | `string` | `null` | no | -| [open\_egress\_enabled](#input\_open\_egress\_enabled) | A convenience. Add to the rules in `var.rules` a rule that allows all egress.
If this is false and `var.rules` does not specify any egress rules, then
no egress will be allowed. | `bool` | `false` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [rule\_matrix](#input\_rule\_matrix) | A convenience. Apply the same list of rules to all the provided security groups and CIDRs and self.
Type is object as specified in the default, but keys are optional except for `rules`.
The `rules` list is a list of maps that are fully compatible with the `aws_security_group_rule` resource,
but any keys already at the top level will be ignored. Rules keys listed in the default are required, except for `description`.
All elements of the list must have the same set of keys and each key must have a consistent value type.
Example:
{
source\_security\_group\_ids = []
cidr\_blocks= []
ipv6\_cidr\_blocks= []
prefix\_list\_ids = []
self = true
rules = [{
type = "egress"
from\_port = 0
to\_port = 65535
protocol = "all"
description = "Allow full egress"
}] | `any` |
{
"rules": []
}
| no | +| [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` |
{
"rules": []
}
| no | | [rules](#input\_rules) | A list of maps of Security Group rules.
The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | | [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the account.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | +| [unstable\_output\_enabled](#input\_unstable\_output\_enabled) | Some outputs are unstable, meaning that they can show a change even when no resource changes are made.
These outputs are suppressed by default. Set `unstable_output_enabled` to enable them. | `bool` | `false` | no | | [vpc\_id](#input\_vpc\_id) | The VPC ID where Security Group will be created. | `string` | n/a | yes | ## Outputs @@ -66,6 +67,6 @@ | [arn](#output\_arn) | The created Security Group ARN | | [id](#output\_id) | The created Security Group ID | | [name](#output\_name) | The created Security Group Name | -| [rules](#output\_rules) | Details about all the security group rules created | -| [security\_group\_details](#output\_security\_group\_details) | Details about the security group created | +| [rules](#output\_rules) | (UNSTABLE) Details about all the security group rules created | +| [security\_group\_details](#output\_security\_group\_details) | (UNSTABLE) Details about the security group created | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index e174904..4c065eb 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -16,7 +16,7 @@ module "vpc" { module "new_security_group" { source = "../.." - vpc_id = module.vpc.vpc_id + vpc_id = module.vpc.vpc_id allow_all_egress = true rule_matrix = { # Allow ingress on ports 22 and 80 from created security grup, existing security group, and CIDR "10.0.0.0/8" diff --git a/main.tf b/main.tf index 1c9876a..7443a32 100644 --- a/main.tf +++ b/main.tf @@ -12,16 +12,7 @@ locals { "`create_security_group` is false, but no ID was supplied ") ) : null - rules = local.enabled && var.rules != null ? { - for indx, rule in flatten(var.rules) : - format("%v-%v-%v-%v-%s", - rule.type, - rule.protocol, - rule.from_port, - rule.to_port, - try(rule["description"], null) == null ? md5(format("Managed by Terraform #%d", indx)) : md5(rule.description) - ) => rule - } : {} + rules = local.enabled && var.rules != null ? var.rules : [] rule_matrix_rule_count = try(length(var.rule_matrix.rules), 0) rule_matrix_enabled = local.enabled && local.rule_matrix_rule_count > 0 @@ -54,21 +45,21 @@ resource "aws_security_group" "cbd" { } resource "aws_security_group_rule" "default" { - for_each = local.rules + count = length(local.rules) security_group_id = local.security_group_id - type = each.value.type - from_port = each.value.from_port - to_port = each.value.to_port - protocol = each.value.protocol - description = lookup(each.value, "description", "Managed by Terraform") + type = local.rules[count.index].type + from_port = local.rules[count.index].from_port + to_port = local.rules[count.index].to_port + protocol = local.rules[count.index].protocol + description = lookup(local.rules[count.index], "description", "Managed by Terraform") # Convert any of a missing key, a value of null, or a value of empty list to null - cidr_blocks = try(length(lookup(each.value, "cidr_blocks", [])), 0) > 0 ? each.value["cidr_blocks"] : null - ipv6_cidr_blocks = try(length(lookup(each.value, "ipv6_cidr_blocks", [])), 0) > 0 ? each.value["ipv6_cidr_blocks"] : null - prefix_list_ids = try(length(lookup(each.value, "prefix_list_ids", [])), 0) > 0 ? each.value["prefix_list_ids"] : null - self = coalesce(lookup(each.value, "self", null), false) ? true : null + cidr_blocks = try(length(lookup(local.rules[count.index], "cidr_blocks", [])), 0) > 0 ? local.rules[count.index]["cidr_blocks"] : null + ipv6_cidr_blocks = try(length(lookup(local.rules[count.index], "ipv6_cidr_blocks", [])), 0) > 0 ? local.rules[count.index]["ipv6_cidr_blocks"] : null + prefix_list_ids = try(length(lookup(local.rules[count.index], "prefix_list_ids", [])), 0) > 0 ? local.rules[count.index]["prefix_list_ids"] : null + self = coalesce(lookup(local.rules[count.index], "self", null), false) ? true : null - source_security_group_id = lookup(each.value, "source_security_group_id", null) + source_security_group_id = lookup(local.rules[count.index], "source_security_group_id", null) } resource "aws_security_group_rule" "self" { diff --git a/test/src/examples_complete_test.go b/test/src/examples_complete_test.go index 4948d6d..d081088 100644 --- a/test/src/examples_complete_test.go +++ b/test/src/examples_complete_test.go @@ -34,6 +34,11 @@ func TestExamplesComplete(t *testing.T) { // At the end of the test, run `terraform destroy` to clean up any resources that were created defer terraform.Destroy(t, terraformOptions) + // If Go runtime crushes, run `terraform destroy` to clean up any resources that were created + defer runtime.HandleCrash(func(i interface{}) { + terraform.Destroy(t, terraformOptions) + }) + // This will run `terraform init` and `terraform apply` and fail the test if there are any errors terraform.InitAndApply(t, terraformOptions) diff --git a/variables.tf b/variables.tf index af06d63..f17ec51 100644 --- a/variables.tf +++ b/variables.tf @@ -66,25 +66,25 @@ variable "allow_all_egress" { } variable "rule_matrix" { - # rule_matrix is independent of the `rules` input. - # Only the rules specified in the `rule_matrix` object are applied to the subjects. - # Schema: - # { - # # these top level lists define all the subjects to which rule_matrix rules will be applied - # source_security_group_ids = list of source security group IDs to apply all rules to - # cidr_blocks = list of ipv4 CIDR blocks to apply all rules to - # ipv6_cidr_blocks= list of ipv6 CIDR blocks to apply all rules to - # prefix_list_ids = list of prefix list IDs to apply all rules to - # self = # set "true" to apply the rules to the created or existing security group - # - # # each rule in the rules list will be applied to every subject defined above - # rules = [{ - # type = "egress" - # from_port = 0 - # to_port = 65535 - # protocol = "all" - # description = "Allow full egress" - # }] + # rule_matrix is independent of the `rules` input. + # Only the rules specified in the `rule_matrix` object are applied to the subjects. + # Schema: + # { + # # these top level lists define all the subjects to which rule_matrix rules will be applied + # source_security_group_ids = list of source security group IDs to apply all rules to + # cidr_blocks = list of ipv4 CIDR blocks to apply all rules to + # ipv6_cidr_blocks= list of ipv6 CIDR blocks to apply all rules to + # prefix_list_ids = list of prefix list IDs to apply all rules to + # self = # set "true" to apply the rules to the created or existing security group + # + # # each rule in the rules list will be applied to every subject defined above + # rules = [{ + # type = "egress" + # from_port = 0 + # to_port = 65535 + # protocol = "all" + # description = "Allow full egress" + # }] type = any default = { rules = [] } From 9cfe55a4572eaff6a89eefa760628e684665dda2 Mon Sep 17 00:00:00 2001 From: Nuru Date: Sun, 27 Jun 2021 21:39:08 -0700 Subject: [PATCH 05/19] Suppress unstable outputs by default --- outputs.tf | 8 ++++---- variables.tf | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/outputs.tf b/outputs.tf index c1152cc..1b30d37 100644 --- a/outputs.tf +++ b/outputs.tf @@ -14,11 +14,11 @@ output "name" { } output "security_group_details" { - description = "Details about the security group created" - value = var.create_before_destroy ? aws_security_group.cbd : aws_security_group.default + description = "(UNSTABLE) Details about the security group created" + value = var.unstable_output_enabled ? (var.create_before_destroy ? aws_security_group.cbd : aws_security_group.default) : null } output "rules" { - description = "Details about all the security group rules created" - value = concat(try(values(aws_security_group_rule.default), []), aws_security_group_rule.sg, aws_security_group_rule.cidr, aws_security_group_rule.self, aws_security_group_rule.egress) + description = "(UNSTABLE) Details about all the security group rules created" + value = var.unstable_output_enabled ? concat(try(values(aws_security_group_rule.default), []), aws_security_group_rule.sg, aws_security_group_rule.cidr, aws_security_group_rule.self, aws_security_group_rule.egress) : null } diff --git a/variables.tf b/variables.tf index f17ec51..530bc95 100644 --- a/variables.tf +++ b/variables.tf @@ -92,3 +92,12 @@ variable "rule_matrix" { A convenient way to apply the same set of rules to a set of subjects. See README for details. EOT } + +variable "unstable_output_enabled" { + type = bool + default = false + description = <<-EOT + Some outputs are unstable, meaning that they can show a change even when no resource changes are made. + These outputs are suppressed by default. Set `unstable_output_enabled` to enable them. + EOT +} From 7f09d26d94bfe2e0bbb4784a910d271a28cca76f Mon Sep 17 00:00:00 2001 From: Nuru Date: Sun, 27 Jun 2021 22:00:19 -0700 Subject: [PATCH 06/19] Remove unstable outputs --- README.md | 3 --- docs/terraform.md | 3 --- examples/complete/outputs.tf | 15 --------------- outputs.tf | 10 ---------- variables.tf | 9 --------- 5 files changed, 40 deletions(-) diff --git a/README.md b/README.md index 84731cd..79040c4 100644 --- a/README.md +++ b/README.md @@ -259,7 +259,6 @@ Available targets: | [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the account.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | -| [unstable\_output\_enabled](#input\_unstable\_output\_enabled) | Some outputs are unstable, meaning that they can show a change even when no resource changes are made.
These outputs are suppressed by default. Set `unstable_output_enabled` to enable them. | `bool` | `false` | no | | [vpc\_id](#input\_vpc\_id) | The VPC ID where Security Group will be created. | `string` | n/a | yes | ## Outputs @@ -269,8 +268,6 @@ Available targets: | [arn](#output\_arn) | The created Security Group ARN | | [id](#output\_id) | The created Security Group ID | | [name](#output\_name) | The created Security Group Name | -| [rules](#output\_rules) | (UNSTABLE) Details about all the security group rules created | -| [security\_group\_details](#output\_security\_group\_details) | (UNSTABLE) Details about the security group created | diff --git a/docs/terraform.md b/docs/terraform.md index c803948..900022d 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -57,7 +57,6 @@ | [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the account.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | -| [unstable\_output\_enabled](#input\_unstable\_output\_enabled) | Some outputs are unstable, meaning that they can show a change even when no resource changes are made.
These outputs are suppressed by default. Set `unstable_output_enabled` to enable them. | `bool` | `false` | no | | [vpc\_id](#input\_vpc\_id) | The VPC ID where Security Group will be created. | `string` | n/a | yes | ## Outputs @@ -67,6 +66,4 @@ | [arn](#output\_arn) | The created Security Group ARN | | [id](#output\_id) | The created Security Group ID | | [name](#output\_name) | The created Security Group Name | -| [rules](#output\_rules) | (UNSTABLE) Details about all the security group rules created | -| [security\_group\_details](#output\_security\_group\_details) | (UNSTABLE) Details about the security group created | diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf index 5b268d4..e0fd779 100644 --- a/examples/complete/outputs.tf +++ b/examples/complete/outputs.tf @@ -13,16 +13,6 @@ output "created_sg_name" { value = module.new_security_group.name } -output "created_sg_details" { - description = "Details about the security group created" - value = module.new_security_group.security_group_details -} - -output "created_sg_rules" { - description = "Details about all the security group rules created for the created security group" - value = module.new_security_group.rules -} - output "existing_sg_id" { description = "The existing Security Group ID" value = module.existing_security_group.id @@ -38,11 +28,6 @@ output "existing_sg_name" { value = module.existing_security_group.name } -output "existing_sg_rules" { - description = "Details about all the security group rules created for the existing security group" - value = module.existing_security_group.rules -} - output "disabled_sg_id" { description = "The disabled Security Group ID (should be empty)" value = module.disabled_security_group.id == null ? "" : module.disabled_security_group.id diff --git a/outputs.tf b/outputs.tf index 1b30d37..77c5c40 100644 --- a/outputs.tf +++ b/outputs.tf @@ -12,13 +12,3 @@ output "name" { description = "The created Security Group Name" value = try(local.created_security_group.name, null) } - -output "security_group_details" { - description = "(UNSTABLE) Details about the security group created" - value = var.unstable_output_enabled ? (var.create_before_destroy ? aws_security_group.cbd : aws_security_group.default) : null -} - -output "rules" { - description = "(UNSTABLE) Details about all the security group rules created" - value = var.unstable_output_enabled ? concat(try(values(aws_security_group_rule.default), []), aws_security_group_rule.sg, aws_security_group_rule.cidr, aws_security_group_rule.self, aws_security_group_rule.egress) : null -} diff --git a/variables.tf b/variables.tf index 530bc95..f17ec51 100644 --- a/variables.tf +++ b/variables.tf @@ -92,12 +92,3 @@ variable "rule_matrix" { A convenient way to apply the same set of rules to a set of subjects. See README for details. EOT } - -variable "unstable_output_enabled" { - type = bool - default = false - description = <<-EOT - Some outputs are unstable, meaning that they can show a change even when no resource changes are made. - These outputs are suppressed by default. Set `unstable_output_enabled` to enable them. - EOT -} From fc1dbdac5df7dd6d43ae051f6ec6a2cc23faaede Mon Sep 17 00:00:00 2001 From: Nuru Date: Sun, 27 Jun 2021 22:24:39 -0700 Subject: [PATCH 07/19] Add Go crash handling to automated tests --- test/src/examples_complete_test.go | 31 +++++++++++++++--------------- test/src/go.mod | 4 ++-- test/src/go.sum | 31 ++++++++++-------------------- 3 files changed, 28 insertions(+), 38 deletions(-) diff --git a/test/src/examples_complete_test.go b/test/src/examples_complete_test.go index d081088..363cc0d 100644 --- a/test/src/examples_complete_test.go +++ b/test/src/examples_complete_test.go @@ -1,6 +1,7 @@ package test import ( + "k8s.io/apimachinery/pkg/util/runtime" "math/rand" "strconv" "testing" @@ -12,7 +13,7 @@ import ( // Test the Terraform module in examples/complete using Terratest. func TestExamplesComplete(t *testing.T) { - // Cannot run in parallel with InitAndApply (parallel inits clobber each other) or default statefile name + // Cannot run in parallel with InitAndApply (parallel inits clobber each other) or default statefile name //t.Parallel() rand.Seed(time.Now().UnixNano()) @@ -34,10 +35,10 @@ func TestExamplesComplete(t *testing.T) { // At the end of the test, run `terraform destroy` to clean up any resources that were created defer terraform.Destroy(t, terraformOptions) - // If Go runtime crushes, run `terraform destroy` to clean up any resources that were created - defer runtime.HandleCrash(func(i interface{}) { - terraform.Destroy(t, terraformOptions) - }) + // If Go runtime crushes, run `terraform destroy` to clean up any resources that were created + defer runtime.HandleCrash(func(i interface{}) { + terraform.Destroy(t, terraformOptions) + }) // This will run `terraform init` and `terraform apply` and fail the test if there are any errors terraform.InitAndApply(t, terraformOptions) @@ -53,17 +54,17 @@ func TestExamplesComplete(t *testing.T) { assert.Contains(t, newSgARN, "arn:aws:ec2", "SG ID should contains substring 'arn:aws:ec2'") assert.Equal(t, "eg-ue2-test-sg-"+randID, newSgName) -/* - Module used to output SG information for existing security groups, but no longer does - // Verify that outputs are valid when `security_group_enabled=false` and `sg_id` set to existing SG ID - existingSgID := terraform.Output(t, terraformOptions, "existing_sg_id") - existingSgARN := terraform.Output(t, terraformOptions, "existing_sg_arn") - existingSgName := terraform.Output(t, terraformOptions, "existing_sg_name") + /* + Module used to output SG information for existing security groups, but no longer does + // Verify that outputs are valid when `security_group_enabled=false` and `sg_id` set to existing SG ID + existingSgID := terraform.Output(t, terraformOptions, "existing_sg_id") + existingSgARN := terraform.Output(t, terraformOptions, "existing_sg_arn") + existingSgName := terraform.Output(t, terraformOptions, "existing_sg_name") - assert.Contains(t, existingSgID, "sg-", "SG ID should contains substring 'sg-'") - assert.Contains(t, existingSgARN, "arn:aws:ec2", "SG ID should contains substring 'arn:aws:ec2'") - assert.Contains(t, existingSgName, "eg-ue2-test-sg-"+randID) -*/ + assert.Contains(t, existingSgID, "sg-", "SG ID should contains substring 'sg-'") + assert.Contains(t, existingSgARN, "arn:aws:ec2", "SG ID should contains substring 'arn:aws:ec2'") + assert.Contains(t, existingSgName, "eg-ue2-test-sg-"+randID) + */ // Verify that outputs are empty when module is disabled disabledSgID := terraform.Output(t, terraformOptions, "disabled_sg_id") diff --git a/test/src/go.mod b/test/src/go.mod index af2dfb2..0f46a9c 100644 --- a/test/src/go.mod +++ b/test/src/go.mod @@ -1,6 +1,6 @@ module github.com/cloudposse/terraform-example-module -go 1.13 +go 1.15 require ( github.com/gruntwork-io/terratest v0.32.8 @@ -9,5 +9,5 @@ require ( golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect golang.org/x/sys v0.0.0-20200828194041-157a740278f4 // indirect gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect - sigs.k8s.io/structured-merge-diff/v3 v3.0.0 // indirect + k8s.io/apimachinery v0.19.3 ) diff --git a/test/src/go.sum b/test/src/go.sum index 2947f4b..48b60c6 100644 --- a/test/src/go.sum +++ b/test/src/go.sum @@ -13,7 +13,6 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v38.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v38.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v46.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= @@ -28,9 +27,7 @@ github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMl github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= github.com/Azure/go-autorest/autorest/adal v0.9.2/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE= -github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM= github.com/Azure/go-autorest/autorest/azure/auth v0.5.1/go.mod h1:ea90/jvmnAwDrSooLH4sRIehEPtG/EPUXavDh31MnA4= -github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw= github.com/Azure/go-autorest/autorest/azure/cli v0.4.0/go.mod h1:JljT387FplPzBA31vUcvsetLKF3pec5bdAxjVU4kI2s= github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= @@ -43,7 +40,6 @@ github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935 github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= -github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI= github.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= @@ -138,6 +134,7 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= @@ -152,6 +149,7 @@ github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -182,6 +180,7 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-containerregistry v0.0.0-20200110202235-f4fb41bf00a3/go.mod h1:2wIuQute9+hhWqvL3vEI7YB0EKluF4WcPzI1eAliazk= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= @@ -197,9 +196,7 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.2.2/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -209,10 +206,7 @@ github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:Fecb github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/gruntwork-io/gruntwork-cli v0.5.1/go.mod h1:IBX21bESC1/LGoV7jhXKUnTQTZgQ6dYRsoj/VqxUSZQ= github.com/gruntwork-io/gruntwork-cli v0.7.0/go.mod h1:jp6Z7NcLF2avpY8v71fBx6hds9eOFPELSuD/VPv7w00= -github.com/gruntwork-io/terratest v0.28.15 h1:in1DRBq8/RjxMyb6Amr1SRrczOK/hGnPi+gQXOOtbZI= -github.com/gruntwork-io/terratest v0.28.15/go.mod h1:PkVylPuUNmItkfOTwSiFreYA4FkanK8AluBuNeGxQOw= github.com/gruntwork-io/terratest v0.32.8 h1:ccIRFH+e6zhSB5difg7baJec4FeOZNXpeIFlZZlKW2M= github.com/gruntwork-io/terratest v0.32.8/go.mod h1:FckR+7ks472IJfSKUPfPvnJfSxV1LKGWGMJ9m/LHegE= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= @@ -252,11 +246,13 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -304,7 +300,6 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -387,7 +382,6 @@ golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -473,11 +467,9 @@ golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200828194041-157a740278f4 h1:kCCpuwSAoYJPkNc6x0xT9yTtV4oKtARo4RGBQWOfg9E= @@ -521,6 +513,7 @@ golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200113040837-eac381796e91/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= @@ -545,7 +538,6 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200108215221-bd8f9a0ef82f/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -568,6 +560,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -594,14 +587,12 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= -k8s.io/api v0.18.3/go.mod h1:UOaMwERbqJMfeeeHc8XJKawj4P9TgDRnViIqqBeH2QA= k8s.io/api v0.19.3/go.mod h1:VF+5FT1B74Pw3KxMdKyinLo+zynBaMBiAfGMuldcNDs= k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= -k8s.io/apimachinery v0.18.3/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko= +k8s.io/apimachinery v0.19.3 h1:bpIQXlKjB4cB/oNpnNnV+BybGPR7iP5oYpsOTEJ4hgc= k8s.io/apimachinery v0.19.3/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= k8s.io/apiserver v0.17.0/go.mod h1:ABM+9x/prjINN6iiffRVNCBR2Wk7uY4z+EtEGZD48cg= k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k= -k8s.io/client-go v0.18.3/go.mod h1:4a/dpQEvzAhT1BbuWW09qvIaGw6Gbu1gZYiQZIi1DMw= k8s.io/client-go v0.19.3/go.mod h1:+eEMktZM+MG0KO+PTkci8xnbCZHvj9TqR6Q1XDUIJOM= k8s.io/cloud-provider v0.17.0/go.mod h1:Ze4c3w2C0bRsjkBUoHpFi+qWe3ob1wI2/7cUn+YQIDE= k8s.io/code-generator v0.0.0-20191121015212-c4c8f8345c7e/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= @@ -612,15 +603,15 @@ k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8 k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= -k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= k8s.io/legacy-cloud-providers v0.17.0/go.mod h1:DdzaepJ3RtRy+e5YhNtrCYwlgyK87j/5+Yfp0L9Syp8= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= @@ -630,8 +621,6 @@ modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= -sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= -sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= From a778c2699eed1c4afad36918cb2277eaaf6e67a5 Mon Sep 17 00:00:00 2001 From: Nuru Date: Mon, 28 Jun 2021 00:29:22 -0700 Subject: [PATCH 08/19] Revise README.yaml explanation of rules and add rule_matrix --- README.md | 72 +++++++++++++++++++++++++++++------------------ README.yaml | 68 ++++++++++++++++++++++++++++---------------- docs/terraform.md | 4 +-- variables.tf | 9 +++--- 4 files changed, 94 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 79040c4..c651cb2 100644 --- a/README.md +++ b/README.md @@ -94,23 +94,48 @@ The table below correctly indicates which inputs are required. -Note: Terraform requires that all the elements of the `rules` list be exactly -the same type. This means you must supply all the same keys and, for each key, -all the values for that key must be the same type. Any optional key, such as -`ipv6_cidr_blocks`, can be omitted from all the rules without problem. However, -if some rules have a key and other rules would omit the key if that were allowed -(e.g one rule has `cidr_blocks` and another rule has `self = true`, and neither -rule can include both `cidr_blocks` and `self`), instead of omitting the key, -include the key with value of `null`, unless the value is a list type, in which case +This module provides 2 ways to set security group rules rules. The `rules` input takes a list of +rule maps. The maps are compatible with (have the same keys and accept the same values) the +Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). +While some of the map keys are optional, Terraform requires that all maps in the list have exactly the same +set of keys, so if you set, for example `prefix_list_ids` in one rule, you need to include that key in all the maps. +In rules where the key would othewise be omitted, include the key with value of `null`, unless the value is a list type, in which case set the value to `[]` (an empty list). -Although `description` is optional, if you do not include a description, -the rule will be deleted and recreated if the index of the rule in the `rules` -list changes, which usually happens as a result of adding or removing a rule. Rules -that include a description will only be modified if the rule itself changes. -Also, if 2 rules specify the same `type`, `protocol`, `from_port`, and `to_port`, -they must not also have the same `description` (although if one or both rules -have no description supplied, that will work). +The other way to set rules is via the `rule_matrix` input. This splits the keys of the `aws_security_group_rule` resource +into to sets: one set defines the rule and descripition, the other set defines the subject of the rule. As with +`rules` and explained in the previous paragraph, all elements of the list must have all the same keys. This also holds +for all the elements of the `rules_matrix.rules` list. + +The schema for the `rule matrix is: + +```hcl +{ + # these top level lists define all the subjects to which rule_matrix rules will be applied + source_security_group_ids = list of source security group IDs to apply all rules to + cidr_blocks = list of ipv4 CIDR blocks to apply all rules to + ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to + prefix_list_ids = list of prefix list IDs to apply all rules to + self = # set "true" to apply the rules to the created or existing security group + + # each rule in the rules list will be applied to every subject defined above + rules = [{ + type = type of rule, either "ingress" or "egress" + from_port = start range of protocol port + to_port = end range of protocol port, max is 65535 + protocol = ip protocol name or number or "all" for all + description = free form text description of the rule + }] +} +``` + +The way Terraform works and the way this module is implemented causes security group rules +to be dependent on their place in the input lists. If a rule is deleted and the other rules therefore move +closer to the start of the list, those rules will be deleted and recreated. This should have no significant +operational impact, but it can make a small change look like a big one when viewing the output of +Terraform plan. After careful consideration, we have decided that this is preferable to the +impositions and limitations that would come from a solution that avoids it. + ```hcl module "label" { @@ -143,6 +168,9 @@ module "sg" { # Cloud Posse recommends pinning every module to a specific version # version = "x.x.x" + # Allow unlimited egress + allow_all_egress = true + rules = [ { type = "ingress" @@ -162,16 +190,6 @@ module "sg" { self = true description = "Allow HTTP from inside the security group" }, - - { - type = "egress" - from_port = 0 - to_port = 65535 - protocol = "all" - cidr_blocks = ["0.0.0.0/0"] - self = null - description = "Allow egress to anywhere" - } ] context = module.label.context @@ -237,10 +255,10 @@ Available targets: | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional tags for appending to tags\_as\_list\_of\_maps. Not added to `tags`. | `map(string)` | `{}` | no | -| [allow\_all\_egress](#input\_allow\_all\_egress) | A convenience that adds to the rules in `var.rules` a rule that allows all egress.
If this is false and `var.rules` does not specify any egress rules, then
no egress will be allowed. | `bool` | `false` | no | +| [allow\_all\_egress](#input\_allow\_all\_egress) | A convenience that adds to the rules specified elsewhere a rule that allows all egress.
If this is false and no egress rules are specified via `rules` or `rule-matrix`, then no egress will be allowed. | `bool` | `false` | no | | [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | 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,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | -| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior.
Note that changing this value will change the security group name. | `bool` | `false` | no | +| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior.
Note that changing this value will change the security group name and cause the security group to be replaced. | `bool` | `false` | no | | [create\_security\_group](#input\_create\_security\_group) | Set `true` to create a new security group. If false, `existing_security_group_id` must be provided. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [description](#input\_description) | The Security Group description. | `string` | `"Managed by Terraform"` | no | diff --git a/README.yaml b/README.yaml index c97f2ae..989aec9 100644 --- a/README.yaml +++ b/README.yaml @@ -61,23 +61,48 @@ description: |- # How to use this module. Should be an easy example to copy and paste. usage: |- - Note: Terraform requires that all the elements of the `rules` list be exactly - the same type. This means you must supply all the same keys and, for each key, - all the values for that key must be the same type. Any optional key, such as - `ipv6_cidr_blocks`, can be omitted from all the rules without problem. However, - if some rules have a key and other rules would omit the key if that were allowed - (e.g one rule has `cidr_blocks` and another rule has `self = true`, and neither - rule can include both `cidr_blocks` and `self`), instead of omitting the key, - include the key with value of `null`, unless the value is a list type, in which case + This module provides 2 ways to set security group rules rules. The `rules` input takes a list of + rule maps. The maps are compatible with (have the same keys and accept the same values) the + Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). + While some of the map keys are optional, Terraform requires that all maps in the list have exactly the same + set of keys, so if you set, for example `prefix_list_ids` in one rule, you need to include that key in all the maps. + In rules where the key would othewise be omitted, include the key with value of `null`, unless the value is a list type, in which case set the value to `[]` (an empty list). - Although `description` is optional, if you do not include a description, - the rule will be deleted and recreated if the index of the rule in the `rules` - list changes, which usually happens as a result of adding or removing a rule. Rules - that include a description will only be modified if the rule itself changes. - Also, if 2 rules specify the same `type`, `protocol`, `from_port`, and `to_port`, - they must not also have the same `description` (although if one or both rules - have no description supplied, that will work). + The other way to set rules is via the `rule_matrix` input. This splits the keys of the `aws_security_group_rule` resource + into to sets: one set defines the rule and descripition, the other set defines the subject of the rule. As with + `rules` and explained in the previous paragraph, all elements of the list must have all the same keys. This also holds + for all the elements of the `rules_matrix.rules` list. + + The schema for the `rule matrix is: + + ```hcl + { + # these top level lists define all the subjects to which rule_matrix rules will be applied + source_security_group_ids = list of source security group IDs to apply all rules to + cidr_blocks = list of ipv4 CIDR blocks to apply all rules to + ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to + prefix_list_ids = list of prefix list IDs to apply all rules to + self = # set "true" to apply the rules to the created or existing security group + + # each rule in the rules list will be applied to every subject defined above + rules = [{ + type = type of rule, either "ingress" or "egress" + from_port = start range of protocol port + to_port = end range of protocol port, max is 65535 + protocol = ip protocol name or number or "all" for all + description = free form text description of the rule + }] + } + ``` + + The way Terraform works and the way this module is implemented causes security group rules + to be dependent on their place in the input lists. If a rule is deleted and the other rules therefore move + closer to the start of the list, those rules will be deleted and recreated. This should have no significant + operational impact, but it can make a small change look like a big one when viewing the output of + Terraform plan. After careful consideration, we have decided that this is preferable to the + impositions and limitations that would come from a solution that avoids it. + ```hcl module "label" { @@ -110,6 +135,9 @@ usage: |- # Cloud Posse recommends pinning every module to a specific version # version = "x.x.x" + # Allow unlimited egress + allow_all_egress = true + rules = [ { type = "ingress" @@ -129,16 +157,6 @@ usage: |- self = true description = "Allow HTTP from inside the security group" }, - - { - type = "egress" - from_port = 0 - to_port = 65535 - protocol = "all" - cidr_blocks = ["0.0.0.0/0"] - self = null - description = "Allow egress to anywhere" - } ] context = module.label.context diff --git a/docs/terraform.md b/docs/terraform.md index 900022d..74df464 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -35,10 +35,10 @@ | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional tags for appending to tags\_as\_list\_of\_maps. Not added to `tags`. | `map(string)` | `{}` | no | -| [allow\_all\_egress](#input\_allow\_all\_egress) | A convenience that adds to the rules in `var.rules` a rule that allows all egress.
If this is false and `var.rules` does not specify any egress rules, then
no egress will be allowed. | `bool` | `false` | no | +| [allow\_all\_egress](#input\_allow\_all\_egress) | A convenience that adds to the rules specified elsewhere a rule that allows all egress.
If this is false and no egress rules are specified via `rules` or `rule-matrix`, then no egress will be allowed. | `bool` | `false` | no | | [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | 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,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | -| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior.
Note that changing this value will change the security group name. | `bool` | `false` | no | +| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior.
Note that changing this value will change the security group name and cause the security group to be replaced. | `bool` | `false` | no | | [create\_security\_group](#input\_create\_security\_group) | Set `true` to create a new security group. If false, `existing_security_group_id` must be provided. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [description](#input\_description) | The Security Group description. | `string` | `"Managed by Terraform"` | no | diff --git a/variables.tf b/variables.tf index f17ec51..59c33ac 100644 --- a/variables.tf +++ b/variables.tf @@ -34,7 +34,7 @@ variable "create_before_destroy" { default = false description = <<-EOT Set `true` to enable terraform `create_before_destroy` behavior. - Note that changing this value will change the security group name. + Note that changing this value will change the security group name and cause the security group to be replaced. EOT } @@ -59,9 +59,8 @@ variable "allow_all_egress" { type = bool default = false description = <<-EOT - A convenience that adds to the rules in `var.rules` a rule that allows all egress. - If this is false and `var.rules` does not specify any egress rules, then - no egress will be allowed. + A convenience that adds to the rules specified elsewhere a rule that allows all egress. + If this is false and no egress rules are specified via `rules` or `rule-matrix`, then no egress will be allowed. EOT } @@ -73,7 +72,7 @@ variable "rule_matrix" { # # these top level lists define all the subjects to which rule_matrix rules will be applied # source_security_group_ids = list of source security group IDs to apply all rules to # cidr_blocks = list of ipv4 CIDR blocks to apply all rules to - # ipv6_cidr_blocks= list of ipv6 CIDR blocks to apply all rules to + # ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to # prefix_list_ids = list of prefix list IDs to apply all rules to # self = # set "true" to apply the rules to the created or existing security group # From d3139ca840c077cbba58bc5abf72f55974ae96d1 Mon Sep 17 00:00:00 2001 From: Nuru Date: Wed, 30 Jun 2021 11:14:59 -0700 Subject: [PATCH 09/19] change `description` to `security_group_description` --- README.md | 2 +- README.yaml | 5 ++++- docs/terraform.md | 2 +- main.tf | 23 +++++++++++++---------- variables.tf | 19 +++++++++++-------- 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c651cb2..1191ebf 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,6 @@ Available targets: | [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior.
Note that changing this value will change the security group name and cause the security group to be replaced. | `bool` | `false` | no | | [create\_security\_group](#input\_create\_security\_group) | Set `true` to create a new security group. If false, `existing_security_group_id` must be provided. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | -| [description](#input\_description) | The Security Group description. | `string` | `"Managed by Terraform"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [existing\_security\_group\_id](#input\_existing\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
Required if `security_group_enabled` is `false`, ignored otherwise. | `string` | `""` | no | @@ -274,6 +273,7 @@ Available targets: | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` |
{
"rules": []
}
| no | | [rules](#input\_rules) | A list of maps of Security Group rules.
The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | +| [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced, which requires everything
associated with the security group to be replaced, which can be very disruptive. | `string` | `"Managed by Terraform"` | no | | [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the account.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | diff --git a/README.yaml b/README.yaml index 989aec9..ffd7229 100644 --- a/README.yaml +++ b/README.yaml @@ -60,8 +60,11 @@ description: |- # How to use this module. Should be an easy example to copy and paste. usage: |- + This module is primarily for setting security group rules on a security group. You can provide the + ID of an existing security group to modify, or, by default, this module will create a new security + group and apply the given rules to it. - This module provides 2 ways to set security group rules rules. The `rules` input takes a list of + This module provides 2 ways to set security group rules. The `rules` input takes a list of rule maps. The maps are compatible with (have the same keys and accept the same values) the Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). While some of the map keys are optional, Terraform requires that all maps in the list have exactly the same diff --git a/docs/terraform.md b/docs/terraform.md index 74df464..a8f9922 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -41,7 +41,6 @@ | [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior.
Note that changing this value will change the security group name and cause the security group to be replaced. | `bool` | `false` | no | | [create\_security\_group](#input\_create\_security\_group) | Set `true` to create a new security group. If false, `existing_security_group_id` must be provided. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | -| [description](#input\_description) | The Security Group description. | `string` | `"Managed by Terraform"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [existing\_security\_group\_id](#input\_existing\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
Required if `security_group_enabled` is `false`, ignored otherwise. | `string` | `""` | no | @@ -54,6 +53,7 @@ | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` |
{
"rules": []
}
| no | | [rules](#input\_rules) | A list of maps of Security Group rules.
The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | +| [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced, which requires everything
associated with the security group to be replaced, which can be very disruptive. | `string` | `"Managed by Terraform"` | no | | [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the account.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | diff --git a/main.tf b/main.tf index 7443a32..62b92af 100644 --- a/main.tf +++ b/main.tf @@ -1,5 +1,6 @@ locals { enabled = module.this.enabled + default_rule_description = "Managed by Terraform" # Because Terraform formatting for `not` (!) changes between versions 0.13 and 0.14, use == false instead create_security_group = local.enabled && var.create_security_group lookup_security_group = local.enabled && var.create_security_group == false @@ -25,7 +26,7 @@ resource "aws_security_group" "default" { count = local.create_security_group && var.create_before_destroy == false ? 1 : 0 name = coalesce(var.security_group_name, module.this.id) - description = var.description + description = var.security_group_description vpc_id = var.vpc_id tags = merge(module.this.tags, length(var.security_group_name) > 0 ? { Name = var.security_group_name } : {}) } @@ -35,7 +36,7 @@ resource "aws_security_group" "cbd" { count = local.create_security_group && var.create_before_destroy == true ? 1 : 0 name_prefix = coalesce(var.security_group_name, format("%s%s", module.this.id, module.this.delimiter)) - description = var.description + description = var.security_group_description vpc_id = var.vpc_id tags = merge(module.this.tags, length(var.security_group_name) > 0 ? { Name = var.security_group_name } : {}) @@ -52,8 +53,8 @@ resource "aws_security_group_rule" "default" { from_port = local.rules[count.index].from_port to_port = local.rules[count.index].to_port protocol = local.rules[count.index].protocol - description = lookup(local.rules[count.index], "description", "Managed by Terraform") - # Convert any of a missing key, a value of null, or a value of empty list to null + description = lookup(local.rules[count.index], "description", local.default_rule_description) + # Convert a missing key, a value of null, or a value of empty list to null cidr_blocks = try(length(lookup(local.rules[count.index], "cidr_blocks", [])), 0) > 0 ? local.rules[count.index]["cidr_blocks"] : null ipv6_cidr_blocks = try(length(lookup(local.rules[count.index], "ipv6_cidr_blocks", [])), 0) > 0 ? local.rules[count.index]["ipv6_cidr_blocks"] : null prefix_list_ids = try(length(lookup(local.rules[count.index], "prefix_list_ids", [])), 0) > 0 ? local.rules[count.index]["prefix_list_ids"] : null @@ -63,7 +64,7 @@ resource "aws_security_group_rule" "default" { } resource "aws_security_group_rule" "self" { - # We use "== true" here because you cannot use `null` as a conditional + # We use "== true" here because you cannot use `null` as a conditional, but null == true is OK count = local.rule_matrix_enabled && try(var.rule_matrix.self, null) == true ? local.rule_matrix_rule_count : 0 security_group_id = local.security_group_id @@ -71,7 +72,7 @@ resource "aws_security_group_rule" "self" { from_port = var.rule_matrix.rules[count.index].from_port to_port = var.rule_matrix.rules[count.index].to_port protocol = var.rule_matrix.rules[count.index].protocol - description = try(var.rule_matrix.rules[count.index].description, "Managed by Terraform") + description = try(var.rule_matrix.rules[count.index].description, local.default_rule_description) self = var.rule_matrix.self } @@ -86,7 +87,7 @@ resource "aws_security_group_rule" "sg" { from_port = var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].from_port to_port = var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].to_port protocol = var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].protocol - description = try(var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].description, "Managed by Terraform") + description = try(var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].description, local.default_rule_description) source_security_group_id = var.rule_matrix.source_security_group_ids[floor(count.index / local.rule_matrix_rule_count)] } @@ -104,7 +105,7 @@ resource "aws_security_group_rule" "cidr" { from_port = var.rule_matrix.rules[count.index].from_port to_port = var.rule_matrix.rules[count.index].to_port protocol = var.rule_matrix.rules[count.index].protocol - description = try(var.rule_matrix.rules[count.index].description, "Managed by Terraform") + description = try(var.rule_matrix.rules[count.index].description, local.default_rule_description) } @@ -112,10 +113,12 @@ resource "aws_security_group_rule" "egress" { count = local.enabled && var.allow_all_egress ? 1 : 0 security_group_id = local.security_group_id + + # Copied from https://registry.terraform.io/providers/hashicorp/aws/3.46.0/docs/resources/security_group#example-usage type = "egress" from_port = 0 - to_port = 65535 - protocol = "all" + to_port = 0 + protocol = "-1" cidr_blocks = ["0.0.0.0/0"] ipv6_cidr_blocks = ["::/0"] description = "Allow all egress" diff --git a/variables.tf b/variables.tf index 59c33ac..808f89b 100644 --- a/variables.tf +++ b/variables.tf @@ -13,6 +13,16 @@ variable "security_group_name" { EOT } +variable "security_group_description" { + type = string + default = "Managed by Terraform" + description = <<-EOT + The description to assign to the created Security Group. + Warning: Changing the description causes the security group to be replaced, which requires everything + associated with the security group to be replaced, which can be very disruptive. + EOT +} + variable "create_security_group" { type = bool default = true @@ -24,11 +34,10 @@ variable "existing_security_group_id" { default = "" description = <<-EOT The ID of an existing Security Group to which Security Group rules will be assigned. - Required if `security_group_enabled` is `false`, ignored otherwise. + Required if `create_security_group` is `false`, ignored otherwise. EOT } - variable "create_before_destroy" { type = bool default = false @@ -38,12 +47,6 @@ variable "create_before_destroy" { EOT } -variable "description" { - type = string - default = "Managed by Terraform" - description = "The Security Group description." -} - variable "rules" { type = list(any) default = [] From cd729e4817f3c51b211b8f321c33ba4b65c1a71a Mon Sep 17 00:00:00 2001 From: cloudpossebot <11232728+cloudpossebot@users.noreply.github.com> Date: Wed, 30 Jun 2021 18:16:01 +0000 Subject: [PATCH 10/19] Change existing_security_group_id to target_security_group_id --- README.md | 7 +++++-- docs/terraform.md | 2 +- examples/complete/main.tf | 14 +++++++------- main.tf | 18 +++++++++--------- variables.tf | 2 +- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 1191ebf..235350e 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,11 @@ the registry shows many of our inputs as required when in fact they are optional The table below correctly indicates which inputs are required. +This module is primarily for setting security group rules on a security group. You can provide the +ID of an existing security group to modify, or, by default, this module will create a new security +group and apply the given rules to it. -This module provides 2 ways to set security group rules rules. The `rules` input takes a list of +This module provides 2 ways to set security group rules. The `rules` input takes a list of rule maps. The maps are compatible with (have the same keys and accept the same values) the Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). While some of the map keys are optional, Terraform requires that all maps in the list have exactly the same @@ -263,7 +266,6 @@ Available targets: | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [existing\_security\_group\_id](#input\_existing\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
Required if `security_group_enabled` is `false`, ignored otherwise. | `string` | `""` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | @@ -277,6 +279,7 @@ Available targets: | [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the account.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | +| [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
Required if `create_security_group` is `false`, ignored otherwise. | `string` | `""` | no | | [vpc\_id](#input\_vpc\_id) | The VPC ID where Security Group will be created. | `string` | n/a | yes | ## Outputs diff --git a/docs/terraform.md b/docs/terraform.md index a8f9922..d9debc4 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -43,7 +43,6 @@ | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [existing\_security\_group\_id](#input\_existing\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
Required if `security_group_enabled` is `false`, ignored otherwise. | `string` | `""` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | @@ -57,6 +56,7 @@ | [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the account.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | +| [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
Required if `create_security_group` is `false`, ignored otherwise. | `string` | `""` | no | | [vpc\_id](#input\_vpc\_id) | The VPC ID where Security Group will be created. | `string` | n/a | yes | ## Outputs diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 4c065eb..54eb870 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -69,10 +69,10 @@ resource "aws_security_group" "existing" { module "existing_security_group" { source = "../.." - vpc_id = module.vpc.vpc_id - existing_security_group_id = aws_security_group.existing.id - rules = var.rules - create_security_group = false + vpc_id = module.vpc.vpc_id + target_security_group_id = aws_security_group.existing.id + rules = var.rules + create_security_group = false context = module.this.context } @@ -82,9 +82,9 @@ module "existing_security_group" { module "disabled_security_group" { source = "../.." - vpc_id = module.vpc.vpc_id - existing_security_group_id = aws_security_group.existing.id - rules = var.rules + vpc_id = module.vpc.vpc_id + target_security_group_id = aws_security_group.existing.id + rules = var.rules context = module.this.context enabled = false diff --git a/main.tf b/main.tf index 62b92af..ca59fb3 100644 --- a/main.tf +++ b/main.tf @@ -1,5 +1,5 @@ locals { - enabled = module.this.enabled + enabled = module.this.enabled default_rule_description = "Managed by Terraform" # Because Terraform formatting for `not` (!) changes between versions 0.13 and 0.14, use == false instead create_security_group = local.enabled && var.create_security_group @@ -9,7 +9,7 @@ locals { ) : null security_group_id = local.enabled ? ( # Use coalesce() here to hack an error message into the output - var.create_security_group ? local.created_security_group.id : coalesce(var.existing_security_group_id, + var.create_security_group ? local.created_security_group.id : coalesce(var.target_security_group_id, "`create_security_group` is false, but no ID was supplied ") ) : null @@ -115,11 +115,11 @@ resource "aws_security_group_rule" "egress" { security_group_id = local.security_group_id # Copied from https://registry.terraform.io/providers/hashicorp/aws/3.46.0/docs/resources/security_group#example-usage - type = "egress" - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - ipv6_cidr_blocks = ["::/0"] - description = "Allow all egress" + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + description = "Allow all egress" } diff --git a/variables.tf b/variables.tf index 808f89b..12dcff9 100644 --- a/variables.tf +++ b/variables.tf @@ -29,7 +29,7 @@ variable "create_security_group" { description = "Set `true` to create a new security group. If false, `existing_security_group_id` must be provided." } -variable "existing_security_group_id" { +variable "target_security_group_id" { type = string default = "" description = <<-EOT From daad071fcab45702c2db6ee7c9b6157b09d0811d Mon Sep 17 00:00:00 2001 From: Nuru Date: Fri, 2 Jul 2021 16:06:27 -0700 Subject: [PATCH 11/19] Support inline rules, deprecate TF 0.13, standardize inputs --- README.md | 154 ++++++++++--- README.yaml | 124 +++++++++-- docs/terraform.md | 30 +-- examples/complete/fixtures.us-east-2.tfvars | 10 +- examples/complete/main.tf | 62 ++++-- examples/complete/variables.tf | 6 + examples/complete/versions.tf | 4 +- examples/terraform-0.13/context.tf | 202 ++++++++++++++++++ .../terraform-0.13/fixtures.us-east-2.tfvars | 28 +++ examples/terraform-0.13/main.tf | 89 ++++++++ examples/terraform-0.13/outputs.tf | 14 ++ examples/terraform-0.13/variables.tf | 7 + examples/terraform-0.13/versions.tf | 14 ++ exports/security_group_inputs.tf | 141 ++++++++++++ main.tf | 200 ++++++++++------- normalize.tf | 146 +++++++++++++ outputs.tf | 9 +- test/src/Makefile | 2 +- variables.tf | 105 ++++++--- versions.tf | 2 +- 20 files changed, 1148 insertions(+), 201 deletions(-) create mode 100644 examples/terraform-0.13/context.tf create mode 100644 examples/terraform-0.13/fixtures.us-east-2.tfvars create mode 100644 examples/terraform-0.13/main.tf create mode 100644 examples/terraform-0.13/outputs.tf create mode 100644 examples/terraform-0.13/variables.tf create mode 100644 examples/terraform-0.13/versions.tf create mode 100644 exports/security_group_inputs.tf create mode 100644 normalize.tf diff --git a/README.md b/README.md index 235350e..2a491e8 100644 --- a/README.md +++ b/README.md @@ -97,41 +97,56 @@ This module is primarily for setting security group rules on a security group. Y ID of an existing security group to modify, or, by default, this module will create a new security group and apply the given rules to it. +##### `rules` input This module provides 2 ways to set security group rules. The `rules` input takes a list of -rule maps. The maps are compatible with (have the same keys and accept the same values) the +rule maps. The maps are compatible with (have the same keys and accept the same values) as the Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). -While some of the map keys are optional, Terraform requires that all maps in the list have exactly the same -set of keys, so if you set, for example `prefix_list_ids` in one rule, you need to include that key in all the maps. -In rules where the key would othewise be omitted, include the key with value of `null`, unless the value is a list type, in which case -set the value to `[]` (an empty list). +While some of the map keys are optional, Terraform requires that all of the maps in a single list have exactly the same set of keys. +See [WARNINGS and error messages](#warnings-and-error-messages) below for details. +##### `rule_matrix` input The other way to set rules is via the `rule_matrix` input. This splits the keys of the `aws_security_group_rule` resource -into to sets: one set defines the rule and descripition, the other set defines the subject of the rule. As with +into to sets: one set defines the rule and descripition, the other set defines the subjects of the rule. As with `rules` and explained in the previous paragraph, all elements of the list must have all the same keys. This also holds for all the elements of the `rules_matrix.rules` list. -The schema for the `rule matrix is: +Any map key that takes a list value must either be absent from all lists or contain lists in all lists. +Use an empty list rather than `null` to indicate "no value". Passing in `null` instead of a list +may cause Terraform v0.13 to crash and may cause other errors in later Terraform versions. + +The schema for the `rule matrix` is: ```hcl { # these top level lists define all the subjects to which rule_matrix rules will be applied source_security_group_ids = list of source security group IDs to apply all rules to - cidr_blocks = list of ipv4 CIDR blocks to apply all rules to - ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to - prefix_list_ids = list of prefix list IDs to apply all rules to - self = # set "true" to apply the rules to the created or existing security group + cidr_blocks = list of ipv4 CIDR blocks to apply all rules to + ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to + prefix_list_ids = list of prefix list IDs to apply all rules to + + self = bool set "true" to apply the rules to the created or existing security group, null otherwise # each rule in the rules list will be applied to every subject defined above rules = [{ - type = type of rule, either "ingress" or "egress" + type = type of rule, either "ingress" or "egress" from_port = start range of protocol port - to_port = end range of protocol port, max is 65535 - protocol = ip protocol name or number or "all" for all + to_port = end range of protocol port, max is 65535 + protocol = ip protocol name or number or "-1" for all protocols and ports + description = free form text description of the rule }] } ``` +##### Create before delete +This module provides a `create_before_delete` option that will, when a security group needs to be replaced, +cause Terraform to create the new one before deleting the old one. We recommend making this `true` for new security groups, +but we default it to `false` because if you import a security group with this setting `true`, that security +group will be deleted and replaced on the first `terraform apply`, which will likely cause a service outage. + +### Important Notes + +##### Unexpected changes during plan and apply The way Terraform works and the way this module is implemented causes security group rules to be dependent on their place in the input lists. If a rule is deleted and the other rules therefore move closer to the start of the list, those rules will be deleted and recreated. This should have no significant @@ -139,6 +154,41 @@ operational impact, but it can make a small change look like a big one when view Terraform plan. After careful consideration, we have decided that this is preferable to the impositions and limitations that would come from a solution that avoids it. +##### WARNINGS and error messages + +**_Terraform v0.13 NOT SUPPORTED_**: While we currently allow use of this module with Terraform v0.13, +it has a number of known issues that are fixed in Terraform v0.14 and this module is not going +to work around. Among them are crashes due to object type conversions and the dreaded, +ubiquitous `Error: Invalid count argument`. Our recommendation if you run into these issues is +to upgrade to Terraform v0.14 or later. As a work around, avoid using `rule_matrix` and only +specify rules via the `rules` input, which has fewer issues with Terraform v0.13. + +**_Objects not of the same type_**: Any time you provide a list of object, Terraform requires that all objects in the list +must be [the exact same type](https://www.terraform.io/docs/language/expressions/type-constraints.html#dynamic-types-the-quot-any-quot-constraint). +This means that all maps in the list have exactly the same set of keys and that the values are all the same type. +So while some keys are optional for this module, if you include a key in any one of the maps in a list, then you +have to include that same key in all of them. +In rules where the key would othewise be omitted, include the key with value of `null`, unless the value is a +list type, in which case set the value to `[]` (an empty list), due to [#28137](https://github.com/hashicorp/terraform/issues/28137). + +**_Setting `inline_rules_enabled` is not recommended and NOT SUPPORTED_**: Any issues arising from setting +`inlne_rules_enabled = true` (including issues about setting it to `false` after setting it to `true`) will +not be addressed, because they flow from [fundamental problems](https://github.com/hashicorp/terraform-provider-aws/issues/20046) +with the underlying `aws_security_group` resource. The setting is provided for people who know and accept the +limitations and trade-offs and want to use it anyway. The main advantage is that when using inline rules, +Terraform will perform "drift detection" and attempt to remove any rules it finds in place but not +specified inline. See [this post](https://github.com/hashicorp/terraform-provider-aws/pull/9032#issuecomment-639545250) +for a discussion of the difference between inline and resource rules, +and some of the reasons inline rules are not satisfactory. + +**_KNOWN ISSUE_** ([#20046](https://github.com/hashicorp/terraform-provider-aws/issues/20046)): +If you set `inline_rules_enabled = true`, you cannot later set it to `false`. If you try, +Terraform will [complain](https://github.com/hashicorp/terraform/pull/2376) and fail. +You will either have to delete and recreate the security group or manually delete all +the security group rules via the AWS console or CLI before applying `inline_rules_enabled = false`. + + +### Example code ```hcl module "label" { @@ -171,6 +221,14 @@ module "sg" { # Cloud Posse recommends pinning every module to a specific version # version = "x.x.x" + # Security Group names must be unique within a VPC. + # This module follows Cloud Posse naming conventions and generates the name + # based on the inputs to the null-label module, which means you cannot + # reuse the label as-is for more than one security group in the VPC. + # + # Here we add an attibute to give the security group a unique name. + attributes = ["primary"] + # Allow unlimited egress allow_all_egress = true @@ -195,8 +253,44 @@ module "sg" { }, ] + vpc_id = module.vpc.vpc_id + context = module.label.context } + +module "sg_mysql" { + source = "cloudposse/security-group/aws" + # Cloud Posse recommends pinning every module to a specific version + # version = "x.x.x" + + # Add an attibute to give the Security Group a unique name + attributes = ["mysql"] + + # Allow unlimited egress + allow_all_egress = true + + rule_matrix =[ + # Allow any of these security groups or the specified prefixes to access MySQL + { + source_security_group_ids = [var.dev_sg, var.uat_sg, var.staging_sg] + prefix_list_ids = [var.mysql_client_prefix_list_id] + rules = [ + { + type = "ingress" + from_port = 3306 + to_port = 3306 + protocol = "tcp" + description = "Allow MySQL access from trusted security groups" + } + ] + } + ] + + vpc_id = module.vpc.vpc_id + + context = module.label.context +} + ``` @@ -204,7 +298,7 @@ module "sg" { ## Examples -Here is an example of using this module: +We have an example of using this module: - [`examples/complete`](https://github.com/cloudposse/terraform-aws-security-group/examples/complete) - complete example of using this module @@ -227,13 +321,13 @@ Available targets: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 0.13.0 | -| [aws](#requirement\_aws) | >= 2.0 | +| [aws](#requirement\_aws) | >= 3.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 2.0 | +| [aws](#provider\_aws) | >= 3.0 | ## Modules @@ -247,11 +341,7 @@ Available targets: |------|------| | [aws_security_group.cbd](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | -| [aws_security_group_rule.cidr](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | -| [aws_security_group_rule.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | -| [aws_security_group_rule.egress](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | -| [aws_security_group_rule.self](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | -| [aws_security_group_rule.sg](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.discrete](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | ## Inputs @@ -261,34 +351,38 @@ Available targets: | [allow\_all\_egress](#input\_allow\_all\_egress) | A convenience that adds to the rules specified elsewhere a rule that allows all egress.
If this is false and no egress rules are specified via `rules` or `rule-matrix`, then no egress will be allowed. | `bool` | `false` | no | | [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | 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,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | -| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior.
Note that changing this value will change the security group name and cause the security group to be replaced. | `bool` | `false` | no | -| [create\_security\_group](#input\_create\_security\_group) | Set `true` to create a new security group. If false, `existing_security_group_id` must be provided. | `bool` | `true` | no | +| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
We recommend setting this `true` on new security groups, but default it to `false` because `true`
will cause existing security groups to be replaced.
Note that changing this value will also cause the security group to be replaced. | `bool` | `false` | no | +| [create\_security\_group](#input\_create\_security\_group) | Set `true` to create a new security group. If false, `target_security_group_id` must be provided. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [inline\_rules\_enabled](#input\_inline\_rules\_enabled) | NOT RECOMMENDED. Create rules "inline" instead of as separate `aws_security_group_rule` resources.
See [#20046](https://github.com/hashicorp/terraform-provider-aws/issues/20046) for one of several issues with inline rules.
See [this post](https://github.com/hashicorp/terraform-provider-aws/pull/9032#issuecomment-639545250) for details on the difference between inline rules and rule resources. | `bool` | `false` | no | | [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | The letter case of output label values (also used in `tags` and `id`).
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Default value: `lower`. | `string` | `null` | no | | [name](#input\_name) | Solution name, e.g. 'app' or 'jenkins' | `string` | `null` | no | | [namespace](#input\_namespace) | Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp' | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` |
{
"rules": []
}
| no | +| [revoke\_rules\_on\_delete](#input\_revoke\_rules\_on\_delete) | Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting
the security group itself. This is normally not needed. | `bool` | `false` | no | +| [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` | `[]` | no | | [rules](#input\_rules) | A list of maps of Security Group rules.
The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | +| [security\_group\_create\_timeout](#input\_security\_group\_create\_timeout) | How long to wait for the security group to be created. | `string` | `"10m"` | no | +| [security\_group\_delete\_timeout](#input\_security\_group\_delete\_timeout) | How long to retry on `DependencyViolation` errors during security group deletion from
lingering ENIs left by certain AWS services such as Elastic Load Balancing. | `string` | `"15m"` | no | | [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced, which requires everything
associated with the security group to be replaced, which can be very disruptive. | `string` | `"Managed by Terraform"` | no | -| [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the account.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | +| [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the VPC.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | | [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
Required if `create_security_group` is `false`, ignored otherwise. | `string` | `""` | no | -| [vpc\_id](#input\_vpc\_id) | The VPC ID where Security Group will be created. | `string` | n/a | yes | +| [vpc\_id](#input\_vpc\_id) | The ID of the VPC where the Security Group will be created. | `string` | n/a | yes | ## Outputs | Name | Description | |------|-------------| -| [arn](#output\_arn) | The created Security Group ARN | -| [id](#output\_id) | The created Security Group ID | -| [name](#output\_name) | The created Security Group Name | +| [arn](#output\_arn) | The created Security Group ARN (null if using existing security group) | +| [id](#output\_id) | The created or target Security Group ID | +| [name](#output\_name) | The created Security Group Name (null if using existing security group) | diff --git a/README.yaml b/README.yaml index ffd7229..16a2fbe 100644 --- a/README.yaml +++ b/README.yaml @@ -64,41 +64,56 @@ usage: |- ID of an existing security group to modify, or, by default, this module will create a new security group and apply the given rules to it. + ##### `rules` input This module provides 2 ways to set security group rules. The `rules` input takes a list of - rule maps. The maps are compatible with (have the same keys and accept the same values) the + rule maps. The maps are compatible with (have the same keys and accept the same values) as the Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). - While some of the map keys are optional, Terraform requires that all maps in the list have exactly the same - set of keys, so if you set, for example `prefix_list_ids` in one rule, you need to include that key in all the maps. - In rules where the key would othewise be omitted, include the key with value of `null`, unless the value is a list type, in which case - set the value to `[]` (an empty list). + While some of the map keys are optional, Terraform requires that all of the maps in a single list have exactly the same set of keys. + See [WARNINGS and error messages](#warnings-and-error-messages) below for details. + ##### `rule_matrix` input The other way to set rules is via the `rule_matrix` input. This splits the keys of the `aws_security_group_rule` resource - into to sets: one set defines the rule and descripition, the other set defines the subject of the rule. As with + into to sets: one set defines the rule and descripition, the other set defines the subjects of the rule. As with `rules` and explained in the previous paragraph, all elements of the list must have all the same keys. This also holds for all the elements of the `rules_matrix.rules` list. - The schema for the `rule matrix is: + Any map key that takes a list value must either be absent from all lists or contain lists in all lists. + Use an empty list rather than `null` to indicate "no value". Passing in `null` instead of a list + may cause Terraform v0.13 to crash and may cause other errors in later Terraform versions. + + The schema for the `rule matrix` is: ```hcl { # these top level lists define all the subjects to which rule_matrix rules will be applied source_security_group_ids = list of source security group IDs to apply all rules to - cidr_blocks = list of ipv4 CIDR blocks to apply all rules to - ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to - prefix_list_ids = list of prefix list IDs to apply all rules to - self = # set "true" to apply the rules to the created or existing security group + cidr_blocks = list of ipv4 CIDR blocks to apply all rules to + ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to + prefix_list_ids = list of prefix list IDs to apply all rules to + + self = bool set "true" to apply the rules to the created or existing security group, null otherwise # each rule in the rules list will be applied to every subject defined above rules = [{ - type = type of rule, either "ingress" or "egress" + type = type of rule, either "ingress" or "egress" from_port = start range of protocol port - to_port = end range of protocol port, max is 65535 - protocol = ip protocol name or number or "all" for all + to_port = end range of protocol port, max is 65535 + protocol = ip protocol name or number or "-1" for all protocols and ports + description = free form text description of the rule }] } ``` + ##### Create before delete + This module provides a `create_before_delete` option that will, when a security group needs to be replaced, + cause Terraform to create the new one before deleting the old one. We recommend making this `true` for new security groups, + but we default it to `false` because if you import a security group with this setting `true`, that security + group will be deleted and replaced on the first `terraform apply`, which will likely cause a service outage. + + ### Important Notes + + ##### Unexpected changes during plan and apply The way Terraform works and the way this module is implemented causes security group rules to be dependent on their place in the input lists. If a rule is deleted and the other rules therefore move closer to the start of the list, those rules will be deleted and recreated. This should have no significant @@ -106,6 +121,41 @@ usage: |- Terraform plan. After careful consideration, we have decided that this is preferable to the impositions and limitations that would come from a solution that avoids it. + ##### WARNINGS and error messages + + **_Terraform v0.13 NOT SUPPORTED_**: While we currently allow use of this module with Terraform v0.13, + it has a number of known issues that are fixed in Terraform v0.14 and this module is not going + to work around. Among them are crashes due to object type conversions and the dreaded, + ubiquitous `Error: Invalid count argument`. Our recommendation if you run into these issues is + to upgrade to Terraform v0.14 or later. As a work around, avoid using `rule_matrix` and only + specify rules via the `rules` input, which has fewer issues with Terraform v0.13. + + **_Objects not of the same type_**: Any time you provide a list of object, Terraform requires that all objects in the list + must be [the exact same type](https://www.terraform.io/docs/language/expressions/type-constraints.html#dynamic-types-the-quot-any-quot-constraint). + This means that all maps in the list have exactly the same set of keys and that the values are all the same type. + So while some keys are optional for this module, if you include a key in any one of the maps in a list, then you + have to include that same key in all of them. + In rules where the key would othewise be omitted, include the key with value of `null`, unless the value is a + list type, in which case set the value to `[]` (an empty list), due to [#28137](https://github.com/hashicorp/terraform/issues/28137). + + **_Setting `inline_rules_enabled` is not recommended and NOT SUPPORTED_**: Any issues arising from setting + `inlne_rules_enabled = true` (including issues about setting it to `false` after setting it to `true`) will + not be addressed, because they flow from [fundamental problems](https://github.com/hashicorp/terraform-provider-aws/issues/20046) + with the underlying `aws_security_group` resource. The setting is provided for people who know and accept the + limitations and trade-offs and want to use it anyway. The main advantage is that when using inline rules, + Terraform will perform "drift detection" and attempt to remove any rules it finds in place but not + specified inline. See [this post](https://github.com/hashicorp/terraform-provider-aws/pull/9032#issuecomment-639545250) + for a discussion of the difference between inline and resource rules, + and some of the reasons inline rules are not satisfactory. + + **_KNOWN ISSUE_** ([#20046](https://github.com/hashicorp/terraform-provider-aws/issues/20046)): + If you set `inline_rules_enabled = true`, you cannot later set it to `false`. If you try, + Terraform will [complain](https://github.com/hashicorp/terraform/pull/2376) and fail. + You will either have to delete and recreate the security group or manually delete all + the security group rules via the AWS console or CLI before applying `inline_rules_enabled = false`. + + + ### Example code ```hcl module "label" { @@ -138,6 +188,14 @@ usage: |- # Cloud Posse recommends pinning every module to a specific version # version = "x.x.x" + # Security Group names must be unique within a VPC. + # This module follows Cloud Posse naming conventions and generates the name + # based on the inputs to the null-label module, which means you cannot + # reuse the label as-is for more than one security group in the VPC. + # + # Here we add an attibute to give the security group a unique name. + attributes = ["primary"] + # Allow unlimited egress allow_all_egress = true @@ -162,13 +220,49 @@ usage: |- }, ] + vpc_id = module.vpc.vpc_id + context = module.label.context } + + module "sg_mysql" { + source = "cloudposse/security-group/aws" + # Cloud Posse recommends pinning every module to a specific version + # version = "x.x.x" + + # Add an attibute to give the Security Group a unique name + attributes = ["mysql"] + + # Allow unlimited egress + allow_all_egress = true + + rule_matrix =[ + # Allow any of these security groups or the specified prefixes to access MySQL + { + source_security_group_ids = [var.dev_sg, var.uat_sg, var.staging_sg] + prefix_list_ids = [var.mysql_client_prefix_list_id] + rules = [ + { + type = "ingress" + from_port = 3306 + to_port = 3306 + protocol = "tcp" + description = "Allow MySQL access from trusted security groups" + } + ] + } + ] + + vpc_id = module.vpc.vpc_id + + context = module.label.context + } + ``` # Example usage examples: |- - Here is an example of using this module: + We have an example of using this module: - [`examples/complete`](https://github.com/cloudposse/terraform-aws-security-group/examples/complete) - complete example of using this module # How to get started quickly diff --git a/docs/terraform.md b/docs/terraform.md index d9debc4..72d2032 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -4,13 +4,13 @@ | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 0.13.0 | -| [aws](#requirement\_aws) | >= 2.0 | +| [aws](#requirement\_aws) | >= 3.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 2.0 | +| [aws](#provider\_aws) | >= 3.0 | ## Modules @@ -24,11 +24,7 @@ |------|------| | [aws_security_group.cbd](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | -| [aws_security_group_rule.cidr](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | -| [aws_security_group_rule.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | -| [aws_security_group_rule.egress](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | -| [aws_security_group_rule.self](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | -| [aws_security_group_rule.sg](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.discrete](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | ## Inputs @@ -38,32 +34,36 @@ | [allow\_all\_egress](#input\_allow\_all\_egress) | A convenience that adds to the rules specified elsewhere a rule that allows all egress.
If this is false and no egress rules are specified via `rules` or `rule-matrix`, then no egress will be allowed. | `bool` | `false` | no | | [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | 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,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | -| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior.
Note that changing this value will change the security group name and cause the security group to be replaced. | `bool` | `false` | no | -| [create\_security\_group](#input\_create\_security\_group) | Set `true` to create a new security group. If false, `existing_security_group_id` must be provided. | `bool` | `true` | no | +| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
We recommend setting this `true` on new security groups, but default it to `false` because `true`
will cause existing security groups to be replaced.
Note that changing this value will also cause the security group to be replaced. | `bool` | `false` | no | +| [create\_security\_group](#input\_create\_security\_group) | Set `true` to create a new security group. If false, `target_security_group_id` must be provided. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [inline\_rules\_enabled](#input\_inline\_rules\_enabled) | NOT RECOMMENDED. Create rules "inline" instead of as separate `aws_security_group_rule` resources.
See [#20046](https://github.com/hashicorp/terraform-provider-aws/issues/20046) for one of several issues with inline rules.
See [this post](https://github.com/hashicorp/terraform-provider-aws/pull/9032#issuecomment-639545250) for details on the difference between inline rules and rule resources. | `bool` | `false` | no | | [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | The letter case of output label values (also used in `tags` and `id`).
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Default value: `lower`. | `string` | `null` | no | | [name](#input\_name) | Solution name, e.g. 'app' or 'jenkins' | `string` | `null` | no | | [namespace](#input\_namespace) | Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp' | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` |
{
"rules": []
}
| no | +| [revoke\_rules\_on\_delete](#input\_revoke\_rules\_on\_delete) | Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting
the security group itself. This is normally not needed. | `bool` | `false` | no | +| [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` | `[]` | no | | [rules](#input\_rules) | A list of maps of Security Group rules.
The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | +| [security\_group\_create\_timeout](#input\_security\_group\_create\_timeout) | How long to wait for the security group to be created. | `string` | `"10m"` | no | +| [security\_group\_delete\_timeout](#input\_security\_group\_delete\_timeout) | How long to retry on `DependencyViolation` errors during security group deletion from
lingering ENIs left by certain AWS services such as Elastic Load Balancing. | `string` | `"15m"` | no | | [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced, which requires everything
associated with the security group to be replaced, which can be very disruptive. | `string` | `"Managed by Terraform"` | no | -| [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the account.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | +| [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the VPC.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | | [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
Required if `create_security_group` is `false`, ignored otherwise. | `string` | `""` | no | -| [vpc\_id](#input\_vpc\_id) | The VPC ID where Security Group will be created. | `string` | n/a | yes | +| [vpc\_id](#input\_vpc\_id) | The ID of the VPC where the Security Group will be created. | `string` | n/a | yes | ## Outputs | Name | Description | |------|-------------| -| [arn](#output\_arn) | The created Security Group ARN | -| [id](#output\_id) | The created Security Group ID | -| [name](#output\_name) | The created Security Group Name | +| [arn](#output\_arn) | The created Security Group ARN (null if using existing security group) | +| [id](#output\_id) | The created or target Security Group ID | +| [name](#output\_name) | The created Security Group Name (null if using existing security group) | diff --git a/examples/complete/fixtures.us-east-2.tfvars b/examples/complete/fixtures.us-east-2.tfvars index bf42246..9624fb2 100644 --- a/examples/complete/fixtures.us-east-2.tfvars +++ b/examples/complete/fixtures.us-east-2.tfvars @@ -15,12 +15,14 @@ rules = [ to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] + description = "SSH wide open" }, { - type = "egress" - from_port = 0 - to_port = 65535 - protocol = "all" + type = "ingress" + from_port = 443 + to_port = 443 + protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] + description = "HTTPS wide open" } ] diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 54eb870..988df73 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -8,22 +8,37 @@ module "vpc" { cidr_block = "10.0.0.0/24" + assign_generated_ipv6_cidr_block = true + context = module.this.context } -# Create new one security group +resource "random_integer" "coin" { + max = 1 + min = 0 +} + +# Create one new security group module "new_security_group" { source = "../.." - vpc_id = module.vpc.vpc_id allow_all_egress = true - rule_matrix = { - # Allow ingress on ports 22 and 80 from created security grup, existing security group, and CIDR "10.0.0.0/8" + + rule_matrix = [{ + # Allow ingress on ports 22 and 80 from created security group, existing security group, and CIDR "10.0.0.0/8" + + # The dynamic value for source_security_group_ids breaks Terraform 0.13 but should work in 0.14 or later source_security_group_ids = [aws_security_group.existing.id] - cidr_blocks = ["10.0.0.0/8"] - prefix_list_ids = null - self = true + # Either dynamic value for CIDRs breaks Terraform 0.13 but should work in 0.14 or later + cidr_blocks = random_integer.coin.result > 0 ? ["10.0.0.0/16"] : ["10.0.0.0/24"] + ipv6_cidr_blocks = [module.vpc.ipv6_cidr_block] + prefix_list_ids = [] + + # Making `self` derived should break count, as it legitimately makes + # the count impossible to predict + # self = random_integer.coin.result > 0 + self = var.rule_matrix_self rules = [ { type = "ingress" @@ -40,24 +55,43 @@ module "new_security_group" { description = "Allow HTTP access" }, ] - } + }] + rules = [ { type = "ingress" from_port = 443 to_port = 443 - protocol = "all" - cidr_blocks = ["0.0.0.0/0"] - ipv6_cidr_blocks = null + protocol = "tcp" + cidr_blocks = ["10.0.0.0/8"] + ipv6_cidr_blocks = [module.vpc.ipv6_cidr_block] # ["::/0"] # source_security_group_id = null - description = null + description = "Discrete HTTPS ingress by CIDR" + self = null + }, + { + type = "ingress" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = [] + ipv6_cidr_blocks = [] + source_security_group_id = aws_security_group.existing.id + description = "Discrete HTTPS ingress for special SG" self = null }, ] + + vpc_id = module.vpc.vpc_id + + security_group_create_timeout = "5m" + security_group_delete_timeout = "2m" + context = module.this.context } + # Create rules for pre-created security group resource "aws_security_group" "existing" { @@ -69,11 +103,13 @@ resource "aws_security_group" "existing" { module "existing_security_group" { source = "../.." - vpc_id = module.vpc.vpc_id + allow_all_egress = true target_security_group_id = aws_security_group.existing.id rules = var.rules create_security_group = false + vpc_id = module.vpc.vpc_id + context = module.this.context } diff --git a/examples/complete/variables.tf b/examples/complete/variables.tf index 014ce51..9533ad0 100644 --- a/examples/complete/variables.tf +++ b/examples/complete/variables.tf @@ -5,3 +5,9 @@ variable "region" { variable "rules" { type = list(any) } + +variable "rule_matrix_self" { + type = bool + description = "Value to set `self` in `rule_matrix` test rule" + default = null +} diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index 3b49b80..28a2720 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -1,10 +1,10 @@ terraform { - required_version = ">= 0.13.0" + required_version = ">= 0.14.0" required_providers { aws = { source = "hashicorp/aws" - version = ">= 2.0" + version = ">= 3.0" } random = { source = "hashicorp/random" diff --git a/examples/terraform-0.13/context.tf b/examples/terraform-0.13/context.tf new file mode 100644 index 0000000..81f99b4 --- /dev/null +++ b/examples/terraform-0.13/context.tf @@ -0,0 +1,202 @@ +# +# 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. +# +# 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.24.1" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + 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 + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = 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 + } + 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 = "Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp'" +} + +variable "environment" { + type = string + default = null + description = "Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = "Solution name, e.g. 'app' or 'jenkins'" +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = "Additional attributes (e.g. `1`)" +} + +variable "tags" { + type = map(string) + default = {} + description = "Additional tags (e.g. `map('BusinessUnit','XYZ')`" +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = "Additional tags for appending to tags_as_list_of_maps. Not added to `tags`." +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The naming order of the id output and Name tag. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 5 elements, 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`. + 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 default, which is `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 + The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`. + 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 + The letter case of output label values (also used in `tags` and `id`). + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + 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`." + } +} +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/examples/terraform-0.13/fixtures.us-east-2.tfvars b/examples/terraform-0.13/fixtures.us-east-2.tfvars new file mode 100644 index 0000000..9624fb2 --- /dev/null +++ b/examples/terraform-0.13/fixtures.us-east-2.tfvars @@ -0,0 +1,28 @@ +region = "us-east-2" + +namespace = "eg" + +environment = "ue2" + +stage = "test" + +name = "sg" + +rules = [ + { + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + description = "SSH wide open" + }, + { + type = "ingress" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + description = "HTTPS wide open" + } +] diff --git a/examples/terraform-0.13/main.tf b/examples/terraform-0.13/main.tf new file mode 100644 index 0000000..87a6638 --- /dev/null +++ b/examples/terraform-0.13/main.tf @@ -0,0 +1,89 @@ +provider "aws" { + region = var.region +} + +module "vpc" { + source = "cloudposse/vpc/aws" + version = "v0.25.0" + + cidr_block = "10.0.0.0/24" + + assign_generated_ipv6_cidr_block = true + + context = module.this.context +} + +# Create one new security group + +module "new_security_group" { + source = "../.." + + allow_all_egress = true + + rule_matrix = [{ + # Allow ingress on ports 22 and 80 from created security group, existing security group, and CIDR "10.0.0.0/8" + + # A derived value for source_security_group_ids breaks Terraform 0.13 + # source_security_group_ids = [aws_security_group.existing.id] + source_security_group_ids = [] + # The dynamic value for CIDRs breaks Terraform 0.13 + cidr_blocks = ["10.0.0.0/16"] + prefix_list_ids = [] + self = null + rules = [ + { + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + description = "Allow SSH access" + }, + { + type = "ingress" + from_port = 80 + to_port = 80 + protocol = "tcp" + description = "Allow HTTP access" + }, + ] + }] + + rules = [ + { + type = "ingress" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["10.0.0.0/8"] + ipv6_cidr_blocks = [module.vpc.ipv6_cidr_block] + source_security_group_id = null + description = "Discrete HTTPS ingress by CIDR" + self = null + }, + { + type = "ingress" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = [] + ipv6_cidr_blocks = [] + source_security_group_id = aws_security_group.existing.id + description = "Discrete HTTPS ingress for special SG" + self = null + }, + ] + + + vpc_id = module.vpc.vpc_id + + context = module.this.context +} + + +# Create rules for pre-created security group + +resource "aws_security_group" "existing" { + name_prefix = format("%s-%s-", module.this.id, "existing") + vpc_id = module.vpc.vpc_id + tags = module.this.tags +} diff --git a/examples/terraform-0.13/outputs.tf b/examples/terraform-0.13/outputs.tf new file mode 100644 index 0000000..a18ce4c --- /dev/null +++ b/examples/terraform-0.13/outputs.tf @@ -0,0 +1,14 @@ +output "created_sg_id" { + description = "The new one Security Group ID" + value = module.new_security_group.id +} + +output "created_sg_arn" { + description = "The new one Security Group ARN" + value = module.new_security_group.arn +} + +output "created_sg_name" { + description = "The new one Security Group Name" + value = module.new_security_group.name +} diff --git a/examples/terraform-0.13/variables.tf b/examples/terraform-0.13/variables.tf new file mode 100644 index 0000000..014ce51 --- /dev/null +++ b/examples/terraform-0.13/variables.tf @@ -0,0 +1,7 @@ +variable "region" { + type = string +} + +variable "rules" { + type = list(any) +} diff --git a/examples/terraform-0.13/versions.tf b/examples/terraform-0.13/versions.tf new file mode 100644 index 0000000..3b49b80 --- /dev/null +++ b/examples/terraform-0.13/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 0.13.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 2.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.0" + } + } +} diff --git a/exports/security_group_inputs.tf b/exports/security_group_inputs.tf new file mode 100644 index 0000000..c92e204 --- /dev/null +++ b/exports/security_group_inputs.tf @@ -0,0 +1,141 @@ +# security_group_inputs Version: 1 +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-aws-security-group +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-aws-security-group/blob/master/exports/security_group_inputs.tf +# +# This file provides the standard inputs that all Cloud Posse Open Source +# Terraform module that create AWS Security Groups should implement. +# This file does NOT provide implementation of the inputs, as that +# of course varies with each module. +# +# This file documents, but does not declare, the standard outputs modules should create, +# again because the implementation will vary with modules. +# +# Unlike null-label context.tf, this file cannot be automatically updated +# because of the tight integration with the module using it. +# + + +variable "create_security_group" { + type = bool + default = true + description = "Set `true` to create a new security group. If false, `target_security_group_id` must be provided." +} + +variable "target_security_group_id" { + type = string + default = "" + description = <<-EOT + The ID of an existing Security Group to which Security Group rules will be assigned. + Required if `create_security_group` is `false`, ignored otherwise. + EOT +} + +variable "security_group_name" { + type = string + default = "" + description = <<-EOT + The name to assign to the created security group. Must be unique within the account. + If not provided, will be derived from the `null-label.context` passed in. + If `create_before_destroy` is true, will be used as a name prefix. + EOT +} + +variable "security_group_description" { + type = string + default = "Managed by Terraform" + description = <<-EOT + The description to assign to the created Security Group. + Warning: Changing the description causes the security group to be replaced, which requires everything + associated with the security group to be replaced, which can be very disruptive. + EOT +} + +variable "security_group_create_before_destroy" { + type = bool + default = false + description = <<-EOT + Set `true` to enable terraform `create_before_destroy` behavior on the created security group. + We recommend setting this `true` on new security groups, but default it to `false` because `true` + will cause existing security groups to be replaced. + Note that changing this value will also cause the security group to be replaced. + EOT +} + +variable "security_group_create_timeout" { + type = string + default = "10m" + description = "How long to wait for the security group to be created." +} + +variable "security_group_delete_timeout" { + type = string + default = "15m" + description = <<-EOT + How long to retry on `DependencyViolation` errors during security group deletion from + lingering ENIs left by certain AWS services such as Elastic Load Balancing. + EOT +} + +# +# +#### The variables below can be omitted if not needed, and may need their descriptions modified +# +# +variable "vpc_id" { + type = string + description = "The ID of the VPC where the Security Group will be created." +} + +variable "revoke_security_group_rules_on_delete" { + type = bool + default = false + description = <<-EOF + Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting + the security group itself. This is normally not needed. + EOF +} + +variable "allow_all_egress" { + type = bool + default = true + description = <<-EOT + If `true`, the created security group will allow egress on all ports and protocols to all IP address. + If this is false and no egress rules are otherwise specified, then no egress will be allowed. + EOT +} + +variable "associated_security_group_ids" { + type = list(string) + default = [] + description = <<-EOT + A list of IDs of Security Groups to associate the created resource with, in addition to the created or target security group. + EOT +} + +variable "allowed_security_group_ids" { + type = list(string) + default = [] + description = <<-EOT + A list of IDs of Security Groups to allow access to the created resource. + EOT +} + +## +## +################# Outputs +## +## +# +# output "security_group_id" { +# value = "" +# description = "The ID of the created or target security group" +# } +# +# output "security_group_name" { +# value = "" +# description = "The name of the created or target security group" +# } diff --git a/main.tf b/main.tf index ca59fb3..13a8037 100644 --- a/main.tf +++ b/main.tf @@ -1,22 +1,23 @@ locals { - enabled = module.this.enabled + enabled = module.this.enabled + inline = var.inline_rules_enabled + + allow_all_egress = local.enabled && var.allow_all_egress + default_rule_description = "Managed by Terraform" + # Because Terraform formatting for `not` (!) changes between versions 0.13 and 0.14, use == false instead create_security_group = local.enabled && var.create_security_group - lookup_security_group = local.enabled && var.create_security_group == false + created_security_group = local.create_security_group ? ( var.create_before_destroy ? aws_security_group.cbd[0] : aws_security_group.default[0] ) : null + security_group_id = local.enabled ? ( # Use coalesce() here to hack an error message into the output var.create_security_group ? local.created_security_group.id : coalesce(var.target_security_group_id, "`create_security_group` is false, but no ID was supplied ") ) : null - - rules = local.enabled && var.rules != null ? var.rules : [] - - rule_matrix_rule_count = try(length(var.rule_matrix.rules), 0) - rule_matrix_enabled = local.enabled && local.rule_matrix_rule_count > 0 } # You cannot toggle `create_before_destroy` based on input, @@ -25,101 +26,136 @@ resource "aws_security_group" "default" { # Because Terraform formatting for `not` (!) changes between versions 0.13 and 0.14, use == false instead count = local.create_security_group && var.create_before_destroy == false ? 1 : 0 - name = coalesce(var.security_group_name, module.this.id) + name = coalesce(var.security_group_name, module.this.id) + + ######################################################################## + ## Everything from here to the end of this resource should be identical + ## (copy and paste) in aws_security_group.default and aws_security_group.cbd + description = var.security_group_description vpc_id = var.vpc_id - tags = merge(module.this.tags, length(var.security_group_name) > 0 ? { Name = var.security_group_name } : {}) + tags = try(length(var.security_group_name), 0) > 0 ? merge(module.this.tags, { Name = var.security_group_name }) : module.this.tags + + revoke_rules_on_delete = var.revoke_rules_on_delete + + dynamic "ingress" { + for_each = local.all_ingress_rules + content { + from_port = ingress.value.from_port + to_port = ingress.value.to_port + protocol = ingress.value.protocol + description = ingress.value.description + cidr_blocks = ingress.value.cidr_blocks + ipv6_cidr_blocks = ingress.value.ipv6_cidr_blocks + prefix_list_ids = ingress.value.prefix_list_ids + security_groups = ingress.value.security_groups + self = ingress.value.self + } + } + + dynamic "egress" { + for_each = local.all_egress_rules + content { + from_port = egress.value.from_port + to_port = egress.value.to_port + protocol = egress.value.protocol + description = egress.value.description + cidr_blocks = egress.value.cidr_blocks + ipv6_cidr_blocks = egress.value.ipv6_cidr_blocks + prefix_list_ids = egress.value.prefix_list_ids + security_groups = egress.value.security_groups + self = egress.value.self + } + } + + timeouts { + create = var.security_group_create_timeout + delete = var.security_group_delete_timeout + } + + ## + ## end of duplicate block + ######################################################################## + } resource "aws_security_group" "cbd" { # Because we use `== false` in the other resource, use `== true` for symmetry count = local.create_security_group && var.create_before_destroy == true ? 1 : 0 - name_prefix = coalesce(var.security_group_name, format("%s%s", module.this.id, module.this.delimiter)) - description = var.security_group_description - vpc_id = var.vpc_id - tags = merge(module.this.tags, length(var.security_group_name) > 0 ? { Name = var.security_group_name } : {}) - + name_prefix = coalesce(var.security_group_name, "${module.this.id}${module.this.delimiter}") lifecycle { create_before_destroy = true } -} -resource "aws_security_group_rule" "default" { - count = length(local.rules) - - security_group_id = local.security_group_id - type = local.rules[count.index].type - from_port = local.rules[count.index].from_port - to_port = local.rules[count.index].to_port - protocol = local.rules[count.index].protocol - description = lookup(local.rules[count.index], "description", local.default_rule_description) - # Convert a missing key, a value of null, or a value of empty list to null - cidr_blocks = try(length(lookup(local.rules[count.index], "cidr_blocks", [])), 0) > 0 ? local.rules[count.index]["cidr_blocks"] : null - ipv6_cidr_blocks = try(length(lookup(local.rules[count.index], "ipv6_cidr_blocks", [])), 0) > 0 ? local.rules[count.index]["ipv6_cidr_blocks"] : null - prefix_list_ids = try(length(lookup(local.rules[count.index], "prefix_list_ids", [])), 0) > 0 ? local.rules[count.index]["prefix_list_ids"] : null - self = coalesce(lookup(local.rules[count.index], "self", null), false) ? true : null - - source_security_group_id = lookup(local.rules[count.index], "source_security_group_id", null) -} - -resource "aws_security_group_rule" "self" { - # We use "== true" here because you cannot use `null` as a conditional, but null == true is OK - count = local.rule_matrix_enabled && try(var.rule_matrix.self, null) == true ? local.rule_matrix_rule_count : 0 + ######################################################################## + ## Everything from here to the end of this resource should be identical + ## (copy and paste) in aws_security_group.default and aws_security_group.cbd - security_group_id = local.security_group_id - type = var.rule_matrix.rules[count.index].type - from_port = var.rule_matrix.rules[count.index].from_port - to_port = var.rule_matrix.rules[count.index].to_port - protocol = var.rule_matrix.rules[count.index].protocol - description = try(var.rule_matrix.rules[count.index].description, local.default_rule_description) - - self = var.rule_matrix.self -} + description = var.security_group_description + vpc_id = var.vpc_id + tags = try(length(var.security_group_name), 0) > 0 ? merge(module.this.tags, { Name = var.security_group_name }) : module.this.tags + + revoke_rules_on_delete = var.revoke_rules_on_delete + + dynamic "ingress" { + for_each = local.all_ingress_rules + content { + from_port = ingress.value.from_port + to_port = ingress.value.to_port + protocol = ingress.value.protocol + description = ingress.value.description + cidr_blocks = ingress.value.cidr_blocks + ipv6_cidr_blocks = ingress.value.ipv6_cidr_blocks + prefix_list_ids = ingress.value.prefix_list_ids + security_groups = ingress.value.security_groups + self = ingress.value.self + } + } -resource "aws_security_group_rule" "sg" { - # source_security_group_ids may be unknown at plan time and there is no valid proxy for them, - # so there is no point in trying to come up with a static string key to use with for_each. - count = local.rule_matrix_enabled ? length(var.rule_matrix.source_security_group_ids) * local.rule_matrix_rule_count : 0 + dynamic "egress" { + for_each = local.all_egress_rules + content { + from_port = egress.value.from_port + to_port = egress.value.to_port + protocol = egress.value.protocol + description = egress.value.description + cidr_blocks = egress.value.cidr_blocks + ipv6_cidr_blocks = egress.value.ipv6_cidr_blocks + prefix_list_ids = egress.value.prefix_list_ids + security_groups = egress.value.security_groups + self = egress.value.self + } + } - security_group_id = local.security_group_id - type = var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].type - from_port = var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].from_port - to_port = var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].to_port - protocol = var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].protocol - description = try(var.rule_matrix.rules[count.index % local.rule_matrix_rule_count].description, local.default_rule_description) + timeouts { + create = var.security_group_create_timeout + delete = var.security_group_delete_timeout + } - source_security_group_id = var.rule_matrix.source_security_group_ids[floor(count.index / local.rule_matrix_rule_count)] -} + ## + ## end of duplicate block + ######################################################################## -resource "aws_security_group_rule" "cidr" { - count = local.rule_matrix_enabled && (try(length(var.rule_matrix.cidr_blocks), 0) + - try(length(var.rule_matrix.ipv6_cidr_blocks), 0) + - try(length(var.rule_matrix.prefix_list_ids), 0)) > 0 ? local.rule_matrix_rule_count : 0 - - security_group_id = local.security_group_id - type = "ingress" - cidr_blocks = try(length(var.rule_matrix.cidr_blocks), 0) > 0 ? var.rule_matrix.cidr_blocks : null - ipv6_cidr_blocks = try(length(var.rule_matrix.ipv6_cidr_blocks), 0) > 0 ? var.rule_matrix.ipv6_cidr_blocks : null - prefix_list_ids = try(length(var.rule_matrix.prefix_list_ids), 0) > 0 ? var.rule_matrix.prefix_list_ids : null - from_port = var.rule_matrix.rules[count.index].from_port - to_port = var.rule_matrix.rules[count.index].to_port - protocol = var.rule_matrix.rules[count.index].protocol - description = try(var.rule_matrix.rules[count.index].description, local.default_rule_description) } +resource "aws_security_group_rule" "discrete" { + # We cannot use for_each here because some of the values in resource_rules may not be available at plan time + count = length(local.resource_rules) -resource "aws_security_group_rule" "egress" { - count = local.enabled && var.allow_all_egress ? 1 : 0 + type = local.resource_rules[count.index].type + from_port = local.resource_rules[count.index].from_port + to_port = local.resource_rules[count.index].to_port + protocol = local.resource_rules[count.index].protocol + description = local.resource_rules[count.index].description + cidr_blocks = length(local.resource_rules[count.index].cidr_blocks) == 0 ? null : local.resource_rules[count.index].cidr_blocks + ipv6_cidr_blocks = length(local.resource_rules[count.index].ipv6_cidr_blocks) == 0 ? null : local.resource_rules[count.index].ipv6_cidr_blocks + prefix_list_ids = length(local.resource_rules[count.index].prefix_list_ids) == 0 ? null : local.resource_rules[count.index].prefix_list_ids + self = local.resource_rules[count.index].self - security_group_id = local.security_group_id + security_group_id = local.security_group_id + source_security_group_id = local.resource_rules[count.index].source_security_group_id - # Copied from https://registry.terraform.io/providers/hashicorp/aws/3.46.0/docs/resources/security_group#example-usage - type = "egress" - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - ipv6_cidr_blocks = ["::/0"] - description = "Allow all egress" + depends_on = [aws_security_group.cbd, aws_security_group.default] } + diff --git a/normalize.tf b/normalize.tf new file mode 100644 index 0000000..69fb81c --- /dev/null +++ b/normalize.tf @@ -0,0 +1,146 @@ +# In this file, we normalize all the rules into full objects with all keys. +# Then we partition the normalized rules for use as either inline or resourced rules. + +locals { + + # 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 + norm_rules = local.enabled ? [for rule in var.rules : { + type = rule.type + from_port = rule.from_port + to_port = rule.to_port + protocol = rule.protocol + description = lookup(rule, "description", local.default_rule_description) + + # Convert a missing key, a value of null, or a value of empty list to [] + cidr_blocks = try(length(rule.cidr_blocks), 0) > 0 ? rule["cidr_blocks"] : [] + ipv6_cidr_blocks = try(length(rule.ipv6_cidr_blocks), 0) > 0 ? rule["ipv6_cidr_blocks"] : [] + prefix_list_ids = try(length(rule.prefix_list_ids), 0) > 0 ? rule["prefix_list_ids"] : [] + + source_security_group_id = lookup(rule, "source_security_group_id", null) + security_groups = [] + + self = lookup(rule, "self", null) + }] : [] + + # in rule_matrix and inline rules, a single rule can have a list of security groups + norm_matrix = local.enabled ? concat(concat([[]], [for subject in var.rule_matrix : [for rule in subject.rules : { + type = rule.type + from_port = rule.from_port + to_port = rule.to_port + protocol = rule.protocol + description = lookup(rule, "description", local.default_rule_description) + + # We tried to be lenient and convert a missing key, a value of null, or a value of empty list to [] + # with cidr_blocks = try(length(rule.cidr_blocks), 0) > 0 ? rule["cidr_blocks"] : [] + # but if a list is provided and any value in the list is not available at plan time, + # that formulation causes problems for `count`, so we must forbid keys present with value of null. + + cidr_blocks = lookup(subject, "cidr_blocks", []) + ipv6_cidr_blocks = lookup(subject, "ipv6_cidr_blocks", []) + prefix_list_ids = lookup(subject, "prefix_list_ids", []) + + source_security_group_id = null + security_groups = lookup(subject, "source_security_group_ids", []) + + self = lookup(subject, "self", null) + }]])...) : [] + + allow_egress_rule = { + type = "egress" + from_port = 0 + to_port = 0 # [sic] from and to port ignored when protocol is "-1", warning if not zero + protocol = "-1" + description = "Allow all egress" + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + prefix_list_ids = [] + self = null + security_groups = [] + source_security_group_id = null + } + + all_inline_rules = concat(local.norm_rules, local.norm_matrix, local.allow_all_egress ? [local.allow_egress_rule] : []) + + # For inline rules, the rules have to be separated into ingress and egress + all_ingress_rules = local.inline ? [for r in local.all_inline_rules : r if r.type == "ingress"] : [] + all_egress_rules = local.inline ? [for r in local.all_inline_rules : r if r.type == "egress"] : [] + + # In `aws_security_group_rule` a rule can only have one security group, not a list, so we have to explode the matrix + # Also, self, source_security_group_id, and CIDRs conflict with each other, so they have to be separated out. + # We must be very careful not to make the computed number of rules in any way dependant + # on a computed input value, we must stick to counting things. + + self_rules = local.inline ? [] : [for rule in local.norm_matrix : { + type = rule.type + from_port = rule.from_port + to_port = rule.to_port + protocol = rule.protocol + description = rule.description + + cidr_blocks = [] + ipv6_cidr_blocks = [] + prefix_list_ids = [] + self = rule.self + + security_groups = [] + source_security_group_id = null + + # To preserve count and order of rules, create rules for `false` if though they do nothing, + # so that toggling to true does not have ripple effects. + } if rule.self != null] + + other_rules = local.inline ? [] : [for rule in local.norm_matrix : { + type = rule.type + from_port = rule.from_port + to_port = rule.to_port + protocol = rule.protocol + description = rule.description + + cidr_blocks = rule.cidr_blocks + ipv6_cidr_blocks = rule.ipv6_cidr_blocks + prefix_list_ids = rule.prefix_list_ids + self = null + + security_groups = [] + source_security_group_id = null + } if length(rule.cidr_blocks) + length(rule.ipv6_cidr_blocks) + length(rule.prefix_list_ids) > 0] + + + # First, collect all the rules with lists of security groups + sg_rules_lists = local.inline ? [] : [for rule in local.all_inline_rules : { + type = rule.type + from_port = rule.from_port + to_port = rule.to_port + protocol = rule.protocol + description = rule.description + + cidr_blocks = [] + ipv6_cidr_blocks = [] + prefix_list_ids = [] + self = null + security_groups = rule.security_groups + } if length(rule.security_groups) > 0] + + # Now we have to explode the lists into individual rules + sg_exploded_rules = flatten([for rule in local.sg_rules_lists : [for sg in rule.security_groups : { + type = rule.type + from_port = rule.from_port + to_port = rule.to_port + protocol = rule.protocol + description = rule.description + + cidr_blocks = [] + ipv6_cidr_blocks = [] + prefix_list_ids = [] + self = null + + security_groups = [] + source_security_group_id = sg + }]]) + + resource_rules = concat(local.norm_rules, local.self_rules, local.sg_exploded_rules, local.other_rules) +} + + diff --git a/outputs.tf b/outputs.tf index 77c5c40..e91e295 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,14 +1,15 @@ + output "id" { - description = "The created Security Group ID" - value = try(local.created_security_group.id, null) + description = "The created or target Security Group ID" + value = local.security_group_id } output "arn" { - description = "The created Security Group ARN" + description = "The created Security Group ARN (null if using existing security group)" value = try(local.created_security_group.arn, null) } output "name" { - description = "The created Security Group Name" + description = "The created Security Group Name (null if using existing security group)" value = try(local.created_security_group.name, null) } diff --git a/test/src/Makefile b/test/src/Makefile index b772822..11c0a16 100644 --- a/test/src/Makefile +++ b/test/src/Makefile @@ -16,7 +16,7 @@ init: ## Run tests test: init go mod download - go test -v -timeout 60m -run TestExamplesComplete + go test -v -timeout 15m -run TestExamplesComplete ## Run tests in docker container docker/test: diff --git a/variables.tf b/variables.tf index 12dcff9..c7816e7 100644 --- a/variables.tf +++ b/variables.tf @@ -1,16 +1,26 @@ -variable "vpc_id" { +variable "create_security_group" { + type = bool + default = true + description = "Set `true` to create a new security group. If false, `target_security_group_id` must be provided." +} + +variable "target_security_group_id" { type = string - description = "The VPC ID where Security Group will be created." + default = "" + description = <<-EOT + The ID of an existing Security Group to which Security Group rules will be assigned. + Required if `create_security_group` is `false`, ignored otherwise. + EOT } variable "security_group_name" { type = string default = "" description = <<-EOT - The name to assign to the security group. Must be unique within the account. + The name to assign to the security group. Must be unique within the VPC. If not provided, will be derived from the `null-label.context` passed in. If `create_before_destroy` is true, will be used as a name prefix. - EOT + EOT } variable "security_group_description" { @@ -23,27 +33,23 @@ variable "security_group_description" { EOT } -variable "create_security_group" { +variable "create_before_destroy" { type = bool - default = true - description = "Set `true` to create a new security group. If false, `existing_security_group_id` must be provided." -} - -variable "target_security_group_id" { - type = string - default = "" + default = false description = <<-EOT - The ID of an existing Security Group to which Security Group rules will be assigned. - Required if `create_security_group` is `false`, ignored otherwise. - EOT + Set `true` to enable terraform `create_before_destroy` behavior on the created security group. + We recommend setting this `true` on new security groups, but default it to `false` because `true` + will cause existing security groups to be replaced. + Note that changing this value will also cause the security group to be replaced. + EOT } -variable "create_before_destroy" { +variable "allow_all_egress" { type = bool default = false description = <<-EOT - Set `true` to enable terraform `create_before_destroy` behavior. - Note that changing this value will change the security group name and cause the security group to be replaced. + A convenience that adds to the rules specified elsewhere a rule that allows all egress. + If this is false and no egress rules are specified via `rules` or `rule-matrix`, then no egress will be allowed. EOT } @@ -55,21 +61,12 @@ variable "rules" { The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except for `security_group_id` which will be ignored. To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . - EOT -} - -variable "allow_all_egress" { - type = bool - default = false - description = <<-EOT - A convenience that adds to the rules specified elsewhere a rule that allows all egress. - If this is false and no egress rules are specified via `rules` or `rule-matrix`, then no egress will be allowed. EOT } variable "rule_matrix" { # rule_matrix is independent of the `rules` input. - # Only the rules specified in the `rule_matrix` object are applied to the subjects. + # Only the rules specified in the `rule_matrix` object are applied to the subjects specified in `rule_matrix`. # Schema: # { # # these top level lists define all the subjects to which rule_matrix rules will be applied @@ -81,16 +78,56 @@ variable "rule_matrix" { # # # each rule in the rules list will be applied to every subject defined above # rules = [{ - # type = "egress" - # from_port = 0 - # to_port = 65535 - # protocol = "all" - # description = "Allow full egress" + # type = "ingress" + # from_port = 433 + # to_port = 433 + # protocol = "tcp" + # description = "Allow HTTPS ingress" # }] type = any - default = { rules = [] } + default = [] description = <<-EOT A convenient way to apply the same set of rules to a set of subjects. See README for details. EOT } + +variable "security_group_create_timeout" { + type = string + default = "10m" + description = "How long to wait for the security group to be created." +} + +variable "security_group_delete_timeout" { + type = string + default = "15m" + description = <<-EOT + How long to retry on `DependencyViolation` errors during security group deletion from + lingering ENIs left by certain AWS services such as Elastic Load Balancing. + EOT +} + +variable "revoke_rules_on_delete" { + type = bool + default = false + description = <<-EOT + Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting + the security group itself. This is normally not needed. + EOT +} + +variable "vpc_id" { + type = string + description = "The ID of the VPC where the Security Group will be created." +} + +variable "inline_rules_enabled" { + type = bool + default = false + description = <<-EOT + NOT RECOMMENDED. Create rules "inline" instead of as separate `aws_security_group_rule` resources. + See [#20046](https://github.com/hashicorp/terraform-provider-aws/issues/20046) for one of several issues with inline rules. + See [this post](https://github.com/hashicorp/terraform-provider-aws/pull/9032#issuecomment-639545250) for details on the difference between inline rules and rule resources. + EOT +} + diff --git a/versions.tf b/versions.tf index 5b2c49b..85d1d00 100644 --- a/versions.tf +++ b/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 2.0" + version = ">= 3.0" } } } From ee5e938454d1d4c147d12cd90b92b0914d8b7c88 Mon Sep 17 00:00:00 2001 From: Nuru Date: Sat, 10 Jul 2021 02:22:24 -0700 Subject: [PATCH 12/19] Enable tagging of rules for stability, completely drop TF 0.13 --- README.md | 33 ++- README.yaml | 26 ++- docs/terraform.md | 7 +- examples/complete/fixtures.us-east-2.tfvars | 2 + examples/complete/main.tf | 15 +- examples/complete/variables.tf | 6 + examples/terraform-0.13/context.tf | 202 ------------------ .../terraform-0.13/fixtures.us-east-2.tfvars | 28 --- examples/terraform-0.13/main.tf | 89 -------- examples/terraform-0.13/outputs.tf | 14 -- examples/terraform-0.13/variables.tf | 7 - examples/terraform-0.13/versions.tf | 14 -- main.tf | 33 ++- normalize.tf | 21 +- outputs.tf | 5 + variables.tf | 4 +- versions.tf | 2 +- 17 files changed, 106 insertions(+), 402 deletions(-) delete mode 100644 examples/terraform-0.13/context.tf delete mode 100644 examples/terraform-0.13/fixtures.us-east-2.tfvars delete mode 100644 examples/terraform-0.13/main.tf delete mode 100644 examples/terraform-0.13/outputs.tf delete mode 100644 examples/terraform-0.13/variables.tf delete mode 100644 examples/terraform-0.13/versions.tf diff --git a/README.md b/README.md index 2a491e8..792ed3d 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,10 @@ group and apply the given rules to it. ##### `rules` input This module provides 2 ways to set security group rules. The `rules` input takes a list of rule maps. The maps are compatible with (have the same keys and accept the same values) as the -Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). +Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule), +except that `security_group_id` will be ignored, and the map can include an optional "key" which, if provided, must have a unique value. +The "key" value, if provided, must be something Terraform can know the value of at "apply" time, and is used to keep the +rule from being affected by its place in the list. See ["Unexplained changes..."](#unexpected-changes-during-plan-and-apply) below for more details. While some of the map keys are optional, Terraform requires that all of the maps in a single list have exactly the same set of keys. See [WARNINGS and error messages](#warnings-and-error-messages) below for details. @@ -108,17 +111,19 @@ See [WARNINGS and error messages](#warnings-and-error-messages) below for detail The other way to set rules is via the `rule_matrix` input. This splits the keys of the `aws_security_group_rule` resource into to sets: one set defines the rule and descripition, the other set defines the subjects of the rule. As with `rules` and explained in the previous paragraph, all elements of the list must have all the same keys. This also holds -for all the elements of the `rules_matrix.rules` list. +for all the elements of the `rules_matrix.rules` list. Again, optional "key" values can provide stability, but +cannot contain derived values. Any map key that takes a list value must either be absent from all lists or contain lists in all lists. Use an empty list rather than `null` to indicate "no value". Passing in `null` instead of a list -may cause Terraform v0.13 to crash and may cause other errors in later Terraform versions. +may cause Terraform to crash or omit confusing error messages (e.g. "number is required"). -The schema for the `rule matrix` is: +The schema for `rule_matrix` is: ```hcl { # these top level lists define all the subjects to which rule_matrix rules will be applied + key = an optional unique key to keep these rules from being affected when other rules change source_security_group_ids = list of source security group IDs to apply all rules to cidr_blocks = list of ipv4 CIDR blocks to apply all rules to ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to @@ -128,6 +133,7 @@ The schema for the `rule matrix` is: # each rule in the rules list will be applied to every subject defined above rules = [{ + key = an optional unique key to keep this rule from being affected when other rules change type = type of rule, either "ingress" or "egress" from_port = start range of protocol port to_port = end range of protocol port, max is 65535 @@ -151,8 +157,13 @@ The way Terraform works and the way this module is implemented causes security g to be dependent on their place in the input lists. If a rule is deleted and the other rules therefore move closer to the start of the list, those rules will be deleted and recreated. This should have no significant operational impact, but it can make a small change look like a big one when viewing the output of -Terraform plan. After careful consideration, we have decided that this is preferable to the -impositions and limitations that would come from a solution that avoids it. +Terraform plan. + +You can avoid this for the most part by providing the optional keys. Rules with keys will not be +changed if their keys do not change and the rules themselves do not change, except in the case of +`rule_matrix`, where the rules are still dependent on the order of the security groups in +`source_security_group_ids`. You can avoid this by using `rules` instead of `rule_matrix` when you have +more than one security group in the list. ##### WARNINGS and error messages @@ -234,6 +245,7 @@ module "sg" { rules = [ { + key = "ssh" type = "ingress" from_port = 22 to_port = 22 @@ -243,6 +255,7 @@ module "sg" { description = "Allow SSH from anywhere" }, { + key = "HTTP" type = "ingress" from_port = 80 to_port = 80 @@ -276,6 +289,7 @@ module "sg_mysql" { prefix_list_ids = [var.mysql_client_prefix_list_id] rules = [ { + key = "mysql" type = "ingress" from_port = 3306 to_port = 3306 @@ -320,7 +334,7 @@ Available targets: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 0.13.0 | +| [terraform](#requirement\_terraform) | >= 0.14.0 | | [aws](#requirement\_aws) | >= 3.0 | ## Providers @@ -341,7 +355,7 @@ Available targets: |------|------| | [aws_security_group.cbd](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | -| [aws_security_group_rule.discrete](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.keyed](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | ## Inputs @@ -366,7 +380,7 @@ Available targets: | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [revoke\_rules\_on\_delete](#input\_revoke\_rules\_on\_delete) | Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting
the security group itself. This is normally not needed. | `bool` | `false` | no | | [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` | `[]` | no | -| [rules](#input\_rules) | A list of maps of Security Group rules.
The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | +| [rules](#input\_rules) | A list of maps of Security Group rules.
The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | | [security\_group\_create\_timeout](#input\_security\_group\_create\_timeout) | How long to wait for the security group to be created. | `string` | `"10m"` | no | | [security\_group\_delete\_timeout](#input\_security\_group\_delete\_timeout) | How long to retry on `DependencyViolation` errors during security group deletion from
lingering ENIs left by certain AWS services such as Elastic Load Balancing. | `string` | `"15m"` | no | | [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced, which requires everything
associated with the security group to be replaced, which can be very disruptive. | `string` | `"Managed by Terraform"` | no | @@ -383,6 +397,7 @@ Available targets: | [arn](#output\_arn) | The created Security Group ARN (null if using existing security group) | | [id](#output\_id) | The created or target Security Group ID | | [name](#output\_name) | The created Security Group Name (null if using existing security group) | +| [rules\_terraform\_ids](#output\_rules\_terraform\_ids) | List of Terraform IDs of created `security_group_rule` resources, primarily provided to enable `depends_on` | diff --git a/README.yaml b/README.yaml index 16a2fbe..c49f0a1 100644 --- a/README.yaml +++ b/README.yaml @@ -67,7 +67,10 @@ usage: |- ##### `rules` input This module provides 2 ways to set security group rules. The `rules` input takes a list of rule maps. The maps are compatible with (have the same keys and accept the same values) as the - Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). + Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule), + except that `security_group_id` will be ignored, and the map can include an optional "key" which, if provided, must have a unique value. + The "key" value, if provided, must be something Terraform can know the value of at "apply" time, and is used to keep the + rule from being affected by its place in the list. See ["Unexplained changes..."](#unexpected-changes-during-plan-and-apply) below for more details. While some of the map keys are optional, Terraform requires that all of the maps in a single list have exactly the same set of keys. See [WARNINGS and error messages](#warnings-and-error-messages) below for details. @@ -75,17 +78,19 @@ usage: |- The other way to set rules is via the `rule_matrix` input. This splits the keys of the `aws_security_group_rule` resource into to sets: one set defines the rule and descripition, the other set defines the subjects of the rule. As with `rules` and explained in the previous paragraph, all elements of the list must have all the same keys. This also holds - for all the elements of the `rules_matrix.rules` list. + for all the elements of the `rules_matrix.rules` list. Again, optional "key" values can provide stability, but + cannot contain derived values. Any map key that takes a list value must either be absent from all lists or contain lists in all lists. Use an empty list rather than `null` to indicate "no value". Passing in `null` instead of a list - may cause Terraform v0.13 to crash and may cause other errors in later Terraform versions. + may cause Terraform to crash or omit confusing error messages (e.g. "number is required"). - The schema for the `rule matrix` is: + The schema for `rule_matrix` is: ```hcl { # these top level lists define all the subjects to which rule_matrix rules will be applied + key = an optional unique key to keep these rules from being affected when other rules change source_security_group_ids = list of source security group IDs to apply all rules to cidr_blocks = list of ipv4 CIDR blocks to apply all rules to ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to @@ -95,6 +100,7 @@ usage: |- # each rule in the rules list will be applied to every subject defined above rules = [{ + key = an optional unique key to keep this rule from being affected when other rules change type = type of rule, either "ingress" or "egress" from_port = start range of protocol port to_port = end range of protocol port, max is 65535 @@ -118,8 +124,13 @@ usage: |- to be dependent on their place in the input lists. If a rule is deleted and the other rules therefore move closer to the start of the list, those rules will be deleted and recreated. This should have no significant operational impact, but it can make a small change look like a big one when viewing the output of - Terraform plan. After careful consideration, we have decided that this is preferable to the - impositions and limitations that would come from a solution that avoids it. + Terraform plan. + + You can avoid this for the most part by providing the optional keys. Rules with keys will not be + changed if their keys do not change and the rules themselves do not change, except in the case of + `rule_matrix`, where the rules are still dependent on the order of the security groups in + `source_security_group_ids`. You can avoid this by using `rules` instead of `rule_matrix` when you have + more than one security group in the list. ##### WARNINGS and error messages @@ -201,6 +212,7 @@ usage: |- rules = [ { + key = "ssh" type = "ingress" from_port = 22 to_port = 22 @@ -210,6 +222,7 @@ usage: |- description = "Allow SSH from anywhere" }, { + key = "HTTP" type = "ingress" from_port = 80 to_port = 80 @@ -243,6 +256,7 @@ usage: |- prefix_list_ids = [var.mysql_client_prefix_list_id] rules = [ { + key = "mysql" type = "ingress" from_port = 3306 to_port = 3306 diff --git a/docs/terraform.md b/docs/terraform.md index 72d2032..2050703 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -3,7 +3,7 @@ | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 0.13.0 | +| [terraform](#requirement\_terraform) | >= 0.14.0 | | [aws](#requirement\_aws) | >= 3.0 | ## Providers @@ -24,7 +24,7 @@ |------|------| | [aws_security_group.cbd](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | -| [aws_security_group_rule.discrete](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.keyed](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | ## Inputs @@ -49,7 +49,7 @@ | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [revoke\_rules\_on\_delete](#input\_revoke\_rules\_on\_delete) | Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting
the security group itself. This is normally not needed. | `bool` | `false` | no | | [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` | `[]` | no | -| [rules](#input\_rules) | A list of maps of Security Group rules.
The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | +| [rules](#input\_rules) | A list of maps of Security Group rules.
The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | | [security\_group\_create\_timeout](#input\_security\_group\_create\_timeout) | How long to wait for the security group to be created. | `string` | `"10m"` | no | | [security\_group\_delete\_timeout](#input\_security\_group\_delete\_timeout) | How long to retry on `DependencyViolation` errors during security group deletion from
lingering ENIs left by certain AWS services such as Elastic Load Balancing. | `string` | `"15m"` | no | | [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced, which requires everything
associated with the security group to be replaced, which can be very disruptive. | `string` | `"Managed by Terraform"` | no | @@ -66,4 +66,5 @@ | [arn](#output\_arn) | The created Security Group ARN (null if using existing security group) | | [id](#output\_id) | The created or target Security Group ID | | [name](#output\_name) | The created Security Group Name (null if using existing security group) | +| [rules\_terraform\_ids](#output\_rules\_terraform\_ids) | List of Terraform IDs of created `security_group_rule` resources, primarily provided to enable `depends_on` | diff --git a/examples/complete/fixtures.us-east-2.tfvars b/examples/complete/fixtures.us-east-2.tfvars index 9624fb2..f732faa 100644 --- a/examples/complete/fixtures.us-east-2.tfvars +++ b/examples/complete/fixtures.us-east-2.tfvars @@ -10,6 +10,7 @@ name = "sg" rules = [ { + key = null # "ssh all" type = "ingress" from_port = 22 to_port = 22 @@ -18,6 +19,7 @@ rules = [ description = "SSH wide open" }, { + key = "https all" type = "ingress" from_port = 443 to_port = 443 diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 988df73..8ecd705 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -14,8 +14,8 @@ module "vpc" { } resource "random_integer" "coin" { - max = 1 - min = 0 + max = 2 + min = 1 } # Create one new security group @@ -23,15 +23,16 @@ resource "random_integer" "coin" { module "new_security_group" { source = "../.." - allow_all_egress = true + allow_all_egress = true + inline_rules_enabled = var.inline_rules_enabled rule_matrix = [{ + key = "stable" # Allow ingress on ports 22 and 80 from created security group, existing security group, and CIDR "10.0.0.0/8" - # The dynamic value for source_security_group_ids breaks Terraform 0.13 but should work in 0.14 or later source_security_group_ids = [aws_security_group.existing.id] # Either dynamic value for CIDRs breaks Terraform 0.13 but should work in 0.14 or later - cidr_blocks = random_integer.coin.result > 0 ? ["10.0.0.0/16"] : ["10.0.0.0/24"] + cidr_blocks = random_integer.coin.result > 1 ? ["10.0.0.0/16"] : ["10.0.0.0/24"] ipv6_cidr_blocks = [module.vpc.ipv6_cidr_block] prefix_list_ids = [] @@ -41,6 +42,7 @@ module "new_security_group" { self = var.rule_matrix_self rules = [ { + key = "ssh" type = "ingress" from_port = 22 to_port = 22 @@ -48,6 +50,7 @@ module "new_security_group" { description = "Allow SSH access" }, { + # key = "http" type = "ingress" from_port = 80 to_port = 80 @@ -59,6 +62,7 @@ module "new_security_group" { rules = [ { + key = "https-cidr" type = "ingress" from_port = 443 to_port = 443 @@ -70,6 +74,7 @@ module "new_security_group" { self = null }, { + key = null # "https-sg" type = "ingress" from_port = 443 to_port = 443 diff --git a/examples/complete/variables.tf b/examples/complete/variables.tf index 9533ad0..dfc3b68 100644 --- a/examples/complete/variables.tf +++ b/examples/complete/variables.tf @@ -11,3 +11,9 @@ variable "rule_matrix_self" { description = "Value to set `self` in `rule_matrix` test rule" default = null } + +variable "inline_rules_enabled" { + type = bool + description = "Value to set true to test inline security group rules" + default = false +} diff --git a/examples/terraform-0.13/context.tf b/examples/terraform-0.13/context.tf deleted file mode 100644 index 81f99b4..0000000 --- a/examples/terraform-0.13/context.tf +++ /dev/null @@ -1,202 +0,0 @@ -# -# 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. -# -# 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.24.1" # requires Terraform >= 0.13.0 - - enabled = var.enabled - namespace = var.namespace - 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 - - context = var.context -} - -# Copy contents of cloudposse/terraform-null-label/variables.tf here - -variable "context" { - type = any - default = { - enabled = true - namespace = 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 - } - 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 = "Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp'" -} - -variable "environment" { - type = string - default = null - description = "Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT'" -} - -variable "stage" { - type = string - default = null - description = "Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release'" -} - -variable "name" { - type = string - default = null - description = "Solution name, e.g. 'app' or 'jenkins'" -} - -variable "delimiter" { - type = string - default = null - description = <<-EOT - Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`. - Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. - EOT -} - -variable "attributes" { - type = list(string) - default = [] - description = "Additional attributes (e.g. `1`)" -} - -variable "tags" { - type = map(string) - default = {} - description = "Additional tags (e.g. `map('BusinessUnit','XYZ')`" -} - -variable "additional_tag_map" { - type = map(string) - default = {} - description = "Additional tags for appending to tags_as_list_of_maps. Not added to `tags`." -} - -variable "label_order" { - type = list(string) - default = null - description = <<-EOT - The naming order of the id output and Name tag. - Defaults to ["namespace", "environment", "stage", "name", "attributes"]. - You can omit any of the 5 elements, 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`. - 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 default, which is `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 - The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`. - 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 - The letter case of output label values (also used in `tags` and `id`). - Possible values: `lower`, `title`, `upper` and `none` (no transformation). - 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`." - } -} -#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/examples/terraform-0.13/fixtures.us-east-2.tfvars b/examples/terraform-0.13/fixtures.us-east-2.tfvars deleted file mode 100644 index 9624fb2..0000000 --- a/examples/terraform-0.13/fixtures.us-east-2.tfvars +++ /dev/null @@ -1,28 +0,0 @@ -region = "us-east-2" - -namespace = "eg" - -environment = "ue2" - -stage = "test" - -name = "sg" - -rules = [ - { - type = "ingress" - from_port = 22 - to_port = 22 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - description = "SSH wide open" - }, - { - type = "ingress" - from_port = 443 - to_port = 443 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - description = "HTTPS wide open" - } -] diff --git a/examples/terraform-0.13/main.tf b/examples/terraform-0.13/main.tf deleted file mode 100644 index 87a6638..0000000 --- a/examples/terraform-0.13/main.tf +++ /dev/null @@ -1,89 +0,0 @@ -provider "aws" { - region = var.region -} - -module "vpc" { - source = "cloudposse/vpc/aws" - version = "v0.25.0" - - cidr_block = "10.0.0.0/24" - - assign_generated_ipv6_cidr_block = true - - context = module.this.context -} - -# Create one new security group - -module "new_security_group" { - source = "../.." - - allow_all_egress = true - - rule_matrix = [{ - # Allow ingress on ports 22 and 80 from created security group, existing security group, and CIDR "10.0.0.0/8" - - # A derived value for source_security_group_ids breaks Terraform 0.13 - # source_security_group_ids = [aws_security_group.existing.id] - source_security_group_ids = [] - # The dynamic value for CIDRs breaks Terraform 0.13 - cidr_blocks = ["10.0.0.0/16"] - prefix_list_ids = [] - self = null - rules = [ - { - type = "ingress" - from_port = 22 - to_port = 22 - protocol = "tcp" - description = "Allow SSH access" - }, - { - type = "ingress" - from_port = 80 - to_port = 80 - protocol = "tcp" - description = "Allow HTTP access" - }, - ] - }] - - rules = [ - { - type = "ingress" - from_port = 443 - to_port = 443 - protocol = "tcp" - cidr_blocks = ["10.0.0.0/8"] - ipv6_cidr_blocks = [module.vpc.ipv6_cidr_block] - source_security_group_id = null - description = "Discrete HTTPS ingress by CIDR" - self = null - }, - { - type = "ingress" - from_port = 443 - to_port = 443 - protocol = "tcp" - cidr_blocks = [] - ipv6_cidr_blocks = [] - source_security_group_id = aws_security_group.existing.id - description = "Discrete HTTPS ingress for special SG" - self = null - }, - ] - - - vpc_id = module.vpc.vpc_id - - context = module.this.context -} - - -# Create rules for pre-created security group - -resource "aws_security_group" "existing" { - name_prefix = format("%s-%s-", module.this.id, "existing") - vpc_id = module.vpc.vpc_id - tags = module.this.tags -} diff --git a/examples/terraform-0.13/outputs.tf b/examples/terraform-0.13/outputs.tf deleted file mode 100644 index a18ce4c..0000000 --- a/examples/terraform-0.13/outputs.tf +++ /dev/null @@ -1,14 +0,0 @@ -output "created_sg_id" { - description = "The new one Security Group ID" - value = module.new_security_group.id -} - -output "created_sg_arn" { - description = "The new one Security Group ARN" - value = module.new_security_group.arn -} - -output "created_sg_name" { - description = "The new one Security Group Name" - value = module.new_security_group.name -} diff --git a/examples/terraform-0.13/variables.tf b/examples/terraform-0.13/variables.tf deleted file mode 100644 index 014ce51..0000000 --- a/examples/terraform-0.13/variables.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "region" { - type = string -} - -variable "rules" { - type = list(any) -} diff --git a/examples/terraform-0.13/versions.tf b/examples/terraform-0.13/versions.tf deleted file mode 100644 index 3b49b80..0000000 --- a/examples/terraform-0.13/versions.tf +++ /dev/null @@ -1,14 +0,0 @@ -terraform { - required_version = ">= 0.13.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = ">= 2.0" - } - random = { - source = "hashicorp/random" - version = ">= 3.0" - } - } -} diff --git a/main.tf b/main.tf index 13a8037..1c55cc9 100644 --- a/main.tf +++ b/main.tf @@ -6,7 +6,6 @@ locals { default_rule_description = "Managed by Terraform" - # Because Terraform formatting for `not` (!) changes between versions 0.13 and 0.14, use == false instead create_security_group = local.enabled && var.create_security_group created_security_group = local.create_security_group ? ( @@ -16,14 +15,14 @@ locals { security_group_id = local.enabled ? ( # Use coalesce() here to hack an error message into the output var.create_security_group ? local.created_security_group.id : coalesce(var.target_security_group_id, - "`create_security_group` is false, but no ID was supplied ") + "`create_security_group` is false, but no security group ID was supplied ") ) : null } # You cannot toggle `create_before_destroy` based on input, # you have to have a completely separate resource to change it. resource "aws_security_group" "default" { - # Because Terraform formatting for `not` (!) changes between versions 0.13 and 0.14, use == false instead + # Because we have 2 almost identical alternatives, use x == false and x == true rather than x and !x count = local.create_security_group && var.create_before_destroy == false ? 1 : 0 name = coalesce(var.security_group_name, module.this.id) @@ -80,7 +79,7 @@ resource "aws_security_group" "default" { } resource "aws_security_group" "cbd" { - # Because we use `== false` in the other resource, use `== true` for symmetry + # Because we have 2 almost identical alternatives, use x == false and x == true rather than x and !x count = local.create_security_group && var.create_before_destroy == true ? 1 : 0 name_prefix = coalesce(var.security_group_name, "${module.this.id}${module.this.delimiter}") @@ -139,23 +138,21 @@ resource "aws_security_group" "cbd" { } -resource "aws_security_group_rule" "discrete" { - # We cannot use for_each here because some of the values in resource_rules may not be available at plan time - count = length(local.resource_rules) +resource "aws_security_group_rule" "keyed" { + for_each = local.keyed_resource_rules - type = local.resource_rules[count.index].type - from_port = local.resource_rules[count.index].from_port - to_port = local.resource_rules[count.index].to_port - protocol = local.resource_rules[count.index].protocol - description = local.resource_rules[count.index].description - cidr_blocks = length(local.resource_rules[count.index].cidr_blocks) == 0 ? null : local.resource_rules[count.index].cidr_blocks - ipv6_cidr_blocks = length(local.resource_rules[count.index].ipv6_cidr_blocks) == 0 ? null : local.resource_rules[count.index].ipv6_cidr_blocks - prefix_list_ids = length(local.resource_rules[count.index].prefix_list_ids) == 0 ? null : local.resource_rules[count.index].prefix_list_ids - self = local.resource_rules[count.index].self + type = each.value.type + from_port = each.value.from_port + to_port = each.value.to_port + protocol = each.value.protocol + description = each.value.description + cidr_blocks = length(each.value.cidr_blocks) == 0 ? null : each.value.cidr_blocks + ipv6_cidr_blocks = length(each.value.ipv6_cidr_blocks) == 0 ? null : each.value.ipv6_cidr_blocks + prefix_list_ids = length(each.value.prefix_list_ids) == 0 ? null : each.value.prefix_list_ids + self = each.value.self security_group_id = local.security_group_id - source_security_group_id = local.resource_rules[count.index].source_security_group_id + source_security_group_id = each.value.source_security_group_id depends_on = [aws_security_group.cbd, aws_security_group.default] } - diff --git a/normalize.tf b/normalize.tf index 69fb81c..3776878 100644 --- a/normalize.tf +++ b/normalize.tf @@ -6,7 +6,8 @@ locals { # 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 - norm_rules = local.enabled ? [for rule in var.rules : { + norm_rules = local.enabled && var.rules != null ? [for i, rule in var.rules : { + key = coalesce(lookup(rule, "key", null), "_n[${i}]") type = rule.type from_port = rule.from_port to_port = rule.to_port @@ -25,7 +26,8 @@ locals { }] : [] # in rule_matrix and inline rules, a single rule can have a list of security groups - norm_matrix = local.enabled ? concat(concat([[]], [for subject in var.rule_matrix : [for rule in subject.rules : { + norm_matrix = local.enabled && var.rule_matrix != null ? concat(concat([[]], [for i, subject in var.rule_matrix : [for j, rule in subject.rules : { + key = "${coalesce(lookup(subject, "key", null), "_m[${i}]")}#${coalesce(lookup(rule, "key", null), "[${j}]")}" type = rule.type from_port = rule.from_port to_port = rule.to_port @@ -48,6 +50,7 @@ locals { }]])...) : [] allow_egress_rule = { + key = "_allow_all_egress_" type = "egress" from_port = 0 to_port = 0 # [sic] from and to port ignored when protocol is "-1", warning if not zero @@ -61,7 +64,8 @@ locals { source_security_group_id = null } - all_inline_rules = concat(local.norm_rules, local.norm_matrix, local.allow_all_egress ? [local.allow_egress_rule] : []) + all_inline_rules = concat(local.norm_rules, local.norm_matrix, local.allow_all_egress ? [ + local.allow_egress_rule] : []) # For inline rules, the rules have to be separated into ingress and egress all_ingress_rules = local.inline ? [for r in local.all_inline_rules : r if r.type == "ingress"] : [] @@ -73,6 +77,7 @@ locals { # on a computed input value, we must stick to counting things. self_rules = local.inline ? [] : [for rule in local.norm_matrix : { + key = "${rule.key}#self" type = rule.type from_port = rule.from_port to_port = rule.to_port @@ -92,6 +97,7 @@ locals { } if rule.self != null] other_rules = local.inline ? [] : [for rule in local.norm_matrix : { + key = "${rule.key}#cidr" type = rule.type from_port = rule.from_port to_port = rule.to_port @@ -110,6 +116,7 @@ locals { # First, collect all the rules with lists of security groups sg_rules_lists = local.inline ? [] : [for rule in local.all_inline_rules : { + key = "${rule.key}#sg" type = rule.type from_port = rule.from_port to_port = rule.to_port @@ -124,7 +131,8 @@ locals { } if length(rule.security_groups) > 0] # Now we have to explode the lists into individual rules - sg_exploded_rules = flatten([for rule in local.sg_rules_lists : [for sg in rule.security_groups : { + sg_exploded_rules = flatten([for rule in local.sg_rules_lists : [for i, sg in rule.security_groups : { + key = "${rule.key}#${i}" type = rule.type from_port = rule.from_port to_port = rule.to_port @@ -140,7 +148,10 @@ locals { source_security_group_id = sg }]]) - resource_rules = concat(local.norm_rules, local.self_rules, local.sg_exploded_rules, local.other_rules) + all_resource_rules = concat(local.norm_rules, local.self_rules, local.sg_exploded_rules, local.other_rules) + keyed_resource_rules = { for r in local.all_resource_rules : r.key => r } + # named_resource_rules = { for r in local.all_resource_rules : r.key => r if r.key != null && r.key != ""} + # unnamed_resource_rules = [ for r in local.all_resource_rules : r if r.key == null || r.key == "" ] } diff --git a/outputs.tf b/outputs.tf index e91e295..f95474d 100644 --- a/outputs.tf +++ b/outputs.tf @@ -13,3 +13,8 @@ output "name" { description = "The created Security Group Name (null if using existing security group)" value = try(local.created_security_group.name, null) } + +output "rules_terraform_ids" { + description = "List of Terraform IDs of created `security_group_rule` resources, primarily provided to enable `depends_on`" + value = values(aws_security_group_rule.keyed).*.id +} diff --git a/variables.tf b/variables.tf index c7816e7..05f82c6 100644 --- a/variables.tf +++ b/variables.tf @@ -59,7 +59,7 @@ variable "rules" { description = <<-EOT A list of maps of Security Group rules. The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except - for `security_group_id` which will be ignored. + for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique. To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . EOT } @@ -70,6 +70,7 @@ variable "rule_matrix" { # Schema: # { # # these top level lists define all the subjects to which rule_matrix rules will be applied + # key = unique key (for stability from plan to plan) # source_security_group_ids = list of source security group IDs to apply all rules to # cidr_blocks = list of ipv4 CIDR blocks to apply all rules to # ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to @@ -78,6 +79,7 @@ variable "rule_matrix" { # # # each rule in the rules list will be applied to every subject defined above # rules = [{ + # key = "unique key" # type = "ingress" # from_port = 433 # to_port = 433 diff --git a/versions.tf b/versions.tf index 85d1d00..fc6bdc5 100644 --- a/versions.tf +++ b/versions.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 0.13.0" + required_version = ">= 0.14.0" required_providers { aws = { From 318a76cf0dae3f738c6b5540e35042490876c3eb Mon Sep 17 00:00:00 2001 From: Nuru Date: Sun, 11 Jul 2021 22:21:31 -0700 Subject: [PATCH 13/19] Make rule input an object to allow lists of different types --- README.md | 159 +++++++++++++++----- README.yaml | 151 ++++++++++++++----- docs/terraform.md | 8 +- examples/complete/fixtures.us-east-2.tfvars | 15 +- examples/complete/main.tf | 13 +- examples/complete/variables.tf | 2 +- exports/security_group_inputs.tf | 121 ++++++++++----- main.tf | 4 + normalize.tf | 23 ++- variables.tf | 20 ++- 10 files changed, 373 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index 792ed3d..f5f5b4f 100644 --- a/README.md +++ b/README.md @@ -98,25 +98,114 @@ ID of an existing security group to modify, or, by default, this module will cre group and apply the given rules to it. ##### `rules` input -This module provides 2 ways to set security group rules. The `rules` input takes a list of -rule maps. The maps are compatible with (have the same keys and accept the same values) as the +This module provides 2 ways to set security group rules. The primary way is via the `rules` input. + +The `rules` input is a complex object due to the way Terraform handles objects and types. + +
Why the input is so complex (click to reveal) + +- Terraform has 3 basic simple types: bool, number, string +- Terraform then has 3 collections of simple types: list, map, and set +- Terraform then has one complex type: object. However, this is not really a single +type. It is a catch-all label for value that is itself a collection of attributes and values. +(This will become a bit clearer after we define `maps` and contrast them with `objects`) + +The rule of the collections types is that the values in the collections must all be they exact same type. +For example, you cannot have a list where some values are boolean and some are string. Maps require +that all keys be strings, but the map values can be any type, except again all the values in a map +must be the same type. In other words, the values of a map must form a valid list. + +Objects look just like maps. The difference between an object and a map is that the values in an +object do not all have to be the same type. The keys (called "attributes" in an object) must still be strings. + +The "type" of an object is itself an object: the attributes are the same, and the values are the types of the values. + +So although `{ foo = "bar", baz = {} }` and `{ foo = "bar", baz = [] }` are both objects, +they are not of the same type. This means you can not put them both in the same list or the same map. +Similarly, and closer to the problem at hand, +```hcl +cidr_rule = { + type = "ingress" + cidr_blocks = ["0.0.0.0/0"] +} +``` +is not the same type as +```hcl +self_rule = { + type = "ingress" + self = true +} +``` +This means you cannot put both of those in the same list. +```hcl +my_rules = tolist([local.cidr_rule, local.self_rule]) +``` +Generates the error +```text +Invalid value for "v" parameter: cannot convert tuple to list of any single type. +``` + +You could make them the same type and put them in a list, +like this: +```hcl +my_rules = tolist([{ + type = "ingress" + cidr_blocks = ["0.0.0.0/0"] + self = null +}, +{ + type = "ingress" + cidr_blocks = [] + self = true +}]) +``` +That remains an option for you when inputting rules, and is probably better when you have full control over all the rules. +However, what if some of the rules are coming from a source outside of your control? You cannot simply add those rules +to your list. So, what to do? Create an object whose attributes' values can be of different types. +```hcl +{ mine = local.my_rules, theirs = var.their_rules } +``` + +That is why the `rules` object has the structure it has. + + + +The `rules` input takes an object. +- The attribute names (keys) of the object can be anything you want, but need to be known during `terraform apply`, +which means they cannot depend on any resources created or changed by the Terraform. +- The values of the attributes are lists of objects, each object representing one Security Group Rule. As explained + above in "Why the input is so complex", each object in the list must be exactly the same type. To use multiple types, + you must put them in separate lists which are values of separate attributes. +- The attributes and values of the rule objects are fully compatible (have the same keys and accept the same values) as the Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule), -except that `security_group_id` will be ignored, and the map can include an optional "key" which, if provided, must have a unique value. -The "key" value, if provided, must be something Terraform can know the value of at "apply" time, and is used to keep the -rule from being affected by its place in the list. See ["Unexplained changes..."](#unexpected-changes-during-plan-and-apply) below for more details. -While some of the map keys are optional, Terraform requires that all of the maps in a single list have exactly the same set of keys. -See [WARNINGS and error messages](#warnings-and-error-messages) below for details. +except + - The `security_group_id` will be ignored, if present + - You can include an optional `key` attribute. If present, its value must be unique among all security group rules in the + security group, and it must be known during `terraform apply`. -##### `rule_matrix` input -The other way to set rules is via the `rule_matrix` input. This splits the keys of the `aws_security_group_rule` resource -into to sets: one set defines the rule and descripition, the other set defines the subjects of the rule. As with -`rules` and explained in the previous paragraph, all elements of the list must have all the same keys. This also holds -for all the elements of the `rules_matrix.rules` list. Again, optional "key" values can provide stability, but -cannot contain derived values. +The `key` attribute value, if provided, will be used to identify the Security Group Rule to Terraform in order to +prevent Terraform from modifying it unnecessarily. If the `key` is not provided, Terraform will assign an identifier +based on the rule's position in its list, which can cause a ripple effect of rules being deleted and recreated if +a rule gets deleted from start of a list, causing all the other rules to shift position. +See ["Unexplained changes..."](#unexpected-changes-during-plan-and-apply) below for more details. -Any map key that takes a list value must either be absent from all lists or contain lists in all lists. -Use an empty list rather than `null` to indicate "no value". Passing in `null` instead of a list -may cause Terraform to crash or omit confusing error messages (e.g. "number is required"). +##### `rule_matrix` input +The other way to set rules is via the `rule_matrix` input. This splits the attributes of the `aws_security_group_rule` +resource into to sets: one set defines the rule and descripition, the other set defines the subjects of the rule. +Again, optional "key" values can provide stability, but cannot contain derived values. + +As with `rules` and explained above in "Why the input is so complex", all elements of the list must be the exact same type. +This also holds for all the elements of the `rules_matrix.rules` list. Because `rule_matrix` is already +so complex, we do not provide the ability to mix types by packing object within more objects. +All of the elements of the `rule_matrix` list must be exactly the same type. You can make them all the same +type by following a few rules: + +- Every object in a list must have the exact same set of attributes. Most attributes are optional and can be omitted, + but any attribute appearing in one object must appear in all the objects. +- Any attribute that takes a list value in any object must contain a list in all objects. + Use an empty list rather than `null` to indicate "no value". Passing in `null` instead of a list + may cause Terraform to crash or omit confusing error messages (e.g. "number is required"). +- Any attribute that takes a value of type other than list can be set to `null` in objects where no value is needed. The schema for `rule_matrix` is: @@ -153,7 +242,7 @@ group will be deleted and replaced on the first `terraform apply`, which will li ### Important Notes ##### Unexpected changes during plan and apply -The way Terraform works and the way this module is implemented causes security group rules +The way Terraform works and the way this module is implemented causes security group rules without keys to be dependent on their place in the input lists. If a rule is deleted and the other rules therefore move closer to the start of the list, those rules will be deleted and recreated. This should have no significant operational impact, but it can make a small change look like a big one when viewing the output of @@ -165,22 +254,7 @@ changed if their keys do not change and the rules themselves do not change, exce `source_security_group_ids`. You can avoid this by using `rules` instead of `rule_matrix` when you have more than one security group in the list. -##### WARNINGS and error messages - -**_Terraform v0.13 NOT SUPPORTED_**: While we currently allow use of this module with Terraform v0.13, -it has a number of known issues that are fixed in Terraform v0.14 and this module is not going -to work around. Among them are crashes due to object type conversions and the dreaded, -ubiquitous `Error: Invalid count argument`. Our recommendation if you run into these issues is -to upgrade to Terraform v0.14 or later. As a work around, avoid using `rule_matrix` and only -specify rules via the `rules` input, which has fewer issues with Terraform v0.13. - -**_Objects not of the same type_**: Any time you provide a list of object, Terraform requires that all objects in the list -must be [the exact same type](https://www.terraform.io/docs/language/expressions/type-constraints.html#dynamic-types-the-quot-any-quot-constraint). -This means that all maps in the list have exactly the same set of keys and that the values are all the same type. -So while some keys are optional for this module, if you include a key in any one of the maps in a list, then you -have to include that same key in all of them. -In rules where the key would othewise be omitted, include the key with value of `null`, unless the value is a -list type, in which case set the value to `[]` (an empty list), due to [#28137](https://github.com/hashicorp/terraform/issues/28137). +##### WARNINGS and Caveats **_Setting `inline_rules_enabled` is not recommended and NOT SUPPORTED_**: Any issues arising from setting `inlne_rules_enabled = true` (including issues about setting it to `false` after setting it to `true`) will @@ -198,6 +272,13 @@ Terraform will [complain](https://github.com/hashicorp/terraform/pull/2376) and You will either have to delete and recreate the security group or manually delete all the security group rules via the AWS console or CLI before applying `inline_rules_enabled = false`. +**_Objects not of the same type_**: Any time you provide a list of object, Terraform requires that all objects in the list +must be [the exact same type](https://www.terraform.io/docs/language/expressions/type-constraints.html#dynamic-types-the-quot-any-quot-constraint). +This means that all objects in the list have exactly the same set of attributes and that each attribute has the same type +of value in every object. So while some attributes are optional for this module, if you include an attribute in any one of the objects in a list, then you +have to include that same attribute in all of them. In rules where the key would othewise be omitted, include the key with value of `null`, +unless the value is a list type, in which case set the value to `[]` (an empty list), due to [#28137](https://github.com/hashicorp/terraform/issues/28137). + ### Example code @@ -243,7 +324,7 @@ module "sg" { # Allow unlimited egress allow_all_egress = true - rules = [ + rules = { in = [ { key = "ssh" type = "ingress" @@ -264,7 +345,7 @@ module "sg" { self = true description = "Allow HTTP from inside the security group" }, - ] + ]} vpc_id = module.vpc.vpc_id @@ -365,7 +446,7 @@ Available targets: | [allow\_all\_egress](#input\_allow\_all\_egress) | A convenience that adds to the rules specified elsewhere a rule that allows all egress.
If this is false and no egress rules are specified via `rules` or `rule-matrix`, then no egress will be allowed. | `bool` | `false` | no | | [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | 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,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | -| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
We recommend setting this `true` on new security groups, but default it to `false` because `true`
will cause existing security groups to be replaced.
Note that changing this value will also cause the security group to be replaced. | `bool` | `false` | no | +| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
We recommend setting this `true` on new security groups, but default it to `false` because `true`
will cause existing security groups to be replaced.
Note that changing this value will always cause the security group to be replaced. | `bool` | `false` | no | | [create\_security\_group](#input\_create\_security\_group) | Set `true` to create a new security group. If false, `target_security_group_id` must be provided. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | @@ -380,14 +461,14 @@ Available targets: | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [revoke\_rules\_on\_delete](#input\_revoke\_rules\_on\_delete) | Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting
the security group itself. This is normally not needed. | `bool` | `false` | no | | [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` | `[]` | no | -| [rules](#input\_rules) | A list of maps of Security Group rules.
The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | +| [rules](#input\_rules) | An object (like a map) of lists of Security Group rule objects. All elements of a list must be exactly the same
type, so this input accepts an object with keys (attributes) whose values are lists so you can separate different
types into different lists and still pass them into one input. Keys must known at "plan" time.
The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `any` | `{}` | no | | [security\_group\_create\_timeout](#input\_security\_group\_create\_timeout) | How long to wait for the security group to be created. | `string` | `"10m"` | no | | [security\_group\_delete\_timeout](#input\_security\_group\_delete\_timeout) | How long to retry on `DependencyViolation` errors during security group deletion from
lingering ENIs left by certain AWS services such as Elastic Load Balancing. | `string` | `"15m"` | no | -| [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced, which requires everything
associated with the security group to be replaced, which can be very disruptive. | `string` | `"Managed by Terraform"` | no | +| [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced. | `string` | `"Managed by Terraform"` | no | | [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the VPC.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | -| [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
Required if `create_security_group` is `false`, ignored otherwise. | `string` | `""` | no | +| [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
The Security Group's description will not be changed.
Not compatible with `inline_rules_enabled` or `revoke_rules_on_delete`.
Required if `create_security_group` is `false`, ignored otherwise. | `string` | `""` | no | | [vpc\_id](#input\_vpc\_id) | The ID of the VPC where the Security Group will be created. | `string` | n/a | yes | ## Outputs diff --git a/README.yaml b/README.yaml index c49f0a1..f904e88 100644 --- a/README.yaml +++ b/README.yaml @@ -65,25 +65,114 @@ usage: |- group and apply the given rules to it. ##### `rules` input - This module provides 2 ways to set security group rules. The `rules` input takes a list of - rule maps. The maps are compatible with (have the same keys and accept the same values) as the + This module provides 2 ways to set security group rules. The primary way is via the `rules` input. + + The `rules` input is a complex object due to the way Terraform handles objects and types. + +
Why the input is so complex (click to reveal) + + - Terraform has 3 basic simple types: bool, number, string + - Terraform then has 3 collections of simple types: list, map, and set + - Terraform then has one complex type: object. However, this is not really a single + type. It is a catch-all label for value that is itself a collection of attributes and values. + (This will become a bit clearer after we define `maps` and contrast them with `objects`) + + The rule of the collections types is that the values in the collections must all be they exact same type. + For example, you cannot have a list where some values are boolean and some are string. Maps require + that all keys be strings, but the map values can be any type, except again all the values in a map + must be the same type. In other words, the values of a map must form a valid list. + + Objects look just like maps. The difference between an object and a map is that the values in an + object do not all have to be the same type. The keys (called "attributes" in an object) must still be strings. + + The "type" of an object is itself an object: the attributes are the same, and the values are the types of the values. + + So although `{ foo = "bar", baz = {} }` and `{ foo = "bar", baz = [] }` are both objects, + they are not of the same type. This means you can not put them both in the same list or the same map. + Similarly, and closer to the problem at hand, + ```hcl + cidr_rule = { + type = "ingress" + cidr_blocks = ["0.0.0.0/0"] + } + ``` + is not the same type as + ```hcl + self_rule = { + type = "ingress" + self = true + } + ``` + This means you cannot put both of those in the same list. + ```hcl + my_rules = tolist([local.cidr_rule, local.self_rule]) + ``` + Generates the error + ```text + Invalid value for "v" parameter: cannot convert tuple to list of any single type. + ``` + + You could make them the same type and put them in a list, + like this: + ```hcl + my_rules = tolist([{ + type = "ingress" + cidr_blocks = ["0.0.0.0/0"] + self = null + }, + { + type = "ingress" + cidr_blocks = [] + self = true + }]) + ``` + That remains an option for you when inputting rules, and is probably better when you have full control over all the rules. + However, what if some of the rules are coming from a source outside of your control? You cannot simply add those rules + to your list. So, what to do? Create an object whose attributes' values can be of different types. + ```hcl + { mine = local.my_rules, theirs = var.their_rules } + ``` + + That is why the `rules` object has the structure it has. + + + + The `rules` input takes an object. + - The attribute names (keys) of the object can be anything you want, but need to be known during `terraform apply`, + which means they cannot depend on any resources created or changed by the Terraform. + - The values of the attributes are lists of objects, each object representing one Security Group Rule. As explained + above in "Why the input is so complex", each object in the list must be exactly the same type. To use multiple types, + you must put them in separate lists which are values of separate attributes. + - The attributes and values of the rule objects are fully compatible (have the same keys and accept the same values) as the Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule), - except that `security_group_id` will be ignored, and the map can include an optional "key" which, if provided, must have a unique value. - The "key" value, if provided, must be something Terraform can know the value of at "apply" time, and is used to keep the - rule from being affected by its place in the list. See ["Unexplained changes..."](#unexpected-changes-during-plan-and-apply) below for more details. - While some of the map keys are optional, Terraform requires that all of the maps in a single list have exactly the same set of keys. - See [WARNINGS and error messages](#warnings-and-error-messages) below for details. + except + - The `security_group_id` will be ignored, if present + - You can include an optional `key` attribute. If present, its value must be unique among all security group rules in the + security group, and it must be known during `terraform apply`. - ##### `rule_matrix` input - The other way to set rules is via the `rule_matrix` input. This splits the keys of the `aws_security_group_rule` resource - into to sets: one set defines the rule and descripition, the other set defines the subjects of the rule. As with - `rules` and explained in the previous paragraph, all elements of the list must have all the same keys. This also holds - for all the elements of the `rules_matrix.rules` list. Again, optional "key" values can provide stability, but - cannot contain derived values. + The `key` attribute value, if provided, will be used to identify the Security Group Rule to Terraform in order to + prevent Terraform from modifying it unnecessarily. If the `key` is not provided, Terraform will assign an identifier + based on the rule's position in its list, which can cause a ripple effect of rules being deleted and recreated if + a rule gets deleted from start of a list, causing all the other rules to shift position. + See ["Unexplained changes..."](#unexpected-changes-during-plan-and-apply) below for more details. - Any map key that takes a list value must either be absent from all lists or contain lists in all lists. - Use an empty list rather than `null` to indicate "no value". Passing in `null` instead of a list - may cause Terraform to crash or omit confusing error messages (e.g. "number is required"). + ##### `rule_matrix` input + The other way to set rules is via the `rule_matrix` input. This splits the attributes of the `aws_security_group_rule` + resource into to sets: one set defines the rule and descripition, the other set defines the subjects of the rule. + Again, optional "key" values can provide stability, but cannot contain derived values. + + As with `rules` and explained above in "Why the input is so complex", all elements of the list must be the exact same type. + This also holds for all the elements of the `rules_matrix.rules` list. Because `rule_matrix` is already + so complex, we do not provide the ability to mix types by packing object within more objects. + All of the elements of the `rule_matrix` list must be exactly the same type. You can make them all the same + type by following a few rules: + + - Every object in a list must have the exact same set of attributes. Most attributes are optional and can be omitted, + but any attribute appearing in one object must appear in all the objects. + - Any attribute that takes a list value in any object must contain a list in all objects. + Use an empty list rather than `null` to indicate "no value". Passing in `null` instead of a list + may cause Terraform to crash or omit confusing error messages (e.g. "number is required"). + - Any attribute that takes a value of type other than list can be set to `null` in objects where no value is needed. The schema for `rule_matrix` is: @@ -120,7 +209,7 @@ usage: |- ### Important Notes ##### Unexpected changes during plan and apply - The way Terraform works and the way this module is implemented causes security group rules + The way Terraform works and the way this module is implemented causes security group rules without keys to be dependent on their place in the input lists. If a rule is deleted and the other rules therefore move closer to the start of the list, those rules will be deleted and recreated. This should have no significant operational impact, but it can make a small change look like a big one when viewing the output of @@ -132,22 +221,7 @@ usage: |- `source_security_group_ids`. You can avoid this by using `rules` instead of `rule_matrix` when you have more than one security group in the list. - ##### WARNINGS and error messages - - **_Terraform v0.13 NOT SUPPORTED_**: While we currently allow use of this module with Terraform v0.13, - it has a number of known issues that are fixed in Terraform v0.14 and this module is not going - to work around. Among them are crashes due to object type conversions and the dreaded, - ubiquitous `Error: Invalid count argument`. Our recommendation if you run into these issues is - to upgrade to Terraform v0.14 or later. As a work around, avoid using `rule_matrix` and only - specify rules via the `rules` input, which has fewer issues with Terraform v0.13. - - **_Objects not of the same type_**: Any time you provide a list of object, Terraform requires that all objects in the list - must be [the exact same type](https://www.terraform.io/docs/language/expressions/type-constraints.html#dynamic-types-the-quot-any-quot-constraint). - This means that all maps in the list have exactly the same set of keys and that the values are all the same type. - So while some keys are optional for this module, if you include a key in any one of the maps in a list, then you - have to include that same key in all of them. - In rules where the key would othewise be omitted, include the key with value of `null`, unless the value is a - list type, in which case set the value to `[]` (an empty list), due to [#28137](https://github.com/hashicorp/terraform/issues/28137). + ##### WARNINGS and Caveats **_Setting `inline_rules_enabled` is not recommended and NOT SUPPORTED_**: Any issues arising from setting `inlne_rules_enabled = true` (including issues about setting it to `false` after setting it to `true`) will @@ -165,6 +239,13 @@ usage: |- You will either have to delete and recreate the security group or manually delete all the security group rules via the AWS console or CLI before applying `inline_rules_enabled = false`. + **_Objects not of the same type_**: Any time you provide a list of object, Terraform requires that all objects in the list + must be [the exact same type](https://www.terraform.io/docs/language/expressions/type-constraints.html#dynamic-types-the-quot-any-quot-constraint). + This means that all objects in the list have exactly the same set of attributes and that each attribute has the same type + of value in every object. So while some attributes are optional for this module, if you include an attribute in any one of the objects in a list, then you + have to include that same attribute in all of them. In rules where the key would othewise be omitted, include the key with value of `null`, + unless the value is a list type, in which case set the value to `[]` (an empty list), due to [#28137](https://github.com/hashicorp/terraform/issues/28137). + ### Example code @@ -210,7 +291,7 @@ usage: |- # Allow unlimited egress allow_all_egress = true - rules = [ + rules = { in = [ { key = "ssh" type = "ingress" @@ -231,7 +312,7 @@ usage: |- self = true description = "Allow HTTP from inside the security group" }, - ] + ]} vpc_id = module.vpc.vpc_id diff --git a/docs/terraform.md b/docs/terraform.md index 2050703..6d222e6 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -34,7 +34,7 @@ | [allow\_all\_egress](#input\_allow\_all\_egress) | A convenience that adds to the rules specified elsewhere a rule that allows all egress.
If this is false and no egress rules are specified via `rules` or `rule-matrix`, then no egress will be allowed. | `bool` | `false` | no | | [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | 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,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | -| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
We recommend setting this `true` on new security groups, but default it to `false` because `true`
will cause existing security groups to be replaced.
Note that changing this value will also cause the security group to be replaced. | `bool` | `false` | no | +| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
We recommend setting this `true` on new security groups, but default it to `false` because `true`
will cause existing security groups to be replaced.
Note that changing this value will always cause the security group to be replaced. | `bool` | `false` | no | | [create\_security\_group](#input\_create\_security\_group) | Set `true` to create a new security group. If false, `target_security_group_id` must be provided. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | @@ -49,14 +49,14 @@ | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [revoke\_rules\_on\_delete](#input\_revoke\_rules\_on\_delete) | Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting
the security group itself. This is normally not needed. | `bool` | `false` | no | | [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` | `[]` | no | -| [rules](#input\_rules) | A list of maps of Security Group rules.
The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | +| [rules](#input\_rules) | An object (like a map) of lists of Security Group rule objects. All elements of a list must be exactly the same
type, so this input accepts an object with keys (attributes) whose values are lists so you can separate different
types into different lists and still pass them into one input. Keys must known at "plan" time.
The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `any` | `{}` | no | | [security\_group\_create\_timeout](#input\_security\_group\_create\_timeout) | How long to wait for the security group to be created. | `string` | `"10m"` | no | | [security\_group\_delete\_timeout](#input\_security\_group\_delete\_timeout) | How long to retry on `DependencyViolation` errors during security group deletion from
lingering ENIs left by certain AWS services such as Elastic Load Balancing. | `string` | `"15m"` | no | -| [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced, which requires everything
associated with the security group to be replaced, which can be very disruptive. | `string` | `"Managed by Terraform"` | no | +| [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced. | `string` | `"Managed by Terraform"` | no | | [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the VPC.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | -| [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
Required if `create_security_group` is `false`, ignored otherwise. | `string` | `""` | no | +| [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
The Security Group's description will not be changed.
Not compatible with `inline_rules_enabled` or `revoke_rules_on_delete`.
Required if `create_security_group` is `false`, ignored otherwise. | `string` | `""` | no | | [vpc\_id](#input\_vpc\_id) | The ID of the VPC where the Security Group will be created. | `string` | n/a | yes | ## Outputs diff --git a/examples/complete/fixtures.us-east-2.tfvars b/examples/complete/fixtures.us-east-2.tfvars index f732faa..9897f6f 100644 --- a/examples/complete/fixtures.us-east-2.tfvars +++ b/examples/complete/fixtures.us-east-2.tfvars @@ -8,7 +8,7 @@ stage = "test" name = "sg" -rules = [ +rules = { default = [ { key = null # "ssh all" type = "ingress" @@ -27,4 +27,15 @@ rules = [ cidr_blocks = ["0.0.0.0/0"] description = "HTTPS wide open" } -] + ], + ipv6 = [ + { + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + ipv6_cidr_blocks = ["::/0"] + description = "SSH wide open" + } + +] } diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 8ecd705..d0fddb4 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -60,7 +60,7 @@ module "new_security_group" { ] }] - rules = [ + rules = merge(var.rules, { new-cidr = [ { key = "https-cidr" type = "ingress" @@ -72,20 +72,17 @@ module "new_security_group" { source_security_group_id = null description = "Discrete HTTPS ingress by CIDR" self = null - }, - { - key = null # "https-sg" + }], + new-sg = [{ type = "ingress" from_port = 443 to_port = 443 protocol = "tcp" - cidr_blocks = [] - ipv6_cidr_blocks = [] source_security_group_id = aws_security_group.existing.id description = "Discrete HTTPS ingress for special SG" self = null - }, - ] + }], + }) vpc_id = module.vpc.vpc_id diff --git a/examples/complete/variables.tf b/examples/complete/variables.tf index dfc3b68..9d32fa1 100644 --- a/examples/complete/variables.tf +++ b/examples/complete/variables.tf @@ -3,7 +3,7 @@ variable "region" { } variable "rules" { - type = list(any) + type = any } variable "rule_matrix_self" { diff --git a/exports/security_group_inputs.tf b/exports/security_group_inputs.tf index c92e204..7377163 100644 --- a/exports/security_group_inputs.tf +++ b/exports/security_group_inputs.tf @@ -1,10 +1,10 @@ # security_group_inputs Version: 1 # -# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-aws-security-group -# All other instances of this file should be a copy of that one -# -# # Copy this file from https://github.com/cloudposse/terraform-aws-security-group/blob/master/exports/security_group_inputs.tf +# and EDIT IT TO SUIT YOUR PROJECT. Update the version number above if you update this file from a later version. +# +# KEEP this top comment block, but REMOVE COMMENTS below that are intended +# for the initial implementor and not maintainers or end users. # # This file provides the standard inputs that all Cloud Posse Open Source # Terraform module that create AWS Security Groups should implement. @@ -22,15 +22,23 @@ variable "create_security_group" { type = bool default = true - description = "Set `true` to create a new security group. If false, `target_security_group_id` must be provided." + description = "Set `true` to create and configure a new security group. If false, `associated_security_group_ids` must be provided." } -variable "target_security_group_id" { - type = string - default = "" +variable "associated_security_group_ids" { + type = list(string) + default = [] description = <<-EOT - The ID of an existing Security Group to which Security Group rules will be assigned. - Required if `create_security_group` is `false`, ignored otherwise. + A list of IDs of Security Groups to associate the created resource with, in addition to the created security group. + These security groups will not be modified and must provide all the required access. + EOT +} + +variable "allowed_security_group_ids" { + type = list(string) + default = [] + description = <<-EOT + A list of IDs of Security Groups to allow access to the security group created by this module. EOT } @@ -38,7 +46,7 @@ variable "security_group_name" { type = string default = "" description = <<-EOT - The name to assign to the created security group. Must be unique within the account. + The name to assign to the created security group. Must be unique within the VPC. If not provided, will be derived from the `null-label.context` passed in. If `create_before_destroy` is true, will be used as a name prefix. EOT @@ -49,20 +57,44 @@ variable "security_group_description" { default = "Managed by Terraform" description = <<-EOT The description to assign to the created Security Group. - Warning: Changing the description causes the security group to be replaced, which requires everything - associated with the security group to be replaced, which can be very disruptive. + Warning: Changing the description causes the security group to be replaced. EOT } +############################### +# +# Decide on a case-by-case basis what the default should be. +# In general, if the resource supports changing security groups without deleting +# the resource or anything it depends on, then default it to `true` and +# note in the release notes and migration documents the option to +# set it to false to preserve the existing security group. +# If the resource has to be deleted to change its security group, +# then set the default to `false` and highlight the option to change +# it to true in the release notes and migration documents. +# +################################ variable "security_group_create_before_destroy" { - type = bool - default = false - description = <<-EOT - Set `true` to enable terraform `create_before_destroy` behavior on the created security group. - We recommend setting this `true` on new security groups, but default it to `false` because `true` - will cause existing security groups to be replaced. - Note that changing this value will also cause the security group to be replaced. - EOT + type = bool + # + # Pick true or false and the associated description + # Replace "the resource" with the name of the resouce, e.g. "EC2 instance" + # + + # default = false + # description = <<-EOT + # Set `true` to enable Terraform `create_before_destroy` behavior on the created security group. + # We recommend setting this `true` on new security groups, but default it to `false` because `true` + # will cause existing security groups to be replaced, requiring the resource to be deleted and recreated. + # Note that changing this value will always cause the security group to be replaced. + # EOT + + # default = true + # description = <<-EOT + # Set `true` to enable Terraform `create_before_destroy` behavior on the created security group. + # We only recommend setting this `false` if you are upgrading this module and need to keep + # the existing security group from being replaced. + # Note that changing this value will always cause the security group to be replaced. + # EOT } variable "security_group_create_timeout" { @@ -80,6 +112,19 @@ variable "security_group_delete_timeout" { EOT } +############################################################################################# +## Special note about inline_rules_enabled and revoke_rules_on_delete +## +## The security-group inputs inline_rules_enabled and revoke_rules_on_delete should not +## be exposed in other modules unless there is a strong reason for them to be used. +## We discourage the use of inline_rules_enabled and we rarely need or want +## revoke_rules_on_delete, so we do not want to clutter our interface with those inputs. +## +## If someone wants to enable either of those options, they have the option +## of creating a security group configured as they like +## and passing it in as the target security group. +############################################################################################# + # # #### The variables below can be omitted if not needed, and may need their descriptions modified @@ -108,21 +153,28 @@ variable "allow_all_egress" { EOT } -variable "associated_security_group_ids" { - type = list(string) +variable "additional_security_group_rules" { + type = list(any) default = [] description = <<-EOT - A list of IDs of Security Groups to associate the created resource with, in addition to the created or target security group. - EOT + A list of Security Group rule objects to add to the created security group, in addition to the ones + this module normally creates. (To suppress the module's rules, set `create_security_group` to false + and supply your own security group via `associated_security_group_ids`.) + The keys and values of the objects are fully compatible with the `aws_security_group_rule` resource, except + for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique and known at "plan" time. + To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . + EOT } -variable "allowed_security_group_ids" { - type = list(string) - default = [] - description = <<-EOT - A list of IDs of Security Groups to allow access to the created resource. - EOT -} +# +# +#### The variable `additional_security_group_rule_matrix` should normally be omitted, for a few reasons: +# - It is a convenience and ultimately provides no rules that cannot be provided via `additional_security_group_rules` +# - It is complicated and can, in some situations, create problems for Terraform `for_each` +# - It is difficult to document and easy to make mistakes using it +# +# + ## ## @@ -132,10 +184,11 @@ variable "allowed_security_group_ids" { # # output "security_group_id" { # value = "" -# description = "The ID of the created or target security group" +# description = "The ID of the created security group" # } # # output "security_group_name" { # value = "" -# description = "The name of the created or target security group" +# description = "The name of the created security group" # } + diff --git a/main.tf b/main.tf index 1c55cc9..39cb1c3 100644 --- a/main.tf +++ b/main.tf @@ -155,4 +155,8 @@ resource "aws_security_group_rule" "keyed" { source_security_group_id = each.value.source_security_group_id depends_on = [aws_security_group.cbd, aws_security_group.default] + + lifecycle { + create_before_destroy = true + } } diff --git a/normalize.tf b/normalize.tf index 3776878..3311313 100644 --- a/normalize.tf +++ b/normalize.tf @@ -6,8 +6,8 @@ locals { # 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 - norm_rules = local.enabled && var.rules != null ? [for i, rule in var.rules : { - key = coalesce(lookup(rule, "key", null), "_n[${i}]") + norm_rules = local.enabled && var.rules != null ? concat(concat([[]], [for k, rules in var.rules : [for i, rule in rules : { + key = coalesce(lookup(rule, "key", null), "${k}[${i}]") type = rule.type from_port = rule.from_port to_port = rule.to_port @@ -15,15 +15,15 @@ locals { description = lookup(rule, "description", local.default_rule_description) # Convert a missing key, a value of null, or a value of empty list to [] - cidr_blocks = try(length(rule.cidr_blocks), 0) > 0 ? rule["cidr_blocks"] : [] - ipv6_cidr_blocks = try(length(rule.ipv6_cidr_blocks), 0) > 0 ? rule["ipv6_cidr_blocks"] : [] - prefix_list_ids = try(length(rule.prefix_list_ids), 0) > 0 ? rule["prefix_list_ids"] : [] + cidr_blocks = try(length(rule.cidr_blocks), 0) > 0 ? rule.cidr_blocks : [] + ipv6_cidr_blocks = try(length(rule.ipv6_cidr_blocks), 0) > 0 ? rule.ipv6_cidr_blocks : [] + prefix_list_ids = try(length(rule.prefix_list_ids), 0) > 0 ? rule.prefix_list_ids : [] source_security_group_id = lookup(rule, "source_security_group_id", null) security_groups = [] self = lookup(rule, "self", null) - }] : [] + }]])...) : [] # in rule_matrix and inline rules, a single rule can have a list of security groups norm_matrix = local.enabled && var.rule_matrix != null ? concat(concat([[]], [for i, subject in var.rule_matrix : [for j, rule in subject.rules : { @@ -35,7 +35,7 @@ locals { description = lookup(rule, "description", local.default_rule_description) # We tried to be lenient and convert a missing key, a value of null, or a value of empty list to [] - # with cidr_blocks = try(length(rule.cidr_blocks), 0) > 0 ? rule["cidr_blocks"] : [] + # with cidr_blocks = try(length(rule.cidr_blocks), 0) > 0 ? rule.cidr_blocks : [] # but if a list is provided and any value in the list is not available at plan time, # that formulation causes problems for `count`, so we must forbid keys present with value of null. @@ -64,8 +64,9 @@ locals { source_security_group_id = null } - all_inline_rules = concat(local.norm_rules, local.norm_matrix, local.allow_all_egress ? [ - local.allow_egress_rule] : []) + extra_rules = local.allow_all_egress ? [local.allow_egress_rule] : [] + + all_inline_rules = concat(local.norm_rules, local.norm_matrix, local.extra_rules) # For inline rules, the rules have to be separated into ingress and egress all_ingress_rules = local.inline ? [for r in local.all_inline_rules : r if r.type == "ingress"] : [] @@ -148,10 +149,8 @@ locals { source_security_group_id = sg }]]) - all_resource_rules = concat(local.norm_rules, local.self_rules, local.sg_exploded_rules, local.other_rules) + all_resource_rules = concat(local.norm_rules, local.self_rules, local.sg_exploded_rules, local.other_rules, local.extra_rules) keyed_resource_rules = { for r in local.all_resource_rules : r.key => r } - # named_resource_rules = { for r in local.all_resource_rules : r.key => r if r.key != null && r.key != ""} - # unnamed_resource_rules = [ for r in local.all_resource_rules : r if r.key == null || r.key == "" ] } diff --git a/variables.tf b/variables.tf index 05f82c6..55bfff3 100644 --- a/variables.tf +++ b/variables.tf @@ -9,6 +9,8 @@ variable "target_security_group_id" { default = "" description = <<-EOT The ID of an existing Security Group to which Security Group rules will be assigned. + The Security Group's description will not be changed. + Not compatible with `inline_rules_enabled` or `revoke_rules_on_delete`. Required if `create_security_group` is `false`, ignored otherwise. EOT } @@ -28,8 +30,7 @@ variable "security_group_description" { default = "Managed by Terraform" description = <<-EOT The description to assign to the created Security Group. - Warning: Changing the description causes the security group to be replaced, which requires everything - associated with the security group to be replaced, which can be very disruptive. + Warning: Changing the description causes the security group to be replaced. EOT } @@ -40,7 +41,7 @@ variable "create_before_destroy" { Set `true` to enable terraform `create_before_destroy` behavior on the created security group. We recommend setting this `true` on new security groups, but default it to `false` because `true` will cause existing security groups to be replaced. - Note that changing this value will also cause the security group to be replaced. + Note that changing this value will always cause the security group to be replaced. EOT } @@ -54,12 +55,15 @@ variable "allow_all_egress" { } variable "rules" { - type = list(any) - default = [] + type = any + default = {} description = <<-EOT - A list of maps of Security Group rules. - The keys and values of the maps are fully compatible with the `aws_security_group_rule` resource, except - for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique. + An object (like a map) of lists of Security Group rule objects. All elements of a list must be exactly the same + type, so this input accepts an object with keys (attributes) whose values are lists so you can separate different + types into different lists and still pass them into one input. Keys must known at "plan" time. + The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource, + except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique + and known at "plan" time. To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . EOT } From 392c17cf37ae3d108f2cf7a40eb84eae51feb309 Mon Sep 17 00:00:00 2001 From: Nuru Date: Tue, 10 Aug 2021 19:40:53 -0700 Subject: [PATCH 14/19] Remove create_security_group, now implied --- README.md | 5 ++--- README.yaml | 2 +- docs/terraform.md | 3 +-- examples/complete/fixtures.us-east-2.tfvars | 1 + examples/complete/main.tf | 17 ++++++++------- examples/complete/outputs.tf | 23 +++++++++++++-------- exports/security_group_inputs.tf | 2 +- main.tf | 6 +++--- test/src/examples_complete_test.go | 16 +++++--------- variables.tf | 15 +++++++------- 10 files changed, 44 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index f5f5b4f..04d3cba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# terraform-aws-security-group [![Latest Release](https://img.shields.io/github/release/cloudposse/terraform-aws-security-group.svg)](https://github.com/cloudposse/terraform-aws-security-group/releases/latest) [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) +# terraform-aws-security-group [![Latest Release](https://img.shields.io/github/release/cloudposse/terraform-aws-security-group.svg)](https://github.com/cloudposse/terraform-aws-security-group) [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) [![README Header][readme_header_img]][readme_header_link] @@ -447,7 +447,6 @@ Available targets: | [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | 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,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | | [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
We recommend setting this `true` on new security groups, but default it to `false` because `true`
will cause existing security groups to be replaced.
Note that changing this value will always cause the security group to be replaced. | `bool` | `false` | no | -| [create\_security\_group](#input\_create\_security\_group) | Set `true` to create a new security group. If false, `target_security_group_id` must be provided. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | @@ -468,7 +467,7 @@ Available targets: | [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the VPC.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | -| [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
The Security Group's description will not be changed.
Not compatible with `inline_rules_enabled` or `revoke_rules_on_delete`.
Required if `create_security_group` is `false`, ignored otherwise. | `string` | `""` | no | +| [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
The Security Group's description will not be changed.
Not compatible with `inline_rules_enabled` or `revoke_rules_on_delete`.
Required if `create_security_group` is `false`, ignored otherwise. | `list(string)` | `[]` | no | | [vpc\_id](#input\_vpc\_id) | The ID of the VPC where the Security Group will be created. | `string` | n/a | yes | ## Outputs diff --git a/README.yaml b/README.yaml index f904e88..ddb451f 100644 --- a/README.yaml +++ b/README.yaml @@ -33,7 +33,7 @@ github_repo: cloudposse/terraform-aws-security-group badges: - name: "Latest Release" image: "https://img.shields.io/github/release/cloudposse/terraform-aws-security-group.svg" - url: "https://github.com/cloudposse/terraform-aws-security-group/releases/latest" + url: "https://github.com/cloudposse/terraform-aws-security-group" - name: "Slack Community" image: "https://slack.cloudposse.com/badge.svg" url: "https://slack.cloudposse.com" diff --git a/docs/terraform.md b/docs/terraform.md index 6d222e6..b1f59cd 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -35,7 +35,6 @@ | [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | 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,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | | [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
We recommend setting this `true` on new security groups, but default it to `false` because `true`
will cause existing security groups to be replaced.
Note that changing this value will always cause the security group to be replaced. | `bool` | `false` | no | -| [create\_security\_group](#input\_create\_security\_group) | Set `true` to create a new security group. If false, `target_security_group_id` must be provided. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | @@ -56,7 +55,7 @@ | [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the VPC.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | -| [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
The Security Group's description will not be changed.
Not compatible with `inline_rules_enabled` or `revoke_rules_on_delete`.
Required if `create_security_group` is `false`, ignored otherwise. | `string` | `""` | no | +| [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
The Security Group's description will not be changed.
Not compatible with `inline_rules_enabled` or `revoke_rules_on_delete`.
Required if `create_security_group` is `false`, ignored otherwise. | `list(string)` | `[]` | no | | [vpc\_id](#input\_vpc\_id) | The ID of the VPC where the Security Group will be created. | `string` | n/a | yes | ## Outputs diff --git a/examples/complete/fixtures.us-east-2.tfvars b/examples/complete/fixtures.us-east-2.tfvars index 9897f6f..723a04c 100644 --- a/examples/complete/fixtures.us-east-2.tfvars +++ b/examples/complete/fixtures.us-east-2.tfvars @@ -30,6 +30,7 @@ rules = { default = [ ], ipv6 = [ { + # no key provided type = "ingress" from_port = 22 to_port = 22 diff --git a/examples/complete/main.tf b/examples/complete/main.tf index d0fddb4..8697f32 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -30,7 +30,7 @@ module "new_security_group" { key = "stable" # Allow ingress on ports 22 and 80 from created security group, existing security group, and CIDR "10.0.0.0/8" # The dynamic value for source_security_group_ids breaks Terraform 0.13 but should work in 0.14 or later - source_security_group_ids = [aws_security_group.existing.id] + source_security_group_ids = [aws_security_group.target.id] # Either dynamic value for CIDRs breaks Terraform 0.13 but should work in 0.14 or later cidr_blocks = random_integer.coin.result > 1 ? ["10.0.0.0/16"] : ["10.0.0.0/24"] ipv6_cidr_blocks = [module.vpc.ipv6_cidr_block] @@ -74,11 +74,12 @@ module "new_security_group" { self = null }], new-sg = [{ + # no key provided type = "ingress" from_port = 443 to_port = 443 protocol = "tcp" - source_security_group_id = aws_security_group.existing.id + source_security_group_id = aws_security_group.target.id description = "Discrete HTTPS ingress for special SG" self = null }], @@ -96,19 +97,19 @@ module "new_security_group" { # Create rules for pre-created security group -resource "aws_security_group" "existing" { +resource "aws_security_group" "target" { name_prefix = format("%s-%s-", module.this.id, "existing") vpc_id = module.vpc.vpc_id tags = module.this.tags } -module "existing_security_group" { +module "target_security_group" { source = "../.." - allow_all_egress = true - target_security_group_id = aws_security_group.existing.id + allow_all_egress = true + # create_security_group = false + target_security_group_id = [aws_security_group.target.id] rules = var.rules - create_security_group = false vpc_id = module.vpc.vpc_id @@ -121,7 +122,7 @@ module "disabled_security_group" { source = "../.." vpc_id = module.vpc.vpc_id - target_security_group_id = aws_security_group.existing.id + target_security_group_id = [aws_security_group.target.id] rules = var.rules context = module.this.context diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf index e0fd779..9844bae 100644 --- a/examples/complete/outputs.tf +++ b/examples/complete/outputs.tf @@ -13,19 +13,24 @@ output "created_sg_name" { value = module.new_security_group.name } -output "existing_sg_id" { - description = "The existing Security Group ID" - value = module.existing_security_group.id +output "test_created_sg_id" { + description = "The security group created by the test to use as \"target\" security group" + value = aws_security_group.target.id } -output "existing_sg_arn" { - description = "The existing Security Group ARN" - value = module.existing_security_group.arn +output "target_sg_id" { + description = "The target Security Group ID" + value = module.target_security_group.id } -output "existing_sg_name" { - description = "The existing Security Group Name" - value = module.existing_security_group.name +output "target_sg_arn" { + description = "The target Security Group ARN" + value = module.target_security_group.arn +} + +output "target_sg_name" { + description = "The target Security Group Name" + value = module.target_security_group.name } output "disabled_sg_id" { diff --git a/exports/security_group_inputs.tf b/exports/security_group_inputs.tf index 7377163..aa8d45a 100644 --- a/exports/security_group_inputs.tf +++ b/exports/security_group_inputs.tf @@ -30,7 +30,7 @@ variable "associated_security_group_ids" { default = [] description = <<-EOT A list of IDs of Security Groups to associate the created resource with, in addition to the created security group. - These security groups will not be modified and must provide all the required access. + These security groups will not be modified and, if `create_security_group` is `false`, must provide all the required access. EOT } diff --git a/main.tf b/main.tf index 39cb1c3..db4c365 100644 --- a/main.tf +++ b/main.tf @@ -6,7 +6,7 @@ locals { default_rule_description = "Managed by Terraform" - create_security_group = local.enabled && var.create_security_group + create_security_group = local.enabled && length(var.target_security_group_id) == 0 created_security_group = local.create_security_group ? ( var.create_before_destroy ? aws_security_group.cbd[0] : aws_security_group.default[0] @@ -14,8 +14,8 @@ locals { security_group_id = local.enabled ? ( # Use coalesce() here to hack an error message into the output - var.create_security_group ? local.created_security_group.id : coalesce(var.target_security_group_id, - "`create_security_group` is false, but no security group ID was supplied ") + local.create_security_group ? local.created_security_group.id : coalesce(var.target_security_group_id[0], + "var.target_security_group_id contains null value. Omit value if you want this module to create a security group.") ) : null } diff --git a/test/src/examples_complete_test.go b/test/src/examples_complete_test.go index 363cc0d..16302a6 100644 --- a/test/src/examples_complete_test.go +++ b/test/src/examples_complete_test.go @@ -45,7 +45,7 @@ func TestExamplesComplete(t *testing.T) { // Run `terraform output` to get the value of an output variable - // Verify that outputs are valid when `security_group_enabled=true` + // Verify that outputs are valid when no target security group is supplied newSgID := terraform.Output(t, terraformOptions, "created_sg_id") newSgARN := terraform.Output(t, terraformOptions, "created_sg_arn") newSgName := terraform.Output(t, terraformOptions, "created_sg_name") @@ -54,17 +54,11 @@ func TestExamplesComplete(t *testing.T) { assert.Contains(t, newSgARN, "arn:aws:ec2", "SG ID should contains substring 'arn:aws:ec2'") assert.Equal(t, "eg-ue2-test-sg-"+randID, newSgName) - /* - Module used to output SG information for existing security groups, but no longer does - // Verify that outputs are valid when `security_group_enabled=false` and `sg_id` set to existing SG ID - existingSgID := terraform.Output(t, terraformOptions, "existing_sg_id") - existingSgARN := terraform.Output(t, terraformOptions, "existing_sg_arn") - existingSgName := terraform.Output(t, terraformOptions, "existing_sg_name") + // Verify that outputs are valid when an existing security group is provided + targetSgID := terraform.Output(t, terraformOptions, "target_sg_id") + testSgID := terraform.Output(t, terraformOptions, "test_created_sg_id") - assert.Contains(t, existingSgID, "sg-", "SG ID should contains substring 'sg-'") - assert.Contains(t, existingSgARN, "arn:aws:ec2", "SG ID should contains substring 'arn:aws:ec2'") - assert.Contains(t, existingSgName, "eg-ue2-test-sg-"+randID) - */ + assert.Equal(t, testSgID, targetSgID, "Module should return provided SG ID as \"id\" output") // Verify that outputs are empty when module is disabled disabledSgID := terraform.Output(t, terraformOptions, "disabled_sg_id") diff --git a/variables.tf b/variables.tf index 55bfff3..587b6ac 100644 --- a/variables.tf +++ b/variables.tf @@ -1,18 +1,17 @@ -variable "create_security_group" { - type = bool - default = true - description = "Set `true` to create a new security group. If false, `target_security_group_id` must be provided." -} - variable "target_security_group_id" { - type = string - default = "" + type = list(string) + default = [] description = <<-EOT The ID of an existing Security Group to which Security Group rules will be assigned. The Security Group's description will not be changed. Not compatible with `inline_rules_enabled` or `revoke_rules_on_delete`. Required if `create_security_group` is `false`, ignored otherwise. EOT + validation { + condition = length(var.target_security_group_id) < 2 + error_message = "Only 1 security group can be targeted." + + } } variable "security_group_name" { From 3e1cc676008fa1f45fd53d0f6ae969028e6710ec Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 12 Aug 2021 22:06:41 -0700 Subject: [PATCH 15/19] Apply suggestions from @aknysh code review Co-authored-by: Andriy Knysh --- README.yaml | 22 +++++++++++----------- examples/complete/main.tf | 4 ++-- examples/complete/outputs.tf | 8 ++++---- examples/complete/variables.tf | 5 +++-- exports/security_group_inputs.tf | 8 ++++---- test/src/go.mod | 2 +- variables.tf | 3 +-- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/README.yaml b/README.yaml index ddb451f..852a7fd 100644 --- a/README.yaml +++ b/README.yaml @@ -77,7 +77,7 @@ usage: |- type. It is a catch-all label for value that is itself a collection of attributes and values. (This will become a bit clearer after we define `maps` and contrast them with `objects`) - The rule of the collections types is that the values in the collections must all be they exact same type. + The rule of the collection types is that the values in the collections must all be the exact same type. For example, you cannot have a list where some values are boolean and some are string. Maps require that all keys be strings, but the map values can be any type, except again all the values in a map must be the same type. In other words, the values of a map must form a valid list. @@ -88,7 +88,7 @@ usage: |- The "type" of an object is itself an object: the attributes are the same, and the values are the types of the values. So although `{ foo = "bar", baz = {} }` and `{ foo = "bar", baz = [] }` are both objects, - they are not of the same type. This means you can not put them both in the same list or the same map. + they are not of the same type. This means you cannot put them both in the same list or the same map. Similarly, and closer to the problem at hand, ```hcl cidr_rule = { @@ -139,7 +139,7 @@ usage: |- The `rules` input takes an object. - The attribute names (keys) of the object can be anything you want, but need to be known during `terraform apply`, - which means they cannot depend on any resources created or changed by the Terraform. + which means they cannot depend on any resources created or changed by Terraform. - The values of the attributes are lists of objects, each object representing one Security Group Rule. As explained above in "Why the input is so complex", each object in the list must be exactly the same type. To use multiple types, you must put them in separate lists which are values of separate attributes. @@ -154,11 +154,11 @@ usage: |- prevent Terraform from modifying it unnecessarily. If the `key` is not provided, Terraform will assign an identifier based on the rule's position in its list, which can cause a ripple effect of rules being deleted and recreated if a rule gets deleted from start of a list, causing all the other rules to shift position. - See ["Unexplained changes..."](#unexpected-changes-during-plan-and-apply) below for more details. + See ["Unexpected changes..."](#unexpected-changes-during-plan-and-apply) below for more details. ##### `rule_matrix` input The other way to set rules is via the `rule_matrix` input. This splits the attributes of the `aws_security_group_rule` - resource into to sets: one set defines the rule and descripition, the other set defines the subjects of the rule. + resource into two sets: one set defines the rule and description, the other set defines the subjects of the rule. Again, optional "key" values can provide stability, but cannot contain derived values. As with `rules` and explained above in "Why the input is so complex", all elements of the list must be the exact same type. @@ -171,7 +171,7 @@ usage: |- but any attribute appearing in one object must appear in all the objects. - Any attribute that takes a list value in any object must contain a list in all objects. Use an empty list rather than `null` to indicate "no value". Passing in `null` instead of a list - may cause Terraform to crash or omit confusing error messages (e.g. "number is required"). + may cause Terraform to crash or emit confusing error messages (e.g. "number is required"). - Any attribute that takes a value of type other than list can be set to `null` in objects where no value is needed. The schema for `rule_matrix` is: @@ -185,7 +185,7 @@ usage: |- ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to prefix_list_ids = list of prefix list IDs to apply all rules to - self = bool set "true" to apply the rules to the created or existing security group, null otherwise + self = boolean value; set it to "true" to apply the rules to the created or existing security group, null otherwise # each rule in the rules list will be applied to every subject defined above rules = [{ @@ -193,7 +193,7 @@ usage: |- type = type of rule, either "ingress" or "egress" from_port = start range of protocol port to_port = end range of protocol port, max is 65535 - protocol = ip protocol name or number or "-1" for all protocols and ports + protocol = IP protocol name or number, or "-1" for all protocols and ports description = free form text description of the rule }] @@ -239,7 +239,7 @@ usage: |- You will either have to delete and recreate the security group or manually delete all the security group rules via the AWS console or CLI before applying `inline_rules_enabled = false`. - **_Objects not of the same type_**: Any time you provide a list of object, Terraform requires that all objects in the list + **_Objects not of the same type_**: Any time you provide a list of objects, Terraform requires that all objects in the list must be [the exact same type](https://www.terraform.io/docs/language/expressions/type-constraints.html#dynamic-types-the-quot-any-quot-constraint). This means that all objects in the list have exactly the same set of attributes and that each attribute has the same type of value in every object. So while some attributes are optional for this module, if you include an attribute in any one of the objects in a list, then you @@ -285,7 +285,7 @@ usage: |- # based on the inputs to the null-label module, which means you cannot # reuse the label as-is for more than one security group in the VPC. # - # Here we add an attibute to give the security group a unique name. + # Here we add an attribute to give the security group a unique name. attributes = ["primary"] # Allow unlimited egress @@ -324,7 +324,7 @@ usage: |- # Cloud Posse recommends pinning every module to a specific version # version = "x.x.x" - # Add an attibute to give the Security Group a unique name + # Add an attribute to give the Security Group a unique name attributes = ["mysql"] # Allow unlimited egress diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 8697f32..79a7fc3 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -18,7 +18,7 @@ resource "random_integer" "coin" { min = 1 } -# Create one new security group +# Create a new security group module "new_security_group" { source = "../.." @@ -36,7 +36,7 @@ module "new_security_group" { ipv6_cidr_blocks = [module.vpc.ipv6_cidr_block] prefix_list_ids = [] - # Making `self` derived should break count, as it legitimately makes + # Making `self` derived should break `count`, as it legitimately makes # the count impossible to predict # self = random_integer.coin.result > 0 self = var.rule_matrix_self diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf index 9844bae..cea6751 100644 --- a/examples/complete/outputs.tf +++ b/examples/complete/outputs.tf @@ -1,15 +1,15 @@ output "created_sg_id" { - description = "The new one Security Group ID" + description = "The ID of the created Security Group" value = module.new_security_group.id } output "created_sg_arn" { - description = "The new one Security Group ARN" + description = "The ARN of the created Security Group" value = module.new_security_group.arn } output "created_sg_name" { - description = "The new one Security Group Name" + description = "The name of the created Security Group" value = module.new_security_group.name } @@ -29,7 +29,7 @@ output "target_sg_arn" { } output "target_sg_name" { - description = "The target Security Group Name" + description = "The target Security Group name" value = module.target_security_group.name } diff --git a/examples/complete/variables.tf b/examples/complete/variables.tf index 9d32fa1..1bd257e 100644 --- a/examples/complete/variables.tf +++ b/examples/complete/variables.tf @@ -3,7 +3,8 @@ variable "region" { } variable "rules" { - type = any + type = any + description = "List of security group rules to apply to the created security group" } variable "rule_matrix_self" { @@ -14,6 +15,6 @@ variable "rule_matrix_self" { variable "inline_rules_enabled" { type = bool - description = "Value to set true to test inline security group rules" + description = "Flag to enable/disable inline security group rules" default = false } diff --git a/exports/security_group_inputs.tf b/exports/security_group_inputs.tf index aa8d45a..3e75e7b 100644 --- a/exports/security_group_inputs.tf +++ b/exports/security_group_inputs.tf @@ -67,16 +67,16 @@ variable "security_group_description" { # In general, if the resource supports changing security groups without deleting # the resource or anything it depends on, then default it to `true` and # note in the release notes and migration documents the option to -# set it to false to preserve the existing security group. +# set it to `false` to preserve the existing security group. # If the resource has to be deleted to change its security group, # then set the default to `false` and highlight the option to change -# it to true in the release notes and migration documents. +# it to `true` in the release notes and migration documents. # ################################ variable "security_group_create_before_destroy" { type = bool # - # Pick true or false and the associated description + # Pick `true` or `false` and the associated description # Replace "the resource" with the name of the resouce, e.g. "EC2 instance" # @@ -148,7 +148,7 @@ variable "allow_all_egress" { type = bool default = true description = <<-EOT - If `true`, the created security group will allow egress on all ports and protocols to all IP address. + If `true`, the created security group will allow egress on all ports and protocols to all IP addresses. If this is false and no egress rules are otherwise specified, then no egress will be allowed. EOT } diff --git a/test/src/go.mod b/test/src/go.mod index 0f46a9c..4317a79 100644 --- a/test/src/go.mod +++ b/test/src/go.mod @@ -1,6 +1,6 @@ module github.com/cloudposse/terraform-example-module -go 1.15 +go 1.16 require ( github.com/gruntwork-io/terratest v0.32.8 diff --git a/variables.tf b/variables.tf index 587b6ac..bed4eb7 100644 --- a/variables.tf +++ b/variables.tf @@ -10,7 +10,6 @@ variable "target_security_group_id" { validation { condition = length(var.target_security_group_id) < 2 error_message = "Only 1 security group can be targeted." - } } @@ -59,7 +58,7 @@ variable "rules" { description = <<-EOT An object (like a map) of lists of Security Group rule objects. All elements of a list must be exactly the same type, so this input accepts an object with keys (attributes) whose values are lists so you can separate different - types into different lists and still pass them into one input. Keys must known at "plan" time. + types into different lists and still pass them into one input. Keys must be known at "plan" time. The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource, except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique and known at "plan" time. From 557db38fbe6e7d6afee9980b3418b172cc11bdda Mon Sep 17 00:00:00 2001 From: cloudpossebot <11232728+cloudpossebot@users.noreply.github.com> Date: Fri, 13 Aug 2021 05:07:40 +0000 Subject: [PATCH 16/19] Auto Format --- README.md | 24 ++++++++++++------------ docs/terraform.md | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 04d3cba..85c9349 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ The `rules` input is a complex object due to the way Terraform handles objects a type. It is a catch-all label for value that is itself a collection of attributes and values. (This will become a bit clearer after we define `maps` and contrast them with `objects`) -The rule of the collections types is that the values in the collections must all be they exact same type. +The rule of the collection types is that the values in the collections must all be the exact same type. For example, you cannot have a list where some values are boolean and some are string. Maps require that all keys be strings, but the map values can be any type, except again all the values in a map must be the same type. In other words, the values of a map must form a valid list. @@ -121,7 +121,7 @@ object do not all have to be the same type. The keys (called "attributes" in an The "type" of an object is itself an object: the attributes are the same, and the values are the types of the values. So although `{ foo = "bar", baz = {} }` and `{ foo = "bar", baz = [] }` are both objects, -they are not of the same type. This means you can not put them both in the same list or the same map. +they are not of the same type. This means you cannot put them both in the same list or the same map. Similarly, and closer to the problem at hand, ```hcl cidr_rule = { @@ -172,7 +172,7 @@ That is why the `rules` object has the structure it has. The `rules` input takes an object. - The attribute names (keys) of the object can be anything you want, but need to be known during `terraform apply`, -which means they cannot depend on any resources created or changed by the Terraform. +which means they cannot depend on any resources created or changed by Terraform. - The values of the attributes are lists of objects, each object representing one Security Group Rule. As explained above in "Why the input is so complex", each object in the list must be exactly the same type. To use multiple types, you must put them in separate lists which are values of separate attributes. @@ -187,11 +187,11 @@ The `key` attribute value, if provided, will be used to identify the Security Gr prevent Terraform from modifying it unnecessarily. If the `key` is not provided, Terraform will assign an identifier based on the rule's position in its list, which can cause a ripple effect of rules being deleted and recreated if a rule gets deleted from start of a list, causing all the other rules to shift position. -See ["Unexplained changes..."](#unexpected-changes-during-plan-and-apply) below for more details. +See ["Unexpected changes..."](#unexpected-changes-during-plan-and-apply) below for more details. ##### `rule_matrix` input The other way to set rules is via the `rule_matrix` input. This splits the attributes of the `aws_security_group_rule` -resource into to sets: one set defines the rule and descripition, the other set defines the subjects of the rule. +resource into two sets: one set defines the rule and description, the other set defines the subjects of the rule. Again, optional "key" values can provide stability, but cannot contain derived values. As with `rules` and explained above in "Why the input is so complex", all elements of the list must be the exact same type. @@ -204,7 +204,7 @@ type by following a few rules: but any attribute appearing in one object must appear in all the objects. - Any attribute that takes a list value in any object must contain a list in all objects. Use an empty list rather than `null` to indicate "no value". Passing in `null` instead of a list - may cause Terraform to crash or omit confusing error messages (e.g. "number is required"). + may cause Terraform to crash or emit confusing error messages (e.g. "number is required"). - Any attribute that takes a value of type other than list can be set to `null` in objects where no value is needed. The schema for `rule_matrix` is: @@ -218,7 +218,7 @@ The schema for `rule_matrix` is: ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to prefix_list_ids = list of prefix list IDs to apply all rules to - self = bool set "true" to apply the rules to the created or existing security group, null otherwise + self = boolean value; set it to "true" to apply the rules to the created or existing security group, null otherwise # each rule in the rules list will be applied to every subject defined above rules = [{ @@ -226,7 +226,7 @@ The schema for `rule_matrix` is: type = type of rule, either "ingress" or "egress" from_port = start range of protocol port to_port = end range of protocol port, max is 65535 - protocol = ip protocol name or number or "-1" for all protocols and ports + protocol = IP protocol name or number, or "-1" for all protocols and ports description = free form text description of the rule }] @@ -272,7 +272,7 @@ Terraform will [complain](https://github.com/hashicorp/terraform/pull/2376) and You will either have to delete and recreate the security group or manually delete all the security group rules via the AWS console or CLI before applying `inline_rules_enabled = false`. -**_Objects not of the same type_**: Any time you provide a list of object, Terraform requires that all objects in the list +**_Objects not of the same type_**: Any time you provide a list of objects, Terraform requires that all objects in the list must be [the exact same type](https://www.terraform.io/docs/language/expressions/type-constraints.html#dynamic-types-the-quot-any-quot-constraint). This means that all objects in the list have exactly the same set of attributes and that each attribute has the same type of value in every object. So while some attributes are optional for this module, if you include an attribute in any one of the objects in a list, then you @@ -318,7 +318,7 @@ module "sg" { # based on the inputs to the null-label module, which means you cannot # reuse the label as-is for more than one security group in the VPC. # - # Here we add an attibute to give the security group a unique name. + # Here we add an attribute to give the security group a unique name. attributes = ["primary"] # Allow unlimited egress @@ -357,7 +357,7 @@ module "sg_mysql" { # Cloud Posse recommends pinning every module to a specific version # version = "x.x.x" - # Add an attibute to give the Security Group a unique name + # Add an attribute to give the Security Group a unique name attributes = ["mysql"] # Allow unlimited egress @@ -460,7 +460,7 @@ Available targets: | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [revoke\_rules\_on\_delete](#input\_revoke\_rules\_on\_delete) | Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting
the security group itself. This is normally not needed. | `bool` | `false` | no | | [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` | `[]` | no | -| [rules](#input\_rules) | An object (like a map) of lists of Security Group rule objects. All elements of a list must be exactly the same
type, so this input accepts an object with keys (attributes) whose values are lists so you can separate different
types into different lists and still pass them into one input. Keys must known at "plan" time.
The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `any` | `{}` | no | +| [rules](#input\_rules) | An object (like a map) of lists of Security Group rule objects. All elements of a list must be exactly the same
type, so this input accepts an object with keys (attributes) whose values are lists so you can separate different
types into different lists and still pass them into one input. Keys must be known at "plan" time.
The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `any` | `{}` | no | | [security\_group\_create\_timeout](#input\_security\_group\_create\_timeout) | How long to wait for the security group to be created. | `string` | `"10m"` | no | | [security\_group\_delete\_timeout](#input\_security\_group\_delete\_timeout) | How long to retry on `DependencyViolation` errors during security group deletion from
lingering ENIs left by certain AWS services such as Elastic Load Balancing. | `string` | `"15m"` | no | | [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced. | `string` | `"Managed by Terraform"` | no | diff --git a/docs/terraform.md b/docs/terraform.md index b1f59cd..e2d4b86 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -48,7 +48,7 @@ | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [revoke\_rules\_on\_delete](#input\_revoke\_rules\_on\_delete) | Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting
the security group itself. This is normally not needed. | `bool` | `false` | no | | [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` | `[]` | no | -| [rules](#input\_rules) | An object (like a map) of lists of Security Group rule objects. All elements of a list must be exactly the same
type, so this input accepts an object with keys (attributes) whose values are lists so you can separate different
types into different lists and still pass them into one input. Keys must known at "plan" time.
The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `any` | `{}` | no | +| [rules](#input\_rules) | An object (like a map) of lists of Security Group rule objects. All elements of a list must be exactly the same
type, so this input accepts an object with keys (attributes) whose values are lists so you can separate different
types into different lists and still pass them into one input. Keys must be known at "plan" time.
The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `any` | `{}` | no | | [security\_group\_create\_timeout](#input\_security\_group\_create\_timeout) | How long to wait for the security group to be created. | `string` | `"10m"` | no | | [security\_group\_delete\_timeout](#input\_security\_group\_delete\_timeout) | How long to retry on `DependencyViolation` errors during security group deletion from
lingering ENIs left by certain AWS services such as Elastic Load Balancing. | `string` | `"15m"` | no | | [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced. | `string` | `"Managed by Terraform"` | no | From 78c93fc8c0c59b6f1e37df1ce575a0d3e12faa1a Mon Sep 17 00:00:00 2001 From: cloudpossebot <11232728+cloudpossebot@users.noreply.github.com> Date: Fri, 27 Aug 2021 08:31:13 +0000 Subject: [PATCH 17/19] Auto Format --- README.md | 685 ++++++++++++++++++++++++++++++++++++++++++++++ docs/terraform.md | 72 +++++ 2 files changed, 757 insertions(+) diff --git a/README.md b/README.md index e69de29..fb1c791 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,685 @@ + + +# terraform-aws-security-group [![Latest Release](https://img.shields.io/github/release/cloudposse/terraform-aws-security-group.svg)](https://github.com/cloudposse/terraform-aws-security-group) [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) + + +[![README Header][readme_header_img]][readme_header_link] + +[![Cloud Posse][logo]](https://cpco.io/homepage) + + + +Terraform module to create AWS Security Group and rules. + +--- + +This project is part of our comprehensive ["SweetOps"](https://cpco.io/sweetops) approach towards DevOps. +[][share_email] +[][share_googleplus] +[][share_facebook] +[][share_reddit] +[][share_linkedin] +[][share_twitter] + + +[![Terraform Open Source Modules](https://docs.cloudposse.com/images/terraform-open-source-modules.svg)][terraform_modules] + + + +It's 100% Open Source and licensed under the [APACHE2](LICENSE). + + + + + + + +We literally have [*hundreds of terraform modules*][terraform_modules] that are Open Source and well-maintained. Check them out! + + + + + + +## Security & Compliance [](https://bridgecrew.io/) + +Security scanning is graciously provided by Bridgecrew. Bridgecrew is the leading fully hosted, cloud-native solution providing continuous Terraform security and compliance. + +| Benchmark | Description | +|--------|---------------| +| [![Infrastructure Security](https://www.bridgecrew.cloud/badges/github/cloudposse/terraform-aws-security-group/general)](https://www.bridgecrew.cloud/link/badge?vcs=github&fullRepo=cloudposse%2Fterraform-aws-security-group&benchmark=INFRASTRUCTURE+SECURITY) | Infrastructure Security Compliance | +| [![CIS KUBERNETES](https://www.bridgecrew.cloud/badges/github/cloudposse/terraform-aws-security-group/cis_kubernetes)](https://www.bridgecrew.cloud/link/badge?vcs=github&fullRepo=cloudposse%2Fterraform-aws-security-group&benchmark=CIS+KUBERNETES+V1.5) | Center for Internet Security, KUBERNETES Compliance | +| [![CIS AWS](https://www.bridgecrew.cloud/badges/github/cloudposse/terraform-aws-security-group/cis_aws)](https://www.bridgecrew.cloud/link/badge?vcs=github&fullRepo=cloudposse%2Fterraform-aws-security-group&benchmark=CIS+AWS+V1.2) | Center for Internet Security, AWS Compliance | +| [![CIS AZURE](https://www.bridgecrew.cloud/badges/github/cloudposse/terraform-aws-security-group/cis_azure)](https://www.bridgecrew.cloud/link/badge?vcs=github&fullRepo=cloudposse%2Fterraform-aws-security-group&benchmark=CIS+AZURE+V1.1) | Center for Internet Security, AZURE Compliance | +| [![PCI-DSS](https://www.bridgecrew.cloud/badges/github/cloudposse/terraform-aws-security-group/pci)](https://www.bridgecrew.cloud/link/badge?vcs=github&fullRepo=cloudposse%2Fterraform-aws-security-group&benchmark=PCI-DSS+V3.2) | Payment Card Industry Data Security Standards Compliance | +| [![NIST-800-53](https://www.bridgecrew.cloud/badges/github/cloudposse/terraform-aws-security-group/nist)](https://www.bridgecrew.cloud/link/badge?vcs=github&fullRepo=cloudposse%2Fterraform-aws-security-group&benchmark=NIST-800-53) | National Institute of Standards and Technology Compliance | +| [![ISO27001](https://www.bridgecrew.cloud/badges/github/cloudposse/terraform-aws-security-group/iso)](https://www.bridgecrew.cloud/link/badge?vcs=github&fullRepo=cloudposse%2Fterraform-aws-security-group&benchmark=ISO27001) | Information Security Management System, ISO/IEC 27001 Compliance | +| [![SOC2](https://www.bridgecrew.cloud/badges/github/cloudposse/terraform-aws-security-group/soc2)](https://www.bridgecrew.cloud/link/badge?vcs=github&fullRepo=cloudposse%2Fterraform-aws-security-group&benchmark=SOC2)| Service Organization Control 2 Compliance | +| [![CIS GCP](https://www.bridgecrew.cloud/badges/github/cloudposse/terraform-aws-security-group/cis_gcp)](https://www.bridgecrew.cloud/link/badge?vcs=github&fullRepo=cloudposse%2Fterraform-aws-security-group&benchmark=CIS+GCP+V1.1) | Center for Internet Security, GCP Compliance | +| [![HIPAA](https://www.bridgecrew.cloud/badges/github/cloudposse/terraform-aws-security-group/hipaa)](https://www.bridgecrew.cloud/link/badge?vcs=github&fullRepo=cloudposse%2Fterraform-aws-security-group&benchmark=HIPAA) | Health Insurance Portability and Accountability Compliance | + + + +## Usage + + +**IMPORTANT:** We do not pin modules to versions in our examples because of the +difficulty of keeping the versions in the documentation in sync with the latest released versions. +We highly recommend that in your code you pin the version to the exact version you are +using so that your infrastructure remains stable, and update versions in a +systematic way so that they do not catch you by surprise. + +Also, because of a bug in the Terraform registry ([hashicorp/terraform#21417](https://github.com/hashicorp/terraform/issues/21417)), +the registry shows many of our inputs as required when in fact they are optional. +The table below correctly indicates which inputs are required. + + +This module is primarily for setting security group rules on a security group. You can provide the +ID of an existing security group to modify, or, by default, this module will create a new security +group and apply the given rules to it. + +##### `rules` input +This module provides 2 ways to set security group rules. The primary way is via the `rules` input. + +The `rules` input is a complex object due to the way Terraform handles objects and types. + +
Why the input is so complex (click to reveal) + +- Terraform has 3 basic simple types: bool, number, string +- Terraform then has 3 collections of simple types: list, map, and set +- Terraform then has one complex type: object. However, this is not really a single +type. It is a catch-all label for value that is itself a collection of attributes and values. +(This will become a bit clearer after we define `maps` and contrast them with `objects`) + +The rule of the collection types is that the values in the collections must all be the exact same type. +For example, you cannot have a list where some values are boolean and some are string. Maps require +that all keys be strings, but the map values can be any type, except again all the values in a map +must be the same type. In other words, the values of a map must form a valid list. + +Objects look just like maps. The difference between an object and a map is that the values in an +object do not all have to be the same type. The keys (called "attributes" in an object) must still be strings. + +The "type" of an object is itself an object: the attributes are the same, and the values are the types of the values. + +So although `{ foo = "bar", baz = {} }` and `{ foo = "bar", baz = [] }` are both objects, +they are not of the same type. This means you cannot put them both in the same list or the same map. +Similarly, and closer to the problem at hand, +```hcl +cidr_rule = { + type = "ingress" + cidr_blocks = ["0.0.0.0/0"] +} +``` +is not the same type as +```hcl +self_rule = { + type = "ingress" + self = true +} +``` +This means you cannot put both of those in the same list. +```hcl +my_rules = tolist([local.cidr_rule, local.self_rule]) +``` +Generates the error +```text +Invalid value for "v" parameter: cannot convert tuple to list of any single type. +``` + +You could make them the same type and put them in a list, +like this: +```hcl +my_rules = tolist([{ + type = "ingress" + cidr_blocks = ["0.0.0.0/0"] + self = null +}, +{ + type = "ingress" + cidr_blocks = [] + self = true +}]) +``` +That remains an option for you when inputting rules, and is probably better when you have full control over all the rules. +However, what if some of the rules are coming from a source outside of your control? You cannot simply add those rules +to your list. So, what to do? Create an object whose attributes' values can be of different types. +```hcl +{ mine = local.my_rules, theirs = var.their_rules } +``` + +That is why the `rules` object has the structure it has. + + + +The `rules` input takes an object. +- The attribute names (keys) of the object can be anything you want, but need to be known during `terraform apply`, +which means they cannot depend on any resources created or changed by Terraform. +- The values of the attributes are lists of objects, each object representing one Security Group Rule. As explained + above in "Why the input is so complex", each object in the list must be exactly the same type. To use multiple types, + you must put them in separate lists which are values of separate attributes. +- The attributes and values of the rule objects are fully compatible (have the same keys and accept the same values) as the +Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule), +except + - The `security_group_id` will be ignored, if present + - You can include an optional `key` attribute. If present, its value must be unique among all security group rules in the + security group, and it must be known during `terraform apply`. + +The `key` attribute value, if provided, will be used to identify the Security Group Rule to Terraform in order to +prevent Terraform from modifying it unnecessarily. If the `key` is not provided, Terraform will assign an identifier +based on the rule's position in its list, which can cause a ripple effect of rules being deleted and recreated if +a rule gets deleted from start of a list, causing all the other rules to shift position. +See ["Unexpected changes..."](#unexpected-changes-during-plan-and-apply) below for more details. + +##### `rule_matrix` input +The other way to set rules is via the `rule_matrix` input. This splits the attributes of the `aws_security_group_rule` +resource into two sets: one set defines the rule and description, the other set defines the subjects of the rule. +Again, optional "key" values can provide stability, but cannot contain derived values. + +As with `rules` and explained above in "Why the input is so complex", all elements of the list must be the exact same type. +This also holds for all the elements of the `rules_matrix.rules` list. Because `rule_matrix` is already +so complex, we do not provide the ability to mix types by packing object within more objects. +All of the elements of the `rule_matrix` list must be exactly the same type. You can make them all the same +type by following a few rules: + +- Every object in a list must have the exact same set of attributes. Most attributes are optional and can be omitted, + but any attribute appearing in one object must appear in all the objects. +- Any attribute that takes a list value in any object must contain a list in all objects. + Use an empty list rather than `null` to indicate "no value". Passing in `null` instead of a list + may cause Terraform to crash or emit confusing error messages (e.g. "number is required"). +- Any attribute that takes a value of type other than list can be set to `null` in objects where no value is needed. + +The schema for `rule_matrix` is: + +```hcl +{ + # these top level lists define all the subjects to which rule_matrix rules will be applied + key = an optional unique key to keep these rules from being affected when other rules change + source_security_group_ids = list of source security group IDs to apply all rules to + cidr_blocks = list of ipv4 CIDR blocks to apply all rules to + ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to + prefix_list_ids = list of prefix list IDs to apply all rules to + + self = boolean value; set it to "true" to apply the rules to the created or existing security group, null otherwise + + # each rule in the rules list will be applied to every subject defined above + rules = [{ + key = an optional unique key to keep this rule from being affected when other rules change + type = type of rule, either "ingress" or "egress" + from_port = start range of protocol port + to_port = end range of protocol port, max is 65535 + protocol = IP protocol name or number, or "-1" for all protocols and ports + + description = free form text description of the rule + }] +} +``` + +##### Create before delete +This module provides a `create_before_delete` option that will, when a security group needs to be replaced, +cause Terraform to create the new one before deleting the old one. We recommend making this `true` for new security groups, +but we default it to `false` because if you import a security group with this setting `true`, that security +group will be deleted and replaced on the first `terraform apply`, which will likely cause a service outage. + +### Important Notes + +##### Unexpected changes during plan and apply +The way Terraform works and the way this module is implemented causes security group rules without keys +to be dependent on their place in the input lists. If a rule is deleted and the other rules therefore move +closer to the start of the list, those rules will be deleted and recreated. This should have no significant +operational impact, but it can make a small change look like a big one when viewing the output of +Terraform plan. + +You can avoid this for the most part by providing the optional keys. Rules with keys will not be +changed if their keys do not change and the rules themselves do not change, except in the case of +`rule_matrix`, where the rules are still dependent on the order of the security groups in +`source_security_group_ids`. You can avoid this by using `rules` instead of `rule_matrix` when you have +more than one security group in the list. + +##### WARNINGS and Caveats + +**_Setting `inline_rules_enabled` is not recommended and NOT SUPPORTED_**: Any issues arising from setting +`inlne_rules_enabled = true` (including issues about setting it to `false` after setting it to `true`) will +not be addressed, because they flow from [fundamental problems](https://github.com/hashicorp/terraform-provider-aws/issues/20046) +with the underlying `aws_security_group` resource. The setting is provided for people who know and accept the +limitations and trade-offs and want to use it anyway. The main advantage is that when using inline rules, +Terraform will perform "drift detection" and attempt to remove any rules it finds in place but not +specified inline. See [this post](https://github.com/hashicorp/terraform-provider-aws/pull/9032#issuecomment-639545250) +for a discussion of the difference between inline and resource rules, +and some of the reasons inline rules are not satisfactory. + +**_KNOWN ISSUE_** ([#20046](https://github.com/hashicorp/terraform-provider-aws/issues/20046)): +If you set `inline_rules_enabled = true`, you cannot later set it to `false`. If you try, +Terraform will [complain](https://github.com/hashicorp/terraform/pull/2376) and fail. +You will either have to delete and recreate the security group or manually delete all +the security group rules via the AWS console or CLI before applying `inline_rules_enabled = false`. + +**_Objects not of the same type_**: Any time you provide a list of objects, Terraform requires that all objects in the list +must be [the exact same type](https://www.terraform.io/docs/language/expressions/type-constraints.html#dynamic-types-the-quot-any-quot-constraint). +This means that all objects in the list have exactly the same set of attributes and that each attribute has the same type +of value in every object. So while some attributes are optional for this module, if you include an attribute in any one of the objects in a list, then you +have to include that same attribute in all of them. In rules where the key would othewise be omitted, include the key with value of `null`, +unless the value is a list type, in which case set the value to `[]` (an empty list), due to [#28137](https://github.com/hashicorp/terraform/issues/28137). + + +### Example code + +```hcl +module "label" { + source = "cloudposse/label/null" + # Cloud Posse recommends pinning every module to a specific version + # version = "x.x.x" + namespace = "eg" + stage = "prod" + name = "bastion" + attributes = ["public"] + delimiter = "-" + + tags = { + "BusinessUnit" = "XYZ", + "Snapshot" = "true" + } +} + +module "vpc" { + source = "cloudposse/vpc/aws" + # Cloud Posse recommends pinning every module to a specific version + # version = "x.x.x" + cidr_block = "10.0.0.0/16" + + context = module.label.context +} + +module "sg" { + source = "cloudposse/security-group/aws" + # Cloud Posse recommends pinning every module to a specific version + # version = "x.x.x" + + vpc_id = module.vpc.vpc_id + + # Security Group names must be unique within a VPC. + # This module follows Cloud Posse naming conventions and generates the name + # based on the inputs to the null-label module, which means you cannot + # reuse the label as-is for more than one security group in the VPC. + # + # Here we add an attribute to give the security group a unique name. + attributes = ["primary"] + + # Allow unlimited egress + allow_all_egress = true + + rules = { in = [ + { + key = "ssh" + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + self = null + description = "Allow SSH from anywhere" + }, + { + key = "HTTP" + type = "ingress" + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = [] + self = true + description = "Allow HTTP from inside the security group" + }, + ]} + + vpc_id = module.vpc.vpc_id + + context = module.label.context +} + +module "sg_mysql" { + source = "cloudposse/security-group/aws" + # Cloud Posse recommends pinning every module to a specific version + # version = "x.x.x" + + # Add an attribute to give the Security Group a unique name + attributes = ["mysql"] + + # Allow unlimited egress + allow_all_egress = true + + rule_matrix =[ + # Allow any of these security groups or the specified prefixes to access MySQL + { + source_security_group_ids = [var.dev_sg, var.uat_sg, var.staging_sg] + prefix_list_ids = [var.mysql_client_prefix_list_id] + rules = [ + { + key = "mysql" + type = "ingress" + from_port = 3306 + to_port = 3306 + protocol = "tcp" + description = "Allow MySQL access from trusted security groups" + } + ] + } + ] + + vpc_id = module.vpc.vpc_id + + context = module.label.context +} + +``` + + + + +## Examples + +We have an example of using this module: +- [`examples/complete`](https://github.com/cloudposse/terraform-aws-security-group/examples/complete) - complete example of using this module + + + + +## Makefile Targets +```text +Available targets: + + help Help screen + help/all Display help for all targets + help/short This help short screen + lint Lint terraform code + +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 0.14.0 | +| [aws](#requirement\_aws) | >= 3.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 3.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_security_group.cbd](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_security_group_rule.keyed](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | 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. | `map(string)` | `{}` | no | +| [allow\_all\_egress](#input\_allow\_all\_egress) | A convenience that adds to the rules specified elsewhere a rule that allows all egress.
If this is false and no egress rules are specified via `rules` or `rule-matrix`, then no egress will be allowed. | `bool` | `false` | no | +| [attributes](#input\_attributes) | 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. | `list(string)` | `[]` | 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 | +| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
We recommend setting this `true` on new security groups, but default it to `false` because `true`
will cause existing security groups to be replaced.
Note that changing this value will always cause the security group to be replaced. | `bool` | `false` | 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 | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | 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`. | `number` | `null` | no | +| [inline\_rules\_enabled](#input\_inline\_rules\_enabled) | NOT RECOMMENDED. Create rules "inline" instead of as separate `aws_security_group_rule` resources.
See [#20046](https://github.com/hashicorp/terraform-provider-aws/issues/20046) for one of several issues with inline rules.
See [this post](https://github.com/hashicorp/terraform-provider-aws/pull/9032#issuecomment-639545250) for details on the difference between inline rules and rule resources. | `bool` | `false` | no | +| [label\_key\_case](#input\_label\_key\_case) | 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`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | 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. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | 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`. | `string` | `null` | no | +| [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 | +| [name](#input\_name) | 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. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | 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 | +| [revoke\_rules\_on\_delete](#input\_revoke\_rules\_on\_delete) | Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting
the security group itself. This is normally not needed. | `bool` | `false` | no | +| [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` | `[]` | no | +| [rules](#input\_rules) | An object (like a map) of lists of Security Group rule objects. All elements of a list must be exactly the same
type, so this input accepts an object with keys (attributes) whose values are lists so you can separate different
types into different lists and still pass them into one input. Keys must be known at "plan" time.
The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `any` | `{}` | no | +| [security\_group\_create\_timeout](#input\_security\_group\_create\_timeout) | How long to wait for the security group to be created. | `string` | `"10m"` | no | +| [security\_group\_delete\_timeout](#input\_security\_group\_delete\_timeout) | How long to retry on `DependencyViolation` errors during security group deletion from
lingering ENIs left by certain AWS services such as Elastic Load Balancing. | `string` | `"15m"` | no | +| [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced. | `string` | `"Managed by Terraform"` | no | +| [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the VPC.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | 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 | +| [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
The Security Group's description will not be changed.
Not compatible with `inline_rules_enabled` or `revoke_rules_on_delete`.
Required if `create_security_group` is `false`, ignored otherwise. | `list(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) | The ID of the VPC where the Security Group will be created. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [arn](#output\_arn) | The created Security Group ARN (null if using existing security group) | +| [id](#output\_id) | The created or target Security Group ID | +| [name](#output\_name) | The created Security Group Name (null if using existing security group) | +| [rules\_terraform\_ids](#output\_rules\_terraform\_ids) | List of Terraform IDs of created `security_group_rule` resources, primarily provided to enable `depends_on` | + + + + +## Share the Love + +Like this project? Please give it a ★ on [our GitHub](https://github.com/cloudposse/terraform-aws-security-group)! (it helps us **a lot**) + +Are you using this project or any of our other projects? Consider [leaving a testimonial][testimonial]. =) + + + +## Related Projects + +Check out these related projects. + +- [terraform-null-label](https://github.com/cloudposse/terraform-null-label) - Terraform module designed to generate consistent names and tags for resources. Use terraform-null-label to implement a strict naming convention. + + +## References + +For additional context, refer to some of these links. + +- [terraform-provider-aws](https://registry.terraform.io/providers/hashicorp/aws/latest) - Terraform AWS provider + + +## Help + +**Got a question?** We got answers. + +File a GitHub [issue](https://github.com/cloudposse/terraform-aws-security-group/issues), send us an [email][email] or join our [Slack Community][slack]. + +[![README Commercial Support][readme_commercial_support_img]][readme_commercial_support_link] + +## DevOps Accelerator for Startups + + +We are a [**DevOps Accelerator**][commercial_support]. We'll help you build your cloud infrastructure from the ground up so you can own it. Then we'll show you how to operate it and stick around for as long as you need us. + +[![Learn More](https://img.shields.io/badge/learn%20more-success.svg?style=for-the-badge)][commercial_support] + +Work directly with our team of DevOps experts via email, slack, and video conferencing. + +We deliver 10x the value for a fraction of the cost of a full-time engineer. Our track record is not even funny. If you want things done right and you need it done FAST, then we're your best bet. + +- **Reference Architecture.** You'll get everything you need from the ground up built using 100% infrastructure as code. +- **Release Engineering.** You'll have end-to-end CI/CD with unlimited staging environments. +- **Site Reliability Engineering.** You'll have total visibility into your apps and microservices. +- **Security Baseline.** You'll have built-in governance with accountability and audit logs for all changes. +- **GitOps.** You'll be able to operate your infrastructure via Pull Requests. +- **Training.** You'll receive hands-on training so your team can operate what we build. +- **Questions.** You'll have a direct line of communication between our teams via a Shared Slack channel. +- **Troubleshooting.** You'll get help to triage when things aren't working. +- **Code Reviews.** You'll receive constructive feedback on Pull Requests. +- **Bug Fixes.** We'll rapidly work with you to fix any bugs in our projects. + +## Slack Community + +Join our [Open Source Community][slack] on Slack. It's **FREE** for everyone! Our "SweetOps" community is where you get to talk with others who share a similar vision for how to rollout and manage infrastructure. This is the best place to talk shop, ask questions, solicit feedback, and work together as a community to build totally *sweet* infrastructure. + +## Discourse Forums + +Participate in our [Discourse Forums][discourse]. Here you'll find answers to commonly asked questions. Most questions will be related to the enormous number of projects we support on our GitHub. Come here to collaborate on answers, find solutions, and get ideas about the products and services we value. It only takes a minute to get started! Just sign in with SSO using your GitHub account. + +## Newsletter + +Sign up for [our newsletter][newsletter] that covers everything on our technology radar. Receive updates on what we're up to on GitHub as well as awesome new projects we discover. + +## Office Hours + +[Join us every Wednesday via Zoom][office_hours] for our weekly "Lunch & Learn" sessions. It's **FREE** for everyone! + +[![zoom](https://img.cloudposse.com/fit-in/200x200/https://cloudposse.com/wp-content/uploads/2019/08/Powered-by-Zoom.png")][office_hours] + +## Contributing + +### Bug Reports & Feature Requests + +Please use the [issue tracker](https://github.com/cloudposse/terraform-aws-security-group/issues) to report any bugs or file feature requests. + +### Developing + +If you are interested in being a contributor and want to get involved in developing this project or [help out](https://cpco.io/help-out) with our other projects, we would love to hear from you! Shoot us an [email][email]. + +In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow. + + 1. **Fork** the repo on GitHub + 2. **Clone** the project to your own machine + 3. **Commit** changes to your own branch + 4. **Push** your work back up to your fork + 5. Submit a **Pull Request** so that we can review your changes + +**NOTE:** Be sure to merge the latest changes from "upstream" before making a pull request! + + + +## Copyrights + +Copyright © 2021-2021 [Cloud Posse, LLC](https://cloudposse.com) + + + + + +## License + +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +See [LICENSE](LICENSE) for full details. + +```text +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +``` + + + + + + + + + +## Trademarks + +All other trademarks referenced herein are the property of their respective owners. + +## About + +This project is maintained and funded by [Cloud Posse, LLC][website]. Like it? Please let us know by [leaving a testimonial][testimonial]! + +[![Cloud Posse][logo]][website] + +We're a [DevOps Professional Services][hire] company based in Los Angeles, CA. We ❤️ [Open Source Software][we_love_open_source]. + +We offer [paid support][commercial_support] on all of our projects. + +Check out [our other projects][github], [follow us on twitter][twitter], [apply for a job][jobs], or [hire us][hire] to help with your cloud strategy and implementation. + + + +### Contributors + + +| [![Erik Osterman][osterman_avatar]][osterman_homepage]
[Erik Osterman][osterman_homepage] | [![Vladimir][SweetOps_avatar]][SweetOps_homepage]
[Vladimir][SweetOps_homepage] | +|---|---| + + + [osterman_homepage]: https://github.com/osterman + [osterman_avatar]: https://img.cloudposse.com/150x150/https://github.com/osterman.png + [SweetOps_homepage]: https://github.com/SweetOps + [SweetOps_avatar]: https://img.cloudposse.com/150x150/https://github.com/SweetOps.png + +[![README Footer][readme_footer_img]][readme_footer_link] +[![Beacon][beacon]][website] + + [logo]: https://cloudposse.com/logo-300x69.svg + [docs]: https://cpco.io/docs?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=docs + [website]: https://cpco.io/homepage?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=website + [github]: https://cpco.io/github?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=github + [jobs]: https://cpco.io/jobs?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=jobs + [hire]: https://cpco.io/hire?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=hire + [slack]: https://cpco.io/slack?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=slack + [linkedin]: https://cpco.io/linkedin?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=linkedin + [twitter]: https://cpco.io/twitter?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=twitter + [testimonial]: https://cpco.io/leave-testimonial?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=testimonial + [office_hours]: https://cloudposse.com/office-hours?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=office_hours + [newsletter]: https://cpco.io/newsletter?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=newsletter + [discourse]: https://ask.sweetops.com/?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=discourse + [email]: https://cpco.io/email?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=email + [commercial_support]: https://cpco.io/commercial-support?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=commercial_support + [we_love_open_source]: https://cpco.io/we-love-open-source?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=we_love_open_source + [terraform_modules]: https://cpco.io/terraform-modules?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=terraform_modules + [readme_header_img]: https://cloudposse.com/readme/header/img + [readme_header_link]: https://cloudposse.com/readme/header/link?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=readme_header_link + [readme_footer_img]: https://cloudposse.com/readme/footer/img + [readme_footer_link]: https://cloudposse.com/readme/footer/link?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=readme_footer_link + [readme_commercial_support_img]: https://cloudposse.com/readme/commercial-support/img + [readme_commercial_support_link]: https://cloudposse.com/readme/commercial-support/link?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=readme_commercial_support_link + [share_twitter]: https://twitter.com/intent/tweet/?text=terraform-aws-security-group&url=https://github.com/cloudposse/terraform-aws-security-group + [share_linkedin]: https://www.linkedin.com/shareArticle?mini=true&title=terraform-aws-security-group&url=https://github.com/cloudposse/terraform-aws-security-group + [share_reddit]: https://reddit.com/submit/?url=https://github.com/cloudposse/terraform-aws-security-group + [share_facebook]: https://facebook.com/sharer/sharer.php?u=https://github.com/cloudposse/terraform-aws-security-group + [share_googleplus]: https://plus.google.com/share?url=https://github.com/cloudposse/terraform-aws-security-group + [share_email]: mailto:?subject=terraform-aws-security-group&body=https://github.com/cloudposse/terraform-aws-security-group + [beacon]: https://ga-beacon.cloudposse.com/UA-76589703-4/cloudposse/terraform-aws-security-group?pixel&cs=github&cm=readme&an=terraform-aws-security-group diff --git a/docs/terraform.md b/docs/terraform.md index e69de29..0de39f1 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -0,0 +1,72 @@ + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 0.14.0 | +| [aws](#requirement\_aws) | >= 3.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 3.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_security_group.cbd](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_security_group_rule.keyed](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | 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. | `map(string)` | `{}` | no | +| [allow\_all\_egress](#input\_allow\_all\_egress) | A convenience that adds to the rules specified elsewhere a rule that allows all egress.
If this is false and no egress rules are specified via `rules` or `rule-matrix`, then no egress will be allowed. | `bool` | `false` | no | +| [attributes](#input\_attributes) | 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. | `list(string)` | `[]` | 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 | +| [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
We recommend setting this `true` on new security groups, but default it to `false` because `true`
will cause existing security groups to be replaced.
Note that changing this value will always cause the security group to be replaced. | `bool` | `false` | 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 | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | 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`. | `number` | `null` | no | +| [inline\_rules\_enabled](#input\_inline\_rules\_enabled) | NOT RECOMMENDED. Create rules "inline" instead of as separate `aws_security_group_rule` resources.
See [#20046](https://github.com/hashicorp/terraform-provider-aws/issues/20046) for one of several issues with inline rules.
See [this post](https://github.com/hashicorp/terraform-provider-aws/pull/9032#issuecomment-639545250) for details on the difference between inline rules and rule resources. | `bool` | `false` | no | +| [label\_key\_case](#input\_label\_key\_case) | 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`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | 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. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | 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`. | `string` | `null` | no | +| [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 | +| [name](#input\_name) | 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. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | 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 | +| [revoke\_rules\_on\_delete](#input\_revoke\_rules\_on\_delete) | Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting
the security group itself. This is normally not needed. | `bool` | `false` | no | +| [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` | `[]` | no | +| [rules](#input\_rules) | An object (like a map) of lists of Security Group rule objects. All elements of a list must be exactly the same
type, so this input accepts an object with keys (attributes) whose values are lists so you can separate different
types into different lists and still pass them into one input. Keys must be known at "plan" time.
The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `any` | `{}` | no | +| [security\_group\_create\_timeout](#input\_security\_group\_create\_timeout) | How long to wait for the security group to be created. | `string` | `"10m"` | no | +| [security\_group\_delete\_timeout](#input\_security\_group\_delete\_timeout) | How long to retry on `DependencyViolation` errors during security group deletion from
lingering ENIs left by certain AWS services such as Elastic Load Balancing. | `string` | `"15m"` | no | +| [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced. | `string` | `"Managed by Terraform"` | no | +| [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the VPC.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | 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 | +| [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
The Security Group's description will not be changed.
Not compatible with `inline_rules_enabled` or `revoke_rules_on_delete`.
Required if `create_security_group` is `false`, ignored otherwise. | `list(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) | The ID of the VPC where the Security Group will be created. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [arn](#output\_arn) | The created Security Group ARN (null if using existing security group) | +| [id](#output\_id) | The created or target Security Group ID | +| [name](#output\_name) | The created Security Group Name (null if using existing security group) | +| [rules\_terraform\_ids](#output\_rules\_terraform\_ids) | List of Terraform IDs of created `security_group_rule` resources, primarily provided to enable `depends_on` | + From a3842ae84edb636c9b63938b8e8c1cdc179e99e0 Mon Sep 17 00:00:00 2001 From: Nuru Date: Sat, 28 Aug 2021 15:41:30 -0700 Subject: [PATCH 18/19] Add rules_map input --- .github/auto-release.yml | 1 + .github/workflows/auto-release.yml | 3 +- README.md | 77 ++++++++++++--------- README.yaml | 66 ++++++++++-------- docs/terraform.md | 5 +- examples/complete/fixtures.us-east-2.tfvars | 24 ++----- examples/complete/main.tf | 28 +++++++- exports/security_group_inputs.tf | 11 +-- main.tf | 8 +-- normalize.tf | 7 +- variables.tf | 28 ++++++-- 11 files changed, 160 insertions(+), 98 deletions(-) diff --git a/.github/auto-release.yml b/.github/auto-release.yml index 39a7f1e..9976e10 100644 --- a/.github/auto-release.yml +++ b/.github/auto-release.yml @@ -17,6 +17,7 @@ version-resolver: - 'bugfix' - 'bug' - 'hotfix' + - 'no-release' default: 'minor' categories: diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 1d06d9b..3a38fae 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -18,9 +18,8 @@ jobs: github_token: ${{ secrets.PUBLIC_REPO_ACCESS_TOKEN }} # Drafts your next Release notes as Pull Requests are merged into "main" - uses: release-drafter/release-drafter@v5 - if: "!contains(steps.get-merged-pull-request.outputs.labels, 'no-release')" with: - publish: true + publish: ${{ !contains(steps.get-merged-pull-request.outputs.labels, 'no-release') }} prerelease: false config-name: auto-release.yml env: diff --git a/README.md b/README.md index fb1c791..78f55b5 100644 --- a/README.md +++ b/README.md @@ -97,32 +97,39 @@ This module is primarily for setting security group rules on a security group. Y ID of an existing security group to modify, or, by default, this module will create a new security group and apply the given rules to it. -##### `rules` input -This module provides 2 ways to set security group rules. The primary way is via the `rules` input. +##### `rules` and `rules_map` inputs +This module provides 3 ways to set security group rules. You can use any or all of them at the same time. -The `rules` input is a complex object due to the way Terraform handles objects and types. +The easy way to specify rules is via the `rules` input. It takes a list of rules. (We will define +a rule [a bit later](#definition-of-a-rule).) The problem is that a Terraform list must be composed +of elements that are all the exact same type, and rules can be any of several +different Terraform types. So to get around this restriction, the second +way to specify rules is via the `rules_map` input, which is more complex.
Why the input is so complex (click to reveal) - Terraform has 3 basic simple types: bool, number, string - Terraform then has 3 collections of simple types: list, map, and set -- Terraform then has one complex type: object. However, this is not really a single -type. It is a catch-all label for value that is itself a collection of attributes and values. +- Terraform then has 2 structural types: object and tuple. However, these are not really single +types. They are catch-all labels for values that are themselves combination of other values. (This will become a bit clearer after we define `maps` and contrast them with `objects`) -The rule of the collection types is that the values in the collections must all be the exact same type. +One [rule of the collection types](https://www.terraform.io/docs/language/expressions/type-constraints.html#collection-types) +is that the values in the collections must all be the exact same type. For example, you cannot have a list where some values are boolean and some are string. Maps require that all keys be strings, but the map values can be any type, except again all the values in a map must be the same type. In other words, the values of a map must form a valid list. Objects look just like maps. The difference between an object and a map is that the values in an -object do not all have to be the same type. The keys (called "attributes" in an object) must still be strings. +object do not all have to be the same type. -The "type" of an object is itself an object: the attributes are the same, and the values are the types of the values. +The "type" of an object is itself an object: the keys are the same, and the values are the types of the values in the object. So although `{ foo = "bar", baz = {} }` and `{ foo = "bar", baz = [] }` are both objects, -they are not of the same type. This means you cannot put them both in the same list or the same map. +they are not of the same type. This means you cannot put them both in the same list or the same map, +even though you can put them in a single tuple or object. Similarly, and closer to the problem at hand, + ```hcl cidr_rule = { type = "ingress" @@ -138,7 +145,7 @@ self_rule = { ``` This means you cannot put both of those in the same list. ```hcl -my_rules = tolist([local.cidr_rule, local.self_rule]) +rules = tolist([local.cidr_rule, local.self_rule]) ``` Generates the error ```text @@ -148,7 +155,7 @@ Invalid value for "v" parameter: cannot convert tuple to list of any single type You could make them the same type and put them in a list, like this: ```hcl -my_rules = tolist([{ +rules = tolist([{ type = "ingress" cidr_blocks = ["0.0.0.0/0"] self = null @@ -159,29 +166,35 @@ my_rules = tolist([{ self = true }]) ``` -That remains an option for you when inputting rules, and is probably better when you have full control over all the rules. +That remains an option for you when generating the rules, and is probably better when you have full control over all the rules. However, what if some of the rules are coming from a source outside of your control? You cannot simply add those rules to your list. So, what to do? Create an object whose attributes' values can be of different types. ```hcl { mine = local.my_rules, theirs = var.their_rules } ``` -That is why the `rules` object has the structure it has. +That is why the `rules_map` input is available. It will accept a structure like that, an object whose +attribute values are lists of rules, where the lists themselves can be different types. -The `rules` input takes an object. -- The attribute names (keys) of the object can be anything you want, but need to be known during `terraform apply`, +The `rules_map` input takes an object. +- The attribute names (keys) of the object can be anything you want, but need to be known during `terraform plan`, which means they cannot depend on any resources created or changed by Terraform. -- The values of the attributes are lists of objects, each object representing one Security Group Rule. As explained +- The values of the attributes are lists of rule objects, each object representing one Security Group Rule. As explained above in "Why the input is so complex", each object in the list must be exactly the same type. To use multiple types, you must put them in separate lists which are values of separate attributes. + +###### Definition of a rule + +For this module, a rule is defined as an object. - The attributes and values of the rule objects are fully compatible (have the same keys and accept the same values) as the Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule), except - The `security_group_id` will be ignored, if present - You can include an optional `key` attribute. If present, its value must be unique among all security group rules in the - security group, and it must be known during `terraform apply`. + security group, and it must be known in the Terraform "plan" phase, meaning it cannot depend on anything being + generated or created by Terraform. The `key` attribute value, if provided, will be used to identify the Security Group Rule to Terraform in order to prevent Terraform from modifying it unnecessarily. If the `key` is not provided, Terraform will assign an identifier @@ -189,6 +202,7 @@ based on the rule's position in its list, which can cause a ripple effect of rul a rule gets deleted from start of a list, causing all the other rules to shift position. See ["Unexpected changes..."](#unexpected-changes-during-plan-and-apply) below for more details. + ##### `rule_matrix` input The other way to set rules is via the `rule_matrix` input. This splits the attributes of the `aws_security_group_rule` resource into two sets: one set defines the rule and description, the other set defines the subjects of the rule. @@ -280,7 +294,13 @@ have to include that same attribute in all of them. In rules where the key woul unless the value is a list type, in which case set the value to `[]` (an empty list), due to [#28137](https://github.com/hashicorp/terraform/issues/28137). -### Example code + + +## Examples + + +See [examples/complete/main.tf](https://github.com/cloudposse/terraform-aws-security-group/examples/complete/main.tf) for +even more examples. ```hcl module "label" { @@ -312,8 +332,6 @@ module "sg" { source = "cloudposse/security-group/aws" # Cloud Posse recommends pinning every module to a specific version # version = "x.x.x" - - vpc_id = module.vpc.vpc_id # Security Group names must be unique within a VPC. # This module follows Cloud Posse naming conventions and generates the name @@ -326,7 +344,7 @@ module "sg" { # Allow unlimited egress allow_all_egress = true - rules = { in = [ + rules = [ { key = "ssh" type = "ingress" @@ -346,8 +364,8 @@ module "sg" { cidr_blocks = [] self = true description = "Allow HTTP from inside the security group" - }, - ]} + } + ] vpc_id = module.vpc.vpc_id @@ -392,14 +410,6 @@ module "sg_mysql" { - -## Examples - -We have an example of using this module: -- [`examples/complete`](https://github.com/cloudposse/terraform-aws-security-group/examples/complete) - complete example of using this module - - - ## Makefile Targets ```text @@ -464,11 +474,12 @@ Available targets: | [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 | | [revoke\_rules\_on\_delete](#input\_revoke\_rules\_on\_delete) | Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting
the security group itself. This is normally not needed. | `bool` | `false` | no | | [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` | `[]` | no | -| [rules](#input\_rules) | An object (like a map) of lists of Security Group rule objects. All elements of a list must be exactly the same
type, so this input accepts an object with keys (attributes) whose values are lists so you can separate different
types into different lists and still pass them into one input. Keys must be known at "plan" time.
The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `any` | `{}` | no | +| [rules](#input\_rules) | A list of Security Group rule objects. All elements of a list must be exactly the same type;
use `rules_map` if you want to supply multiple lists of different types.
The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | +| [rules\_map](#input\_rules\_map) | A map-like object of lists of Security Group rule objects. All elements of a list must be exactly the same type,
so this input accepts an object with keys (attributes) whose values are lists so you can separate different
types into different lists and still pass them into one input. Keys must be known at "plan" time.
The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `any` | `{}` | no | | [security\_group\_create\_timeout](#input\_security\_group\_create\_timeout) | How long to wait for the security group to be created. | `string` | `"10m"` | no | | [security\_group\_delete\_timeout](#input\_security\_group\_delete\_timeout) | How long to retry on `DependencyViolation` errors during security group deletion from
lingering ENIs left by certain AWS services such as Elastic Load Balancing. | `string` | `"15m"` | no | | [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced. | `string` | `"Managed by Terraform"` | no | -| [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the VPC.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | +| [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the VPC.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `list(string)` | `[]` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | 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 | | [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
The Security Group's description will not be changed.
Not compatible with `inline_rules_enabled` or `revoke_rules_on_delete`.
Required if `create_security_group` is `false`, ignored otherwise. | `list(string)` | `[]` | no | diff --git a/README.yaml b/README.yaml index 8bb4a42..c454734 100644 --- a/README.yaml +++ b/README.yaml @@ -64,32 +64,39 @@ usage: |- ID of an existing security group to modify, or, by default, this module will create a new security group and apply the given rules to it. - ##### `rules` input - This module provides 2 ways to set security group rules. The primary way is via the `rules` input. + ##### `rules` and `rules_map` inputs + This module provides 3 ways to set security group rules. You can use any or all of them at the same time. - The `rules` input is a complex object due to the way Terraform handles objects and types. + The easy way to specify rules is via the `rules` input. It takes a list of rules. (We will define + a rule [a bit later](#definition-of-a-rule).) The problem is that a Terraform list must be composed + of elements that are all the exact same type, and rules can be any of several + different Terraform types. So to get around this restriction, the second + way to specify rules is via the `rules_map` input, which is more complex.
Why the input is so complex (click to reveal) - Terraform has 3 basic simple types: bool, number, string - Terraform then has 3 collections of simple types: list, map, and set - - Terraform then has one complex type: object. However, this is not really a single - type. It is a catch-all label for value that is itself a collection of attributes and values. + - Terraform then has 2 structural types: object and tuple. However, these are not really single + types. They are catch-all labels for values that are themselves combination of other values. (This will become a bit clearer after we define `maps` and contrast them with `objects`) - The rule of the collection types is that the values in the collections must all be the exact same type. + One [rule of the collection types](https://www.terraform.io/docs/language/expressions/type-constraints.html#collection-types) + is that the values in the collections must all be the exact same type. For example, you cannot have a list where some values are boolean and some are string. Maps require that all keys be strings, but the map values can be any type, except again all the values in a map must be the same type. In other words, the values of a map must form a valid list. Objects look just like maps. The difference between an object and a map is that the values in an - object do not all have to be the same type. The keys (called "attributes" in an object) must still be strings. + object do not all have to be the same type. - The "type" of an object is itself an object: the attributes are the same, and the values are the types of the values. + The "type" of an object is itself an object: the keys are the same, and the values are the types of the values in the object. So although `{ foo = "bar", baz = {} }` and `{ foo = "bar", baz = [] }` are both objects, - they are not of the same type. This means you cannot put them both in the same list or the same map. + they are not of the same type. This means you cannot put them both in the same list or the same map, + even though you can put them in a single tuple or object. Similarly, and closer to the problem at hand, + ```hcl cidr_rule = { type = "ingress" @@ -105,7 +112,7 @@ usage: |- ``` This means you cannot put both of those in the same list. ```hcl - my_rules = tolist([local.cidr_rule, local.self_rule]) + rules = tolist([local.cidr_rule, local.self_rule]) ``` Generates the error ```text @@ -115,7 +122,7 @@ usage: |- You could make them the same type and put them in a list, like this: ```hcl - my_rules = tolist([{ + rules = tolist([{ type = "ingress" cidr_blocks = ["0.0.0.0/0"] self = null @@ -126,29 +133,35 @@ usage: |- self = true }]) ``` - That remains an option for you when inputting rules, and is probably better when you have full control over all the rules. + That remains an option for you when generating the rules, and is probably better when you have full control over all the rules. However, what if some of the rules are coming from a source outside of your control? You cannot simply add those rules to your list. So, what to do? Create an object whose attributes' values can be of different types. ```hcl { mine = local.my_rules, theirs = var.their_rules } ``` - That is why the `rules` object has the structure it has. + That is why the `rules_map` input is available. It will accept a structure like that, an object whose + attribute values are lists of rules, where the lists themselves can be different types. - The `rules` input takes an object. - - The attribute names (keys) of the object can be anything you want, but need to be known during `terraform apply`, + The `rules_map` input takes an object. + - The attribute names (keys) of the object can be anything you want, but need to be known during `terraform plan`, which means they cannot depend on any resources created or changed by Terraform. - - The values of the attributes are lists of objects, each object representing one Security Group Rule. As explained + - The values of the attributes are lists of rule objects, each object representing one Security Group Rule. As explained above in "Why the input is so complex", each object in the list must be exactly the same type. To use multiple types, you must put them in separate lists which are values of separate attributes. + + ###### Definition of a rule + + For this module, a rule is defined as an object. - The attributes and values of the rule objects are fully compatible (have the same keys and accept the same values) as the Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule), except - The `security_group_id` will be ignored, if present - You can include an optional `key` attribute. If present, its value must be unique among all security group rules in the - security group, and it must be known during `terraform apply`. + security group, and it must be known in the Terraform "plan" phase, meaning it cannot depend on anything being + generated or created by Terraform. The `key` attribute value, if provided, will be used to identify the Security Group Rule to Terraform in order to prevent Terraform from modifying it unnecessarily. If the `key` is not provided, Terraform will assign an identifier @@ -156,6 +169,7 @@ usage: |- a rule gets deleted from start of a list, causing all the other rules to shift position. See ["Unexpected changes..."](#unexpected-changes-during-plan-and-apply) below for more details. + ##### `rule_matrix` input The other way to set rules is via the `rule_matrix` input. This splits the attributes of the `aws_security_group_rule` resource into two sets: one set defines the rule and description, the other set defines the subjects of the rule. @@ -247,7 +261,11 @@ usage: |- unless the value is a list type, in which case set the value to `[]` (an empty list), due to [#28137](https://github.com/hashicorp/terraform/issues/28137). - ### Example code +# Example usage +examples: |- + + See [examples/complete/main.tf](https://github.com/cloudposse/terraform-aws-security-group/examples/complete/main.tf) for + even more examples. ```hcl module "label" { @@ -279,8 +297,6 @@ usage: |- source = "cloudposse/security-group/aws" # Cloud Posse recommends pinning every module to a specific version # version = "x.x.x" - - vpc_id = module.vpc.vpc_id # Security Group names must be unique within a VPC. # This module follows Cloud Posse naming conventions and generates the name @@ -293,7 +309,7 @@ usage: |- # Allow unlimited egress allow_all_egress = true - rules = { in = [ + rules = [ { key = "ssh" type = "ingress" @@ -313,8 +329,8 @@ usage: |- cidr_blocks = [] self = true description = "Allow HTTP from inside the security group" - }, - ]} + } + ] vpc_id = module.vpc.vpc_id @@ -357,10 +373,6 @@ usage: |- ``` -# Example usage -examples: |- - We have an example of using this module: - - [`examples/complete`](https://github.com/cloudposse/terraform-aws-security-group/examples/complete) - complete example of using this module # How to get started quickly #quickstart: |- diff --git a/docs/terraform.md b/docs/terraform.md index 0de39f1..db124f5 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -50,11 +50,12 @@ | [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 | | [revoke\_rules\_on\_delete](#input\_revoke\_rules\_on\_delete) | Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting
the security group itself. This is normally not needed. | `bool` | `false` | no | | [rule\_matrix](#input\_rule\_matrix) | A convenient way to apply the same set of rules to a set of subjects. See README for details. | `any` | `[]` | no | -| [rules](#input\_rules) | An object (like a map) of lists of Security Group rule objects. All elements of a list must be exactly the same
type, so this input accepts an object with keys (attributes) whose values are lists so you can separate different
types into different lists and still pass them into one input. Keys must be known at "plan" time.
The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `any` | `{}` | no | +| [rules](#input\_rules) | A list of Security Group rule objects. All elements of a list must be exactly the same type;
use `rules_map` if you want to supply multiple lists of different types.
The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | +| [rules\_map](#input\_rules\_map) | A map-like object of lists of Security Group rule objects. All elements of a list must be exactly the same type,
so this input accepts an object with keys (attributes) whose values are lists so you can separate different
types into different lists and still pass them into one input. Keys must be known at "plan" time.
The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `any` | `{}` | no | | [security\_group\_create\_timeout](#input\_security\_group\_create\_timeout) | How long to wait for the security group to be created. | `string` | `"10m"` | no | | [security\_group\_delete\_timeout](#input\_security\_group\_delete\_timeout) | How long to retry on `DependencyViolation` errors during security group deletion from
lingering ENIs left by certain AWS services such as Elastic Load Balancing. | `string` | `"15m"` | no | | [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced. | `string` | `"Managed by Terraform"` | no | -| [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the VPC.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `string` | `""` | no | +| [security\_group\_name](#input\_security\_group\_name) | The name to assign to the security group. Must be unique within the VPC.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `list(string)` | `[]` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | 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 | | [target\_security\_group\_id](#input\_target\_security\_group\_id) | The ID of an existing Security Group to which Security Group rules will be assigned.
The Security Group's description will not be changed.
Not compatible with `inline_rules_enabled` or `revoke_rules_on_delete`.
Required if `create_security_group` is `false`, ignored otherwise. | `list(string)` | `[]` | no | diff --git a/examples/complete/fixtures.us-east-2.tfvars b/examples/complete/fixtures.us-east-2.tfvars index 723a04c..095567a 100644 --- a/examples/complete/fixtures.us-east-2.tfvars +++ b/examples/complete/fixtures.us-east-2.tfvars @@ -8,7 +8,7 @@ stage = "test" name = "sg" -rules = { default = [ +rules = [ { key = null # "ssh all" type = "ingress" @@ -19,24 +19,12 @@ rules = { default = [ description = "SSH wide open" }, { - key = "https all" + key = "telnet all" type = "ingress" - from_port = 443 - to_port = 443 + from_port = 23 + to_port = 23 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] - description = "HTTPS wide open" + description = "Telenet wide open" } - ], - ipv6 = [ - { - # no key provided - type = "ingress" - from_port = 22 - to_port = 22 - protocol = "tcp" - ipv6_cidr_blocks = ["::/0"] - description = "SSH wide open" - } - -] } +] diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 79a7fc3..f4786b0 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -1,3 +1,9 @@ +# Terraform for testing with terratest +# +# For this module, a large portion of the test is simply +# verifying that Terraform can generate a plan without errors. +# + provider "aws" { region = var.region } @@ -18,6 +24,21 @@ resource "random_integer" "coin" { min = 1 } +locals { + coin = [random_integer.coin.result] +} + +module "simple_security_group" { + source = "../.." + + attributes = ["simple"] + rules = var.rules + + vpc_id = module.vpc.vpc_id + + context = module.this.context +} + # Create a new security group module "new_security_group" { @@ -32,6 +53,8 @@ module "new_security_group" { # The dynamic value for source_security_group_ids breaks Terraform 0.13 but should work in 0.14 or later source_security_group_ids = [aws_security_group.target.id] # Either dynamic value for CIDRs breaks Terraform 0.13 but should work in 0.14 or later + # In TF 0.14 and later (through 1.0.x) if the length of the cidr_blocks + # list is not available at plan time, the module breaks. cidr_blocks = random_integer.coin.result > 1 ? ["10.0.0.0/16"] : ["10.0.0.0/24"] ipv6_cidr_blocks = [module.vpc.ipv6_cidr_block] prefix_list_ids = [] @@ -60,7 +83,8 @@ module "new_security_group" { ] }] - rules = merge(var.rules, { new-cidr = [ + rules = var.rules + rules_map = merge({ new-cidr = [ { key = "https-cidr" type = "ingress" @@ -72,7 +96,7 @@ module "new_security_group" { source_security_group_id = null description = "Discrete HTTPS ingress by CIDR" self = null - }], + }] }, { new-sg = [{ # no key provided type = "ingress" diff --git a/exports/security_group_inputs.tf b/exports/security_group_inputs.tf index 3e75e7b..84e8c72 100644 --- a/exports/security_group_inputs.tf +++ b/exports/security_group_inputs.tf @@ -43,8 +43,8 @@ variable "allowed_security_group_ids" { } variable "security_group_name" { - type = string - default = "" + type = list(string) + default = [] description = <<-EOT The name to assign to the created security group. Must be unique within the VPC. If not provided, will be derived from the `null-label.context` passed in. @@ -84,7 +84,7 @@ variable "security_group_create_before_destroy" { # description = <<-EOT # Set `true` to enable Terraform `create_before_destroy` behavior on the created security group. # We recommend setting this `true` on new security groups, but default it to `false` because `true` - # will cause existing security groups to be replaced, requiring the resource to be deleted and recreated. + # will cause existing security groups to be replaced, possibly requiring the resource to be deleted and recreated. # Note that changing this value will always cause the security group to be replaced. # EOT @@ -159,10 +159,11 @@ variable "additional_security_group_rules" { description = <<-EOT A list of Security Group rule objects to add to the created security group, in addition to the ones this module normally creates. (To suppress the module's rules, set `create_security_group` to false - and supply your own security group via `associated_security_group_ids`.) + and supply your own security group(s) via `associated_security_group_ids`.) The keys and values of the objects are fully compatible with the `aws_security_group_rule` resource, except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique and known at "plan" time. - To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . + For more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule + and https://github.com/cloudposse/terraform-aws-security-group. EOT } diff --git a/main.tf b/main.tf index db4c365..292607c 100644 --- a/main.tf +++ b/main.tf @@ -25,7 +25,7 @@ resource "aws_security_group" "default" { # Because we have 2 almost identical alternatives, use x == false and x == true rather than x and !x count = local.create_security_group && var.create_before_destroy == false ? 1 : 0 - name = coalesce(var.security_group_name, module.this.id) + name = concat(var.security_group_name, [module.this.id])[0] ######################################################################## ## Everything from here to the end of this resource should be identical @@ -33,7 +33,7 @@ resource "aws_security_group" "default" { description = var.security_group_description vpc_id = var.vpc_id - tags = try(length(var.security_group_name), 0) > 0 ? merge(module.this.tags, { Name = var.security_group_name }) : module.this.tags + tags = merge(module.this.tags, try(length(var.security_group_name), 0) > 0 ? { Name = var.security_group_name } : {}) revoke_rules_on_delete = var.revoke_rules_on_delete @@ -82,7 +82,7 @@ resource "aws_security_group" "cbd" { # Because we have 2 almost identical alternatives, use x == false and x == true rather than x and !x count = local.create_security_group && var.create_before_destroy == true ? 1 : 0 - name_prefix = coalesce(var.security_group_name, "${module.this.id}${module.this.delimiter}") + name_prefix = concat(var.security_group_name, ["${module.this.id}${module.this.delimiter}"])[0] lifecycle { create_before_destroy = true } @@ -93,7 +93,7 @@ resource "aws_security_group" "cbd" { description = var.security_group_description vpc_id = var.vpc_id - tags = try(length(var.security_group_name), 0) > 0 ? merge(module.this.tags, { Name = var.security_group_name }) : module.this.tags + tags = merge(module.this.tags, try(length(var.security_group_name), 0) > 0 ? { Name = var.security_group_name } : {}) revoke_rules_on_delete = var.revoke_rules_on_delete diff --git a/normalize.tf b/normalize.tf index 3311313..01d6daa 100644 --- a/normalize.tf +++ b/normalize.tf @@ -3,10 +3,15 @@ locals { + # We have var.rules_map as a key-value object where the values are lists of different types. + # For convenience, the ordinary use cases, and ease of understanding, we also have var.rules, + # which is a single list of rules. First thing we do is to combine the 2 into one object. + rules = merge({ _list_ = var.rules }, var.rules_map) + # 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 - norm_rules = local.enabled && var.rules != null ? concat(concat([[]], [for k, rules in var.rules : [for i, rule in rules : { + norm_rules = local.enabled && local.rules != null ? concat(concat([[]], [for k, rules in local.rules : [for i, rule in rules : { key = coalesce(lookup(rule, "key", null), "${k}[${i}]") type = rule.type from_port = rule.from_port diff --git a/variables.tf b/variables.tf index bed4eb7..ccfaee2 100644 --- a/variables.tf +++ b/variables.tf @@ -14,15 +14,20 @@ variable "target_security_group_id" { } variable "security_group_name" { - type = string - default = "" + type = list(string) + default = [] description = <<-EOT The name to assign to the security group. Must be unique within the VPC. If not provided, will be derived from the `null-label.context` passed in. If `create_before_destroy` is true, will be used as a name prefix. EOT + validation { + condition = length(var.security_group_name) < 2 + error_message = "Only 1 security group name can be provided." + } } + variable "security_group_description" { type = string default = "Managed by Terraform" @@ -53,11 +58,24 @@ variable "allow_all_egress" { } variable "rules" { + type = list(any) + default = [] + description = <<-EOT + A list of Security Group rule objects. All elements of a list must be exactly the same type; + use `rules_map` if you want to supply multiple lists of different types. + The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource, + except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique + and known at "plan" time. + To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . + EOT +} + +variable "rules_map" { type = any default = {} description = <<-EOT - An object (like a map) of lists of Security Group rule objects. All elements of a list must be exactly the same - type, so this input accepts an object with keys (attributes) whose values are lists so you can separate different + A map-like object of lists of Security Group rule objects. All elements of a list must be exactly the same type, + so this input accepts an object with keys (attributes) whose values are lists so you can separate different types into different lists and still pass them into one input. Keys must be known at "plan" time. The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource, except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique @@ -69,6 +87,8 @@ variable "rules" { variable "rule_matrix" { # rule_matrix is independent of the `rules` input. # Only the rules specified in the `rule_matrix` object are applied to the subjects specified in `rule_matrix`. + # The `key` attributes are optional, but if supplied, must be known at plan time or else + # you will get an error from Terraform. If the value is triggering an error, just omit it. # Schema: # { # # these top level lists define all the subjects to which rule_matrix rules will be applied From d68775be0ceea47e0429eef8bde10ce030a4211c Mon Sep 17 00:00:00 2001 From: Nuru Date: Sun, 29 Aug 2021 14:59:19 -0700 Subject: [PATCH 19/19] Fix type Co-authored-by: Andriy Knysh --- examples/complete/fixtures.us-east-2.tfvars | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/complete/fixtures.us-east-2.tfvars b/examples/complete/fixtures.us-east-2.tfvars index 095567a..524fe04 100644 --- a/examples/complete/fixtures.us-east-2.tfvars +++ b/examples/complete/fixtures.us-east-2.tfvars @@ -25,6 +25,6 @@ rules = [ to_port = 23 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] - description = "Telenet wide open" + description = "Telnet wide open" } ]