Azure Container Registry + AcrPull
What this skill produces
| Resource | Setting |
|---|---|
azurerm_container_registry |
SKU Standard (Basic for cost-sensitive dev), zone-redundant false, public access enabled |
azurerm_role_assignment |
AcrPull for the AKS kubelet identity on the ACR scope |
This skill creates the registry only. Populating it with NVCF images (~50 images from the SBOM) is covered by the acr-mirror skill — the operational counterpart to this one. Run acr-mirror after acr and before any NVCF stack install.
For a one-off sanity-check mirror (e.g., right after creation to verify the AcrPull binding works), see the Verifying the pull works section below.
Inputs
Source from streaming-env.sh + generated/.env.iam-vpc + generated/.env.aks-cluster.
Required:
| Variable | Description |
|---|---|
AZURE_SUBSCRIPTION_ID |
Subscription |
RESOURCE_GROUP |
RG |
LOCATION |
Region |
ACR_NAME |
ACR name — 5–50 chars, lowercase alphanumeric only, NO hyphens, globally unique |
KUBELET_IDENTITY_OBJECT_ID |
from .env.aks-cluster |
Optional (firm defaults):
| Variable | Description | Default |
|---|---|---|
ACR_SKU |
ACR tier (Basic, Standard, Premium) |
Standard |
ACR_ADMIN_ENABLED |
Admin user (avoid for prod) | false |
ACR_PUBLIC_ACCESS |
Public network access | true (set false + add private endpoint for hardening) |
OUTPUT_FORMAT |
bash or terraform |
bash |
Outputs
- Bash:
generated/stage04-<cluster-name>-acr.sh - Terraform:
generated/<cluster-name>/acr/main.tf
Writes generated/.env.acr with ACR_ID, ACR_LOGIN_SERVER, ACR_NAME.
Bash snippet (reference)
#!/bin/bash
set -euo pipefail
# Resolve repo root from script location — works regardless of CWD.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "$REPO_ROOT/streaming-env.sh"
if [[ ! -f "$REPO_ROOT/generated/.env.aks-cluster" ]]; then
echo "ERROR: $REPO_ROOT/generated/.env.aks-cluster not found. Run stage02 first." >&2
exit 1
fi
source "$REPO_ROOT/generated/.env.aks-cluster"
: "${AZURE_SUBSCRIPTION_ID:?}" "${RESOURCE_GROUP:?}" "${LOCATION:?}" "${ACR_NAME:?}" "${KUBELET_IDENTITY_OBJECT_ID:?}"
ACR_SKU="${ACR_SKU:-Standard}"
# ACR name validation: 5–50 chars, lowercase alphanumeric only
if [[ ! "$ACR_NAME" =~ ^[a-z0-9]{5,50}$ ]]; then
echo "ERROR: ACR_NAME='$ACR_NAME' must be 5–50 chars, lowercase alphanumeric only (no hyphens)."
exit 1
fi
az account set --subscription "$AZURE_SUBSCRIPTION_ID"
echo "--- Creating ACR $ACR_NAME ($ACR_SKU) ---"
az acr create \
--name "$ACR_NAME" \
--resource-group "$RESOURCE_GROUP" \
--location "$LOCATION" \
--sku "$ACR_SKU" \
--admin-enabled "${ACR_ADMIN_ENABLED:-false}" \
--only-show-errors
ACR_ID=$(az acr show -n "$ACR_NAME" -g "$RESOURCE_GROUP" --query id -o tsv)
ACR_LOGIN_SERVER=$(az acr show -n "$ACR_NAME" -g "$RESOURCE_GROUP" --query loginServer -o tsv)
echo "--- Granting AcrPull on $ACR_NAME to AKS kubelet identity ---"
# Idempotent: az role assignment create errors if it exists; ignore that one specific error.
az role assignment create \
--assignee-object-id "$KUBELET_IDENTITY_OBJECT_ID" \
--assignee-principal-type ServicePrincipal \
--role AcrPull \
--scope "$ACR_ID" \
--only-show-errors 2>&1 | grep -v "already exists" || true
mkdir -p "$REPO_ROOT/generated"
cat > "$REPO_ROOT/generated/.env.acr" <<EOF
export ACR_ID="$ACR_ID"
export ACR_LOGIN_SERVER="$ACR_LOGIN_SERVER"
export ACR_NAME="$ACR_NAME"
EOF
echo "Wrote generated/.env.acr"
Terraform snippet (reference)
resource "azurerm_container_registry" "main" {
name = var.acr_name
resource_group_name = var.resource_group_name
location = var.location
sku = var.acr_sku # "Basic" | "Standard" | "Premium"
admin_enabled = var.acr_admin_enabled
tags = var.tags
lifecycle { prevent_destroy = true }
}
resource "azurerm_role_assignment" "acr_pull" {
scope = azurerm_container_registry.main.id
role_definition_name = "AcrPull"
principal_id = var.kubelet_identity_object_id
principal_type = "ServicePrincipal"
}
output "acr_id" { value = azurerm_container_registry.main.id }
output "acr_login_server" { value = azurerm_container_registry.main.login_server }
output "acr_name" { value = azurerm_container_registry.main.name }
Teardown
ACR contains image data — prevent_destroy = true in TF; bash teardown should require --yes confirmation:
az acr delete --name "$ACR_NAME" --yes
Verifying the pull works
After the role assignment lands (RBAC propagation: 30s–2min):
# Mirror one small image (manual)
az acr import --name "$ACR_NAME" \
--source mcr.microsoft.com/azuredocs/aks-helloworld:v1 \
--image aks-helloworld:v1
# Pull from AKS to confirm the kubelet can authenticate
kubectl run hello --image "${ACR_LOGIN_SERVER}/aks-helloworld:v1" --restart=Never
kubectl wait --for=condition=Ready pod/hello --timeout=60s
kubectl delete pod hello
If the pull fails with imagePullBackOff and unauthorized, the role assignment hasn't propagated yet — wait 2 minutes and retry. If it still fails, verify KUBELET_IDENTITY_OBJECT_ID matches az aks show ... --query identityProfile.kubeletidentity.objectId.
Validation checklist
-
ACR_NAMEmatches^[a-z0-9]{5,50}$ -
AcrPullrole assignment on the ACR scope (not on a parent RG or subscription — least privilege) -
principal_type = ServicePrincipal(kubelet identity is a SP, not a User) -
prevent_destroy = trueon the ACR resource (Terraform) - Idempotent (role assignment "already exists" error tolerated)
- No
--admin-enabled truefor prod-intent deploys -
generated/.env.acrwritten
References
| Topic | Source |
|---|---|
| ACR + AKS passwordless pull | https://learn.microsoft.com/azure/aks/cluster-container-registry-integration |
| ACR naming rules | https://learn.microsoft.com/azure/azure-resource-manager/management/resource-name-rules#microsoftcontainerregistry |
az acr import (image mirror) |
https://learn.microsoft.com/cli/azure/acr#az-acr-import |
| ACR private endpoint (hardening) | https://learn.microsoft.com/azure/container-registry/container-registry-private-link |