├── .editorconfig
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── bug_report.yml
│ ├── config.yml
│ ├── feature_request.md
│ ├── feature_request.yml
│ └── question.md
├── PULL_REQUEST_TEMPLATE.md
├── banner.png
├── mergify.yml
├── renovate.json
├── settings.yml
└── workflows
│ ├── branch.yml
│ ├── chatops.yml
│ ├── release.yml
│ └── scheduled.yml
├── .gitignore
├── LICENSE
├── README.md
├── README.yaml
├── atmos.yaml
├── context.tf
├── docs
└── migration-v1-v2.md
├── examples
└── complete
│ ├── context.tf
│ ├── fixtures.us-east-2.tfvars
│ ├── main.tf
│ ├── outputs.tf
│ ├── variables.tf
│ └── versions.tf
├── exports
└── security-group-variables.tf
├── main.tf
├── normalize.tf
├── outputs.tf
├── test
├── .gitignore
├── Makefile
├── Makefile.alpine
└── src
│ ├── .gitignore
│ ├── Makefile
│ ├── examples_complete_test.go
│ ├── go.mod
│ └── go.sum
├── variables.tf
└── versions.tf
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | # Unix-style newlines with a newline ending every file
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | indent_size = 2
9 | indent_style = space
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.{tf,tfvars}]
14 | indent_size = 2
15 | indent_style = space
16 |
17 | [*.md]
18 | max_line_length = 0
19 | trim_trailing_whitespace = true
20 | indent_style = space
21 | indent_size = 2
22 |
23 | # Override for Makefile
24 | [{Makefile, makefile, GNUmakefile, Makefile.*}]
25 | tab_width = 2
26 | indent_style = tab
27 | indent_size = 4
28 |
29 | [COMMIT_EDITMSG]
30 | max_line_length = 0
31 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Use this file to define individuals or teams that are responsible for code in a repository.
2 | # Read more:
3 | #
4 | # Order is important: the last matching pattern has the highest precedence
5 |
6 | # These owners will be the default owners for everything
7 | * @cloudposse/engineering @cloudposse/contributors
8 |
9 | # Cloud Posse must review any changes to Makefiles
10 | **/Makefile @cloudposse/engineering
11 | **/Makefile.* @cloudposse/engineering
12 |
13 | # Cloud Posse must review any changes to GitHub actions
14 | .github/* @cloudposse/engineering
15 |
16 | # Cloud Posse must review any changes to standard context definition,
17 | # but some changes can be rubber-stamped.
18 | **/*.tf @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers
19 | README.yaml @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers
20 | README.md @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers
21 | docs/*.md @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers
22 |
23 | # Cloud Posse Admins must review all changes to CODEOWNERS or the mergify configuration
24 | .github/mergify.yml @cloudposse/admins
25 | .github/CODEOWNERS @cloudposse/admins
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: 'bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 | Found a bug? Maybe our [Slack Community](https://slack.cloudposse.com) can help.
11 |
12 | [](https://slack.cloudposse.com)
13 |
14 | ## Describe the Bug
15 | A clear and concise description of what the bug is.
16 |
17 | ## Expected Behavior
18 | A clear and concise description of what you expected to happen.
19 |
20 | ## Steps to Reproduce
21 | Steps to reproduce the behavior:
22 | 1. Go to '...'
23 | 2. Run '....'
24 | 3. Enter '....'
25 | 4. See error
26 |
27 | ## Screenshots
28 | If applicable, add screenshots or logs to help explain your problem.
29 |
30 | ## Environment (please complete the following information):
31 |
32 | Anything that will help us triage the bug will help. Here are some ideas:
33 | - OS: [e.g. Linux, OSX, WSL, etc]
34 | - Version [e.g. 10.15]
35 |
36 | ## Additional Context
37 | Add any other context about the problem here.
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | description: Create a report to help us improve
4 | labels: ["bug"]
5 | assignees: [""]
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: |
10 | Found a bug?
11 |
12 | Please checkout our [Slack Community](https://slack.cloudposse.com)
13 | or visit our [Slack Archive](https://archive.sweetops.com/).
14 |
15 | [](https://slack.cloudposse.com)
16 |
17 | - type: textarea
18 | id: concise-description
19 | attributes:
20 | label: Describe the Bug
21 | description: A clear and concise description of what the bug is.
22 | placeholder: What is the bug about?
23 | validations:
24 | required: true
25 |
26 | - type: textarea
27 | id: expected
28 | attributes:
29 | label: Expected Behavior
30 | description: A clear and concise description of what you expected.
31 | placeholder: What happened?
32 | validations:
33 | required: true
34 |
35 | - type: textarea
36 | id: reproduction-steps
37 | attributes:
38 | label: Steps to Reproduce
39 | description: Steps to reproduce the behavior.
40 | placeholder: How do we reproduce it?
41 | validations:
42 | required: true
43 |
44 | - type: textarea
45 | id: screenshots
46 | attributes:
47 | label: Screenshots
48 | description: If applicable, add screenshots or logs to help explain.
49 | validations:
50 | required: false
51 |
52 | - type: textarea
53 | id: environment
54 | attributes:
55 | label: Environment
56 | description: Anything that will help us triage the bug.
57 | placeholder: |
58 | - OS: [e.g. Linux, OSX, WSL, etc]
59 | - Version [e.g. 10.15]
60 | - Module version
61 | - Terraform version
62 | validations:
63 | required: false
64 |
65 | - type: textarea
66 | id: additional
67 | attributes:
68 | label: Additional Context
69 | description: |
70 | Add any other context about the problem here.
71 | validations:
72 | required: false
73 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
3 | contact_links:
4 |
5 | - name: Community Slack Team
6 | url: https://cloudposse.com/slack/
7 | about: |-
8 | Please ask and answer questions here.
9 |
10 | - name: Office Hours
11 | url: https://cloudposse.com/office-hours/
12 | about: |-
13 | Join us every Wednesday for FREE Office Hours (lunch & learn).
14 |
15 | - name: DevOps Accelerator Program
16 | url: https://cloudposse.com/accelerate/
17 | about: |-
18 | Own your infrastructure in record time. We build it. You drive it.
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: 'feature request'
6 | assignees: ''
7 |
8 | ---
9 |
10 | Have a question? Please checkout our [Slack Community](https://slack.cloudposse.com) or visit our [Slack Archive](https://archive.sweetops.com/).
11 |
12 | [](https://slack.cloudposse.com)
13 |
14 | ## Describe the Feature
15 |
16 | A clear and concise description of what the bug is.
17 |
18 | ## Expected Behavior
19 |
20 | A clear and concise description of what you expected to happen.
21 |
22 | ## Use Case
23 |
24 | Is your feature request related to a problem/challenge you are trying to solve? Please provide some additional context of why this feature or capability will be valuable.
25 |
26 | ## Describe Ideal Solution
27 |
28 | A clear and concise description of what you want to happen. If you don't know, that's okay.
29 |
30 | ## Alternatives Considered
31 |
32 | Explain what alternative solutions or features you've considered.
33 |
34 | ## Additional Context
35 |
36 | Add any other context or screenshots about the feature request here.
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | description: Suggest an idea for this project
4 | labels: ["feature request"]
5 | assignees: [""]
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: |
10 | Have a question?
11 |
12 | Please checkout our [Slack Community](https://slack.cloudposse.com)
13 | or visit our [Slack Archive](https://archive.sweetops.com/).
14 |
15 | [](https://slack.cloudposse.com)
16 |
17 | - type: textarea
18 | id: concise-description
19 | attributes:
20 | label: Describe the Feature
21 | description: A clear and concise description of what the feature is.
22 | placeholder: What is the feature about?
23 | validations:
24 | required: true
25 |
26 | - type: textarea
27 | id: expected
28 | attributes:
29 | label: Expected Behavior
30 | description: A clear and concise description of what you expected.
31 | placeholder: What happened?
32 | validations:
33 | required: true
34 |
35 | - type: textarea
36 | id: use-case
37 | attributes:
38 | label: Use Case
39 | description: |
40 | Is your feature request related to a problem/challenge you are trying
41 | to solve?
42 |
43 | Please provide some additional context of why this feature or
44 | capability will be valuable.
45 | validations:
46 | required: true
47 |
48 | - type: textarea
49 | id: ideal-solution
50 | attributes:
51 | label: Describe Ideal Solution
52 | description: A clear and concise description of what you want to happen.
53 | validations:
54 | required: true
55 |
56 | - type: textarea
57 | id: alternatives-considered
58 | attributes:
59 | label: Alternatives Considered
60 | description: Explain alternative solutions or features considered.
61 | validations:
62 | required: false
63 |
64 | - type: textarea
65 | id: additional
66 | attributes:
67 | label: Additional Context
68 | description: |
69 | Add any other context about the problem here.
70 | validations:
71 | required: false
72 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudposse/terraform-aws-security-group/094a2ad3108277a5276dbbb920d9af3322c409c1/.github/ISSUE_TEMPLATE/question.md
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## what
2 |
3 |
7 |
8 | ## why
9 |
10 |
15 |
16 | ## references
17 |
18 |
22 |
--------------------------------------------------------------------------------
/.github/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudposse/terraform-aws-security-group/094a2ad3108277a5276dbbb920d9af3322c409c1/.github/banner.png
--------------------------------------------------------------------------------
/.github/mergify.yml:
--------------------------------------------------------------------------------
1 | extends: .github
2 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base",
4 | ":preserveSemverRanges"
5 | ],
6 | "baseBranches": ["main", "master", "/^release\\/v\\d{1,2}$/"],
7 | "labels": ["auto-update"],
8 | "dependencyDashboardAutoclose": true,
9 | "enabledManagers": ["terraform"],
10 | "terraform": {
11 | "ignorePaths": ["**/context.tf", "examples/**"]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.github/settings.yml:
--------------------------------------------------------------------------------
1 | # Upstream changes from _extends are only recognized when modifications are made to this file in the default branch.
2 | _extends: .github
3 | repository:
4 | name: terraform-aws-security-group
5 | description: Terraform module to provision an AWS Security Group
6 | homepage: https://cloudposse.com/accelerate
7 | topics: aws, terraform-module, terraform, terraform-modules
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/workflows/branch.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Branch
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | - release/**
8 | types: [opened, synchronize, reopened, labeled, unlabeled]
9 | push:
10 | branches:
11 | - main
12 | - release/v*
13 | paths-ignore:
14 | - '.github/**'
15 | - 'docs/**'
16 | - 'examples/**'
17 | - 'test/**'
18 | - 'README.md'
19 |
20 | permissions: {}
21 |
22 | jobs:
23 | terraform-module:
24 | uses: cloudposse/.github/.github/workflows/shared-terraform-module.yml@main
25 | secrets: inherit
26 |
--------------------------------------------------------------------------------
/.github/workflows/chatops.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: chatops
3 | on:
4 | issue_comment:
5 | types: [created]
6 |
7 | permissions:
8 | pull-requests: write
9 | id-token: write
10 | contents: write
11 | statuses: write
12 |
13 | jobs:
14 | test:
15 | uses: cloudposse/.github/.github/workflows/shared-terraform-chatops.yml@main
16 | if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '/terratest') }}
17 | secrets: inherit
18 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: release
3 | on:
4 | release:
5 | types:
6 | - published
7 |
8 | permissions:
9 | id-token: write
10 | contents: write
11 | pull-requests: write
12 |
13 | jobs:
14 | terraform-module:
15 | uses: cloudposse/.github/.github/workflows/shared-release-branches.yml@main
16 | secrets: inherit
17 |
--------------------------------------------------------------------------------
/.github/workflows/scheduled.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: scheduled
3 | on:
4 | workflow_dispatch: { } # Allows manually trigger this workflow
5 | schedule:
6 | - cron: "0 3 * * *"
7 |
8 | permissions:
9 | pull-requests: write
10 | id-token: write
11 | contents: write
12 |
13 | jobs:
14 | scheduled:
15 | uses: cloudposse/.github/.github/workflows/shared-terraform-scheduled.yml@main
16 | secrets: inherit
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled files
2 | *.tfstate
3 | *.tfstate.backup
4 |
5 | # Module directory
6 | .terraform
7 | .idea
8 | *.iml
9 | **/.terraform.lock.hcl
10 |
11 | .build-harness
12 | build-harness
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2021-2022 Cloud Posse, LLC
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 
5 |
6 | 


7 |
8 |
9 |
29 |
30 | Terraform module to create AWS Security Group and rules.
31 |
32 |
33 | > [!TIP]
34 | > #### 👽 Use Atmos with Terraform
35 | > Cloud Posse uses [`atmos`](https://atmos.tools) to easily orchestrate multiple environments using Terraform.
36 | > Works with [Github Actions](https://atmos.tools/integrations/github-actions/), [Atlantis](https://atmos.tools/integrations/atlantis), or [Spacelift](https://atmos.tools/integrations/spacelift).
37 | >
38 | >
39 | > Watch demo of using Atmos with Terraform
40 | > 
41 | > Example of running atmos
to manage infrastructure from our Quick Start tutorial.
42 | >
43 |
44 |
45 |
46 |
47 |
48 | ## Usage
49 |
50 | This module is primarily for setting security group rules on a security group. You can provide the
51 | ID of an existing security group to modify, or, by default, this module will create a new security
52 | group and apply the given rules to it.
53 |
54 | This module can be used very simply, but it is actually quite complex because it is attempting to handle
55 | numerous interrelationships, restrictions, and a few bugs in ways that offer a choice between zero
56 | service interruption for updates to a security group not referenced by other security groups
57 | (by replacing the security group with a new one) versus brief service interruptions for security groups that must be preserved.
58 |
59 | ### Avoiding Service Interruptions
60 |
61 | It is desirable to avoid having service interruptions when updating a security group. This is not always
62 | possible due to the way Terraform organizes its activities and the fact that AWS will reject an attempt
63 | to create a duplicate of an existing security group rule. There is also the issue that while most AWS
64 | resources can be associated with and disassociated from security groups at any time, there remain some
65 | that may not have their security group association changed, and an attempt to change their security group
66 | will cause Terraform to delete and recreate the resource.
67 |
68 | #### The 2 Ways Security Group Changes Cause Service Interruptions
69 |
70 | Changes to a security group can cause service interruptions in 2 ways:
71 |
72 | 1. Changing rules may be implemented as deleting existing rules and creating new ones. During the
73 | period between deleting the old rules and creating the new rules, the security group will block
74 | traffic intended to be allowed by the new rules.
75 | 2. Changing rules may alternately be implemented as creating a new security group with the new rules
76 | and replacing the existing security group with the new one (then deleting the old one).
77 | This usually works with no service interruption in the case where all resources that reference the
78 | security group are part of the same Terraform plan.
79 | However, if, for example, the security group ID is referenced in a security group
80 | rule in a security group that is not part of the same Terraform plan, then AWS will not allow the
81 | existing (referenced) security group to be deleted, and even if it did, Terraform would not know
82 | to update the rule to reference the new security group.
83 |
84 | The key question you need to answer to decide which configuration to use is "will anything break
85 | if the security group ID changes". If not, then use the defaults `create_before_destroy = true` and
86 | `preserve_security_group_id = false` and do not worry about providing "keys" for
87 | security group rules. This is the default because it is the easiest and safest solution when
88 | the way the security group is being used allows it.
89 |
90 | If things will break when the security group ID changes, then set `preserve_security_group_id`
91 | to `true`. Also read and follow the guidance below about [keys](#the-importance-of-keys) and
92 | [limiting Terraform security group rules to a single AWS security group rule](#terraform-rules-vs-aws-rules)
93 | if you want to mitigate against service interruptions caused by rule changes.
94 | Note that even in this case, you probably want to keep `create_before_destroy = true` because otherwise,
95 | if some change requires the security group to be replaced, Terraform will likely succeed
96 | in deleting all the security group rules but fail to delete the security group itself,
97 | leaving the associated resources completely inaccessible. At least with `create_before_destroy = true`,
98 | the new security group will be created and used where Terraform can make the changes,
99 | even though the old security group will still fail to be deleted.
100 |
101 | #### The 3 Ways to Mitigate Against Service Interruptions
102 |
103 | ##### Security Group `create_before_destroy = true`
104 |
105 | The most important option is `create_before_destroy` which, when set to `true` (the default),
106 | ensures that a new replacement security group is created before an existing one is destroyed.
107 | This is particularly important because a security group cannot be destroyed while it is associated with
108 | a resource (e.g. a load balancer), but "destroy before create" behavior causes Terraform
109 | to try to destroy the security group before disassociating it from associated resources,
110 | so plans fail to apply with the error
111 |
112 | ```
113 | Error deleting security group: DependencyViolation: resource sg-XXX has a dependent object
114 | ```
115 |
116 | With "create before destroy" and any resources dependent on the security group as part of the
117 | same Terraform plan, replacement happens successfully:
118 |
119 | 1. New security group is created
120 | 2. Resource is associated with the new security group and disassociated from the old one
121 | 3. Old security group is deleted successfully because there is no longer anything associated with it
122 |
123 | (If there is a resource dependent on the security group that is also outside the scope of
124 | the Terraform plan, the old security group will fail to be deleted and you will have to
125 | address the dependency manually.)
126 |
127 | Note that the module's default configuration of `create_before_destroy = true` and
128 | `preserve_security_group_id = false` will force "create before destroy" behavior on the target security
129 | group, even if the module did not create it and instead you provided a `target_security_group_id`.
130 |
131 | Unfortunately, just creating the new security group first is not enough to prevent a service interruption. Keep reading.
132 |
133 | ##### Setting Rule Changes to Force Replacement of the Security Group
134 |
135 | A security group by itself is just a container for rules. It only functions as desired when all the rules are in place.
136 | If using the Terraform default "destroy before create" behavior for rules, even when using `create_before_destroy` for the
137 | security group itself, an outage occurs when updating the rules or security group, because the order of operations is:
138 |
139 | 1. Delete existing security group rules (triggering a service interruption)
140 | 2. Create the new security group
141 | 3. Associate the new security group with resources and disassociate the old one (which can take a substantial
142 | amount of time for a resource like a NAT Gateway)
143 | 4. Create the new security group rules (restoring service)
144 | 5. Delete the old security group
145 |
146 | To resolve this issue, the module's default configuration of `create_before_destroy = true` and
147 | `preserve_security_group_id = false` causes any change in the security group rules
148 | to trigger the creation of a new security group. With that, a rule change causes operations to occur in this order:
149 |
150 | 1. Create the new security group
151 | 2. Create the new security group rules
152 | 3. Associate the new security group with resources and disassociate the old one
153 | 4. Delete the old security group rules
154 | 5. Delete the old security group
155 |
156 | ##### Preserving the Security Group
157 |
158 | There can be a downside to creating a new security group with every rule change.
159 | If you want to prevent the security group ID from changing unless absolutely necessary, perhaps because the associated
160 | resource does not allow the security group to be changed or because the ID is referenced somewhere (like in
161 | another security group's rules) outside of this Terraform plan, then you need to set `preserve_security_group_id` to `true`.
162 |
163 | The main drawback of this configuration is that there will normally be
164 | a service outage during an update, because existing rules will be deleted before replacement
165 | rules are created. Using keys to identify rules can help limit the impact, but even with keys, simply adding a
166 | CIDR to the list of allowed CIDRs will cause that entire rule to be deleted and recreated, causing a temporary
167 | access denial for all of the CIDRs in the rule. (For more on this and how to mitigate against it, see [The Importance
168 | of Keys](#the-importance-of-keys) below.)
169 |
170 | Also note that setting `preserve_security_group_id` to `true` does not prevent Terraform from replacing the
171 | security group when modifying it is not an option, such as when its name or description changes.
172 | However, if you can control the configuration adequately, you can maintain the security group ID and eliminate
173 | impact on other security groups by setting `preserve_security_group_id` to `true`. We still recommend
174 | leaving `create_before_destroy` set to `true` for the times when the security group must be replaced,
175 | to avoid the `DependencyViolation` described above.
176 |
177 | ### Defining Security Group Rules
178 |
179 | We provide a number of different ways to define rules for the security group for a few reasons:
180 | - Terraform type constraints make it difficult to create collections of objects with optional members
181 | - Terraform resource addressing can cause resources that did not actually change to nevertheless be replaced
182 | (deleted and recreated), which, in the case of security group rules, then causes a brief service interruption
183 | - Terraform resource addresses must be known at `plan` time, making it challenging to create rules that
184 | depend on resources being created during `apply` and at the same time are not replaced needlessly when something else changes
185 | - When Terraform rules can be successfully created before being destroyed, there is no service interruption for the resources
186 | associated with that security group (unless the security group ID is used in other security group rules outside
187 | of the scope of the Terraform plan)
188 |
189 | #### The Importance of Keys
190 |
191 | If you are using "create before destroy" behavior for the security group and security group rules, then
192 | you can skip this section and much of the discussion about keys in the later sections, because keys do not matter
193 | in this configuration. However, if you are using "destroy before create" behavior, then a full understanding of keys
194 | as applied to security group rules will help you minimize service interruptions due to changing rules.
195 |
196 | When creating a collection of resources, Terraform requires each resource to be identified by a key,
197 | so that each resource has a unique "address", and changes to resources are tracked by that key.
198 | Every security group rule input to this module accepts optional identifying keys (arbitrary strings) for each rule.
199 | If you do not supply keys, then the rules are treated as a list,
200 | and the index of the rule in the list will be used as its key. This has the unwelcome behavior that removing a rule
201 | from the list will cause all the rules later in the list to be destroyed and recreated. For example, changing
202 | `[A, B, C, D]` to `[A, C, D]` causes rules 1(`B`), 2(`C`), and 3(`D`) to be deleted and new rules 1(`C`) and
203 | 2(`D`) to be created.
204 |
205 | To mitigate against this problem, we allow you to specify keys (arbitrary strings) for each rule. (Exactly how you specify
206 | the key is explained in the next sections.) Going back to our example, if the
207 | initial set of rules were specified with keys, e.g. `[{A: A}, {B: B}, {C: C}, {D: D}]`, then removing `B` from the list
208 | would only cause `B` to be deleted, leaving `C` and `D` intact.
209 |
210 | Note, however, two cautions. First, the keys must be known at `terraform plan` time and therefore cannot depend
211 | on resources that will be created during `apply`. Second, in order to be helpful, the keys must remain consistently
212 | attached to the same rules. For example, if you did
213 |
214 | ```hcl
215 | rule_map = { for i, v in rule_list : i => v }
216 | ```
217 |
218 | then you will have merely recreated the initial problem with using a plain list. If you cannot attach
219 | meaningful keys to the rules, there is no advantage to specifying keys at all.
220 |
221 | #### Terraform Rules vs AWS Rules
222 |
223 | A single security group rule input can actually specify multiple AWS security group rules. For example,
224 | `ipv6_cidr_blocks` takes a list of CIDRs. However, AWS security group rules do not allow for a list
225 | of CIDRs, so the AWS Terraform provider converts that list of CIDRs into a list of AWS security group rules,
226 | one for each CIDR. (This is the underlying cause of several AWS Terraform provider bugs,
227 | such as [#25173](https://github.com/hashicorp/terraform-provider-aws/issues/25173).)
228 | As of this writing, any change to any element of such a rule will cause
229 | all the AWS rules specified by the Terraform rule to be deleted and recreated, causing the same kind of
230 | service interruption we sought to avoid by providing keys for the rules, or, when create_before_destroy = true,
231 | causing a complete failure as Terraform tries to create duplicate rules which AWS rejects. To guard against this issue,
232 | when not using the default behavior, you should avoid the convenience of specifying multiple AWS rules
233 | in a single Terraform rule and instead create a separate Terraform rule for each source or destination specification.
234 |
235 | ##### `rules` and `rules_map` inputs
236 | This module provides 3 ways to set security group rules. You can use any or all of them at the same time.
237 |
238 | The easy way to specify rules is via the `rules` input. It takes a list of rules. (We will define
239 | a rule [a bit later](#definition-of-a-rule).) The problem is that a Terraform list must be composed
240 | of elements that are all the exact same type, and rules can be any of several
241 | different Terraform types. So to get around this restriction, the second
242 | way to specify rules is via the `rules_map` input, which is more complex.
243 |
244 | Why the input is so complex (click to reveal)
245 |
246 | - Terraform has 3 basic simple types: bool, number, string
247 | - Terraform then has 3 collections of simple types: list, map, and set
248 | - Terraform then has 2 structural types: object and tuple. However, these are not really single
249 | types. They are catch-all labels for values that are themselves combination of other values.
250 | (This will become a bit clearer after we define `maps` and contrast them with `objects`)
251 |
252 | One [rule of the collection types](https://www.terraform.io/docs/language/expressions/type-constraints.html#collection-types)
253 | is that the values in the collections must all be the exact same type.
254 | For example, you cannot have a list where some values are boolean and some are string. Maps require
255 | that all keys be strings, but the map values can be any type, except again all the values in a map
256 | must be the same type. In other words, the values of a map must form a valid list.
257 |
258 | Objects look just like maps. The difference between an object and a map is that the values in an
259 | object do not all have to be the same type.
260 |
261 | 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.
262 |
263 | So although `{ foo = "bar", baz = {} }` and `{ foo = "bar", baz = [] }` are both objects,
264 | they are not of the same type, and you can get error messages like
265 |
266 | ```
267 | Error: Inconsistent conditional result types
268 | The true and false result expressions must have consistent types. The given
269 | expressions are object and object, respectively.
270 | ```
271 |
272 | This means you cannot put them both in the same list or the same map,
273 | even though you can put them in a single tuple or object.
274 | Similarly, and closer to the problem at hand,
275 |
276 | ```hcl
277 | cidr_rule = {
278 | type = "ingress"
279 | cidr_blocks = ["0.0.0.0/0"]
280 | }
281 | ```
282 | is not the same type as
283 |
284 | ```hcl
285 | self_rule = {
286 | type = "ingress"
287 | self = true
288 | }
289 | ```
290 |
291 | This means you cannot put both of those in the same list.
292 |
293 | ```hcl
294 | rules = tolist([local.cidr_rule, local.self_rule])
295 | ```
296 |
297 | Generates the error
298 |
299 | ```text
300 | Invalid value for "v" parameter: cannot convert tuple to list of any single type.
301 | ```
302 |
303 | You could make them the same type and put them in a list,
304 | like this:
305 |
306 | ```hcl
307 | rules = tolist([{
308 | type = "ingress"
309 | cidr_blocks = ["0.0.0.0/0"]
310 | self = null
311 | },
312 | {
313 | type = "ingress"
314 | cidr_blocks = []
315 | self = true
316 | }])
317 | ```
318 |
319 | That remains an option for you when generating the rules, and is probably better when you have full control over all the rules.
320 | However, what if some of the rules are coming from a source outside of your control? You cannot simply add those rules
321 | to your list. So, what to do? Create an object whose attributes' values can be of different types.
322 |
323 | ```hcl
324 | { mine = local.my_rules, theirs = var.their_rules }
325 | ```
326 |
327 | That is why the `rules_map` input is available. It will accept a structure like that, an object whose
328 | attribute values are lists of rules, where the lists themselves can be different types.
329 |
330 |
331 |
332 | The `rules_map` input takes an object.
333 | - The attribute names (keys) of the object can be anything you want, but need to be known during `terraform plan`,
334 | which means they cannot depend on any resources created or changed by Terraform.
335 | - The values of the attributes are lists of rule objects, each object representing one Security Group Rule. As explained
336 | above in "Why the input is so complex", each object in the list must be exactly the same type. To use multiple types,
337 | you must put them in separate lists and put the lists in a map with distinct keys.
338 |
339 | Example:
340 |
341 | ```hcl
342 | rules_map = {
343 | ingress = [{
344 | key = "ingress"
345 | type = "ingress"
346 | from_port = 0
347 | to_port = 2222
348 | protocol = "tcp"
349 | cidr_blocks = module.subnets.nat_gateway_public_ips
350 | self = null
351 | description = "2222"
352 | }],
353 | egress = [{
354 | key = "egress"
355 | type = "egress"
356 | from_port = 0
357 | to_port = 0
358 | protocol = "-1"
359 | cidr_blocks = ["0.0.0.0/0"]
360 | self = null
361 | description = "All output traffic"
362 | }]
363 | }
364 | ```
365 |
366 | ###### Definition of a Rule
367 |
368 | For this module, a rule is defined as an object.
369 | - The attributes and values of the rule objects are fully compatible (have the same keys and accept the same values) as the
370 | Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule),
371 | except:
372 | - The `security_group_id` will be ignored, if present
373 | - You can include an optional `key` attribute. If present, its value must be unique among all security group rules in the
374 | security group, and it must be known in the Terraform "plan" phase, meaning it cannot depend on anything being
375 | generated or created by Terraform.
376 |
377 | The `key` attribute value, if provided, will be used to identify the Security Group Rule to Terraform in order to
378 | prevent Terraform from modifying it unnecessarily. If the `key` is not provided, Terraform will assign an identifier
379 | based on the rule's position in its list, which can cause a ripple effect of rules being deleted and recreated if
380 | a rule gets deleted from start of a list, causing all the other rules to shift position.
381 | See ["Unexpected changes..."](#unexpected-changes-during-plan-and-apply) below for more details.
382 |
383 |
384 | ##### `rule_matrix` Input
385 |
386 | The other way to set rules is via the `rule_matrix` input. This splits the attributes of the `aws_security_group_rule`
387 | resource into two sets: one set defines the rule and description, the other set defines the subjects of the rule.
388 | Again, optional "key" values can provide stability, but cannot contain derived values. This input is an attempt
389 | at convenience, and should not be used unless you are using the default settings of `create_before_destroy = true` and
390 | `preserve_security_group_id = false`, or else a number of failure modes or service interruptions are possible: use
391 | `rules_map` instead.
392 |
393 | As with `rules` and explained above in "Why the input is so complex", all elements of the list must be the exact same type.
394 | This also holds for all the elements of the `rules_matrix.rules` list. Because `rule_matrix` is already
395 | so complex, we do not provide the ability to mix types by packing object within more objects.
396 | All of the elements of the `rule_matrix` list must be exactly the same type. You can make them all the same
397 | type by following a few rules:
398 |
399 | - Every object in a list must have the exact same set of attributes. Most attributes are optional and can be omitted,
400 | but any attribute appearing in one object must appear in all the objects.
401 | - Any attribute that takes a list value in any object must contain a list in all objects.
402 | Use an empty list rather than `null` to indicate "no value". Passing in `null` instead of a list
403 | may cause Terraform to crash or emit confusing error messages (e.g. "number is required").
404 | - Any attribute that takes a value of type other than list can be set to `null` in objects where no value is needed.
405 |
406 | The schema for `rule_matrix` is:
407 |
408 | ```hcl
409 | {
410 | # these top level lists define all the subjects to which rule_matrix rules will be applied
411 | key = an optional unique key to keep these rules from being affected when other rules change
412 | source_security_group_ids = list of source security group IDs to apply all rules to
413 | cidr_blocks = list of ipv4 CIDR blocks to apply all rules to
414 | ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to
415 | prefix_list_ids = list of prefix list IDs to apply all rules to
416 |
417 | self = boolean value; set it to "true" to apply the rules to the created or existing security group, null otherwise
418 |
419 | # each rule in the rules list will be applied to every subject defined above
420 | rules = [{
421 | key = an optional unique key to keep this rule from being affected when other rules change
422 | type = type of rule, either "ingress" or "egress"
423 | from_port = start range of protocol port
424 | to_port = end range of protocol port, max is 65535
425 | protocol = IP protocol name or number, or "-1" for all protocols and ports
426 |
427 | description = free form text description of the rule
428 | }]
429 | }
430 | ```
431 |
432 | ### Important Notes
433 |
434 | ##### Unexpected changes during plan and apply
435 |
436 | When configuring this module for "create before destroy" behavior, any change to
437 | a security group rule will cause an entire new security group to be created with
438 | all new rules. This can make a small change look like a big one, but is intentional
439 | and should not cause concern.
440 |
441 | As explained above under [The Importance of Keys](#the-importance-of-keys),
442 | when using "destroy before create" behavior, security group rules without keys
443 | are identified by their indices in the input lists. If a rule is deleted and the other rules therefore move
444 | closer to the start of the list, those rules will be deleted and recreated. This
445 | can make a small change look like a big one when viewing the output of Terraform plan,
446 | and will likely cause a brief (seconds) service interruption.
447 |
448 | You can avoid this for the most part by providing the optional keys, and [limiting each rule
449 | to a single source or destination](#terraform-rules-vs-aws-rules). Rules with keys will not be
450 | changed if their keys do not change and the rules themselves do not change, except in the case of
451 | `rule_matrix`, where the rules are still dependent on the order of the security groups in
452 | `source_security_group_ids`. You can avoid this by using `rules` or `rules_map` instead of `rule_matrix` when you have
453 | more than one security group in the list. You cannot avoid this by sorting the
454 | `source_security_group_ids`, because that leads to the "Invalid `for_each` argument" error
455 | because of [terraform#31035](https://github.com/hashicorp/terraform/issues/31035).
456 |
457 | ##### Invalid for_each argument
458 |
459 | You can supply a number of rules as inputs to this module, and they (usually) get transformed into
460 | `aws_security_group_rule` resources. However, Terraform works in 2 steps: a `plan` step where it
461 | calculates the changes to be made, and an `apply` step where it makes the changes. This is so you
462 | can review and approve the plan before changing anything. One big limitation of this approach is
463 | that it requires that Terraform be able to count the number of resources to create without the
464 | benefit of any data generated during the `apply` phase. So if you try to generate a rule based
465 | on something you are creating at the same time, you can get an error like
466 |
467 | ```
468 | Error: Invalid for_each argument
469 | The "for_each" value depends on resource attributes that cannot be determined until apply,
470 | so Terraform cannot predict how many instances will be created.
471 | ```
472 |
473 | This module uses lists to minimize the chance of that happening, as all it needs to know
474 | is the length of the list, not the values in it, but this error still can
475 | happen for subtle reasons. Most commonly, using a function like `compact` on a list
476 | will cause the length to become unknown (since the values have to be checked and `null`s removed).
477 | In the case of `source_security_group_ids`, just sorting the list using `sort`
478 | will cause this error. (See [terraform#31035](https://github.com/hashicorp/terraform/issues/31035).)
479 | If you run into this error, check for functions like `compact` somewhere
480 | in the chain that produces the list and remove them if you find them.
481 |
482 |
483 | ##### WARNINGS and Caveats
484 |
485 | **_Setting `inline_rules_enabled` is not recommended and NOT SUPPORTED_**: Any issues arising from setting
486 | `inlne_rules_enabled = true` (including issues about setting it to `false` after setting it to `true`) will
487 | not be addressed, because they flow from [fundamental problems](https://github.com/hashicorp/terraform-provider-aws/issues/20046)
488 | with the underlying `aws_security_group` resource. The setting is provided for people who know and accept the
489 | limitations and trade-offs and want to use it anyway. The main advantage is that when using inline rules,
490 | Terraform will perform "drift detection" and attempt to remove any rules it finds in place but not
491 | specified inline. See [this post](https://github.com/hashicorp/terraform-provider-aws/pull/9032#issuecomment-639545250)
492 | for a discussion of the difference between inline and resource rules,
493 | and some of the reasons inline rules are not satisfactory.
494 |
495 | **_KNOWN ISSUE_** ([#20046](https://github.com/hashicorp/terraform-provider-aws/issues/20046)):
496 | If you set `inline_rules_enabled = true`, you cannot later set it to `false`. If you try,
497 | Terraform will [complain](https://github.com/hashicorp/terraform/pull/2376) and fail.
498 | You will either have to delete and recreate the security group or manually delete all
499 | the security group rules via the AWS console or CLI before applying `inline_rules_enabled = false`.
500 |
501 | **_Objects not of the same type_**: Any time you provide a list of objects, Terraform requires that all objects in the list
502 | must be [the exact same type](https://www.terraform.io/docs/language/expressions/type-constraints.html#dynamic-types-the-quot-any-quot-constraint).
503 | This means that all objects in the list have exactly the same set of attributes and that each attribute has the same type
504 | 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
505 | 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`,
506 | 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).
507 |
508 | > [!IMPORTANT]
509 | > In Cloud Posse's examples, we avoid pinning modules to specific versions to prevent discrepancies between the documentation
510 | > and the latest released versions. However, for your own projects, we strongly advise pinning each module to the exact version
511 | > you're using. This practice ensures the stability of your infrastructure. Additionally, we recommend implementing a systematic
512 | > approach for updating versions to avoid unexpected changes.
513 |
514 |
515 |
516 |
517 |
518 | ## Examples
519 |
520 | See [examples/complete/main.tf](https://github.com/cloudposse/terraform-aws-security-group/blob/master/examples/complete/main.tf) for
521 | even more examples.
522 |
523 | ```hcl
524 | module "label" {
525 | source = "cloudposse/label/null"
526 | # Cloud Posse recommends pinning every module to a specific version
527 | # version = "x.x.x"
528 | namespace = "eg"
529 | stage = "prod"
530 | name = "bastion"
531 | attributes = ["public"]
532 | delimiter = "-"
533 |
534 | tags = {
535 | "BusinessUnit" = "XYZ",
536 | "Snapshot" = "true"
537 | }
538 | }
539 |
540 | module "vpc" {
541 | source = "cloudposse/vpc/aws"
542 | # Cloud Posse recommends pinning every module to a specific version
543 | # version = "x.x.x"
544 | cidr_block = "10.0.0.0/16"
545 |
546 | context = module.label.context
547 | }
548 |
549 | module "sg" {
550 | source = "cloudposse/security-group/aws"
551 | # Cloud Posse recommends pinning every module to a specific version
552 | # version = "x.x.x"
553 |
554 | # Security Group names must be unique within a VPC.
555 | # This module follows Cloud Posse naming conventions and generates the name
556 | # based on the inputs to the null-label module, which means you cannot
557 | # reuse the label as-is for more than one security group in the VPC.
558 | #
559 | # Here we add an attribute to give the security group a unique name.
560 | attributes = ["primary"]
561 |
562 | # Allow unlimited egress
563 | allow_all_egress = true
564 |
565 | rules = [
566 | {
567 | key = "ssh"
568 | type = "ingress"
569 | from_port = 22
570 | to_port = 22
571 | protocol = "tcp"
572 | cidr_blocks = ["0.0.0.0/0"]
573 | self = null # preferable to self = false
574 | description = "Allow SSH from anywhere"
575 | },
576 | {
577 | key = "HTTP"
578 | type = "ingress"
579 | from_port = 80
580 | to_port = 80
581 | protocol = "tcp"
582 | cidr_blocks = []
583 | self = true
584 | description = "Allow HTTP from inside the security group"
585 | }
586 | ]
587 |
588 | vpc_id = module.vpc.vpc_id
589 |
590 | context = module.label.context
591 | }
592 |
593 | module "sg_mysql" {
594 | source = "cloudposse/security-group/aws"
595 | # Cloud Posse recommends pinning every module to a specific version
596 | # version = "x.x.x"
597 |
598 | # Add an attribute to give the Security Group a unique name
599 | attributes = ["mysql"]
600 |
601 | # Allow unlimited egress
602 | allow_all_egress = true
603 |
604 | rule_matrix =[
605 | # Allow any of these security groups or the specified prefixes to access MySQL
606 | {
607 | source_security_group_ids = [var.dev_sg, var.uat_sg, var.staging_sg]
608 | prefix_list_ids = [var.mysql_client_prefix_list_id]
609 | rules = [
610 | {
611 | key = "mysql"
612 | type = "ingress"
613 | from_port = 3306
614 | to_port = 3306
615 | protocol = "tcp"
616 | description = "Allow MySQL access from trusted security groups"
617 | }
618 | ]
619 | }
620 | ]
621 |
622 | vpc_id = module.vpc.vpc_id
623 |
624 | context = module.label.context
625 | }
626 |
627 | ```
628 |
629 | > [!TIP]
630 | > #### Use Terraform Reference Architectures for AWS
631 | >
632 | > Use Cloud Posse's ready-to-go [terraform architecture blueprints](https://cloudposse.com/reference-architecture/) for AWS to get up and running quickly.
633 | >
634 | > ✅ We build it together with your team.
635 | > ✅ Your team owns everything.
636 | > ✅ 100% Open Source and backed by fanatical support.
637 | >
638 | >
639 | > 📚 Learn More
640 | >
641 | >
642 | >
643 | > Cloud Posse is the leading [**DevOps Accelerator**](https://cpco.io/commercial-support?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=commercial_support) for funded startups and enterprises.
644 | >
645 | > *Your team can operate like a pro today.*
646 | >
647 | > Ensure that your team succeeds by using Cloud Posse's proven process and turnkey blueprints. Plus, we stick around until you succeed.
648 | > #### Day-0: Your Foundation for Success
649 | > - **Reference Architecture.** You'll get everything you need from the ground up built using 100% infrastructure as code.
650 | > - **Deployment Strategy.** Adopt a proven deployment strategy with GitHub Actions, enabling automated, repeatable, and reliable software releases.
651 | > - **Site Reliability Engineering.** Gain total visibility into your applications and services with Datadog, ensuring high availability and performance.
652 | > - **Security Baseline.** Establish a secure environment from the start, with built-in governance, accountability, and comprehensive audit logs, safeguarding your operations.
653 | > - **GitOps.** Empower your team to manage infrastructure changes confidently and efficiently through Pull Requests, leveraging the full power of GitHub Actions.
654 | >
655 | >
656 | >
657 | > #### Day-2: Your Operational Mastery
658 | > - **Training.** Equip your team with the knowledge and skills to confidently manage the infrastructure, ensuring long-term success and self-sufficiency.
659 | > - **Support.** Benefit from a seamless communication over Slack with our experts, ensuring you have the support you need, whenever you need it.
660 | > - **Troubleshooting.** Access expert assistance to quickly resolve any operational challenges, minimizing downtime and maintaining business continuity.
661 | > - **Code Reviews.** Enhance your team’s code quality with our expert feedback, fostering continuous improvement and collaboration.
662 | > - **Bug Fixes.** Rely on our team to troubleshoot and resolve any issues, ensuring your systems run smoothly.
663 | > - **Migration Assistance.** Accelerate your migration process with our dedicated support, minimizing disruption and speeding up time-to-value.
664 | > - **Customer Workshops.** Engage with our team in weekly workshops, gaining insights and strategies to continuously improve and innovate.
665 | >
666 | >
667 | >
668 |
669 |
670 |
671 |
672 | ## Requirements
673 |
674 | | Name | Version |
675 | |------|---------|
676 | | [terraform](#requirement\_terraform) | >= 1.0.0 |
677 | | [aws](#requirement\_aws) | >= 3.0 |
678 | | [null](#requirement\_null) | >= 3.0 |
679 | | [random](#requirement\_random) | >= 3.0 |
680 |
681 | ## Providers
682 |
683 | | Name | Version |
684 | |------|---------|
685 | | [aws](#provider\_aws) | >= 3.0 |
686 | | [null](#provider\_null) | >= 3.0 |
687 | | [random](#provider\_random) | >= 3.0 |
688 |
689 | ## Modules
690 |
691 | | Name | Source | Version |
692 | |------|--------|---------|
693 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 |
694 |
695 | ## Resources
696 |
697 | | Name | Type |
698 | |------|------|
699 | | [aws_security_group.cbd](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
700 | | [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
701 | | [aws_security_group_rule.dbc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
702 | | [aws_security_group_rule.keyed](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
703 | | [null_resource.sync_rules_and_sg_lifecycles](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource |
704 | | [random_id.rule_change_forces_new_security_group](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource |
705 |
706 | ## Inputs
707 |
708 | | Name | Description | Type | Default | Required |
709 | |------|-------------|------|---------|:--------:|
710 | | [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 |
711 | | [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` | `true` | no |
712 | | [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 |
713 | | [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 |
714 | | [create\_before\_destroy](#input\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
We only recommend setting this `false` if you are importing an existing security group
that you do not want replaced and therefore need full control over its name.
Note that changing this value will always cause the security group to be replaced. | `bool` | `true` | no |
715 | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no |
716 | | [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 |
717 | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no |
718 | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no |
719 | | [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 |
720 | | [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 |
721 | | [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 |
722 | | [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 |
723 | | [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 |
724 | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` | [
"default"
]
| no |
725 | | [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 |
726 | | [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 |
727 | | [preserve\_security\_group\_id](#input\_preserve\_security\_group\_id) | When `false` and `create_before_destroy` is `true`, changes to security group rules
cause a new security group to be created with the new rules, and the existing security group is then
replaced with the new one, eliminating any service interruption.
When `true` or when changing the value (from `false` to `true` or from `true` to `false`),
existing security group rules will be deleted before new ones are created, resulting in a service interruption,
but preserving the security group itself.
**NOTE:** Setting this to `true` does not guarantee the security group will never be replaced,
it only keeps changes to the security group rules from triggering a replacement.
See the README for further discussion. | `bool` | `false` | no |
728 | | [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 |
729 | | [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 |
730 | | [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 |
731 | | [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 the `security_group_rule` [documentation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule).
\_\_\_Note:\_\_\_ The length of the list must be known at plan time.
This means you cannot use functions like `compact` or `sort` when computing the list. | `list(any)` | `[]` | no |
732 | | [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 the `security_group_rule` [documentation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). | `any` | `{}` | no |
733 | | [security\_group\_create\_timeout](#input\_security\_group\_create\_timeout) | How long to wait for the security group to be created. | `string` | `"10m"` | no |
734 | | [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 |
735 | | [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 |
736 | | [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 |
737 | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no |
738 | | [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 |
739 | | [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 name and description will not be changed.
Not compatible with `inline_rules_enabled` or `revoke_rules_on_delete`.
If not provided (the default), this module will create a security group. | `list(string)` | `[]` | no |
740 | | [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 |
741 | | [vpc\_id](#input\_vpc\_id) | The ID of the VPC where the Security Group will be created. | `string` | n/a | yes |
742 |
743 | ## Outputs
744 |
745 | | Name | Description |
746 | |------|-------------|
747 | | [arn](#output\_arn) | The created Security Group ARN (null if using existing security group) |
748 | | [id](#output\_id) | The created or target Security Group ID |
749 | | [name](#output\_name) | The created Security Group Name (null if using existing security group) |
750 | | [rules\_terraform\_ids](#output\_rules\_terraform\_ids) | List of Terraform IDs of created `security_group_rule` resources, primarily provided to enable `depends_on` |
751 |
752 |
753 |
754 |
755 |
756 |
757 |
758 |
759 | ## Related Projects
760 |
761 | Check out these related projects.
762 |
763 | - [terraform-null-label](https://github.com/cloudposse/terraform-null-label) - Terraform module designed to generate consistent names and tags for resources. Use terraform-null-label to implement a strict naming convention.
764 |
765 |
766 | ## References
767 |
768 | For additional context, refer to some of these links.
769 |
770 | - [terraform-provider-aws](https://registry.terraform.io/providers/hashicorp/aws/latest) - Terraform AWS provider
771 |
772 |
773 |
774 |
775 | ## ✨ Contributing
776 |
777 | This project is under active development, and we encourage contributions from our community.
778 |
779 |
780 |
781 | Many thanks to our outstanding contributors:
782 |
783 |
784 |
785 |
786 |
787 | For 🐛 bug reports & feature requests, please use the [issue tracker](https://github.com/cloudposse/terraform-aws-security-group/issues).
788 |
789 | In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow.
790 | 1. Review our [Code of Conduct](https://github.com/cloudposse/terraform-aws-security-group/?tab=coc-ov-file#code-of-conduct) and [Contributor Guidelines](https://github.com/cloudposse/.github/blob/main/CONTRIBUTING.md).
791 | 2. **Fork** the repo on GitHub
792 | 3. **Clone** the project to your own machine
793 | 4. **Commit** changes to your own branch
794 | 5. **Push** your work back up to your fork
795 | 6. Submit a **Pull Request** so that we can review your changes
796 |
797 | **NOTE:** Be sure to merge the latest changes from "upstream" before making a pull request!
798 |
799 | ### 🌎 Slack Community
800 |
801 | Join our [Open Source Community](https://cpco.io/slack?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=slack) on Slack. It's **FREE** for everyone! Our "SweetOps" community is where you get to talk with others who share a similar vision for how to rollout and manage infrastructure. This is the best place to talk shop, ask questions, solicit feedback, and work together as a community to build totally *sweet* infrastructure.
802 |
803 | ### 📰 Newsletter
804 |
805 | Sign up for [our newsletter](https://cpco.io/newsletter?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=newsletter) and join 3,000+ DevOps engineers, CTOs, and founders who get insider access to the latest DevOps trends, so you can always stay in the know.
806 | Dropped straight into your Inbox every week — and usually a 5-minute read.
807 |
808 | ### 📆 Office Hours
809 |
810 | [Join us every Wednesday via Zoom](https://cloudposse.com/office-hours?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-security-group&utm_content=office_hours) for your weekly dose of insider DevOps trends, AWS news and Terraform insights, all sourced from our SweetOps community, plus a _live Q&A_ that you can’t find anywhere else.
811 | It's **FREE** for everyone!
812 | ## License
813 |
814 |
815 |
816 |
817 | Preamble to the Apache License, Version 2.0
818 |
819 |
820 |
821 | Complete license is available in the [`LICENSE`](LICENSE) file.
822 |
823 | ```text
824 | Licensed to the Apache Software Foundation (ASF) under one
825 | or more contributor license agreements. See the NOTICE file
826 | distributed with this work for additional information
827 | regarding copyright ownership. The ASF licenses this file
828 | to you under the Apache License, Version 2.0 (the
829 | "License"); you may not use this file except in compliance
830 | with the License. You may obtain a copy of the License at
831 |
832 | https://www.apache.org/licenses/LICENSE-2.0
833 |
834 | Unless required by applicable law or agreed to in writing,
835 | software distributed under the License is distributed on an
836 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
837 | KIND, either express or implied. See the License for the
838 | specific language governing permissions and limitations
839 | under the License.
840 | ```
841 |
842 |
843 | ## Trademarks
844 |
845 | All other trademarks referenced herein are the property of their respective owners.
846 |
847 |
848 | ## Copyrights
849 |
850 | Copyright © 2021-2025 [Cloud Posse, LLC](https://cloudposse.com)
851 |
852 |
853 |
854 |
855 |
856 |
857 |
--------------------------------------------------------------------------------
/README.yaml:
--------------------------------------------------------------------------------
1 | #
2 | # This is the canonical configuration for the `README.md`
3 | # Run `make readme` to rebuild the `README.md`
4 | #
5 |
6 | # Name of this project
7 | name: terraform-aws-security-group
8 |
9 | # Tags of this project
10 | tags:
11 | - aws
12 | - security-group
13 | - terraform
14 | - terraform-modules
15 |
16 | # Logo for this project
17 | #logo: docs/logo.png
18 |
19 | # License of this project
20 | license: "APACHE2"
21 |
22 | # Copyrights
23 | copyrights:
24 | - name: "Cloud Posse, LLC"
25 | url: "https://cloudposse.com"
26 | year: "2021"
27 |
28 | # Canonical GitHub repo
29 | github_repo: cloudposse/terraform-aws-security-group
30 |
31 | # Badges to display
32 | badges:
33 | - name: Latest Release
34 | image: https://img.shields.io/github/release/cloudposse/terraform-aws-security-group.svg?style=for-the-badge
35 | url: https://github.com/cloudposse/terraform-aws-security-group/releases/latest
36 | - name: Last Updated
37 | image: https://img.shields.io/github/last-commit/cloudposse/terraform-aws-security-group.svg?style=for-the-badge
38 | url: https://github.com/cloudposse/terraform-aws-security-group/commits
39 | - name: Slack Community
40 | image: https://slack.cloudposse.com/for-the-badge.svg
41 | url: https://cloudposse.com/slack
42 |
43 | # List any related terraform modules that this module may be used with or that this module depends on.
44 | related:
45 | - name: "terraform-null-label"
46 | description: "Terraform module designed to generate consistent names and tags for resources. Use terraform-null-label to implement a strict naming convention."
47 | url: "https://github.com/cloudposse/terraform-null-label"
48 |
49 | # List any resources helpful for someone to get started. For example, link to the hashicorp documentation or AWS documentation.
50 | references:
51 | - name: terraform-provider-aws
52 | description: Terraform AWS provider
53 | url: https://registry.terraform.io/providers/hashicorp/aws/latest
54 |
55 | # Short description of this project
56 | description: |-
57 | Terraform module to create AWS Security Group and rules.
58 |
59 | # Introduction to the project
60 | #introduction: |-
61 | # This is an introduction.
62 |
63 | # How to use this module. Should be an easy example to copy and paste.
64 | usage: |-
65 | This module is primarily for setting security group rules on a security group. You can provide the
66 | ID of an existing security group to modify, or, by default, this module will create a new security
67 | group and apply the given rules to it.
68 |
69 | This module can be used very simply, but it is actually quite complex because it is attempting to handle
70 | numerous interrelationships, restrictions, and a few bugs in ways that offer a choice between zero
71 | service interruption for updates to a security group not referenced by other security groups
72 | (by replacing the security group with a new one) versus brief service interruptions for security groups that must be preserved.
73 |
74 | ### Avoiding Service Interruptions
75 |
76 | It is desirable to avoid having service interruptions when updating a security group. This is not always
77 | possible due to the way Terraform organizes its activities and the fact that AWS will reject an attempt
78 | to create a duplicate of an existing security group rule. There is also the issue that while most AWS
79 | resources can be associated with and disassociated from security groups at any time, there remain some
80 | that may not have their security group association changed, and an attempt to change their security group
81 | will cause Terraform to delete and recreate the resource.
82 |
83 | #### The 2 Ways Security Group Changes Cause Service Interruptions
84 |
85 | Changes to a security group can cause service interruptions in 2 ways:
86 |
87 | 1. Changing rules may be implemented as deleting existing rules and creating new ones. During the
88 | period between deleting the old rules and creating the new rules, the security group will block
89 | traffic intended to be allowed by the new rules.
90 | 2. Changing rules may alternately be implemented as creating a new security group with the new rules
91 | and replacing the existing security group with the new one (then deleting the old one).
92 | This usually works with no service interruption in the case where all resources that reference the
93 | security group are part of the same Terraform plan.
94 | However, if, for example, the security group ID is referenced in a security group
95 | rule in a security group that is not part of the same Terraform plan, then AWS will not allow the
96 | existing (referenced) security group to be deleted, and even if it did, Terraform would not know
97 | to update the rule to reference the new security group.
98 |
99 | The key question you need to answer to decide which configuration to use is "will anything break
100 | if the security group ID changes". If not, then use the defaults `create_before_destroy = true` and
101 | `preserve_security_group_id = false` and do not worry about providing "keys" for
102 | security group rules. This is the default because it is the easiest and safest solution when
103 | the way the security group is being used allows it.
104 |
105 | If things will break when the security group ID changes, then set `preserve_security_group_id`
106 | to `true`. Also read and follow the guidance below about [keys](#the-importance-of-keys) and
107 | [limiting Terraform security group rules to a single AWS security group rule](#terraform-rules-vs-aws-rules)
108 | if you want to mitigate against service interruptions caused by rule changes.
109 | Note that even in this case, you probably want to keep `create_before_destroy = true` because otherwise,
110 | if some change requires the security group to be replaced, Terraform will likely succeed
111 | in deleting all the security group rules but fail to delete the security group itself,
112 | leaving the associated resources completely inaccessible. At least with `create_before_destroy = true`,
113 | the new security group will be created and used where Terraform can make the changes,
114 | even though the old security group will still fail to be deleted.
115 |
116 | #### The 3 Ways to Mitigate Against Service Interruptions
117 |
118 | ##### Security Group `create_before_destroy = true`
119 |
120 | The most important option is `create_before_destroy` which, when set to `true` (the default),
121 | ensures that a new replacement security group is created before an existing one is destroyed.
122 | This is particularly important because a security group cannot be destroyed while it is associated with
123 | a resource (e.g. a load balancer), but "destroy before create" behavior causes Terraform
124 | to try to destroy the security group before disassociating it from associated resources,
125 | so plans fail to apply with the error
126 |
127 | ```
128 | Error deleting security group: DependencyViolation: resource sg-XXX has a dependent object
129 | ```
130 |
131 | With "create before destroy" and any resources dependent on the security group as part of the
132 | same Terraform plan, replacement happens successfully:
133 |
134 | 1. New security group is created
135 | 2. Resource is associated with the new security group and disassociated from the old one
136 | 3. Old security group is deleted successfully because there is no longer anything associated with it
137 |
138 | (If there is a resource dependent on the security group that is also outside the scope of
139 | the Terraform plan, the old security group will fail to be deleted and you will have to
140 | address the dependency manually.)
141 |
142 | Note that the module's default configuration of `create_before_destroy = true` and
143 | `preserve_security_group_id = false` will force "create before destroy" behavior on the target security
144 | group, even if the module did not create it and instead you provided a `target_security_group_id`.
145 |
146 | Unfortunately, just creating the new security group first is not enough to prevent a service interruption. Keep reading.
147 |
148 | ##### Setting Rule Changes to Force Replacement of the Security Group
149 |
150 | A security group by itself is just a container for rules. It only functions as desired when all the rules are in place.
151 | If using the Terraform default "destroy before create" behavior for rules, even when using `create_before_destroy` for the
152 | security group itself, an outage occurs when updating the rules or security group, because the order of operations is:
153 |
154 | 1. Delete existing security group rules (triggering a service interruption)
155 | 2. Create the new security group
156 | 3. Associate the new security group with resources and disassociate the old one (which can take a substantial
157 | amount of time for a resource like a NAT Gateway)
158 | 4. Create the new security group rules (restoring service)
159 | 5. Delete the old security group
160 |
161 | To resolve this issue, the module's default configuration of `create_before_destroy = true` and
162 | `preserve_security_group_id = false` causes any change in the security group rules
163 | to trigger the creation of a new security group. With that, a rule change causes operations to occur in this order:
164 |
165 | 1. Create the new security group
166 | 2. Create the new security group rules
167 | 3. Associate the new security group with resources and disassociate the old one
168 | 4. Delete the old security group rules
169 | 5. Delete the old security group
170 |
171 | ##### Preserving the Security Group
172 |
173 | There can be a downside to creating a new security group with every rule change.
174 | If you want to prevent the security group ID from changing unless absolutely necessary, perhaps because the associated
175 | resource does not allow the security group to be changed or because the ID is referenced somewhere (like in
176 | another security group's rules) outside of this Terraform plan, then you need to set `preserve_security_group_id` to `true`.
177 |
178 | The main drawback of this configuration is that there will normally be
179 | a service outage during an update, because existing rules will be deleted before replacement
180 | rules are created. Using keys to identify rules can help limit the impact, but even with keys, simply adding a
181 | CIDR to the list of allowed CIDRs will cause that entire rule to be deleted and recreated, causing a temporary
182 | access denial for all of the CIDRs in the rule. (For more on this and how to mitigate against it, see [The Importance
183 | of Keys](#the-importance-of-keys) below.)
184 |
185 | Also note that setting `preserve_security_group_id` to `true` does not prevent Terraform from replacing the
186 | security group when modifying it is not an option, such as when its name or description changes.
187 | However, if you can control the configuration adequately, you can maintain the security group ID and eliminate
188 | impact on other security groups by setting `preserve_security_group_id` to `true`. We still recommend
189 | leaving `create_before_destroy` set to `true` for the times when the security group must be replaced,
190 | to avoid the `DependencyViolation` described above.
191 |
192 | ### Defining Security Group Rules
193 |
194 | We provide a number of different ways to define rules for the security group for a few reasons:
195 | - Terraform type constraints make it difficult to create collections of objects with optional members
196 | - Terraform resource addressing can cause resources that did not actually change to nevertheless be replaced
197 | (deleted and recreated), which, in the case of security group rules, then causes a brief service interruption
198 | - Terraform resource addresses must be known at `plan` time, making it challenging to create rules that
199 | depend on resources being created during `apply` and at the same time are not replaced needlessly when something else changes
200 | - When Terraform rules can be successfully created before being destroyed, there is no service interruption for the resources
201 | associated with that security group (unless the security group ID is used in other security group rules outside
202 | of the scope of the Terraform plan)
203 |
204 | #### The Importance of Keys
205 |
206 | If you are using "create before destroy" behavior for the security group and security group rules, then
207 | you can skip this section and much of the discussion about keys in the later sections, because keys do not matter
208 | in this configuration. However, if you are using "destroy before create" behavior, then a full understanding of keys
209 | as applied to security group rules will help you minimize service interruptions due to changing rules.
210 |
211 | When creating a collection of resources, Terraform requires each resource to be identified by a key,
212 | so that each resource has a unique "address", and changes to resources are tracked by that key.
213 | Every security group rule input to this module accepts optional identifying keys (arbitrary strings) for each rule.
214 | If you do not supply keys, then the rules are treated as a list,
215 | and the index of the rule in the list will be used as its key. This has the unwelcome behavior that removing a rule
216 | from the list will cause all the rules later in the list to be destroyed and recreated. For example, changing
217 | `[A, B, C, D]` to `[A, C, D]` causes rules 1(`B`), 2(`C`), and 3(`D`) to be deleted and new rules 1(`C`) and
218 | 2(`D`) to be created.
219 |
220 | To mitigate against this problem, we allow you to specify keys (arbitrary strings) for each rule. (Exactly how you specify
221 | the key is explained in the next sections.) Going back to our example, if the
222 | initial set of rules were specified with keys, e.g. `[{A: A}, {B: B}, {C: C}, {D: D}]`, then removing `B` from the list
223 | would only cause `B` to be deleted, leaving `C` and `D` intact.
224 |
225 | Note, however, two cautions. First, the keys must be known at `terraform plan` time and therefore cannot depend
226 | on resources that will be created during `apply`. Second, in order to be helpful, the keys must remain consistently
227 | attached to the same rules. For example, if you did
228 |
229 | ```hcl
230 | rule_map = { for i, v in rule_list : i => v }
231 | ```
232 |
233 | then you will have merely recreated the initial problem with using a plain list. If you cannot attach
234 | meaningful keys to the rules, there is no advantage to specifying keys at all.
235 |
236 | #### Terraform Rules vs AWS Rules
237 |
238 | A single security group rule input can actually specify multiple AWS security group rules. For example,
239 | `ipv6_cidr_blocks` takes a list of CIDRs. However, AWS security group rules do not allow for a list
240 | of CIDRs, so the AWS Terraform provider converts that list of CIDRs into a list of AWS security group rules,
241 | one for each CIDR. (This is the underlying cause of several AWS Terraform provider bugs,
242 | such as [#25173](https://github.com/hashicorp/terraform-provider-aws/issues/25173).)
243 | As of this writing, any change to any element of such a rule will cause
244 | all the AWS rules specified by the Terraform rule to be deleted and recreated, causing the same kind of
245 | service interruption we sought to avoid by providing keys for the rules, or, when create_before_destroy = true,
246 | causing a complete failure as Terraform tries to create duplicate rules which AWS rejects. To guard against this issue,
247 | when not using the default behavior, you should avoid the convenience of specifying multiple AWS rules
248 | in a single Terraform rule and instead create a separate Terraform rule for each source or destination specification.
249 |
250 | ##### `rules` and `rules_map` inputs
251 | This module provides 3 ways to set security group rules. You can use any or all of them at the same time.
252 |
253 | The easy way to specify rules is via the `rules` input. It takes a list of rules. (We will define
254 | a rule [a bit later](#definition-of-a-rule).) The problem is that a Terraform list must be composed
255 | of elements that are all the exact same type, and rules can be any of several
256 | different Terraform types. So to get around this restriction, the second
257 | way to specify rules is via the `rules_map` input, which is more complex.
258 |
259 | Why the input is so complex (click to reveal)
260 |
261 | - Terraform has 3 basic simple types: bool, number, string
262 | - Terraform then has 3 collections of simple types: list, map, and set
263 | - Terraform then has 2 structural types: object and tuple. However, these are not really single
264 | types. They are catch-all labels for values that are themselves combination of other values.
265 | (This will become a bit clearer after we define `maps` and contrast them with `objects`)
266 |
267 | One [rule of the collection types](https://www.terraform.io/docs/language/expressions/type-constraints.html#collection-types)
268 | is that the values in the collections must all be the exact same type.
269 | For example, you cannot have a list where some values are boolean and some are string. Maps require
270 | that all keys be strings, but the map values can be any type, except again all the values in a map
271 | must be the same type. In other words, the values of a map must form a valid list.
272 |
273 | Objects look just like maps. The difference between an object and a map is that the values in an
274 | object do not all have to be the same type.
275 |
276 | 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.
277 |
278 | So although `{ foo = "bar", baz = {} }` and `{ foo = "bar", baz = [] }` are both objects,
279 | they are not of the same type, and you can get error messages like
280 |
281 | ```
282 | Error: Inconsistent conditional result types
283 | The true and false result expressions must have consistent types. The given
284 | expressions are object and object, respectively.
285 | ```
286 |
287 | This means you cannot put them both in the same list or the same map,
288 | even though you can put them in a single tuple or object.
289 | Similarly, and closer to the problem at hand,
290 |
291 | ```hcl
292 | cidr_rule = {
293 | type = "ingress"
294 | cidr_blocks = ["0.0.0.0/0"]
295 | }
296 | ```
297 | is not the same type as
298 |
299 | ```hcl
300 | self_rule = {
301 | type = "ingress"
302 | self = true
303 | }
304 | ```
305 |
306 | This means you cannot put both of those in the same list.
307 |
308 | ```hcl
309 | rules = tolist([local.cidr_rule, local.self_rule])
310 | ```
311 |
312 | Generates the error
313 |
314 | ```text
315 | Invalid value for "v" parameter: cannot convert tuple to list of any single type.
316 | ```
317 |
318 | You could make them the same type and put them in a list,
319 | like this:
320 |
321 | ```hcl
322 | rules = tolist([{
323 | type = "ingress"
324 | cidr_blocks = ["0.0.0.0/0"]
325 | self = null
326 | },
327 | {
328 | type = "ingress"
329 | cidr_blocks = []
330 | self = true
331 | }])
332 | ```
333 |
334 | That remains an option for you when generating the rules, and is probably better when you have full control over all the rules.
335 | However, what if some of the rules are coming from a source outside of your control? You cannot simply add those rules
336 | to your list. So, what to do? Create an object whose attributes' values can be of different types.
337 |
338 | ```hcl
339 | { mine = local.my_rules, theirs = var.their_rules }
340 | ```
341 |
342 | That is why the `rules_map` input is available. It will accept a structure like that, an object whose
343 | attribute values are lists of rules, where the lists themselves can be different types.
344 |
345 |
346 |
347 | The `rules_map` input takes an object.
348 | - The attribute names (keys) of the object can be anything you want, but need to be known during `terraform plan`,
349 | which means they cannot depend on any resources created or changed by Terraform.
350 | - The values of the attributes are lists of rule objects, each object representing one Security Group Rule. As explained
351 | above in "Why the input is so complex", each object in the list must be exactly the same type. To use multiple types,
352 | you must put them in separate lists and put the lists in a map with distinct keys.
353 |
354 | Example:
355 |
356 | ```hcl
357 | rules_map = {
358 | ingress = [{
359 | key = "ingress"
360 | type = "ingress"
361 | from_port = 0
362 | to_port = 2222
363 | protocol = "tcp"
364 | cidr_blocks = module.subnets.nat_gateway_public_ips
365 | self = null
366 | description = "2222"
367 | }],
368 | egress = [{
369 | key = "egress"
370 | type = "egress"
371 | from_port = 0
372 | to_port = 0
373 | protocol = "-1"
374 | cidr_blocks = ["0.0.0.0/0"]
375 | self = null
376 | description = "All output traffic"
377 | }]
378 | }
379 | ```
380 |
381 | ###### Definition of a Rule
382 |
383 | For this module, a rule is defined as an object.
384 | - The attributes and values of the rule objects are fully compatible (have the same keys and accept the same values) as the
385 | Terraform [aws_security_group_rule resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule),
386 | except:
387 | - The `security_group_id` will be ignored, if present
388 | - You can include an optional `key` attribute. If present, its value must be unique among all security group rules in the
389 | security group, and it must be known in the Terraform "plan" phase, meaning it cannot depend on anything being
390 | generated or created by Terraform.
391 |
392 | The `key` attribute value, if provided, will be used to identify the Security Group Rule to Terraform in order to
393 | prevent Terraform from modifying it unnecessarily. If the `key` is not provided, Terraform will assign an identifier
394 | based on the rule's position in its list, which can cause a ripple effect of rules being deleted and recreated if
395 | a rule gets deleted from start of a list, causing all the other rules to shift position.
396 | See ["Unexpected changes..."](#unexpected-changes-during-plan-and-apply) below for more details.
397 |
398 |
399 | ##### `rule_matrix` Input
400 |
401 | The other way to set rules is via the `rule_matrix` input. This splits the attributes of the `aws_security_group_rule`
402 | resource into two sets: one set defines the rule and description, the other set defines the subjects of the rule.
403 | Again, optional "key" values can provide stability, but cannot contain derived values. This input is an attempt
404 | at convenience, and should not be used unless you are using the default settings of `create_before_destroy = true` and
405 | `preserve_security_group_id = false`, or else a number of failure modes or service interruptions are possible: use
406 | `rules_map` instead.
407 |
408 | As with `rules` and explained above in "Why the input is so complex", all elements of the list must be the exact same type.
409 | This also holds for all the elements of the `rules_matrix.rules` list. Because `rule_matrix` is already
410 | so complex, we do not provide the ability to mix types by packing object within more objects.
411 | All of the elements of the `rule_matrix` list must be exactly the same type. You can make them all the same
412 | type by following a few rules:
413 |
414 | - Every object in a list must have the exact same set of attributes. Most attributes are optional and can be omitted,
415 | but any attribute appearing in one object must appear in all the objects.
416 | - Any attribute that takes a list value in any object must contain a list in all objects.
417 | Use an empty list rather than `null` to indicate "no value". Passing in `null` instead of a list
418 | may cause Terraform to crash or emit confusing error messages (e.g. "number is required").
419 | - Any attribute that takes a value of type other than list can be set to `null` in objects where no value is needed.
420 |
421 | The schema for `rule_matrix` is:
422 |
423 | ```hcl
424 | {
425 | # these top level lists define all the subjects to which rule_matrix rules will be applied
426 | key = an optional unique key to keep these rules from being affected when other rules change
427 | source_security_group_ids = list of source security group IDs to apply all rules to
428 | cidr_blocks = list of ipv4 CIDR blocks to apply all rules to
429 | ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to
430 | prefix_list_ids = list of prefix list IDs to apply all rules to
431 |
432 | self = boolean value; set it to "true" to apply the rules to the created or existing security group, null otherwise
433 |
434 | # each rule in the rules list will be applied to every subject defined above
435 | rules = [{
436 | key = an optional unique key to keep this rule from being affected when other rules change
437 | type = type of rule, either "ingress" or "egress"
438 | from_port = start range of protocol port
439 | to_port = end range of protocol port, max is 65535
440 | protocol = IP protocol name or number, or "-1" for all protocols and ports
441 |
442 | description = free form text description of the rule
443 | }]
444 | }
445 | ```
446 |
447 | ### Important Notes
448 |
449 | ##### Unexpected changes during plan and apply
450 |
451 | When configuring this module for "create before destroy" behavior, any change to
452 | a security group rule will cause an entire new security group to be created with
453 | all new rules. This can make a small change look like a big one, but is intentional
454 | and should not cause concern.
455 |
456 | As explained above under [The Importance of Keys](#the-importance-of-keys),
457 | when using "destroy before create" behavior, security group rules without keys
458 | are identified by their indices in the input lists. If a rule is deleted and the other rules therefore move
459 | closer to the start of the list, those rules will be deleted and recreated. This
460 | can make a small change look like a big one when viewing the output of Terraform plan,
461 | and will likely cause a brief (seconds) service interruption.
462 |
463 | You can avoid this for the most part by providing the optional keys, and [limiting each rule
464 | to a single source or destination](#terraform-rules-vs-aws-rules). Rules with keys will not be
465 | changed if their keys do not change and the rules themselves do not change, except in the case of
466 | `rule_matrix`, where the rules are still dependent on the order of the security groups in
467 | `source_security_group_ids`. You can avoid this by using `rules` or `rules_map` instead of `rule_matrix` when you have
468 | more than one security group in the list. You cannot avoid this by sorting the
469 | `source_security_group_ids`, because that leads to the "Invalid `for_each` argument" error
470 | because of [terraform#31035](https://github.com/hashicorp/terraform/issues/31035).
471 |
472 | ##### Invalid for_each argument
473 |
474 | You can supply a number of rules as inputs to this module, and they (usually) get transformed into
475 | `aws_security_group_rule` resources. However, Terraform works in 2 steps: a `plan` step where it
476 | calculates the changes to be made, and an `apply` step where it makes the changes. This is so you
477 | can review and approve the plan before changing anything. One big limitation of this approach is
478 | that it requires that Terraform be able to count the number of resources to create without the
479 | benefit of any data generated during the `apply` phase. So if you try to generate a rule based
480 | on something you are creating at the same time, you can get an error like
481 |
482 | ```
483 | Error: Invalid for_each argument
484 | The "for_each" value depends on resource attributes that cannot be determined until apply,
485 | so Terraform cannot predict how many instances will be created.
486 | ```
487 |
488 | This module uses lists to minimize the chance of that happening, as all it needs to know
489 | is the length of the list, not the values in it, but this error still can
490 | happen for subtle reasons. Most commonly, using a function like `compact` on a list
491 | will cause the length to become unknown (since the values have to be checked and `null`s removed).
492 | In the case of `source_security_group_ids`, just sorting the list using `sort`
493 | will cause this error. (See [terraform#31035](https://github.com/hashicorp/terraform/issues/31035).)
494 | If you run into this error, check for functions like `compact` somewhere
495 | in the chain that produces the list and remove them if you find them.
496 |
497 |
498 | ##### WARNINGS and Caveats
499 |
500 | **_Setting `inline_rules_enabled` is not recommended and NOT SUPPORTED_**: Any issues arising from setting
501 | `inlne_rules_enabled = true` (including issues about setting it to `false` after setting it to `true`) will
502 | not be addressed, because they flow from [fundamental problems](https://github.com/hashicorp/terraform-provider-aws/issues/20046)
503 | with the underlying `aws_security_group` resource. The setting is provided for people who know and accept the
504 | limitations and trade-offs and want to use it anyway. The main advantage is that when using inline rules,
505 | Terraform will perform "drift detection" and attempt to remove any rules it finds in place but not
506 | specified inline. See [this post](https://github.com/hashicorp/terraform-provider-aws/pull/9032#issuecomment-639545250)
507 | for a discussion of the difference between inline and resource rules,
508 | and some of the reasons inline rules are not satisfactory.
509 |
510 | **_KNOWN ISSUE_** ([#20046](https://github.com/hashicorp/terraform-provider-aws/issues/20046)):
511 | If you set `inline_rules_enabled = true`, you cannot later set it to `false`. If you try,
512 | Terraform will [complain](https://github.com/hashicorp/terraform/pull/2376) and fail.
513 | You will either have to delete and recreate the security group or manually delete all
514 | the security group rules via the AWS console or CLI before applying `inline_rules_enabled = false`.
515 |
516 | **_Objects not of the same type_**: Any time you provide a list of objects, Terraform requires that all objects in the list
517 | must be [the exact same type](https://www.terraform.io/docs/language/expressions/type-constraints.html#dynamic-types-the-quot-any-quot-constraint).
518 | This means that all objects in the list have exactly the same set of attributes and that each attribute has the same type
519 | 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
520 | 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`,
521 | 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).
522 |
523 |
524 | # Example usage
525 | examples: |2-
526 |
527 | See [examples/complete/main.tf](https://github.com/cloudposse/terraform-aws-security-group/blob/master/examples/complete/main.tf) for
528 | even more examples.
529 |
530 | ```hcl
531 | module "label" {
532 | source = "cloudposse/label/null"
533 | # Cloud Posse recommends pinning every module to a specific version
534 | # version = "x.x.x"
535 | namespace = "eg"
536 | stage = "prod"
537 | name = "bastion"
538 | attributes = ["public"]
539 | delimiter = "-"
540 |
541 | tags = {
542 | "BusinessUnit" = "XYZ",
543 | "Snapshot" = "true"
544 | }
545 | }
546 |
547 | module "vpc" {
548 | source = "cloudposse/vpc/aws"
549 | # Cloud Posse recommends pinning every module to a specific version
550 | # version = "x.x.x"
551 | cidr_block = "10.0.0.0/16"
552 |
553 | context = module.label.context
554 | }
555 |
556 | module "sg" {
557 | source = "cloudposse/security-group/aws"
558 | # Cloud Posse recommends pinning every module to a specific version
559 | # version = "x.x.x"
560 |
561 | # Security Group names must be unique within a VPC.
562 | # This module follows Cloud Posse naming conventions and generates the name
563 | # based on the inputs to the null-label module, which means you cannot
564 | # reuse the label as-is for more than one security group in the VPC.
565 | #
566 | # Here we add an attribute to give the security group a unique name.
567 | attributes = ["primary"]
568 |
569 | # Allow unlimited egress
570 | allow_all_egress = true
571 |
572 | rules = [
573 | {
574 | key = "ssh"
575 | type = "ingress"
576 | from_port = 22
577 | to_port = 22
578 | protocol = "tcp"
579 | cidr_blocks = ["0.0.0.0/0"]
580 | self = null # preferable to self = false
581 | description = "Allow SSH from anywhere"
582 | },
583 | {
584 | key = "HTTP"
585 | type = "ingress"
586 | from_port = 80
587 | to_port = 80
588 | protocol = "tcp"
589 | cidr_blocks = []
590 | self = true
591 | description = "Allow HTTP from inside the security group"
592 | }
593 | ]
594 |
595 | vpc_id = module.vpc.vpc_id
596 |
597 | context = module.label.context
598 | }
599 |
600 | module "sg_mysql" {
601 | source = "cloudposse/security-group/aws"
602 | # Cloud Posse recommends pinning every module to a specific version
603 | # version = "x.x.x"
604 |
605 | # Add an attribute to give the Security Group a unique name
606 | attributes = ["mysql"]
607 |
608 | # Allow unlimited egress
609 | allow_all_egress = true
610 |
611 | rule_matrix =[
612 | # Allow any of these security groups or the specified prefixes to access MySQL
613 | {
614 | source_security_group_ids = [var.dev_sg, var.uat_sg, var.staging_sg]
615 | prefix_list_ids = [var.mysql_client_prefix_list_id]
616 | rules = [
617 | {
618 | key = "mysql"
619 | type = "ingress"
620 | from_port = 3306
621 | to_port = 3306
622 | protocol = "tcp"
623 | description = "Allow MySQL access from trusted security groups"
624 | }
625 | ]
626 | }
627 | ]
628 |
629 | vpc_id = module.vpc.vpc_id
630 |
631 | context = module.label.context
632 | }
633 |
634 | ```
635 |
636 |
637 | # How to get started quickly
638 | #quickstart: |-
639 | # Here's how to get started...
640 |
641 | # Other files to include in this README from the project folder
642 | include: []
643 | contributors: []
644 |
--------------------------------------------------------------------------------
/atmos.yaml:
--------------------------------------------------------------------------------
1 | # Atmos Configuration — powered by https://atmos.tools
2 | #
3 | # This configuration enables centralized, DRY, and consistent project scaffolding using Atmos.
4 | #
5 | # Included features:
6 | # - Organizational custom commands: https://atmos.tools/core-concepts/custom-commands
7 | # - Automated README generation: https://atmos.tools/cli/commands/docs/generate
8 | #
9 |
10 | # Import shared configuration used by all modules
11 | import:
12 | - https://raw.githubusercontent.com/cloudposse/.github/refs/heads/main/.github/atmos/terraform-module.yaml
13 |
--------------------------------------------------------------------------------
/context.tf:
--------------------------------------------------------------------------------
1 | #
2 | # ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label
3 | # All other instances of this file should be a copy of that one
4 | #
5 | #
6 | # Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf
7 | # and then place it in your Terraform module to automatically get
8 | # Cloud Posse's standard configuration inputs suitable for passing
9 | # to Cloud Posse modules.
10 | #
11 | # curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf
12 | #
13 | # Modules should access the whole context as `module.this.context`
14 | # to get the input variables with nulls for defaults,
15 | # for example `context = module.this.context`,
16 | # and access individual variables as `module.this.`,
17 | # with final values filled in.
18 | #
19 | # For example, when using defaults, `module.this.context.delimiter`
20 | # will be null, and `module.this.delimiter` will be `-` (hyphen).
21 | #
22 |
23 | module "this" {
24 | source = "cloudposse/label/null"
25 | version = "0.25.0" # requires Terraform >= 0.13.0
26 |
27 | enabled = var.enabled
28 | namespace = var.namespace
29 | tenant = var.tenant
30 | environment = var.environment
31 | stage = var.stage
32 | name = var.name
33 | delimiter = var.delimiter
34 | attributes = var.attributes
35 | tags = var.tags
36 | additional_tag_map = var.additional_tag_map
37 | label_order = var.label_order
38 | regex_replace_chars = var.regex_replace_chars
39 | id_length_limit = var.id_length_limit
40 | label_key_case = var.label_key_case
41 | label_value_case = var.label_value_case
42 | descriptor_formats = var.descriptor_formats
43 | labels_as_tags = var.labels_as_tags
44 |
45 | context = var.context
46 | }
47 |
48 | # Copy contents of cloudposse/terraform-null-label/variables.tf here
49 |
50 | variable "context" {
51 | type = any
52 | default = {
53 | enabled = true
54 | namespace = null
55 | tenant = null
56 | environment = null
57 | stage = null
58 | name = null
59 | delimiter = null
60 | attributes = []
61 | tags = {}
62 | additional_tag_map = {}
63 | regex_replace_chars = null
64 | label_order = []
65 | id_length_limit = null
66 | label_key_case = null
67 | label_value_case = null
68 | descriptor_formats = {}
69 | # Note: we have to use [] instead of null for unset lists due to
70 | # https://github.com/hashicorp/terraform/issues/28137
71 | # which was not fixed until Terraform 1.0.0,
72 | # but we want the default to be all the labels in `label_order`
73 | # and we want users to be able to prevent all tag generation
74 | # by setting `labels_as_tags` to `[]`, so we need
75 | # a different sentinel to indicate "default"
76 | labels_as_tags = ["unset"]
77 | }
78 | description = <<-EOT
79 | Single object for setting entire context at once.
80 | See description of individual variables for details.
81 | Leave string and numeric variables as `null` to use default value.
82 | Individual variable settings (non-null) override settings in context object,
83 | except for attributes, tags, and additional_tag_map, which are merged.
84 | EOT
85 |
86 | validation {
87 | condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"])
88 | error_message = "Allowed values: `lower`, `title`, `upper`."
89 | }
90 |
91 | validation {
92 | condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"])
93 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`."
94 | }
95 | }
96 |
97 | variable "enabled" {
98 | type = bool
99 | default = null
100 | description = "Set to false to prevent the module from creating any resources"
101 | }
102 |
103 | variable "namespace" {
104 | type = string
105 | default = null
106 | description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique"
107 | }
108 |
109 | variable "tenant" {
110 | type = string
111 | default = null
112 | description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for"
113 | }
114 |
115 | variable "environment" {
116 | type = string
117 | default = null
118 | description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'"
119 | }
120 |
121 | variable "stage" {
122 | type = string
123 | default = null
124 | description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'"
125 | }
126 |
127 | variable "name" {
128 | type = string
129 | default = null
130 | description = <<-EOT
131 | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
132 | This is the only ID element not also included as a `tag`.
133 | The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input.
134 | EOT
135 | }
136 |
137 | variable "delimiter" {
138 | type = string
139 | default = null
140 | description = <<-EOT
141 | Delimiter to be used between ID elements.
142 | Defaults to `-` (hyphen). Set to `""` to use no delimiter at all.
143 | EOT
144 | }
145 |
146 | variable "attributes" {
147 | type = list(string)
148 | default = []
149 | description = <<-EOT
150 | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
151 | in the order they appear in the list. New attributes are appended to the
152 | end of the list. The elements of the list are joined by the `delimiter`
153 | and treated as a single ID element.
154 | EOT
155 | }
156 |
157 | variable "labels_as_tags" {
158 | type = set(string)
159 | default = ["default"]
160 | description = <<-EOT
161 | Set of labels (ID elements) to include as tags in the `tags` output.
162 | Default is to include all labels.
163 | Tags with empty values will not be included in the `tags` output.
164 | Set to `[]` to suppress all generated tags.
165 | **Notes:**
166 | The value of the `name` tag, if included, will be the `id`, not the `name`.
167 | Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
168 | changed in later chained modules. Attempts to change it will be silently ignored.
169 | EOT
170 | }
171 |
172 | variable "tags" {
173 | type = map(string)
174 | default = {}
175 | description = <<-EOT
176 | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
177 | Neither the tag keys nor the tag values will be modified by this module.
178 | EOT
179 | }
180 |
181 | variable "additional_tag_map" {
182 | type = map(string)
183 | default = {}
184 | description = <<-EOT
185 | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
186 | This is for some rare cases where resources want additional configuration of tags
187 | and therefore take a list of maps with tag key, value, and additional configuration.
188 | EOT
189 | }
190 |
191 | variable "label_order" {
192 | type = list(string)
193 | default = null
194 | description = <<-EOT
195 | The order in which the labels (ID elements) appear in the `id`.
196 | Defaults to ["namespace", "environment", "stage", "name", "attributes"].
197 | You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present.
198 | EOT
199 | }
200 |
201 | variable "regex_replace_chars" {
202 | type = string
203 | default = null
204 | description = <<-EOT
205 | Terraform regular expression (regex) string.
206 | Characters matching the regex will be removed from the ID elements.
207 | If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits.
208 | EOT
209 | }
210 |
211 | variable "id_length_limit" {
212 | type = number
213 | default = null
214 | description = <<-EOT
215 | Limit `id` to this many characters (minimum 6).
216 | Set to `0` for unlimited length.
217 | Set to `null` for keep the existing setting, which defaults to `0`.
218 | Does not affect `id_full`.
219 | EOT
220 | validation {
221 | condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0
222 | error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length."
223 | }
224 | }
225 |
226 | variable "label_key_case" {
227 | type = string
228 | default = null
229 | description = <<-EOT
230 | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
231 | Does not affect keys of tags passed in via the `tags` input.
232 | Possible values: `lower`, `title`, `upper`.
233 | Default value: `title`.
234 | EOT
235 |
236 | validation {
237 | condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case)
238 | error_message = "Allowed values: `lower`, `title`, `upper`."
239 | }
240 | }
241 |
242 | variable "label_value_case" {
243 | type = string
244 | default = null
245 | description = <<-EOT
246 | Controls the letter case of ID elements (labels) as included in `id`,
247 | set as tag values, and output by this module individually.
248 | Does not affect values of tags passed in via the `tags` input.
249 | Possible values: `lower`, `title`, `upper` and `none` (no transformation).
250 | Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
251 | Default value: `lower`.
252 | EOT
253 |
254 | validation {
255 | condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case)
256 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`."
257 | }
258 | }
259 |
260 | variable "descriptor_formats" {
261 | type = any
262 | default = {}
263 | description = <<-EOT
264 | Describe additional descriptors to be output in the `descriptors` output map.
265 | Map of maps. Keys are names of descriptors. Values are maps of the form
266 | `{
267 | format = string
268 | labels = list(string)
269 | }`
270 | (Type is `any` so the map values can later be enhanced to provide additional options.)
271 | `format` is a Terraform format string to be passed to the `format()` function.
272 | `labels` is a list of labels, in order, to pass to `format()` function.
273 | Label values will be normalized before being passed to `format()` so they will be
274 | identical to how they appear in `id`.
275 | Default is `{}` (`descriptors` output will be empty).
276 | EOT
277 | }
278 |
279 | #### End of copy of cloudposse/terraform-null-label/variables.tf
280 |
--------------------------------------------------------------------------------
/docs/migration-v1-v2.md:
--------------------------------------------------------------------------------
1 | # Migration Notes for Security Group v2.0
2 |
3 | ## Key changes in v2.0
4 | - `create_before_destory` default changed from `false` to `true`
5 | - `preserve_security_group_id` added, defaults to `false`
6 | - Terraform version 1.0.0 or later required
7 |
8 | ## Migration Guide
9 |
10 | The defaults under v1 were the equivalent of the v2
11 | `create_before_destroy = false` and `preserve_security_group_id = true`.
12 | This combination is not allowed under v2 (`preserve_security_group_id` is ignored
13 | when `create_before_destory` is `false`), because it causes Terraform to fail
14 | by trying to create duplicate security group rules. Therefore, something must
15 | change. Asses your tolerance for change and choose one of the following options.
16 |
17 | Note: This migration guide is for the case where you are using this module,
18 | perhaps indirectly, as a component of a larger Terraform configuration that is
19 | all managed by a single Terraform state file. If you are using this module in
20 | some other way, you will need to extrapolate the instructions to fit your situation.
21 |
22 | ### Adjust your timeout
23 |
24 | At least during migration, you may want to shorten `security_group_delete_timeout`
25 | to something like 3 minutes. This is because there is a high likelihood that
26 | Terraform will want to delete the existing security group (and create a new one)
27 | before removing everything in the group. This will fail, and there is no point
28 | in waiting 15 minutes for it to fail.
29 |
30 | Alternately, you may want to use `terraform state mv` to move the existing
31 | `create_before_destroy = false` security group to the new
32 | `create_before_destroy = true` Terraform state address. Terraform will still
33 | want to delete the old security group because its name has changed,
34 | but it will create a new one first. You might want to lengthen the timeout
35 | so that you can manually move resources to the new security group and remove
36 | them from the old group so that the delete will succeed before it times out.
37 |
38 | ### Assess your situation
39 |
40 | Please read the [README](https://github.com/cloudposse/terraform-aws-security-group/#avoiding-service-interruptions) for this module,
41 | at least the section titled "Avoiding Service Interruptions", and determine your desired final configuration.
42 | For the purposes of migration, we are mainly concerned with the settings for `create_before_destroy` and `preserve_security_group_id`.
43 |
44 | Three key questions for you to answer:
45 |
46 | 1. Did you already set `create_before_destroy = true` in your configuration?
47 | 2. Do you need to preserve the security group ID?
48 | 3. Are there resources outside this Terraform plan that reference the security group?
49 | 1. Can you tolerate an interruption in network access to your resources?
50 |
51 | #### Did you already set `create_before_destroy = true` in your configuration?
52 |
53 | ##### Was `true`, staying `true` is the best case
54 | If you did, then migration will be a lot easier. If you are comfortable with
55 | the default `preserve_security_group_id` setting of `false`, then the
56 | upgrade will probably succeed without a service outage without need
57 | for any special action on your part.
58 |
59 | ##### Was `false`, staying `false` is discouraged
60 |
61 | If you did not previously set `create_before_destroy = true`, and want to
62 | preserve the previous default by now explicitly setting `create_before_destroy = false`,
63 | the security group rules will be deleted and recreated. This will cause a service
64 | interruption, as will any future change to the security group rules, because
65 | current rules will be deleted before new ones are created. Changes
66 | necessitating a new security group will cause longer service interruptions,
67 | because the security group will be deleted before the new one is created,
68 | and before it can be deleted it will be disassociated from all resources,
69 | leaving them without network access during the process.
70 |
71 | ##### Was `false`, switching to `true` is what most people are facing
72 |
73 | If you did not previously set `create_before_destroy = true`, and want
74 | to switch to that setting now (highly recommended), then the
75 | existing security group will be destroyed. (This is a requirement because
76 | security group names cannot be modified and must be unique, so
77 | in order to support `create_before_destroy` the name must include a generated suffix
78 | so that the new security group has a different name than the existing one.) Without
79 | some intervention on your part, Terraform will fail, because it will try to delete
80 | the existing security group before it has disassociated all the resources from it.
81 | There is no avoiding this, but you can mitigate the impact by running
82 | `terraform plan` to find the Terraform state addresses of the old and new
83 | security groups, and then use `terraform state mv` to move the old security group
84 | to the new address. This will cause Terraform to create the new security group
85 | before deleting the old one. You can then manually move resources to the new
86 | security group and remove them from the old one, so that the delete will succeed.
87 |
88 |
89 |
90 | #### Do you need to preserve the security group ID?
91 |
92 | If the security group ID is referenced by resources (such as security group rules
93 | in other security groups) outside this Terraform plan, then you want to
94 | preserve the security group ID where possible. In that case, you should set
95 |
96 | ```hcl
97 | create_before_destroy = true
98 | preserve_security_group_id = true
99 | ```
100 |
101 | Setting `preserve_security_group_id` to `true` will cause a service
102 | interruption, as will any future change to the security group rules, because
103 | current rules will be deleted before new ones are created.
104 | This is a limitation of the AWS provider: it is not smart enough to
105 | know to leave in place (rather than delete and recreate) security group
106 | rules, and attempts to create a duplicate security group rule will fail,
107 | so existing rules are deleted and then new ones are created.
108 |
109 |
110 | #### Use the default configuration if you can
111 |
112 | If:
113 |
114 | 1. The security group ID is **_NOT_** referenced by resources (such as security group rules
115 | in other security groups) outside this Terraform plan, _and_
116 | 2. the resources associated with the security group allow the associations to be changed without requiring
117 | the resources themselves to be destroyed and recreated
118 | 3. you can tolerate an interruption in network access to your resources one time during the upgrade process
119 |
120 | Then we recommend explicitly configuring this module with its defaults:
121 |
122 | ```hcl
123 | create_before_destroy = true
124 | preserve_security_group_id = false
125 | ```
126 |
--------------------------------------------------------------------------------
/examples/complete/context.tf:
--------------------------------------------------------------------------------
1 | #
2 | # ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label
3 | # All other instances of this file should be a copy of that one
4 | #
5 | #
6 | # Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf
7 | # and then place it in your Terraform module to automatically get
8 | # Cloud Posse's standard configuration inputs suitable for passing
9 | # to Cloud Posse modules.
10 | #
11 | # curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf
12 | #
13 | # Modules should access the whole context as `module.this.context`
14 | # to get the input variables with nulls for defaults,
15 | # for example `context = module.this.context`,
16 | # and access individual variables as `module.this.`,
17 | # with final values filled in.
18 | #
19 | # For example, when using defaults, `module.this.context.delimiter`
20 | # will be null, and `module.this.delimiter` will be `-` (hyphen).
21 | #
22 |
23 | module "this" {
24 | source = "cloudposse/label/null"
25 | version = "0.25.0" # requires Terraform >= 0.13.0
26 |
27 | enabled = var.enabled
28 | namespace = var.namespace
29 | tenant = var.tenant
30 | environment = var.environment
31 | stage = var.stage
32 | name = var.name
33 | delimiter = var.delimiter
34 | attributes = var.attributes
35 | tags = var.tags
36 | additional_tag_map = var.additional_tag_map
37 | label_order = var.label_order
38 | regex_replace_chars = var.regex_replace_chars
39 | id_length_limit = var.id_length_limit
40 | label_key_case = var.label_key_case
41 | label_value_case = var.label_value_case
42 | descriptor_formats = var.descriptor_formats
43 | labels_as_tags = var.labels_as_tags
44 |
45 | context = var.context
46 | }
47 |
48 | # Copy contents of cloudposse/terraform-null-label/variables.tf here
49 |
50 | variable "context" {
51 | type = any
52 | default = {
53 | enabled = true
54 | namespace = null
55 | tenant = null
56 | environment = null
57 | stage = null
58 | name = null
59 | delimiter = null
60 | attributes = []
61 | tags = {}
62 | additional_tag_map = {}
63 | regex_replace_chars = null
64 | label_order = []
65 | id_length_limit = null
66 | label_key_case = null
67 | label_value_case = null
68 | descriptor_formats = {}
69 | # Note: we have to use [] instead of null for unset lists due to
70 | # https://github.com/hashicorp/terraform/issues/28137
71 | # which was not fixed until Terraform 1.0.0,
72 | # but we want the default to be all the labels in `label_order`
73 | # and we want users to be able to prevent all tag generation
74 | # by setting `labels_as_tags` to `[]`, so we need
75 | # a different sentinel to indicate "default"
76 | labels_as_tags = ["unset"]
77 | }
78 | description = <<-EOT
79 | Single object for setting entire context at once.
80 | See description of individual variables for details.
81 | Leave string and numeric variables as `null` to use default value.
82 | Individual variable settings (non-null) override settings in context object,
83 | except for attributes, tags, and additional_tag_map, which are merged.
84 | EOT
85 |
86 | validation {
87 | condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"])
88 | error_message = "Allowed values: `lower`, `title`, `upper`."
89 | }
90 |
91 | validation {
92 | condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"])
93 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`."
94 | }
95 | }
96 |
97 | variable "enabled" {
98 | type = bool
99 | default = null
100 | description = "Set to false to prevent the module from creating any resources"
101 | }
102 |
103 | variable "namespace" {
104 | type = string
105 | default = null
106 | description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique"
107 | }
108 |
109 | variable "tenant" {
110 | type = string
111 | default = null
112 | description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for"
113 | }
114 |
115 | variable "environment" {
116 | type = string
117 | default = null
118 | description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'"
119 | }
120 |
121 | variable "stage" {
122 | type = string
123 | default = null
124 | description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'"
125 | }
126 |
127 | variable "name" {
128 | type = string
129 | default = null
130 | description = <<-EOT
131 | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
132 | This is the only ID element not also included as a `tag`.
133 | The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input.
134 | EOT
135 | }
136 |
137 | variable "delimiter" {
138 | type = string
139 | default = null
140 | description = <<-EOT
141 | Delimiter to be used between ID elements.
142 | Defaults to `-` (hyphen). Set to `""` to use no delimiter at all.
143 | EOT
144 | }
145 |
146 | variable "attributes" {
147 | type = list(string)
148 | default = []
149 | description = <<-EOT
150 | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
151 | in the order they appear in the list. New attributes are appended to the
152 | end of the list. The elements of the list are joined by the `delimiter`
153 | and treated as a single ID element.
154 | EOT
155 | }
156 |
157 | variable "labels_as_tags" {
158 | type = set(string)
159 | default = ["default"]
160 | description = <<-EOT
161 | Set of labels (ID elements) to include as tags in the `tags` output.
162 | Default is to include all labels.
163 | Tags with empty values will not be included in the `tags` output.
164 | Set to `[]` to suppress all generated tags.
165 | **Notes:**
166 | The value of the `name` tag, if included, will be the `id`, not the `name`.
167 | Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
168 | changed in later chained modules. Attempts to change it will be silently ignored.
169 | EOT
170 | }
171 |
172 | variable "tags" {
173 | type = map(string)
174 | default = {}
175 | description = <<-EOT
176 | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
177 | Neither the tag keys nor the tag values will be modified by this module.
178 | EOT
179 | }
180 |
181 | variable "additional_tag_map" {
182 | type = map(string)
183 | default = {}
184 | description = <<-EOT
185 | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
186 | This is for some rare cases where resources want additional configuration of tags
187 | and therefore take a list of maps with tag key, value, and additional configuration.
188 | EOT
189 | }
190 |
191 | variable "label_order" {
192 | type = list(string)
193 | default = null
194 | description = <<-EOT
195 | The order in which the labels (ID elements) appear in the `id`.
196 | Defaults to ["namespace", "environment", "stage", "name", "attributes"].
197 | You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present.
198 | EOT
199 | }
200 |
201 | variable "regex_replace_chars" {
202 | type = string
203 | default = null
204 | description = <<-EOT
205 | Terraform regular expression (regex) string.
206 | Characters matching the regex will be removed from the ID elements.
207 | If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits.
208 | EOT
209 | }
210 |
211 | variable "id_length_limit" {
212 | type = number
213 | default = null
214 | description = <<-EOT
215 | Limit `id` to this many characters (minimum 6).
216 | Set to `0` for unlimited length.
217 | Set to `null` for keep the existing setting, which defaults to `0`.
218 | Does not affect `id_full`.
219 | EOT
220 | validation {
221 | condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0
222 | error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length."
223 | }
224 | }
225 |
226 | variable "label_key_case" {
227 | type = string
228 | default = null
229 | description = <<-EOT
230 | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
231 | Does not affect keys of tags passed in via the `tags` input.
232 | Possible values: `lower`, `title`, `upper`.
233 | Default value: `title`.
234 | EOT
235 |
236 | validation {
237 | condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case)
238 | error_message = "Allowed values: `lower`, `title`, `upper`."
239 | }
240 | }
241 |
242 | variable "label_value_case" {
243 | type = string
244 | default = null
245 | description = <<-EOT
246 | Controls the letter case of ID elements (labels) as included in `id`,
247 | set as tag values, and output by this module individually.
248 | Does not affect values of tags passed in via the `tags` input.
249 | Possible values: `lower`, `title`, `upper` and `none` (no transformation).
250 | Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
251 | Default value: `lower`.
252 | EOT
253 |
254 | validation {
255 | condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case)
256 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`."
257 | }
258 | }
259 |
260 | variable "descriptor_formats" {
261 | type = any
262 | default = {}
263 | description = <<-EOT
264 | Describe additional descriptors to be output in the `descriptors` output map.
265 | Map of maps. Keys are names of descriptors. Values are maps of the form
266 | `{
267 | format = string
268 | labels = list(string)
269 | }`
270 | (Type is `any` so the map values can later be enhanced to provide additional options.)
271 | `format` is a Terraform format string to be passed to the `format()` function.
272 | `labels` is a list of labels, in order, to pass to `format()` function.
273 | Label values will be normalized before being passed to `format()` so they will be
274 | identical to how they appear in `id`.
275 | Default is `{}` (`descriptors` output will be empty).
276 | EOT
277 | }
278 |
279 | #### End of copy of cloudposse/terraform-null-label/variables.tf
280 |
--------------------------------------------------------------------------------
/examples/complete/fixtures.us-east-2.tfvars:
--------------------------------------------------------------------------------
1 | region = "us-east-2"
2 |
3 | namespace = "eg"
4 |
5 | environment = "ue2"
6 |
7 | stage = "test"
8 |
9 | name = "sg"
10 |
11 | rules = [
12 | {
13 | key = null # "ssh all"
14 | type = "ingress"
15 | from_port = 22
16 | to_port = 22
17 | protocol = "tcp"
18 | cidr_blocks = ["0.0.0.0/0"]
19 | description = "SSH wide open"
20 | },
21 | {
22 | key = "telnet all"
23 | type = "ingress"
24 | from_port = 23
25 | to_port = 23
26 | protocol = "tcp"
27 | cidr_blocks = ["0.0.0.0/0"]
28 | description = "Telnet wide open"
29 | }
30 | ]
31 |
--------------------------------------------------------------------------------
/examples/complete/main.tf:
--------------------------------------------------------------------------------
1 | # Terraform for testing with terratest
2 | #
3 | # For this module, a large portion of the test is simply
4 | # verifying that Terraform can generate a plan without errors.
5 | #
6 |
7 | provider "aws" {
8 | region = var.region
9 | }
10 |
11 | module "vpc" {
12 | source = "cloudposse/vpc/aws"
13 | version = "v2.0.0"
14 |
15 | ipv4_primary_cidr_block = "10.0.0.0/24"
16 |
17 | assign_generated_ipv6_cidr_block = true
18 |
19 | context = module.this.context
20 | }
21 |
22 | resource "random_integer" "coin" {
23 | count = local.enabled ? 1 : 0
24 | max = 2
25 | min = 1
26 | }
27 |
28 | locals {
29 | enabled = module.this.enabled
30 | coin = local.enabled ? random_integer.coin[0].result : 0
31 | }
32 |
33 | module "simple_security_group" {
34 | source = "../.."
35 |
36 | attributes = ["simple"]
37 | rules = var.rules
38 |
39 | vpc_id = module.vpc.vpc_id
40 |
41 | context = module.this.context
42 | }
43 |
44 | # Create a new security group
45 |
46 | module "new_security_group" {
47 | source = "../.."
48 |
49 | allow_all_egress = true
50 | inline_rules_enabled = var.inline_rules_enabled
51 |
52 | rule_matrix = [{
53 | key = "stable"
54 | # Allow ingress on ports 22 and 80 from created security group, existing security group, and CIDR "10.0.0.0/8"
55 | # The dynamic value for source_security_group_ids breaks Terraform 0.13 but should work in 0.14 or later
56 | source_security_group_ids = local.enabled ? [aws_security_group.target[0].id] : ["disabled"]
57 | # Either dynamic value for CIDRs breaks Terraform 0.13 but should work in 0.14 or later
58 | # In TF 0.14 and later (through 1.0.x) if the length of the cidr_blocks
59 | # list is not available at plan time, the module breaks.
60 | cidr_blocks = local.coin > 1 ? ["10.0.0.0/16"] : ["10.0.0.0/24"]
61 | ipv6_cidr_blocks = [module.vpc.vpc_ipv6_cidr_block]
62 | prefix_list_ids = []
63 |
64 | # Making `self` derived should break `count`, as it legitimately makes
65 | # the count impossible to predict
66 | # self = random_integer.coin.result > 0
67 | self = var.rule_matrix_self
68 | rules = [
69 | {
70 | key = "ssh"
71 | type = "ingress"
72 | from_port = 22
73 | to_port = 22
74 | protocol = "tcp"
75 | description = "Allow SSH access"
76 | },
77 | {
78 | # key = "http"
79 | type = "ingress"
80 | from_port = 80
81 | to_port = 80
82 | protocol = "tcp"
83 | description = "Allow HTTP access"
84 | },
85 | ]
86 | }]
87 |
88 | rules = var.rules
89 | rules_map = merge({ new-cidr = [
90 | {
91 | key = "https-cidr"
92 | type = "ingress"
93 | from_port = 443
94 | to_port = 443
95 | protocol = "tcp"
96 | cidr_blocks = ["10.0.0.0/8"]
97 | ipv6_cidr_blocks = [module.vpc.vpc_ipv6_cidr_block] # ["::/0"] #
98 | source_security_group_id = null
99 | description = "Discrete HTTPS ingress by CIDR"
100 | self = false
101 | }] }, {
102 | new-sg = [{
103 | # no key provided
104 | type = "ingress"
105 | from_port = 443
106 | to_port = 443
107 | protocol = "tcp"
108 | source_security_group_id = local.enabled ? aws_security_group.target[0].id : "disabled"
109 | description = "Discrete HTTPS ingress for special SG"
110 | self = null
111 | }],
112 | })
113 |
114 |
115 | vpc_id = module.vpc.vpc_id
116 |
117 | security_group_create_timeout = "5m"
118 | security_group_delete_timeout = "2m"
119 |
120 | security_group_name = [format("%s-%s", module.this.id, "new-")]
121 |
122 | context = module.this.context
123 | }
124 |
125 |
126 | # Create rules for pre-created security group
127 |
128 | resource "aws_security_group" "target" {
129 | #bridgecrew:skip=BC_AWS_NETWORKING_31:Not needed for testing
130 | #bridgecrew:skip=BC_AWS_NETWORKING_51:Not needed for testing
131 | count = local.enabled ? 1 : 0
132 |
133 | name_prefix = format("%s-%s-", module.this.id, "existing")
134 | vpc_id = module.vpc.vpc_id
135 | tags = module.this.tags
136 | }
137 |
138 | module "target_security_group" {
139 | source = "../.."
140 |
141 | allow_all_egress = true
142 | # create_security_group = false
143 | target_security_group_id = local.enabled ? [aws_security_group.target[0].id] : ["disabled"]
144 | rules = var.rules
145 |
146 | security_group_name = local.enabled ? [aws_security_group.target[0].name_prefix] : ["disabled"]
147 | vpc_id = module.vpc.vpc_id
148 |
149 | context = module.this.context
150 | }
151 |
--------------------------------------------------------------------------------
/examples/complete/outputs.tf:
--------------------------------------------------------------------------------
1 | output "created_sg_id" {
2 | description = "The ID of the created Security Group"
3 | value = module.new_security_group.id
4 | }
5 |
6 | output "created_sg_arn" {
7 | description = "The ARN of the created Security Group"
8 | value = module.new_security_group.arn
9 | }
10 |
11 | output "created_sg_name" {
12 | description = "The name of the created Security Group"
13 | value = module.new_security_group.name
14 | }
15 |
16 | output "test_created_sg_id" {
17 | description = "The security group created by the test to use as \"target\" security group"
18 | value = local.enabled ? aws_security_group.target[0].id : null
19 | }
20 |
21 | output "target_sg_id" {
22 | description = "The target Security Group ID"
23 | value = module.target_security_group.id
24 | }
25 |
26 | output "target_sg_arn" {
27 | description = "The target Security Group ARN"
28 | value = module.target_security_group.arn
29 | }
30 |
31 | output "target_sg_name" {
32 | description = "The target Security Group name"
33 | value = module.target_security_group.name
34 | }
35 |
36 | output "rules_terraform_ids" {
37 | description = "List of Terraform IDs of created `security_group_rule` resources"
38 | value = module.new_security_group.rules_terraform_ids
39 | }
40 |
--------------------------------------------------------------------------------
/examples/complete/variables.tf:
--------------------------------------------------------------------------------
1 | variable "region" {
2 | type = string
3 | }
4 |
5 | variable "rules" {
6 | type = any
7 | description = "List of security group rules to apply to the created security group"
8 | }
9 |
10 | variable "rule_matrix_self" {
11 | type = bool
12 | description = "Value to set `self` in `rule_matrix` test rule"
13 | default = null
14 | }
15 |
16 | variable "inline_rules_enabled" {
17 | type = bool
18 | description = "Flag to enable/disable inline security group rules"
19 | default = false
20 | }
21 |
--------------------------------------------------------------------------------
/examples/complete/versions.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_version = ">= 1.0.0"
3 |
4 | required_providers {
5 | aws = {
6 | source = "hashicorp/aws"
7 | version = ">= 3.0"
8 | }
9 | random = {
10 | source = "hashicorp/random"
11 | version = ">= 3.0"
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/exports/security-group-variables.tf:
--------------------------------------------------------------------------------
1 | # security-group-variables Version: 3
2 | #
3 | # Copy this file from https://github.com/cloudposse/terraform-aws-security-group/blob/master/exports/security-group-variables.tf
4 | # and EDIT IT TO SUIT YOUR PROJECT. Update the version number above if you update this file from a later version.
5 | # Unlike null-label context.tf, this file cannot be automatically updated
6 | # because of the tight integration with the module using it.
7 | ##
8 | # Delete this top comment block, except for the first line (version number),
9 | # REMOVE COMMENTS below that are intended for the initial implementor and not maintainers or end users.
10 | #
11 | # This file provides the standard inputs that all Cloud Posse Open Source
12 | # Terraform module that create AWS Security Groups should implement.
13 | # This file does NOT provide implementation of the inputs, as that
14 | # of course varies with each module.
15 | #
16 | # This file declares some standard outputs modules should create,
17 | # but the declarations should be moved to `outputs.tf` and of course
18 | # may need to be modified based on the module's use of security-group.
19 | #
20 |
21 |
22 | variable "create_security_group" {
23 | type = bool
24 | description = "Set `true` to create and configure a new security group. If false, `associated_security_group_ids` must be provided."
25 | default = true
26 | }
27 |
28 | variable "associated_security_group_ids" {
29 | type = list(string)
30 | description = <<-EOT
31 | A list of IDs of Security Groups to associate the created resource with, in addition to the created security group.
32 | These security groups will not be modified and, if `create_security_group` is `false`, must have rules providing the desired access.
33 | EOT
34 | default = []
35 | }
36 |
37 | ##
38 | ## allowed_* inputs are optional, because the same thing can be accomplished by
39 | ## providing `additional_security_group_rules`. However, if the rules this
40 | ## module creates are non-trivial (for example, opening ports based on
41 | ## feature settings, see https://github.com/cloudposse/terraform-aws-msk-apache-kafka-cluster/blob/3fe23c402cc420799ae721186812482335f78d24/main.tf#L14-L53 )
42 | ## then it makes sense to include these.
43 | ## Reasons not to include some or all of these inputs include
44 | ## - too hard to implement
45 | ## - does not make sense (particularly the IPv6 inputs if the underlying resource does not yet support IPv6)
46 | ## - likely to confuse users
47 | ## - likely to invite count/for_each issues
48 | variable "allowed_security_group_ids" {
49 | type = list(string)
50 | description = <<-EOT
51 | A list of IDs of Security Groups to allow access to the security group created by this module.
52 | The length of this list must be known at "plan" time.
53 | EOT
54 | default = []
55 | }
56 |
57 | variable "allowed_cidr_blocks" {
58 | type = list(string)
59 | description = <<-EOT
60 | A list of IPv4 CIDRs to allow access to the security group created by this module.
61 | The length of this list must be known at "plan" time.
62 | EOT
63 | default = []
64 | }
65 |
66 | variable "allowed_ipv6_cidr_blocks" {
67 | type = list(string)
68 | description = <<-EOT
69 | A list of IPv6 CIDRs to allow access to the security group created by this module.
70 | The length of this list must be known at "plan" time.
71 | EOT
72 | default = []
73 | }
74 |
75 | variable "allowed_ipv6_prefix_list_ids" {
76 | type = list(string)
77 | description = <<-EOT
78 | A list of IPv6 Prefix Lists IDs to allow access to the security group created by this module.
79 | The length of this list must be known at "plan" time.
80 | EOT
81 | default = []
82 | }
83 | ## End of optional allowed_* ###########
84 |
85 | variable "security_group_name" {
86 | type = list(string)
87 | description = <<-EOT
88 | The name to assign to the created security group. Must be unique within the VPC.
89 | If not provided, will be derived from the `null-label.context` passed in.
90 | If `create_before_destroy` is true, will be used as a name prefix.
91 | EOT
92 | default = []
93 | }
94 |
95 | variable "security_group_description" {
96 | type = string
97 | description = <<-EOT
98 | The description to assign to the created Security Group.
99 | Warning: Changing the description causes the security group to be replaced.
100 | EOT
101 | default = "Managed by Terraform"
102 | }
103 |
104 | variable "security_group_create_before_destroy" {
105 | type = bool
106 | description = <<-EOT
107 | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
108 | We only recommend setting this `false` if you are importing an existing security group
109 | that you do not want replaced and therefore need full control over its name.
110 | Note that changing this value will always cause the security group to be replaced.
111 | EOT
112 | default = true
113 | }
114 |
115 | variable "preserve_security_group_id" {
116 | type = bool
117 | description = <<-EOT
118 | When `false` and `security_group_create_before_destroy` is `true`, changes to security group rules
119 | cause a new security group to be created with the new rules, and the existing security group is then
120 | replaced with the new one, eliminating any service interruption.
121 | When `true` or when changing the value (from `false` to `true` or from `true` to `false`),
122 | existing security group rules will be deleted before new ones are created, resulting in a service interruption,
123 | but preserving the security group itself.
124 | **NOTE:** Setting this to `true` does not guarantee the security group will never be replaced,
125 | it only keeps changes to the security group rules from triggering a replacement.
126 | See the [terraform-aws-security-group README](https://github.com/cloudposse/terraform-aws-security-group) for further discussion.
127 | EOT
128 | default = false
129 | }
130 |
131 | variable "security_group_create_timeout" {
132 | type = string
133 | description = "How long to wait for the security group to be created."
134 | default = "10m"
135 | }
136 |
137 | variable "security_group_delete_timeout" {
138 | type = string
139 | description = <<-EOT
140 | How long to retry on `DependencyViolation` errors during security group deletion from
141 | lingering ENIs left by certain AWS services such as Elastic Load Balancing.
142 | EOT
143 | default = "15m"
144 | }
145 |
146 | variable "allow_all_egress" {
147 | type = bool
148 | description = <<-EOT
149 | If `true`, the created security group will allow egress on all ports and protocols to all IP addresses.
150 | If this is false and no egress rules are otherwise specified, then no egress will be allowed.
151 | EOT
152 | default = true
153 | }
154 |
155 | variable "additional_security_group_rules" {
156 | type = list(any)
157 | description = <<-EOT
158 | A list of Security Group rule objects to add to the created security group, in addition to the ones
159 | this module normally creates. (To suppress the module's rules, set `create_security_group` to false
160 | and supply your own security group(s) via `associated_security_group_ids`.)
161 | The keys and values of the objects are fully compatible with the `aws_security_group_rule` resource, except
162 | for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique and known at "plan" time.
163 | For more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule
164 | and https://github.com/cloudposse/terraform-aws-security-group.
165 | EOT
166 | default = []
167 | }
168 |
169 | #### We do not expose an `additional_security_group_rule_matrix` input for a few reasons:
170 | # - It is a convenience and ultimately provides no rules that cannot be provided via `additional_security_group_rules`
171 | # - It is complicated and can, in some situations, create problems for Terraform `for_each`
172 | # - It is difficult to document and easy to make mistakes using it
173 |
174 |
175 | ## vpc_id is required, but if needed for reasons other than the security group,
176 | ## it should be defined in the main `variables.tf` file, not here.
177 | variable "vpc_id" {
178 | type = string
179 | description = "The ID of the VPC where the Security Group will be created."
180 | }
181 |
182 |
183 | #
184 | #
185 | #### The variables below (but not the outputs) can be omitted if not needed, and may need their descriptions modified
186 | #
187 | #
188 |
189 | #############################################################################################
190 | ## Special note about inline_rules_enabled and revoke_rules_on_delete
191 | ##
192 | ## The security-group inputs inline_rules_enabled and revoke_rules_on_delete should not
193 | ## be exposed in other modules unless there is a strong reason for them to be used.
194 | ## We discourage the use of inline_rules_enabled and we rarely need or want
195 | ## revoke_rules_on_delete, so we do not want to clutter our interface with those inputs.
196 | ##
197 | ## If someone wants to enable either of those options, they have the option
198 | ## of creating a security group configured as they like
199 | ## and passing it in as the target security group.
200 | #############################################################################################
201 |
202 | variable "inline_rules_enabled" {
203 | type = bool
204 | description = <<-EOT
205 | NOT RECOMMENDED. Create rules "inline" instead of as separate `aws_security_group_rule` resources.
206 | See [#20046](https://github.com/hashicorp/terraform-provider-aws/issues/20046) for one of several issues with inline rules.
207 | 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.
208 | EOT
209 | default = false
210 | }
211 |
212 | variable "revoke_security_group_rules_on_delete" {
213 | type = bool
214 | description = <<-EOT
215 | Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting
216 | the security group itself. This is normally not needed.
217 | EOT
218 | default = false
219 | }
220 |
221 |
222 | ##
223 | ##
224 | ################# Outputs
225 | ##
226 | ## Move to `outputs.tf`
227 | ##
228 | ##
229 |
230 | output "security_group_id" {
231 | value = join("", module.security_group.*.id)
232 | description = "The ID of the created security group"
233 | }
234 |
235 | output "security_group_arn" {
236 | value = join("", module.security_group.*.arn)
237 | description = "The ARN of the created security group"
238 | }
239 |
240 | output "security_group_name" {
241 | value = join("", module.security_group.*.name)
242 | description = "The name of the created security group"
243 | }
244 |
245 |
--------------------------------------------------------------------------------
/main.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | enabled = module.this.enabled
3 | inline = var.inline_rules_enabled
4 |
5 | allow_all_egress = local.enabled && var.allow_all_egress
6 |
7 | default_rule_description = "Managed by Terraform"
8 |
9 | create_security_group = local.enabled && length(var.target_security_group_id) == 0
10 | sg_create_before_destroy = var.create_before_destroy
11 | # If the security group is not being created by this module, we need to treat it as
12 | # needing to be preserved, because we cannot replace it here.
13 | preserve_security_group_id = var.preserve_security_group_id || length(var.target_security_group_id) > 0
14 |
15 | created_security_group = local.create_security_group ? (
16 | local.sg_create_before_destroy ? aws_security_group.cbd[0] : aws_security_group.default[0]
17 | ) : null
18 |
19 | target_security_group_id = try(var.target_security_group_id[0], "")
20 |
21 | # This clever construction makes `security_group_id` the ID of either the Target security group (SG) supplied,
22 | # or the 1 of the 2 flavors we create: the "create before destroy (CBD)" (`create_before_destroy = true`) SG
23 | # or the "destroy before create (DBC)" (`create_before_destroy = false`) SG. Unfortunately, the way it is constructed,
24 | # Terraform considers `local.security_group_id` dependent on the DBC SG, which means that
25 | # when it is referenced by the CBD security group rules, Terraform forces
26 | # unwanted CBD behavior on the DBC SG, so we can only use it for the DBC SG rules.
27 | security_group_id = local.enabled ? (
28 | # Use coalesce() here to hack an error message into the output
29 | local.create_security_group ? local.created_security_group.id : coalesce(local.target_security_group_id,
30 | "var.target_security_group_id contains an empty value. Omit any value if you want this module to create a security group.")
31 | ) : null
32 |
33 | # Setting `create_before_destroy` on the security group rules forces `create_before_destroy` behavior
34 | # on the security group, so we have to disable it on the rules if disabled on the security group.
35 | # It also forces a new security group to be created whenever any rule changes, so we disable it
36 | # when `local.preserve_security_group_id` is `true`. In the case where this Terraform module
37 | # did not create the security group, Terraform cannot replace the security group, and
38 | # `create_before_destroy` on the rules would fail due to duplicate rules being created, so again we must not allow it.
39 | rule_create_before_destroy = local.sg_create_before_destroy && !local.preserve_security_group_id
40 | # We also have to make it clear to Terraform that the "create before destroy" (CBD) rules
41 | # will never reference the "destroy before create" (DBC) security group (SG)
42 | # by keeping any conditional reference to the DBC SG out of the expression (unlike the `security_group_id` expression above).
43 | cbd_security_group_id = local.create_security_group ? one(aws_security_group.cbd[*].id) : local.target_security_group_id
44 |
45 | # The only way to guarantee success when creating new rules before destroying old ones
46 | # is to make the new rules part of a new security group.
47 | # See https://github.com/cloudposse/terraform-aws-security-group/issues/34
48 | rule_change_forces_new_security_group = local.enabled && local.rule_create_before_destroy
49 | }
50 |
51 | # We force a new security group by changing its name, using `random_id` to generate a part of the name prefix
52 | resource "random_id" "rule_change_forces_new_security_group" {
53 | count = local.rule_change_forces_new_security_group ? 1 : 0
54 | byte_length = 3
55 | keepers = {
56 | rules = jsonencode(local.keyed_resource_rules)
57 | }
58 | }
59 |
60 | # You cannot toggle `create_before_destroy` based on input,
61 | # you have to have a completely separate resource to change it.
62 | resource "aws_security_group" "default" {
63 | # Because we have 2 almost identical alternatives, use x == false and x == true rather than x and !x
64 | count = local.create_security_group && local.sg_create_before_destroy == false ? 1 : 0
65 |
66 | name = concat(var.security_group_name, [module.this.id])[0]
67 | lifecycle {
68 | create_before_destroy = false
69 | }
70 |
71 | ########################################################################
72 | ## Everything from here to the end of this resource should be identical
73 | ## (copy and paste) in aws_security_group.default and aws_security_group.cbd
74 |
75 | description = var.security_group_description
76 | vpc_id = var.vpc_id
77 | tags = merge(module.this.tags, try(length(var.security_group_name[0]), 0) > 0 ? { Name = var.security_group_name[0] } : {})
78 |
79 | revoke_rules_on_delete = var.revoke_rules_on_delete
80 |
81 | dynamic "ingress" {
82 | for_each = local.all_ingress_rules
83 | content {
84 | from_port = ingress.value.from_port
85 | to_port = ingress.value.to_port
86 | protocol = ingress.value.protocol
87 | description = ingress.value.description
88 | cidr_blocks = ingress.value.cidr_blocks
89 | ipv6_cidr_blocks = ingress.value.ipv6_cidr_blocks
90 | prefix_list_ids = ingress.value.prefix_list_ids
91 | security_groups = ingress.value.security_groups
92 | self = ingress.value.self
93 | }
94 | }
95 |
96 | dynamic "egress" {
97 | for_each = local.all_egress_rules
98 | content {
99 | from_port = egress.value.from_port
100 | to_port = egress.value.to_port
101 | protocol = egress.value.protocol
102 | description = egress.value.description
103 | cidr_blocks = egress.value.cidr_blocks
104 | ipv6_cidr_blocks = egress.value.ipv6_cidr_blocks
105 | prefix_list_ids = egress.value.prefix_list_ids
106 | security_groups = egress.value.security_groups
107 | self = egress.value.self
108 | }
109 | }
110 |
111 | timeouts {
112 | create = var.security_group_create_timeout
113 | delete = var.security_group_delete_timeout
114 | }
115 |
116 | ##
117 | ## end of duplicate block
118 | ########################################################################
119 |
120 | }
121 |
122 | locals {
123 | sg_name_prefix_base = concat(var.security_group_name, ["${module.this.id}${module.this.delimiter}"])[0]
124 | # Force a new security group to be created by changing its name prefix, using `random_id` to create a short ID string
125 | # that changes when the rules change, and adding that to the configured name prefix.
126 | sg_name_prefix_forced = "${local.sg_name_prefix_base}${module.this.delimiter}${join("", random_id.rule_change_forces_new_security_group[*].b64_url)}${module.this.delimiter}"
127 | sg_name_prefix = local.rule_change_forces_new_security_group ? local.sg_name_prefix_forced : local.sg_name_prefix_base
128 | }
129 |
130 |
131 | resource "aws_security_group" "cbd" {
132 | # Because we have 2 almost identical alternatives, use x == false and x == true rather than x and !x
133 | count = local.create_security_group && local.sg_create_before_destroy == true ? 1 : 0
134 |
135 | name_prefix = local.sg_name_prefix
136 | lifecycle {
137 | create_before_destroy = true
138 | }
139 |
140 | ########################################################################
141 | ## Everything from here to the end of this resource should be identical
142 | ## (copy and paste) in aws_security_group.default and aws_security_group.cbd
143 |
144 | description = var.security_group_description
145 | vpc_id = var.vpc_id
146 | tags = merge(module.this.tags, try(length(var.security_group_name[0]), 0) > 0 ? { Name = var.security_group_name[0] } : {})
147 |
148 | revoke_rules_on_delete = var.revoke_rules_on_delete
149 |
150 | dynamic "ingress" {
151 | for_each = local.all_ingress_rules
152 | content {
153 | from_port = ingress.value.from_port
154 | to_port = ingress.value.to_port
155 | protocol = ingress.value.protocol
156 | description = ingress.value.description
157 | cidr_blocks = ingress.value.cidr_blocks
158 | ipv6_cidr_blocks = ingress.value.ipv6_cidr_blocks
159 | prefix_list_ids = ingress.value.prefix_list_ids
160 | security_groups = ingress.value.security_groups
161 | self = ingress.value.self
162 | }
163 | }
164 |
165 | dynamic "egress" {
166 | for_each = local.all_egress_rules
167 | content {
168 | from_port = egress.value.from_port
169 | to_port = egress.value.to_port
170 | protocol = egress.value.protocol
171 | description = egress.value.description
172 | cidr_blocks = egress.value.cidr_blocks
173 | ipv6_cidr_blocks = egress.value.ipv6_cidr_blocks
174 | prefix_list_ids = egress.value.prefix_list_ids
175 | security_groups = egress.value.security_groups
176 | self = egress.value.self
177 | }
178 | }
179 |
180 | timeouts {
181 | create = var.security_group_create_timeout
182 | delete = var.security_group_delete_timeout
183 | }
184 |
185 | ##
186 | ## end of duplicate block
187 | ########################################################################
188 |
189 | }
190 |
191 | # We would like to always have `create_before_destroy` for security group rules,
192 | # but duplicates are not allowed so `create_before_destroy` has a high probability of failing.
193 | # See https://github.com/hashicorp/terraform-provider-aws/issues/25173 and its References.
194 | # You cannot toggle `create_before_destroy` based on input,
195 | # you have to have a completely separate resource to change it.
196 | resource "aws_security_group_rule" "keyed" {
197 | for_each = local.rule_create_before_destroy ? local.keyed_resource_rules : {}
198 |
199 | lifecycle {
200 | create_before_destroy = true
201 | }
202 |
203 | ########################################################################
204 | ## Everything from here to the end of this resource should be identical
205 | ## (copy and paste) in aws_security_group_rule.keyed and aws_security_group.dbc
206 |
207 |
208 | security_group_id = local.cbd_security_group_id
209 |
210 | type = each.value.type
211 | from_port = each.value.from_port
212 | to_port = each.value.to_port
213 | protocol = each.value.protocol
214 | description = each.value.description
215 |
216 | cidr_blocks = length(each.value.cidr_blocks) == 0 ? null : each.value.cidr_blocks
217 | ipv6_cidr_blocks = length(each.value.ipv6_cidr_blocks) == 0 ? null : each.value.ipv6_cidr_blocks
218 | prefix_list_ids = length(each.value.prefix_list_ids) == 0 ? [] : each.value.prefix_list_ids
219 | self = each.value.self
220 | source_security_group_id = each.value.source_security_group_id
221 |
222 | ##
223 | ## end of duplicate block
224 | ########################################################################
225 |
226 | }
227 |
228 | resource "aws_security_group_rule" "dbc" {
229 | for_each = local.rule_create_before_destroy ? {} : local.keyed_resource_rules
230 |
231 | lifecycle {
232 | # This has no actual effect, it is just here for emphasis
233 | create_before_destroy = false
234 | }
235 | ########################################################################
236 | ## Everything from here to the end of this resource should be identical
237 | ## (copy and paste) in aws_security_group.default and aws_security_group.cbd
238 |
239 |
240 | security_group_id = local.security_group_id
241 |
242 | type = each.value.type
243 | from_port = each.value.from_port
244 | to_port = each.value.to_port
245 | protocol = each.value.protocol
246 | description = each.value.description
247 |
248 | cidr_blocks = length(each.value.cidr_blocks) == 0 ? null : each.value.cidr_blocks
249 | ipv6_cidr_blocks = length(each.value.ipv6_cidr_blocks) == 0 ? null : each.value.ipv6_cidr_blocks
250 | prefix_list_ids = length(each.value.prefix_list_ids) == 0 ? [] : each.value.prefix_list_ids
251 | self = each.value.self
252 | source_security_group_id = each.value.source_security_group_id
253 |
254 | ##
255 | ## end of duplicate block
256 | ########################################################################
257 |
258 | }
259 |
260 | # This null resource prevents an outage when a new Security Group needs to be provisioned
261 | # and `local.rule_create_before_destroy` is `true`:
262 | # 1. It prevents the deposed security group rules from being deleted until after all
263 | # references to it have been changed to refer to the new security group.
264 | # 2. It ensures the new security group rules are created before
265 | # the new security group is associated with existing resources
266 | resource "null_resource" "sync_rules_and_sg_lifecycles" {
267 | # NOTE: This resource affects the lifecycles even when count = 0,
268 | # see https://github.com/hashicorp/terraform/issues/31316#issuecomment-1167450615
269 | # Still, we can avoid creating it when we do not need it to be triggered.
270 | count = local.enabled && local.rule_create_before_destroy ? 1 : 0
271 | # Replacement of the security group requires re-provisioning
272 | triggers = {
273 | sg_ids = one(aws_security_group.cbd[*].id)
274 | }
275 |
276 | depends_on = [aws_security_group_rule.keyed]
277 |
278 | lifecycle {
279 | create_before_destroy = true
280 | }
281 | }
282 |
--------------------------------------------------------------------------------
/normalize.tf:
--------------------------------------------------------------------------------
1 | # In this file, we normalize all the rules into full objects with all keys.
2 | # Then we partition the normalized rules for use as either inline or resourced rules.
3 |
4 | locals {
5 |
6 | # We have var.rules_map as a key-value object where the values are lists of different types.
7 | # For convenience, the ordinary use cases, and ease of understanding, we also have var.rules,
8 | # which is a single list of rules. First thing we do is to combine the 2 into one object.
9 | rules = merge({ _list_ = var.rules }, var.rules_map)
10 |
11 | # Note: we have to use [] instead of null for unset lists due to
12 | # https://github.com/hashicorp/terraform/issues/28137
13 | # which was not fixed until Terraform 1.0.0
14 | norm_rules = local.enabled && local.rules != null ? concat(concat([[]], [for k, rules in local.rules : [for i, rule in rules : {
15 | key = coalesce(lookup(rule, "key", null), "${k}[${i}]")
16 | type = rule.type
17 | from_port = rule.from_port
18 | to_port = rule.to_port
19 | protocol = rule.protocol
20 | description = lookup(rule, "description", local.default_rule_description)
21 |
22 | # Convert a missing key, a value of null, or a value of empty list to []
23 | cidr_blocks = try(length(rule.cidr_blocks), 0) > 0 ? rule.cidr_blocks : []
24 | ipv6_cidr_blocks = try(length(rule.ipv6_cidr_blocks), 0) > 0 ? rule.ipv6_cidr_blocks : []
25 | prefix_list_ids = try(length(rule.prefix_list_ids), 0) > 0 ? rule.prefix_list_ids : []
26 |
27 | source_security_group_id = lookup(rule, "source_security_group_id", null)
28 | security_groups = []
29 |
30 | # self conflicts with other arguments, so it must either be
31 | # null or true (in which case we split it out into a separate rule)
32 | self = lookup(rule, "self", null) == true ? true : null
33 | }]])...) : []
34 |
35 | # in rule_matrix and inline rules, a single rule can have a list of security groups
36 | norm_matrix = local.enabled && var.rule_matrix != null ? concat(concat([[]], [for i, subject in var.rule_matrix : [for j, rule in subject.rules : {
37 | key = "${coalesce(lookup(subject, "key", null), "_m[${i}]")}#${coalesce(lookup(rule, "key", null), "[${j}]")}"
38 | type = rule.type
39 | from_port = rule.from_port
40 | to_port = rule.to_port
41 | protocol = rule.protocol
42 | description = lookup(rule, "description", local.default_rule_description)
43 |
44 | # We tried to be lenient and convert a missing key, a value of null, or a value of empty list to []
45 | # with cidr_blocks = try(length(rule.cidr_blocks), 0) > 0 ? rule.cidr_blocks : []
46 | # but if a list is provided and any value in the list is not available at plan time,
47 | # that formulation causes problems for `count`, so we must forbid keys present with value of null.
48 |
49 | cidr_blocks = lookup(subject, "cidr_blocks", [])
50 | ipv6_cidr_blocks = lookup(subject, "ipv6_cidr_blocks", [])
51 | prefix_list_ids = lookup(subject, "prefix_list_ids", [])
52 |
53 | source_security_group_id = null
54 | security_groups = lookup(subject, "source_security_group_ids", [])
55 |
56 | # self conflicts with other arguments, so it must either be
57 | # null or true (in which case we split it out into a separate rule)
58 | self = lookup(rule, "self", null) == true ? true : null
59 | }]])...) : []
60 |
61 | allow_egress_rule = {
62 | key = "_allow_all_egress_"
63 | type = "egress"
64 | from_port = 0
65 | to_port = 0 # [sic] from and to port ignored when protocol is "-1", warning if not zero
66 | protocol = "-1"
67 | description = "Allow all egress"
68 | cidr_blocks = ["0.0.0.0/0"]
69 | ipv6_cidr_blocks = ["::/0"]
70 | prefix_list_ids = []
71 | self = null
72 | security_groups = []
73 | source_security_group_id = null
74 | }
75 |
76 | extra_rules = local.allow_all_egress ? [local.allow_egress_rule] : []
77 |
78 | all_inline_rules = concat(local.norm_rules, local.norm_matrix, local.extra_rules)
79 |
80 | # For inline rules, the rules have to be separated into ingress and egress
81 | all_ingress_rules = local.inline ? [for r in local.all_inline_rules : r if r.type == "ingress"] : []
82 | all_egress_rules = local.inline ? [for r in local.all_inline_rules : r if r.type == "egress"] : []
83 |
84 | # In `aws_security_group_rule` a rule can only have one security group, not a list, so we have to explode the matrix
85 | # Also, self, source_security_group_id, and CIDRs conflict with each other, so they have to be separated out.
86 | # We must be very careful not to make the computed number of rules in any way dependant
87 | # on a computed input value, we must stick to counting things.
88 |
89 | self_rules = local.inline ? [] : [for rule in local.norm_matrix : {
90 | key = "${rule.key}#self"
91 | type = rule.type
92 | from_port = rule.from_port
93 | to_port = rule.to_port
94 | protocol = rule.protocol
95 | description = rule.description
96 |
97 | cidr_blocks = []
98 | ipv6_cidr_blocks = []
99 | prefix_list_ids = []
100 | self = true
101 |
102 | security_groups = []
103 | source_security_group_id = null
104 |
105 | # To preserve count and order of rules, we would like to create rules for `false`
106 | # even though they do nothing, but an empty rule is not allowed, so we have to
107 | # create the rule only when `self` is `true`.
108 | # We use `== true` because `self` could be `null` and `if null` is not allowed.
109 | } if rule.self == true]
110 |
111 | other_rules = local.inline ? [] : [for rule in local.norm_matrix : {
112 | key = "${rule.key}#cidr"
113 | type = rule.type
114 | from_port = rule.from_port
115 | to_port = rule.to_port
116 | protocol = rule.protocol
117 | description = rule.description
118 |
119 | cidr_blocks = rule.cidr_blocks
120 | ipv6_cidr_blocks = rule.ipv6_cidr_blocks
121 | prefix_list_ids = rule.prefix_list_ids
122 | self = null
123 |
124 | security_groups = []
125 | source_security_group_id = null
126 | } if length(rule.cidr_blocks) + length(rule.ipv6_cidr_blocks) + length(rule.prefix_list_ids) > 0]
127 |
128 |
129 | # First, collect all the rules with lists of security groups
130 | sg_rules_lists = local.inline ? [] : [for rule in local.all_inline_rules : {
131 | key = "${rule.key}#sg"
132 | type = rule.type
133 | from_port = rule.from_port
134 | to_port = rule.to_port
135 | protocol = rule.protocol
136 | description = rule.description
137 |
138 | cidr_blocks = []
139 | ipv6_cidr_blocks = []
140 | prefix_list_ids = []
141 | self = null
142 | security_groups = rule.security_groups
143 | } if length(rule.security_groups) > 0]
144 |
145 | # Now we have to explode the lists into individual rules
146 | sg_exploded_rules = flatten([for rule in local.sg_rules_lists : [for i, sg in rule.security_groups : {
147 | key = "${rule.key}#${i}"
148 | type = rule.type
149 | from_port = rule.from_port
150 | to_port = rule.to_port
151 | protocol = rule.protocol
152 | description = rule.description
153 |
154 | cidr_blocks = []
155 | ipv6_cidr_blocks = []
156 | prefix_list_ids = []
157 | self = null
158 |
159 | security_groups = []
160 | source_security_group_id = sg
161 | }]])
162 |
163 | all_resource_rules = concat(local.norm_rules, local.self_rules, local.sg_exploded_rules, local.other_rules, local.extra_rules)
164 | keyed_resource_rules = { for r in local.all_resource_rules : r.key => r }
165 | }
166 |
--------------------------------------------------------------------------------
/outputs.tf:
--------------------------------------------------------------------------------
1 | output "id" {
2 | description = "The created or target Security Group ID"
3 | value = local.security_group_id
4 | }
5 |
6 | output "arn" {
7 | description = "The created Security Group ARN (null if using existing security group)"
8 | value = try(local.created_security_group.arn, null)
9 | }
10 |
11 | output "name" {
12 | description = "The created Security Group Name (null if using existing security group)"
13 | value = try(local.created_security_group.name, null)
14 | }
15 |
16 | output "rules_terraform_ids" {
17 | description = "List of Terraform IDs of created `security_group_rule` resources, primarily provided to enable `depends_on`"
18 | value = values(aws_security_group_rule.keyed).*.id
19 | }
20 |
--------------------------------------------------------------------------------
/test/.gitignore:
--------------------------------------------------------------------------------
1 | .test-harness
2 |
--------------------------------------------------------------------------------
/test/Makefile:
--------------------------------------------------------------------------------
1 | TEST_HARNESS ?= https://github.com/cloudposse/test-harness.git
2 | TEST_HARNESS_BRANCH ?= master
3 | TEST_HARNESS_PATH = $(realpath .test-harness)
4 | BATS_ARGS ?= --tap
5 | BATS_LOG ?= test.log
6 |
7 | # Define a macro to run the tests
8 | define RUN_TESTS
9 | @echo "Running tests in $(1)"
10 | @cd $(1) && bats $(BATS_ARGS) $(addsuffix .bats,$(addprefix $(TEST_HARNESS_PATH)/test/terraform/,$(TESTS)))
11 | endef
12 |
13 | default: all
14 |
15 | -include Makefile.*
16 |
17 | ## Provision the test-harnesss
18 | .test-harness:
19 | [ -d $@ ] || git clone --depth=1 -b $(TEST_HARNESS_BRANCH) $(TEST_HARNESS) $@
20 |
21 | ## Initialize the tests
22 | init: .test-harness
23 |
24 | ## Install all dependencies (OS specific)
25 | deps::
26 | @exit 0
27 |
28 | ## Clean up the test harness
29 | clean:
30 | [ "$(TEST_HARNESS_PATH)" == "/" ] || rm -rf $(TEST_HARNESS_PATH)
31 |
32 | ## Run all tests
33 | all: module examples/complete
34 |
35 | ## Run basic sanity checks against the module itself
36 | module: export TESTS ?= installed lint module-pinning provider-pinning validate terraform-docs input-descriptions output-descriptions
37 | module: deps
38 | $(call RUN_TESTS, ../)
39 |
40 | ## Run tests against example
41 | examples/complete: export TESTS ?= installed lint validate
42 | examples/complete: deps
43 | $(call RUN_TESTS, ../$@)
44 |
--------------------------------------------------------------------------------
/test/Makefile.alpine:
--------------------------------------------------------------------------------
1 | ifneq (,$(wildcard /sbin/apk))
2 | ## Install all dependencies for alpine
3 | deps:: init
4 | @apk add --update terraform-docs@cloudposse json2hcl@cloudposse
5 | endif
6 |
--------------------------------------------------------------------------------
/test/src/.gitignore:
--------------------------------------------------------------------------------
1 | .gopath
2 | vendor/
3 |
--------------------------------------------------------------------------------
/test/src/Makefile:
--------------------------------------------------------------------------------
1 | export TERRAFORM_VERSION ?= $(shell curl -s https://checkpoint-api.hashicorp.com/v1/check/terraform | jq -r -M '.current_version' | cut -d. -f1)
2 |
3 | .DEFAULT_GOAL : all
4 | .PHONY: all
5 |
6 | ## Default target
7 | all: test
8 |
9 | .PHONY : init
10 | ## Initialize tests
11 | init:
12 | @exit 0
13 |
14 | .PHONY : test
15 | ## Run tests
16 | test: init
17 | go mod download
18 | go test -v -timeout 15m
19 |
20 | ## Run tests in docker container
21 | docker/test:
22 | docker run --name terratest --rm -it -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN -e GITHUB_TOKEN \
23 | -e PATH="/usr/local/terraform/$(TERRAFORM_VERSION)/bin:/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
24 | -v $(CURDIR)/../../:/module/ cloudposse/test-harness:latest -C /module/test/src test
25 |
26 | .PHONY : clean
27 | ## Clean up files
28 | clean:
29 | rm -rf ../../examples/complete/*.tfstate*
30 |
--------------------------------------------------------------------------------
/test/src/examples_complete_test.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "os"
5 | "regexp"
6 | "strings"
7 | "testing"
8 |
9 | "github.com/gruntwork-io/terratest/modules/random"
10 | "github.com/gruntwork-io/terratest/modules/terraform"
11 | testStructure "github.com/gruntwork-io/terratest/modules/test-structure"
12 | "github.com/stretchr/testify/assert"
13 |
14 | "k8s.io/apimachinery/pkg/util/runtime"
15 | )
16 |
17 | func cleanup(t *testing.T, terraformOptions *terraform.Options, tempTestFolder string) {
18 | terraform.Destroy(t, terraformOptions)
19 | os.RemoveAll(tempTestFolder)
20 | }
21 |
22 | // Test the Terraform module in examples/complete using Terratest.
23 | func TestExamplesComplete(t *testing.T) {
24 | t.Parallel()
25 | randID := strings.ToLower(random.UniqueId())
26 | attributes := []string{randID}
27 |
28 | rootFolder := "../../"
29 | terraformFolderRelativeToRoot := "examples/complete"
30 | varFiles := []string{"fixtures.us-east-2.tfvars"}
31 |
32 | tempTestFolder := testStructure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot)
33 |
34 | terraformOptions := &terraform.Options{
35 | // The path to where our Terraform code is located
36 | TerraformDir: tempTestFolder,
37 | Upgrade: true,
38 | // Variables to pass to our Terraform code using -var-file options
39 | VarFiles: varFiles,
40 | Vars: map[string]interface{}{
41 | "attributes": attributes,
42 | },
43 | }
44 |
45 | // At the end of the test, run `terraform destroy` to clean up any resources that were created
46 | defer cleanup(t, terraformOptions, tempTestFolder)
47 |
48 | // If Go runtime crushes, run `terraform destroy` to clean up any resources that were created
49 | defer runtime.HandleCrash(func(i interface{}) {
50 | cleanup(t, terraformOptions, tempTestFolder)
51 | })
52 |
53 | // This will run `terraform init` and `terraform apply` and fail the test if there are any errors
54 | terraform.InitAndApply(t, terraformOptions)
55 |
56 | // Run `terraform output` to get the value of an output variable
57 |
58 | // Verify that outputs are valid when no target security group is supplied
59 | newSgID := terraform.Output(t, terraformOptions, "created_sg_id")
60 | newSgARN := terraform.Output(t, terraformOptions, "created_sg_arn")
61 | newSgName := terraform.Output(t, terraformOptions, "created_sg_name")
62 |
63 | assert.Contains(t, newSgID, "sg-", "SG ID should contains substring 'sg-'")
64 | assert.Contains(t, newSgARN, "arn:aws:ec2", "SG ID should contains substring 'arn:aws:ec2'")
65 | assert.Contains(t, newSgName, "eg-ue2-test-sg-"+randID+"-new-")
66 |
67 | // Verify that outputs are valid when an existing security group is provided
68 | targetSgID := terraform.Output(t, terraformOptions, "target_sg_id")
69 | testSgID := terraform.Output(t, terraformOptions, "test_created_sg_id")
70 |
71 | assert.Equal(t, testSgID, targetSgID, "Module should return provided SG ID as \"id\" output")
72 | }
73 |
74 | func TestExamplesCompleteDisabled(t *testing.T) {
75 | t.Parallel()
76 | randID := strings.ToLower(random.UniqueId())
77 | attributes := []string{randID}
78 |
79 | rootFolder := "../../"
80 | terraformFolderRelativeToRoot := "examples/complete"
81 | varFiles := []string{"fixtures.us-east-2.tfvars"}
82 |
83 | tempTestFolder := testStructure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot)
84 |
85 | terraformOptions := &terraform.Options{
86 | // The path to where our Terraform code is located
87 | TerraformDir: tempTestFolder,
88 | Upgrade: true,
89 | // Variables to pass to our Terraform code using -var-file options
90 | VarFiles: varFiles,
91 | Vars: map[string]interface{}{
92 | "attributes": attributes,
93 | "enabled": "false",
94 | },
95 | }
96 |
97 | // At the end of the test, run `terraform destroy` to clean up any resources that were created
98 | defer cleanup(t, terraformOptions, tempTestFolder)
99 |
100 | // This will run `terraform init` and `terraform apply` and fail the test if there are any errors
101 | results := terraform.InitAndApply(t, terraformOptions)
102 |
103 | // Should complete successfully without creating or changing any resources.
104 | // Extract the "Resources:" section of the output to make the error message more readable.
105 | re := regexp.MustCompile(`Resources: [^.]+\.`)
106 | match := re.FindString(results)
107 | assert.Equal(t, "Resources: 0 added, 0 changed, 0 destroyed.", match, "Re-applying the same configuration should not change any resources")
108 | }
109 |
--------------------------------------------------------------------------------
/test/src/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/cloudposse/terraform-aws-security-group
2 |
3 | go 1.19
4 |
5 | require (
6 | // Known security flaws in terratest dependencies prior to v0.40.15
7 | github.com/gruntwork-io/terratest v0.41.16
8 | github.com/stretchr/testify v1.8.2
9 | k8s.io/apimachinery v0.20.6
10 | )
11 |
12 | require (
13 | cloud.google.com/go v0.110.0 // indirect
14 | cloud.google.com/go/compute v1.19.1 // indirect
15 | cloud.google.com/go/compute/metadata v0.2.3 // indirect
16 | cloud.google.com/go/iam v0.13.0 // indirect
17 | cloud.google.com/go/storage v1.28.1 // indirect
18 | github.com/agext/levenshtein v1.2.3 // indirect
19 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
20 | github.com/aws/aws-sdk-go v1.44.122 // indirect
21 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
22 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
23 | github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
24 | github.com/davecgh/go-spew v1.1.1 // indirect
25 | github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect
26 | github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 // indirect
27 | github.com/go-logr/logr v1.2.3 // indirect
28 | github.com/go-sql-driver/mysql v1.4.1 // indirect
29 | github.com/gogo/protobuf v1.3.2 // indirect
30 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
31 | github.com/golang/protobuf v1.5.3 // indirect
32 | github.com/google/go-cmp v0.5.9 // indirect
33 | github.com/google/gofuzz v1.1.0 // indirect
34 | github.com/google/uuid v1.3.0 // indirect
35 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
36 | github.com/googleapis/gax-go/v2 v2.7.1 // indirect
37 | github.com/googleapis/gnostic v0.4.1 // indirect
38 | github.com/gruntwork-io/go-commons v0.8.0 // indirect
39 | github.com/hashicorp/errwrap v1.0.0 // indirect
40 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
41 | github.com/hashicorp/go-getter v1.7.5 // indirect
42 | github.com/hashicorp/go-multierror v1.1.0 // indirect
43 | github.com/hashicorp/go-safetemp v1.0.0 // indirect
44 | github.com/hashicorp/go-version v1.6.0 // indirect
45 | github.com/hashicorp/hcl/v2 v2.9.1 // indirect
46 | github.com/hashicorp/terraform-json v0.13.0 // indirect
47 | github.com/imdario/mergo v0.3.11 // indirect
48 | github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a // indirect
49 | github.com/jmespath/go-jmespath v0.4.0 // indirect
50 | github.com/json-iterator/go v1.1.12 // indirect
51 | github.com/klauspost/compress v1.15.11 // indirect
52 | github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 // indirect
53 | github.com/mitchellh/go-homedir v1.1.0 // indirect
54 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect
55 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect
56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
57 | github.com/modern-go/reflect2 v1.0.2 // indirect
58 | github.com/pmezard/go-difflib v1.0.0 // indirect
59 | github.com/pquerna/otp v1.2.0 // indirect
60 | github.com/russross/blackfriday/v2 v2.1.0 // indirect
61 | github.com/spf13/pflag v1.0.5 // indirect
62 | github.com/tmccombs/hcl2json v0.3.3 // indirect
63 | github.com/ulikunitz/xz v0.5.10 // indirect
64 | github.com/urfave/cli v1.22.2 // indirect
65 | github.com/zclconf/go-cty v1.9.1 // indirect
66 | go.opencensus.io v0.24.0 // indirect
67 | golang.org/x/crypto v0.17.0 // indirect
68 | golang.org/x/net v0.10.0 // indirect
69 | golang.org/x/oauth2 v0.7.0 // indirect
70 | golang.org/x/sys v0.15.0 // indirect
71 | golang.org/x/term v0.15.0 // indirect
72 | golang.org/x/text v0.14.0 // indirect
73 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
74 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
75 | google.golang.org/api v0.114.0 // indirect
76 | google.golang.org/appengine v1.6.7 // indirect
77 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
78 | google.golang.org/grpc v1.56.3 // indirect
79 | google.golang.org/protobuf v1.30.0 // indirect
80 | gopkg.in/inf.v0 v0.9.1 // indirect
81 | gopkg.in/yaml.v2 v2.4.0 // indirect
82 | gopkg.in/yaml.v3 v3.0.1 // indirect
83 | k8s.io/api v0.20.6 // indirect
84 | k8s.io/client-go v0.20.6 // indirect
85 | k8s.io/klog/v2 v2.80.1 // indirect
86 | k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect
87 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
88 | sigs.k8s.io/yaml v1.3.0 // indirect
89 | )
90 |
--------------------------------------------------------------------------------
/variables.tf:
--------------------------------------------------------------------------------
1 | variable "target_security_group_id" {
2 | type = list(string)
3 | description = <<-EOT
4 | The ID of an existing Security Group to which Security Group rules will be assigned.
5 | The Security Group's name and description will not be changed.
6 | Not compatible with `inline_rules_enabled` or `revoke_rules_on_delete`.
7 | If not provided (the default), this module will create a security group.
8 | EOT
9 | default = []
10 | validation {
11 | condition = length(var.target_security_group_id) < 2
12 | error_message = "Only 1 security group can be targeted."
13 | }
14 | }
15 |
16 | variable "security_group_name" {
17 | type = list(string)
18 | description = <<-EOT
19 | The name to assign to the security group. Must be unique within the VPC.
20 | If not provided, will be derived from the `null-label.context` passed in.
21 | If `create_before_destroy` is true, will be used as a name prefix.
22 | EOT
23 | default = []
24 | validation {
25 | condition = length(var.security_group_name) < 2
26 | error_message = "Only 1 security group name can be provided."
27 | }
28 | }
29 |
30 |
31 | variable "security_group_description" {
32 | type = string
33 | description = <<-EOT
34 | The description to assign to the created Security Group.
35 | Warning: Changing the description causes the security group to be replaced.
36 | EOT
37 | default = "Managed by Terraform"
38 | }
39 |
40 | variable "create_before_destroy" {
41 | type = bool
42 | description = <<-EOT
43 | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
44 | We only recommend setting this `false` if you are importing an existing security group
45 | that you do not want replaced and therefore need full control over its name.
46 | Note that changing this value will always cause the security group to be replaced.
47 | EOT
48 | default = true
49 | }
50 |
51 | variable "preserve_security_group_id" {
52 | type = bool
53 | description = <<-EOT
54 | When `false` and `create_before_destroy` is `true`, changes to security group rules
55 | cause a new security group to be created with the new rules, and the existing security group is then
56 | replaced with the new one, eliminating any service interruption.
57 | When `true` or when changing the value (from `false` to `true` or from `true` to `false`),
58 | existing security group rules will be deleted before new ones are created, resulting in a service interruption,
59 | but preserving the security group itself.
60 | **NOTE:** Setting this to `true` does not guarantee the security group will never be replaced,
61 | it only keeps changes to the security group rules from triggering a replacement.
62 | See the README for further discussion.
63 | EOT
64 | default = false
65 | }
66 |
67 | variable "allow_all_egress" {
68 | type = bool
69 | description = <<-EOT
70 | A convenience that adds to the rules specified elsewhere a rule that allows all egress.
71 | If this is false and no egress rules are specified via `rules` or `rule-matrix`, then no egress will be allowed.
72 | EOT
73 | default = true
74 | }
75 |
76 | variable "rules" {
77 | type = list(any)
78 | description = <<-EOT
79 | A list of Security Group rule objects. All elements of a list must be exactly the same type;
80 | use `rules_map` if you want to supply multiple lists of different types.
81 | The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
82 | except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
83 | and known at "plan" time.
84 | To get more info see the `security_group_rule` [documentation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule).
85 | ___Note:___ The length of the list must be known at plan time.
86 | This means you cannot use functions like `compact` or `sort` when computing the list.
87 | EOT
88 | default = []
89 | }
90 |
91 | variable "rules_map" {
92 | type = any
93 | description = <<-EOT
94 | A map-like object of lists of Security Group rule objects. All elements of a list must be exactly the same type,
95 | so this input accepts an object with keys (attributes) whose values are lists so you can separate different
96 | types into different lists and still pass them into one input. Keys must be known at "plan" time.
97 | The keys and values of the Security Group rule objects are fully compatible with the `aws_security_group_rule` resource,
98 | except for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique
99 | and known at "plan" time.
100 | To get more info see the `security_group_rule` [documentation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule).
101 | EOT
102 | default = {}
103 | }
104 |
105 | variable "rule_matrix" {
106 | # rule_matrix is independent of the `rules` input.
107 | # Only the rules specified in the `rule_matrix` object are applied to the subjects specified in `rule_matrix`.
108 | # The `key` attributes are optional, but if supplied, must be known at plan time or else
109 | # you will get an error from Terraform. If the value is triggering an error, just omit it.
110 | # Schema:
111 | # {
112 | # # these top level lists define all the subjects to which rule_matrix rules will be applied
113 | # key = unique key (for stability from plan to plan)
114 | # source_security_group_ids = list of source security group IDs to apply all rules to
115 | # cidr_blocks = list of ipv4 CIDR blocks to apply all rules to
116 | # ipv6_cidr_blocks = list of ipv6 CIDR blocks to apply all rules to
117 | # prefix_list_ids = list of prefix list IDs to apply all rules to
118 | # self = # set "true" to apply the rules to the created or existing security group
119 | #
120 | # # each rule in the rules list will be applied to every subject defined above
121 | # rules = [{
122 | # key = "unique key"
123 | # type = "ingress"
124 | # from_port = 433
125 | # to_port = 433
126 | # protocol = "tcp"
127 | # description = "Allow HTTPS ingress"
128 | # }]
129 |
130 | type = any
131 | description = <<-EOT
132 | A convenient way to apply the same set of rules to a set of subjects. See README for details.
133 | EOT
134 | default = []
135 | }
136 |
137 | variable "security_group_create_timeout" {
138 | type = string
139 | description = "How long to wait for the security group to be created."
140 | default = "10m"
141 | }
142 |
143 | variable "security_group_delete_timeout" {
144 | type = string
145 | description = <<-EOT
146 | How long to retry on `DependencyViolation` errors during security group deletion from
147 | lingering ENIs left by certain AWS services such as Elastic Load Balancing.
148 | EOT
149 | default = "15m"
150 | }
151 |
152 | variable "revoke_rules_on_delete" {
153 | type = bool
154 | description = <<-EOT
155 | Instruct Terraform to revoke all of the Security Group's attached ingress and egress rules before deleting
156 | the security group itself. This is normally not needed.
157 | EOT
158 | default = false
159 | }
160 |
161 | variable "vpc_id" {
162 | type = string
163 | description = "The ID of the VPC where the Security Group will be created."
164 | }
165 |
166 | variable "inline_rules_enabled" {
167 | type = bool
168 | description = <<-EOT
169 | NOT RECOMMENDED. Create rules "inline" instead of as separate `aws_security_group_rule` resources.
170 | See [#20046](https://github.com/hashicorp/terraform-provider-aws/issues/20046) for one of several issues with inline rules.
171 | 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.
172 | EOT
173 | default = false
174 | }
175 |
--------------------------------------------------------------------------------
/versions.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_version = ">= 1.0.0"
3 |
4 | required_providers {
5 | aws = {
6 | source = "hashicorp/aws"
7 | version = ">= 3.0"
8 | }
9 | null = {
10 | source = "hashicorp/null"
11 | version = ">= 3.0"
12 | }
13 | random = {
14 | source = "hashicorp/random"
15 | version = ">= 3.0"
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------