Repository: kubernetes-sigs/cloud-provider-azure
Pull request #982 (2022-01-10 02:24:20)

Cherry-pick #607, #627 and #841 to branch release-0.7
<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide/first-contribution.md#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. For reference on required PR/issue labels, read here:
https://git.k8s.io/community/contributors/devel/sig-release/release.md#issuepr-kind-label
3. Ensure you have added or ran the appropriate tests for your PR: https://git.k8s.io/community/contributors/devel/sig-testing/testing.md
4. If you want *faster* PR reviews, read how: https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
5. If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

#### What type of PR is this?

<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind design

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->

#### What this PR does / why we need it:
Cherry-pick #607, #627 and #841 to branch release-0.7 to solve flaky tests.

#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->
```release-note
Cherry-pick #607 to solve flaky tests:
* should add all nodes in different agent pools to backends [MultipleAgentPools]: fail to find `backendAddressPoolID` in `lbBackendAddressPoolsIDMap`
* Found no or more than 1 virtual network in resource group same as cluster name
* cannot obtain the master node

Failures e.g.
https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/969/pull-cloud-provider-azure-e2e-ccm-capz-1-20/1479028839588827136

Cherry-pick #627 to solve "get outbound rules" issue
Cherry-pick #841 to solve "not equal 0" issue

Failures e.g.
https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/982/pull-cloud-provider-azure-e2e-ccm-capz-1-20/1480370475220602880
```

#### Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:

<!--
This section can be blank if this pull request does not require a release note.

When adding links which point to resources within git repositories, like
KEPs or supporting documentation, please reference a specific commit and avoid
linking directly to the master branch. This ensures that links reference a
specific point in time, rather than a document that may change over time.

See here for guidance on getting permanent links to files: https://help.github.com/en/articles/getting-permanent-links-to-files

Please use the following format for linking documentation:
- [KEP]: <link>
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs

```


Patch:
diff --git a/cloud-node-manager.Dockerfile b/cloud-node-manager.Dockerfile
index adaabdec7..dac1adf3a 100644
--- a/cloud-node-manager.Dockerfile
+++ b/cloud-node-manager.Dockerfile
@@ -14,7 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-FROM --platform=linux/amd64 golang:1.16.6-stretch AS builder
+FROM golang:1.16.6-stretch AS builder
 
 ARG ENABLE_GIT_COMMAND=true
 ARG ARCH=amd64
diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go
index e1a92c059..3620f77ed 100644
--- a/pkg/consts/consts.go
+++ b/pkg/consts/consts.go
@@ -83,6 +83,10 @@ const (
 
 	// NodeLabelRole specifies the role of a node
 	NodeLabelRole = "kubernetes.io/role"
+	// MasterNodeRoleLabel specifies is the master node label for a node
+	MasterNodeRoleLabel = "node-role.kubernetes.io/master"
+	// ControlPlaneNodeRoleLabel specifies is the control-plane node label for a node
+	ControlPlaneNodeRoleLabel = "node-role.kubernetes.io/control-plane"
 
 	// NicFailedState is the failed state of a nic
 	NicFailedState = "Failed"
diff --git a/pkg/provider/azure_backoff.go b/pkg/provider/azure_backoff.go
index 9558d0ab5..fb2987761 100644
--- a/pkg/provider/azure_backoff.go
+++ b/pkg/provider/azure_backoff.go
@@ -271,11 +271,6 @@ func (az *Cloud) ListManagedLBs(service *v1.Service, nodes []*v1.Node, clusterNa
 		return nil, nil
 	}
 
-	// return early if wantLb=false
-	if nodes == nil {
-		return allLBs, nil
-	}
-
 	agentPoolLBs := make([]network.LoadBalancer, 0)
 	agentPoolVMSetNames, err := az.VMSet.GetAgentPoolVMSetNames(nodes)
 	if err != nil {
diff --git a/pkg/provider/azure_standard.go b/pkg/provider/azure_standard.go
index eb6c9638f..365c54fb5 100644
--- a/pkg/provider/azure_standard.go
+++ b/pkg/provider/azure_standard.go
@@ -148,14 +148,20 @@ func (az *Cloud) getAzureLoadBalancerName(clusterName string, vmSetName string,
 	return lbNamePrefix
 }
 
-// isMasterNode returns true if the node has a master role label.
-// The master role is determined by looking for:
-// * a kubernetes.io/role="master" label
-func isMasterNode(node *v1.Node) bool {
+// isControlPlaneNode returns true if the node has a control-plane role label.
+// The control-plane role is determined by looking for:
+// * a node-role.kubernetes.io/control-plane or node-role.kubernetes.io/master="" label
+func isControlPlaneNode(node *v1.Node) bool {
+	if _, ok := node.Labels[consts.ControlPlaneNodeRoleLabel]; ok {
+		return true
+	}
+	// include master role labels for k8s < 1.19
+	if _, ok := node.Labels[consts.MasterNodeRoleLabel]; ok {
+		return true
+	}
 	if val, ok := node.Labels[consts.NodeLabelRole]; ok && val == "master" {
 		return true
 	}
-
 	return false
 }
 
@@ -682,7 +688,7 @@ func (as *availabilitySet) getAgentPoolAvailabilitySets(vms []compute.VirtualMac
 	agentPoolAvailabilitySets = &[]string{}
 	for nx := range nodes {
 		nodeName := (*nodes[nx]).Name
-		if isMasterNode(nodes[nx]) {
+		if isControlPlaneNode(nodes[nx]) {
 			continue
 		}
 		asID, ok := vmNameToAvailabilitySetID[nodeName]
@@ -936,7 +942,7 @@ func (as *availabilitySet) EnsureHostsInPool(service *v1.Service, nodes []*v1.No
 	hostUpdates := make([]func() error, 0, len(nodes))
 	for _, node := range nodes {
 		localNodeName := node.Name
-		if as.useStandardLoadBalancer() && as.excludeMasterNodesFromStandardLB() && isMasterNode(node) {
+		if as.useStandardLoadBalancer() && as.excludeMasterNodesFromStandardLB() && isControlPlaneNode(node) {
 			klog.V(4).Infof("Excluding master node %q from load balancer backendpool %q", localNodeName, backendPoolID)
 			continue
 		}
diff --git a/pkg/provider/azure_vmss.go b/pkg/provider/azure_vmss.go
index 5dad26ae4..98876a2b7 100644
--- a/pkg/provider/azure_vmss.go
+++ b/pkg/provider/azure_vmss.go
@@ -740,7 +740,7 @@ func (ss *ScaleSet) listScaleSetVMs(scaleSetName, resourceGroup string) ([]compu
 func (ss *ScaleSet) getAgentPoolScaleSets(nodes []*v1.Node) (*[]string, error) {
 	agentPoolScaleSets := &[]string{}
 	for nx := range nodes {
-		if isMasterNode(nodes[nx]) {
+		if isControlPlaneNode(nodes[nx]) {
 			continue
 		}
 
@@ -1094,7 +1094,7 @@ func (ss *ScaleSet) ensureVMSSInPool(service *v1.Service, nodes []*v1.Node, back
 	// multiple standard load balancers and the basic load balancer doesn't
 	if ss.useStandardLoadBalancer() && !ss.EnableMultipleStandardLoadBalancers {
 		for _, node := range nodes {
-			if ss.excludeMasterNodesFromStandardLB() && isMasterNode(node) {
+			if ss.excludeMasterNodesFromStandardLB() && isControlPlaneNode(node) {
 				continue
 			}
 
@@ -1241,7 +1241,7 @@ func (ss *ScaleSet) EnsureHostsInPool(service *v1.Service, nodes []*v1.Node, bac
 	for _, node := range nodes {
 		localNodeName := node.Name
 
-		if ss.useStandardLoadBalancer() && ss.excludeMasterNodesFromStandardLB() && isMasterNode(node) {
+		if ss.useStandardLoadBalancer() && ss.excludeMasterNodesFromStandardLB() && isControlPlaneNode(node) {
 			klog.V(4).Infof("Excluding master node %q from load balancer backendpool %q", localNodeName, backendPoolID)
 			continue
 		}
@@ -1535,8 +1535,10 @@ func (ss *ScaleSet) EnsureBackendPoolDeleted(service *v1.Service, backendPoolID,
 
 		nodeResourceGroup, nodeVMSS, nodeInstanceID, nodeVMSSVM, err := ss.ensureBackendPoolDeletedFromNode(nodeName, backendPoolID)
 		if err != nil {
-			klog.Errorf("EnsureBackendPoolDeleted(%s): backendPoolID(%s) - failed with error %v", getServiceName(service), backendPoolID, err)
-			allErrs = append(allErrs, err)
+			if !errors.Is(err, ErrorNotVmssInstance) { // Do nothing for the VMAS nodes.
+				klog.Errorf("EnsureBackendPoolDeleted(%s): backendPoolID(%s) - failed with error %v", getServiceName(service), backendPoolID, err)
+				allErrs = append(allErrs, err)
+			}
 			continue
 		}
 
diff --git a/site/content/en/topics/tagging-resources.md b/site/content/en/topics/tagging-resources.md
index c6bf0373b..b21a90ed0 100644
--- a/site/content/en/topics/tagging-resources.md
+++ b/site/content/en/topics/tagging-resources.md
@@ -21,6 +21,8 @@ the controller manager would parse this configuration and tag the shared resourc
 
 The non-shared resource (public IP) could be tagged by setting `tags` in `azure.json` or service annotation `service.beta.kubernetes.io/azure-pip-tags`. The format of the two is similiar and the tags in the annotation would be considered first when there are conflicts between the configuration file and the annotation.
 
+> The annotation `service.beta.kubernetes.io/azure-pip-tags` only works for managed public IPs. For BYO public IPs, the cloud provider would not apply any tags to them.
+
 When the configuration, file or annotation, is updated, the old ones would be updated if there are conflicts. For example, after updating `{"tags": "a=b,c=d"}` to `{"tags": "a=c,e=f"}`, the new tags would be `a=c,c=d,e=f`.
 
 ## Integrating with system tags


Test patch:
diff --git a/pkg/provider/azure_standard_test.go b/pkg/provider/azure_standard_test.go
index 7fcb5fa02..4dd56739c 100644
--- a/pkg/provider/azure_standard_test.go
+++ b/pkg/provider/azure_standard_test.go
@@ -51,27 +51,45 @@ const (
 	primary                       = "primary"
 )
 
-func TestIsMasterNode(t *testing.T) {
-	if isMasterNode(&v1.Node{}) {
+func TestIsControlPlaneNode(t *testing.T) {
+	if isControlPlaneNode(&v1.Node{}) {
 		t.Errorf("Empty node should not be master!")
 	}
-	if isMasterNode(&v1.Node{
+	if isControlPlaneNode(&v1.Node{
 		ObjectMeta: meta.ObjectMeta{
 			Labels: map[string]string{
 				consts.NodeLabelRole: "worker",
 			},
 		},
 	}) {
-		t.Errorf("Node labelled 'worker' should not be master!")
+		t.Errorf("Node labelled 'worker' should not be control plane!")
 	}
-	if !isMasterNode(&v1.Node{
+	if !isControlPlaneNode(&v1.Node{
 		ObjectMeta: meta.ObjectMeta{
 			Labels: map[string]string{
 				consts.NodeLabelRole: "master",
 			},
 		},
 	}) {
-		t.Errorf("Node should be master!")
+		t.Errorf("Node with kubernetes.io/role: \"master\" label should be control plane!")
+	}
+	if !isControlPlaneNode(&v1.Node{
+		ObjectMeta: meta.ObjectMeta{
+			Labels: map[string]string{
+				consts.MasterNodeRoleLabel: "",
+			},
+		},
+	}) {
+		t.Errorf("Node with node-role.kubernetes.io/master: \"\" label should be control plane!")
+	}
+	if !isControlPlaneNode(&v1.Node{
+		ObjectMeta: meta.ObjectMeta{
+			Labels: map[string]string{
+				consts.ControlPlaneNodeRoleLabel: "",
+			},
+		},
+	}) {
+		t.Errorf("Node with node-role.kubernetes.io/control-plane: \"\" label should be control plane!")
 	}
 }
 
diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go
index 512ffe3d6..a1d4d3e0c 100644
--- a/tests/e2e/e2e_test.go
+++ b/tests/e2e/e2e_test.go
@@ -37,6 +37,7 @@ import (
 
 const (
 	reportDirEnv     = "CCM_JUNIT_REPORT_DIR"
+	artifactsDirEnv  = "ARTIFACTS"
 	defaultReportDir = "_report/"
 )
 
@@ -56,7 +57,12 @@ func TestAzureTest(t *testing.T) {
 	RegisterFailHandler(Fail)
 	reportDir := os.Getenv(reportDirEnv)
 	if reportDir == "" {
-		reportDir = defaultReportDir
+		artifactsDir := os.Getenv(artifactsDirEnv)
+		if artifactsDir != "" {
+			reportDir = artifactsDir
+		} else {
+			reportDir = defaultReportDir
+		}
 	}
 	var r []Reporter
 	if reportDir != "" {
diff --git a/tests/e2e/network/ensureloadbalancer.go b/tests/e2e/network/ensureloadbalancer.go
index 330142428..fe2e1bd0b 100644
--- a/tests/e2e/network/ensureloadbalancer.go
+++ b/tests/e2e/network/ensureloadbalancer.go
@@ -18,6 +18,7 @@ package network
 
 import (
 	"context"
+	"fmt"
 	"os"
 	"strings"
 	"time"
@@ -87,6 +88,45 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		tc = nil
 	})
 
+	It("should support BYO public IP", func() {
+		By("creating a public IP with tags")
+		ipName := basename + "-public-IP" + string(uuid.NewUUID())[0:4]
+		pip := defaultPublicIPAddress(ipName)
+		expectedTags := map[string]*string{
+			"foo": to.StringPtr("bar"),
+		}
+		pip.Tags = expectedTags
+		pip, err := utils.WaitCreatePIP(tc, ipName, tc.GetResourceGroup(), pip)
+		Expect(err).NotTo(HaveOccurred())
+		Expect(pip.Tags).To(Equal(expectedTags))
+		targetIP := to.String(pip.IPAddress)
+		utils.Logf("created pip with address %s", targetIP)
+
+		By("creating a service referencing the public IP")
+		service := utils.CreateLoadBalancerServiceManifest(testServiceName, nil, labels, ns.Name, ports)
+		service = updateServiceBalanceIP(service, false, targetIP)
+		_, err = cs.CoreV1().Services(ns.Name).Create(context.TODO(), service, metav1.CreateOptions{})
+		Expect(err).NotTo(HaveOccurred())
+		ip, err := utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, testServiceName, "")
+		Expect(err).NotTo(HaveOccurred())
+		Expect(ip).To(Equal(targetIP))
+
+		By("deleting the service")
+		err = cs.CoreV1().Services(ns.Name).Delete(context.TODO(), testServiceName, metav1.DeleteOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		By("test if the pip still exists")
+		pip, err = utils.WaitGetPIP(tc, ipName)
+		Expect(err).NotTo(HaveOccurred())
+
+		By("test if the tags are changed")
+		Expect(pip.Tags).To(Equal(expectedTags))
+
+		By("cleaning up")
+		err = utils.DeletePIPWithRetry(tc, ipName, "")
+		Expect(err).NotTo(HaveOccurred())
+	})
+
 	// Public w/o IP -> Public w/ IP
 	It("should support assigning to specific IP when updating public service", func() {
 		annotation := map[string]string{
@@ -113,7 +153,7 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		}()
 
 		By("Waiting for exposure of the original service without assigned lb IP")
-		ip1, err := utils.WaitServiceExposure(cs, ns.Name, testServiceName)
+		ip1, err := utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, testServiceName, "")
 		Expect(err).NotTo(HaveOccurred())
 
 		Expect(ip1).NotTo(Equal(targetIP))
@@ -126,8 +166,9 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		_, err = cs.CoreV1().Services(ns.Name).Update(context.TODO(), service, metav1.UpdateOptions{})
 		Expect(err).NotTo(HaveOccurred())
 
-		err = utils.WaitUpdateServiceExposure(cs, ns.Name, testServiceName, targetIP, true)
+		ip, err := utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, testServiceName, targetIP)
 		Expect(err).NotTo(HaveOccurred())
+		Expect(ip).To(Equal(targetIP))
 	})
 
 	// Internal w/ IP -> Internal w/ IP
@@ -150,14 +191,15 @@ var _ = Describe("Ensure LoadBalancer", func() {
 			Expect(err).NotTo(HaveOccurred())
 		}()
 		By("Waiting for exposure of internal service with specific IP")
-		err = utils.WaitUpdateServiceExposure(cs, ns.Name, testServiceName, ip1, true)
+		ip, err := utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, testServiceName, ip1)
+		Expect(err).NotTo(HaveOccurred())
+		Expect(ip).To(Equal(ip1))
 		list, errList := cs.CoreV1().Events(ns.Name).List(context.TODO(), metav1.ListOptions{})
 		Expect(errList).NotTo(HaveOccurred())
 		utils.Logf("Events list:")
 		for i, event := range list.Items {
 			utils.Logf("%d. %v", i, event)
 		}
-		Expect(err).NotTo(HaveOccurred())
 
 		ip2, err := utils.SelectAvailablePrivateIP(tc)
 		Expect(err).NotTo(HaveOccurred())
@@ -169,8 +211,9 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		_, err = cs.CoreV1().Services(ns.Name).Update(context.TODO(), service, metav1.UpdateOptions{})
 		Expect(err).NotTo(HaveOccurred())
 
-		err = utils.WaitUpdateServiceExposure(cs, ns.Name, testServiceName, ip2, true)
+		ip, err = utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, testServiceName, ip2)
 		Expect(err).NotTo(HaveOccurred())
+		Expect(ip).To(Equal(ip2))
 	})
 
 	// internal w/o IP -> public w/ IP
@@ -198,15 +241,15 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		}()
 
 		By("Waiting for exposure of the original service without assigned lb private IP")
-		ip1, err := utils.WaitServiceExposure(cs, ns.Name, testServiceName)
+		ip1, err := utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, testServiceName, "")
+		Expect(err).NotTo(HaveOccurred())
+		Expect(ip1).NotTo(Equal(targetIP))
 		list, errList := cs.CoreV1().Events(ns.Name).List(context.TODO(), metav1.ListOptions{})
 		Expect(errList).NotTo(HaveOccurred())
 		utils.Logf("Events list:")
 		for i, event := range list.Items {
 			utils.Logf("%d. %v", i, event)
 		}
-		Expect(err).NotTo(HaveOccurred())
-		Expect(ip1).NotTo(Equal(targetIP))
 
 		By("Updating service to bound to specific public IP")
 		utils.Logf("will update IP to %s, %v", targetIP, len(targetIP))
@@ -216,8 +259,9 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		_, err = cs.CoreV1().Services(ns.Name).Update(context.TODO(), service, metav1.UpdateOptions{})
 		Expect(err).NotTo(HaveOccurred())
 
-		err = utils.WaitUpdateServiceExposure(cs, ns.Name, testServiceName, targetIP, true)
+		ip, err := utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, testServiceName, targetIP)
 		Expect(err).NotTo(HaveOccurred())
+		Expect(ip).To(Equal(targetIP))
 	})
 
 	It("should have no operation since no change in service when update [Slow]", func() {
@@ -245,7 +289,7 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		}()
 
 		By("Waiting for exposure of the original service with assigned lb private IP")
-		targetIP, err = utils.WaitServiceExposure(cs, ns.Name, testServiceName)
+		targetIP, err = utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, testServiceName, targetIP)
 		Expect(err).NotTo(HaveOccurred())
 
 		By("Update without changing the service and wait for a while")
@@ -256,8 +300,29 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		_, err = cs.CoreV1().Services(ns.Name).Update(context.TODO(), service, metav1.UpdateOptions{})
 		Expect(err).NotTo(HaveOccurred())
 
-		//Wait for 10 minutes, there should return timeout err, since external ip should not change
-		err = utils.WaitUpdateServiceExposure(cs, ns.Name, testServiceName, targetIP, false /*expectSame*/)
+		// Wait for 5 minutes, there should return timeout err, since external ip should not change
+		err = wait.PollImmediate(10*time.Second, 5*time.Minute, func() (bool, error) {
+			service, err = cs.CoreV1().Services(ns.Name).Get(context.TODO(), testServiceName, metav1.GetOptions{})
+			if err != nil {
+				if utils.IsRetryableAPIError(err) {
+					return false, nil
+				}
+				return false, err
+			}
+
+			IngressList := service.Status.LoadBalancer.Ingress
+			if len(IngressList) == 0 {
+				err = fmt.Errorf("Cannot find Ingress in limited time")
+				utils.Logf("Fail to get ingress, retry it in %s seconds", 10)
+				return false, nil
+			}
+			if targetIP == service.Status.LoadBalancer.Ingress[0].IP {
+				utils.Logf("External IP is still %s", targetIP)
+				return false, nil
+			}
+			utils.Logf("succeeded")
+			return true, nil
+		})
 		Expect(err).To(Equal(wait.ErrWaitTimeout))
 	})
 
@@ -271,8 +336,8 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		service1 = updateServiceBalanceIP(service1, false, targetIP)
 		_, err = cs.CoreV1().Services(ns.Name).Create(context.TODO(), service1, metav1.CreateOptions{})
 		Expect(err).NotTo(HaveOccurred())
-		ip, err := utils.WaitServiceExposure(cs, ns.Name, "service1")
-		Expect(ip).To(Equal(targetIP))
+		ip, err := utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, "service1", targetIP)
+		Expect(err).NotTo(HaveOccurred())
 		utils.Logf("Successfully created LoadBalancer service1 in namespace %s with IP %s", ns.Name, ip)
 
 		ports2 := []v1.ServicePort{{
@@ -283,8 +348,8 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		service2 = updateServiceBalanceIP(service2, false, targetIP)
 		_, err = cs.CoreV1().Services(ns.Name).Create(context.TODO(), service2, metav1.CreateOptions{})
 		Expect(err).NotTo(HaveOccurred())
-		ip, err = utils.WaitServiceExposure(cs, ns.Name, "service2")
-		Expect(ip).To(Equal(targetIP))
+		ip, err = utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, "service2", targetIP)
+		Expect(err).NotTo(HaveOccurred())
 		utils.Logf("Successfully created LoadBalancer service2 in namespace %s with IP %s", ns.Name, ip)
 
 		defer func() {
@@ -302,7 +367,7 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		service1 := utils.CreateLoadBalancerServiceManifest("service1", nil, labels, ns.Name, ports)
 		_, err := cs.CoreV1().Services(ns.Name).Create(context.TODO(), service1, metav1.CreateOptions{})
 		Expect(err).NotTo(HaveOccurred())
-		ip, err := utils.WaitServiceExposure(cs, ns.Name, "service1")
+		ip, err := utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, "service1", "")
 		Expect(err).NotTo(HaveOccurred())
 		utils.Logf("Successfully created LoadBalancer service1 in namespace %s with IP %s", ns.Name, ip)
 
@@ -314,17 +379,15 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		service2 = updateServiceBalanceIP(service2, false, ip)
 		_, err = cs.CoreV1().Services(ns.Name).Create(context.TODO(), service2, metav1.CreateOptions{})
 		Expect(err).NotTo(HaveOccurred())
-		ip2, err := utils.WaitServiceExposure(cs, ns.Name, "service2")
+		_, err = utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, "service2", ip)
 		Expect(err).NotTo(HaveOccurred())
-		Expect(ip2).To(Equal(ip))
 		utils.Logf("Successfully created LoadBalancer service2 in namespace %s with IP %s", ns.Name, ip)
 
 		By("Deleting one service and check if the other service works well")
 		err = cs.CoreV1().Services(ns.Name).Delete(context.TODO(), "service1", metav1.DeleteOptions{})
 		Expect(err).NotTo(HaveOccurred())
-		ip3, err := utils.WaitServiceExposure(cs, ns.Name, "service2")
+		_, err = utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, "service2", ip)
 		Expect(err).NotTo(HaveOccurred())
-		Expect(ip3).To(Equal(ip))
 
 		By("Deleting all services")
 		err = cs.CoreV1().Services(ns.Name).Delete(context.TODO(), "service2", metav1.DeleteOptions{})
@@ -356,7 +419,7 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		service1 := utils.CreateLoadBalancerServiceManifest("service1", annotation, labels, ns.Name, ports)
 		_, err := cs.CoreV1().Services(ns.Name).Create(context.TODO(), service1, metav1.CreateOptions{})
 		Expect(err).NotTo(HaveOccurred())
-		ip, err := utils.WaitServiceExposure(cs, ns.Name, "service1")
+		ip, err := utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, "service1", "")
 		Expect(err).NotTo(HaveOccurred())
 		utils.Logf("Successfully created LoadBalancer service1 in namespace %s with IP %s", ns.Name, ip)
 
@@ -368,8 +431,8 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		service2.Spec.LoadBalancerIP = ip
 		_, err = cs.CoreV1().Services(ns.Name).Create(context.TODO(), service2, metav1.CreateOptions{})
 		Expect(err).NotTo(HaveOccurred())
-		ip2, err := utils.WaitServiceExposure(cs, ns.Name, "service2")
-		Expect(ip2).To(Equal(ip))
+		_, err = utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, "service2", ip)
+		Expect(err).NotTo(HaveOccurred())
 		utils.Logf("Successfully created LoadBalancer service2 in namespace %s with IP %s", ns.Name, ip)
 
 		defer func() {
@@ -394,7 +457,7 @@ var _ = Describe("Ensure LoadBalancer", func() {
 		service := utils.CreateLoadBalancerServiceManifest(testServiceName, nil, labels, ns.Name, ports)
 		_, err = cs.CoreV1().Services(ns.Name).Create(context.TODO(), service, metav1.CreateOptions{})
 		Expect(err).NotTo(HaveOccurred())
-		publicIP, err := utils.WaitServiceExposure(cs, ns.Name, testServiceName)
+		publicIP, err := utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, testServiceName, "")
 		Expect(err).NotTo(HaveOccurred())
 
 		By("Checking the initial node number in the LB backend pool")
diff --git a/tests/e2e/network/network_security_group.go b/tests/e2e/network/network_security_group.go
index 7d8b7c372..5d5e1a44d 100644
--- a/tests/e2e/network/network_security_group.go
+++ b/tests/e2e/network/network_security_group.go
@@ -25,6 +25,7 @@ import (
 
 	aznetwork "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2020-08-01/network"
 	"github.com/Azure/go-autorest/autorest/to"
+
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/intstr"
@@ -93,9 +94,9 @@ var _ = Describe("Network security group", func() {
 
 		By("Validating ip exists in Security Group")
 		port := fmt.Sprintf("%v", nginxPort)
-		nsg, err := tc.GetClusterSecurityGroup()
+		nsgs, err := tc.GetClusterSecurityGroups()
 		Expect(err).NotTo(HaveOccurred())
-		Expect(validateUnsharedSecurityRuleExists(nsg, ip, port)).To(BeTrue(), "Security rule for service %s not exists", serviceName)
+		Expect(validateUnsharedSecurityRuleExists(nsgs, ip, port)).To(BeTrue(), "Security rule for service %s not exists", serviceName)
 
 		By("Validating network security group working")
 		var code int
@@ -124,9 +125,9 @@ var _ = Describe("Network security group", func() {
 		Expect(utils.DeleteService(cs, ns.Name, serviceName)).NotTo(HaveOccurred())
 		isDeleted := false
 		for i := 1; i <= 30; i++ {
-			nsg, err := tc.GetClusterSecurityGroup()
+			nsgs, err := tc.GetClusterSecurityGroups()
 			Expect(err).NotTo(HaveOccurred())
-			if !validateUnsharedSecurityRuleExists(nsg, ip, port) {
+			if !validateUnsharedSecurityRuleExists(nsgs, ip, port) {
 				utils.Logf("Target rule successfully deleted")
 				isDeleted = true
 				break
@@ -141,18 +142,33 @@ var _ = Describe("Network security group", func() {
 		annotation := map[string]string{
 			consts.ServiceAnnotationAllowedServiceTag: "AzureCloud",
 		}
-		_ = createAndExposeDefaultServiceWithAnnotation(cs, serviceName, ns.Name, labels, annotation, ports)
+		utils.Logf("Creating service " + serviceName + " in namespace " + ns.Name)
+		service := utils.CreateLoadBalancerServiceManifest(serviceName, annotation, labels, ns.Name, ports)
+		_, err := cs.CoreV1().Services(ns.Name).Create(context.TODO(), service, metav1.CreateOptions{})
+		Expect(err).NotTo(HaveOccurred())
+		utils.Logf("Successfully created LoadBalancer service " + serviceName + " in namespace " + ns.Name)
+
+		By("Waiting for the service to be exposed")
+		_, err = utils.WaitServiceExposure(cs, ns.Name, serviceName, "")
+		Expect(err).NotTo(HaveOccurred())
 
 		By("Validating if the corresponding IP prefix existing in nsg")
-		nsg, err := tc.GetClusterSecurityGroup()
+		nsgs, err := tc.GetClusterSecurityGroups()
 		Expect(err).NotTo(HaveOccurred())
 
-		rules := nsg.SecurityRules
-		Expect(len(*rules)).NotTo(Equal(0))
 		var found bool
-		for _, rule := range *rules {
-			if rule.SourceAddressPrefix != nil && strings.Contains(*rule.SourceAddressPrefix, "AzureCloud") {
-				found = true
+		for _, nsg := range nsgs {
+			rules := nsg.SecurityRules
+			if rules == nil {
+				continue
+			}
+			for _, rule := range *rules {
+				if rule.SourceAddressPrefix != nil && strings.Contains(*rule.SourceAddressPrefix, "AzureCloud") {
+					found = true
+					break
+				}
+			}
+			if found {
 				break
 			}
 		}
@@ -170,13 +186,13 @@ var _ = Describe("Network security group", func() {
 		Expect(err).NotTo(HaveOccurred())
 
 		By("Waiting for the service to expose")
-		internalIP, err := utils.WaitServiceExposure(cs, ns.Name, serviceName)
+		internalIP, err := utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, serviceName, "")
 		Expect(err).NotTo(HaveOccurred())
 
 		By("Checking if there is a deny_all rule")
-		nsg, err := tc.GetClusterSecurityGroup()
+		nsgs, err := tc.GetClusterSecurityGroups()
 		Expect(err).NotTo(HaveOccurred())
-		found := validateDenyAllSecurityRuleExists(nsg, internalIP)
+		found := validateDenyAllSecurityRuleExists(nsgs, internalIP)
 		Expect(found).ToNot(BeTrue())
 
 		By("Deleting the service")
@@ -189,83 +205,91 @@ var _ = Describe("Network security group", func() {
 		Expect(err).NotTo(HaveOccurred())
 
 		By("Waiting for the service to expose")
-		internalIP, err = utils.WaitServiceExposure(cs, ns.Name, serviceName)
+		internalIP, err = utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, serviceName, "")
 		Expect(err).NotTo(HaveOccurred())
 
 		By("Checking if there is a LoadBalancerSourceRanges rule")
-		nsg, err = tc.GetClusterSecurityGroup()
+		nsgs, err = tc.GetClusterSecurityGroups()
 		Expect(err).NotTo(HaveOccurred())
-		found = validateLoadBalancerSourceRangesRuleExists(nsg, internalIP, "1.2.3.4/32", "1.2.3.4_32")
+		found = validateLoadBalancerSourceRangesRuleExists(nsgs, internalIP, "1.2.3.4/32", "1.2.3.4_32")
 		Expect(found).To(BeTrue())
 
 		By("Checking if there is a deny_all rule")
-		found = validateDenyAllSecurityRuleExists(nsg, internalIP)
+		found = validateDenyAllSecurityRuleExists(nsgs, internalIP)
 		Expect(found).To(BeTrue())
 	})
 
 })
 
-func validateUnsharedSecurityRuleExists(nsg *aznetwork.SecurityGroup, ip string, port string) bool {
-	if nsg == nil || nsg.SecurityRules == nil {
-		return false
-	}
-	for _, securityRule := range *nsg.SecurityRules {
-		if strings.EqualFold(to.String(securityRule.DestinationAddressPrefix), ip) && strings.EqualFold(to.String(securityRule.DestinationPortRange), port) {
-			utils.Logf("Find target security rule")
-			return true
+func validateUnsharedSecurityRuleExists(nsgs []aznetwork.SecurityGroup, ip string, port string) bool {
+	for _, nsg := range nsgs {
+		if nsg.SecurityRules == nil {
+			continue
+		}
+		for _, securityRule := range *nsg.SecurityRules {
+			if strings.EqualFold(to.String(securityRule.DestinationAddressPrefix), ip) && strings.EqualFold(to.String(securityRule.DestinationPortRange), port) {
+				utils.Logf("Found target security rule")
+				return true
+			}
 		}
 	}
 	return false
 }
 
-func validateSharedSecurityRuleExists(nsg *aznetwork.SecurityGroup, ips []string, port string) bool {
-	if nsg == nil || nsg.SecurityRules == nil {
-		return false
-	}
-	for _, securityRule := range *nsg.SecurityRules {
-		if strings.EqualFold(to.String(securityRule.DestinationPortRange), port) {
-			isFind := true
-			for _, ip := range ips {
-				if !utils.StringInSlice(ip, *securityRule.DestinationAddressPrefixes) {
-					utils.Logf("Find target security rule")
-					isFind = false
-					break
+func validateSharedSecurityRuleExists(nsgs []aznetwork.SecurityGroup, ips []string, port string) bool {
+	for _, nsg := range nsgs {
+		if nsg.SecurityRules == nil {
+			continue
+		}
+		for _, securityRule := range *nsg.SecurityRules {
+			if strings.EqualFold(to.String(securityRule.DestinationPortRange), port) {
+				found := true
+				for _, ip := range ips {
+					if !utils.StringInSlice(ip, *securityRule.DestinationAddressPrefixes) {
+						found = false
+						break
+					}
+				}
+				if found {
+					utils.Logf("Found target security rule")
+					return true
 				}
-			}
-			if isFind {
-				return true
 			}
 		}
 	}
 	return false
 }
 
-func validateLoadBalancerSourceRangesRuleExists(nsg *aznetwork.SecurityGroup, ip, sourceAddressPrefix, ipRangesSuffix string) bool {
-	if nsg == nil || nsg.SecurityRules == nil {
-		return false
-	}
-	for _, securityRule := range *nsg.SecurityRules {
-		if securityRule.Access == aznetwork.SecurityRuleAccessAllow &&
-			strings.EqualFold(to.String(securityRule.DestinationAddressPrefix), ip) &&
-			strings.HasSuffix(to.String(securityRule.Name), ipRangesSuffix) &&
-			strings.EqualFold(to.String(securityRule.SourceAddressPrefix), sourceAddressPrefix) {
-			return true
+func validateLoadBalancerSourceRangesRuleExists(nsgs []aznetwork.SecurityGroup, ip, sourceAddressPrefix, ipRangesSuffix string) bool {
+	for _, nsg := range nsgs {
+		if nsg.SecurityRules == nil {
+			continue
+		}
+		for _, securityRule := range *nsg.SecurityRules {
+			if securityRule.Access == aznetwork.SecurityRuleAccessAllow &&
+				strings.EqualFold(to.String(securityRule.DestinationAddressPrefix), ip) &&
+				strings.HasSuffix(to.String(securityRule.Name), ipRangesSuffix) &&
+				strings.EqualFold(to.String(securityRule.SourceAddressPrefix), sourceAddressPrefix) {
+				return true
+			}
 		}
 	}
 
 	return false
 }
 
-func validateDenyAllSecurityRuleExists(nsg *aznetwork.SecurityGroup, ip string) bool {
-	if nsg == nil || nsg.SecurityRules == nil {
-		return false
-	}
-	for _, securityRule := range *nsg.SecurityRules {
-		if securityRule.Access == aznetwork.SecurityRuleAccessDeny &&
-			strings.EqualFold(to.String(securityRule.DestinationAddressPrefix), ip) &&
-			strings.HasSuffix(to.String(securityRule.Name), "deny_all") &&
-			strings.EqualFold(to.String(securityRule.SourceAddressPrefix), "*") {
-			return true
+func validateDenyAllSecurityRuleExists(nsgs []aznetwork.SecurityGroup, ip string) bool {
+	for _, nsg := range nsgs {
+		if nsg.SecurityRules == nil {
+			continue
+		}
+		for _, securityRule := range *nsg.SecurityRules {
+			if securityRule.Access == aznetwork.SecurityRuleAccessDeny &&
+				strings.EqualFold(to.String(securityRule.DestinationAddressPrefix), ip) &&
+				strings.HasSuffix(to.String(securityRule.Name), "deny_all") &&
+				strings.EqualFold(to.String(securityRule.SourceAddressPrefix), "*") {
+				return true
+			}
 		}
 	}
 
diff --git a/tests/e2e/network/service_annotations.go b/tests/e2e/network/service_annotations.go
index 0e857291f..9e0c630b1 100644
--- a/tests/e2e/network/service_annotations.go
+++ b/tests/e2e/network/service_annotations.go
@@ -53,8 +53,6 @@ var (
 const (
 	nginxPort       = 80
 	nginxStatusCode = 200
-	pullInterval    = 20 * time.Second
-	pullTimeout     = 10 * time.Minute
 	testingPort     = 81
 )
 
@@ -156,17 +154,12 @@ var _ = Describe("Service with annotation", func() {
 		}
 
 		// create service with given annotation and wait it to expose
-		ip := createAndExposeDefaultServiceWithAnnotation(cs, serviceName, ns.Name, labels, annotation, ports)
+		_ = createAndExposeDefaultServiceWithAnnotation(cs, serviceName, ns.Name, labels, annotation, ports)
 		defer func() {
 			utils.Logf("cleaning up test service %s", serviceName)
 			err := utils.DeleteService(cs, ns.Name, serviceName)
 			Expect(err).NotTo(HaveOccurred())
 		}()
-
-		By("Validating whether the load balancer is internal")
-		url := fmt.Sprintf("%s:%v", ip, ports[0].Port)
-		err := validateInternalLoadBalancer(cs, ns.Name, url)
-		Expect(err).NotTo(HaveOccurred())
 	})
 
 	It("should support service annotation 'service.beta.kubernetes.io/azure-load-balancer-internal-subnet'", func() {
@@ -287,7 +280,7 @@ var _ = Describe("Service with annotation", func() {
 
 		//wait and get service's public IP Address
 		By("Waiting service to expose...")
-		_, err = utils.WaitServiceExposure(cs, ns.Name, serviceName)
+		_, err = utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, serviceName, to.String(pip.IPAddress))
 		Expect(err).NotTo(HaveOccurred())
 
 		lb := getAzureLoadBalancerFromPIP(tc, *pip.IPAddress, *rg.Name, "")
@@ -316,11 +309,11 @@ var _ = Describe("Service with annotation", func() {
 
 		By("Validate shared security rule exists")
 		port := fmt.Sprintf("%v", nginxPort)
-		nsg, err := tc.GetClusterSecurityGroup()
+		nsgs, err := tc.GetClusterSecurityGroups()
 		Expect(err).NotTo(HaveOccurred())
 
 		ipList := []string{ip1, ip2}
-		Expect(validateSharedSecurityRuleExists(nsg, ipList, port)).To(BeTrue(), "Security rule for service %s not exists", serviceName)
+		Expect(validateSharedSecurityRuleExists(nsgs, ipList, port)).To(BeTrue(), "Security rule for service %s not exists", serviceName)
 	})
 
 	It("should support service annotation `service.beta.kubernetes.io/azure-pip-tags`", func() {
@@ -333,7 +326,7 @@ var _ = Describe("Service with annotation", func() {
 		Expect(err).NotTo(HaveOccurred())
 
 		By("Waiting service to expose...")
-		ip, err := utils.WaitServiceExposure(cs, ns.Name, serviceName)
+		ip, err := utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, serviceName, "")
 		Expect(err).NotTo(HaveOccurred())
 
 		defer func() {
@@ -400,7 +393,7 @@ var _ = Describe("Service with annotation", func() {
 		Expect(err).NotTo(HaveOccurred())
 
 		By("Waiting for the service to expose")
-		ip, err := utils.WaitServiceExposure(cs, ns.Name, serviceName)
+		ip, err := utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, serviceName, "")
 		Expect(err).NotTo(HaveOccurred())
 		Expect(ip).To(Equal(to.String(pip1.IPAddress)))
 
@@ -412,7 +405,7 @@ var _ = Describe("Service with annotation", func() {
 		Expect(err).NotTo(HaveOccurred())
 
 		By("Waiting for service IP to be updated")
-		err = utils.WaitServiceIPEqualTo(cs, to.String(pip2.IPAddress), serviceName, ns.Name)
+		_, err = utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, serviceName, to.String(pip2.IPAddress))
 		Expect(err).NotTo(HaveOccurred())
 	})
 })
@@ -476,7 +469,7 @@ var _ = Describe("[[Multi-Nodepool]][VMSS]", func() {
 		vmssNames := sets.NewString()
 		var resourceGroupName string
 		for i, node := range nodes {
-			if utils.IsMasterNode(&nodes[i]) {
+			if utils.IsControlPlaneNode(&nodes[i]) {
 				continue
 			}
 			providerID := node.Spec.ProviderID
@@ -564,7 +557,7 @@ func createAndExposeDefaultServiceWithAnnotation(cs clientset.Interface, service
 
 	//wait and get service's public IP Address
 	utils.Logf("Waiting service to expose...")
-	publicIP, err := utils.WaitServiceExposure(cs, nsName, serviceName)
+	publicIP, err := utils.WaitServiceExposureAndValidateConnectivity(cs, nsName, serviceName, "")
 	Expect(err).NotTo(HaveOccurred())
 
 	return publicIP
@@ -607,73 +600,6 @@ func createNginxDeploymentManifest(name string, labels map[string]string) *appsv
 	}
 }
 
-// validate internal source can access to ILB
-// nolint:unused
-func validateInternalLoadBalancer(c clientset.Interface, ns string, url string) error {
-	// create a pod to access to the service
-	utils.Logf("Validating external IP not be public and internal accessible")
-	utils.Logf("Create a front pod to connect to service")
-	podName := "front-pod"
-	pod := &v1.Pod{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: podName,
-		},
-		Spec: v1.PodSpec{
-			Hostname: podName,
-			Containers: []v1.Container{
-				{
-					Name:            "test-app",
-					Image:           "appropriate/curl",
-					ImagePullPolicy: v1.PullIfNotPresent,
-					Command: []string{
-						"/bin/sh",
-						"-c",
-						"code=0; while [ $code != 200 ]; do code=$(curl -s -o /dev/null -w \"%{http_code}\" " + url + "); sleep 1; done; echo $code",
-					},
-				},
-			},
-			RestartPolicy: v1.RestartPolicyNever,
-		},
-	}
-	_, err := c.CoreV1().Pods(ns).Create(context.TODO(), pod, metav1.CreateOptions{})
-	if err != nil {
-		return err
-	}
-	defer func() {
-		utils.Logf("Deleting front pod")
-		err = utils.DeletePod(c, ns, podName)
-	}()
-
-	// publicFlag shows whether public accessible test ends
-	// internalFlag shows whether internal accessible test ends
-	utils.Logf("Call from the created pod")
-	err = wait.PollImmediate(pullInterval, pullTimeout, func() (bool, error) {
-		pod, err := c.CoreV1().Pods(ns).Get(context.TODO(), podName, metav1.GetOptions{})
-		if err != nil {
-			if utils.IsRetryableAPIError(err) {
-				return false, nil
-			}
-			return false, err
-		}
-		if pod.Status.Phase != v1.PodSucceeded {
-			utils.Logf("waiting for the pod succeeded")
-			return false, nil
-		}
-		if pod.Status.ContainerStatuses[0].State.Terminated == nil || pod.Status.ContainerStatuses[0].State.Terminated.Reason != "Completed" {
-			utils.Logf("waiting for the container completed")
-			return false, nil
-		}
-		utils.Logf("Still testing internal access from front pod to internal service")
-		log, err := c.CoreV1().Pods(ns).GetLogs(pod.Name, &v1.PodLogOptions{}).Do(context.TODO()).Raw()
-		if err != nil {
-			return false, nil
-		}
-		return strings.Contains(fmt.Sprintf("%s", log), "200"), nil
-	})
-	utils.Logf("validation finished")
-	return err
-}
-
 func validateLoadBalancerBackendPools(tc *utils.AzureTestClient, vmssName string, cs clientset.Interface, serviceName string, labels map[string]string, ns string, ports []v1.ServicePort, resourceGroupName string) {
 	serviceName = fmt.Sprintf("%s-%s", serviceName, vmssName)
 
@@ -689,7 +615,7 @@ func validateLoadBalancerBackendPools(tc *utils.AzureTestClient, vmssName string
 
 	//wait and get service's public IP Address
 	By("Waiting for service exposure")
-	publicIP, err := utils.WaitServiceExposure(cs, ns, serviceName)
+	publicIP, err := utils.WaitServiceExposureAndValidateConnectivity(cs, ns, serviceName, "")
 	Expect(err).NotTo(HaveOccurred())
 
 	// Invoking azure network client to get list of public IP Addresses
diff --git a/tests/e2e/network/standard_lb.go b/tests/e2e/network/standard_lb.go
index f3ec7cda7..6da52058b 100644
--- a/tests/e2e/network/standard_lb.go
+++ b/tests/e2e/network/standard_lb.go
@@ -108,7 +108,7 @@ var _ = Describe("[StandardLoadBalancer] Standard load balancer", func() {
 		var found bool
 		for _, nic := range *NICList {
 			found = false
-			if strings.Split(*nic.Name, "-")[1] == "master" {
+			if strings.Split(*nic.Name, "-")[1] == "master" || strings.Contains(*nic.Name, "control-plane") {
 				continue
 			}
 			for _, ipConfig := range *nic.IPConfigurations {
@@ -161,7 +161,9 @@ var _ = Describe("[StandardLoadBalancer] Standard load balancer", func() {
 				}
 			}
 		}
-		Expect(len(outboundRuleIPs)).NotTo(Equal(0))
+		if len(outboundRuleIPs) == 0 {
+			Skip("skip validating outbound IPs since outbound rules are not configured on SLB")
+		}
 
 		podTemplate := createPodGetIP()
 		err = utils.CreatePod(cs, ns.Name, podTemplate)
diff --git a/tests/e2e/utils/network_utils.go b/tests/e2e/utils/network_utils.go
index 93c2d649c..e12651ab3 100644
--- a/tests/e2e/utils/network_utils.go
+++ b/tests/e2e/utils/network_utils.go
@@ -21,7 +21,6 @@ import (
 	"fmt"
 	"net"
 	"strings"
-	"time"
 
 	aznetwork "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2020-08-01/network"
 	"github.com/Azure/go-autorest/autorest/to"
@@ -29,12 +28,9 @@ import (
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/wait"
-	clientset "k8s.io/client-go/kubernetes"
 )
 
-const k8sVNetPrefix = "k8s-vnet-"
-
-// getVirtualNetworkList is a wapper around listing VirtualNetwork
+// getVirtualNetworkList returns the list of virtual networks in the cluster resource group.
 func (azureTestClient *AzureTestClient) getVirtualNetworkList() (result aznetwork.VirtualNetworkListResultPage, err error) {
 	Logf("Getting virtual network list")
 	vNetClient := azureTestClient.createVirtualNetworksClient()
@@ -51,38 +47,27 @@ func (azureTestClient *AzureTestClient) getVirtualNetworkList() (result aznetwor
 	return
 }
 
-// GetClusterVirtualNetwork gets the only vnet of the cluster
+// GetClusterVirtualNetwork returns the cluster's virtual network.
 func (azureTestClient *AzureTestClient) GetClusterVirtualNetwork() (virtualNetwork aznetwork.VirtualNetwork, err error) {
 	vNetList, err := azureTestClient.getVirtualNetworkList()
 	if err != nil {
 		return
 	}
 
-	k8sVNetCount := 0
-	returnIndex := 0
-	for i, vNet := range vNetList.Values() {
-		if strings.HasPrefix(to.String(vNet.Name), k8sVNetPrefix) {
-			Logf("found one k8s virtual network %s", to.String(vNet.Name))
-			k8sVNetCount++
-			returnIndex = i
-		} else {
-			Logf("found other virtual network %s, skip", to.String(vNet.Name))
-		}
-	}
-	switch k8sVNetCount {
+	switch len(vNetList.Values()) {
 	case 0:
-		err = fmt.Errorf("Found no virtual network of the corresponding cluster in resource group")
+		err = fmt.Errorf("found no virtual network in resource group %s", azureTestClient.GetResourceGroup())
 		return
 	case 1:
-		virtualNetwork = vNetList.Values()[returnIndex]
+		virtualNetwork = vNetList.Values()[0]
 		return
 	default:
-		err = fmt.Errorf("Found more than one virtual network of the corresponding cluster in resource group")
+		err = fmt.Errorf("found more than one virtual network in resource group %s", azureTestClient.GetResourceGroup())
 		return
 	}
 }
 
-// CreateSubnet will create a new subnet in certain virtual network
+// CreateSubnet creates a new subnet in the specified virtual network.
 func (azureTestClient *AzureTestClient) CreateSubnet(vnet aznetwork.VirtualNetwork, subnetName *string, prefix *string) error {
 	Logf("creating a new subnet %s, %s", *subnetName, *prefix)
 	subnetParameter := (*vnet.Subnets)[0]
@@ -93,7 +78,7 @@ func (azureTestClient *AzureTestClient) CreateSubnet(vnet aznetwork.VirtualNetwo
 	return err
 }
 
-// DeleteSubnet delete a subnet with retry
+// DeleteSubnet deletes a subnet with retry.
 func (azureTestClient *AzureTestClient) DeleteSubnet(vnetName string, subnetName string) error {
 	subnetClient := azureTestClient.createSubnetsClient()
 	return wait.PollImmediate(poll, singleCallTimeout, func() (bool, error) {
@@ -115,14 +100,14 @@ func (azureTestClient *AzureTestClient) DeleteSubnet(vnetName string, subnetName
 			Logf("subnet %s has been deleted", subnetName)
 			return true, nil
 		}
-		Logf("encountered unexpected error %v during deleting subnet %s", err, subnetName)
+		Logf("encountered unexpected error %v while deleting subnet %s", err, subnetName)
 		return true, nil
 	})
 }
 
-// GetNextSubnetCIDR obtains a new ip address which has no overlapping with other subnet
+// GetNextSubnetCIDR obtains a new ip address which has no overlap with existing subnets.
 func GetNextSubnetCIDR(vnet aznetwork.VirtualNetwork) (string, error) {
-	if len((*vnet.AddressSpace.AddressPrefixes)) == 0 {
+	if len(*vnet.AddressSpace.AddressPrefixes) == 0 {
 		return "", fmt.Errorf("vNet has no prefix")
 	}
 	vnetCIDR := (*vnet.AddressSpace.AddressPrefixes)[0]
@@ -134,7 +119,7 @@ func GetNextSubnetCIDR(vnet aznetwork.VirtualNetwork) (string, error) {
 	return getNextSubnet(vnetCIDR, existSubnets)
 }
 
-// getSecurityGroupList is a wrapper around listing VirtualNetwork
+// getSecurityGroupList returns the list of security groups in the cluster resource group.
 func (azureTestClient *AzureTestClient) getSecurityGroupList() (result aznetwork.SecurityGroupListResultPage, err error) {
 	Logf("Getting virtual network list")
 	securityGroupsClient := azureTestClient.CreateSecurityGroupsClient()
@@ -152,20 +137,20 @@ func (azureTestClient *AzureTestClient) getSecurityGroupList() (result aznetwork
 	return
 }
 
-// GetClusterSecurityGroup gets the only vnet of the cluster
-func (azureTestClient *AzureTestClient) GetClusterSecurityGroup() (ret *aznetwork.SecurityGroup, err error) {
+// GetClusterSecurityGroups gets the security groups of the cluster.
+func (azureTestClient *AzureTestClient) GetClusterSecurityGroups() (ret []aznetwork.SecurityGroup, err error) {
 	securityGroupsList, err := azureTestClient.getSecurityGroupList()
 	if err != nil {
 		return
 	}
 	Logf("got sg list, length = %d", len(securityGroupsList.Values()))
 
-	// Assume there is only one cluster in one resource group
-	if len(securityGroupsList.Values()) != 1 {
-		err = fmt.Errorf("Found no or more than 1 virtual network in resource group same as cluster name")
+	if len(securityGroupsList.Values()) != 0 {
+		ret = securityGroupsList.Values()
 		return
 	}
-	ret = &securityGroupsList.Values()[0]
+
+	err = fmt.Errorf("could not find the cluster security group in resource group %s", azureTestClient.GetResourceGroup())
 	return
 }
 
@@ -174,6 +159,7 @@ func CreateLoadBalancerServiceManifest(name string, annotation map[string]string
 	return &v1.Service{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:        name,
+			Namespace:   namespace,
 			Annotations: annotation,
 		},
 		Spec: v1.ServiceSpec{
@@ -251,6 +237,15 @@ func SelectAvailablePrivateIP(tc *AzureTestClient) (string, error) {
 		return "", fmt.Errorf("failed to find a subnet in vNet %s", to.String(vNet.Name))
 	}
 	subnet := to.String((*vNet.Subnets)[0].AddressPrefix)
+	if len(*vNet.Subnets) > 1 {
+		for _, sn := range *vNet.Subnets {
+			// if there is more than one subnet, select the first one we find.
+			if !strings.Contains(*sn.Name, "controlplane") && !strings.Contains(*sn.Name, "control-plane") {
+				subnet = *sn.AddressPrefix
+				break
+			}
+		}
+	}
 	ip, _, err := net.ParseCIDR(subnet)
 	if err != nil {
 		return "", fmt.Errorf("failed to parse subnet CIDR in vNet %s: %w", to.String(vNet.Name), err)
@@ -297,13 +292,3 @@ func (azureTestClient *AzureTestClient) GetLoadBalancer(resourceGroupName, lbNam
 	lbClient := azureTestClient.creteLoadBalancerClient()
 	return lbClient.Get(context.Background(), resourceGroupName, lbName, "")
 }
-
-func WaitServiceIPEqualTo(cs clientset.Interface, expectedIP, serviceName, namespace string) error {
-	return wait.PollImmediate(10*time.Second, 10*time.Minute, func() (done bool, err error) {
-		ip, err := WaitServiceExposure(cs, namespace, serviceName)
-		if err != nil {
-			return false, err
-		}
-		return strings.EqualFold(ip, expectedIP), nil
-	})
-}
diff --git a/tests/e2e/utils/node_utils.go b/tests/e2e/utils/node_utils.go
index a2795463e..3a91c9e4a 100644
--- a/tests/e2e/utils/node_utils.go
+++ b/tests/e2e/utils/node_utils.go
@@ -32,9 +32,11 @@ import (
 )
 
 const (
-	nodeLabelRole     = "kubernetes.io/role"
-	typeLabel         = "type"
-	agentpoolLabelKey = "agentpool"
+	masterNodeRoleLabel       = "node-role.kubernetes.io/master"
+	controlPlaneNodeRoleLabel = "node-role.kubernetes.io/control-plane"
+	nodeLabelRole             = "kubernetes.io/role"
+	typeLabel                 = "type"
+	agentpoolLabelKey         = "agentpool"
 
 	// GPUResourceKey is the key of the GPU in the resource map of a node
 	GPUResourceKey = "nvidia.com/gpu"
@@ -68,7 +70,7 @@ func GetAgentNodes(cs clientset.Interface) ([]v1.Node, error) {
 	}
 	ret := make([]v1.Node, 0)
 	for i, node := range nodesList.Items {
-		if !isMasterNode(&nodesList.Items[i]) && !isVirtualKubeletNode(&nodesList.Items[i]) {
+		if !IsControlPlaneNode(&nodesList.Items[i]) && !isVirtualKubeletNode(&nodesList.Items[i]) {
 			ret = append(ret, node)
 		}
 	}
@@ -120,7 +122,7 @@ func GetMaster(cs clientset.Interface) (*v1.Node, error) {
 		return nil, err
 	}
 	for i, node := range nodesList.Items {
-		if isMasterNode(&nodesList.Items[i]) {
+		if IsControlPlaneNode(&nodesList.Items[i]) {
 			return &node, nil
 		}
 	}
@@ -235,10 +237,17 @@ func WaitAutoScaleNodes(cs clientset.Interface, targetNodeCount int, isScaleDown
 	return err
 }
 
-// isMasterNode returns true if the node has a master role label.
-// The master role is determined by looking for:
-// * a kubernetes.io/role="master" label
-func isMasterNode(node *v1.Node) bool {
+// IsControlPlaneNode returns true if the node has a control-plane role label.
+// The control-plane role is determined by looking for:
+// * a node-role.kubernetes.io/control-plane or node-role.kubernetes.io/master="" label
+func IsControlPlaneNode(node *v1.Node) bool {
+	if _, ok := node.Labels[controlPlaneNodeRoleLabel]; ok {
+		return true
+	}
+	// include master role labels for k8s < 1.19
+	if _, ok := node.Labels[masterNodeRoleLabel]; ok {
+		return true
+	}
 	if val, ok := node.Labels[nodeLabelRole]; ok && val == "master" {
 		return true
 	}
diff --git a/tests/e2e/utils/service_utils.go b/tests/e2e/utils/service_utils.go
index d11afc6e2..d79c94f0a 100644
--- a/tests/e2e/utils/service_utils.go
+++ b/tests/e2e/utils/service_utils.go
@@ -19,10 +19,13 @@ package utils
 import (
 	"context"
 	"fmt"
+	"net/http"
 	"os"
 	"strings"
 	"time"
 
+	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
+
 	aznetwork "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2020-08-01/network"
 
 	v1 "k8s.io/api/core/v1"
@@ -35,6 +38,8 @@ import (
 const (
 	serviceTimeout        = 5 * time.Minute
 	serviceTimeoutBasicLB = 10 * time.Minute
+	pullInterval          = 20 * time.Second
+	pullTimeout           = 1 * time.Minute
 )
 
 // DeleteService deletes a service
@@ -71,10 +76,35 @@ func GetServiceDomainName(prefix string) (ret string) {
 	return
 }
 
-// WaitServiceExposure returns ip of ingress
-func WaitServiceExposure(cs clientset.Interface, namespace string, name string) (string, error) {
+// WaitServiceExposureAndValidateConnectivity returns ip of the service and check the connectivity if it is a public IP
+func WaitServiceExposureAndValidateConnectivity(cs clientset.Interface, namespace string, name string, targetIP string) (string, error) {
 	var service *v1.Service
 	var err error
+	var ip string
+
+	service, err = WaitServiceExposure(cs, namespace, name, targetIP)
+	if err != nil {
+		return "", err
+	}
+
+	ip = service.Status.LoadBalancer.Ingress[0].IP
+
+	if !isInternalService(service) {
+		Logf("checking the connectivity of the public IP %s", ip)
+		port := service.Spec.Ports[0].Port
+		if err := ValidateExternalServiceConnectivity(ip, int(port)); err != nil {
+			return ip, err
+		}
+	}
+
+	return ip, nil
+}
+
+// WaitServiceExposure waits for the exposure of the external IP of the service
+func WaitServiceExposure(cs clientset.Interface, namespace string, name string, targetIP string) (*v1.Service, error) {
+	var service *v1.Service
+	var err error
+	var ip string
 
 	timeout := serviceTimeout
 	if skuEnv := os.Getenv(LoadBalancerSkuEnv); skuEnv != "" {
@@ -95,51 +125,62 @@ func WaitServiceExposure(cs clientset.Interface, namespace string, name string)
 		IngressList := service.Status.LoadBalancer.Ingress
 		if len(IngressList) == 0 {
 			err = fmt.Errorf("Cannot find Ingress in limited time")
-			Logf("Fail to find ingress, retry it in 10 seconds")
+			Logf("Fail to find ingress, retry in 10 seconds")
+			return false, nil
+		}
+
+		ip = service.Status.LoadBalancer.Ingress[0].IP
+		if targetIP != "" && !strings.EqualFold(ip, targetIP) {
+			Logf("expected IP is %s, current IP is %s, retry in 10 seconds", targetIP, ip)
 			return false, nil
 		}
+
 		return true, nil
 	}) != nil {
-		return "", err
+		return nil, err
 	}
-	ip := service.Status.LoadBalancer.Ingress[0].IP
+
 	Logf("Exposure successfully, get external ip: %s", ip)
-	return ip, nil
+	return service, nil
 }
 
-// WaitUpdateServiceExposure returns ip of ingress
-func WaitUpdateServiceExposure(cs clientset.Interface, namespace string, name string, targetIP string, expectSame bool) error {
-	var service *v1.Service
-	var err error
-	poll := 10 * time.Second
-	timeout := 10 * time.Minute
+func isInternalService(service *v1.Service) bool {
+	var (
+		val string
+		ok  bool
+	)
+	if val, ok = service.Annotations[consts.ServiceAnnotationLoadBalancerInternal]; !ok {
+		return false
+	}
 
-	return wait.PollImmediate(poll, timeout, func() (bool, error) {
-		service, err = cs.CoreV1().Services(namespace).Get(context.TODO(), name, metav1.GetOptions{})
-		if err != nil {
-			if IsRetryableAPIError(err) {
-				return false, nil
-			}
-			return false, err
-		}
+	return strings.EqualFold(val, "true")
+}
 
-		IngressList := service.Status.LoadBalancer.Ingress
-		if len(IngressList) == 0 {
-			err = fmt.Errorf("Cannot find Ingress in limited time")
-			Logf("Fail to get ingress, retry it in %v seconds", poll)
+// ValidateExternalServiceConnectivity validates the connectivity of the service IP
+func ValidateExternalServiceConnectivity(serviceIP string, port int) error {
+	// the default nginx port is 80, skip other ports
+	if port != 80 {
+		return nil
+	}
+
+	err := wait.PollImmediate(pullInterval, pullTimeout, func() (done bool, err error) {
+		resp, err := http.Get(fmt.Sprintf("http://%s:%d", serviceIP, port))
+		if err != nil {
+			Logf("got error %v, will retry", err)
 			return false, nil
 		}
-		if targetIP != service.Status.LoadBalancer.Ingress[0].IP == expectSame {
-			if expectSame {
-				Logf("still unmatched external IP, retry it in %v seconds", poll)
-			} else {
-				Logf("External IP is still %s", targetIP)
-			}
-			return false, nil
+
+		if 200 <= resp.StatusCode && resp.StatusCode < 300 {
+			Logf("succeeded")
+			return true, nil
 		}
-		Logf("Exposure successfully")
-		return true, nil
+
+		Logf("got status code %d", resp.StatusCode)
+		return false, nil
 	})
+
+	Logf("validation finished")
+	return err
 }
 
 // extractSuffix obtains the server domain name suffix
@@ -148,5 +189,8 @@ func extractSuffix() string {
 	prefix := ExtractDNSPrefix()
 	url := c.Clusters[prefix].Server
 	suffix := url[strings.Index(url, "."):]
+	if strings.Contains(suffix, ":") {
+		suffix = suffix[:strings.Index(suffix, ":")]
+	}
 	return suffix
 }
diff --git a/tests/e2e/utils/utils.go b/tests/e2e/utils/utils.go
index 32bd8b660..7eccd732b 100644
--- a/tests/e2e/utils/utils.go
+++ b/tests/e2e/utils/utils.go
@@ -21,6 +21,7 @@ import (
 	"fmt"
 	"os"
 	"reflect"
+	"strings"
 	"time"
 
 	v1 "k8s.io/api/core/v1"
@@ -170,7 +171,8 @@ func IsRetryableAPIError(err error) bool {
 // ExtractDNSPrefix obtains the cluster DNS prefix
 func ExtractDNSPrefix() string {
 	c := obtainConfig()
-	return c.CurrentContext
+	parts := strings.Split(c.CurrentContext, "@")
+	return parts[len(parts)-1]
 }
 
 // Load config from file
@@ -190,17 +192,6 @@ func StringInSlice(s string, list []string) bool {
 	return false
 }
 
-// IsMasterNode returns true if the node has a master role label.
-// The master role is determined by looking for:
-// * a kubernetes.io/role="master" label
-func IsMasterNode(node *v1.Node) bool {
-	if val, ok := node.Labels[nodeLabelRole]; ok && val == "master" {
-		return true
-	}
-
-	return false
-}
-
 // HandleVMNotFoundErr returns true if the input error is errVMNotFound or nil
 func HandleVMNotFoundErr(err error) bool {
 	return err == nil || reflect.DeepEqual(err, errVMNotFound)