Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support virtual networks for container groups #3716

Merged
127 changes: 124 additions & 3 deletions azurerm/resource_arm_container_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package azurerm

import (
"bytes"
"context"
"fmt"
"log"
"strings"
"time"

"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-12-01/network"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/suppress"

"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
Expand Down Expand Up @@ -47,9 +51,23 @@ func resourceArmContainerGroup() *schema.Resource {
DiffSuppressFunc: suppress.CaseDifference,
ValidateFunc: validation.StringInSlice([]string{
string(containerinstance.Public),
string(containerinstance.Private),
}, true),
},

"network_profile_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validate.NoEmptyStrings,
/* Container groups deployed to a virtual network don't currently support exposing containers directly to the internet with a public IP address or a fully qualified domain name.
* Name resolution for Azure resources in the virtual network via the internal Azure DNS is not supported
* You cannot use a managed identity in a container group deployed to a virtual network.
* https://docs.microsoft.com/en-us/azure/container-instances/container-instances-vnet#virtual-network-deployment-limitations
* https://docs.microsoft.com/en-us/azure/container-instances/container-instances-vnet#preview-limitations */
ConflictsWith: []string{"dns_name_label", "identity"},
},

"os_type": {
Type: schema.TypeString,
Required: true,
Expand Down Expand Up @@ -443,7 +461,6 @@ func resourceArmContainerGroupCreate(d *schema.ResourceData, meta interface{}) e
IPAddressType := d.Get("ip_address_type").(string)
tags := d.Get("tags").(map[string]interface{})
restartPolicy := d.Get("restart_policy").(string)

diagnosticsRaw := d.Get("diagnostics").([]interface{})
diagnostics := expandContainerGroupDiagnostics(diagnosticsRaw)

Expand Down Expand Up @@ -471,6 +488,17 @@ func resourceArmContainerGroupCreate(d *schema.ResourceData, meta interface{}) e
containerGroup.ContainerGroupProperties.IPAddress.DNSNameLabel = &dnsNameLabel
}

// https://docs.microsoft.com/en-us/azure/container-instances/container-instances-vnet#virtual-network-deployment-limitations
// https://docs.microsoft.com/en-us/azure/container-instances/container-instances-vnet#preview-limitations
if networkProfileID := d.Get("network_profile_id").(string); networkProfileID != "" {
if strings.ToLower(OSType) != "linux" {
return fmt.Errorf("Currently only Linux containers can be deployed to virtual networks")
}
containerGroup.ContainerGroupProperties.NetworkProfile = &containerinstance.ContainerGroupNetworkProfile{
ID: &networkProfileID,
}
}

future, err := client.CreateOrUpdate(ctx, resGroup, name, containerGroup)
if err != nil {
return fmt.Errorf("Error creating/updating container group %q (Resource Group %q): %+v", name, resGroup, err)
Expand Down Expand Up @@ -569,16 +597,109 @@ func resourceArmContainerGroupDelete(d *schema.ResourceData, meta interface{}) e
resourceGroup := id.ResourceGroup
name := id.Path["containerGroups"]

networkProfileId := ""
existing, err := client.Get(ctx, resourceGroup, name)
if err != nil {
if utils.ResponseWasNotFound(existing.Response) {
// already deleted
return nil
}

return fmt.Errorf("Error retrieving Container Group %q (Resource Group %q): %+v", name, resourceGroup, err)
}

if props := existing.ContainerGroupProperties; props != nil {
if profile := props.NetworkProfile; profile != nil {
if profile.ID != nil {
networkProfileId = *profile.ID
}
}
}

resp, err := client.Delete(ctx, resourceGroup, name)
if err != nil {
if !utils.ResponseWasNotFound(resp.Response) {
return fmt.Errorf("Error deleting Container Group %q (Resource Group %q): %+v", name, resourceGroup, err)
if utils.ResponseWasNotFound(resp.Response) {
return nil
}

return fmt.Errorf("Error deleting Container Group %q (Resource Group %q): %+v", name, resourceGroup, err)
}

if networkProfileId != "" {
parsedProfileId, err := parseAzureResourceID(networkProfileId)
if err != nil {
return err
}

networkProfileClient := meta.(*ArmClient).netProfileClient
networkProfileResourceGroup := parsedProfileId.ResourceGroup
networkProfileName := parsedProfileId.Path["networkProfiles"]

// TODO: remove when https://github.com/Azure/azure-sdk-for-go/issues/5082 has been fixed
log.Printf("[DEBUG] Waiting for Container Group %q (Resource Group %q) to be finish deleting", name, resourceGroup)
stateConf := &resource.StateChangeConf{
Pending: []string{"Attached"},
Target: []string{"Detached"},
Refresh: containerGroupEnsureDetachedFromNetworkProfileRefreshFunc(ctx, networkProfileClient, networkProfileResourceGroup, networkProfileName, resourceGroup, name),
Timeout: 10 * time.Minute,
MinTimeout: 15 * time.Second,
ContinuousTargetOccurence: 5,
}
if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf("Error waiting for Container Group %q (Resource Group %q) to finish deleting: %s", name, resourceGroup, err)
}
}

return nil
}

func containerGroupEnsureDetachedFromNetworkProfileRefreshFunc(ctx context.Context,
client network.ProfilesClient,
networkProfileResourceGroup, networkProfileName,
containerResourceGroupName, containerName string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
profile, err := client.Get(ctx, networkProfileResourceGroup, networkProfileName, "")
if err != nil {
return nil, "Error", fmt.Errorf("Error retrieving Network Profile %q (Resource Group %q): %s", networkProfileName, networkProfileResourceGroup, err)
}

exists := false
if props := profile.ProfilePropertiesFormat; props != nil {
if nics := props.ContainerNetworkInterfaces; nics != nil {
for _, nic := range *nics {
nicProps := nic.ContainerNetworkInterfacePropertiesFormat
if nicProps == nil || nicProps.Container == nil || nicProps.Container.ID == nil {
continue
}

parsedId, err := parseAzureResourceID(*nicProps.Container.ID)
if err != nil {
return nil, "", err
}

if parsedId.ResourceGroup != containerResourceGroupName {
continue
}

name := parsedId.Path["containerGroups"]
if name == "" || name != containerName {
continue
}

exists = true
break
}
}
}

if exists {
return nil, "Attached", nil
}

return profile, "Detached", nil
}
}

func expandContainerGroupContainers(d *schema.ResourceData) (*[]containerinstance.Container, *[]containerinstance.Port, *[]containerinstance.Volume) {
containersConfig := d.Get("container").([]interface{})
containers := make([]containerinstance.Container, 0)
Expand Down
102 changes: 98 additions & 4 deletions azurerm/resource_arm_container_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package azurerm

import (
"fmt"
"net/http"
"testing"

"github.com/hashicorp/terraform/helper/acctest"
Expand Down Expand Up @@ -355,6 +354,33 @@ func TestAccAzureRMContainerGroup_linuxComplete(t *testing.T) {
})
}

func TestAccAzureRMContainerGroup_virtualNetwork(t *testing.T) {
resourceName := "azurerm_container_group.test"
ri := tf.AccRandTimeInt()
config := testAccAzureRMContainerGroup_virtualNetwork(ri, testLocation())

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckAzureRMContainerGroupDestroy,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMContainerGroupExists(resourceName),
resource.TestCheckNoResourceAttr(resourceName, "dns_label_name"),
resource.TestCheckNoResourceAttr(resourceName, "identity"),
resource.TestCheckResourceAttr(resourceName, "container.#", "1"),
resource.TestCheckResourceAttr(resourceName, "os_type", "Linux"),
resource.TestCheckResourceAttr(resourceName, "container.0.port", "80"),
resource.TestCheckResourceAttr(resourceName, "ip_address_type", "Private"),
resource.TestCheckResourceAttrSet(resourceName, "network_profile_id"),
),
},
},
})
}

func TestAccAzureRMContainerGroup_windowsBasic(t *testing.T) {
resourceName := "azurerm_container_group.test"
ri := tf.AccRandTimeInt()
Expand Down Expand Up @@ -522,7 +548,7 @@ resource "azurerm_container_group" "test" {
type = "UserAssigned"
identity_ids = ["${azurerm_user_assigned_identity.test.id}"]
}

tags = {
environment = "Testing"
}
Expand Down Expand Up @@ -563,7 +589,7 @@ resource "azurerm_container_group" "test" {
type = "SystemAssigned, UserAssigned"
identity_ids = ["${azurerm_user_assigned_identity.test.id}"]
}

tags = {
environment = "Testing"
}
Expand Down Expand Up @@ -762,6 +788,74 @@ resource "azurerm_container_group" "test" {
`, ri, location, ri)
}

func testAccAzureRMContainerGroup_virtualNetwork(ri int, location string) string {
return fmt.Sprintf(`
resource "azurerm_resource_group" "test" {
name = "acctestRG-%d"
location = "%s"
}

resource "azurerm_virtual_network" "test" {
name = "testvnet"
location = "${azurerm_resource_group.test.location}"
resource_group_name = "${azurerm_resource_group.test.name}"
address_space = ["10.1.0.0/16"]
}

resource "azurerm_subnet" "test" {
name = "testsubnet"
resource_group_name = "${azurerm_resource_group.test.name}"
virtual_network_name = "${azurerm_virtual_network.test.name}"
address_prefix = "10.1.0.0/24"

delegation {
name = "delegation"

service_delegation {
name = "Microsoft.ContainerInstance/containerGroups"
actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
}
}
}

resource "azurerm_network_profile" "test" {
name = "testnetprofile"
location = "${azurerm_resource_group.test.location}"
resource_group_name = "${azurerm_resource_group.test.name}"

container_network_interface {
name = "testcnic"

ip_configuration {
name = "testipconfig"
subnet_id = "${azurerm_subnet.test.id}"
}
}
}

resource "azurerm_container_group" "test" {
name = "acctestcontainergroup-%d"
location = "${azurerm_resource_group.test.location}"
resource_group_name = "${azurerm_resource_group.test.name}"
ip_address_type = "Private"
network_profile_id = "${azurerm_network_profile.test.id}"
os_type = "Linux"

container {
name = "hw"
image = "microsoft/aci-helloworld:latest"
cpu = "0.5"
memory = "0.5"
port = 80
}

tags = {
environment = "Testing"
}
}
`, ri, location, ri)
}

func testAccAzureRMContainerGroup_windowsBasic(ri int, location string) string {
return fmt.Sprintf(`
resource "azurerm_resource_group" "test" {
Expand Down Expand Up @@ -1072,7 +1166,7 @@ func testCheckAzureRMContainerGroupDestroy(s *terraform.State) error {
resp, err := conn.Get(ctx, resourceGroup, name)

if err != nil {
if resp.StatusCode != http.StatusNotFound {
if !utils.ResponseWasNotFound(resp.Response) {
return fmt.Errorf("Container Group still exists:\n%#v", resp)
}

Expand Down
14 changes: 11 additions & 3 deletions website/docs/r/container_group.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,27 @@ The following arguments are supported:

* `identity` - (Optional) An `identity` block as defined below.

* `container` - (Required) The definition of a container that is part of the group as documented in the `container` block below. Changing this forces a new resource to be created.
~> **Note:** managed identities are not supported for containers in virtual networks.

~> **Note:** if `os_type` is set to `Windows` currently only a single `container` block is supported.
* `container` - (Required) The definition of a container that is part of the group as documented in the `container` block below. Changing this forces a new resource to be created.

* `os_type` - (Required) The OS for the container group. Allowed values are `Linux` and `Windows`. Changing this forces a new resource to be created.

~> **Note:** if `os_type` is set to `Windows` currently only a single `container` block is supported. Windows containers are not supported in virtual networks.

---

* `diagnostics` - (Optional) A `diagnostics` block as documented below.

* `dns_name_label` - (Optional) The DNS label/name for the container groups IP. Changing this forces a new resource to be created.

* `ip_address_type` - (Optional) Specifies the ip address type of the container. `Public` is the only acceptable value at this time. Changing this forces a new resource to be created.
~> **Note:** DNS label/name is not supported when deploying to virtual networks.

* `ip_address_type` - (Optional) Specifies the ip address type of the container. `Public` or `Private`. Changing this forces a new resource to be created. If set to `Private`, `network_profile_id` also needs to be set.

~> **Note:** `dns_name_label`, `identity` and `os_type` set to `windows` are not compatible with `Private` `ip_address_type`

* `network_profile_id` - (Optional) Network profile ID for deploying to virtual network.
juho9000 marked this conversation as resolved.
Show resolved Hide resolved

* `image_registry_credential` - (Optional) A `image_registry_credential` block as documented below. Changing this forces a new resource to be created.

Expand Down