From 72ecb6e98c319094f55817702de45eccc8612dbc Mon Sep 17 00:00:00 2001
From: Neil Ye <yechenwei2007@126.com>
Date: Wed, 27 May 2020 08:20:24 +0800
Subject: [PATCH] Add virtual resource association:
 azurerm_nat_gateway_public_ip (#6450)

fixes #6052
---
 ..._gateway_public_ip_association_resource.go | 195 ++++++++++++
 .../services/network/nat_gateway_resource.go  |   2 +
 .../services/network/parse/nat_gateway.go     |  31 ++
 .../network/parse/nat_gateway_test.go         |  70 +++++
 .../internal/services/network/registration.go |   1 +
 ...way_public_ip_association_resource_test.go | 293 ++++++++++++++++++
 .../services/network/validate/nat_gateway.go  |  22 ++
 website/azurerm.erb                           |   4 +
 website/docs/d/nat_gateway.html.markdown      |   2 +
 website/docs/r/nat_gateway.html.markdown      |   2 +-
 ...ateway_public_ip_association.html.markdown |  75 +++++
 11 files changed, 696 insertions(+), 1 deletion(-)
 create mode 100644 azurerm/internal/services/network/nat_gateway_public_ip_association_resource.go
 create mode 100644 azurerm/internal/services/network/parse/nat_gateway.go
 create mode 100644 azurerm/internal/services/network/parse/nat_gateway_test.go
 create mode 100644 azurerm/internal/services/network/tests/nat_gateway_public_ip_association_resource_test.go
 create mode 100644 azurerm/internal/services/network/validate/nat_gateway.go
 create mode 100644 website/docs/r/nat_gateway_public_ip_association.html.markdown

diff --git a/azurerm/internal/services/network/nat_gateway_public_ip_association_resource.go b/azurerm/internal/services/network/nat_gateway_public_ip_association_resource.go
new file mode 100644
index 0000000000000..e1a5e229ce276
--- /dev/null
+++ b/azurerm/internal/services/network/nat_gateway_public_ip_association_resource.go
@@ -0,0 +1,195 @@
+package network
+
+import (
+	"fmt"
+	"log"
+	"time"
+
+	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2020-03-01/network"
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+	"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure"
+	"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf"
+	"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients"
+	"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/locks"
+	"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/parse"
+	"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/validate"
+	azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema"
+	"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts"
+	"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
+)
+
+func resourceArmNatGatewayPublicIpAssociation() *schema.Resource {
+	return &schema.Resource{
+		Create: resourceArmNatGatewayPublicIpAssociationCreate,
+		Read:   resourceArmNatGatewayPublicIpAssociationRead,
+		Delete: resourceArmNatGatewayPublicIpAssociationDelete,
+
+		Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error {
+			_, err := parse.NatGatewayID(id)
+			return err
+		}),
+
+		Timeouts: &schema.ResourceTimeout{
+			Create: schema.DefaultTimeout(30 * time.Minute),
+			Read:   schema.DefaultTimeout(5 * time.Minute),
+			Delete: schema.DefaultTimeout(30 * time.Minute),
+		},
+
+		Schema: map[string]*schema.Schema{
+			"nat_gateway_id": {
+				Type:         schema.TypeString,
+				Required:     true,
+				ForceNew:     true,
+				ValidateFunc: validate.NatGatewayID,
+			},
+
+			"public_ip_address_id": {
+				Type:         schema.TypeString,
+				Required:     true,
+				ForceNew:     true,
+				ValidateFunc: azure.ValidateResourceID,
+			},
+		},
+	}
+}
+
+func resourceArmNatGatewayPublicIpAssociationCreate(d *schema.ResourceData, meta interface{}) error {
+	client := meta.(*clients.Client).Network.NatGatewayClient
+	ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d)
+	defer cancel()
+
+	log.Printf("[INFO] preparing arguments for Nat Gateway <-> Public Ip Association creation.")
+	natGatewayId := d.Get("nat_gateway_id").(string)
+	publicIpAddressId := d.Get("public_ip_address_id").(string)
+	parsedNatGatewayId, err := parse.NatGatewayID(natGatewayId)
+	if err != nil {
+		return err
+	}
+
+	locks.ByName(parsedNatGatewayId.Name, natGatewayResourceName)
+	defer locks.UnlockByName(parsedNatGatewayId.Name, natGatewayResourceName)
+
+	natGateway, err := client.Get(ctx, parsedNatGatewayId.ResourceGroup, parsedNatGatewayId.Name, "")
+	if err != nil {
+		if utils.ResponseWasNotFound(natGateway.Response) {
+			return fmt.Errorf("Nat Gateway %q (Resource Group %q) was not found.", parsedNatGatewayId.Name, parsedNatGatewayId.ResourceGroup)
+		}
+		return fmt.Errorf("failed to retrieve Nat Gateway %q (Resource Group %q): %+v", parsedNatGatewayId.Name, parsedNatGatewayId.ResourceGroup, err)
+	}
+
+	publicIpAddresses := make([]network.SubResource, 0)
+	if natGateway.PublicIPAddresses != nil {
+		for _, existingPublicIPAddress := range *natGateway.PublicIPAddresses {
+			if existingPublicIPAddress.ID != nil {
+				if *existingPublicIPAddress.ID == publicIpAddressId {
+					return tf.ImportAsExistsError("azurerm_nat_gateway_public_ip_association", *natGateway.ID)
+				}
+
+				publicIpAddresses = append(publicIpAddresses, existingPublicIPAddress)
+			}
+		}
+	}
+
+	publicIpAddresses = append(publicIpAddresses, network.SubResource{
+		ID: utils.String(publicIpAddressId),
+	})
+	natGateway.PublicIPAddresses = &publicIpAddresses
+
+	future, err := client.CreateOrUpdate(ctx, parsedNatGatewayId.ResourceGroup, parsedNatGatewayId.Name, natGateway)
+	if err != nil {
+		return fmt.Errorf("failed to update Public IP Association for Nat Gateway %q (Resource Group %q): %+v", parsedNatGatewayId.Name, parsedNatGatewayId.ResourceGroup, err)
+	}
+
+	if err = future.WaitForCompletionRef(ctx, client.Client); err != nil {
+		return fmt.Errorf("failed to wait for completion of Public IP Association for Nat Gateway %q (Resource Group %q): %+v", parsedNatGatewayId.Name, parsedNatGatewayId.ResourceGroup, err)
+	}
+
+	resp, err := client.Get(ctx, parsedNatGatewayId.ResourceGroup, parsedNatGatewayId.Name, "")
+	if err != nil {
+		return fmt.Errorf("failed to retrieve Nat Gateway %q (Resource Group %q): %+v", parsedNatGatewayId.Name, parsedNatGatewayId.ResourceGroup, err)
+	}
+	d.SetId(*resp.ID)
+
+	return resourceArmNatGatewayPublicIpAssociationRead(d, meta)
+}
+
+func resourceArmNatGatewayPublicIpAssociationRead(d *schema.ResourceData, meta interface{}) error {
+	client := meta.(*clients.Client).Network.NatGatewayClient
+	ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
+	defer cancel()
+
+	id, err := parse.NatGatewayID(d.Id())
+	if err != nil {
+		return err
+	}
+
+	natGateway, err := client.Get(ctx, id.ResourceGroup, id.Name, "")
+	if err != nil {
+		if utils.ResponseWasNotFound(natGateway.Response) {
+			log.Printf("[DEBUG] Nat Gateway %q (Resource Group %q) could not be found - removing from state!", id.Name, id.ResourceGroup)
+			d.SetId("")
+			return nil
+		}
+		return fmt.Errorf("failed to retrieve Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err)
+	}
+
+	if natGateway.PublicIPAddresses == nil {
+		log.Printf("[DEBUG] Nat Gateway %q (Resource Group %q) doesn't have a Public IP - removing from state!", id.Name, id.ResourceGroup)
+		d.SetId("")
+		return nil
+	}
+
+	d.Set("nat_gateway_id", natGateway.ID)
+
+	return nil
+}
+
+func resourceArmNatGatewayPublicIpAssociationDelete(d *schema.ResourceData, meta interface{}) error {
+	client := meta.(*clients.Client).Network.NatGatewayClient
+	ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d)
+	defer cancel()
+
+	id, err := parse.NatGatewayID(d.Id())
+	if err != nil {
+		return err
+	}
+
+	publicIpAddressId := d.Get("public_ip_address_id").(string)
+
+	locks.ByName(id.Name, natGatewayResourceName)
+	defer locks.UnlockByName(id.Name, natGatewayResourceName)
+
+	natGateway, err := client.Get(ctx, id.ResourceGroup, id.Name, "")
+	if err != nil {
+		if utils.ResponseWasNotFound(natGateway.Response) {
+			return fmt.Errorf("Nat Gateway %q (Resource Group %q) was not found.", id.Name, id.ResourceGroup)
+		}
+
+		return fmt.Errorf("failed to retrieve Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err)
+	}
+
+	publicIpAddresses := make([]network.SubResource, 0)
+	if publicIPAddresses := natGateway.PublicIPAddresses; publicIPAddresses != nil {
+		for _, publicIPAddress := range *publicIPAddresses {
+			if publicIPAddress.ID == nil {
+				continue
+			}
+
+			if *publicIPAddress.ID != publicIpAddressId {
+				publicIpAddresses = append(publicIpAddresses, publicIPAddress)
+			}
+		}
+	}
+	natGateway.PublicIPAddresses = &publicIpAddresses
+
+	future, err := client.CreateOrUpdate(ctx, id.ResourceGroup, id.Name, natGateway)
+	if err != nil {
+		return fmt.Errorf("failed to remove Public Ip Association for Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err)
+	}
+
+	if err = future.WaitForCompletionRef(ctx, client.Client); err != nil {
+		return fmt.Errorf("failed to wait for removal of Public Ip Association for Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err)
+	}
+
+	return nil
+}
diff --git a/azurerm/internal/services/network/nat_gateway_resource.go b/azurerm/internal/services/network/nat_gateway_resource.go
index 46b7f7a0dd27b..cc9783747c88e 100644
--- a/azurerm/internal/services/network/nat_gateway_resource.go
+++ b/azurerm/internal/services/network/nat_gateway_resource.go
@@ -60,10 +60,12 @@ func resourceArmNatGateway() *schema.Resource {
 			"public_ip_address_ids": {
 				Type:     schema.TypeSet,
 				Optional: true,
+				Computed: true,
 				Elem: &schema.Schema{
 					Type:         schema.TypeString,
 					ValidateFunc: azure.ValidateResourceID,
 				},
+				Deprecated: "Deprecated in favor of `azurerm_nat_gateway_public_ip_association`. The dependency relation between `azurerm_nat_gateway` and `azurerm_public_ip` isn't detected by implicit dependency. So `azurerm_nat_gateway_public_ip_association` is added to resolve this issue.",
 			},
 
 			"public_ip_prefix_ids": {
diff --git a/azurerm/internal/services/network/parse/nat_gateway.go b/azurerm/internal/services/network/parse/nat_gateway.go
new file mode 100644
index 0000000000000..c730d001f34d4
--- /dev/null
+++ b/azurerm/internal/services/network/parse/nat_gateway.go
@@ -0,0 +1,31 @@
+package parse
+
+import (
+	"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure"
+)
+
+type NatGatewayId struct {
+	Name          string
+	ResourceGroup string
+}
+
+func NatGatewayID(input string) (*NatGatewayId, error) {
+	id, err := azure.ParseAzureResourceID(input)
+	if err != nil {
+		return nil, err
+	}
+
+	natGateway := NatGatewayId{
+		ResourceGroup: id.ResourceGroup,
+	}
+
+	if natGateway.Name, err = id.PopSegment("natGateways"); err != nil {
+		return nil, err
+	}
+
+	if err := id.ValidateNoEmptySegments(input); err != nil {
+		return nil, err
+	}
+
+	return &natGateway, nil
+}
diff --git a/azurerm/internal/services/network/parse/nat_gateway_test.go b/azurerm/internal/services/network/parse/nat_gateway_test.go
new file mode 100644
index 0000000000000..d93cb021cf27f
--- /dev/null
+++ b/azurerm/internal/services/network/parse/nat_gateway_test.go
@@ -0,0 +1,70 @@
+package parse
+
+import (
+	"testing"
+)
+
+func TestNatGatewayID(t *testing.T) {
+	testData := []struct {
+		Name   string
+		Input  string
+		Error  bool
+		Expect *NatGatewayId
+	}{
+		{
+			Name:  "Empty",
+			Input: "",
+			Error: true,
+		},
+		{
+			Name:  "No Resource Groups Segment",
+			Input: "/subscriptions/00000000-0000-0000-0000-000000000000",
+			Error: true,
+		},
+		{
+			Name:  "No Resource Groups Value",
+			Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups",
+			Error: true,
+		},
+		{
+			Name:  "Resource Group ID",
+			Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1",
+			Error: true,
+		},
+		{
+			Name:  "Missing Nat Gateway Value",
+			Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Network/natGateways",
+			Error: true,
+		},
+		{
+			Name:  "Nat Gateway ID",
+			Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Network/natGateways/gateway1",
+			Error: false,
+			Expect: &NatGatewayId{
+				Name:          "gateway1",
+				ResourceGroup: "group1",
+			},
+		},
+	}
+
+	for _, v := range testData {
+		t.Logf("[DEBUG] Testing %q", v.Name)
+
+		actual, err := NatGatewayID(v.Input)
+		if err != nil {
+			if v.Error {
+				continue
+			}
+
+			t.Fatalf("Expected a value but got an error: %s", err)
+		}
+
+		if actual.Name != v.Expect.Name {
+			t.Fatalf("Expected %q but got %q for Name", v.Expect.Name, actual.Name)
+		}
+
+		if actual.ResourceGroup != v.Expect.ResourceGroup {
+			t.Fatalf("Expected %q but got %q for Resource Group", v.Expect.ResourceGroup, actual.ResourceGroup)
+		}
+	}
+}
diff --git a/azurerm/internal/services/network/registration.go b/azurerm/internal/services/network/registration.go
index 1bd4e2ba5bc4d..c6bdf4c1e2258 100644
--- a/azurerm/internal/services/network/registration.go
+++ b/azurerm/internal/services/network/registration.go
@@ -86,6 +86,7 @@ func (r Registration) SupportedResources() map[string]*schema.Resource {
 		"azurerm_private_endpoint":                                                       resourceArmPrivateEndpoint(),
 		"azurerm_private_link_service":                                                   resourceArmPrivateLinkService(),
 		"azurerm_public_ip":                                                              resourceArmPublicIp(),
+		"azurerm_nat_gateway_public_ip_association":                                      resourceArmNatGatewayPublicIpAssociation(),
 		"azurerm_public_ip_prefix":                                                       resourceArmPublicIpPrefix(),
 		"azurerm_network_security_group":                                                 resourceArmNetworkSecurityGroup(),
 		"azurerm_network_security_rule":                                                  resourceArmNetworkSecurityRule(),
diff --git a/azurerm/internal/services/network/tests/nat_gateway_public_ip_association_resource_test.go b/azurerm/internal/services/network/tests/nat_gateway_public_ip_association_resource_test.go
new file mode 100644
index 0000000000000..39aa699e72ce7
--- /dev/null
+++ b/azurerm/internal/services/network/tests/nat_gateway_public_ip_association_resource_test.go
@@ -0,0 +1,293 @@
+package tests
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2020-03-01/network"
+	"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/terraform"
+	"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance"
+	"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients"
+	"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/parse"
+)
+
+func TestAccAzureRMNatGatewayPublicIpAssociation_basic(t *testing.T) {
+	data := acceptance.BuildTestData(t, "azurerm_nat_gateway_public_ip_association", "test")
+	resource.ParallelTest(t, resource.TestCase{
+		PreCheck:  func() { acceptance.PreCheck(t) },
+		Providers: acceptance.SupportedProviders,
+		// intentional as this is a Virtual Resource
+		CheckDestroy: testCheckAzureRMNatGatewayDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccAzureRMNatGatewayPublicIpAssociation_basic(data),
+				Check: resource.ComposeTestCheckFunc(
+					testCheckAzureRMNatGatewayPublicIpAssociationExists(data.ResourceName),
+				),
+			},
+			// `public_ip_address_id` cannot be retrieved in read function while importing.
+			data.ImportStep("public_ip_address_id"),
+		},
+	})
+}
+
+func TestAccAzureRMNatGatewayPublicIpAssociation_requiresImport(t *testing.T) {
+	data := acceptance.BuildTestData(t, "azurerm_nat_gateway_public_ip_association", "test")
+	resource.ParallelTest(t, resource.TestCase{
+		PreCheck:  func() { acceptance.PreCheck(t) },
+		Providers: acceptance.SupportedProviders,
+		// intentional as this is a Virtual Resource
+		CheckDestroy: testCheckAzureRMNatGatewayDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccAzureRMNatGatewayPublicIpAssociation_basic(data),
+				Check: resource.ComposeTestCheckFunc(
+					testCheckAzureRMNatGatewayPublicIpAssociationExists(data.ResourceName),
+				),
+			},
+			data.RequiresImportErrorStep(testAccAzureRMNatGatewayPublicIpAssociation_requiresImport),
+		},
+	})
+}
+
+func TestAccAzureRMNatGatewayPublicIpAssociation_complete(t *testing.T) {
+	data := acceptance.BuildTestData(t, "azurerm_nat_gateway_public_ip_association", "test")
+	resource.ParallelTest(t, resource.TestCase{
+		PreCheck:  func() { acceptance.PreCheck(t) },
+		Providers: acceptance.SupportedProviders,
+		// intentional as this is a Virtual Resource
+		CheckDestroy: testCheckAzureRMNatGatewayDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccAzureRMNatGatewayPublicIpAssociation_complete(data),
+				Check: resource.ComposeTestCheckFunc(
+					testCheckAzureRMNatGatewayPublicIpAssociationExists(data.ResourceName),
+				),
+			},
+			data.ImportStep("public_ip_address_id"),
+		},
+	})
+}
+
+func TestAccAzureRMNatGatewayPublicIpAssociation_update(t *testing.T) {
+	data := acceptance.BuildTestData(t, "azurerm_nat_gateway_public_ip_association", "test")
+	resource.ParallelTest(t, resource.TestCase{
+		PreCheck:  func() { acceptance.PreCheck(t) },
+		Providers: acceptance.SupportedProviders,
+		// intentional as this is a Virtual Resource
+		CheckDestroy: testCheckAzureRMNatGatewayDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccAzureRMNatGatewayPublicIpAssociation_basic(data),
+				Check: resource.ComposeTestCheckFunc(
+					testCheckAzureRMNatGatewayPublicIpAssociationExists(data.ResourceName),
+				),
+			},
+			data.ImportStep("public_ip_address_id"),
+			{
+				Config: testAccAzureRMNatGatewayPublicIpAssociation_update(data),
+				Check: resource.ComposeTestCheckFunc(
+					testCheckAzureRMNatGatewayPublicIpAssociationExists(data.ResourceName),
+				),
+			},
+			data.ImportStep("public_ip_address_id"),
+		},
+	})
+}
+
+func TestAccAzureRMNatGatewayPublicIpAssociation_deleted(t *testing.T) {
+	data := acceptance.BuildTestData(t, "azurerm_nat_gateway_public_ip_association", "test")
+
+	resource.ParallelTest(t, resource.TestCase{
+		PreCheck:  func() { acceptance.PreCheck(t) },
+		Providers: acceptance.SupportedProviders,
+		// intentional as this is a Virtual Resource
+		CheckDestroy: testCheckAzureRMNatGatewayDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccAzureRMNatGatewayPublicIpAssociation_basic(data),
+				Check: resource.ComposeTestCheckFunc(
+					testCheckAzureRMNatGatewayPublicIpAssociationExists(data.ResourceName),
+					testCheckAzureRMNatGatewayPublicIpAssociationDisappears(data.ResourceName),
+				),
+				ExpectNonEmptyPlan: true,
+			},
+		},
+	})
+}
+
+func testAccAzureRMNatGatewayPublicIpAssociation_requiresImport(data acceptance.TestData) string {
+	template := testAccAzureRMNatGatewayPublicIpAssociation_basic(data)
+	return fmt.Sprintf(`
+%s
+
+resource "azurerm_nat_gateway_public_ip_association" "import" {
+  nat_gateway_id       = azurerm_nat_gateway_public_ip_association.test.nat_gateway_id
+  public_ip_address_id = azurerm_nat_gateway_public_ip_association.test.public_ip_address_id
+}
+`, template)
+}
+
+func testCheckAzureRMNatGatewayPublicIpAssociationExists(resourceName string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		client := acceptance.AzureProvider.Meta().(*clients.Client).Network.NatGatewayClient
+		ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext
+
+		// Ensure we have enough information in state to look up in API
+		rs, ok := s.RootModule().Resources[resourceName]
+		if !ok {
+			return fmt.Errorf("Not found: %s", resourceName)
+		}
+
+		id, err := parse.NatGatewayID(rs.Primary.ID)
+		if err != nil {
+			return err
+		}
+		publicIpAddressId := rs.Primary.Attributes["public_ip_address_id"]
+
+		resp, err := client.Get(ctx, id.ResourceGroup, id.Name, "")
+		if err != nil {
+			return fmt.Errorf("failed to retrieve Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err)
+		}
+
+		if publicIpAddresses := resp.PublicIPAddresses; publicIpAddresses != nil {
+			for _, publicIpAddress := range *publicIpAddresses {
+				if *publicIpAddress.ID == publicIpAddressId {
+					return nil
+				}
+			}
+		}
+
+		return fmt.Errorf("Association between Nat Gateway %q and Public Ip %q was not found.", id.Name, publicIpAddressId)
+	}
+}
+
+func testCheckAzureRMNatGatewayPublicIpAssociationDisappears(resourceName string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		client := acceptance.AzureProvider.Meta().(*clients.Client).Network.NatGatewayClient
+		ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext
+
+		// Ensure we have enough information in state to look up in API
+		rs, ok := s.RootModule().Resources[resourceName]
+		if !ok {
+			return fmt.Errorf("Not found: %s", resourceName)
+		}
+
+		id, err := parse.NatGatewayID(rs.Primary.ID)
+		if err != nil {
+			return err
+		}
+		publicIpAddressId := rs.Primary.Attributes["public_ip_address_id"]
+
+		resp, err := client.Get(ctx, id.ResourceGroup, id.Name, "")
+		if err != nil {
+			return fmt.Errorf("failed to retrieve Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err)
+		}
+
+		updatedAddresses := make([]network.SubResource, 0)
+		if publicIpAddresses := resp.PublicIPAddresses; publicIpAddresses != nil {
+			for _, publicIpAddress := range *publicIpAddresses {
+				if *publicIpAddress.ID != publicIpAddressId {
+					updatedAddresses = append(updatedAddresses, publicIpAddress)
+				}
+			}
+		}
+		resp.PublicIPAddresses = &updatedAddresses
+
+		future, err := client.CreateOrUpdate(ctx, id.ResourceGroup, id.Name, resp)
+		if err != nil {
+			return fmt.Errorf("failed to remove Nat Gateway Public Ip Association for Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err)
+		}
+
+		if err = future.WaitForCompletionRef(ctx, client.Client); err != nil {
+			return fmt.Errorf("failed to wait for removal of Nat Gateway Public Ip Association for Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err)
+		}
+
+		return nil
+	}
+}
+
+func testAccAzureRMNatGatewayPublicIpAssociation_basic(data acceptance.TestData) string {
+	template := testAccAzureRMNatGatewayPublicIpAssociation_template(data)
+	return fmt.Sprintf(`
+%s
+
+resource "azurerm_nat_gateway_public_ip_association" "test" {
+  nat_gateway_id       = azurerm_nat_gateway.test.id
+  public_ip_address_id = azurerm_public_ip.test.id
+}
+`, template)
+}
+
+func testAccAzureRMNatGatewayPublicIpAssociation_complete(data acceptance.TestData) string {
+	template := testAccAzureRMNatGatewayPublicIpAssociation_template(data)
+	return fmt.Sprintf(`
+%s
+
+resource "azurerm_nat_gateway_public_ip_association" "test" {
+  nat_gateway_id       = azurerm_nat_gateway.test.id
+  public_ip_address_id = azurerm_public_ip.test.id
+}
+
+resource "azurerm_public_ip" "test2" {
+  name                = "acctest-PIP2-%d"
+  location            = azurerm_resource_group.test.location
+  resource_group_name = azurerm_resource_group.test.name
+  allocation_method   = "Static"
+  sku                 = "Standard"
+}
+
+resource "azurerm_nat_gateway_public_ip_association" "test2" {
+  nat_gateway_id       = azurerm_nat_gateway.test.id
+  public_ip_address_id = azurerm_public_ip.test2.id
+}
+`, template, data.RandomInteger)
+}
+
+func testAccAzureRMNatGatewayPublicIpAssociation_update(data acceptance.TestData) string {
+	template := testAccAzureRMNatGatewayPublicIpAssociation_template(data)
+	return fmt.Sprintf(`
+%s
+
+resource "azurerm_nat_gateway" "test2" {
+  name                = "acctest-NatGateway2-%d"
+  location            = azurerm_resource_group.test.location
+  resource_group_name = azurerm_resource_group.test.name
+  sku_name            = "Standard"
+}
+
+resource "azurerm_nat_gateway_public_ip_association" "test" {
+  nat_gateway_id       = azurerm_nat_gateway.test2.id
+  public_ip_address_id = azurerm_public_ip.test.id
+}
+`, template, data.RandomInteger)
+}
+
+func testAccAzureRMNatGatewayPublicIpAssociation_template(data acceptance.TestData) string {
+	return fmt.Sprintf(`
+provider "azurerm" {
+  features {}
+}
+
+resource "azurerm_resource_group" "test" {
+  name     = "acctestRG-ngpi-%d"
+  location = "%s"
+}
+
+resource "azurerm_public_ip" "test" {
+  name                = "acctest-PIP-%d"
+  location            = azurerm_resource_group.test.location
+  resource_group_name = azurerm_resource_group.test.name
+  allocation_method   = "Static"
+  sku                 = "Standard"
+}
+
+resource "azurerm_nat_gateway" "test" {
+  name                = "acctest-NatGateway-%d"
+  location            = azurerm_resource_group.test.location
+  resource_group_name = azurerm_resource_group.test.name
+  sku_name            = "Standard"
+}
+`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger)
+}
diff --git a/azurerm/internal/services/network/validate/nat_gateway.go b/azurerm/internal/services/network/validate/nat_gateway.go
new file mode 100644
index 0000000000000..7ec9cdd42e6f4
--- /dev/null
+++ b/azurerm/internal/services/network/validate/nat_gateway.go
@@ -0,0 +1,22 @@
+package validate
+
+import (
+	"fmt"
+
+	"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/parse"
+)
+
+func NatGatewayID(i interface{}, k string) (warnings []string, errors []error) {
+	v, ok := i.(string)
+	if !ok {
+		errors = append(errors, fmt.Errorf("expected type of %q to be string", k))
+		return
+	}
+
+	if _, err := parse.NatGatewayID(v); err != nil {
+		errors = append(errors, fmt.Errorf("Can not parse %q as a resource id: %v", k, err))
+		return
+	}
+
+	return warnings, errors
+}
diff --git a/website/azurerm.erb b/website/azurerm.erb
index 25d10b1ed772d..972c812fe1e34 100644
--- a/website/azurerm.erb
+++ b/website/azurerm.erb
@@ -2016,6 +2016,10 @@
                   <a href="/docs/providers/azurerm/r/nat_gateway.html">azurerm_nat_gateway</a>
                 </li>
 
+                <li>
+                  <a href="/docs/providers/azurerm/r/nat_gateway_public_ip_association.html">azurerm_nat_gateway_public_ip_association</a>
+                </li>
+
                 <li>
                   <a href="/docs/providers/azurerm/r/network_interface.html">azurerm_network_interface</a>
                 </li>
diff --git a/website/docs/d/nat_gateway.html.markdown b/website/docs/d/nat_gateway.html.markdown
index 6fa9f69d3b7a9..2f43f56a643f8 100644
--- a/website/docs/d/nat_gateway.html.markdown
+++ b/website/docs/d/nat_gateway.html.markdown
@@ -38,6 +38,8 @@ The following attributes are exported:
 
 * `zones` - A list of Availability Zones which the NAT Gateway exists in.
 
+~> **NOTE:** The field `public_ip_address_ids` has been deprecated in favor of `azurerm_nat_gateway_public_ip_association`.
+
 ## Timeouts
 
 The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions:
diff --git a/website/docs/r/nat_gateway.html.markdown b/website/docs/r/nat_gateway.html.markdown
index d114170ea0b5e..c72b3f88e7c19 100644
--- a/website/docs/r/nat_gateway.html.markdown
+++ b/website/docs/r/nat_gateway.html.markdown
@@ -58,7 +58,7 @@ The following arguments are supported:
 
 * `idle_timeout_in_minutes` - (Optional) The idle timeout which should be used in minutes. Defaults to `4`.
 
-* `public_ip_address_ids` - (Optional) A list of Public IP Address ID's which should be associated with the NAT Gateway resource.
+* `public_ip_address_ids` - (Optional / **Deprecated in favour of `azurerm_nat_gateway_public_ip_association`**) A list of Public IP Address ID's which should be associated with the NAT Gateway resource.
 
 * `public_ip_prefix_ids` - (Optional) A list of Public IP Prefix ID's which should be associated with the NAT Gateway resource.
 
diff --git a/website/docs/r/nat_gateway_public_ip_association.html.markdown b/website/docs/r/nat_gateway_public_ip_association.html.markdown
new file mode 100644
index 0000000000000..2385233513713
--- /dev/null
+++ b/website/docs/r/nat_gateway_public_ip_association.html.markdown
@@ -0,0 +1,75 @@
+---
+subcategory: "Network"
+layout: "azurerm"
+page_title: "Azure Resource Manager: azurerm_nat_gateway_public_ip_association"
+description: |-
+  Manages the association between a Nat Gateway and a Public IP.
+
+---
+
+# azurerm_nat_gateway_public_ip_association
+
+Manages the association between a Nat Gateway and a Public IP.
+
+## Example Usage
+
+```hcl
+provider "azurerm" {
+  features {}
+}
+
+resource "azurerm_resource_group" "example" {
+  name     = "example-resources"
+  location = "West Europe"
+}
+
+resource "azurerm_public_ip" "example" {
+  name                = "example-PIP"
+  location            = azurerm_resource_group.example.location
+  resource_group_name = azurerm_resource_group.example.name
+  allocation_method   = "Static"
+  sku                 = "Standard"
+}
+
+resource "azurerm_nat_gateway" "example" {
+  name                = "example-NatGateway"
+  location            = azurerm_resource_group.example.location
+  resource_group_name = azurerm_resource_group.example.name
+  sku_name            = "Standard"
+}
+
+resource "azurerm_nat_gateway_public_ip_association" "example" {
+  nat_gateway_id       = azurerm_nat_gateway.example.id
+  public_ip_address_id = azurerm_public_ip.example.id
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `nat_gateway_id` - (Required) The ID of the Nat Gateway. Changing this forces a new resource to be created.
+
+* `public_ip_address_id` - (Required) The ID of the Public IP which this Nat Gateway which should be connected to. Changing this forces a new resource to be created.
+
+## Attributes Reference
+
+The following attributes are exported:
+
+* `id` - The (Terraform specific) ID of the Association between the Nat Gateway and the Public IP.
+
+## Timeouts
+
+The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions:
+
+* `create` - (Defaults to 30 minutes) Used when creating the association between the Nat Gateway and the Public IP.
+* `read` - (Defaults to 5 minutes) Used when retrieving the association between the Nat Gateway and the Public IP.
+* `delete` - (Defaults to 30 minutes) Used when deleting the association between the Nat Gateway and the Public IP.
+
+## Import
+
+Associations between Nat Gateway and Public IP Addresses can be imported using the `resource id`, e.g.
+
+```shell
+terraform import azurerm_nat_gateway_public_ip_association.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Network/natGateways/gateway1
+```