IT/AWS

ECR pull-through cache 도입 삽질기

gfrog 2026. 9. 14. 03:11

지난주 새벽에 알람이 울렸다. GHCR 레이트리밋에 걸려서 프로덕션 노드 30대 중 12대가 이미지 풀에 실패했다는 내용이었다. 사실 이게 처음이 아니다. 올해 들어서만 세 번째. 원인은 뻔했다. GitHub 조직 계정으로 익명 풀이 아니라 인증 풀을 쓰고 있었는데도, 신규 노드가 확 늘어나는 시간대에는 어김없이 걸렸다. 근데 원인이 뻔하다고 해결이 쉬운 건 아니었다.

그래서 ECR pull-through cache를 도입하기로 했다. 결론부터 말하면 지금은 잘 돌고 있는데, 그 과정에서 두 번 정도 멘탈이 나갔다.

왜 하필 pull-through cache였나

선택지는 대충 세 가지였다. Harbor를 직접 띄우거나, Docker Hub 유료 요금제를 그대로 쓰거나, ECR pull-through cache를 쓰거나. 우리 팀은 Harbor 운영 경험이 없었고 (그거 하나 붙이자고 팀 리소스를 또 뺄 여유도 없었다), Docker Hub는 GHCR/quay/mcr 등 다른 레지스트리는 커버가 안 됐다. 그러다 보니 자연스럽게 ECR로 눈이 갔다.

특히 올해 5월쯤에 AWS가 pull-through cache 지원 대상에 GHCR과 GitLab을 정식으로 추가하면서 (그 전엔 Docker Hub, quay, mcr, k8s.io 정도만 됐다) 우리 케이스에 딱 맞았다. 우리 이미지 대부분이 GHCR에 있었으니까.

첫 번째 삽질: 인증 정보를 어디에 넣어야 하지

가이드를 대충 훑고 나서 만든 초기 구성은 이랬다.

aws ecr create-pull-through-cache-rule \
  --ecr-repository-prefix ghcr \
  --upstream-registry-url ghcr.io \
  --credential-arn arn:aws:secretsmanager:ap-northeast-2:...:secret:ghcr-cred

이거 자체는 잘 됐다. 근데 문제는 Secrets Manager에 저장한 credential이었다. 처음엔 그냥 { "username": "...", "password": "..." } 이런 형태로 넣었는데, 계속 401이 떴다. 왜지?

문서를 다시 봤다. Secret 이름이 반드시 ecr-pullthroughcache/ 로 시작해야 했다. 그리고 그렇게 안 만들면 IAM 정책상 ECR이 접근을 못 한다. 이건 실수라기보다는 문서를 대충 읽은 내 잘못이다. 이름 바꾸고 나니 바로 통과.

여기서 30분 정도 날렸다. 별거 아닌 것 같은데, 새벽에 뭐라도 하나 걸리면 시간이 훨씬 오래 걸린다.

두 번째 삽질: 노드가 여전히 GHCR을 때리고 있었다

인증 문제 해결하고 나서 이미지 매니페스트도 정상적으로 캐시가 되는 걸 확인했다. aws ecr describe-images --repository-name ghcr/ourorg/api 하면 캐시된 태그가 뜬다. 여기까진 좋았다.

그런데 노드 로그를 보니 여전히 상당수 이미지가 GHCR로 직접 요청을 보내고 있었다. 아니 이럴 리가.

원인은 containerd 설정이었다. containerd.registry.mirrors 설정을 하긴 했는데, 이게 hostsd 방식이 아니라 예전 방식이라 최신 containerd 1.7+에서 부분적으로만 동작했다. Karpenter로 노드를 뜨우면서 userdata에 mirror 설정을 넣긴 했는데, 정작 이미지 pull은 원본 URL을 그대로 요청하는 이미지가 섞여 있었다.

해결은 /etc/containerd/certs.d/ghcr.io/hosts.toml 를 이렇게 바꾸는 거였다.

server = "https://ghcr.io"

[host."https://123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/v2/ghcr"]
  capabilities = ["pull", "resolve"]
  override_path = true

여기서 포인트는 override_path = true. 이걸 빼먹으면 요청 경로가 이상하게 조립돼서 404가 뜬다. 나는 이걸 빼먹었다가 30분 더 태웠다.

그리고 하나 더. hosts.toml은 노드가 뜰 때 넣어야 한다. 이미 떠 있는 노드에 넣어도 containerd 재시작 없이는 안 먹는다. Karpenter가 노드를 자주 갈아치우는 환경이라 결국 신규 노드부터 자동 적용됐고, 기존 노드는 그냥 자연스럽게 소멸되도록 뒀다.

캐시 웜업은 생각보다 오래 걸렸다

pull-through cache는 이름 그대로 첫 pull이 들어와야 그때 upstream에서 가져와 저장한다. 그러니까 도입 직후 첫 몇 시간은 오히려 latency가 살짝 늘어난다 (원본 → ECR → 노드 경로가 되니까). 이걸 미리 생각 못 하고 배포 타이밍이랑 겹쳤다가 배포가 평소보다 2분 정도 더 걸리는 걸 보고 아 이거 실수했다 싶었다.

지금은 자주 쓰는 이미지들을 야간에 미리 warm-up 하는 작은 CronJob을 하나 붙여놨다. 그냥 crane pull 로 태그들을 한번씩 긁어오는 게 전부다.

이관 후 뭐가 좋아졌나

숫자로 보면 이렇다. 도입 전에는 노드 스케일링 시간대에 이미지 pull 실패율이 평균 2~3%였다. 이관 완료 후 2주 지켜본 결과 0.1% 아래로 떨어졌다. 대부분은 원본 upstream이 아예 죽었을 때 발생하는 것들이라 우리가 어쩔 수 없는 부분이다.

비용은 조금 오히려 늘었다. 캐시 저장 용량이 있고, ECR API 호출 자체도 요금이 붙는다. 근데 새벽에 잠 못 자고 트리아지 하던 시간을 생각하면 이 정도는 감수할 만하다.

아직 남은 숙제

이미지 서명 검증(cosign)을 pull-through cache 뒤에서 어떻게 유지할지 애매하다. ECR이 매니페스트 다이제스트는 그대로 보존해주니까 이론상은 검증이 되긴 하는데, 팀 내부에서도 아직 검증 프로세스를 확정 못 했다. 이건 다음에 별도로 다뤄보려고 한다.

그리고 크로스 리전 상황. 우리 클러스터가 서울/도쿄 두 리전에 있는데, 지금은 각 리전 ECR에 pull-through cache를 따로 붙여놨다. 관리 오버헤드가 살짝 있어서 이걸 어떻게 정리할지도 고민 중이다.

혹시 pull-through cache 이미 쓰시는 분들 있으면 어떤 함정에 빠졌었는지 궁금하다. 아마 우리보다 훨씬 큰 규모에서 운영하시는 분들은 다른 이슈가 있을 것 같은데.