Repository: kubernetes-sigs/cluster-api-provider-azure
Pull request #1544 (2021-07-20 09:02:14)

🐛managedcluster/agentpool in non-terminal state should return error
 <!-- If this is your first PR, welcome! Please make sure you read the [contributing guidelines](../CONTRIBUTING.md). -->

 <!-- Please label this pull request according to what type of issue you are addressing (see ../CONTRIBUTING.md) -->
**What type of PR is this?**
/kind bug

**What this PR does / why we need it**:

these are errors which should trigger a requeue, there is no point not requeuing since we will never get a notification from Azure the operation is complete without polling.

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
related to https://github.com/kubernetes-sigs/cluster-api-provider-azure/pull/1488

**Special notes for your reviewer**:

_Please confirm that if this PR changes any image versions, then that's the sole change this PR makes._

**TODOs**:
<!-- Put an "X" character inside the brackets of each completed task. Some may be optional depending on the PR. -->

- [x] squashed commits
- [ ] includes documentation
- [ ] adds unit tests

**Release note**:
<!--  Write your release note:
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
2. If no release note is required, just write "NONE".
-->
```release-note
NONE
```


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **cecilerobertmichon** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @cecilerobertmichon` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fcluster-api-provider-azure).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/cluster-api-provider-azure/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["cecilerobertmichon"]} -->
CecileRobertMichon: lgtm pending the comment about constants

Patch:
diff --git a/api/v1alpha3/azureclusteridentity_conversion.go b/api/v1alpha3/azureclusteridentity_conversion.go
index ff2b30ea1..006909119 100644
--- a/api/v1alpha3/azureclusteridentity_conversion.go
+++ b/api/v1alpha3/azureclusteridentity_conversion.go
@@ -17,17 +17,12 @@ limitations under the License.
 package v1alpha3
 
 import (
-	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	apiconversion "k8s.io/apimachinery/pkg/conversion"
 	infrav1alpha4 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4"
 	utilconversion "sigs.k8s.io/cluster-api/util/conversion"
 	"sigs.k8s.io/controller-runtime/pkg/conversion"
 )
 
-const (
-	AzureClusterKind = "AzureCluster"
-)
-
 // ConvertTo converts this AzureCluster to the Hub version (v1alpha4).
 func (src *AzureClusterIdentity) ConvertTo(dstRaw conversion.Hub) error { // nolint
 	dst := dstRaw.(*infrav1alpha4.AzureClusterIdentity)
@@ -49,15 +44,6 @@ func (src *AzureClusterIdentity) ConvertTo(dstRaw conversion.Hub) error { // nol
 		dst.Spec.AllowedNamespaces.Selector = restored.Spec.AllowedNamespaces.Selector
 	}
 
-	// removing ownerReference for AzureCluster as ownerReference is not required from v1alpha4 onwards.
-	var restoredOwnerReferences []v1.OwnerReference
-	for _, ownerRef :=  range dst.OwnerReferences {
-		if ownerRef.Kind != AzureClusterKind {
-			restoredOwnerReferences = append(restoredOwnerReferences, ownerRef)
-		}
-	}
-	dst.OwnerReferences = restoredOwnerReferences
-
 	return nil
 }
 
diff --git a/api/v1alpha3/azuremachine_conversion.go b/api/v1alpha3/azuremachine_conversion.go
index 798073e81..801101994 100644
--- a/api/v1alpha3/azuremachine_conversion.go
+++ b/api/v1alpha3/azuremachine_conversion.go
@@ -45,8 +45,6 @@ func (src *AzureMachine) ConvertTo(dstRaw conversion.Hub) error { // nolint
 		}
 	}
 
-	dst.Spec.SubnetName = restored.Spec.SubnetName
-
 	return nil
 }
 
diff --git a/api/v1alpha3/azuremachinetemplate_conversion.go b/api/v1alpha3/azuremachinetemplate_conversion.go
index 54ea85cff..845e300dc 100644
--- a/api/v1alpha3/azuremachinetemplate_conversion.go
+++ b/api/v1alpha3/azuremachinetemplate_conversion.go
@@ -51,8 +51,6 @@ func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { // nol
 		dst.Spec.Template.Spec.Image.SharedGallery.SKU = restored.Spec.Template.Spec.Image.SharedGallery.SKU
 	}
 
-	dst.Spec.Template.Spec.SubnetName = restored.Spec.Template.Spec.SubnetName
-
 	return nil
 }
 
diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go
index 0fdef6699..73bd5a44d 100644
--- a/api/v1alpha3/zz_generated.conversion.go
+++ b/api/v1alpha3/zz_generated.conversion.go
@@ -863,7 +863,6 @@ func autoConvert_v1alpha4_AzureMachineSpec_To_v1alpha3_AzureMachineSpec(in *v1al
 	out.AcceleratedNetworking = (*bool)(unsafe.Pointer(in.AcceleratedNetworking))
 	out.SpotVMOptions = (*SpotVMOptions)(unsafe.Pointer(in.SpotVMOptions))
 	out.SecurityProfile = (*SecurityProfile)(unsafe.Pointer(in.SecurityProfile))
-	// WARNING: in.SubnetName requires manual conversion: does not exist in peer-type
 	return nil
 }
 
diff --git a/api/v1alpha4/azurecluster_default.go b/api/v1alpha4/azurecluster_default.go
index dad4817ca..ea2b17098 100644
--- a/api/v1alpha4/azurecluster_default.go
+++ b/api/v1alpha4/azurecluster_default.go
@@ -29,8 +29,6 @@ const (
 	DefaultControlPlaneSubnetCIDR = "10.0.0.0/16"
 	// DefaultNodeSubnetCIDR is the default Node Subnet CIDR.
 	DefaultNodeSubnetCIDR = "10.1.0.0/16"
-	// DefaultNodeSubnetCIDRPattern is the pattern that will be used to generate the default subnets CIDRs.
-	DefaultNodeSubnetCIDRPattern = "10.%d.0.0/16"
 	// DefaultAzureBastionSubnetCIDR is the default Subnet CIDR for AzureBastion.
 	DefaultAzureBastionSubnetCIDR = "10.255.255.224/27"
 	// DefaultAzureBastionSubnetName is the default Subnet Name for AzureBastion.
@@ -89,6 +87,12 @@ func (c *AzureCluster) setSubnetDefaults() {
 		c.Spec.NetworkSpec.Subnets = append(c.Spec.NetworkSpec.Subnets, cpSubnet)
 	}
 
+	nodeSubnet, err := c.Spec.NetworkSpec.GetNodeSubnet()
+	if err != nil {
+		nodeSubnet = SubnetSpec{Role: SubnetNode}
+		c.Spec.NetworkSpec.Subnets = append(c.Spec.NetworkSpec.Subnets, nodeSubnet)
+	}
+
 	if cpSubnet.Name == "" {
 		cpSubnet.Name = generateControlPlaneSubnetName(c.ObjectMeta.Name)
 	}
@@ -100,51 +104,28 @@ func (c *AzureCluster) setSubnetDefaults() {
 	}
 	setSecurityRuleDefaults(&cpSubnet.SecurityGroup)
 
-	c.Spec.NetworkSpec.UpdateControlPlaneSubnet(cpSubnet)
-
-	var nodeSubnetFound bool
-	var nodeSubnetCounter int
-	for i, subnet := range c.Spec.NetworkSpec.Subnets {
-		if subnet.Role == SubnetNode {
-			nodeSubnetCounter++
-			nodeSubnetFound = true
-			if subnet.Name == "" {
-				subnet.Name = withIndex(generateNodeSubnetName(c.ObjectMeta.Name), nodeSubnetCounter)
-			}
-			if len(subnet.CIDRBlocks) == 0 {
-				subnet.CIDRBlocks = []string{fmt.Sprintf(DefaultNodeSubnetCIDRPattern, nodeSubnetCounter)}
-			}
-			if subnet.SecurityGroup.Name == "" {
-				subnet.SecurityGroup.Name = generateNodeSecurityGroupName(c.ObjectMeta.Name)
-			}
-			setSecurityRuleDefaults(&subnet.SecurityGroup)
-			if subnet.RouteTable.Name == "" {
-				subnet.RouteTable.Name = generateNodeRouteTableName(c.ObjectMeta.Name)
-			}
-			if subnet.IsNatGatewayEnabled() {
-				if subnet.NatGateway.NatGatewayIP.Name == "" {
-					subnet.NatGateway.NatGatewayIP.Name = generateNatGatewayIPName(c.ObjectMeta.Name, subnet.Name)
-				}
-			}
-
-			c.Spec.NetworkSpec.Subnets[i] = subnet
-		}
+	if nodeSubnet.Name == "" {
+		nodeSubnet.Name = generateNodeSubnetName(c.ObjectMeta.Name)
+	}
+	if len(nodeSubnet.CIDRBlocks) == 0 {
+		nodeSubnet.CIDRBlocks = []string{DefaultNodeSubnetCIDR}
+	}
+	if nodeSubnet.SecurityGroup.Name == "" {
+		nodeSubnet.SecurityGroup.Name = generateNodeSecurityGroupName(c.ObjectMeta.Name)
+	}
+	setSecurityRuleDefaults(&nodeSubnet.SecurityGroup)
+	if nodeSubnet.RouteTable.Name == "" {
+		nodeSubnet.RouteTable.Name = generateNodeRouteTableName(c.ObjectMeta.Name)
 	}
 
-	if !nodeSubnetFound {
-		nodeSubnet := SubnetSpec{
-			Role:       SubnetNode,
-			Name:       generateNodeSubnetName(c.ObjectMeta.Name),
-			CIDRBlocks: []string{DefaultNodeSubnetCIDR},
-			SecurityGroup: SecurityGroup{
-				Name: generateNodeSecurityGroupName(c.ObjectMeta.Name),
-			},
-			RouteTable: RouteTable{
-				Name: generateNodeRouteTableName(c.ObjectMeta.Name),
-			},
+	if nodeSubnet.NatGateway.Name != "" {
+		if nodeSubnet.NatGateway.NatGatewayIP.Name == "" {
+			nodeSubnet.NatGateway.NatGatewayIP.Name = generateNatGatewayIPName(c.ObjectMeta.Name, nodeSubnet.Name)
 		}
-		c.Spec.NetworkSpec.Subnets = append(c.Spec.NetworkSpec.Subnets, nodeSubnet)
 	}
+
+	c.Spec.NetworkSpec.UpdateControlPlaneSubnet(cpSubnet)
+	c.Spec.NetworkSpec.UpdateNodeSubnet(nodeSubnet)
 }
 
 func setSecurityRuleDefaults(sg *SecurityGroup) {
@@ -201,22 +182,11 @@ func (c *AzureCluster) setNodeOutboundLBDefaults() {
 		if c.Spec.NetworkSpec.APIServerLB.Type == Internal {
 			return
 		}
-
-		var oneSubnetWithoutNatGateway bool
-		for _, subnet := range c.Spec.NetworkSpec.Subnets {
-			if subnet.Role == SubnetNode && !subnet.IsNatGatewayEnabled() {
-				oneSubnetWithoutNatGateway = true
-				break
-			}
-		}
-
-		// If we don't default the outbound LB when there are some subnets with nat gateway,
-		// and some without, those without wouldn't have outbound traffic. So taking the
-		// safer route, we configure the outbound LB in that scenario.
-		if len(c.Spec.NetworkSpec.Subnets) > 0 && !oneSubnetWithoutNatGateway {
+		nodeSubnet, err := c.Spec.NetworkSpec.GetNodeSubnet()
+		// Only one outbound mechanism can be defined, so if Nat Gateway is defined, we don't default the LB.
+		if err == nil && nodeSubnet.NatGateway.Name != "" {
 			return
 		}
-
 		c.Spec.NetworkSpec.NodeOutboundLB = &LoadBalancerSpec{}
 	}
 
diff --git a/api/v1alpha4/azurecluster_validation.go b/api/v1alpha4/azurecluster_validation.go
index 0d26f67cd..3fc2cbf2e 100644
--- a/api/v1alpha4/azurecluster_validation.go
+++ b/api/v1alpha4/azurecluster_validation.go
@@ -127,21 +127,15 @@ func validateNetworkSpec(networkSpec NetworkSpec, old NetworkSpec, fldPath *fiel
 	if err != nil {
 		allErrs = append(allErrs, field.Invalid(fldPath.Child("subnets"), networkSpec.Subnets, "ControlPlaneSubnet invalid"))
 	}
-
+	nodeSubnet, err := networkSpec.GetNodeSubnet()
+	if err != nil {
+		allErrs = append(allErrs, field.Invalid(fldPath.Child("subnets"), networkSpec.Subnets, "NodeSubnet invalid"))
+	}
 	cidrBlocks = controlPlaneSubnet.CIDRBlocks
 
 	allErrs = append(allErrs, validateAPIServerLB(networkSpec.APIServerLB, old.APIServerLB, cidrBlocks, fldPath.Child("apiServerLB"))...)
 
-	var oneSubnetWithoutNatGateway bool
-	for _, subnet := range networkSpec.Subnets {
-		if subnet.Role == SubnetNode && !subnet.IsNatGatewayEnabled() {
-			oneSubnetWithoutNatGateway = true
-			break
-		}
-	}
-	if oneSubnetWithoutNatGateway {
-		allErrs = append(allErrs, validateNodeOutboundLB(networkSpec.NodeOutboundLB, old.NodeOutboundLB, networkSpec.APIServerLB, fldPath.Child("nodeOutboundLB"))...)
-	}
+	allErrs = append(allErrs, validateNodeOutboundLB(networkSpec.NodeOutboundLB, old.NodeOutboundLB, networkSpec.APIServerLB, nodeSubnet, fldPath.Child("nodeOutboundLB"))...)
 
 	allErrs = append(allErrs, validateControlPlaneOutboundLB(networkSpec.ControlPlaneOutboundLB, networkSpec.APIServerLB, fldPath.Child("controlPlaneOutboundLB"))...)
 
@@ -361,7 +355,7 @@ func validateAPIServerLB(lb LoadBalancerSpec, old LoadBalancerSpec, cidrs []stri
 	return allErrs
 }
 
-func validateNodeOutboundLB(lb *LoadBalancerSpec, old *LoadBalancerSpec, apiserverLB LoadBalancerSpec, fldPath *field.Path) field.ErrorList {
+func validateNodeOutboundLB(lb *LoadBalancerSpec, old *LoadBalancerSpec, apiserverLB LoadBalancerSpec, nodeSubnet SubnetSpec, fldPath *field.Path) field.ErrorList {
 	var allErrs field.ErrorList
 
 	// LB can be nil when disabled for private clusters.
@@ -369,6 +363,11 @@ func validateNodeOutboundLB(lb *LoadBalancerSpec, old *LoadBalancerSpec, apiserv
 		return allErrs
 	}
 
+	// no need to validate LB when using nat gateway.
+	if lb == nil && nodeSubnet.NatGateway.Name != "" {
+		return allErrs
+	}
+
 	if lb == nil {
 		allErrs = append(allErrs, field.Required(fldPath, "Node outbound load balancer cannot be nil for public clusters."))
 		return allErrs
diff --git a/api/v1alpha4/azuremachine_types.go b/api/v1alpha4/azuremachine_types.go
index 5a093044c..95bf551fe 100644
--- a/api/v1alpha4/azuremachine_types.go
+++ b/api/v1alpha4/azuremachine_types.go
@@ -108,10 +108,6 @@ type AzureMachineSpec struct {
 	// SecurityProfile specifies the Security profile settings for a virtual machine.
 	// +optional
 	SecurityProfile *SecurityProfile `json:"securityProfile,omitempty"`
-
-	// SubnetName selects the Subnet where the VM will be placed
-	// +optional
-	SubnetName string `json:"subnetName,omitempty"`
 }
 
 // SpotVMOptions defines the options relevant to running the Machine on Spot VMs.
diff --git a/api/v1alpha4/types.go b/api/v1alpha4/types.go
index fce36cdf3..c18c5029c 100644
--- a/api/v1alpha4/types.go
+++ b/api/v1alpha4/types.go
@@ -245,6 +245,8 @@ const (
 	Succeeded ProvisioningState = "Succeeded"
 	// Updating ...
 	Updating ProvisioningState = "Updating"
+	// Canceled represents an action which was initiated but terminated by the user before completion.
+	Canceled ProvisioningState = "Canceled"
 	// Deleted represents a deleted VM
 	// NOTE: This state is specific to capz, and does not have corresponding mapping in Azure API (https://docs.microsoft.com/en-us/azure/virtual-machines/states-billing#provisioning-states)
 	Deleted ProvisioningState = "Deleted"
@@ -515,6 +517,16 @@ func (n *NetworkSpec) UpdateControlPlaneSubnet(subnet SubnetSpec) {
 	}
 }
 
+// GetNodeSubnet returns the cluster node subnet.
+func (n *NetworkSpec) GetNodeSubnet() (SubnetSpec, error) {
+	for _, sn := range n.Subnets {
+		if sn.Role == SubnetNode {
+			return sn, nil
+		}
+	}
+	return SubnetSpec{}, errors.Errorf("no subnet found with role %s", SubnetNode)
+}
+
 // UpdateNodeSubnet updates the cluster node subnet.
 func (n *NetworkSpec) UpdateNodeSubnet(subnet SubnetSpec) {
 	for i, sn := range n.Subnets {
@@ -524,11 +536,6 @@ func (n *NetworkSpec) UpdateNodeSubnet(subnet SubnetSpec) {
 	}
 }
 
-// IsNatGatewayEnabled returns whether or not a Nat Gateway is enabled on the subnet.
-func (s SubnetSpec) IsNatGatewayEnabled() bool {
-	return s.NatGateway.Name != ""
-}
-
 // SecurityProfile specifies the Security profile settings for a
 // virtual machine or virtual machine scale set.
 type SecurityProfile struct {
diff --git a/azure/interfaces.go b/azure/interfaces.go
index 27b3f0a09..8a1640390 100644
--- a/azure/interfaces.go
+++ b/azure/interfaces.go
@@ -62,13 +62,13 @@ type Authorizer interface {
 type NetworkDescriber interface {
 	Vnet() *infrav1.VnetSpec
 	IsVnetManaged() bool
+	NodeSubnet() infrav1.SubnetSpec
 	ControlPlaneSubnet() infrav1.SubnetSpec
-	Subnets() infrav1.Subnets
-	Subnet(string) infrav1.SubnetSpec
-	NodeSubnets() []infrav1.SubnetSpec
 	SetSubnet(infrav1.SubnetSpec)
 	IsIPv6Enabled() bool
+	NodeRouteTable() infrav1.RouteTable
 	ControlPlaneRouteTable() infrav1.RouteTable
+	NodeNatGateway() infrav1.NatGateway
 	APIServerLBName() string
 	APIServerLBPoolName(string) string
 	IsAPIServerPrivate() bool
diff --git a/azure/mocks/service_mock.go b/azure/mocks/service_mock.go
index f5bd6ffe8..44952552f 100644
--- a/azure/mocks/service_mock.go
+++ b/azure/mocks/service_mock.go
@@ -467,18 +467,46 @@ func (mr *MockNetworkDescriberMockRecorder) IsVnetManaged() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsVnetManaged", reflect.TypeOf((*MockNetworkDescriber)(nil).IsVnetManaged))
 }
 
-// NodeSubnets mocks base method.
-func (m *MockNetworkDescriber) NodeSubnets() []v1alpha4.SubnetSpec {
+// NodeNatGateway mocks base method.
+func (m *MockNetworkDescriber) NodeNatGateway() v1alpha4.NatGateway {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "NodeSubnets")
-	ret0, _ := ret[0].([]v1alpha4.SubnetSpec)
+	ret := m.ctrl.Call(m, "NodeNatGateway")
+	ret0, _ := ret[0].(v1alpha4.NatGateway)
 	return ret0
 }
 
-// NodeSubnets indicates an expected call of NodeSubnets.
-func (mr *MockNetworkDescriberMockRecorder) NodeSubnets() *gomock.Call {
+// NodeNatGateway indicates an expected call of NodeNatGateway.
+func (mr *MockNetworkDescriberMockRecorder) NodeNatGateway() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnets", reflect.TypeOf((*MockNetworkDescriber)(nil).NodeSubnets))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeNatGateway", reflect.TypeOf((*MockNetworkDescriber)(nil).NodeNatGateway))
+}
+
+// NodeRouteTable mocks base method.
+func (m *MockNetworkDescriber) NodeRouteTable() v1alpha4.RouteTable {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeRouteTable")
+	ret0, _ := ret[0].(v1alpha4.RouteTable)
+	return ret0
+}
+
+// NodeRouteTable indicates an expected call of NodeRouteTable.
+func (mr *MockNetworkDescriberMockRecorder) NodeRouteTable() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeRouteTable", reflect.TypeOf((*MockNetworkDescriber)(nil).NodeRouteTable))
+}
+
+// NodeSubnet mocks base method.
+func (m *MockNetworkDescriber) NodeSubnet() v1alpha4.SubnetSpec {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeSubnet")
+	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
+	return ret0
+}
+
+// NodeSubnet indicates an expected call of NodeSubnet.
+func (mr *MockNetworkDescriberMockRecorder) NodeSubnet() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnet", reflect.TypeOf((*MockNetworkDescriber)(nil).NodeSubnet))
 }
 
 // OutboundLBName mocks base method.
@@ -521,34 +549,6 @@ func (mr *MockNetworkDescriberMockRecorder) SetSubnet(arg0 interface{}) *gomock.
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSubnet", reflect.TypeOf((*MockNetworkDescriber)(nil).SetSubnet), arg0)
 }
 
-// Subnet mocks base method.
-func (m *MockNetworkDescriber) Subnet(arg0 string) v1alpha4.SubnetSpec {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnet", arg0)
-	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
-	return ret0
-}
-
-// Subnet indicates an expected call of Subnet.
-func (mr *MockNetworkDescriberMockRecorder) Subnet(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnet", reflect.TypeOf((*MockNetworkDescriber)(nil).Subnet), arg0)
-}
-
-// Subnets mocks base method.
-func (m *MockNetworkDescriber) Subnets() v1alpha4.Subnets {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnets")
-	ret0, _ := ret[0].(v1alpha4.Subnets)
-	return ret0
-}
-
-// Subnets indicates an expected call of Subnets.
-func (mr *MockNetworkDescriberMockRecorder) Subnets() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnets", reflect.TypeOf((*MockNetworkDescriber)(nil).Subnets))
-}
-
 // Vnet mocks base method.
 func (m *MockNetworkDescriber) Vnet() *v1alpha4.VnetSpec {
 	m.ctrl.T.Helper()
@@ -1071,18 +1071,46 @@ func (mr *MockClusterScoperMockRecorder) Location() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Location", reflect.TypeOf((*MockClusterScoper)(nil).Location))
 }
 
-// NodeSubnets mocks base method.
-func (m *MockClusterScoper) NodeSubnets() []v1alpha4.SubnetSpec {
+// NodeNatGateway mocks base method.
+func (m *MockClusterScoper) NodeNatGateway() v1alpha4.NatGateway {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "NodeSubnets")
-	ret0, _ := ret[0].([]v1alpha4.SubnetSpec)
+	ret := m.ctrl.Call(m, "NodeNatGateway")
+	ret0, _ := ret[0].(v1alpha4.NatGateway)
 	return ret0
 }
 
-// NodeSubnets indicates an expected call of NodeSubnets.
-func (mr *MockClusterScoperMockRecorder) NodeSubnets() *gomock.Call {
+// NodeNatGateway indicates an expected call of NodeNatGateway.
+func (mr *MockClusterScoperMockRecorder) NodeNatGateway() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnets", reflect.TypeOf((*MockClusterScoper)(nil).NodeSubnets))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeNatGateway", reflect.TypeOf((*MockClusterScoper)(nil).NodeNatGateway))
+}
+
+// NodeRouteTable mocks base method.
+func (m *MockClusterScoper) NodeRouteTable() v1alpha4.RouteTable {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeRouteTable")
+	ret0, _ := ret[0].(v1alpha4.RouteTable)
+	return ret0
+}
+
+// NodeRouteTable indicates an expected call of NodeRouteTable.
+func (mr *MockClusterScoperMockRecorder) NodeRouteTable() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeRouteTable", reflect.TypeOf((*MockClusterScoper)(nil).NodeRouteTable))
+}
+
+// NodeSubnet mocks base method.
+func (m *MockClusterScoper) NodeSubnet() v1alpha4.SubnetSpec {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeSubnet")
+	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
+	return ret0
+}
+
+// NodeSubnet indicates an expected call of NodeSubnet.
+func (mr *MockClusterScoperMockRecorder) NodeSubnet() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnet", reflect.TypeOf((*MockClusterScoper)(nil).NodeSubnet))
 }
 
 // OutboundLBName mocks base method.
@@ -1139,34 +1167,6 @@ func (mr *MockClusterScoperMockRecorder) SetSubnet(arg0 interface{}) *gomock.Cal
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSubnet", reflect.TypeOf((*MockClusterScoper)(nil).SetSubnet), arg0)
 }
 
-// Subnet mocks base method.
-func (m *MockClusterScoper) Subnet(arg0 string) v1alpha4.SubnetSpec {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnet", arg0)
-	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
-	return ret0
-}
-
-// Subnet indicates an expected call of Subnet.
-func (mr *MockClusterScoperMockRecorder) Subnet(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnet", reflect.TypeOf((*MockClusterScoper)(nil).Subnet), arg0)
-}
-
-// Subnets mocks base method.
-func (m *MockClusterScoper) Subnets() v1alpha4.Subnets {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnets")
-	ret0, _ := ret[0].(v1alpha4.Subnets)
-	return ret0
-}
-
-// Subnets indicates an expected call of Subnets.
-func (mr *MockClusterScoperMockRecorder) Subnets() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnets", reflect.TypeOf((*MockClusterScoper)(nil).Subnets))
-}
-
 // SubscriptionID mocks base method.
 func (m *MockClusterScoper) SubscriptionID() string {
 	m.ctrl.T.Helper()
diff --git a/azure/scope/cluster.go b/azure/scope/cluster.go
index d851cbbdc..7e7038e40 100644
--- a/azure/scope/cluster.go
+++ b/azure/scope/cluster.go
@@ -139,17 +139,16 @@ func (s *ClusterScope) PublicIPSpecs() []azure.PublicIPSpec {
 		publicIPSpecs = append(publicIPSpecs, nodeOutboundIPSpecs...)
 	}
 
-	// Public IP specs for node nat gateways
+	// Public IP specs for node nat gateway
 	var nodeNatGatewayIPSpecs []azure.PublicIPSpec
-	for _, subnet := range s.NodeSubnets() {
-		if subnet.IsNatGatewayEnabled() {
-			nodeNatGatewayIPSpecs = append(nodeNatGatewayIPSpecs, azure.PublicIPSpec{
-				Name:    subnet.NatGateway.NatGatewayIP.Name,
-				DNSName: subnet.NatGateway.NatGatewayIP.DNSName,
-			})
-		}
-		publicIPSpecs = append(publicIPSpecs, nodeNatGatewayIPSpecs...)
+	nodeNatGateway := s.NodeNatGateway()
+	if nodeNatGateway.Name != "" {
+		nodeNatGatewayIPSpecs = append(nodeNatGatewayIPSpecs, azure.PublicIPSpec{
+			Name:    nodeNatGateway.NatGatewayIP.Name,
+			DNSName: nodeNatGateway.NatGatewayIP.DNSName,
+		})
 	}
+	publicIPSpecs = append(publicIPSpecs, nodeNatGatewayIPSpecs...)
 
 	if s.AzureCluster.Spec.BastionSpec.AzureBastion != nil {
 		// public IP for Azure Bastion.
@@ -212,12 +211,12 @@ func (s *ClusterScope) LBSpecs() []azure.LBSpec {
 // RouteTableSpecs returns the node route table.
 func (s *ClusterScope) RouteTableSpecs() []azure.RouteTableSpec {
 	routetables := []azure.RouteTableSpec{}
-	for _, subnet := range s.AzureCluster.Spec.NetworkSpec.Subnets {
-		if subnet.RouteTable.Name != "" {
-			routetables = append(routetables, azure.RouteTableSpec{Name: subnet.RouteTable.Name, Subnet: subnet})
-		}
+	if s.ControlPlaneRouteTable().Name != "" {
+		routetables = append(routetables, azure.RouteTableSpec{Name: s.ControlPlaneRouteTable().Name, Subnet: s.ControlPlaneSubnet()})
+	}
+	if s.NodeRouteTable().Name != "" {
+		routetables = append(routetables, azure.RouteTableSpec{Name: s.NodeRouteTable().Name, Subnet: s.NodeSubnet()})
 	}
-
 	return routetables
 }
 
@@ -226,48 +225,53 @@ func (s *ClusterScope) NatGatewaySpecs() []azure.NatGatewaySpec {
 	natGateways := []azure.NatGatewaySpec{}
 
 	// We ignore the control plane nat gateway, as we will always use a LB to enable egress on the control plane.
-	for _, subnet := range s.NodeSubnets() {
-		if subnet.IsNatGatewayEnabled() {
-			natGateways = append(natGateways, azure.NatGatewaySpec{
-				Name: subnet.NatGateway.Name,
-				NatGatewayIP: infrav1.PublicIPSpec{
-					Name: subnet.NatGateway.NatGatewayIP.Name,
-				},
-				Subnet: subnet,
-			})
-		}
+	natGateway := s.NodeNatGateway()
+	if natGateway.Name != "" {
+		natGateways = append(natGateways, azure.NatGatewaySpec{
+			Name: natGateway.Name,
+			NatGatewayIP: infrav1.PublicIPSpec{
+				Name: natGateway.NatGatewayIP.Name,
+			},
+			Subnet: s.NodeSubnet(),
+		})
 	}
-
 	return natGateways
 }
 
 // NSGSpecs returns the security group specs.
 func (s *ClusterScope) NSGSpecs() []azure.NSGSpec {
-	nsgspecs := []azure.NSGSpec{}
-	for _, subnet := range s.AzureCluster.Spec.NetworkSpec.Subnets {
-		nsgspecs = append(nsgspecs, azure.NSGSpec{
-			Name:          subnet.SecurityGroup.Name,
-			SecurityRules: subnet.SecurityGroup.SecurityRules,
-		})
+	return []azure.NSGSpec{
+		{
+			Name:          s.ControlPlaneSubnet().SecurityGroup.Name,
+			SecurityRules: s.ControlPlaneSubnet().SecurityGroup.SecurityRules,
+		},
+		{
+			Name:          s.NodeSubnet().SecurityGroup.Name,
+			SecurityRules: s.NodeSubnet().SecurityGroup.SecurityRules,
+		},
 	}
-
-	return nsgspecs
 }
 
 // SubnetSpecs returns the subnets specs.
 func (s *ClusterScope) SubnetSpecs() []azure.SubnetSpec {
-	subnetSpecs := []azure.SubnetSpec{}
-	for _, subnet := range s.AzureCluster.Spec.NetworkSpec.Subnets {
-		subnetSpec := azure.SubnetSpec{
-			Name:              subnet.Name,
-			CIDRs:             subnet.CIDRBlocks,
+	subnetSpecs := []azure.SubnetSpec{
+		{
+			Name:              s.ControlPlaneSubnet().Name,
+			CIDRs:             s.ControlPlaneSubnet().CIDRBlocks,
 			VNetName:          s.Vnet().Name,
-			SecurityGroupName: subnet.SecurityGroup.Name,
-			RouteTableName:    subnet.RouteTable.Name,
-			Role:              subnet.Role,
-			NatGatewayName:    subnet.NatGateway.Name,
-		}
-		subnetSpecs = append(subnetSpecs, subnetSpec)
+			SecurityGroupName: s.ControlPlaneSubnet().SecurityGroup.Name,
+			Role:              s.ControlPlaneSubnet().Role,
+			RouteTableName:    s.ControlPlaneRouteTable().Name,
+		},
+		{
+			Name:              s.NodeSubnet().Name,
+			CIDRs:             s.NodeSubnet().CIDRBlocks,
+			VNetName:          s.Vnet().Name,
+			SecurityGroupName: s.NodeSubnet().SecurityGroup.Name,
+			Role:              s.NodeSubnet().Role,
+			RouteTableName:    s.NodeRouteTable().Name,
+			NatGatewayName:    s.NodeNatGateway().Name,
+		},
 	}
 
 	if s.AzureCluster.Spec.BastionSpec.AzureBastion != nil {
@@ -360,33 +364,16 @@ func (s *ClusterScope) ControlPlaneSubnet() infrav1.SubnetSpec {
 	return subnet
 }
 
-// NodeSubnets returns the subnets with the node role.
-func (s *ClusterScope) NodeSubnets() []infrav1.SubnetSpec {
-	subnets := []infrav1.SubnetSpec{}
-	for _, subnet := range s.AzureCluster.Spec.NetworkSpec.Subnets {
-		if subnet.Role == infrav1.SubnetNode {
-			subnets = append(subnets, subnet)
-		}
-	}
-
-	return subnets
-}
-
-// Subnet returns the subnet with the provided name.
-func (s *ClusterScope) Subnet(name string) infrav1.SubnetSpec {
-	for _, sn := range s.AzureCluster.Spec.NetworkSpec.Subnets {
-		if sn.Name == name {
-			return sn
-		}
-	}
-
-	return infrav1.SubnetSpec{}
+// NodeSubnet returns the cluster node subnet.
+func (s *ClusterScope) NodeSubnet() infrav1.SubnetSpec {
+	subnet, _ := s.AzureCluster.Spec.NetworkSpec.GetNodeSubnet()
+	return subnet
 }
 
-// SetSubnet sets the subnet spec for the subnet with the same name.
+// SetSubnet sets the subnet spec for the subnet with the same role.
 func (s *ClusterScope) SetSubnet(subnetSpec infrav1.SubnetSpec) {
 	for i, sn := range s.AzureCluster.Spec.NetworkSpec.Subnets {
-		if sn.Name == subnetSpec.Name {
+		if sn.Role == subnetSpec.Role {
 			s.AzureCluster.Spec.NetworkSpec.Subnets[i] = subnetSpec
 			return
 		}
@@ -399,6 +386,25 @@ func (s *ClusterScope) ControlPlaneRouteTable() infrav1.RouteTable {
 	return subnet.RouteTable
 }
 
+// NodeRouteTable returns the cluster node routetable.
+func (s *ClusterScope) NodeRouteTable() infrav1.RouteTable {
+	subnet, _ := s.AzureCluster.Spec.NetworkSpec.GetNodeSubnet()
+	return subnet.RouteTable
+}
+
+// NodeNatGateway returns the cluster node nat gateway.
+func (s *ClusterScope) NodeNatGateway() infrav1.NatGateway {
+	subnet, _ := s.AzureCluster.Spec.NetworkSpec.GetNodeSubnet()
+	return subnet.NatGateway
+}
+
+// SetNodeNatGatewayID updates the node subnet nat gateway ID.
+func (s *ClusterScope) SetNodeNatGatewayID(id string) {
+	nodeSubnet := s.NodeSubnet()
+	nodeSubnet.NatGateway.ID = id
+	s.SetSubnet(nodeSubnet)
+}
+
 // APIServerLB returns the cluster API Server load balancer.
 func (s *ClusterScope) APIServerLB() *infrav1.LoadBalancerSpec {
 	return &s.AzureCluster.Spec.NetworkSpec.APIServerLB
diff --git a/azure/scope/machine.go b/azure/scope/machine.go
index 8f71d522d..fb6e11e1e 100644
--- a/azure/scope/machine.go
+++ b/azure/scope/machine.go
@@ -151,7 +151,7 @@ func (m *MachineScope) NICSpecs() []azure.NICSpec {
 		MachineName:           m.Name(),
 		VNetName:              m.Vnet().Name,
 		VNetResourceGroup:     m.Vnet().ResourceGroup,
-		SubnetName:            m.AzureMachine.Spec.SubnetName,
+		SubnetName:            m.Subnet().Name,
 		VMSize:                m.AzureMachine.Spec.VMSize,
 		AcceleratedNetworking: m.AzureMachine.Spec.AcceleratedNetworking,
 		IPv6Enabled:           m.IsIPv6Enabled(),
@@ -168,9 +168,8 @@ func (m *MachineScope) NICSpecs() []azure.NICSpec {
 			spec.PublicLBAddressPoolName = m.APIServerLBPoolName(m.APIServerLBName())
 		}
 	}
-
-	// If Nat Gateway is not enabled, then the NIC needs to reference the LB to get outbound traffic.
-	if m.Role() == infrav1.Node && !m.Subnet().IsNatGatewayEnabled() {
+	// If Nat Gateway is not specified, then the NIC needs to reference the LB to get outbound traffic.
+	if m.Role() == infrav1.Node && m.NodeNatGateway().Name == "" {
 		spec.PublicLBName = m.OutboundLBName(m.Role())
 		spec.PublicLBAddressPoolName = m.OutboundPoolName(m.OutboundLBName(m.Role()))
 	}
@@ -181,7 +180,7 @@ func (m *MachineScope) NICSpecs() []azure.NICSpec {
 			MachineName:           m.Name(),
 			VNetName:              m.Vnet().Name,
 			VNetResourceGroup:     m.Vnet().ResourceGroup,
-			SubnetName:            m.AzureMachine.Spec.SubnetName,
+			SubnetName:            m.Subnet().Name,
 			PublicIPName:          azure.GenerateNodePublicIPName(m.Name()),
 			VMSize:                m.AzureMachine.Spec.VMSize,
 			AcceleratedNetworking: m.AzureMachine.Spec.AcceleratedNetworking,
@@ -193,9 +192,8 @@ func (m *MachineScope) NICSpecs() []azure.NICSpec {
 
 // NICNames returns the NIC names.
 func (m *MachineScope) NICNames() []string {
-	nicspecs := m.NICSpecs()
-	nicNames := make([]string, len(nicspecs))
-	for i, nic := range nicspecs {
+	nicNames := make([]string, len(m.NICSpecs()))
+	for i, nic := range m.NICSpecs() {
 		nicNames[i] = nic.Name
 	}
 	return nicNames
@@ -247,15 +245,12 @@ func (m *MachineScope) VMExtensionSpecs() []azure.VMExtensionSpec {
 	return []azure.VMExtensionSpec{}
 }
 
-// Subnet returns the machine's subnet.
+// Subnet returns the machine's subnet based on its role.
 func (m *MachineScope) Subnet() infrav1.SubnetSpec {
-	for _, subnet := range m.Subnets() {
-		if subnet.Name == m.AzureMachine.Spec.SubnetName {
-			return subnet
-		}
+	if m.IsControlPlane() {
+		return m.ControlPlaneSubnet()
 	}
-
-	return infrav1.SubnetSpec{}
+	return m.NodeSubnet()
 }
 
 // AvailabilityZone returns the AzureMachine Availability Zone.
@@ -537,27 +532,3 @@ func (m *MachineScope) GetVMImage() (*infrav1.Image, error) {
 	m.Info("No image specified for machine, using default Linux Image", "machine", m.AzureMachine.GetName())
 	return azure.GetDefaultUbuntuImage(to.String(m.Machine.Spec.Version))
 }
-
-// SetSubnetName defaults the AzureMachine subnet name to the name of one the subnets with the machine role when there is only one of them.
-// Note: this logic exists only for purposes of ensuring backwards compatibility for old clusters created without the `subnetName` field being
-// set, and should be removed in the future when this field is no longer optional.
-func (m *MachineScope) SetSubnetName() error {
-	if m.AzureMachine.Spec.SubnetName == "" {
-		subnetName := ""
-		subnets := m.Subnets()
-		var subnetCount int
-		for _, subnet := range subnets {
-			if string(subnet.Role) == m.Role() {
-				subnetCount++
-				subnetName = subnet.Name
-			}
-		}
-		if subnetCount == 0 || subnetCount > 1 || subnetName == "" {
-			return errors.New("a subnet name must be specified when no subnets are specified or more than 1 subnet of the same role exist")
-		}
-
-		m.AzureMachine.Spec.SubnetName = subnetName
-	}
-
-	return nil
-}
diff --git a/azure/scope/machinepool.go b/azure/scope/machinepool.go
index 71208b998..bbe66e5bf 100644
--- a/azure/scope/machinepool.go
+++ b/azure/scope/machinepool.go
@@ -116,7 +116,7 @@ func (m *MachinePoolScope) ScaleSetSpec() azure.ScaleSetSpec {
 		SSHKeyData:              m.AzureMachinePool.Spec.Template.SSHPublicKey,
 		OSDisk:                  m.AzureMachinePool.Spec.Template.OSDisk,
 		DataDisks:               m.AzureMachinePool.Spec.Template.DataDisks,
-		SubnetName:              m.AzureMachinePool.Spec.Template.SubnetName,
+		SubnetName:              m.NodeSubnet().Name,
 		VNetName:                m.Vnet().Name,
 		VNetResourceGroup:       m.Vnet().ResourceGroup,
 		PublicLBName:            m.OutboundLBName(infrav1.Node),
@@ -599,22 +599,3 @@ func (m *MachinePoolScope) getDeploymentStrategy() machinepool.TypedDeleteSelect
 
 	return machinepool.NewMachinePoolDeploymentStrategy(m.AzureMachinePool.Spec.Strategy)
 }
-
-// SetSubnetName defaults the AzureMachinePool subnet name to the name of the subnet with role 'node' when there is only one of them.
-// Note: this logic exists only for purposes of ensuring backwards compatibility for old clusters created without the `subnetName` field being
-// set, and should be removed in the future when this field is no longer optional.
-func (m *MachinePoolScope) SetSubnetName() error {
-	if m.AzureMachinePool.Spec.Template.SubnetName == "" {
-		subnetName := ""
-		for _, subnet := range m.NodeSubnets() {
-			subnetName = subnet.Name
-		}
-		if len(m.NodeSubnets()) == 0 || len(m.NodeSubnets()) > 1 || subnetName == "" {
-			return errors.New("a subnet name must be specified when no subnets are specified or more than 1 subnet of role 'node' exist")
-		}
-
-		m.AzureMachinePool.Spec.Template.SubnetName = subnetName
-	}
-
-	return nil
-}
diff --git a/azure/scope/managedcontrolplane.go b/azure/scope/managedcontrolplane.go
index 7574bb01e..2d28c0fc4 100644
--- a/azure/scope/managedcontrolplane.go
+++ b/azure/scope/managedcontrolplane.go
@@ -207,11 +207,6 @@ func (s *ManagedControlPlaneScope) SubnetSpecs() []azure.SubnetSpec {
 	}
 }
 
-// Subnets returns the subnets specs.
-func (s *ManagedControlPlaneScope) Subnets() infrav1.Subnets {
-	return infrav1.Subnets{}
-}
-
 // NodeSubnet returns the cluster node subnet.
 func (s *ManagedControlPlaneScope) NodeSubnet() infrav1.SubnetSpec {
 	return infrav1.SubnetSpec{
@@ -231,27 +226,6 @@ func (s *ManagedControlPlaneScope) ControlPlaneSubnet() infrav1.SubnetSpec {
 	return infrav1.SubnetSpec{}
 }
 
-// NodeSubnets returns the subnets with the node role.
-func (s *ManagedControlPlaneScope) NodeSubnets() []infrav1.SubnetSpec {
-	return []infrav1.SubnetSpec{
-		{
-			Name:       s.ControlPlane.Spec.VirtualNetwork.Subnet.Name,
-			CIDRBlocks: []string{s.ControlPlane.Spec.VirtualNetwork.Subnet.CIDRBlock},
-		},
-	}
-}
-
-// Subnet returns the subnet with the provided name.
-func (s *ManagedControlPlaneScope) Subnet(name string) infrav1.SubnetSpec {
-	subnet := infrav1.SubnetSpec{}
-	if name == s.ControlPlane.Spec.VirtualNetwork.Subnet.Name {
-		subnet.Name = s.ControlPlane.Spec.VirtualNetwork.Subnet.Name
-		subnet.CIDRBlocks = []string{s.ControlPlane.Spec.VirtualNetwork.Subnet.CIDRBlock}
-	}
-
-	return subnet
-}
-
 // IsIPv6Enabled returns true if a cluster is ipv6 enabled.
 // Currently always false as managed control planes do not currently implement ipv6.
 func (s *ManagedControlPlaneScope) IsIPv6Enabled() bool {
diff --git a/azure/services/agentpools/agentpools.go b/azure/services/agentpools/agentpools.go
index 83a87a48d..604fa9b5b 100644
--- a/azure/services/agentpools/agentpools.go
+++ b/azure/services/agentpools/agentpools.go
@@ -18,12 +18,14 @@ package agentpools
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2020-02-01/containerservice"
 	"github.com/google/go-cmp/cmp"
 	"github.com/pkg/errors"
 	"k8s.io/klog/v2"
 
+	infrav1alpha4 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4"
 	"sigs.k8s.io/cluster-api-provider-azure/azure"
 	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
 )
@@ -79,9 +81,10 @@ func (s *Service) Reconcile(ctx context.Context, spec interface{}) error {
 		}
 	} else {
 		ps := *existingPool.ManagedClusterAgentPoolProfileProperties.ProvisioningState
-		if ps != "Canceled" && ps != "Failed" && ps != "Succeeded" {
-			klog.V(2).Infof("Unable to update existing agent pool in non terminal state.  Agent pool must be in one of the following provisioning states: canceled, failed, or succeeded")
-			return nil
+		if ps != string(infrav1alpha4.Canceled) && ps != string(infrav1alpha4.Failed) && ps != string(infrav1alpha4.Succeeded) {
+			msg := fmt.Sprintf("Unable to update existing agent pool in non terminal state. Agent pool must be in one of the following provisioning states: canceled, failed, or succeeded. Actual state: %s", ps)
+			klog.V(2).Infof(msg)
+			return errors.New(msg)
 		}
 
 		// Normalize individual agent pools to diff in case we need to update
diff --git a/azure/services/bastionhosts/mocks_bastionhosts/bastionhosts_mock.go b/azure/services/bastionhosts/mocks_bastionhosts/bastionhosts_mock.go
index 0683f524c..0fb0e7ee2 100644
--- a/azure/services/bastionhosts/mocks_bastionhosts/bastionhosts_mock.go
+++ b/azure/services/bastionhosts/mocks_bastionhosts/bastionhosts_mock.go
@@ -381,18 +381,46 @@ func (mr *MockBastionScopeMockRecorder) Location() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Location", reflect.TypeOf((*MockBastionScope)(nil).Location))
 }
 
-// NodeSubnets mocks base method.
-func (m *MockBastionScope) NodeSubnets() []v1alpha4.SubnetSpec {
+// NodeNatGateway mocks base method.
+func (m *MockBastionScope) NodeNatGateway() v1alpha4.NatGateway {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "NodeSubnets")
-	ret0, _ := ret[0].([]v1alpha4.SubnetSpec)
+	ret := m.ctrl.Call(m, "NodeNatGateway")
+	ret0, _ := ret[0].(v1alpha4.NatGateway)
 	return ret0
 }
 
-// NodeSubnets indicates an expected call of NodeSubnets.
-func (mr *MockBastionScopeMockRecorder) NodeSubnets() *gomock.Call {
+// NodeNatGateway indicates an expected call of NodeNatGateway.
+func (mr *MockBastionScopeMockRecorder) NodeNatGateway() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnets", reflect.TypeOf((*MockBastionScope)(nil).NodeSubnets))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeNatGateway", reflect.TypeOf((*MockBastionScope)(nil).NodeNatGateway))
+}
+
+// NodeRouteTable mocks base method.
+func (m *MockBastionScope) NodeRouteTable() v1alpha4.RouteTable {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeRouteTable")
+	ret0, _ := ret[0].(v1alpha4.RouteTable)
+	return ret0
+}
+
+// NodeRouteTable indicates an expected call of NodeRouteTable.
+func (mr *MockBastionScopeMockRecorder) NodeRouteTable() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeRouteTable", reflect.TypeOf((*MockBastionScope)(nil).NodeRouteTable))
+}
+
+// NodeSubnet mocks base method.
+func (m *MockBastionScope) NodeSubnet() v1alpha4.SubnetSpec {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeSubnet")
+	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
+	return ret0
+}
+
+// NodeSubnet indicates an expected call of NodeSubnet.
+func (mr *MockBastionScopeMockRecorder) NodeSubnet() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnet", reflect.TypeOf((*MockBastionScope)(nil).NodeSubnet))
 }
 
 // OutboundLBName mocks base method.
@@ -449,34 +477,6 @@ func (mr *MockBastionScopeMockRecorder) SetSubnet(arg0 interface{}) *gomock.Call
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSubnet", reflect.TypeOf((*MockBastionScope)(nil).SetSubnet), arg0)
 }
 
-// Subnet mocks base method.
-func (m *MockBastionScope) Subnet(arg0 string) v1alpha4.SubnetSpec {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnet", arg0)
-	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
-	return ret0
-}
-
-// Subnet indicates an expected call of Subnet.
-func (mr *MockBastionScopeMockRecorder) Subnet(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnet", reflect.TypeOf((*MockBastionScope)(nil).Subnet), arg0)
-}
-
-// Subnets mocks base method.
-func (m *MockBastionScope) Subnets() v1alpha4.Subnets {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnets")
-	ret0, _ := ret[0].(v1alpha4.Subnets)
-	return ret0
-}
-
-// Subnets indicates an expected call of Subnets.
-func (mr *MockBastionScopeMockRecorder) Subnets() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnets", reflect.TypeOf((*MockBastionScope)(nil).Subnets))
-}
-
 // SubscriptionID mocks base method.
 func (m *MockBastionScope) SubscriptionID() string {
 	m.ctrl.T.Helper()
diff --git a/azure/services/loadbalancers/mock_loadbalancers/loadbalancers_mock.go b/azure/services/loadbalancers/mock_loadbalancers/loadbalancers_mock.go
index afa1ab531..e16c1d356 100644
--- a/azure/services/loadbalancers/mock_loadbalancers/loadbalancers_mock.go
+++ b/azure/services/loadbalancers/mock_loadbalancers/loadbalancers_mock.go
@@ -381,18 +381,46 @@ func (mr *MockLBScopeMockRecorder) Location() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Location", reflect.TypeOf((*MockLBScope)(nil).Location))
 }
 
-// NodeSubnets mocks base method.
-func (m *MockLBScope) NodeSubnets() []v1alpha4.SubnetSpec {
+// NodeNatGateway mocks base method.
+func (m *MockLBScope) NodeNatGateway() v1alpha4.NatGateway {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "NodeSubnets")
-	ret0, _ := ret[0].([]v1alpha4.SubnetSpec)
+	ret := m.ctrl.Call(m, "NodeNatGateway")
+	ret0, _ := ret[0].(v1alpha4.NatGateway)
 	return ret0
 }
 
-// NodeSubnets indicates an expected call of NodeSubnets.
-func (mr *MockLBScopeMockRecorder) NodeSubnets() *gomock.Call {
+// NodeNatGateway indicates an expected call of NodeNatGateway.
+func (mr *MockLBScopeMockRecorder) NodeNatGateway() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnets", reflect.TypeOf((*MockLBScope)(nil).NodeSubnets))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeNatGateway", reflect.TypeOf((*MockLBScope)(nil).NodeNatGateway))
+}
+
+// NodeRouteTable mocks base method.
+func (m *MockLBScope) NodeRouteTable() v1alpha4.RouteTable {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeRouteTable")
+	ret0, _ := ret[0].(v1alpha4.RouteTable)
+	return ret0
+}
+
+// NodeRouteTable indicates an expected call of NodeRouteTable.
+func (mr *MockLBScopeMockRecorder) NodeRouteTable() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeRouteTable", reflect.TypeOf((*MockLBScope)(nil).NodeRouteTable))
+}
+
+// NodeSubnet mocks base method.
+func (m *MockLBScope) NodeSubnet() v1alpha4.SubnetSpec {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeSubnet")
+	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
+	return ret0
+}
+
+// NodeSubnet indicates an expected call of NodeSubnet.
+func (mr *MockLBScopeMockRecorder) NodeSubnet() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnet", reflect.TypeOf((*MockLBScope)(nil).NodeSubnet))
 }
 
 // OutboundLBName mocks base method.
@@ -449,34 +477,6 @@ func (mr *MockLBScopeMockRecorder) SetSubnet(arg0 interface{}) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSubnet", reflect.TypeOf((*MockLBScope)(nil).SetSubnet), arg0)
 }
 
-// Subnet mocks base method.
-func (m *MockLBScope) Subnet(arg0 string) v1alpha4.SubnetSpec {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnet", arg0)
-	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
-	return ret0
-}
-
-// Subnet indicates an expected call of Subnet.
-func (mr *MockLBScopeMockRecorder) Subnet(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnet", reflect.TypeOf((*MockLBScope)(nil).Subnet), arg0)
-}
-
-// Subnets mocks base method.
-func (m *MockLBScope) Subnets() v1alpha4.Subnets {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnets")
-	ret0, _ := ret[0].(v1alpha4.Subnets)
-	return ret0
-}
-
-// Subnets indicates an expected call of Subnets.
-func (mr *MockLBScopeMockRecorder) Subnets() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnets", reflect.TypeOf((*MockLBScope)(nil).Subnets))
-}
-
 // SubscriptionID mocks base method.
 func (m *MockLBScope) SubscriptionID() string {
 	m.ctrl.T.Helper()
diff --git a/azure/services/managedclusters/managedclusters.go b/azure/services/managedclusters/managedclusters.go
index 1b79c1ac6..65ff48039 100644
--- a/azure/services/managedclusters/managedclusters.go
+++ b/azure/services/managedclusters/managedclusters.go
@@ -27,6 +27,7 @@ import (
 	"github.com/pkg/errors"
 	"k8s.io/klog/v2"
 
+	infrav1alpha4 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4"
 	"sigs.k8s.io/cluster-api-provider-azure/azure"
 	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
 )
@@ -202,9 +203,10 @@ func (s *Service) Reconcile(ctx context.Context, spec interface{}) error {
 		}
 	} else {
 		ps := *existingMC.ManagedClusterProperties.ProvisioningState
-		if ps != "Canceled" && ps != "Failed" && ps != "Succeeded" {
-			klog.V(2).Infof("Unable to update existing managed cluster in non terminal state.  Managed cluster must be in one of the following provisioning states: canceled, failed, or succeeded")
-			return nil
+		if ps != string(infrav1alpha4.Canceled) && ps != string(infrav1alpha4.Failed) && ps != string(infrav1alpha4.Succeeded) {
+			msg := fmt.Sprintf("Unable to update existing managed cluster in non terminal state. Managed cluster must be in one of the following provisioning states: canceled, failed, or succeeded. Actual state: %s", ps)
+			klog.V(2).Infof(msg)
+			return errors.New(msg)
 		}
 
 		// Normalize properties for the desired (CR spec) and existing managed
diff --git a/azure/services/natgateways/mock_natgateways/natgateways_mock.go b/azure/services/natgateways/mock_natgateways/natgateways_mock.go
index 41ca05061..9c65b81e8 100644
--- a/azure/services/natgateways/mock_natgateways/natgateways_mock.go
+++ b/azure/services/natgateways/mock_natgateways/natgateways_mock.go
@@ -381,18 +381,46 @@ func (mr *MockNatGatewayScopeMockRecorder) NatGatewaySpecs() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NatGatewaySpecs", reflect.TypeOf((*MockNatGatewayScope)(nil).NatGatewaySpecs))
 }
 
-// NodeSubnets mocks base method.
-func (m *MockNatGatewayScope) NodeSubnets() []v1alpha4.SubnetSpec {
+// NodeNatGateway mocks base method.
+func (m *MockNatGatewayScope) NodeNatGateway() v1alpha4.NatGateway {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "NodeSubnets")
-	ret0, _ := ret[0].([]v1alpha4.SubnetSpec)
+	ret := m.ctrl.Call(m, "NodeNatGateway")
+	ret0, _ := ret[0].(v1alpha4.NatGateway)
 	return ret0
 }
 
-// NodeSubnets indicates an expected call of NodeSubnets.
-func (mr *MockNatGatewayScopeMockRecorder) NodeSubnets() *gomock.Call {
+// NodeNatGateway indicates an expected call of NodeNatGateway.
+func (mr *MockNatGatewayScopeMockRecorder) NodeNatGateway() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnets", reflect.TypeOf((*MockNatGatewayScope)(nil).NodeSubnets))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeNatGateway", reflect.TypeOf((*MockNatGatewayScope)(nil).NodeNatGateway))
+}
+
+// NodeRouteTable mocks base method.
+func (m *MockNatGatewayScope) NodeRouteTable() v1alpha4.RouteTable {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeRouteTable")
+	ret0, _ := ret[0].(v1alpha4.RouteTable)
+	return ret0
+}
+
+// NodeRouteTable indicates an expected call of NodeRouteTable.
+func (mr *MockNatGatewayScopeMockRecorder) NodeRouteTable() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeRouteTable", reflect.TypeOf((*MockNatGatewayScope)(nil).NodeRouteTable))
+}
+
+// NodeSubnet mocks base method.
+func (m *MockNatGatewayScope) NodeSubnet() v1alpha4.SubnetSpec {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeSubnet")
+	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
+	return ret0
+}
+
+// NodeSubnet indicates an expected call of NodeSubnet.
+func (mr *MockNatGatewayScopeMockRecorder) NodeSubnet() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnet", reflect.TypeOf((*MockNatGatewayScope)(nil).NodeSubnet))
 }
 
 // OutboundLBName mocks base method.
@@ -437,44 +465,28 @@ func (mr *MockNatGatewayScopeMockRecorder) ResourceGroup() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResourceGroup", reflect.TypeOf((*MockNatGatewayScope)(nil).ResourceGroup))
 }
 
-// SetSubnet mocks base method.
-func (m *MockNatGatewayScope) SetSubnet(arg0 v1alpha4.SubnetSpec) {
+// SetNodeNatGatewayID mocks base method.
+func (m *MockNatGatewayScope) SetNodeNatGatewayID(id string) {
 	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "SetSubnet", arg0)
+	m.ctrl.Call(m, "SetNodeNatGatewayID", id)
 }
 
-// SetSubnet indicates an expected call of SetSubnet.
-func (mr *MockNatGatewayScopeMockRecorder) SetSubnet(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSubnet", reflect.TypeOf((*MockNatGatewayScope)(nil).SetSubnet), arg0)
-}
-
-// Subnet mocks base method.
-func (m *MockNatGatewayScope) Subnet(arg0 string) v1alpha4.SubnetSpec {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnet", arg0)
-	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
-	return ret0
-}
-
-// Subnet indicates an expected call of Subnet.
-func (mr *MockNatGatewayScopeMockRecorder) Subnet(arg0 interface{}) *gomock.Call {
+// SetNodeNatGatewayID indicates an expected call of SetNodeNatGatewayID.
+func (mr *MockNatGatewayScopeMockRecorder) SetNodeNatGatewayID(id interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnet", reflect.TypeOf((*MockNatGatewayScope)(nil).Subnet), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNodeNatGatewayID", reflect.TypeOf((*MockNatGatewayScope)(nil).SetNodeNatGatewayID), id)
 }
 
-// Subnets mocks base method.
-func (m *MockNatGatewayScope) Subnets() v1alpha4.Subnets {
+// SetSubnet mocks base method.
+func (m *MockNatGatewayScope) SetSubnet(arg0 v1alpha4.SubnetSpec) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnets")
-	ret0, _ := ret[0].(v1alpha4.Subnets)
-	return ret0
+	m.ctrl.Call(m, "SetSubnet", arg0)
 }
 
-// Subnets indicates an expected call of Subnets.
-func (mr *MockNatGatewayScopeMockRecorder) Subnets() *gomock.Call {
+// SetSubnet indicates an expected call of SetSubnet.
+func (mr *MockNatGatewayScopeMockRecorder) SetSubnet(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnets", reflect.TypeOf((*MockNatGatewayScope)(nil).Subnets))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSubnet", reflect.TypeOf((*MockNatGatewayScope)(nil).SetSubnet), arg0)
 }
 
 // SubscriptionID mocks base method.
diff --git a/azure/services/natgateways/natgateways.go b/azure/services/natgateways/natgateways.go
index 877e65db4..2f93f64f9 100644
--- a/azure/services/natgateways/natgateways.go
+++ b/azure/services/natgateways/natgateways.go
@@ -34,6 +34,7 @@ type NatGatewayScope interface {
 	logr.Logger
 	azure.ClusterScoper
 	NatGatewaySpecs() []azure.NatGatewaySpec
+	SetNodeNatGatewayID(id string)
 }
 
 // Service provides operations on azure resources.
@@ -70,13 +71,11 @@ func (s *Service) Reconcile(ctx context.Context) error {
 		case err == nil:
 			// nat gateway already exists
 			s.Scope.V(4).Info("nat gateway already exists", "nat gateway", natGatewaySpec.Name)
-			natGatewaySpec.Subnet.NatGateway.ID = existingNatGateway.ID
+			s.Scope.SetNodeNatGatewayID(existingNatGateway.ID)
 
 			if existingNatGateway.NatGatewayIP.Name == natGatewaySpec.NatGatewayIP.Name {
 				// Skip update for Nat Gateway as it exists with expected values
 				s.Scope.V(4).Info("Nat Gateway exists with expected values, skipping update", "nat gateway", natGatewaySpec.Name)
-				natGatewaySpec.Subnet.NatGateway = *existingNatGateway
-				s.Scope.SetSubnet(natGatewaySpec.Subnet)
 				continue
 			}
 		default:
@@ -100,15 +99,7 @@ func (s *Service) Reconcile(ctx context.Context) error {
 			return errors.Wrapf(err, "failed to create nat gateway %s in resource group %s", natGatewaySpec.Name, s.Scope.ResourceGroup())
 		}
 		s.Scope.V(2).Info("successfully created nat gateway", "nat gateway", natGatewaySpec.Name)
-		natGateway := infrav1.NatGateway{
-			ID:   azure.NatGatewayID(s.Scope.SubscriptionID(), s.Scope.ResourceGroup(), natGatewaySpec.Name),
-			Name: natGatewaySpec.Name,
-			NatGatewayIP: infrav1.PublicIPSpec{
-				Name: natGatewaySpec.NatGatewayIP.Name,
-			},
-		}
-		natGatewaySpec.Subnet.NatGateway = natGateway
-		s.Scope.SetSubnet(natGatewaySpec.Subnet)
+		s.Scope.SetNodeNatGatewayID(azure.NatGatewayID(s.Scope.SubscriptionID(), s.Scope.ResourceGroup(), natGatewaySpec.Name))
 	}
 	return nil
 }
diff --git a/azure/services/resourceskus/sku.go b/azure/services/resourceskus/sku.go
index fa2d404ba..fa72528b4 100644
--- a/azure/services/resourceskus/sku.go
+++ b/azure/services/resourceskus/sku.go
@@ -66,8 +66,6 @@ const (
 	EncryptionAtHost = "EncryptionAtHostSupported"
 	// MaximumPlatformFaultDomainCount identifies the maximum fault domain count for an availability set in a region.
 	MaximumPlatformFaultDomainCount = "MaximumPlatformFaultDomainCount"
-	// UltraSSDAvailable identifies the capability for the support of UltraSSD data disks.
-	UltraSSDAvailable = "UltraSSDAvailable"
 )
 
 // HasCapability return true for a capability which can be either
@@ -130,38 +128,3 @@ func (s SKU) GetCapability(name string) (string, bool) {
 	}
 	return "", false
 }
-
-// HasLocationCapability returns true if the provided resource supports the location capability.
-func (s SKU) HasLocationCapability(capabilityName, location, zone string) bool {
-	if s.LocationInfo == nil {
-		return false
-	}
-
-	for _, info := range *s.LocationInfo {
-		if info.Location == nil || *info.Location != location || info.ZoneDetails == nil {
-			continue
-		}
-
-		for _, zoneDetail := range *info.ZoneDetails {
-			if zoneDetail.Capabilities == nil {
-				continue
-			}
-
-			for _, capability := range *zoneDetail.Capabilities {
-				if capability.Name != nil && *capability.Name == capabilityName {
-					if zoneDetail.Name == nil {
-						return false
-					}
-
-					for _, name := range *zoneDetail.Name {
-						if name == zone {
-							return true
-						}
-					}
-					return false
-				}
-			}
-		}
-	}
-	return false
-}
diff --git a/azure/services/routetables/mock_routetables/routetables_mock.go b/azure/services/routetables/mock_routetables/routetables_mock.go
index 4ebc9949f..a90a917e6 100644
--- a/azure/services/routetables/mock_routetables/routetables_mock.go
+++ b/azure/services/routetables/mock_routetables/routetables_mock.go
@@ -367,18 +367,46 @@ func (mr *MockRouteTableScopeMockRecorder) Location() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Location", reflect.TypeOf((*MockRouteTableScope)(nil).Location))
 }
 
-// NodeSubnets mocks base method.
-func (m *MockRouteTableScope) NodeSubnets() []v1alpha4.SubnetSpec {
+// NodeNatGateway mocks base method.
+func (m *MockRouteTableScope) NodeNatGateway() v1alpha4.NatGateway {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "NodeSubnets")
-	ret0, _ := ret[0].([]v1alpha4.SubnetSpec)
+	ret := m.ctrl.Call(m, "NodeNatGateway")
+	ret0, _ := ret[0].(v1alpha4.NatGateway)
 	return ret0
 }
 
-// NodeSubnets indicates an expected call of NodeSubnets.
-func (mr *MockRouteTableScopeMockRecorder) NodeSubnets() *gomock.Call {
+// NodeNatGateway indicates an expected call of NodeNatGateway.
+func (mr *MockRouteTableScopeMockRecorder) NodeNatGateway() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnets", reflect.TypeOf((*MockRouteTableScope)(nil).NodeSubnets))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeNatGateway", reflect.TypeOf((*MockRouteTableScope)(nil).NodeNatGateway))
+}
+
+// NodeRouteTable mocks base method.
+func (m *MockRouteTableScope) NodeRouteTable() v1alpha4.RouteTable {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeRouteTable")
+	ret0, _ := ret[0].(v1alpha4.RouteTable)
+	return ret0
+}
+
+// NodeRouteTable indicates an expected call of NodeRouteTable.
+func (mr *MockRouteTableScopeMockRecorder) NodeRouteTable() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeRouteTable", reflect.TypeOf((*MockRouteTableScope)(nil).NodeRouteTable))
+}
+
+// NodeSubnet mocks base method.
+func (m *MockRouteTableScope) NodeSubnet() v1alpha4.SubnetSpec {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeSubnet")
+	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
+	return ret0
+}
+
+// NodeSubnet indicates an expected call of NodeSubnet.
+func (mr *MockRouteTableScopeMockRecorder) NodeSubnet() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnet", reflect.TypeOf((*MockRouteTableScope)(nil).NodeSubnet))
 }
 
 // OutboundLBName mocks base method.
@@ -449,34 +477,6 @@ func (mr *MockRouteTableScopeMockRecorder) SetSubnet(arg0 interface{}) *gomock.C
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSubnet", reflect.TypeOf((*MockRouteTableScope)(nil).SetSubnet), arg0)
 }
 
-// Subnet mocks base method.
-func (m *MockRouteTableScope) Subnet(arg0 string) v1alpha4.SubnetSpec {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnet", arg0)
-	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
-	return ret0
-}
-
-// Subnet indicates an expected call of Subnet.
-func (mr *MockRouteTableScopeMockRecorder) Subnet(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnet", reflect.TypeOf((*MockRouteTableScope)(nil).Subnet), arg0)
-}
-
-// Subnets mocks base method.
-func (m *MockRouteTableScope) Subnets() v1alpha4.Subnets {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnets")
-	ret0, _ := ret[0].(v1alpha4.Subnets)
-	return ret0
-}
-
-// Subnets indicates an expected call of Subnets.
-func (mr *MockRouteTableScopeMockRecorder) Subnets() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnets", reflect.TypeOf((*MockRouteTableScope)(nil).Subnets))
-}
-
 // SubscriptionID mocks base method.
 func (m *MockRouteTableScope) SubscriptionID() string {
 	m.ctrl.T.Helper()
diff --git a/azure/services/scalesets/scalesets.go b/azure/services/scalesets/scalesets.go
index 73df52920..afd100d57 100644
--- a/azure/services/scalesets/scalesets.go
+++ b/azure/services/scalesets/scalesets.go
@@ -37,11 +37,6 @@ import (
 	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
 )
 
-const (
-	// UltraSSDStorageAccountType identifies the Ultra disk storage account type.
-	UltraSSDStorageAccountType = "UltraSSD_LRS"
-)
-
 type (
 	// ScaleSetScope defines the scope interface for a scale sets service.
 	ScaleSetScope interface {
@@ -87,20 +82,17 @@ func (s *Service) Reconcile(ctx context.Context) (retErr error) {
 		return err
 	}
 
-	var err error
-
-	scaleSetSpec := s.Scope.ScaleSetSpec()
-
 	// check if there is an ongoing long running operation
 	var (
 		future      = s.Scope.GetLongRunningOperationState()
 		fetchedVMSS *azure.VMSS
+		err         error
 	)
 
 	defer func() {
 		// save the updated state of the VMSS for the MachinePoolScope to use for updating K8s state
 		if fetchedVMSS == nil {
-			fetchedVMSS, err = s.getVirtualMachineScaleSet(ctx, scaleSetSpec.Name)
+			fetchedVMSS, err = s.getVirtualMachineScaleSet(ctx)
 			if err != nil && !azure.ResourceNotFound(err) {
 				s.Scope.Error(err, "failed to get vmss in deferred update")
 			}
@@ -113,7 +105,7 @@ func (s *Service) Reconcile(ctx context.Context) (retErr error) {
 	}()
 
 	if future == nil {
-		fetchedVMSS, err = s.getVirtualMachineScaleSet(ctx, scaleSetSpec.Name)
+		fetchedVMSS, err = s.getVirtualMachineScaleSet(ctx)
 	} else {
 		fetchedVMSS, err = s.getVirtualMachineScaleSetIfDone(ctx, future)
 	}
@@ -121,7 +113,7 @@ func (s *Service) Reconcile(ctx context.Context) (retErr error) {
 	switch {
 	case err != nil && !azure.ResourceNotFound(err):
 		// There was an error and it was not an HTTP 404 not found. This is either a transient error, like long running operation not done, or an Azure service error.
-		return errors.Wrapf(err, "failed to get VMSS %s", scaleSetSpec.Name)
+		return errors.Wrapf(err, "failed to get VMSS %s", s.Scope.ScaleSetSpec().Name)
 	case err != nil && azure.ResourceNotFound(err):
 		// HTTP(404) resource was not found, so we need to create it with a PUT
 		future, err = s.createVMSS(ctx)
@@ -143,7 +135,7 @@ func (s *Service) Reconcile(ctx context.Context) (retErr error) {
 	if future != nil {
 		fetchedVMSS, err = s.getVirtualMachineScaleSetIfDone(ctx, future)
 		if err != nil {
-			return errors.Wrapf(err, "failed to get VMSS %s after create or update", scaleSetSpec.Name)
+			return errors.Wrapf(err, "failed to get VMSS %s after create or update", s.Scope.ScaleSetSpec().Name)
 		}
 	}
 
@@ -158,13 +150,9 @@ func (s *Service) Delete(ctx context.Context) error {
 	ctx, span := tele.Tracer().Start(ctx, "scalesets.Service.Delete")
 	defer span.End()
 
-	var err error
-
-	vmssSpec := s.Scope.ScaleSetSpec()
-
 	defer func() {
 		// save the updated state of the VMSS for the MachinePoolScope to use for updating K8s state
-		fetchedVMSS, err := s.getVirtualMachineScaleSet(ctx, vmssSpec.Name)
+		fetchedVMSS, err := s.getVirtualMachineScaleSet(ctx)
 		if err != nil && !azure.ResourceNotFound(err) {
 			s.Scope.Error(err, "failed to get vmss in deferred update")
 		}
@@ -189,8 +177,9 @@ func (s *Service) Delete(ctx context.Context) error {
 	}
 
 	// no long running delete operation is active, so delete the ScaleSet
+	vmssSpec := s.Scope.ScaleSetSpec()
 	s.Scope.V(2).Info("deleting VMSS", "scale set", vmssSpec.Name)
-	future, err = s.Client.DeleteAsync(ctx, s.Scope.ResourceGroup(), vmssSpec.Name)
+	future, err := s.Client.DeleteAsync(ctx, s.Scope.ResourceGroup(), vmssSpec.Name)
 	if err != nil {
 		if azure.ResourceNotFound(err) {
 			// already deleted
@@ -217,7 +206,6 @@ func (s *Service) createVMSS(ctx context.Context) (*infrav1.Future, error) {
 	defer span.End()
 
 	spec := s.Scope.ScaleSetSpec()
-
 	vmss, err := s.buildVMSSFromSpec(ctx, spec)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed building VMSS from spec")
@@ -238,7 +226,6 @@ func (s *Service) patchVMSSIfNeeded(ctx context.Context, infraVMSS *azure.VMSS)
 	defer span.End()
 
 	spec := s.Scope.ScaleSetSpec()
-
 	vmss, err := s.buildVMSSFromSpec(ctx, spec)
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to generate scale set update parameters for %s", spec.Name)
@@ -293,7 +280,6 @@ func (s *Service) validateSpec(ctx context.Context) error {
 	defer span.End()
 
 	spec := s.Scope.ScaleSetSpec()
-
 	sku, err := s.resourceSKUCache.Get(ctx, spec.Size, resourceskus.VirtualMachines)
 	if err != nil {
 		return azure.WithTerminalError(errors.Wrapf(err, "failed to get SKU %s in compute api", spec.Size))
@@ -328,21 +314,6 @@ func (s *Service) validateSpec(ctx context.Context) error {
 		return azure.WithTerminalError(errors.Errorf("encryption at host is not supported for VM type %s", spec.Size))
 	}
 
-	// check the support for ultra disks based on location and vm size
-	for _, disks := range spec.DataDisks {
-		location := s.Scope.Location()
-		zones, err := s.resourceSKUCache.GetZones(ctx, location)
-		if err != nil {
-			return azure.WithTerminalError(errors.Wrapf(err, "failed to get the zones for location %s", location))
-		}
-
-		for _, zone := range zones {
-			if disks.ManagedDisk != nil && disks.ManagedDisk.StorageAccountType == UltraSSDStorageAccountType && !sku.HasLocationCapability(resourceskus.UltraSSDAvailable, location, zone) {
-				return azure.WithTerminalError(fmt.Errorf("vm size %s does not support ultra disks in location %s. select a different vm size or disable ultra disks", spec.Size, location))
-			}
-		}
-	}
-
 	// Checking if selected availability zones are available selected VM type in location
 	azsInLocation, err := s.resourceSKUCache.GetZonesWithVMSize(ctx, spec.Size, s.Scope.Location())
 	if err != nil {
@@ -481,14 +452,6 @@ func (s *Service) buildVMSSFromSpec(ctx context.Context, vmssSpec azure.ScaleSet
 		}
 	}
 
-	for _, dataDisk := range vmssSpec.DataDisks {
-		if dataDisk.ManagedDisk != nil && dataDisk.ManagedDisk.StorageAccountType == UltraSSDStorageAccountType {
-			vmss.VirtualMachineScaleSetProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{
-				UltraSSDEnabled: to.BoolPtr(true),
-			}
-		}
-	}
-
 	tags := infrav1.Build(infrav1.BuildParams{
 		ClusterName: s.Scope.ClusterName(),
 		Lifecycle:   infrav1.ResourceLifecycleOwned,
@@ -502,16 +465,17 @@ func (s *Service) buildVMSSFromSpec(ctx context.Context, vmssSpec azure.ScaleSet
 }
 
 // getVirtualMachineScaleSet provides information about a Virtual Machine Scale Set and its instances.
-func (s *Service) getVirtualMachineScaleSet(ctx context.Context, vmssName string) (*azure.VMSS, error) {
+func (s *Service) getVirtualMachineScaleSet(ctx context.Context) (*azure.VMSS, error) {
 	ctx, span := tele.Tracer().Start(ctx, "scalesets.Service.getVirtualMachineScaleSet")
 	defer span.End()
 
-	vmss, err := s.Client.Get(ctx, s.Scope.ResourceGroup(), vmssName)
+	name := s.Scope.ScaleSetSpec().Name
+	vmss, err := s.Client.Get(ctx, s.Scope.ResourceGroup(), name)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to get existing vmss")
 	}
 
-	vmssInstances, err := s.Client.ListInstances(ctx, s.Scope.ResourceGroup(), vmssName)
+	vmssInstances, err := s.Client.ListInstances(ctx, s.Scope.ResourceGroup(), name)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to list instances")
 	}
diff --git a/azure/services/securitygroups/mock_securitygroups/securitygroups_mock.go b/azure/services/securitygroups/mock_securitygroups/securitygroups_mock.go
index 07c323e1b..df7383d6f 100644
--- a/azure/services/securitygroups/mock_securitygroups/securitygroups_mock.go
+++ b/azure/services/securitygroups/mock_securitygroups/securitygroups_mock.go
@@ -381,18 +381,46 @@ func (mr *MockNSGScopeMockRecorder) NSGSpecs() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NSGSpecs", reflect.TypeOf((*MockNSGScope)(nil).NSGSpecs))
 }
 
-// NodeSubnets mocks base method.
-func (m *MockNSGScope) NodeSubnets() []v1alpha4.SubnetSpec {
+// NodeNatGateway mocks base method.
+func (m *MockNSGScope) NodeNatGateway() v1alpha4.NatGateway {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "NodeSubnets")
-	ret0, _ := ret[0].([]v1alpha4.SubnetSpec)
+	ret := m.ctrl.Call(m, "NodeNatGateway")
+	ret0, _ := ret[0].(v1alpha4.NatGateway)
 	return ret0
 }
 
-// NodeSubnets indicates an expected call of NodeSubnets.
-func (mr *MockNSGScopeMockRecorder) NodeSubnets() *gomock.Call {
+// NodeNatGateway indicates an expected call of NodeNatGateway.
+func (mr *MockNSGScopeMockRecorder) NodeNatGateway() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnets", reflect.TypeOf((*MockNSGScope)(nil).NodeSubnets))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeNatGateway", reflect.TypeOf((*MockNSGScope)(nil).NodeNatGateway))
+}
+
+// NodeRouteTable mocks base method.
+func (m *MockNSGScope) NodeRouteTable() v1alpha4.RouteTable {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeRouteTable")
+	ret0, _ := ret[0].(v1alpha4.RouteTable)
+	return ret0
+}
+
+// NodeRouteTable indicates an expected call of NodeRouteTable.
+func (mr *MockNSGScopeMockRecorder) NodeRouteTable() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeRouteTable", reflect.TypeOf((*MockNSGScope)(nil).NodeRouteTable))
+}
+
+// NodeSubnet mocks base method.
+func (m *MockNSGScope) NodeSubnet() v1alpha4.SubnetSpec {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeSubnet")
+	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
+	return ret0
+}
+
+// NodeSubnet indicates an expected call of NodeSubnet.
+func (mr *MockNSGScopeMockRecorder) NodeSubnet() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnet", reflect.TypeOf((*MockNSGScope)(nil).NodeSubnet))
 }
 
 // OutboundLBName mocks base method.
@@ -449,34 +477,6 @@ func (mr *MockNSGScopeMockRecorder) SetSubnet(arg0 interface{}) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSubnet", reflect.TypeOf((*MockNSGScope)(nil).SetSubnet), arg0)
 }
 
-// Subnet mocks base method.
-func (m *MockNSGScope) Subnet(arg0 string) v1alpha4.SubnetSpec {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnet", arg0)
-	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
-	return ret0
-}
-
-// Subnet indicates an expected call of Subnet.
-func (mr *MockNSGScopeMockRecorder) Subnet(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnet", reflect.TypeOf((*MockNSGScope)(nil).Subnet), arg0)
-}
-
-// Subnets mocks base method.
-func (m *MockNSGScope) Subnets() v1alpha4.Subnets {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnets")
-	ret0, _ := ret[0].(v1alpha4.Subnets)
-	return ret0
-}
-
-// Subnets indicates an expected call of Subnets.
-func (mr *MockNSGScopeMockRecorder) Subnets() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnets", reflect.TypeOf((*MockNSGScope)(nil).Subnets))
-}
-
 // SubscriptionID mocks base method.
 func (m *MockNSGScope) SubscriptionID() string {
 	m.ctrl.T.Helper()
diff --git a/azure/services/subnets/mock_subnets/subnets_mock.go b/azure/services/subnets/mock_subnets/subnets_mock.go
index 43f6e7856..28dca309b 100644
--- a/azure/services/subnets/mock_subnets/subnets_mock.go
+++ b/azure/services/subnets/mock_subnets/subnets_mock.go
@@ -367,18 +367,46 @@ func (mr *MockSubnetScopeMockRecorder) Location() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Location", reflect.TypeOf((*MockSubnetScope)(nil).Location))
 }
 
-// NodeSubnets mocks base method.
-func (m *MockSubnetScope) NodeSubnets() []v1alpha4.SubnetSpec {
+// NodeNatGateway mocks base method.
+func (m *MockSubnetScope) NodeNatGateway() v1alpha4.NatGateway {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "NodeSubnets")
-	ret0, _ := ret[0].([]v1alpha4.SubnetSpec)
+	ret := m.ctrl.Call(m, "NodeNatGateway")
+	ret0, _ := ret[0].(v1alpha4.NatGateway)
 	return ret0
 }
 
-// NodeSubnets indicates an expected call of NodeSubnets.
-func (mr *MockSubnetScopeMockRecorder) NodeSubnets() *gomock.Call {
+// NodeNatGateway indicates an expected call of NodeNatGateway.
+func (mr *MockSubnetScopeMockRecorder) NodeNatGateway() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnets", reflect.TypeOf((*MockSubnetScope)(nil).NodeSubnets))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeNatGateway", reflect.TypeOf((*MockSubnetScope)(nil).NodeNatGateway))
+}
+
+// NodeRouteTable mocks base method.
+func (m *MockSubnetScope) NodeRouteTable() v1alpha4.RouteTable {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeRouteTable")
+	ret0, _ := ret[0].(v1alpha4.RouteTable)
+	return ret0
+}
+
+// NodeRouteTable indicates an expected call of NodeRouteTable.
+func (mr *MockSubnetScopeMockRecorder) NodeRouteTable() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeRouteTable", reflect.TypeOf((*MockSubnetScope)(nil).NodeRouteTable))
+}
+
+// NodeSubnet mocks base method.
+func (m *MockSubnetScope) NodeSubnet() v1alpha4.SubnetSpec {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeSubnet")
+	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
+	return ret0
+}
+
+// NodeSubnet indicates an expected call of NodeSubnet.
+func (mr *MockSubnetScopeMockRecorder) NodeSubnet() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeSubnet", reflect.TypeOf((*MockSubnetScope)(nil).NodeSubnet))
 }
 
 // OutboundLBName mocks base method.
@@ -435,20 +463,6 @@ func (mr *MockSubnetScopeMockRecorder) SetSubnet(arg0 interface{}) *gomock.Call
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSubnet", reflect.TypeOf((*MockSubnetScope)(nil).SetSubnet), arg0)
 }
 
-// Subnet mocks base method.
-func (m *MockSubnetScope) Subnet(arg0 string) v1alpha4.SubnetSpec {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnet", arg0)
-	ret0, _ := ret[0].(v1alpha4.SubnetSpec)
-	return ret0
-}
-
-// Subnet indicates an expected call of Subnet.
-func (mr *MockSubnetScopeMockRecorder) Subnet(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnet", reflect.TypeOf((*MockSubnetScope)(nil).Subnet), arg0)
-}
-
 // SubnetSpecs mocks base method.
 func (m *MockSubnetScope) SubnetSpecs() []azure.SubnetSpec {
 	m.ctrl.T.Helper()
@@ -463,20 +477,6 @@ func (mr *MockSubnetScopeMockRecorder) SubnetSpecs() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubnetSpecs", reflect.TypeOf((*MockSubnetScope)(nil).SubnetSpecs))
 }
 
-// Subnets mocks base method.
-func (m *MockSubnetScope) Subnets() v1alpha4.Subnets {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subnets")
-	ret0, _ := ret[0].(v1alpha4.Subnets)
-	return ret0
-}
-
-// Subnets indicates an expected call of Subnets.
-func (mr *MockSubnetScopeMockRecorder) Subnets() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnets", reflect.TypeOf((*MockSubnetScope)(nil).Subnets))
-}
-
 // SubscriptionID mocks base method.
 func (m *MockSubnetScope) SubscriptionID() string {
 	m.ctrl.T.Helper()
diff --git a/azure/services/subnets/subnets.go b/azure/services/subnets/subnets.go
index 66bb7e3b5..05da5f087 100644
--- a/azure/services/subnets/subnets.go
+++ b/azure/services/subnets/subnets.go
@@ -64,9 +64,15 @@ func (s *Service) Reconcile(ctx context.Context) error {
 		case err == nil:
 			// subnet already exists, update the spec and skip creation
 			var subnet infrav1.SubnetSpec
+			if subnetSpec.Role == infrav1.SubnetControlPlane {
+				subnet = s.Scope.ControlPlaneSubnet()
+			} else if subnetSpec.Role == infrav1.SubnetNode {
+				subnet = s.Scope.NodeSubnet()
+			} else {
+				continue
+			}
+
 			subnet.ID = existingSubnet.ID
-			subnet.Name = existingSubnet.Name
-			subnet.Role = existingSubnet.Role
 			subnet.CIDRBlocks = existingSubnet.CIDRBlocks
 
 			s.Scope.SetSubnet(subnet)
diff --git a/azure/services/virtualmachines/virtualmachines.go b/azure/services/virtualmachines/virtualmachines.go
index bbd6bdf0b..d8ac87f3c 100644
--- a/azure/services/virtualmachines/virtualmachines.go
+++ b/azure/services/virtualmachines/virtualmachines.go
@@ -39,11 +39,6 @@ import (
 	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
 )
 
-const (
-	// UltraSSDStorageAccountType identifies the Ultra disk storage account type.
-	UltraSSDStorageAccountType = "UltraSSD_LRS"
-)
-
 // VMScope defines the scope interface for a virtual machines service.
 type VMScope interface {
 	logr.Logger
@@ -198,14 +193,6 @@ func (s *Service) Reconcile(ctx context.Context) error {
 			}
 		}
 
-		for _, dataDisk := range vmSpec.DataDisks {
-			if dataDisk.ManagedDisk != nil && dataDisk.ManagedDisk.StorageAccountType == UltraSSDStorageAccountType {
-				virtualMachine.VirtualMachineProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{
-					UltraSSDEnabled: to.BoolPtr(true),
-				}
-			}
-		}
-
 		if err := s.Client.CreateOrUpdate(ctx, s.Scope.ResourceGroup(), vmSpec.Name, virtualMachine); err != nil {
 			return errors.Wrapf(err, "failed to create VM %s in resource group %s", vmSpec.Name, s.Scope.ResourceGroup())
 		}
@@ -433,12 +420,6 @@ func (s *Service) generateStorageProfile(ctx context.Context, vmSpec azure.VMSpe
 			if disk.ManagedDisk.DiskEncryptionSet != nil {
 				dataDisks[i].ManagedDisk.DiskEncryptionSet = &compute.DiskEncryptionSetParameters{ID: to.StringPtr(disk.ManagedDisk.DiskEncryptionSet.ID)}
 			}
-
-			// check the support for ultra disks based on location and vm size
-			location := s.Scope.Location()
-			if disk.ManagedDisk.StorageAccountType == UltraSSDStorageAccountType && !sku.HasLocationCapability(resourceskus.UltraSSDAvailable, location, vmSpec.Zone) {
-				return nil, azure.WithTerminalError(fmt.Errorf("vm size %s does not support ultra disks in location %s. select a different vm size or disable ultra disks", vmSpec.Size, location))
-			}
 		}
 	}
 	storageProfile.DataDisks = &dataDisks
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml
index 485f7d289..721b1e536 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml
@@ -967,10 +967,6 @@ spec:
                     description: SSHPublicKey is the SSH public key string base64
                       encoded to add to a Virtual Machine
                     type: string
-                  subnetName:
-                    description: SubnetName selects the Subnet where the VMSS will
-                      be placed
-                    type: string
                   terminateNotificationTimeout:
                     description: TerminateNotificationTimeout enables or disables
                       VMSS scheduled events termination notification with specified
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml
index 644701f84..cf3b8078c 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml
@@ -819,9 +819,6 @@ spec:
                 type: object
               sshPublicKey:
                 type: string
-              subnetName:
-                description: SubnetName selects the Subnet where the VM will be placed
-                type: string
               userAssignedIdentities:
                 description: UserAssignedIdentities is a list of standalone Azure
                   identities provided by the user The lifecycle of a user-assigned
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml
index 5d6bef096..00f05c58f 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml
@@ -702,10 +702,6 @@ spec:
                         type: object
                       sshPublicKey:
                         type: string
-                      subnetName:
-                        description: SubnetName selects the Subnet where the VM will
-                          be placed
-                        type: string
                       userAssignedIdentities:
                         description: UserAssignedIdentities is a list of standalone
                           Azure identities provided by the user The lifecycle of a
diff --git a/controllers/azurecluster_reconciler.go b/controllers/azurecluster_reconciler.go
index 8259cc2f7..6ba64fda1 100644
--- a/controllers/azurecluster_reconciler.go
+++ b/controllers/azurecluster_reconciler.go
@@ -141,10 +141,6 @@ func (s *azureClusterService) Delete(ctx context.Context) error {
 
 	if err := s.groupsSvc.Delete(ctx); err != nil {
 		if errors.Is(err, azure.ErrNotOwned) {
-			if err := s.bastionSvc.Delete(ctx); err != nil {
-				return errors.Wrap(err, "failed to delete bastion")
-			}
-
 			if err := s.privateDNSSvc.Delete(ctx); err != nil {
 				return errors.Wrap(err, "failed to delete private dns")
 			}
@@ -176,6 +172,10 @@ func (s *azureClusterService) Delete(ctx context.Context) error {
 			if err := s.vnetSvc.Delete(ctx); err != nil {
 				return errors.Wrap(err, "failed to delete virtual network")
 			}
+
+			if err := s.bastionSvc.Delete(ctx); err != nil {
+				return errors.Wrap(err, "failed to delete bastion")
+			}
 		} else {
 			return errors.Wrap(err, "failed to delete resource group")
 		}
diff --git a/controllers/azurejson_machinepool_controller.go b/controllers/azurejson_machinepool_controller.go
index c5d1f22fd..14557b1ee 100644
--- a/controllers/azurejson_machinepool_controller.go
+++ b/controllers/azurejson_machinepool_controller.go
@@ -125,12 +125,6 @@ func (r *AzureJSONMachinePoolReconciler) Reconcile(ctx context.Context, req ctrl
 		return reconcile.Result{}, err
 	}
 
-	// Construct secret for this machine
-	userAssignedIdentityIfExists := ""
-	if len(azureMachinePool.Spec.UserAssignedIdentities) > 0 {
-		userAssignedIdentityIfExists = azureMachinePool.Spec.UserAssignedIdentities[0].ProviderID
-	}
-
 	// Create the scope.
 	clusterScope, err := scope.NewClusterScope(ctx, scope.ClusterScopeParams{
 		Client:       r.Client,
@@ -161,7 +155,7 @@ func (r *AzureJSONMachinePoolReconciler) Reconcile(ctx context.Context, req ctrl
 		azureMachinePool.Name,
 		owner,
 		azureMachinePool.Spec.Identity,
-		userAssignedIdentityIfExists,
+		"",
 	)
 
 	if err != nil {
diff --git a/controllers/azuremachine_reconciler.go b/controllers/azuremachine_reconciler.go
index 43ddcaf1f..66b38fe38 100644
--- a/controllers/azuremachine_reconciler.go
+++ b/controllers/azuremachine_reconciler.go
@@ -38,7 +38,6 @@ import (
 
 // azureMachineService is the group of services called by the AzureMachine controller.
 type azureMachineService struct {
-	scope                *scope.MachineScope
 	networkInterfacesSvc azure.Reconciler
 	inboundNatRulesSvc   azure.Reconciler
 	virtualMachinesSvc   azure.Reconciler
@@ -61,7 +60,6 @@ func newAzureMachineService(machineScope *scope.MachineScope) (*azureMachineServ
 	}
 
 	return &azureMachineService{
-		scope:                machineScope,
 		inboundNatRulesSvc:   inboundnatrules.New(machineScope),
 		networkInterfacesSvc: networkinterfaces.New(machineScope, cache),
 		virtualMachinesSvc:   virtualmachines.New(machineScope, cache),
@@ -80,10 +78,6 @@ func (s *azureMachineService) Reconcile(ctx context.Context) error {
 	ctx, span := tele.Tracer().Start(ctx, "controllers.azureMachineService.Reconcile")
 	defer span.End()
 
-	if err := s.scope.SetSubnetName(); err != nil {
-		return errors.Wrap(err, "failed defaulting subnet name")
-	}
-
 	if err := s.publicIPsSvc.Reconcile(ctx); err != nil {
 		return errors.Wrap(err, "failed to create public IP")
 	}
diff --git a/controllers/helpers.go b/controllers/helpers.go
index 44e963d12..54aee5bb7 100644
--- a/controllers/helpers.go
+++ b/controllers/helpers.go
@@ -237,7 +237,6 @@ func userAssignedIdentityCloudProviderConfig(d azure.ClusterScoper, identityID s
 }
 
 func newCloudProviderConfig(d azure.ClusterScoper) (controlPlaneConfig *CloudProviderConfig, workerConfig *CloudProviderConfig) {
-	subnet := getOneNodeSubnet(d)
 	return (&CloudProviderConfig{
 			Cloud:                        d.CloudEnvironment(),
 			AadClientID:                  d.ClientID(),
@@ -245,14 +244,14 @@ func newCloudProviderConfig(d azure.ClusterScoper) (controlPlaneConfig *CloudPro
 			TenantID:                     d.TenantID(),
 			SubscriptionID:               d.SubscriptionID(),
 			ResourceGroup:                d.ResourceGroup(),
-			SecurityGroupName:            subnet.SecurityGroup.Name,
+			SecurityGroupName:            d.NodeSubnet().SecurityGroup.Name,
 			SecurityGroupResourceGroup:   d.Vnet().ResourceGroup,
 			Location:                     d.Location(),
 			VMType:                       "vmss",
 			VnetName:                     d.Vnet().Name,
 			VnetResourceGroup:            d.Vnet().ResourceGroup,
-			SubnetName:                   subnet.Name,
-			RouteTableName:               subnet.RouteTable.Name,
+			SubnetName:                   d.NodeSubnet().Name,
+			RouteTableName:               d.NodeRouteTable().Name,
 			LoadBalancerSku:              "Standard",
 			MaximumLoadBalancerRuleCount: 250,
 			UseManagedIdentityExtension:  false,
@@ -265,14 +264,14 @@ func newCloudProviderConfig(d azure.ClusterScoper) (controlPlaneConfig *CloudPro
 			TenantID:                     d.TenantID(),
 			SubscriptionID:               d.SubscriptionID(),
 			ResourceGroup:                d.ResourceGroup(),
-			SecurityGroupName:            subnet.SecurityGroup.Name,
+			SecurityGroupName:            d.NodeSubnet().SecurityGroup.Name,
 			SecurityGroupResourceGroup:   d.Vnet().ResourceGroup,
 			Location:                     d.Location(),
 			VMType:                       "vmss",
 			VnetName:                     d.Vnet().Name,
 			VnetResourceGroup:            d.Vnet().ResourceGroup,
-			SubnetName:                   subnet.Name,
-			RouteTableName:               subnet.RouteTable.Name,
+			SubnetName:                   d.NodeSubnet().Name,
+			RouteTableName:               d.NodeRouteTable().Name,
 			LoadBalancerSku:              "Standard",
 			MaximumLoadBalancerRuleCount: 250,
 			UseManagedIdentityExtension:  false,
@@ -280,16 +279,6 @@ func newCloudProviderConfig(d azure.ClusterScoper) (controlPlaneConfig *CloudPro
 		}).overrideFromSpec(d)
 }
 
-// getOneNodeSubnet returns one of the subnets for the node role.
-func getOneNodeSubnet(d azure.ClusterScoper) infrav1.SubnetSpec {
-	for _, subnet := range d.Subnets() {
-		if subnet.Role == infrav1.SubnetNode {
-			return subnet
-		}
-	}
-	return infrav1.SubnetSpec{}
-}
-
 // overrideFromSpec overrides cloud provider config with the values provided in cluster spec.
 func (cpc *CloudProviderConfig) overrideFromSpec(d azure.ClusterScoper) *CloudProviderConfig {
 	if d.CloudProviderConfigOverrides() == nil {
diff --git a/docs/book/src/topics/custom-vnet.md b/docs/book/src/topics/custom-vnet.md
index ff9119650..4bb31f7d0 100644
--- a/docs/book/src/topics/custom-vnet.md
+++ b/docs/book/src/topics/custom-vnet.md
@@ -133,84 +133,3 @@ spec:
           - 10.0.2.0/24
   resourceGroup: cluster-example
 ```
-
-### Custom subnets
-
-Sometimes it's desirable to use different subnets for different node pools.
-Several subnets can be specified in the `networkSpec` to be later referenced by name from other CR's like `AzureMachine` or `AzureMachinePool`.
-When more than one `node` subnet is specified, the `subnetName` field in those other CR's becomes mandatory because the controllers wouldn't know which subnet to use.
-
-The subnet used for the control plane must use the role `control-plane` while the subnets for the worker nodes must use the role `node`.
-
-
-```yaml
----
-apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-kind: AzureCluster
-metadata:
-  name: cluster-example
-  namespace: default
-spec:
-  location: southcentralus
-  networkSpec:
-    subnets:
-    - name: control-plane-subnet
-      role: control-plane
-    - name: subnet-mp-1
-      role: node
-    - name: subnet-mp-2
-      role: node
-    vnet:
-      name: my-vnet
-      cidrBlocks:
-        - 10.0.0.0/16
-  resourceGroup: cluster-example
----
-apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-kind: AzureMachinePool
-metadata:
-  name: mp1
-  namespace: default
-spec:
-  location: southcentralus
-  strategy:
-    rollingUpdate:
-      deletePolicy: Oldest
-      maxSurge: 25%
-      maxUnavailable: 1
-    type: RollingUpdate
-  template:
-    osDisk:
-      diskSizeGB: 30
-      managedDisk:
-        storageAccountType: Premium_LRS
-      osType: Linux
-    sshPublicKey: ${YOUR_SSH_PUB_KEY}
-    subnetName: subnet-mp-1
-    vmSize: Standard_D2s_v3
----
-apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-kind: AzureMachinePool
-metadata:
-  name: mp2
-  namespace: default
-spec:
-  location: southcentralus
-  strategy:
-    rollingUpdate:
-      deletePolicy: Oldest
-      maxSurge: 25%
-      maxUnavailable: 1
-    type: RollingUpdate
-  template:
-    osDisk:
-      diskSizeGB: 30
-      managedDisk:
-        storageAccountType: Premium_LRS
-      osType: Linux
-    sshPublicKey: ${YOUR_SSH_PUB_KEY}
-    subnetName: subnet-mp-2
-    vmSize: Standard_D2s_v3
-```
-
-If you don't specify any `node` subnets, one subnet with role `node` will be created and added to the `networkSpec` definition.
diff --git a/docs/book/src/topics/data-disks.md b/docs/book/src/topics/data-disks.md
index aa3346a6e..8adcaaa0d 100644
--- a/docs/book/src/topics/data-disks.md
+++ b/docs/book/src/topics/data-disks.md
@@ -24,15 +24,6 @@ See [Introduction to Azure managed disks](https://docs.microsoft.com/en-us/azure
  
  > IMPORTANT! The `lun` specified in the AzureMachine Spec must match the LUN used to refer to the device in Kubeadm diskSetup. See below for an example.
 
-### Ultra disk support for data disks
-If we use StorageAccountType as `UltraSSD_LRS` in Managed Disks, the ultra disk support will be enabled for the region and zone which supports the `UltraSSDAvailable` capability.
-
-To check all available vm-sizes in a given region which supports availability zone that has the `UltraSSDAvailable` capability supported, execute following using Azure CLI:
-```bash
-az vm list-skus -l <location> -z -s <VM-size>
-```
-See [Ultra disk](https://docs.microsoft.com/en-us/azure/virtual-machines/disks-types#ultra-disk) for ultra disk performance and GA scope.
-
 ## Configuring partitions, file systems and mounts 
 
 `KubeadmConfig` makes it easy to partition, format, and mount your data disk so your Linux VM can use it. Use the `diskSetup` and `mounts` options to describe partitions, file systems and mounts.
diff --git a/docs/book/src/topics/vm-identity.md b/docs/book/src/topics/vm-identity.md
index d51684453..21c209382 100644
--- a/docs/book/src/topics/vm-identity.md
+++ b/docs/book/src/topics/vm-identity.md
@@ -37,16 +37,6 @@ To create an Azure VM with the system-assigned managed identity enabled, your Az
 
 #### User-assigned
 
-<aside class="note"> 
-
-<h1> Note </h1>
-
-While CAPZ allows you to specify multiple user-assigned identities, only the first one will be used for Cloud Provider authentication. The other identities are left at the user's discretion for other use cases.
-
-The first user assigned identity should have the `Contributor` role on the resource group or, if resources are spread across multiple resource groups (e.g. custom vnet in a separate RG), `Contributor` role on the subscription. You may also want to grant `acrpull` permissions to allow your nodes to access [Azure Container Registries](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-authentication-managed-identity).
-
-</aside>
-
 * In Machines
 
 ```yaml
diff --git a/exp/api/v1alpha3/azuremachinepool_conversion.go b/exp/api/v1alpha3/azuremachinepool_conversion.go
index 46061553b..66a7ab2ad 100644
--- a/exp/api/v1alpha3/azuremachinepool_conversion.go
+++ b/exp/api/v1alpha3/azuremachinepool_conversion.go
@@ -46,8 +46,6 @@ func (src *AzureMachinePool) ConvertTo(dstRaw conversion.Hub) error { // nolint
 		}
 	}
 
-	dst.Spec.Template.SubnetName = restored.Spec.Template.SubnetName
-
 	dst.Spec.Strategy.Type = restored.Spec.Strategy.Type
 	if restored.Spec.Strategy.RollingUpdate != nil {
 
@@ -95,10 +93,6 @@ func (dst *AzureMachinePool) ConvertFrom(srcRaw conversion.Hub) error { // nolin
 	return nil
 }
 
-func Convert_v1alpha4_AzureMachinePoolMachineTemplate_To_v1alpha3_AzureMachinePoolMachineTemplate(in *expv1alpha4.AzureMachinePoolMachineTemplate, out *AzureMachinePoolMachineTemplate, s convert.Scope) error { //nolint
-	return autoConvert_v1alpha4_AzureMachinePoolMachineTemplate_To_v1alpha3_AzureMachinePoolMachineTemplate(in, out, s)
-}
-
 func Convert_v1alpha4_AzureMachinePoolSpec_To_v1alpha3_AzureMachinePoolSpec(in *expv1alpha4.AzureMachinePoolSpec, out *AzureMachinePoolSpec, s convert.Scope) error {
 	return autoConvert_v1alpha4_AzureMachinePoolSpec_To_v1alpha3_AzureMachinePoolSpec(in, out, s)
 }
diff --git a/exp/api/v1alpha3/zz_generated.conversion.go b/exp/api/v1alpha3/zz_generated.conversion.go
index 5ba9791f3..07e3d5278 100644
--- a/exp/api/v1alpha3/zz_generated.conversion.go
+++ b/exp/api/v1alpha3/zz_generated.conversion.go
@@ -75,6 +75,11 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddGeneratedConversionFunc((*v1alpha4.AzureMachinePoolMachineTemplate)(nil), (*AzureMachinePoolMachineTemplate)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha4_AzureMachinePoolMachineTemplate_To_v1alpha3_AzureMachinePoolMachineTemplate(a.(*v1alpha4.AzureMachinePoolMachineTemplate), b.(*AzureMachinePoolMachineTemplate), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*AzureMachinePoolSpec)(nil), (*v1alpha4.AzureMachinePoolSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha3_AzureMachinePoolSpec_To_v1alpha4_AzureMachinePoolSpec(a.(*AzureMachinePoolSpec), b.(*v1alpha4.AzureMachinePoolSpec), scope)
 	}); err != nil {
@@ -240,11 +245,6 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddConversionFunc((*v1alpha4.AzureMachinePoolMachineTemplate)(nil), (*AzureMachinePoolMachineTemplate)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1alpha4_AzureMachinePoolMachineTemplate_To_v1alpha3_AzureMachinePoolMachineTemplate(a.(*v1alpha4.AzureMachinePoolMachineTemplate), b.(*AzureMachinePoolMachineTemplate), scope)
-	}); err != nil {
-		return err
-	}
 	if err := s.AddConversionFunc((*v1alpha4.AzureMachinePoolSpec)(nil), (*AzureMachinePoolSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha4_AzureMachinePoolSpec_To_v1alpha3_AzureMachinePoolSpec(a.(*v1alpha4.AzureMachinePoolSpec), b.(*AzureMachinePoolSpec), scope)
 	}); err != nil {
@@ -425,10 +425,14 @@ func autoConvert_v1alpha4_AzureMachinePoolMachineTemplate_To_v1alpha3_AzureMachi
 	out.TerminateNotificationTimeout = (*int)(unsafe.Pointer(in.TerminateNotificationTimeout))
 	out.SecurityProfile = (*clusterapiproviderazureapiv1alpha3.SecurityProfile)(unsafe.Pointer(in.SecurityProfile))
 	out.SpotVMOptions = (*clusterapiproviderazureapiv1alpha3.SpotVMOptions)(unsafe.Pointer(in.SpotVMOptions))
-	// WARNING: in.SubnetName requires manual conversion: does not exist in peer-type
 	return nil
 }
 
+// Convert_v1alpha4_AzureMachinePoolMachineTemplate_To_v1alpha3_AzureMachinePoolMachineTemplate is an autogenerated conversion function.
+func Convert_v1alpha4_AzureMachinePoolMachineTemplate_To_v1alpha3_AzureMachinePoolMachineTemplate(in *v1alpha4.AzureMachinePoolMachineTemplate, out *AzureMachinePoolMachineTemplate, s conversion.Scope) error {
+	return autoConvert_v1alpha4_AzureMachinePoolMachineTemplate_To_v1alpha3_AzureMachinePoolMachineTemplate(in, out, s)
+}
+
 func autoConvert_v1alpha3_AzureMachinePoolSpec_To_v1alpha4_AzureMachinePoolSpec(in *AzureMachinePoolSpec, out *v1alpha4.AzureMachinePoolSpec, s conversion.Scope) error {
 	out.Location = in.Location
 	if err := Convert_v1alpha3_AzureMachinePoolMachineTemplate_To_v1alpha4_AzureMachinePoolMachineTemplate(&in.Template, &out.Template, s); err != nil {
diff --git a/exp/api/v1alpha4/azuremachinepool_types.go b/exp/api/v1alpha4/azuremachinepool_types.go
index 9ada3a2be..37b65e1d4 100644
--- a/exp/api/v1alpha4/azuremachinepool_types.go
+++ b/exp/api/v1alpha4/azuremachinepool_types.go
@@ -84,10 +84,6 @@ type (
 		// SpotVMOptions allows the ability to specify the Machine should use a Spot VM
 		// +optional
 		SpotVMOptions *infrav1.SpotVMOptions `json:"spotVMOptions,omitempty"`
-
-		// SubnetName selects the Subnet where the VMSS will be placed
-		// +optional
-		SubnetName string `json:"subnetName,omitempty"`
 	}
 
 	// AzureMachinePoolSpec defines the desired state of AzureMachinePool.
diff --git a/exp/api/v1alpha4/azuremachinepool_webhook.go b/exp/api/v1alpha4/azuremachinepool_webhook.go
index ac6c89f96..854ccc78b 100644
--- a/exp/api/v1alpha4/azuremachinepool_webhook.go
+++ b/exp/api/v1alpha4/azuremachinepool_webhook.go
@@ -54,7 +54,6 @@ func (amp *AzureMachinePool) Default() {
 	if err != nil {
 		azuremachinepoollog.Error(err, "SetDefaultSshPublicKey failed")
 	}
-	amp.SetIdentityDefaults()
 }
 
 // +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1alpha4-azuremachinepool,mutating=false,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=azuremachinepools,versions=v1alpha4,name=validation.azuremachinepool.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
diff --git a/exp/controllers/azuremachinepool_reconciler.go b/exp/controllers/azuremachinepool_reconciler.go
index 2a0a3e303..4b826d91e 100644
--- a/exp/controllers/azuremachinepool_reconciler.go
+++ b/exp/controllers/azuremachinepool_reconciler.go
@@ -32,7 +32,6 @@ import (
 
 // azureMachinePoolService is the group of services called by the AzureMachinePool controller.
 type azureMachinePoolService struct {
-	scope                      *scope.MachinePoolScope
 	virtualMachinesScaleSetSvc azure.Reconciler
 	skuCache                   *resourceskus.Cache
 	roleAssignmentsSvc         azure.Reconciler
@@ -49,7 +48,6 @@ func newAzureMachinePoolService(machinePoolScope *scope.MachinePoolScope) (*azur
 	}
 
 	return &azureMachinePoolService{
-		scope:                      machinePoolScope,
 		virtualMachinesScaleSetSvc: scalesets.NewService(machinePoolScope, cache),
 		skuCache:                   cache,
 		roleAssignmentsSvc:         roleassignments.New(machinePoolScope),
@@ -62,10 +60,6 @@ func (s *azureMachinePoolService) Reconcile(ctx context.Context) error {
 	ctx, span := tele.Tracer().Start(ctx, "controllers.azureMachinePoolService.Reconcile")
 	defer span.End()
 
-	if err := s.scope.SetSubnetName(); err != nil {
-		return errors.Wrap(err, "failed defaulting subnet name")
-	}
-
 	if err := s.virtualMachinesScaleSetSvc.Reconcile(ctx); err != nil {
 		return errors.Wrap(err, "failed to create scale set")
 	}
diff --git a/exp/controllers/azuremanagedmachinepool_controller.go b/exp/controllers/azuremanagedmachinepool_controller.go
index b92298a1c..f9aca267c 100644
--- a/exp/controllers/azuremanagedmachinepool_controller.go
+++ b/exp/controllers/azuremanagedmachinepool_controller.go
@@ -91,7 +91,7 @@ func (r *AzureManagedMachinePoolReconciler) SetupWithManager(ctx context.Context
 		// watch for changes in CAPI MachinePool resources
 		Watches(
 			&source.Kind{Type: &clusterv1exp.MachinePool{}},
-			handler.EnqueueRequestsFromMapFunc(MachinePoolToInfrastructureMapFunc(clusterv1exp.GroupVersion.WithKind("AzureManagedMachinePool"), ctrl.LoggerFrom(ctx))),
+			handler.EnqueueRequestsFromMapFunc(util.MachineToInfrastructureMapFunc(infrav1exp.GroupVersion.WithKind("AzureManagedMachinePool"))),
 		).
 		// watch for changes in AzureManagedControlPlanes
 		Watches(
diff --git a/go.mod b/go.mod
index 128a3424e..32bffda42 100644
--- a/go.mod
+++ b/go.mod
@@ -44,7 +44,6 @@ require (
 	sigs.k8s.io/cluster-api/test v0.4.0
 	sigs.k8s.io/controller-runtime v0.9.1
 	sigs.k8s.io/kind v0.11.1
-	sigs.k8s.io/yaml v1.2.0
 )
 
 replace sigs.k8s.io/cluster-api => sigs.k8s.io/cluster-api v0.4.0
diff --git a/templates/cluster-template-machinepool-multiple-subnets.yaml b/templates/cluster-template-machinepool-multiple-subnets.yaml
deleted file mode 100644
index b8edcf02c..000000000
--- a/templates/cluster-template-machinepool-multiple-subnets.yaml
+++ /dev/null
@@ -1,294 +0,0 @@
-apiVersion: cluster.x-k8s.io/v1alpha4
-kind: Cluster
-metadata:
-  labels:
-    cni: calico
-  name: ${CLUSTER_NAME}
-  namespace: default
-spec:
-  clusterNetwork:
-    pods:
-      cidrBlocks:
-      - 192.168.0.0/16
-  controlPlaneRef:
-    apiVersion: controlplane.cluster.x-k8s.io/v1alpha4
-    kind: KubeadmControlPlane
-    name: ${CLUSTER_NAME}-control-plane
-  infrastructureRef:
-    apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-    kind: AzureCluster
-    name: ${CLUSTER_NAME}
----
-apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-kind: AzureCluster
-metadata:
-  name: ${CLUSTER_NAME}
-  namespace: default
-spec:
-  identityRef:
-    apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-    kind: AzureClusterIdentity
-    name: ${CLUSTER_IDENTITY_NAME}
-  location: ${AZURE_LOCATION}
-  networkSpec:
-    subnets:
-    - name: control-plane-subnet
-      role: control-plane
-    - name: ${CLUSTER_NAME}-mp-0
-      role: node
-    - name: ${CLUSTER_NAME}-mp-1
-      role: node
-    vnet:
-      name: ${AZURE_VNET_NAME:=${CLUSTER_NAME}-vnet}
-  resourceGroup: ${AZURE_RESOURCE_GROUP:=${CLUSTER_NAME}}
-  subscriptionID: ${AZURE_SUBSCRIPTION_ID}
----
-apiVersion: controlplane.cluster.x-k8s.io/v1alpha4
-kind: KubeadmControlPlane
-metadata:
-  name: ${CLUSTER_NAME}-control-plane
-  namespace: default
-spec:
-  kubeadmConfigSpec:
-    clusterConfiguration:
-      apiServer:
-        extraArgs:
-          cloud-config: /etc/kubernetes/azure.json
-          cloud-provider: azure
-        extraVolumes:
-        - hostPath: /etc/kubernetes/azure.json
-          mountPath: /etc/kubernetes/azure.json
-          name: cloud-config
-          readOnly: true
-        timeoutForControlPlane: 20m
-      controllerManager:
-        extraArgs:
-          allocate-node-cidrs: "false"
-          cloud-config: /etc/kubernetes/azure.json
-          cloud-provider: azure
-          cluster-name: ${CLUSTER_NAME}
-        extraVolumes:
-        - hostPath: /etc/kubernetes/azure.json
-          mountPath: /etc/kubernetes/azure.json
-          name: cloud-config
-          readOnly: true
-      etcd:
-        local:
-          dataDir: /var/lib/etcddisk/etcd
-    diskSetup:
-      filesystems:
-      - device: /dev/disk/azure/scsi1/lun0
-        extraOpts:
-        - -E
-        - lazy_itable_init=1,lazy_journal_init=1
-        filesystem: ext4
-        label: etcd_disk
-      - device: ephemeral0.1
-        filesystem: ext4
-        label: ephemeral0
-        replaceFS: ntfs
-      partitions:
-      - device: /dev/disk/azure/scsi1/lun0
-        layout: true
-        overwrite: false
-        tableType: gpt
-    files:
-    - contentFrom:
-        secret:
-          key: control-plane-azure.json
-          name: ${CLUSTER_NAME}-control-plane-azure-json
-      owner: root:root
-      path: /etc/kubernetes/azure.json
-      permissions: "0644"
-    initConfiguration:
-      nodeRegistration:
-        kubeletExtraArgs:
-          azure-container-registry-config: /etc/kubernetes/azure.json
-          cloud-config: /etc/kubernetes/azure.json
-          cloud-provider: azure
-        name: '{{ ds.meta_data["local_hostname"] }}'
-    joinConfiguration:
-      nodeRegistration:
-        kubeletExtraArgs:
-          azure-container-registry-config: /etc/kubernetes/azure.json
-          cloud-config: /etc/kubernetes/azure.json
-          cloud-provider: azure
-        name: '{{ ds.meta_data["local_hostname"] }}'
-    mounts:
-    - - LABEL=etcd_disk
-      - /var/lib/etcddisk
-  machineTemplate:
-    infrastructureRef:
-      apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-      kind: AzureMachineTemplate
-      name: ${CLUSTER_NAME}-control-plane
-  replicas: ${CONTROL_PLANE_MACHINE_COUNT}
-  version: ${KUBERNETES_VERSION}
----
-apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-kind: AzureMachineTemplate
-metadata:
-  name: ${CLUSTER_NAME}-control-plane
-  namespace: default
-spec:
-  template:
-    spec:
-      dataDisks:
-      - diskSizeGB: 256
-        lun: 0
-        nameSuffix: etcddisk
-      osDisk:
-        diskSizeGB: 128
-        osType: Linux
-      sshPublicKey: ${AZURE_SSH_PUBLIC_KEY_B64:=""}
-      vmSize: ${AZURE_CONTROL_PLANE_MACHINE_TYPE}
----
-apiVersion: cluster.x-k8s.io/v1alpha4
-kind: MachinePool
-metadata:
-  name: ${CLUSTER_NAME}-mp-0
-  namespace: default
-spec:
-  clusterName: ${CLUSTER_NAME}
-  replicas: ${WORKER_MACHINE_COUNT}
-  template:
-    spec:
-      bootstrap:
-        configRef:
-          apiVersion: bootstrap.cluster.x-k8s.io/v1alpha4
-          kind: KubeadmConfig
-          name: ${CLUSTER_NAME}-mp-0
-      clusterName: ${CLUSTER_NAME}
-      infrastructureRef:
-        apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-        kind: AzureMachinePool
-        name: ${CLUSTER_NAME}-mp-0
-      version: ${KUBERNETES_VERSION}
----
-apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-kind: AzureMachinePool
-metadata:
-  name: ${CLUSTER_NAME}-mp-0
-  namespace: default
-spec:
-  location: ${AZURE_LOCATION}
-  strategy:
-    rollingUpdate:
-      deletePolicy: Oldest
-      maxSurge: 25%
-      maxUnavailable: 1
-    type: RollingUpdate
-  template:
-    osDisk:
-      diskSizeGB: 30
-      managedDisk:
-        storageAccountType: Premium_LRS
-      osType: Linux
-    sshPublicKey: ${AZURE_SSH_PUBLIC_KEY_B64:=""}
-    subnetName: ${CLUSTER_NAME}-mp-0
-    vmSize: ${AZURE_NODE_MACHINE_TYPE}
----
-apiVersion: bootstrap.cluster.x-k8s.io/v1alpha4
-kind: KubeadmConfig
-metadata:
-  name: ${CLUSTER_NAME}-mp-0
-  namespace: default
-spec:
-  files:
-  - contentFrom:
-      secret:
-        key: worker-node-azure.json
-        name: ${CLUSTER_NAME}-mp-0-azure-json
-    owner: root:root
-    path: /etc/kubernetes/azure.json
-    permissions: "0644"
-  joinConfiguration:
-    nodeRegistration:
-      kubeletExtraArgs:
-        azure-container-registry-config: /etc/kubernetes/azure.json
-        cloud-config: /etc/kubernetes/azure.json
-        cloud-provider: azure
-      name: '{{ ds.meta_data["local_hostname"] }}'
----
-apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-kind: AzureClusterIdentity
-metadata:
-  labels:
-    clusterctl.cluster.x-k8s.io/move-hierarchy: "true"
-  name: ${CLUSTER_IDENTITY_NAME}
-  namespace: default
-spec:
-  allowedNamespaces: {}
-  clientID: ${AZURE_CLIENT_ID}
-  clientSecret:
-    name: ${AZURE_CLUSTER_IDENTITY_SECRET_NAME}
-    namespace: ${AZURE_CLUSTER_IDENTITY_SECRET_NAMESPACE}
-  tenantID: ${AZURE_TENANT_ID}
-  type: ServicePrincipal
----
-apiVersion: cluster.x-k8s.io/v1alpha4
-kind: MachinePool
-metadata:
-  name: ${CLUSTER_NAME}-mp-1
-  namespace: default
-spec:
-  clusterName: ${CLUSTER_NAME}
-  replicas: ${WORKER_MACHINE_COUNT}
-  template:
-    spec:
-      bootstrap:
-        configRef:
-          apiVersion: bootstrap.cluster.x-k8s.io/v1alpha4
-          kind: KubeadmConfig
-          name: ${CLUSTER_NAME}-mp-1
-      clusterName: ${CLUSTER_NAME}
-      infrastructureRef:
-        apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-        kind: AzureMachinePool
-        name: ${CLUSTER_NAME}-mp-1
-      version: ${KUBERNETES_VERSION}
----
-apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-kind: AzureMachinePool
-metadata:
-  name: ${CLUSTER_NAME}-mp-1
-  namespace: default
-spec:
-  location: ${AZURE_LOCATION}
-  strategy:
-    rollingUpdate:
-      deletePolicy: Oldest
-      maxSurge: 25%
-      maxUnavailable: 1
-    type: RollingUpdate
-  template:
-    osDisk:
-      diskSizeGB: 30
-      managedDisk:
-        storageAccountType: Premium_LRS
-      osType: Linux
-    sshPublicKey: ${AZURE_SSH_PUBLIC_KEY_B64:=""}
-    subnetName: ${CLUSTER_NAME}-mp-1
-    vmSize: ${AZURE_NODE_MACHINE_TYPE}
----
-apiVersion: bootstrap.cluster.x-k8s.io/v1alpha4
-kind: KubeadmConfig
-metadata:
-  name: ${CLUSTER_NAME}-mp-1
-  namespace: default
-spec:
-  files:
-  - contentFrom:
-      secret:
-        key: worker-node-azure.json
-        name: ${CLUSTER_NAME}-mp-0-azure-json
-    owner: root:root
-    path: /etc/kubernetes/azure.json
-    permissions: "0644"
-  joinConfiguration:
-    nodeRegistration:
-      kubeletExtraArgs:
-        azure-container-registry-config: /etc/kubernetes/azure.json
-        cloud-config: /etc/kubernetes/azure.json
-        cloud-provider: azure
-      name: '{{ ds.meta_data["local_hostname"] }}'
diff --git a/templates/flavors/machinepool-multiple-subnets/kustomization.yaml b/templates/flavors/machinepool-multiple-subnets/kustomization.yaml
deleted file mode 100644
index 4cd6ffe71..000000000
--- a/templates/flavors/machinepool-multiple-subnets/kustomization.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace: default
-resources:
-  - ../machinepool
-  - machine-pool-deployment.yaml
-patchesStrategicMerge:
-  - patches/machine-pool-subnet.yaml
-  - patches/azurecluster-subnets.yaml
diff --git a/templates/flavors/machinepool-multiple-subnets/machine-pool-deployment.yaml b/templates/flavors/machinepool-multiple-subnets/machine-pool-deployment.yaml
deleted file mode 100644
index 93a05c700..000000000
--- a/templates/flavors/machinepool-multiple-subnets/machine-pool-deployment.yaml
+++ /dev/null
@@ -1,64 +0,0 @@
----
-apiVersion: cluster.x-k8s.io/v1alpha4
-kind: MachinePool
-metadata:
-  name: "${CLUSTER_NAME}-mp-1"
-spec:
-  clusterName: "${CLUSTER_NAME}"
-  replicas: ${WORKER_MACHINE_COUNT}
-  template:
-    spec:
-      clusterName: "${CLUSTER_NAME}"
-      version: "${KUBERNETES_VERSION}"
-      bootstrap:
-        configRef:
-          name: "${CLUSTER_NAME}-mp-1"
-          apiVersion: bootstrap.cluster.x-k8s.io/v1alpha4
-          kind: KubeadmConfig
-      infrastructureRef:
-        name: "${CLUSTER_NAME}-mp-1"
-        apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-        kind: AzureMachinePool
----
-apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-kind: AzureMachinePool
-metadata:
-  name: "${CLUSTER_NAME}-mp-1"
-spec:
-  location: ${AZURE_LOCATION}
-  strategy:
-    type: RollingUpdate
-    rollingUpdate:
-      maxSurge: 25%
-      maxUnavailable: 1
-      deletePolicy: Oldest
-  template:
-    vmSize: ${AZURE_NODE_MACHINE_TYPE}
-    osDisk:
-      osType: "Linux"
-      diskSizeGB: 30
-      managedDisk:
-        storageAccountType: "Premium_LRS"
-    sshPublicKey: ${AZURE_SSH_PUBLIC_KEY_B64:=""}
-    subnetName: "${CLUSTER_NAME}-mp-1"
----
-apiVersion: bootstrap.cluster.x-k8s.io/v1alpha4
-kind: KubeadmConfig
-metadata:
-  name: "${CLUSTER_NAME}-mp-1"
-spec:
-  joinConfiguration:
-    nodeRegistration:
-      name: '{{ ds.meta_data["local_hostname"] }}'
-      kubeletExtraArgs:
-        cloud-provider: azure
-        cloud-config: /etc/kubernetes/azure.json
-        azure-container-registry-config: /etc/kubernetes/azure.json
-  files:
-  - contentFrom:
-      secret:
-        name: ${CLUSTER_NAME}-mp-0-azure-json
-        key: worker-node-azure.json
-    owner: root:root
-    path: /etc/kubernetes/azure.json
-    permissions: "0644"
diff --git a/templates/flavors/machinepool-multiple-subnets/patches/azurecluster-subnets.yaml b/templates/flavors/machinepool-multiple-subnets/patches/azurecluster-subnets.yaml
deleted file mode 100644
index 1287b1808..000000000
--- a/templates/flavors/machinepool-multiple-subnets/patches/azurecluster-subnets.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
----
-apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-kind: AzureCluster
-metadata:
-  name: ${CLUSTER_NAME}
-spec:
-  networkSpec:
-    subnets:
-      - name: control-plane-subnet
-        role: control-plane
-      - name: "${CLUSTER_NAME}-mp-0"
-        role: node
-      - name: "${CLUSTER_NAME}-mp-1"
-        role: node
diff --git a/templates/flavors/machinepool-multiple-subnets/patches/machine-pool-subnet.yaml b/templates/flavors/machinepool-multiple-subnets/patches/machine-pool-subnet.yaml
deleted file mode 100644
index a09a5dd2c..000000000
--- a/templates/flavors/machinepool-multiple-subnets/patches/machine-pool-subnet.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-kind: AzureMachinePool
-apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
-metadata:
-  name: "${CLUSTER_NAME}-mp-0"
-spec:
-  template:
-    subnetName: "${CLUSTER_NAME}-mp-0"


Test patch:
diff --git a/api/v1alpha4/azurecluster_validation_test.go b/api/v1alpha4/azurecluster_validation_test.go
index 0ac8eb817..ba7edaffc 100644
--- a/api/v1alpha4/azurecluster_validation_test.go
+++ b/api/v1alpha4/azurecluster_validation_test.go
@@ -273,10 +273,13 @@ func TestNetworkSpecWithPreexistingVnetLackRequiredSubnets(t *testing.T) {
 
 	t.Run(testCase.name, func(t *testing.T) {
 		errs := validateNetworkSpec(testCase.networkSpec, NetworkSpec{}, field.NewPath("spec").Child("networkSpec"))
-		g.Expect(errs).To(HaveLen(1))
+		g.Expect(errs).To(HaveLen(2))
 		g.Expect(errs[0].Type).To(Equal(field.ErrorTypeRequired))
 		g.Expect(errs[0].Field).To(Equal("spec.networkSpec.subnets"))
 		g.Expect(errs[0].Error()).To(ContainSubstring("required role node not included"))
+		g.Expect(errs[1].Type).To(Equal(field.ErrorTypeInvalid))
+		g.Expect(errs[1].Field).To(Equal("spec.networkSpec.subnets"))
+		g.Expect(errs[1].Error()).To(ContainSubstring("NodeSubnet invalid"))
 	})
 }
 
@@ -956,6 +959,7 @@ func TestValidateNodeOutboundLB(t *testing.T) {
 		lb          *LoadBalancerSpec
 		old         *LoadBalancerSpec
 		apiServerLB LoadBalancerSpec
+		nodeSubnet  SubnetSpec
 		wantErr     bool
 		expectedErr field.Error
 	}{
@@ -963,6 +967,7 @@ func TestValidateNodeOutboundLB(t *testing.T) {
 			name:        "no lb for public clusters",
 			lb:          nil,
 			apiServerLB: LoadBalancerSpec{Type: Public},
+			nodeSubnet:  createValidNodeSubnet(),
 			wantErr:     true,
 			expectedErr: field.Error{
 				Type:     "FieldValueRequired",
@@ -985,7 +990,8 @@ func TestValidateNodeOutboundLB(t *testing.T) {
 			old: &LoadBalancerSpec{
 				ID: "old-id",
 			},
-			wantErr: true,
+			nodeSubnet: createValidNodeSubnet(),
+			wantErr:    true,
 			expectedErr: field.Error{
 				Type:     "FieldValueForbidden",
 				Field:    "nodeOutboundLB.id",
@@ -1001,7 +1007,8 @@ func TestValidateNodeOutboundLB(t *testing.T) {
 			old: &LoadBalancerSpec{
 				Name: "old-name",
 			},
-			wantErr: true,
+			nodeSubnet: createValidNodeSubnet(),
+			wantErr:    true,
 			expectedErr: field.Error{
 				Type:     "FieldValueForbidden",
 				Field:    "nodeOutboundLB.name",
@@ -1017,7 +1024,8 @@ func TestValidateNodeOutboundLB(t *testing.T) {
 			old: &LoadBalancerSpec{
 				SKU: "old-sku",
 			},
-			wantErr: true,
+			nodeSubnet: createValidNodeSubnet(),
+			wantErr:    true,
 			expectedErr: field.Error{
 				Type:     "FieldValueForbidden",
 				Field:    "nodeOutboundLB.sku",
@@ -1037,7 +1045,8 @@ func TestValidateNodeOutboundLB(t *testing.T) {
 					Name: "old-frontend-ip",
 				}},
 			},
-			wantErr: true,
+			nodeSubnet: createValidNodeSubnet(),
+			wantErr:    true,
 			expectedErr: field.Error{
 				Type:  "FieldValueForbidden",
 				Field: "nodeOutboundLB.frontendIPs[0]",
@@ -1062,14 +1071,16 @@ func TestValidateNodeOutboundLB(t *testing.T) {
 					Name: "old-frontend-ip",
 				}},
 			},
-			wantErr: false,
+			nodeSubnet: createValidNodeSubnet(),
+			wantErr:    false,
 		},
 		{
 			name: "frontend ips count exceeds max value",
 			lb: &LoadBalancerSpec{
 				FrontendIPsCount: pointer.Int32Ptr(100),
 			},
-			wantErr: true,
+			nodeSubnet: createValidNodeSubnet(),
+			wantErr:    true,
 			expectedErr: field.Error{
 				Type:     "FieldValueInvalid",
 				Field:    "nodeOutboundLB.frontendIPsCount",
@@ -1077,13 +1088,20 @@ func TestValidateNodeOutboundLB(t *testing.T) {
 				Detail:   "Max front end ips allowed is 16",
 			},
 		},
+		{
+			name:        "no lb when using nat gateway",
+			lb:          nil,
+			apiServerLB: LoadBalancerSpec{Type: Public},
+			nodeSubnet:  createValidNodeSubnetWithNatGateway(),
+			wantErr:     false,
+		},
 	}
 
 	for _, test := range testcases {
 		test := test
 		t.Run(test.name, func(t *testing.T) {
 			t.Parallel()
-			err := validateNodeOutboundLB(test.lb, test.old, test.apiServerLB, field.NewPath("nodeOutboundLB"))
+			err := validateNodeOutboundLB(test.lb, test.old, test.apiServerLB, test.nodeSubnet, field.NewPath("nodeOutboundLB"))
 			if test.wantErr {
 				g.Expect(err).NotTo(HaveLen(0))
 				found := false
@@ -1306,6 +1324,21 @@ func createValidSubnets() Subnets {
 	}
 }
 
+func createValidNodeSubnetWithNatGateway() SubnetSpec {
+	return SubnetSpec{
+		Role:       "node",
+		Name:       "node-subnet",
+		NatGateway: NatGateway{Name: "node-natgateway"},
+	}
+}
+
+func createValidNodeSubnet() SubnetSpec {
+	return SubnetSpec{
+		Role: "node",
+		Name: "node-subnet",
+	}
+}
+
 func createValidVnet() VnetSpec {
 	return VnetSpec{
 		ResourceGroup: "custom-vnet",
diff --git a/azure/services/agentpools/agentpools_test.go b/azure/services/agentpools/agentpools_test.go
index 26eeac90d..e91b128c5 100644
--- a/azure/services/agentpools/agentpools_test.go
+++ b/azure/services/agentpools/agentpools_test.go
@@ -92,7 +92,7 @@ func TestReconcile(t *testing.T) {
 				Name:          "my-agentpool",
 			},
 			provisioningStatesToTest: []string{"Deleting", "InProgress", "randomStringHere"},
-			expectedError:            "",
+			expectedError:            "Unable to update existing agent pool in non terminal state. Agent pool must be in one of the following provisioning states: canceled, failed, or succeeded. Actual state: randomStringHere",
 			expect: func(m *mock_agentpools.MockClientMockRecorder, provisioningstate string) {
 				m.Get(gomockinternal.AContext(), "my-rg", "my-cluster", "my-agentpool").Return(containerservice.AgentPool{ManagedClusterAgentPoolProfileProperties: &containerservice.ManagedClusterAgentPoolProfileProperties{
 					ProvisioningState: &provisioningstate,
@@ -122,8 +122,8 @@ func TestReconcile(t *testing.T) {
 
 				err := s.Reconcile(context.TODO(), &tc.agentpoolSpec)
 				if tc.expectedError != "" {
-					g.Expect(err).To(HaveOccurred())
-					g.Expect(err).To(MatchError(tc.expectedError))
+					g.Expect(err.Error()).To(HavePrefix(tc.expectedError))
+					g.Expect(err.Error()).To(ContainSubstring(provisioningstate))
 				} else {
 					g.Expect(err).NotTo(HaveOccurred())
 				}
diff --git a/azure/services/managedclusters/managedclusters_test.go b/azure/services/managedclusters/managedclusters_test.go
index 461f1a959..f383e6c1f 100644
--- a/azure/services/managedclusters/managedclusters_test.go
+++ b/azure/services/managedclusters/managedclusters_test.go
@@ -60,7 +60,7 @@ func TestReconcile(t *testing.T) {
 				ResourceGroupName: "my-rg",
 			},
 			provisioningStatesToTest: []string{"Deleting", "InProgress", "randomStringHere"},
-			expectedError:            "",
+			expectedError:            "Unable to update existing managed cluster in non terminal state. Managed cluster must be in one of the following provisioning states: canceled, failed, or succeeded. Actual state",
 			expect: func(m *mock_managedclusters.MockClientMockRecorder, provisioningstate string) {
 				m.Get(gomockinternal.AContext(), "my-rg", "my-managedcluster").Return(containerservice.ManagedCluster{ManagedClusterProperties: &containerservice.ManagedClusterProperties{
 					ProvisioningState: &provisioningstate,
@@ -89,7 +89,8 @@ func TestReconcile(t *testing.T) {
 				err := s.Reconcile(context.TODO(), &tc.managedclusterspec)
 				if tc.expectedError != "" {
 					g.Expect(err).To(HaveOccurred())
-					g.Expect(err).To(MatchError(tc.expectedError))
+					g.Expect(err.Error()).To(HavePrefix(tc.expectedError))
+					g.Expect(err.Error()).To(ContainSubstring(provisioningstate))
 				} else {
 					g.Expect(err).NotTo(HaveOccurred())
 				}
diff --git a/azure/services/natgateways/natgateways_test.go b/azure/services/natgateways/natgateways_test.go
index d598fa754..6a6b65968 100644
--- a/azure/services/natgateways/natgateways_test.go
+++ b/azure/services/natgateways/natgateways_test.go
@@ -85,30 +85,19 @@ func TestReconcileNatGateways(t *testing.T) {
 							Name: "node-subnet",
 							Role: infrav1.SubnetNode,
 						},
-						NatGatewayIP: infrav1.PublicIPSpec{Name: "pip-node-subnet"},
 					},
 				})
 
 				s.SubscriptionID().AnyTimes().Return("123")
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-node-natgateway").Return(network.NatGateway{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")).Times(1)
+				s.SetNodeNatGatewayID(gomock.Any()).Times(1)
 				s.Location().Return("westus")
-				s.SetSubnet(infrav1.SubnetSpec{
-					Role: infrav1.SubnetNode,
-					Name: "node-subnet",
-					NatGateway: infrav1.NatGateway{
-						ID:   "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/natGateways/my-node-natgateway",
-						Name: "my-node-natgateway",
-						NatGatewayIP: infrav1.PublicIPSpec{
-							Name: "pip-node-subnet",
-						},
-					},
-				})
 				m.CreateOrUpdate(gomockinternal.AContext(), "my-rg", "my-node-natgateway", gomock.AssignableToTypeOf(network.NatGateway{})).Times(1)
 			},
 		},
 		{
-			name: "update nat gateway if already exists but it's out of date",
+			name: "update nat gateway if already exists",
 			tags: infrav1.Tags{
 				"Name": "my-vnet",
 				"sigs.k8s.io_cluster-api-provider-azure_cluster_test-cluster": "owned",
@@ -128,9 +117,6 @@ func TestReconcileNatGateways(t *testing.T) {
 							Name: "node-subnet",
 							Role: infrav1.SubnetNode,
 						},
-						NatGatewayIP: infrav1.PublicIPSpec{
-							Name: "different-pip-name",
-						},
 					},
 				})
 
@@ -138,75 +124,14 @@ func TestReconcileNatGateways(t *testing.T) {
 				s.ResourceGroup().Return("my-rg").AnyTimes()
 				m.Get(gomockinternal.AContext(), "my-rg", "my-node-natgateway").Times(1).Return(network.NatGateway{
 					Name: to.StringPtr("my-node-natgateway"),
-					ID:   to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/natGateways/my-node-natgateway"),
+					ID:   to.StringPtr("1"),
 					NatGatewayPropertiesFormat: &network.NatGatewayPropertiesFormat{PublicIPAddresses: &[]network.SubResource{
-						{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/publicIPAddresses/pip-my-node-natgateway-node-subnet-natgw")},
+						{ID: to.StringPtr("1")},
 					}},
 				}, nil)
-				s.SetSubnet(infrav1.SubnetSpec{
-					Role: infrav1.SubnetNode,
-					Name: "node-subnet",
-					NatGateway: infrav1.NatGateway{
-						ID:   "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/natGateways/my-node-natgateway",
-						Name: "my-node-natgateway",
-						NatGatewayIP: infrav1.PublicIPSpec{
-							Name: "different-pip-name",
-						},
-					},
-				})
+				s.SetNodeNatGatewayID(gomock.Any()).Times(2)
 				s.Location().Return("westus")
-				m.CreateOrUpdate(gomockinternal.AContext(), "my-rg", "my-node-natgateway", gomock.AssignableToTypeOf(network.NatGateway{}))
-			},
-		},
-		{
-			name: "nat gateway is not updated if it's up to date",
-			tags: infrav1.Tags{
-				"Name": "my-vnet",
-				"sigs.k8s.io_cluster-api-provider-azure_cluster_test-cluster": "owned",
-				"sigs.k8s.io_cluster-api-provider-azure_role":                 "common",
-			},
-			expectedError: "",
-			expect: func(s *mock_natgateways.MockNatGatewayScopeMockRecorder, m *mock_natgateways.MockclientMockRecorder) {
-				s.Vnet().Return(&infrav1.VnetSpec{
-					Name: "my-vnet",
-				})
-				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
-				s.ClusterName()
-				s.NatGatewaySpecs().Return([]azure.NatGatewaySpec{
-					{
-						Name: "my-node-natgateway",
-						Subnet: infrav1.SubnetSpec{
-							Name: "node-subnet",
-							Role: infrav1.SubnetNode,
-						},
-						NatGatewayIP: infrav1.PublicIPSpec{
-							Name: "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/publicIPAddresses/pip-my-node-natgateway-node-subnet-natgw",
-						},
-					},
-				})
-
-				s.SubscriptionID().AnyTimes().Return("123")
-				s.ResourceGroup().Return("my-rg").AnyTimes()
-				m.Get(gomockinternal.AContext(), "my-rg", "my-node-natgateway").Times(1).Return(network.NatGateway{
-					Name: to.StringPtr("my-node-natgateway"),
-					ID:   to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/natGateways/my-node-natgateway"),
-					NatGatewayPropertiesFormat: &network.NatGatewayPropertiesFormat{PublicIPAddresses: &[]network.SubResource{
-						{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/publicIPAddresses/pip-my-node-natgateway-node-subnet-natgw")},
-					}},
-				}, nil)
-				s.SetSubnet(infrav1.SubnetSpec{
-					Role: infrav1.SubnetNode,
-					Name: "node-subnet",
-					NatGateway: infrav1.NatGateway{
-						ID:   "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/natGateways/my-node-natgateway",
-						Name: "my-node-natgateway",
-						NatGatewayIP: infrav1.PublicIPSpec{
-							Name: "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/publicIPAddresses/pip-my-node-natgateway-node-subnet-natgw",
-						},
-					},
-				})
-				s.Location().Return("westus").Times(0)
-				m.CreateOrUpdate(gomockinternal.AContext(), "my-rg", "my-node-natgateway", gomock.AssignableToTypeOf(network.NatGateway{})).Times(0)
+				m.CreateOrUpdate(gomockinternal.AContext(), "my-rg", "my-node-natgateway", gomock.AssignableToTypeOf(network.NatGateway{})).Times(1)
 			},
 		},
 		{
@@ -235,6 +160,7 @@ func TestReconcileNatGateways(t *testing.T) {
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-node-natgateway").Return(network.NatGateway{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error"))
 				m.CreateOrUpdate(gomockinternal.AContext(), gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(network.NatGateway{})).Times(0)
+				s.NodeNatGateway().Times(0)
 			},
 		},
 		{
diff --git a/azure/services/routetables/routetables_test.go b/azure/services/routetables/routetables_test.go
index 7afdad7c4..a9755a32f 100644
--- a/azure/services/routetables/routetables_test.go
+++ b/azure/services/routetables/routetables_test.go
@@ -99,6 +99,7 @@ func TestReconcileRouteTables(t *testing.T) {
 				m.Get(gomockinternal.AContext(), "my-rg", "my-cp-routetable").Return(network.RouteTable{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found"))
 				s.Location().Return("westus")
 				m.CreateOrUpdate(gomockinternal.AContext(), "my-rg", "my-cp-routetable", gomock.AssignableToTypeOf(network.RouteTable{}))
+				s.NodeRouteTable().AnyTimes().Return(infrav1.RouteTable{Name: "my-node-routetable"})
 				m.Get(gomockinternal.AContext(), "my-rg", "my-node-routetable").Return(network.RouteTable{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found"))
 				s.Location().Return("westus")
 				m.CreateOrUpdate(gomockinternal.AContext(), "my-rg", "my-node-routetable", gomock.AssignableToTypeOf(network.RouteTable{}))
@@ -149,6 +150,8 @@ func TestReconcileRouteTables(t *testing.T) {
 						Name: "my-cp-routetable",
 					},
 				}).Times(1)
+				s.NodeSubnet().AnyTimes().Return(infrav1.SubnetSpec{Name: "node-subnet", Role: infrav1.SubnetNode})
+				s.NodeRouteTable().AnyTimes().Return(infrav1.RouteTable{Name: "my-node-routetable"})
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-node-routetable").Return(network.RouteTable{
 					Name: to.StringPtr("my-node-routetable"),
@@ -190,6 +193,7 @@ func TestReconcileRouteTables(t *testing.T) {
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-cp-routetable").Return(network.RouteTable{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error"))
 				m.CreateOrUpdate(gomockinternal.AContext(), gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(network.RouteTable{})).Times(0)
+				s.NodeRouteTable().Times(0)
 			},
 		},
 		{
@@ -308,6 +312,7 @@ func TestDeleteRouteTable(t *testing.T) {
 				s.ControlPlaneRouteTable().AnyTimes().Return(infrav1.RouteTable{Name: "my-cp-routetable"})
 				s.ResourceGroup().Return("my-rg")
 				m.Delete(gomockinternal.AContext(), "my-rg", "my-cp-routetable")
+				s.NodeRouteTable().AnyTimes().Return(infrav1.RouteTable{Name: "my-node-routetable"})
 				s.ResourceGroup().Return("my-rg")
 				m.Delete(gomockinternal.AContext(), "my-rg", "my-node-routetable")
 			},
@@ -345,6 +350,7 @@ func TestDeleteRouteTable(t *testing.T) {
 				s.ControlPlaneRouteTable().AnyTimes().Return(infrav1.RouteTable{Name: "my-cp-routetable"})
 				s.ResourceGroup().Return("my-rg")
 				m.Delete(gomockinternal.AContext(), "my-rg", "my-cp-routetable").Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not Found"))
+				s.NodeRouteTable().AnyTimes().Return(infrav1.RouteTable{Name: "my-node-routetable"})
 				s.ResourceGroup().Return("my-rg")
 				m.Delete(gomockinternal.AContext(), "my-rg", "my-node-routetable").Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not Found"))
 			},
@@ -373,6 +379,7 @@ func TestDeleteRouteTable(t *testing.T) {
 				s.ControlPlaneRouteTable().AnyTimes().Return(infrav1.RouteTable{Name: "my-cp-routetable"})
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				m.Delete(gomockinternal.AContext(), "my-rg", "my-cp-routetable").Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error"))
+				s.NodeRouteTable().Times(0)
 			},
 		},
 	}
diff --git a/azure/services/scalesets/scalesets_test.go b/azure/services/scalesets/scalesets_test.go
index aa2ad8d96..087488c0e 100644
--- a/azure/services/scalesets/scalesets_test.go
+++ b/azure/services/scalesets/scalesets_test.go
@@ -21,8 +21,6 @@ import (
 	"net/http"
 	"testing"
 
-	"k8s.io/utils/pointer"
-
 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-30/compute"
 	"github.com/Azure/go-autorest/autorest"
 	"github.com/Azure/go-autorest/autorest/to"
@@ -112,6 +110,7 @@ func TestGetExistingVMSS(t *testing.T) {
 			result:        &azure.VMSS{},
 			expectedError: "failed to get existing vmss: #: Not found: StatusCode=404",
 			expect: func(s *mock_scalesets.MockScaleSetScopeMockRecorder, m *mock_scalesets.MockClientMockRecorder) {
+				s.ScaleSetSpec().Return(newDefaultVMSSSpec())
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vmss").Return(compute.VirtualMachineScaleSet{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found"))
@@ -140,6 +139,7 @@ func TestGetExistingVMSS(t *testing.T) {
 			},
 			expectedError: "",
 			expect: func(s *mock_scalesets.MockScaleSetScopeMockRecorder, m *mock_scalesets.MockClientMockRecorder) {
+				s.ScaleSetSpec().Return(newDefaultVMSSSpec())
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vmss").Return(compute.VirtualMachineScaleSet{
@@ -176,6 +176,7 @@ func TestGetExistingVMSS(t *testing.T) {
 			result:        &azure.VMSS{},
 			expectedError: "failed to list instances: #: Not found: StatusCode=404",
 			expect: func(s *mock_scalesets.MockScaleSetScopeMockRecorder, m *mock_scalesets.MockClientMockRecorder) {
+				s.ScaleSetSpec().Return(newDefaultVMSSSpec())
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vmss").Return(compute.VirtualMachineScaleSet{
@@ -209,7 +210,7 @@ func TestGetExistingVMSS(t *testing.T) {
 				Client: clientMock,
 			}
 
-			result, err := s.getVirtualMachineScaleSet(context.TODO(), tc.vmssName)
+			result, err := s.getVirtualMachineScaleSet(context.TODO())
 			if tc.expectedError != "" {
 				g.Expect(err).To(HaveOccurred())
 				t.Log(err.Error())
@@ -247,21 +248,12 @@ func TestReconcileVMSS(t *testing.T) {
 			expectedError: "failed to get VMSS my-vmss after create or update: failed to get result from future: operation type PUT on Azure resource my-rg/my-vmss is not done",
 			expect: func(g *WithT, s *mock_scalesets.MockScaleSetScopeMockRecorder, m *mock_scalesets.MockClientMockRecorder) {
 				defaultSpec := newDefaultVMSSSpec()
-				defaultSpec.DataDisks = append(defaultSpec.DataDisks, infrav1.DataDisk{
-					NameSuffix: "my_disk_with_ultra_disks",
-					DiskSizeGB: 128,
-					Lun:        to.Int32Ptr(3),
-					ManagedDisk: &infrav1.ManagedDiskParameters{
-						StorageAccountType: "UltraSSD_LRS",
-					},
-				})
 				s.ScaleSetSpec().Return(defaultSpec).AnyTimes()
 				setupDefaultVMSSStartCreatingExpectations(s, m)
-				vmss := newDefaultVMSS("VM_SIZE")
-				vmss.VirtualMachineScaleSetProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{UltraSSDEnabled: pointer.Bool(true)}
+				vmss := newDefaultVMSS()
 				m.CreateOrUpdateAsync(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName, gomockinternal.DiffEq(vmss)).
 					Return(putFuture, nil)
-				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS("VM_SIZE"), putFuture)
+				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS(), putFuture)
 			},
 		},
 		{
@@ -270,7 +262,7 @@ func TestReconcileVMSS(t *testing.T) {
 			expect: func(g *WithT, s *mock_scalesets.MockScaleSetScopeMockRecorder, m *mock_scalesets.MockClientMockRecorder) {
 				defaultSpec := newDefaultVMSSSpec()
 				s.ScaleSetSpec().Return(defaultSpec).AnyTimes()
-				createdVMSS := newDefaultVMSS("VM_SIZE")
+				createdVMSS := newDefaultVMSS()
 				instances := newDefaultInstances()
 				_ = setupDefaultVMSSInProgressOperationDoneExpectations(s, m, createdVMSS, instances)
 				s.SetLongRunningOperationState(nil)
@@ -296,13 +288,13 @@ func TestReconcileVMSS(t *testing.T) {
 				spec.Size = "VM_SIZE_AN"
 				s.ScaleSetSpec().Return(spec).AnyTimes()
 				setupDefaultVMSSStartCreatingExpectations(s, m)
-				vmss := newDefaultVMSS("VM_SIZE_AN")
+				vmss := newDefaultVMSS()
 				netConfigs := vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations
 				(*netConfigs)[0].EnableAcceleratedNetworking = to.BoolPtr(true)
 				vmss.Sku.Name = to.StringPtr(spec.Size)
 				m.CreateOrUpdateAsync(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName, gomockinternal.DiffEq(vmss)).
 					Return(putFuture, nil)
-				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS("VM_SIZE_AN"), putFuture)
+				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS(), putFuture)
 			},
 		},
 		{
@@ -311,23 +303,14 @@ func TestReconcileVMSS(t *testing.T) {
 			expect: func(g *WithT, s *mock_scalesets.MockScaleSetScopeMockRecorder, m *mock_scalesets.MockClientMockRecorder) {
 				spec := newDefaultVMSSSpec()
 				spec.SpotVMOptions = &infrav1.SpotVMOptions{}
-				spec.DataDisks = append(spec.DataDisks, infrav1.DataDisk{
-					NameSuffix: "my_disk_with_ultra_disks",
-					DiskSizeGB: 128,
-					Lun:        to.Int32Ptr(3),
-					ManagedDisk: &infrav1.ManagedDiskParameters{
-						StorageAccountType: "UltraSSD_LRS",
-					},
-				})
 				s.ScaleSetSpec().Return(spec).AnyTimes()
 				setupDefaultVMSSStartCreatingExpectations(s, m)
-				vmss := newDefaultVMSS("VM_SIZE")
-				vmss.VirtualMachineScaleSetProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{UltraSSDEnabled: pointer.Bool(true)}
+				vmss := newDefaultVMSS()
 				vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.Priority = compute.Spot
 				vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.EvictionPolicy = compute.Deallocate
 				m.CreateOrUpdateAsync(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName, gomockinternal.DiffEq(vmss)).
 					Return(putFuture, nil)
-				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS("VM_SIZE"), putFuture)
+				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS(), putFuture)
 			},
 		},
 		{
@@ -339,26 +322,17 @@ func TestReconcileVMSS(t *testing.T) {
 				spec.SpotVMOptions = &infrav1.SpotVMOptions{
 					MaxPrice: &maxPrice,
 				}
-				spec.DataDisks = append(spec.DataDisks, infrav1.DataDisk{
-					NameSuffix: "my_disk_with_ultra_disks",
-					DiskSizeGB: 128,
-					Lun:        to.Int32Ptr(3),
-					ManagedDisk: &infrav1.ManagedDiskParameters{
-						StorageAccountType: "UltraSSD_LRS",
-					},
-				})
 				s.ScaleSetSpec().Return(spec).AnyTimes()
 				setupDefaultVMSSStartCreatingExpectations(s, m)
-				vmss := newDefaultVMSS("VM_SIZE")
+				vmss := newDefaultVMSS()
 				vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.Priority = compute.Spot
 				vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.BillingProfile = &compute.BillingProfile{
 					MaxPrice: to.Float64Ptr(0.001),
 				}
-				vmss.VirtualMachineScaleSetProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{UltraSSDEnabled: pointer.Bool(true)}
 				vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.EvictionPolicy = compute.Deallocate
 				m.CreateOrUpdateAsync(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName, gomockinternal.DiffEq(vmss)).
 					Return(putFuture, nil)
-				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS("VM_SIZE"), putFuture)
+				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS(), putFuture)
 			},
 		},
 		{
@@ -369,18 +343,9 @@ func TestReconcileVMSS(t *testing.T) {
 				spec.OSDisk.ManagedDisk.DiskEncryptionSet = &infrav1.DiskEncryptionSetParameters{
 					ID: "my-diskencryptionset-id",
 				}
-				spec.DataDisks = append(spec.DataDisks, infrav1.DataDisk{
-					NameSuffix: "my_disk_with_ultra_disks",
-					DiskSizeGB: 128,
-					Lun:        to.Int32Ptr(3),
-					ManagedDisk: &infrav1.ManagedDiskParameters{
-						StorageAccountType: "UltraSSD_LRS",
-					},
-				})
 				s.ScaleSetSpec().Return(spec).AnyTimes()
 				setupDefaultVMSSStartCreatingExpectations(s, m)
-				vmss := newDefaultVMSS("VM_SIZE")
-				vmss.VirtualMachineScaleSetProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{UltraSSDEnabled: pointer.Bool(true)}
+				vmss := newDefaultVMSS()
 				osdisk := vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.StorageProfile.OsDisk
 				osdisk.ManagedDisk = &compute.VirtualMachineScaleSetManagedDiskParameters{
 					StorageAccountType: "Premium_LRS",
@@ -390,7 +355,7 @@ func TestReconcileVMSS(t *testing.T) {
 				}
 				m.CreateOrUpdateAsync(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName, gomockinternal.DiffEq(vmss)).
 					Return(putFuture, nil)
-				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS("VM_SIZE"), putFuture)
+				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS(), putFuture)
 			},
 		},
 		{
@@ -398,14 +363,6 @@ func TestReconcileVMSS(t *testing.T) {
 			expectedError: "failed to get VMSS my-vmss after create or update: failed to get result from future: operation type PUT on Azure resource my-rg/my-vmss is not done",
 			expect: func(g *WithT, s *mock_scalesets.MockScaleSetScopeMockRecorder, m *mock_scalesets.MockClientMockRecorder) {
 				spec := newDefaultVMSSSpec()
-				spec.DataDisks = append(spec.DataDisks, infrav1.DataDisk{
-					NameSuffix: "my_disk_with_ultra_disks",
-					DiskSizeGB: 128,
-					Lun:        to.Int32Ptr(3),
-					ManagedDisk: &infrav1.ManagedDiskParameters{
-						StorageAccountType: "UltraSSD_LRS",
-					},
-				})
 				spec.Identity = infrav1.VMIdentityUserAssigned
 				spec.UserAssignedIdentities = []infrav1.UserAssignedIdentity{
 					{
@@ -414,8 +371,7 @@ func TestReconcileVMSS(t *testing.T) {
 				}
 				s.ScaleSetSpec().Return(spec).AnyTimes()
 				setupDefaultVMSSStartCreatingExpectations(s, m)
-				vmss := newDefaultVMSS("VM_SIZE")
-				vmss.VirtualMachineScaleSetProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{UltraSSDEnabled: pointer.Bool(true)}
+				vmss := newDefaultVMSS()
 				vmss.Identity = &compute.VirtualMachineScaleSetIdentity{
 					Type: compute.ResourceIdentityTypeUserAssigned,
 					UserAssignedIdentities: map[string]*compute.VirtualMachineScaleSetIdentityUserAssignedIdentitiesValue{
@@ -424,7 +380,7 @@ func TestReconcileVMSS(t *testing.T) {
 				}
 				m.CreateOrUpdateAsync(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName, gomockinternal.DiffEq(vmss)).
 					Return(putFuture, nil)
-				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS("VM_SIZE"), putFuture)
+				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS(), putFuture)
 			},
 		},
 		{
@@ -436,14 +392,14 @@ func TestReconcileVMSS(t *testing.T) {
 				spec.SecurityProfile = &infrav1.SecurityProfile{EncryptionAtHost: to.BoolPtr(true)}
 				s.ScaleSetSpec().Return(spec).AnyTimes()
 				setupDefaultVMSSStartCreatingExpectations(s, m)
-				vmss := newDefaultVMSS("VM_SIZE_EAH")
+				vmss := newDefaultVMSS()
 				vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.SecurityProfile = &compute.SecurityProfile{
 					EncryptionAtHost: to.BoolPtr(true),
 				}
 				vmss.Sku.Name = to.StringPtr(spec.Size)
 				m.CreateOrUpdateAsync(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName, gomockinternal.DiffEq(vmss)).
 					Return(putFuture, nil)
-				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS("VM_SIZE_EAH"), putFuture)
+				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS(), putFuture)
 			},
 		},
 		{
@@ -465,29 +421,17 @@ func TestReconcileVMSS(t *testing.T) {
 			expect: func(g *WithT, s *mock_scalesets.MockScaleSetScopeMockRecorder, m *mock_scalesets.MockClientMockRecorder) {
 				spec := newDefaultVMSSSpec()
 				spec.Capacity = 2
-				spec.DataDisks = append(spec.DataDisks, infrav1.DataDisk{
-					NameSuffix: "my_disk_with_ultra_disks",
-					DiskSizeGB: 128,
-					Lun:        to.Int32Ptr(3),
-					ManagedDisk: &infrav1.ManagedDiskParameters{
-						StorageAccountType: "UltraSSD_LRS",
-					},
-				})
 				s.ScaleSetSpec().Return(spec).AnyTimes()
 
 				setupDefaultVMSSUpdateExpectations(s)
-				existingVMSS := newDefaultExistingVMSS("VM_SIZE")
-				existingVMSS.VirtualMachineScaleSetProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{UltraSSDEnabled: pointer.Bool(true)}
+				existingVMSS := newDefaultExistingVMSS()
 				existingVMSS.Sku.Capacity = to.Int64Ptr(2)
-				existingVMSS.VirtualMachineScaleSetProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{UltraSSDEnabled: pointer.Bool(true)}
 				instances := newDefaultInstances()
 				m.Get(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName).Return(existingVMSS, nil)
 				m.ListInstances(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName).Return(instances, nil)
 
-				clone := newDefaultExistingVMSS("VM_SIZE")
+				clone := newDefaultExistingVMSS()
 				clone.Sku.Capacity = to.Int64Ptr(3)
-				clone.VirtualMachineScaleSetProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{UltraSSDEnabled: pointer.Bool(true)}
-
 				patchVMSS, err := getVMSSUpdateFromVMSS(clone)
 				g.Expect(err).NotTo(HaveOccurred())
 				patchVMSS.VirtualMachineProfile.StorageProfile.ImageReference.Version = to.StringPtr("2.0")
@@ -549,26 +493,6 @@ func TestReconcileVMSS(t *testing.T) {
 					Return(compute.VirtualMachineScaleSet{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found"))
 			},
 		},
-		{
-			name:          "fail to create a vm with ultra disk enabled",
-			expectedError: "reconcile error that cannot be recovered occurred: vm size VM_SIZE_USSD does not support ultra disks in location test-location. select a different vm size or disable ultra disks. Object will not be requeued",
-			expect: func(g *WithT, s *mock_scalesets.MockScaleSetScopeMockRecorder, m *mock_scalesets.MockClientMockRecorder) {
-				s.ScaleSetSpec().Return(azure.ScaleSetSpec{
-					Name:       defaultVMSSName,
-					Size:       "VM_SIZE_USSD",
-					Capacity:   2,
-					SSHKeyData: "ZmFrZXNzaGtleQo=",
-					DataDisks: []infrav1.DataDisk{
-						{
-							ManagedDisk: &infrav1.ManagedDiskParameters{
-								StorageAccountType: "UltraSSD_LRS",
-							},
-						},
-					},
-				})
-				s.Location().AnyTimes().Return("test-location")
-			},
-		},
 	}
 
 	for _, tc := range testcases {
@@ -664,7 +588,7 @@ func TestDeleteVMSS(t *testing.T) {
 				m.DeleteAsync(gomockinternal.AContext(), resourceGroup, name).
 					Return(nil, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error"))
 				m.Get(gomockinternal.AContext(), resourceGroup, name).
-					Return(newDefaultVMSS("VM_SIZE"), nil)
+					Return(newDefaultVMSS(), nil)
 				m.ListInstances(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName).Return(newDefaultInstances(), nil).AnyTimes()
 				s.SetVMSSState(gomock.AssignableToTypeOf(&azure.VMSS{}))
 			},
@@ -712,17 +636,6 @@ func getFakeSkus() []compute.ResourceSku {
 				{
 					Location: to.StringPtr("test-location"),
 					Zones:    &[]string{"1", "3"},
-					ZoneDetails: &[]compute.ResourceSkuZoneDetails{
-						{
-							Capabilities: &[]compute.ResourceSkuCapabilities{
-								{
-									Name:  pointer.String("UltraSSDAvailable"),
-									Value: pointer.String("True"),
-								},
-							},
-							Name: &[]string{"1", "3"},
-						},
-					},
 				},
 			},
 			Capabilities: &[]compute.ResourceSkuCapabilities{
@@ -751,16 +664,6 @@ func getFakeSkus() []compute.ResourceSku {
 				{
 					Location: to.StringPtr("test-location"),
 					Zones:    &[]string{"1", "3"},
-					//ZoneDetails: &[]compute.ResourceSkuZoneDetails{
-					//	{
-					//		Capabilities: &[]compute.ResourceSkuCapabilities{
-					//			{
-					//				Name:  pointer.String("UltraSSDAvailable"),
-					//				Value: pointer.String("True"),
-					//			},
-					//		},
-					//	},
-					//},
 				},
 			},
 			Capabilities: &[]compute.ResourceSkuCapabilities{
@@ -845,16 +748,6 @@ func getFakeSkus() []compute.ResourceSku {
 				{
 					Location: to.StringPtr("test-location"),
 					Zones:    &[]string{"1", "3"},
-					//ZoneDetails: &[]compute.ResourceSkuZoneDetails{
-					//	{
-					//		Capabilities: &[]compute.ResourceSkuCapabilities{
-					//			{
-					//				Name:  pointer.String("UltraSSDAvailable"),
-					//				Value: pointer.String("True"),
-					//			},
-					//		},
-					//	},
-					//},
 				},
 			},
 			Capabilities: &[]compute.ResourceSkuCapabilities{
@@ -872,34 +765,6 @@ func getFakeSkus() []compute.ResourceSku {
 				},
 			},
 		},
-		{
-			Name:         to.StringPtr("VM_SIZE_USSD"),
-			ResourceType: to.StringPtr(string(resourceskus.VirtualMachines)),
-			Kind:         to.StringPtr(string(resourceskus.VirtualMachines)),
-			Locations: &[]string{
-				"test-location",
-			},
-			LocationInfo: &[]compute.ResourceSkuLocationInfo{
-				{
-					Location: to.StringPtr("test-location"),
-					Zones:    &[]string{"1", "3"},
-				},
-			},
-			Capabilities: &[]compute.ResourceSkuCapabilities{
-				{
-					Name:  to.StringPtr(resourceskus.AcceleratedNetworking),
-					Value: to.StringPtr(string(resourceskus.CapabilitySupported)),
-				},
-				{
-					Name:  to.StringPtr(resourceskus.VCPUs),
-					Value: to.StringPtr("4"),
-				},
-				{
-					Name:  to.StringPtr(resourceskus.MemoryGB),
-					Value: to.StringPtr("6"),
-				},
-			},
-		},
 	}
 }
 
@@ -959,14 +824,14 @@ func newWindowsVMSSSpec() azure.ScaleSetSpec {
 	return vmss
 }
 
-func newDefaultExistingVMSS(vmSize string) compute.VirtualMachineScaleSet {
-	vmss := newDefaultVMSS(vmSize)
+func newDefaultExistingVMSS() compute.VirtualMachineScaleSet {
+	vmss := newDefaultVMSS()
 	vmss.ID = to.StringPtr("vmss-id")
 	return vmss
 }
 
 func newDefaultWindowsVMSS() compute.VirtualMachineScaleSet {
-	vmss := newDefaultVMSS("VM_SIZE")
+	vmss := newDefaultVMSS()
 	vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.StorageProfile.OsDisk.OsType = compute.Windows
 	vmss.VirtualMachineProfile.OsProfile.LinuxConfiguration = nil
 	vmss.VirtualMachineProfile.OsProfile.WindowsConfiguration = &compute.WindowsConfiguration{
@@ -975,8 +840,7 @@ func newDefaultWindowsVMSS() compute.VirtualMachineScaleSet {
 	return vmss
 }
 
-func newDefaultVMSS(vmSize string) compute.VirtualMachineScaleSet {
-	dataDisk := fetchDataDiskBasedOnSize(vmSize)
+func newDefaultVMSS() compute.VirtualMachineScaleSet {
 	return compute.VirtualMachineScaleSet{
 		Location: to.StringPtr("test-location"),
 		Tags: map[string]*string{
@@ -985,7 +849,7 @@ func newDefaultVMSS(vmSize string) compute.VirtualMachineScaleSet {
 			"sigs.k8s.io_cluster-api-provider-azure_role":               to.StringPtr("node"),
 		},
 		Sku: &compute.Sku{
-			Name:     to.StringPtr(vmSize),
+			Name:     to.StringPtr("VM_SIZE"),
 			Tier:     to.StringPtr("Standard"),
 			Capacity: to.Int64Ptr(2),
 		},
@@ -1028,7 +892,35 @@ func newDefaultVMSS(vmSize string) compute.VirtualMachineScaleSet {
 							StorageAccountType: "Premium_LRS",
 						},
 					},
-					DataDisks: dataDisk,
+					DataDisks: &[]compute.VirtualMachineScaleSetDataDisk{
+						{
+							Lun:          to.Int32Ptr(0),
+							Name:         to.StringPtr("my-vmss_my_disk"),
+							CreateOption: "Empty",
+							DiskSizeGB:   to.Int32Ptr(128),
+						},
+						{
+							Lun:          to.Int32Ptr(1),
+							Name:         to.StringPtr("my-vmss_my_disk_with_managed_disk"),
+							CreateOption: "Empty",
+							DiskSizeGB:   to.Int32Ptr(128),
+							ManagedDisk: &compute.VirtualMachineScaleSetManagedDiskParameters{
+								StorageAccountType: "Standard_LRS",
+							},
+						},
+						{
+							Lun:          to.Int32Ptr(2),
+							Name:         to.StringPtr("my-vmss_managed_disk_with_encryption"),
+							CreateOption: "Empty",
+							DiskSizeGB:   to.Int32Ptr(128),
+							ManagedDisk: &compute.VirtualMachineScaleSetManagedDiskParameters{
+								StorageAccountType: "Standard_LRS",
+								DiskEncryptionSet: &compute.DiskEncryptionSetParameters{
+									ID: to.StringPtr("encryption_id"),
+								},
+							},
+						},
+					},
 				},
 				DiagnosticsProfile: &compute.DiagnosticsProfile{
 					BootDiagnostics: &compute.BootDiagnostics{
@@ -1080,81 +972,6 @@ func newDefaultVMSS(vmSize string) compute.VirtualMachineScaleSet {
 	}
 }
 
-func fetchDataDiskBasedOnSize(vmSize string) *[]compute.VirtualMachineScaleSetDataDisk {
-	var dataDisk *[]compute.VirtualMachineScaleSetDataDisk
-	if vmSize == "VM_SIZE" {
-		dataDisk = &[]compute.VirtualMachineScaleSetDataDisk{
-			{
-				Lun:          to.Int32Ptr(0),
-				Name:         to.StringPtr("my-vmss_my_disk"),
-				CreateOption: "Empty",
-				DiskSizeGB:   to.Int32Ptr(128),
-			},
-			{
-				Lun:          to.Int32Ptr(1),
-				Name:         to.StringPtr("my-vmss_my_disk_with_managed_disk"),
-				CreateOption: "Empty",
-				DiskSizeGB:   to.Int32Ptr(128),
-				ManagedDisk: &compute.VirtualMachineScaleSetManagedDiskParameters{
-					StorageAccountType: "Standard_LRS",
-				},
-			},
-			{
-				Lun:          to.Int32Ptr(2),
-				Name:         to.StringPtr("my-vmss_managed_disk_with_encryption"),
-				CreateOption: "Empty",
-				DiskSizeGB:   to.Int32Ptr(128),
-				ManagedDisk: &compute.VirtualMachineScaleSetManagedDiskParameters{
-					StorageAccountType: "Standard_LRS",
-					DiskEncryptionSet: &compute.DiskEncryptionSetParameters{
-						ID: to.StringPtr("encryption_id"),
-					},
-				},
-			},
-			{
-				Lun:          to.Int32Ptr(3),
-				Name:         to.StringPtr("my-vmss_my_disk_with_ultra_disks"),
-				CreateOption: "Empty",
-				DiskSizeGB:   to.Int32Ptr(128),
-				ManagedDisk: &compute.VirtualMachineScaleSetManagedDiskParameters{
-					StorageAccountType: "UltraSSD_LRS",
-				},
-			},
-		}
-	} else {
-		dataDisk = &[]compute.VirtualMachineScaleSetDataDisk{
-			{
-				Lun:          to.Int32Ptr(0),
-				Name:         to.StringPtr("my-vmss_my_disk"),
-				CreateOption: "Empty",
-				DiskSizeGB:   to.Int32Ptr(128),
-			},
-			{
-				Lun:          to.Int32Ptr(1),
-				Name:         to.StringPtr("my-vmss_my_disk_with_managed_disk"),
-				CreateOption: "Empty",
-				DiskSizeGB:   to.Int32Ptr(128),
-				ManagedDisk: &compute.VirtualMachineScaleSetManagedDiskParameters{
-					StorageAccountType: "Standard_LRS",
-				},
-			},
-			{
-				Lun:          to.Int32Ptr(2),
-				Name:         to.StringPtr("my-vmss_managed_disk_with_encryption"),
-				CreateOption: "Empty",
-				DiskSizeGB:   to.Int32Ptr(128),
-				ManagedDisk: &compute.VirtualMachineScaleSetManagedDiskParameters{
-					StorageAccountType: "Standard_LRS",
-					DiskEncryptionSet: &compute.DiskEncryptionSetParameters{
-						ID: to.StringPtr("encryption_id"),
-					},
-				},
-			},
-		}
-	}
-	return dataDisk
-}
-
 func newDefaultInstances() []compute.VirtualMachineScaleSetVM {
 	return []compute.VirtualMachineScaleSetVM{
 		{
diff --git a/azure/services/subnets/subnets_test.go b/azure/services/subnets/subnets_test.go
index 7aa82b0d8..b3759714e 100644
--- a/azure/services/subnets/subnets_test.go
+++ b/azure/services/subnets/subnets_test.go
@@ -209,6 +209,14 @@ func TestReconcileSubnets(t *testing.T) {
 					},
 				})
 				s.Vnet().AnyTimes().Return(&infrav1.VnetSpec{Name: "my-vnet"})
+				s.NodeSubnet().AnyTimes().Return(infrav1.SubnetSpec{
+					Name: "my-subnet",
+					Role: infrav1.SubnetNode,
+				})
+				s.ControlPlaneSubnet().AnyTimes().Return(infrav1.SubnetSpec{
+					Name: "my-subnet-1",
+					Role: infrav1.SubnetControlPlane,
+				})
 				s.ClusterName().AnyTimes().Return("fake-cluster")
 				s.SubscriptionID().AnyTimes().Return("123")
 				s.ResourceGroup().AnyTimes().Return("my-rg")
@@ -282,6 +290,14 @@ func TestReconcileSubnets(t *testing.T) {
 					},
 				})
 				s.Vnet().AnyTimes().Return(&infrav1.VnetSpec{Name: "my-vnet"})
+				s.NodeSubnet().AnyTimes().Return(infrav1.SubnetSpec{
+					Name: "my-subnet",
+					Role: infrav1.SubnetNode,
+				})
+				s.ControlPlaneSubnet().AnyTimes().Return(infrav1.SubnetSpec{
+					Name: "my-subnet-1",
+					Role: infrav1.SubnetControlPlane,
+				})
 				s.ClusterName().AnyTimes().Return("fake-cluster")
 				s.IsIPv6Enabled().AnyTimes().Return(true)
 				s.SubscriptionID().AnyTimes().Return("123")
@@ -307,7 +323,7 @@ func TestReconcileSubnets(t *testing.T) {
 					}, nil)
 				s.SetSubnet(infrav1.SubnetSpec{
 					ID:         "subnet-id",
-					Name:       "my-ipv6-subnet",
+					Name:       "my-subnet",
 					Role:       infrav1.SubnetNode,
 					CIDRBlocks: []string{"10.0.0.0/16", "2001:1234:5678:9abd::/64"},
 				}).Times(1)
@@ -332,7 +348,7 @@ func TestReconcileSubnets(t *testing.T) {
 					}, nil)
 				s.SetSubnet(infrav1.SubnetSpec{
 					ID:         "subnet-id-1",
-					Name:       "my-ipv6-subnet-cp",
+					Name:       "my-subnet-1",
 					Role:       infrav1.SubnetControlPlane,
 					CIDRBlocks: []string{"10.2.0.0/16", "2001:1234:5678:9abc::/64"},
 				}).Times(1)
diff --git a/azure/services/virtualmachines/virtualmachines_test.go b/azure/services/virtualmachines/virtualmachines_test.go
index 64066dfa8..0d42e1393 100644
--- a/azure/services/virtualmachines/virtualmachines_test.go
+++ b/azure/services/virtualmachines/virtualmachines_test.go
@@ -21,8 +21,6 @@ import (
 	"net/http"
 	"testing"
 
-	"k8s.io/utils/pointer"
-
 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-30/compute"
 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network"
 	"github.com/Azure/go-autorest/autorest"
@@ -336,7 +334,7 @@ func TestReconcileVM(t *testing.T) {
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				s.AdditionalTags()
-				s.Location().Return("test-location").AnyTimes()
+				s.Location().Return("test-location")
 				s.ClusterName().Return("my-cluster")
 				s.ProviderID().Return("")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vm").
@@ -499,7 +497,7 @@ func TestReconcileVM(t *testing.T) {
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				s.AdditionalTags()
-				s.Location().Return("test-location").AnyTimes()
+				s.Location().Return("test-location")
 				s.ClusterName().Return("my-cluster")
 				s.ProviderID().Return("")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vm").
@@ -570,7 +568,7 @@ func TestReconcileVM(t *testing.T) {
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				s.AdditionalTags()
-				s.Location().Return("test-location").AnyTimes()
+				s.Location().Return("test-location")
 				s.ClusterName().Return("my-cluster")
 				s.ProviderID().Return("")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vm").
@@ -641,7 +639,7 @@ func TestReconcileVM(t *testing.T) {
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				s.AdditionalTags()
-				s.Location().Return("test-location").AnyTimes()
+				s.Location().Return("test-location")
 				s.ClusterName().Return("my-cluster")
 				s.ProviderID().Return("")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vm").
@@ -727,7 +725,7 @@ func TestReconcileVM(t *testing.T) {
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				s.AdditionalTags()
-				s.Location().Return("test-location").AnyTimes()
+				s.Location().Return("test-location")
 				s.ClusterName().Return("my-cluster")
 				s.ProviderID().Return("")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vm").
@@ -806,7 +804,7 @@ func TestReconcileVM(t *testing.T) {
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				s.AdditionalTags()
-				s.Location().Return("test-location").AnyTimes()
+				s.Location().Return("test-location")
 				s.ClusterName().Return("my-cluster")
 				s.ProviderID().Return("")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vm").
@@ -873,7 +871,7 @@ func TestReconcileVM(t *testing.T) {
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				s.AdditionalTags()
-				s.Location().Return("test-location").AnyTimes()
+				s.Location().Return("test-location")
 				s.ClusterName().Return("my-cluster")
 				s.ProviderID().Return("")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vm").
@@ -959,7 +957,7 @@ func TestReconcileVM(t *testing.T) {
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				s.AdditionalTags()
-				s.Location().Return("test-location").AnyTimes()
+				s.Location().Return("test-location")
 				s.ClusterName().Return("my-cluster")
 				s.ProviderID().Return("")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vm").
@@ -1105,7 +1103,6 @@ func TestReconcileVM(t *testing.T) {
 						Version:   "1.0",
 					},
 				}, nil)
-				s.Location().Return("test-location").AnyTimes()
 				s.ProviderID().Return("")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vm").
 					Return(compute.VirtualMachine{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found"))
@@ -1161,7 +1158,7 @@ func TestReconcileVM(t *testing.T) {
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				s.AdditionalTags()
-				s.Location().Return("test-location").AnyTimes()
+				s.Location().Return("test-location")
 				s.ClusterName().Return("my-cluster")
 				s.ProviderID().Return("")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vm").
@@ -1449,7 +1446,7 @@ func TestReconcileVM(t *testing.T) {
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				s.AdditionalTags()
-				s.Location().Return("test-location").AnyTimes()
+				s.Location().Return("test-location")
 				s.ClusterName().Return("my-cluster")
 				s.ProviderID().Return("")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vm").
@@ -1610,7 +1607,7 @@ func TestReconcileVM(t *testing.T) {
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				s.AdditionalTags()
-				s.Location().Return("test-location").AnyTimes()
+				s.Location().Return("test-location")
 				s.ClusterName().Return("my-cluster")
 				s.ProviderID().Return("")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vm").
@@ -1787,7 +1784,7 @@ func TestReconcileVM(t *testing.T) {
 				s.ResourceGroup().AnyTimes().Return("my-rg")
 				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
 				s.AdditionalTags()
-				s.Location().Return("test-location").AnyTimes()
+				s.Location().Return("test-location")
 				s.ClusterName().Return("my-cluster")
 				s.ProviderID().Return("")
 				m.Get(gomockinternal.AContext(), "my-rg", "my-vm").
@@ -1915,301 +1912,6 @@ func TestReconcileVM(t *testing.T) {
 				svc.resourceSKUCache = resourceSkusCache
 			},
 		},
-		{
-			Name: "can create a vm with ultra disk enabled",
-			Expect: func(g *WithT, s *mock_virtualmachines.MockVMScopeMockRecorder, m *mock_virtualmachines.MockClientMockRecorder,
-				mnic *mock_networkinterfaces.MockClientMockRecorder, mpip *mock_publicips.MockClientMockRecorder) {
-				s.VMSpec().Return(azure.VMSpec{
-					Name:       "my-ultra-ssd-vm",
-					Role:       infrav1.ControlPlane,
-					NICNames:   []string{"my-nic", "second-nic"},
-					SSHKeyData: "ZmFrZXNzaGtleQo=",
-					Size:       "Standard_D2v3",
-					Zone:       "1",
-					Identity:   infrav1.VMIdentityNone,
-					OSDisk: infrav1.OSDisk{
-						OSType:     "Linux",
-						DiskSizeGB: to.Int32Ptr(128),
-						ManagedDisk: &infrav1.ManagedDiskParameters{
-							StorageAccountType: "Premium_LRS",
-						},
-					},
-					DataDisks: []infrav1.DataDisk{
-						{
-							NameSuffix: "mydisk",
-							DiskSizeGB: 64,
-							Lun:        to.Int32Ptr(0),
-						},
-						{
-							NameSuffix: "myDiskWithUltraDisk",
-							DiskSizeGB: 128,
-							Lun:        to.Int32Ptr(1),
-							ManagedDisk: &infrav1.ManagedDiskParameters{
-								StorageAccountType: "UltraSSD_LRS",
-							},
-						},
-						{
-							NameSuffix: "myDiskWithManagedDisk",
-							DiskSizeGB: 128,
-							Lun:        to.Int32Ptr(2),
-							ManagedDisk: &infrav1.ManagedDiskParameters{
-								StorageAccountType: "Premium_LRS",
-							},
-						},
-						{
-							NameSuffix: "managedDiskWithEncryption",
-							DiskSizeGB: 128,
-							Lun:        to.Int32Ptr(3),
-							ManagedDisk: &infrav1.ManagedDiskParameters{
-								StorageAccountType: "Premium_LRS",
-								DiskEncryptionSet: &infrav1.DiskEncryptionSetParameters{
-									ID: "my_id",
-								},
-							},
-						},
-					},
-					UserAssignedIdentities: nil,
-					SpotVMOptions:          nil,
-				})
-				s.SubscriptionID().AnyTimes().Return("123")
-				s.ResourceGroup().AnyTimes().Return("my-rg")
-				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
-				s.AdditionalTags()
-				s.Location().Return("test-location").AnyTimes()
-				s.ClusterName().Return("my-cluster")
-				s.ProviderID().Return("")
-				m.Get(gomockinternal.AContext(), "my-rg", "my-ultra-ssd-vm").
-					Return(compute.VirtualMachine{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found"))
-				s.GetVMImage().AnyTimes().Return(&infrav1.Image{
-					Marketplace: &infrav1.AzureMarketplaceImage{
-						Publisher: "fake-publisher",
-						Offer:     "my-offer",
-						SKU:       "sku-id",
-						Version:   "1.0",
-					},
-				}, nil)
-				s.GetBootstrapData(gomockinternal.AContext()).Return("fake-bootstrap-data", nil)
-				s.AvailabilitySet().Return("", false)
-				m.CreateOrUpdate(gomockinternal.AContext(), "my-rg", "my-ultra-ssd-vm", gomockinternal.DiffEq(compute.VirtualMachine{
-					VirtualMachineProperties: &compute.VirtualMachineProperties{
-						HardwareProfile: &compute.HardwareProfile{VMSize: "Standard_D2v3"},
-						StorageProfile: &compute.StorageProfile{
-							ImageReference: &compute.ImageReference{
-								Publisher: to.StringPtr("fake-publisher"),
-								Offer:     to.StringPtr("my-offer"),
-								Sku:       to.StringPtr("sku-id"),
-								Version:   to.StringPtr("1.0"),
-							},
-							OsDisk: &compute.OSDisk{
-								OsType:       "Linux",
-								Name:         to.StringPtr("my-ultra-ssd-vm_OSDisk"),
-								CreateOption: "FromImage",
-								DiskSizeGB:   to.Int32Ptr(128),
-								ManagedDisk: &compute.ManagedDiskParameters{
-									StorageAccountType: "Premium_LRS",
-								},
-							},
-							DataDisks: &[]compute.DataDisk{
-								{
-									Lun:          to.Int32Ptr(0),
-									Name:         to.StringPtr("my-ultra-ssd-vm_mydisk"),
-									CreateOption: "Empty",
-									DiskSizeGB:   to.Int32Ptr(64),
-								},
-								{
-									Lun:          to.Int32Ptr(1),
-									Name:         to.StringPtr("my-ultra-ssd-vm_myDiskWithUltraDisk"),
-									CreateOption: "Empty",
-									DiskSizeGB:   to.Int32Ptr(128),
-									ManagedDisk: &compute.ManagedDiskParameters{
-										StorageAccountType: "UltraSSD_LRS",
-									},
-								},
-								{
-									Lun:          to.Int32Ptr(2),
-									Name:         to.StringPtr("my-ultra-ssd-vm_myDiskWithManagedDisk"),
-									CreateOption: "Empty",
-									DiskSizeGB:   to.Int32Ptr(128),
-									ManagedDisk: &compute.ManagedDiskParameters{
-										StorageAccountType: "Premium_LRS",
-									},
-								},
-								{
-									Lun:          to.Int32Ptr(3),
-									Name:         to.StringPtr("my-ultra-ssd-vm_managedDiskWithEncryption"),
-									CreateOption: "Empty",
-									DiskSizeGB:   to.Int32Ptr(128),
-									ManagedDisk: &compute.ManagedDiskParameters{
-										StorageAccountType: "Premium_LRS",
-										DiskEncryptionSet: &compute.DiskEncryptionSetParameters{
-											ID: to.StringPtr("my_id"),
-										},
-									},
-								},
-							},
-						},
-						OsProfile: &compute.OSProfile{
-							ComputerName:  to.StringPtr("my-ultra-ssd-vm"),
-							AdminUsername: to.StringPtr("capi"),
-							CustomData:    to.StringPtr("fake-bootstrap-data"),
-							LinuxConfiguration: &compute.LinuxConfiguration{
-								DisablePasswordAuthentication: to.BoolPtr(true),
-								SSH: &compute.SSHConfiguration{
-									PublicKeys: &[]compute.SSHPublicKey{
-										{
-											Path:    to.StringPtr("/home/capi/.ssh/authorized_keys"),
-											KeyData: to.StringPtr("fakesshkey\n"),
-										},
-									},
-								},
-							},
-						},
-						DiagnosticsProfile: &compute.DiagnosticsProfile{
-							BootDiagnostics: &compute.BootDiagnostics{
-								Enabled: to.BoolPtr(true),
-							},
-						},
-						NetworkProfile: &compute.NetworkProfile{
-							NetworkInterfaces: &[]compute.NetworkInterfaceReference{
-								{
-									NetworkInterfaceReferenceProperties: &compute.NetworkInterfaceReferenceProperties{Primary: to.BoolPtr(true)},
-									ID:                                  to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/networkInterfaces/my-nic"),
-								},
-								{
-									NetworkInterfaceReferenceProperties: &compute.NetworkInterfaceReferenceProperties{Primary: to.BoolPtr(false)},
-									ID:                                  to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/networkInterfaces/second-nic"),
-								},
-							},
-						},
-						AdditionalCapabilities: &compute.AdditionalCapabilities{UltraSSDEnabled: pointer.Bool(true)},
-					},
-					Resources: nil,
-					Identity:  nil,
-					Zones:     &[]string{"1"},
-					ID:        nil,
-					Name:      nil,
-					Type:      nil,
-					Location:  to.StringPtr("test-location"),
-					Tags: map[string]*string{
-						"Name": to.StringPtr("my-ultra-ssd-vm"),
-						"sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"),
-						"sigs.k8s.io_cluster-api-provider-azure_role":               to.StringPtr("control-plane"),
-					},
-				}))
-			},
-			ExpectedError: "",
-			SetupSKUs: func(svc *Service) {
-				skus := []compute.ResourceSku{
-					{
-						Name: to.StringPtr("Standard_D2v3"),
-						Kind: to.StringPtr(string(resourceskus.VirtualMachines)),
-						Locations: &[]string{
-							"test-location",
-						},
-						LocationInfo: &[]compute.ResourceSkuLocationInfo{
-							{
-								Location: to.StringPtr("test-location"),
-								Zones:    &[]string{"1"},
-								ZoneDetails: &[]compute.ResourceSkuZoneDetails{
-									{
-										Capabilities: &[]compute.ResourceSkuCapabilities{
-											{
-												Name:  pointer.String("UltraSSDAvailable"),
-												Value: pointer.String("True"),
-											},
-										},
-										Name: &[]string{"1"},
-									},
-								},
-							},
-						},
-						Capabilities: &[]compute.ResourceSkuCapabilities{
-							{
-								Name:  to.StringPtr(resourceskus.VCPUs),
-								Value: to.StringPtr("2"),
-							},
-							{
-								Name:  to.StringPtr(resourceskus.MemoryGB),
-								Value: to.StringPtr("4"),
-							},
-						},
-					},
-				}
-				resourceSkusCache := resourceskus.NewStaticCache(skus, "test-location")
-				svc.resourceSKUCache = resourceSkusCache
-			},
-		},
-		{
-			Name: "fail to create a vm with ultra disk enabled",
-			Expect: func(g *WithT, s *mock_virtualmachines.MockVMScopeMockRecorder, m *mock_virtualmachines.MockClientMockRecorder,
-				mnic *mock_networkinterfaces.MockClientMockRecorder, mpip *mock_publicips.MockClientMockRecorder) {
-				s.VMSpec().Return(azure.VMSpec{
-					Name:       "my-ultra-ssd-vm",
-					Role:       infrav1.ControlPlane,
-					NICNames:   []string{"my-nic", "second-nic"},
-					SSHKeyData: "ZmFrZXNzaGtleQo=",
-					Size:       "Standard_D2v3",
-					Zone:       "1",
-					Identity:   infrav1.VMIdentityNone,
-					OSDisk: infrav1.OSDisk{
-						OSType:     "Linux",
-						DiskSizeGB: to.Int32Ptr(128),
-						ManagedDisk: &infrav1.ManagedDiskParameters{
-							StorageAccountType: "Premium_LRS",
-						},
-					},
-					DataDisks: []infrav1.DataDisk{
-						{
-							NameSuffix: "myDiskWithUltraDisk",
-							DiskSizeGB: 128,
-							Lun:        to.Int32Ptr(1),
-							ManagedDisk: &infrav1.ManagedDiskParameters{
-								StorageAccountType: "UltraSSD_LRS",
-							},
-						},
-					},
-					UserAssignedIdentities: nil,
-					SpotVMOptions:          nil,
-				})
-				s.SubscriptionID().AnyTimes().Return("123")
-				s.ResourceGroup().AnyTimes().Return("my-rg")
-				s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New())
-				s.Location().Return("test-location").AnyTimes()
-				s.ProviderID().Return("")
-				m.Get(gomockinternal.AContext(), "my-rg", "my-ultra-ssd-vm").
-					Return(compute.VirtualMachine{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found"))
-			},
-			ExpectedError: "reconcile error that cannot be recovered occurred: vm size Standard_D2v3 does not support ultra disks in location test-location. select a different vm size or disable ultra disks. Object will not be requeued",
-			SetupSKUs: func(svc *Service) {
-				skus := []compute.ResourceSku{
-					{
-						Name: to.StringPtr("Standard_D2v3"),
-						Kind: to.StringPtr(string(resourceskus.VirtualMachines)),
-						Locations: &[]string{
-							"test-location",
-						},
-						LocationInfo: &[]compute.ResourceSkuLocationInfo{
-							{
-								Location: to.StringPtr("test-location"),
-								Zones:    &[]string{"1"},
-							},
-						},
-						Capabilities: &[]compute.ResourceSkuCapabilities{
-							{
-								Name:  to.StringPtr(resourceskus.VCPUs),
-								Value: to.StringPtr("2"),
-							},
-							{
-								Name:  to.StringPtr(resourceskus.MemoryGB),
-								Value: to.StringPtr("4"),
-							},
-						},
-					},
-				}
-				resourceSkusCache := resourceskus.NewStaticCache(skus, "")
-				svc.resourceSKUCache = resourceSkusCache
-			},
-		},
 	}
 
 	for _, tc := range testcases {
diff --git a/controllers/azurecluster_reconciler_test.go b/controllers/azurecluster_reconciler_test.go
index 7f42ad1b5..e85b88f5e 100644
--- a/controllers/azurecluster_reconciler_test.go
+++ b/controllers/azurecluster_reconciler_test.go
@@ -59,7 +59,6 @@ func TestAzureClusterReconcilerDelete(t *testing.T) {
 			expect: func(grp *mocks.MockReconcilerMockRecorder, vnet *mocks.MockReconcilerMockRecorder, sg *mocks.MockReconcilerMockRecorder, rt *mocks.MockReconcilerMockRecorder, sn *mocks.MockReconcilerMockRecorder, natg *mocks.MockReconcilerMockRecorder, pip *mocks.MockReconcilerMockRecorder, lb *mocks.MockReconcilerMockRecorder, dns *mocks.MockReconcilerMockRecorder, bastion *mocks.MockReconcilerMockRecorder) {
 				gomock.InOrder(
 					grp.Delete(gomockinternal.AContext()).Return(azure.ErrNotOwned),
-					bastion.Delete(gomockinternal.AContext()),
 					dns.Delete(gomockinternal.AContext()),
 					lb.Delete(gomockinternal.AContext()),
 					sn.Delete(gomockinternal.AContext()),
@@ -68,6 +67,7 @@ func TestAzureClusterReconcilerDelete(t *testing.T) {
 					rt.Delete(gomockinternal.AContext()),
 					sg.Delete(gomockinternal.AContext()),
 					vnet.Delete(gomockinternal.AContext()),
+					bastion.Delete(gomockinternal.AContext()),
 				)
 			},
 		},
@@ -76,7 +76,6 @@ func TestAzureClusterReconcilerDelete(t *testing.T) {
 			expect: func(grp *mocks.MockReconcilerMockRecorder, vnet *mocks.MockReconcilerMockRecorder, sg *mocks.MockReconcilerMockRecorder, rt *mocks.MockReconcilerMockRecorder, sn *mocks.MockReconcilerMockRecorder, pip *mocks.MockReconcilerMockRecorder, natg *mocks.MockReconcilerMockRecorder, lb *mocks.MockReconcilerMockRecorder, dns *mocks.MockReconcilerMockRecorder, bastion *mocks.MockReconcilerMockRecorder) {
 				gomock.InOrder(
 					grp.Delete(gomockinternal.AContext()).Return(azure.ErrNotOwned),
-					bastion.Delete(gomockinternal.AContext()),
 					dns.Delete(gomockinternal.AContext()),
 					lb.Delete(gomockinternal.AContext()).Return(errors.New("some error happened")),
 				)
@@ -87,7 +86,6 @@ func TestAzureClusterReconcilerDelete(t *testing.T) {
 			expect: func(grp *mocks.MockReconcilerMockRecorder, vnet *mocks.MockReconcilerMockRecorder, sg *mocks.MockReconcilerMockRecorder, rt *mocks.MockReconcilerMockRecorder, sn *mocks.MockReconcilerMockRecorder, pip *mocks.MockReconcilerMockRecorder, natg *mocks.MockReconcilerMockRecorder, lb *mocks.MockReconcilerMockRecorder, dns *mocks.MockReconcilerMockRecorder, bastion *mocks.MockReconcilerMockRecorder) {
 				gomock.InOrder(
 					grp.Delete(gomockinternal.AContext()).Return(azure.ErrNotOwned),
-					bastion.Delete(gomockinternal.AContext()),
 					dns.Delete(gomockinternal.AContext()),
 					lb.Delete(gomockinternal.AContext()),
 					sn.Delete(gomockinternal.AContext()),
diff --git a/exp/api/v1alpha4/azuremachinepool_webhook_test.go b/exp/api/v1alpha4/azuremachinepool_webhook_test.go
index 222f07c43..97db5f47f 100644
--- a/exp/api/v1alpha4/azuremachinepool_webhook_test.go
+++ b/exp/api/v1alpha4/azuremachinepool_webhook_test.go
@@ -29,8 +29,6 @@ import (
 	. "github.com/onsi/gomega"
 	"golang.org/x/crypto/ssh"
 
-	guuid "github.com/google/uuid"
-
 	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4"
 )
 
@@ -226,7 +224,7 @@ func TestAzureMachinePool_ValidateUpdate(t *testing.T) {
 	}
 }
 
-func TestAzureMachinePool_Default(t *testing.T) {
+func TestAzureMachine_Default(t *testing.T) {
 	g := NewWithT(t)
 
 	type test struct {
@@ -237,25 +235,6 @@ func TestAzureMachinePool_Default(t *testing.T) {
 	publicKeyExistTest := test{amp: createMachinePoolWithSSHPublicKey(existingPublicKey)}
 	publicKeyNotExistTest := test{amp: createMachinePoolWithSSHPublicKey("")}
 
-	existingRoleAssignmentName := "42862306-e485-4319-9bf0-35dbc6f6fe9c"
-	roleAssignmentExistTest := test{amp: &AzureMachinePool{Spec: AzureMachinePoolSpec{
-		Identity:           "SystemAssigned",
-		RoleAssignmentName: existingRoleAssignmentName,
-	}}}
-
-	roleAssignmentEmptyTest := test{amp: &AzureMachinePool{Spec: AzureMachinePoolSpec{
-		Identity:           "SystemAssigned",
-		RoleAssignmentName: "",
-	}}}
-
-	roleAssignmentExistTest.amp.Default()
-	g.Expect(roleAssignmentExistTest.amp.Spec.RoleAssignmentName).To(Equal(existingRoleAssignmentName))
-
-	roleAssignmentEmptyTest.amp.Default()
-	g.Expect(roleAssignmentEmptyTest.amp.Spec.RoleAssignmentName).To(Not(BeEmpty()))
-	_, err := guuid.Parse(roleAssignmentEmptyTest.amp.Spec.RoleAssignmentName)
-	g.Expect(err).To(Not(HaveOccurred()))
-
 	publicKeyExistTest.amp.Default()
 	g.Expect(publicKeyExistTest.amp.Spec.Template.SSHPublicKey).To(Equal(existingPublicKey))
 
diff --git a/exp/controllers/azuremachinepool_controller_unit_test.go b/exp/controllers/azuremachinepool_controller_unit_test.go
index 75e2144f2..d0cdb9c31 100644
--- a/exp/controllers/azuremachinepool_controller_unit_test.go
+++ b/exp/controllers/azuremachinepool_controller_unit_test.go
@@ -123,17 +123,6 @@ func newMachinePoolWithInfrastructureRef(clusterName, poolName string) *clusterv
 	return m
 }
 
-func newManagedMachinePoolWithInfrastructureRef(clusterName, poolName string) *clusterv1exp.MachinePool {
-	m := newMachinePool(clusterName, poolName)
-	m.Spec.Template.Spec.InfrastructureRef = v1.ObjectReference{
-		Kind:       "AzureManagedMachinePool",
-		Namespace:  m.Namespace,
-		Name:       "azure" + poolName,
-		APIVersion: infrav1exp.GroupVersion.String(),
-	}
-	return m
-}
-
 func newAzureCluster(clusterName string) *infrav1.AzureCluster {
 	return &infrav1.AzureCluster{
 		ObjectMeta: metav1.ObjectMeta{
diff --git a/exp/controllers/helpers_test.go b/exp/controllers/helpers_test.go
index 42a537b74..a9a2cc8cd 100644
--- a/exp/controllers/helpers_test.go
+++ b/exp/controllers/helpers_test.go
@@ -457,6 +457,7 @@ func Test_MachinePoolToInfrastructureMapFunc(t *testing.T) {
 	for _, c := range cases {
 		c := c
 		t.Run(c.Name, func(t *testing.T) {
+			t.Parallel()
 			g := NewWithT(t)
 
 			mockCtrl := gomock.NewController(t)
@@ -473,76 +474,6 @@ func Test_MachinePoolToInfrastructureMapFunc(t *testing.T) {
 	}
 }
 
-func Test_ManagedMachinePoolToInfrastructureMapFunc(t *testing.T) {
-	cases := []struct {
-		Name             string
-		Setup            func(logMock *mock_log.MockLogger)
-		MapObjectFactory func(*GomegaWithT) client.Object
-		Expect           func(*GomegaWithT, []reconcile.Request)
-	}{
-		{
-			Name: "MachinePoolToAzureManagedMachinePool",
-			MapObjectFactory: func(g *GomegaWithT) client.Object {
-				return newManagedMachinePoolWithInfrastructureRef("azureManagedCluster", "ManagedMachinePool")
-			},
-			Expect: func(g *GomegaWithT, reqs []reconcile.Request) {
-				g.Expect(reqs).To(HaveLen(1))
-				g.Expect(reqs[0]).To(Equal(reconcile.Request{
-					NamespacedName: types.NamespacedName{
-						Name:      "azureManagedMachinePool",
-						Namespace: "default",
-					},
-				}))
-			},
-		},
-		{
-			Name: "MachinePoolWithoutMatchingInfraRef",
-			MapObjectFactory: func(g *GomegaWithT) client.Object {
-				return newMachinePool("azureManagedCluster", "machinePool")
-			},
-			Setup: func(logMock *mock_log.MockLogger) {
-				ampGK := infrav1exp.GroupVersion.WithKind("AzureManagedMachinePool").GroupKind()
-				logMock.EXPECT().V(4).Return(logMock)
-				logMock.EXPECT().Info("gk does not match", "gk", ampGK, "infraGK", gomock.Any())
-			},
-			Expect: func(g *GomegaWithT, reqs []reconcile.Request) {
-				g.Expect(reqs).To(HaveLen(0))
-			},
-		},
-		{
-			Name: "NotAMachinePool",
-			MapObjectFactory: func(g *GomegaWithT) client.Object {
-				return newCluster("azureManagedCluster")
-			},
-			Setup: func(logMock *mock_log.MockLogger) {
-				logMock.EXPECT().V(4).Return(logMock)
-				logMock.EXPECT().Info("attempt to map incorrect type", "type", "*v1alpha4.Cluster")
-			},
-			Expect: func(g *GomegaWithT, reqs []reconcile.Request) {
-				g.Expect(reqs).To(HaveLen(0))
-			},
-		},
-	}
-
-	for _, c := range cases {
-		c := c
-		t.Run(c.Name, func(t *testing.T) {
-			g := NewWithT(t)
-
-			mockCtrl := gomock.NewController(t)
-			defer mockCtrl.Finish()
-
-			log := mock_log.NewMockLogger(mockCtrl)
-			if c.Setup != nil {
-				c.Setup(log)
-			}
-			f := MachinePoolToInfrastructureMapFunc(infrav1exp.GroupVersion.WithKind("AzureManagedMachinePool"), log)
-			reqs := f(c.MapObjectFactory(g))
-			c.Expect(g, reqs)
-		})
-	}
-}
-
 func Test_azureClusterToAzureMachinePoolsFunc(t *testing.T) {
 	cases := []struct {
 		Name             string
diff --git a/test/e2e/aks.go b/test/e2e/aks.go
index 082515f8f..1256ca7f6 100644
--- a/test/e2e/aks.go
+++ b/test/e2e/aks.go
@@ -22,11 +22,13 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-30/compute"
 	"github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2020-02-01/containerservice"
 	"github.com/Azure/go-autorest/autorest/azure/auth"
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
 	"golang.org/x/mod/semver"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
 	infraexpv1 "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1alpha4"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
@@ -236,3 +238,39 @@ func byClusterOptions(name, namespace string) []client.ListOption {
 		},
 	}
 }
+
+// AKSResourcesValidationSpecInput is the input for AKSResourcesValidationSpec
+type AKSResourcesValidationSpecInput struct {
+	BootstrapClusterProxy framework.ClusterProxy
+	Namespace             *corev1.Namespace
+	ClusterName           string
+}
+
+// AKSResourcesValidationSpec implements a test that verifies the resources created by an AKS cluster.
+func AKSResourcesValidationSpec(ctx context.Context, inputGetter func() AKSResourcesValidationSpecInput) {
+	var (
+		specName = "aks-resources-validation"
+		input    AKSResourcesValidationSpecInput
+	)
+
+	input = inputGetter()
+	Expect(input.ClusterName).NotTo(BeEmpty(), "Invalid argument. input.ClusterName can't be empty when calling %s spec", specName)
+
+	By("creating Azure clients with the workload cluster's subscription, and querying for vmss")
+	var controlPlane = &infraexpv1.AzureManagedControlPlane{}
+	Expect(input.BootstrapClusterProxy.GetClient().Get(ctx,
+		types.NamespacedName{input.Namespace.Name, input.ClusterName}, controlPlane)).To(Succeed())
+
+	settings, err := auth.GetSettingsFromEnvironment()
+	Expect(err).NotTo(HaveOccurred())
+	subscriptionID := settings.GetSubscriptionID()
+	authorizer, err := settings.GetAuthorizer()
+	Expect(err).NotTo(HaveOccurred())
+	vmssClient := compute.NewVirtualMachineScaleSetsClient(subscriptionID)
+	vmssClient.Authorizer = authorizer
+
+	rgName := controlPlane.Spec.NodeResourceGroupName
+	scalesets, err := vmssClient.List(ctx, rgName)
+	Expect(err).NotTo(HaveOccurred())
+	Expect(len(scalesets.Values())).To(BeNumerically(">", 0))
+}
diff --git a/test/e2e/azure_privatecluster.go b/test/e2e/azure_privatecluster.go
index 309447405..1056963a1 100644
--- a/test/e2e/azure_privatecluster.go
+++ b/test/e2e/azure_privatecluster.go
@@ -23,25 +23,18 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
-	"strings"
-	"time"
 
-	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-30/compute"
 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network"
 	"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-05-01/resources"
-	azuresdk "github.com/Azure/go-autorest/autorest/azure"
 	"github.com/Azure/go-autorest/autorest/azure/auth"
-	"github.com/Azure/go-autorest/autorest/to"
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/utils/pointer"
 	"sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4"
-	"sigs.k8s.io/cluster-api-provider-azure/azure"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
 	capi_e2e "sigs.k8s.io/cluster-api/test/e2e"
 	"sigs.k8s.io/cluster-api/test/framework"
@@ -126,7 +119,7 @@ func AzurePrivateClusterSpec(ctx context.Context, inputGetter func() AzurePrivat
 	//*************
 
 	By("Creating a private workload cluster")
-	clusterName = fmt.Sprintf("capz-e2e-%s-%s", util.RandomString(6), "private")
+	clusterName = fmt.Sprintf("capz-e2e-%s", util.RandomString(6))
 	Expect(os.Setenv(AzureInternalLBIP, "10.128.0.100")).NotTo(HaveOccurred())
 	result := &clusterctl.ApplyClusterTemplateAndWaitResult{}
 	clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{
@@ -151,33 +144,8 @@ func AzurePrivateClusterSpec(ctx context.Context, inputGetter func() AzurePrivat
 
 	Expect(cluster).ToNot(BeNil())
 
-	defer func() {
-		// Delete the private cluster, so that all of the Azure resources will be cleaned up when the public
-		// cluster is deleted at the end of the test. If we don't delete this cluster, the Azure resource delete
-		// verification will fail.
-		Logf("deleting private cluster %q in namespace %q", cluster.Name, cluster.Namespace)
-		Expect(publicClusterProxy.GetClient().Delete(ctx, cluster)).To(Succeed())
-		Eventually(func() error {
-			var c clusterv1.Cluster
-			err := publicClusterProxy.GetClient().Get(ctx, client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, &c)
-			if apierrors.IsNotFound(err) {
-				// 404 the cluster has been deleted
-				return nil
-			}
-
-			if err != nil {
-				// some unexpected error occurred; return it
-				return err
-			}
-
-			return fmt.Errorf("cluster %q as not yet been deleted", cluster.Name)
-		}, input.E2EConfig.GetIntervals(specName, "wait-delete-cluster")...).Should(BeNil())
-		Logf("deleted private cluster %q in namespace %q", cluster.Name, cluster.Namespace)
-	}()
-
 	// Check that azure bastion is provisioned successfully.
 	{
-		By("verifying the Azure Bastion Host was create successfully")
 		settings, err := auth.GetSettingsFromEnvironment()
 		Expect(err).To(BeNil())
 
@@ -217,7 +185,7 @@ func AzurePrivateClusterSpec(ctx context.Context, inputGetter func() AzurePrivat
 }
 
 // SetupExistingVNet creates a resource group and a VNet to be used by a workload cluster.
-func SetupExistingVNet(ctx context.Context, vnetCidr string, cpSubnetCidrs, nodeSubnetCidrs map[string]string) func() {
+func SetupExistingVNet(ctx context.Context, vnetCidr string, cpSubnetCidrs, nodeSubnetCidrs map[string]string) {
 	By("creating Azure clients with the workload cluster's subscription")
 	settings, err := auth.GetSettingsFromEnvironment()
 	Expect(err).NotTo(HaveOccurred())
@@ -361,88 +329,4 @@ func SetupExistingVNet(ctx context.Context, vnetCidr string, cpSubnetCidrs, node
 	Expect(err).To(BeNil())
 	err = vnetFuture.WaitForCompletionRef(ctx, vnetClient.Client)
 	Expect(err).To(BeNil())
-
-	return func() {
-		Logf("deleting an existing virtual network %q", os.Getenv(AzureVNetName))
-		vFuture, err := vnetClient.Delete(ctx, groupName, os.Getenv(AzureVNetName))
-		Expect(err).NotTo(HaveOccurred())
-		Expect(vFuture.WaitForCompletionRef(ctx, vnetClient.Client)).ToNot(HaveOccurred())
-
-		Logf("deleting an existing route table %q", routeTableName)
-		rtFuture, err := routetableClient.Delete(ctx, groupName, routeTableName)
-		Expect(err).NotTo(HaveOccurred())
-		Expect(rtFuture.WaitForCompletionRef(ctx, routetableClient.Client)).ToNot(HaveOccurred())
-
-		Logf("deleting an existing network security group %q", nsgNodeName)
-		nsgFuture, err := nsgClient.Delete(ctx, groupName, nsgNodeName)
-		Expect(err).NotTo(HaveOccurred())
-		Expect(nsgFuture.WaitForCompletionRef(ctx, nsgClient.Client)).NotTo(HaveOccurred())
-
-		Logf("deleting an existing network security group %q", nsgName)
-		nsgFuture, err = nsgClient.Delete(ctx, groupName, nsgName)
-		Expect(err).NotTo(HaveOccurred())
-		Expect(nsgFuture.WaitForCompletionRef(ctx, nsgClient.Client)).NotTo(HaveOccurred())
-
-		Logf("verifying the existing resource group %q is empty", groupName)
-		resClient := resources.NewClient(subscriptionID)
-		resClient.Authorizer = authorizer
-		Eventually(func() ([]resources.GenericResourceExpanded, error) {
-			page, err := resClient.ListByResourceGroup(ctx, groupName, "", "provisioningState", to.Int32Ptr(10))
-			if err != nil {
-				return nil, err
-			}
-
-			// for each resource do a GET directly for that resource to avoid hitting Azure list cache
-			var foundResources []resources.GenericResourceExpanded
-			for _, genericResource := range page.Values() {
-				apiversion, err := getAPIVersion(*genericResource.ID)
-				if err != nil {
-					Logf("failed to get API version for %q with %+v", *genericResource.ID, err)
-				}
-
-				_, err = resClient.GetByID(ctx, *genericResource.ID, apiversion)
-				if err != nil && azure.ResourceNotFound(err) {
-					// the resources is returned in the list, but it's actually 404
-					continue
-				}
-
-				// unexpected error calling GET on the resource
-				if err != nil {
-					Logf("failed GETing resource %q with %+v", *genericResource.ID, err)
-					return nil, err
-				}
-
-				// if resource is still there, then append to foundResources
-				foundResources = append(foundResources, genericResource)
-			}
-			return foundResources, nil
-			// add some tolerance for Azure caching of resource group resource caching
-		}, 5*time.Minute, 10*time.Second).Should(HaveLen(0), "Expect the manually created resource group is empty after removing the manually created resources.")
-
-		Logf("deleting the existing resource group %q", groupName)
-		grpFuture, err := groupClient.Delete(ctx, groupName)
-		Expect(err).NotTo(HaveOccurred())
-		Expect(grpFuture.WaitForCompletionRef(ctx, nsgClient.Client)).NotTo(HaveOccurred())
-	}
-}
-
-func getAPIVersion(resourceID string) (string, error) {
-	parsed, err := azuresdk.ParseResourceID(resourceID)
-	if err != nil {
-		return "", errors.Wrap(err, fmt.Sprintf("unable to parse resource ID %q", resourceID))
-	}
-
-	switch parsed.Provider {
-	case "Microsoft.Network":
-		return getAPIVersionFromUserAgent(network.UserAgent()), nil
-	case "Microsoft.Compute":
-		return getAPIVersionFromUserAgent(compute.UserAgent()), nil
-	default:
-		return "", fmt.Errorf("failed to find an API version for resource provider %q", parsed.Provider)
-	}
-}
-
-func getAPIVersionFromUserAgent(userAgent string) string {
-	splits := strings.Split(userAgent, "/")
-	return splits[len(splits) - 1]
 }
diff --git a/test/e2e/azure_selfhosted.go b/test/e2e/azure_selfhosted.go
index 0dfeb9054..a568634f8 100644
--- a/test/e2e/azure_selfhosted.go
+++ b/test/e2e/azure_selfhosted.go
@@ -75,7 +75,6 @@ func SelfHostedSpec(ctx context.Context, inputGetter func() SelfHostedSpecInput)
 		// Setup a Namespace where to host objects for this spec and create a watcher for the namespace events.
 		var err error
 		namespace, cancelWatches, err = setupSpecNamespace(ctx, specName, input.BootstrapClusterProxy, input.ArtifactFolder)
-		Expect(err).ToNot(HaveOccurred())
 		clusterResources = new(clusterctl.ApplyClusterTemplateAndWaitResult)
 
 		spClientSecret := os.Getenv(AzureClientSecret)
@@ -228,16 +227,7 @@ func SelfHostedSpec(ctx context.Context, inputGetter func() SelfHostedSpecInput)
 			selfHostedCancelWatches()
 		}
 
-		cleanInput := cleanupInput{
-			SpecName:        specName,
-			Cluster:         clusterResources.Cluster,
-			ClusterProxy:    input.BootstrapClusterProxy,
-			Namespace:       namespace,
-			CancelWatches:   cancelWatches,
-			IntervalsGetter: input.E2EConfig.GetIntervals,
-			SkipCleanup:     input.SkipCleanup,
-		}
 		// Dumps all the resources in the spec namespace, then cleanups the cluster object and the spec namespace itself.
-		dumpSpecResourcesAndCleanup(ctx, cleanInput)
+		dumpSpecResourcesAndCleanup(ctx, specName, input.BootstrapClusterProxy, input.ArtifactFolder, namespace, cancelWatches, clusterResources.Cluster, input.E2EConfig.GetIntervals, input.SkipCleanup)
 	})
 }
diff --git a/test/e2e/azure_test.go b/test/e2e/azure_test.go
index 06332b523..58f286a63 100644
--- a/test/e2e/azure_test.go
+++ b/test/e2e/azure_test.go
@@ -40,15 +40,13 @@ import (
 
 var _ = Describe("Workload cluster creation", func() {
 	var (
-		ctx               = context.TODO()
-		specName          = "create-workload-cluster"
-		namespace         *corev1.Namespace
-		cancelWatches     context.CancelFunc
-		result            *clusterctl.ApplyClusterTemplateAndWaitResult
-		clusterName       string
-		clusterNamePrefix string
-		additionalCleanup func()
-		specTimes         = map[string]time.Time{}
+		ctx           = context.TODO()
+		specName      = "create-workload-cluster"
+		namespace     *corev1.Namespace
+		cancelWatches context.CancelFunc
+		result        *clusterctl.ApplyClusterTemplateAndWaitResult
+		clusterName   string
+		specTimes     = map[string]time.Time{}
 	)
 
 	BeforeEach(func() {
@@ -59,15 +57,22 @@ var _ = Describe("Workload cluster creation", func() {
 		Expect(clusterctlConfigPath).To(BeAnExistingFile(), "Invalid argument. clusterctlConfigPath must be an existing file when calling %s spec", specName)
 		Expect(bootstrapClusterProxy).ToNot(BeNil(), "Invalid argument. bootstrapClusterProxy can't be nil when calling %s spec", specName)
 		Expect(os.MkdirAll(artifactFolder, 0755)).To(Succeed(), "Invalid argument. artifactFolder can't be created for %s spec", specName)
+
 		Expect(e2eConfig.Variables).To(HaveKey(capi_e2e.KubernetesVersion))
 
-		clusterNamePrefix = fmt.Sprintf("capz-e2e-%s", util.RandomString(6))
+		clusterName = os.Getenv("CLUSTER_NAME")
+		if clusterName == "" {
+			clusterName = fmt.Sprintf("capz-e2e-%s", util.RandomString(6))
+		}
+		fmt.Fprintf(GinkgoWriter, "INFO: Cluster name is %s\n", clusterName)
 
 		// Setup a Namespace where to host objects for this spec and create a watcher for the namespace events.
 		var err error
-		namespace, cancelWatches, err = setupSpecNamespace(ctx, clusterNamePrefix, bootstrapClusterProxy, artifactFolder)
+		namespace, cancelWatches, err = setupSpecNamespace(ctx, clusterName, bootstrapClusterProxy, artifactFolder)
 		Expect(err).NotTo(HaveOccurred())
 
+		Expect(os.Setenv(AzureResourceGroup, clusterName)).NotTo(HaveOccurred())
+		Expect(os.Setenv(AzureVNetName, fmt.Sprintf("%s-vnet", clusterName))).NotTo(HaveOccurred())
 		result = new(clusterctl.ApplyClusterTemplateAndWaitResult)
 
 		spClientSecret := os.Getenv(AzureClientSecret)
@@ -90,7 +95,6 @@ var _ = Describe("Workload cluster creation", func() {
 		Expect(os.Setenv(ClusterIdentityNamespace, namespace.Name)).NotTo(HaveOccurred())
 		Expect(os.Setenv(ClusterIdentitySecretName, "cluster-identity-secret")).NotTo(HaveOccurred())
 		Expect(os.Setenv(ClusterIdentitySecretNamespace, namespace.Name)).NotTo(HaveOccurred())
-		additionalCleanup = nil
 	})
 
 	AfterEach(func() {
@@ -98,18 +102,7 @@ var _ = Describe("Workload cluster creation", func() {
 			// this means the cluster failed to come up. We make an attempt to find the cluster to be able to fetch logs for the failed bootstrapping.
 			_ = bootstrapClusterProxy.GetClient().Get(ctx, types.NamespacedName{Name: clusterName, Namespace: namespace.Name}, result.Cluster)
 		}
-
-		cleanInput := cleanupInput{
-			SpecName:          specName,
-			Cluster:           result.Cluster,
-			ClusterProxy:      bootstrapClusterProxy,
-			Namespace:         namespace,
-			CancelWatches:     cancelWatches,
-			IntervalsGetter:   e2eConfig.GetIntervals,
-			SkipCleanup:       skipCleanup,
-			AdditionalCleanup: additionalCleanup,
-		}
-		dumpSpecResourcesAndCleanup(ctx, cleanInput)
+		dumpSpecResourcesAndCleanup(ctx, specName, bootstrapClusterProxy, artifactFolder, namespace, cancelWatches, result.Cluster, e2eConfig.GetIntervals, skipCleanup)
 		Expect(os.Unsetenv(AzureResourceGroup)).NotTo(HaveOccurred())
 		Expect(os.Unsetenv(AzureVNetName)).NotTo(HaveOccurred())
 
@@ -119,14 +112,13 @@ var _ = Describe("Workload cluster creation", func() {
 	if os.Getenv("LOCAL_ONLY") != "true" {
 		Context("Creating a private cluster", func() {
 			It("Creates a public management cluster in the same vnet", func() {
-				clusterName = getClusterName(clusterNamePrefix, "public-custom-vnet")
 				Context("Creating a custom virtual network", func() {
 					Expect(os.Setenv(AzureVNetName, "custom-vnet")).NotTo(HaveOccurred())
 					cpCIDR := "10.128.0.0/16"
 					Expect(os.Setenv(AzureCPSubnetCidr, cpCIDR)).NotTo(HaveOccurred())
 					nodeCIDR := "10.129.0.0/16"
 					Expect(os.Setenv(AzureNodeSubnetCidr, nodeCIDR)).NotTo(HaveOccurred())
-					additionalCleanup = SetupExistingVNet(ctx,
+					SetupExistingVNet(ctx,
 						"10.0.0.0/8",
 						map[string]string{fmt.Sprintf("%s-controlplane-subnet", clusterName): "10.0.0.0/16", "private-cp-subnet": cpCIDR},
 						map[string]string{fmt.Sprintf("%s-node-subnet", clusterName): "10.1.0.0/16", "private-node-subnet": nodeCIDR})
@@ -180,7 +172,6 @@ var _ = Describe("Workload cluster creation", func() {
 	}
 
 	It("With 3 control-plane nodes and 2 worker nodes", func() {
-		clusterName = getClusterName(clusterNamePrefix, "ha")
 		clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{
 			ClusterProxy: bootstrapClusterProxy,
 			ConfigCluster: clusterctl.ConfigClusterInput{
@@ -254,7 +245,6 @@ var _ = Describe("Workload cluster creation", func() {
 
 	Context("Creating a ipv6 control-plane cluster", func() {
 		It("With ipv6 worker node", func() {
-			clusterName = getClusterName(clusterNamePrefix, "ipv6")
 			clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{
 				ClusterProxy: bootstrapClusterProxy,
 				ConfigCluster: clusterctl.ConfigClusterInput{
@@ -300,7 +290,6 @@ var _ = Describe("Workload cluster creation", func() {
 
 	Context("Creating a VMSS cluster", func() {
 		It("with a single control plane node and an AzureMachinePool with 2 nodes", func() {
-			clusterName = getClusterName(clusterNamePrefix, "vmss")
 			clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{
 				ClusterProxy: bootstrapClusterProxy,
 				ConfigCluster: clusterctl.ConfigClusterInput{
@@ -362,7 +351,6 @@ var _ = Describe("Workload cluster creation", func() {
 	// See https://azure.microsoft.com/en-us/pricing/details/virtual-machines/linux/ for pricing.
 	Context("Creating a GPU-enabled cluster", func() {
 		It("with a single control plane node and 1 node", func() {
-			clusterName = getClusterName(clusterNamePrefix, "gpu")
 			clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{
 				ClusterProxy: bootstrapClusterProxy,
 				ConfigCluster: clusterctl.ConfigClusterInput{
@@ -412,7 +400,6 @@ var _ = Describe("Workload cluster creation", func() {
 	// To include this test, set `GINKGO_SKIP=""`.
 	Context("Creating a cluster that uses the external cloud provider", func() {
 		It("with a 1 control plane nodes and 2 worker nodes", func() {
-			clusterName = getClusterName(clusterNamePrefix, "oot")
 			clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{
 				ClusterProxy: bootstrapClusterProxy,
 				ConfigCluster: clusterctl.ConfigClusterInput{
@@ -445,9 +432,8 @@ var _ = Describe("Workload cluster creation", func() {
 		})
 	})
 
-	Context("Creating an AKS cluster", func() {
+	Context("Creating an AKS cluster using a different SP identity", func() {
 		It("with a single control plane node and 1 node", func() {
-			clusterName = getClusterName(clusterNamePrefix, "aks")
 			kubernetesVersion, err := GetAKSKubernetesVersion(ctx, e2eConfig)
 			Expect(err).To(BeNil())
 
@@ -474,9 +460,9 @@ var _ = Describe("Workload cluster creation", func() {
 				},
 			}, result)
 
-			Context("Validating AKS time synchronization", func() {
-				AzureDaemonsetTimeSyncSpec(ctx, func() AzureTimeSyncSpecInput {
-					return AzureTimeSyncSpecInput{
+			Context("Validating AKS Resources", func() {
+				AKSResourcesValidationSpec(ctx, func() AKSResourcesValidationSpecInput {
+					return AKSResourcesValidationSpecInput{
 						BootstrapClusterProxy: bootstrapClusterProxy,
 						Namespace:             namespace,
 						ClusterName:           clusterName,
@@ -489,7 +475,6 @@ var _ = Describe("Workload cluster creation", func() {
 	Context("Creating a Windows Enabled cluster", func() {
 		// Requires 3 control planes due to https://github.com/kubernetes-sigs/cluster-api-provider-azure/issues/857
 		It("With 3 control-plane nodes and 1 Linux worker node and 1 Windows worker node", func() {
-			clusterName = getClusterName(clusterNamePrefix, "win-ha")
 			clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{
 				ClusterProxy: bootstrapClusterProxy,
 				ConfigCluster: clusterctl.ConfigClusterInput{
@@ -539,7 +524,6 @@ var _ = Describe("Workload cluster creation", func() {
 
 	Context("Creating a Windows enabled VMSS cluster", func() {
 		It("with a single control plane node and an Linux AzureMachinePool with 1 nodes and Windows AzureMachinePool with 1 node", func() {
-			clusterName = getClusterName(clusterNamePrefix, "win-vmss")
 			clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{
 				ClusterProxy: bootstrapClusterProxy,
 				ConfigCluster: clusterctl.ConfigClusterInput{
diff --git a/test/e2e/azure_timesync.go b/test/e2e/azure_timesync.go
index f6131d886..4252d5a38 100644
--- a/test/e2e/azure_timesync.go
+++ b/test/e2e/azure_timesync.go
@@ -21,21 +21,14 @@ package e2e
 import (
 	"context"
 	"fmt"
-	"io/ioutil"
 	"strings"
 	"time"
 
 	. "github.com/onsi/gomega"
 	"github.com/pkg/errors"
-	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/types"
-	e2e_pod "sigs.k8s.io/cluster-api-provider-azure/test/e2e/kubernetes/pod"
 	"sigs.k8s.io/cluster-api/test/framework"
-	"sigs.k8s.io/controller-runtime/pkg/client"
 	kinderrors "sigs.k8s.io/kind/pkg/errors"
-	"sigs.k8s.io/yaml"
 )
 
 // AzureTimeSyncSpecInput is the input for AzureTimeSyncSpec.
@@ -101,137 +94,3 @@ func AzureTimeSyncSpec(ctx context.Context, inputGetter func() AzureTimeSyncSpec
 		return kinderrors.AggregateConcurrent(testFuncs)
 	}, thirty, thirty).Should(Succeed())
 }
-
-const (
-	nsenterWorkloadFile = "workloads/nsenter/daemonset.yaml"
-)
-
-// AzureDaemonsetTimeSyncSpec implements a test that verifies time synchronization is healthy for
-// the nodes in a cluster. It uses a privileged daemonset and nsenter instead of SSH.
-func AzureDaemonsetTimeSyncSpec(ctx context.Context, inputGetter func() AzureTimeSyncSpecInput) {
-	var (
-		specName = "azure-timesync"
-		input    AzureTimeSyncSpecInput
-		sixty    = 60 * time.Second
-		five     = 5 * time.Second
-	)
-
-	input = inputGetter()
-	Expect(input.BootstrapClusterProxy).NotTo(BeNil(), "Invalid argument. input.BootstrapClusterProxy can't be nil when calling %s spec", specName)
-	namespace, clusterName := input.Namespace.Name, input.ClusterName
-	workloadCluster := input.BootstrapClusterProxy.GetWorkloadCluster(ctx, namespace, clusterName)
-	kubeclient := workloadCluster.GetClient()
-	clientset := workloadCluster.GetClientSet()
-	config := workloadCluster.GetRESTConfig()
-	var nsenterDs unstructured.Unstructured
-
-	yamlData, err := ioutil.ReadFile(nsenterWorkloadFile)
-	Expect(err).NotTo(HaveOccurred(), "failed to unmarshal nsenter daemonset from yaml")
-
-	var jsonData []byte
-	jsonData, err = yaml.YAMLToJSON(yamlData)
-	Expect(err).NotTo(HaveOccurred(), "failed to convert nsenter yaml to json")
-
-	err = nsenterDs.UnmarshalJSON(jsonData)
-	Expect(err).NotTo(HaveOccurred(), "failed to unmarshal nsenter json bytes to unstructured")
-
-	nsenterDs.SetNamespace("default")
-
-	err = kubeclient.Create(ctx, &nsenterDs)
-	Expect(err).NotTo(HaveOccurred(), "failed to create daemonset for time sync check")
-
-	var nodes corev1.NodeList
-	kubeclient.List(ctx, &nodes)
-	Expect(err).NotTo(HaveOccurred(), "failed to list nodes for daemonset timesync check")
-	Expect(len(nodes.Items)).Should(BeNumerically(">", 1), "expected to find >= 1 node for timesync check")
-	desired := int32(len(nodes.Items))
-
-	Eventually(func() error {
-		var ds appsv1.DaemonSet
-		if err := kubeclient.Get(ctx, types.NamespacedName{"default", "nsenter"}, &ds); err != nil {
-			Logf("failed to get nsenter ds: %s", err)
-			return err
-		}
-		ready := ds.Status.NumberReady
-		available := ds.Status.NumberAvailable
-		allReadyAndAvailable := desired == ready && desired == available
-		generationOk := ds.ObjectMeta.Generation == ds.Status.ObservedGeneration
-
-		msg := fmt.Sprintf("want %d instances, found %d ready and %d available. generation: %d, observedGeneration: %d", desired, ready, available, ds.ObjectMeta.Generation, ds.Status.ObservedGeneration)
-		Logf(msg)
-		if allReadyAndAvailable && generationOk {
-			return nil
-		}
-		return fmt.Errorf(msg)
-	}, sixty, five).Should(Succeed())
-
-	var podList corev1.PodList
-	err = kubeclient.List(ctx, &podList, client.MatchingLabels(map[string]string{"app": "nsenter"}))
-	Expect(err).NotTo(HaveOccurred(), "failed to list pods for daemonset timesync check")
-
-	Logf("mapping nsenter pods to hostnames for host-by-host execution")
-	podMap := map[string]corev1.Pod{}
-	for _, pod := range podList.Items {
-		podMap[pod.Spec.NodeName] = pod
-	}
-
-	for k, v := range podMap {
-		Logf("found host %s with pod %s", k, v.Name)
-	}
-
-	Eventually(func() error {
-		execInfo, err := getClusterSSHInfo(ctx, input.BootstrapClusterProxy, namespace, clusterName)
-		if err != nil {
-			return err
-		}
-
-		if len(execInfo) <= 0 {
-			return errors.New("execInfo did not contain any machines")
-		}
-
-		nsenterCmd := []string{"nsenter", "--target", "1", "--mount=/proc/1/ns/mnt", "--", "/bin/sh", "-c"}
-		var testFuncs []func() error
-		for key := range execInfo {
-			s := execInfo[key]
-			Byf("checking that time synchronization is healthy on %s", s.Hostname)
-			execToStringFn := func(expected, command string) func() error {
-				// don't assert in this test func, just return errors
-				return func() error {
-					pod, exists := podMap[s.Hostname]
-					if !exists {
-						return fmt.Errorf("failed to find pod matching host %s", s.Hostname)
-					}
-
-					fullCommand := make([]string, 0, len(nsenterCmd)+1)
-					fullCommand = append(fullCommand, nsenterCmd...)
-					fullCommand = append(fullCommand, command)
-					stdout, err := e2e_pod.ExecWithOutput(clientset, config, pod, fullCommand)
-					if err != nil {
-						return fmt.Errorf("failed to nsenter host %s, error: '%s', stdout:  '%s'", s.Hostname, err, stdout.String())
-					}
-
-					if !strings.Contains(stdout.String(), expected) {
-						return fmt.Errorf("expected \"%s\" in command output:\n%s", expected, stdout.String())
-					}
-
-					Byf("time sync OK for host %s", s.Hostname)
-
-					return nil
-				}
-			}
-
-			testFuncs = append(testFuncs,
-				execToStringFn(
-					"chronyd is active",
-					"systemctl is-active chronyd && echo chronyd is active",
-				),
-				execToStringFn(
-					"Reference ID",
-					"chronyc tracking",
-				),
-			)
-		}
-
-		return kinderrors.AggregateConcurrent(testFuncs)
-	}, sixty, five).Should(Succeed())
-}
diff --git a/test/e2e/common.go b/test/e2e/common.go
index f8d5eac2c..ddce618b4 100644
--- a/test/e2e/common.go
+++ b/test/e2e/common.go
@@ -22,21 +22,19 @@ import (
 	"context"
 	"fmt"
 	"io/ioutil"
-	"os"
 	"os/exec"
 	"path"
 	"path/filepath"
 
-	"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2020-10-01/resources"
-	"github.com/Azure/go-autorest/autorest/azure/auth"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+
+	e2e_namespace "sigs.k8s.io/cluster-api-provider-azure/test/e2e/kubernetes/namespace"
+
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/clientcmd"
-	"sigs.k8s.io/cluster-api-provider-azure/azure"
-	e2e_namespace "sigs.k8s.io/cluster-api-provider-azure/test/e2e/kubernetes/namespace"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
 	"sigs.k8s.io/cluster-api/test/framework"
 	"sigs.k8s.io/cluster-api/util/kubeconfig"
@@ -106,79 +104,45 @@ func setupSpecNamespace(ctx context.Context, namespaceName string, clusterProxy
 	return namespace, cancelWatches, nil
 }
 
-type cleanupInput struct {
-	SpecName          string
-	ClusterProxy      framework.ClusterProxy
-	ArtifactFolder    string
-	Namespace         *corev1.Namespace
-	CancelWatches     context.CancelFunc
-	Cluster           *clusterv1.Cluster
-	IntervalsGetter   func(spec, key string) []interface{}
-	SkipCleanup       bool
-	AdditionalCleanup func()
-}
-
-func dumpSpecResourcesAndCleanup(ctx context.Context, input cleanupInput) {
+func dumpSpecResourcesAndCleanup(ctx context.Context, specName string, clusterProxy framework.ClusterProxy, artifactFolder string, namespace *corev1.Namespace, cancelWatches context.CancelFunc, cluster *clusterv1.Cluster, intervalsGetter func(spec, key string) []interface{}, skipCleanup bool) {
 	defer func() {
-		input.CancelWatches()
+		cancelWatches()
 		redactLogs()
 	}()
 
-	if input.Cluster == nil {
+	if cluster == nil {
 		By("Unable to dump workload cluster logs as the cluster is nil")
 	} else {
-		Byf("Dumping logs from the %q workload cluster", input.Cluster.Name)
-		input.ClusterProxy.CollectWorkloadClusterLogs(ctx, input.Cluster.Namespace, input.Cluster.Name, filepath.Join(input.ArtifactFolder, "clusters", input.Cluster.Name))
+		Byf("Dumping logs from the %q workload cluster", cluster.Name)
+		clusterProxy.CollectWorkloadClusterLogs(ctx, cluster.Namespace, cluster.Name, filepath.Join(artifactFolder, "clusters", cluster.Name))
 	}
 
-	Byf("Dumping all the Cluster API resources in the %q namespace", input.Namespace.Name)
+	Byf("Dumping all the Cluster API resources in the %q namespace", namespace.Name)
 	// Dump all Cluster API related resources to artifacts before deleting them.
 	framework.DumpAllResources(ctx, framework.DumpAllResourcesInput{
-		Lister:    input.ClusterProxy.GetClient(),
-		Namespace: input.Namespace.Name,
-		LogPath:   filepath.Join(input.ArtifactFolder, "clusters", input.ClusterProxy.GetName(), "resources"),
+		Lister:    clusterProxy.GetClient(),
+		Namespace: namespace.Name,
+		LogPath:   filepath.Join(artifactFolder, "clusters", clusterProxy.GetName(), "resources"),
 	})
 
-	if input.SkipCleanup {
+	if skipCleanup {
 		return
 	}
 
-	Byf("Deleting all clusters in the %s namespace", input.Namespace.Name)
+	Byf("Deleting all clusters in the %s namespace", namespace.Name)
 	// While https://github.com/kubernetes-sigs/cluster-api/issues/2955 is addressed in future iterations, there is a chance
 	// that cluster variable is not set even if the cluster exists, so we are calling DeleteAllClustersAndWait
 	// instead of DeleteClusterAndWait
 	framework.DeleteAllClustersAndWait(ctx, framework.DeleteAllClustersAndWaitInput{
-		Client:    input.ClusterProxy.GetClient(),
-		Namespace: input.Namespace.Name,
-	}, input.IntervalsGetter(input.SpecName, "wait-delete-cluster")...)
+		Client:    clusterProxy.GetClient(),
+		Namespace: namespace.Name,
+	}, intervalsGetter(specName, "wait-delete-cluster")...)
 
-	Byf("Deleting namespace used for hosting the %q test spec", input.SpecName)
+	Byf("Deleting namespace used for hosting the %q test spec", specName)
 	framework.DeleteNamespace(ctx, framework.DeleteNamespaceInput{
-		Deleter: input.ClusterProxy.GetClient(),
-		Name:    input.Namespace.Name,
+		Deleter: clusterProxy.GetClient(),
+		Name:    namespace.Name,
 	})
-
-	if input.AdditionalCleanup != nil {
-		Byf("Running additional cleanup for the %q test spec", input.SpecName)
-		input.AdditionalCleanup()
-	}
-
-	Byf("Checking if any resources are left over in Azure for spec %q", input.SpecName)
-	ExpectResourceGroupToBe404(ctx)
-}
-
-// ExpectResourceGroupToBe404 performs a GET request to Azure to determine if the cluster resource group still exists.
-// If it does still exist, it means the cluster was not deleted and is leaking Azure resources.
-func ExpectResourceGroupToBe404(ctx context.Context) {
-	settings, err := auth.GetSettingsFromEnvironment()
-	Expect(err).NotTo(HaveOccurred())
-	subscriptionID := settings.GetSubscriptionID()
-	authorizer, err := settings.GetAuthorizer()
-	Expect(err).NotTo(HaveOccurred())
-	groupsClient := resources.NewGroupsClient(subscriptionID)
-	groupsClient.Authorizer = authorizer
-	_, err = groupsClient.Get(ctx, os.Getenv(AzureResourceGroup))
-	Expect(azure.ResourceNotFound(err)).To(BeTrue(), "The resource group in Azure still exists. After deleting the cluster all of the Azure resources should also be deleted.")
 }
 
 func redactLogs() {
diff --git a/test/e2e/conformance_test.go b/test/e2e/conformance_test.go
index 47f3f5131..139504c84 100644
--- a/test/e2e/conformance_test.go
+++ b/test/e2e/conformance_test.go
@@ -193,17 +193,8 @@ var _ = Describe("Conformance Tests", func() {
 			_ = bootstrapClusterProxy.GetClient().Get(ctx, types.NamespacedName{Name: clusterName, Namespace: namespace.Name}, result.Cluster)
 		}
 
-		cleanInput := cleanupInput{
-			SpecName:          specName,
-			Cluster:           result.Cluster,
-			ClusterProxy:      bootstrapClusterProxy,
-			Namespace:         namespace,
-			CancelWatches:     cancelWatches,
-			IntervalsGetter:   e2eConfig.GetIntervals,
-			SkipCleanup:       skipCleanup,
-		}
 		// Dumps all the resources in the spec namespace, then cleanups the cluster object and the spec namespace itself.
-		dumpSpecResourcesAndCleanup(ctx, cleanInput)
+		dumpSpecResourcesAndCleanup(ctx, specName, bootstrapClusterProxy, artifactFolder, namespace, cancelWatches, result.Cluster, e2eConfig.GetIntervals, skipCleanup)
 
 		Expect(os.Unsetenv(AzureResourceGroup)).NotTo(HaveOccurred())
 		Expect(os.Unsetenv(AzureVNetName)).NotTo(HaveOccurred())
diff --git a/test/e2e/helpers.go b/test/e2e/helpers.go
index f0ffa5372..83978626d 100644
--- a/test/e2e/helpers.go
+++ b/test/e2e/helpers.go
@@ -303,20 +303,6 @@ func logCheckpoint(specTimes map[string]time.Time) {
 	}
 }
 
-// getClusterName gets the cluster name for the test cluster
-// and sets the environment variables that depend on it.
-func getClusterName(prefix, specName string) string {
-	clusterName := os.Getenv("CLUSTER_NAME")
-	if clusterName == "" {
-		clusterName = fmt.Sprintf("%s-%s", prefix, specName)
-	}
-	fmt.Fprintf(GinkgoWriter, "INFO: Cluster name is %s\n", clusterName)
-
-	Expect(os.Setenv(AzureResourceGroup, clusterName)).NotTo(HaveOccurred())
-	Expect(os.Setenv(AzureVNetName, fmt.Sprintf("%s-vnet", clusterName))).NotTo(HaveOccurred())
-	return clusterName
-}
-
 // nodeSSHInfo provides information to establish an SSH connection to a VM or VMSS instance.
 type nodeSSHInfo struct {
 	Endpoint string // Endpoint is the control plane hostname or IP address for initial connection.
diff --git a/test/e2e/kubernetes/pod/pod.go b/test/e2e/kubernetes/pod/pod.go
index 63e02c013..908cf869d 100644
--- a/test/e2e/kubernetes/pod/pod.go
+++ b/test/e2e/kubernetes/pod/pod.go
@@ -19,7 +19,6 @@ limitations under the License.
 package pod
 
 import (
-	"bytes"
 	"os"
 
 	v1 "k8s.io/api/core/v1"
@@ -57,31 +56,3 @@ func Exec(clientset *kubernetes.Clientset, config *restclient.Config, pod v1.Pod
 
 	return nil
 }
-
-func ExecWithOutput(clientset *kubernetes.Clientset, config *restclient.Config, pod v1.Pod, command []string) (*bytes.Buffer, error) {
-	req := clientset.CoreV1().RESTClient().Post().Resource("pods").Name(pod.GetName()).
-		Namespace(pod.GetNamespace()).SubResource("exec")
-	option := &v1.PodExecOptions{
-		Command: command,
-		Stdin:   false,
-		Stdout:  true,
-		Stderr:  true,
-		TTY:     true,
-	}
-	req.VersionedParams(
-		option,
-		scheme.ParameterCodec,
-	)
-	exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
-	if err != nil {
-		return nil, err
-	}
-	stdout := bytes.NewBuffer(nil)
-	err = exec.Stream(remotecommand.StreamOptions{
-		Stdout: stdout,
-		// needs to be populated or else exec hangs, but we don't need the output. Failures write to stdout when TTY enabled.
-		Stderr: bytes.NewBuffer(nil),
-	})
-
-	return stdout, err
-}
diff --git a/test/e2e/workloads/nsenter/daemonset.yaml b/test/e2e/workloads/nsenter/daemonset.yaml
deleted file mode 100644
index 5e6d5f729..000000000
--- a/test/e2e/workloads/nsenter/daemonset.yaml
+++ /dev/null
@@ -1,25 +0,0 @@
-apiVersion: apps/v1
-kind: DaemonSet
-metadata:
-  name: &name nsenter
-  labels:
-    app: *name
-spec:
-  selector:
-    matchLabels:
-      app: *name
-  template:
-    metadata:
-      labels:
-        app: *name
-    spec:
-      hostPID: true
-      containers:
-      - image: us.gcr.io/k8s-artifacts-prod/busybox:1.27.2
-        name: nsenter
-        command: ["sleep", "100d"] # busybox sleep doesn't support infinity, so use a really long time.
-        resources:
-          requests: {}
-          limits: {}
-        securityContext:
-          privileged: true