├── .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 | [![Slack Community](https://slack.cloudposse.com/badge.svg)](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 | [![Slack Community](https://slack.cloudposse.com/badge.svg)](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 | [![Slack Community](https://slack.cloudposse.com/badge.svg)](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 | [![Slack Community](https://slack.cloudposse.com/badge.svg)](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 | Project Banner
5 |

6 | Latest ReleaseLast UpdatedSlack Community

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 | > Request Quote 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 | > Request Quote 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 | > Request Quote 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 | License 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 | README footer 855 | 856 | Beacon 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 | --------------------------------------------------------------------------------