Skip to content

Commit

Permalink
Validate address spaces in configuration (pulumi#683)
Browse files Browse the repository at this point in the history
* Validate config and update api

* Fix errata and edge cases

* Clarify comments

* Remove unnecessary variable

* Move configuration into module

* Clarify comments
  • Loading branch information
jamesianberry committed May 17, 2020
1 parent 818b9f3 commit de060e6
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 134 deletions.
2 changes: 1 addition & 1 deletion azure-py-virtual-data-center/Pulumi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ template:
default: public
azure:location:
description: Azure region to use (e.g. `australiaeast` or `australiasoutheast`)
default: australiasoutheast
default: australiaeast
8 changes: 4 additions & 4 deletions azure-py-virtual-data-center/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ This example deploys Azure Virtual Data Center (VDC) hub-and-spoke network stack

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).

The intention is for matching stacks to be deployed in Azure [paired regions](https://docs.microsoft.com/en-us/azure/best-practices-availability-paired-regions), configured as either 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.
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 configuration of network addresses.
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

Expand Down Expand Up @@ -43,7 +43,7 @@ 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 australiasoutheast
$ 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
```
Expand Down Expand Up @@ -168,7 +168,7 @@ 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 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
```
Expand Down
72 changes: 20 additions & 52 deletions azure-py-virtual-data-center/__main__.py
Original file line number Diff line number Diff line change
@@ -1,93 +1,61 @@
from ipaddress import ip_network
from pulumi import Config, get_stack, get_project, export
import config
import vdc
from hub import HubProps, Hub
from spoke import SpokeProps, Spoke
import vdc
from pulumi import export

# retrieve the configuration data
config = Config()
# set default tags to be applied to all taggable resources
stack = get_stack()
default_tags = {
'environment': stack
}
# set vdc default
vdc.tags = default_tags
# set required vdc variable before calling function
vdc.tags = config.default_tags
# all resources will be created in configuration location
resource_group_name = vdc.resource_group(stack)

# Azure Bastion hosts in hub and spokes (until it works over peerings)
azure_bastion = config.get_bool('azure_bastion')

# another stack in the same project and organization may be peered
peer = config.get('peer')
if peer:
org = config.require('org')
project = get_project()
reference = f'{org}/{project}/{peer}'
else:
reference = None

# locate hub_address_space within supernet for contiguous spoke_address_space
hub_address_space = config.require('hub_address_space')
hub_nw = ip_network(hub_address_space)
pfl_diff = int(hub_nw.prefixlen / 2)
super_nw = hub_nw.supernet(prefixlen_diff=pfl_diff)
stack_sn = super_nw.subnets(prefixlen_diff=pfl_diff)
hub_as = next(stack_sn)
while hub_as.compare_networks(hub_nw) < 0:
hub_as = next(stack_sn)
# assert that hub_address_space == str(hub_as)
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 = azure_bastion,
forced_tunnel = config.get_bool('forced_tunnel'),
firewall_address_space = config.require('firewall_address_space'),
hub_address_space = hub_address_space,
peer = peer,
reference = reference,
azure_bastion = config.azure_bastion,
forced_tunnel = config.forced_tunnel,
firewall_address_space = config.firewall_address_space,
hub_address_space = config.hub_address_space,
peer = config.peer,
reference = config.reference,
resource_group_name = resource_group_name,
stack = stack,
stack = config.stack,
subnets = [ # extra columns for future NSGs
('domain', 'any', 'any'),
('files', 'any', 'none'),
],
tags = default_tags,
tags = config.default_tags,
),
)

# multiple spokes for application environments with bastion access (optional)
spoke_address_space = str(next(stack_sn))
spoke1 = Spoke('s01', # stem of child resource names (<6 chars)
SpokeProps(
azure_bastion = azure_bastion,
azure_bastion = config.azure_bastion,
hub = hub,
resource_group_name = resource_group_name,
spoke_address_space = spoke_address_space,
spoke_address_space = str(next(config.stack_sn)),
subnets = [ # extra columns for future NSGs
('web', 'any', 'app'),
('app', 'web', 'db'),
('db', 'app', 'none'),
],
tags = default_tags,
tags = config.default_tags,
),
)

spoke_address_space = str(next(stack_sn))
spoke2 = Spoke('s02', # stem of child resource names (<6 chars)
SpokeProps(
azure_bastion = azure_bastion,
azure_bastion = config.azure_bastion,
hub = hub,
resource_group_name = resource_group_name,
spoke_address_space = spoke_address_space,
spoke_address_space = str(next(config.stack_sn)),
subnets = [ # extra columns for future NSGs
('web', 'any', 'app'),
('app', 'web', 'db'),
('db', 'app', 'none'),
],
tags = default_tags,
tags = config.default_tags,
),
)

Expand Down
78 changes: 78 additions & 0 deletions azure-py-virtual-data-center/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from ipaddress import ip_network
from pulumi import Config, get_stack, get_project

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()

# 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_bool('forced_tunnel')

# another stack in the same project and organization may be peered
peer = config.get('peer')
if peer:
org = config.require('org')
project = get_project()
reference = f'{org}/{project}/{peer}'
else:
reference = None

# 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')
Loading

0 comments on commit de060e6

Please sign in to comment.