diff --git a/environments/network-isolation-no-tunneling.j2.yaml b/environments/network-isolation-no-tunneling.j2.yaml
index 6bf00f1ea4..df978de6f7 100644
--- a/environments/network-isolation-no-tunneling.j2.yaml
+++ b/environments/network-isolation-no-tunneling.j2.yaml
@@ -23,12 +23,23 @@ resource_registry:
 
   # Port assignments for each role are determined by the role definition.
 {%- for role in roles %}
+{#-     Convert net map or net list to internal list of networks #}
+{#-     NOTE(hjensas): For backward compatibility support role data with both #}
+{#-                    networks map (new schema) and network list (old schema). #}
+{%-     set _role_networks = [] %}
+{%-     if role.networks is mapping %}
+{%-         for key,val in role.networks.items() %}
+{%-             set _role_networks = _role_networks.append(key) %}
+{%-         endfor %}
+{%-     else %}
+{%-         set _role_networks = role.networks %}
+{%-     endif %}
   # Port assignments for the {{role.name}} role.
-  {%- for network in networks %}
-    {%- if network.name in role.networks|default([]) and network.enabled|default(true) and network.name != 'Tenant'%}
+{%-     for network in networks %}
+{%-         if network.name in _role_networks and network.enabled|default(true) and network.name != 'Tenant'%}
   OS::TripleO::{{role.name}}::Ports::{{network.name}}Port: ../network/ports/{{network.name_lower|default(network.name.lower())}}.yaml
-    {%- elif network.enabled|default(true) %}
+{%-         elif network.enabled|default(true) %}
   OS::TripleO::{{role.name}}::Ports::{{network.name}}Port: ../network/ports/noop.yaml
-    {%- endif %}
-  {%- endfor %}
-{% endfor %}
+{%-         endif %}
+{%-     endfor %}
+{%- endfor %}
diff --git a/environments/network-isolation-v6.j2.yaml b/environments/network-isolation-v6.j2.yaml
index 1a0a71c29b..27cdba4053 100644
--- a/environments/network-isolation-v6.j2.yaml
+++ b/environments/network-isolation-v6.j2.yaml
@@ -35,16 +35,27 @@ resource_registry:
 
   # Port assignments by role, edit role definition to assign networks to roles.
 {%- for role in roles %}
+{#-     Convert net map or net list to internal list of networks #}
+{#-     NOTE(hjensas): For backward compatibility support role data with both #}
+{#-                    networks map (new schema) and network list (old schema). #}
+{%-     set _role_networks = [] %}
+{%-     if role.networks is mapping %}
+{%-         for key,val in role.networks.items() %}
+{%-             set _role_networks = _role_networks.append(key) %}
+{%-         endfor %}
+{%-     else %}
+{%-         set _role_networks = role.networks %}
+{%-     endif %}
   # Port assignments for the {{role.name}}
-  {%- for network in networks %}
-    {%- if network.name in role.networks|default([]) and network.enabled|default(true) and network.name != 'Tenant' %}
+{%-     for network in networks %}
+{%-         if network.name in _role_networks and network.enabled|default(true) and network.name != 'Tenant' %}
   OS::TripleO::{{role.name}}::Ports::{{network.name}}Port: ../network/ports/{{network.name_lower|default(network.name.lower())}}_v6.yaml
-    {%- elif network.name in role.networks|default([]) and network.enabled|default(true) and network.name == 'Tenant' %}
-    # IPv4 until OVS and Neutron support IPv6 tunnel endpoints
+{%-         elif network.name in _role_networks and network.enabled|default(true) and network.name == 'Tenant' %}
+  # IPv4 until OVS and Neutron support IPv6 tunnel endpoints
   OS::TripleO::{{role.name}}::Ports::{{network.name}}Port: ../network/ports/{{network.name_lower|default(network.name.lower())}}.yaml
-    {%- endif %}
-  {%- endfor %}
-{% endfor %}
+{%-         endif %}
+{%-     endfor %}
+{%- endfor %}
 
 
 parameter_defaults:
diff --git a/environments/network-isolation.j2.yaml b/environments/network-isolation.j2.yaml
index 3d4f59b603..6ed4fb6de2 100644
--- a/environments/network-isolation.j2.yaml
+++ b/environments/network-isolation.j2.yaml
@@ -24,10 +24,21 @@ resource_registry:
 
   # Port assignments by role, edit role definition to assign networks to roles.
 {%- for role in roles %}
+{#-     Convert net map or net list to internal list of networks #}
+{#-     NOTE(hjensas): For backward compatibility support role data with both #}
+{#-                    networks map (new schema) and network list (old schema). #}
+{%-     set _role_networks = [] %}
+{%-     if role.networks is mapping %}
+{%-         for key,val in role.networks.items() %}
+{%-             set _role_networks = _role_networks.append(key) %}
+{%-         endfor %}
+{%-     else %}
+{%-         set _role_networks = role.networks %}
+{%-     endif %}
   # Port assignments for the {{role.name}}
-  {%- for network in networks %}
-    {%- if network.name in role.networks|default([]) and network.enabled|default(true) %}
+{%-     for network in networks %}
+{%-         if network.name in _role_networks and network.enabled|default(true) %}
   OS::TripleO::{{role.name}}::Ports::{{network.name}}Port: ../network/ports/{{network.name_lower|default(network.name.lower())}}.yaml
-    {%- endif %}
-  {%- endfor %}
-{% endfor %}
+{%-         endif %}
+{%-     endfor %}
+{%- endfor %}
diff --git a/network/config/2-linux-bonds-vlans/role.role.j2.yaml b/network/config/2-linux-bonds-vlans/role.role.j2.yaml
index 3356dc2a57..696a0dcef6 100644
--- a/network/config/2-linux-bonds-vlans/role.role.j2.yaml
+++ b/network/config/2-linux-bonds-vlans/role.role.j2.yaml
@@ -1,3 +1,14 @@
+{#- Convert net map or net list to internal list of networks #}
+{#- NOTE(hjensas): For backward compatibility support role data with both #}
+{#-                networks map (new schema) and network list (old schema). #}
+{%- set _role_networks = [] %}
+{%- if role.networks is mapping %}
+{%-     for key,val in role.networks.items() %}
+{%-         set _role_networks = _role_networks.append(key) %}
+{%-     endfor %}
+{%- else %}
+{%-     set _role_networks = role.networks %}
+{%- endif %}
 heat_template_version: rocky
 description: >
   Software Config to drive os-net-config with 2 Linux bonds. One bond is on a
@@ -32,7 +43,7 @@ parameters:
       guaranteed to pass through the data path of the segments in the network.
       (The parameter is automatically resolved from the ctlplane network's mtu attribute.)
     type: number
-{%- for network in networks if network.enabled|default(true) and network.name in role.networks %}
+{%- for network in networks if network.enabled|default(true) and network.name in _role_networks %}
 
   {{network.name}}IpSubnet:
     default: ''
@@ -110,7 +121,7 @@ resources:
           expression: $.data.max()
           data:
             - {get_param: ControlPlaneMtu}
-{%- for network in networks if network.enabled|default(true) and network.name in role.networks and not network.name.startswith('Tenant') %}
+{%- for network in networks if network.enabled|default(true) and network.name in _role_networks and not network.name.startswith('Tenant') %}
             - {get_param: {{network.name}}Mtu}
 {%- endfor %}
 
@@ -127,7 +138,7 @@ resources:
           expression: $.data.max()
           data:
             - {get_param: ControlPlaneMtu}
-{%- for network in networks if network.name.startswith('Tenant') and network.enabled|default(true) and network.name in role.networks %}
+{%- for network in networks if network.name.startswith('Tenant') and network.enabled|default(true) and network.name in _role_networks %}
             - {get_param: {{network.name}}Mtu}
 {%- endfor %}
 
@@ -183,7 +194,7 @@ resources:
                     name: nic3
                     mtu:
                       get_attr: [MinViableMtuBondApi, value]
-{%- for network in networks if network.enabled|default(true) and network.name in role.networks and not network.name.startswith('Tenant') %}
+{%- for network in networks if network.enabled|default(true) and network.name in _role_networks and not network.name.startswith('Tenant') %}
               - type: vlan
                 device: bond_api
                 mtu:
@@ -224,7 +235,7 @@ resources:
                     name: nic5
                     mtu:
                       get_attr: [MinViableMtuBondData, value]
-  {%- for network in networks if network.name.startswith('Tenant') and network.name in role.networks and network.enabled|default(true) %}
+  {%- for network in networks if network.name.startswith('Tenant') and network.name in _role_networks and network.enabled|default(true) %}
               - type: vlan
                 device: bond-data
                 mtu:
@@ -247,7 +258,7 @@ resources:
               - type: ovs_user_bridge
                 name: br-dpdk0
                 use_dhcp: false
-  {%- for network in networks if network.name.startswith('Tenant') and network.name in role.networks and network.enabled|default(true) %}
+  {%- for network in networks if network.name.startswith('Tenant') and network.name in _role_networks and network.enabled|default(true) %}
                 ovs_extra:
                   - str_replace:
                       template: set port br-dpdk0 tag=_VLAN_TAG_
diff --git a/network/config/bond-with-vlans/role.role.j2.yaml b/network/config/bond-with-vlans/role.role.j2.yaml
index 7d55720482..2bd747ca9f 100644
--- a/network/config/bond-with-vlans/role.role.j2.yaml
+++ b/network/config/bond-with-vlans/role.role.j2.yaml
@@ -1,3 +1,14 @@
+{#- Convert net map or net list to internal list of networks #}
+{#- NOTE(hjensas): For backward compatibility support role data with both #}
+{#-                networks map (new schema) and network list (old schema). #}
+{%- set _role_networks = [] %}
+{%- if role.networks is mapping %}
+{%-     for key,val in role.networks.items() %}
+{%-         set _role_networks = _role_networks.append(key) %}
+{%-     endfor %}
+{%- else %}
+{%-     set _role_networks = role.networks %}
+{%- endif %}
 heat_template_version: rocky
 description: >
   Software Config to drive os-net-config with 2 bonded nics on a bridge with VLANs attached for the {{role.name}} role.
@@ -168,7 +179,7 @@ resources:
                     name: nic3
                     mtu:
                       get_attr: [MinViableMtu, value]
-{%-     for network in networks if network.enabled|default(true) and network.name in role.networks %}
+{%-     for network in networks if network.enabled|default(true) and network.name in _role_networks %}
                 - type: vlan
                   mtu:
                     get_param: {{network.name}}Mtu
@@ -207,7 +218,7 @@ resources:
                   name: nic3
                   mtu:
                     get_attr: [MinViableMtu, value]
-{%-     for network in networks if network.enabled|default(true) and network.name in role.networks %}
+{%-     for network in networks if network.enabled|default(true) and network.name in _role_networks %}
               - type: vlan
                 device: bond_api
                 mtu:
diff --git a/network/config/multiple-nics/role.role.j2.yaml b/network/config/multiple-nics/role.role.j2.yaml
index bad30162bc..e58207ab83 100644
--- a/network/config/multiple-nics/role.role.j2.yaml
+++ b/network/config/multiple-nics/role.role.j2.yaml
@@ -1,3 +1,14 @@
+{#- Convert net map or net list to internal list of networks #}
+{#- NOTE(hjensas): For backward compatibility support role data with both #}
+{#-                networks map (new schema) and network list (old schema). #}
+{%- set _role_networks = [] %}
+{%- if role.networks is mapping %}
+{%-     for key,val in role.networks.items() %}
+{%-         set _role_networks = _role_networks.append(key) %}
+{%-     endfor %}
+{%- else %}
+{%-     set _role_networks = role.networks %}
+{%- endif %}
 heat_template_version: rocky
 description: >
   Software Config to drive os-net-config to configure multiple interfaces for the {{role.name}} role.
@@ -123,7 +134,7 @@ resources:
 {%- set nics_used = [1] %}
 {%- for network in networks if network.enabled|default(true) and network.name not in role.networks_skip_config|default([]) %}
 {%-     if network.name not in ["External", "Tenant"] %}
-{%-         if network.name in role.networks %}
+{%-         if network.name in _role_networks %}
               - type: interface
                 name: nic{{loop.index + 1}}
                 mtu:
@@ -153,7 +164,7 @@ resources:
                 dns_servers:
                   get_param: DnsServers
                 use_dhcp: false
-{%-         if network.name in role.networks %}
+{%-         if network.name in _role_networks %}
                 addresses:
                 - ip_netmask:
                     get_param: {{network.name}}IpSubnet
diff --git a/network/config/single-nic-linux-bridge-vlans/role.role.j2.yaml b/network/config/single-nic-linux-bridge-vlans/role.role.j2.yaml
index 14b1d8f26c..2c73894f30 100644
--- a/network/config/single-nic-linux-bridge-vlans/role.role.j2.yaml
+++ b/network/config/single-nic-linux-bridge-vlans/role.role.j2.yaml
@@ -1,3 +1,14 @@
+{#- Convert net map or net list to internal list of networks #}
+{#- NOTE(hjensas): For backward compatibility support role data with both #}
+{#-                networks map (new schema) and network list (old schema). #}
+{%- set _role_networks = [] %}
+{%- if role.networks is mapping %}
+{%-     for key,val in role.networks.items() %}
+{%-         set _role_networks = _role_networks.append(key) %}
+{%-     endfor %}
+{%- else %}
+{%-     set _role_networks = role.networks %}
+{%- endif %}
 heat_template_version: rocky
 description: >
   Software Config to drive os-net-config to configure VLANs for the {{role.name}} role.
@@ -31,7 +42,7 @@ parameters:
       guaranteed to pass through the data path of the segments in the network.
       (The parameter is automatically resolved from the ctlplane network's mtu attribute.)
     type: number
-{% for network in networks if network.enabled|default(true) and network.name in role.networks %}
+{% for network in networks if network.enabled|default(true) and network.name in _role_networks %}
   {{network.name}}IpSubnet:
     default: ''
     description: IP address/subnet on the {{network.name_lower}} network
@@ -93,7 +104,7 @@ resources:
           expression: $.data.max()
           data:
             - {get_param: ControlPlaneMtu}
-{%- for network in networks if network.enabled|default(true) and network.name in role.networks %}
+{%- for network in networks if network.enabled|default(true) and network.name in _role_networks %}
             - {get_param: {{network.name}}Mtu}
 {%- endfor %}
 
@@ -142,7 +153,7 @@ resources:
                   mtu:
                     get_attr: [MinViableMtu, value]
                   primary: true
-{%- for network in networks if network.enabled|default(true) and network.name in role.networks
+{%- for network in networks if network.enabled|default(true) and network.name in _role_networks
     and network.name not in role.networks_skip_config|default([]) %}
               - type: vlan
                 mtu:
diff --git a/network/config/single-nic-vlans/role.role.j2.yaml b/network/config/single-nic-vlans/role.role.j2.yaml
index d7fb2e3c19..54eb7f1df2 100644
--- a/network/config/single-nic-vlans/role.role.j2.yaml
+++ b/network/config/single-nic-vlans/role.role.j2.yaml
@@ -1,3 +1,14 @@
+{#- Convert net map or net list to internal list of networks #}
+{#- NOTE(hjensas): For backward compatibility support role data with both #}
+{#-                networks map (new schema) and network list (old schema). #}
+{%- set _role_networks = [] %}
+{%- if role.networks is mapping %}
+{%-     for key,val in role.networks.items() %}
+{%-         set _role_networks = _role_networks.append(key) %}
+{%-     endfor %}
+{%- else %}
+{%-     set _role_networks = role.networks %}
+{%- endif %}
 heat_template_version: rocky
 description: >
   Software Config to drive os-net-config to configure VLANs for the {{role.name}} role.
@@ -143,7 +154,7 @@ resources:
                     get_attr: [MinViableMtu, value]
                   # force the MAC address of the bridge to this interface
                   primary: true
-{%- for network in networks if network.enabled|default(true) and network.name in role.networks %}
+{%- for network in networks if network.enabled|default(true) and network.name in _role_networks %}
                 - type: vlan
                   mtu:
                     get_param: {{network.name}}Mtu
diff --git a/network/ports/ctlplane_vip.yaml b/network/ports/ctlplane_vip.yaml
index 4d3aa589f7..a4b32be1da 100644
--- a/network/ports/ctlplane_vip.yaml
+++ b/network/ports/ctlplane_vip.yaml
@@ -45,7 +45,9 @@ parameters:
 conditions:
   fixed_ip_not_set:
     equals:
-    - get_param: FixedIPs
+    - yaql:
+        expression: $.data.where($.get('ip_address'))
+        data: {get_param: FixedIPs}
     - []
 
 resources:
diff --git a/network/ports/port.j2 b/network/ports/port.j2
index 19ac0d9271..587da45b32 100644
--- a/network/ports/port.j2
+++ b/network/ports/port.j2
@@ -51,7 +51,9 @@ conditions:
     - ctlplane
   fixed_ip_not_set:
     equals:
-    - get_param: FixedIPs
+    - yaql:
+        expression: $.data.where($.get('ip_address'))
+        data: {get_param: FixedIPs}
     - []
   net_is_ctlplane_and_fixed_ip_not_set:
     and:
diff --git a/network/ports/vip.yaml b/network/ports/vip.yaml
index 4cbe70ce43..4010ea11de 100644
--- a/network/ports/vip.yaml
+++ b/network/ports/vip.yaml
@@ -49,7 +49,9 @@ conditions:
     - ctlplane
   fixed_ip_not_set:
     equals:
-    - get_param: FixedIPs
+    - yaql:
+        expression: $.data.where($.get('ip_address'))
+        data: {get_param: FixedIPs}
     - []
   net_is_ctlplane_and_fixed_ip_not_set:
     and:
diff --git a/network/ports/vip_v6.yaml b/network/ports/vip_v6.yaml
index 6cc5510d3e..ca15dc955a 100644
--- a/network/ports/vip_v6.yaml
+++ b/network/ports/vip_v6.yaml
@@ -49,7 +49,9 @@ conditions:
     - ctlplane
   fixed_ip_not_set:
     equals:
-    - get_param: FixedIPs
+    - yaql:
+        expression: $.data.where($.get('ip_address'))
+        data: {get_param: FixedIPs}
     - []
   net_is_ctlplane_and_fixed_ip_not_set:
     and:
diff --git a/network/service_net_map.j2.yaml b/network/service_net_map.j2.yaml
index c5f9c490c0..0083d73635 100644
--- a/network/service_net_map.j2.yaml
+++ b/network/service_net_map.j2.yaml
@@ -95,6 +95,26 @@ parameters:
                  via parameter_defaults in the resource registry.
     type: json
 
+  ControlPlaneSubnet:
+    description: The name of the undercloud Neutron control plane subnet
+    default: ctlplane-subnet
+    type: string
+
+  VipSubnetMap:
+    description: Map of (network_name or service_name) -> subnet_name that
+                 defines which subnet to host the VIP.
+    default: {}
+    type: json
+
+  VipSubnetMapDefaults:
+    default:
+      ctlplane: ctlplane-subnet
+{%- for network in networks if network.vip|default(false) %}
+      {{network.name}}: {{network.name_lower}}_subnet
+{%- endfor %}
+      redis: internal_api_subnet
+    type: json
+
   # We define mappings to work around names that break when doing the
   # CamelCase to snake_case conversion to align with service_names
   ServiceNetMapDeprecatedMapping:
@@ -141,6 +161,19 @@ resources:
              - {get_param: ServiceNetMap}
              - keys: {get_param: ServiceNetMapDeprecatedMapping}
 
+  VipSubnetMapValue:
+    type: OS::Heat::Value
+    properties:
+      type: json
+      value:
+        map_merge:
+          # (hjensas): We need to map_replace the ctlplane-subnet to support
+          # legacy parameter ControlPlaneSubnet.
+          - map_replace:
+            - {get_param: VipSubnetMapDefaults}
+            - values:
+                ctlplane-subnet: {get_param: ControlPlaneSubnet}
+          - {get_param: VipSubnetMap}
 
 outputs:
   service_net_map:
@@ -155,3 +188,6 @@ outputs:
         expression: dict($.data.map.items().select([ regex(`([a-z0-9])([A-Z])`).replace($[0], '\\1_\\2').toLower(), $[1]]))
         data:
           map: {get_attr: [ServiceNetMapValue, value]}
+
+  vip_subnet_map:
+    value: {get_attr: [VipSubnetMapValue, value]}
diff --git a/overcloud.j2.yaml b/overcloud.j2.yaml
index ca38be9d5d..da18dfee42 100644
--- a/overcloud.j2.yaml
+++ b/overcloud.j2.yaml
@@ -110,8 +110,6 @@ parameters:
         Control the IP allocation for the ControlVirtualIP port. E.g.
         [{'ip_address':'1.2.3.4'}]
     type: json
-{%- for network in networks if network.vip|default(false) %}
-{%- if network.name == 'External' %}
   # TODO (dsneddon) Legacy name, eventually refactor to match network name
   PublicVirtualFixedIPs:
     default: []
@@ -119,14 +117,13 @@ parameters:
         Control the IP allocation for the PublicVirtualInterface port. E.g.
         [{'ip_address':'1.2.3.4'}]
     type: json
-{%- else %}
+{%- for network in networks if network.vip|default(false) and network.name != 'External' %}
   {{network.name}}VirtualFixedIPs:
     default: []
     description: >
         Control the IP allocation for the {{network.name}}VirtualInterface port. E.g.
         [{'ip_address':'1.2.3.4'}]
     type: json
-{%- endif %}
 {%- endfor %}
   RabbitCookieSalt:
     type: string
@@ -269,6 +266,23 @@ conditions:
   ctlplane_subnet_cidr_set:
     not:
       equals: [{get_param: ControlPlaneSubnetCidr}, '']
+{%- for network in networks if network.name != 'External' %}
+  {{network.name_lower}}_virtual_fixed_ip_set:
+    not:
+      equals:
+        - get_param: {{network.name}}VirtualFixedIPs
+        - []
+{%- endfor %}
+  public_virtual_fixed_ip_set:
+    not:
+      equals:
+        - get_param: PublicVirtualFixedIPs
+        - []
+  redis_virtual_fixed_ip_set:
+    not:
+      equals:
+        - get_param: RedisVirtualFixedIPs
+        - []
 
 resources:
 
@@ -799,20 +813,20 @@ resources:
     type: OS::TripleO::Network
 
   ControlVirtualIP:
+    depends_on: [Networks, ServiceNetMap]
     type: OS::TripleO::Network::Ports::ControlPlaneVipPort
-    depends_on: Networks
     properties:
       name: control_virtual_ip
       network: {get_param: NeutronControlPlaneID}
       fixed_ips:
         if:
         - control_fixed_ip_not_set
-        - [{subnet: {get_param: ControlPlaneSubnet}}]
+        - [{subnet: {get_attr: [ServiceNetMap, vip_subnet_map, ctlplane]}}]
         - get_param: ControlFixedIPs
       replacement_policy: AUTO
 
   RedisVirtualIP:
-    depends_on: Networks
+    depends_on: [Networks, ServiceNetMap]
     type: OS::TripleO::Network::Ports::RedisVipPort
     properties:
       ControlPlaneIP: {get_attr: [ControlVirtualIP, fixed_ips, 0, ip_address]}
@@ -825,13 +839,17 @@ resources:
       PortName: redis_virtual_ip
       NetworkName: {get_attr: [ServiceNetMap, service_net_map, RedisNetwork]}
       ServiceName: redis
-      FixedIPs: {get_param: RedisVirtualFixedIPs}
+      FixedIPs:
+        if:
+        - redis_virtual_fixed_ip_set
+        - {get_param: RedisVirtualFixedIPs}
+        - [{subnet: {get_attr: [ServiceNetMap, vip_subnet_map, redis]}}]
 
 {%- for network in networks if network.vip|default(false) %}
 {%- if network.name == 'External' %}
   # The public VIP is on the External net, falls back to ctlplane
   PublicVirtualIP:
-    depends_on: Networks
+    depends_on: [Networks, ServiceNetMap]
     type: OS::TripleO::Network::Ports::ExternalVipPort
     properties:
       ControlPlaneIP: {get_attr: [ControlVirtualIP, fixed_ips, 0, ip_address]}
@@ -842,10 +860,14 @@ resources:
           - {str_split: ['/', {get_attr: [ControlVirtualIP, subnets, 0, cidr]}, 1]}
       ControlPlaneNetwork: {get_param: NeutronControlPlaneID}
       PortName: public_virtual_ip
-      FixedIPs: {get_param: PublicVirtualFixedIPs}
+      FixedIPs:
+        if:
+        - public_virtual_fixed_ip_set
+        - {get_param: PublicVirtualFixedIPs}
+        - [{subnet: {get_attr: [ServiceNetMap, vip_subnet_map, {{network.name}}]}}]
 {%- else %}
   {{network.name}}VirtualIP:
-    depends_on: Networks
+    depends_on: [Networks, ServiceNetMap]
     type: OS::TripleO::Network::Ports::{{network.name}}VipPort
     properties:
       ControlPlaneIP: {get_attr: [ControlVirtualIP, fixed_ips, 0, ip_address]}
@@ -855,7 +877,11 @@ resources:
           - {get_param: ControlPlaneSubnetCidr}
           - {str_split: ['/', {get_attr: [ControlVirtualIP, subnets, 0, cidr]}, 1]}
       PortName: {{network.name_lower}}_virtual_ip
-      FixedIPs: {get_param: {{network.name}}VirtualFixedIPs}
+      FixedIPs:
+        if:
+        - {{network.name_lower}}_virtual_fixed_ip_set
+        - {get_param: {{network.name}}VirtualFixedIPs}
+        - [{subnet: {get_attr: [ServiceNetMap, vip_subnet_map, {{network.name}}]}}]
 {%- endif %}
 {%- endfor %}
 
diff --git a/puppet/role.role.j2.yaml b/puppet/role.role.j2.yaml
index 1b224673dc..de3cd571c2 100644
--- a/puppet/role.role.j2.yaml
+++ b/puppet/role.role.j2.yaml
@@ -466,7 +466,11 @@ resources:
         if:
           - {{role.name}}_{{network.name}}_fixed_ip_set
           - [{ip_address: {get_param: [{{role.name}}IPs, '{{network.name_lower}}', {get_param: NodeIndex}]}}]
-          - []
+{%-     if role.networks is mapping and role.networks.get(network.name) %}
+          - [{subnet: {{role.networks[network.name].get('subnet', network.name_lower + '_subnet')}}}]
+{%-     else %}
+          - [{subnet: {{network.name_lower}}_subnet}]
+{%-     endif %}
       ControlPlaneSubnetCidr:
         if:
           - ctlplane_subnet_cidr_set
@@ -476,7 +480,6 @@ resources:
           - yaql:
               expression: str("{0}".format($.data).split("/")[-1])
               data: {get_attr: [{{server_resource_name}}, addresses, ctlplane, 0, subnets, 0, cidr]}
-
       IPPool:
         map_merge:
 {%- if role.deprecated_param_ips is defined %}
diff --git a/releasenotes/notes/composable-network-subnets-fbfcb6283a54ace7.yaml b/releasenotes/notes/composable-network-subnets-fbfcb6283a54ace7.yaml
new file mode 100644
index 0000000000..40142a7c1e
--- /dev/null
+++ b/releasenotes/notes/composable-network-subnets-fbfcb6283a54ace7.yaml
@@ -0,0 +1,48 @@
+---
+features:
+  - |
+    Composable Networks now support creating L3 routed networks. L3 networks
+    use multiple L2 network segments and multiple ip subnets. In addition to
+    the base subnet automatically created for any composable network,
+    additional subnets can be defined under the ``subnets`` key for each
+    network in the data file (``network_data.yaml``) used by composable
+    networks. Please refer to the ``network_data_subnets_routed.yaml`` file for
+    an example demonstrating how to define composable L3 routed networks.
+  - |
+    For composable roles it is now possible to control which subnet in a L3
+    routed network will host network ports for the role. This is done by
+    setting the subnet for each network in the role defenition
+    (``roles_data.yaml``). For example::
+
+      - name: <role_name>
+        networks:
+        InternalApi:
+          subnet: internal_api_leaf2
+        Tenant:
+          subnet: tenant_leaf2
+        Storage:
+          subnet: storage_leaf2
+  - |
+    To enable control of which subnet is used for virtual IPs on L3 routed
+    composable networks the new parameter ``VipSubnetMap`` where added. This
+    allow the user to override the subnet where the VIP port should be hosted.
+    For example::
+
+      parameter_defaults:
+        VipSubnetMap:
+          ctlplane: ctlplane-leaf1
+          InternalApi: internal_api_leaf1
+          Storage: storage_leaf1
+          redis: internal_api_leaf1
+upgrade:
+  - |
+    Deployments using custom names for subnets must also set the subnet to use
+    for the roles used in the deployment. I.e if ``NetworkNameSubnetName``
+    parameter was used to define a non-default subnet name for any network, the
+    role defenition (``roles_data.yaml``) and ``VipSubnetMap`` parameter
+    must use the same value.
+
+    .. Warning:: The update will fail if ``<NetworkName>SubnetName`` was used
+                 to set a custom subnet name, and the role defenition and/or
+                 the ``VipSubnetMap`` is not set to match the custom subnet
+                 name.
diff --git a/roles/BlockStorage.yaml b/roles/BlockStorage.yaml
index 1348d0ab89..48faf5eb6f 100644
--- a/roles/BlockStorage.yaml
+++ b/roles/BlockStorage.yaml
@@ -5,9 +5,12 @@
   description: |
     Cinder Block Storage node role
   networks:
-    - InternalApi
-    - Storage
-    - StorageMgmt
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
   uses_deprecated_params: False
   deprecated_nic_config_name: 'cinder-storage.yaml'
   ServicesDefault:
diff --git a/roles/CephAll.yaml b/roles/CephAll.yaml
index cda47655c2..bc9189971d 100644
--- a/roles/CephAll.yaml
+++ b/roles/CephAll.yaml
@@ -5,8 +5,10 @@
   description: |
     Standalone Storage Full Role (OSD + MON + RGW + MDS + MGR + RBD Mirroring)
   networks:
-    - Storage
-    - StorageMgmt
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
   HostnameFormatDefault: '%stackname%-ceph-all-%index%'
   ServicesDefault:
     - OS::TripleO::Services::Aide
diff --git a/roles/CephFile.yaml b/roles/CephFile.yaml
index a03c3efc72..a8ad4d1ec9 100644
--- a/roles/CephFile.yaml
+++ b/roles/CephFile.yaml
@@ -5,8 +5,10 @@
   description: |
     Standalone Scale-out File Role (OSD + MDS)
   networks:
-    - Storage
-    - StorageMgmt
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
   HostnameFormatDefault: '%stackname%-ceph-file-%index%'
   ServicesDefault:
     - OS::TripleO::Services::Aide
diff --git a/roles/CephObject.yaml b/roles/CephObject.yaml
index 717511219c..2dc1527aec 100644
--- a/roles/CephObject.yaml
+++ b/roles/CephObject.yaml
@@ -5,8 +5,10 @@
   description: |
     Standalone Scale-out Object Role (OSD + RGW)
   networks:
-    - Storage
-    - StorageMgmt
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
   HostnameFormatDefault: '%stackname%-ceph-object-%index%'
   ServicesDefault:
     - OS::TripleO::Services::Aide
diff --git a/roles/CephStorage.yaml b/roles/CephStorage.yaml
index 79fb0be99b..c52a60a59c 100644
--- a/roles/CephStorage.yaml
+++ b/roles/CephStorage.yaml
@@ -5,8 +5,10 @@
   description: |
     Ceph OSD Storage node role
   networks:
-    - Storage
-    - StorageMgmt
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
   uses_deprecated_params: False
   deprecated_nic_config_name: 'ceph-storage.yaml'
   ServicesDefault:
diff --git a/roles/Compute.yaml b/roles/Compute.yaml
index 561af88aae..4352565623 100644
--- a/roles/Compute.yaml
+++ b/roles/Compute.yaml
@@ -6,9 +6,12 @@
     Basic Compute Node role
   CountDefault: 1
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
   HostnameFormatDefault: '%stackname%-novacompute-%index%'
   RoleParametersDefault:
     TunedProfileName: "virtual-host"
diff --git a/roles/ComputeAlt.yaml b/roles/ComputeAlt.yaml
index dda7ed37a1..3222e24506 100644
--- a/roles/ComputeAlt.yaml
+++ b/roles/ComputeAlt.yaml
@@ -6,9 +6,12 @@
    Alternate Compute Node role
   CountDefault: 0
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
   HostnameFormatDefault: '%stackname%-novacomputealt-%index%'
   disable_constraints: True
   RoleParametersDefault:
diff --git a/roles/ComputeDVR.yaml b/roles/ComputeDVR.yaml
index bbbc9b05f5..6a33cd1689 100644
--- a/roles/ComputeDVR.yaml
+++ b/roles/ComputeDVR.yaml
@@ -6,9 +6,12 @@
     DVR enabled Compute Node role
   CountDefault: 1
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
   HostnameFormatDefault: '%stackname%-novacompute-dvr-%index%'
   RoleParametersDefault:
     TunedProfileName: "virtual-host"
diff --git a/roles/ComputeHCI.yaml b/roles/ComputeHCI.yaml
index 8e513b8b3f..3483b85260 100644
--- a/roles/ComputeHCI.yaml
+++ b/roles/ComputeHCI.yaml
@@ -5,10 +5,14 @@
   description: |
     Compute Node role hosting Ceph OSD too
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
-    - StorageMgmt
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
   RoleParametersDefault:
     TunedProfileName: "throughput-performance"
   ServicesDefault:
diff --git a/roles/ComputeInstanceHA.yaml b/roles/ComputeInstanceHA.yaml
index b235d52927..0bc51976c8 100644
--- a/roles/ComputeInstanceHA.yaml
+++ b/roles/ComputeInstanceHA.yaml
@@ -6,9 +6,12 @@
     Compute Instance HA Node role to be used with -e environments/compute-instanceha.yaml
   CountDefault: 1
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
   HostnameFormatDefault: '%stackname%-novacomputeiha-%index%'
   RoleParametersDefault:
     TunedProfileName: "virtual-host"
diff --git a/roles/ComputeLiquidio.yaml b/roles/ComputeLiquidio.yaml
index d79118bc3a..2be1e3351c 100644
--- a/roles/ComputeLiquidio.yaml
+++ b/roles/ComputeLiquidio.yaml
@@ -6,9 +6,12 @@
     Compute Node with Cavium Liquidio smart NIC
   CountDefault: 1
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
   networks_skip_config:
     - Tenant
   HostnameFormatDefault: '%stackname%-lionovacompute-%index%'
diff --git a/roles/ComputeOvsDpdk.yaml b/roles/ComputeOvsDpdk.yaml
index f1892cfca6..471fb509a5 100644
--- a/roles/ComputeOvsDpdk.yaml
+++ b/roles/ComputeOvsDpdk.yaml
@@ -6,9 +6,12 @@
     Compute OvS DPDK Role
   CountDefault: 1
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
   deprecated_nic_config_name: 'compute-dpdk.yaml'
   RoleParametersDefault:
     VhostuserSocketGroup: "hugetlbfs"
diff --git a/roles/ComputeOvsDpdkRT.yaml b/roles/ComputeOvsDpdkRT.yaml
index 5d1667bd72..7af4604fd3 100644
--- a/roles/ComputeOvsDpdkRT.yaml
+++ b/roles/ComputeOvsDpdkRT.yaml
@@ -6,9 +6,12 @@
     Compute OvS DPDK RealTime Role
   CountDefault: 1
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
   ImageDefault: overcloud-realtime-compute
   RoleParametersDefault:
     VhostuserSocketGroup: "hugetlbfs"
diff --git a/roles/ComputePPC64LE.yaml b/roles/ComputePPC64LE.yaml
index 378678b0e5..989cc2a629 100644
--- a/roles/ComputePPC64LE.yaml
+++ b/roles/ComputePPC64LE.yaml
@@ -6,9 +6,12 @@
     Basic Compute Node role for ppc64le servers
   CountDefault: 0
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
   HostnameFormatDefault: '%stackname%-novacomputeppc64le-%index%'
   ImageDefault: ppc64le-overcloud-full
   RoleParametersDefault:
diff --git a/roles/ComputeRealTime.yaml b/roles/ComputeRealTime.yaml
index af53314198..335a2cb32b 100644
--- a/roles/ComputeRealTime.yaml
+++ b/roles/ComputeRealTime.yaml
@@ -9,9 +9,12 @@
     accordingly to the hardware of the real-time compute nodes.
   CountDefault: 1
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
   HostnameFormatDefault: '%stackname%-computerealtime-%index%'
   ImageDefault: overcloud-realtime-compute
   RoleParametersDefault:
diff --git a/roles/ComputeSriov.yaml b/roles/ComputeSriov.yaml
index f2a0f8aec7..6cace0ce5c 100644
--- a/roles/ComputeSriov.yaml
+++ b/roles/ComputeSriov.yaml
@@ -6,9 +6,12 @@
     Compute SR-IOV Role
   CountDefault: 1
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
   RoleParametersDefault:
     TunedProfileName: "cpu-partitioning"
   ServicesDefault:
diff --git a/roles/ComputeSriovRT.yaml b/roles/ComputeSriovRT.yaml
index 2975a93b34..70c9b7820d 100644
--- a/roles/ComputeSriovRT.yaml
+++ b/roles/ComputeSriovRT.yaml
@@ -6,9 +6,12 @@
     Compute SR-IOV RealTime Role
   CountDefault: 1
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
   ImageDefault: overcloud-realtime-compute
   RoleParametersDefault:
     TunedProfileName: "realtime-virtual-host"
diff --git a/roles/Controller.yaml b/roles/Controller.yaml
index d13e5e42d8..1d54caff04 100644
--- a/roles/Controller.yaml
+++ b/roles/Controller.yaml
@@ -10,11 +10,16 @@
     - primary
     - controller
   networks:
-    - External
-    - InternalApi
-    - Storage
-    - StorageMgmt
-    - Tenant
+    External:
+      subnet: external_subnet
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
+    Tenant:
+      subnet: tenant_subnet
   # For systems with both IPv4 and IPv6, you may specify a gateway network for
   # each, such as ['ControlPlane', 'External']
   default_route_networks: ['External']
diff --git a/roles/ControllerAllNovaStandalone.yaml b/roles/ControllerAllNovaStandalone.yaml
index 6b66f9318a..ed81443c49 100644
--- a/roles/ControllerAllNovaStandalone.yaml
+++ b/roles/ControllerAllNovaStandalone.yaml
@@ -10,11 +10,16 @@
     - primary
     - controller
   networks:
-    - External
-    - InternalApi
-    - Storage
-    - StorageMgmt
-    - Tenant
+    External:
+      subnet: external_subnet
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
+    Tenant:
+      subnet: tenant_subnet
   default_route_networks: ['External']
   HostnameFormatDefault: '%stackname%-controller-%index%'
   ServicesDefault:
diff --git a/roles/ControllerNoCeph.yaml b/roles/ControllerNoCeph.yaml
index 5a8028edf7..7f8cd28ccd 100644
--- a/roles/ControllerNoCeph.yaml
+++ b/roles/ControllerNoCeph.yaml
@@ -10,11 +10,16 @@
     - primary
     - controller
   networks:
-    - External
-    - InternalApi
-    - Storage
-    - StorageMgmt
-    - Tenant
+    External:
+      subnet: external_subnet
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
+    Tenant:
+      subnet: tenant_subnet
   default_route_networks: ['External']
   HostnameFormatDefault: '%stackname%-controller-no-ceph-%index%'
   # Deprecated & backward-compatible values (FIXME: Make parameters consistent)
diff --git a/roles/ControllerNovaStandalone.yaml b/roles/ControllerNovaStandalone.yaml
index 5e43152039..ab8ff2cb74 100644
--- a/roles/ControllerNovaStandalone.yaml
+++ b/roles/ControllerNovaStandalone.yaml
@@ -9,11 +9,16 @@
     - primary
     - controller
   networks:
-    - External
-    - InternalApi
-    - Storage
-    - StorageMgmt
-    - Tenant
+    External:
+      subnet: external_subnet
+    InternalApi:
+      subnet: interanl_api_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
+    Tenant:
+      subnet: tenant_subnet
   default_route_networks: ['External']
   HostnameFormatDefault: '%stackname%-controller-%index%'
   ServicesDefault:
diff --git a/roles/ControllerOpenstack.yaml b/roles/ControllerOpenstack.yaml
index 1d2be05de3..84a29aa783 100644
--- a/roles/ControllerOpenstack.yaml
+++ b/roles/ControllerOpenstack.yaml
@@ -10,11 +10,16 @@
     - primary
     - controller
   networks:
-    - External
-    - InternalApi
-    - Storage
-    - StorageMgmt
-    - Tenant
+    External:
+      subnet: external_subnet
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
+    Tenant:
+      subnet: tenant_subnet
   default_route_networks: ['External']
   HostnameFormatDefault: '%stackname%-controller-%index%'
   ServicesDefault:
diff --git a/roles/ControllerStorageNfs.yaml b/roles/ControllerStorageNfs.yaml
index 1d1cd93a17..fe836515fe 100644
--- a/roles/ControllerStorageNfs.yaml
+++ b/roles/ControllerStorageNfs.yaml
@@ -13,12 +13,18 @@
     - primary
     - controller
   networks:
-    - External
-    - InternalApi
-    - Storage
-    - StorageMgmt
-    - StorageNFS
-    - Tenant
+    External:
+      subnet: external_subnet
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
+    StorageNFS:
+      subnet: storage_nfs_subnet
+    Tenant:
+      subnet: tenant_subnet
   HostnameFormatDefault: '%stackname%-controller-%index%'
   # Deprecated & backward-compatible values (FIXME: Make parameters consistent)
   # Set uses_deprecated_params to True if any deprecated params are used.
diff --git a/roles/Database.yaml b/roles/Database.yaml
index 7b694cfc5f..92d0323aa2 100644
--- a/roles/Database.yaml
+++ b/roles/Database.yaml
@@ -5,7 +5,8 @@
   description: |
     Standalone database role with the database being managed via Pacemaker
   networks:
-    - InternalApi
+    InternalApi:
+      subnet: internal_api_subnet
   HostnameFormatDefault: '%stackname%-database-%index%'
   ServicesDefault:
     - OS::TripleO::Services::Aide
diff --git a/roles/HciCephAll.yaml b/roles/HciCephAll.yaml
index 552d652d4a..543c55134b 100644
--- a/roles/HciCephAll.yaml
+++ b/roles/HciCephAll.yaml
@@ -5,10 +5,14 @@
   description: |
     HCI Full Stack Role (OSD + MON + Nova + RGW + MDS + MGR + RBD Mirroring)
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
-    - StorageMgmt
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
   HostnameFormatDefault: '%stackname%-hci-ceph-all-%index%'
   RoleParametersDefault:
     TunedProfileName: "throughput-performance"
diff --git a/roles/HciCephFile.yaml b/roles/HciCephFile.yaml
index 3fa6a389b1..f23a70cf8d 100644
--- a/roles/HciCephFile.yaml
+++ b/roles/HciCephFile.yaml
@@ -5,10 +5,14 @@
   description: |
     HCI Scale-out File Role (OSD + Nova + MDS)
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
-    - StorageMgmt
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
   HostnameFormatDefault: '%stackname%-hci-ceph-file-%index%'
   RoleParametersDefault:
     TunedProfileName: "throughput-performance"
diff --git a/roles/HciCephMon.yaml b/roles/HciCephMon.yaml
index c3290c0a66..a25fa9912d 100644
--- a/roles/HciCephMon.yaml
+++ b/roles/HciCephMon.yaml
@@ -5,10 +5,14 @@
   description: |
     HCI Scale-out Block Full Role (OSD + MON + MGR + Nova)
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
-    - StorageMgmt
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
   HostnameFormatDefault: '%stackname%-hci-ceph-mon-%index%'
   RoleParametersDefault:
     TunedProfileName: "throughput-performance"
diff --git a/roles/HciCephObject.yaml b/roles/HciCephObject.yaml
index ccf57013e2..5420bc0107 100644
--- a/roles/HciCephObject.yaml
+++ b/roles/HciCephObject.yaml
@@ -5,10 +5,14 @@
   description: |
     HCI Scale-out Object Role (OSD + Nova + RGW)
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
-    - StorageMgmt
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
   HostnameFormatDefault: '%stackname%-hci-ceph-object-%index%'
   RoleParametersDefault:
     TunedProfileName: "throughput-performance"
diff --git a/roles/IronicConductor.yaml b/roles/IronicConductor.yaml
index 90ba297fd0..46992cf2e7 100644
--- a/roles/IronicConductor.yaml
+++ b/roles/IronicConductor.yaml
@@ -5,8 +5,10 @@
   description: |
     Ironic Conductor node role
   networks:
-    - InternalApi
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
   HostnameFormatDefault: '%stackname%-ironic-%index%'
   ServicesDefault:
     - OS::TripleO::Services::Aide
diff --git a/roles/Messaging.yaml b/roles/Messaging.yaml
index f4f0794389..a5207d9132 100644
--- a/roles/Messaging.yaml
+++ b/roles/Messaging.yaml
@@ -5,7 +5,8 @@
   description: |
     Standalone messaging role with backends being managed via Pacemaker
   networks:
-    - InternalApi
+    InternalApi:
+      subnet: internal_api_subnet
   HostnameFormatDefault: '%stackname%-messaging-%index%'
   ServicesDefault:
     - OS::TripleO::Services::Aide
diff --git a/roles/Networker.yaml b/roles/Networker.yaml
index b73d6cde70..33ead995e4 100644
--- a/roles/Networker.yaml
+++ b/roles/Networker.yaml
@@ -5,8 +5,10 @@
   description: |
     Standalone networking role to run Neutron agents on their own.
   networks:
-    - InternalApi
-    - Tenant
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
   HostnameFormatDefault: '%stackname%-networker-%index%'
   ServicesDefault:
     - OS::TripleO::Services::Aide
diff --git a/roles/Novacontrol.yaml b/roles/Novacontrol.yaml
index 5cbf623e2e..5beaf2636f 100644
--- a/roles/Novacontrol.yaml
+++ b/roles/Novacontrol.yaml
@@ -5,8 +5,10 @@
   description: |
     Standalone nova-control role to run Nova control agents on their own.
   networks:
-    - InternalApi
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
   HostnameFormatDefault: '%stackname%-novacontrol-%index%'
   ServicesDefault:
     - OS::TripleO::Services::AuditD
diff --git a/roles/ObjectStorage.yaml b/roles/ObjectStorage.yaml
index 055a4d9e8b..f75da525b1 100644
--- a/roles/ObjectStorage.yaml
+++ b/roles/ObjectStorage.yaml
@@ -5,9 +5,12 @@
   description: |
     Swift Object Storage node role
   networks:
-    - InternalApi
-    - Storage
-    - StorageMgmt
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
   # Deprecated & backward-compatible values (FIXME: Make parameters consistent)
   # Set uses_deprecated_params to True if any deprecated params are used.
   uses_deprecated_params: True
diff --git a/roles/OpenShiftAllInOne.yaml b/roles/OpenShiftAllInOne.yaml
index 69520b1765..f3e4919a51 100644
--- a/roles/OpenShiftAllInOne.yaml
+++ b/roles/OpenShiftAllInOne.yaml
@@ -13,9 +13,12 @@
     - controller
     - openshift
   networks:
-    - External
-    - InternalApi
-    - Storage
+    External:
+      subnet: external_subnet
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
   # For systems with both IPv4 and IPv6, you may specify a gateway network for
   # each, such as ['ControlPlane', 'External']
   default_route_networks: ['External']
diff --git a/roles/OpenShiftInfra.yaml b/roles/OpenShiftInfra.yaml
index 3781fe450f..6569aa6c35 100644
--- a/roles/OpenShiftInfra.yaml
+++ b/roles/OpenShiftInfra.yaml
@@ -11,8 +11,10 @@
   tags:
     - openshift
   networks:
-    - InternalApi
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
   # For systems with both IPv4 and IPv6, you may specify a gateway network for
   # each, such as ['ControlPlane', 'External']
   default_route_networks: ['ControlPlane']
diff --git a/roles/OpenShiftMaster.yaml b/roles/OpenShiftMaster.yaml
index c16302aa79..a657ec87d3 100644
--- a/roles/OpenShiftMaster.yaml
+++ b/roles/OpenShiftMaster.yaml
@@ -13,9 +13,12 @@
     - controller
     - openshift
   networks:
-    - External
-    - InternalApi
-    - Storage
+    External:
+      subnet: external_subnet
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
   # For systems with both IPv4 and IPv6, you may specify a gateway network for
   # each, such as ['ControlPlane', 'External']
   default_route_networks: ['External']
diff --git a/roles/OpenShiftWorker.yaml b/roles/OpenShiftWorker.yaml
index 3c4bd2fb1b..572010d188 100644
--- a/roles/OpenShiftWorker.yaml
+++ b/roles/OpenShiftWorker.yaml
@@ -11,8 +11,10 @@
   tags:
     - openshift
   networks:
-    - InternalApi
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
   # For systems with both IPv4 and IPv6, you may specify a gateway network for
   # each, such as ['ControlPlane', 'External']
   default_route_networks: ['ControlPlane']
diff --git a/roles/Standalone.yaml b/roles/Standalone.yaml
index cbfcc1ff27..9b70024d8b 100644
--- a/roles/Standalone.yaml
+++ b/roles/Standalone.yaml
@@ -12,11 +12,16 @@
     - primary
     - controller
   networks:
-    - External
-    - InternalApi
-    - Storage
-    - StorageMgmt
-    - Tenant
+    External:
+      subnet: external_subnet
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
+    Tenant:
+      subnet: tenant_subnet
   disable_constraints: True
   ServicesDefault:
     - OS::TripleO::Services::Aide
diff --git a/roles/Telemetry.yaml b/roles/Telemetry.yaml
index ca7ceb3e2e..fd2019ad23 100644
--- a/roles/Telemetry.yaml
+++ b/roles/Telemetry.yaml
@@ -5,8 +5,10 @@
   description: |
     Telemetry role that has all the telemetry services.
   networks:
-    - InternalApi
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
   HostnameFormatDefault: '%stackname%-telemetry-%index%'
   ServicesDefault:
     - OS::TripleO::Services::Aide
diff --git a/roles/Undercloud.yaml b/roles/Undercloud.yaml
index aa11222d64..f3f93152fb 100644
--- a/roles/Undercloud.yaml
+++ b/roles/Undercloud.yaml
@@ -11,11 +11,16 @@
     - primary
     - controller
   networks:
-    - External
-    - InternalApi
-    - Storage
-    - StorageMgmt
-    - Tenant
+    External:
+      subnet: external_subnet
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
+    Tenant:
+      subnet: tenant_subnet
   ServicesDefault:
     - OS::TripleO::Services::Aide
     - OS::TripleO::Services::AodhApi
diff --git a/roles_data.yaml b/roles_data.yaml
index b214b2e099..6a7a741c24 100644
--- a/roles_data.yaml
+++ b/roles_data.yaml
@@ -13,11 +13,16 @@
     - primary
     - controller
   networks:
-    - External
-    - InternalApi
-    - Storage
-    - StorageMgmt
-    - Tenant
+    External:
+      subnet: external_subnet
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
+    Tenant:
+      subnet: tenant_subnet
   # For systems with both IPv4 and IPv6, you may specify a gateway network for
   # each, such as ['ControlPlane', 'External']
   default_route_networks: ['External']
@@ -193,9 +198,12 @@
     Basic Compute Node role
   CountDefault: 1
   networks:
-    - InternalApi
-    - Tenant
-    - Storage
+    InternalApi:
+      subnet: internal_api_subnet
+    Tenant:
+      subnet: tenant_subnet
+    Storage:
+      subnet: storage_subnet
   HostnameFormatDefault: '%stackname%-novacompute-%index%'
   RoleParametersDefault:
     TunedProfileName: "virtual-host"
@@ -265,9 +273,12 @@
   description: |
     Cinder Block Storage node role
   networks:
-    - InternalApi
-    - Storage
-    - StorageMgmt
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
   uses_deprecated_params: False
   deprecated_nic_config_name: 'cinder-storage.yaml'
   ServicesDefault:
@@ -307,9 +318,12 @@
   description: |
     Swift Object Storage node role
   networks:
-    - InternalApi
-    - Storage
-    - StorageMgmt
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
   # Deprecated & backward-compatible values (FIXME: Make parameters consistent)
   # Set uses_deprecated_params to True if any deprecated params are used.
   uses_deprecated_params: True
@@ -355,8 +369,10 @@
   description: |
     Ceph OSD Storage node role
   networks:
-    - Storage
-    - StorageMgmt
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
   uses_deprecated_params: False
   deprecated_nic_config_name: 'ceph-storage.yaml'
   ServicesDefault:
diff --git a/roles_data_undercloud.yaml b/roles_data_undercloud.yaml
index 0b2b85f183..d2289d58ae 100644
--- a/roles_data_undercloud.yaml
+++ b/roles_data_undercloud.yaml
@@ -14,11 +14,16 @@
     - primary
     - controller
   networks:
-    - External
-    - InternalApi
-    - Storage
-    - StorageMgmt
-    - Tenant
+    External:
+      subnet: external_subnet
+    InternalApi:
+      subnet: internal_api_subnet
+    Storage:
+      subnet: storage_subnet
+    StorageMgmt:
+      subnet: storage_mgmt_subnet
+    Tenant:
+      subnet: tenant_subnet
   ServicesDefault:
     - OS::TripleO::Services::Aide
     - OS::TripleO::Services::AodhApi
diff --git a/tools/yaml-validate.py b/tools/yaml-validate.py
index d0fbce675a..a8690a8e13 100755
--- a/tools/yaml-validate.py
+++ b/tools/yaml-validate.py
@@ -478,7 +478,7 @@ def validate_multiarch_compute_roles(role_filename, role_tpl):
     errors = 0
     roles_dir = os.path.dirname(role_filename)
     compute_services = set(role_tpl[0].get('ServicesDefault', []))
-    compute_networks = set(role_tpl[0].get('networks', []))
+    compute_networks = role_tpl[0].get('networks', [])
 
     for arch in ['ppc64le']:
         arch_filename = os.path.join(roles_dir,
@@ -493,7 +493,7 @@ def validate_multiarch_compute_roles(role_filename, role_tpl):
             print('ERROR problems with: %s' % (','.join(compute_services.symmetric_difference(arch_services))))
             errors = 1
 
-        arch_networks = set(arch_tpl[0].get('networks', []))
+        arch_networks = arch_tpl[0].get('networks', [])
         if compute_networks != arch_networks:
             print('ERROR networks in %s and %s do not match' %
                   (role_filename, arch_filename))