From 4bec8b8eabe6c210120aa5fcb63dd1ebead72388 Mon Sep 17 00:00:00 2001
From: Brian Lieberman <blieberman@newrelic.com>
Date: Tue, 7 Jun 2022 08:12:07 -0400
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20multiple=20ne?=
 =?UTF-8?q?twork=20interfaces?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Dane Thorsen <dthorsen@users.noreply.github.com>
---
 api/v1alpha3/azuremachine_conversion.go       |   5 +
 .../azuremachinetemplate_conversion.go        |   5 +
 api/v1alpha3/zz_generated.conversion.go       |   1 +
 api/v1alpha4/azuremachine_conversion.go       |   4 +
 .../azuremachinetemplate_conversion.go        |   4 +
 api/v1alpha4/zz_generated.conversion.go       |   1 +
 api/v1beta1/azuremachine_default.go           |  29 ++
 api/v1beta1/azuremachine_default_test.go      |  81 ++++
 api/v1beta1/azuremachine_types.go             |  13 +-
 api/v1beta1/azuremachine_validation.go        |  23 +
 api/v1beta1/azuremachine_validation_test.go   |  93 ++++
 api/v1beta1/azuremachine_webhook.go           |  20 +
 api/v1beta1/azuremachine_webhook_test.go      |  55 +++
 api/v1beta1/azuremachinetemplate_webhook.go   |   5 +
 .../azuremachinetemplate_webhook_test.go      | 104 +++++
 api/v1beta1/types.go                          |  18 +
 api/v1beta1/zz_generated.deepcopy.go          |  27 ++
 azure/defaults.go                             |   5 +-
 azure/scope/clients.go                        |   1 -
 azure/scope/machine.go                        |  83 ++--
 azure/scope/machine_test.go                   | 432 +++++++++++++++++-
 azure/scope/machinepool.go                    |   9 +-
 azure/scope/machinepool_test.go               |  84 +++-
 .../networkinterfaces_test.go                 |  21 +
 azure/services/networkinterfaces/spec.go      |  62 ++-
 azure/services/networkinterfaces/spec_test.go | 236 ++++++++++
 azure/services/scalesets/scalesets.go         |  96 ++++
 azure/services/scalesets/scalesets_test.go    |  70 +++
 azure/types.go                                |   1 +
 ...re.cluster.x-k8s.io_azuremachinepools.yaml |  38 +-
 ...ucture.cluster.x-k8s.io_azuremachines.yaml |  37 +-
 ...luster.x-k8s.io_azuremachinetemplates.yaml |  40 +-
 controllers/azuremachine_reconciler.go        |   3 +
 .../v1alpha3/azuremachinepool_conversion.go   |   5 +
 exp/api/v1alpha3/zz_generated.conversion.go   |   1 +
 .../v1alpha4/azuremachinepool_conversion.go   |   8 +
 .../azuremachinepoolmachine_conversion.go     |  12 +-
 exp/api/v1alpha4/zz_generated.conversion.go   |   1 +
 exp/api/v1beta1/azuremachinepool_default.go   |  28 ++
 .../v1beta1/azuremachinepool_default_test.go  |  94 ++++
 exp/api/v1beta1/azuremachinepool_types.go     |  13 +-
 exp/api/v1beta1/azuremachinepool_webhook.go   |  10 +
 .../v1beta1/azuremachinepool_webhook_test.go  |  44 ++
 exp/api/v1beta1/zz_generated.deepcopy.go      |   7 +
 .../azuremachinepool_reconciler.go            |   3 +
 test/e2e/aks_versions.go                      |   3 +-
 util/azure/azure.go                           |   2 +-
 47 files changed, 1853 insertions(+), 84 deletions(-)

diff --git a/api/v1alpha3/azuremachine_conversion.go b/api/v1alpha3/azuremachine_conversion.go
index ae03ca080ba..601290a64fd 100644
--- a/api/v1alpha3/azuremachine_conversion.go
+++ b/api/v1alpha3/azuremachine_conversion.go
@@ -74,6 +74,11 @@ func (src *AzureMachine) ConvertTo(dstRaw conversion.Hub) error {
 		dst.Spec.Diagnostics = restored.Spec.Diagnostics
 	}
 
+	if restored.Spec.NetworkInterfaces != nil {
+		dst.Spec.NetworkInterfaces = restored.Spec.NetworkInterfaces
+	}
+
+	//nolint:staticcheck // SubnetName is now deprecated, but the v1beta1 defaulting webhook will migrate it to the networkInterfaces field
 	dst.Spec.SubnetName = restored.Spec.SubnetName
 
 	dst.Status.LongRunningOperationStates = restored.Status.LongRunningOperationStates
diff --git a/api/v1alpha3/azuremachinetemplate_conversion.go b/api/v1alpha3/azuremachinetemplate_conversion.go
index faf6673b62b..32c9a8d9f8b 100644
--- a/api/v1alpha3/azuremachinetemplate_conversion.go
+++ b/api/v1alpha3/azuremachinetemplate_conversion.go
@@ -62,6 +62,7 @@ func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error {
 		dst.Spec.Template.Spec.Diagnostics = restored.Spec.Template.Spec.Diagnostics
 	}
 
+	//nolint:staticcheck // SubnetName is now deprecated, but the v1beta1 defaulting webhook will migrate it to the networkInterfaces field
 	dst.Spec.Template.Spec.SubnetName = restored.Spec.Template.Spec.SubnetName
 	dst.Spec.Template.ObjectMeta = restored.Spec.Template.ObjectMeta
 
@@ -77,6 +78,10 @@ func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error {
 		dst.Spec.Template.Spec.SpotVMOptions.EvictionPolicy = restored.Spec.Template.Spec.SpotVMOptions.EvictionPolicy
 	}
 
+	if restored.Spec.Template.Spec.NetworkInterfaces != nil {
+		dst.Spec.Template.Spec.NetworkInterfaces = restored.Spec.Template.Spec.NetworkInterfaces
+	}
+
 	return nil
 }
 
diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go
index 609600b5dc5..62ead6593c4 100644
--- a/api/v1alpha3/zz_generated.conversion.go
+++ b/api/v1alpha3/zz_generated.conversion.go
@@ -925,6 +925,7 @@ func autoConvert_v1beta1_AzureMachineSpec_To_v1alpha3_AzureMachineSpec(in *v1bet
 	// WARNING: in.SubnetName requires manual conversion: does not exist in peer-type
 	// WARNING: in.DNSServers requires manual conversion: does not exist in peer-type
 	// WARNING: in.VMExtensions requires manual conversion: does not exist in peer-type
+	// WARNING: in.NetworkInterfaces requires manual conversion: does not exist in peer-type
 	return nil
 }
 
diff --git a/api/v1alpha4/azuremachine_conversion.go b/api/v1alpha4/azuremachine_conversion.go
index 31ad3e83fa3..0d5aa517fb7 100644
--- a/api/v1alpha4/azuremachine_conversion.go
+++ b/api/v1alpha4/azuremachine_conversion.go
@@ -60,6 +60,10 @@ func (src *AzureMachine) ConvertTo(dstRaw conversion.Hub) error {
 		dst.Spec.Diagnostics = restored.Spec.Diagnostics
 	}
 
+	if restored.Spec.NetworkInterfaces != nil {
+		dst.Spec.NetworkInterfaces = restored.Spec.NetworkInterfaces
+	}
+
 	return nil
 }
 
diff --git a/api/v1alpha4/azuremachinetemplate_conversion.go b/api/v1alpha4/azuremachinetemplate_conversion.go
index 1f4c6ae99ee..a6ecc51dc24 100644
--- a/api/v1alpha4/azuremachinetemplate_conversion.go
+++ b/api/v1alpha4/azuremachinetemplate_conversion.go
@@ -62,6 +62,10 @@ func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error {
 		dst.Spec.Template.Spec.SpotVMOptions.EvictionPolicy = restored.Spec.Template.Spec.SpotVMOptions.EvictionPolicy
 	}
 
+	if restored.Spec.Template.Spec.NetworkInterfaces != nil {
+		dst.Spec.Template.Spec.NetworkInterfaces = restored.Spec.Template.Spec.NetworkInterfaces
+	}
+
 	return nil
 }
 
diff --git a/api/v1alpha4/zz_generated.conversion.go b/api/v1alpha4/zz_generated.conversion.go
index 922a2ceb565..0a5d960f000 100644
--- a/api/v1alpha4/zz_generated.conversion.go
+++ b/api/v1alpha4/zz_generated.conversion.go
@@ -1072,6 +1072,7 @@ func autoConvert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(in *v1bet
 	out.SubnetName = in.SubnetName
 	// WARNING: in.DNSServers requires manual conversion: does not exist in peer-type
 	// WARNING: in.VMExtensions requires manual conversion: does not exist in peer-type
+	// WARNING: in.NetworkInterfaces requires manual conversion: does not exist in peer-type
 	return nil
 }
 
diff --git a/api/v1beta1/azuremachine_default.go b/api/v1beta1/azuremachine_default.go
index 5ebd53a26e3..6a838d3972a 100644
--- a/api/v1beta1/azuremachine_default.go
+++ b/api/v1beta1/azuremachine_default.go
@@ -115,6 +115,34 @@ func (s *AzureMachineSpec) SetDiagnosticsDefaults() {
 	}
 }
 
+// SetNetworkInterfacesDefaults sets the defaults for the network interfaces.
+func (s *AzureMachineSpec) SetNetworkInterfacesDefaults() {
+	// Ensure the deprecated fields and new fields are not populated simultaneously
+	if (s.SubnetName != "" || s.AcceleratedNetworking != nil) && len(s.NetworkInterfaces) > 0 {
+		// Both the deprecated and the new fields are both set, return without changes
+		// and reject the request in the validating webhook which runs later.
+		return
+	}
+
+	if len(s.NetworkInterfaces) == 0 {
+		s.NetworkInterfaces = []NetworkInterface{
+			{
+				SubnetName:            s.SubnetName,
+				AcceleratedNetworking: s.AcceleratedNetworking,
+			},
+		}
+		s.SubnetName = ""
+		s.AcceleratedNetworking = nil
+	}
+
+	// Ensure that PrivateIPConfigs defaults to 1 if not specified.
+	for i := 0; i < len(s.NetworkInterfaces); i++ {
+		if s.NetworkInterfaces[i].PrivateIPConfigs == 0 {
+			s.NetworkInterfaces[i].PrivateIPConfigs = 1
+		}
+	}
+}
+
 // SetDefaults sets to the defaults for the AzureMachineSpec.
 func (s *AzureMachineSpec) SetDefaults() {
 	if err := s.SetDefaultSSHPublicKey(); err != nil {
@@ -125,4 +153,5 @@ func (s *AzureMachineSpec) SetDefaults() {
 	s.SetIdentityDefaults()
 	s.SetSpotEvictionPolicyDefaults()
 	s.SetDiagnosticsDefaults()
+	s.SetNetworkInterfacesDefaults()
 }
diff --git a/api/v1beta1/azuremachine_default_test.go b/api/v1beta1/azuremachine_default_test.go
index dce02b5a724..6d862140620 100644
--- a/api/v1beta1/azuremachine_default_test.go
+++ b/api/v1beta1/azuremachine_default_test.go
@@ -266,6 +266,87 @@ func TestAzureMachineSpec_SetDataDisksDefaults(t *testing.T) {
 	}
 }
 
+func TestAzureMachineSpec_SetNetworkInterfacesDefaults(t *testing.T) {
+	g := NewWithT(t)
+
+	tests := []struct {
+		name    string
+		machine *AzureMachine
+		want    *AzureMachine
+	}{
+		{
+			name: "defaulting webhook updates machine with deprecated subnetName field",
+			machine: &AzureMachine{
+				Spec: AzureMachineSpec{
+					SubnetName: "test-subnet",
+				},
+			},
+			want: &AzureMachine{
+				Spec: AzureMachineSpec{
+					SubnetName: "",
+					NetworkInterfaces: []NetworkInterface{
+						{
+							SubnetName:       "test-subnet",
+							PrivateIPConfigs: 1,
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "defaulting webhook updates machine with deprecated acceleratedNetworking field",
+			machine: &AzureMachine{
+				Spec: AzureMachineSpec{
+					SubnetName:            "test-subnet",
+					AcceleratedNetworking: to.BoolPtr(true),
+				},
+			},
+			want: &AzureMachine{
+				Spec: AzureMachineSpec{
+					SubnetName:            "",
+					AcceleratedNetworking: nil,
+					NetworkInterfaces: []NetworkInterface{
+						{
+							SubnetName:            "test-subnet",
+							PrivateIPConfigs:      1,
+							AcceleratedNetworking: to.BoolPtr(true),
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "defaulting webhook does nothing if both new and deprecated subnetName fields are set",
+			machine: &AzureMachine{
+				Spec: AzureMachineSpec{
+					SubnetName: "test-subnet",
+					NetworkInterfaces: []NetworkInterface{{
+						SubnetName: "test-subnet",
+					}},
+				},
+			},
+			want: &AzureMachine{
+				Spec: AzureMachineSpec{
+					SubnetName:            "test-subnet",
+					AcceleratedNetworking: nil,
+					NetworkInterfaces: []NetworkInterface{
+						{
+							SubnetName: "test-subnet",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			tc.machine.Spec.SetNetworkInterfacesDefaults()
+			g.Expect(tc.machine).To(Equal(tc.want))
+		})
+	}
+}
+
 func createMachineWithSSHPublicKey(sshPublicKey string) *AzureMachine {
 	machine := hardcodedAzureMachineWithSSHKey(sshPublicKey)
 	return machine
diff --git a/api/v1beta1/azuremachine_types.go b/api/v1beta1/azuremachine_types.go
index 08164734867..0ec8f78238b 100644
--- a/api/v1beta1/azuremachine_types.go
+++ b/api/v1beta1/azuremachine_types.go
@@ -100,9 +100,7 @@ type AzureMachineSpec struct {
 	// +optional
 	EnableIPForwarding bool `json:"enableIPForwarding,omitempty"`
 
-	// AcceleratedNetworking enables or disables Azure accelerated networking. If omitted, it will be set based on
-	// whether the requested VMSize supports accelerated networking.
-	// If AcceleratedNetworking is set to true with a VMSize that does not support it, Azure will return an error.
+	// Deprecated: AcceleratedNetworking should be set in the networkInterfaces field.
 	// +kubebuilder:validation:nullable
 	// +optional
 	AcceleratedNetworking *bool `json:"acceleratedNetworking,omitempty"`
@@ -120,7 +118,7 @@ type AzureMachineSpec struct {
 	// +optional
 	SecurityProfile *SecurityProfile `json:"securityProfile,omitempty"`
 
-	// SubnetName selects the Subnet where the VM will be placed
+	// Deprecated: SubnetName should be set in the networkInterfaces field.
 	// +optional
 	SubnetName string `json:"subnetName,omitempty"`
 
@@ -131,6 +129,13 @@ type AzureMachineSpec struct {
 	// VMExtensions specifies a list of extensions to be added to the virtual machine.
 	// +optional
 	VMExtensions []VMExtension `json:"vmExtensions,omitempty"`
+
+	// NetworkInterfaces specifies a list of network interface configurations.
+	// If left unspecified, the VM will get a single network interface with a
+	// single IPConfig in the subnet specified in the cluster's node subnet field.
+	// The primary interface will be the first networkInterface specified (index 0) in the list.
+	// +optional
+	NetworkInterfaces []NetworkInterface `json:"networkInterfaces,omitempty"`
 }
 
 // SpotVMOptions defines the options relevant to running the Machine on Spot VMs.
diff --git a/api/v1beta1/azuremachine_validation.go b/api/v1beta1/azuremachine_validation.go
index 339f3b152f2..894664c858f 100644
--- a/api/v1beta1/azuremachine_validation.go
+++ b/api/v1beta1/azuremachine_validation.go
@@ -54,9 +54,32 @@ func ValidateAzureMachineSpec(spec AzureMachineSpec) field.ErrorList {
 		allErrs = append(allErrs, errs...)
 	}
 
+	if errs := ValidateNetwork(spec.SubnetName, spec.AcceleratedNetworking, spec.NetworkInterfaces, field.NewPath("networkInterfaces")); len(errs) > 0 {
+		allErrs = append(allErrs, errs...)
+	}
+
 	return allErrs
 }
 
+// ValidateNetwork validates the network configuration.
+func ValidateNetwork(subnetName string, acceleratedNetworking *bool, networkInterfaces []NetworkInterface, fldPath *field.Path) field.ErrorList {
+	if (networkInterfaces != nil) && len(networkInterfaces) > 0 && subnetName != "" {
+		return field.ErrorList{field.Invalid(fldPath, networkInterfaces, "cannot set both networkInterfaces and machine subnetName")}
+	}
+
+	if (networkInterfaces != nil) && len(networkInterfaces) > 0 && acceleratedNetworking != nil {
+		return field.ErrorList{field.Invalid(fldPath, networkInterfaces, "cannot set both networkInterfaces and machine acceleratedNetworking")}
+	}
+
+	for _, nic := range networkInterfaces {
+		if nic.PrivateIPConfigs < 1 {
+			return field.ErrorList{field.Invalid(fldPath, networkInterfaces, "number of privateIPConfigs per interface must be at least 1")}
+		}
+	}
+
+	return field.ErrorList{}
+}
+
 // ValidateSSHKey validates an SSHKey.
 func ValidateSSHKey(sshKey string, fldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
diff --git a/api/v1beta1/azuremachine_validation_test.go b/api/v1beta1/azuremachine_validation_test.go
index 9a743814e53..931bd668a34 100644
--- a/api/v1beta1/azuremachine_validation_test.go
+++ b/api/v1beta1/azuremachine_validation_test.go
@@ -694,3 +694,96 @@ func TestAzureMachine_ValidateDataDisksUpdate(t *testing.T) {
 		})
 	}
 }
+
+func TestAzureMachine_ValidateNetwork(t *testing.T) {
+	g := NewWithT(t)
+
+	tests := []struct {
+		name                  string
+		subnetName            string
+		acceleratedNetworking *bool
+		networkInterfaces     []NetworkInterface
+		wantErr               bool
+	}{
+		{
+			name:                  "valid config with deprecated network fields",
+			subnetName:            "subnet1",
+			acceleratedNetworking: to.BoolPtr(true),
+			networkInterfaces:     nil,
+			wantErr:               false,
+		},
+		{
+			name:                  "valid config with networkInterfaces fields",
+			subnetName:            "",
+			acceleratedNetworking: nil,
+			networkInterfaces: []NetworkInterface{{
+				SubnetName:            "subnet1",
+				AcceleratedNetworking: to.BoolPtr(true),
+				PrivateIPConfigs:      1,
+			}},
+			wantErr: false,
+		},
+		{
+			name:                  "valid config with multiple networkInterfaces",
+			subnetName:            "",
+			acceleratedNetworking: nil,
+			networkInterfaces: []NetworkInterface{
+				{
+					SubnetName:            "subnet1",
+					AcceleratedNetworking: to.BoolPtr(true),
+					PrivateIPConfigs:      1,
+				},
+				{
+					SubnetName:            "subnet2",
+					AcceleratedNetworking: to.BoolPtr(true),
+					PrivateIPConfigs:      30,
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name:                  "invalid config using both deprecated subnetName and networkInterfaces",
+			subnetName:            "subnet1",
+			acceleratedNetworking: nil,
+			networkInterfaces: []NetworkInterface{{
+				SubnetName:            "subnet1",
+				AcceleratedNetworking: nil,
+				PrivateIPConfigs:      1,
+			}},
+			wantErr: true,
+		},
+		{
+			name:                  "invalid config using both deprecated acceleratedNetworking and networkInterfaces",
+			subnetName:            "",
+			acceleratedNetworking: to.BoolPtr(true),
+			networkInterfaces: []NetworkInterface{{
+				SubnetName:            "subnet1",
+				AcceleratedNetworking: to.BoolPtr(true),
+				PrivateIPConfigs:      1,
+			}},
+			wantErr: true,
+		},
+		{
+			name:                  "invalid config setting privateIPConfigs to less than 1",
+			subnetName:            "",
+			acceleratedNetworking: nil,
+			networkInterfaces: []NetworkInterface{{
+				SubnetName:            "subnet1",
+				AcceleratedNetworking: to.BoolPtr(true),
+				PrivateIPConfigs:      0,
+			}},
+			wantErr: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			err := ValidateNetwork(test.subnetName, test.acceleratedNetworking, test.networkInterfaces, field.NewPath("networkInterfaces"))
+			if test.wantErr {
+				g.Expect(err).ToNot(BeEmpty())
+			} else {
+				g.Expect(err).To(BeEmpty())
+			}
+		})
+	}
+}
diff --git a/api/v1beta1/azuremachine_webhook.go b/api/v1beta1/azuremachine_webhook.go
index ba2e1c8c525..8f7f5f1700d 100644
--- a/api/v1beta1/azuremachine_webhook.go
+++ b/api/v1beta1/azuremachine_webhook.go
@@ -17,6 +17,8 @@ limitations under the License.
 package v1beta1
 
 import (
+	"reflect"
+
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
@@ -152,6 +154,24 @@ func (m *AzureMachine) ValidateUpdate(oldRaw runtime.Object) error {
 		}
 	}
 
+	if !reflect.DeepEqual(m.Spec.NetworkInterfaces, old.Spec.NetworkInterfaces) {
+		// The defaulting webhook may have migrated values from the old SubnetName field to the new NetworkInterfaces format.
+		old.Spec.SetNetworkInterfacesDefaults()
+
+		// The reconciler will populate the SubnetName on the first interface if the user left it blank.
+		if old.Spec.NetworkInterfaces[0].SubnetName == "" && m.Spec.NetworkInterfaces[0].SubnetName != "" {
+			old.Spec.NetworkInterfaces[0].SubnetName = m.Spec.NetworkInterfaces[0].SubnetName
+		}
+
+		// Enforce immutability for all other changes to NetworkInterfaces.
+		if !reflect.DeepEqual(m.Spec.NetworkInterfaces, old.Spec.NetworkInterfaces) {
+			allErrs = append(allErrs,
+				field.Invalid(field.NewPath("spec", "networkInterfaces"),
+					m.Spec.NetworkInterfaces, "field is immutable"),
+			)
+		}
+	}
+
 	if len(allErrs) == 0 {
 		return nil
 	}
diff --git a/api/v1beta1/azuremachine_webhook_test.go b/api/v1beta1/azuremachine_webhook_test.go
index 03ce5906052..10c50986cec 100644
--- a/api/v1beta1/azuremachine_webhook_test.go
+++ b/api/v1beta1/azuremachine_webhook_test.go
@@ -128,6 +128,21 @@ func TestAzureMachine_ValidateCreate(t *testing.T) {
 			machine: createMachineWithDiagnostics(UserManagedDiagnosticsStorage, nil),
 			wantErr: true,
 		},
+		{
+			name:    "azuremachine with invalid network configuration",
+			machine: createMachineWithNetworkConfig("subnet", nil, []NetworkInterface{{SubnetName: "subnet1"}}),
+			wantErr: true,
+		},
+		{
+			name:    "azuremachine with valid legacy network configuration",
+			machine: createMachineWithNetworkConfig("subnet", nil, []NetworkInterface{}),
+			wantErr: false,
+		},
+		{
+			name:    "azuremachine with valid network configuration",
+			machine: createMachineWithNetworkConfig("", nil, []NetworkInterface{{SubnetName: "subnet", PrivateIPConfigs: 1}}),
+			wantErr: false,
+		},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
@@ -604,6 +619,34 @@ func TestAzureMachine_ValidateUpdate(t *testing.T) {
 			},
 			wantErr: true,
 		},
+		{
+			name: "validTest: azuremachine.spec.networkInterfaces is immutable",
+			oldMachine: &AzureMachine{
+				Spec: AzureMachineSpec{
+					NetworkInterfaces: []NetworkInterface{{SubnetName: "subnet"}},
+				},
+			},
+			newMachine: &AzureMachine{
+				Spec: AzureMachineSpec{
+					NetworkInterfaces: []NetworkInterface{{SubnetName: "subnet"}},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "invalidtest: azuremachine.spec.networkInterfaces is immutable",
+			oldMachine: &AzureMachine{
+				Spec: AzureMachineSpec{
+					NetworkInterfaces: []NetworkInterface{{SubnetName: "subnet1"}},
+				},
+			},
+			newMachine: &AzureMachine{
+				Spec: AzureMachineSpec{
+					NetworkInterfaces: []NetworkInterface{{SubnetName: "subnet2"}},
+				},
+			},
+			wantErr: true,
+		},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
@@ -645,6 +688,18 @@ func TestAzureMachine_Default(t *testing.T) {
 	}
 }
 
+func createMachineWithNetworkConfig(subnetName string, acceleratedNetworking *bool, interfaces []NetworkInterface) *AzureMachine {
+	return &AzureMachine{
+		Spec: AzureMachineSpec{
+			SubnetName:            subnetName,
+			NetworkInterfaces:     interfaces,
+			AcceleratedNetworking: acceleratedNetworking,
+			OSDisk:                validOSDisk,
+			SSHPublicKey:          validSSHPublicKey,
+		},
+	}
+}
+
 func createMachineWithSharedImage(subscriptionID, resourceGroup, name, gallery, version string) *AzureMachine {
 	image := &Image{
 		SharedGallery: &AzureSharedGalleryImage{
diff --git a/api/v1beta1/azuremachinetemplate_webhook.go b/api/v1beta1/azuremachinetemplate_webhook.go
index cbb38bcc837..1d7bd8242ff 100644
--- a/api/v1beta1/azuremachinetemplate_webhook.go
+++ b/api/v1beta1/azuremachinetemplate_webhook.go
@@ -64,6 +64,10 @@ func (r *AzureMachineTemplate) ValidateCreate(ctx context.Context, obj runtime.O
 		)
 	}
 
+	if (r.Spec.Template.Spec.NetworkInterfaces != nil) && len(r.Spec.Template.Spec.NetworkInterfaces) > 0 && r.Spec.Template.Spec.SubnetName != "" {
+		allErrs = append(allErrs, field.Invalid(field.NewPath("AzureMachineTemplate", "spec", "template", "spec", "networkInterfaces"), r.Spec.Template.Spec.NetworkInterfaces, "cannot set both NetworkInterfaces and machine SubnetName"))
+	}
+
 	if len(allErrs) == 0 {
 		return nil
 	}
@@ -128,5 +132,6 @@ func (r *AzureMachineTemplate) Default(ctx context.Context, obj runtime.Object)
 	}
 	t.Spec.Template.Spec.SetDefaultCachingType()
 	t.Spec.Template.Spec.SetDataDisksDefaults()
+	t.Spec.Template.Spec.SetNetworkInterfacesDefaults()
 	return nil
 }
diff --git a/api/v1beta1/azuremachinetemplate_webhook_test.go b/api/v1beta1/azuremachinetemplate_webhook_test.go
index 7d229161b22..c1872d30658 100644
--- a/api/v1beta1/azuremachinetemplate_webhook_test.go
+++ b/api/v1beta1/azuremachinetemplate_webhook_test.go
@@ -278,6 +278,9 @@ func TestAzureMachineTemplate_ValidateUpdate(t *testing.T) {
 							},
 							DataDisks:    []DataDisk{},
 							SSHPublicKey: "fake ssh key",
+							NetworkInterfaces: []NetworkInterface{{
+								PrivateIPConfigs: 1,
+							}},
 						},
 					},
 				},
@@ -331,6 +334,107 @@ func TestAzureMachineTemplate_ValidateUpdate(t *testing.T) {
 			},
 			wantErr: true,
 		},
+		{
+			name: "AzureMachineTemplate with legacy subnetName updated to new networkInterfaces",
+			oldTemplate: &AzureMachineTemplate{
+				Spec: AzureMachineTemplateSpec{
+					Template: AzureMachineTemplateResource{
+						Spec: AzureMachineSpec{
+							VMSize:        "size",
+							FailureDomain: &failureDomain,
+							OSDisk: OSDisk{
+								OSType:      "type",
+								DiskSizeGB:  to.Int32Ptr(11),
+								CachingType: "None",
+							},
+							DataDisks:             []DataDisk{},
+							SSHPublicKey:          "fake ssh key",
+							SubnetName:            "subnet1",
+							AcceleratedNetworking: to.BoolPtr(true),
+						},
+					},
+				},
+			},
+			template: &AzureMachineTemplate{
+				Spec: AzureMachineTemplateSpec{
+					Template: AzureMachineTemplateResource{
+						Spec: AzureMachineSpec{
+							VMSize:        "size",
+							FailureDomain: &failureDomain,
+							OSDisk: OSDisk{
+								OSType:      "type",
+								DiskSizeGB:  to.Int32Ptr(11),
+								CachingType: "None",
+							},
+							DataDisks:             []DataDisk{},
+							SSHPublicKey:          "fake ssh key",
+							SubnetName:            "",
+							AcceleratedNetworking: nil,
+							NetworkInterfaces: []NetworkInterface{
+								{
+									SubnetName:            "subnet1",
+									AcceleratedNetworking: to.BoolPtr(true),
+									PrivateIPConfigs:      1,
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "AzureMachineTemplate with modified networkInterfaces is immutable",
+			oldTemplate: &AzureMachineTemplate{
+				Spec: AzureMachineTemplateSpec{
+					Template: AzureMachineTemplateResource{
+						Spec: AzureMachineSpec{
+							VMSize:        "size",
+							FailureDomain: &failureDomain,
+							OSDisk: OSDisk{
+								OSType:      "type",
+								DiskSizeGB:  to.Int32Ptr(11),
+								CachingType: "None",
+							},
+							DataDisks:    []DataDisk{},
+							SSHPublicKey: "fake ssh key",
+							NetworkInterfaces: []NetworkInterface{
+								{
+									SubnetName:            "subnet1",
+									AcceleratedNetworking: to.BoolPtr(true),
+									PrivateIPConfigs:      1,
+								},
+							},
+						},
+					},
+				},
+			},
+			template: &AzureMachineTemplate{
+				Spec: AzureMachineTemplateSpec{
+					Template: AzureMachineTemplateResource{
+						Spec: AzureMachineSpec{
+							VMSize:        "size",
+							FailureDomain: &failureDomain,
+							OSDisk: OSDisk{
+								OSType:      "type",
+								DiskSizeGB:  to.Int32Ptr(11),
+								CachingType: "None",
+							},
+							DataDisks:    []DataDisk{},
+							SSHPublicKey: "fake ssh key",
+							NetworkInterfaces: []NetworkInterface{
+								{
+									SubnetName:            "subnet2",
+									AcceleratedNetworking: to.BoolPtr(true),
+									PrivateIPConfigs:      1,
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
 	}
 
 	// dry-run=true
diff --git a/api/v1beta1/types.go b/api/v1beta1/types.go
index f9b66ebc4c6..abaf2e40dbd 100644
--- a/api/v1beta1/types.go
+++ b/api/v1beta1/types.go
@@ -635,6 +635,24 @@ type ServiceEndpointSpec struct {
 	Locations []string `json:"locations"`
 }
 
+// NetworkInterface defines a network interface.
+type NetworkInterface struct {
+	// SubnetName specifies the subnet in which the new network interface will be placed.
+	SubnetName string `json:"subnetName,omitempty"`
+
+	// PrivateIPConfigs specifies the number of private IP addresses to attach to the interface.
+	// Defaults to 1 if not specified.
+	// +optional
+	PrivateIPConfigs int `json:"privateIPConfigs,omitempty"`
+
+	// AcceleratedNetworking enables or disables Azure accelerated networking. If omitted, it will be set based on
+	// whether the requested VMSize supports accelerated networking.
+	// If AcceleratedNetworking is set to true with a VMSize that does not support it, Azure will return an error.
+	// +kubebuilder:validation:nullable
+	// +optional
+	AcceleratedNetworking *bool `json:"acceleratedNetworking,omitempty"`
+}
+
 // GetControlPlaneSubnet returns the cluster control plane subnet.
 func (n *NetworkSpec) GetControlPlaneSubnet() (SubnetSpec, error) {
 	for _, sn := range n.Subnets {
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index 2d31ae363f9..f5731fd91a9 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -640,6 +640,13 @@ func (in *AzureMachineSpec) DeepCopyInto(out *AzureMachineSpec) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
+	if in.NetworkInterfaces != nil {
+		in, out := &in.NetworkInterfaces, &out.NetworkInterfaces
+		*out = make([]NetworkInterface, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureMachineSpec.
@@ -1317,6 +1324,26 @@ func (in *NetworkClassSpec) DeepCopy() *NetworkClassSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NetworkInterface) DeepCopyInto(out *NetworkInterface) {
+	*out = *in
+	if in.AcceleratedNetworking != nil {
+		in, out := &in.AcceleratedNetworking, &out.AcceleratedNetworking
+		*out = new(bool)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkInterface.
+func (in *NetworkInterface) DeepCopy() *NetworkInterface {
+	if in == nil {
+		return nil
+	}
+	out := new(NetworkInterface)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *NetworkSpec) DeepCopyInto(out *NetworkSpec) {
 	*out = *in
diff --git a/azure/defaults.go b/azure/defaults.go
index d156aa6c6b4..1aae34270bf 100644
--- a/azure/defaults.go
+++ b/azure/defaults.go
@@ -174,7 +174,10 @@ func GenerateVNetLinkName(vnetName string) string {
 }
 
 // GenerateNICName generates the name of a network interface based on the name of a VM.
-func GenerateNICName(machineName string) string {
+func GenerateNICName(machineName string, multiNIC bool, index int) string {
+	if multiNIC {
+		return fmt.Sprintf("%s-nic-%d", machineName, index)
+	}
 	return fmt.Sprintf("%s-nic", machineName)
 }
 
diff --git a/azure/scope/clients.go b/azure/scope/clients.go
index 0c2b424e51a..89075d5aafa 100644
--- a/azure/scope/clients.go
+++ b/azure/scope/clients.go
@@ -27,7 +27,6 @@ import (
 	"github.com/Azure/go-autorest/autorest"
 	"github.com/Azure/go-autorest/autorest/azure"
 	"github.com/Azure/go-autorest/autorest/azure/auth"
-
 	azureutil "sigs.k8s.io/cluster-api-provider-azure/util/azure"
 )
 
diff --git a/azure/scope/machine.go b/azure/scope/machine.go
index dd42518ee40..c2f839f69bb 100644
--- a/azure/scope/machine.go
+++ b/azure/scope/machine.go
@@ -229,50 +229,78 @@ func (m *MachineScope) InboundNatSpecs() []azure.ResourceSpecGetter {
 
 // NICSpecs returns the network interface specs.
 func (m *MachineScope) NICSpecs() []azure.ResourceSpecGetter {
+	nicSpecs := []azure.ResourceSpecGetter{}
+
+	// For backwards compatibility we need to ensure the NIC Name does not change on existing machines
+	// created prior to multiple NIC support
+	isMultiNIC := len(m.AzureMachine.Spec.NetworkInterfaces) > 1
+
+	for i := 0; i < len(m.AzureMachine.Spec.NetworkInterfaces); i++ {
+		isPrimary := i == 0
+		nicName := azure.GenerateNICName(m.Name(), isMultiNIC, i)
+		nicSpecs = append(nicSpecs, m.BuildNICSpec(nicName, m.AzureMachine.Spec.NetworkInterfaces[i], isPrimary))
+	}
+	return nicSpecs
+}
+
+// BuildNICSpec takes a NetworkInterface from the AzureMachineSpec and returns a NICSpec for use by the networkinterfaces service.
+func (m *MachineScope) BuildNICSpec(nicName string, infrav1NetworkInterface infrav1.NetworkInterface, primaryNetworkInterface bool) *networkinterfaces.NICSpec {
 	spec := &networkinterfaces.NICSpec{
-		Name:                  azure.GenerateNICName(m.Name()),
+		Name:                  nicName,
 		ResourceGroup:         m.ResourceGroup(),
 		Location:              m.Location(),
 		SubscriptionID:        m.SubscriptionID(),
 		MachineName:           m.Name(),
 		VNetName:              m.Vnet().Name,
 		VNetResourceGroup:     m.Vnet().ResourceGroup,
-		SubnetName:            m.AzureMachine.Spec.SubnetName,
-		AcceleratedNetworking: m.AzureMachine.Spec.AcceleratedNetworking,
-		DNSServers:            m.AzureMachine.Spec.DNSServers,
+		AcceleratedNetworking: infrav1NetworkInterface.AcceleratedNetworking,
 		IPv6Enabled:           m.IsIPv6Enabled(),
 		EnableIPForwarding:    m.AzureMachine.Spec.EnableIPForwarding,
+		SubnetName:            infrav1NetworkInterface.SubnetName,
 		AdditionalTags:        m.AdditionalTags(),
 		ClusterName:           m.ClusterName(),
+		IPConfigs:             []networkinterfaces.IPConfig{},
 	}
 
-	if m.Role() == infrav1.ControlPlane {
-		spec.PublicLBName = m.OutboundLBName(m.Role())
-		spec.PublicLBAddressPoolName = m.OutboundPoolName(m.OutboundLBName(m.Role()))
-		if m.IsAPIServerPrivate() {
-			spec.InternalLBName = m.APIServerLBName()
-			spec.InternalLBAddressPoolName = m.APIServerLBPoolName(m.APIServerLBName())
-		} else {
-			spec.PublicLBNATRuleName = m.Name()
-			spec.PublicLBAddressPoolName = m.APIServerLBPoolName(m.APIServerLBName())
-		}
+	if m.cache != nil {
+		spec.SKU = &m.cache.VMSKU
 	}
 
-	// If NAT gateway is not enabled and node has no public IP, then the NIC needs to reference the LB to get outbound traffic.
-	if m.Role() == infrav1.Node && !m.Subnet().IsNatGatewayEnabled() && !m.AzureMachine.Spec.AllocatePublicIP {
-		spec.PublicLBName = m.OutboundLBName(m.Role())
-		spec.PublicLBAddressPoolName = m.OutboundPoolName(m.OutboundLBName(m.Role()))
+	for i := 0; i < infrav1NetworkInterface.PrivateIPConfigs; i++ {
+		spec.IPConfigs = append(spec.IPConfigs, networkinterfaces.IPConfig{})
 	}
 
-	if m.Role() == infrav1.Node && m.AzureMachine.Spec.AllocatePublicIP {
-		spec.PublicIPName = azure.GenerateNodePublicIPName(m.Name())
-	}
+	if primaryNetworkInterface {
+		spec.DNSServers = m.AzureMachine.Spec.DNSServers
 
-	if m.cache != nil {
-		spec.SKU = &m.cache.VMSKU
+		if m.Role() == infrav1.ControlPlane {
+			spec.PublicLBName = m.OutboundLBName(m.Role())
+			spec.PublicLBAddressPoolName = m.OutboundPoolName(m.OutboundLBName(m.Role()))
+			if m.IsAPIServerPrivate() {
+				spec.InternalLBName = m.APIServerLBName()
+				spec.InternalLBAddressPoolName = m.APIServerLBPoolName(m.APIServerLBName())
+			} else {
+				spec.PublicLBNATRuleName = m.Name()
+				spec.PublicLBAddressPoolName = m.APIServerLBPoolName(m.APIServerLBName())
+			}
+		}
+
+		if m.Role() == infrav1.Node && m.AzureMachine.Spec.AllocatePublicIP {
+			spec.PublicIPName = azure.GenerateNodePublicIPName(m.Name())
+		}
+		// If the NAT gateway is not enabled and node has no public IP, then the NIC needs to reference the LB to get outbound traffic.
+		if m.Role() == infrav1.Node && !m.Subnet().IsNatGatewayEnabled() && !m.AzureMachine.Spec.AllocatePublicIP {
+			spec.PublicLBName = m.OutboundLBName(m.Role())
+			spec.PublicLBAddressPoolName = m.OutboundPoolName(m.OutboundLBName(m.Role()))
+		}
+		// If the NAT gateway is not enabled and node has no public IP, then the NIC needs to reference the LB to get outbound traffic.
+		if m.Role() == infrav1.Node && !m.Subnet().IsNatGatewayEnabled() && !m.AzureMachine.Spec.AllocatePublicIP {
+			spec.PublicLBName = m.OutboundLBName(m.Role())
+			spec.PublicLBAddressPoolName = m.OutboundPoolName(m.OutboundLBName(m.Role()))
+		}
 	}
 
-	return []azure.ResourceSpecGetter{spec}
+	return spec
 }
 
 // NICIDs returns the NIC resource IDs.
@@ -282,6 +310,7 @@ func (m *MachineScope) NICIDs() []string {
 	for i, nic := range nicspecs {
 		nicIDs[i] = azure.NetworkInterfaceID(m.SubscriptionID(), nic.ResourceGroupName(), nic.ResourceName())
 	}
+
 	return nicIDs
 }
 
@@ -365,7 +394,7 @@ func (m *MachineScope) VMExtensionSpecs() []azure.ResourceSpecGetter {
 // Subnet returns the machine's subnet.
 func (m *MachineScope) Subnet() infrav1.SubnetSpec {
 	for _, subnet := range m.Subnets() {
-		if subnet.Name == m.AzureMachine.Spec.SubnetName {
+		if subnet.Name == m.AzureMachine.Spec.NetworkInterfaces[0].SubnetName {
 			return subnet
 		}
 	}
@@ -681,7 +710,7 @@ func (m *MachineScope) GetVMImage(ctx context.Context) (*infrav1.Image, error) {
 // 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 == "" {
+	if m.AzureMachine.Spec.NetworkInterfaces[0].SubnetName == "" {
 		subnetName := ""
 		subnets := m.Subnets()
 		var subnetCount int
@@ -695,7 +724,7 @@ func (m *MachineScope) SetSubnetName() error {
 			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
+		m.AzureMachine.Spec.NetworkInterfaces[0].SubnetName = subnetName
 	}
 
 	return nil
diff --git a/azure/scope/machine_test.go b/azure/scope/machine_test.go
index a6fa8a53c4e..c314de8ac26 100644
--- a/azure/scope/machine_test.go
+++ b/azure/scope/machine_test.go
@@ -852,7 +852,9 @@ func TestMachineScope_Subnet(t *testing.T) {
 						Name: "machine-name",
 					},
 					Spec: infrav1.AzureMachineSpec{
-						SubnetName: "machine-name-subnet",
+						NetworkInterfaces: []infrav1.NetworkInterface{{
+							SubnetName: "machine-name-subnet",
+						}},
 					},
 				},
 				ClusterScoper: &ClusterScope{
@@ -1678,7 +1680,10 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 					},
 					Spec: infrav1.AzureMachineSpec{
 						ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"),
-						SubnetName: "subnet1",
+						NetworkInterfaces: []infrav1.NetworkInterface{{
+							SubnetName:       "subnet1",
+							PrivateIPConfigs: 1,
+						}},
 					},
 				},
 				Machine: &clusterv1.Machine{
@@ -1698,6 +1703,7 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 					SubscriptionID:            "123",
 					MachineName:               "machine-name",
 					SubnetName:                "subnet1",
+					IPConfigs:                 []networkinterfaces.IPConfig{{}},
 					VNetName:                  "vnet1",
 					VNetResourceGroup:         "rg1",
 					PublicLBName:              "outbound-lb",
@@ -1778,7 +1784,10 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 					},
 					Spec: infrav1.AzureMachineSpec{
 						ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"),
-						SubnetName: "subnet1",
+						NetworkInterfaces: []infrav1.NetworkInterface{{
+							SubnetName:       "subnet1",
+							PrivateIPConfigs: 1,
+						}},
 					},
 				},
 				Machine: &clusterv1.Machine{
@@ -1803,6 +1812,7 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 					SubscriptionID:            "123",
 					MachineName:               "machine-name",
 					SubnetName:                "subnet1",
+					IPConfigs:                 []networkinterfaces.IPConfig{{}},
 					VNetName:                  "vnet1",
 					VNetResourceGroup:         "rg1",
 					PublicLBName:              "outbound-lb",
@@ -1890,7 +1900,10 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 					},
 					Spec: infrav1.AzureMachineSpec{
 						ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"),
-						SubnetName: "subnet1",
+						NetworkInterfaces: []infrav1.NetworkInterface{{
+							SubnetName:       "subnet1",
+							PrivateIPConfigs: 1,
+						}},
 					},
 				},
 				Machine: &clusterv1.Machine{
@@ -1910,6 +1923,7 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 					SubscriptionID:            "123",
 					MachineName:               "machine-name",
 					SubnetName:                "subnet1",
+					IPConfigs:                 []networkinterfaces.IPConfig{{}},
 					VNetName:                  "vnet1",
 					VNetResourceGroup:         "rg1",
 					PublicLBName:              "",
@@ -1989,8 +2003,11 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 						Name: "machine",
 					},
 					Spec: infrav1.AzureMachineSpec{
-						ProviderID:       to.StringPtr("azure://compute/virtual-machines/machine-name"),
-						SubnetName:       "subnet1",
+						ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"),
+						NetworkInterfaces: []infrav1.NetworkInterface{{
+							SubnetName:       "subnet1",
+							PrivateIPConfigs: 1,
+						}},
 						AllocatePublicIP: true,
 					},
 				},
@@ -2011,6 +2028,7 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 					SubscriptionID:            "123",
 					MachineName:               "machine-name",
 					SubnetName:                "subnet1",
+					IPConfigs:                 []networkinterfaces.IPConfig{{}},
 					VNetName:                  "vnet1",
 					VNetResourceGroup:         "rg1",
 					PublicLBName:              "",
@@ -2097,7 +2115,10 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 					},
 					Spec: infrav1.AzureMachineSpec{
 						ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"),
-						SubnetName: "subnet1",
+						NetworkInterfaces: []infrav1.NetworkInterface{{
+							SubnetName:       "subnet1",
+							PrivateIPConfigs: 1,
+						}},
 					},
 				},
 				Machine: &clusterv1.Machine{
@@ -2117,6 +2138,7 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 					SubscriptionID:            "123",
 					MachineName:               "machine-name",
 					SubnetName:                "subnet1",
+					IPConfigs:                 []networkinterfaces.IPConfig{{}},
 					VNetName:                  "vnet1",
 					VNetResourceGroup:         "rg1",
 					PublicLBName:              "",
@@ -2200,7 +2222,10 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 					},
 					Spec: infrav1.AzureMachineSpec{
 						ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"),
-						SubnetName: "subnet1",
+						NetworkInterfaces: []infrav1.NetworkInterface{{
+							SubnetName:       "subnet1",
+							PrivateIPConfigs: 1,
+						}},
 					},
 				},
 				Machine: &clusterv1.Machine{
@@ -2220,6 +2245,7 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 					SubscriptionID:            "123",
 					MachineName:               "machine-name",
 					SubnetName:                "subnet1",
+					IPConfigs:                 []networkinterfaces.IPConfig{{}},
 					VNetName:                  "vnet1",
 					VNetResourceGroup:         "rg1",
 					PublicLBName:              "api-lb",
@@ -2303,7 +2329,10 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 					},
 					Spec: infrav1.AzureMachineSpec{
 						ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"),
-						SubnetName: "subnet1",
+						NetworkInterfaces: []infrav1.NetworkInterface{{
+							SubnetName:       "subnet1",
+							PrivateIPConfigs: 1,
+						}},
 						DNSServers: []string{"123.123.123.123", "124.124.124.124"},
 					},
 				},
@@ -2324,6 +2353,7 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 					SubscriptionID:            "123",
 					MachineName:               "machine-name",
 					SubnetName:                "subnet1",
+					IPConfigs:                 []networkinterfaces.IPConfig{{}},
 					VNetName:                  "vnet1",
 					VNetResourceGroup:         "rg1",
 					PublicLBName:              "api-lb",
@@ -2344,11 +2374,395 @@ func TestMachineScope_NICSpecs(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "Node Machine with multiple Network Interfaces",
+			machineScope: MachineScope{
+				ClusterScoper: &ClusterScope{
+					AzureClients: AzureClients{
+						EnvironmentSettings: auth.EnvironmentSettings{
+							Values: map[string]string{
+								auth.SubscriptionID: "123",
+							},
+						},
+					},
+					Cluster: &clusterv1.Cluster{
+						ObjectMeta: metav1.ObjectMeta{
+							Name:      "cluster",
+							Namespace: "default",
+						},
+					},
+					AzureCluster: &infrav1.AzureCluster{
+						ObjectMeta: metav1.ObjectMeta{
+							Name:      "cluster",
+							Namespace: "default",
+							OwnerReferences: []metav1.OwnerReference{
+								{
+									APIVersion: "cluster.x-k8s.io/v1beta1",
+									Kind:       "Cluster",
+									Name:       "cluster",
+								},
+							},
+						},
+						Spec: infrav1.AzureClusterSpec{
+							ResourceGroup: "my-rg",
+							AzureClusterClassSpec: infrav1.AzureClusterClassSpec{
+								Location: "westus",
+							},
+							NetworkSpec: infrav1.NetworkSpec{
+								Vnet: infrav1.VnetSpec{
+									Name:          "vnet1",
+									ResourceGroup: "rg1",
+								},
+								Subnets: []infrav1.SubnetSpec{
+									{
+										SubnetClassSpec: infrav1.SubnetClassSpec{
+											Role: infrav1.SubnetNode,
+											Name: "subnet1",
+										},
+									},
+								},
+								APIServerLB: infrav1.LoadBalancerSpec{
+									Name: "api-lb",
+								},
+								NodeOutboundLB: &infrav1.LoadBalancerSpec{
+									Name: "outbound-lb",
+								},
+							},
+						},
+					},
+				},
+				AzureMachine: &infrav1.AzureMachine{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "machine",
+					},
+					Spec: infrav1.AzureMachineSpec{
+						ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"),
+						NetworkInterfaces: []infrav1.NetworkInterface{
+							{
+								SubnetName:            "subnet1",
+								AcceleratedNetworking: pointer.Bool(true),
+								PrivateIPConfigs:      1,
+							},
+							{
+								SubnetName:            "subnet2",
+								AcceleratedNetworking: pointer.Bool(true),
+								PrivateIPConfigs:      2,
+							},
+						},
+					},
+				},
+				Machine: &clusterv1.Machine{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:   "machine",
+						Labels: map[string]string{},
+					},
+				},
+			},
+			want: []azure.ResourceSpecGetter{
+				&networkinterfaces.NICSpec{
+					Name:                      "machine-name-nic-0",
+					ResourceGroup:             "my-rg",
+					Location:                  "westus",
+					SubscriptionID:            "123",
+					MachineName:               "machine-name",
+					SubnetName:                "subnet1",
+					IPConfigs:                 []networkinterfaces.IPConfig{{}},
+					VNetName:                  "vnet1",
+					VNetResourceGroup:         "rg1",
+					PublicLBName:              "outbound-lb",
+					PublicLBAddressPoolName:   "outbound-lb-outboundBackendPool",
+					PublicLBNATRuleName:       "",
+					InternalLBName:            "",
+					InternalLBAddressPoolName: "",
+					PublicIPName:              "",
+					AcceleratedNetworking:     pointer.Bool(true),
+					IPv6Enabled:               false,
+					EnableIPForwarding:        false,
+					SKU:                       nil,
+					ClusterName:               "cluster",
+					AdditionalTags: map[string]string{
+						"kubernetes.io_cluster_cluster": "owned",
+					},
+				},
+				&networkinterfaces.NICSpec{
+					Name:                      "machine-name-nic-1",
+					ResourceGroup:             "my-rg",
+					Location:                  "westus",
+					SubscriptionID:            "123",
+					MachineName:               "machine-name",
+					SubnetName:                "subnet2",
+					IPConfigs:                 []networkinterfaces.IPConfig{{}, {}},
+					VNetName:                  "vnet1",
+					VNetResourceGroup:         "rg1",
+					PublicLBName:              "",
+					PublicLBAddressPoolName:   "",
+					PublicLBNATRuleName:       "",
+					InternalLBName:            "",
+					InternalLBAddressPoolName: "",
+					PublicIPName:              "",
+					AcceleratedNetworking:     pointer.Bool(true),
+					IPv6Enabled:               false,
+					EnableIPForwarding:        false,
+					SKU:                       nil,
+					ClusterName:               "cluster",
+					AdditionalTags: map[string]string{
+						"kubernetes.io_cluster_cluster": "owned",
+					},
+				},
+			},
+		},
+		{
+			name: "Node Machine with multiple Network Interfaces and Public IP Allocation enabled",
+			machineScope: MachineScope{
+				ClusterScoper: &ClusterScope{
+					AzureClients: AzureClients{
+						EnvironmentSettings: auth.EnvironmentSettings{
+							Values: map[string]string{
+								auth.SubscriptionID: "123",
+							},
+						},
+					},
+					Cluster: &clusterv1.Cluster{
+						ObjectMeta: metav1.ObjectMeta{
+							Name:      "cluster",
+							Namespace: "default",
+						},
+					},
+					AzureCluster: &infrav1.AzureCluster{
+						ObjectMeta: metav1.ObjectMeta{
+							Name:      "cluster",
+							Namespace: "default",
+							OwnerReferences: []metav1.OwnerReference{
+								{
+									APIVersion: "cluster.x-k8s.io/v1beta1",
+									Kind:       "Cluster",
+									Name:       "cluster",
+								},
+							},
+						},
+						Spec: infrav1.AzureClusterSpec{
+							ResourceGroup: "my-rg",
+							AzureClusterClassSpec: infrav1.AzureClusterClassSpec{
+								Location: "westus",
+							},
+							NetworkSpec: infrav1.NetworkSpec{
+								Vnet: infrav1.VnetSpec{
+									Name:          "vnet1",
+									ResourceGroup: "rg1",
+								},
+								Subnets: []infrav1.SubnetSpec{
+									{
+										SubnetClassSpec: infrav1.SubnetClassSpec{
+											Role: infrav1.SubnetNode,
+											Name: "subnet1",
+										},
+									},
+								},
+								APIServerLB: infrav1.LoadBalancerSpec{
+									Name: "api-lb",
+								},
+								NodeOutboundLB: &infrav1.LoadBalancerSpec{
+									Name: "outbound-lb",
+								},
+							},
+						},
+					},
+				},
+				AzureMachine: &infrav1.AzureMachine{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "machine",
+					},
+					Spec: infrav1.AzureMachineSpec{
+						ProviderID:       to.StringPtr("azure://compute/virtual-machines/machine-name"),
+						AllocatePublicIP: true,
+						NetworkInterfaces: []infrav1.NetworkInterface{
+							{
+								SubnetName:            "subnet1",
+								AcceleratedNetworking: pointer.Bool(true),
+								PrivateIPConfigs:      1,
+							},
+							{
+								SubnetName:            "subnet2",
+								AcceleratedNetworking: pointer.Bool(true),
+								PrivateIPConfigs:      2,
+							},
+						},
+					},
+				},
+				Machine: &clusterv1.Machine{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:   "machine",
+						Labels: map[string]string{},
+					},
+				},
+			},
+			want: []azure.ResourceSpecGetter{
+				&networkinterfaces.NICSpec{
+					Name:                      "machine-name-nic-0",
+					ResourceGroup:             "my-rg",
+					Location:                  "westus",
+					SubscriptionID:            "123",
+					MachineName:               "machine-name",
+					SubnetName:                "subnet1",
+					IPConfigs:                 []networkinterfaces.IPConfig{{}},
+					VNetName:                  "vnet1",
+					VNetResourceGroup:         "rg1",
+					PublicLBName:              "",
+					PublicLBAddressPoolName:   "",
+					PublicLBNATRuleName:       "",
+					InternalLBName:            "",
+					InternalLBAddressPoolName: "",
+					PublicIPName:              "pip-machine-name",
+					AcceleratedNetworking:     pointer.Bool(true),
+					IPv6Enabled:               false,
+					EnableIPForwarding:        false,
+					SKU:                       nil,
+					ClusterName:               "cluster",
+					AdditionalTags: map[string]string{
+						"kubernetes.io_cluster_cluster": "owned",
+					},
+				},
+				&networkinterfaces.NICSpec{
+					Name:                      "machine-name-nic-1",
+					ResourceGroup:             "my-rg",
+					Location:                  "westus",
+					SubscriptionID:            "123",
+					MachineName:               "machine-name",
+					SubnetName:                "subnet2",
+					IPConfigs:                 []networkinterfaces.IPConfig{{}, {}},
+					VNetName:                  "vnet1",
+					VNetResourceGroup:         "rg1",
+					PublicLBName:              "",
+					PublicLBAddressPoolName:   "",
+					PublicLBNATRuleName:       "",
+					InternalLBName:            "",
+					InternalLBAddressPoolName: "",
+					PublicIPName:              "",
+					AcceleratedNetworking:     pointer.Bool(true),
+					IPv6Enabled:               false,
+					EnableIPForwarding:        false,
+					SKU:                       nil,
+					ClusterName:               "cluster",
+					AdditionalTags: map[string]string{
+						"kubernetes.io_cluster_cluster": "owned",
+					},
+				},
+			},
+		},
+		{
+			name: "Node Machine with multiple IPConfigs",
+			machineScope: MachineScope{
+				ClusterScoper: &ClusterScope{
+					AzureClients: AzureClients{
+						EnvironmentSettings: auth.EnvironmentSettings{
+							Values: map[string]string{
+								auth.SubscriptionID: "123",
+							},
+						},
+					},
+					Cluster: &clusterv1.Cluster{
+						ObjectMeta: metav1.ObjectMeta{
+							Name:      "cluster",
+							Namespace: "default",
+						},
+					},
+					AzureCluster: &infrav1.AzureCluster{
+						ObjectMeta: metav1.ObjectMeta{
+							Name:      "cluster",
+							Namespace: "default",
+							OwnerReferences: []metav1.OwnerReference{
+								{
+									APIVersion: "cluster.x-k8s.io/v1beta1",
+									Kind:       "Cluster",
+									Name:       "cluster",
+								},
+							},
+						},
+						Spec: infrav1.AzureClusterSpec{
+							ResourceGroup: "my-rg",
+							AzureClusterClassSpec: infrav1.AzureClusterClassSpec{
+								Location: "westus",
+							},
+							NetworkSpec: infrav1.NetworkSpec{
+								Vnet: infrav1.VnetSpec{
+									Name:          "vnet1",
+									ResourceGroup: "rg1",
+								},
+								Subnets: []infrav1.SubnetSpec{
+									{
+										SubnetClassSpec: infrav1.SubnetClassSpec{
+											Role: infrav1.SubnetNode,
+											Name: "subnet1",
+										},
+									},
+								},
+								APIServerLB: infrav1.LoadBalancerSpec{
+									Name: "api-lb",
+								},
+								NodeOutboundLB: &infrav1.LoadBalancerSpec{
+									Name: "outbound-lb",
+								},
+							},
+						},
+					},
+				},
+				AzureMachine: &infrav1.AzureMachine{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "machine",
+					},
+					Spec: infrav1.AzureMachineSpec{
+						ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"),
+						NetworkInterfaces: []infrav1.NetworkInterface{
+							{
+								SubnetName:            "subnet1",
+								AcceleratedNetworking: pointer.Bool(true),
+								PrivateIPConfigs:      10,
+							},
+						},
+					},
+				},
+				Machine: &clusterv1.Machine{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:   "machine",
+						Labels: map[string]string{},
+					},
+				},
+			},
+			want: []azure.ResourceSpecGetter{
+				&networkinterfaces.NICSpec{
+					Name:                      "machine-name-nic",
+					ResourceGroup:             "my-rg",
+					Location:                  "westus",
+					SubscriptionID:            "123",
+					MachineName:               "machine-name",
+					SubnetName:                "subnet1",
+					IPConfigs:                 []networkinterfaces.IPConfig{{}, {}, {}, {}, {}, {}, {}, {}, {}, {}},
+					VNetName:                  "vnet1",
+					VNetResourceGroup:         "rg1",
+					PublicLBName:              "outbound-lb",
+					PublicLBAddressPoolName:   "outbound-lb-outboundBackendPool",
+					PublicLBNATRuleName:       "",
+					InternalLBName:            "",
+					InternalLBAddressPoolName: "",
+					PublicIPName:              "",
+					AcceleratedNetworking:     pointer.Bool(true),
+					IPv6Enabled:               false,
+					EnableIPForwarding:        false,
+					SKU:                       nil,
+					ClusterName:               "cluster",
+					AdditionalTags: map[string]string{
+						"kubernetes.io_cluster_cluster": "owned",
+					},
+				},
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
 			gotNicSpecs := tt.machineScope.NICSpecs()
 			if !reflect.DeepEqual(gotNicSpecs, tt.want) {
+				g.Expect(gotNicSpecs).To(BeEquivalentTo(tt.want))
 				t.Errorf("NICSpecs(), gotNicSpecs = %s, want %s", specArrayToString(gotNicSpecs), specArrayToString(tt.want))
 			}
 		})
diff --git a/azure/scope/machinepool.go b/azure/scope/machinepool.go
index f164d9bf3ff..5086bd689db 100644
--- a/azure/scope/machinepool.go
+++ b/azure/scope/machinepool.go
@@ -115,12 +115,12 @@ 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.AzureMachinePool.Spec.Template.NetworkInterfaces[0].SubnetName,
 		VNetName:                     m.Vnet().Name,
 		VNetResourceGroup:            m.Vnet().ResourceGroup,
 		PublicLBName:                 m.OutboundLBName(infrav1.Node),
 		PublicLBAddressPoolName:      azure.GenerateOutboundBackendAddressPoolName(m.OutboundLBName(infrav1.Node)),
-		AcceleratedNetworking:        m.AzureMachinePool.Spec.Template.AcceleratedNetworking,
+		AcceleratedNetworking:        m.AzureMachinePool.Spec.Template.NetworkInterfaces[0].AcceleratedNetworking,
 		Identity:                     m.AzureMachinePool.Spec.Identity,
 		UserAssignedIdentities:       m.AzureMachinePool.Spec.UserAssignedIdentities,
 		DiagnosticsProfile:           m.AzureMachinePool.Spec.Template.Diagnostics,
@@ -128,6 +128,7 @@ func (m *MachinePoolScope) ScaleSetSpec() azure.ScaleSetSpec {
 		SpotVMOptions:                m.AzureMachinePool.Spec.Template.SpotVMOptions,
 		FailureDomains:               m.MachinePool.Spec.FailureDomains,
 		TerminateNotificationTimeout: m.AzureMachinePool.Spec.Template.TerminateNotificationTimeout,
+		NetworkInterfaces:            m.AzureMachinePool.Spec.Template.NetworkInterfaces,
 	}
 }
 
@@ -655,7 +656,7 @@ func (m *MachinePoolScope) getDeploymentStrategy() machinepool.TypedDeleteSelect
 // 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 == "" {
+	if m.AzureMachinePool.Spec.Template.NetworkInterfaces[0].SubnetName == "" {
 		subnetName := ""
 		for _, subnet := range m.NodeSubnets() {
 			subnetName = subnet.Name
@@ -664,7 +665,7 @@ func (m *MachinePoolScope) SetSubnetName() error {
 			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
+		m.AzureMachinePool.Spec.Template.NetworkInterfaces[0].SubnetName = subnetName
 	}
 
 	return nil
diff --git a/azure/scope/machinepool_test.go b/azure/scope/machinepool_test.go
index fbc6ead0291..6939a08e9b7 100644
--- a/azure/scope/machinepool_test.go
+++ b/azure/scope/machinepool_test.go
@@ -97,6 +97,89 @@ func TestMachinePoolScope_Name(t *testing.T) {
 		})
 	}
 }
+func TestMachinePoolScope_NetworkInterfaces(t *testing.T) {
+	tests := []struct {
+		name             string
+		machinePoolScope MachinePoolScope
+		want             int
+	}{
+		{
+			name: "zero network interfaces",
+			machinePoolScope: MachinePoolScope{
+				MachinePool: nil,
+				AzureMachinePool: &infrav1exp.AzureMachinePool{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "default-nics",
+					},
+					Spec: infrav1exp.AzureMachinePoolSpec{
+						Template: infrav1exp.AzureMachinePoolMachineTemplate{
+							AcceleratedNetworking: to.BoolPtr(true),
+							SubnetName:            "node-subnet",
+						},
+					},
+				},
+				ClusterScoper: nil,
+			},
+			want: 0,
+		},
+		{
+			name: "one network interface",
+			machinePoolScope: MachinePoolScope{
+				MachinePool: nil,
+				AzureMachinePool: &infrav1exp.AzureMachinePool{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "single-nic",
+					},
+					Spec: infrav1exp.AzureMachinePoolSpec{
+						Template: infrav1exp.AzureMachinePoolMachineTemplate{
+							NetworkInterfaces: []infrav1.NetworkInterface{
+								{
+									SubnetName: "node-subnet",
+								},
+							},
+						},
+					},
+				},
+				ClusterScoper: nil,
+			},
+			want: 1,
+		},
+		{
+			name: "two network interfaces",
+			machinePoolScope: MachinePoolScope{
+				MachinePool: nil,
+				AzureMachinePool: &infrav1exp.AzureMachinePool{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "dual-nics",
+					},
+					Spec: infrav1exp.AzureMachinePoolSpec{
+						Template: infrav1exp.AzureMachinePoolMachineTemplate{
+							NetworkInterfaces: []infrav1.NetworkInterface{
+								{
+									SubnetName: "control-plane-subnet",
+								},
+								{
+									SubnetName: "node-subnet",
+								},
+							},
+						},
+					},
+				},
+				ClusterScoper: nil,
+			},
+			want: 2,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := len(tt.machinePoolScope.AzureMachinePool.Spec.Template.NetworkInterfaces)
+			if got != tt.want {
+				t.Errorf("MachinePoolScope.Name() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
 
 func TestMachinePoolScope_SetBootstrapConditions(t *testing.T) {
 	cases := []struct {
@@ -309,7 +392,6 @@ func TestMachinePoolScope_GetVMImage(t *testing.T) {
 	clusterMock.EXPECT().BaseURI().AnyTimes()
 	clusterMock.EXPECT().Location().AnyTimes()
 	clusterMock.EXPECT().SubscriptionID().AnyTimes()
-
 	cases := []struct {
 		Name   string
 		Setup  func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool)
diff --git a/azure/services/networkinterfaces/networkinterfaces_test.go b/azure/services/networkinterfaces/networkinterfaces_test.go
index 5d20a15ded3..59af9f25bf3 100644
--- a/azure/services/networkinterfaces/networkinterfaces_test.go
+++ b/azure/services/networkinterfaces/networkinterfaces_test.go
@@ -58,6 +58,18 @@ var (
 		AcceleratedNetworking: nil,
 		SKU:                   &fakeSku,
 	}
+	fakeNICSpec3 = NICSpec{
+		Name:                  "nic-3",
+		ResourceGroup:         "my-rg",
+		Location:              "fake-location",
+		SubscriptionID:        "123",
+		MachineName:           "azure-test1",
+		VNetName:              "my-vnet",
+		VNetResourceGroup:     "my-rg",
+		AcceleratedNetworking: nil,
+		SKU:                   &fakeSku,
+		IPConfigs:             []IPConfig{{}, {}},
+	}
 	internalError = autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: http.StatusInternalServerError}, "Internal Server Error")
 )
 
@@ -83,6 +95,15 @@ func TestReconcileNetworkInterface(t *testing.T) {
 				s.UpdatePutStatus(infrav1.NetworkInterfaceReadyCondition, serviceName, nil)
 			},
 		},
+		{
+			name:          "successfully create a network interface with multiple IPConfigs",
+			expectedError: "",
+			expect: func(s *mock_networkinterfaces.MockNICScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
+				s.NICSpecs().Return([]azure.ResourceSpecGetter{&fakeNICSpec3})
+				r.CreateOrUpdateResource(gomockinternal.AContext(), &fakeNICSpec3, serviceName).Return(nil, nil)
+				s.UpdatePutStatus(infrav1.NetworkInterfaceReadyCondition, serviceName, nil)
+			},
+		},
 		{
 			name:          "successfully create multiple network interfaces",
 			expectedError: "",
diff --git a/azure/services/networkinterfaces/spec.go b/azure/services/networkinterfaces/spec.go
index aba03939497..4e73cf6101a 100644
--- a/azure/services/networkinterfaces/spec.go
+++ b/azure/services/networkinterfaces/spec.go
@@ -18,6 +18,7 @@ package networkinterfaces
 
 import (
 	"context"
+	"strconv"
 
 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-08-01/network"
 	"github.com/Azure/go-autorest/autorest/to"
@@ -52,6 +53,14 @@ type NICSpec struct {
 	DNSServers                []string
 	AdditionalTags            infrav1.Tags
 	ClusterName               string
+	IPConfigs                 []IPConfig
+}
+
+// IPConfig defines the specification for an IP address configuration.
+type IPConfig struct {
+	PrivateIP       string
+	PublicIP        bool
+	PublicIPAddress string
 }
 
 // ResourceName returns the name of the network interface.
@@ -79,17 +88,19 @@ func (s *NICSpec) Parameters(ctx context.Context, existing interface{}) (paramet
 		return nil, nil
 	}
 
-	nicConfig := &network.InterfaceIPConfigurationPropertiesFormat{}
+	primaryIPConfig := &network.InterfaceIPConfigurationPropertiesFormat{
+		Primary: to.BoolPtr(true),
+	}
 
 	subnet := &network.Subnet{
 		ID: to.StringPtr(azure.SubnetID(s.SubscriptionID, s.VNetResourceGroup, s.VNetName, s.SubnetName)),
 	}
-	nicConfig.Subnet = subnet
+	primaryIPConfig.Subnet = subnet
 
-	nicConfig.PrivateIPAllocationMethod = network.IPAllocationMethodDynamic
+	primaryIPConfig.PrivateIPAllocationMethod = network.IPAllocationMethodDynamic
 	if s.StaticIPAddress != "" {
-		nicConfig.PrivateIPAllocationMethod = network.IPAllocationMethodStatic
-		nicConfig.PrivateIPAddress = to.StringPtr(s.StaticIPAddress)
+		primaryIPConfig.PrivateIPAllocationMethod = network.IPAllocationMethodStatic
+		primaryIPConfig.PrivateIPAddress = to.StringPtr(s.StaticIPAddress)
 	}
 
 	backendAddressPools := []network.BackendAddressPool{}
@@ -101,7 +112,7 @@ func (s *NICSpec) Parameters(ctx context.Context, existing interface{}) (paramet
 				})
 		}
 		if s.PublicLBNATRuleName != "" {
-			nicConfig.LoadBalancerInboundNatRules = &[]network.InboundNatRule{
+			primaryIPConfig.LoadBalancerInboundNatRules = &[]network.InboundNatRule{
 				{
 					ID: to.StringPtr(azure.NATRuleID(s.SubscriptionID, s.ResourceGroup, s.PublicLBName, s.PublicLBNATRuleName)),
 				},
@@ -114,10 +125,10 @@ func (s *NICSpec) Parameters(ctx context.Context, existing interface{}) (paramet
 				ID: to.StringPtr(azure.AddressPoolID(s.SubscriptionID, s.ResourceGroup, s.InternalLBName, s.InternalLBAddressPoolName)),
 			})
 	}
-	nicConfig.LoadBalancerBackendAddressPools = &backendAddressPools
+	primaryIPConfig.LoadBalancerBackendAddressPools = &backendAddressPools
 
 	if s.PublicIPName != "" {
-		nicConfig.PublicIPAddress = &network.PublicIPAddress{
+		primaryIPConfig.PublicIPAddress = &network.PublicIPAddress{
 			ID: to.StringPtr(azure.PublicIPID(s.SubscriptionID, s.ResourceGroup, s.PublicIPName)),
 		}
 	}
@@ -140,10 +151,43 @@ func (s *NICSpec) Parameters(ctx context.Context, existing interface{}) (paramet
 	ipConfigurations := []network.InterfaceIPConfiguration{
 		{
 			Name:                                     to.StringPtr("pipConfig"),
-			InterfaceIPConfigurationPropertiesFormat: nicConfig,
+			InterfaceIPConfigurationPropertiesFormat: primaryIPConfig,
 		},
 	}
 
+	// Build additional IPConfigs if more than 1 is specified
+	for i := 1; i < len(s.IPConfigs); i++ {
+		c := s.IPConfigs[i]
+		newIPConfigPropertiesFormat := &network.InterfaceIPConfigurationPropertiesFormat{}
+		newIPConfigPropertiesFormat.Subnet = subnet
+		config := network.InterfaceIPConfiguration{
+			Name:                                     to.StringPtr(s.Name + "-" + strconv.Itoa(i)),
+			InterfaceIPConfigurationPropertiesFormat: newIPConfigPropertiesFormat,
+		}
+		if c.PrivateIP == "" {
+			config.InterfaceIPConfigurationPropertiesFormat.PrivateIPAllocationMethod = network.IPAllocationMethodDynamic
+		} else {
+			config.InterfaceIPConfigurationPropertiesFormat.PrivateIPAllocationMethod = network.IPAllocationMethodStatic
+			config.InterfaceIPConfigurationPropertiesFormat.PrivateIPAddress = &c.PrivateIP
+		}
+
+		if c.PublicIP && c.PublicIPAddress != "" {
+			config.InterfaceIPConfigurationPropertiesFormat.PublicIPAddress = &network.PublicIPAddress{
+				PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{
+					PublicIPAllocationMethod: network.IPAllocationMethodStatic,
+					IPAddress:                &c.PublicIPAddress,
+				},
+			}
+		} else if c.PublicIP {
+			config.InterfaceIPConfigurationPropertiesFormat.PublicIPAddress = &network.PublicIPAddress{
+				PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{
+					PublicIPAllocationMethod: network.IPAllocationMethodDynamic,
+				},
+			}
+		}
+		config.InterfaceIPConfigurationPropertiesFormat.Primary = to.BoolPtr(false)
+		ipConfigurations = append(ipConfigurations, config)
+	}
 	if s.IPv6Enabled {
 		ipv6Config := network.InterfaceIPConfiguration{
 			Name: to.StringPtr("ipConfigv6"),
diff --git a/azure/services/networkinterfaces/spec_test.go b/azure/services/networkinterfaces/spec_test.go
index a9addeedff6..466aa1107cc 100644
--- a/azure/services/networkinterfaces/spec_test.go
+++ b/azure/services/networkinterfaces/spec_test.go
@@ -24,6 +24,7 @@ import (
 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-08-01/network"
 	"github.com/Azure/go-autorest/autorest/to"
 	. "github.com/onsi/gomega"
+	"github.com/onsi/gomega/format"
 	"sigs.k8s.io/cluster-api-provider-azure/azure/services/resourceskus"
 )
 
@@ -180,6 +181,74 @@ var (
 		DNSServers:                fakeCustomDNSServers,
 		ClusterName:               "my-cluster",
 	}
+	fakeDefaultIPconfigNICSpec = NICSpec{
+		Name:                  "my-net-interface",
+		ResourceGroup:         "my-rg",
+		Location:              "fake-location",
+		SubscriptionID:        "123",
+		MachineName:           "azure-test1",
+		SubnetName:            "my-subnet",
+		VNetName:              "my-vnet",
+		IPv6Enabled:           false,
+		VNetResourceGroup:     "my-rg",
+		PublicLBName:          "my-public-lb",
+		AcceleratedNetworking: nil,
+		SKU:                   &fakeSku,
+		EnableIPForwarding:    true,
+		IPConfigs:             []IPConfig{},
+		ClusterName:           "my-cluster",
+	}
+	fakeOneIPconfigNICSpec = NICSpec{
+		Name:                  "my-net-interface",
+		ResourceGroup:         "my-rg",
+		Location:              "fake-location",
+		SubscriptionID:        "123",
+		MachineName:           "azure-test1",
+		SubnetName:            "my-subnet",
+		VNetName:              "my-vnet",
+		IPv6Enabled:           false,
+		VNetResourceGroup:     "my-rg",
+		PublicLBName:          "my-public-lb",
+		AcceleratedNetworking: nil,
+		SKU:                   &fakeSku,
+		EnableIPForwarding:    true,
+		IPConfigs:             []IPConfig{{}},
+		ClusterName:           "my-cluster",
+	}
+	fakeTwoIPconfigNICSpec = NICSpec{
+		Name:                  "my-net-interface",
+		ResourceGroup:         "my-rg",
+		Location:              "fake-location",
+		SubscriptionID:        "123",
+		MachineName:           "azure-test1",
+		SubnetName:            "my-subnet",
+		VNetName:              "my-vnet",
+		IPv6Enabled:           false,
+		VNetResourceGroup:     "my-rg",
+		PublicLBName:          "my-public-lb",
+		AcceleratedNetworking: nil,
+		SKU:                   &fakeSku,
+		EnableIPForwarding:    true,
+		IPConfigs:             []IPConfig{{}, {}},
+		ClusterName:           "my-cluster",
+	}
+	fakeTwoIPconfigWithPublicNICSpec = NICSpec{
+		Name:                  "my-net-interface",
+		ResourceGroup:         "my-rg",
+		Location:              "fake-location",
+		SubscriptionID:        "123",
+		MachineName:           "azure-test1",
+		SubnetName:            "my-subnet",
+		VNetName:              "my-vnet",
+		IPv6Enabled:           false,
+		VNetResourceGroup:     "my-rg",
+		PublicIPName:          "pip-azure-test1",
+		AcceleratedNetworking: nil,
+		SKU:                   &fakeSku,
+		EnableIPForwarding:    true,
+		IPConfigs:             []IPConfig{{}, {}},
+		ClusterName:           "my-cluster",
+	}
 )
 
 func TestParameters(t *testing.T) {
@@ -212,6 +281,7 @@ func TestParameters(t *testing.T) {
 					},
 					Location: to.StringPtr("fake-location"),
 					InterfacePropertiesFormat: &network.InterfacePropertiesFormat{
+						Primary:                     nil,
 						EnableAcceleratedNetworking: to.BoolPtr(true),
 						EnableIPForwarding:          to.BoolPtr(false),
 						DNSSettings:                 &network.InterfaceDNSSettings{},
@@ -219,6 +289,7 @@ func TestParameters(t *testing.T) {
 							{
 								Name: to.StringPtr("pipConfig"),
 								InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
+									Primary:                         to.BoolPtr(true),
 									LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/loadBalancers/my-public-lb/backendAddressPools/cluster-name-outboundBackendPool")}},
 									PrivateIPAllocationMethod:       network.IPAllocationMethodStatic,
 									PrivateIPAddress:                to.StringPtr("fake.static.ip"),
@@ -247,10 +318,12 @@ func TestParameters(t *testing.T) {
 						EnableAcceleratedNetworking: to.BoolPtr(true),
 						EnableIPForwarding:          to.BoolPtr(false),
 						DNSSettings:                 &network.InterfaceDNSSettings{},
+						Primary:                     nil,
 						IPConfigurations: &[]network.InterfaceIPConfiguration{
 							{
 								Name: to.StringPtr("pipConfig"),
 								InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
+									Primary:                         to.BoolPtr(true),
 									LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/loadBalancers/my-public-lb/backendAddressPools/cluster-name-outboundBackendPool")}},
 									PrivateIPAllocationMethod:       network.IPAllocationMethodDynamic,
 									Subnet:                          &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")},
@@ -278,10 +351,12 @@ func TestParameters(t *testing.T) {
 						EnableAcceleratedNetworking: to.BoolPtr(true),
 						EnableIPForwarding:          to.BoolPtr(false),
 						DNSSettings:                 &network.InterfaceDNSSettings{},
+						Primary:                     nil,
 						IPConfigurations: &[]network.InterfaceIPConfiguration{
 							{
 								Name: to.StringPtr("pipConfig"),
 								InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
+									Primary:                     to.BoolPtr(true),
 									Subnet:                      &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")},
 									PrivateIPAllocationMethod:   network.IPAllocationMethodDynamic,
 									LoadBalancerInboundNatRules: &[]network.InboundNatRule{{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/loadBalancers/my-public-lb/inboundNatRules/azure-test1")}},
@@ -309,6 +384,7 @@ func TestParameters(t *testing.T) {
 					},
 					Location: to.StringPtr("fake-location"),
 					InterfacePropertiesFormat: &network.InterfacePropertiesFormat{
+						Primary:                     nil,
 						EnableAcceleratedNetworking: to.BoolPtr(true),
 						EnableIPForwarding:          to.BoolPtr(false),
 						DNSSettings:                 &network.InterfaceDNSSettings{},
@@ -316,6 +392,7 @@ func TestParameters(t *testing.T) {
 							{
 								Name: to.StringPtr("pipConfig"),
 								InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
+									Primary:                         to.BoolPtr(true),
 									Subnet:                          &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")},
 									PrivateIPAllocationMethod:       network.IPAllocationMethodDynamic,
 									LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{},
@@ -340,6 +417,7 @@ func TestParameters(t *testing.T) {
 					},
 					Location: to.StringPtr("fake-location"),
 					InterfacePropertiesFormat: &network.InterfacePropertiesFormat{
+						Primary:                     nil,
 						EnableAcceleratedNetworking: to.BoolPtr(false),
 						EnableIPForwarding:          to.BoolPtr(false),
 						DNSSettings:                 &network.InterfaceDNSSettings{},
@@ -347,6 +425,7 @@ func TestParameters(t *testing.T) {
 							{
 								Name: to.StringPtr("pipConfig"),
 								InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
+									Primary:                         to.BoolPtr(true),
 									Subnet:                          &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")},
 									PrivateIPAllocationMethod:       network.IPAllocationMethodDynamic,
 									LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{},
@@ -371,6 +450,7 @@ func TestParameters(t *testing.T) {
 					},
 					Location: to.StringPtr("fake-location"),
 					InterfacePropertiesFormat: &network.InterfacePropertiesFormat{
+						Primary:                     nil,
 						EnableAcceleratedNetworking: to.BoolPtr(true),
 						EnableIPForwarding:          to.BoolPtr(true),
 						DNSSettings:                 &network.InterfaceDNSSettings{},
@@ -378,6 +458,7 @@ func TestParameters(t *testing.T) {
 							{
 								Name: to.StringPtr("pipConfig"),
 								InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
+									Primary:                         to.BoolPtr(true),
 									Subnet:                          &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")},
 									PrivateIPAllocationMethod:       network.IPAllocationMethodDynamic,
 									LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{},
@@ -397,6 +478,159 @@ func TestParameters(t *testing.T) {
 			},
 			expectedError: "",
 		},
+		{
+			name:     "get parameters for network interface default ipconfig",
+			spec:     &fakeDefaultIPconfigNICSpec,
+			existing: nil,
+			expect: func(g *WithT, result interface{}) {
+				g.Expect(result).To(BeAssignableToTypeOf(network.Interface{}))
+				g.Expect(result.(network.Interface)).To(Equal(network.Interface{
+					Tags: map[string]*string{
+						"Name": to.StringPtr("my-net-interface"),
+						"sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"),
+					},
+					Location: to.StringPtr("fake-location"),
+					InterfacePropertiesFormat: &network.InterfacePropertiesFormat{
+						Primary:                     nil,
+						EnableAcceleratedNetworking: to.BoolPtr(true),
+						EnableIPForwarding:          to.BoolPtr(true),
+						DNSSettings:                 &network.InterfaceDNSSettings{},
+						IPConfigurations: &[]network.InterfaceIPConfiguration{
+							{
+								Name: to.StringPtr("pipConfig"),
+								InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
+									Primary:                         to.BoolPtr(true),
+									Subnet:                          &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")},
+									PrivateIPAllocationMethod:       network.IPAllocationMethodDynamic,
+									LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{},
+								},
+							},
+						},
+					},
+				}))
+			},
+			expectedError: "",
+		},
+		{
+			name:     "get parameters for network interface with one ipconfig",
+			spec:     &fakeOneIPconfigNICSpec,
+			existing: nil,
+			expect: func(g *WithT, result interface{}) {
+				g.Expect(result).To(BeAssignableToTypeOf(network.Interface{}))
+				g.Expect(result.(network.Interface)).To(Equal(network.Interface{
+					Tags: map[string]*string{
+						"Name": to.StringPtr("my-net-interface"),
+						"sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"),
+					},
+					Location: to.StringPtr("fake-location"),
+					InterfacePropertiesFormat: &network.InterfacePropertiesFormat{
+						Primary:                     nil,
+						EnableAcceleratedNetworking: to.BoolPtr(true),
+						EnableIPForwarding:          to.BoolPtr(true),
+						DNSSettings:                 &network.InterfaceDNSSettings{},
+						IPConfigurations: &[]network.InterfaceIPConfiguration{
+							{
+								Name: to.StringPtr("pipConfig"),
+								InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
+									Primary:                         to.BoolPtr(true),
+									Subnet:                          &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")},
+									PrivateIPAllocationMethod:       network.IPAllocationMethodDynamic,
+									LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{},
+								},
+							},
+						},
+					},
+				}))
+			},
+			expectedError: "",
+		},
+		{
+			name:     "get parameters for network interface with two ipconfigs",
+			spec:     &fakeTwoIPconfigNICSpec,
+			existing: nil,
+			expect: func(g *WithT, result interface{}) {
+				g.Expect(result).To(BeAssignableToTypeOf(network.Interface{}))
+				g.Expect(result.(network.Interface)).To(Equal(network.Interface{
+					Tags: map[string]*string{
+						"Name": to.StringPtr("my-net-interface"),
+						"sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"),
+					},
+					Location: to.StringPtr("fake-location"),
+					InterfacePropertiesFormat: &network.InterfacePropertiesFormat{
+						Primary:                     nil,
+						EnableAcceleratedNetworking: to.BoolPtr(true),
+						EnableIPForwarding:          to.BoolPtr(true),
+						DNSSettings:                 &network.InterfaceDNSSettings{},
+						IPConfigurations: &[]network.InterfaceIPConfiguration{
+							{
+								Name: to.StringPtr("pipConfig"),
+								InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
+									Primary:                         to.BoolPtr(true),
+									Subnet:                          &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")},
+									PrivateIPAllocationMethod:       network.IPAllocationMethodDynamic,
+									LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{},
+								},
+							},
+							{
+								Name: to.StringPtr("my-net-interface-1"),
+								InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
+									Primary:                         to.BoolPtr(false),
+									Subnet:                          &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")},
+									PrivateIPAllocationMethod:       network.IPAllocationMethodDynamic,
+									LoadBalancerBackendAddressPools: nil,
+								},
+							},
+						},
+					},
+				}))
+			},
+			expectedError: "",
+		},
+		{
+			name:     "get parameters for network interface with two ipconfigs and a public ip",
+			spec:     &fakeTwoIPconfigWithPublicNICSpec,
+			existing: nil,
+			expect: func(g *WithT, result interface{}) {
+				g.Expect(result).To(BeAssignableToTypeOf(network.Interface{}))
+				g.Expect(result.(network.Interface)).To(Equal(network.Interface{
+					Tags: map[string]*string{
+						"Name": to.StringPtr("my-net-interface"),
+						"sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"),
+					},
+					Location: to.StringPtr("fake-location"),
+					InterfacePropertiesFormat: &network.InterfacePropertiesFormat{
+						Primary:                     nil,
+						EnableAcceleratedNetworking: to.BoolPtr(true),
+						EnableIPForwarding:          to.BoolPtr(true),
+						DNSSettings:                 &network.InterfaceDNSSettings{},
+						IPConfigurations: &[]network.InterfaceIPConfiguration{
+							{
+								Name: to.StringPtr("pipConfig"),
+								InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
+									Primary:                   to.BoolPtr(true),
+									Subnet:                    &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")},
+									PrivateIPAllocationMethod: network.IPAllocationMethodDynamic,
+									PublicIPAddress: &network.PublicIPAddress{
+										ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/publicIPAddresses/pip-azure-test1"),
+									},
+									LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{},
+								},
+							},
+							{
+								Name: to.StringPtr("my-net-interface-1"),
+								InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
+									Primary:                         to.BoolPtr(false),
+									Subnet:                          &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")},
+									PrivateIPAllocationMethod:       network.IPAllocationMethodDynamic,
+									LoadBalancerBackendAddressPools: nil,
+								},
+							},
+						},
+					},
+				}))
+			},
+			expectedError: "",
+		},
 		{
 			name:     "get parameters for control plane network interface with DNS servers",
 			spec:     &fakeControlPlaneCustomDNSSettingsNICSpec,
@@ -420,6 +654,7 @@ func TestParameters(t *testing.T) {
 								Name: to.StringPtr("pipConfig"),
 								InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
 									Subnet:                      &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")},
+									Primary:                     to.BoolPtr(true),
 									PrivateIPAllocationMethod:   network.IPAllocationMethodDynamic,
 									LoadBalancerInboundNatRules: &[]network.InboundNatRule{{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/loadBalancers/my-public-lb/inboundNatRules/azure-test1")}},
 									LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{
@@ -434,6 +669,7 @@ func TestParameters(t *testing.T) {
 			expectedError: "",
 		},
 	}
+	format.MaxLength = 10000
 	for _, tc := range testcases {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
diff --git a/azure/services/scalesets/scalesets.go b/azure/services/scalesets/scalesets.go
index a5f8aaa8bb1..1b52552d938 100644
--- a/azure/services/scalesets/scalesets.go
+++ b/azure/services/scalesets/scalesets.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"encoding/base64"
 	"fmt"
+	"strconv"
 	"time"
 
 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2021-11-01/compute"
@@ -517,6 +518,68 @@ func (s *Service) buildVMSSFromSpec(ctx context.Context, vmssSpec azure.ScaleSet
 		},
 	}
 
+	// Use custom NIC definitions in VMSS if set
+	if len(vmssSpec.NetworkInterfaces) > 0 {
+		nicConfigs := []compute.VirtualMachineScaleSetNetworkConfiguration{}
+		for i, n := range vmssSpec.NetworkInterfaces {
+			nicConfig := compute.VirtualMachineScaleSetNetworkConfiguration{}
+			nicConfig.VirtualMachineScaleSetNetworkConfigurationProperties = &compute.VirtualMachineScaleSetNetworkConfigurationProperties{}
+			nicConfig.Name = to.StringPtr(vmssSpec.Name + "-" + strconv.Itoa(i))
+			if to.Bool(n.AcceleratedNetworking) {
+				nicConfig.VirtualMachineScaleSetNetworkConfigurationProperties.EnableAcceleratedNetworking = to.BoolPtr(true)
+			} else {
+				nicConfig.VirtualMachineScaleSetNetworkConfigurationProperties.EnableAcceleratedNetworking = to.BoolPtr(false)
+			}
+			if n.PrivateIPConfigs == 0 {
+				nicConfig.VirtualMachineScaleSetNetworkConfigurationProperties.IPConfigurations = &[]compute.VirtualMachineScaleSetIPConfiguration{
+					{
+						Name: to.StringPtr(vmssSpec.Name + "-" + strconv.Itoa(i)),
+						VirtualMachineScaleSetIPConfigurationProperties: &compute.VirtualMachineScaleSetIPConfigurationProperties{
+							Subnet: &compute.APIEntityReference{
+								ID: to.StringPtr(azure.SubnetID(s.Scope.SubscriptionID(), vmssSpec.VNetResourceGroup, vmssSpec.VNetName, n.SubnetName)),
+							},
+							Primary:                         to.BoolPtr(true),
+							PrivateIPAddressVersion:         compute.IPVersionIPv4,
+							LoadBalancerBackendAddressPools: &backendAddressPools,
+						},
+					},
+				}
+			} else {
+				ipconfigs := []compute.VirtualMachineScaleSetIPConfiguration{}
+
+				// Create IPConfigs
+				for j := 0; j < n.PrivateIPConfigs; j++ {
+					ipconfig := compute.VirtualMachineScaleSetIPConfiguration{
+						Name: to.StringPtr(fmt.Sprintf("private-ipConfig-%v", j)),
+						VirtualMachineScaleSetIPConfigurationProperties: &compute.VirtualMachineScaleSetIPConfigurationProperties{
+							PrivateIPAddressVersion: compute.IPVersionIPv4,
+							Subnet: &compute.APIEntityReference{
+								ID: to.StringPtr(azure.SubnetID(s.Scope.SubscriptionID(), vmssSpec.VNetResourceGroup, vmssSpec.VNetName, n.SubnetName)),
+							},
+						},
+					}
+
+					ipconfig.Subnet = &compute.APIEntityReference{
+						ID: to.StringPtr(azure.SubnetID(s.Scope.SubscriptionID(), vmssSpec.VNetResourceGroup, vmssSpec.VNetName, n.SubnetName)),
+					}
+					ipconfigs = append(ipconfigs, ipconfig)
+				}
+				if i == 0 {
+					ipconfigs[0].LoadBalancerBackendAddressPools = &backendAddressPools
+				}
+				// Always use the first IPConfig as the Primary
+				ipconfigs[0].Primary = to.BoolPtr(true)
+				nicConfig.VirtualMachineScaleSetNetworkConfigurationProperties.IPConfigurations = &ipconfigs
+			}
+			nicConfigs = append(nicConfigs, nicConfig)
+		}
+		nicConfigs[0].VirtualMachineScaleSetNetworkConfigurationProperties.Primary = to.BoolPtr(true)
+		vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations = &nicConfigs
+	} else {
+		// Set default interface configuration if no custom ones are specified
+		vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations = s.getVirtualMachineScaleSetDefaultNetworkConfiguration(vmssSpec)
+	}
+
 	// Assign Identity to VMSS
 	if vmssSpec.Identity == infrav1.VMIdentitySystemAssigned {
 		vmss.Identity = &compute.VirtualMachineScaleSetIdentity{
@@ -572,6 +635,39 @@ func (s *Service) buildVMSSFromSpec(ctx context.Context, vmssSpec azure.ScaleSet
 	return vmss, nil
 }
 
+func (s *Service) getVirtualMachineScaleSetDefaultNetworkConfiguration(vmssSpec azure.ScaleSetSpec) *[]compute.VirtualMachineScaleSetNetworkConfiguration {
+	var backendAddressPools []compute.SubResource
+	if vmssSpec.PublicLBName != "" {
+		if vmssSpec.PublicLBAddressPoolName != "" {
+			backendAddressPools = append(backendAddressPools,
+				compute.SubResource{
+					ID: to.StringPtr(azure.AddressPoolID(s.Scope.SubscriptionID(), s.Scope.ResourceGroup(), vmssSpec.PublicLBName, vmssSpec.PublicLBAddressPoolName)),
+				})
+		}
+	}
+	return &[]compute.VirtualMachineScaleSetNetworkConfiguration{{
+		Name: to.StringPtr(vmssSpec.Name),
+		VirtualMachineScaleSetNetworkConfigurationProperties: &compute.VirtualMachineScaleSetNetworkConfigurationProperties{
+			Primary:            to.BoolPtr(true),
+			EnableIPForwarding: to.BoolPtr(true),
+			IPConfigurations: &[]compute.VirtualMachineScaleSetIPConfiguration{
+				{
+					Name: to.StringPtr(vmssSpec.Name),
+					VirtualMachineScaleSetIPConfigurationProperties: &compute.VirtualMachineScaleSetIPConfigurationProperties{
+						Subnet: &compute.APIEntityReference{
+							ID: to.StringPtr(azure.SubnetID(s.Scope.SubscriptionID(), vmssSpec.VNetResourceGroup, vmssSpec.VNetName, vmssSpec.SubnetName)),
+						},
+						Primary:                         to.BoolPtr(true),
+						PrivateIPAddressVersion:         compute.IPVersionIPv4,
+						LoadBalancerBackendAddressPools: &backendAddressPools,
+					},
+				},
+			},
+			EnableAcceleratedNetworking: vmssSpec.AcceleratedNetworking,
+		},
+	}}
+}
+
 // getVirtualMachineScaleSet provides information about a Virtual Machine Scale Set and its instances.
 func (s *Service) getVirtualMachineScaleSet(ctx context.Context, vmssName string) (*azure.VMSS, error) {
 	ctx, _, done := tele.StartSpanWithLogger(ctx, "scalesets.Service.getVirtualMachineScaleSet")
diff --git a/azure/services/scalesets/scalesets_test.go b/azure/services/scalesets/scalesets_test.go
index 6598f3314dc..5ebc14c480c 100644
--- a/azure/services/scalesets/scalesets_test.go
+++ b/azure/services/scalesets/scalesets_test.go
@@ -258,6 +258,76 @@ func TestReconcileVMSS(t *testing.T) {
 				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS("VM_SIZE_AN"), putFuture)
 			},
 		},
+		{
+			name:          "should start creating vmss with custom networking when specified",
+			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.NetworkInterfaces = []infrav1.NetworkInterface{
+					{
+						SubnetName:            "my-subnet",
+						PrivateIPConfigs:      1,
+						AcceleratedNetworking: pointer.Bool(true),
+					},
+					{
+						SubnetName:            "subnet2",
+						PrivateIPConfigs:      2,
+						AcceleratedNetworking: pointer.Bool(true),
+					},
+				}
+				s.ScaleSetSpec().Return(spec).AnyTimes()
+				setupDefaultVMSSStartCreatingExpectations(s, m)
+				vmss := newDefaultVMSS("VM_SIZE")
+				vmss.VirtualMachineScaleSetProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{UltraSSDEnabled: pointer.Bool(true)}
+				netConfigs := vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations
+				(*netConfigs)[0].Name = to.StringPtr("my-vmss-0")
+				(*netConfigs)[0].EnableIPForwarding = nil
+				nic1IPConfigs := (*netConfigs)[0].IPConfigurations
+				(*nic1IPConfigs)[0].Name = to.StringPtr("private-ipConfig-0")
+				(*nic1IPConfigs)[0].PrivateIPAddressVersion = compute.IPVersionIPv4
+				(*netConfigs)[0].EnableAcceleratedNetworking = to.BoolPtr(true)
+				(*netConfigs)[0].Primary = to.BoolPtr(true)
+				vmssIPConfigs := []compute.VirtualMachineScaleSetIPConfiguration{
+					{
+						Name: to.StringPtr("private-ipConfig-0"),
+						VirtualMachineScaleSetIPConfigurationProperties: &compute.VirtualMachineScaleSetIPConfigurationProperties{
+							Primary:                 to.BoolPtr(true),
+							PrivateIPAddressVersion: compute.IPVersionIPv4,
+							Subnet: &compute.APIEntityReference{
+								ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/subnet2"),
+							},
+						},
+					},
+					{
+						Name: to.StringPtr("private-ipConfig-1"),
+						VirtualMachineScaleSetIPConfigurationProperties: &compute.VirtualMachineScaleSetIPConfigurationProperties{
+							PrivateIPAddressVersion: compute.IPVersionIPv4,
+							Subnet: &compute.APIEntityReference{
+								ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/subnet2"),
+							},
+						},
+					},
+				}
+				*netConfigs = append(*netConfigs, compute.VirtualMachineScaleSetNetworkConfiguration{
+					Name: to.StringPtr("my-vmss-1"),
+					VirtualMachineScaleSetNetworkConfigurationProperties: &compute.VirtualMachineScaleSetNetworkConfigurationProperties{
+						EnableAcceleratedNetworking: to.BoolPtr(true),
+						IPConfigurations:            &vmssIPConfigs,
+					},
+				})
+				m.CreateOrUpdateAsync(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName, gomockinternal.DiffEq(vmss)).
+					Return(putFuture, nil)
+				setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS("VM_SIZE"), putFuture)
+			},
+		},
 		{
 			name:          "should start creating a vmss with spot vm",
 			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",
diff --git a/azure/types.go b/azure/types.go
index 8b6598e73a3..c1dc9b2f05b 100644
--- a/azure/types.go
+++ b/azure/types.go
@@ -66,6 +66,7 @@ type ScaleSetSpec struct {
 	DiagnosticsProfile           *infrav1.Diagnostics
 	FailureDomains               []string
 	VMExtensions                 []infrav1.VMExtension
+	NetworkInterfaces            []infrav1.NetworkInterface
 }
 
 // TagsSpec defines the specification for a set of tags.
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 e9842965d0b..e97903748fb 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml
@@ -1468,11 +1468,8 @@ spec:
                   virtual machine within the Machine Pool
                 properties:
                   acceleratedNetworking:
-                    description: AcceleratedNetworking enables or disables Azure accelerated
-                      networking. If omitted, it will be set based on whether the
-                      requested VMSize supports accelerated networking. If AcceleratedNetworking
-                      is set to true with a VMSize that does not support it, Azure
-                      will return an error.
+                    description: 'Deprecated: AcceleratedNetworking should be set
+                      in the networkInterfaces field.'
                     type: boolean
                   dataDisks:
                     description: DataDisks specifies the list of data disks to be
@@ -1751,6 +1748,33 @@ spec:
                         - version
                         type: object
                     type: object
+                  networkInterfaces:
+                    description: NetworkInterfaces specifies a list of network interface
+                      configurations. If left unspecified, the VM will get a single
+                      network interface with a single IPConfig in the subnet specified
+                      in the cluster's node subnet field. The primary interface will
+                      be the first networkInterface specified (index 0) in the list.
+                    items:
+                      description: NetworkInterface defines a network interface.
+                      properties:
+                        acceleratedNetworking:
+                          description: AcceleratedNetworking enables or disables Azure
+                            accelerated networking. If omitted, it will be set based
+                            on whether the requested VMSize supports accelerated networking.
+                            If AcceleratedNetworking is set to true with a VMSize
+                            that does not support it, Azure will return an error.
+                          type: boolean
+                        privateIPConfigs:
+                          description: PrivateIPConfigs specifies the number of private
+                            IP addresses to attach to the interface. Defaults to 1
+                            if not specified.
+                          type: integer
+                        subnetName:
+                          description: SubnetName specifies the subnet in which the
+                            new network interface will be placed.
+                          type: string
+                      type: object
+                    type: array
                   osDisk:
                     description: OSDisk contains the operating system disk information
                       for a Virtual Machine
@@ -1837,8 +1861,8 @@ spec:
                       encoded to add to a Virtual Machine
                     type: string
                   subnetName:
-                    description: SubnetName selects the Subnet where the VMSS will
-                      be placed
+                    description: 'Deprecated: SubnetName should be set in the networkInterfaces
+                      field.'
                     type: string
                   terminateNotificationTimeout:
                     description: TerminateNotificationTimeout enables or disables
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 7516cf104b8..214faa90679 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml
@@ -1044,11 +1044,8 @@ spec:
             description: AzureMachineSpec defines the desired state of AzureMachine.
             properties:
               acceleratedNetworking:
-                description: AcceleratedNetworking enables or disables Azure accelerated
-                  networking. If omitted, it will be set based on whether the requested
-                  VMSize supports accelerated networking. If AcceleratedNetworking
-                  is set to true with a VMSize that does not support it, Azure will
-                  return an error.
+                description: 'Deprecated: AcceleratedNetworking should be set in the
+                  networkInterfaces field.'
                 type: boolean
               additionalCapabilities:
                 description: AdditionalCapabilities specifies additional capabilities
@@ -1374,6 +1371,33 @@ spec:
                     - version
                     type: object
                 type: object
+              networkInterfaces:
+                description: NetworkInterfaces specifies a list of network interface
+                  configurations. If left unspecified, the VM will get a single network
+                  interface with a single IPConfig in the subnet specified in the
+                  cluster's node subnet field. The primary interface will be the first
+                  networkInterface specified (index 0) in the list.
+                items:
+                  description: NetworkInterface defines a network interface.
+                  properties:
+                    acceleratedNetworking:
+                      description: AcceleratedNetworking enables or disables Azure
+                        accelerated networking. If omitted, it will be set based on
+                        whether the requested VMSize supports accelerated networking.
+                        If AcceleratedNetworking is set to true with a VMSize that
+                        does not support it, Azure will return an error.
+                      type: boolean
+                    privateIPConfigs:
+                      description: PrivateIPConfigs specifies the number of private
+                        IP addresses to attach to the interface. Defaults to 1 if
+                        not specified.
+                      type: integer
+                    subnetName:
+                      description: SubnetName specifies the subnet in which the new
+                        network interface will be placed.
+                      type: string
+                  type: object
+                type: array
               osDisk:
                 description: OSDisk specifies the parameters for the operating system
                   disk of the machine
@@ -1467,7 +1491,8 @@ spec:
               sshPublicKey:
                 type: string
               subnetName:
-                description: SubnetName selects the Subnet where the VM will be placed
+                description: 'Deprecated: SubnetName should be set in the networkInterfaces
+                  field.'
                 type: string
               userAssignedIdentities:
                 description: UserAssignedIdentities is a list of standalone Azure
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 2f0df657bc8..afe99143e37 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml
@@ -812,11 +812,8 @@ spec:
                       of the machine.
                     properties:
                       acceleratedNetworking:
-                        description: AcceleratedNetworking enables or disables Azure
-                          accelerated networking. If omitted, it will be set based
-                          on whether the requested VMSize supports accelerated networking.
-                          If AcceleratedNetworking is set to true with a VMSize that
-                          does not support it, Azure will return an error.
+                        description: 'Deprecated: AcceleratedNetworking should be
+                          set in the networkInterfaces field.'
                         type: boolean
                       additionalCapabilities:
                         description: AdditionalCapabilities specifies additional capabilities
@@ -1159,6 +1156,35 @@ spec:
                             - version
                             type: object
                         type: object
+                      networkInterfaces:
+                        description: NetworkInterfaces specifies a list of network
+                          interface configurations. If left unspecified, the VM will
+                          get a single network interface with a single IPConfig in
+                          the subnet specified in the cluster's node subnet field.
+                          The primary interface will be the first networkInterface
+                          specified (index 0) in the list.
+                        items:
+                          description: NetworkInterface defines a network interface.
+                          properties:
+                            acceleratedNetworking:
+                              description: AcceleratedNetworking enables or disables
+                                Azure accelerated networking. If omitted, it will
+                                be set based on whether the requested VMSize supports
+                                accelerated networking. If AcceleratedNetworking is
+                                set to true with a VMSize that does not support it,
+                                Azure will return an error.
+                              type: boolean
+                            privateIPConfigs:
+                              description: PrivateIPConfigs specifies the number of
+                                private IP addresses to attach to the interface. Defaults
+                                to 1 if not specified.
+                              type: integer
+                            subnetName:
+                              description: SubnetName specifies the subnet in which
+                                the new network interface will be placed.
+                              type: string
+                          type: object
+                        type: array
                       osDisk:
                         description: OSDisk specifies the parameters for the operating
                           system disk of the machine
@@ -1253,8 +1279,8 @@ spec:
                       sshPublicKey:
                         type: string
                       subnetName:
-                        description: SubnetName selects the Subnet where the VM will
-                          be placed
+                        description: 'Deprecated: SubnetName should be set in the
+                          networkInterfaces field.'
                         type: string
                       userAssignedIdentities:
                         description: UserAssignedIdentities is a list of standalone
diff --git a/controllers/azuremachine_reconciler.go b/controllers/azuremachine_reconciler.go
index e80accfad0d..5eb0ef60bb9 100644
--- a/controllers/azuremachine_reconciler.go
+++ b/controllers/azuremachine_reconciler.go
@@ -73,6 +73,9 @@ func (s *azureMachineService) Reconcile(ctx context.Context) error {
 	ctx, _, done := tele.StartSpanWithLogger(ctx, "controllers.azureMachineService.Reconcile")
 	defer done()
 
+	// Ensure that the deprecated networking field values have been migrated to the new NetworkInterfaces field.
+	s.scope.AzureMachine.Spec.SetNetworkInterfacesDefaults()
+
 	if err := s.scope.SetSubnetName(); err != nil {
 		return errors.Wrap(err, "failed defaulting subnet name")
 	}
diff --git a/exp/api/v1alpha3/azuremachinepool_conversion.go b/exp/api/v1alpha3/azuremachinepool_conversion.go
index 9120505d052..e3c5c4e1f3c 100644
--- a/exp/api/v1alpha3/azuremachinepool_conversion.go
+++ b/exp/api/v1alpha3/azuremachinepool_conversion.go
@@ -49,6 +49,7 @@ func (src *AzureMachinePool) ConvertTo(dstRaw conversion.Hub) error {
 		}
 	}
 
+	//nolint:staticcheck // SubnetName is now deprecated, but the v1beta1 defaulting webhook will migrate it to the networkInterfaces field
 	dst.Spec.Template.SubnetName = restored.Spec.Template.SubnetName
 
 	dst.Spec.Strategy.Type = restored.Spec.Strategy.Type
@@ -78,6 +79,10 @@ func (src *AzureMachinePool) ConvertTo(dstRaw conversion.Hub) error {
 		dst.Spec.Template.Image.ComputeGallery = restored.Spec.Template.Image.ComputeGallery
 	}
 
+	if restored.Spec.Template.NetworkInterfaces != nil {
+		dst.Spec.Template.NetworkInterfaces = restored.Spec.Template.NetworkInterfaces
+	}
+
 	if len(dst.Annotations) == 0 {
 		dst.Annotations = nil
 	}
diff --git a/exp/api/v1alpha3/zz_generated.conversion.go b/exp/api/v1alpha3/zz_generated.conversion.go
index 1df50cfeb61..4572aedbd54 100644
--- a/exp/api/v1alpha3/zz_generated.conversion.go
+++ b/exp/api/v1alpha3/zz_generated.conversion.go
@@ -487,6 +487,7 @@ func autoConvert_v1beta1_AzureMachinePoolMachineTemplate_To_v1alpha3_AzureMachin
 	}
 	// WARNING: in.SubnetName requires manual conversion: does not exist in peer-type
 	// WARNING: in.VMExtensions requires manual conversion: does not exist in peer-type
+	// WARNING: in.NetworkInterfaces requires manual conversion: does not exist in peer-type
 	return nil
 }
 
diff --git a/exp/api/v1alpha4/azuremachinepool_conversion.go b/exp/api/v1alpha4/azuremachinepool_conversion.go
index 537a95878b5..c68047ad3b9 100644
--- a/exp/api/v1alpha4/azuremachinepool_conversion.go
+++ b/exp/api/v1alpha4/azuremachinepool_conversion.go
@@ -41,6 +41,10 @@ func (src *AzureMachinePool) ConvertTo(dstRaw conversion.Hub) error {
 		return err
 	}
 
+	if restored.Spec.Template.NetworkInterfaces != nil {
+		dst.Spec.Template.NetworkInterfaces = restored.Spec.Template.NetworkInterfaces
+	}
+
 	if restored.Spec.Template.Image != nil && restored.Spec.Template.Image.ComputeGallery != nil {
 		dst.Spec.Template.Image.ComputeGallery = restored.Spec.Template.Image.ComputeGallery
 	}
@@ -71,6 +75,10 @@ func (dst *AzureMachinePool) ConvertFrom(srcRaw conversion.Hub) error {
 		return err
 	}
 
+	if err := utilconversion.MarshalData(src, dst); err != nil {
+		return err
+	}
+
 	// Preserve Hub data on down-conversion.
 	return utilconversion.MarshalData(src, dst)
 }
diff --git a/exp/api/v1alpha4/azuremachinepoolmachine_conversion.go b/exp/api/v1alpha4/azuremachinepoolmachine_conversion.go
index 307c3634417..cecfdc1915f 100644
--- a/exp/api/v1alpha4/azuremachinepoolmachine_conversion.go
+++ b/exp/api/v1alpha4/azuremachinepoolmachine_conversion.go
@@ -18,13 +18,23 @@ package v1alpha4
 
 import (
 	infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1beta1"
+	utilconversion "sigs.k8s.io/cluster-api/util/conversion"
 	"sigs.k8s.io/controller-runtime/pkg/conversion"
 )
 
 // ConvertTo converts this AzureMachinePoolMachine to the Hub version (v1beta1).
 func (src *AzureMachinePoolMachine) ConvertTo(dstRaw conversion.Hub) error {
 	dst := dstRaw.(*infrav1exp.AzureMachinePoolMachine)
-	return Convert_v1alpha4_AzureMachinePoolMachine_To_v1beta1_AzureMachinePoolMachine(src, dst, nil)
+	if err := Convert_v1alpha4_AzureMachinePoolMachine_To_v1beta1_AzureMachinePoolMachine(src, dst, nil); err != nil {
+		return err
+	}
+	restored := &infrav1exp.AzureMachinePoolMachine{}
+	if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok {
+		return err
+	}
+	dst.Spec = restored.Spec
+
+	return nil
 }
 
 // ConvertFrom converts from the Hub version (v1beta1) to this version.
diff --git a/exp/api/v1alpha4/zz_generated.conversion.go b/exp/api/v1alpha4/zz_generated.conversion.go
index 0d1d238f5d1..6bb6b329dee 100644
--- a/exp/api/v1alpha4/zz_generated.conversion.go
+++ b/exp/api/v1alpha4/zz_generated.conversion.go
@@ -743,6 +743,7 @@ func autoConvert_v1beta1_AzureMachinePoolMachineTemplate_To_v1alpha4_AzureMachin
 	}
 	out.SubnetName = in.SubnetName
 	// WARNING: in.VMExtensions requires manual conversion: does not exist in peer-type
+	// WARNING: in.NetworkInterfaces requires manual conversion: does not exist in peer-type
 	return nil
 }
 
diff --git a/exp/api/v1beta1/azuremachinepool_default.go b/exp/api/v1beta1/azuremachinepool_default.go
index 7da43c31503..ece0b4ac026 100644
--- a/exp/api/v1beta1/azuremachinepool_default.go
+++ b/exp/api/v1beta1/azuremachinepool_default.go
@@ -74,3 +74,31 @@ func (amp *AzureMachinePool) SetDiagnosticsDefaults() {
 		amp.Spec.Template.Diagnostics.Boot = bootDefault
 	}
 }
+
+// SetNetworkInterfacesDefaults sets the defaults for the network interfaces.
+func (amp *AzureMachinePool) SetNetworkInterfacesDefaults() {
+	// Ensure the deprecated fields and new fields are not populated simultaneously
+	if (amp.Spec.Template.SubnetName != "" || amp.Spec.Template.AcceleratedNetworking != nil) && len(amp.Spec.Template.NetworkInterfaces) > 0 {
+		// Both the deprecated and the new fields are both set, return without changes
+		// and reject the request in the validating webhook which runs later.
+		return
+	}
+
+	if len(amp.Spec.Template.NetworkInterfaces) == 0 {
+		amp.Spec.Template.NetworkInterfaces = []infrav1.NetworkInterface{
+			{
+				SubnetName:            amp.Spec.Template.SubnetName,
+				AcceleratedNetworking: amp.Spec.Template.AcceleratedNetworking,
+			},
+		}
+		amp.Spec.Template.SubnetName = ""
+		amp.Spec.Template.AcceleratedNetworking = nil
+	}
+
+	// Ensure that PrivateIPConfigs defaults to 1 if not specified.
+	for i := 0; i < len(amp.Spec.Template.NetworkInterfaces); i++ {
+		if amp.Spec.Template.NetworkInterfaces[i].PrivateIPConfigs == 0 {
+			amp.Spec.Template.NetworkInterfaces[i].PrivateIPConfigs = 1
+		}
+	}
+}
diff --git a/exp/api/v1beta1/azuremachinepool_default_test.go b/exp/api/v1beta1/azuremachinepool_default_test.go
index 2719a48f73f..a7b982a349d 100644
--- a/exp/api/v1beta1/azuremachinepool_default_test.go
+++ b/exp/api/v1beta1/azuremachinepool_default_test.go
@@ -19,6 +19,7 @@ package v1beta1
 import (
 	"testing"
 
+	"github.com/Azure/go-autorest/autorest/to"
 	"github.com/google/uuid"
 	. "github.com/onsi/gomega"
 	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
@@ -143,6 +144,99 @@ func TestAzureMachinePool_SetDiagnosticsDefaults(t *testing.T) {
 	g.Expect(nilDiagnostics.machinePool.Spec.Template.Diagnostics.Boot.StorageAccountType).To(Equal(infrav1.ManagedDiagnosticsStorage))
 }
 
+func TestAzureMachinePool_SetNetworkInterfacesDefaults(t *testing.T) {
+	g := NewWithT(t)
+
+	testCases := []struct {
+		name        string
+		machinePool *AzureMachinePool
+		want        *AzureMachinePool
+	}{
+		{
+			name: "defaulting webhook updates MachinePool with deprecated subnetName field",
+			machinePool: &AzureMachinePool{
+				Spec: AzureMachinePoolSpec{
+					Template: AzureMachinePoolMachineTemplate{
+						SubnetName: "test-subnet",
+					},
+				},
+			},
+			want: &AzureMachinePool{
+				Spec: AzureMachinePoolSpec{
+					Template: AzureMachinePoolMachineTemplate{
+						SubnetName: "",
+						NetworkInterfaces: []infrav1.NetworkInterface{
+							{
+								SubnetName:       "test-subnet",
+								PrivateIPConfigs: 1,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "defaulting webhook updates MachinePool with deprecated acceleratedNetworking field",
+			machinePool: &AzureMachinePool{
+				Spec: AzureMachinePoolSpec{
+					Template: AzureMachinePoolMachineTemplate{
+						SubnetName:            "test-subnet",
+						AcceleratedNetworking: to.BoolPtr(true),
+					},
+				},
+			},
+			want: &AzureMachinePool{
+				Spec: AzureMachinePoolSpec{
+					Template: AzureMachinePoolMachineTemplate{
+						SubnetName:            "",
+						AcceleratedNetworking: nil,
+						NetworkInterfaces: []infrav1.NetworkInterface{
+							{
+								SubnetName:            "test-subnet",
+								PrivateIPConfigs:      1,
+								AcceleratedNetworking: to.BoolPtr(true),
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "defaulting webhook does nothing if both new and deprecated subnetName fields are set",
+			machinePool: &AzureMachinePool{
+				Spec: AzureMachinePoolSpec{
+					Template: AzureMachinePoolMachineTemplate{
+						SubnetName: "test-subnet",
+						NetworkInterfaces: []infrav1.NetworkInterface{{
+							SubnetName: "test-subnet",
+						}},
+					},
+				},
+			},
+			want: &AzureMachinePool{
+				Spec: AzureMachinePoolSpec{
+					Template: AzureMachinePoolMachineTemplate{
+						SubnetName:            "test-subnet",
+						AcceleratedNetworking: nil,
+						NetworkInterfaces: []infrav1.NetworkInterface{
+							{
+								SubnetName: "test-subnet",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			tc.machinePool.SetNetworkInterfacesDefaults()
+			g.Expect(tc.machinePool).To(Equal(tc.want))
+		})
+	}
+}
+
 func createMachinePoolWithSSHPublicKey(sshPublicKey string) *AzureMachinePool {
 	return hardcodedAzureMachinePoolWithSSHKey(sshPublicKey)
 }
diff --git a/exp/api/v1beta1/azuremachinepool_types.go b/exp/api/v1beta1/azuremachinepool_types.go
index b8f64aeba44..d9c0e50fa7a 100644
--- a/exp/api/v1beta1/azuremachinepool_types.go
+++ b/exp/api/v1beta1/azuremachinepool_types.go
@@ -65,9 +65,7 @@ type (
 		// SSHPublicKey is the SSH public key string base64 encoded to add to a Virtual Machine
 		SSHPublicKey string `json:"sshPublicKey"`
 
-		// AcceleratedNetworking enables or disables Azure accelerated networking. If omitted, it will be set based on
-		// whether the requested VMSize supports accelerated networking.
-		// If AcceleratedNetworking is set to true with a VMSize that does not support it, Azure will return an error.
+		// Deprecated: AcceleratedNetworking should be set in the networkInterfaces field.
 		// +optional
 		AcceleratedNetworking *bool `json:"acceleratedNetworking,omitempty"`
 
@@ -89,13 +87,20 @@ type (
 		// +optional
 		SpotVMOptions *infrav1.SpotVMOptions `json:"spotVMOptions,omitempty"`
 
-		// SubnetName selects the Subnet where the VMSS will be placed
+		// Deprecated: SubnetName should be set in the networkInterfaces field.
 		// +optional
 		SubnetName string `json:"subnetName,omitempty"`
 
 		// VMExtensions specifies a list of extensions to be added to the scale set.
 		// +optional
 		VMExtensions []infrav1.VMExtension `json:"vmExtensions,omitempty"`
+
+		// NetworkInterfaces specifies a list of network interface configurations.
+		// If left unspecified, the VM will get a single network interface with a
+		// single IPConfig in the subnet specified in the cluster's node subnet field.
+		// The primary interface will be the first networkInterface specified (index 0) in the list.
+		// +optional
+		NetworkInterfaces []infrav1.NetworkInterface `json:"networkInterfaces,omitempty"`
 	}
 
 	// AzureMachinePoolSpec defines the desired state of AzureMachinePool.
diff --git a/exp/api/v1beta1/azuremachinepool_webhook.go b/exp/api/v1beta1/azuremachinepool_webhook.go
index 4e47bb6ab16..526dbd5fb45 100644
--- a/exp/api/v1beta1/azuremachinepool_webhook.go
+++ b/exp/api/v1beta1/azuremachinepool_webhook.go
@@ -50,6 +50,7 @@ func (amp *AzureMachinePool) Default() {
 	}
 	amp.SetIdentityDefaults()
 	amp.SetDiagnosticsDefaults()
+	amp.SetNetworkInterfacesDefaults()
 }
 
 // +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-azuremachinepool,mutating=false,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=azuremachinepools,versions=v1beta1,name=validation.azuremachinepool.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
@@ -89,6 +90,7 @@ func (amp *AzureMachinePool) Validate(old runtime.Object) error {
 		amp.ValidateDiagnostics,
 		amp.ValidateStrategy(),
 		amp.ValidateSystemAssignedIdentity(old),
+		amp.ValidateNetwork,
 	}
 
 	var errs []error
@@ -101,6 +103,14 @@ func (amp *AzureMachinePool) Validate(old runtime.Object) error {
 	return kerrors.NewAggregate(errs)
 }
 
+// ValidateNetwork of an AzureMachinePool.
+func (amp *AzureMachinePool) ValidateNetwork() error {
+	if (amp.Spec.Template.NetworkInterfaces != nil) && len(amp.Spec.Template.NetworkInterfaces) > 0 && amp.Spec.Template.SubnetName != "" {
+		return errors.New("cannot set both NetworkInterfaces and machine SubnetName")
+	}
+	return nil
+}
+
 // ValidateImage of an AzureMachinePool.
 func (amp *AzureMachinePool) ValidateImage() error {
 	if amp.Spec.Template.Image != nil {
diff --git a/exp/api/v1beta1/azuremachinepool_webhook_test.go b/exp/api/v1beta1/azuremachinepool_webhook_test.go
index fa70a1ec858..b6acac14b3c 100644
--- a/exp/api/v1beta1/azuremachinepool_webhook_test.go
+++ b/exp/api/v1beta1/azuremachinepool_webhook_test.go
@@ -169,6 +169,21 @@ func TestAzureMachinePool_ValidateCreate(t *testing.T) {
 			}),
 			wantErr: false,
 		},
+		{
+			name:    "azuremachinepool with valid legacy network configuration",
+			amp:     createMachinePoolWithNetworkConfig("testSubnet", []infrav1.NetworkInterface{}),
+			wantErr: false,
+		},
+		{
+			name:    "azuremachinepool with invalid legacy network configuration",
+			amp:     createMachinePoolWithNetworkConfig("testSubnet", []infrav1.NetworkInterface{{SubnetName: "testSubnet"}}),
+			wantErr: true,
+		},
+		{
+			name:    "azuremachinepool with valid networkinterface configuration",
+			amp:     createMachinePoolWithNetworkConfig("", []infrav1.NetworkInterface{{SubnetName: "testSubnet"}}),
+			wantErr: false,
+		},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
@@ -248,6 +263,24 @@ func TestAzureMachinePool_ValidateUpdate(t *testing.T) {
 			}),
 			wantErr: false,
 		},
+		{
+			name:    "azuremachinepool with valid network interface config",
+			oldAMP:  createMachinePoolWithNetworkConfig("", []infrav1.NetworkInterface{{SubnetName: "testSubnet"}}),
+			amp:     createMachinePoolWithNetworkConfig("", []infrav1.NetworkInterface{{SubnetName: "testSubnet2"}}),
+			wantErr: false,
+		},
+		{
+			name:    "azuremachinepool with valid network interface config",
+			oldAMP:  createMachinePoolWithNetworkConfig("", []infrav1.NetworkInterface{{SubnetName: "testSubnet"}}),
+			amp:     createMachinePoolWithNetworkConfig("subnet", []infrav1.NetworkInterface{{SubnetName: "testSubnet2"}}),
+			wantErr: true,
+		},
+		{
+			name:    "azuremachinepool with valid network interface config",
+			oldAMP:  createMachinePoolWithNetworkConfig("subnet", []infrav1.NetworkInterface{}),
+			amp:     createMachinePoolWithNetworkConfig("subnet", []infrav1.NetworkInterface{{SubnetName: "testSubnet2"}}),
+			wantErr: true,
+		},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
@@ -347,6 +380,17 @@ func createMachinePoolWithSharedImage(subscriptionID, resourceGroup, name, galle
 	}
 }
 
+func createMachinePoolWithNetworkConfig(subnetName string, interfaces []infrav1.NetworkInterface) *AzureMachinePool {
+	return &AzureMachinePool{
+		Spec: AzureMachinePoolSpec{
+			Template: AzureMachinePoolMachineTemplate{
+				SubnetName:        subnetName,
+				NetworkInterfaces: interfaces,
+			},
+		},
+	}
+}
+
 func createMachinePoolWithImageByID(imageID string, terminateNotificationTimeout *int) *AzureMachinePool {
 	image := infrav1.Image{
 		ID: &imageID,
diff --git a/exp/api/v1beta1/zz_generated.deepcopy.go b/exp/api/v1beta1/zz_generated.deepcopy.go
index 79ac9e8031a..17bea0d6a65 100644
--- a/exp/api/v1beta1/zz_generated.deepcopy.go
+++ b/exp/api/v1beta1/zz_generated.deepcopy.go
@@ -376,6 +376,13 @@ func (in *AzureMachinePoolMachineTemplate) DeepCopyInto(out *AzureMachinePoolMac
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
+	if in.NetworkInterfaces != nil {
+		in, out := &in.NetworkInterfaces, &out.NetworkInterfaces
+		*out = make([]apiv1beta1.NetworkInterface, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureMachinePoolMachineTemplate.
diff --git a/exp/controllers/azuremachinepool_reconciler.go b/exp/controllers/azuremachinepool_reconciler.go
index 86416076fb8..ee48e23af59 100644
--- a/exp/controllers/azuremachinepool_reconciler.go
+++ b/exp/controllers/azuremachinepool_reconciler.go
@@ -57,6 +57,9 @@ func (s *azureMachinePoolService) Reconcile(ctx context.Context) error {
 	ctx, _, done := tele.StartSpanWithLogger(ctx, "controllers.azureMachinePoolService.Reconcile")
 	defer done()
 
+	// Ensure that the deprecated networking field values have been migrated to the new NetworkInterfaces field.
+	s.scope.AzureMachinePool.SetNetworkInterfacesDefaults()
+
 	if err := s.scope.SetSubnetName(); err != nil {
 		return errors.Wrap(err, "failed defaulting subnet name")
 	}
diff --git a/test/e2e/aks_versions.go b/test/e2e/aks_versions.go
index 933e43b0c07..1952d220932 100644
--- a/test/e2e/aks_versions.go
+++ b/test/e2e/aks_versions.go
@@ -28,11 +28,10 @@ import (
 	. "github.com/onsi/gomega"
 	"github.com/pkg/errors"
 	"golang.org/x/mod/semver"
+	azureutil "sigs.k8s.io/cluster-api-provider-azure/util/azure"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	"sigs.k8s.io/cluster-api/test/framework/clusterctl"
 	"sigs.k8s.io/controller-runtime/pkg/client"
-
-	azureutil "sigs.k8s.io/cluster-api-provider-azure/util/azure"
 )
 
 // GetAKSKubernetesVersion gets the kubernetes version for AKS clusters as specified by the environment variable defined by versionVar.
diff --git a/util/azure/azure.go b/util/azure/azure.go
index 24ddd67bd80..4d8f90d329b 100644
--- a/util/azure/azure.go
+++ b/util/azure/azure.go
@@ -60,7 +60,7 @@ func getCloudConfig(environment azure.Environment) cloud.Configuration {
 	return config
 }
 
-// GetAuthorizer returns an autorest.Authorizer-compatible object from MSAL
+// GetAuthorizer returns an autorest.Authorizer-compatible object from MSAL.
 func GetAuthorizer(settings auth.EnvironmentSettings) (autorest.Authorizer, error) {
 	// azidentity uses different envvars for certificate authentication:
 	//  azidentity: AZURE_CLIENT_CERTIFICATE_{PATH,PASSWORD}