ECR Pull-Through Cache 적용기(feat: Docker Rate Limit 을 피해서)

ECR Pull-Through Cache 적용기(feat: Docker Rate Limit 을 피해서)

Hits

들어가며

EKS 환경에서 Helm Chart를 이용해 여러 애플리케이션을 배포하다 보면, Docker Hub(registry-1.docker.io)의 이미지를 그대로 사용하는 경우가 많습니다.

그런데 NAT IP가 고정된 상태에서 노드가 자주 추가/삭제 되는 스케일 아웃/인을 반복하는 환경이라면 Docker Hub 의 Image Pull Rate Limit 에 걸릴 우려가 있습니다.

특히, 2024년 6월 30일부터 시행된 기존 규정에 따르면 인증되지 않은 사용자(anonymous)에 대해서는 6시간 동안 최대 100회의 Pull만 허용되었습니다.

2025년 4월 1일부터는 인증되지 않은 사용자의 Pull 횟수가 ‘시간당 10번’으로 제한되고, 무료 사용자(로그인 상태)도 시간당 100번까지만 이미지를 Pull할 수 있도록 제한이 더욱 강화될 예정입니다.

이런 변화는 Kubernetes 클러스터에서 노드 교체가 잦은 EKS 환경에 큰 영향을 줄 수밖에 없으며, 결과적으로 애플리케이션 배포 및 스케일링 과정에서 이미지 Pull 실패 위험을 한층 높이게 됩니다.

Pulls
Learn about pull usage and limits for Docker Hub.

따라서 개인 용도로 사용하는 케이스가 아닌 실제 프로덕션에서 Kubernetes 클러스터를 이용 중이며, Docker Hub 에 퍼블리싱된 이미지를 인증(Docker 계정)없이 사용하는 경우에는 Image Pull Rate Limit 을 마주할 확률이 더욱 커진다고 볼 수 있습니다.

이미지 캐싱에 대한 오해

"Kubernetes에서는 한 번 Pull한 이미지를 노드 레벨에서 캐싱하기 때문에, 굳이 Rate Limit 걱정을 할 필요가 없지 않을까?"라는 의문이 들 수 있습니다.

실제로 Kubelet은 기존에 Pull된 이미지를 재사용하며(imagePullPolicy 가 Always 가 아닌 이상), 설정에 따라 특정 주기에 오래된 이미지를 정리(pruning)하거나 디스크 공간이 부족하면 이미지를 제거할 수 있습니다.

다만, EKS 환경에서 Karpenter와 같은 오토스케일링 도구를 사용하는 경우 노드 자체의 생명 주기가 짧아집니다.

스팟 인스턴스가 자주 교체되거나 부하 변화에 따라 노드가 빠르게 추가/삭제되면서, 이전에 캐싱해둔 이미지를 재활용할 기회가 크게 줄어들 수밖에 없습니다. 결국 이러한 이유로 인해, Rate Limit에 걸릴 위험성이 더욱 높아지게 됩니다.

그렇다면 Docker Hub의 Rate Limit을 우회하기 위한 방법은 무엇이 있을까요?

해당 문제를 겪었을 때 들었던 방법은 몇가지가 있었습니다.

  1. Docker Hub 무료 계정 사용 + imagePullSecrets 적용
    1. Docker Hub의 무료 계정을 생성하고, 이를 Kubernetes에 imagePullSecrets로 등록해 인증된 Pull을 시도하는 방법입니다.
      • 하지만 무료 계정 역시 Rate Limit이 존재하며, 장기적으로 안정적인 해결책이 아니라 판단되어 제외하였습니다.
  2. 직접 ECR Private Repository에 이미지를 업로드
    1. 우리가 사용하는 이미지를 ECR Private Repository에 일일이 Push해두고, Helm Chart나 매니페스트에서 해당 Private Repository를 사용하도록 변경하는 방법입니다.
    2. 클러스터 컴포넌트나 애드온 이미지를 자주 교체하지는 않으나, 여전히 수동으로 매번 업로드·업데이트해야 한다는 점이 번거로웠습니다.
  3. Docker Hub를 미러링하는 서드파티 레지스트리 사용
    1. 외부에서 Docker Hub 이미지를 미러링해주는 레지스트리를 찾아서, 해당 레지스트리를 대신 사용합니다.
    2. 하지만 서드파티 레지스트리에 대한 신뢰 문제, 장애나 중단 발생 시 치명적인 영향을 받을 수 있다는 점이 우려되었습니다.
    3. 신뢰 문제가 아니더라도 모든 퍼블릭 레지스트리에 대해서 미러링하는 서비스는 찾지 못하였습니다.

세가지 방법 중 2번(직접 업로드)를 자동화 한 형태에 가장 가까운 ECR Pull-Through Cache 가 가장 적합하다고 판단하였습니다.

ECR Pull-Through Cache 를 사용하면 ECR에서 퍼블릭 레지스트리마다 캐싱 정책을 설정한 뒤, 퍼블릭 레지스트리별로 생성된 ECR 레포지토리 주소를 통해 이미지를 Pull 할 수 있습니다.

이렇게 하면 처음 이미지를 가져올 때 ECR이 자동으로 원본 퍼블릭 레지스트리에서 이미지를 받아 ECR Private Repository에 저장하고, 이후에는 캐싱된 이미지를 통해 간편하게 운영할 수 있게 됩니다.

ECR Pull-Through Cache 의 개념 및 동작 방식

ECR Pull-Through Cache 는 특정 퍼블릭 레지스트리(예: Docker Hub)에 대해 프록시(Proxy) 처럼 동작합니다.

ECR에 이미지 요청이 들어오면 먼저 ECR 레포지토리 내부에 해당 이미지가 캐싱되어 있는지 확인하고, 없다면 퍼블릭 레지스트리에서 이미지를 자동으로 가져와 ECR에 저장합니다.

따라서 우리는 퍼블릭 레지스트리에 직접 접근 할 필요 없이 항상 ECR 레포지토리를 통해 이미지를 Pull할 수 있게 됩니다.

2025년 2월 기준, ECR Pull-Through Cache 가 지원하는 퍼블릭 레지스트리

  • ECR Public Registry
  • Quay (quay.io)
  • Docker Hub (registry.docker.io)
  • GitHub Container Registry (ghcr)
  • Azure Container Registry
  • GitLab Container Registry

일반적으로 대부분의 Helm Chart가 Quay, Docker Hub, GitHub Container Registry 등을 자주 사용하므로 대부분의 유즈케이스를 충분히 커버 할 수 있습니다.

ECR Pull-Through Cache 설정 방법 (웹 콘솔)

우선 웹 콘솔에서의 사용 방법을 간단히 소개합니다.

AWS ECR 서비스의 Private Registry -> Features & Settings 에서 Pull-Through Cache 규칙 설정을 접근합니다.

이후 규칙 추가를 선택합니다.

어떤 퍼블릭 레지스트리(업스트림)에 대해 규칙을 생성할지 선택 할 수 있습니다.

Docker Hub 과 GitHub Container Registry 의 경우 Public Image 에 접근하더라도 인증 정보를 사용해야하는것에 유의해야 합니다.

우선 인증이 필요하지 않은 quay.io 에 대해 먼저 설정해봅니다.

위와 같이 네임스페이스를 지정 할 수 있습니다. 웹콘솔로 진행하는경우 위와같이 quay.io 에 대한 네임스페이스는 quay 로 지정됩니다.

위와 같이 사용해도 무방하지만, 실제 업스트림 레지스트리의 네이밍을 그대로 따라가는것을 추천합니다. (예: ghcr.io , quay.io , registry.k8s.io / docker.io -> 실제 업스트림 레지스트리url 은 registry-1.docker.io 입니다)

생성을 하고 나면 위와 같이 캐시 네임스페이스가 생성됩니다.

{ACCOUNT_ID}.dkr.ecr.ap-northeast-2.amazonaws.com/quay.io/{repository}/{image}:{tag} 와 같은 형태로 이미지 Pull 요청을 하면 실제 업스트림인 quay.io/{repository}/{image}:{tag} 에서 이미지를 갖고와 캐싱해서 사용할 수 있게 됩니다.

예를들어 quay.io 레지스트리를 사용하는 argo-cd 를 테스트해보겠습니다.

캐시 규칙을 설정해두었지만 현재 ECR Private Repository 에 아무것도 없는 상태입니다.

$ aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin {ACCOUNT_ID}.dkr.ecr.ap-northeast-2.amazonaws.com 와 같은 형태로 docker 에 ecr을 통해 인증을 하고나서

$ docker pull {ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/quay.io/argoproj/argocd:v2.5.6 와 같이 ECR Private Repository 를 통해 image pull 을 요청합니다. 우리가 아까 정의한 캐시 네임스페이스를 사용하는것에 유의합니다.

이렇게 하면 실제로는 해당 경로에 아직 이미지가 없지만, 설정한 캐시 규칙을 통해 업스트림 레지스트리에서 이미지를 자동으로 가져와 캐싱 네임스페이스 기반의 ECR Private Repository에 저장하게 됩니다.

이후에는 그 이미지를 곧바로 사용할 수 있습니다.

실제로 우리가 ECR Private Repository 를 직접 생성하지 않았지만. 캐시 규칙에 의해 자동으로 Private Repository 를 생성하고, 이미지를 해당 레포지토리에 저장하게됩니다.

아까 처음에 Docker Hub 및 GitHub Container Registry 는 ECR Pull-Through Cache 정책을 사용하기 위해 인증 정보가 필요하다고 이야기 했었습니다.

Docker Hub 이나 GitHub Container Registry 를 업스트림으로 하는 캐시를 웹콘솔에서 생성하려고 하면 볼 수 있는 화면입니다.

ecr-pullthroughcache/ 를 접두사로 하는 암호를 AWS Secrets Manager 에 생성하고 사용해야 하는것만 더 추가됩니다.

이 때 AWS Secrets Manager 에 생성하는 암호에는 usernameaccessToken 이라는 키를 사용합니다.

ECR Pull-Through Cache 설정 방법(Terraform)

위와 같이 aws_ecr_pull_through_cache_rule 리소스를 사용 할 수 있습니다.

Docker Hub 와 GitHub Container Registry 에 대해서는 AWS Secrets Manager 에 정의된 암호를 사용해야하며, 해당 부분을 어떻게 설정하는지는 이 글의 범위를 벗어나니 생략하겠습니다.

EKS 환경에서 사용하기

위에서 docker pull 로 이미지를 pull 할 때에는, 제가 사용한 IAM User 의 역할(IAM Role)에 ECR 관련된 정책이 이미 연결되어 있었기 때문에 별다른 문제 없이 사용이 가능했습니다.

다만 우리가 실제로 사용하게 될 EKS 워커노드 환경에서는 해당 권한들이 기본적으로 세팅되어있지 않기 때문에 아래와 같은 권한을 worker node 가 사용하는 IAM Role 에 추가해야 합니다.

statement {
  effect = "Allow"
  actions = [
    "ecr:BatchImportUpstreamImage",
    "ecr:CreateRepository",
    "ecr:ReplicateImage"
  ]
  resources = ["*"]
}

ecr 과 관련된 정책을 추가해야 한다. resources 의 경우 각자의 환경에 맞게 수정해서 사용하면 된다.

Terraform EKS 모듈을 사용하고 self managed node group 을 사용한다면 아래와 같이 사용할 수 있습니다.

Helm Chart에서의 이미지 경로 설정

대부분의 Helm Chart에서는 values.yaml 내의 image.repository 또는 image.registry 값을 조합해 최종 Pull 경로를 결정합니다. 이 로직은 보통

  1. _helpers.tpl 파일에서 템플릿 함수로 구현되어 있거나
  2. deployment.yaml, statefulset.yaml, daemonset.yaml워크로드 매니페스트 내부의 containers[].image 항목에 인라인으로 정의되어 있습니다.

예를 들어 ArgoCD Helm Chart(예: argo-cd 차트의 argocd-server 부분) deployment.yaml 에서 deployment.yaml을 보면 (link)

      containers:
      - name: {{ .Values.server.name }}
        image: {{ default .Values.global.image.repository .Values.server.image.repository }}:{{ default (include "argo-cd.defaultTag" .) .Values.server.image.tag }}
        imagePullPolicy: {{ default .Values.global.image.imagePullPolicy .Values.server.image.imagePullPolicy }}

여기서 default A B라는 Helm 함수는 “B가 설정되어 있으면 B를, 없다면 A를 사용”한다는 의미입니다. 즉,

  • .Values.server.image.repository가 있으면 그 값을 사용,
  • 없으면 .Values.global.image.repository를 사용하게 됩니다.
## Globally shared configuration
global:
  # -- Default domain used by all components
  ## Used for ingresses, certificates, SSO, notifications, etc.
  domain: argocd.example.com

  # -- Common labels for the all resources
  additionalLabels: {}
    # app: argo-cd

  # -- Number of old deployment ReplicaSets to retain. The rest will be garbage collected.
  revisionHistoryLimit: 3

  # Default image used by all components
  image:
    # -- If defined, a repository applied to all Argo CD deployments
    repository: quay.io/argoproj/argocd

따라서 global.image.repository 값을 수정하면, ArgoCD 컴포넌트 전반에 걸친 이미지 레포지토리를 일괄적으로 변경할 수 있습니다.

또한 Dex와 Redis는 서브차트가 아닌, ArgoCD 차트의 템플릿 일부로 포함되어 있는데, 이들은 global.image.repository를 참조하도록 구현되어 있지 않으므로 별도dex.image.repository, redis.image.repository 등을 설정해줘야 합니다.

이 포스트에서 다룰 예시 차트들

아래 Helm Chart들을 예시로 들어, 이미지 레지스트리를 ECR Pull-Through Cache 주소로 바꾸는 과정을 살펴보겠습니다. (괄호 안은 테스트 시점의 Chart 버전)

  • argo-cd (7.8.5)
  • cert-manager(v1.15.3)
  • external-secrets(0.9.16)
  • keda(2.14.0)
  • istiod(1.21.2)
  • fluentd(0.5.2)
  • kube-prometheus-stack(58.3.1)
  • external-dns(7.2.0)

argocd

global.image.repository 를 통해 argocd 애플리케이션들의 이미지 레포지토리를 일괄 지정합니다. repository 라는 이름으로 쓰이면 pull-path 로 정의해야 합니다. ({registry}/{repository}/{image} -> {ACCOUNT-ID}.dkr.ecr.ap-northeast-2.amazonaws.com/quay.io/argoproj/argocd)

global.image.tag 값을 정의하지 않는 이유는 argocd helm chart 내부에서 처리하기 때문에 정의하지 않았습니다. (링크 참고)

dexredis 는 Helm 의 sub chart 기능을 활용한것은 아니고 argocd 차트의 템플릿중 일부로 포함되어있습니다. dexredisglobal.image.repository 의 영향을 받지 않고 별도로 지정하도록 되어있습니다. (링크 참고)

cert-manager

cert-manager 의 helm chart 를 보면 controller 자체는 image.registry 로 레지스트리만 변경 할 수 있습니다. (values.yaml 에는 image.registry 항목이 없는데 사실 _helpers.tpl 쪽에 받아서 처리 할 수 있게 구성되어있다 / 링크 참고)

webhook 과 cainjector 의 경우는 webhook.image.registry 와 같은것으로 레지스트리만 변경이 불가능하고 repository 를 변경해야 합니다.

여기서도 느끼겠지만 보통 registry 의 경우 레지스트리 정보만 주입하면 되는 반면, repository 의 경우 registry 를 포함해서 레포지토리/이미지 값까지 포함해야 한다는 점이 차이가 있습니다.

external-secrets

image.repository 는 external-secrets 메인 컴포넌트의 이미지 레포지토리를 지정하고, webhook , certController 또한 각각 별도로 지정해줘야 합니다.

external-dns

registry 만 변경할 수 있도록 되어있고, external-dns 자체가 여러 컴포넌트가 없기 때문에 매우 단순하게 변경 할 수 있습니다.

keda

keda 의 모든 컴포넌트의 이미지 레지스트리를 일괄 변경 할 수 있도록 global.image.registry 값이 존재하므로 그대로 사용하면 됩니다.

istiod

defaults.pilot.hub 의 경우 istiod 가 사용할 이미지에 대한 registry 를 지정하는 값입니다.

defaults.global.hub 의 경우 sidecar injection 을 수행할 때 사용할 registry 및 defaults.pilot.hub 값이 정의되어있지 않을 때 사용할 기본 값(istiod 의 이미지)입니다.

fluentd

kube-prometheus-stack

kube-prometheus-stack 은 여러 helm sub chart 가 함께 사용 되는 형태입니다.

kube-prometheus-stack 의 helm chart. charts/ 에 있는 차트들은 서브차트들이다.

따라서 values.yaml 에서도 서브차트 / kube-prometheus-stack 자체의 차트 각각에 쓰이는 값들이 혼재되어있습니다.

서브차트의 경우 서브차트의 이름을 통해서 values 값을 전달해야 합니다.(예: kube-state-metrics 에 전달할 값은 kube-state-metrics.image.registry -> 해당 서브 차트의 .Values.image.registry)

alertmanager , prometheusOperator , prometheus 는 모두 kube-prometheus-stack 의 자체 템플릿이고. prometheusOperator 는 자체 deployment 로 뜨기 때문에 prometheusOperator.image.registry 형태로 직접 전달하고. alertmanagerprometheus 는 모두 CustomResource 로 생성되기때문에 Spec 이라는 postfix 로 value 를 처리합니다.

여기서 유의할점은. grafana , prometheusOperator 와 같이 하나의 추상적인 컴포넌트도 서브 이미지(사이드카 등)들이 각각 다른 레지스트리를 쓸 수도 있으므로 values.yaml 에서 모든 이미지 부분을 확인하는 것이 중요합니다.

Helm Chart 예시들을 보고 나서

앞서 살펴본 바와 같이, Helm Chart에서 이미지 레지스트리(또는 레포지토리)를 설정하는 방식은 차트마다 제각각입니다.

  1. image.registry vs. image.repository
    1. 어떤 차트는 단순히 image.registry를 변경하면 나머지 경로(리포지토리/이미지명)는 템플릿이 자동으로 붙여주는 구조를 갖습니다.
    2. 반면 다른 차트는 image.repository 자체에 레지스트리+조직+이미지명을 전부 넣도록 설계되어 있을 수 있습니다.
  2. global 설정
    1. 일부 차트는 global.image.repository(or global.image.registry)만 설정하면 여러 컴포넌트가 일괄적으로 해당 값을 참조하도록 만들어졌습니다.
    2. 하지만 서브차트(혹은 같은 차트 내 여러 템플릿)마다 별도의 image.repository 값을 사용하도록 구현되어 있으면, 각각 별도로 오버라이드해야 합니다.
  3. 서브차트 & 사이드카
    1. kube-prometheus-stack처럼 복잡한 차트는 서브차트, CRD, 사이드카 이미지가 섞여 있어, 여러 위치에 레지스트리 설정이 흩어져 있을 수 있습니다.
    2. 따라서 values.yaml 파일에서 모든 이미지 관련 파라미터를 빠짐없이 확인하고, 필요하다면 각각 ECR Pull-Through Cache 경로로 바꿔줘야 합니다.

결론

결국 Docker Hub의 이미지 Pull Rate Limit 이슈는, 스팟 인스턴스나 Karpenter 같은 오토스케일링 환경에서 노드가 자주 교체되는 EKS 클러스터에 특히 취약하게 작용합니다.

이러한 문제를 해결하기 위해 직접 이미지를 업로드하거나, 외부 미러링 레지스트리에 의존하기보다는 AWS ECR Pull-Through Cache를 활용하여 퍼블릭 레지스트리의 이미지를 ECR 프라이빗 레포지토리에 캐싱하는것이 가장 안정적이고 간단한 방법이라고 생각합니다.

물론 Helm Chart마다 image.registry, image.repository, global.image.* 등의 설정 방식이 다르므로, 필요한 컴포넌트·서브차트별로 꼼꼼히 레지스트리 경로를 변경해야 합니다.

하지만 이 작업을 한 번 진행하고 나면, 대부분 Helm Chart에서 image 와 관련된 템플릿 부분이 변경되지는 않기 때문에 더 이상 신경쓰지 않고 여러분의 ECR 프라이빗 레포지토리에 퍼블릭 이미지를 캐싱해서 사용할 수 있습니다.

이를 통해 퍼블릭 레지스트리의 정책 변화에 영향을 받지 않고 안정적으로 프로덕션 환경에서 이미지를 관리할 수 있습니다.