-
원리부터 파악하는 컨테이너 이미지 PULL (w/ curl)Kubernetes 2023. 6. 30. 09:00
시작 전 잡담 - 1
본래 프로젝트 라이프사이클 관점에서의 개발 방법론에 대한 글을 쓰고 싶었지만
현재 진행하고 있는 강의에 대한 홍보글 겸 겸사겸사 적어본다. (많은 관심 부탁드립니다~~)
이 내용은 실제 강의에서 다뤘던 내용을 기반으로 한다.시작 전 잡담 - 2
이 내용은 단순히 도커의 작동 원리 측면에서 이해가 아닌, 시스템 아키텍처의 하나의 사례로 읽어보면 꽤 흥미롭다. 적어도 개인적으로는 그랬다. 전 세계에서 넷플릭스 다음으로 많은 트래픽을 처리하고 있을(어쩌면 넷플릭스보다 많을지도 모른다) Docker Hub의 구조가 궁금하지 않은가?
필자는 이 기사를 보고 질문을 떠올렸고, 이 글은 이 질문에 대한 그 내용을 요약한 것이다.
서론
정말 감사하게도
docker pull
이라는 명령어를 쓰면 참 쉽게Docker Hub
혹은 그에 대응하는 GHCR 이라던지 GCR 같은Container Registry
(이하CR
) 로 부터 컨테이너 이미지가 다운받아진다. 이렇게 받아진 이미지를 기반으로rootfs
를 만들고 [참고: 도커없이 컨테이너 만들기] 해당 가짜 rootfs를 진짜 rootfs라고 믿도록 contained 된 프로세스를 실행하면 실행 중인 컨테이너 완성이다.물론 저것만 하는 건 아니고 세부적으로 들어가면 여러 네임스페이스가 나오기는 하지만 적어도 Borg는 초창기에는 저것만 격리해 두고 컨테이너라고 불렀다고 한다. 컨테이너는 생각만큼 잘 정의된 개념이 아니다.
여기서 궁금한 것은
docker pull
명령어를 입력했을 때 어디랑 어떻게 통신하며, 특정 레이어를 이미 가지고 있을 때는 어떻게 중복되는 다운로드를 피할 수 있는 것일까? 어떻게 이들을 병렬적으로 다운로드 받을 수 있으며, Private Repository 에서 이미지를 다운받을때 입력하는 Secret 은 실제 어떻게 작동하고 있는것일까?배경지식
컨테이너 세상에서 표준이라고 부를만한 것은 세 가지이다.
각각을 엄밀하진 않지만 이해하기 쉽게 요약해 보면
Runtime Spec 은 RootFS 지정하면 그걸 어떻게 실행할 지에 대한 설정 및 CLI 명령어 표준이 담겨있고
Image Spec 은 그 RootFS를 만들 정보를 어떻게 정의할지에 대한 파일 규격에 대한 표준이 담겨있고
Distribution Spec 은 RootFS 를 만들 정보를 정의한 파일들을 어떻게 배포할지에 대한 HTTP 기반 API 표준이 담겨있다.이 글에서는 Image Spec과 Distribution Spec에 대해서 다뤄볼 예정이다.
OCI Image Spec
OCI Image Spec
정의에 따르면 컨테이너 이미지는 크게 4가지로 나뉜다.- Image Index
- Image Manifest
- Image Config
- Image Layer
간단하게 설명하면
- Index는 각 CPU, OS 환경별로 어떠한 manifest 가 있는지 기록된 JSON 양식을 따르는 파일이다.
- manifests는 하나의 컨테이너 이미지에 대한 정보가 담긴 JSON 파일이며 Config와 Layer에 대한 정보가 있다.
- config는 이 컨테이너 이미지가 어떻게 만들어졌고, 어떻게 실행 가능한지 (환경변수, 커맨드) 등등에 대한 메타데이터가 담겨있다.
- layer는 tar 형식으로 압축된 각 레이어에 대한 파일이다.
자세히 설명하면, 아래를 참고하자
Image Spec - Index
이름: Index
목표: 각 OS (Linux, Windows, Solaris 등등)와 CPU (amd64, arm64, ppc64le 등등) 별로 manifest의 id 가 어떻게 되는지 알려준다.
특이사항: Index는 사실 Optional이다. 없어도 된다.
Media Types:application/vnd.oci.image.index.v1+json
예시:ubuntu:latest
의 경우{ "mediaType": "application/vnd.oci.image.index.v1+json", "schemaVersion": 2, "manifests": [ { "digest": "sha256:83f0c2a8d6f266d687d55b5cb1cb2201148eb7ac449e4202d9646b9083f1cee0", "mediaType": "application/vnd.oci.image.manifest.v1+json", "platform": { "architecture": "amd64", "os": "linux" }, "size": 424 }, { "digest": "sha256:25de8a960c338b7d38aa15c012ceee70d8e29239db97596d6ecc50a5085d8f7a", "mediaType": "application/vnd.oci.image.manifest.v1+json", "platform": { "architecture": "arm64", "os": "linux", "variant": "v8" }, "size": 424 } /* 나머지는 생략 */ ] }
위와 같은 양식을 가지며 sha256sum 으로 계산된 digest 값으로 manifest를 찾을 수 있도록 digest 값이 명시되어 있다.
Image Spec - Manifest
이름: manifest
목표: 하나의 컨테이너 이미지를 조립하기 위한 정보를 제공한다. 정확히는 Config와 Layer에 대한 정보 제공을 목표로 한다.
Media Types:application/vnd.oci.image.manifest.v1+json
예시:ubuntu:latest
의amd64 / linux
이미지인sha256:83f0c2a8d6f266d687d55b5cb1cb2201148eb7ac449e4202d9646b9083f1cee0
경우{ "schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json", "config": { "mediaType": "application/vnd.oci.image.config.v1+json", "size": 2297, "digest": "sha256:99284ca6cea039c7784d1414608c6e846dd56830c2a13e1341be681c3ffcc8ac" }, "layers": [ // ubuntu 이미지는 base 이미지로 많이 사용되기때문에 단일 레이어로 구성되어있다. { "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "size": 29533050, // 28.16MB "digest": "sha256:6b851dcae6ca1461dde247915abc5048061f34332929ca8fb37d9dc18f2e2f44" } ] }
Image Spec - Config
이름: config
목표: 이 컨테이너 이미지를 실행하기 위한 메타데이터를 제공한다.
Media Types:application/vnd.oci.image.config.v1+json
예시: 위 예시에서 명시된sha256:99284ca6cea039c7784d1414608c6e846dd56830c2a13e1341be681c3ffcc8ac
의 경우{ "created": "2023-06-05T17:00:39.361599721Z", "docker_version": "20.10.21", "architecture": "amd64", "os": "linux", "rootfs": { "type": "layers", "diff_ids": [ "sha256:cdd7c73923174e45ea648d66996665c288e1b17a0f45efdbeca860f6dafdf731" // DiffID: 압축 해제한 Layer 에 대한 sha256sum 한 값 // ChainID(A) = DiffID(A) // ChainID(A|B) = Digest(ChainID(A) + " " + DiffID(B)) // ChainID(A|B|C) = Digest(ChainID(A|B) + " " + DiffID(C)) // diff_ids = ChainID // 각 레이어별로 적용시 무결점을 보장하기위한 메타데이터 ] }, "config": { "Hostname": "", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "Tty": false, "OpenStdin": false, "StdinOnce": false, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], "Cmd": [ "/bin/bash" ], "Image": "sha256:d17b4ab170168d7b2fbd685124f529e405acf8aada0933843bde9ee6330303ea",// 이 JSON 데이터를 sha256sum 한 것. Immutable 보장용도 "Volumes": null, "WorkingDir": "", "Entrypoint": null, "OnBuild": null, "Labels": { "org.opencontainers.image.ref.name": "ubuntu", "org.opencontainers.image.version": "22.04" } }, "history": [ { "created": "2023-06-05T17:00:37.587967605Z", "created_by": "/bin/sh -c #(nop) ARG RELEASE", "empty_layer": true }, { "created": "2023-06-05T17:00:37.631302347Z", "created_by": "/bin/sh -c #(nop) ARG LAUNCHPAD_BUILD_ARCH", "empty_layer": true }, { "created": "2023-06-05T17:00:37.67809659Z", "created_by": "/bin/sh -c #(nop) LABEL org.opencontainers.image.ref.name=ubuntu", "empty_layer": true }, { "created": "2023-06-05T17:00:37.722501654Z", "created_by": "/bin/sh -c #(nop) LABEL org.opencontainers.image.version=22.04", "empty_layer": true }, { "created": "2023-06-05T17:00:39.16987586Z", "created_by": "/bin/sh -c #(nop) ADD file:0ad2ee2cfb186802f49c9bf4148674d1c6fc201f83478ec01ffaa7086d491323 in / " }, { "created": "2023-06-05T17:00:39.361599721Z", "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", "empty_layer": true } ], // 이 밑 부터는 OCI 표준은 아니지만 Docker 에서 맘대로 사용하는 중 // 주로 `docker history` 명령어와 관련된 기능이라고 함. "container": "d0606c58733ffd6f0353d72893f1dec960e9b551c629c1f250029be012e0771f", "container_config": { "Hostname": "d0606c58733f", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "Tty": false, "OpenStdin": false, "StdinOnce": false, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], "Cmd": [ "/bin/sh", "-c", "#(nop) ", "CMD [\"/bin/bash\"]" ], "Image": "sha256:d17b4ab170168d7b2fbd685124f529e405acf8aada0933843bde9ee6330303ea", "Volumes": null, "WorkingDir": "", "Entrypoint": null, "OnBuild": null, "Labels": { "org.opencontainers.image.ref.name": "ubuntu", "org.opencontainers.image.version": "22.04" } } }
Image Spec - Layer
이름: layer
목표: 특정 레이어의 데이터를 제공해서 하나의 완전한 rootfs를 조립할 수 있도록 한다.
Media Types:application/vnd.oci.image.layer.v1.tar
: 단순 tar 일 경우application/vnd.oci.image.layer.v1.tar+gzip
: 위 tar을 gzip 한 경우application/vnd.oci.image.layer.nondistributable.v1.tar(+gzip)
: (주로 라이선스적인 이유로) 재배포되면 안 되는 레이어일 경우
예시:sha256:6b851dcae6ca1461dde247915abc5048061f34332929ca8fb37d9dc18f2e2f44
레이어를 압축해제하면 다음과 같은 디렉터리 구조를 가진 파일들이 나온다.
> file layer.tar layer.tar: gzip compressed data, was "977b072da3857fb0dbff2784372883885594f9e65793c8346da0f6c6cdd9711b.tar", last modified: Mon Jun 5 17:00:41 2023, max compression, original size modulo 2^32 80334848 > cat layer.tar | sha256sum 6b851dcae6ca1461dde247915abc5048061f34332929ca8fb37d9dc18f2e2f44 - # Layer Digest 값과 동일 > tar -xvf layer.tar > tree # 중간 생략 692 directories, 2820 files > tree -L 1 . ├── bin -> usr/bin ├── boot ├── dev ├── etc ├── home ├── lib -> usr/lib ├── lib32 -> usr/lib32 ├── lib64 -> usr/lib64 ├── libx32 -> usr/libx32 ├── media ├── mnt ├── opt ├── proc ├── root ├── run ├── sbin -> usr/sbin ├── srv ├── sys ├── tmp ├── usr └── var 21 directories, 0 files
참고로 아래와 같은 Dockerfile을 빌드할 경우
FROM ubuntu:latest RUN apt-get update
2번째 레이어의 상태는 다음과 같다.
> tar -xvf layer.tar tmp/ var/ var/lib/ var/lib/apt/ var/lib/apt/lists/ var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_jammy-backports_InRelease var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_jammy-backports_main_binary-amd64_Packages.lz4 var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_jammy-backports_universe_binary-amd64_Packages.lz4 var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_jammy-updates_InRelease var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_jammy-updates_main_binary-amd64_Packages.lz4 var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_jammy-updates_multiverse_binary-amd64_Packages.lz4 var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_jammy-updates_restricted_binary-amd64_Packages.lz4 var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_jammy-updates_universe_binary-amd64_Packages.lz4 var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_jammy_InRelease var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_jammy_main_binary-amd64_Packages.lz4 var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_jammy_multiverse_binary-amd64_Packages.lz4 var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_jammy_restricted_binary-amd64_Packages.lz4 var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_jammy_universe_binary-amd64_Packages.lz4 var/lib/apt/lists/auxfiles/ var/lib/apt/lists/lock var/lib/apt/lists/partial/ var/lib/apt/lists/security.ubuntu.com_ubuntu_dists_jammy-security_InRelease var/lib/apt/lists/security.ubuntu.com_ubuntu_dists_jammy-security_main_binary-amd64_Packages.lz4 var/lib/apt/lists/security.ubuntu.com_ubuntu_dists_jammy-security_multiverse_binary-amd64_Packages.lz4 var/lib/apt/lists/security.ubuntu.com_ubuntu_dists_jammy-security_restricted_binary-amd64_Packages.lz4 var/lib/apt/lists/security.ubuntu.com_ubuntu_dists_jammy-security_universe_binary-amd64_Packages.lz4
이런 느낌으로 이전 Layer 대비 차이점만을 기록해서 tar 파일 형태로 디렉터리구조 통째로 압축을 행한 것이 Layer 기반 이미지의 정체이다.
TMI 지만
docker pull
할 때 파일 아래 이미지와 같이 첫 번째 레이어부터 "Extracting"을 하는 것은 tar + gz 형태로 압축된 레이어를 첫 번째부터 풀면서 sha256sum 값을 구해 config에 명시된 diff_ids와 값이 동일한지를 검증하는 과정이다.OCI Distribution Spec
위 내용을 통해서 우리는 "컨테이너 이미지"라는 것은 사실
- index (json)
- manifest (json)
- config (json)
- layer (tar)
4가지 종류의 데이터로 구성되어 있다는 사실을 알게 되었다. 그럼 다운로드하아야 하는 데이터도 4가지라는 소리이다.
OCI Distribution Spec
은 이 4가지 종류의 데이터를 어떻게 다운받게 할지 HTTP 기반 API 스펙을 정의하고 있다. OpenAPI(Swagger) 기반으로 정의되면 정말 매우 많이 좋았겠지만 유감스럽게도 그건 아니고 모든 것은 줄글로 여기에 정의해 뒀다. 다만 줄글로 보기에는 내용이 모호하고 추상적인 부분이 많아서 개인적으로는 conformance test 쪽을 보는 것을 더 추천한다.핵심적인 요소 몇 개만 확인해 보면....
인증과 인가
확인해 보기 전에 먼저 이해해야 하는 것은 Distribution API의 인증과 인가이다. 결론부터 말하면, 인증과 인가는 OCI Distribution Spec에서 다뤄지지 않는다.
하지만 Docker hub는 인증과 인가 시스템이 들어가 있으며, 몇 가지 API 가 실제 작동하는지 알아보려면 Docker Hub 가 구현한 (그리고 Distribution 에도 같은 시스템이 적용되어 있으며, 먼저 말해두면 HARBOR 도 Distribution을 쓴다.) 인증 및 인가 시스템을 이해할 필요가 있다.
Docker hub (그리고 Distribution) 은 아키텍처적으로 실제
OCI Distribution Sepc
을 구현하며 이미지를 내려주는Registry
서버와 사용자에 대해 인증과 인가를 수행하는Authorization
서버 두 가지로 나눠져 있다.Authorization
서버는- 사용자의 인증 정보 (ID & PW / OAuth / Token 등등)
- 접근하려는 Repository 정보 / 수행할 Action 정보
를 받아 인증 및 해당 유저에 대한 인가 여부를 결정한 뒤 하나의 JWT를 만들어준다. 다음은
Authorization
서버로의 요청 예시이다.> curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/ubuntu:pull" {"token":"{JWT}","access_token":"{앞과 동일한 JWT, OAuth 표준 맞추려는 용도로 보임}","expires_in":300,"issued_at":"2023-06-29T13:36:19.076088341Z"}
Docker Hub의 경우 Anonnymous 유저로 Public Repository 접근이 허용되기 때문에
library/ubuntu:pull
에 대한 위 요청은 성공할 것이며 5분 (300초) 간 유효한 JWT 에는 다음과 같은 정보가 담기게 된다.// jwt.io { "access": [ { "type": "repository", "name": "library/ubuntu", "actions": [ "pull" ], "parameters": { "pull_limit": "100", "pull_limit_interval": "21600" } } ], "aud": "registry.docker.io", "exp": 1688017290, "iat": 1688016990, "iss": "auth.docker.io", "jti": "dckr_jti_ghuFBB82iRhCEytGqCsNzDPY6ug=", "nbf": 1688016690, "sub": "" }
보는 것과 같이
library/ubuntu
에 대하여pull
만 허용을 하는 JWT 가 발급된다. 이제 Registry 서버에 요청을 할 때Authorization
헤더에 이 토큰을 넣어주기만 하면library/ubuntu
pull
과 관련된 행위에 대해서는 5분간 유효한 요청을 보낼 수 있다.index 다운받기
OCI Image Spec에서 첫 관문인 index를 먼저 다운받아보자.
> export TOKEN="$(curl --silent --header 'GET' "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/ubuntu:pull" | jq -r '.token' )" > curl -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.oci.image.index.v1+json" https://registry.hub.docker.com/v2/library/ubuntu/manifests/latest | jq # 앞선 index 예시와 동일
주의사항은 Accept Header를 제대로 명시하지 않으면 다음과 같은 메시지가 뜬다.
> curl -H "Authorization: Bearer $TOKEN" -H "Accept: *" "https://registry.hub.docker.com/v2/library/ubuntu/manifests/latest" {"errors":[{"code":"MANIFEST_UNKNOWN","message":"OCI index found, but accept header does not support OCI indexes"}]}
메시지는
OCI index
를 찾았으나*
로 명시한 accept header 가OCI indexes
를 지원 안 한다고 한다;;
업스트림 쪽에 이슈가 올라간 걸로 알고 있는데 뭔가 뭔가 이유로 아직까지 처리 안된 걸로 알고 있다.여담이지만 Accept 헤더 없이 지원이 안 되는 이유는 현재 Image와 관련해서 두 가지 표준이 존재하기 때문이다.
하나는 Open Container Initiative에서 만든OCI Image Spec
,
나머지 하나는 Docker에서 만든Image Manifest V 2, Schema 2
(Schema 1 은 deprecated 되었지만docker save
명령어로 나오는 게 이 규격으로 알고 있다.)두 규격은 상당히 유사하지만 미묘하게 다른 점이 있어서 호환이 안된다. 만약 컨테이너를 만들 때
OCI Image Spec
에 맞춰 만들었으면application/vnd.oci.image.*
만 유효하며Image Manifest V 2, Schema 2
에 맞춰 만들었으면application/vnd.docker.*
만 유효하다.일례로 ubuntu 이미지의 경우 OCI 표준에 맞게 제작되기 때문에
Accept: application/vnd.oci.image.index.v1+json
를 통한 요청은 유효하다. 하지만Accept: application/vnd.docker.distribution.manifest.v2+json
은 유효하지 않다.반대로 nginx 이미지의 경우
Image Manifest V 2
에 따라 제작되기 때문에Accept: application/vnd.docker.distribution.manifest.v2+json
만 유효하다.보통은
Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.v2+json
와 같이 양쪽 다 Accept 한 뒤 Client 단에서 알아서 적절히 처리해 주지만 간혹 매우 옛날 시스템 (AWS ECR 이라던지 AWS Fargate 라던지 AWS Lambda 가 존재한다.) 은 한쪽만 지원을 해서 문제가 되는 경우가 존재했었다.
대게
docker build
로 생성한 이미지는vnd.docker*
로 그 외 나머지 (jib
,buildpack
,podman
등등)으로 생성된 것들은vnd.oci*
로 생성된다.manifest 다운 받기
본래 index는 optional이다. 멀티아키텍처를 지원하지 않는 경우 위 latest로 요청을 했을 때 index 대신 manifest 가 돌아온다.
하지만 ubuntu는 멀티아키텍처를 지원하므로 manifest를 받기 위해서는 latest 대신 sha256sum 으로 요청을 보내면 된다.> curl -H "Authorization: Bearer $TOKEN" -H "Accept: *" https://registry.hub.docker.com/v2/library/ubuntu/manifests/latest {"errors":[{"code":"MANIFEST_UNKNOWN","message":"OCI index found, but accept header does not support OCI indexes"}]}
layer, config 다운 받기
layer와 config는 둘 다 blob으로 취급된다.
> curl -L -H "Authorization: Bearer $TOKEN" https://registry.hub.docker.com/v2/library/ubuntu/blobs/sha256:99284ca6cea039c7784d1414608c6e846dd56830c2a13e1341be681c3ffcc8ac # 앞선 config 예시와 동일 > curl -L -H "Authorization: Bearer $TOKEN" https://registry.hub.docker.com/v2/library/ubuntu/blobs/sha256:6b851dcae6ca1461dde247915abc5048061f34332929ca8fb37d9dc18f2e2f44 # 앞선 layer 예시와 동일
한 가지 재미있는 것은 curl에
-L
옵션을 추가했다. 그 이유는 위 링크로 요청은 redirect 응답이 나오기 때문이다.> curl -H "Authorization: Bearer $TOKEN" https://registry.hub.docker.com/v2/library/ubuntu/blobs/sha256:99284ca6cea039c7784d1414608c6e846dd56830c2a13e1341be681c3ffcc8ac -v # 생략 < HTTP/1.1 307 Temporary Redirect < location: https://production.cloudflare.docker.com/registry-v2/docker/registry/v2/blobs/sha256/99/99284ca6cea039c7784d1414608c6e846dd56830c2a13e1341be681c3ffcc8ac/data?verify=1688002890-ZvtnD3I7N88OS5Wd0Z30MkuhHok%3D # 생략
registry.hub.docker.com
요청에 대해production.cloudflare.docker.com
로 리다이렉트 응답이 왔으며, 이는 딱 1시간 동안 유효한 서명을 받은 요청 링크이다. (verify
가 S3 표준은 아닌데 자체구현인지 다른 규격인지는 잘 모르겠다. 서론에 적은 기사에 따르면 실제 데이터는 AWS S3에 저장되어 있고 Cloudflare로 엣지캐싱이 이뤄지고 있다고 한다.)즉, 부하분산을 위해서 API에 대한 응답은
registry.hub.docker.com
에서 오지만 실제 대부분의 트래픽이 발생하는blob
데이터에 대해서는 Cloudflare를 사용하는 구조이다.이는 Distribution 에도 존재하는 옵션이고 취향 것 선택해서 적용하면 된다.
결론
이 글을 통해서 Container Image는
Index
,Manifest
,Config
,Layer
4가지 요소로 구성되어 있으며
curl을 통해서application/vnd.oci*
와application/vnd.docker*
가 대차게 싸우는 과정을 살펴보았다.개인적으로는
application/vnd.oci*
가 이겼으면 좋....application/vnd.docker*
가 이겼으면 좋겠다!후기
제발 Registry 가지고 삽질하는 구조 좀 그만보고 싶다
라는 생각으로 이 글을 썼습니다.제발 Harbor 찬양글 좀 그만 보고 싶다
라는 생각으로 이 글을 썼습니다.- 진짜 API 스키마 조금만 바꾸면 REST API 의 모범사례로 뽑을정도로 REST 의 모든 아키텍처적인 Constraints 를 만족하는데 HATEOAS 를 충족 못해서 모범사례라고 하긴 미묘하네요. (OpenStack..? 거긴 REST 하지만 API 는 개판입니다.)
- 혹시 쿠버네티스 혹은 컨테이너와 관련된 블로그 글 작성하기 재미난 주제 없을까요?
(아이디어는 몇 개 있는데 별로 글로 작성할 의욕이 없네요...)
'Kubernetes' 카테고리의 다른 글
[Kubernetes] Controlplane 죽이기 (1) 2023.06.26