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 958978b..78f55b5 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] @@ -93,24 +93,214 @@ 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. -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 -set the value to `[]` (an empty list). +##### `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. -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 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 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`) + +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 "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, +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" + 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 +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 +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 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_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_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 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 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 +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). + + + + +## 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" { @@ -142,11 +332,21 @@ 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 = [ { + key = "ssh" type = "ingress" from_port = 22 to_port = 22 @@ -156,6 +356,7 @@ module "sg" { description = "Allow SSH from anywhere" }, { + key = "HTTP" type = "ingress" from_port = 80 to_port = 80 @@ -163,30 +364,49 @@ module "sg" { cidr_blocks = [] 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" } ] + 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" + } + ] + } + ] -## Examples + vpc_id = module.vpc.vpc_id -Here is an example of using this module: -- [`examples/complete`](https://github.com/cloudposse/terraform-aws-security-group/examples/complete) - complete example of using this module + context = module.label.context +} + +``` @@ -207,14 +427,14 @@ Available targets: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 0.13.0 | -| [aws](#requirement\_aws) | >= 2.0 | +| [terraform](#requirement\_terraform) | >= 0.14.0 | +| [aws](#requirement\_aws) | >= 3.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 2.0 | +| [aws](#provider\_aws) | >= 3.0 | ## Modules @@ -226,24 +446,25 @@ 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.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.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 | -| [description](#input\_description) | The Security Group description. | `string` | `"Managed by Terraform"` | 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](#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 | | [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 | @@ -251,21 +472,28 @@ Available targets: | [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 | -| [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 | +| [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 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. | `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 | | [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 | -| [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 | +| [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 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 (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 0650841..c454734 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" @@ -60,24 +60,212 @@ 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. - 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 - 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). + ##### `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 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 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`) + + 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 "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, + 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" + 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 + 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 + 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 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_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_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 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 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 + 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 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" { @@ -109,11 +297,21 @@ 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 + # 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 = [ { + key = "ssh" type = "ingress" from_port = 22 to_port = 22 @@ -123,6 +321,7 @@ usage: |- description = "Allow SSH from anywhere" }, { + key = "HTTP" type = "ingress" from_port = 80 to_port = 80 @@ -130,27 +329,50 @@ usage: |- 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 { - type = "egress" - from_port = 0 - to_port = 65535 - protocol = "all" - cidr_blocks = ["0.0.0.0/0"] - self = null - description = "Allow egress to anywhere" + 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 } + ``` -# Example usage -examples: |- - Here is 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 d5a8db9..db124f5 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -3,14 +3,14 @@ | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 0.13.0 | -| [aws](#requirement\_aws) | >= 2.0 | +| [terraform](#requirement\_terraform) | >= 0.14.0 | +| [aws](#requirement\_aws) | >= 3.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 2.0 | +| [aws](#provider\_aws) | >= 3.0 | ## Modules @@ -22,24 +22,25 @@ | 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.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.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 | -| [description](#input\_description) | The Security Group description. | `string` | `"Managed by Terraform"` | 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](#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 | | [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 | @@ -47,19 +48,26 @@ | [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 | -| [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 | +| [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 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. | `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 | | [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 | -| [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 | +| [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 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 (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 bf42246..524fe04 100644 --- a/examples/complete/fixtures.us-east-2.tfvars +++ b/examples/complete/fixtures.us-east-2.tfvars @@ -10,17 +10,21 @@ name = "sg" rules = [ { + key = null # "ssh all" type = "ingress" from_port = 22 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" + key = "telnet all" + type = "ingress" + from_port = 23 + to_port = 23 + protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] + description = "Telnet wide open" } ] diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 90aa4b7..f4786b0 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -1,87 +1,141 @@ +# 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 } module "vpc" { source = "cloudposse/vpc/aws" - version = "v0.18.2" + version = "v0.25.0" + + cidr_block = "10.0.0.0/24" - cidr_block = "10.0.0.0/16" + assign_generated_ipv6_cidr_block = true context = module.this.context } -# Create new one security group +resource "random_integer" "coin" { + max = 2 + min = 1 +} -module "new_security_group" { +locals { + coin = [random_integer.coin.result] +} + +module "simple_security_group" { source = "../.." + attributes = ["simple"] + rules = var.rules + vpc_id = module.vpc.vpc_id - 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 - }, + + context = module.this.context +} + +# Create a new security group + +module "new_security_group" { + source = "../.." + + 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.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 = [] + + # 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 = [ + { + key = "ssh" + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + description = "Allow SSH access" + }, + { + # key = "http" + type = "ingress" + from_port = 80 + to_port = 80 + protocol = "tcp" + description = "Allow HTTP access" + }, + ] + }] + + rules = var.rules + rules_map = merge({ new-cidr = [ { + key = "https-cidr" type = "ingress" - from_port = 22 - to_port = 22 + from_port = 443 + to_port = 443 protocol = "tcp" - cidr_blocks = [] - ipv6_cidr_blocks = null + cidr_blocks = ["10.0.0.0/8"] + ipv6_cidr_blocks = [module.vpc.ipv6_cidr_block] # ["::/0"] # source_security_group_id = null - description = "Allow SSH access from our own SG" - self = true - }, - { + description = "Discrete HTTPS ingress by CIDR" + self = null + }] }, { + new-sg = [{ + # no key provided type = "ingress" from_port = 443 to_port = 443 - protocol = "all" - cidr_blocks = ["0.0.0.0/0"] - ipv6_cidr_blocks = null - source_security_group_id = null - 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" + protocol = "tcp" + source_security_group_id = aws_security_group.target.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" "external" { - name_prefix = format("%s-%s-", module.this.id, "external") +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 "external_security_group" { +module "target_security_group" { source = "../.." - vpc_id = module.vpc.vpc_id - id = aws_security_group.external.id - rules = var.rules - security_group_enabled = false + allow_all_egress = true + # create_security_group = false + target_security_group_id = [aws_security_group.target.id] + rules = var.rules + + vpc_id = module.vpc.vpc_id context = module.this.context } @@ -91,9 +145,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 + target_security_group_id = [aws_security_group.target.id] + rules = var.rules + context = module.this.context enabled = false } diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf index 1044ee0..cea6751 100644 --- a/examples/complete/outputs.tf +++ b/examples/complete/outputs.tf @@ -1,44 +1,49 @@ -output "new_sg_id" { - description = "The new one Security Group ID" +output "created_sg_id" { + description = "The ID of the created Security Group" value = module.new_security_group.id } -output "new_sg_arn" { - description = "The new one Security Group ARN" +output "created_sg_arn" { + description = "The ARN of the created Security Group" value = module.new_security_group.arn } -output "new_sg_name" { - description = "The new one Security Group Name" +output "created_sg_name" { + description = "The name of the created Security Group" value = module.new_security_group.name } -output "external_sg_id" { - description = "The external Security Group ID" - value = module.external_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 "external_sg_arn" { - description = "The external Security Group ARN" - value = module.external_security_group.arn +output "target_sg_id" { + description = "The target Security Group ID" + value = module.target_security_group.id } -output "external_sg_name" { - description = "The external Security Group Name" - value = module.external_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" { 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/variables.tf b/examples/complete/variables.tf index 014ce51..1bd257e 100644 --- a/examples/complete/variables.tf +++ b/examples/complete/variables.tf @@ -3,5 +3,18 @@ variable "region" { } variable "rules" { - type = list(any) + type = any + description = "List of security group rules to apply to the created security group" +} + +variable "rule_matrix_self" { + type = bool + description = "Value to set `self` in `rule_matrix` test rule" + default = null +} + +variable "inline_rules_enabled" { + type = bool + description = "Flag to enable/disable inline security group rules" + default = false } diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index 5b2c49b..28a2720 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -1,10 +1,14 @@ 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" + version = ">= 3.0" } } } diff --git a/exports/security_group_inputs.tf b/exports/security_group_inputs.tf new file mode 100644 index 0000000..84e8c72 --- /dev/null +++ b/exports/security_group_inputs.tf @@ -0,0 +1,195 @@ +# security_group_inputs Version: 1 +# +# 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. +# 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 and configure a new security group. If false, `associated_security_group_ids` must be provided." +} + +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 security group. + These security groups will not be modified and, if `create_security_group` is `false`, 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 +} + +variable "security_group_name" { + 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. + 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. + 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 + # + # 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, possibly 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" { + 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 +} + +############################################################################################# +## 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 +# +# +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 addresses. + If this is false and no egress rules are otherwise specified, then no egress will be allowed. + EOT +} + +variable "additional_security_group_rules" { + type = list(any) + default = [] + 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(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. + 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 +} + +# +# +#### 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 +# +# + + +## +## +################# Outputs +## +## +# +# output "security_group_id" { +# value = "" +# description = "The ID of the created security group" +# } +# +# output "security_group_name" { +# value = "" +# description = "The name of the created security group" +# } + diff --git a/main.tf b/main.tf index 30d9a02..292607c 100644 --- a/main.tf +++ b/main.tf @@ -1,57 +1,162 @@ 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 ? { - 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 - } : {} -} + enabled = module.this.enabled + inline = var.inline_rules_enabled + + allow_all_egress = local.enabled && var.allow_all_egress + + default_rule_description = "Managed by Terraform" + + 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] + ) : null -data "aws_security_group" "external" { - count = local.is_external ? 1 : 0 - id = var.id - vpc_id = var.vpc_id + security_group_id = local.enabled ? ( + # Use coalesce() here to hack an error message into the output + 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 } +# 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 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 = local.use_name - name_prefix = local.use_name_prefix - description = var.description + name = concat(var.security_group_name, [module.this.id])[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 + + description = var.security_group_description vpc_id = var.vpc_id - tags = 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 + + 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 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 = concat(var.security_group_name, ["${module.this.id}${module.this.delimiter}"])[0] lifecycle { create_before_destroy = true } + + ######################################################################## + ## 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, try(length(var.security_group_name), 0) > 0 ? { Name = var.security_group_name } : {}) + + 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_rule" "default" { - for_each = local.rules - - security_group_id = local.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") - # 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 - - source_security_group_id = lookup(each.value, "source_security_group_id", null) +resource "aws_security_group_rule" "keyed" { + for_each = local.keyed_resource_rules + + 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 = 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 new file mode 100644 index 0000000..01d6daa --- /dev/null +++ b/normalize.tf @@ -0,0 +1,161 @@ +# 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 { + + # 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 && 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 + 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 && 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 + 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 = { + 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 + 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 + } + + 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"] : [] + 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 : { + key = "${rule.key}#self" + 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 : { + key = "${rule.key}#cidr" + 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 : { + key = "${rule.key}#sg" + 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 i, sg in rule.security_groups : { + key = "${rule.key}#${i}" + 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 + }]]) + + 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 } +} + + diff --git a/outputs.tf b/outputs.tf index 8f9c7f2..f95474d 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,14 +1,20 @@ + output "id" { - description = "The Security Group ID" - value = try(local.id, null) + description = "The created or target Security Group ID" + value = local.security_group_id } output "arn" { - description = "The Security Group ARN" - value = try(local.arn, null) + description = "The created Security Group ARN (null if using existing security group)" + 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 (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/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/test/src/examples_complete_test.go b/test/src/examples_complete_test.go index b92023d..16302a6 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,28 +35,30 @@ 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) // 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") + // 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") 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'") 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") + // 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, 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.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/test/src/go.mod b/test/src/go.mod index af2dfb2..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.13 +go 1.16 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= diff --git a/variables.tf b/variables.tf index 3d5572c..ccfaee2 100644 --- a/variables.tf +++ b/variables.tf @@ -1,41 +1,157 @@ -variable "vpc_id" { +variable "target_security_group_id" { + 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" { + 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 - description = "The VPC ID where Security Group will be created." + 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. + EOT } -variable "security_group_enabled" { +variable "create_before_destroy" { type = bool - default = true - description = "Whether to create Security Group." + 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 always cause the security group to be replaced. + EOT } -variable "use_name_prefix" { +variable "allow_all_egress" { type = bool default = false - description = "Whether to create a unique name beginning with the normalized prefix." + 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 "description" { +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 + 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 . + 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 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 + # 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 + # 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 = [{ + # key = "unique key" + # type = "ingress" + # from_port = 433 + # to_port = 433 + # protocol = "tcp" + # description = "Allow HTTPS ingress" + # }] + + type = any + 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 = "Managed by Terraform" - description = "The Security Group description." + default = "10m" + description = "How long to wait for the security group to be created." } -variable "id" { +variable "security_group_delete_timeout" { type = string - default = "" + default = "15m" description = <<-EOT - The external Security Group ID to which Security Group rules will be assigned. - Required to set `security_group_enabled` to `false`. - 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 "rules" { - type = list(any) - default = null +variable "revoke_rules_on_delete" { + type = bool + default = false description = <<-EOT - 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 . - 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..fc6bdc5 100644 --- a/versions.tf +++ b/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" } } }