Пример #1
0
    def __init__(self,
                 name: str,
                 props: HubProps,
                 opts: ResourceOptions = None):
        super().__init__('vdc:network:Hub', name, {}, opts)

        # set required vdc variables before calling functions
        vdc.resource_group_name = props.resource_group_name
        vdc.tags = props.tags
        vdc.self = self

        # calculate the subnets in the firewall_address_space
        fwz_nw = ip_network(props.firewall_address_space)
        fwz_sn = fwz_nw.subnets(new_prefix=25)  # two /26 subnets required
        fwx_nw = next(fwz_sn)  # for Azure Firewall and Management subnets
        fwz_sn = fwz_nw.address_exclude(fwx_nw)  # consolidate remainder
        dmz_nw = next(fwz_sn)  # largest remaining subnet for DMZ
        fwx_sn = fwx_nw.subnets(new_prefix=26)  # split the /25 into two /26
        fws_nw = next(fwx_sn)  # AzureFirewallSubnet
        fwm_nw = next(fwx_sn)  # AzureFirewallManagementSubnet

        # calculate the subnets in the hub_address_space
        hub_nw = ip_network(props.hub_address_space)
        if hub_nw.prefixlen < 20:  # split evenly between subnets and hosts
            sub_diff = int((hub_nw.max_prefixlen - hub_nw.prefixlen) / 2)
        else:
            sub_diff = 25 - hub_nw.prefixlen  # minimum /25 subnet
        subnets = hub_nw.subnets(prefixlen_diff=sub_diff)
        next_sn = next(subnets)  # first subnet reserved for special uses
        first_sn = next_sn.subnets(new_prefix=26)  # split it into /26 subnets
        gws_nw = next(first_sn)  # GatewaySubnet /26
        rem_nw = next(first_sn)  # at least one more /26 subnet, perhaps more
        rem_sn = rem_nw.subnets(new_prefix=27)  # only need /27 save the rest
        abs_nw = next(rem_sn)  # AzureBastionSubnet /27 or greater

        # cast repeatedly referenced networks to strings
        dmz_ar = str(dmz_nw)
        gws_ar = str(gws_nw)

        # Azure Virtual Network to which spokes will be peered
        # separate address spaces to simplify custom routing
        hub = vdc.virtual_network(
            name,
            [
                props.firewall_address_space,
                props.hub_address_space,
            ],
        )

        # Azure will deploy gateways into this subnet
        hub_gw_sn = vdc.subnet_special(
            stem=f'{name}-gw',
            name='GatewaySubnet',  # name required
            virtual_network_name=hub.name,
            address_prefix=gws_ar,
        )

        # A perimeter network for Internet-facing services
        hub_dmz_sn = vdc.subnet_special(  #ToDo add NSG
            stem=f'{name}-dmz',
            name='DMZ',  # name not required but preferred
            virtual_network_name=hub.name,
            address_prefix=dmz_ar,
        )

        # Azure will deploy the firewall into this subnet
        hub_fw_sn = vdc.subnet_special(
            stem=f'{name}-fw',
            name='AzureFirewallSubnet',  # name required
            virtual_network_name=hub.name,
            address_prefix=str(fws_nw),
        )

        # Azure requires this subnet in case of forced_tunnel
        hub_fwm_sn = vdc.subnet_special(
            stem=f'{name}-fwm',
            name='AzureFirewallManagementSubnet',  # name required
            virtual_network_name=hub.name,
            address_prefix=str(fwm_nw),
        )

        # Gateways and Firewall depends_on special subnets
        # to avoid contention in the Azure control plane

        # Azure Firewall
        hub_fw = vdc.firewall(
            stem=name,
            fw_sn_id=hub_fw_sn.id,
            fwm_sn_id=hub_fwm_sn.id,
            depends_on=[hub_dmz_sn, hub_fw_sn, hub_fwm_sn, hub_gw_sn],
        )

        # VPN Gateway
        hub_vpn_gw = vdc.vpn_gateway(
            stem=name,
            subnet_id=hub_gw_sn.id,
            depends_on=[hub_dmz_sn, hub_fw_sn, hub_fwm_sn, hub_gw_sn],
        )

        # ExpressRoute Gateway
        hub_er_gw = vdc.expressroute_gateway(
            stem=name,
            subnet_id=hub_gw_sn.id,
            depends_on=[hub_dmz_sn, hub_fw_sn, hub_fwm_sn, hub_gw_sn],
        )

        # Azure Bastion subnet and host (optional)
        if props.azure_bastion:
            hub_ab_sn = vdc.subnet_special(  #ToDo add NSG if required
                stem=f'{name}-ab',
                name='AzureBastionSubnet',  # name required
                virtual_network_name=hub.name,
                address_prefix=str(abs_nw),
                depends_on=[hub_er_gw, hub_fw, hub_vpn_gw],  # avoid contention
            )
            hub_ab = vdc.bastion_host(
                stem=name,
                subnet_id=hub_ab_sn.id,
            )

        #ToDo requires Azure API version 2019-11-01 or later
        #if props.forced_tunnel:
        # https://docs.microsoft.com/en-us/azure/firewall/forced-tunneling

        # work around https://github.com/pulumi/pulumi/issues/4040
        hub_fw_ip = hub_fw.ip_configurations.apply(
            lambda ipc: ipc[0].get('private_ip_address'))

        # Route Table only to be associated with GatewaySubnet
        hub_gw_rt = vdc.route_table(
            stem=f'{name}-gw',
            disable_bgp_route_propagation=False,
            depends_on=[hub_er_gw, hub_fw, hub_vpn_gw],  # avoid contention
        )
        hub_gw_sn_rta = vdc.subnet_route_table(
            stem=f'{name}-gw',
            route_table_id=hub_gw_rt.id,
            subnet_id=hub_gw_sn.id,
        )

        # Route Table only to be associated with DMZ subnet
        hub_dmz_rt = vdc.route_table(
            stem=f'{name}-dmz',
            disable_bgp_route_propagation=True,
            depends_on=[hub_er_gw, hub_fw, hub_vpn_gw],  # avoid contention
        )
        hub_dmz_sn_rta = vdc.subnet_route_table(
            stem=f'{name}-dmz',
            route_table_id=hub_dmz_rt.id,
            subnet_id=hub_dmz_sn.id,
        )

        # Route Table only to be associated with hub shared services subnets
        hub_ss_rt = vdc.route_table(
            stem=f'{name}-ss',
            disable_bgp_route_propagation=True,
            depends_on=[hub_er_gw, hub_fw, hub_vpn_gw],  # avoid contention
        )

        # protect intra-GatewaySubnet traffic from being redirected
        vdc.route_to_virtual_network(
            stem=f'gw-gw',
            route_table_name=hub_gw_rt.name,
            address_prefix=gws_ar,
        )

        # it is very important to ensure that there is never a route with an
        # address_prefix which covers the AzureFirewallSubnet.

        # partially or fully invalidate system routes to redirect traffic
        for route in [
            (f'gw-dmz', hub_gw_rt.name, dmz_ar),
            (f'gw-hub', hub_gw_rt.name, props.hub_address_space),
            (f'dmz-dg', hub_dmz_rt.name, '0.0.0.0/0'),
            (f'dmz-dmz', hub_dmz_rt.name, dmz_ar),
            (f'dmz-hub', hub_dmz_rt.name, props.hub_address_space),
            (f'ss-dg', hub_ss_rt.name, '0.0.0.0/0'),
            (f'ss-dmz', hub_ss_rt.name, dmz_ar),
            (f'ss-gw', hub_ss_rt.name, gws_ar),
        ]:
            vdc.route_to_virtual_appliance(
                stem=route[0],
                route_table_name=route[1],
                address_prefix=route[2],
                next_hop_in_ip_address=hub_fw_ip,
            )

        # VNet Peering between stacks using StackReference
        if props.peer:
            peer_stack = StackReference(props.reference)
            peer_hub_id = peer_stack.get_output('hub_id')

            # VNet Peering (Global) in one direction from stack to peer
            hub_hub = vdc.vnet_peering(
                stem=props.stack,
                virtual_network_name=hub.name,
                peer=props.peer,
                remote_virtual_network_id=peer_hub_id,
                allow_forwarded_traffic=True,
                allow_gateway_transit=False,  # as both hubs have gateways
            )

            # need to invalidate system routes created by Global VNet Peering
            peer_dmz_ar = peer_stack.get_output('dmz_ar')
            peer_fw_ip = peer_stack.get_output('fw_ip')
            peer_hub_as = peer_stack.get_output('hub_as')

            for route in [
                (f'dmz-{props.peer}-dmz', hub_dmz_rt.name, peer_dmz_ar),
                (f'dmz-{props.peer}-hub', hub_dmz_rt.name, peer_hub_as),
                (f'gw-{props.peer}-dmz', hub_gw_rt.name, peer_dmz_ar),
                (f'gw-{props.peer}-hub', hub_gw_rt.name, peer_hub_as),
                (f'ss-{props.peer}-dmz', hub_ss_rt.name, peer_dmz_ar),
                (f'ss-{props.peer}-hub', hub_ss_rt.name, peer_hub_as),
            ]:
                vdc.route_to_virtual_appliance(
                    stem=route[0],
                    route_table_name=route[1],
                    address_prefix=route[2],
                    next_hop_in_ip_address=peer_fw_ip,
                )

        # shared services subnets starting with the second subnet
        for subnet in props.subnets:
            next_sn = next(subnets)
            hub_sn = vdc.subnet(  #ToDo add NSG
                stem=f'{name}-{subnet[0]}',
                virtual_network_name=hub.name,
                address_prefix=str(next_sn),
                depends_on=[hub_ss_rt],  # avoid contention
            )
            hub_sn_rta = vdc.subnet_route_table(
                stem=f'{name}-{subnet[0]}',
                route_table_id=hub_ss_rt.id,
                subnet_id=hub_sn.id,
            )

        # assign properties to hub including from child resources
        self.address_spaces = hub.address_spaces  # informational
        self.dmz_ar = dmz_ar  # used for routes to the hub
        self.dmz_rt_name = hub_dmz_rt.name  # used to add routes to spokes
        self.er_gw = hub_er_gw  # needed prior to VNet Peering from spokes
        self.fw = hub_fw  # needed prior to VNet Peering from spokes
        self.fw_ip = hub_fw_ip  # used for routes to the hub
        self.gw_rt_name = hub_gw_rt.name  # used to add routes to spokes
        self.hub_as = props.hub_address_space  # used for routes to the hub
        self.id = hub.id  # exported and used for stack and spoke peering
        self.location = hub.location  # informational
        self.name = hub.name  # exported and used for spoke peering
        self.peer = props.peer  # informational
        self.resource_group_name = props.resource_group_name  # informational
        self.subnets = hub.subnets  # exported as informational
        self.stack = props.stack  # informational
        self.stem = name  # used for VNet Peering from spokes
        self.ss_rt_name = hub_ss_rt.name  # used to add routes to spokes
        self.tags = props.tags  # informational
        self.vpn_gw = hub_vpn_gw  # needed prior to VNet Peering from spokes
        self.register_outputs({})
Пример #2
0
    def __init__(self, name: str, props: SpokeProps,
            opts: ResourceOptions=None):
        super().__init__('vdc:network:Spoke', name, {}, opts)

        # set required vdc variables before calling functions
        vdc.resource_group_name = props.resource_group_name
        vdc.tags = props.tags
        vdc.self = self

        # calculate the subnets in spoke_address_space
        spoke_nw = ip_network(props.spoke_address_space)
        if spoke_nw.prefixlen < 24: # split evenly between subnets and hosts
            sub_diff = int((spoke_nw.max_prefixlen - spoke_nw.prefixlen) / 2)
        else:
            sub_diff = 27 - spoke_nw.prefixlen # minimum /27 subnet
        subnets = spoke_nw.subnets(prefixlen_diff=sub_diff)
        next_sn = next(subnets) # first subnet reserved for special uses
        first_sn = next_sn.subnets(new_prefix=27) # subdivide if possible
        abs_nw = next(first_sn) # AzureBastionSubnet /27 or greater

        # Azure Virtual Network to be peered to the hub
        spoke = vdc.virtual_network(name, [props.spoke_address_space])

        # VNet Peering from the hub to spoke
        hub_spoke = vdc.vnet_peering(
            stem = props.hub.stem,
            virtual_network_name = props.hub.name,
            peer = name,
            remote_virtual_network_id = spoke.id,
            allow_gateway_transit = True,
            depends_on=[props.hub.er_gw, props.hub.vpn_gw], # avoid contention
        )

        # VNet Peering from spoke to the hub
        spoke_hub = vdc.vnet_peering(
            stem = name,
            virtual_network_name = spoke.name,
            peer = props.hub.stem,
            remote_virtual_network_id = props.hub.id,
            allow_forwarded_traffic = True,
            use_remote_gateways = True, # requires at least one gateway
            depends_on=[props.hub.er_gw, props.hub.vpn_gw],
        )

        # add routes to spokes in peered stack
        if props.peer:
            peer_fw_ip = props.reference.get_output('fw_ip')
            peer_spoke_as = props.reference.get_output(f'{name}_address_spaces')
            for address_prefix in peer_spoke_as:
                vdc.route_to_virtual_appliance(
                    stem = f'fw-{props.peer}-{name}',
                    route_table_name = props.fw_rt_name,
                    address_prefix = address_prefix,
                    next_hop_in_ip_address = peer_fw_ip,
                ) # only one address_space per spoke at present...

        # Azure Bastion subnet and host (optional)
        if props.azure_bastion:
            spoke_ab_sn = vdc.subnet_special(
                stem = f'{name}-ab',
                name = 'AzureBastionSubnet',
                virtual_network_name = spoke.name,
                address_prefix = str(abs_nw),
                depends_on = [hub_spoke, spoke_hub], # avoid contention
            )
            spoke_ab = vdc.bastion_host(
                stem = name,
                subnet_id = spoke_ab_sn.id,
            )

        # Route Table only to be associated with ordinary spoke subnets
        spoke_rt = vdc.route_table(
            stem = f'{name}',
            disable_bgp_route_propagation = True,
            depends_on = [hub_spoke, spoke_hub], # avoid contention
        )

        # VNet Peering may not be specified as next_hop_type, so a separate
        # hub address space from the firewall is necessary to allow routes
        # from spokes to remain unchanged when hub subnets are added

        # it is very important to ensure that there is never a route with an
        # address_prefix which covers the AzureFirewallSubnet.

        # partially or fully invalidate system routes to redirect traffic
        for route in [
            (f'dmz-{name}', props.hub.dmz_rt_name, props.spoke_address_space),
            (f'gw-{name}', props.hub.gw_rt_name, props.spoke_address_space),
            (f'ss-{name}', props.hub.ss_rt_name, props.spoke_address_space),
            (f'{name}-dg', spoke_rt.name, '0.0.0.0/0'),
            (f'{name}-dmz', spoke_rt.name, props.hub.dmz_ar),
            (f'{name}-hub', spoke_rt.name, props.hub.hub_as),
        ]:
            vdc.route_to_virtual_appliance(
                stem = route[0],
                route_table_name = route[1],
                address_prefix = route[2],
                next_hop_in_ip_address = props.hub.fw_ip,
            )

        # ordinary spoke subnets starting with the second subnet
        for subnet in props.subnets:
            next_sn = next(subnets)
            spoke_sn = vdc.subnet(
                stem = f'{name}-{subnet[0]}',
                virtual_network_name = spoke.name,
                address_prefix = str(next_sn),
                depends_on = [spoke_rt], # avoid contention
            )
            spoke_sn_rta = vdc.subnet_route_table(
                stem = f'{name}-{subnet[0]}',
                route_table_id = spoke_rt.id,
                subnet_id = spoke_sn.id,
            )

        # assign properties to spoke including from child resources
        self.address_spaces = spoke.address_spaces #exported
        self.hub = props.hub.id
        self.id = spoke.id # exported
        self.location = spoke.location
        self.name = spoke.name # exported
        self.resource_group_name = props.resource_group_name
        self.subnets = spoke.subnets
        self.stem = name
        self.tags = props.tags
        self.register_outputs({})
Пример #3
0
    def __init__(self,
                 name: str,
                 props: SpokeProps,
                 opts: ResourceOptions = None):
        super().__init__('vdc:network:Spoke', name, {}, opts)

        # set vdc defaults
        vdc.resource_group_name = props.resource_group_name
        vdc.tags = props.tags
        vdc.self = self

        # Azure Virtual Network to be peered to the hub
        spoke = vdc.virtual_network(name, [props.spoke_as])

        # VNet Peering from the hub to spoke
        hub_spoke = vdc.vnet_peering(
            stem=props.hub.stem,
            virtual_network_name=props.hub.name,
            peer=name,
            remote_virtual_network_id=spoke.id,
            allow_gateway_transit=True,
            depends_on=[props.hub.er_gw, props.hub.vpn_gw],  # avoid contention
        )

        # VNet Peering from spoke to the hub
        spoke_hub = vdc.vnet_peering(
            stem=name,
            virtual_network_name=spoke.name,
            peer=props.hub.stem,
            remote_virtual_network_id=props.hub.id,
            allow_forwarded_traffic=True,
            use_remote_gateways=True,  # requires at least one gateway
            depends_on=[props.hub.er_gw, props.hub.vpn_gw],
        )

        # provisioning of optional subnet and routes depends_on VNet Peerings
        # to avoid contention in the Azure control plane

        # AzureBastionSubnet (optional)
        if props.sbs_ar:
            spoke_sbs_sn = vdc.subnet_special(
                stem=f'{name}-ab',
                name='AzureBastionSubnet',
                virtual_network_name=spoke.name,
                address_prefix=props.sbs_ar,
                depends_on=[hub_spoke, spoke_hub],
            )

        # Route Table only to be associated with ordinary spoke subnets
        spoke_rt = vdc.route_table(
            stem=f'{name}',
            disable_bgp_route_propagation=True,
            depends_on=[hub_spoke, spoke_hub],
        )

        # as VNet Peering may not be specified as next_hop_type, a separate
        # address space in the hub from the firewall allows routes from the
        # spoke to remain unchanged when subnets are added in the hub

        # it is very important to ensure that there is never a route with an
        # address_prefix which covers the AzureFirewallSubnet.
        #ToDo check AzureFirewallManagementSubnet requirements

        # partially or fully invalidate system routes to redirect traffic
        for route in [
            (f'dmz-{name}', props.hub.dmz_rt_name, props.spoke_as),
            (f'gw-{name}', props.hub.gw_rt_name, props.spoke_as),
            (f'ss-{name}', props.hub.ss_rt_name, props.spoke_as),
            (f'{name}-dg', spoke_rt.name, '0.0.0.0/0'),
            (f'{name}-dmz', spoke_rt.name, props.hub.dmz_ar),
            (f'{name}-hub', spoke_rt.name, props.hub.hub_as),
        ]:
            vdc.route_to_virtual_appliance(
                stem=route[0],
                route_table_name=route[1],
                address_prefix=route[2],
                next_hop_in_ip_address=props.hub.fw_ip,
            )

        # provisioning of subnets depends_on Route Table (VNet Peerings)
        # to avoid contention in the Azure control plane

        # ordinary spoke subnets
        subnet_range = props.spoke_ar
        for subnet in props.subnets:
            spoke_sn = vdc.subnet(
                stem=f'{name}-{subnet[0]}',
                virtual_network_name=spoke.name,
                address_prefix=subnet_range,
                depends_on=[spoke_rt],
            )
            # associate all ordinary spoke subnets to Route Table
            spoke_sn_rta = vdc.subnet_route_table(
                stem=f'{name}-{subnet[0]}',
                route_table_id=spoke_rt.id,
                subnet_id=spoke_sn.id,
            )
            subnet_range = vdc.subnet_next(props.spoke_as, subnet_range)

        # assign properties to spoke including from child resources
        self.address_spaces = spoke.address_spaces
        self.hub = props.hub.id
        self.id = spoke.id
        self.location = spoke.location
        self.name = spoke.name
        self.resource_group_name = props.resource_group_name
        self.subnets = spoke.subnets
        self.stem = name
        self.tags = props.tags
        self.register_outputs({})
Пример #4
0
    def __init__(self, name: str, props: HubProps, opts: ResourceOptions=None):
        super().__init__('vdc:network:Hub', name, {}, opts)

        # retrieve configuration
        dmz_ar = props.config.require('dmz_ar')
        fwm_ar = props.config.get('fwm_ar')
        fws_ar = props.config.require('fws_ar')
        fwz_as = props.config.require('fwz_as')
        gws_ar = props.config.require('gws_ar')
        hbs_ar = props.config.get('hbs_ar')
        hub_ar = props.config.get('hub_ar')
        hub_as = props.config.require('hub_as')

        # set vdc defaults
        vdc.resource_group_name = props.resource_group.name
        vdc.location = props.resource_group.location
        vdc.tags = props.tags
        vdc.self = self

        # Azure Virtual Network to which spokes will be peered
        # separate address spaces to simplify custom routing
        hub = vdc.virtual_network(name, [fwz_as, hub_as])

        # DMZ subnet
        hub_dmz_sn = vdc.subnet_special( #ToDo add NSG
            stem = f'{name}-dmz',
            name = 'DMZ', # name not required but preferred
            virtual_network_name = hub.name,
            address_prefix = dmz_ar,
        )

        # AzureFirewallSubnet
        hub_fw_sn = vdc.subnet_special(
            stem = f'{name}-fw',
            name = 'AzureFirewallSubnet', # name required
            virtual_network_name = hub.name,
            address_prefix = fws_ar,
        )

        # GatewaySubnet
        hub_gw_sn = vdc.subnet_special(
            stem = f'{name}-gw',
            name = 'GatewaySubnet', # name required
            virtual_network_name = hub.name,
            address_prefix = gws_ar,
        )

        # provisioning of Gateways and Firewall depends_on subnets
        # to avoid contention in the Azure control plane

        # VPN Gateway
        hub_vpn_gw = vdc.vpn_gateway(
            stem = name,
            subnet_id = hub_gw_sn.id,
            depends_on=[hub_dmz_sn, hub_fw_sn, hub_gw_sn],
        )

        # ExpressRoute Gateway
        hub_er_gw = vdc.expressroute_gateway(
            stem = name,
            subnet_id = hub_gw_sn.id,
            depends_on=[hub_dmz_sn, hub_fw_sn, hub_gw_sn],
        )

        # Azure Firewall
        hub_fw = vdc.firewall(
            stem = name,
            subnet_id = hub_fw_sn.id,
            depends_on=[hub_dmz_sn, hub_fw_sn, hub_gw_sn],
        )

        # provisioning of optional subnets depends_on Gateways and Firewall
        # to avoid contention in the Azure control plane

        # AzureBastionSubnet (optional)
        if hbs_ar:
            hub_ab_sn = vdc.subnet_special( #ToDo add NSG if required
                stem = f'{name}-ab',
                name = 'AzureBastionSubnet', # name required
                virtual_network_name = hub.name,
                address_prefix = hbs_ar,
                depends_on=[hub_er_gw, hub_fw, hub_vpn_gw],
            )

        # AzureFirewallManagementSubnet (optional)
        if fwm_ar:
            hub_fwm_sn = vdc.subnet_special(
                stem = f'{name}-fwm',
                name = 'AzureFirewallManagementSubnet', # name required
                virtual_network_name = hub.name,
                address_prefix = fwm_ar,
                depends_on=[hub_er_gw, hub_fw, hub_vpn_gw],
            )

        # work around https://github.com/pulumi/pulumi/issues/4040
        hub_fw_ip = hub_fw.ip_configurations.apply(
            lambda ipc: ipc[0].get('private_ip_address')
        )

        # provisioning of Route Tables depends_on Gateways and Firewall
        # to avoid contention in the Azure control plane

        # Route Table only to be associated with the GatewaySubnet
        hub_gw_rt = vdc.route_table(
            stem = f'{name}-gw',
            disable_bgp_route_propagation = False,
            depends_on=[hub_er_gw, hub_fw, hub_vpn_gw],
        )

        # associate GatewaySubnet with Route Table
        hub_gw_sn_rta = vdc.subnet_route_table(
            stem = f'{name}-gw',
            route_table_id = hub_gw_rt.id,
            subnet_id = hub_gw_sn.id,
        )

        # Route Table only to be associated with DMZ subnet
        hub_dmz_rt = vdc.route_table(
            stem = f'{name}-dmz',
            disable_bgp_route_propagation = True,
            depends_on=[hub_er_gw, hub_fw, hub_vpn_gw],
        )

        # associate DMZ subnet with Route Table
        hub_dmz_sn_rta = vdc.subnet_route_table(
            stem = f'{name}-dmz',
            route_table_id = hub_dmz_rt.id,
            subnet_id = hub_dmz_sn.id,
        )

        # Route Table only to be associated with ordinary subnets in hub
        hub_sn_rt = vdc.route_table(
            stem = f'{name}-sn',
            disable_bgp_route_propagation = True,
            depends_on=[hub_er_gw, hub_fw, hub_vpn_gw],
        )

        # protect intra-GatewaySubnet traffic from being redirected
        vdc.route_to_virtual_network(
            stem = f'gw-gw',
            route_table_name = hub_gw_rt.name,
            address_prefix = gws_ar,
        )

        # partially or fully invalidate system routes to redirect traffic
        for route in [
            (f'gw-dmz', hub_gw_rt.name, dmz_ar),
            (f'gw-hub', hub_gw_rt.name, hub_as),
            (f'dmz-dg', hub_dmz_rt.name, '0.0.0.0/0'),
            (f'dmz-dmz', hub_dmz_rt.name, dmz_ar),
            (f'dmz-hub', hub_dmz_rt.name, hub_as),
            (f'sn-dg', hub_sn_rt.name, '0.0.0.0/0'),
            (f'sn-dmz', hub_sn_rt.name, dmz_ar),
            (f'sn-gw', hub_sn_rt.name, gws_ar),
        ]:
            vdc.route_to_virtual_appliance(
                stem = route[0],
                route_table_name = route[1],
                address_prefix = route[2],
                next_hop_in_ip_address = hub_fw_ip,
            )

        # VNet Peering between stacks using StackReference
        peer = props.config.get('peer')
        if peer:
            org = props.config.require('org')
            project = get_project()
            peer_stack = StackReference(f'{org}/{project}/{peer}')
            peer_hub_id = peer_stack.get_output('hub_id')
            peer_fw_ip = peer_stack.get_output('hub_fw_ip')
            peer_dmz_ar = peer_stack.get_output('dmz_ar') 
            peer_hub_as = peer_stack.get_output('hub_as')

            # VNet Peering (Global) in one direction from stack to peer
            hub_hub = vdc.vnet_peering(
                stem = props.stack,
                virtual_network_name = hub.name,
                peer = peer,
                remote_virtual_network_id = peer_hub_id,
                allow_forwarded_traffic = True,
                allow_gateway_transit = False, # as both hubs have gateways
            )

            # need to invalidate system routes created by Global VNet Peering
            for route in [
                (f'dmz-{peer}-dmz', hub_dmz_rt.name, peer_dmz_ar),
                (f'dmz-{peer}-hub', hub_dmz_rt.name, peer_hub_as),
                (f'gw-{peer}-dmz', hub_gw_rt.name, peer_dmz_ar),
                (f'gw-{peer}-hub', hub_gw_rt.name, peer_hub_as),
                (f'sn-{peer}-dmz', hub_sn_rt.name, peer_dmz_ar),
                (f'sn-{peer}-hub', hub_sn_rt.name, peer_hub_as),
            ]:
                vdc.route_to_virtual_appliance(
                    stem = route[0],
                    route_table_name = route[1],
                    address_prefix = route[2],
                    next_hop_in_ip_address = peer_fw_ip,
                )
        
        # provisioning of subnets depends_on Route Table (Gateways & Firewall)
        # to avoid contention in the Azure control plane

        # only one shared subnet is provisioned as an example, but many can be
        if hub_ar: #ToDo replace with loop
            hub_example_sn = vdc.subnet( #ToDo add NSG
                stem = f'{name}-example',
                virtual_network_name = hub.name,
                address_prefix = hub_ar,
                depends_on=[hub_sn_rt],
            )

            # associate all hub shared services subnets to Route Table        
            hub_example_sn_rta = vdc.subnet_route_table(
                stem = f'{name}-example',
                route_table_id = hub_sn_rt.id,
                subnet_id = hub_example_sn.id,
            )

        combined_output = Output.all(
            hub_dmz_rt.name,
            hub_er_gw,
            hub_fw,
            hub_fw_ip,
            hub_gw_rt.name,
            hub.id,
            hub.name,
            hub_sn_rt.name,
            hub.subnets,
            hub_vpn_gw,
        ).apply

        self.hub_dmz_rt_name = hub_dmz_rt.name # used to add routes to spokes
        self.hub_er_gw = hub_er_gw # needed prior to VNet Peering from spokes
        self.hub_fw = hub_fw # needed prior to VNet Peering from spokes
        self.hub_fw_ip = hub_fw_ip # used to construct routes
        self.hub_gw_rt_name = hub_gw_rt.name # used to add routes to spokes
        self.hub_id = hub.id # exported and used for peering
        self.hub_name = hub.name # exported and used for peering
        self.hub_sn_rt_name = hub_sn_rt.name # used to add routes to spokes
        self.hub_subnets = hub.subnets # exported as informational
        self.hub_vpn_gw = hub_vpn_gw # needed prior to VNet Peering from spokes
        self.register_outputs({})
Пример #5
0
    def __init__(self,
                 name: str,
                 props: SpokeProps,
                 opts: ResourceOptions = None):
        super().__init__('vdc:network:Spoke', name, {}, opts)

        # retrieve configuration
        dmz_ar = props.config.require('dmz_ar')
        hub_as = props.config.require('hub_as')
        hub_stem = props.config.require('hub_stem')
        sbs_ar = props.config.get('sbs_ar')
        spoke_ar = props.config.get('spoke_ar')
        spoke_as = props.config.require('spoke_as')

        # set vdc defaults
        vdc.resource_group_name = props.resource_group.name
        vdc.location = props.resource_group.location
        vdc.tags = props.tags
        vdc.self = self

        # Azure Virtual Network to be peered to the hub
        spoke = vdc.virtual_network(name, [spoke_as])

        # VNet Peering from the hub to spoke
        hub_spoke = vdc.vnet_peering(
            stem=hub_stem,
            virtual_network_name=props.hub.hub_name,
            peer=name,
            remote_virtual_network_id=spoke.id,
            allow_gateway_transit=True,
        )

        # VNet Peering from spoke to the hub
        spoke_hub = vdc.vnet_peering(
            stem=name,
            virtual_network_name=spoke.name,
            peer=hub_stem,
            remote_virtual_network_id=props.hub.hub_id,
            allow_forwarded_traffic=True,
            use_remote_gateways=True,
        )

        # provisioning of optional subnet and routes depends_on VNet Peerings
        # to avoid contention in the Azure control plane

        # AzureBastionSubnet (optional)
        if sbs_ar:
            spoke_sbs_sn = vdc.subnet_special(
                stem=f'{name}-ab',
                name='AzureBastionSubnet',
                virtual_network_name=spoke.name,
                address_prefix=sbs_ar,
                depends_on=[hub_spoke, spoke_hub],
            )

        # Route Table only to be associated with ordinary spoke subnets
        spoke_sn_rt = vdc.route_table(
            stem=f'{name}-sn',
            disable_bgp_route_propagation=True,
            depends_on=[hub_spoke, spoke_hub],
        )

        # provisioning of subnets depends_on VNet Peerings and Route Table
        # to avoid contention in the Azure control plane

        # only one spoke subnet is provisioned as an example, but many can be
        if spoke_ar:  # replace with a loop
            spoke_example_sn = vdc.subnet(
                stem=f'{name}-example',
                virtual_network_name=spoke.name,
                address_prefix=spoke_ar,
                depends_on=[spoke_sn_rt],
            )
            # associate all ordinary spoke subnets to Route Table
            spoke_example_sn_rta = vdc.subnet_route_table(
                stem=f'{name}-example',
                route_table_id=spoke_sn_rt.id,
                subnet_id=spoke_example_sn.id,
            )

        # as VNet Peering may not be specified as next_hop_type, a separate
        # address space in the hub from the firewall allows routes from the
        # spoke to remain unchanged when subnets are added in the hub

        # it is very important to ensure that there is never a route with an
        # address_prefix which covers the AzureFirewallSubnet.
        #ToDo check AzureFirewallManagementSubnet requirements

        # partially or fully invalidate system routes to redirect traffic
        for route in [
            (f'dmz-{name}', props.hub.hub_dmz_rt_name, spoke_as),
            (f'gw-{name}', props.hub.hub_gw_rt_name, spoke_as),
            (f'sn-{name}', props.hub.hub_sn_rt_name, spoke_as),
            (f'{name}-dg', spoke_sn_rt.name, '0.0.0.0/0'),
            (f'{name}-dmz', spoke_sn_rt.name, dmz_ar),
            (f'{name}-hub', spoke_sn_rt.name, hub_as),
        ]:
            vdc.route_to_virtual_appliance(
                stem=route[0],
                route_table_name=route[1],
                address_prefix=route[2],
                next_hop_in_ip_address=props.hub.hub_fw_ip,
            )

        combined_output = Output.all(spoke.name, spoke.id, spoke.subnets).apply

        self.spoke_id = spoke.id  # exported as informational
        self.spoke_name = spoke.name  # exported as informational
        self.spoke_subnets = spoke.subnets  # exported as informational
        self.register_outputs({})
Пример #6
0
    def __init__(self, name: str, props: HubProps, opts: ResourceOptions = None):
        super().__init__('vdc:network:Hub', name, {}, opts)

        # set required vdc variables before calling functions
        vdc.location = props.location
        vdc.resource_group_name = props.resource_group_name
        vdc.s = props.separator
        vdc.self = self
        vdc.suffix = props.suffix
        vdc.tags = props.tags

        # calculate the subnets in the firewall_address_space
        fwz_nw = ip_network(props.firewall_address_space)
        fwz_sn = fwz_nw.subnets(new_prefix=25)  # two /26 subnets required
        fwx_nw = next(fwz_sn)  # for Azure Firewall and Management subnets
        fwz_sn = fwz_nw.address_exclude(fwx_nw)  # consolidate remainder
        dmz_nw = next(fwz_sn)  # largest remaining subnet for DMZ
        fwx_sn = fwx_nw.subnets(new_prefix=26)  # split the /25 into two /26
        fws_nw = next(fwx_sn)  # AzureFirewallSubnet
        fwm_nw = next(fwx_sn)  # AzureFirewallManagementSubnet

        # calculate the subnets in the hub_address_space
        hub_nw = ip_network(props.hub_address_space)
        if hub_nw.prefixlen < 20:  # split evenly between subnets and hosts
            sub_diff = int((hub_nw.max_prefixlen - hub_nw.prefixlen) / 2)
        else:
            sub_diff = 25 - hub_nw.prefixlen  # minimum /25 subnet
        subnets = hub_nw.subnets(prefixlen_diff=sub_diff)
        next_sn = next(subnets)  # first subnet reserved for special uses
        first_sn = next_sn.subnets(new_prefix=26)  # split it into /26 subnets
        gws_nw = next(first_sn)  # GatewaySubnet /26
        rem_nw = next(first_sn)  # at least one more /26 subnet, perhaps more
        rem_sn = rem_nw.subnets(new_prefix=27)  # only need /27 save the rest
        abs_nw = next(rem_sn)  # AzureBastionSubnet /27 or greater

        # cast repeatedly referenced networks to strings
        dmz_ar = str(dmz_nw)
        gws_ar = str(gws_nw)

        # set the separator to be used in resource names
        s = props.separator

        # Azure Virtual Network to which spokes will be peered
        # separate address spaces to simplify custom routing
        hub = vdc.virtual_network(name, [
            props.firewall_address_space,
            props.hub_address_space,
        ],
                                  )

        # AzureFirewallManagementSubnet and Route Table
        # https://docs.microsoft.com/en-us/azure/firewall/forced-tunneling
        hub_fwm_rt = vdc.route_table(
            stem=f'{name}{s}fwm',
            disable_bgp_route_propagation=True,  # required
        )
        # only a default route to the Internet is permitted
        hub_fwm_dg = vdc.route_to_internet(
            stem=f'fwm{s}internet',
            route_table_name=hub_fwm_rt.name,
        )
        hub_fwm_sn = vdc.subnet_special(
            stem=f'{name}{s}fwm',
            name='AzureFirewallManagementSubnet',  # name required
            virtual_network_name=hub.name,
            address_prefix=str(fwm_nw),
            route_table_id=hub_fwm_rt.id,
            depends_on=[hub, hub_fwm_rt, hub_fwm_dg],
        )

        # AzureFirewallSubnet and Route Table 
        hub_fw_rt = vdc.route_table(
            stem=f'{name}{s}fw',
            disable_bgp_route_propagation=False,
        )
        # default route either direct to Internet or forced tunnel
        # turn off SNAT if the next_hop_ip_address is public
        # https://docs.microsoft.com/en-us/azure/firewall/snat-private-range
        private_ranges = 'IANAPrivateRanges'
        if not props.forced_tunnel:
            hub_fw_dg = vdc.route_to_internet(
                stem=f'fw{s}internet',
                route_table_name=hub_fw_rt.name,
            )
        else:
            hub_fw_dg = vdc.route_to_virtual_appliance(
                stem=f'fw{s}tunnel',
                route_table_name=hub_fw_rt.name,
                address_prefix='0.0.0.0/0',
                next_hop_ip_address=props.forced_tunnel,
            )
            ft_ip = ip_address(props.forced_tunnel)
            if not ft_ip.is_private:
                private_ranges = '0.0.0.0/0'
        hub_fw_sn = vdc.subnet_special(
            stem=f'{name}{s}fw',
            name='AzureFirewallSubnet',  # name required
            virtual_network_name=hub.name,
            address_prefix=str(fws_nw),
            route_table_id=hub_fw_rt.id,
            depends_on=[hub, hub_fw_rt, hub_fw_dg],
        )

        # Azure Firewall
        hub_fw = vdc.firewall(
            stem=name,
            fw_sn_id=hub_fw_sn.id,
            fwm_sn_id=hub_fwm_sn.id,
            private_ranges=private_ranges,
            depends_on=[hub_fw_sn, hub_fwm_sn],
        )

        # wait for the private ip address of the firewall to become available
        hub_fw_ip = hub_fw.ip_configurations.apply(
            lambda ipc: ipc[0].private_ip_address
        )
        # It is very important to ensure that there is never a route with an
        # address_prefix which covers the AzureFirewallSubnet.

        # DMZ subnet and Route Table
        hub_dmz_rt = vdc.route_table(
            stem=f'{name}{s}dmz',
            disable_bgp_route_propagation=True,
            depends_on=[hub_fw],
        )
        # default route from DMZ via the firewall
        hub_dmz_dg = vdc.route_to_virtual_appliance(
            stem=f'dmz{s}dg',
            route_table_name=hub_dmz_rt.name,
            address_prefix='0.0.0.0/0',
            next_hop_ip_address=hub_fw_ip,
        )
        # redirect intra-DMZ traffic via the firewall
        hub_dmz_dmz = vdc.route_to_virtual_appliance(
            stem=f'dmz{s}dmz',
            route_table_name=hub_dmz_rt.name,
            address_prefix=dmz_ar,
            next_hop_ip_address=hub_fw_ip,
        )
        # redirect traffic from DMZ to hub via the firewall
        hub_dmz_hub = vdc.route_to_virtual_appliance(
            stem=f'dmz{s}hub',
            route_table_name=hub_dmz_rt.name,
            address_prefix=props.hub_address_space,
            next_hop_ip_address=hub_fw_ip,
        )
        hub_dmz_sn = vdc.subnet_special(  # ToDo add NSG
            stem=f'{name}{s}dmz',
            name='DMZ',  # name not required but preferred
            virtual_network_name=hub.name,
            address_prefix=dmz_ar,
            route_table_id=hub_dmz_rt.id,
            depends_on=[hub_dmz_rt, hub_dmz_dg, hub_dmz_dmz, hub_dmz_hub],
        )

        # GatewaySubnet and Route Table
        hub_gw_rt = vdc.route_table(
            stem=f'{name}{s}gw',
            disable_bgp_route_propagation=False,
            depends_on=[hub_dmz_sn],
        )
        # protect intra-GatewaySubnet traffic from being redirected:
        hub_gw_gw = vdc.route_to_virtual_network(
            stem=f'gw{s}gw',
            route_table_name=hub_gw_rt.name,
            address_prefix=gws_ar,
        )
        # redirect traffic from gateways to DMZ via firewall
        hub_gw_dmz = vdc.route_to_virtual_appliance(
            stem=f'gw{s}dmz',
            route_table_name=hub_gw_rt.name,
            address_prefix=dmz_ar,
            next_hop_ip_address=hub_fw_ip,
        )
        # redirect traffic from gateways to hub via firewall
        hub_gw_hub = vdc.route_to_virtual_appliance(
            stem=f'gw{s}hub',
            route_table_name=hub_gw_rt.name,
            address_prefix=props.hub_address_space,
            next_hop_ip_address=hub_fw_ip,
        )
        hub_gw_sn = vdc.subnet_special(
            stem=f'{name}{s}gw',
            name='GatewaySubnet',  # name required
            virtual_network_name=hub.name,
            address_prefix=gws_ar,
            route_table_id=hub_gw_rt.id,
            depends_on=[hub_gw_rt, hub_gw_gw, hub_gw_dmz, hub_gw_hub],
        )

        # VPN Gateway
        hub_vpn_gw = vdc.vpn_gateway(
            stem=name,
            subnet_id=hub_gw_sn.id,
            depends_on=[hub_gw_sn],
        )

        # ExpressRoute Gateway
        hub_er_gw = vdc.expressroute_gateway(
            stem=name,
            subnet_id=hub_gw_sn.id,
            depends_on=[hub_gw_sn],
        )

        # Route Table to be associated with all hub shared services subnets
        hub_ss_rt = vdc.route_table(
            stem=f'{name}{s}ss',
            disable_bgp_route_propagation=True,
            depends_on=[hub_er_gw, hub_vpn_gw],
        )
        # default route from hub via the firewall
        hub_ss_dg = vdc.route_to_virtual_appliance(
            stem=f'ss{s}dg',
            route_table_name=hub_ss_rt.name,
            address_prefix='0.0.0.0/0',
            next_hop_ip_address=hub_fw_ip,
        )
        # redirect traffic from hub to DMZ via the firewall
        hub_ss_dmz = vdc.route_to_virtual_appliance(
            stem=f'ss{s}dmz',
            route_table_name=hub_ss_rt.name,
            address_prefix=dmz_ar,
            next_hop_ip_address=hub_fw_ip,
        )
        # redirect traffic from hub to gateways via the firewall
        hub_ss_gw = vdc.route_to_virtual_appliance(
            stem=f'ss{s}gw',
            route_table_name=hub_ss_rt.name,
            address_prefix=gws_ar,
            next_hop_ip_address=hub_fw_ip,
        )
        # shared services subnets starting with the second subnet
        for subnet in props.subnets:
            next_sn = next(subnets)
            hub_sn = vdc.subnet(  # ToDo add NSG
                stem=f'{name}{s}{subnet[0]}',
                virtual_network_name=hub.name,
                address_prefix=str(next_sn),
                route_table_id=hub_ss_rt.id,
                depends_on=[hub_ss_rt, hub_ss_dg, hub_ss_dmz, hub_ss_gw],
            )

        # Azure Bastion subnet and host (optional)
        if props.azure_bastion:
            hub_ab = vdc.bastion_host(
                stem=name,
                virtual_network_name=hub.name,
                address_prefix=str(abs_nw),
                depends_on=[hub_er_gw, hub_vpn_gw],
            )

        # VNet Peering between stacks using StackReference (optional)
        if props.peer:
            peer_hub_id = props.reference.get_output('hub_id')
            # VNet Peering (Global) in one direction from stack to peer
            hub_hub = vdc.vnet_peering(
                stem=props.stack,
                virtual_network_name=hub.name,
                peer=props.peer,
                remote_virtual_network_id=peer_hub_id,
                allow_forwarded_traffic=True,
                allow_gateway_transit=False,  # as both hubs have gateways
            )
            # need to invalidate system routes created by VNet Peering
            peer_dmz_ar = props.reference.get_output('dmz_ar')
            peer_fw_ip = props.reference.get_output('fw_ip')
            peer_hub_as = props.reference.get_output('hub_as')
            for route in [
                (f'dmz{s}{props.peer}{s}dmz', hub_dmz_rt.name, peer_dmz_ar),
                (f'dmz{s}{props.peer}{s}hub', hub_dmz_rt.name, peer_hub_as),
                (f'gw{s}{props.peer}{s}dmz', hub_gw_rt.name, peer_dmz_ar),
                (f'gw{s}{props.peer}{s}hub', hub_gw_rt.name, peer_hub_as),
                (f'ss{s}{props.peer}{s}dmz', hub_ss_rt.name, peer_dmz_ar),
                (f'ss{s}{props.peer}{s}hub', hub_ss_rt.name, peer_hub_as),
            ]:
                vdc.route_to_virtual_appliance(
                    stem=route[0],
                    route_table_name=route[1],
                    address_prefix=route[2],
                    next_hop_ip_address=peer_fw_ip,
                )

        # assign properties to hub including from child resources
        self.address_space = props.hub_address_space  # used for routes to the hub
        self.dmz_ar = dmz_ar  # used for routes to the hub
        self.dmz_rt_name = hub_dmz_rt.name  # used to add routes to spokes
        self.er_gw = hub_er_gw  # needed prior to VNet Peering from spokes
        self.fw = hub_fw  # needed prior to VNet Peering from spokes
        self.fw_ip = hub_fw_ip  # used for routes to the hub
        self.fw_rt_name = hub_fw_rt.name  # used for route to the peered spokes
        self.gw_rt_name = hub_gw_rt.name  # used to add routes to spokes
        self.id = hub.id  # exported and used for stack and spoke peering
        self.location = hub.location  # informational
        self.name = hub.name  # exported and used for spoke peering
        self.peer = props.peer  # informational
        self.resource_group_name = props.resource_group_name  # informational
        self.subnets = hub.subnets  # informational
        self.stack = props.stack  # informational
        self.stem = name  # used for VNet Peering from spokes
        self.ss_rt_name = hub_ss_rt.name  # used to add routes to spokes
        self.tags = props.tags  # informational
        self.vpn_gw = hub_vpn_gw  # needed prior to VNet Peering from spokes
        self.register_outputs({})
Пример #7
0
    def __init__(self,
                 name: str,
                 props: HubProps,
                 opts: ResourceOptions = None):
        super().__init__('vdc:network:Hub', name, {}, opts)

        # set vdc defaults
        vdc.resource_group_name = props.resource_group_name
        vdc.tags = props.tags
        vdc.self = self

        # Azure Virtual Network to which spokes will be peered
        # separate address spaces to simplify custom routing
        hub = vdc.virtual_network(name, [props.fwz_as, props.hub_as])

        # DMZ subnet
        hub_dmz_sn = vdc.subnet_special(  #ToDo add NSG
            stem=f'{name}-dmz',
            name='DMZ',  # name not required but preferred
            virtual_network_name=hub.name,
            address_prefix=props.dmz_ar,
        )

        # AzureFirewallSubnet
        hub_fw_sn = vdc.subnet_special(
            stem=f'{name}-fw',
            name='AzureFirewallSubnet',  # name required
            virtual_network_name=hub.name,
            address_prefix=props.fws_ar,
        )

        # GatewaySubnet
        hub_gw_sn = vdc.subnet_special(
            stem=f'{name}-gw',
            name='GatewaySubnet',  # name required
            virtual_network_name=hub.name,
            address_prefix=props.gws_ar,
        )

        # provisioning of Gateways and Firewall depends_on subnets
        # to avoid contention in the Azure control plane

        # VPN Gateway
        hub_vpn_gw = vdc.vpn_gateway(
            stem=name,
            subnet_id=hub_gw_sn.id,
            depends_on=[hub_dmz_sn, hub_fw_sn, hub_gw_sn],
        )

        # ExpressRoute Gateway
        hub_er_gw = vdc.expressroute_gateway(
            stem=name,
            subnet_id=hub_gw_sn.id,
            depends_on=[hub_dmz_sn, hub_fw_sn, hub_gw_sn],
        )

        # Azure Firewall
        hub_fw = vdc.firewall(
            stem=name,
            subnet_id=hub_fw_sn.id,
            depends_on=[hub_dmz_sn, hub_fw_sn, hub_gw_sn],
        )

        # provisioning of optional subnets depends_on Gateways and Firewall
        # to avoid contention in the Azure control plane

        # AzureBastionSubnet (optional)
        if props.hbs_ar:
            hub_ab_sn = vdc.subnet_special(  #ToDo add NSG if required
                stem=f'{name}-ab',
                name='AzureBastionSubnet',  # name required
                virtual_network_name=hub.name,
                address_prefix=props.hbs_ar,
                depends_on=[hub_er_gw, hub_fw, hub_vpn_gw],
            )

        # AzureFirewallManagementSubnet (optional)
        if props.fwm_ar:
            hub_fwm_sn = vdc.subnet_special(
                stem=f'{name}-fwm',
                name='AzureFirewallManagementSubnet',  # name required
                virtual_network_name=hub.name,
                address_prefix=props.fwm_ar,
                depends_on=[hub_er_gw, hub_fw, hub_vpn_gw],
            )

        # work around https://github.com/pulumi/pulumi/issues/4040
        hub_fw_ip = hub_fw.ip_configurations.apply(
            lambda ipc: ipc[0].get('private_ip_address'))

        # provisioning of Route Tables depends_on Gateways and Firewall
        # to avoid contention in the Azure control plane

        # Route Table only to be associated with the GatewaySubnet
        hub_gw_rt = vdc.route_table(
            stem=f'{name}-gw',
            disable_bgp_route_propagation=False,
            depends_on=[hub_er_gw, hub_fw, hub_vpn_gw],
        )

        # associate GatewaySubnet with Route Table
        hub_gw_sn_rta = vdc.subnet_route_table(
            stem=f'{name}-gw',
            route_table_id=hub_gw_rt.id,
            subnet_id=hub_gw_sn.id,
        )

        # Route Table only to be associated with DMZ subnet
        hub_dmz_rt = vdc.route_table(
            stem=f'{name}-dmz',
            disable_bgp_route_propagation=True,
            depends_on=[hub_er_gw, hub_fw, hub_vpn_gw],
        )

        # associate DMZ subnet with Route Table
        hub_dmz_sn_rta = vdc.subnet_route_table(
            stem=f'{name}-dmz',
            route_table_id=hub_dmz_rt.id,
            subnet_id=hub_dmz_sn.id,
        )

        # Route Table only to be associated with shared services subnets in hub
        hub_ss_rt = vdc.route_table(
            stem=f'{name}-ss',
            disable_bgp_route_propagation=True,
            depends_on=[hub_er_gw, hub_fw, hub_vpn_gw],
        )

        # protect intra-GatewaySubnet traffic from being redirected
        vdc.route_to_virtual_network(
            stem=f'gw-gw',
            route_table_name=hub_gw_rt.name,
            address_prefix=props.gws_ar,
        )

        # partially or fully invalidate system routes to redirect traffic
        for route in [
            (f'gw-dmz', hub_gw_rt.name, props.dmz_ar),
            (f'gw-hub', hub_gw_rt.name, props.hub_as),
            (f'dmz-dg', hub_dmz_rt.name, '0.0.0.0/0'),
            (f'dmz-dmz', hub_dmz_rt.name, props.dmz_ar),
            (f'dmz-hub', hub_dmz_rt.name, props.hub_as),
            (f'ss-dg', hub_ss_rt.name, '0.0.0.0/0'),
            (f'ss-dmz', hub_ss_rt.name, props.dmz_ar),
            (f'ss-gw', hub_ss_rt.name, props.gws_ar),
        ]:
            vdc.route_to_virtual_appliance(
                stem=route[0],
                route_table_name=route[1],
                address_prefix=route[2],
                next_hop_in_ip_address=hub_fw_ip,
            )

        # VNet Peering between stacks using StackReference
        if props.peer:
            peer_stack = StackReference(props.ref)
            peer_hub_id = peer_stack.get_output('hub_id')

            # VNet Peering (Global) in one direction from stack to peer
            hub_hub = vdc.vnet_peering(
                stem=props.stack,
                virtual_network_name=hub.name,
                peer=props.peer,
                remote_virtual_network_id=peer_hub_id,
                allow_forwarded_traffic=True,
                allow_gateway_transit=False,  # as both hubs have gateways
            )

            # need to invalidate system routes created by Global VNet Peering
            peer_dmz_ar = peer_stack.get_output('dmz_ar')
            peer_fw_ip = peer_stack.get_output('fw_ip')
            peer_hub_as = peer_stack.get_output('hub_as')

            for route in [
                (f'dmz-{props.peer}-dmz', hub_dmz_rt.name, peer_dmz_ar),
                (f'dmz-{props.peer}-hub', hub_dmz_rt.name, peer_hub_as),
                (f'gw-{props.peer}-dmz', hub_gw_rt.name, peer_dmz_ar),
                (f'gw-{props.peer}-hub', hub_gw_rt.name, peer_hub_as),
                (f'ss-{props.peer}-dmz', hub_ss_rt.name, peer_dmz_ar),
                (f'ss-{props.peer}-hub', hub_ss_rt.name, peer_hub_as),
            ]:
                vdc.route_to_virtual_appliance(
                    stem=route[0],
                    route_table_name=route[1],
                    address_prefix=route[2],
                    next_hop_in_ip_address=peer_fw_ip,
                )

        # provisioning of subnets depends_on Route Table (Gateways & Firewall)
        # to avoid contention in the Azure control plane

        # shared services subnets
        subnet_range = props.hub_ar
        for subnet in props.subnets:
            hub_sn = vdc.subnet(  #ToDo add NSG
                stem=f'{name}-{subnet[0]}',
                virtual_network_name=hub.name,
                address_prefix=subnet_range,
                depends_on=[hub_ss_rt],
            )
            # associate all hub shared services subnets to Route Table
            hub_sn_rta = vdc.subnet_route_table(
                stem=f'{name}-{subnet[0]}',
                route_table_id=hub_ss_rt.id,
                subnet_id=hub_sn.id,
            )
            subnet_range = vdc.subnet_next(props.hub_as, subnet_range)

        # assign properties to hub including from child resources
        self.address_spaces = hub.address_spaces  # informational
        self.dmz_ar = props.dmz_ar  # used to construct routes to the hub
        self.dmz_rt_name = hub_dmz_rt.name  # used to add routes to spokes
        self.er_gw = hub_er_gw  # needed prior to VNet Peering from spokes
        self.fw = hub_fw  # needed prior to VNet Peering from spokes
        self.fw_ip = hub_fw_ip  # used to construct routes to the hub
        self.gw_rt_name = hub_gw_rt.name  # used to add routes to spokes
        self.hub_as = props.hub_as  # used to construct routes to the hub
        self.id = hub.id  # exported and used for stack and spoke peering
        self.location = hub.location  # informational
        self.name = hub.name  # exported and used for spoke peering
        self.peer = props.peer  # informational
        self.resource_group_name = props.resource_group_name  # informational
        self.subnets = hub.subnets  # exported as informational
        self.stack = props.stack  # informational
        self.stem = name  # used for VNet Peering from spokes
        self.ss_rt_name = hub_ss_rt.name  # used to add routes to spokes
        self.tags = props.tags  # informational
        self.vpn_gw = hub_vpn_gw  # needed prior to VNet Peering from spokes
        self.register_outputs({})