├── examples └── startup │ ├── variables.tf │ ├── outputs.tf │ ├── providers.tf │ ├── main.tf │ ├── main.spoke1.tf │ ├── main.spoke2.tf │ └── TestRecord.md ├── unit-fixture ├── locals.tf ├── variables.tf ├── fake_module.tf └── outputs.tf ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── Feature_Request.yml │ └── Bug_Report.yml ├── dependabot.yml ├── workflows │ ├── post-push.yaml │ ├── breaking-change-detect.yaml │ ├── pr-check.yaml │ └── acc-test.yaml └── pull_request_template.md ├── GNUmakefile ├── terraform.tf ├── CODE_OF_CONDUCT.md ├── test ├── e2e │ └── terraform_e2e_test.go ├── go.mod └── unit │ └── terraform_unit_test.go ├── .devcontainer └── devcontainer.json ├── LICENSE ├── SUPPORT.md ├── .gitignore ├── .terraform-docs.yml ├── _header.md ├── outputs.tf ├── SECURITY.md ├── CHANGELOG.md ├── _footer.md ├── locals.tf ├── main.tf ├── variables.tf └── README.md /examples/startup/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /unit-fixture/locals.tf: -------------------------------------------------------------------------------- 1 | ../locals.tf -------------------------------------------------------------------------------- /unit-fixture/variables.tf: -------------------------------------------------------------------------------- 1 | ../variables.tf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/test" 5 | schedule: 6 | interval: "weekly" -------------------------------------------------------------------------------- /examples/startup/outputs.tf: -------------------------------------------------------------------------------- 1 | output "spoke2_pip" { 2 | value = azurerm_public_ip.spoke2.ip_address 3 | 4 | depends_on = [azurerm_linux_virtual_machine.spoke2] 5 | } 6 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | $(shell curl -H 'Cache-Control: no-cache, no-store' -sSL "https://raw.githubusercontent.com/Azure/tfmod-scaffold/refs/heads/main/GNUmakefile" -o tfvmmakefile) 4 | -include tfvmmakefile -------------------------------------------------------------------------------- /terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3.0" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = ">= 3.7.0, < 4.0" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/post-push.yaml: -------------------------------------------------------------------------------- 1 | name: Post Push 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | tags: 8 | - '*' 9 | 10 | permissions: write-all 11 | 12 | jobs: 13 | post-push: 14 | if: github.actor != 'github-actions[bot]' 15 | uses: Azure/tfmod-scaffold/.github/workflows/post-push.yaml@main -------------------------------------------------------------------------------- /.github/workflows/breaking-change-detect.yaml: -------------------------------------------------------------------------------- 1 | name: 'Comment on PR' 2 | 3 | permissions: 4 | contents: read 5 | pull-requests: read 6 | 7 | on: 8 | workflow_run: 9 | workflows: ["Pre Pull Request Check"] 10 | types: 11 | - completed 12 | 13 | jobs: 14 | comment: 15 | uses: Azure/tfmod-scaffold/.github/workflows/breaking-change-detect.yaml@main 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Describe your changes 2 | 3 | ## Issue number 4 | 5 | #000 6 | 7 | ## Checklist before requesting a review 8 | - [ ] The pr title can be used to describe what this pr did in `CHANGELOG.md` file 9 | - [ ] I have executed pre-commit on my machine 10 | - [ ] I have passed pr-check on my machine 11 | 12 | Thanks for your cooperation! 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/pr-check.yaml: -------------------------------------------------------------------------------- 1 | name: Pre Pull Request Check 2 | on: 3 | pull_request: 4 | types: ['opened', 'synchronize'] 5 | paths: 6 | - '.github/**' 7 | - '**.go' 8 | - '**.tf' 9 | - '.github/workflows/**' 10 | - '**.md' 11 | - '**/go.mod' 12 | 13 | permissions: 14 | contents: read 15 | pull-requests: read 16 | 17 | jobs: 18 | prepr-check: 19 | uses: Azure/tfmod-scaffold/.github/workflows/pr-check.yaml@main 20 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /test/e2e/terraform_e2e_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | test_helper "github.com/Azure/terraform-module-test-helper" 8 | "github.com/gruntwork-io/terratest/modules/terraform" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestExmaples_startup(t *testing.T) { 13 | test_helper.RunE2ETest(t, "../../", "examples/startup", terraform.Options{ 14 | Upgrade: true, 15 | }, func(t *testing.T, output test_helper.TerraformOutput) { 16 | pip := output["spoke2_pip"].(string) 17 | assert.NotNil(t, net.ParseIP(pip)) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /examples/startup/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | 4 | required_providers { 5 | azurerm = { 6 | source = "hashicorp/azurerm" 7 | version = ">=3.7.0, < 4.0" 8 | } 9 | local = { 10 | source = "hashicorp/local" 11 | version = "2.3.0" 12 | } 13 | random = { 14 | source = "hashicorp/random" 15 | version = "~> 3.0" 16 | } 17 | tls = { 18 | source = "hashicorp/tls" 19 | version = "4.0.4" 20 | } 21 | } 22 | } 23 | 24 | provider "azurerm" { 25 | features { 26 | resource_group { 27 | prevent_deletion_if_contains_resources = false 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/azterraform:latest", 3 | 4 | "runArgs": [ 5 | "--cap-add=SYS_PTRACE", 6 | "--security-opt", 7 | "seccomp=unconfined", 8 | "--init", 9 | "--network=host" 10 | ], 11 | 12 | "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ], 13 | "customizations": { 14 | "vscode": { 15 | "settings": { 16 | "go.toolsManagement.checkForUpdates": "local", 17 | "go.useLanguageServer": true, 18 | "go.goroot": "/usr/local/go" 19 | }, 20 | "extensions": [ 21 | "hashicorp.terraform", 22 | "golang.Go" 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /unit-fixture/fake_module.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | hub_routing = { 3 | for vnet in var.hub_virtual_networks : 4 | vnet.name => { 5 | id = "${vnet.name}_route_table_id" 6 | } 7 | } 8 | firewall_private_ip = { 9 | for vnet_name, vnet in var.hub_virtual_networks : vnet_name => "${vnet_name}-fake-fw-private-ip" 10 | if vnet.firewall != null 11 | } 12 | virtual_networks_modules = { 13 | for k, vnet in var.hub_virtual_networks : 14 | k => { 15 | vnet_name = vnet.name 16 | vnet_id = "${vnet.name}_id" 17 | vnet_location = vnet.location 18 | vnet_subnets_name_id = { for subnet_name, subnet in vnet.subnets : subnet_name => "${subnet_name}_id" } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /unit-fixture/outputs.tf: -------------------------------------------------------------------------------- 1 | output "hub_peering_map" { 2 | value = local.hub_peering_map 3 | } 4 | 5 | output "resource_group_data" { 6 | value = local.resource_group_data 7 | } 8 | 9 | output "route_map" { 10 | value = local.route_map 11 | } 12 | 13 | output "subnet_route_table_association_map" { 14 | value = local.subnet_route_table_association_map 15 | } 16 | 17 | output "subnet_external_route_table_association_map" { 18 | value = local.subnet_external_route_table_association_map 19 | } 20 | 21 | output "fw_default_ip_configuration_pip" { 22 | value = local.fw_default_ip_configuration_pip 23 | } 24 | 25 | output "firewalls" { 26 | value = local.firewalls 27 | } 28 | 29 | output "fw_management_ip_configuration_pip" { 30 | value = local.fw_management_ip_configuration_pip 31 | } 32 | 33 | output "firewall_management_subnets" { 34 | value = local.firewall_management_subnets 35 | } -------------------------------------------------------------------------------- /.github/workflows/acc-test.yaml: -------------------------------------------------------------------------------- 1 | name: E2E Test 2 | on: 3 | pull_request: 4 | types: [ 'opened', 'synchronize' ] 5 | paths: 6 | - '.github/**' 7 | - '**.go' 8 | - '**.tf' 9 | - '.github/workflows/**' 10 | - '**.md' 11 | - '**/go.mod' 12 | 13 | jobs: 14 | check: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checking for Fork 18 | shell: pwsh 19 | run: | 20 | $isFork = "${{ github.event.pull_request.head.repo.fork }}" 21 | if($isFork -eq "true") { 22 | echo "### WARNING: This workflow is disabled for forked repositories. Please follow the [release branch process](https://azure.github.io/Azure-Verified-Modules/contributing/terraform/terraform-contribution-flow/#5-create-a-pull-request-to-the-upstream-repository) if end to end tests are required." >> $env:GITHUB_STEP_SUMMARY 23 | } 24 | 25 | run-e2e-tests: 26 | if: github.event.pull_request.head.repo.fork == false 27 | uses: Azure/tfmod-scaffold/.github/workflows/tfvm_e2e.yaml@main 28 | name: end to end 29 | secrets: inherit 30 | permissions: 31 | id-token: write 32 | contents: read -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. 7 | - **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | **/.terraform.lock.hcl 9 | .terraform.lock.hcl 10 | 11 | # Crash log files 12 | crash.log 13 | crash.*.log 14 | 15 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 16 | # password, private keys, and other secrets. These should not be part of version 17 | # control as they are data points which are potentially sensitive and subject 18 | # to change depending on the environment. 19 | *.tfvars 20 | *.tfvars.json 21 | 22 | # Ignore override files as they are usually used to override resources locally and so 23 | # are not checked in 24 | override.tf 25 | override.tf.json 26 | *_override.tf 27 | *_override.tf.json 28 | 29 | # Include override files you do wish to add to version control using negated pattern 30 | # !example_override.tf 31 | 32 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 33 | # example: *tfplan* 34 | 35 | # Ignore CLI configuration files 36 | .terraformrc 37 | terraform.rc 38 | 39 | .tflint.hcl 40 | .tflint_example.hcl 41 | tfmod-scaffold 42 | scripts 43 | **/go.sum 44 | **/go_mod 45 | 46 | **/vendor/ 47 | 48 | .DS_Store 49 | .idea 50 | README-generated.md 51 | 52 | **/TestRecord.md.tmp 53 | 54 | # Certificate files generated by examples 55 | **/*.pem 56 | 57 | tfvmmakefile 58 | -------------------------------------------------------------------------------- /.terraform-docs.yml: -------------------------------------------------------------------------------- 1 | ### To generate the output file to partially incorporate in the README.md, 2 | ### Execute this command in the Terraform module's code folder: 3 | # terraform-docs -c .tfdocs-config.yml . 4 | 5 | formatter: "markdown document" # this is required 6 | 7 | version: "0.16.0" 8 | 9 | header-from: "_header.md" 10 | footer-from: "_footer.md" 11 | 12 | recursive: 13 | enabled: false 14 | path: modules 15 | 16 | sections: 17 | hide: [] 18 | show: [] 19 | 20 | hide-all: false # deprecated in v0.13.0, removed in v0.15.0 21 | show-all: true # deprecated in v0.13.0, removed in v0.15.0 22 | 23 | content: |- 24 | {{ .Header }} 25 | 26 | ## Documentation 27 | 28 | 29 | {{ .Requirements }} 30 | 31 | {{ .Modules }} 32 | 33 | 34 | {{ .Inputs }} 35 | 36 | {{ .Resources }} 37 | 38 | {{ .Outputs }} 39 | 40 | 41 | {{ .Footer }} 42 | 43 | output: 44 | file: README.md 45 | mode: replace 46 | template: |- 47 | 48 | {{ .Content }} 49 | 50 | output-values: 51 | enabled: false 52 | from: "" 53 | 54 | sort: 55 | enabled: true 56 | by: required 57 | 58 | settings: 59 | anchor: true 60 | color: true 61 | default: true 62 | description: false 63 | escape: true 64 | hide-empty: false 65 | html: true 66 | indent: 2 67 | lockfile: true 68 | read-comments: true 69 | required: true 70 | sensitive: true 71 | type: true 72 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_Request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: I have a suggestion (and might want to implement myself)! 3 | title: "Support for [thing]" 4 | body: 5 | - type: checkboxes 6 | attributes: 7 | label: Is there an existing issue for this? 8 | description: Please search to see if an issue already exists for the feature you are requesting. 9 | options: 10 | - label: I have searched the existing issues 11 | required: true 12 | - type: textarea 13 | id: description 14 | attributes: 15 | label: Description 16 | description: Please leave a helpful description of the feature request here. 17 | validations: 18 | required: true 19 | - type: input 20 | id: resource 21 | attributes: 22 | label: New or Affected Resource(s)/Data Source(s) 23 | description: Please list the new or affected resources and/or data sources. 24 | placeholder: azurerm_XXXXX 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: config 29 | attributes: 30 | label: Potential Terraform Configuration 31 | description: Please provide an example of what the enhancement could look like on this Terraform module. 32 | render: hcl 33 | - type: textarea 34 | id: references 35 | attributes: 36 | label: References 37 | description: | 38 | Information about referencing Github Issues: https://help.github.com/articles/basic-writing-and-formatting-syntax/#referencing-issues-and-pull-requests 39 | 40 | Are there any other GitHub issues (open or closed) or pull requests that should be linked here? Vendor blog posts or documentation? For example: 41 | 42 | * https://azure.microsoft.com/en-us/roadmap/virtual-network-service-endpoint-for-azure-cosmos-db/ -------------------------------------------------------------------------------- /_header.md: -------------------------------------------------------------------------------- 1 | # Terraform Verified Module for multi-hub network architectures 2 | 3 | [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/Azure/terraform-azure-hubnetworking.svg)](http://isitmaintained.com/project/Azure/terraform-azure-hubnetworking "Average time to resolve an issue") 4 | [![Percentage of issues still open](http://isitmaintained.com/badge/open/Azure/terraform-azure-hubnetworking.svg)](http://isitmaintained.com/project/Azure/terraform-azure-hubnetworking "Percentage of issues still open") 5 | 6 | This module is designed to simplify the creation of multi-region hub networks in Azure. It will create a number of virtual networks and subnets, and optionally peer them together in a mesh topology with routing. 7 | 8 | ## Features 9 | 10 | - This module will deploy `n` number of virtual networks and subnets. 11 | Optionally, these virtual networks can be peered in a mesh topology. 12 | - A routing address space can be specified for each hub network, this module will then create route tables for the other hub networks and associate them with the subnets. 13 | - Azure Firewall can be deployed iun each hub network. This module will configure routing for the AzureFirewallSubnet. 14 | 15 | ## Example 16 | 17 | ```terraform 18 | module "hubnetworks" { 19 | source = "Azure/hubnetworking/azure" 20 | version = "" # change this to your desired version, https://www.terraform.io/language/expressions/version-constraints 21 | 22 | hub_virtual_networks = { 23 | weu-hub = { 24 | name = "vnet-prod-weu-0001" 25 | address_space = ["192.168.0.0/23"] 26 | routing_address_space = ["192.168.0.0/20"] 27 | firewall = { 28 | subnet_address_prefix = "192.168.1.0/24" 29 | sku_tier = "Premium" 30 | sku_name = "AZFW_VNet" 31 | } 32 | } 33 | } 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "firewalls" { 2 | value = { 3 | for vnet_name, fw in azurerm_firewall.fw : vnet_name => { 4 | id = fw.id 5 | name = fw.name 6 | private_ip_address = try(fw.ip_configuration[0].private_ip_address, null) 7 | public_ip_address = try(azurerm_public_ip.fw_default_ip_configuration_pip[vnet_name].ip_address) 8 | management_public_ip_address = try(azurerm_public_ip.fw_management_ip_configuration_pip[vnet_name].ip_address, null) 9 | } 10 | } 11 | description = "A curated output of the firewalls created by this module." 12 | } 13 | 14 | output "hub_route_tables" { 15 | value = { 16 | for vnet_name, rt in azurerm_route_table.hub_routing : vnet_name => { 17 | name = rt.name 18 | id = rt.id 19 | routes = [ 20 | for r in rt.route : { 21 | name = r.name 22 | address_prefix = r.address_prefix 23 | next_hop_type = r.next_hop_type 24 | next_hop_in_ip_address = r.next_hop_in_ip_address 25 | } 26 | ] 27 | } 28 | } 29 | description = "A curated output of the route tables created by this module." 30 | } 31 | 32 | output "resource_groups" { 33 | value = { 34 | for rg_name, rg in azurerm_resource_group.rg : rg_name => { 35 | name = rg.name 36 | location = rg.location 37 | id = rg.id 38 | } 39 | } 40 | description = "A curated output of the resource groups created by this module." 41 | } 42 | 43 | output "virtual_networks" { 44 | value = { 45 | for vnet_name, vnet_mod in module.hub_virtual_networks : vnet_name => { 46 | name = vnet_mod.vnet_name 47 | resource_group_name = var.hub_virtual_networks[vnet_name].resource_group_name 48 | id = vnet_mod.vnet_id 49 | location = vnet_mod.vnet_location 50 | address_spaces = vnet_mod.vnet_address_space 51 | subnets_name_id = vnet_mod.vnet_subnets_name_id 52 | hub_router_ip_address = try(azurerm_firewall.fw[vnet_name].ip_configuration[0].private_ip_address, var.hub_virtual_networks[vnet_name].hub_router_ip_address) 53 | } 54 | } 55 | description = "A curated output of the virtual networks created by this module." 56 | } 57 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/startup/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | regions = toset(["eastus", "eastus2"]) 3 | } 4 | 5 | resource "azurerm_resource_group" "hub_rg" { 6 | for_each = local.regions 7 | 8 | location = each.value 9 | name = "hubandspokedemo-hub-${each.value}-${random_pet.rand.id}" 10 | } 11 | 12 | resource "random_pet" "rand" {} 13 | 14 | module "hub_mesh" { 15 | source = "../.." 16 | hub_virtual_networks = { 17 | eastus-hub = { 18 | name = "eastus-hub" 19 | address_space = ["10.0.0.0/16"] 20 | location = "eastus" 21 | resource_group_name = azurerm_resource_group.hub_rg["eastus"].name 22 | resource_group_creation_enabled = false 23 | resource_group_lock_enabled = false 24 | mesh_peering_enabled = true 25 | route_table_name = "contosohotel-eastus-hub-rt" 26 | routing_address_space = ["10.0.0.0/16", "192.168.0.0/24"] 27 | firewall = { 28 | sku_name = "AZFW_VNet" 29 | sku_tier = "Standard" 30 | subnet_address_prefix = "10.0.1.0/24" 31 | firewall_policy_id = azurerm_firewall_policy.fwpolicy.id 32 | } 33 | } 34 | eastus2-hub = { 35 | name = "eastus2-hub" 36 | address_space = ["10.1.0.0/16"] 37 | location = "eastus2" 38 | resource_group_name = azurerm_resource_group.hub_rg["eastus2"].name 39 | resource_group_creation_enabled = false 40 | resource_group_lock_enabled = false 41 | mesh_peering_enabled = true 42 | route_table_name = "contoso-eastus2-hub-rt" 43 | routing_address_space = ["10.1.0.0/16", "192.168.1.0/24"] 44 | firewall = { 45 | sku_name = "AZFW_VNet" 46 | sku_tier = "Standard" 47 | subnet_address_prefix = "10.1.1.0/24" 48 | firewall_policy_id = azurerm_firewall_policy.fwpolicy.id 49 | } 50 | } 51 | } 52 | 53 | depends_on = [azurerm_firewall_policy_rule_collection_group.allow_internal] 54 | } 55 | 56 | resource "tls_private_key" "key" { 57 | algorithm = "RSA" 58 | rsa_bits = 4096 59 | } 60 | 61 | resource "local_sensitive_file" "private_key" { 62 | filename = "key.pem" 63 | content = tls_private_key.key.private_key_pem 64 | } 65 | 66 | resource "azurerm_resource_group" "fwpolicy" { 67 | location = "eastus" 68 | name = "fwpolicy-${random_pet.rand.id}" 69 | } 70 | 71 | resource "azurerm_firewall_policy" "fwpolicy" { 72 | location = azurerm_resource_group.fwpolicy.location 73 | name = "allow-internal" 74 | resource_group_name = azurerm_resource_group.fwpolicy.name 75 | sku = "Standard" 76 | } 77 | 78 | resource "azurerm_firewall_policy_rule_collection_group" "allow_internal" { 79 | firewall_policy_id = azurerm_firewall_policy.fwpolicy.id 80 | name = "allow-rfc1918" 81 | priority = 100 82 | 83 | network_rule_collection { 84 | action = "Allow" 85 | name = "rfc1918" 86 | priority = 100 87 | 88 | rule { 89 | destination_ports = ["*"] 90 | name = "rfc1918" 91 | protocols = ["Any"] 92 | destination_addresses = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] 93 | source_addresses = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.2.0](https://github.com/Azure/terraform-azurerm-hubnetworking/tree/v0.2.0) (2023-05-02) 4 | 5 | **Merged pull requests:** 6 | 7 | - fix: peering fails when hub network name is not equal to map key \#24 [\#25](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/25) ([matt-FFFFFF](https://github.com/matt-FFFFFF)) 8 | - build\(deps\): bump github.com/gruntwork-io/terratest from 0.41.18 to 0.41.23 in /test [\#23](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/23) ([dependabot[bot]](https://github.com/apps/dependabot)) 9 | 10 | ## [v0.1.0](https://github.com/Azure/terraform-azurerm-hubnetworking/tree/v0.1.0) (2023-04-25) 11 | 12 | **Merged pull requests:** 13 | 14 | - build\(deps\): bump github.com/gruntwork-io/terratest from 0.41.15 to 0.41.18 in /test [\#22](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/22) ([dependabot[bot]](https://github.com/apps/dependabot)) 15 | - build\(deps\): bump github.com/Azure/terraform-module-test-helper from 0.12.0 to 0.13.0 in /test [\#21](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/21) ([dependabot[bot]](https://github.com/apps/dependabot)) 16 | - build\(deps\): bump github.com/thanhpk/randstr from 1.0.4 to 1.0.5 in /test [\#19](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/19) ([dependabot[bot]](https://github.com/apps/dependabot)) 17 | - build\(deps\): bump github.com/Azure/terraform-module-test-helper from 0.9.2-0.20230221054038-98bfb4448c9a to 0.12.0 in /test [\#17](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/17) ([dependabot[bot]](https://github.com/apps/dependabot)) 18 | - docs: update readme and refactor tests [\#16](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/16) ([matt-FFFFFF](https://github.com/matt-FFFFFF)) 19 | - build\(deps\): bump github.com/gruntwork-io/terratest from 0.41.10 to 0.41.12 in /test [\#13](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/13) ([dependabot[bot]](https://github.com/apps/dependabot)) 20 | - 0.1.0 Candidate [\#12](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/12) ([lonegunmanb](https://github.com/lonegunmanb)) 21 | - build\(deps\): bump github.com/stretchr/testify from 1.8.1 to 1.8.2 in /test [\#11](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/11) ([dependabot[bot]](https://github.com/apps/dependabot)) 22 | - Add support for azure firewall [\#10](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/10) ([lonegunmanb](https://github.com/lonegunmanb)) 23 | - Add unit test to verify `local.hub_peering_map` logic [\#7](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/7) ([lonegunmanb](https://github.com/lonegunmanb)) 24 | - build\(deps\): bump github.com/hashicorp/go-getter from 1.6.1 to 1.7.0 in /test [\#6](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/6) ([dependabot[bot]](https://github.com/apps/dependabot)) 25 | - build\(deps\): bump github.com/hashicorp/go-getter/v2 from 2.1.1 to 2.2.0 in /test [\#5](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/5) ([dependabot[bot]](https://github.com/apps/dependabot)) 26 | - Suggest [\#4](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/4) ([lonegunmanb](https://github.com/lonegunmanb)) 27 | - Matt ffffff/initial tf [\#1](https://github.com/Azure/terraform-azurerm-hubnetworking/pull/1) ([matt-FFFFFF](https://github.com/matt-FFFFFF)) 28 | 29 | 30 | 31 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 32 | -------------------------------------------------------------------------------- /_footer.md: -------------------------------------------------------------------------------- 1 | ## Enable or disable tracing tags 2 | 3 | We're using [BridgeCrew Yor](https://github.com/bridgecrewio/yor) and [yorbox](https://github.com/lonegunmanb/yorbox) to help manage tags consistently across infrastructure as code (IaC) frameworks. In this module you might see tags like: 4 | 5 | ```hcl 6 | resource "azurerm_resource_group" "rg" { 7 | location = "eastus" 8 | name = random_pet.name 9 | tags = merge(var.tags, (/**/ (var.tracing_tags_enabled ? { for k, v in /**/ { 10 | avm_git_commit = "3077cc6d0b70e29b6e106b3ab98cee6740c916f6" 11 | avm_git_file = "main.tf" 12 | avm_git_last_modified_at = "2023-05-05 08:57:54" 13 | avm_git_org = "lonegunmanb" 14 | avm_git_repo = "terraform-yor-tag-test-module" 15 | avm_yor_trace = "a0425718-c57d-401c-a7d5-f3d88b2551a4" 16 | } /**/ : replace(k, "avm_", var.tracing_tags_prefix) => v } : {}) /**/)) 17 | } 18 | ``` 19 | 20 | To enable tracing tags, set the variable to true: 21 | 22 | ```hcl 23 | module "example" { 24 | source = 25 | ... 26 | tracing_tags_enabled = true 27 | } 28 | ``` 29 | 30 | The `tracing_tags_enabled` is default to `false`. 31 | 32 | To customize the prefix for your tracing tags, set the `tracing_tags_prefix` variable value in your Terraform configuration: 33 | 34 | ```hcl 35 | module "example" { 36 | source = 37 | ... 38 | tracing_tags_prefix = "custom_prefix_" 39 | } 40 | ``` 41 | 42 | The actual applied tags would be: 43 | 44 | ```text 45 | { 46 | custom_prefix_git_commit = "3077cc6d0b70e29b6e106b3ab98cee6740c916f6" 47 | custom_prefix_git_file = "main.tf" 48 | custom_prefix_git_last_modified_at = "2023-05-05 08:57:54" 49 | custom_prefix_git_org = "lonegunmanb" 50 | custom_prefix_git_repo = "terraform-yor-tag-test-module" 51 | custom_prefix_yor_trace = "a0425718-c57d-401c-a7d5-f3d88b2551a4" 52 | } 53 | ``` 54 | 55 | 56 | ## Contributing 57 | 58 | 59 | This project welcomes contributions and suggestions. 60 | Most contributions require you to agree to a Contributor License Agreement (CLA) 61 | declaring that you have the right to, and actually do, grant us the rights to use your contribution. 62 | For details, visit [https://cla.opensource.microsoft.com](https://cla.opensource.microsoft.com). 63 | 64 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 65 | a CLA and decorate the PR appropriately (e.g., status check, comment). 66 | Simply follow the instructions provided by the bot. 67 | You will only need to do this once across all repos using our CLA. 68 | 69 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 70 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 71 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 72 | 73 | ## Trademarks 74 | 75 | This project may contain trademarks or logos for projects, products, or services. 76 | Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft's Trademark & Brand Guidelines. 77 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 78 | Any use of third-party trademarks or logos are subject to those third-party's policies. 79 | -------------------------------------------------------------------------------- /examples/startup/main.spoke1.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_resource_group" "spoke1" { 2 | location = "eastus" 3 | name = "spoke1-${random_pet.rand.id}" 4 | } 5 | 6 | module "spoke1_vnet" { 7 | source = "Azure/subnets/azurerm" 8 | version = "1.0.0" 9 | 10 | resource_group_name = azurerm_resource_group.spoke1.name 11 | subnets = { 12 | spoke1-subnet = { 13 | address_prefixes = ["192.168.0.0/24"] 14 | route_table = { 15 | id = azurerm_route_table.spoke1.id 16 | } 17 | } 18 | } 19 | virtual_network_address_space = ["192.168.0.0/24"] 20 | virtual_network_location = azurerm_resource_group.spoke1.location 21 | virtual_network_name = "spoke1-vnet-${random_pet.rand.id}" 22 | } 23 | 24 | resource "azurerm_route_table" "spoke1" { 25 | location = azurerm_resource_group.spoke1.location 26 | name = "spoke1-rt" 27 | resource_group_name = azurerm_resource_group.spoke1.name 28 | } 29 | 30 | resource "azurerm_route" "spoke1_to_hub" { 31 | address_prefix = "192.168.0.0/16" 32 | name = "to-hub" 33 | next_hop_type = "VirtualAppliance" 34 | resource_group_name = azurerm_resource_group.spoke1.name 35 | route_table_name = azurerm_route_table.spoke1.name 36 | next_hop_in_ip_address = module.hub_mesh.virtual_networks["eastus-hub"].hub_router_ip_address 37 | } 38 | 39 | resource "azurerm_route" "spoke1_to_hub2" { 40 | address_prefix = "10.0.0.0/8" 41 | name = "to-hub2" 42 | next_hop_type = "VirtualAppliance" 43 | resource_group_name = azurerm_resource_group.spoke1.name 44 | route_table_name = azurerm_route_table.spoke1.name 45 | next_hop_in_ip_address = module.hub_mesh.virtual_networks["eastus-hub"].hub_router_ip_address 46 | } 47 | 48 | resource "azurerm_virtual_network_peering" "spoke1_peering" { 49 | name = "spoke1-peering" 50 | remote_virtual_network_id = module.hub_mesh.virtual_networks["eastus-hub"].id 51 | resource_group_name = azurerm_resource_group.spoke1.name 52 | virtual_network_name = module.spoke1_vnet.vnet_name 53 | allow_forwarded_traffic = true 54 | allow_gateway_transit = false 55 | allow_virtual_network_access = true 56 | use_remote_gateways = false 57 | } 58 | 59 | resource "azurerm_virtual_network_peering" "spoke1_peering_back" { 60 | name = "spoke1-peering-back" 61 | remote_virtual_network_id = module.spoke1_vnet.vnet_id 62 | resource_group_name = module.hub_mesh.virtual_networks["eastus-hub"].resource_group_name 63 | virtual_network_name = module.hub_mesh.virtual_networks["eastus-hub"].name 64 | allow_forwarded_traffic = false 65 | allow_gateway_transit = false 66 | allow_virtual_network_access = true 67 | use_remote_gateways = false 68 | } 69 | 70 | resource "azurerm_network_interface" "spoke1" { 71 | location = azurerm_resource_group.spoke1.location 72 | name = "spoke1-machine-nic" 73 | resource_group_name = azurerm_resource_group.spoke1.name 74 | 75 | ip_configuration { 76 | name = "internal" 77 | private_ip_address_allocation = "Dynamic" 78 | subnet_id = module.spoke1_vnet.vnet_subnets_name_id["spoke1-subnet"] 79 | } 80 | } 81 | 82 | resource "azurerm_linux_virtual_machine" "spoke1" { 83 | #checkov:skip=CKV_AZURE_50:Only for connectivity test so we use vm extension 84 | #checkov:skip=CKV_AZURE_179:Only for connectivity test so we use vm extension 85 | admin_username = "adminuser" 86 | location = azurerm_resource_group.spoke1.location 87 | name = "spoke1-machine" 88 | network_interface_ids = [ 89 | azurerm_network_interface.spoke1.id, 90 | ] 91 | resource_group_name = azurerm_resource_group.spoke1.name 92 | size = "Standard_B2ms" 93 | 94 | 95 | os_disk { 96 | caching = "ReadWrite" 97 | storage_account_type = "Standard_LRS" 98 | } 99 | admin_ssh_key { 100 | public_key = tls_private_key.key.public_key_openssh 101 | username = "adminuser" 102 | } 103 | source_image_reference { 104 | offer = "0001-com-ubuntu-server-jammy" 105 | publisher = "Canonical" 106 | sku = "22_04-lts" 107 | version = "latest" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_Report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: If something isn't working as expected. 3 | labels: [bug] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for taking the time to fill out a bug report. 9 | 10 | If you are not running the latest version of this module, please try to reproduce your bug with the latest version before opening an issue. 11 | - type: checkboxes 12 | attributes: 13 | label: Is there an existing issue for this? 14 | description: Please search to see if an issue already exists for the bug you encountered. 15 | options: 16 | - label: I have searched the existing issues 17 | required: true 18 | - type: dropdown 19 | attributes: 20 | label: Greenfield/Brownfield provisioning 21 | description: Do you reproduce the bug with a new infrastructure provisioning (greenfield) or you need an existing infrastructure with an existing terraform state (brownfield) to reproduce the bug ? 22 | multiple: false 23 | options: 24 | - greenfield 25 | - brownfield 26 | validations: 27 | required: true 28 | - type: input 29 | id: terraform 30 | attributes: 31 | label: Terraform Version 32 | description: Which Terraform version are you using? 33 | placeholder: Example value, 1.2.8 34 | validations: 35 | required: true 36 | - type: input 37 | id: module 38 | attributes: 39 | label: Module Version 40 | description: Which module version are you using? 41 | placeholder: Example value, 6.0.0 42 | validations: 43 | required: true 44 | - type: input 45 | id: azurerm 46 | attributes: 47 | label: AzureRM Provider Version 48 | description: Which AzureRM Provider version are you using? 49 | placeholder: Example value, 3.21.1 50 | validations: 51 | required: true 52 | - type: input 53 | id: resource 54 | attributes: 55 | label: Affected Resource(s)/Data Source(s) 56 | description: Please list the affected resources and/or data sources. 57 | placeholder: azurerm_XXXXX 58 | validations: 59 | required: true 60 | - type: textarea 61 | id: config 62 | attributes: 63 | label: Terraform Configuration Files 64 | description: | 65 | Please provide a minimal Terraform configuration that can reproduce the issue. 66 | render: hcl 67 | validations: 68 | required: true 69 | - type: textarea 70 | id: tfvars 71 | attributes: 72 | label: tfvars variables values 73 | description: | 74 | Please provide the necessary tfvars variables values to reproduce the issue. Do not share secrets or sensitive information. 75 | render: hcl 76 | validations: 77 | required: true 78 | - type: textarea 79 | id: debug 80 | attributes: 81 | label: Debug Output/Panic Output 82 | description: | 83 | For long debug logs please provide a link to a GitHub Gist containing the complete debug output. Please do NOT paste the debug output in the issue; just paste a link to the Gist. 84 | 85 | To obtain the debug output, see the [Terraform documentation on debugging](https://www.terraform.io/docs/internals/debugging.html). 86 | render: shell 87 | validations: 88 | required: true 89 | - type: textarea 90 | id: expected 91 | attributes: 92 | label: Expected Behaviour 93 | description: What should have happened? 94 | - type: textarea 95 | id: actual 96 | attributes: 97 | label: Actual Behaviour 98 | description: What actually happened? 99 | - type: textarea 100 | id: reproduce 101 | attributes: 102 | label: Steps to Reproduce 103 | description: | 104 | Please list the steps required to reproduce the issue, e.g. 105 | 106 | 1. `terraform apply` 107 | - type: input 108 | id: facts 109 | attributes: 110 | label: Important Factoids 111 | description: | 112 | Are there anything atypical about your accounts that we should know? For example: Running in a Azure China/Germany/Government? 113 | - type: textarea 114 | id: references 115 | attributes: 116 | label: References 117 | description: | 118 | Information about referencing Github Issues: https://help.github.com/articles/basic-writing-and-formatting-syntax/#referencing-issues-and-pull-requests 119 | 120 | Are there any other GitHub issues (open or closed) or pull requests that should be linked here? Such as vendor documentation? 121 | -------------------------------------------------------------------------------- /examples/startup/main.spoke2.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_resource_group" "spoke2" { 2 | location = "eastus2" 3 | name = "spoke2-${random_pet.rand.id}" 4 | } 5 | 6 | module "spoke2_vnet" { 7 | source = "Azure/subnets/azurerm" 8 | version = "1.0.0" 9 | 10 | resource_group_name = azurerm_resource_group.spoke2.name 11 | subnets = { 12 | spoke2-subnet = { 13 | address_prefixes = ["192.168.1.0/24"] 14 | route_table = { 15 | id = azurerm_route_table.spoke2.id 16 | } 17 | } 18 | } 19 | virtual_network_address_space = ["192.168.1.0/24"] 20 | virtual_network_location = azurerm_resource_group.spoke2.location 21 | virtual_network_name = "spoke2-vnet-${random_pet.rand.id}" 22 | } 23 | 24 | resource "azurerm_route_table" "spoke2" { 25 | location = azurerm_resource_group.spoke2.location 26 | name = "spoke2-rt" 27 | resource_group_name = azurerm_resource_group.spoke2.name 28 | } 29 | 30 | resource "azurerm_route" "spoke2_to_hub" { 31 | address_prefix = "192.168.0.0/16" 32 | name = "to-hub" 33 | next_hop_type = "VirtualAppliance" 34 | resource_group_name = azurerm_resource_group.spoke2.name 35 | route_table_name = azurerm_route_table.spoke2.name 36 | next_hop_in_ip_address = module.hub_mesh.virtual_networks["eastus2-hub"].hub_router_ip_address 37 | } 38 | 39 | resource "azurerm_route" "spoke2_to_hub2" { 40 | address_prefix = "10.0.0.0/8" 41 | name = "to-hub2" 42 | next_hop_type = "VirtualAppliance" 43 | resource_group_name = azurerm_resource_group.spoke2.name 44 | route_table_name = azurerm_route_table.spoke2.name 45 | next_hop_in_ip_address = module.hub_mesh.virtual_networks["eastus2-hub"].hub_router_ip_address 46 | } 47 | 48 | resource "azurerm_virtual_network_peering" "spoke2_peering" { 49 | name = "spoke2-peering" 50 | remote_virtual_network_id = module.hub_mesh.virtual_networks["eastus2-hub"].id 51 | resource_group_name = azurerm_resource_group.spoke2.name 52 | virtual_network_name = module.spoke2_vnet.vnet_name 53 | allow_forwarded_traffic = true 54 | allow_gateway_transit = true 55 | allow_virtual_network_access = true 56 | use_remote_gateways = false 57 | } 58 | 59 | resource "azurerm_virtual_network_peering" "spoke2_peering_back" { 60 | name = "spoke2-peering-back" 61 | remote_virtual_network_id = module.spoke2_vnet.vnet_id 62 | resource_group_name = module.hub_mesh.virtual_networks["eastus2-hub"].resource_group_name 63 | virtual_network_name = module.hub_mesh.virtual_networks["eastus2-hub"].name 64 | allow_forwarded_traffic = true 65 | allow_gateway_transit = true 66 | allow_virtual_network_access = true 67 | use_remote_gateways = false 68 | } 69 | 70 | resource "azurerm_public_ip" "spoke2" { 71 | allocation_method = "Static" 72 | location = azurerm_resource_group.spoke2.location 73 | name = "vm1-pip" 74 | resource_group_name = azurerm_resource_group.spoke2.name 75 | } 76 | 77 | resource "azurerm_network_interface" "spoke2" { 78 | #checkov:skip=CKV_AZURE_119:It's only for connectivity test 79 | location = azurerm_resource_group.spoke2.location 80 | name = "spoke2-machine-nic" 81 | resource_group_name = azurerm_resource_group.spoke2.name 82 | 83 | ip_configuration { 84 | name = "nic" 85 | private_ip_address_allocation = "Dynamic" 86 | public_ip_address_id = azurerm_public_ip.spoke2.id 87 | subnet_id = module.spoke2_vnet.vnet_subnets_name_id["spoke2-subnet"] 88 | } 89 | } 90 | 91 | resource "azurerm_linux_virtual_machine" "spoke2" { 92 | #checkov:skip=CKV_AZURE_50:Only for connectivity test so we use vm extension 93 | #checkov:skip=CKV_AZURE_179:Only for connectivity test so we use vm extension 94 | admin_username = "adminuser" 95 | location = azurerm_resource_group.spoke2.location 96 | name = "spoke2-machine" 97 | network_interface_ids = [ 98 | azurerm_network_interface.spoke2.id, 99 | ] 100 | resource_group_name = azurerm_resource_group.spoke2.name 101 | size = "Standard_B2ms" 102 | 103 | os_disk { 104 | caching = "ReadWrite" 105 | storage_account_type = "Standard_LRS" 106 | } 107 | admin_ssh_key { 108 | public_key = tls_private_key.key.public_key_openssh 109 | username = "adminuser" 110 | } 111 | source_image_reference { 112 | offer = "0001-com-ubuntu-server-jammy" 113 | publisher = "Canonical" 114 | sku = "22_04-lts" 115 | version = "latest" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /examples/startup/TestRecord.md: -------------------------------------------------------------------------------- 1 | ## 07 May 23 00:42 UTC 2 | 3 | Success: false 4 | 5 | ### Versions 6 | 7 | Terraform v1.4.5 8 | on linux_amd64 9 | + provider registry.terraform.io/hashicorp/azurerm v3.55.0 10 | + provider registry.terraform.io/hashicorp/local v2.3.0 11 | + provider registry.terraform.io/hashicorp/random v3.5.1 12 | + provider registry.terraform.io/hashicorp/tls v4.0.4 13 | 14 | ### Error 15 | 16 | 17 | 18 | --- 19 | 20 | ## 02 May 23 13:52 UTC 21 | 22 | Success: true 23 | 24 | ### Versions 25 | 26 | Terraform v1.4.5 27 | on linux_amd64 28 | + provider registry.terraform.io/hashicorp/azurerm v3.54.0 29 | + provider registry.terraform.io/hashicorp/local v2.3.0 30 | + provider registry.terraform.io/hashicorp/random v3.5.1 31 | + provider registry.terraform.io/hashicorp/tls v4.0.4 32 | 33 | ### Error 34 | 35 | 36 | 37 | --- 38 | 39 | ## 30 Apr 23 00:44 UTC 40 | 41 | Success: true 42 | 43 | ### Versions 44 | 45 | Terraform v1.4.5 46 | on linux_amd64 47 | + provider registry.terraform.io/hashicorp/azurerm v3.54.0 48 | + provider registry.terraform.io/hashicorp/local v2.3.0 49 | + provider registry.terraform.io/hashicorp/random v3.5.1 50 | + provider registry.terraform.io/hashicorp/tls v4.0.4 51 | 52 | ### Error 53 | 54 | 55 | 56 | --- 57 | 58 | ## 25 Apr 23 14:52 UTC 59 | 60 | Success: true 61 | 62 | ### Versions 63 | 64 | Terraform v1.4.4 65 | on linux_amd64 66 | + provider registry.terraform.io/hashicorp/azurerm v3.53.0 67 | + provider registry.terraform.io/hashicorp/local v2.3.0 68 | + provider registry.terraform.io/hashicorp/random v3.5.1 69 | + provider registry.terraform.io/hashicorp/tls v4.0.4 70 | 71 | ### Error 72 | 73 | 74 | 75 | --- 76 | 77 | ## 23 Apr 23 00:45 UTC 78 | 79 | Success: true 80 | 81 | ### Versions 82 | 83 | Terraform v1.4.4 84 | on linux_amd64 85 | + provider registry.terraform.io/hashicorp/azurerm v3.53.0 86 | + provider registry.terraform.io/hashicorp/local v2.3.0 87 | + provider registry.terraform.io/hashicorp/random v3.5.1 88 | + provider registry.terraform.io/hashicorp/tls v4.0.4 89 | 90 | ### Error 91 | 92 | 93 | 94 | --- 95 | 96 | ## 16 Apr 23 00:45 UTC 97 | 98 | Success: true 99 | 100 | ### Versions 101 | 102 | Terraform v1.4.3 103 | on linux_amd64 104 | + provider registry.terraform.io/hashicorp/azurerm v3.52.0 105 | + provider registry.terraform.io/hashicorp/local v2.3.0 106 | + provider registry.terraform.io/hashicorp/random v3.5.1 107 | + provider registry.terraform.io/hashicorp/tls v4.0.4 108 | 109 | ### Error 110 | 111 | 112 | 113 | --- 114 | 115 | ## 23 Mar 23 08:22 UTC 116 | 117 | Success: true 118 | 119 | ### Versions 120 | 121 | Terraform v1.4.1 122 | on linux_amd64 123 | + provider registry.terraform.io/hashicorp/azurerm v3.48.0 124 | + provider registry.terraform.io/hashicorp/local v2.3.0 125 | + provider registry.terraform.io/hashicorp/random v3.4.3 126 | + provider registry.terraform.io/hashicorp/tls v4.0.4 127 | 128 | ### Error 129 | 130 | 131 | 132 | --- 133 | 134 | ## 09 Apr 23 00:43 UTC 135 | 136 | Success: true 137 | 138 | ### Versions 139 | 140 | Terraform v1.4.2 141 | on linux_amd64 142 | + provider registry.terraform.io/hashicorp/azurerm v3.51.0 143 | + provider registry.terraform.io/hashicorp/local v2.3.0 144 | + provider registry.terraform.io/hashicorp/random v3.4.3 145 | + provider registry.terraform.io/hashicorp/tls v4.0.4 146 | 147 | ### Error 148 | 149 | 150 | 151 | --- 152 | 153 | ## 02 Apr 23 00:42 UTC 154 | 155 | Success: true 156 | 157 | ### Versions 158 | 159 | Terraform v1.4.1 160 | on linux_amd64 161 | + provider registry.terraform.io/hashicorp/azurerm v3.50.0 162 | + provider registry.terraform.io/hashicorp/local v2.3.0 163 | + provider registry.terraform.io/hashicorp/random v3.4.3 164 | + provider registry.terraform.io/hashicorp/tls v4.0.4 165 | 166 | ### Error 167 | 168 | 169 | 170 | --- 171 | 172 | ## 26 Mar 23 00:42 UTC 173 | 174 | Success: true 175 | 176 | ### Versions 177 | 178 | Terraform v1.4.1 179 | on linux_amd64 180 | + provider registry.terraform.io/hashicorp/azurerm v3.49.0 181 | + provider registry.terraform.io/hashicorp/local v2.3.0 182 | + provider registry.terraform.io/hashicorp/random v3.4.3 183 | + provider registry.terraform.io/hashicorp/tls v4.0.4 184 | 185 | ### Error 186 | 187 | 188 | 189 | --- 190 | 191 | ## 19 Mar 23 00:51 UTC 192 | 193 | Success: true 194 | 195 | ### Versions 196 | 197 | Terraform v1.4.0 198 | on linux_amd64 199 | + provider registry.terraform.io/hashicorp/azurerm v3.48.0 200 | + provider registry.terraform.io/hashicorp/local v2.3.0 201 | + provider registry.terraform.io/hashicorp/random v3.4.3 202 | + provider registry.terraform.io/hashicorp/tls v4.0.4 203 | 204 | ### Error 205 | 206 | 207 | 208 | --- 209 | 210 | ## 03 Mar 23 10:40 UTC 211 | 212 | Success: true 213 | 214 | ### Versions 215 | 216 | Terraform v1.3.8 217 | on linux_amd64 218 | + provider registry.terraform.io/hashicorp/azurerm v3.46.0 219 | + provider registry.terraform.io/hashicorp/local v2.3.0 220 | + provider registry.terraform.io/hashicorp/random v3.4.3 221 | + provider registry.terraform.io/hashicorp/tls v4.0.4 222 | 223 | ### Error 224 | 225 | 226 | 227 | --- 228 | 229 | -------------------------------------------------------------------------------- /test/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Azure/terraform-azure-hubandspoke/test 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/Azure/terraform-module-test-helper v0.14.0 7 | github.com/ahmetb/go-linq/v3 v3.2.0 8 | github.com/gruntwork-io/terratest v0.43.8 9 | github.com/mitchellh/mapstructure v1.5.0 10 | github.com/stretchr/testify v1.8.4 11 | github.com/thanhpk/randstr v1.0.6 12 | ) 13 | 14 | require ( 15 | cloud.google.com/go v0.110.0 // indirect 16 | cloud.google.com/go/compute v1.18.0 // indirect 17 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 18 | cloud.google.com/go/iam v0.12.0 // indirect 19 | cloud.google.com/go/storage v1.29.0 // indirect 20 | github.com/agext/levenshtein v1.2.3 // indirect 21 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect 22 | github.com/aws/aws-sdk-go v1.44.220 // indirect 23 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect 24 | github.com/boombuler/barcode v1.0.1 // indirect 25 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 26 | github.com/davecgh/go-spew v1.1.1 // indirect 27 | github.com/emicklei/go-restful/v3 v3.10.2 // indirect 28 | github.com/go-errors/errors v1.4.2 // indirect 29 | github.com/go-logr/logr v1.2.3 // indirect 30 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 31 | github.com/go-openapi/jsonreference v0.20.2 // indirect 32 | github.com/go-openapi/swag v0.22.3 // indirect 33 | github.com/go-sql-driver/mysql v1.7.0 // indirect 34 | github.com/gogo/protobuf v1.3.2 // indirect 35 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 36 | github.com/golang/protobuf v1.5.3 // indirect 37 | github.com/google/gnostic v0.6.9 // indirect 38 | github.com/google/go-cmp v0.5.9 // indirect 39 | github.com/google/go-github/v42 v42.0.0 // indirect 40 | github.com/google/go-querystring v1.1.0 // indirect 41 | github.com/google/gofuzz v1.2.0 // indirect 42 | github.com/google/uuid v1.3.0 // indirect 43 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect 44 | github.com/googleapis/gax-go/v2 v2.7.1 // indirect 45 | github.com/gruntwork-io/go-commons v0.15.0 // indirect 46 | github.com/hashicorp/errwrap v1.1.0 // indirect 47 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 48 | github.com/hashicorp/go-getter v1.7.1 // indirect 49 | github.com/hashicorp/go-getter/v2 v2.2.1 // indirect 50 | github.com/hashicorp/go-multierror v1.1.1 // indirect 51 | github.com/hashicorp/go-safetemp v1.0.0 // indirect 52 | github.com/hashicorp/go-version v1.6.0 // indirect 53 | github.com/hashicorp/hcl v1.0.0 // indirect 54 | github.com/hashicorp/hcl/v2 v2.16.2 // indirect 55 | github.com/hashicorp/terraform-config-inspect v0.0.0-20230313152339-7c9946b1df49 // indirect 56 | github.com/hashicorp/terraform-json v0.16.0 // indirect 57 | github.com/imdario/mergo v0.3.13 // indirect 58 | github.com/jinzhu/copier v0.3.5 // indirect 59 | github.com/jmespath/go-jmespath v0.4.0 // indirect 60 | github.com/josharian/intern v1.0.0 // indirect 61 | github.com/json-iterator/go v1.1.12 // indirect 62 | github.com/klauspost/compress v1.16.3 // indirect 63 | github.com/lonegunmanb/tfmodredirector v0.1.0 // indirect 64 | github.com/magodo/hclgrep v0.0.0-20220303061548-1b2b24c7caf6 // indirect 65 | github.com/mailru/easyjson v0.7.7 // indirect 66 | github.com/mattn/go-zglob v0.0.4 // indirect 67 | github.com/minamijoyo/hcledit v0.2.6 // indirect 68 | github.com/mitchellh/go-homedir v1.1.0 // indirect 69 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 70 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 71 | github.com/moby/spdystream v0.2.0 // indirect 72 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 73 | github.com/modern-go/reflect2 v1.0.2 // indirect 74 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 75 | github.com/pmezard/go-difflib v1.0.0 // indirect 76 | github.com/pquerna/otp v1.4.0 // indirect 77 | github.com/r3labs/diff/v3 v3.0.1 // indirect 78 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 79 | github.com/spf13/afero v1.9.5 // indirect 80 | github.com/spf13/pflag v1.0.5 // indirect 81 | github.com/tmccombs/hcl2json v0.5.0 // indirect 82 | github.com/ulikunitz/xz v0.5.11 // indirect 83 | github.com/urfave/cli/v2 v2.25.0 // indirect 84 | github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect 85 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 86 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 87 | github.com/zclconf/go-cty v1.13.0 // indirect 88 | go.opencensus.io v0.24.0 // indirect 89 | golang.org/x/crypto v0.14.0 // indirect 90 | golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 // indirect 91 | golang.org/x/mod v0.10.0 // indirect 92 | golang.org/x/net v0.17.0 // indirect 93 | golang.org/x/oauth2 v0.8.0 // indirect 94 | golang.org/x/sys v0.13.0 // indirect 95 | golang.org/x/term v0.13.0 // indirect 96 | golang.org/x/text v0.13.0 // indirect 97 | golang.org/x/time v0.3.0 // indirect 98 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 99 | google.golang.org/api v0.112.0 // indirect 100 | google.golang.org/appengine v1.6.7 // indirect 101 | google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect 102 | google.golang.org/grpc v1.53.0 // indirect 103 | google.golang.org/protobuf v1.31.0 // indirect 104 | gopkg.in/inf.v0 v0.9.1 // indirect 105 | gopkg.in/yaml.v2 v2.4.0 // indirect 106 | gopkg.in/yaml.v3 v3.0.1 // indirect 107 | k8s.io/api v0.27.2 // indirect 108 | k8s.io/apimachinery v0.27.2 // indirect 109 | k8s.io/client-go v0.27.2 // indirect 110 | k8s.io/klog/v2 v2.90.1 // indirect 111 | k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect 112 | k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 // indirect 113 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 114 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 115 | sigs.k8s.io/yaml v1.3.0 // indirect 116 | ) 117 | -------------------------------------------------------------------------------- /locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | firewalls = { 3 | for vnet_name, vnet in var.hub_virtual_networks : vnet_name => { 4 | name = coalesce(vnet.firewall.name, "afw-${vnet_name}") 5 | sku_name = vnet.firewall.sku_name 6 | sku_tier = vnet.firewall.sku_tier 7 | subnet_address_prefix = vnet.firewall.subnet_address_prefix 8 | subnet_route_table_id = vnet.firewall.subnet_route_table_id 9 | dns_servers = vnet.firewall.dns_servers 10 | firewall_policy_id = vnet.firewall.firewall_policy_id 11 | private_ip_ranges = vnet.firewall.private_ip_ranges 12 | tags = vnet.firewall.tags 13 | threat_intel_mode = vnet.firewall.threat_intel_mode 14 | default_ip_configuration = { 15 | name = try(coalesce(vnet.firewall.management_ip_configuration.name, "default"), "default") 16 | } 17 | management_ip_configuration = { 18 | name = try(coalesce(vnet.firewall.management_ip_configuration.name, "defaultMgmt"), "defaultMgmt") 19 | } 20 | zones = vnet.firewall.zones 21 | } if vnet.firewall != null 22 | } 23 | firewall_management_subnets = { 24 | for k, v in var.hub_virtual_networks : k => { 25 | address_prefixes = [v.firewall.management_subnet_address_prefix] 26 | name = "AzureFirewallManagementSubnet" 27 | resource_group_name = v.resource_group_name 28 | virtual_network_name = v.name 29 | } 30 | if try(v.firewall.sku_tier, "FirewallNull") == "Basic" && v.firewall != null 31 | } 32 | fw_default_ip_configuration_pip = { 33 | for vnet_name, vnet in var.hub_virtual_networks : vnet_name => { 34 | location = local.virtual_networks_modules[vnet_name].vnet_location 35 | name = try(vnet.firewall.default_ip_configuration.public_ip_config.name, "pip-afw-${vnet_name}") 36 | resource_group_name = vnet.resource_group_name 37 | tags = try(vnet.firewall.default_ip_configuration.tags, null) 38 | ip_version = try(vnet.firewall.default_ip_configuration.public_ip_config.ip_version, "IPv4") 39 | sku_tier = try(vnet.firewall.default_ip_configuration.public_ip_config.sku_tier, "Regional") 40 | zones = try(vnet.firewall.default_ip_configuration.public_ip_config.zones, null) 41 | } if vnet.firewall != null 42 | } 43 | fw_management_ip_configuration_pip = { 44 | for k, v in var.hub_virtual_networks : k => { 45 | location = local.virtual_networks_modules[k].vnet_location 46 | name = try(v.firewall.management_ip_configuration.public_ip_config.name, "pip-afw-mgmt-${k}") 47 | resource_group_name = v.resource_group_name 48 | tags = try(v.firewall.management_ip_configuration.tags, null) 49 | ip_version = try(v.firewall.management_ip_configuration.public_ip_config.ip_version, "IPv4") 50 | sku_tier = try(v.firewall.management_ip_configuration.public_ip_config.sku_tier, "Regional") 51 | zones = try(v.firewall.management_ip_configuration.public_ip_config.zones, null) 52 | } if try(v.firewall.sku_tier, "FirewallNull") == "Basic" && v.firewall != null 53 | } 54 | hub_peering_map = { 55 | for peerconfig in flatten([ 56 | for k_src, v_src in var.hub_virtual_networks : 57 | [ 58 | for k_dst, v_dst in var.hub_virtual_networks : 59 | { 60 | name = "${local.virtual_networks_modules[k_src].vnet_name}-${local.virtual_networks_modules[k_dst].vnet_name}" 61 | src_key = k_src 62 | dst_key = k_dst 63 | virtual_network_name = local.virtual_networks_modules[k_src].vnet_name 64 | remote_virtual_network_id = local.virtual_networks_modules[k_dst].vnet_id 65 | allow_virtual_network_access = true 66 | allow_forwarded_traffic = true 67 | allow_gateway_transit = true 68 | use_remote_gateways = false 69 | } if k_src != k_dst && v_dst.mesh_peering_enabled 70 | ] if v_src.mesh_peering_enabled 71 | ]) : peerconfig.name => peerconfig 72 | } 73 | resource_group_data = toset([ 74 | for k, v in var.hub_virtual_networks : { 75 | name = v.resource_group_name 76 | location = v.location 77 | lock = v.resource_group_lock_enabled 78 | lock_name = v.resource_group_lock_name 79 | tags = v.resource_group_tags 80 | } if v.resource_group_creation_enabled 81 | ]) 82 | route_map = { 83 | for k_src, v_src in var.hub_virtual_networks : k_src => { 84 | mesh_routes = flatten([ 85 | # Generated routes for hub mesh 86 | for k_dst, v_dst in var.hub_virtual_networks : [ 87 | for cidr in v_dst.routing_address_space : { 88 | name = "${k_dst}-${replace(cidr, "/", "-")}" 89 | address_prefix = cidr 90 | next_hop_type = "VirtualAppliance" 91 | next_hop_ip_address = try(local.firewall_private_ip[k_dst], v_dst.hub_router_ip_address) 92 | } 93 | ] if k_src != k_dst && v_dst.mesh_peering_enabled && can(v_dst.routing_address_space[0]) 94 | ]) 95 | user_routes = v_src.route_table_entries 96 | } 97 | } 98 | subnet_external_route_table_association_map = { 99 | for assoc in flatten([ 100 | for k, v in var.hub_virtual_networks : [ 101 | for subnetName, subnet in v.subnets : { 102 | name = "${k}-${subnetName}" 103 | subnet_id = local.virtual_networks_modules[k].vnet_subnets_name_id[subnetName] 104 | route_table_id = subnet.external_route_table_id 105 | } if subnet.external_route_table_id != null 106 | ] 107 | ]) : assoc.name => assoc 108 | } 109 | subnet_route_table_association_map = { 110 | for assoc in flatten([ 111 | for k, v in var.hub_virtual_networks : [ 112 | for subnetName, subnet in v.subnets : { 113 | name = "${k}-${subnetName}" 114 | subnet_id = local.virtual_networks_modules[k].vnet_subnets_name_id[subnetName] 115 | route_table_id = local.hub_routing[k].id 116 | } if subnet.assign_generated_route_table 117 | ] 118 | ]) : assoc.name => assoc 119 | } 120 | subnets_map = { 121 | for k, v in var.hub_virtual_networks : k => { 122 | for subnetKey, subnet in v.subnets : subnetKey => { 123 | address_prefixes = subnet.address_prefixes 124 | nat_gateway = subnet.nat_gateway 125 | network_security_group = subnet.network_security_group 126 | private_endpoint_network_policies_enabled = subnet.private_endpoint_network_policies_enabled 127 | private_link_service_network_policies_enabled = subnet.private_link_service_network_policies_enabled 128 | service_endpoints = subnet.service_endpoints 129 | service_endpoint_policy_ids = subnet.service_endpoint_policy_ids 130 | delegations = subnet.delegations 131 | } 132 | } 133 | } 134 | } 135 | 136 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | # These locals defined here to avoid conflict with test framework 2 | locals { 3 | firewall_private_ip = { 4 | for vnet_name, fw in azurerm_firewall.fw : vnet_name => fw.ip_configuration[0].private_ip_address 5 | } 6 | hub_routing = azurerm_route_table.hub_routing 7 | virtual_networks_modules = { 8 | for vnet_key, vnet_module in module.hub_virtual_networks : vnet_key => vnet_module 9 | } 10 | } 11 | 12 | # Create rgs as defined by var.hub_networks 13 | resource "azurerm_resource_group" "rg" { 14 | for_each = { for rg in local.resource_group_data : rg.name => rg } 15 | 16 | location = each.value.location 17 | name = each.key 18 | tags = each.value.tags 19 | } 20 | 21 | resource "azurerm_management_lock" "rg_lock" { 22 | for_each = { for r in local.resource_group_data : r.name => r if r.lock } 23 | 24 | lock_level = "CanNotDelete" 25 | name = coalesce(each.value.lock_name, substr("lock-${each.key}", 0, 90)) 26 | scope = azurerm_resource_group.rg[each.key].id 27 | } 28 | 29 | # Module to create virtual networks and subnets 30 | # Useful outputs: 31 | # - vnet_id - the resource id of vnet 32 | # - vnet_subnets_name_ids - a map of subnet name to subnet resource id, e.g. use lookup(module.hub_virtual_networks["key"].vnet_subnets_name_id, "subnet1") 33 | module "hub_virtual_networks" { 34 | for_each = var.hub_virtual_networks 35 | source = "Azure/subnets/azurerm" 36 | version = "1.0.0" 37 | # ... TODO add required inputs 38 | 39 | # added to make sure dependency graph is correct 40 | virtual_network_name = each.value.name 41 | virtual_network_address_space = each.value.address_space 42 | virtual_network_location = each.value.location 43 | resource_group_name = try(azurerm_resource_group.rg[each.value.resource_group_name].name, each.value.resource_group_name) 44 | virtual_network_bgp_community = each.value.bgp_community 45 | virtual_network_ddos_protection_plan = each.value.ddos_protection_plan_id == null ? null : { 46 | id = each.value.ddos_protection_plan_id 47 | enable = true 48 | } 49 | virtual_network_dns_servers = each.value.dns_servers == null ? null : { 50 | dns_servers = each.value.dns_servers 51 | } 52 | virtual_network_flow_timeout_in_minutes = each.value.flow_timeout_in_minutes 53 | virtual_network_tags = each.value.tags 54 | subnets = try(local.subnets_map[each.key], {}) 55 | } 56 | 57 | resource "azurerm_virtual_network_peering" "hub_peering" { 58 | for_each = local.hub_peering_map 59 | 60 | name = each.key 61 | # added to make sure dependency graph is correct 62 | remote_virtual_network_id = each.value.remote_virtual_network_id 63 | resource_group_name = try(azurerm_resource_group.rg[var.hub_virtual_networks[each.value.src_key].resource_group_name].name, var.hub_virtual_networks[each.value.src_key].resource_group_name) 64 | virtual_network_name = each.value.virtual_network_name 65 | allow_forwarded_traffic = each.value.allow_forwarded_traffic 66 | allow_gateway_transit = each.value.allow_gateway_transit 67 | allow_virtual_network_access = each.value.allow_virtual_network_access 68 | use_remote_gateways = each.value.use_remote_gateways 69 | } 70 | 71 | resource "azurerm_route_table" "hub_routing" { 72 | for_each = local.route_map 73 | 74 | location = var.hub_virtual_networks[each.key].location 75 | name = coalesce(var.hub_virtual_networks[each.key].route_table_name, "route-${each.key}") 76 | resource_group_name = try(azurerm_resource_group.rg[var.hub_virtual_networks[each.key].resource_group_name].name, var.hub_virtual_networks[each.key].resource_group_name) 77 | disable_bgp_route_propagation = false 78 | tags = var.hub_virtual_networks[each.key].route_table_tags 79 | 80 | route { 81 | address_prefix = "0.0.0.0/0" 82 | name = "internet" 83 | next_hop_type = "Internet" 84 | } 85 | dynamic "route" { 86 | for_each = toset(each.value.mesh_routes) 87 | 88 | content { 89 | address_prefix = route.value.address_prefix 90 | name = route.value.name 91 | next_hop_in_ip_address = route.value.next_hop_ip_address 92 | next_hop_type = route.value.next_hop_type 93 | } 94 | } 95 | dynamic "route" { 96 | for_each = toset(each.value.user_routes) 97 | 98 | content { 99 | address_prefix = route.value.address_prefix 100 | name = route.value.name 101 | next_hop_in_ip_address = route.value.next_hop_ip_address 102 | next_hop_type = route.value.next_hop_type 103 | } 104 | } 105 | } 106 | 107 | resource "azurerm_subnet_route_table_association" "hub_routing_creat" { 108 | for_each = local.subnet_route_table_association_map 109 | 110 | route_table_id = each.value.route_table_id 111 | subnet_id = each.value.subnet_id 112 | } 113 | 114 | resource "azurerm_subnet_route_table_association" "hub_routing_external" { 115 | for_each = local.subnet_external_route_table_association_map 116 | 117 | route_table_id = each.value.route_table_id 118 | subnet_id = each.value.subnet_id 119 | } 120 | 121 | resource "azurerm_public_ip" "fw_default_ip_configuration_pip" { 122 | for_each = local.fw_default_ip_configuration_pip 123 | 124 | allocation_method = "Static" 125 | location = each.value.location 126 | name = each.value.name 127 | resource_group_name = each.value.resource_group_name 128 | ip_version = each.value.ip_version 129 | sku = "Standard" 130 | sku_tier = each.value.sku_tier 131 | tags = each.value.tags 132 | zones = each.value.zones 133 | } 134 | 135 | resource "azurerm_public_ip" "fw_management_ip_configuration_pip" { 136 | for_each = local.fw_management_ip_configuration_pip 137 | 138 | allocation_method = "Static" 139 | location = each.value.location 140 | name = each.value.name 141 | resource_group_name = each.value.resource_group_name 142 | ip_version = each.value.ip_version 143 | sku = "Standard" 144 | sku_tier = each.value.sku_tier 145 | tags = each.value.tags 146 | zones = each.value.zones 147 | } 148 | 149 | resource "azurerm_subnet" "fw_subnet" { 150 | for_each = local.firewalls 151 | 152 | address_prefixes = [each.value.subnet_address_prefix] 153 | name = "AzureFirewallSubnet" 154 | resource_group_name = var.hub_virtual_networks[each.key].resource_group_name 155 | virtual_network_name = module.hub_virtual_networks[each.key].vnet_name 156 | } 157 | 158 | resource "azurerm_subnet" "fw_management_subnet" { 159 | for_each = local.firewall_management_subnets 160 | 161 | address_prefixes = each.value.address_prefixes 162 | name = each.value.name 163 | resource_group_name = each.value.resource_group_name 164 | virtual_network_name = each.value.virtual_network_name 165 | 166 | depends_on = [ 167 | module.hub_virtual_networks 168 | ] 169 | } 170 | 171 | resource "azurerm_subnet_route_table_association" "fw_subnet_routing_creat" { 172 | for_each = { for vnet_name, fw in local.firewalls : vnet_name => fw if fw.subnet_route_table_id == null } 173 | 174 | route_table_id = azurerm_route_table.hub_routing[each.key].id 175 | subnet_id = azurerm_subnet.fw_subnet[each.key].id 176 | } 177 | 178 | resource "azurerm_subnet_route_table_association" "fw_subnet_routing_external" { 179 | for_each = { for vnet_name, fw in local.firewalls : vnet_name => fw if fw.subnet_route_table_id != null } 180 | 181 | route_table_id = each.value.subnet_route_table_id 182 | subnet_id = azurerm_subnet.fw_subnet[each.key].id 183 | } 184 | 185 | resource "azurerm_firewall" "fw" { 186 | for_each = local.firewalls 187 | 188 | location = module.hub_virtual_networks[each.key].vnet_location 189 | name = each.value.name 190 | resource_group_name = var.hub_virtual_networks[each.key].resource_group_name 191 | sku_name = each.value.sku_name 192 | sku_tier = each.value.sku_tier 193 | dns_servers = each.value.dns_servers 194 | firewall_policy_id = each.value.firewall_policy_id 195 | private_ip_ranges = each.value.private_ip_ranges 196 | tags = each.value.tags 197 | threat_intel_mode = each.value.threat_intel_mode 198 | zones = each.value.zones 199 | 200 | ip_configuration { 201 | name = each.value.default_ip_configuration.name 202 | public_ip_address_id = azurerm_public_ip.fw_default_ip_configuration_pip[each.key].id 203 | subnet_id = azurerm_subnet.fw_subnet[each.key].id 204 | } 205 | 206 | dynamic "management_ip_configuration" { 207 | for_each = each.value.sku_tier == "Basic" ? ["managementIpConfiguration"] : [] 208 | 209 | content { 210 | name = each.value.management_ip_configuration.name 211 | public_ip_address_id = azurerm_public_ip.fw_management_ip_configuration_pip[each.key].id 212 | subnet_id = azurerm_subnet.fw_management_subnet[each.key].id 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "hub_virtual_networks" { 2 | type = map(object({ 3 | name = string 4 | address_space = list(string) 5 | location = string 6 | resource_group_name = string 7 | route_table_name = optional(string) 8 | route_table_tags = optional(map(string)) 9 | bgp_community = optional(string) 10 | ddos_protection_plan_id = optional(string) 11 | dns_servers = optional(list(string)) 12 | flow_timeout_in_minutes = optional(number, 4) 13 | mesh_peering_enabled = optional(bool, true) 14 | resource_group_creation_enabled = optional(bool, true) 15 | resource_group_lock_enabled = optional(bool, true) 16 | resource_group_lock_name = optional(string) 17 | resource_group_tags = optional(map(string)) 18 | routing_address_space = optional(list(string), []) 19 | hub_router_ip_address = optional(string) 20 | tags = optional(map(string), {}) 21 | 22 | route_table_entries = optional(set(object({ 23 | name = string 24 | address_prefix = string 25 | next_hop_type = string 26 | 27 | has_bgp_override = optional(bool, false) 28 | next_hop_ip_address = optional(string) 29 | })), []) 30 | 31 | subnets = optional(map(object( 32 | { 33 | address_prefixes = list(string) 34 | nat_gateway = optional(object({ 35 | id = string 36 | })) 37 | network_security_group = optional(object({ 38 | id = string 39 | })) 40 | private_endpoint_network_policies_enabled = optional(bool, true) 41 | private_link_service_network_policies_enabled = optional(bool, true) 42 | assign_generated_route_table = optional(bool, true) 43 | external_route_table_id = optional(string) 44 | service_endpoints = optional(set(string)) 45 | service_endpoint_policy_ids = optional(set(string)) 46 | delegations = optional(list( 47 | object( 48 | { 49 | name = string 50 | service_delegation = object({ 51 | name = string 52 | actions = optional(list(string)) 53 | }) 54 | } 55 | ) 56 | )) 57 | } 58 | )), {}) 59 | 60 | firewall = optional(object({ 61 | sku_name = string 62 | sku_tier = string 63 | subnet_address_prefix = string 64 | dns_servers = optional(list(string)) 65 | firewall_policy_id = optional(string) 66 | management_subnet_address_prefix = optional(string, null) 67 | name = optional(string) 68 | private_ip_ranges = optional(list(string)) 69 | subnet_route_table_id = optional(string) 70 | tags = optional(map(string)) 71 | threat_intel_mode = optional(string, "Alert") 72 | zones = optional(list(string)) 73 | default_ip_configuration = optional(object({ 74 | name = optional(string) 75 | tags = optional(map(string)) 76 | public_ip_config = optional(object({ 77 | ip_version = optional(string) 78 | name = optional(string) 79 | sku_tier = optional(string, "Regional") 80 | zones = optional(set(string)) 81 | })) 82 | })) 83 | management_ip_configuration = optional(object({ 84 | name = optional(string) 85 | tags = optional(map(string)) 86 | public_ip_config = optional(object({ 87 | ip_version = optional(string) 88 | name = optional(string) 89 | sku_tier = optional(string, "Regional") 90 | zones = optional(set(string)) 91 | })) 92 | })) 93 | })) 94 | })) 95 | default = {} 96 | description = <`. 181 | - `zones` - (Optional) A list of availability zones to use for the public IP configuration. If not specified will be `null`. 182 | - `ip_version` - (Optional) The IP version to use for the public IP configuration. Possible values include `IPv4`, `IPv6`. If not specified will be `IPv4`. 183 | - `sku_tier` - (Optional) The SKU tier to use for the public IP configuration. Possible values include `Regional`, `Global`. If not specified will be `Regional`. 184 | DESCRIPTION 185 | nullable = false 186 | 187 | validation { 188 | condition = alltrue([for k, v in var.hub_virtual_networks : v.firewall.sku_tier == "Basic" ? length(regexall("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/\\d{1,2}$", coalesce(v.firewall.management_subnet_address_prefix, "NonIp"))) > 0 : true if v.firewall != null]) 189 | error_message = "A valid management_subnet_address_prefix must be specified when using Basic SKU for Azure Firewall." 190 | } 191 | validation { 192 | condition = alltrue([for k, v in var.hub_virtual_networks : contains(["AZFW_VNet"], v.firewall.sku_name) if v.firewall != null]) 193 | error_message = "Azure Firewall SKU must be AZFW_VNet." 194 | } 195 | validation { 196 | condition = alltrue(flatten([for v_src in var.hub_virtual_networks : [for v_dst in var.hub_virtual_networks : coalesce(v_dst.hub_router_ip_address, "") != "" if v_dst.firewall == null && v_dst.routing_address_space != null && v_src != v_dst]])) 197 | error_message = "A valid hub_router_ip_address must be provided if there is no Firewall in the remote hub but routing_address_space is specified in the remote hub." 198 | } 199 | } 200 | 201 | # tflint-ignore: terraform_unused_declarations 202 | variable "tracing_tags_enabled" { 203 | type = bool 204 | description = "Whether enable tracing tags that generated by BridgeCrew Yor." 205 | default = false 206 | nullable = false 207 | } 208 | 209 | # tflint-ignore: terraform_unused_declarations 210 | variable "tracing_tags_prefix" { 211 | type = string 212 | description = "Default prefix for generated tracing tags" 213 | default = "avm_" 214 | nullable = false 215 | } 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Terraform Verified Module for multi-hub network architectures 3 | 4 | [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/Azure/terraform-azure-hubnetworking.svg)](http://isitmaintained.com/project/Azure/terraform-azure-hubnetworking "Average time to resolve an issue") 5 | [![Percentage of issues still open](http://isitmaintained.com/badge/open/Azure/terraform-azure-hubnetworking.svg)](http://isitmaintained.com/project/Azure/terraform-azure-hubnetworking "Percentage of issues still open") 6 | 7 | This module is designed to simplify the creation of multi-region hub networks in Azure. It will create a number of virtual networks and subnets, and optionally peer them together in a mesh topology with routing. 8 | 9 | ## Features 10 | 11 | - This module will deploy `n` number of virtual networks and subnets. 12 | Optionally, these virtual networks can be peered in a mesh topology. 13 | - A routing address space can be specified for each hub network, this module will then create route tables for the other hub networks and associate them with the subnets. 14 | - Azure Firewall can be deployed iun each hub network. This module will configure routing for the AzureFirewallSubnet. 15 | 16 | ## Example 17 | 18 | ```terraform 19 | module "hubnetworks" { 20 | source = "Azure/hubnetworking/azure" 21 | version = "" # change this to your desired version, https://www.terraform.io/language/expressions/version-constraints 22 | 23 | hub_virtual_networks = { 24 | weu-hub = { 25 | name = "vnet-prod-weu-0001" 26 | address_space = ["192.168.0.0/23"] 27 | routing_address_space = ["192.168.0.0/20"] 28 | firewall = { 29 | subnet_address_prefix = "192.168.1.0/24" 30 | sku_tier = "Premium" 31 | sku_name = "AZFW_VNet" 32 | } 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | ## Documentation 39 | 40 | 41 | ## Requirements 42 | 43 | The following requirements are needed by this module: 44 | 45 | - [terraform](#requirement\_terraform) (>= 1.3.0) 46 | 47 | - [azurerm](#requirement\_azurerm) (>= 3.7.0, < 4.0) 48 | 49 | ## Modules 50 | 51 | The following Modules are called: 52 | 53 | ### [hub\_virtual\_networks](#module\_hub\_virtual\_networks) 54 | 55 | Source: Azure/subnets/azurerm 56 | 57 | Version: 1.0.0 58 | 59 | 60 | ## Required Inputs 61 | 62 | No required inputs. 63 | 64 | ## Optional Inputs 65 | 66 | The following input variables are optional (have default values): 67 | 68 | ### [hub\_virtual\_networks](#input\_hub\_virtual\_networks) 69 | 70 | Description: A map of the hub virtual networks to create. The map key is an arbitrary value to avoid Terraform's restriction that map keys must be known at plan time. 71 | 72 | ### Mandatory fields 73 | 74 | - `name` - The name of the Virtual Network. 75 | - `address_space` - A list of IPv4 address spaces that are used by this virtual network in CIDR format, e.g. `["192.168.0.0/24"]`. 76 | - `location` - The Azure location where the virtual network should be created. 77 | - `resource_group_name` - The name of the resource group in which the virtual network should be created. 78 | 79 | ### Optional fields 80 | 81 | - `bgp_community` - The BGP community associated with the virtual network. 82 | - `ddos_protection_plan_id` - The ID of the DDoS protection plan associated with the virtual network. 83 | - `dns_servers` - A list of DNS servers IP addresses for the virtual network. 84 | - `flow_timeout_in_minutes` - The flow timeout in minutes for the virtual network. Default `4`. 85 | - `mesh_peering_enabled` - Should the virtual network be peered to other hub networks with this flag enabled? Default `true`. 86 | - `resource_group_creation_enabled` - Should the resource group for this virtual network be created by this module? Default `true`. 87 | - `resource_group_lock_enabled` - Should the resource group for this virtual network be locked? Default `true`. 88 | - `resource_group_lock_name` - The name of the resource group lock. 89 | - `resource_group_tags` - A map of tags to apply to the resource group. 90 | - `routing_address_space` - A list of IPv4 address spaces in CIDR format that are used for routing to this hub, e.g. `["192.168.0.0","172.16.0.0/12"]`. 91 | - `hub_router_ip_address` - If not using Azure Firewall, this is the IP address of the hub router. This is used to create route table entries for other hub networks. 92 | - `tags` - A map of tags to apply to the virtual network. 93 | 94 | #### Route table entries 95 | 96 | - `route_table_entries` - (Optional) A set of additional route table entries to add to the route table for this hub network. Default empty `[]`. The value is an object with the following fields: 97 | - `name` - The name of the route table entry. 98 | - `address_prefix` - The address prefix to match for this route table entry. 99 | - `next_hop_type` - The type of the next hop. Possible values include `Internet`, `VirtualAppliance`, `VirtualNetworkGateway`, `VnetLocal`, `None`. 100 | - `has_bgp_override` - Should the BGP override be enabled for this route table entry? Default `false`. 101 | - `next_hop_ip_address` - The IP address of the next hop. Required if `next_hop_type` is `VirtualAppliance`. 102 | 103 | #### Subnets 104 | 105 | - `subnets` - (Optional) A map of subnets to create in the virtual network. The value is an object with the following fields: 106 | - `address_prefixes` - The IPv4 address prefixes to use for the subnet in CIDR format. 107 | - `nat_gateway` - (Optional) An object with the following fields: 108 | - `id` - The ID of the NAT Gateway which should be associated with the Subnet. Changing this forces a new resource to be created. 109 | - `network_security_group` - (Optional) An object with the following fields: 110 | - `id` - The ID of the Network Security Group which should be associated with the Subnet. Changing this forces a new association to be created. 111 | - `private_endpoint_network_policies_enabled` - (Optional) Enable or Disable network policies for the private endpoint on the subnet. Setting this to true will Enable the policy and setting this to false will Disable the policy. Defaults to true. 112 | - `private_link_service_network_policies_enabled` - (Optional) Enable or Disable network policies for the private link service on the subnet. Setting this to true will Enable the policy and setting this to false will Disable the policy. Defaults to true. 113 | - `assign_generated_route_table` - (Optional) Should the Route Table generated by this module be associated with this Subnet? Default `true`. Cannot be used with `external_route_table_id`. 114 | - `external_route_table_id` - (Optional) The ID of the Route Table which should be associated with the Subnet. Changing this forces a new association to be created. Cannot be used with `assign_generated_route_table`. 115 | - `service_endpoints` - (Optional) The list of Service endpoints to associate with the subnet. 116 | - `service_endpoint_policy_ids` - (Optional) The list of Service Endpoint Policy IDs to associate with the subnet. 117 | - `service_endpoint_policy_assignment_enabled` - (Optional) Should the Service Endpoint Policy be assigned to the subnet? Default `true`. 118 | - `delegation` - (Optional) An object with the following fields: 119 | - `name` - The name of the delegation. 120 | - `service_delegation` - An object with the following fields: 121 | - `name` - The name of the service delegation. 122 | - `actions` - A list of actions that should be delegated, the list is specific to the service being delegated. 123 | 124 | #### Azure Firewall 125 | 126 | - `firewall` - (Optional) An object with the following fields: 127 | - `sku_name` - The name of the SKU to use for the Azure Firewall. Possible values include `AZFW_Hub`, `AZFW_VNet`. 128 | - `sku_tier` - The tier of the SKU to use for the Azure Firewall. Possible values include `Basic`, `Standard`, `Premium`. 129 | - `subnet_address_prefix` - The IPv4 address prefix to use for the Azure Firewall subnet in CIDR format. Needs to be a part of the virtual network's address space. 130 | - `dns_servers` - (Optional) A list of DNS server IP addresses for the Azure Firewall. 131 | - `firewall_policy_id` - (Optional) The resource id of the Azure Firewall Policy to associate with the Azure Firewall. 132 | - `management_subnet_address_prefix` - (Optional) The IPv4 address prefix to use for the Azure Firewall management subnet in CIDR format. Needs to be a part of the virtual network's address space. 133 | - `name` - (Optional) The name of the firewall resource. If not specified will use `afw-{vnetname}`. 134 | - `private_ip_ranges` - (Optional) A list of private IP ranges to use for the Azure Firewall, to which the firewall will not NAT traffic. If not specified will use RFC1918. 135 | - `subnet_route_table_id` = (Optional) The resource id of the Route Table which should be associated with the Azure Firewall subnet. If not specified the module will assign the generated route table. 136 | - `tags` - (Optional) A map of tags to apply to the Azure Firewall. If not specified 137 | - `threat_intel_mode` - (Optional) The threat intelligence mode for the Azure Firewall. Possible values include `Alert`, `Deny`, `Off`. 138 | - `zones` - (Optional) A list of availability zones to use for the Azure Firewall. If not specified will be `null`. 139 | - `default_ip_configuration` - (Optional) An object with the following fields. If not specified the defaults below will be used: 140 | - `name` - (Optional) The name of the default IP configuration. If not specified will use `default`. 141 | - `tags` - (Optional) A map of tags to apply to the public IP configuration. 142 | - `public_ip_config` - (Optional) An object with the following fields: 143 | - `name` - (Optional) The name of the public IP configuration. If not specified will use `pip-afw-{vnetname}`. 144 | - `zones` - (Optional) A list of availability zones to use for the public IP configuration. If not specified will be `null`. 145 | - `ip_version` - (Optional) The IP version to use for the public IP configuration. Possible values include `IPv4`, `IPv6`. If not specified will be `IPv4`. 146 | - `sku_tier` - (Optional) The SKU tier to use for the public IP configuration. Possible values include `Regional`, `Global`. If not specified will be `Regional`. 147 | - `management_ip_configuration` - (Optional) An object with the following fields. If not specified the defaults below will be used: 148 | - `name` - (Optional) The name of the management IP configuration. If not specified will use `defaultMgmt`. 149 | - `tags` - (Optional) A map of tags to apply to the public IP configuration. 150 | - `public_ip_config` - (Optional) An object with the following fields: 151 | - `name` - (Optional) The name of the public IP configuration. If not specified will use `pip-afw-mgmt-`. 152 | - `zones` - (Optional) A list of availability zones to use for the public IP configuration. If not specified will be `null`. 153 | - `ip_version` - (Optional) The IP version to use for the public IP configuration. Possible values include `IPv4`, `IPv6`. If not specified will be `IPv4`. 154 | - `sku_tier` - (Optional) The SKU tier to use for the public IP configuration. Possible values include `Regional`, `Global`. If not specified will be `Regional`. 155 | 156 | Type: 157 | 158 | ```hcl 159 | map(object({ 160 | name = string 161 | address_space = list(string) 162 | location = string 163 | resource_group_name = string 164 | route_table_name = optional(string) 165 | bgp_community = optional(string) 166 | ddos_protection_plan_id = optional(string) 167 | dns_servers = optional(list(string)) 168 | flow_timeout_in_minutes = optional(number, 4) 169 | mesh_peering_enabled = optional(bool, true) 170 | resource_group_creation_enabled = optional(bool, true) 171 | resource_group_lock_enabled = optional(bool, true) 172 | resource_group_lock_name = optional(string) 173 | resource_group_tags = optional(map(string)) 174 | routing_address_space = optional(list(string), []) 175 | hub_router_ip_address = optional(string) 176 | tags = optional(map(string), {}) 177 | 178 | route_table_entries = optional(set(object({ 179 | name = string 180 | address_prefix = string 181 | next_hop_type = string 182 | 183 | has_bgp_override = optional(bool, false) 184 | next_hop_ip_address = optional(string) 185 | })), []) 186 | 187 | subnets = optional(map(object( 188 | { 189 | address_prefixes = list(string) 190 | nat_gateway = optional(object({ 191 | id = string 192 | })) 193 | network_security_group = optional(object({ 194 | id = string 195 | })) 196 | private_endpoint_network_policies_enabled = optional(bool, true) 197 | private_link_service_network_policies_enabled = optional(bool, true) 198 | assign_generated_route_table = optional(bool, true) 199 | external_route_table_id = optional(string) 200 | service_endpoints = optional(set(string)) 201 | service_endpoint_policy_ids = optional(set(string)) 202 | delegations = optional(list( 203 | object( 204 | { 205 | name = string 206 | service_delegation = object({ 207 | name = string 208 | actions = optional(list(string)) 209 | }) 210 | } 211 | ) 212 | )) 213 | } 214 | )), {}) 215 | 216 | firewall = optional(object({ 217 | sku_name = string 218 | sku_tier = string 219 | subnet_address_prefix = string 220 | dns_servers = optional(list(string)) 221 | firewall_policy_id = optional(string) 222 | management_subnet_address_prefix = optional(string, null) 223 | name = optional(string) 224 | private_ip_ranges = optional(list(string)) 225 | subnet_route_table_id = optional(string) 226 | tags = optional(map(string)) 227 | threat_intel_mode = optional(string, "Alert") 228 | zones = optional(list(string)) 229 | default_ip_configuration = optional(object({ 230 | name = optional(string) 231 | tags = optional(string) 232 | public_ip_config = optional(object({ 233 | ip_version = optional(string) 234 | name = optional(string) 235 | sku_tier = optional(string, "Regional") 236 | zones = optional(set(string)) 237 | })) 238 | })) 239 | management_ip_configuration = optional(object({ 240 | name = optional(string) 241 | tags = optional(string) 242 | public_ip_config = optional(object({ 243 | ip_version = optional(string) 244 | name = optional(string) 245 | sku_tier = optional(string, "Regional") 246 | zones = optional(set(string)) 247 | })) 248 | })) 249 | })) 250 | })) 251 | ``` 252 | 253 | Default: `{}` 254 | 255 | ### [tracing\_tags\_enabled](#input\_tracing\_tags\_enabled) 256 | 257 | Description: Whether enable tracing tags that generated by BridgeCrew Yor. 258 | 259 | Type: `bool` 260 | 261 | Default: `false` 262 | 263 | ### [tracing\_tags\_prefix](#input\_tracing\_tags\_prefix) 264 | 265 | Description: Default prefix for generated tracing tags 266 | 267 | Type: `string` 268 | 269 | Default: `"avm_"` 270 | 271 | ## Resources 272 | 273 | The following resources are used by this module: 274 | 275 | - [azurerm_firewall.fw](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/firewall) (resource) 276 | - [azurerm_management_lock.rg_lock](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/management_lock) (resource) 277 | - [azurerm_public_ip.fw_default_ip_configuration_pip](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/public_ip) (resource) 278 | - [azurerm_public_ip.fw_management_ip_configuration_pip](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/public_ip) (resource) 279 | - [azurerm_resource_group.rg](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) (resource) 280 | - [azurerm_route_table.hub_routing](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/route_table) (resource) 281 | - [azurerm_subnet.fw_management_subnet](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet) (resource) 282 | - [azurerm_subnet.fw_subnet](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet) (resource) 283 | - [azurerm_subnet_route_table_association.fw_subnet_routing_creat](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet_route_table_association) (resource) 284 | - [azurerm_subnet_route_table_association.fw_subnet_routing_external](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet_route_table_association) (resource) 285 | - [azurerm_subnet_route_table_association.hub_routing_creat](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet_route_table_association) (resource) 286 | - [azurerm_subnet_route_table_association.hub_routing_external](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet_route_table_association) (resource) 287 | - [azurerm_virtual_network_peering.hub_peering](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network_peering) (resource) 288 | 289 | ## Outputs 290 | 291 | The following outputs are exported: 292 | 293 | ### [firewalls](#output\_firewalls) 294 | 295 | Description: A curated output of the firewalls created by this module. 296 | 297 | ### [hub\_route\_tables](#output\_hub\_route\_tables) 298 | 299 | Description: A curated output of the route tables created by this module. 300 | 301 | ### [resource\_groups](#output\_resource\_groups) 302 | 303 | Description: A curated output of the resource groups created by this module. 304 | 305 | ### [virtual\_networks](#output\_virtual\_networks) 306 | 307 | Description: A curated output of the virtual networks created by this module. 308 | 309 | 310 | ## Enable or disable tracing tags 311 | 312 | We're using [BridgeCrew Yor](https://github.com/bridgecrewio/yor) and [yorbox](https://github.com/lonegunmanb/yorbox) to help manage tags consistently across infrastructure as code (IaC) frameworks. In this module you might see tags like: 313 | 314 | ```hcl 315 | resource "azurerm_resource_group" "rg" { 316 | location = "eastus" 317 | name = random_pet.name 318 | tags = merge(var.tags, (/**/ (var.tracing_tags_enabled ? { for k, v in /**/ { 319 | avm_git_commit = "3077cc6d0b70e29b6e106b3ab98cee6740c916f6" 320 | avm_git_file = "main.tf" 321 | avm_git_last_modified_at = "2023-05-05 08:57:54" 322 | avm_git_org = "lonegunmanb" 323 | avm_git_repo = "terraform-yor-tag-test-module" 324 | avm_yor_trace = "a0425718-c57d-401c-a7d5-f3d88b2551a4" 325 | } /**/ : replace(k, "avm_", var.tracing_tags_prefix) => v } : {}) /**/)) 326 | } 327 | ``` 328 | 329 | To enable tracing tags, set the variable to true: 330 | 331 | ```hcl 332 | module "example" { 333 | source = 334 | ... 335 | tracing_tags_enabled = true 336 | } 337 | ``` 338 | 339 | The `tracing_tags_enabled` is default to `false`. 340 | 341 | To customize the prefix for your tracing tags, set the `tracing_tags_prefix` variable value in your Terraform configuration: 342 | 343 | ```hcl 344 | module "example" { 345 | source = 346 | ... 347 | tracing_tags_prefix = "custom_prefix_" 348 | } 349 | ``` 350 | 351 | The actual applied tags would be: 352 | 353 | ```text 354 | { 355 | custom_prefix_git_commit = "3077cc6d0b70e29b6e106b3ab98cee6740c916f6" 356 | custom_prefix_git_file = "main.tf" 357 | custom_prefix_git_last_modified_at = "2023-05-05 08:57:54" 358 | custom_prefix_git_org = "lonegunmanb" 359 | custom_prefix_git_repo = "terraform-yor-tag-test-module" 360 | custom_prefix_yor_trace = "a0425718-c57d-401c-a7d5-f3d88b2551a4" 361 | } 362 | ``` 363 | 364 | 365 | ## Contributing 366 | 367 | 368 | This project welcomes contributions and suggestions. 369 | Most contributions require you to agree to a Contributor License Agreement (CLA) 370 | declaring that you have the right to, and actually do, grant us the rights to use your contribution. 371 | For details, visit [https://cla.opensource.microsoft.com](https://cla.opensource.microsoft.com). 372 | 373 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 374 | a CLA and decorate the PR appropriately (e.g., status check, comment). 375 | Simply follow the instructions provided by the bot. 376 | You will only need to do this once across all repos using our CLA. 377 | 378 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 379 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 380 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 381 | 382 | ## Trademarks 383 | 384 | This project may contain trademarks or logos for projects, products, or services. 385 | Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft's Trademark & Brand Guidelines. 386 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 387 | Any use of third-party trademarks or logos are subject to those third-party's policies. 388 | -------------------------------------------------------------------------------- /test/unit/terraform_unit_test.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | 12 | test_helper "github.com/Azure/terraform-module-test-helper" 13 | "github.com/ahmetb/go-linq/v3" 14 | "github.com/gruntwork-io/terratest/modules/logger" 15 | "github.com/gruntwork-io/terratest/modules/terraform" 16 | "github.com/mitchellh/mapstructure" 17 | "github.com/stretchr/testify/assert" 18 | "github.com/stretchr/testify/require" 19 | "github.com/thanhpk/randstr" 20 | ) 21 | 22 | type vars map[string]any 23 | 24 | func (v vars) toFile(t *testing.T) string { 25 | return varFile(t, v, fmt.Sprintf("../../unit-fixture/terraform%s.tfvars.json", randstr.Hex(8))) 26 | } 27 | 28 | type vnet struct { 29 | Name string `json:"name"` 30 | MeshPeeringEnabled bool `json:"mesh_peering_enabled"` 31 | Subnets map[string]subnet `json:"subnets"` 32 | AddressSpace []string `json:"address_space"` 33 | ResourceGroupName string `json:"resource_group_name"` 34 | Location string `json:"location"` 35 | ResourceGroupLockEnabled bool `json:"resource_group_lock_enabled"` 36 | ResourceGroupLockName string `json:"resource_group_lock_name"` 37 | ResourceGroupCreation bool `json:"resource_group_creation_enabled"` 38 | RoutingAddressSpace []string `json:"routing_address_space"` 39 | Firewall *firewall `json:"firewall"` 40 | HubRouterIpAddress *string `json:"hub_router_ip_address"` 41 | Routes []routeEntry `json:"route_table_entries"` 42 | } 43 | 44 | type routeEntry struct { 45 | Name string `json:"name"` 46 | AddressPrefix string `json:"address_prefix"` 47 | NextHopType string `json:"next_hop_type"` 48 | NextHopIpAddres *string `json:"next_hop_ip_address"` 49 | } 50 | 51 | type firewall struct { 52 | Name *string `json:"name"` 53 | SkuName string `json:"sku_name"` 54 | SkuTier string `json:"sku_tier"` 55 | SubnetAddressPrefix string `json:"subnet_address_prefix"` 56 | ManagementSubnetAddressPrefix string `json:"management_subnet_address_prefix"` 57 | SubnetRouteTableId *string `json:"subnet_route_table_id"` 58 | } 59 | 60 | type firewallOutputEntry struct { 61 | Name string `mapstructure:"name"` 62 | SkuName string `mapstructure:"sku_name"` 63 | SkuTier string `mapstructure:"sku_tier"` 64 | SubnetAddressPrefix string `mapstructure:"subnet_address_prefix"` 65 | ManagementSubnetAddressPrefix string `mapstructure:"management_subnet_address_prefix"` 66 | SubnetRouteTableId *string `mapstructure:"subnet_route_table_id"` 67 | DnsServers []string `mapstructure:"dns_servers"` 68 | FirewallPolicyId *string `mapstructure:"firewall_policy_id"` 69 | PrivateIpRanges []string `mapstructure:"private_ip_ranges"` 70 | Tags map[string]string `mapstructure:"tags"` 71 | ThreatIntelMode string `mapstructure:"threat_intel_mode"` 72 | Zones []string `mapstructure:"zones"` 73 | DefaultIpConfig *IpConfigOutputEntry `mapstructure:"default_ip_configuration"` 74 | } 75 | 76 | type IpConfigOutputEntry struct { 77 | Name string `mapstructure:"name"` 78 | } 79 | 80 | type subnet struct { 81 | AddressPrefixes []string `json:"address_prefixes"` 82 | AssignGeneratedRouteTable bool `json:"assign_generated_route_table"` 83 | ExternalRouteTableId *string `json:"external_route_table_id"` 84 | } 85 | 86 | func aSubnet(addressSpace string) subnet { 87 | return subnet{ 88 | AddressPrefixes: []string{addressSpace}, 89 | } 90 | } 91 | 92 | func (s subnet) UseGenerateRouteTable() subnet { 93 | s.AssignGeneratedRouteTable = true 94 | return s 95 | } 96 | 97 | func (s subnet) WithExternalRouteTableId(rtId string) subnet { 98 | s.ExternalRouteTableId = &rtId 99 | return s 100 | } 101 | 102 | func aVnet(name string, meshPeering bool) vnet { 103 | return vnet{ 104 | Name: name, 105 | Location: "eastus", 106 | MeshPeeringEnabled: meshPeering, 107 | Subnets: make(map[string]subnet, 0), 108 | ResourceGroupCreation: false, 109 | HubRouterIpAddress: String("dummyIp"), 110 | AddressSpace: make([]string, 0, 2), 111 | } 112 | } 113 | 114 | func (n vnet) withAddressSpace(cidr string) vnet { 115 | n.AddressSpace = append(n.AddressSpace, cidr) 116 | return n 117 | } 118 | 119 | func (n vnet) withResourceGroupCreation(b bool) vnet { 120 | n.ResourceGroupCreation = b 121 | return n 122 | } 123 | 124 | func (n vnet) withResourceGroupName(name string) vnet { 125 | n.ResourceGroupName = name 126 | return n 127 | } 128 | 129 | func (n vnet) withRoutingAddressSpace(cidr string) vnet { 130 | n.RoutingAddressSpace = append(n.RoutingAddressSpace, cidr) 131 | return n 132 | } 133 | 134 | func (n vnet) withEmptyRoutingAddressSpace() vnet { 135 | n.RoutingAddressSpace = []string{} 136 | return n 137 | } 138 | 139 | func (n vnet) withSubnet(name string, s subnet) vnet { 140 | n.Subnets[name] = s 141 | return n 142 | } 143 | 144 | func (n vnet) withFirewall(f firewall) vnet { 145 | n.Firewall = &f 146 | n.HubRouterIpAddress = nil 147 | return n 148 | } 149 | 150 | func (n vnet) withHubRouterIpAddress(ip string) vnet { 151 | n.HubRouterIpAddress = String(ip) 152 | return n 153 | } 154 | 155 | func (n vnet) withUserRouteEntry(r routeEntry) vnet { 156 | if n.Routes == nil { 157 | n.Routes = make([]routeEntry, 0) 158 | } 159 | n.Routes = append(n.Routes, r) 160 | return n 161 | } 162 | 163 | type routeMap struct { 164 | MeshRoutes []routeEntryOutput `mapstructure:"mesh_routes"` 165 | UserRoutes []routeEntryOutput `mapstructure:"user_routes"` 166 | } 167 | 168 | type routeEntryOutput struct { 169 | Name string `mapstructure:"name"` 170 | AddressPrefix string `mapstructure:"address_prefix"` 171 | NextHopType string `mapstructure:"next_hop_type"` 172 | NextHopIpAddress *string `mapstructure:"next_hop_ip_address"` 173 | } 174 | 175 | func TestUnit_VnetWithMeshPeeringShouldAppearInHubPeeringMap(t *testing.T) { 176 | inputs := []struct { 177 | vars vars 178 | expectedPeeringCount int 179 | }{ 180 | { 181 | vars: vars{ 182 | "hub_virtual_networks": map[string]vnet{ 183 | "vnet0": aVnet("vnet-zero", true).withAddressSpace("10.0.0.0/16"), 184 | "vnet1": aVnet("vnet-one", true).withAddressSpace("10.1.0.0/16"), 185 | "nonMeshVnet": aVnet("nonMeshVnet", false).withAddressSpace("10.2.0.0/16"), 186 | }, 187 | }, 188 | expectedPeeringCount: 2, 189 | }, 190 | { 191 | vars: vars{ 192 | "hub_virtual_networks": map[string]vnet{ 193 | "vnet0": aVnet("vnet0", true).withAddressSpace("10.0.0.0/16"), 194 | "vnet1": aVnet("vnet1", true).withAddressSpace("10.1.0.0/16"), 195 | "vnet2": aVnet("vnet2", true).withAddressSpace("10.2.0.0/16"), 196 | "nonMeshVnet": aVnet("nonMeshVnet", false).withAddressSpace("10.3.0.0/16"), 197 | }, 198 | }, 199 | expectedPeeringCount: 6, 200 | }, 201 | } 202 | 203 | for _, input := range inputs { 204 | i := input 205 | t.Run(strconv.Itoa(i.expectedPeeringCount), func(t *testing.T) { 206 | varFilePath := i.vars.toFile(t) 207 | defer func() { _ = os.Remove(varFilePath) }() 208 | test_helper.RunUnitTest(t, "../../", "unit-fixture", terraform.Options{ 209 | Upgrade: true, 210 | VarFiles: []string{varFilePath}, 211 | Logger: logger.Discard, 212 | }, func(t *testing.T, output test_helper.TerraformOutput) { 213 | peeringMap := output["hub_peering_map"].(map[string]any) 214 | assert.Equal(t, i.expectedPeeringCount, len(peeringMap)) 215 | for k, v := range peeringMap { 216 | m := v.(map[string]any) 217 | srcVnetName := i.vars["hub_virtual_networks"].(map[string]vnet)[m["src_key"].(string)].Name 218 | dstVnetName := i.vars["hub_virtual_networks"].(map[string]vnet)[m["dst_key"].(string)].Name 219 | expectedPeeringName := fmt.Sprintf("%s-%s", srcVnetName, dstVnetName) 220 | assert.Equal(t, expectedPeeringName, k) 221 | assert.Equal(t, srcVnetName, m["virtual_network_name"]) 222 | assert.Equal(t, fmt.Sprintf("%s_id", dstVnetName), m["remote_virtual_network_id"]) 223 | assert.False(t, strings.Contains(k, "nonMeshVnet")) 224 | } 225 | }) 226 | }) 227 | } 228 | } 229 | 230 | func TestUnit_VnetWithResourceGroupCreationWouldBeGatheredInResourceGroupData(t *testing.T) { 231 | varFilePath := vars{ 232 | "hub_virtual_networks": map[string]vnet{ 233 | "vnet0": aVnet("vnet0", true).withResourceGroupName("newRg").withResourceGroupCreation(true).withAddressSpace("10.0.0.0/16"), 234 | "vnet1": aVnet("vnet1", true).withResourceGroupName("existedRg").withResourceGroupCreation(false).withAddressSpace("10.1.0.0/16"), 235 | }, 236 | }.toFile(t) 237 | defer func() { _ = os.Remove(varFilePath) }() 238 | test_helper.RunUnitTest(t, "../../", "unit-fixture", terraform.Options{ 239 | Upgrade: true, 240 | VarFiles: []string{varFilePath}, 241 | Logger: logger.Discard, 242 | }, func(t *testing.T, output test_helper.TerraformOutput) { 243 | data, ok := output["resource_group_data"].([]any) 244 | require.True(t, ok) 245 | assert.Len(t, data, 1) 246 | assert.Equal(t, "newRg", data[0].(map[string]any)["name"]) 247 | }) 248 | } 249 | 250 | func TestUnit_VnetWithRoutingAddressSpaceWouldProvisionRouteEntries(t *testing.T) { 251 | inputs := []struct { 252 | name string 253 | networks map[string]vnet 254 | expected map[string][]routeEntryOutput 255 | }{ 256 | { 257 | name: "null routing address should create empty route table", 258 | networks: map[string]vnet{ 259 | "vnet0": aVnet("vnet0", true).withAddressSpace("10.0.0.0/16"), 260 | "vnet1": aVnet("vnet1", true).withAddressSpace("10.1.0.0/16"), 261 | }, 262 | expected: map[string][]routeEntryOutput{ 263 | "vnet0": {}, 264 | "vnet1": {}, 265 | }, 266 | }, 267 | { 268 | name: "empty routing address should create empty route table", 269 | networks: map[string]vnet{ 270 | "vnet0": aVnet("vnet0", true).withEmptyRoutingAddressSpace().withAddressSpace("10.0.0.0/16"), 271 | "vnet1": aVnet("vnet1", true).withEmptyRoutingAddressSpace().withAddressSpace("10.1.0.0/16"), 272 | }, 273 | expected: map[string][]routeEntryOutput{ 274 | "vnet0": {}, 275 | "vnet1": {}, 276 | }, 277 | }, 278 | { 279 | name: "uni-directional route", 280 | networks: map[string]vnet{ 281 | "vnet0": aVnet("vnet0", true).withAddressSpace("10.0.0.0/16"), 282 | "vnet1": aVnet("vnet1", true).withAddressSpace("10.1.0.0/16").withRoutingAddressSpace("10.0.0.0/16"), 283 | }, 284 | expected: map[string][]routeEntryOutput{ 285 | "vnet0": { 286 | { 287 | Name: "vnet1-10.0.0.0-16", 288 | AddressPrefix: "10.0.0.0/16", 289 | NextHopType: "VirtualAppliance", 290 | NextHopIpAddress: String("dummyIp"), 291 | }, 292 | }, 293 | "vnet1": {}, 294 | }, 295 | }, 296 | { 297 | name: "bi-directional route", 298 | networks: map[string]vnet{ 299 | "vnet0": aVnet("vnet0", true). 300 | withAddressSpace("10.0.0.0/16"). 301 | withRoutingAddressSpace("10.0.0.0/16"). 302 | withFirewall(firewall{ 303 | SkuName: "AZFW_VNet", 304 | SkuTier: "Standard", 305 | SubnetAddressPrefix: "10.0.1.0/24", 306 | }). 307 | withSubnet("AzureFirewallSubnet", subnet{ 308 | AddressPrefixes: []string{"10.0.255.0/24"}, 309 | }), 310 | "vnet1": aVnet("vnet1", true). 311 | withAddressSpace("10.1.0.0/16"). 312 | withRoutingAddressSpace("10.1.0.0/16"). 313 | withFirewall(firewall{ 314 | SkuName: "AZFW_VNet", 315 | SkuTier: "Standard", 316 | SubnetAddressPrefix: "10.1.1.0/24", 317 | }). 318 | withSubnet("AzureFirewallSubnet", subnet{ 319 | AddressPrefixes: []string{"10.0.255.0/24"}, 320 | }), 321 | }, 322 | expected: map[string][]routeEntryOutput{ 323 | "vnet0": { 324 | { 325 | Name: "vnet1-10.1.0.0-16", 326 | AddressPrefix: "10.1.0.0/16", 327 | NextHopType: "VirtualAppliance", 328 | NextHopIpAddress: String("vnet1-fake-fw-private-ip"), 329 | }, 330 | }, 331 | "vnet1": { 332 | { 333 | Name: "vnet0-10.0.0.0-16", 334 | AddressPrefix: "10.0.0.0/16", 335 | NextHopType: "VirtualAppliance", 336 | NextHopIpAddress: String("vnet0-fake-fw-private-ip"), 337 | }, 338 | }, 339 | }, 340 | }, 341 | } 342 | 343 | for i := 0; i < len(inputs); i++ { 344 | input := inputs[i] 345 | t.Run(input.name, func(t *testing.T) { 346 | varFilePath := vars{ 347 | "hub_virtual_networks": input.networks, 348 | }.toFile(t) 349 | defer func() { _ = os.Remove(varFilePath) }() 350 | test_helper.RunUnitTest(t, "../../", "unit-fixture", terraform.Options{ 351 | Upgrade: true, 352 | VarFiles: []string{varFilePath}, 353 | Logger: logger.Discard, 354 | }, func(t *testing.T, output test_helper.TerraformOutput) { 355 | var actual map[string]routeMap 356 | err := mapstructure.Decode(output["route_map"], &actual) 357 | require.NoError(t, err) 358 | require.Equal(t, len(input.expected), len(actual)) 359 | for vnetName, a := range actual { 360 | meshRoutes := sortRouteEntryOutputs(a.MeshRoutes) 361 | expected := sortRouteEntryOutputs(input.expected[vnetName]) 362 | assert.Equal(t, expected, meshRoutes) 363 | } 364 | }) 365 | }) 366 | } 367 | } 368 | 369 | func TestUnit_VnetWithUserRouteEntriesWouldProvisionUserRouteEntries(t *testing.T) { 370 | inputs := []struct { 371 | name string 372 | networks map[string]vnet 373 | expected map[string][]routeEntryOutput 374 | }{ 375 | { 376 | name: "null routing address should create empty route table", 377 | networks: map[string]vnet{ 378 | "vnet0": aVnet("vnet0", true). 379 | withAddressSpace("10.0.0.0/16"). 380 | withUserRouteEntry(routeEntry{ 381 | Name: "no_internet", 382 | AddressPrefix: "0.0.0.0/0", 383 | NextHopType: "None", 384 | }).withUserRouteEntry(routeEntry{ 385 | Name: "intranet", 386 | AddressPrefix: "10.0.0.0/16", 387 | NextHopType: "VnetLocal", 388 | }), 389 | }, 390 | expected: map[string][]routeEntryOutput{ 391 | "vnet0": { 392 | routeEntryOutput{ 393 | Name: "intranet", 394 | AddressPrefix: "10.0.0.0/16", 395 | NextHopType: "VnetLocal", 396 | }, 397 | routeEntryOutput{ 398 | Name: "no_internet", 399 | AddressPrefix: "0.0.0.0/0", 400 | NextHopType: "None", 401 | }, 402 | }, 403 | }, 404 | }, 405 | { 406 | name: "bi-directional route", 407 | networks: map[string]vnet{ 408 | "vnet0": aVnet("vnet0", true). 409 | withAddressSpace("10.0.0.0/16"). 410 | withRoutingAddressSpace("10.0.0.0/16"). 411 | withFirewall(firewall{ 412 | SkuName: "AZFW_VNet", 413 | SkuTier: "Standard", 414 | SubnetAddressPrefix: "10.0.0.0/24", 415 | }). 416 | withSubnet("AzureFirewallSubnet", subnet{ 417 | AddressPrefixes: []string{"10.0.255.0/24"}, 418 | }).withUserRouteEntry(routeEntry{ 419 | Name: "no_internet", 420 | AddressPrefix: "0.0.0.0/0", 421 | NextHopType: "None", 422 | }).withUserRouteEntry(routeEntry{ 423 | Name: "intranet", 424 | AddressPrefix: "10.0.0.0/16", 425 | NextHopType: "VnetLocal", 426 | }), 427 | "vnet1": aVnet("vnet1", true). 428 | withAddressSpace("10.1.0.0/16"). 429 | withRoutingAddressSpace("10.1.0.0/16"). 430 | withFirewall(firewall{ 431 | SkuName: "AZFW_VNet", 432 | SkuTier: "Standard", 433 | SubnetAddressPrefix: "10.1.0.0/24", 434 | }). 435 | withSubnet("AzureFirewallSubnet", subnet{ 436 | AddressPrefixes: []string{"10.0.255.0/24"}, 437 | }), 438 | }, 439 | expected: map[string][]routeEntryOutput{ 440 | "vnet0": { 441 | { 442 | Name: "intranet", 443 | AddressPrefix: "10.0.0.0/16", 444 | NextHopType: "VnetLocal", 445 | }, 446 | { 447 | Name: "no_internet", 448 | AddressPrefix: "0.0.0.0/0", 449 | NextHopType: "None", 450 | }, 451 | }, 452 | "vnet1": nil, 453 | }, 454 | }, 455 | } 456 | 457 | for i := 0; i < len(inputs); i++ { 458 | input := inputs[i] 459 | t.Run(input.name, func(t *testing.T) { 460 | varFilePath := vars{ 461 | "hub_virtual_networks": input.networks, 462 | }.toFile(t) 463 | defer func() { _ = os.Remove(varFilePath) }() 464 | test_helper.RunUnitTest(t, "../../", "unit-fixture", terraform.Options{ 465 | Upgrade: true, 466 | VarFiles: []string{varFilePath}, 467 | Logger: logger.Discard, 468 | }, func(t *testing.T, output test_helper.TerraformOutput) { 469 | var actual map[string]routeMap 470 | err := mapstructure.Decode(output["route_map"], &actual) 471 | require.NoError(t, err) 472 | require.Equal(t, len(input.expected), len(actual)) 473 | for vnetName, a := range actual { 474 | userRoutes := sortRouteEntryOutputs(a.UserRoutes) 475 | expected := sortRouteEntryOutputs(input.expected[vnetName]) 476 | assert.Equal(t, expected, userRoutes) 477 | } 478 | }) 479 | }) 480 | } 481 | } 482 | 483 | func TestUnit_SubnetAssignGeneratedRouteTableWouldProvisionGeneratedRouteTableAssociation(t *testing.T) { 484 | inputs := []struct { 485 | name string 486 | network vnet 487 | expected map[string]any 488 | }{ 489 | { 490 | name: "no association to generated route table", 491 | network: aVnet("vnet0", false). 492 | withAddressSpace("10.0.0.0/16"). 493 | withSubnet("subnet0", aSubnet("10.0.0.0/24")), 494 | expected: map[string]any{}, 495 | }, 496 | { 497 | name: "association to generated route table", 498 | network: aVnet("vnet0", false). 499 | withAddressSpace("10.0.0.0/16"). 500 | withSubnet("subnetAssociatedWithGeneratedRouteTable", aSubnet("10.0.0.0/24").UseGenerateRouteTable()). 501 | withSubnet("subnetAssociatedWithExternalRouteTable", aSubnet("10.0.1.0/24").WithExternalRouteTableId("external_route_table_id")), 502 | expected: map[string]any{ 503 | "vnet0-subnetAssociatedWithGeneratedRouteTable": map[string]any{ 504 | "name": "vnet0-subnetAssociatedWithGeneratedRouteTable", 505 | "subnet_id": "subnetAssociatedWithGeneratedRouteTable_id", 506 | "route_table_id": "vnet0_route_table_id", 507 | }, 508 | }, 509 | }, 510 | } 511 | for i := 0; i < len(inputs); i++ { 512 | input := inputs[i] 513 | t.Run(input.name, func(t *testing.T) { 514 | varFilePath := vars{ 515 | "hub_virtual_networks": map[string]any{ 516 | input.network.Name: input.network, 517 | }, 518 | }.toFile(t) 519 | defer func() { _ = os.Remove(varFilePath) }() 520 | test_helper.RunUnitTest(t, "../../", "unit-fixture", terraform.Options{ 521 | Upgrade: true, 522 | VarFiles: []string{varFilePath}, 523 | Logger: logger.Discard, 524 | }, func(t *testing.T, output test_helper.TerraformOutput) { 525 | subnetGenerateRouteTableAssociatoins := output["subnet_route_table_association_map"].(map[string]any) 526 | assert.Equal(t, input.expected, subnetGenerateRouteTableAssociatoins) 527 | }) 528 | }) 529 | } 530 | } 531 | 532 | func TestUnit_SubnetAssignExternalRouteTableWouldProvisionAssociationToExternalRouteTable(t *testing.T) { 533 | inputs := []struct { 534 | name string 535 | network vnet 536 | expected map[string]any 537 | }{ 538 | { 539 | name: "no association to external route table", 540 | network: aVnet("vnet0", false). 541 | withAddressSpace("10.0.0.0/16"). 542 | withAddressSpace("10.0.0.0/16"). 543 | withSubnet("subnet0", aSubnet("10.0.0.0/24")), 544 | expected: map[string]any{}, 545 | }, 546 | { 547 | name: "association to external route table", 548 | network: aVnet("vnet0", false). 549 | withAddressSpace("10.0.0.0/16"). 550 | withSubnet("subnetAssociatedWithGeneratedRouteTable", aSubnet("10.0.0.0/24").UseGenerateRouteTable()). 551 | withSubnet("subnetAssociatedWithExternalRouteTable", aSubnet("10.0.1.0/24").WithExternalRouteTableId("external_route_table_id")), 552 | expected: map[string]any{ 553 | "vnet0-subnetAssociatedWithExternalRouteTable": map[string]any{ 554 | "name": "vnet0-subnetAssociatedWithExternalRouteTable", 555 | "subnet_id": "subnetAssociatedWithExternalRouteTable_id", 556 | "route_table_id": "external_route_table_id", 557 | }, 558 | }, 559 | }, 560 | } 561 | for i := 0; i < len(inputs); i++ { 562 | input := inputs[i] 563 | t.Run(input.name, func(t *testing.T) { 564 | varFilePath := vars{ 565 | "hub_virtual_networks": map[string]any{ 566 | input.network.Name: input.network, 567 | }, 568 | }.toFile(t) 569 | defer func() { _ = os.Remove(varFilePath) }() 570 | test_helper.RunUnitTest(t, "../../", "unit-fixture", terraform.Options{ 571 | Upgrade: true, 572 | VarFiles: []string{varFilePath}, 573 | Logger: logger.Discard, 574 | }, func(t *testing.T, output test_helper.TerraformOutput) { 575 | externalRouteTableAssociations := output["subnet_external_route_table_association_map"].(map[string]any) 576 | assert.Equal(t, input.expected, externalRouteTableAssociations) 577 | }) 578 | }) 579 | } 580 | } 581 | 582 | func TestUnit_VnetWithFirewallShouldCreatePublicIp(t *testing.T) { 583 | inputs := []struct { 584 | name string 585 | network vnet 586 | expected map[string]any 587 | }{ 588 | { 589 | name: "vnet without firewall should not create public ip", 590 | network: aVnet("vnet", false). 591 | withResourceGroupName("rg0"). 592 | withAddressSpace("10.0.0.0/16"). 593 | withSubnet("AzureFirewallSubnet", subnet{ 594 | AddressPrefixes: []string{"10.0.255.0/24"}, 595 | AssignGeneratedRouteTable: false, 596 | ExternalRouteTableId: nil, 597 | }), 598 | expected: map[string]any{}, 599 | }, 600 | { 601 | name: "vnet firewall should create public ip", 602 | network: aVnet("vnet", false). 603 | withResourceGroupName("rg0"). 604 | withAddressSpace("10.0.0.0/16"). 605 | withFirewall(firewall{ 606 | SkuName: "AZFW_VNet", 607 | SkuTier: "Basic", 608 | SubnetAddressPrefix: "10.0.255.0/24", 609 | ManagementSubnetAddressPrefix: "10.0.1.0/24", 610 | }), 611 | expected: map[string]any{ 612 | "vnet": map[string]any{ 613 | "location": "eastus", 614 | "name": "pip-afw-vnet", 615 | "resource_group_name": "rg0", 616 | "ip_version": "IPv4", 617 | "sku_tier": "Regional", 618 | "tags": nil, 619 | "zones": nil, 620 | }, 621 | }, 622 | }, 623 | } 624 | 625 | for i := 0; i < len(inputs); i++ { 626 | input := inputs[i] 627 | t.Run(input.name, func(t *testing.T) { 628 | varFilePath := vars{ 629 | "hub_virtual_networks": map[string]any{ 630 | input.network.Name: input.network, 631 | }, 632 | }.toFile(t) 633 | defer func() { _ = os.Remove(varFilePath) }() 634 | test_helper.RunUnitTest(t, "../../", "unit-fixture", terraform.Options{ 635 | Upgrade: true, 636 | VarFiles: []string{varFilePath}, 637 | Logger: logger.Discard, 638 | }, func(t *testing.T, output test_helper.TerraformOutput) { 639 | pips := output["fw_default_ip_configuration_pip"] 640 | assert.Equal(t, input.expected, pips) 641 | }) 642 | }) 643 | } 644 | } 645 | 646 | func TestUnit_VnetWithFirewallShouldCreateFirewall(t *testing.T) { 647 | inputs := []struct { 648 | name string 649 | network vnet 650 | expected map[string]firewallOutputEntry 651 | }{ 652 | { 653 | name: "vnet without firewall should not create firewall", 654 | network: aVnet("vnet", false). 655 | withResourceGroupName("rg0"). 656 | withAddressSpace("10.0.0.0/16"), 657 | expected: map[string]firewallOutputEntry{}, 658 | }, 659 | { 660 | name: "vnet firewall should create firewall", 661 | network: aVnet("vnet", false). 662 | withResourceGroupName("rg0"). 663 | withAddressSpace("10.0.0.0/16"). 664 | withFirewall(firewall{ 665 | SkuName: "AZFW_VNet", 666 | SkuTier: "Standard", 667 | SubnetAddressPrefix: "10.0.255.0/24", 668 | }), 669 | expected: map[string]firewallOutputEntry{ 670 | "vnet": { 671 | Name: "afw-vnet", 672 | SkuName: "AZFW_VNet", 673 | SkuTier: "Standard", 674 | SubnetAddressPrefix: "10.0.255.0/24", 675 | ThreatIntelMode: "Alert", 676 | DefaultIpConfig: &IpConfigOutputEntry{ 677 | Name: "default", 678 | }, 679 | }, 680 | }, 681 | }, 682 | } 683 | 684 | for i := 0; i < len(inputs); i++ { 685 | input := inputs[i] 686 | t.Run(input.name, func(t *testing.T) { 687 | varFilePath := vars{ 688 | "hub_virtual_networks": map[string]any{ 689 | input.network.Name: input.network, 690 | }, 691 | }.toFile(t) 692 | defer func() { _ = os.Remove(varFilePath) }() 693 | test_helper.RunUnitTest(t, "../../", "unit-fixture", terraform.Options{ 694 | Upgrade: true, 695 | VarFiles: []string{varFilePath}, 696 | Logger: logger.Discard, 697 | }, func(t *testing.T, output test_helper.TerraformOutput) { 698 | actual := make(map[string]firewallOutputEntry, 0) 699 | err := mapstructure.Decode(output["firewalls"], &actual) 700 | require.NoError(t, err) 701 | assert.Equal(t, input.expected, actual) 702 | }) 703 | }) 704 | } 705 | } 706 | 707 | func TestUnit_RoutingAddressSpaceShouldGenerateMeshRoutes(t *testing.T) { 708 | inputs := []struct { 709 | name string 710 | network []vnet 711 | expected map[string][]routeEntryOutput 712 | }{ 713 | { 714 | name: "vnet without firewall should not create firewall", 715 | network: []vnet{ 716 | aVnet("vnet0", true). 717 | withAddressSpace("10.0.0.0/16"). 718 | withRoutingAddressSpace("10.0.0.0/16"). 719 | withRoutingAddressSpace("192.168.0.0/24"). 720 | withHubRouterIpAddress("fake_fw_vnet0_ip"), 721 | aVnet("vnet1", true). 722 | withAddressSpace("10.1.0.0/16"). 723 | withRoutingAddressSpace("10.1.0.0/16"). 724 | withRoutingAddressSpace("192.168.1.0/24"). 725 | withHubRouterIpAddress("fake_fw_vnet1_ip"), 726 | }, 727 | expected: map[string][]routeEntryOutput{ 728 | "vnet0": { 729 | { 730 | Name: "vnet1-10.1.0.0-16", 731 | AddressPrefix: "10.1.0.0/16", 732 | NextHopType: "VirtualAppliance", 733 | NextHopIpAddress: String("fake_fw_vnet1_ip"), 734 | }, 735 | { 736 | Name: "vnet1-192.168.1.0-24", 737 | AddressPrefix: "192.168.1.0/24", 738 | NextHopType: "VirtualAppliance", 739 | NextHopIpAddress: String("fake_fw_vnet1_ip"), 740 | }, 741 | }, 742 | "vnet1": { 743 | { 744 | Name: "vnet0-10.0.0.0-16", 745 | AddressPrefix: "10.0.0.0/16", 746 | NextHopType: "VirtualAppliance", 747 | NextHopIpAddress: String("fake_fw_vnet0_ip"), 748 | }, 749 | { 750 | Name: "vnet0-192.168.0.0-24", 751 | AddressPrefix: "192.168.0.0/24", 752 | NextHopType: "VirtualAppliance", 753 | NextHopIpAddress: String("fake_fw_vnet0_ip"), 754 | }, 755 | }, 756 | }, 757 | }, 758 | } 759 | 760 | for i := 0; i < len(inputs); i++ { 761 | input := inputs[i] 762 | t.Run(input.name, func(t *testing.T) { 763 | networks := make(map[string]any) 764 | linq.From(input.network).ToMapBy(&networks, func(i interface{}) interface{} { 765 | return i.(vnet).Name 766 | }, func(i interface{}) interface{} { 767 | return i 768 | }) 769 | varFilePath := vars{ 770 | "hub_virtual_networks": networks, 771 | }.toFile(t) 772 | defer func() { _ = os.Remove(varFilePath) }() 773 | test_helper.RunUnitTest(t, "../../", "unit-fixture", terraform.Options{ 774 | Upgrade: true, 775 | VarFiles: []string{varFilePath}, 776 | Logger: logger.Discard, 777 | }, func(t *testing.T, output test_helper.TerraformOutput) { 778 | var actual map[string]routeMap 779 | err := mapstructure.Decode(output["route_map"], &actual) 780 | require.NoError(t, err) 781 | require.Equal(t, len(input.expected), len(actual)) 782 | for vnetName, a := range actual { 783 | meshRoutes := sortRouteEntryOutputs(a.MeshRoutes) 784 | expected := sortRouteEntryOutputs(input.expected[vnetName]) 785 | assert.Equal(t, expected, meshRoutes) 786 | } 787 | }) 788 | }) 789 | } 790 | } 791 | 792 | func varFile(t *testing.T, inputs map[string]interface{}, path string) string { 793 | cleanPath := filepath.Clean(path) 794 | varFile, err := os.Create(cleanPath) 795 | require.Nil(t, err) 796 | c, err := json.Marshal(inputs) 797 | require.Nil(t, err) 798 | _, err = varFile.Write(c) 799 | require.Nil(t, err) 800 | _ = varFile.Close() 801 | varFilePath, err := filepath.Abs(cleanPath) 802 | require.Nil(t, err) 803 | return varFilePath 804 | } 805 | 806 | func String(s string) *string { 807 | return &s 808 | } 809 | 810 | func sortRouteEntryOutputs(routes []routeEntryOutput) []routeEntryOutput { 811 | var r []routeEntryOutput 812 | linq.From(routes).Sort(func(i, j interface{}) bool { 813 | return strings.Compare(i.(routeEntryOutput).Name, j.(routeEntryOutput).Name) < 0 814 | }).ToSlice(&r) 815 | return r 816 | } 817 | --------------------------------------------------------------------------------