S3를 컨테이너 레지스트리로 사용
(ochagavia.nl)- 컨테이너 이미지는 보통 전용 레지스트리에 올리지만, S3 버킷을 HTTP로 노출하고 파일을 정해진 경로에 배치하면
docker pull대상처럼 사용할 수 있음 - 커스텀 이미지 빌더에서 pull 가능한 이미지를 몇 초 안에 만드는 과정이 병목이 되었고, 레이어 push 시간이 큰 비중을 차지함
- 198 MiB 레이어 업로드 벤치마크에서 ECR은 24~28 MiB/s, S3는 115~190 MiB/s로 측정되어 S3가 최대 8배 빠름
- 차이는 S3가 단일 레이어의 청크를 병렬 업로드할 수 있는 반면, OCI Distribution Spec 기반 ECR은 청크를 순차적으로 처리해야 하는 데서 발생함
- 이 방식은
docker push, 이미지 유효성 검증, 보안 스캔, 프라이빗 저장소 접근 제어 같은 레지스트리 기능을 대체하지 못하므로 실험적 최적화로 봐야 함
S3 버킷에서 docker pull이 되는 조건
- S3를 컨테이너 레지스트리처럼 쓰려면 버킷을 HTTP로 노출하고, Docker가 기대하는 경로에 이미지 파일을 업로드해야 함
- 조건이 맞으면 일반 레지스트리처럼
docker pull로 이미지를 가져올 수 있음 - 데모 이미지는 cowsay를 실행하며, 버킷 URL을 이미지 이름처럼 사용해
docker run --rm .../cowsay로 동작함 - 데모에는 Cloudflare R2가 사용됨
- 무료 egress가 선택 이유임
- R2와 S3는 API 호환성이 있어 AWS SDK로 R2에 이미지를 업로드함
왜 전용 레지스트리 대신 S3를 검토했나
- 컨테이너 이미지는 보통 DockerHub, GitHub Container Registry, ECR 같은 전용 레지스트리에 호스팅함
- 커스텀 이미지 빌더의 목표는 요구사항에서 pull 가능한 이미지까지 몇 초 안에 도달하는 것임
- AWS 환경에서는 ECR이 가장 쉬운 선택이지만, 실제 업로드 속도에서 S3와 ECR의 차이가 크게 나타남
- 성능 최적화를 위해 코드에 실행 추적(trace)을 넣자, 레이어를 컨테이너 레지스트리에 push하는 시간이 큰 병목으로 확인됨
198 MiB 레이어 업로드 결과
- 작은 벤치마크는 198 MiB 레이어를 ECR과 S3에 각각 업로드해 소요 시간과 처리량을 비교함
- 관측된 속도는 다음과 같음
- ECR: 최소 24 MiB/s, 8.2초
- ECR: 최대 28 MiB/s, 7.0초
- S3: 최소 115 MiB/s, 1.7초
- S3: 최대 190 MiB/s, 1.0초
- 결과상 S3는 ECR보다 최대 8배 빠른 수준임
- 실험 코드는 AWS에서 실행되었고, S3와 ECR은 VPC를 통해 내부 연결되어 퍼블릭 인터넷을 거치지 않음
- 이 조건은 가능한 좋은 지연 시간과 대역폭을 제공함
병렬 청크 업로드가 만든 속도 차이
- S3 업로드는 단일 레이어의 청크를 병렬로 업로드할 수 있음
- 충분한 대역폭이 있으면 병렬 청크 업로드로 처리량이 크게 늘어남
- AWS 문서도 대역폭 사용을 극대화하기 위해 병렬 청크 업로드를 권장함
- ECR은 OCI Distribution Spec을 구현함
- 이 명세는 여러 레지스트리에서
docker pull과docker push가 동작하게 하는 표준임 - 레이어 push는 순차적으로 진행되어야 하며, 청크 업로드를 하더라도 이전 청크가 끝나야 다음 청크로 넘어갈 수 있음
- 이 명세는 여러 레지스트리에서
- S3에서도 순차 업로드를 테스트하면 처리량이 ECR과 비슷한 수준으로 내려감
docker pull의 실제 요청 구조
docker pull의 내부 요청은 여러 개의 HEAD와 GET 요청으로 구성됨- 예시 흐름은 다음과 같음
- 이미지 매니페스트 존재 여부 확인:
HEAD /v2/my-image/manifests/latest - 이미지 매니페스트 다운로드:
GET /v2/my-image/manifests/latest - 매니페스트 해시로 다시 다운로드:
GET /v2/my-image/manifests/sha256:... - 이미지 메타데이터 blob 다운로드:
GET /v2/my-image/blobs/sha256:... - 이미지 레이어 blob 다운로드:
GET /v2/my-image/blobs/sha256:...
- 이미지 매니페스트 존재 여부 확인:
- 결국
docker pull은 필요한 파일을 HTTP로 다운로드하는 과정에 가까움 - 정적 파일 서버가 필요한 파일을 기대 경로에 두고, 각 요청에 맞는 Content-Type 헤더를 설정하면 컨테이너 이미지를 pull할 수 있음
- S3 버킷은 이 두 조건을 충족할 수 있어 조심스럽게 구성하면 컨테이너 레지스트리처럼 동작함
실험적 접근의 한계
- 이 방식은 아직 실험적이며, 더 조사하기 전에는 강한 결론을 내리기 어려움
- S3는 엄밀한 의미의 컨테이너 레지스트리가 아님
docker push를 할 수 없음docker pull이 되는 것은 HTTP 요청 구조와 정적 파일 제공 방식이 맞아떨어진 결과임
- 기존 컨테이너 레지스트리는 버킷에 파일을 올리는 방식보다 많은 기능을 제공함
- 표준 push 방식으로 업로드한 이미지가 실제로 유효하다고 신뢰할 수 있음
- 레이어에 대한 자동 보안 스캔과 경고를 제공할 수 있음
- 프라이빗 저장소 접근 권한을 네이티브로 지정할 수 있음
- 기대한 대로 동작한다면 퍼블릭 컨테이너 이미지를 Cloudflare R2에 호스팅하는 방식도 가능해질 수 있음
댓글과 토론
Hacker News 의견들
-
OCI Distribution Spec은 잘 설계된 명세처럼 읽히지 않아 아쉬움
명세상 레이어 푸시는 순차적으로 해야 해서, 청크로 업로드하더라도 각 청크가 끝나야 다음으로 넘어갈 수 있음. DockerHub와 GHCR에서 테스트한 바로는 청크 업로드 자체도 어차피 깨져 있고, 클라이언트들은 각 블롭/레이어를 통째로 올리는 편임. 명세는 RFC7233 형식과 맞지 않는Content-Range값 형식도 권장함
물론 블롭 단위 병렬성은 있지만, 블롭 내부 병렬성은 없음. 또 태그 목록의 페이지네이션 표준화 기회를 놓친 것도 불만임. 표준에서 관련 문구가 실수로 삭제되면서 [1], 레지스트리마다 제각각 구현하게 됨
[1] https://github.com/opencontainers/distribution-spec/issues/4...- Docker와 컨테이너 주변 기술은 대체로 이런 식임. 사용자 경험으로서의 Docker는 훌륭하지만, 기술로서는 엉망에 가까움
그렇다고 완전히 비난하려는 건 아님. 실제로 혁명적이었고, Linux 네임스페이스 사용을 이전보다 훨씬 쉽게 만들었으며, 세상을 더 나은 방향으로 바꿨음. 다만 항상 기술적 완성도보다 사용자 경험을 우선해 왔고, 그 자체가 꼭 나쁜 것도 아님. Perl이나 FTP로 주고받는 CSV로 비싼 문제를 푸는 지루한 회사들이 많듯, 지루하거나 심지어 나쁜 기술도 좋은 패키지로 제공하면 큰 가치가 있음
그래도 가끔은 지금보다 훨씬 더 좋아질 수 있었을 텐데 싶어 씁쓸함 - 여기에 더해 OCI 명세가 문제인지 AWS가 특이한 건지 모르겠지만, GitLab이나 Nexus와 달리 AWS ECR은 폴더 자동 생성을 지원하지 않음
예를 들어.dkr.ecr..amazonaws.com/foo/bar/baz:tag같은 형태가 안 되고, 평면 저장만 가능해서 이미지 이름이나 태그가 지나치게 길어짐. 이론적으로는 Terraform에서 ECR 리포지토리 객체를 만들어 비슷하게 흉내 낼 수 있지만, 결과 이미지 경로가 동적인 파이프라인에서는 별로임. CI 파이프라인의 IAM 역할에 불편할 정도로 많은 권한을 줘야 하고, 중앙 Terraform 저장소 밖에서 AWS 리소스가 관리되는 것도 마음에 들지 않음
[1] https://stackoverflow.com/questions/64232268/storing-images-...
- Docker와 컨테이너 주변 기술은 대체로 이런 식임. 사용자 경험으로서의 Docker는 훌륭하지만, 기술로서는 엉망에 가까움
-
Cloudflare가 R2를 사용하는 컨테이너 레지스트리 서버를 오픈소스로 공개한 적이 있음
써본 사람이 있는지 궁금함
[1]: https://github.com/cloudflare/serverless-registry- 좋아 보임. 다만 레이어당 500MB 제한이 있다고 되어 있음
어떤 용도에서는 큰 문제가 아닐 수 있지만, 다른 경우에는 바로 탈락 조건이 될 수 있음
- 좋아 보임. 다만 레이어당 500MB 제한이 있다고 되어 있음
-
글쓴이임. OCI 명세에서 왜 레이어 푸시가 순차적이어야 하는지 아는 사람이 있으면 알려주면 좋겠음
단순한 역사적 우연인지, 숨겨진 이유가 있는지 궁금함. 명확히 하자면 여러 레이어는 당연히 병렬로 푸시할 수 있고, 여기서는 단일 레이어 내용을 순차적으로 푸시해야 하는 부분을 말하는 것임- 정리를 단순하게 만들기 때문일 수 있음. “마지막” 청크까지 도달하지 못했다면
N+Timeout이후 완료되지 않은 업로드라는 게 명확하므로 제거하면 됨
부분 업로드를 어떻게 처리할지라는 구현 세부사항을 단순화하는 셈임. 그렇지 않으면 매 청크 끝마다 다른 청크들이 모두 있는지 확인하고 완료 처리를 해야 함. 다만 이건 구현 세부사항이고, 의미 있거나 의도된 설계였는지는 의심스러움. S3 접근 방식은 잘 작동할 것 같고, 예전에 큰 이미지를 배포하던 회사에서 비슷하게 해본 적이 있음. 월 GB당 0.10달러가 꽤 크게 쌓였음
ECR의 부가 기능은 잃지만, 개인적으로 그 기능들은 꽤 제한적이라고 봄 - 푸시는 다뤄본 적 없지만 이런 접근은 반가움. Docker 초창기에는 쓸 만한 사설 레지스트리 컨테이너가 없어서 이미지를 nginx 뒤에 놓고 pull했었고, 그래서 글을 재미있게 읽었음
- OCI 호환 레지스트리 [1]를 구현해봤고, 명세가 복잡해서 대부분은 명세보다 참조 구현 [2]의 동작을 따라갔음
클라이언트가 블롭 업로드를 마무리할 때 전체 블롭의 다이제스트를 제공해야 함. 이 요구사항은 서버가 전달받은 바이트의 무결성을 검증할 수 있게 하려는 것으로 보임. 서버가 최종 HTTP 요청에서야 다이제스트 검사를 시작한다면, 이전 HTTP 요청들에서 이미 저장소에 쓴 블롭 내용을 전부 다시 읽어야 함. 큰 레이어에서는 이 지연이 감당하기 어려울 수 있음. 특정 클라이언트 요구 때문에 150GiB 블롭까지 동작하도록 검증했음
대신 구현에서는 요청 시퀀스 전체에 걸쳐 다이제스트 계산을 계속 진행함. 블롭 데이터를 청크 단위로 받으면서 동시에 다이제스트 계산과 블롭 저장소로 스트리밍함. 각 요청 사이에는 다이제스트 계산 상태를Location헤더로 클라이언트에 돌려주는 업로드 URL 안에 직렬화함. 대략 이 코드에서 처리됨: https://github.com/sapcc/keppel/blob/7e43d1f6e77ca72f0020645...
참조 구현도 같은 접근을 쓰는 것으로 알고 있음. 다이제스트 계산은 순차적으로만 가능하므로 업로드도 순차적으로 진행되어야 함
[1] https://github.com/sapcc/keppel
[2] https://github.com/distribution/distribution - 블로그 글 고마움. “지난 4개월 동안 Outerbounds와 협업하며 커스텀 컨테이너 이미지 빌더를 개발했다”고 했는데, 별도 글감이라고는 했지만 조금이라도 세부 내용을 알려줄 수 있는지 궁금함
GitHub 저장소 링크라도 있으면 좋겠음. 배경은$PROGRAMMING_LANGUAGE안에서 프로그래밍 방식으로 OCI 이미지를 만들 방법을 찾고 있거나 직접 구현할 생각이 있기 때문임. Buildah처럼 하되 명령줄 인터페이스가 아니라 실제 프로그래밍 언어용 API였으면 함. 물론 Buildah를 하위 프로세스로 호출할 수도 있지만 다소 번거롭고, Buildah 내부 상태와 상호작용하거나 정리하는 문제도 신경 써야 하며, 현재 Buildah는 Mac도 지원하지 않음 - 뚜렷한 이유는 바로 떠오르지 않고, 부하 기반일 수도 있겠음
예전에 Docker에 병렬 푸시를 추가했던 것 같지만, pull과 push를 헷갈리는 것일 수도 있음. 나중에 보니 내 작업은 최종 푸시가 아니라 검사 병렬화 쪽이었음. 레이어가 어느 레이어 “위에” 올라가는지 명시한다면, 가리키는 ID가 이미 존재해야 해서 그럴 가능성도 있음
- 정리를 단순하게 만들기 때문일 수 있음. “마지막” 청크까지 도달하지 못했다면
-
꽤 멋진 사용 사례임
개인적으로는 Nexus를 그냥 씀. 충분히 잘 동작하고 OCI 이미지부터 apt 패키지, 커스텀 Maven, NuGet, npm 저장소 같은 것까지 지원함. 다만 설정과 리소스 사용량은 좀 성가시고, 특히 정리 정책 쪽이 그렇음: https://www.sonatype.com/products/sonatype-nexus-repository
그래도docker pull이 “그냥”HEAD와GET요청 묶음이라는 점은 정말 좋음. 오래전부터 잘 동작하던 것을 그대로 쓰고, 괜히 복잡하게 만들지 않는 이런 상식적인 결정을 더 많은 기술에서 보고 싶음. 인증과 정리 기능까지 있는 단순한 컨테이너 저장소가 더 많지 않은 게 의외임. Nexus와 Harbor는 실제로 쓰기엔 둘 다 꽤 복잡함- 패키지용으로만 Gitea를 쓰고 있음. Docker, npm, Python 등을 처리함
이 스레드에서 아무도 언급하지 않은 게 의외임
- 패키지용으로만 Gitea를 쓰고 있음. Docker, npm, Python 등을 처리함
-
CNCF의 Distribution, 예전 Docker Registry에는 S3에서 가져오는 CloudFront 서명 URL로 레지스트리를 뒷받침하는 기능이 포함되어 있음 [1]
https://distribution.github.io/distribution/storage-drivers/... -
https://github.com/distribution/distribution는 뭐가 문제인지 궁금함
- 이건 전에 못 봤고 실제로 S3를 지원하긴 함. 그런데 클라이언트에게 다운로드를 S3에서 직접 제공하는지, 아니면 S3를 자체 저장소 백엔드로만 사용해서 pull할 때 사실상 프록시처럼 동작하는지 궁금함
-
이 방식은 아주 비싸 보이고, 글에서 비용도 다뤘으면 좋았을 것 같음. S3와 R2 둘 다 궁금함
- S3 표준 티어는 저장 GB당 비용 기준으로 ECR의 5분의 1임
무료 인터넷으로 나가는 트래픽 비용은 동일하지만, 공개 ECR 저장소는 AWS 내부 사용으로 나가는 트래픽을 무료로 해주는 예외가 있음 - 비용은 결국 S3 비용임. 리전과 저장 티어에 따라 다르지만, GB당 저장 비용,
GET/PUT비용, 대역폭 비용은 AWS 웹사이트에서 확인할 수 있음: https://aws.amazon.com/s3/pricing/
- S3 표준 티어는 저장 GB당 비용 기준으로 ECR의 5분의 1임
-
개발 도구 밖에서는 Docker를 많이 쓰지 않지만, 왜 사설 컨테이너 레지스트리가 존재해야 하는지 이해한 적이 없음
그냥 지대 추구처럼 보임. 직접 관리하는 이미지 파일 같은 걸 만들어 원하는 대로 쓰는 것보다 실제로 어떤 장점이 있는지 궁금함- 꼭 쓸 필요는 없음.
docker save와docker import를 쓰면 됨
docker save alpine:3.19 > alpine.tar
docker load < alpine.tar
하지만 이제 그 tar 파일을 관리해야 하고, 모든 시스템이 그 위치와 접근 방법을 알아야 함. 아니면 바퀴를 다시 발명하지 않고 Docker가 이미 제공하는 방식을 쓰면 됨 - 세상에 공개하지 않을 이미지가 있을 가능성이 큼. 그런 이미지는 보통 k8s 클러스터나 CI/CD 러너 같은 인프라에서 접근 가능해야 함
그래서 직접 레지스트리를 만들거나 누군가에게 비용을 내고 맡겨야 함. 물론 개발용으로만 이미지를 쓴다면 이런 건 다 의미 없고, 개발 머신에 이미지를 저장하면 됨 - 코드 저장소 대신 파일을 이메일로 주고받지 않는 이유와 같음
이전 버전이 모두 있고 여러 소비자가 쉽게 접근할 수 있는 중앙 저장소가 필요하기 때문임. 앱을 빌드한 뒤 실행될 수 있는 모든 곳에 일일이 밀어 넣고 싶지 않음. 한 번 빌드해서 중앙 저장소에 푸시하고, 모든 곳이 그 저장소를 참조하게 하면 됨
사설 저장소 호스팅에 꼭 돈을 낼 필요도 없음. 직접 호스팅할 수 있는 도구가 많이 있음 - 사설 클라우드 레지스트리는 Docker 이미지와 관련해 필수 인증/인가 요구사항이 있는 프로젝트에서 매우 유용함
환경별로 Terraform, Bicep, Pulumi로 전부 구성할 수 있음 - 그걸 어떻게 관리할지도 문제임. 공개 이미지에 쓰는 것과 같은 도구를 쓰려면 컨테이너 레지스트리를 운영하면 됨
- 꼭 쓸 필요는 없음.
-
ECR은 실제로 이미지 레이어를 여러 부분으로 업로드할 수 있게 설계된 것처럼 보임
관련 ECR API로는 각 이미지 레이어 업로드 시작 시 호출하는InitiateLayerUpload API, 각 레이어 청크마다 호출하는UploadLayerPart API(최대 20MB), 레이어 업로드 후 이미지 레이어 참조를 담은 이미지 매니페스트를 푸시하는PutImage API가 있음. 이상한 점은 레이어 청크를 base64 인코딩으로 업로드해야 해서 데이터가 약 33% 늘어난다는 것임- 그 API를 직접 써봤는데, 아쉽게도 거기서도 순서 있는 업로드를 요구했음
-
파일 경로 배치를 엔드포인트 제어 수단으로 쓰는 아이디어가 흥미로움
다만Docker-Content-Digest헤더를 어떻게 처리할지 궁금함. 필수는 아니지만 응답에 포함하는 것이 권장되고, 많은 클라이언트가 이를 기대하며 헤더가 없는 레이어를 거부할 수 있음. 또 OCI 1.1 명세의 referrers API 같은 기능은 놓치게 될 수 있음. 구현이 꽤 까다로울 것 같음