Skip to main content
Jorge Bernhardt Jorge Bernhardt
  1. Posts/

Terraform – Simplified Azure Container Instances (ACI) Deployment

·1712 words·9 mins· 100 views · 5 likes ·
Azure CLI Azure Cloud Shell ACI Microsoft

Hi everyone! Today, in this blog post, we will explore how to deploy Azure Container Instances (ACI) using Terraform. As we have discussed several times on this blog, ACI is a vital Azure service for running Docker containers without the need to manage underlying virtual machines. It offers a fast, straightforward, and scalable solution for container-based applications.

In this guide, I will detail the steps necessary to configure and deploy ACI efficiently and effectively with Terraform, maximizing the benefits of automation and reproducibility that infrastructure as code provides. This guide covers all the basic and essential aspects, from setting up a resource group and virtual network to deploying container instances. In a future post, we will delve deeper into incorporating additional features and more advanced configurations. However, for a simple and functional deployment, this guide covers everything you need.

Prerequisites>

Prerequisites #

  • You need Terraform CLI on your local machine, if you’re new to using Terraform to deploy Microsoft Azure resources, then I recommend you check out this link.
  • A text editor or IDE of your choice (Visual Studio Code with terraform extension is my recommendation)
Declare Azure Provider in Terraform>

Declare Azure Provider in Terraform #

The provider.tf file in Terraform is used to specify and configure the providers used in your Terraform configuration. A provider is a service or platform where the resources will be managed. This could be a cloud provider like Microsoft Azure, AWS, Google Cloud, etc.

This file is important because it tells Terraform which provider’s API to use when creating, updating, and deleting resources. Without it, Terraform wouldn’t know where to manage your resources.

provider "azurerm" {
  features {}
  subscription_id = var.subscription_id
}

Important: Terraform now requires an explicit subscription ID in the provider configuration for clearer, more predictable, and secure infrastructure management in complex cloud environments.

Deploy Azure Resources Using Terraform>

Deploy Azure Resources Using Terraform #

In the case of Azure Container Instances (ACI) deployment, the main.tf file contains the following key components:

  • azurerm_resource_group: This block sets up the Azure Resource Group where all other resources will be deployed.
  • azurerm_virtual_network: This block defines the Azure Virtual Network (VNet), which provides an isolated network environment for your resources.
  • azurerm_subnet: This block defines the subnets within the Virtual Network, allowing segmentation of the network. It uses local.flattened_subnets to iterate over the subnet configurations.
  • azurerm_container_group: This block configures the Azure Container Instances, specifying the container settings, including image, CPU, memory, and network configurations. It supports multiple containers within a single container group, managed using the for_each construct from var.aci_instances.
  • image_registry_credential : This block is necessary when pulling container images from the Docker Hub repository. It addresses rate limits when pulling public images from Docker Hub, where authentication increases pull limits.
resource "azurerm_resource_group" "rg" {
  name     = var.resource_group_name
  location = var.resource_group_location
  tags     = var.tags
}

resource "azurerm_virtual_network" "vnet" {
  for_each            = var.networks
  name                = each.key
  address_space       = [each.value.address_space]
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  tags                = var.tags
}

locals {
  flattened_subnets = flatten([
    for vnet_key, vnet_value in var.networks : [
      for subnet in vnet_value.subnets : {
        vnet_name      = vnet_key
        subnet_name    = subnet.name
        address_prefix = subnet.address_prefix
      }
    ]
  ])
}

resource "azurerm_subnet" "subnet" {
  for_each = { for subnet in local.flattened_subnets : "${subnet.vnet_name}-${subnet.subnet_name}" => subnet }

  name                 = each.value.subnet_name
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = each.value.vnet_name
  address_prefixes     = [each.value.address_prefix]

  delegation {
    name = "container-instance-delegation"
    service_delegation {
      name    = "Microsoft.ContainerInstance/containerGroups"
      actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
    }
  }
  depends_on = [azurerm_virtual_network.vnet]
}

resource "azurerm_container_group" "aci" {
  for_each            = { for idx, aci in var.aci_instances : idx => aci }
  name                = each.value.name
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  os_type             = "Linux"
  restart_policy      = each.value.restart_policy

  ip_address_type = each.value.is_public ? "Public" : "Private"
  subnet_ids      = each.value.is_public ? null : [azurerm_subnet.subnet["${each.value.vnet_name}-${each.value.subnet_name}"].id]
  dns_name_label  = each.value.is_public ? each.value.dns_label : null

  dynamic "container" {
    for_each = each.value.containers
    content {
      name   = container.value.name
      image  = container.value.image
      cpu    = container.value.cpu
      memory = container.value.memory

      dynamic "ports" {
        for_each = container.value.ports
        content {
          port     = ports.value.port
          protocol = ports.value.protocol
        }
      }
    }
  }

  image_registry_credential {
    server   = "index.docker.io"
    username = var.docker_username
    password = var.docker_password
  }

  tags       = var.tags
  depends_on = [azurerm_subnet.subnet]
}
Declaration of input variables>

Declaration of input variables #

The variables.tf file in Terraform defines the variables used in the main.tf file. These variables allow for more flexibility and reusability in the code.

In this example, the variables defined in the variables.tf include:

  • subscription_id: This variable is used to dynamically pass the Azure subscription ID to Terraform, improving flexibility and security.
  • resource_group_name: This block declares a variable named resource_group_name, which is a string. It is used to specify the name of the Azure Resource Group where all resources will be deployed.
  • resource_group_location: This block declares a variable named resource_group_location, which is a string. It specifies the location/region where the Azure Resource Group will be created.
  • tags: This block declares a variable named tags, which is a map of strings. It is used to assign tags to the Azure resources being created. For example, you can use a key-value pair such as - Terraform = true to indicate that the resource was deployed with Terraform.
  • networks: This variable is a map of objects, each representing the configuration for a virtual network. It includes properties such as the address space and a list of subnets with their respective names and address prefixes.
  • aci_instances: This variable is a map of objects, each representing the configuration for an Azure Container Instance. It encapsulates several properties, from basic setup like name, image, CPU, and memory, to more advanced settings like port, protocol, public accessibility, restart policy, and DNS label. It supports multiple containers within each ACI instance.
  • Docker Hub credentials: These variables are optional, here we can define the access credentials to Docker Hub to avoid errors when accessing public images due to rate limiting.
variable "subscription_id" {
  type        = string
  description = "Azure Subscription ID"
}

variable "resource_group_name" {
  description = "The name of the resource group"
  type        = string
}

variable "resource_group_location" {
  description = "The location of the resource group"
  type        = string
}

variable "networks" {
  description = "Map of virtual networks and their subnets"
  type = map(object({
    address_space = string
    subnets = list(object({
      name           = string
      address_prefix = string
    }))
  }))
}

variable "aci_instances" {
  type = map(object({
    name           = string
    vnet_name      = string
    subnet_name    = string
    containers     = list(object({
      name    = string
      image   = string
      cpu     = number
      memory  = number
      ports   = list(object({
        port     = number
        protocol = string
      }))
    }))
    is_public      = bool
    restart_policy = string
    dns_label      = string
  }))

  validation {
    condition     = alltrue([for aci in var.aci_instances : alltrue([for container in aci.containers : alltrue([for port in container.ports : port.protocol == "TCP" || port.protocol == "UDP"])])])
    error_message = "Protocol must be either 'TCP' or 'UDP'."
  }

  validation {
    condition     = alltrue([for aci in var.aci_instances : aci.restart_policy == "Always" || aci.restart_policy == "OnFailure" || aci.restart_policy == "Never"])
    error_message = "Restart policy must be either 'Always', 'OnFailure', or 'Never'."
  }
}

variable "docker_username" {
  description = "Docker Hub username (optional)"
  type        = string
  default     = null
}

variable "docker_password" {
  description = "Docker Hub password (optional)"
  type        = string
  default     = null
  sensitive   = true
}

variable "tags" {
  description = "Common tags for all resources"
  type        = map(string)
  default = {
    Environment = "www.jorgebernhardt.com"
    Terraform   = "true"
  }
}
Declaration of output values>

Declaration of output values #

The output.tf file in Terraform extracts and displays information about the resources created or managed by your Terraform configuration. These outputs are defined using the output keyword and can be used to return information that can be useful for the user, for other Terraform configurations, or for programmatically using the information in scripts or other tools.

In this example, the output.tf file returns information about the resource group, virtual networks, subnets, and Azure Container Instances (ACI) that were created.

Once Terraform has finished applying your configuration, it will display the defined outputs.

output "resource_group_name" {
  description = "The name of the resource group"
  value       = azurerm_resource_group.rg.name
}

output "virtual_networks" {
  description = "List of virtual networks with their name and address space"
  value = [
    for vnet in azurerm_virtual_network.vnet : {
      name          = vnet.name
      address_space = vnet.address_space
    }
  ]
}

output "subnets" {
  description = "List of subnets with their name, address prefix, and associated virtual network name"
  value = [
    for subnet in azurerm_subnet.subnet : {
      name           = subnet.name
      address_prefix = subnet.address_prefixes
      vnet_name      = subnet.virtual_network_name
    }
  ]
}

output "aci_instances" {
  description = "Details of the deployed ACI instances"
  value = {
    for aci in azurerm_container_group.aci : aci.name => {
      name           = aci.name
      location       = aci.location
      restart_policy = aci.restart_policy
      resource_id    = aci.id
      ip_address     = aci.ip_address
      fqdn           = aci.fqdn
      containers = [
        for container in aci.container : {
          name   = container.name
          image  = container.image
          cpu    = container.cpu
          memory = container.memory
          ports  = [
            for port in container.ports : {
              port     = port.port
              protocol = port.protocol
            }
          ]
        }
      ]
    }
  }
}
Executing the Terraform Deployment>

Executing the Terraform Deployment #

Now that you’ve declared the resources correctly, it’s time to take the following steps to deploy them in your Azure environment.

  • Initialization: To begin, execute the terraform init command. This will initialize your working directory that holds the .tf files and download the provider specified in the provider.tf file, and configure the Terraform backend. If you want to know how, check this link.

  • Planning: Next, execute the terraform plan. This command creates an execution plan and shows Terraform’s actions to achieve the desired state defined in your .tf files. This gives you a chance to review the changes before applying them.

  • Apply: When you’re satisfied with the plan, execute the terraform apply command. This will implement the required modifications to attain the intended infrastructure state. Before making any changes, you will be asked to confirm your decision.

  • Inspection: After applying the changes, you can use terraform show command to see the current state of your infrastructure.

  • Destroy (optional): when a project is no longer needed or when resources have become outdated. You can use the terraform destroy command. This will remove all the resources that Terraform has created.

References and useful links #

Thank you for taking the time to read my post. I sincerely hope that you find it helpful.