aztfmod / terraform-azurerm-caf-virtual-network

Azure Virtual Network module for Cloud Adoption Framework for Azure landing zones
MIT License
9 stars 23 forks source link

Negate the use of Special Subnets vs Subnets #23

Open fluffy-cakes opened 4 years ago

fluffy-cakes commented 4 years ago

I understand the original idea around creating special subnets, but we have found that sometimes you do actually need a custom route, but no NSG, or vice versa. Our internal client needed to have a route table but no NSG on one of their special subnets, and thus I wen about creating the option for this. Whilst working this out I realised that I don't really need a special subnet to split up the functions of what a deployed subnet needs/consumes; however given that the subnet already being used and would require some down time to rebuild it all, I've had to continue with the split sections.

Essentially I enabled a 'switch' on the .tfvars for special subnets; whether or no they want to associate with a route table. The same method used for this could be the same method for attaching an NSG, or not.

My .tfvars file, take note of subnet objects with the key of route and how they link in with the route_tables object;

vnet_spoke_object                                          = {
    vnet                                                   = {
        name                                               = "asdf-dev"
        address_space                                      = ["1.1.1.0/24"] # placeholder: not in design doc
        dns                                                = ["1.1.1.132", "1.1.1.133"] # could be replaced with a lookup for the bind servers
        enable_ddos_std                                    = false
        ddos_id                                            = "placeholder"
    }
    specialsubnets                                         = {
        Subnet_1                                           = {
            name                                           = "test1"
            cidr                                           = "1.1.1.0/25"
            enforce_private_link_endpoint_network_policies = true
            enforce_private_link_service_network_policies  = true
            nsg_inbound                                    = [
                # {"Name", "Description", "Priority", "Direction", "Action", "Protocol", "source_port_range", "destination_port_range", "source_address_prefix", "destination_address_prefix" }
            ]
            nsg_outbound                                   = []
            route                                          = "Route1"
            service_endpoints                              = []
        }
    }
    subnets                                                = {
        Subnet_2                                           = {
            name                                           = "test2"
            cidr                                           = "1.1.1.128/25"
            enforce_private_link_endpoint_network_policies = true
            enforce_private_link_service_network_policies  = true
            nsg_inbound                                    = [
                # {"Name", "Description", "Priority", "Direction", "Action", "Protocol", "source_port_range", "destination_port_range", "source_address_prefix", "destination_address_prefix" }
            ]
            nsg_outbound                                   = []
            route                                          = "route_table_az_to_afw"
            service_endpoints                              = []
        }
    }
    netwatcher                                             = {
        # create the network watcher for a subscription and for the location of the vnet
        create                                             = true
        # name of the network watcher to be created
        name                                               = "NetworkWatcher"
        flow_logs_settings                                 = {
            enabled                                        = true
            period                                         = 7
            retention                                      = true
        }
        # enabling this sends to Log Analytics
        traffic_analytics_settings                         = {
            enabled                                        = true
        }
    }
}

route_tables                                               = {
    Route1                                                 = {
        name                                               = "test1"
        disable_bgp_route_propagation                      = true #optional
        resource_group_name                                = "vnet-spoke"
        route_entries                                      = {
            re1                                            = {
                name                                       = "rt-rfc-10-8"
                prefix                                     = "10.0.0.0/8"
                next_hop_type                              = "VirtualAppliance"
                next_hop_in_ip_address                     = "1.1.1.1" #required if next_hop_type is "VirtualAppliance"
            }
        }
    }

    route_table_az_to_afw                                  = {
        name                                               = "az_afw"
        disable_bgp_route_propagation                      = true #optional
        resource_group_name                                = "vnet-spoke"
        route_entries                                      = {
            re1                                            = {
                name                                       = "rt-rfc-10-8"
                prefix                                     = "10.0.0.0/8"
                next_hop_type                              = "VirtualAppliance"
                next_hop_in_ip_address                     = "1.1.1.1" #required if next_hop_type is "VirtualAppliance"
            },
            re2                                            = {
                name                                       = "rt-rfc-172-12"
                prefix                                     = "172.16.0.0/12"
                next_hop_type                              = "VirtualAppliance"
                next_hop_in_ip_address                     = "1.1.1.1" #required if next_hop_type is "VirtualAppliance"
            },
            re3                                            = {
                name                                       = "rt-rfc-192-16"
                prefix                                     = "192.168.0.0/16"
                next_hop_type                              = "VirtualAppliance"
                next_hop_in_ip_address                     = "1.1.1.1" #required if next_hop_type is "VirtualAppliance"
            },
            re4                                            = {
                name                                       = "rt-default"
                prefix                                     = "0.0.0.0/0"
                # next_hop_type                            = "Internet"
                next_hop_type                              = "VirtualAppliance"
                next_hop_in_ip_address                     = "1.1.1.1" #required if next_hop_type is "VirtualAppliance"
            }
        }
    }
}

vnet_peering_settings                                      = {
    peer1                                                  = {
        source_to_peer                                     = { # hub
            allow_virtual_network_access                   = true
            allow_forwarded_traffic                        = true
            allow_gateway_transit                          = true
            use_remote_gateways                            = false
        }
        peer_to_source                                     = { # spoke
            allow_virtual_network_access                   = true
            allow_forwarded_traffic                        = true
            allow_gateway_transit                          = false
            use_remote_gateways                            = true
        }
    }
}

Now I run a for_each on creating routes (bear in mind I use a naming convention module that I reference):

resource "azurerm_route_table" "route_tables" {
    for_each                       = var.route_tables

    name                           = "${module.names.standard["route-table"]}-${each.value.name}"
    location                       = var.ARM_LOCATION
    resource_group_name            = "${module.names.standard["resource-group"]}-${each.value.resource_group_name}"
    disable_bgp_route_propagation  = each.value.disable_bgp_route_propagation

    dynamic "route" {
        for_each                   = each.value.route_entries
        content {
            name                   = route.value.name
            address_prefix         = route.value.prefix
            next_hop_type          = route.value.next_hop_type
            next_hop_in_ip_address = contains(keys(route.value), "next_hop_in_ip_address") ? route.value.next_hop_in_ip_address: null
        }
    }
}

And then finally I attach the route to the subnet via for_each using the object which comes from the CAF module;

resource "azurerm_subnet_route_table_association" "special_subnets" {
    for_each = {
        for key, value in var.vnet_spoke_object.specialsubnets:
            key => value
            if value.route != null
    }

    subnet_id                      = lookup(module.vnet_spoke.vnet_subnets, "${module.names.standard["subnet"]}-${each.value.name}", null)
    route_table_id                 = azurerm_route_table.route_tables[lookup(each.value, "route", null)].id
}

resource "azurerm_subnet_route_table_association" "subnets" {
    for_each = {
        for key, value in var.vnet_spoke_object.subnets:
            key => value
            if value.route != null
    }

    subnet_id                      = lookup(module.vnet_spoke.vnet_subnets, "${module.names.standard["subnet"]}-${each.value.name}", null)
    route_table_id                 = azurerm_route_table.route_tables[lookup(each.value, "route", null)].id
}

Now if I hadn't already had stuff deployed in Special Subnets, I would have done away with that completely and then I wouldn't have needed the two route table associations. I would have needed to create a solution for the NSGs which required more work, but it still would have been better in the long run; subnets/routes/nsg would have been far more customisable.

Note; if you don't want a subnet to have a route associated with it, the key/vaule for that subnet must be; route = null

fluffy-cakes commented 4 years ago

In addition to that, I've now added an option on the NSG creation in the subnets field of .tfvars. The biggest issue around CAF is when a requirement changes, such as the client wanting/not-wanting an NSG/route; it usually involves destruction, which doesn't work out when you have things knocking about in your subnets.

Here's a screenshot of my fix for an option adding an NSG in the subnet `.tfvars.

nsg-option