From 9cb78d2cbcf37a441032a5b6cb4e9f96ff9c8a0a Mon Sep 17 00:00:00 2001 From: James Berry <60497203+jamesianberry@users.noreply.github.com> Date: Mon, 20 Jul 2020 22:12:15 +1000 Subject: [PATCH] add routes to peered spokes (#747) --- azure-py-virtual-data-center/.gitignore | 2 + azure-py-virtual-data-center/README.md | 4 +- azure-py-virtual-data-center/__main__.py | 12 +++-- azure-py-virtual-data-center/config.py | 14 ++++-- azure-py-virtual-data-center/hub.py | 63 +++++++++++++++++++----- azure-py-virtual-data-center/spoke.py | 24 +++++++-- azure-py-virtual-data-center/vdc.py | 11 +++++ 7 files changed, 107 insertions(+), 23 deletions(-) diff --git a/azure-py-virtual-data-center/.gitignore b/azure-py-virtual-data-center/.gitignore index 894a640fc..3fcfba667 100644 --- a/azure-py-virtual-data-center/.gitignore +++ b/azure-py-virtual-data-center/.gitignore @@ -1,3 +1,5 @@ *.pyc +.venv/ +.vscode/ venv/ __pycache__ diff --git a/azure-py-virtual-data-center/README.md b/azure-py-virtual-data-center/README.md index 8cfd79ec5..1c0f511a4 100644 --- a/azure-py-virtual-data-center/README.md +++ b/azure-py-virtual-data-center/README.md @@ -50,7 +50,7 @@ After cloning this repo, `cd` into the `azure-py-virtual-data-center` directory Optional: ```bash $ pulumi config set azure_bastion "true" - $ pulumi config set forced_tunnel "true" + $ pulumi config set forced_tunnel "10.0.100.1" ``` 1. Deploy the `prod` stack with the `pulumi up` command. This may take up to an hour to provision all the Azure resources specified, including gateways, firewall and bastion hosts: @@ -175,7 +175,7 @@ After cloning this repo, `cd` into the `azure-py-virtual-data-center` directory Optional: ```bash $ pulumi config set azure_bastion "true" - $ pulumi config set forced_tunnel "true" + $ pulumi config set forced_tunnel "10.0.200.1" ``` 1. Deploy the `dr` stack with the `pulumi up` command. Once again, this may take up to an hour to provision all the Azure resources specified, including gateways, firewall and bastion hosts: diff --git a/azure-py-virtual-data-center/__main__.py b/azure-py-virtual-data-center/__main__.py index e51caf92d..904672e8c 100644 --- a/azure-py-virtual-data-center/__main__.py +++ b/azure-py-virtual-data-center/__main__.py @@ -32,7 +32,10 @@ spoke1 = Spoke('s01', # stem of child resource names (<6 chars) SpokeProps( azure_bastion = config.azure_bastion, + fw_rt_name = hub.fw_rt_name, hub = hub, + peer = config.peer, + reference = config.reference, resource_group_name = resource_group_name, spoke_address_space = str(next(config.stack_sn)), subnets = [ # extra columns for future NSGs @@ -47,7 +50,10 @@ spoke2 = Spoke('s02', # stem of child resource names (<6 chars) SpokeProps( azure_bastion = config.azure_bastion, + fw_rt_name = hub.fw_rt_name, hub = hub, + peer = config.peer, + reference = config.reference, resource_group_name = resource_group_name, spoke_address_space = str(next(config.stack_sn)), subnets = [ # extra columns for future NSGs @@ -65,10 +71,10 @@ export('hub_as', hub.hub_as) # required for stack peering export('hub_id', hub.id) # required for stack peering export('hub_name', hub.name) -export('hub_subnets', hub.subnets) +export('hub_address_spaces', hub.address_spaces) export('s01_id', spoke1.id) export('s01_name', spoke1.name) -export('s01_subnets', spoke1.subnets) +export('s01_address_spaces', spoke1.address_spaces) export('s02_id', spoke2.id) export('s02_name', spoke2.name) -export('s02_subnets', spoke2.subnets) +export('s02_address_spaces', spoke2.address_spaces) diff --git a/azure-py-virtual-data-center/config.py b/azure-py-virtual-data-center/config.py index b03be7ed3..1b290108a 100644 --- a/azure-py-virtual-data-center/config.py +++ b/azure-py-virtual-data-center/config.py @@ -1,5 +1,5 @@ -from ipaddress import ip_network -from pulumi import Config, get_stack, get_project +from ipaddress import ip_address, ip_network +from pulumi import Config, get_stack, get_project, StackReference class Error(Exception): """Base class for exceptions in this module.""" @@ -30,13 +30,21 @@ def __init__(self, keys: [str], message: str): # Azure Firewall to route all Internet-bound traffic to designated next hop forced_tunnel = config.get_bool('forced_tunnel') +# turn off SNAT (private_ranges not yet available on Azure API?) +# https://docs.microsoft.com/en-us/azure/firewall/snat-private-range +if forced_tunnel: + ft_ip = ip_address(forced_tunnel) + if ft_ip.is_private: + private_ranges = 'IANAPrivateRanges' + else: + private_ranges = '0.0.0.0./0' # another stack in the same project and organization may be peered peer = config.get('peer') if peer: org = config.require('org') project = get_project() - reference = f'{org}/{project}/{peer}' + reference = StackReference(f'{org}/{project}/{peer}') else: reference = None diff --git a/azure-py-virtual-data-center/hub.py b/azure-py-virtual-data-center/hub.py index 0a6d1ac2a..b61667024 100644 --- a/azure-py-virtual-data-center/hub.py +++ b/azure-py-virtual-data-center/hub.py @@ -10,7 +10,7 @@ def __init__( firewall_address_space: str, hub_address_space: str, peer: str, - reference: str, + reference: StackReference, resource_group_name: str, stack: str, subnets: [str, str, str], @@ -143,10 +143,6 @@ def __init__(self, name: str, props: HubProps, opts: ResourceOptions=None): 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') @@ -176,6 +172,49 @@ def __init__(self, name: str, props: HubProps, opts: ResourceOptions=None): subnet_id = hub_dmz_sn.id, ) + #ToDo forced_tunnel requires Azure API version 2019-11-01 or later + # https://docs.microsoft.com/en-us/azure/firewall/forced-tunneling + + # Route Table only to be associated with AzureFirewallSubnet + hub_fw_rt = vdc.route_table( + stem = f'{name}-fw', + disable_bgp_route_propagation = False, + depends_on = [hub_er_gw, hub_fw, hub_vpn_gw], # avoid contention + ) # for routes to peered spokes and Internet (including forced_tunnel) + if props.forced_tunnel: + vdc.route_to_virtual_appliance( + stem = f'fw-tunnel', + route_table_name = hub_fw_rt.name, + address_prefix = '0.0.0.0/0', + next_hop_in_ip_address = props.forced_tunnel, + ) + else: + vdc.route_to_internet( + stem = f'fw-internet', + route_table_name = hub_fw_rt.name, + ) + hub_fw_sn_rta = vdc.subnet_route_table( + stem = f'{name}-fw', + route_table_id = hub_fw_rt.id, + subnet_id = hub_fw_sn.id, + ) + + # Route Table only to be associated with AzureFirewallManagementSubnet + hub_fwm_rt = vdc.route_table( + stem = f'{name}-fwm', + disable_bgp_route_propagation = True, + depends_on = [hub_er_gw, hub_fw, hub_vpn_gw], # avoid contention + ) + vdc.route_to_internet( + stem = f'fwm-internet', + route_table_name = hub_fwm_rt.name, + ) + hub_fwm_sn_rta = vdc.subnet_route_table( + stem = f'{name}-fwm', + route_table_id = hub_fwm_rt.id, + subnet_id = hub_fwm_sn.id, + ) + # Route Table only to be associated with hub shared services subnets hub_ss_rt = vdc.route_table( stem = f'{name}-ss', @@ -213,8 +252,7 @@ def __init__(self, name: str, props: HubProps, opts: ResourceOptions=None): # VNet Peering between stacks using StackReference if props.peer: - peer_stack = StackReference(props.reference) - peer_hub_id = peer_stack.get_output('hub_id') + peer_hub_id = props.reference.get_output('hub_id') # VNet Peering (Global) in one direction from stack to peer hub_hub = vdc.vnet_peering( @@ -227,9 +265,9 @@ def __init__(self, name: str, props: HubProps, opts: ResourceOptions=None): ) # 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') + 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-{props.peer}-dmz', hub_dmz_rt.name, peer_dmz_ar), @@ -262,12 +300,13 @@ def __init__(self, name: str, props: HubProps, opts: ResourceOptions=None): ) # assign properties to hub including from child resources - self.address_spaces = hub.address_spaces # informational + self.address_spaces = hub.address_spaces # exported 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.hub_as = props.hub_address_space # used for routes to the hub self.id = hub.id # exported and used for stack and spoke peering @@ -275,7 +314,7 @@ def __init__(self, name: str, props: HubProps, opts: ResourceOptions=None): 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.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 diff --git a/azure-py-virtual-data-center/spoke.py b/azure-py-virtual-data-center/spoke.py index d2a5ba239..bf52e8c98 100644 --- a/azure-py-virtual-data-center/spoke.py +++ b/azure-py-virtual-data-center/spoke.py @@ -1,5 +1,5 @@ from ipaddress import ip_network -from pulumi import ComponentResource, ResourceOptions +from pulumi import ComponentResource, ResourceOptions, StackReference from hub import Hub import vdc @@ -7,14 +7,20 @@ class SpokeProps: def __init__( self, azure_bastion: bool, + fw_rt_name: str, hub: Hub, + peer: str, + reference: StackReference, resource_group_name: str, spoke_address_space: str, subnets: [str, str, str], tags: [str, str], ): self.azure_bastion = azure_bastion + self.fw_rt_name = fw_rt_name self.hub = hub + self.peer = peer + self.reference = reference self.resource_group_name = resource_group_name self.spoke_address_space = spoke_address_space self.subnets = subnets @@ -65,6 +71,18 @@ def __init__(self, name: str, props: SpokeProps, 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( @@ -125,13 +143,13 @@ def __init__(self, name: str, props: SpokeProps, ) # assign properties to spoke including from child resources - self.address_spaces = spoke.address_spaces + 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 # exported + self.subnets = spoke.subnets self.stem = name self.tags = props.tags self.register_outputs({}) diff --git a/azure-py-virtual-data-center/vdc.py b/azure-py-virtual-data-center/vdc.py index a5154942b..43115d9f7 100644 --- a/azure-py-virtual-data-center/vdc.py +++ b/azure-py-virtual-data-center/vdc.py @@ -122,6 +122,17 @@ def route_table(stem, disable_bgp_route_propagation=None, depends_on=None): ) return rt +def route_to_internet(stem, route_table_name): + r_i = network.Route( + f'{stem}-r-', + resource_group_name = resource_group_name, + address_prefix = '0.0.0.0/0', + next_hop_type = 'Internet', + route_table_name = route_table_name, + opts = ResourceOptions(parent=self), + ) + return r_i + def route_to_virtual_appliance( stem, route_table_name,