Terraform – Simplified Azure Container Instances (ACI) Deployment
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 #
- 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 #
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 #
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 #
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 #
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 #
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.