diff --git a/ansible/filter_plugins/networks.py b/ansible/filter_plugins/networks.py
index a6c19045f..b0e91f8ae 100644
--- a/ansible/filter_plugins/networks.py
+++ b/ansible/filter_plugins/networks.py
@@ -157,6 +157,11 @@ def net_is_bridge(context, name, inventory_hostname=None):
     return net_bridge_ports(context, name) is not None
 
 
+@jinja2.contextfilter
+def net_is_vlan(context, name, inventory_hostname=None):
+    return net_vlan(context, name) is not None
+
+
 @jinja2.contextfilter
 def net_select_ethers(context, names):
     return [name for name in names if net_is_ether(context, name)]
@@ -167,6 +172,16 @@ def net_select_bridges(context, names):
     return [name for name in names if net_is_bridge(context, name)]
 
 
+@jinja2.contextfilter
+def net_select_vlans(context, names):
+    return [name for name in names if net_is_vlan(context, name)]
+
+
+@jinja2.contextfilter
+def net_reject_vlans(context, names):
+    return [name for name in names if not net_is_vlan(context, name)]
+
+
 @jinja2.contextfilter
 def net_configdrive_network_device(context, name, inventory_hostname=None):
     device = net_interface(context, name, inventory_hostname)
@@ -212,7 +227,10 @@ class FilterModule(object):
             'net_bridge_obj': net_bridge_obj,
             'net_is_ether': net_is_ether,
             'net_is_bridge': net_is_bridge,
+            'net_is_vlan': net_is_vlan,
             'net_select_ethers': net_select_ethers,
             'net_select_bridges': net_select_bridges,
+            'net_select_vlans': net_select_vlans,
+            'net_reject_vlans': net_reject_vlans,
             'net_configdrive_network_device': net_configdrive_network_device,
         }
diff --git a/ansible/group_vars/all/ironic b/ansible/group_vars/all/ironic
new file mode 100644
index 000000000..ac7f8b788
--- /dev/null
+++ b/ansible/group_vars/all/ironic
@@ -0,0 +1,16 @@
+---
+###############################################################################
+# Ironic configuration.
+
+# List of enabled Ironic drivers.
+kolla_ironic_drivers:
+  - agent_ssh
+  - agent_ipmitool
+  - pxe_ssh
+  - pxe_ipmitool
+
+# Name of the Neutron network to use for cleaning.
+kolla_ironic_cleaning_network: 'provision-net'
+
+# Name of the Neutron network to use for provisioning.
+kolla_ironic_provisioning_network: 'provision-net'
diff --git a/ansible/ip-allocation.yml b/ansible/ip-allocation.yml
index 167c59b34..c3f582385 100644
--- a/ansible/ip-allocation.yml
+++ b/ansible/ip-allocation.yml
@@ -20,6 +20,7 @@
             }]
           }}
       with_items: "{{ network_interfaces }}"
+      when: "{{ item|net_cidr != None }}"
   roles:
     - role: ip-allocation
       ip_allocation_filename: "{{ kayobe_config_path }}/network-allocation.yml"
diff --git a/ansible/kolla-ansible.yml b/ansible/kolla-ansible.yml
index b8457efcd..d77de0ddb 100644
--- a/ansible/kolla-ansible.yml
+++ b/ansible/kolla-ansible.yml
@@ -52,24 +52,35 @@
             kolla_cluster_interface: "{{ storage_mgmt_net_name | net_interface(controller_host) | replace('-', '_') }}"
             kolla_provision_interface: "{{ provision_wl_net_name | net_interface(controller_host) | replace('-', '_') }}"
             kolla_inspector_dnsmasq_interface: "{{ provision_wl_net_name | net_interface(controller_host) | replace('-', '_') }}"
+            # Initialise the following lists.
+            kolla_neutron_bridge_names: []
+            kolla_neutron_external_interfaces: []
+            kolla_neutron_bridge_interfaces: []
 
-        - name: Set facts containing the Neutron bridge and interface names for the provisioning network
+        # When these networks are VLANs, we need to use the underlying tagged
+        # bridge interface rather than the untagged interface. We therefore
+        # strip the .<vlan> suffix of the interface name. We use a union here
+        # as a single tagged interface may be shared between these networks.
+        - name: Set a fact containing the bridges to be patched to the Neutron OVS bridges
           set_fact:
-            kolla_neutron_bridge_names:
-              - "{{ provision_wl_net_name | net_interface(controller_host) ~ network_bridge_suffix_ovs }}"
-            kolla_neutron_external_interfaces:
-              - "{{ network_patch_prefix ~ provision_wl_net_name | net_interface(controller_host) ~ network_patch_suffix_ovs }}"
+            kolla_neutron_bridge_interfaces: >
+              {{ kolla_neutron_bridge_interfaces |
+                 union([item | net_interface(controller_host) | replace('.' ~ item | net_vlan(controller_host) | default('!nomatch!'), '')]) |
+                 list }}
+          with_items:
+            - "{{ provision_wl_net_name }}"
+            - "{{ external_net_name }}"
+          when: "{{ item in hostvars[controller_host].network_interfaces }}"
 
-        - name: Update facts containing the Neutron bridge and interface names for the external network
+        - name: Set facts containing the Neutron bridge and interface names
           set_fact:
             kolla_neutron_bridge_names: >
               {{ kolla_neutron_bridge_names +
-                 [external_net_name | net_interface(controller_host) ~ network_bridge_suffix_ovs] }}
+                 [item ~ network_bridge_suffix_ovs] }}
             kolla_neutron_external_interfaces: >
-              {{ kolla_neutron_external_interfaces +
-                 [network_patch_prefix ~ external_net_name | net_interface(controller_host) ~ network_patch_suffix_ovs] }}
-          when:
-            - "{{ provision_wl_net_name != external_net_name }}"
+              {{ kolla_neutron_bridge_names +
+                 [network_patch_prefix ~ item ~ network_patch_suffix_ovs] }}
+          with_items: "{{ kolla_neutron_bridge_interfaces }}"
 
         - name: Validate controller Kolla Ansible network configuration
           fail:
diff --git a/ansible/network.yml b/ansible/network.yml
index 26e124ed2..41a01566d 100644
--- a/ansible/network.yml
+++ b/ansible/network.yml
@@ -1,6 +1,11 @@
 ---
 - name: Ensure networking is configured
   hosts: seed:controllers
+  tags:
+    - config
+  vars:
+    ether_interfaces: "{{ network_interfaces | net_select_ethers | list }}"
+    bridge_interfaces: "{{ network_interfaces | net_select_bridges | list }}"
   pre_tasks:
     - block:
         - name: Validate network interface configuration
@@ -8,7 +13,7 @@
             msg: >
               Network interface validation failed - no interface configured for
               {{ item }}. This should be configured via '{{ item }}_interface'.
-          with_items: "{{ network_interfaces | net_select_ethers | list }}"
+          with_items: "{{ ether_interfaces }}"
           when: "{{ not item | net_interface }}"
 
         - name: Validate bridge interface configuration
@@ -16,10 +21,9 @@
             msg: >
               Bridge interface validation failed - no interface configured for
               {{ item }}. This should be configured via '{{ item }}_interface'.
-          with_items: "{{ network_interfaces | net_select_bridges | list }}"
+          with_items: "{{ bridge_interfaces }}"
           when: "{{ not item | net_interface }}"
       tags:
-        - config
         - config-validation
 
     - name: Ensure NetworkManager is disabled
@@ -37,53 +41,67 @@
   roles:
     - role: ahuffman.resolv
       become: True
-      tags:
-        - config
+
+    # On the first pass we configure all ethernet interfaces that are not on
+    # VLANs and all bridges. On the second pass we configure all ethernet
+    # interfaces that are on VLANs. This allows us to specify VLAN interfaces
+    # on bridges.
 
     - role: MichaelRigart.interfaces
       interfaces_ether_interfaces: >
-        {{ network_interfaces |
-           net_select_ethers |
+        {{ ether_interfaces |
+           net_reject_vlans |
            map('net_interface_obj') |
            list }}
       interfaces_bridge_interfaces: >
-        {{ network_interfaces |
-           net_select_bridges |
+        {{ bridge_interfaces |
            map('net_bridge_obj') |
            list }}
       become: True
-      tags:
-        - config
 
+    - role: MichaelRigart.interfaces
+      interfaces_ether_interfaces: >
+        {{ ether_interfaces |
+           net_select_vlans |
+           map('net_interface_obj') |
+           list }}
+      become: True
+
+# Configure a virtual ethernet patch links to connect the workload provision
+# and external network bridges to the Neutron OVS bridge.
 - name: Ensure controller workload OVS patch links exist
   hosts: controllers
-  roles:
-    # Configure a virtual ethernet patch link to connect the workload provision
-    # network bridge to the Neutron OVS bridge.
-    - role: veth
-      veth_interfaces:
-        - device: "{{ network_patch_prefix ~ provision_wl_net_name | net_interface ~ network_patch_suffix_phy }}"
-          bootproto: "static"
-          bridge: "{{ provision_wl_net_name | net_interface }}"
-          peer_device: "{{ network_patch_prefix ~ provision_wl_net_name | net_interface ~ network_patch_suffix_ovs }}"
-          peer_bootproto: "static"
-          onboot: yes
-      when: "{{ provision_wl_net_name in network_interfaces }}"
-      tags:
-        - config
+  tags:
+    - config
+  vars:
+    veth_bridges: []
+    veth_interfaces: []
+  pre_tasks:
+    # When these networks are VLANs, we need to use the underlying tagged
+    # bridge interface rather than the untagged interface. We therefore strip
+    # the .<vlan> suffix of the interface name. We use a union here as a single
+    # tagged interface may be shared between these networks.
+    - name: Update a fact containing bridges to be patched to the Neutron OVS bridge
+      set_fact:
+        veth_bridges: >
+          {{ veth_bridges |
+             union([item | net_interface | replace('.' ~ item | net_vlan | default('!nomatch!'), '')]) |
+             list }}
+      with_items:
+        - "{{ provision_wl_net_name }}"
+        - "{{ external_net_name }}"
+      when: "{{ item in network_interfaces }}"
 
-    # Configure a virtual ethernet patch link to connect the external network
-    # bridge to the Neutron OVS bridge.
+    - name: Update a fact containing veth interfaces
+      set_fact:
+        veth_interfaces: >
+          {{ veth_interfaces +
+             [{'device': network_patch_prefix ~ item ~ network_patch_suffix_phy,
+               'bootproto': 'static',
+               'bridge': item,
+               'peer_device': network_patch_prefix ~ item ~ network_patch_suffix_ovs,
+               'peer_bootproto': 'static',
+               'onboot': 'yes'}] }}
+      with_items: "{{ veth_bridges }}"
+  roles:
     - role: veth
-      veth_interfaces:
-        - device: "{{ network_patch_prefix ~ external_net_name | net_interface ~ network_patch_suffix_phy }}"
-          bootproto: "static"
-          bridge: "{{ external_net_name | net_interface }}"
-          peer_device: "{{ network_patch_prefix ~ external_net_name | net_interface ~ network_patch_suffix_ovs }}"
-          peer_bootproto: "static"
-          onboot: yes
-      when:
-        - "{{ external_net_name in network_interfaces }}"
-        - "{{ external_net_name != provision_wl_net_name }}"
-      tags:
-        - config
diff --git a/ansible/provision-net.yml b/ansible/provision-net.yml
index a9f1cd4f5..567f49d92 100644
--- a/ansible/provision-net.yml
+++ b/ansible/provision-net.yml
@@ -10,13 +10,13 @@
       neutron_net_openstack_auth_type: "{{ openstack_auth_type }}"
       neutron_net_openstack_auth: "{{ openstack_auth }}"
       # Network configuration.
-      neutron_net_name: "provision-net"
-      neutron_net_type: "flat"
+      neutron_net_name: "{{ kolla_ironic_provisioning_network }}"
+      neutron_net_type: "{% if provision_wl_net_name | net_vlan %}vlan{% else %}flat{% endif %}"
       neutron_net_physical_network: "physnet1"
-      neutron_net_segmentation_id:
+      neutron_net_segmentation_id: "{{ provision_wl_net_name | net_vlan }}"
       neutron_net_shared: True
       # Subnet configuration.
-      neutron_net_subnet_name: "provision-subnet"
+      neutron_net_subnet_name: "{{ kolla_ironic_provisioning_network }}"
       neutron_net_cidr: "{{ provision_wl_net_name | net_cidr }}"
       neutron_net_gateway_ip: "{{ provision_wl_net_name | net_gateway }}"
       neutron_net_allocation_pool_start: "{{ provision_wl_net_name | net_allocation_pool_start }}"
diff --git a/ansible/roles/kolla-openstack/defaults/main.yml b/ansible/roles/kolla-openstack/defaults/main.yml
index 971c42a79..746aef5a5 100644
--- a/ansible/roles/kolla-openstack/defaults/main.yml
+++ b/ansible/roles/kolla-openstack/defaults/main.yml
@@ -6,12 +6,18 @@ kolla_node_custom_config_path:
 # Ironic configuration.
 
 # List of enabled Ironic drivers.
-ironic_drivers:
+kolla_ironic_drivers:
   - agent_ssh
   - agent_ipmitool
   - pxe_ssh
   - pxe_ipmitool
 
+# Name or UUID of the Neutron network to use for cleaning.
+kolla_ironic_cleaning_network:
+
+# Name or UUID of the Neutron network to use for provisioning.
+kolla_ironic_provisioning_network:
+
 # Free form extra configuration to append to ironic.conf.
 kolla_extra_ironic:
 
diff --git a/ansible/roles/kolla-openstack/templates/ironic.conf.j2 b/ansible/roles/kolla-openstack/templates/ironic.conf.j2
index 9d01a7b3e..2b6d84a15 100644
--- a/ansible/roles/kolla-openstack/templates/ironic.conf.j2
+++ b/ansible/roles/kolla-openstack/templates/ironic.conf.j2
@@ -1,7 +1,7 @@
 # {{ ansible_managed }}
 
 [DEFAULT]
-enabled_drivers = {{ ironic_drivers | join(',') }}
+enabled_drivers = {{ kolla_ironic_drivers | join(',') }}
 
 [conductor]
 {% raw %}
@@ -11,6 +11,10 @@ api_url = {{ internal_protocol }}://{{ hostvars[inventory_hostname]['ansible_' +
 [agent]
 deploy_logs_local_path = /var/log/kolla/ironic/deploy
 
+[neutron]
+cleaning_network = {{ kolla_ironic_cleaning_network }}
+provisioning_network = {{ kolla_ironic_provisioning_network }}
+
 [pxe]
 {% raw %}
 tftp_server = {{ hostvars[inventory_hostname]['ansible_' + provision_interface | replace('-', '_')]['ipv4']['address'] }}
diff --git a/etc/kayobe/ironic.yml b/etc/kayobe/ironic.yml
new file mode 100644
index 000000000..6d9a3c526
--- /dev/null
+++ b/etc/kayobe/ironic.yml
@@ -0,0 +1,16 @@
+---
+###############################################################################
+# Ironic configuration.
+
+# List of enabled Ironic drivers.
+#kolla_ironic_drivers:
+
+# Name of the Neutron network to use for cleaning.
+#kolla_ironic_cleaning_network:
+
+# Name of the Neutron network to use for provisioning.
+#kolla_ironic_provisioning_network:
+
+###############################################################################
+# Dummy variable to allow Ansible to accept this file.
+workaround_ansible_issue_8743: yes