Skip to content

Create Azure Container Registry (ACR) using terraform

Introduction

Azure Container Registry (ACR) is a managed Docker registry service provided by Microsoft Azure. It allows you to store and manage container images for your applications in a secure and private environment.

In this lab, I will guide you through the process of creating an Azure Container Registry using Terraform. Furthermore, I will demonstrate how to verify its successful deployment within the Azure portal and provide insights on how to utilize it effectively post-creation.

ACR provides a number of benefits, including:

  • Private repository: ACR provides a private Docker registry, which means that you can store your Docker images securely and privately, and only authorized users or services can access them.

  • High availability: ACR is built on Azure, so it benefits from Azure's global network and high availability features. This means that your container images are always available, and you can easily replicate them across regions for disaster recovery.

  • Integration with Azure services: ACR integrates seamlessly with other Azure services, such as Azure Kubernetes Service (AKS), Azure Web Apps, and Azure DevOps, making it easy to incorporate ACR into your existing workflows.

  • Security and compliance: ACR provides built-in security features, such as role-based access control, network security, and encryption at rest, to help you meet your security and compliance requirements.

  • Geo-replication: ACR allows you to replicate your container images across multiple regions for improved performance and disaster recovery.

To get started with ACR, we are going to use terraform to create a new Azure Container Registry. Once you have a registry, you can push your Docker images to it using the Docker CLI, Azure CLI, or other tools, and manage your images using the Azure portal or a variety of third-party tools.

Technical Scenario

As a Cloud Engineer, you have been asked to store and manage organization application development private container images and helm charts in secure way in cloud so that these are not directly accessible outside of the company network. also, make sure that azure Container Registry should provide organization users with direct control of their container content, with integrated authentication.

Objective

In this exercise we will accomplish & learn how to implement following:

  • Task-1: Create ACR resource group
  • Task-2: Configure variables for ACR
  • Task-3: Create ACR user assigned identity
  • Task-4: Create Azure Container Registry (ACR) using terraform
  • Task-5: Create Diagnostics Settings for ACR
  • Task-6: Lock ACR resource group
  • Task-7: Validate ACR resource
    • Task-7.1: Log in to registry
    • Task-7.2: Push image to registry
    • Task-7.3: Pull image from registry
    • Task-7.4: List container images
  • Task-8: Restrict Access Using Private Endpoint
    • Task-8.1: Configure the Private DNS Zone
    • Task-8.2: Create a Virtual Network Link Association
    • Task-8.3: Create a Private Endpoint Using Terraform
    • Task-8.4: Validate private link connection using nslookup or dig

Through these tasks, you will gain practical experience on Azure Container Registry.

Architecture diagram

Here is the reference architecture diagram of Azure container registry.

Alt text

Prerequisites

  • Download & Install Terraform
  • Download & Install Azure CLI
  • Azure subscription
  • Visual studio code
  • Azure DevOps Project & repo
  • Terraform Foundation
  • Log Analytics workspace - for configuring diagnostic settings.
  • Virtual Network with subnet - for configuring a private endpoint.
  • Basic knowledge of terraform and azure concepts.

Implementation details

Open the terraform project folder in Visual Studio code and creating new file named acr.tf for Azure container registry specific azure resources;

login to Azure

Verify that you are logged into the right Azure subscription before start anything in visual studio code

# Login to Azure
az login 

# Shows current Azure subscription
az account show

# Lists all available Azure subscriptions
az account list

# Sets Azure subscription to desired subscription using ID
az account set -s "anji.keesari"

Task-1: Define and declare ACR variables

This section covers list of variables used to create Azure container registry with detailed description and purpose of each variable with default values.

Variable Name Description Type Default Value
acr_name (Required) Specifies the name of the Container Registry. Changing this forces a new resource to be created. string acr1dev
acr_rg_name (Required) The name of the resource group in which to create the Container Registry. Changing this forces a new resource to be created. string acr1
acr_location Location in which to deploy the Container Registry string "East US"
acr_admin_enabled (Optional) Specifies whether the admin user is enabled. Defaults to false. bool false
acr_sku (Optional) The SKU name of the container registry. Possible values are Basic, Standard, and Premium. Defaults to Basic string "Basic"
acr_georeplication_locations (Optional) A list of Azure locations where the container registry should be geo-replicated. list(string) ["Central US", "East US"]
acr_log_analytics_retention_days Specifies the number of days of the retention policy number 7
acr_tags (Optional) Specifies the tags of the ACR map(any) {}
data_endpoint_enabled (Optional) Whether to enable dedicated data endpoints for this Container Registry? Defaults to false. This is only supported on resources with the Premium SKU. bool true

Variables Prefixed

Here is the list of new prefixes used in this lab

variables_prefix.tf
variable "acr_prefix" {
  type        = string
  default     = "acr"
  description = "Prefix of the Azure Container Registry (ACR) name that's combined with name of the ACR"
}

Declare Variables

Here is the list of new variables used in this lab

variables.tf
// ========================== Azure Container Registry (ACR) ==========================

variable "acr_name" {
  description = "(Required) Specifies the name of the Container Registry. Changing this forces a new resource to be created."
  type        = string
}

variable "acr_rg_name" {
  description = "(Required) The name of the resource group in which to create the Container Registry. Changing this forces a new resource to be created."
  type        = string
}

variable "acr_location" {
  description = "Location in which to deploy the Container Registry"
  type        = string
  default     = "East US"
}

variable "acr_admin_enabled" {
  description = "(Optional) Specifies whether the admin user is enabled. Defaults to false."
  type        = string
  default     = false
}

variable "acr_sku" {
  description = "(Optional) The SKU name of the container registry. Possible values are Basic, Standard and Premium. Defaults to Basic"
  type        = string
  default     = "Basic"

  validation {
    condition     = contains(["Basic", "Standard", "Premium"], var.acr_sku)
    error_message = "The container registry sku is invalid."
  }
}
variable "acr_georeplication_locations" {
  description = "(Optional) A list of Azure locations where the container registry should be geo-replicated."
  type        = list(string)
  default     = ["Central US", "East US"]
}

variable "acr_log_analytics_retention_days" {
  description = "Specifies the number of days of the retention policy"
  type        = number
  default     = 7
}
variable "acr_tags" {
  description = "(Optional) Specifies the tags of the ACR"
  type        = map(any)
  default     = {}
}
variable "data_endpoint_enabled" {
  description = "(Optional) Whether to enable dedicated data endpoints for this Container Registry? Defaults to false. This is only supported on resources with the Premium SKU."
  default     = true
  type        = bool
}
variable "pe_acr_subresource_names" {
  description = "(Optional) Specifies a subresource names which the Private Endpoint is able to connect to ACR."
  type        = list(string)
  default     = ["registry"]
}
Define variables

Here is the list of new variables used in this lab

dev-variables.tfvar - update this existing file for ACR values for development environment.

dev-variables.tfvar
# container registry
acr_rg_name                         = "acr"
acr_name                            = "acr1dev"
acr_sku                             = "Basic"
acr_admin_enabled                   = true
data_endpoint_enabled               = false

output variables

Here is the list of output variables used in this lab

output.tf
// ========================== Azure Container Registry (ACR) ==========================

output "acr_name" {
  description = "Specifies the name of the container registry."
  value       = azurerm_container_registry.acr.name
}

output "acr_id" {
  description = "Specifies the resource id of the container registry."
  value       = azurerm_container_registry.acr.id
}

output "acr_resource_group_name" {
  description = "Specifies the name of the resource group."
  value       = azurerm_container_registry.acr.resource_group_name
}

output "acr_login_server" {
  description = "Specifies the login server of the container registry."
  value       = azurerm_container_registry.acr.login_server
}

output "acr_login_server_url" {
  description = "Specifies the login server url of the container registry."
  value       = "https://${azurerm_container_registry.acr.login_server}"
}

output "acr_admin_username" {
  description = "Specifies the admin username of the container registry."
  value       = azurerm_container_registry.acr.admin_username
}

Task-2: Create a resource group for ACR

We will create separate resource group for ACR and related resources. add following terraform configuration in acr.tf file for creating ACR resource group.

In this task, we will create Azure resource group by using the terraform

acr.tf
# Create the resource group
resource "azurerm_resource_group" "rg_acr" {
  name     = lower("${var.rg_prefix}-${var.acr_rg_name}-${local.environment}")
  location = var.acr_location
  tags     = merge(local.default_tags)
  lifecycle {
    ignore_changes = [
      tags
    ]
  }
}
run terraform validate & format

terraform validate
terraform fmt

run terraform plan & apply

terraform plan -out=dev-plan -var-file="./environments/dev-variables.tfvars"
terraform apply dev-plan

Alt text

Task-3: Create ACR user assigned identity

Use the following terraform configuration for creating user assigned identity which is going be used in ACR

User assigned managed identities enable Azure resources to authenticate to cloud services (e.g. Azure Key Vault) without storing credentials in code.

User Assigned Identity in Azure Container Registry provides improved security, simplified management, better integration with Azure services, RBAC, and better compliance, making it a beneficial feature for organizations that use ACR.

acr.tf
# Create ACR user assigned identity
resource "azurerm_user_assigned_identity" "acr_identity" {  
  resource_group_name = azurerm_resource_group.rg_acr.name
  location            = azurerm_resource_group.rg_acr.location
  tags                = merge(local.default_tags, var.acr_tags)

  name = "${var.acr_name}Identity"
  depends_on = [
    azurerm_resource_group.rg_acr,
  ]
  lifecycle {
    ignore_changes = [
      tags
    ]
  }
}

Alt text

Task-4: Create Azure Container Registry (ACR) using terraform

Use the following terraform configuration for creating ACR.

acr.tf
# Create the Container Registry
resource "azurerm_container_registry" "acr" {  
  name                = var.acr_name
  resource_group_name = azurerm_resource_group.rg_acr.name
  location            = azurerm_resource_group.rg_acr.location
  sku                 = var.acr_sku
  admin_enabled       = var.acr_admin_enabled
  # zone_redundancy_enabled = true
  data_endpoint_enabled = var.data_endpoint_enabled
  identity {
    type = "UserAssigned"
    identity_ids = [
      azurerm_user_assigned_identity.acr_identity.id
    ]
  }

  # dynamic "georeplications" {
  #   for_each = var.acr_georeplication_locations

  #   content {
  #     location = georeplications.value
  #     tags     = merge(local.default_tags, var.acr_tags)
  #   }
  # }
  tags = merge(local.default_tags, var.acr_tags)
  lifecycle {
    ignore_changes = [
      tags
    ]
  }
  depends_on = [
    azurerm_resource_group.rg_acr,
    azurerm_log_analytics_workspace.workspace
  ]
}

run terraform validate & format

terraform validate
terraform fmt

run terraform plan & apply

terraform plan -out=dev-plan -var-file="./environments/dev-variables.tfvars"
terraform apply dev-plan

Alt text

Task-5: Create Diagnostics Settings for ACR

we are going to use diagnostics settings for all kind of azure resources to manage logs and metrics etc... Let's create diagnostics settings for ACR for storing Logs and Metric with default retention of 30 days or as per the requirements.

acr.tf
# create Diagnostics Settings for ACR
resource "azurerm_monitor_diagnostic_setting" "diag_acr" {  
  name                       = "DiagnosticsSettings"
  target_resource_id         = azurerm_container_registry.acr.id
  log_analytics_workspace_id = azurerm_log_analytics_workspace.workspace.id

  log {
    category = "ContainerRegistryRepositoryEvents"
    enabled  = true

    retention_policy {
      enabled = true
      days    = var.acr_log_analytics_retention_days
    }
  }

  log {
    category = "ContainerRegistryLoginEvents"
    enabled  = true

    retention_policy {
      enabled = true
      days    = var.acr_log_analytics_retention_days
    }
  }

  metric {
    category = "AllMetrics"

    retention_policy {
      enabled = true
      days    = var.acr_log_analytics_retention_days
    }
  }
}
run terraform validate & format

terraform validate
terraform fmt

run terraform plan & apply

terraform plan -out=dev-plan -var-file="./environments/dev-variables.tfvars"
terraform apply dev-plan
Alt text

Task-6: Lock the resource group

Finally, it is time to lock the resource group created part of this exercise, so that we can avoid the accidental deletion of the azure resources created here.

acr.tf
# Lock the resource group
resource "azurerm_management_lock" "rg_acr" {  
  name       = "CanNotDelete"
  scope      = azurerm_resource_group.rg_acr.id
  lock_level = "CanNotDelete"
  notes      = "This resource group can not be deleted - lock set by Terraform"
  depends_on = [
    azurerm_resource_group.rg_acr,
    azurerm_monitor_diagnostic_setting.diag_acr,    
  ]
}
run terraform validate & format

terraform validate
terraform fmt

run terraform plan & apply

terraform plan -out=dev-plan -var-file="./environments/dev-variables.tfvars"
terraform apply dev-plan
list of resources in this ACR resource group

Alt text

Task-7: Validate ACR resource

Task-7.1: Log in to registry

az acr login --name acr1dev

Task-7.2: Push image to registry

az acr login --name acr1dev
docker tag sample/aspnet-api:20230226.1 acr1dev.azurecr.io/sample/aspnet-api:20230226.1
docker push acr1dev.azurecr.io/sample/aspnet-api:20230226.1
or
az acr push --name acr1dev sample/aspnet-api:20230226.1

Task-7.3: Pull image from registry

az acr login --name acr1dev
docker pull acr1dev.azurecr.io/sample/aspnet-api:20230226.1

Task-7.4: List container images

az acr login --name acr1dev
az acr repository list --name acr1dev

for more information look into the az acr cheat-sheet az-acr-cheat-sheet

Alt text

Task-8: Restrict Access Using Private Endpoint

To enhance security and limit access to an Azure Container Registry (ACR), you can utilize private endpoints and Azure Private Link. This approach assigns virtual network private IP addresses to the registry endpoints, ensuring that network traffic between clients on the virtual network and the registry's private endpoints traverses a secure path on the Microsoft backbone network, eliminating exposure from the public internet.

Additionally, you can configure DNS settings for the registry's private endpoints, allowing clients and services in the network to access the registry using its fully qualified domain name, such as myregistry.azurecr.io.

This section guides you through configuring a private endpoint for your ACR using Terraform. Note that this feature is available in the Premium container registry service tier.

Task-8.1: Configure the Private DNS Zone

acr.tf:

# Create private DNS zone for Azure container registry
resource "azurerm_private_dns_zone" "pdz_acr" {
  name                = "privatelink.azurecr.io"
  resource_group_name = azurerm_virtual_network.vnet.resource_group_name
  tags                = merge(local.default_tags)

  lifecycle {
    ignore_changes = [
      tags
    ]
  }
  depends_on = [
    azurerm_virtual_network.vnet
  ]
}

run terraform validate & format

terraform validate
terraform fmt

run terraform plan & apply

terraform plan -out=dev-plan -var-file="./environments/dev-variables.tfvars"
terraform apply dev-plan

Confirm the Private DNS zone configuration by navigating to rg-vnet1-dev -> privatelink.azurecr.io -> Overview blade.

Alt text

acr.tf:

# Create private virtual network link to Virtual Network
resource "azurerm_private_dns_zone_virtual_network_link" "acr_pdz_vnet_link" {
  name                  = "privatelink_to_${azurerm_virtual_network.vnet.name}"
  resource_group_name   = azurerm_resource_group.rg.name
  virtual_network_id    = azurerm_virtual_network.vnet.id
  private_dns_zone_name = azurerm_private_dns_zone.pdz_acr.name

  lifecycle {
    ignore_changes = [
      tags
    ]
  }
  depends_on = [
    azurerm_resource_group.rg,
    azurerm_virtual_network.vnet,
    azurerm_private_dns_zone.pdz_acr
  ]
}

run terraform validate & format

terraform validate
terraform fmt

run terraform plan & apply

terraform plan -out=dev-plan -var-file="./environments/dev-variables.tfvars"
terraform apply dev-plan

Confirm the Virtual network links configuration by navigating to rg-vnet1-dev -> privatelink.azurecr.io -> Virtual network links.

Alt text

Task-8.3: Create a Private Endpoint Using Terraform

acr.tf:

# Create private endpoint for Azure container registry
resource "azurerm_private_endpoint" "pe_acr" {  
  name                = lower("${var.private_endpoint_prefix}-${azurerm_container_registry.acr.name}")
  location            = azurerm_container_registry.acr.location
  resource_group_name = azurerm_container_registry.acr.resource_group_name
  subnet_id           = azurerm_subnet.jumpbox.id
  tags                = merge(local.default_tags, var.acr_tags)

  private_service_connection {
    name                           = "pe-${azurerm_container_registry.acr.name}"
    private_connection_resource_id = azurerm_container_registry.acr.id
    is_manual_connection           = false
    subresource_names              = var.pe_acr_subresource_names
    request_message                = try(var.request_message, null)
  }

  private_dns_zone_group {
    name                 = "default" //var.pe_acr_private_dns_zone_group_name
    private_dns_zone_ids = [azurerm_private_dns_zone.pdz_acr.id]
  }

  lifecycle {
    ignore_changes = [
      tags,
    ]
  }
  depends_on = [
    azurerm_container_registry.acr,
    azurerm_private_dns_zone.pdz_acr
  ]
}

run terraform validate & format

terraform validate
terraform fmt

run terraform plan & apply

terraform plan -out=dev-plan -var-file="./environments/dev-variables.tfvars"
terraform apply dev-plan

Confirm the endpoint configuration by navigating to Container registry -> Networking -> Private access — you will see the new private endpoint details.

Navigate to Private endpoint -> Overview to verify the Virtual network/subnet and Network interface.

Navigate to Private endpoint -> DNS Configuration to verify the Network Interface and Configuration name.

Navigate to Network interface -> Overview to verify the private IP address attached to properties.

Alt text

To validate the private link connection, connect to the virtual machine you set up in the virtual network. Run a utility such as nslookup or dig to look up the IP address of your registry over the private link.

This will ensures that the private link connection is successfully established and allows for the verification of the expected private IP address associated with the registry in the given virtual network.

Validate using dig example:

Positive test case connecting from internal vm (private access):

Run the dig utility to look up the private IP address (10.64.3.5) of your registry over the private link:

dig acr1dev.azurecr.io

output

; <<>> DiG 9.16.1-Ubuntu <<>> acr1dev.azurecr.io
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 31549
;; flags: qr rd ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;acr1dev.azurecr.io.                IN      A

;; ANSWER SECTION:
acr1dev.azurecr.io. 0       IN      CNAME   acr1dev.privatelink.azurecr.io.
acr1dev.privatelink.azurecr.io. 0 IN A      10.64.3.5

;; Query time: 10 msec
;; SERVER: 172.30.80.1#53(172.30.80.1)
;; WHEN: Tue Dec 26 14:57:14 UTC 2023
;; MSG SIZE  rcvd: 160

Nagetive test case connecting from external (public access), compare this result with the public IP address in dig output for the same registry over a public endpoint:

dig acr1dev.azurecr.io
output

; <<>> DiG 9.16.1-Ubuntu <<>> acr1dev.azurecr.io
;; global options: +cmd
acr1dev.azurecr.io. 0       IN      CNAME   acr1dev.privatelink.azurecr.io.
acr1dev.privatelink.azurecr.io. 0 IN CNAME  ncus.fe.azcr.io.
ncus.fe.azcr.io.        0       IN      CNAME   ncus-acr-reg.trafficmanager.net.
ncus-acr-reg.trafficmanager.net. 0 IN   CNAME   r1029ncus.northcentralus.cloudapp.azure.com.
r1029ncus.northcentralus.cloudapp.azure.com. 0 IN A 52.240.241.132

;; Query time: 50 msec
;; SERVER: 172.29.48.1#53(172.29.48.1)
;; WHEN: Tue Dec 26 06:56:26 PST 2023
;; MSG SIZE  rcvd: 380

Validate using nslookup example:

Connecting from internal VM (private access):

nslookup acr1dev.azurecr.io
output

Server:         172.30.80.1
Address:        172.30.80.1#53

Non-authoritative answer:
acr1dev.azurecr.io  canonical name = acr1dev.privatelink.azurecr.io.
Name:   acr1dev.privatelink.azurecr.io
Address: 10.64.3.5

Connecting from external (public access):

nslookup acr1dev.azurecr.io

output

Server:         172.29.48.1   
Address:        172.29.48.1#53

Non-authoritative answer:
acr1dev.azurecr.io  canonical name = acr1dev.privatelink.azurecr.io.
acr1dev.privatelink.azurecr.io      canonical name = ncus.fe.azcr.io.
ncus.fe.azcr.io canonical name = ncus-acr-reg.trafficmanager.net.
ncus-acr-reg.trafficmanager.net canonical name = r1029ncus.northcentralus.cloudapp.azure.com.
Name:   r1029ncus.northcentralus.cloudapp.azure.com
Address: 52.240.241.132

This process ensures that the private link connection is successfully established and allows expected private IP address associated with our resource in the private virtual network.

References