A Step-by-Step Guide to Setting Up an Azure Container Registry and Managing Access

Prageesha Galagedara
12 min readSep 10, 2023

--

Introduction

In the modern era of containerized applications and microservices, efficient management of container images has become paramount for enterprises. Azure Container Registry (ACR) offers a robust solution, allowing organizations to securely store, manage, and deploy container images. In this comprehensive guide, we’ll walk you through setting up an end-to-end ACR infrastructure tailored to enterprise needs.

In this post, I want to cover end-to-end secure Public ACR architecture with scope-based access. I have used Standard ACR which has a public endpoint. In future articles, I am planning to cover a Private Secure ACR setup too.

High-Level Architecture

In my design I have the following:

  • I have 2teams/squads (devops squad and online squad) on my hypothetical organization.
  • There is a 1 ACR which will share by the entire organizaton and its deployed into the shared resource group (rg-shared)
  • Each team has their own Azure Key Vault where it store their build/pull tokens for ACR
  • Build/Pull token will be created by terraform and have role-based access to each repos.
  • You can find the git source to this blog post in here: https://github.com/prageeshag/tech-blog-public/tree/main/acr-with-tokens/azure-terraform

Folder structure for terraform code

Terraform

Azure Container Registry

Let's create the Azure Container registry. First I am creating a resource group named rg-shared services to store the ACR. I have created a module to create Resource Groups, so we can reuse that module to create other Resource Groups. I assume that you have already set up provider.tf with the credentials which require Terraform to access your Azure account and provision resources.

Lets create a module for the Azure Resource Group

Creating a Terraform module for managing Azure Resource Groups is a great way to standardize your infrastructure as code (IaC) and make it more reusable. Terraform is a popular infrastructure provisioning and management tool, and creating modules allows you to encapsulate common configurations and resources. Below, I’ll guide you through creating a Terraform module for Azure Resource Groups:

Step 1: Set Up Your Project Structure

First, create a project directory structure to organize your Terraform code. You can structure your project like this:

Step 2: Define Input Variables

In the variables.tf file, define the input variables that your module will accept. For a Resource Group module, you might want to include variables for the name of the resource group, the location, and any additional tags:

################################################################################
# This is the set of variable for the module
# file name : modules/rg/variable.tf
###############################################################################

variable "name" {
default = ""
}

variable "location" {
default = ""
}

Step 3: Create the Resource Group

In the main.tf file, define the resource group using the input variables you’ve specified:

################################################################################
# This is the main file of the module which has the resource
# file name : modules/rg/main.tf
################################################################################

resource "azurerm_resource_group" "rg" {
name = var.name
location = var.location
}

Step 4: Define Output Values

In the outputs.tf file, define any output values you want to expose to users of your module. For example, you might want to output the Resource Group’s ID:

################################################################################
# This is the output values
# file name : modules/rg/output.tf
################################################################################output "object" {
value = azurerm_resource_group.rg
description = "returns the full Object"
}

output "name" {
value = azurerm_resource_group.rg.name
description = "returns the name of the resurce group"
}

output "id" {
value = azurerm_resource_group.rg.id
description = "returns the ID of Azure Resource Group"
}

Let's create a module for the Azure Container Registry

Similarly let's create a module for ACR also.

1.Define your variables in variables.tf:

################################################################################
# This is the set of variable for the module
# file name : modules/acr/variable.tf
################################################################################

variable "name" {
default = "prageeshaTechBlog"
}

variable "resource_group" {
default = ""
}

variable "location" {
default = ""

}

2. Implement the Azure container registry in main.tf:

################################################################################
# This is the main file of the module which has the resource
# file name : modules/acr/main.tf
################################################################################

resource "azurerm_container_registry" "acr" {
name = var.name
resource_group_name = var.resource_group
location = var.location
sku = "Standard"
admin_enabled = false
}

3.Define your outputs in output.tf:

################################################################################
# This is the output values
# file name : modules/rg/output.tf
################################################################################

output "object" {
value = azurerm_container_registry.acr
description = "returns the full Azure Key Vault Object"
}

output "name" {
value = azurerm_container_registry.acr.name
}

output "id" {
value = azurerm_container_registry.acr.id
}

Let's create shared resources

I am planning to place my ACR and other shared resources in a shared service resource group. Because this ACR will be used by multiple teams I am going to place it in the shared service.

1. Create resource group and ACR by using the modules we already created in the above steps

################################################################################
# This is the output values
# file name : shared-services/main.tf
################################################################################

locals {
prefix-shared = "shared-services"
shared-location = "eastus"
shared-resource-group = "rg-shared-services"
acr_name = "prageeshaTechACR"
}

module "rg_shared_services" {
source = "../modules/rg"
name = local.shared-resource-group
location = local.shared-location
}

module "acr_shared" {
source = "../modules/acr"
name = local.acr_name
location = local.shared-location
resource_group = module.rg_shared_services.name
}

2. Define your outputs in output.tf:

################################################################################
# This is the output values
# file name : shared-services/output.tf
################################################################################

output "acr_shared_name" {
value = module.acr_shared.name
}

output "acr_shared_id" {
value = module.acr_shared.id
}

output "rg_shared_name" {
value = module.rg_shared_services.name
}

3. Initialize Terraform

Navigate to the directory where your Terraform configuration files are located. Open your terminal and run: This command initializes Terraform, downloads the necessary providers, and sets up your working directory.

terraform init 

Initializing the backend...
Initializing modules...
Initializing provider plugins...
- Reusing previous version of hashicorp/azurerm from the dependency lock file
- Using previously-installed hashicorp/azurerm v3.70.0
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

4. Plan the Configuration

After initializing Terraform, you can generate an execution plan to see what actions Terraform will take. Run:

terraform plan -out my-tf-plan.out
Acquiring state lock. This may take a few moments...
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# module.acr_shared.azurerm_container_registry.acr will be created
+ resource "azurerm_container_registry" "acr" {
+ admin_enabled = false
+ admin_password = (sensitive value)
+ admin_username = (known after apply)
+ encryption = (known after apply)
+ export_policy_enabled = true
+ id = (known after apply)
+ location = "eastus"
+ login_server = (known after apply)
+ name = "prageeshaTechBlog"
+ network_rule_bypass_option = "AzureServices"
+ network_rule_set = (known after apply)
+ public_network_access_enabled = true
+ resource_group_name = "rg-shared-services"
+ retention_policy = (known after apply)
+ sku = "Standard"
+ trust_policy = (known after apply)
+ zone_redundancy_enabled = false
}
# module.rg_shared_services.azurerm_resource_group.rg will be created
+ resource "azurerm_resource_group" "rg" {
+ id = (known after apply)
+ location = "eastus"
+ name = "rg-shared-services"
}
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ acr_shared_id = (known after apply)
+ acr_shared_name = "prageeshaTechBlog"
+ rg_shared_name = "rg-shared-services"
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Saved the plan to: my-tf-plan.out
To perform exactly these actions, run the following command to apply:
terraform apply "my-tf-plan.out"
Releasing state lock. This may take a few moments...

5. Apply the Configuration (Optional) If the plan looks correct and you’re ready to create or update resources, you can apply the configuration by running:

terraform apply my-tf-plan.out

Now you can see that the ACR is created.

Let's give ACR access to our devops-squad

Now we have created the ACR to store container images. Lets see how we provide access to the ACR. Devops Squad is one of the squad that require access to this ACR. We are using container resgistry scopes to grant access to the repositories.

1. Create AKV for devops squad

I am creating Azure Key Vault for devops-squad so that they can store their secrets in this vault. When I am generating auth tokens for ACR access I am storing those token secrets in this AKV.

Let's create a module for the Azure Key Vault first.

  1. Define your variables in variables.tf:
################################################################################
# This is the set of variable for the module
# file name : modules/akv/variable.tf
################################################################################

variable "name" {
default = ""
}

variable "location" {
default = ""
}

variable "resource_group" {
default = ""
}

variable "tenant_id" {
default = ""
}

variable "access_policies" {
description = "List of access policies"
type = list(object({
tenant_id = string
object_id = string
secret_permissions = list(string)
storage_permissions = list(string)
key_permissions = list(string)
}))
}

2. Implement the azure key vault in main.tf:

################################################################################
# This is the main file of the module which has the resource
# file name : modules/akv/main.tf
################################################################################

data "azurerm_client_config" "current" {}

resource "azurerm_key_vault" "akv" {
name = var.name
location = var.location
resource_group_name = var.resource_group
enabled_for_disk_encryption = false
enabled_for_deployment = false
enabled_for_template_deployment = false
enable_rbac_authorization = false
tenant_id = var.tenant_id
soft_delete_retention_days = 7
purge_protection_enabled = false
sku_name = "standard"
# Create access policies
dynamic "access_policy" {
for_each = var.access_policies
content {
object_id = access_policy.value.object_id
tenant_id = access_policy.value.tenant_id
secret_permissions = access_policy.value.secret_permissions
key_permissions = access_policy.value.key_permissions
storage_permissions = access_policy.value.storage_permissions
}
}
}

3. Define your outputs in output.tf:

################################################################################
# This is the output values
# file name : modules/akv/output.tf
################################################################################

output "object" {
value = azurerm_key_vault.akv
description = "returns the full Azure Key Vault Object"
}
output "name" {
value = azurerm_key_vault.akv.name
description = "returns the name of Azure Key Vault"
}
output "id" {
value = azurerm_key_vault.akv.id
description = "returns the ID of Azure Key Vault"
}
output "vault_uri" {
value = azurerm_key_vault.akv.vault_uri
description = "returns the vault URI of Azure Key Vault"
}

Lets create Resource Group and Azure Key Vault for devops-squad

locals {
prefix-devops = "devops-squad"
devops-location = "eastus"
devops-resource-group = "rg-devops-squad"
}


data "azurerm_client_config" "current" {}

module "rg_devops_squad" {
source = "../modules/rg"
name = local.devops-resource-group
location = local.devops-location
}


module "akv_devops_squad" {
source = "../modules/akv"
name = "${local.prefix-devops}-vault"
location = local.devops-location
resource_group = module.rg_devops_squad.name
tenant_id = data.azurerm_client_config.current.tenant_id

access_policies = [
{
# This is to grant AKV permissions to my terraform service principal so
# that it can update secret values
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = data.azurerm_client_config.current.object_id
key_permissions = ["Get", "List", "Encrypt", "Decrypt", "Create"]
secret_permissions = ["Get", "List", "Delete", "Set"]
storage_permissions = ["Get"]
},
{
# This is to grant AKV permission to my azure web console user
# that it can read the secret values from web for this demo purpose
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = var.my_ui_account_object_id
secret_permissions = ["Get", "List", "Set"]
key_permissions = []
storage_permissions = []
}
# Add more access policies as needed
]
}

Once you apply the above resource you should have a resource group and Azure Key Vault created for the devops-squad

The Challenge of Access Control

Azure Container Registry provides various mechanisms for access control, including Azure Active Directory (Azure AD) authentication, service principals, and managed identities. While these mechanisms offer robust security, there are situations where you need to provide temporary or scoped access to your registry without sharing credentials. This is where Azure Container Registry Token Management shines.

To streamline the process of creating and managing Azure Container Registry tokens, we can leverage Terraform. Terraform allows us to define and manage our Azure resources in a declarative way. Let’s break down the Terraform module step by step.

  1. Container Registry Scope Map
resource "azurerm_container_registry_scope_map" "scope_map" {
name = var.scope_map_name
container_registry_name = var.container_registry_name
resource_group_name = var.resource_group_name
actions = var.actions
}

The first step is to create a scope map. A scope map defines the level of access for a specific token. You can specify the actions permitted, such as read, write, or delete, and associate it with your Azure Container Registry. This allows for fine-grained control over what a token can and cannot do.

2. Container Registry Token

resource "azurerm_container_registry_token" "token" {
name = var.scope_map_name
container_registry_name = var.container_registry_name
resource_group_name = var.resource_group_name
scope_map_id = azurerm_container_registry_scope_map.scope_map.id
}

Once we have a scope map in place, we create a token based on that scope map. This token can be considered as a bearer token with temporary access to the resources defined in the scope map. It is a dynamic way to grant access without revealing any credentials.

3. Container Registry Token Password

resource "azurerm_container_registry_token_password" "token_pwd" {
container_registry_token_id = azurerm_container_registry_token.token.id
password1 {
expiry = var.token_expiry
}
}

Tokens are issued with an associated password, which serves as a shared secret between the token issuer and the consumer. The password can have an expiration date to ensure that the access is temporary. In this example, we set the expiry date using the var.token_expiry variable.

4. Key Vault Secret

resource "azurerm_key_vault_secret" "secret" {
name = var.scope_map_name
value = azurerm_container_registry_token_password.token_pwd.password1[0].value
key_vault_id = var.key_vault_id
depends_on = [azurerm_container_registry_token_password.token_pwd]
}

To make this token easily accessible to the applications and services that need it, we store it securely in an Azure Key Vault. The azurerm_key_vault_secret resource allows us to create a secret in the Key Vault and populate it with the token password.

I created a module from the above resources and used that for each team to provision their access scope for the ACR.

For each squad, I am creating below two tokens/scopes using above created module.

builder: This will have access to read/write to the repositories. This can be used in your pipeline to push images to the allowed devops repositories.

reader: This will have read access only. This can be used to pull images and can be used in your deployments to pull the application image.

Builder Scope

In this configuration, we create a scope map specifically tailored for builder activities, granting them access to both reading and writing container images. This scoped token is designed to expire automatically for added security. The following is placed in the main.tf my devops squad terrform codebase.

# Define a variable for devops_apps with a default value
variable "devops_apps" {
type = list(any)
default = [
"app1",
"app2"
]
}


# Define locals for read_repos and write_repos
locals {
read_repos = [for value in var.devops_apps : "repositories/${local.prefix-devops}/${value}/content/read"]
write_repos = [for value in var.devops_apps : "repositories/${local.prefix-devops}/${value}/content/write"]
}

module "acr_scope_map_devops_builder" {
source = "../modules/acr-scopes"
scope_map_name = "devops-squad-builder"
container_registry_name = data.terraform_remote_state.azr-shared-services.outputs.acr_shared_name
resource_group_name = data.terraform_remote_state.azr-shared-services.outputs.rg_shared_name
actions = concat(local.read_repos, local.write_repos)
key_vault_id = module.akv_devops_squad.id
token_expiry = "2024-03-22T17:57:36+08:00"
}

Reader Scope

In some cases, you might need to provide read-only access to container images for auditing or monitoring purposes. This is where the reader scope comes into play.

module "acr_scope_map_devops_reader" {
source = "../modules/acr-scopes"
scope_map_name = "devops-squad-reader"
container_registry_name = data.terraform_remote_state.azr-shared-services.outputs.acr_shared_name
resource_group_name = data.terraform_remote_state.azr-shared-services.outputs.rg_shared_name
actions = local.read_repos
key_vault_id = module.akv_devops_squad.id
token_expiry = "2024-03-22T17:57:36+08:00"
}

Once you apply the above terraform snippet you will get scope maps like below, and have access to the repositories mentioned.

Tokens for the above scope maps

Since we instruct to store tokens to AKV, you can retrieve tokens from the devops-squad key vault.

Verify scope-based access

Let's login to ACR with the reader token. Now you can fetch the tokens from the devops squad Azure Key Vault

READER_TOKEN=<token>
docker login prageeshatechblog.azurecr.io --username devops-squad-reader --password-stdin $READER_TOKEN

Now let's try to push using reader login, and this should fail, because we are using the reader token. I want to show you how the role-based access works with the tokens.

Now let's log in using the builder token and try to push the same image, since the builder token has permission to read and write to those repositories this should work.

You can see now that the image is pushed to ACR

Conclusion

Managing access to Azure Container Registry can be complex, but with Terraform and Azure Container Registry Token Management, you can simplify the process. By creating fine-grained tokens and securely storing them in Azure Key Vault, you can grant temporary and scoped access to your container images without exposing sensitive credentials. This not only enhances security but also streamlines access control for your containerized applications.

Incorporate this Terraform module into your infrastructure provisioning workflow to ensure secure and efficient access control for your Azure Container Registry.

--

--