From e942d6130cd402d48db56889581ce2db1879803f Mon Sep 17 00:00:00 2001 From: James Berry <60497203+jamesianberry@users.noreply.github.com> Date: Thu, 8 Oct 2020 21:58:11 +1100 Subject: [PATCH] Compare and contrast azure-py-vdc and azureng-py-vdc (#806) --- .../.gitignore | 5 + .../Pulumi.yaml | 34 ++ .../README.md | 235 ++++++++++++ .../__main__.py | 88 +++++ .../config.py | 101 +++++ azure-nextgen-py-virtual-data-center/hub.py | 339 +++++++++++++++++ .../requirements.txt | 2 + azure-nextgen-py-virtual-data-center/spoke.py | 150 ++++++++ azure-nextgen-py-virtual-data-center/vdc.py | 344 ++++++++++++++++++ azure-py-virtual-data-center/Pulumi.yaml | 17 +- azure-py-virtual-data-center/README.md | 149 ++++---- azure-py-virtual-data-center/__main__.py | 26 +- azure-py-virtual-data-center/config.py | 28 +- azure-py-virtual-data-center/hub.py | 320 ++++++++-------- azure-py-virtual-data-center/spoke.py | 83 ++--- azure-py-virtual-data-center/vdc.py | 201 +++++----- 16 files changed, 1719 insertions(+), 403 deletions(-) create mode 100644 azure-nextgen-py-virtual-data-center/.gitignore create mode 100644 azure-nextgen-py-virtual-data-center/Pulumi.yaml create mode 100644 azure-nextgen-py-virtual-data-center/README.md create mode 100644 azure-nextgen-py-virtual-data-center/__main__.py create mode 100644 azure-nextgen-py-virtual-data-center/config.py create mode 100644 azure-nextgen-py-virtual-data-center/hub.py create mode 100644 azure-nextgen-py-virtual-data-center/requirements.txt create mode 100644 azure-nextgen-py-virtual-data-center/spoke.py create mode 100644 azure-nextgen-py-virtual-data-center/vdc.py diff --git a/azure-nextgen-py-virtual-data-center/.gitignore b/azure-nextgen-py-virtual-data-center/.gitignore new file mode 100644 index 000000000..3fcfba667 --- /dev/null +++ b/azure-nextgen-py-virtual-data-center/.gitignore @@ -0,0 +1,5 @@ +*.pyc +.venv/ +.vscode/ +venv/ +__pycache__ diff --git a/azure-nextgen-py-virtual-data-center/Pulumi.yaml b/azure-nextgen-py-virtual-data-center/Pulumi.yaml new file mode 100644 index 000000000..cabd116cf --- /dev/null +++ b/azure-nextgen-py-virtual-data-center/Pulumi.yaml @@ -0,0 +1,34 @@ +name: azureng-py-vdc +runtime: + name: python + options: + virtualenv: venv +description: A minimal Azure Virtual Data Center described in Python +template: + config: + azure_bastion: + description: Azure Bastion provides secure RDP and SSH connectivity to VMs (optional) + default: false + firewall_address_space: + description: Address space in the hub for Azure Firewall and DMZ + default: 192.168.100.0/24 + forced_tunnel: + description: Route all Internet-bound traffic to this designated next hop IP address (optional) + default: 10.0.100.1 + hub_address_space: + description: Address space in the hub for connectivity and shared services subnets + default: 10.100.0.0/16 + location: + description: Azure region to deploy to (e.g. `australiaeast` or `australiasoutheast`) + default: australiaeast + org: + description: Pulumi organization in which this project resides (optional) + peer: + description: Another stack in same organization and project to peer hubs with (optional) + project: + description: Another project defining a stack with the same hub and spoke names to peer with (optional) + separator: + description: A dash (-) breaks up names by default; specify valid character or ' ' for none (optional) + suffix: + description: A short string appended to each name to allow multiple stacks (optional) + default: ae diff --git a/azure-nextgen-py-virtual-data-center/README.md b/azure-nextgen-py-virtual-data-center/README.md new file mode 100644 index 000000000..cd8cdbfa0 --- /dev/null +++ b/azure-nextgen-py-virtual-data-center/README.md @@ -0,0 +1,235 @@ +[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new) + +# Azure Virtual Data Center (VDC) + +This example deploys Azure Virtual Data Center (VDC) hub-and-spoke network stacks in Azure, complete with ExpressRoute and VPN Gateways, Azure Firewall (with provision for forced tunnelling) guarding a DMZ, and Azure Bastion. In addition, as many subnets as required for shared services in the hub and application environments in the spokes may be simply specified. + +In this implementation, the Azure Firewall is central. Custom routing redirects all traffic to and from hub and spokes, as well as all traffic to, within and from the DMZ, through the firewall (which scales out as a service to handle the throughput). Firewall rules are required to allow traffic through (not yet implemented). Traffic between shared services subnets in the hub and between subnets within the spokes is not redirected through the firewall, and should instead be controlled using Network Security Groups (not yet implemented). + +With minimal configuration, matching stacks may be deployed in Azure [paired regions](https://docs.microsoft.com/en-us/azure/best-practices-availability-paired-regions), configured for Production/Disaster Recovery or High Availability (or both for different applications). Global VNet Peering between the hubs connects the separate stacks into one symmetric network. + +Although the VDC pattern is in widespread use, Azure now offers a managed service intended to replace it, comprising Virtual Hub along with partner SD-WAN components, with a [migration plan](https://docs.microsoft.com/en-us/azure/virtual-wan/migrate-from-hub-spoke-topology) that illustrates the differences between the two patterns. But if you want or need to manage your own network infrastructure, VDC is still relevant. + +This example uses `pulumi.ComponentResource` as described [here](https://www.pulumi.com/docs/intro/concepts/programming-model/#components) which demonstrates how multiple low-level resources can be composed into a higher-level, reusable abstraction. It also demonstrates use of `pulumi.StackReference` as described [here](https://www.pulumi.com/docs/intro/concepts/organizing-stacks-projects/#inter-stack-dependencies) to relate multiple stacks. Finally, it uses Python's ```ipaddress``` module to simplify and validate configuration of network addresses. + +## Prerequisites + +1. [Install Pulumi](https://www.pulumi.com/docs/get-started/install/) +1. [Configure Pulumi for Azure](https://www.pulumi.com/docs/intro/cloud-providers/azure/setup/) +1. [Configure Pulumi for Python](https://www.pulumi.com/docs/intro/languages/python/) + +# Running the Example + +After cloning this repo, `cd` into the `azure-py-virtual-data-center` directory and run the following commands. + +1. (recommended) Create a Python virtualenv, activate it, and install the dependent packages [needed](https://www.pulumi.com/docs/intro/concepts/how-pulumi-works/) for our Pulumi program: + + ```bash + $ python3 -m venv venv + $ source venv/bin/activate + $ pip3 install -r requirements.txt + ``` + +1. Create a new stack intended for Production (for example's sake): + + ```bash + $ pulumi stack init prod + ``` + + This will appear within your Pulumi organization under the `azure-py-vdc` project (as specified in `Pulumi.yaml`). + +1. Set the configuration variables for this stack to suit yourself, following guidance in `Pulumi.yaml`. This will create a new `Pulumi.prod.yaml` file (named after the stack) in which to store them: + + Required: + ```bash + $ pulumi config set firewall_address_space 192.168.100.0/24 + $ pulumi config set hub_address_space 10.100.0.0/16 + $ pulumi config set location australiaeast + ``` + Optional: + ```bash + $ pulumi config set azure_bastion true + $ pulumi config set forced_tunnel 10.0.100.1 + $ pulumi config set separator " " + $ pulumi config set suffix "ae" + ``` + + Note that it is advisable to add Azure Bastion on the second pass to avoid contention. + +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: + + ```bash + $ pulumi up + ``` + +1. After a while, your Production stack will be ready. + + ``` + Updating (prod) + + View Live: https://app.pulumi.com/organization/azureng-py-vdc/prod/updates/1 + + Type Name Status + + pulumi:pulumi:Stack azureng-py-vdc-prod created + + ├─ vdc:network:Hub hub created + + │ ├─ azure-nextgen:network/latest:VirtualNetwork hub-vn created + + │ ├─ azure-nextgen:network/latest:RouteTable hub-gw-rt created + + │ ├─ azure-nextgen:network/latest:RouteTable hub-dmz-rt created + + │ ├─ azure-nextgen:network/latest:RouteTable hub-fw-rt created + + │ ├─ azure-nextgen:network/latest:RouteTable hub-fwm-rt created + + │ ├─ azure-nextgen:network/latest:PublicIPAddress hub-fw-pip created + + │ ├─ azure-nextgen:network/latest:PublicIPAddress hub-fwm-pip created + + │ ├─ azure-nextgen:network/latest:PublicIPAddress hub-vpn-gw-pip created + + │ ├─ azure-nextgen:network/latest:PublicIPAddress hub-er-gw-pip created + + │ ├─ azure-nextgen:network/latest:PublicIPAddress hub-ab-pip created + + │ ├─ azure-nextgen:network/latest:Route fwm-internet-r created + + │ ├─ azure-nextgen:network/latest:Route fw-tunnel-r created + + │ ├─ azure-nextgen:network/latest:Route gw-gw-r created + + │ ├─ azure-nextgen:network/latest:Subnet hub-fwm-sn created + + │ ├─ azure-nextgen:network/latest:Subnet hub-dmz-sn created + + │ ├─ azure-nextgen:network/latest:Subnet hub-fw-sn created + + │ ├─ azure-nextgen:network/latest:Subnet hub-gw-sn created + + │ ├─ azure-nextgen:network/latest:AzureFirewall hub-fw created + + │ ├─ azure-nextgen:network/latest:VirtualNetworkGateway hub-vpn-gw created + + │ ├─ azure-nextgen:network/latest:VirtualNetworkGateway hub-er-gw created + + │ ├─ azure-nextgen:network/latest:Route gw-dmz-r created + + │ ├─ azure-nextgen:network/latest:Route gw-hub-r created + + │ ├─ azure-nextgen:network/latest:Route dmz-dmz-r created + + │ ├─ azure-nextgen:network/latest:Route dmz-hub-r created + + │ ├─ azure-nextgen:network/latest:Route dmz-dg-r created + + │ ├─ azure-nextgen:network/latest:RouteTable hub-ss-rt created + + │ ├─ azure-nextgen:network/latest:Subnet hub-ab-sn created + + │ ├─ azure-nextgen:network/latest:Route ss-dg-r created + + │ ├─ azure-nextgen:network/latest:Route ss-dmz-r created + + │ ├─ azure-nextgen:network/latest:Route ss-gw-r created + + │ ├─ azure-nextgen:network/latest:Subnet hub-domain-sn created + + │ ├─ azure-nextgen:network/latest:Subnet hub-files-sn created + + │ └─ azure-nextgen:network/latest:BastionHost hub-ab created + + ├─ vdc:network:Spoke s01 created + + │ ├─ azure-nextgen:network/latest:VirtualNetwork s01-vn created + + │ ├─ azure-nextgen:network/latest:PublicIPAddress s01-ab-pip created + + │ ├─ azure-nextgen:network/latest:Route dmz-s01-r created + + │ ├─ azure-nextgen:network/latest:Route gw-s01-r created + + │ ├─ azure-nextgen:network/latest:VirtualNetworkPeering hub-s01-vnp created + + │ ├─ azure-nextgen:network/latest:VirtualNetworkPeering s01-hub-vnp created + + │ ├─ azure-nextgen:network/latest:Route ss-s01-r created + + │ ├─ azure-nextgen:network/latest:RouteTable s01-rt created + + │ ├─ azure-nextgen:network/latest:Subnet s01-ab-sn created + + │ ├─ azure-nextgen:network/latest:Route s01-dg-r created + + │ ├─ azure-nextgen:network/latest:Route s01-dmz-r created + + │ ├─ azure-nextgen:network/latest:Route s01-hub-r created + + │ ├─ azure-nextgen:network/latest:Subnet s01-app-sn created + + │ ├─ azure-nextgen:network/latest:Subnet s01-web-sn created + + │ ├─ azure-nextgen:network/latest:Subnet s01-db-sn created + + │ └─ azure-nextgen:network/latest:BastionHost s01-ab created + + ├─ vdc:network:Spoke s02 created + + │ ├─ azure-nextgen:network/latest:VirtualNetwork s02-vn created + + │ ├─ azure-nextgen:network/latest:PublicIPAddress s02-ab-pip created + + │ ├─ azure-nextgen:network/latest:Route gw-s02-r created + + │ ├─ azure-nextgen:network/latest:Route dmz-s02-r created + + │ ├─ azure-nextgen:network/latest:VirtualNetworkPeering hub-s02-vnp created + + │ ├─ azure-nextgen:network/latest:VirtualNetworkPeering s02-hub-vnp created + + │ ├─ azure-nextgen:network/latest:Route ss-s02-r created + + │ ├─ azure-nextgen:network/latest:RouteTable s02-rt created + + │ ├─ azure-nextgen:network/latest:Subnet s02-ab-sn created + + │ ├─ azure-nextgen:network/latest:Route s02-dg-r created + + │ ├─ azure-nextgen:network/latest:Route s02-dmz-r created + + │ ├─ azure-nextgen:network/latest:Subnet s02-web-sn created + + │ ├─ azure-nextgen:network/latest:Route s02-hub-r created + + │ ├─ azure-nextgen:network/latest:Subnet s02-app-sn created + + │ ├─ azure-nextgen:network/latest:Subnet s02-db-sn created + + │ └─ azure-nextgen:network/latest:BastionHost s02-ab created + + └─ azure-nextgen:resources/latest:ResourceGroup prod-vdc-rg created + + Outputs: + dmz_ar: "192.168.100.128/25" + fw_ip : "192.168.100.4" + hub_as: "10.100.0.0/16" + hub_id: "/subscriptions/subscription/resourceGroups/prod-vdc-rg-ae/providers/Microsoft.Network/virtualNetworks/hub-vn-ae" + s01_as: "10.101.0.0/16" + s01_id: "/subscriptions/subscription/resourceGroups/prod-vdc-rg-ae/providers/Microsoft.Network/virtualNetworks/s01-vn-ae" + s02_as: "10.102.0.0/16" + s02_id: "/subscriptions/subscription/resourceGroups/prod-vdc-rg-ae/providers/Microsoft.Network/virtualNetworks/s02-vn-ae" + + Resources: + + 70 created + + Duration: 34m34s + ``` + + Feel free to modify your program, and then run `pulumi up` again. Pulumi automatically detects differences and makes the minimal changes necessary to achieved the desired state. If any changes to resources are made outside of Pulumi, you should first do a `pulumi refresh` so that Pulumi can discover the actual situation, and then `pulumi up` to return to desired state. + + Note that auto-naming is not yet implemented in azure-nextgen. Instead the same suffix is appended to each physical name so that multiple stacks may be created without conflict. + +1. Create another new stack intended for Disaster Recovery (following the example): + + ```bash + $ pulumi stack init dr + ``` + + This will also appear within your Pulumi organization under the `azure-py-vdc` project (as specified in `Pulumi.yaml`). + +1. Set the configuration variables for this stack which will be stored in a new `Pulumi.dr.yaml` file (change the values below to suit yourself): + + Required: + ```bash + $ pulumi config set firewall_address_space 192.168.200.0/24 + $ pulumi config set hub_address_space 10.200.0.0/16 + $ pulumi config set location australiasoutheast + ``` + Optional: + ```bash + $ pulumi config set azure_bastion true + $ pulumi config set forced_tunnel 10.0.200.1 + $ pulumi config set separator "_" + $ pulumi config set suffix "ase" + ``` + +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: + + ```bash + $ pulumi up + ``` + +1. Once you have both Production and Disaster Recovery stacks (ideally in paired regions), you can connect their hubs using Global (between regions) VNet Peering: + + Required: + ```bash + $ pulumi stack select prod + $ pulumi config set peer dr + $ pulumi up + $ pulumi stack select dr + $ pulumi config set peer prod + $ pulumi up + ``` + Optional (for each stack): + ```bash + $ pulumi config set org organization + $ pulumi config set project project + ``` + + Note: you may specify another organization and/or project (corresponding hub and spoke names should be the same). It isn't yet [possible](https://github.com/pulumi/pulumi/issues/2800) to discover the Pulumi organization from within the program. + + If you later destroy a stack, you need to remove the corresponding `peer` variable in the other stack and run `pulumi up`. If you want to tear down the peerings, you should remove the `peer` variables in both stacks and run `pulumi up`: + + ```bash + $ pulumi stack select prod + $ pulumi config rm peer + $ pulumi up + $ pulumi stack select dr + $ pulumi config rm peer + $ pulumi up + ``` + + You need to remove both peerings before you can connect the hubs again. + +1. When you are finished experimenting, you can destroy all of the resources, and the stacks: + + ```bash + $ pulumi stack select prod + $ pulumi destroy + $ pulumi stack rm + $ pulumi stack select dr + $ pulumi destroy + $ pulumi stack rm + ``` \ No newline at end of file diff --git a/azure-nextgen-py-virtual-data-center/__main__.py b/azure-nextgen-py-virtual-data-center/__main__.py new file mode 100644 index 000000000..d42b747a1 --- /dev/null +++ b/azure-nextgen-py-virtual-data-center/__main__.py @@ -0,0 +1,88 @@ +import config +import vdc +from hub import HubProps, Hub +from spoke import SpokeProps, Spoke +from pulumi import export + +# set required vdc variables before calling function +vdc.location = config.location +vdc.s = config.separator +vdc.suffix = config.suffix +vdc.tags = config.default_tags +# all resources will be created in configuration location +resource_group_name = vdc.resource_group(config.stack) + +# single hub with gateways, firewall, DMZ, shared services, bastion (optional) +hub = Hub('hub', # stem of child resource names (<4 chars) + HubProps( + azure_bastion = config.azure_bastion, + forced_tunnel = config.forced_tunnel, + firewall_address_space = config.firewall_address_space, + hub_address_space = config.hub_address_space, + location = config.location, + peer = config.peer, + reference = config.reference, + resource_group_name = resource_group_name, + separator = config.separator, + stack = config.stack, + subnets = [ # extra columns for future ASGs + ('domain', 'any', 'any'), + ('files', 'any', 'none'), + ], + suffix = config.suffix, + tags = config.default_tags, + ), +) + +# multiple spokes for application environments with bastion access (optional) +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, + location = config.location, + peer = config.peer, + reference = config.reference, + resource_group_name = resource_group_name, + separator = config.separator, + spoke_address_space = str(next(config.stack_sn)), + subnets = [ # extra columns for future ASGs + ('web', 'any', 'app'), + ('app', 'web', 'db'), + ('db', 'app', 'none'), + ], + suffix = config.suffix, + tags = config.default_tags, + ), +) + +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, + location = config.location, + peer = config.peer, + reference = config.reference, + resource_group_name = resource_group_name, + separator = config.separator, + spoke_address_space = str(next(config.stack_sn)), + subnets = [ # extra columns for future ASGs + ('web', 'any', 'app'), + ('app', 'web', 'db'), + ('db', 'app', 'none'), + ], + suffix = config.suffix, + tags = config.default_tags, + ), +) + +# export information about the stack required for stack peering +export('dmz_ar', hub.dmz_ar) +export('fw_ip', hub.fw_ip) +export('hub_as', hub.address_space) +export('hub_id', hub.id) +export('s01_as', spoke1.address_space) +export('s01_id', spoke1.id) +export('s02_as', spoke2.address_space) +export('s02_id', spoke2.id) diff --git a/azure-nextgen-py-virtual-data-center/config.py b/azure-nextgen-py-virtual-data-center/config.py new file mode 100644 index 000000000..a77b4c136 --- /dev/null +++ b/azure-nextgen-py-virtual-data-center/config.py @@ -0,0 +1,101 @@ +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.""" + pass + +class ConfigError(Error): + """Exception raised for errors in Pulumi Config. + + Attributes: + keys -- Config keys with the error + message -- explanation of the error + """ + def __init__(self, keys: [str], message: str): + self.keys = keys + self.message = message + +# retrieve the stack configuration data +config = Config() + +# retrieve the location +location = config.require('location') + +# retrieve optional separator choice and suffix +separator = config.get('separator') +if not separator: + separator = '-' +else: + separator = separator[0] +if separator == ' ': + separator = '' +suffix = config.get('suffix') +if not suffix: + suffix = '' + +# set default tags to be applied to all taggable resources +stack = get_stack() +default_tags = { + 'environment': stack +} + +# Azure Bastion hosts in hub and spokes (until functional across peerings) +azure_bastion = config.get_bool('azure_bastion') + +# Azure Firewall to route all Internet-bound traffic to designated next hop +forced_tunnel = config.get('forced_tunnel') +if forced_tunnel: + ft_ip = ip_address(forced_tunnel) # check IP address is valid + +# another stack may be peered in the same project, even across organizations +org = config.get('org') +peer = config.get('peer') +project = config.get('project') +if org and not project: + project = get_project() +if not org: + org = '' +if not project: + project = '' +if not peer: + reference = None +else: + reference = StackReference(f'{org}/{project}/{peer}') + +# validate firewall_address_space and hub_address_space +firewall_address_space = config.require('firewall_address_space') +fwz_nw = ip_network(firewall_address_space) +if not fwz_nw.is_private: + raise ConfigError(['firewall_address_space'], 'must be private') +if fwz_nw.prefixlen > 24: + raise ConfigError(['firewall_address_space'], 'must be /24 or larger') +hub_address_space = config.require('hub_address_space') +hub_nw = ip_network(hub_address_space) +if not hub_nw.is_private: + raise ConfigError(['hub_address_space'], 'must be private') +if hub_nw.prefixlen > 24: + raise ConfigError(['hub_address_space'], 'must be /24 or larger') +if fwz_nw.overlaps(hub_nw): + raise ConfigError( + ['firewall_address_space', 'hub_address_space'], + 'may not overlap' + ) + +# locate hub_address_space within supernet for contiguous spoke_address_space +sup_diff = hub_nw.prefixlen - 8 # largest private IPv4 network is 10/8 +super_nw = hub_nw.supernet(prefixlen_diff=sup_diff) +while not super_nw.is_private: # accommodate longer private network prefixes + sup_diff = sup_diff - 1 + super_nw = hub_nw.supernet(prefixlen_diff=sup_diff) +if sup_diff <= 0: + raise ConfigError( + ['hub_address_space'], + 'must be a subnet of a private supernet' + ) +stack_sn = super_nw.subnets(prefixlen_diff=sup_diff) +hub_as = next(stack_sn) +while hub_as < hub_nw: + hub_as = next(stack_sn) +if hub_address_space != str(hub_as): + raise ConfigError(['hub_address_space'], 'check assumptions') diff --git a/azure-nextgen-py-virtual-data-center/hub.py b/azure-nextgen-py-virtual-data-center/hub.py new file mode 100644 index 000000000..2ef67b7cf --- /dev/null +++ b/azure-nextgen-py-virtual-data-center/hub.py @@ -0,0 +1,339 @@ +from ipaddress import ip_network, ip_address +from pulumi import ComponentResource, ResourceOptions, StackReference +import vdc + +class HubProps: + def __init__( + self, + azure_bastion: bool, + forced_tunnel: bool, + firewall_address_space: str, + hub_address_space: str, + location: str, + peer: str, + reference: StackReference, + resource_group_name: str, + separator: str, + stack: str, + subnets: [str, str, str], + suffix: str, + tags: [str, str], + ): + self.azure_bastion = azure_bastion + self.forced_tunnel = forced_tunnel + self.firewall_address_space = firewall_address_space + self.hub_address_space = hub_address_space + self.location = location + self.peer = peer + self.reference = reference + self.resource_group_name = resource_group_name + self.separator = separator + self.stack = stack + self.subnets = subnets + self.suffix = suffix + self.tags = tags + +class Hub(ComponentResource): + 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], + ) + + # work around https://github.com/pulumi/pulumi/issues/4040 + hub_fw_ip = hub_fw.ip_configurations.apply( + lambda ipc: ipc[0].get('privateIPAddress') + ) + # 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({}) diff --git a/azure-nextgen-py-virtual-data-center/requirements.txt b/azure-nextgen-py-virtual-data-center/requirements.txt new file mode 100644 index 000000000..053305791 --- /dev/null +++ b/azure-nextgen-py-virtual-data-center/requirements.txt @@ -0,0 +1,2 @@ +pulumi>=2.0.0,<3.0.0 +pulumi-azure-nextgen>=0.2.0 diff --git a/azure-nextgen-py-virtual-data-center/spoke.py b/azure-nextgen-py-virtual-data-center/spoke.py new file mode 100644 index 000000000..2bd471874 --- /dev/null +++ b/azure-nextgen-py-virtual-data-center/spoke.py @@ -0,0 +1,150 @@ +from ipaddress import ip_network +from pulumi import ComponentResource, ResourceOptions, StackReference +from hub import Hub +import vdc + +class SpokeProps: + def __init__( + self, + azure_bastion: bool, + fw_rt_name: str, + hub: Hub, + location: str, + peer: str, + reference: StackReference, + resource_group_name: str, + separator: str, + spoke_address_space: str, + subnets: [str, str, str], + suffix: str, + tags: [str, str], + ): + self.azure_bastion = azure_bastion + self.fw_rt_name = fw_rt_name + self.hub = hub + self.location = location + self.peer = peer + self.reference = reference + self.resource_group_name = resource_group_name + self.separator = separator + self.spoke_address_space = spoke_address_space + self.subnets = subnets + self.suffix = suffix + self.tags = tags + +class Spoke(ComponentResource): + 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.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 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 + + # set the separator to be used in resource names + s = props.separator + + # 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=[spoke], + ) + + # 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=[spoke, props.hub.er_gw, props.hub.vpn_gw], + ) + + # Route Table to be associated with all ordinary spoke subnets + spoke_rt = vdc.route_table( + stem = f'{name}', + disable_bgp_route_propagation = True, + ) + # it is very important to ensure that there is never a route with an + # address_prefix which covers the AzureFirewallSubnet, and as VNet + # Peering may not be specified as next_hop_type, a separate address + # space for the firewall in the hub makes for simpler routes + for route in [ + (f'dmz{s}{name}', props.hub.dmz_rt_name, props.spoke_address_space), + (f'gw{s}{name}', props.hub.gw_rt_name, props.spoke_address_space), + (f'ss{s}{name}', props.hub.ss_rt_name, props.spoke_address_space), + (f'{name}{s}dg', spoke_rt.name, '0.0.0.0/0'), + (f'{name}{s}dmz', spoke_rt.name, props.hub.dmz_ar), + (f'{name}{s}hub', spoke_rt.name, props.hub.address_space), + ]: + vdc.route_to_virtual_appliance( + stem = route[0], + route_table_name = route[1], + address_prefix = route[2], + next_hop_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}{s}{subnet[0]}', + virtual_network_name = spoke.name, + address_prefix = str(next_sn), + route_table_id = spoke_rt.id, + depends_on = [spoke_rt, hub_spoke, spoke_hub], + ) + + # Azure Bastion subnet and host (optional) + if props.azure_bastion: + spoke_ab = vdc.bastion_host( + stem = name, + virtual_network_name = spoke.name, + address_prefix = str(abs_nw), + depends_on = [hub_spoke, spoke_hub, spoke_rt], + ) + + # add route from firewall to corresponding spoke in peered stack + if props.peer: + peer_fw_ip = props.reference.get_output('fw_ip') + peer_spoke_as = props.reference.get_output(f'{name}_as') + fw_peer_spoke = vdc.route_to_virtual_appliance( + stem = f'fw{s}{props.peer}{s}{name}', + route_table_name = props.fw_rt_name, + address_prefix = peer_spoke_as, + next_hop_ip_address = peer_fw_ip, + ) + + # assign properties to spoke including from child resources + self.address_space = props.spoke_address_space + 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({}) diff --git a/azure-nextgen-py-virtual-data-center/vdc.py b/azure-nextgen-py-virtual-data-center/vdc.py new file mode 100644 index 000000000..5a13a1f3a --- /dev/null +++ b/azure-nextgen-py-virtual-data-center/vdc.py @@ -0,0 +1,344 @@ +from pulumi import ResourceOptions +from pulumi.resource import CustomTimeouts +from pulumi_azure_nextgen.resources import latest as resources +from pulumi_azure_nextgen.network import latest as network + +# Variables that may need to be injected 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 + +def bastion_host(stem, virtual_network_name, address_prefix, depends_on=None): + ab_sn = network.Subnet(f'{stem}{s}ab{s}sn', + subnet_name = 'AzureBastionSubnet', # name required + resource_group_name = resource_group_name, + virtual_network_name = virtual_network_name, + address_prefix = address_prefix, + opts = ResourceOptions( + parent = self, + delete_before_replace = True, + depends_on = depends_on, + ), + ) + ab_pip = network.PublicIPAddress(f'{stem}{s}ab{s}pip', + public_ip_address_name = f'{stem}{s}ab{s}pip{s}{suffix}', + resource_group_name = resource_group_name, + location = location, + sku = network.PublicIPAddressSkuArgs( + name = 'Standard', + ), + public_ip_allocation_method = 'Static', + tags = tags, + opts = ResourceOptions(parent=self, depends_on=depends_on), + ) + ab = network.BastionHost(f'{stem}{s}ab', + bastion_host_name = f'{stem}{s}ab{s}{suffix}', + resource_group_name = resource_group_name, + location = location, + ip_configurations = [network.BastionHostIPConfigurationArgs( + name = f'{stem}{s}ab{s}ipconf{s}{suffix}', + public_ip_address = network.PublicIPAddressArgs( + id = ab_pip.id, + ), + subnet = network.SubnetArgs( + id = ab_sn.id, + ), + )], + tags = tags, + opts = ResourceOptions(parent=self, depends_on=depends_on), + ) + return ab + +def expressroute_gateway(stem, subnet_id, depends_on=None): + er_gw_pip = network.PublicIPAddress(f'{stem}{s}er{s}gw{s}pip', + public_ip_address_name = f'{stem}{s}er{s}gw{s}pip{s}{suffix}', + resource_group_name = resource_group_name, + location = location, + public_ip_allocation_method = 'Dynamic', + tags = tags, + opts = ResourceOptions(parent=self, depends_on=depends_on), + ) + er_gw = network.VirtualNetworkGateway(f'{stem}{s}er{s}gw', + virtual_network_gateway_name = f'{stem}{s}er{s}gw{s}{suffix}', + resource_group_name = resource_group_name, + location = location, + sku = network.VirtualNetworkGatewaySkuArgs( + name = 'Standard', + tier = 'Standard', + ), + gateway_type = 'ExpressRoute', + vpn_type = 'RouteBased', + enable_bgp = True, + ip_configurations = [network.VirtualNetworkGatewayIPConfigurationArgs( + name = f'{stem}{s}er{s}gw{s}ipconf{s}{suffix}', + public_ip_address = network.PublicIPAddressArgs( + id = er_gw_pip.id, + ), + subnet = network.SubnetArgs( + id = subnet_id, + ), + )], + tags = tags, + opts = ResourceOptions( + parent = self, + depends_on = depends_on, + custom_timeouts = CustomTimeouts( + create = '1h', + update = '1h', + delete = '1h', + ), + ), + ) + return er_gw + +def firewall(stem, fw_sn_id, fwm_sn_id, private_ranges, depends_on=None): + fw_pip = network.PublicIPAddress(f'{stem}{s}fw{s}pip', + public_ip_address_name = f'{stem}{s}fw{s}pip{s}{suffix}', + resource_group_name = resource_group_name, + location = location, + sku = network.PublicIPAddressSkuArgs( + name = 'Standard', + ), + public_ip_allocation_method = 'Static', + tags = tags, + opts = ResourceOptions(parent=self, depends_on=depends_on), + ) + fwm_pip = network.PublicIPAddress(f'{stem}{s}fwm{s}pip', + public_ip_address_name = f'{stem}{s}fwm{s}pip{s}{suffix}', + resource_group_name = resource_group_name, + location = location, + sku = network.PublicIPAddressSkuArgs( + name = 'Standard', + ), + public_ip_allocation_method = 'Static', + tags = tags, + opts = ResourceOptions(parent=self, depends_on=depends_on), + ) + fw = network.AzureFirewall(f'{stem}{s}fw', + azure_firewall_name = f'{stem}{s}fw{s}{suffix}', + resource_group_name = resource_group_name, + location = location, + additional_properties = { + "Network.SNAT.PrivateRanges": private_ranges, + }, + sku = network.AzureFirewallSkuArgs( + name = 'AZFW_VNet', + tier = 'Standard', + ), + ip_configurations = [network.AzureFirewallIPConfigurationArgs( + name = f'{stem}{s}fw{s}ipconf{s}{suffix}', + public_ip_address = network.PublicIPAddressArgs( + id = fw_pip.id, + ), + subnet = network.SubnetArgs( + id = fw_sn_id, + ), + )], + management_ip_configuration = network.AzureFirewallIPConfigurationArgs( + name = f'{stem}{s}fwm{s}ipconf{s}{suffix}', + public_ip_address = network.PublicIPAddressArgs( + id = fwm_pip.id, + ), + subnet = network.SubnetArgs( + id = fwm_sn_id, + ), + ), + tags = tags, + opts = ResourceOptions( + parent = self, + depends_on = depends_on, + custom_timeouts = CustomTimeouts( + create = '1h', + update = '1h', + delete = '1h', + ), + ), + ) + return fw + +def resource_group(stem): + rg = resources.ResourceGroup(f'{stem}{s}vdc{s}rg', + resource_group_name = f'{stem}{s}vdc{s}rg{s}{suffix}', + location = location, + tags = tags, + ) + return rg.name + +def route_table(stem, disable_bgp_route_propagation=None, depends_on=None): + rt = network.RouteTable(f'{stem}{s}rt', + route_table_name = f'{stem}{s}rt{s}{suffix}', + resource_group_name = resource_group_name, + location = location, + disable_bgp_route_propagation = disable_bgp_route_propagation, + tags = tags, + opts = ResourceOptions(parent=self, depends_on=depends_on), + ) + return rt + +def route_to_internet(stem, route_table_name): + r_i = network.Route(f'{stem}{s}r', + route_name = 'FirewallDefaultRoute', # name required + 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, delete_before_replace=True), + ) + return r_i + +def route_to_virtual_appliance( + stem, + route_table_name, + address_prefix, + next_hop_ip_address, + ): + r_va = network.Route(f'{stem}{s}r', + route_name = f'{stem}{s}r{s}{suffix}', + resource_group_name = resource_group_name, + address_prefix = address_prefix, + next_hop_type = 'VirtualAppliance', + next_hop_ip_address = next_hop_ip_address, + route_table_name = route_table_name, + opts = ResourceOptions(parent=self), + ) + return r_va + +def route_to_virtual_network(stem, route_table_name, address_prefix): + r_vn = network.Route(f'{stem}{s}r', + route_name = f'{stem}{s}r{s}{suffix}', + resource_group_name = resource_group_name, + address_prefix = address_prefix, + next_hop_type = 'VnetLocal', + route_table_name = route_table_name, + opts = ResourceOptions(parent=self), + ) + return r_vn + +def subnet( + stem, + virtual_network_name, + address_prefix, + route_table_id, + depends_on = None, + ): + sn = network.Subnet(f'{stem}{s}sn', + subnet_name = f'{stem}{s}sn{s}{suffix}', + resource_group_name = resource_group_name, + virtual_network_name = virtual_network_name, + address_prefix = address_prefix, + route_table = network.RouteTableArgs( + id = route_table_id, + ), + opts = ResourceOptions(parent=self, depends_on=depends_on), + ) + return sn + +def subnet_special( + stem, + name, + virtual_network_name, + address_prefix, + route_table_id, + depends_on = None, + ): + sn = network.Subnet(f'{stem}{s}sn', + subnet_name = name, + resource_group_name = resource_group_name, + virtual_network_name = virtual_network_name, + address_prefix = address_prefix, + route_table = network.RouteTableArgs( + id = route_table_id, + ), + opts = ResourceOptions( + parent = self, + delete_before_replace = True, + depends_on = depends_on, + ), + ) + return sn + +def virtual_network(stem, address_spaces): + vn = network.VirtualNetwork(f'{stem}{s}vn', + virtual_network_name = f'{stem}{s}vn{s}{suffix}', + resource_group_name = resource_group_name, + location = location, + address_space = network.AddressSpaceArgs( + address_prefixes = address_spaces, + ), + tags = tags, + opts = ResourceOptions(parent=self), + ) + return vn + +def vnet_peering( + stem, + virtual_network_name, + peer, + remote_virtual_network_id, + allow_forwarded_traffic = None, + allow_gateway_transit = None, + use_remote_gateways = None, + depends_on = None, + ): + vnp = network.VirtualNetworkPeering(f'{stem}{s}{peer}{s}vnp', + virtual_network_peering_name = f'{stem}{s}{peer}{s}vnp{s}{suffix}', + resource_group_name = resource_group_name, + virtual_network_name = virtual_network_name, + remote_virtual_network = network.SubResourceArgs( + id = remote_virtual_network_id + ), + allow_forwarded_traffic = allow_forwarded_traffic, + allow_gateway_transit = allow_gateway_transit, + use_remote_gateways = use_remote_gateways, + allow_virtual_network_access = True, + opts = ResourceOptions(parent=self, depends_on=depends_on), + ) + return vnp + +def vpn_gateway(stem, subnet_id, depends_on=None): + vpn_gw_pip = network.PublicIPAddress(f'{stem}{s}vpn{s}gw{s}pip', + public_ip_address_name = f'{stem}{s}vpn{s}gw{s}pip{s}{suffix}', + resource_group_name = resource_group_name, + location = location, + public_ip_allocation_method = 'Dynamic', + tags = tags, + opts = ResourceOptions(parent=self, depends_on=depends_on), + ) + vpn_gw = network.VirtualNetworkGateway(f'{stem}{s}vpn{s}gw', + virtual_network_gateway_name = f'{stem}{s}vpn{s}gw{s}{suffix}', + resource_group_name = resource_group_name, + location = location, + sku = network.VirtualNetworkGatewaySkuArgs( + name = 'VpnGw1', + tier = 'VpnGw1', + ), + gateway_type = 'Vpn', + vpn_type = 'RouteBased', + enable_bgp = True, + ip_configurations = [network.VirtualNetworkGatewayIPConfigurationArgs( + name = f'{stem}{s}vpn{s}gw{s}ipconf{s}{suffix}', + public_ip_address = network.PublicIPAddressArgs( + id = vpn_gw_pip.id, + ), + subnet = network.SubnetArgs( + id = subnet_id, + ), + )], + tags = tags, + opts = ResourceOptions( + parent = self, + depends_on = depends_on, + custom_timeouts = CustomTimeouts( + create = '1h', + update = '1h', + delete = '1h', + ), + ), + ) + return vpn_gw + +if __name__ == "__main__": + print(dir()) diff --git a/azure-py-virtual-data-center/Pulumi.yaml b/azure-py-virtual-data-center/Pulumi.yaml index 9c95330c6..730b43395 100644 --- a/azure-py-virtual-data-center/Pulumi.yaml +++ b/azure-py-virtual-data-center/Pulumi.yaml @@ -1,24 +1,29 @@ name: azure-py-vdc -runtime: python +runtime: + name: python + options: + virtualenv: venv description: A minimal Azure Virtual Data Center described in Python template: config: azure-py-vdc:azure_bastion: - description: Azure Bastion provides secure RDP and SSH connectivity to VMs - default: "false" + description: Azure Bastion provides secure RDP and SSH connectivity to VMs (optional) + default: false azure-py-vdc:firewall_address_space: description: Address space in the hub for Azure Firewall and DMZ default: 192.168.100.0/24 azure-py-vdc:forced_tunnel: - description: Route all Internet-bound traffic to a designated next hop (preview) - default: "false" + description: Route all Internet-bound traffic to this designated next hop IP address (optional) + default: 10.0.100.1 azure-py-vdc:hub_address_space: description: Address space in the hub for connectivity and shared services subnets default: 10.100.0.0/16 azure-py-vdc:org: - description: Pulumi organization in which this project resides (from app.pulumi.com) + description: Pulumi organization in which this project resides (optional) azure-py-vdc:peer: description: Another stack in same organization and project to peer hubs with (optional) + azure-py-vdc:project: + description: Another project defining a stack with the same hub and spoke names to peer with (optional) azure:environment: description: Azure environment to use (`public`, `usgovernment`, `german`, `china`) default: public diff --git a/azure-py-virtual-data-center/README.md b/azure-py-virtual-data-center/README.md index 1c0f511a4..b766b49a9 100644 --- a/azure-py-virtual-data-center/README.md +++ b/azure-py-virtual-data-center/README.md @@ -21,7 +21,7 @@ This example uses `pulumi.ComponentResource` as described [here](https://www.pul # Running the Example After cloning this repo, `cd` into the `azure-py-virtual-data-center` directory and run the following commands. - + 1. (recommended) Create a Python virtualenv, activate it, and install the dependent packages [needed](https://www.pulumi.com/docs/intro/concepts/how-pulumi-works/) for our Pulumi program: ```bash @@ -42,16 +42,18 @@ After cloning this repo, `cd` into the `azure-py-virtual-data-center` directory Required: ```bash - $ pulumi config set azure:environment public - $ pulumi config set azure:location australiaeast - $ pulumi config set firewall_address_space 192.168.100.0/24 - $ pulumi config set hub_address_space 10.100.0.0/16 + $ pulumi config set azure:environment public + $ pulumi config set azure:location australiaeast + $ pulumi config set firewall_address_space 192.168.100.0/24 + $ pulumi config set hub_address_space 10.100.0.0/16 ``` Optional: ```bash - $ pulumi config set azure_bastion "true" - $ pulumi config set forced_tunnel "10.0.100.1" + $ pulumi config set azure_bastion "true" + $ pulumi config set forced_tunnel "10.0.100.1" ``` + + Note that it is advisable to add Azure Bastion on the second pass to avoid contention. 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: @@ -62,97 +64,113 @@ After cloning this repo, `cd` into the `azure-py-virtual-data-center` directory 1. After a while, your Production stack will be ready. ``` - Updating (prod): + Updating (prod) + + View Live: https://app.pulumi.com/organization/azure-py-vdc/prod/updates/1 + Type Name Status + pulumi:pulumi:Stack azure-py-vdc-prod created + ├─ vdc:network:Hub hub created + │ ├─ azure:network:VirtualNetwork hub-vn- created - + │ ├─ azure:network:PublicIp hub-vpn-gw-pip- created + + │ ├─ azure:network:RouteTable hub-fwm-rt- created + + │ ├─ azure:network:RouteTable hub-fw-rt- created + + │ ├─ azure:network:Route fwm-internet-r- created + + │ ├─ azure:network:Route fw-tunnel-r- created + + │ ├─ azure:network:Subnet hub-fwm-sn created + + │ ├─ azure:network:Subnet hub-fw-sn created + + │ ├─ azure:network:SubnetRouteTableAssociation hub-fwm-sn-rta created + │ ├─ azure:network:PublicIp hub-fw-pip- created - + │ ├─ azure:network:PublicIp hub-er-gw-pip- created - + │ ├─ azure:network:Subnet hub-gw-sn created + + │ ├─ azure:network:PublicIp hub-fwm-pip- created + + │ ├─ azure:network:SubnetRouteTableAssociation hub-fw-sn-rta created + + │ ├─ azure:network:Firewall hub-fw- created + + │ ├─ azure:network:RouteTable hub-dmz-rt- created + + │ ├─ azure:network:Route dmz-dg-r- created + + │ ├─ azure:network:Route dmz-dmz-r- created + + │ ├─ azure:network:Route dmz-hub-r- created + │ ├─ azure:network:Subnet hub-dmz-sn created - + │ ├─ azure:network:Subnet hub-fw-sn created - + │ ├─ azure:network:Subnet hub-fwm-sn created + + │ ├─ azure:network:RouteTable hub-gw-rt- created + + │ ├─ azure:network:SubnetRouteTableAssociation hub-dmz-sn-rta created + + │ ├─ azure:network:Route gw-gw-r- created + + │ ├─ azure:network:Route gw-dmz-r- created + + │ ├─ azure:network:Route gw-hub-r- created + + │ ├─ azure:network:Subnet hub-gw-sn created + + │ ├─ azure:network:PublicIp hub-vpn-gw-pip- created + + │ ├─ azure:network:PublicIp hub-er-gw-pip- created + + │ ├─ azure:network:SubnetRouteTableAssociation hub-gw-sn-rta created + │ ├─ azure:network:VirtualNetworkGateway hub-vpn-gw- created - + │ ├─ azure:network:Firewall hub-fw- created + │ ├─ azure:network:VirtualNetworkGateway hub-er-gw- created - + │ ├─ azure:network:RouteTable hub-gw-rt- created - + │ ├─ azure:network:RouteTable hub-dmz-rt- created + │ ├─ azure:network:RouteTable hub-ss-rt- created + │ ├─ azure:network:Route ss-dg-r- created + │ ├─ azure:network:Route ss-dmz-r- created + │ ├─ azure:network:Route ss-gw-r- created + │ ├─ azure:network:Subnet hub-domain-sn- created + │ ├─ azure:network:Subnet hub-files-sn- created - + │ ├─ azure:network:SubnetRouteTableAssociation hub-dmz-sn-rta created - + │ ├─ azure:network:Route dmz-dg-r- created - + │ ├─ azure:network:Route dmz-dmz-r- created - + │ ├─ azure:network:Route dmz-hub-r- created - + │ ├─ azure:network:Route gw-gw-r- created - + │ ├─ azure:network:SubnetRouteTableAssociation hub-gw-sn-rta created - + │ ├─ azure:network:Route gw-dmz-r- created - + │ ├─ azure:network:Route gw-hub-r- created + + │ ├─ azure:network:PublicIp hub-ab-pip- created + + │ ├─ azure:network:Subnet hub-ab-sn created + + │ ├─ azure:network:SubnetRouteTableAssociation hub-files-sn-rta created + │ ├─ azure:network:SubnetRouteTableAssociation hub-domain-sn-rta created - + │ └─ azure:network:SubnetRouteTableAssociation hub-files-sn-rta created + + │ └─ azure:compute:BastionHost hub-ab- created + ├─ vdc:network:Spoke s01 created + │ ├─ azure:network:VirtualNetwork s01-vn- created - + │ ├─ azure:network:VirtualNetworkPeering s01-hub-vnp- created - + │ ├─ azure:network:VirtualNetworkPeering hub-s01-vnp- created - + │ ├─ azure:network:Route ss-s01-r- created - + │ ├─ azure:network:Route dmz-s01-r- created - + │ ├─ azure:network:Route gw-s01-r- created + │ ├─ azure:network:RouteTable s01-rt- created + + │ ├─ azure:network:VirtualNetworkPeering hub-s01-vnp- created + │ ├─ azure:network:Route s01-dg-r- created + + │ ├─ azure:network:Route s01-hub-r- created + │ ├─ azure:network:Route s01-dmz-r- created + + │ ├─ azure:network:Route dmz-s01-r- created + + ├─ vdc:network:Spoke s01 created + + │ ├─ azure:network:Route ss-s01-r- created + + ├─ vdc:network:Spoke s01 created + + ├─ vdc:network:Spoke s01 created + │ ├─ azure:network:Subnet s01-web-sn- created - + │ ├─ azure:network:Route s01-hub-r- created + │ ├─ azure:network:Subnet s01-db-sn- created + + │ ├─ azure:network:Subnet s01-ab-sn created + │ ├─ azure:network:Subnet s01-app-sn- created + │ ├─ azure:network:SubnetRouteTableAssociation s01-web-sn-rta created + │ ├─ azure:network:SubnetRouteTableAssociation s01-db-sn-rta created + + │ ├─ azure:compute:BastionHost s01-ab- created + │ └─ azure:network:SubnetRouteTableAssociation s01-app-sn-rta created + ├─ vdc:network:Spoke s02 created + │ ├─ azure:network:VirtualNetwork s02-vn- created - + │ ├─ azure:network:VirtualNetworkPeering hub-s02-vnp- created - + │ ├─ azure:network:VirtualNetworkPeering s02-hub-vnp- created - + │ ├─ azure:network:Route ss-s02-r- created - + │ ├─ azure:network:Route dmz-s02-r- created - + │ ├─ azure:network:Route gw-s02-r- created + │ ├─ azure:network:RouteTable s02-rt- created + + │ ├─ azure:network:VirtualNetworkPeering hub-s02-vnp- created + │ ├─ azure:network:Route s02-dg-r- created + │ ├─ azure:network:Route s02-dmz-r- created + │ ├─ azure:network:Route s02-hub-r- created - + │ ├─ azure:network:Subnet s02-app-sn- created + + │ ├─ azure:network:Route dmz-s02-r- created + + │ ├─ azure:network:Route gw-s02-r- created + + │ ├─ azure:network:Route ss-s02-r- created + + │ ├─ azure:network:VirtualNetworkPeering s02-hub-vnp- created + + │ ├─ azure:network:PublicIp s02-ab-pip- created + │ ├─ azure:network:Subnet s02-web-sn- created + + │ ├─ azure:network:Subnet s02-app-sn- created + + │ ├─ azure:network:Subnet s02-ab-sn created + │ ├─ azure:network:Subnet s02-db-sn- created - + │ ├─ azure:network:SubnetRouteTableAssociation s02-app-sn-rta created + │ ├─ azure:network:SubnetRouteTableAssociation s02-web-sn-rta created + + │ ├─ azure:network:SubnetRouteTableAssociation s02-app-sn-rta created + + │ ├─ azure:compute:BastionHost s02-ab- created + │ └─ azure:network:SubnetRouteTableAssociation s02-db-sn-rta created + └─ azure:core:ResourceGroup prod-vdc-rg- created Outputs: - dmz_ar : "192.168.100.128/25" - fw_ip : "192.168.100.4" - hub_as : "10.100.0.0/16" - hub_id : "/subscriptions/subscription/resourceGroups/prod-vdc-rg-79a57e4b/providers/Microsoft.Network/virtualNetworks/hub-vn-46689586" - hub_name: "hub-vn-46689586" - s01_id : "/subscriptions/subscription/resourceGroups/prod-vdc-rg-79a57e4b/providers/Microsoft.Network/virtualNetworks/s01-vn-c2d9fe6f" - s01_name: "s01-vn-c2d9fe6f" - s02_id : "/subscriptions/subscription/resourceGroups/prod-vdc-rg-79a57e4b/providers/Microsoft.Network/virtualNetworks/s02-vn-61f44736" - s02_name: "s02-vn-61f44736" + dmz_ar: "192.168.100.128/25" + fw_ip : "192.168.100.4" + hub_as: "10.100.0.0/16" + hub_id: "/subscriptions/subscription/resourceGroups/prod-vdc-rg-7652071b/providers/Microsoft.Network/virtualNetworks/hub-vn-4805c98e" + s01_as: "10.101.0.0/16" + s01_id: "/subscriptions/subscription/resourceGroups/prod-vdc-rg-7652071b/providers/Microsoft.Network/virtualNetworks/s01-vn-99afe43a" + s02_as: "10.102.0.0/16" + s02_id: "/subscriptions/subscription/resourceGroups/prod-vdc-rg-7652071b/providers/Microsoft.Network/virtualNetworks/s02-vn-ebd4ec2e" Resources: - + 66 created + + 82 created - Duration: 31m27s - - Permalink: https://app.pulumi.com/organization/azure-py-vdc/prod/updates/1 + Duration: 41m34s ``` - + Feel free to modify your program, and then run `pulumi up` again. Pulumi automatically detects differences and makes the minimal changes necessary to achieved the desired state. If any changes to resources are made outside of Pulumi, you should first do a `pulumi refresh` so that Pulumi can discover the actual situation, and then `pulumi up` to return to desired state. - + Note that because most resources are [auto-named](https://www.pulumi.com/docs/intro/concepts/programming-model/#autonaming), the trailing dashes that you see above will actually be followed by random suffixes that appear in the Outputs and in Azure. 1. Create another new stack intended for Disaster Recovery (following the example): @@ -160,22 +178,22 @@ After cloning this repo, `cd` into the `azure-py-virtual-data-center` directory ```bash $ pulumi stack init dr ``` - + This will also appear within your Pulumi organization under the `azure-py-vdc` project (as specified in `Pulumi.yaml`). 1. Set the configuration variables for this stack which will be stored in a new `Pulumi.dr.yaml` file (change the values below to suit yourself): Required: ```bash - $ pulumi config set azure:environment public - $ pulumi config set azure:location australiasoutheast - $ pulumi config set firewall_address_space 192.168.200.0/24 - $ pulumi config set hub_address_space 10.200.0.0/16 + $ pulumi config set azure:environment public + $ pulumi config set azure:location australiasoutheast + $ pulumi config set firewall_address_space 192.168.200.0/24 + $ pulumi config set hub_address_space 10.200.0.0/16 ``` Optional: ```bash - $ pulumi config set azure_bastion "true" - $ pulumi config set forced_tunnel "10.0.200.1" + $ pulumi config set azure_bastion 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: @@ -186,17 +204,22 @@ After cloning this repo, `cd` into the `azure-py-virtual-data-center` directory 1. Once you have both Production and Disaster Recovery stacks (ideally in paired regions), you can connect their hubs using Global (between regions) VNet Peering: + Required: ```bash $ pulumi stack select prod - $ pulumi config set org $ pulumi config set peer dr $ pulumi up $ pulumi stack select dr - $ pulumi config set org $ pulumi config set peer prod $ pulumi up ``` - Note: it isn't yet [possible](https://github.com/pulumi/pulumi/issues/2800) to discover the Pulumi organization from within the program, which is why you need to set the `org` configuration variable for each stack that needs to peer with another stack. + Optional (for each stack): + ```bash + $ pulumi config set org organization + $ pulumi config set project project + ``` + + Note: you may specify another organization and/or project (corresponding hub and spoke names should be the same). It isn't yet [possible](https://github.com/pulumi/pulumi/issues/2800) to discover the Pulumi organization from within the program. If you later destroy a stack, you need to remove the corresponding `peer` variable in the other stack and run `pulumi up`. If you want to tear down the peerings, you should remove the `peer` variables in both stacks and run `pulumi up`: diff --git a/azure-py-virtual-data-center/__main__.py b/azure-py-virtual-data-center/__main__.py index 904672e8c..753479688 100644 --- a/azure-py-virtual-data-center/__main__.py +++ b/azure-py-virtual-data-center/__main__.py @@ -4,7 +4,7 @@ from spoke import SpokeProps, Spoke from pulumi import export -# set required vdc variable before calling function +# set required vdc variables before calling function vdc.tags = config.default_tags # all resources will be created in configuration location resource_group_name = vdc.resource_group(config.stack) @@ -20,7 +20,7 @@ reference = config.reference, resource_group_name = resource_group_name, stack = config.stack, - subnets = [ # extra columns for future NSGs + subnets = [ # extra columns for future ASGs ('domain', 'any', 'any'), ('files', 'any', 'none'), ], @@ -38,7 +38,7 @@ reference = config.reference, resource_group_name = resource_group_name, spoke_address_space = str(next(config.stack_sn)), - subnets = [ # extra columns for future NSGs + subnets = [ # extra columns for future ASGs ('web', 'any', 'app'), ('app', 'web', 'db'), ('db', 'app', 'none'), @@ -56,7 +56,7 @@ reference = config.reference, resource_group_name = resource_group_name, spoke_address_space = str(next(config.stack_sn)), - subnets = [ # extra columns for future NSGs + subnets = [ # extra columns for future ASGs ('web', 'any', 'app'), ('app', 'web', 'db'), ('db', 'app', 'none'), @@ -65,16 +65,12 @@ ), ) -# export information about the stack -export('dmz_ar', hub.dmz_ar) # required for stack peering -export('fw_ip', hub.fw_ip) # required for stack peering -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_address_spaces', hub.address_spaces) +# export information about the stack required for stack peering +export('dmz_ar', hub.dmz_ar) +export('fw_ip', hub.fw_ip) +export('hub_as', hub.address_space) +export('hub_id', hub.id) +export('s01_as', spoke1.address_space) export('s01_id', spoke1.id) -export('s01_name', spoke1.name) -export('s01_address_spaces', spoke1.address_spaces) +export('s02_as', spoke2.address_space) export('s02_id', spoke2.id) -export('s02_name', spoke2.name) -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 1b290108a..59fc56fec 100644 --- a/azure-py-virtual-data-center/config.py +++ b/azure-py-virtual-data-center/config.py @@ -29,24 +29,24 @@ def __init__(self, keys: [str], message: str): azure_bastion = config.get_bool('azure_bastion') # 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 +forced_tunnel = config.get('forced_tunnel') 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' + ft_ip = ip_address(forced_tunnel) # check IP address is valid -# another stack in the same project and organization may be peered +# another stack may be peered in the same project, even across organizations +org = config.get('org') peer = config.get('peer') -if peer: - org = config.require('org') +project = config.get('project') +if org and not project: project = get_project() - reference = StackReference(f'{org}/{project}/{peer}') -else: +if not org: + org = "" +if not project: + project = "" +if not peer: reference = None +else: + reference = StackReference(f'{org}/{project}/{peer}') # validate firewall_address_space and hub_address_space firewall_address_space = config.require('firewall_address_space') @@ -81,6 +81,6 @@ def __init__(self, keys: [str], message: str): stack_sn = super_nw.subnets(prefixlen_diff=sup_diff) hub_as = next(stack_sn) while hub_as < hub_nw: - hub_as = next(stack_sn) + hub_as = next(stack_sn) if hub_address_space != str(hub_as): raise ConfigError(['hub_address_space'], 'check assumptions') diff --git a/azure-py-virtual-data-center/hub.py b/azure-py-virtual-data-center/hub.py index b61667024..1ee4d65fb 100644 --- a/azure-py-virtual-data-center/hub.py +++ b/azure-py-virtual-data-center/hub.py @@ -1,4 +1,4 @@ -from ipaddress import ip_network +from ipaddress import ip_network, ip_address from pulumi import ComponentResource, ResourceOptions, StackReference import vdc @@ -33,8 +33,8 @@ def __init__(self, name: str, props: HubProps, opts: ResourceOptions=None): # set required vdc variables before calling functions vdc.resource_group_name = props.resource_group_name - vdc.tags = props.tags vdc.self = self + vdc.tags = props.tags # calculate the subnets in the firewall_address_space fwz_nw = ip_network(props.firewall_address_space) @@ -72,188 +72,210 @@ def __init__(self, name: str, props: HubProps, opts: ResourceOptions=None): ], ) - # 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, + # AzureFirewallManagementSubnet and Route Table + # https://docs.microsoft.com/en-us/azure/firewall/forced-tunneling + hub_fwm_rt = vdc.route_table( + stem = f'{name}-fwm', + disable_bgp_route_propagation = True, #required ) - - # 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 + # only a default route to the Internet is permitted + hub_fwm_dg = vdc.route_to_internet( + stem = 'fwm-internet', + route_table_name = hub_fwm_rt.name, + ) + hub_fwm_sn = vdc.subnet_special( + stem = f'{name}-fwm', + name = 'AzureFirewallManagementSubnet', # name required virtual_network_name = hub.name, - address_prefix = dmz_ar, + address_prefix = str(fwm_nw), + route_table_id = hub_fwm_rt.id, + depends_on = [hub, hub_fwm_rt, hub_fwm_dg], ) - # Azure will deploy the firewall into this subnet + # AzureFirewallSubnet and Route Table + hub_fw_rt = vdc.route_table( + stem = f'{name}-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 = 'fw-internet', + route_table_name = hub_fw_rt.name, + ) + else: + hub_fw_dg = vdc.route_to_virtual_appliance( + stem = 'fw-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}-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 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], + private_ranges = private_ranges, + depends_on = [hub_fw_sn, hub_fwm_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, - ) - # 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') ) + # It is very important to ensure that there is never a route with an + # address_prefix which covers the AzureFirewallSubnet. - # 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 + # DMZ subnet and Route Table 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 + depends_on = [hub_fw], ) - hub_dmz_sn_rta = vdc.subnet_route_table( + # default route from DMZ via the firewall + hub_dmz_dg = vdc.route_to_virtual_appliance( + stem = f'dmz-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-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-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}-dmz', + name = 'DMZ', # name not required but preferred + virtual_network_name = hub.name, + address_prefix = dmz_ar, route_table_id = hub_dmz_rt.id, - subnet_id = hub_dmz_sn.id, + depends_on = [hub_dmz_rt, hub_dmz_dg, hub_dmz_dmz, hub_dmz_hub], ) - #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', + # GatewaySubnet and Route Table + 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 - ) # 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, + depends_on = [hub_dmz_sn], + ) + # protect intra-GatewaySubnet traffic from being redirected: + hub_gw_gw = vdc.route_to_virtual_network( + stem = f'gw-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-dmz', + route_table_name = hub_gw_rt.name, + address_prefix = dmz_ar, + next_hop_ip_address = hub_fw_ip, ) - else: - vdc.route_to_internet( - stem = f'fw-internet', - route_table_name = hub_fw_rt.name, + # redirect traffic from gateways to hub via firewall + hub_gw_hub = vdc.route_to_virtual_appliance( + stem = f'gw-hub', + route_table_name = hub_gw_rt.name, + address_prefix = props.hub_address_space, + next_hop_ip_address = hub_fw_ip, ) - 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, + hub_gw_sn = vdc.subnet_special( + stem = f'{name}-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], ) - # 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, + # VPN Gateway + hub_vpn_gw = vdc.vpn_gateway( + stem = name, + subnet_id = hub_gw_sn.id, + depends_on = [hub_gw_sn], ) - 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, + + # ExpressRoute Gateway + hub_er_gw = vdc.expressroute_gateway( + stem = name, + subnet_id = hub_gw_sn.id, + depends_on = [hub_gw_sn], ) - # Route Table only to be associated with hub shared services subnets + # Route Table to be associated with all 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 + 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-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-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-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}-{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], + ) - # 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, + # 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 + # 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, @@ -263,12 +285,10 @@ def __init__(self, name: str, props: HubProps, opts: ResourceOptions=None): allow_forwarded_traffic = True, allow_gateway_transit = False, # as both hubs have gateways ) - - # need to invalidate system routes created by Global VNet Peering + # 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') - + peer_hub_as = props.reference.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), @@ -281,26 +301,11 @@ def __init__(self, name: str, props: HubProps, opts: ResourceOptions=None): stem = route[0], route_table_name = route[1], address_prefix = route[2], - next_hop_in_ip_address = peer_fw_ip, + next_hop_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 # exported + 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 @@ -308,7 +313,6 @@ def __init__(self, name: str, props: HubProps, opts: ResourceOptions=None): 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 self.location = hub.location # informational self.name = hub.name # exported and used for spoke peering diff --git a/azure-py-virtual-data-center/spoke.py b/azure-py-virtual-data-center/spoke.py index 680bc535e..302ad4bfe 100644 --- a/azure-py-virtual-data-center/spoke.py +++ b/azure-py-virtual-data-center/spoke.py @@ -33,8 +33,8 @@ def __init__(self, name: str, props: SpokeProps, # set required vdc variables before calling functions vdc.resource_group_name = props.resource_group_name - vdc.tags = props.tags vdc.self = self + vdc.tags = props.tags # calculate the subnets in spoke_address_space spoke_nw = ip_network(props.spoke_address_space) @@ -57,7 +57,7 @@ def __init__(self, name: str, props: SpokeProps, peer = name, remote_virtual_network_id = spoke.id, allow_gateway_transit = True, - depends_on=[props.hub.er_gw, props.hub.vpn_gw], # avoid contention + depends_on=[spoke], ) # VNet Peering from spoke to the hub @@ -68,65 +68,32 @@ def __init__(self, name: str, props: SpokeProps, 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], + depends_on=[spoke, 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 = peer_spoke_as[0], #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 + # Route Table to be associated with all 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 + # address_prefix which covers the AzureFirewallSubnet, and as VNet + # Peering may not be specified as next_hop_type, a separate address + # space for the firewall in the hub makes for simpler routes 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), + (f'{name}-hub', spoke_rt.name, props.hub.address_space), ]: 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, + next_hop_ip_address = props.hub.fw_ip, ) - # ordinary spoke subnets starting with the second subnet for subnet in props.subnets: next_sn = next(subnets) @@ -134,20 +101,36 @@ def __init__(self, name: str, props: SpokeProps, 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, + depends_on = [spoke_rt, hub_spoke, spoke_hub], + ) + + # Azure Bastion subnet and host (optional) + if props.azure_bastion: + spoke_ab = vdc.bastion_host( + stem = name, + virtual_network_name = spoke.name, + address_prefix = str(abs_nw), + depends_on = [hub_spoke, spoke_hub, spoke_rt], + ) + + # add route from firewall to corresponding spoke in peered stack + if props.peer: + peer_fw_ip = props.reference.get_output('fw_ip') + peer_spoke_as = props.reference.get_output(f'{name}_as') + fw_peer_spoke = vdc.route_to_virtual_appliance( + stem = f'fw-{props.peer}-{name}', + route_table_name = props.fw_rt_name, + address_prefix = peer_spoke_as, + next_hop_ip_address = peer_fw_ip, ) # assign properties to spoke including from child resources - self.address_spaces = spoke.address_spaces #exported + self.address_space = props.spoke_address_space self.hub = props.hub.id - self.id = spoke.id # exported + self.id = spoke.id self.location = spoke.location - self.name = spoke.name # exported + self.name = spoke.name self.resource_group_name = props.resource_group_name self.subnets = spoke.subnets self.stem = name diff --git a/azure-py-virtual-data-center/vdc.py b/azure-py-virtual-data-center/vdc.py index d17a1f8f8..511cdb04d 100644 --- a/azure-py-virtual-data-center/vdc.py +++ b/azure-py-virtual-data-center/vdc.py @@ -4,25 +4,34 @@ # Variables that may need to be injected before calling functions: # vdc.resource_group_name = props.resource_group_name -# vdc.tags = props.tags # vdc.self = self +# vdc.tags = props.tags -def bastion_host(stem, subnet_id, depends_on=None): - ab_pip = network.PublicIp( - f'{stem}-ab-pip-', +def bastion_host(stem, virtual_network_name, address_prefix, depends_on=None): + ab_sn = network.Subnet(f'{stem}-ab-sn', + name = 'AzureBastionSubnet', # name required + resource_group_name = resource_group_name, + virtual_network_name = virtual_network_name, + address_prefixes = [address_prefix], + opts = ResourceOptions( + parent = self, + delete_before_replace = True, + depends_on = depends_on, + ), + ) + ab_pip = network.PublicIp(f'{stem}-ab-pip-', resource_group_name = resource_group_name, sku = 'Standard', allocation_method = 'Static', tags = tags, - opts = ResourceOptions(parent=self), + opts = ResourceOptions(parent=self, depends_on=depends_on), ) - ab = compute.BastionHost( - f'{stem}-ab-', + ab = compute.BastionHost(f'{stem}-ab-', resource_group_name = resource_group_name, ip_configuration = compute.BastionHostIpConfigurationArgs( name = f'{stem}-ab-ipconf', public_ip_address_id = ab_pip.id, - subnet_id = subnet_id, + subnet_id = ab_sn.id, ), tags = tags, opts = ResourceOptions(parent=self, depends_on=depends_on), @@ -30,15 +39,13 @@ def bastion_host(stem, subnet_id, depends_on=None): return ab def expressroute_gateway(stem, subnet_id, depends_on=None): - er_gw_pip = network.PublicIp( - f'{stem}-er-gw-pip-', + er_gw_pip = network.PublicIp(f'{stem}-er-gw-pip-', resource_group_name = resource_group_name, allocation_method = 'Dynamic', tags = tags, - opts = ResourceOptions(parent=self), + opts = ResourceOptions(parent=self, depends_on=depends_on), ) - er_gw = network.VirtualNetworkGateway( - f'{stem}-er-gw-', + er_gw = network.VirtualNetworkGateway(f'{stem}-er-gw-', resource_group_name = resource_group_name, sku = 'Standard', type = 'ExpressRoute', @@ -50,56 +57,56 @@ def expressroute_gateway(stem, subnet_id, depends_on=None): )], tags = tags, opts = ResourceOptions( - parent=self, - depends_on=depends_on, - custom_timeouts=CustomTimeouts( - create='1h', - update='1h', - delete='1h', + parent = self, + depends_on = depends_on, + custom_timeouts = CustomTimeouts( + create = '1h', + update = '1h', + delete = '1h', ), ), ) return er_gw -def firewall(stem, fw_sn_id, fwm_sn_id, depends_on=None): - fw_pip = network.PublicIp( - f'{stem}-fw-pip-', +def firewall(stem, fw_sn_id, fwm_sn_id, private_ranges, depends_on=None): + fw_pip = network.PublicIp(f'{stem}-fw-pip-', resource_group_name = resource_group_name, sku = 'Standard', allocation_method = 'Static', tags = tags, - opts = ResourceOptions(parent=self), + opts = ResourceOptions(parent=self, depends_on=depends_on), ) -# fwm_pip = network.PublicIp( # requires api 2019-11-01 or later -# f'{stem}-fwm-pip-', -# resource_group_name = resource_group_name, -# sku = 'Standard', -# allocation_method = 'Static', -# tags = tags, -# opts = ResourceOptions(parent=self), -# ) - fw = network.Firewall( - f'{stem}-fw-', + fwm_pip = network.PublicIp(f'{stem}-fwm-pip-', resource_group_name = resource_group_name, -# sku = 'AZFW_VNet', # not required but distinguishes from 'AZFW_Hub' + sku = 'Standard', + allocation_method = 'Static', + tags = tags, + opts = ResourceOptions(parent=self, depends_on=depends_on), + ) + fw = network.Firewall(f'{stem}-fw-', + resource_group_name = resource_group_name, +# additional_properties = { +# "Network.SNAT.PrivateRanges": private_ranges, +# }, +# sku = 'AZFW_VNet', ip_configurations = [network.FirewallIpConfigurationArgs( name = f'{stem}-fw-ipconf', public_ip_address_id = fw_pip.id, subnet_id = fw_sn_id, )], -# management_ip_configuration = { # requires api 2019-11-01 or later -# 'name': f'{stem}-fwm-ipconf', -# 'publicIpAddressId': fwm_pip.id, -# 'subnet_id': fwm_sn_id, -# }, + management_ip_configuration = network.FirewallIpConfigurationArgs( + name = f'{stem}-fwm-ipconf', + public_ip_address_id = fwm_pip.id, + subnet_id = fwm_sn_id, + ), tags = tags, opts = ResourceOptions( - parent=self, - depends_on=depends_on, - custom_timeouts=CustomTimeouts( - create='1h', - update='1h', - delete='1h', + parent = self, + depends_on = depends_on, + custom_timeouts = CustomTimeouts( + create = '1h', + update = '1h', + delete = '1h', ), ), ) @@ -113,8 +120,7 @@ def resource_group(stem): return rg.name def route_table(stem, disable_bgp_route_propagation=None, depends_on=None): - rt = network.RouteTable( - f'{stem}-rt-', + rt = network.RouteTable(f'{stem}-rt-', resource_group_name = resource_group_name, disable_bgp_route_propagation = disable_bgp_route_propagation, tags = tags, @@ -123,13 +129,13 @@ 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-', + r_i = network.Route(f'{stem}-r', + name = 'FirewallDefaultRoute', # name required 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), + opts = ResourceOptions(parent=self, delete_before_replace=True), ) return r_i @@ -137,22 +143,20 @@ def route_to_virtual_appliance( stem, route_table_name, address_prefix, - next_hop_in_ip_address, + next_hop_ip_address, ): - r_va = network.Route( - f'{stem}-r-', + r_va = network.Route(f'{stem}-r-', resource_group_name = resource_group_name, address_prefix = address_prefix, next_hop_type = 'VirtualAppliance', - next_hop_in_ip_address = next_hop_in_ip_address, + next_hop_in_ip_address = next_hop_ip_address, route_table_name = route_table_name, opts = ResourceOptions(parent=self), ) return r_va def route_to_virtual_network(stem, route_table_name, address_prefix): - r_vn = network.Route( - f'{stem}-r-', + r_vn = network.Route(f'{stem}-r-', resource_group_name = resource_group_name, address_prefix = address_prefix, next_hop_type = 'VnetLocal', @@ -161,49 +165,54 @@ def route_to_virtual_network(stem, route_table_name, address_prefix): ) return r_vn -def subnet(stem, virtual_network_name, address_prefix, depends_on=None): - sn = network.Subnet( - f'{stem}-sn-', +def subnet( + stem, + virtual_network_name, + address_prefix, + route_table_id, + depends_on = None, + ): + sn = network.Subnet(f'{stem}-sn-', resource_group_name = resource_group_name, - address_prefixes = [address_prefix], virtual_network_name = virtual_network_name, + address_prefixes = [address_prefix], opts = ResourceOptions(parent=self, depends_on=depends_on), ) - return sn - -def subnet_route_table(stem, route_table_id, subnet_id): - rta = network.SubnetRouteTableAssociation( - f'{stem}-sn-rta', + rta = network.SubnetRouteTableAssociation(f'{stem}-sn-rta', route_table_id = route_table_id, - subnet_id = subnet_id, - opts = ResourceOptions(parent=self), + subnet_id = sn.id, + opts = ResourceOptions(parent=self, depends_on=depends_on), ) - return rta + return sn def subnet_special( stem, name, virtual_network_name, address_prefix, - depends_on=[], + route_table_id, + depends_on = None, ): - sn = network.Subnet( - f'{stem}-sn', + sn = network.Subnet(f'{stem}-sn', name = name, resource_group_name = resource_group_name, - address_prefixes = [address_prefix], virtual_network_name = virtual_network_name, + address_prefixes = [address_prefix], opts = ResourceOptions( - parent=self, - delete_before_replace=True, - depends_on=depends_on, + parent = self, + delete_before_replace = True, + depends_on = depends_on, ), ) + rta = network.SubnetRouteTableAssociation(f'{stem}-sn-rta', + route_table_id = route_table_id, + subnet_id = sn.id, + opts = ResourceOptions(parent=self, depends_on=depends_on), + ) return sn def virtual_network(stem, address_spaces): - vn = network.VirtualNetwork( - f'{stem}-vn-', + vn = network.VirtualNetwork(f'{stem}-vn-', resource_group_name = resource_group_name, address_spaces = address_spaces, tags = tags, @@ -216,13 +225,12 @@ def vnet_peering( virtual_network_name, peer, remote_virtual_network_id, - allow_forwarded_traffic=None, - allow_gateway_transit=None, - use_remote_gateways=None, - depends_on=None, + allow_forwarded_traffic = None, + allow_gateway_transit = None, + use_remote_gateways = None, + depends_on = None, ): - vnp = network.VirtualNetworkPeering( - f'{stem}-{peer}-vnp-', + vnp = network.VirtualNetworkPeering(f'{stem}-{peer}-vnp-', resource_group_name = resource_group_name, virtual_network_name = virtual_network_name, remote_virtual_network_id = remote_virtual_network_id, @@ -235,32 +243,31 @@ def vnet_peering( return vnp def vpn_gateway(stem, subnet_id, depends_on=None): - vpn_gw_pip = network.PublicIp( - f'{stem}-vpn-gw-pip-', + vpn_gw_pip = network.PublicIp(f'{stem}-vpn-gw-pip-', resource_group_name = resource_group_name, allocation_method = 'Dynamic', tags = tags, - opts = ResourceOptions(parent=self), + opts = ResourceOptions(parent=self, depends_on=depends_on), ) - vpn_gw = network.VirtualNetworkGateway( - f'{stem}-vpn-gw-', + vpn_gw = network.VirtualNetworkGateway(f'{stem}-vpn-gw-', resource_group_name = resource_group_name, sku = 'VpnGw1', type = 'Vpn', vpn_type = 'RouteBased', + enable_bgp = True, ip_configurations = [network.VirtualNetworkGatewayIpConfigurationArgs( - name=f'{stem}-vpn-gw-ipconf', - public_ip_address_id=vpn_gw_pip.id, - subnet_id=subnet_id, + name = f'{stem}-vpn-gw-ipconf', + public_ip_address_id = vpn_gw_pip.id, + subnet_id = subnet_id, )], tags = tags, opts = ResourceOptions( - parent=self, - depends_on=depends_on, - custom_timeouts=CustomTimeouts( - create='1h', - update='1h', - delete='1h', + parent = self, + depends_on = depends_on, + custom_timeouts = CustomTimeouts( + create = '1h', + update = '1h', + delete = '1h', ), ), )