Network & Server Factory

개인 공부 기록

Cloud/Azure

[Azure] Azure Container Registry 사용법 정리

1nfra 2026. 9. 27. 18:20
ACR(Azure Container Registry)은 Azure가 관리해 주는 컨테이너 이미지 저장소입니다. 이 글에서는 요금 등급 고르기, 개발자와 Jenkins, AKS가 각각 어떻게 로그인하고 어떤 권한을 받는지, Azure CLI로 만들고 권한 주는 방법, 태그를 안전하게 쓰는 법과 오래된 이미지 정리까지 실무에서 바로 쓰는 내용 위주로 정리합니다.

1. ACR이 하는 일

Docker Hub처럼 이미지를 올리고(push) 받는(pull) 저장소인데, Azure 구독 안에 만들고 Azure의 계정과 권한 체계(Entra ID, RBAC)로 접근을 관리한다는 점이 다릅니다. [CI/CD] GitHub, Jenkins, ArgoCD로 dev/prd 배포 아키텍처 설계의 구성에서는 Jenkins가 빌드한 이미지를 ACR에 올리고, dev와 prd AKS가 같은 ACR에서 이미지를 받습니다.

 

용어 뜻 예시
레지스트리 ACR 리소스 하나, 이름이 로그인 주소가 됨 exampleacr.azurecr.io
리포지토리 같은 이미지의 버전을 모아 두는 곳 demo-api
태그 특정 버전을 가리키는 이름표, 바꿔 달 수 있음 v1.4.0, develop-3f9c2e1
digest 이미지 내용으로 계산한 고유 값, 내용이 같으면 항상 같음 sha256:4a58…

 

레지스트리 이름은 Azure 전체에서 유일해야 하고, 영문과 숫자 5~50자로 정합니다. 이름이 곧 exampleacr.azurecr.io 같은 로그인 주소가 되므로 나중에 바꾸기 어렵습니다. 저는 회사나 서비스 이름을 앞에 두고 환경은 붙이지 않는 편입니다. 이 글처럼 dev와 prd가 하나의 ACR을 같이 쓰는 구성이 많기 때문입니다.

2. 요금 등급 고르기

ACR은 Basic, Standard, Premium 세 등급이 있습니다. 세 등급 모두 push, pull 같은 기본 기능은 같고, 포함 저장 용량과 처리량, 일부 기능이 다릅니다.

 

항목 Basic Standard Premium
포함 저장 용량 10 GiB 100 GiB 500 GiB
Webhook 개수 2 10 500
Private Endpoint 안 됨 안 됨 됨
지역 복제 안 됨 안 됨 됨
태그 없는 이미지 자동 보관 기간 설정 안 됨 안 됨 됨

 

저는 이렇게 고릅니다. 개인 학습은 Basic, 일반적인 운영은 Standard로 시작하고, 레지스트리를 인터넷에 열지 않고 사설망(Private Endpoint)으로만 쓰거나 여러 리전에 복제해야 할 때 Premium으로 올립니다. 등급은 만든 뒤에도 바꿀 수 있어서, 처음부터 Premium으로 시작할 필요는 없습니다.

3. 누가 어떻게 로그인하는가

ACR에 접근하는 주체는 보통 셋입니다. 사람(개발자), CI(Jenkins), 그리고 이미지를 받아 실행하는 AKS입니다. 셋 모두 Entra ID로 인증하고, 할 수 있는 일은 역할(RBAC)로 정해집니다. Entra ID와 관리 ID(Managed Identity)의 기본 개념은 [Azure] Entra ID, RBAC, Managed Identity 정리에 정리했습니다.

Jenkins에는 push 권한, AKS에는 pull 권한만 주고, 관리자 계정은 켜지 않습니다

 

누가 로그인 방법과 줄 역할
개발자 az acr login, 토큰은 3시간 유효
역할: 필요한 만큼 AcrPull 또는 AcrPush
Jenkins 서비스 주체로 docker login, Azure VM이면 관리 ID도 가능
역할: AcrPush
AKS kubelet 관리 ID, az aks update --attach-acr로 연결
역할: AcrPull
관리자 계정 admin user, 기본값 꺼짐
켜지 않는 것을 권장

 

관리자 계정은 레지스트리당 하나뿐인 공용 계정이라 누가 썼는지 구분되지 않고 역할로 권한을 나눌 수도 없습니다. Microsoft 문서도 테스트 용도로만 쓰라고 안내합니다. 아래는 각 주체가 로그인하는 명령 예시입니다.

# 개발자: Entra ID 계정으로 로그인, 3시간 뒤 다시 실행
az login
az acr login --name exampleacr

# Jenkins: 서비스 주체 ID와 비밀번호로 docker login
echo "$SP_PASSWORD" | docker login exampleacr.azurecr.io \
  --username "$SP_APP_ID" --password-stdin

 

역할은 AcrPull(받기), AcrPush(받기와 올리기), AcrDelete(삭제) 세 가지를 주로 씁니다. 한 가지 알아 둘 점이 있습니다. 레지스트리를 리포지토리 단위 권한 모드(ABAC)로 바꾸면 이 세 역할이 더 이상 적용되지 않고 Container Registry Repository Reader, Writer 같은 새 역할을 써야 합니다. Microsoft 문서에 따르면 --attach-acr도 이 모드에서는 지원되지 않습니다. 모드를 바꿀 때는 새 역할을 먼저 부여한 뒤 바꿔야 접근이 끊기지 않습니다.

4. Azure CLI로 만들고 권한 주기

포털에서도 만들 수 있지만, 저는 레지스트리 생성과 권한 부여 명령을 스크립트로 남겨 두는 방법을 권합니다. 누가 어떤 권한을 받았는지 나중에 그대로 다시 확인할 수 있기 때문입니다. 아래는 Microsoft 문서의 az 명령을 기준으로 한 구성 예시입니다. 이 글의 실습 환경에는 Azure 구독이 없어서 이 명령들은 실행하지 않았습니다.

# 1) 리소스 그룹과 레지스트리 만들기 (관리자 계정은 기본으로 꺼져 있음)
az group create --name rg-acr --location koreacentral
az acr create --name exampleacr --resource-group rg-acr --sku Standard

# 2) Jenkins 서비스 주체에는 이 레지스트리의 push 권한만 부여
ACR_ID=$(az acr show --name exampleacr --query id --output tsv)
az role assignment create --assignee <JENKINS_SP_APP_ID> --role AcrPush --scope "$ACR_ID"

# 3) dev, prd AKS의 kubelet 관리 ID에 pull 권한 부여 (구독 Owner 권한 필요)
az aks update --name aks-dev --resource-group rg-dev --attach-acr exampleacr
az aks update --name aks-prd --resource-group rg-prd --attach-acr exampleacr

# 4) 확인: 관리자 계정 상태, 부여된 역할, AKS에서의 접근
az acr show --name exampleacr --query adminUserEnabled
az role assignment list --scope "$ACR_ID" --output table
az aks check-acr --name aks-dev --resource-group rg-dev --acr exampleacr.azurecr.io

 

역할을 줄 때 --scope를 이 레지스트리 하나로 좁힌 점이 중요합니다. 구독이나 리소스 그룹 전체에 AcrPush를 주면 그 안의 다른 레지스트리에도 이미지를 올릴 수 있게 되기 때문입니다. 포털에서는 레지스트리의 액세스 제어(IAM)에서 역할 할당 추가로 같은 작업을 할 수 있고, --attach-acr는 AKS를 처음 만들 때 az aks create에도 붙일 수 있습니다.

5. 태그 관리: 같은 태그를 다시 쓰지 않기

ACR에서 태그는 바꿔 달 수 있는 이름표입니다. 같은 태그로 다시 push하면 어떻게 되는지 로컬 레지스트리(distribution 3.0.0)로 확인했습니다. ACR도 같은 OCI 레지스트리 규격을 따르므로 동작은 같습니다.

#!/usr/bin/env bash
# 같은 태그(v1.4.0)로 두 번 push하면 레지스트리에서 무슨 일이 생기는지 확인
set -e
REG=127.0.0.1:5000
digest() { curl -s -I -H 'Accept: application/vnd.oci.image.manifest.v1+json' \
  "http://$REG/v2/demo-api/manifests/$1" | awk -F': ' 'tolower($1)=="docker-content-digest"{print $2}' | tr -d '\r'; }

mkdir -p app && cd app
printf 'FROM scratch\nCOPY version.txt /\n' > Dockerfile

echo "build A" > version.txt
docker build -q -t $REG/demo-api:v1.4.0 . >/dev/null && docker push -q $REG/demo-api:v1.4.0 >/dev/null
FIRST=$(digest v1.4.0); echo "1차 push  v1.4.0 -> $FIRST"

echo "build B" > version.txt
docker build -q -t $REG/demo-api:v1.4.0 . >/dev/null && docker push -q $REG/demo-api:v1.4.0 >/dev/null
echo "2차 push  v1.4.0 -> $(digest v1.4.0)"

echo "태그 목록: $(curl -s http://$REG/v2/demo-api/tags/list)"
echo "1차 digest로 조회: HTTP $(curl -s -o /dev/null -w '%{http_code}' -H 'Accept: application/vnd.oci.image.manifest.v1+json' http://$REG/v2/demo-api/manifests/$FIRST)"
1차 push  v1.4.0 -> sha256:b9cb9490a95caba82a5db2106c4369738dd07a496c42b18f6973caa628311aea
2차 push  v1.4.0 -> sha256:4a58c2652c471ab00b276e14a79fe6cb697275f128e9685d870f0853ac870c5b
태그 목록: {"name":"demo-api","tags":["v1.4.0"]}
1차 digest로 조회: HTTP 200

 

v1.4.0 태그는 새 이미지로 옮겨 가고, 처음 이미지는 태그 없이 저장소에 남습니다

 

두 번 push한 뒤 태그 목록에는 v1.4.0 하나뿐이지만 가리키는 digest가 바뀌었습니다. 운영에서 이 일이 생기면 곤란합니다. 같은 v1.4.0인데 어제 배포한 파드와 오늘 새로 뜬 파드가 서로 다른 코드를 실행할 수 있기 때문입니다. 그래서 저는 세 가지를 지킵니다.

  • 태그는 빌드마다 새로 만듭니다. [CI/CD] GitHub, Jenkins, ArgoCD로 dev/prd 배포 아키텍처 설계처럼 develop-커밋해시, v버전처럼 겹치지 않는 값을 씁니다.
  • prd에 나간 태그는 잠급니다. 잠그면 같은 태그로 덮어쓰거나 지울 수 없습니다.
  • 정말 고정해야 하는 곳은 태그 대신 digest로 지정합니다. image: exampleacr.azurecr.io/demo-api@sha256:… 형식입니다.

 

# prd에 나간 태그를 덮어쓰기와 삭제 모두 막기
az acr repository update --name exampleacr --image demo-api:v1.4.0 \
  --write-enabled false --delete-enabled false

# 현재 설정 확인
az acr repository show --name exampleacr --image demo-api:v1.4.0 --output jsonc

6. 오래된 이미지 정리

실습 마지막 줄에서 보았듯이 태그가 옮겨 간 이전 이미지는 digest로 여전히 조회되고, 그만큼 저장 용량을 차지합니다. develop 브랜치처럼 하루에도 여러 번 빌드하는 리포지토리는 금방 쌓이므로 정리 규칙이 필요합니다. ACR에서는 acr purge 명령을 ACR Tasks로 실행합니다. 이 명령은 아직 미리 보기(preview) 기능이고, 지운 이미지는 되돌릴 수 없어서 먼저 --dry-run으로 지워질 목록을 확인합니다.

# 지워질 목록만 확인: develop- 태그 중 7일 지난 것, 최근 10개는 남김
az acr run --registry exampleacr /dev/null \
  --cmd "acr purge --filter 'demo-api:^develop-.*' --ago 7d --keep 10 --untagged --dry-run"

# 확인 후 매일 0시(UTC)에 자동 실행
az acr task create --name purge-develop --registry exampleacr --context /dev/null \
  --schedule "0 0 * * *" \
  --cmd "acr purge --filter 'demo-api:^develop-.*' --ago 7d --keep 10 --untagged"

 

필터를 develop- 태그로만 좁힌 점이 중요합니다. v버전 태그까지 지우면 롤백할 이미지가 사라지기 때문입니다. 앞에서 잠근 태그는 purge 대상에서도 지워지지 않습니다. Premium 등급이라면 태그 없는 이미지를 며칠 뒤 자동으로 지우는 보관 정책(retention policy)을 따로 켤 수도 있습니다.

7. 자주 막히는 지점

  • 파드가 ImagePullBackOff, 401 Unauthorized: AKS kubelet 관리 ID에 AcrPull이 없는 경우가 대부분입니다. az aks check-acr로 확인하고 --attach-acr로 연결하거나 역할을 부여합니다.
  • 어제 되던 docker push가 unauthorized: az acr login 토큰(3시간)이 만료된 것입니다. 다시 로그인하고, CI는 서비스 주체나 관리 ID를 씁니다.
  • 역할을 줬는데도 거부됨: 레지스트리가 ABAC 모드라 AcrPull, AcrPush가 적용되지 않는 경우입니다. 포털에서 권한 모드를 확인하고 Repository Reader, Writer 역할을 줍니다.
  • --attach-acr 실패: 실행한 계정에 구독 Owner 같은 역할 부여 권한이 없습니다. 권한 있는 사람이 실행하거나 역할을 직접 부여합니다.
  • 저장 용량이 계속 늘어남: 태그 없는 이전 이미지와 develop 태그가 쌓인 것입니다. acr purge를 예약 실행하고, Premium이면 보관 정책도 켭니다.

 

AKS 쪽 설정은 [Azure] Azure Kubernetes Service 구성 정리에서 이어서 볼 수 있습니다.

8. 정리

  • ACR은 Standard로 시작해 Private Endpoint나 지역 복제가 필요할 때 Premium으로 올리고, 관리자 계정은 켜지 않습니다.
  • 개발자는 az acr login, Jenkins는 서비스 주체나 관리 ID에 AcrPush, AKS는 --attach-acr로 AcrPull만 주고, 권한 부여 명령은 스크립트로 남겨 둡니다.
  • 태그는 빌드마다 새로 만들고 prd 태그는 잠그며, 쌓이는 develop 이미지는 acr purge를 --dry-run으로 확인한 뒤 예약 실행합니다.

참고

728x90
서울
--:--:--
-전체 글
-카테고리
오늘 방문