knqyf263's blog

自分のためのメモとして残しておくためのブログ。

curlでdocker pullをする

コンテナレジストリからイメージをpullする時にcurlで行えたら便利なのになと思うことが誰しもあると思います。自分は2ヶ月に1回ぐらいそういう時がやってくるのですが、大体やり方を忘れていて非常に時間を無駄にしていることに気づいてしまったのでメモを残しておきます。

コマンド

このあと細かく説明を書いていますが、自分で備忘録的に見返すことが多いのでコマンドだけ先に書いておきます。以下はalpine:3.10を操作する例です。

Bearerトークン取得

$ export TOKEN=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/alpine:pull" | jq -r '.token')

マニフェストファイル取得

上のBearerトークン取得後に以下を実行。v2のスキーマが欲しい場合はAcceptヘッダが重要。

$ curl -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/alpine/manifests/3.10

Configファイル取得

historyとか色々載ってるやつ

$ export IMAGE_ID=$(curl -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/alpine/manifests/3.10 | jq -r .config.digest)
$ curl -L -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/alpine/blobs/$IMAGE_ID

Layer取得

以下は1つめのLayerをダウンロードする例

$ export LAYER_ID=$(curl -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/alpine/manifests/3.10 | jq -r '.layers[0].digest')
$ curl -L -o layer.tar.gz -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/alpine/blobs/$LAYER_ID

Docker Registry v2

とりあえず挙動を確認したいことが多いので、Docker Hubでのやり方だけ書いておきます。気が向いたら他のレジストリも書くかもしれません。

Docker Registry v2の認証の仕組みは以下に書いてあります。

docs.docker.com

上の図をふわっと要約すると以下のような流れです

  1. まずレジストリにpush/pullを試みます
  2. レジストリが認可を必要とする場合は、認証の方法とともに401を返してきます
  3. Bearerトークンを得るために指定された認可サービスに対してリクエストを送ります
  4. 認可サービスはBearerトークンを返します
  5. そのBearerトークンをAuthorizationヘッダに埋め込んで再度レジストリにリクエストを送ります
  6. レジストリはBearerトークンを見て検証を行い、push/pullを開始します

ということでやってみます。仕様の解説をしたいわけではないので説明とかオプションとかはちょくちょく省いてます。単にcurlを使ってdocker pullしてみるという話です。

レジストリへのpush/pull

レジストリにある各イメージはマニフェストファイルというものを持っているので、それをダウンロードしてみます。 以下ではalpine:3.10のイメージに対して操作を行います。他のイメージの場合も流れは基本同じです。公式イメージであればalpineの部分を他のイメージ名に差し替えればよいですし、独自のイメージであればlibrary/alpineの部分をorg_name/image_nameに差し替えて下さい。

$ curl -v https://registry-1.docker.io/v2/library/alpine/manifests/3.10

レジストリが401を返す

上のコマンドを打つと以下のようなレスポンスが返ってきます

< HTTP/1.1 401 Unauthorized
< Content-Type: application/json
< Docker-Distribution-Api-Version: registry/2.0
< Www-Authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/alpine:pull"
< Date: Thu, 28 Nov 2019 18:31:35 GMT
< Content-Length: 157
< Strict-Transport-Security: max-age=31536000
<
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"repository","Class":"","Name":"library/alpine","Action":"pull"}]}]}

このように401エラーが返ってきていることが分かります。ヘッダを見ると以下のようなWww-Authenticateヘッダが含まれています。

Www-Authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/alpine:pull"

https://auth.docker.io/token にserviceとscopeを指定してリクエストしろということみたいです。

認可サービスに対してBearerトークンを要求

service="registry.docker.io",scope="repository:library/alpine:pull"を付けろという話だったので、GETのクエリパラメータとして追加し、auth.docker.io にリクエストを投げます。この際、alpineはパブリックイメージなので特に認証は不要です。プライベートイメージを操作したい場合はユーザIDとパスワードをBasic認証する必要があるので、 -u $USER:$PASS が必要です。他にもOAuth2でも認証可能みたいですが自分はやったことはないです。

$ curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/alpine:pull"

認可サービスがBearerトークンを返却

$ curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/alpine:pull" | jq .
{
  "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlDK2pDQ0FwK2dBd0lCQWdJQkFEQUtCZ2dxaGtqT1BRUURBakJHTVVRd1FnWURWUVFERXpzeVYwNVpPbFZMUzFJNlJFMUVVanBTU1U5Rk9reEhOa0U2UTFWWVZEcE5SbFZNT2tZelNFVTZOVkF5VlRwTFNqTkdPa05CTmxrNlNrbEVVVEFlRncweE9UQXhNVEl3TURJeU5EVmFGdzB5TURBeE1USXdNREl5TkRWYU1FWXhSREJDQmdOVkJBTVRPMUpMTkZNNlMwRkxVVHBEV0RWRk9rRTJSMVE2VTBwTVR6cFFNbEpMT2tOWlZVUTZTMEpEU0RwWFNVeE1Pa3hUU2xrNldscFFVVHBaVWxsRU1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjY2bXkveXpHN21VUzF3eFQ3dFplS2pqRzcvNnBwZFNMY3JCcko5VytwcndzMGtIUDVwUHRkMUpkcFdEWU1OZWdqQXhpUWtRUUNvd25IUnN2ODVUalBUdE5wUkdKVTRkeHJkeXBvWGc4TVhYUEUzL2lRbHhPS2VNU0prNlRKbG5wNGFtWVBHQlhuQXRoQzJtTlR5ak1zdFh2ZmNWN3VFYWpRcnlOVUcyUVdXQ1k1Ujl0a2k5ZG54Z3dCSEF6bG8wTzJCczFmcm5JbmJxaCtic3ZSZ1FxU3BrMWhxYnhSU3AyRlNrL2tBL1gyeUFxZzJQSUJxWFFMaTVQQ3krWERYZElJczV6VG9ZbWJUK0pmbnZaMzRLcG5mSkpNalpIRW4xUVJtQldOZXJZcVdtNVhkQVhUMUJrQU9aditMNFVwSTk3NFZFZ2ppY1JINVdBeWV4b1BFclRRSURBUUFCbzRHeU1JR3ZNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVBCZ05WSFNVRUNEQUdCZ1JWSFNVQU1FUUdBMVVkRGdROUJEdFNTelJUT2t0QlMxRTZRMWcxUlRwQk5rZFVPbE5LVEU4NlVESlNTenBEV1ZWRU9rdENRMGc2VjBsTVREcE1VMHBaT2xwYVVGRTZXVkpaUkRCR0JnTlZIU01FUHpBOWdEc3lWMDVaT2xWTFMxSTZSRTFFVWpwU1NVOUZPa3hITmtFNlExVllWRHBOUmxWTU9rWXpTRVU2TlZBeVZUcExTak5HT2tOQk5sazZTa2xFVVRBS0JnZ3Foa2pPUFFRREFnTkpBREJHQWlFQXFOSXEwMFdZTmM5Z2tDZGdSUzRSWUhtNTRZcDBTa05Rd2lyMm5hSWtGd3dDSVFEMjlYdUl5TmpTa1cvWmpQaFlWWFB6QW9TNFVkRXNvUUhyUVZHMDd1N3ZsUT09Il19.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvYWxwaW5lIiwiYWN0aW9ucyI6WyJwdWxsIl19XSwiYXVkIjoicmVnaXN0cnkuZG9ja2VyLmlvIiwiZXhwIjoxNTc0OTY5MTI4LCJpYXQiOjE1NzQ5Njg4MjgsImlzcyI6ImF1dGguZG9ja2VyLmlvIiwianRpIjoiNXRlVzl6bktuby1CR3Vad1c1aTYiLCJuYmYiOjE1NzQ5Njg1MjgsInN1YiI6IiJ9.rjvWkJSKgw9f6_-kE4kcsYHDUEwVvIE-FeBkdDSG9ExBtYp9YK5hKIsOSlIEfGzsLFgWpEXtRV4h4FwP0jQ6BDCVHKvyQdhtnSm4Ad3BIcMFX87DgnGbYfboOzo9INpeCsMa8Hy44VH_4RbtdBVb7MYN1pFMN8za3cNYt-AF0YWsp7L86HWzJ4-Tp4-WS0JXT3fvQlevoYVrWNr7Nrl15aVx16_RT9S4Vhmrbc30vrNHzwOh9vEk6VLvxZiro8RiGdPgnXWMsplmTZNTkHygT5N8MEBcyEJNsvWKLcbPtEU5lk4ZMTRoyVKeIT8LyNXAWu_KATTIXXl1hHrENEcBsw",
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlDK2pDQ0FwK2dBd0lCQWdJQkFEQUtCZ2dxaGtqT1BRUURBakJHTVVRd1FnWURWUVFERXpzeVYwNVpPbFZMUzFJNlJFMUVVanBTU1U5Rk9reEhOa0U2UTFWWVZEcE5SbFZNT2tZelNFVTZOVkF5VlRwTFNqTkdPa05CTmxrNlNrbEVVVEFlRncweE9UQXhNVEl3TURJeU5EVmFGdzB5TURBeE1USXdNREl5TkRWYU1FWXhSREJDQmdOVkJBTVRPMUpMTkZNNlMwRkxVVHBEV0RWRk9rRTJSMVE2VTBwTVR6cFFNbEpMT2tOWlZVUTZTMEpEU0RwWFNVeE1Pa3hUU2xrNldscFFVVHBaVWxsRU1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjY2bXkveXpHN21VUzF3eFQ3dFplS2pqRzcvNnBwZFNMY3JCcko5VytwcndzMGtIUDVwUHRkMUpkcFdEWU1OZWdqQXhpUWtRUUNvd25IUnN2ODVUalBUdE5wUkdKVTRkeHJkeXBvWGc4TVhYUEUzL2lRbHhPS2VNU0prNlRKbG5wNGFtWVBHQlhuQXRoQzJtTlR5ak1zdFh2ZmNWN3VFYWpRcnlOVUcyUVdXQ1k1Ujl0a2k5ZG54Z3dCSEF6bG8wTzJCczFmcm5JbmJxaCtic3ZSZ1FxU3BrMWhxYnhSU3AyRlNrL2tBL1gyeUFxZzJQSUJxWFFMaTVQQ3krWERYZElJczV6VG9ZbWJUK0pmbnZaMzRLcG5mSkpNalpIRW4xUVJtQldOZXJZcVdtNVhkQVhUMUJrQU9aditMNFVwSTk3NFZFZ2ppY1JINVdBeWV4b1BFclRRSURBUUFCbzRHeU1JR3ZNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVBCZ05WSFNVRUNEQUdCZ1JWSFNVQU1FUUdBMVVkRGdROUJEdFNTelJUT2t0QlMxRTZRMWcxUlRwQk5rZFVPbE5LVEU4NlVESlNTenBEV1ZWRU9rdENRMGc2VjBsTVREcE1VMHBaT2xwYVVGRTZXVkpaUkRCR0JnTlZIU01FUHpBOWdEc3lWMDVaT2xWTFMxSTZSRTFFVWpwU1NVOUZPa3hITmtFNlExVllWRHBOUmxWTU9rWXpTRVU2TlZBeVZUcExTak5HT2tOQk5sazZTa2xFVVRBS0JnZ3Foa2pPUFFRREFnTkpBREJHQWlFQXFOSXEwMFdZTmM5Z2tDZGdSUzRSWUhtNTRZcDBTa05Rd2lyMm5hSWtGd3dDSVFEMjlYdUl5TmpTa1cvWmpQaFlWWFB6QW9TNFVkRXNvUUhyUVZHMDd1N3ZsUT09Il19.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvYWxwaW5lIiwiYWN0aW9ucyI6WyJwdWxsIl19XSwiYXVkIjoicmVnaXN0cnkuZG9ja2VyLmlvIiwiZXhwIjoxNTc0OTY5MTI4LCJpYXQiOjE1NzQ5Njg4MjgsImlzcyI6ImF1dGguZG9ja2VyLmlvIiwianRpIjoiNXRlVzl6bktuby1CR3Vad1c1aTYiLCJuYmYiOjE1NzQ5Njg1MjgsInN1YiI6IiJ9.rjvWkJSKgw9f6_-kE4kcsYHDUEwVvIE-FeBkdDSG9ExBtYp9YK5hKIsOSlIEfGzsLFgWpEXtRV4h4FwP0jQ6BDCVHKvyQdhtnSm4Ad3BIcMFX87DgnGbYfboOzo9INpeCsMa8Hy44VH_4RbtdBVb7MYN1pFMN8za3cNYt-AF0YWsp7L86HWzJ4-Tp4-WS0JXT3fvQlevoYVrWNr7Nrl15aVx16_RT9S4Vhmrbc30vrNHzwOh9vEk6VLvxZiro8RiGdPgnXWMsplmTZNTkHygT5N8MEBcyEJNsvWKLcbPtEU5lk4ZMTRoyVKeIT8LyNXAWu_KATTIXXl1hHrENEcBsw",
  "expires_in": 300,
  "issued_at": "2019-11-28T19:20:28.603403106Z"
}

上記のようなレスポンスを得ます。token, access_token, expires_in, issued_atがありますが、とりあえずtokenがBearerトークンになっているのでこれだけ取っておけば良いです。細かく知りたい人は以下を呼んで下さい。

Token Authentication Specification | Docker Documentation

いちいち手で取得するのも面倒なので、jqを使って環境変数に入れておきます。

$ export TOKEN=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/alpine:pull" | jq -r '.token')

ちなみにBearerトークンのexpires_inは300と書いており5分しか有効ではないため、401が出るようになったら再度上のコマンドを打ってBearerトークンを更新する必要があります。

Bearerトークンを使ってレジストリにリクエストを送信

あとはAuthorizationヘッダに上のBearerトークンを入れるだけです。今度こそマニフェストファイルを取得してみます。

$ curl -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/alpine/manifests/3.10
{
   "schemaVersion": 1,
   "name": "library/alpine",
   "tag": "3.10",
   "architecture": "amd64",
   "fsLayers": [
      {
         "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
      },
      {
         "blobSum": "sha256:89d9c30c1d48bac627e5c6cb0d1ed1eec28e7dbdfbcc04712e4c79c0f83faf17"
      }
   ],
   "history": [
      {
         "v1Compatibility": "{\"architecture\":\"amd64\",\"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/sh\"],\"ArgsEscaped\":true,\"Image\":\"sha256:e8bf85e28fac8a4cd1707985780af20622f0f5de7d6c912ea1dc82a626981cb0\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"container\":\"baae288169b1ae2f6bd82e7b605d8eb35a79e846385800e305eccc55b9bd5986\",\"container_config\":{\"Hostname\":\"baae288169b1\",\"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/sh\\\"]\"],\"ArgsEscaped\":true,\"Image\":\"sha256:e8bf85e28fac8a4cd1707985780af20622f0f5de7d6c912ea1dc82a626981cb0\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"created\":\"2019-10-21T17:21:42.387111039Z\",\"docker_version\":\"18.06.1-ce\",\"id\":\"66a1145c315c6751d846723eb45515a780f1658f77dad2eb318b497d0da6b01a\",\"os\":\"linux\",\"parent\":\"3096cc24d0eb306b978ec89242e14a6285b20272f98feaae7327b34fb70bf400\",\"throwaway\":true}"
      },
      {
         "v1Compatibility": "{\"id\":\"3096cc24d0eb306b978ec89242e14a6285b20272f98feaae7327b34fb70bf400\",\"created\":\"2019-10-21T17:21:42.078618181Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD file:fe1f09249227e2da2089afb4d07e16cbf832eeb804120074acd2b8192876cd28 in / \"]}}"
      }
   ],
   "signatures": [
      {
         "header": {
            "jwk": {
               "crv": "P-256",
               "kid": "H67G:5NV2:2QJK:NNMD:SAEE:N56S:JDRH:XDSX:ON5Y:WM6G:HHYQ:RANM",
               "kty": "EC",
               "x": "4TjKKDnLECLUP_NjC3U4z1-ePiAyQSVz1FFKgVNwhgk",
               "y": "tkdpqye8X9jwqAcN3_aSr56QOTEJg6flURPbqA3Dbmg"
            },
            "alg": "ES256"
         },
         "signature": "PL-llANjJFNRBZ7_hEtra4NXJ8s4pgY_MP4wpCjDs8CsqPDVf8Kp4eGjE56ejvnrFzcjsCwbxkE8uRcxLMJU3A",
         "protected": "eyJmb3JtYXRMZW5ndGgiOjIxMzksImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxOS0xMS0yOFQxOTozMDozNVoifQ"
      }
   ]
}

ということで取得できたのですが、実はこれは罠でv1のSchemaになっています。

docs.docker.com

単に自分が無知だっただけで罠でも何でもないのですが、v2のSchemaを得るためにはAcceptヘッダが必要でした。何も指定しないと "application/vnd.docker.distribution.manifest.v1+json" 相当になるようです。以下のページでMedia Typeについて記載がありますが、とりあえず "application/vnd.docker.distribution.manifest.v2+json"辺りを付けておくと良さそうです。

docs.docker.com

$ curl -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/alpine/manifests/3.10
{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
   "config": {
      "mediaType": "application/vnd.docker.container.image.v1+json",
      "size": 1512,
      "digest": "sha256:965ea09ff2ebd2b9eeec88cd822ce156f6674c7e99be082c7efac3c62f3ff652"
   },
   "layers": [
      {
         "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
         "size": 2787134,
         "digest": "sha256:89d9c30c1d48bac627e5c6cb0d1ed1eec28e7dbdfbcc04712e4c79c0f83faf17"
      }
   ]
}

ということでv2のschemaを得ました。

Configファイルを取得する

上のマニフェスト内にdigestというkeyがあります。こいつがイメージのID相当のやつのはずです。レジストリでは実際のファイルなどはblobとして管理されているので、Configファイルをダウンロードする際にもblobのAPIを叩く必要があります。

docs.docker.com

これも手で取得するのは面倒なのでjqを使います。

$ export IMAGE_ID=$(curl -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/alpine/manifests/3.10 | jq -r .config.digest)

blobを取得します。

$ curl -v -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/alpine/blobs/$IMAGE_ID
< HTTP/1.1 307 Temporary Redirect
< Content-Type: application/octet-stream
< Docker-Distribution-Api-Version: registry/2.0
< Location: https://production.cloudflare.docker.com/registry-v2/docker/registry/v2/blobs/sha256/96/965ea09ff2ebd2b9eeec88cd822ce156f6674c7e99be082c7efac3c62f3ff652/data?verify=1574972987-k95e45HTysMmwaTRSq%2FAkptLEII%3D
< Date: Thu, 28 Nov 2019 19:39:47 GMT
< Content-Length: 0
< Strict-Transport-Security: max-age=31536000

リダイレクトされます。上記のLocationヘッダのURLに自分でアクセスしても良いですし、curlの-Lオプションを使ってcurlでリダイレクト先にアクセスしても良いです。

$ curl -L -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/alpine/blobs/$IMAGE_ID | jq .
{
  "architecture": "amd64",
  "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/sh"
    ],
    "ArgsEscaped": true,
    "Image": "sha256:e8bf85e28fac8a4cd1707985780af20622f0f5de7d6c912ea1dc82a626981cb0",
    "Volumes": null,
    "WorkingDir": "",
    "Entrypoint": null,
    "OnBuild": null,
    "Labels": null
  },
  "container": "baae288169b1ae2f6bd82e7b605d8eb35a79e846385800e305eccc55b9bd5986",
  "container_config": {
    "Hostname": "baae288169b1",
    "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/sh\"]"
    ],
    "ArgsEscaped": true,
    "Image": "sha256:e8bf85e28fac8a4cd1707985780af20622f0f5de7d6c912ea1dc82a626981cb0",
    "Volumes": null,
    "WorkingDir": "",
    "Entrypoint": null,
    "OnBuild": null,
    "Labels": {}
  },
  "created": "2019-10-21T17:21:42.387111039Z",
  "docker_version": "18.06.1-ce",
  "history": [
    {
      "created": "2019-10-21T17:21:42.078618181Z",
      "created_by": "/bin/sh -c #(nop) ADD file:fe1f09249227e2da2089afb4d07e16cbf832eeb804120074acd2b8192876cd28 in / "
    },
    {
      "created": "2019-10-21T17:21:42.387111039Z",
      "created_by": "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
      "empty_layer": true
    }
  ],
  "os": "linux",
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:77cae8ab23bf486355d1b3191259705374f4a11d483b24964d2f729dd8c076a0"
    ]
  }
}

ということでConfigファイルを得ました。ちなみにこのConfigファイルのハッシュ値がイメージのIDになっています。

$ echo $IMAGE_ID
sha256:965ea09ff2ebd2b9eeec88cd822ce156f6674c7e99be082c7efac3c62f3ff652
$ curl -L -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/alpine/blobs/$IMAGE_ID > alpine-310.json
$ sha256sum alpine-310.json
965ea09ff2ebd2b9eeec88cd822ce156f6674c7e99be082c7efac3c62f3ff652  alpine-310.json

そして余談ですが、全く同じDockerfileをキャッシュを使わずに2回ビルドすると異なるイメージIDになりますが、上記Configのhistoryなどにcreated_byなどの時刻が含まれており、時刻は当然ビルドするたびに変わるためハッシュ値が変化し結果としてイメージIDも変わります。

Layerのダウンロード

では実際にレイヤーをダウンロードしてみます。 先程のマニフェスト内にLayerのIDが書いてあるので、まずそちらを再度取得します。

$ curl -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/alpine/manifests/3.10 |  jq '.layers[].digest'
"sha256:89d9c30c1d48bac627e5c6cb0d1ed1eec28e7dbdfbcc04712e4c79c0f83faf17"

今回は一つしかないですが、複数のLayerで構成されているイメージの場合はこれが複数になります。あとはこのIDをblobsにくっつけてリクエストするだけです。圧縮されているので、tar.gzで降ってきます。 マニフェストファイルにはMedia Typeが"application/vnd.docker.image.rootfs.diff.tar.gzip"と書いてあるので、ヘッダにつけておいたほうが無難な気もしますがDocker Hubでは指定しなくても同じファイルでした。違うレジストリだとまた異なる挙動かもしれません。

$ curl -L -o layer.tar.gz -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/alpine/blobs/sha256:89d9c30c1d48bac627e5c6cb0d1ed1eec28e7dbdfbcc04712e4c79c0f83faf17

解凍してみます。

$ tar zxvf layer.tar.gz 
x bin/
x bin/arch
x bin/ash
x bin/base64
x bin/bbconfig
...

ということでLayerに含まれるファイルを手に入れることができました。Layerが複数ある場合はもちろん並列に取得も可能です。

参考

まとめ

普段docker pullとかしていてもあまり裏側の通信がどうなってるか意識することは少ないですが、curlを使って自分で簡易的に再現してみると理解が深まるかもしれません。