on
Containerd の Resolver 実装のメモ
はじめに
Containerd の Resolver インターフェースについてみていく。 Resolver インターフェースは、イメージのプル/プッシュ時に独自のイメージプル/プッシュ機構に差し替える機能である。 WithResolver オプションを使用して、Pull/Push クライアントに渡すことができる。
Containerd のデフォルトでは、Docker Resolver が使用される。
本メモでは、プルに焦点を絞る。
Resolver インターフェース
v1.5.7 現在、次のメソッドを実装することでインターフェースを満たすことができる。
ref
はその Resolver が定めるイメージの URI フォーマットである。
Docker の Resolver であれば、スキーマレスのイメージの URI (例: docker.io/library/ubuntu)である。
Resolver インターフェース
|
|
Resolve
は、イメージ URI より、そのイメージの OCI Spec の Descriptor を返す。
Descriptor には、MediaType や Digest が含まれており、どのような種類のアーティファクトか判定できる。
例えば、MediaType より Image Index や Image Manifest であることがわかる。
Resolve
は、指定したイメージタグのイメージに対応する Descriptor が存在するか(解決できるか)を確認するメソッドと言える。
解決された Descriptor は Fetcher の Fetch で再度取得されるため、あくまで解決できるかの確認を担う。
Docker の Resolve は、Image Index(Image Manifest List)もしくは、Image Manifest へアクセスを試みてレスポンスから Descriptor を返す。 アクセスとは、具体的にはじめに HEAD リクエスト送り、Docker-Content-Digest ヘッダーがあればそれを使用する。 ヘッダーが存在しなければ続いて GET リクエストを送信してそのレスポンスからダイジェストを計算する。
Fetcher インターフェース
|
|
Fetcher
インターフェースのの Fetch
は Descriptor を引数に渡すことで、その Descriptor に対応したデータ(Config 等)をリモートからフェッチする。
Docker の Fetch は OCI Registory の実装に従っている。
例えば、渡された Descriptor の MediaType が Image Index や Manifest であれば、manifests
エンドポイントにリクエストをし、そうでなければ、(レイヤーと判断し) blobs
エンドポイントにリクエストをする。
プル時の Authorizer
Docker の Resolver は、Authorizer を使用する。 Token Authentication Specification にあるように Docker Hub からイメージをプルするためには、認可トークンが必要である。これは、パブリックイメージでも必要となる。
そのため、Image Index などを取得する際には、事前にトークンエンドポイントより認可トークンを取得する必要があり、そのトークンを使用して Manifest を取得する。
その仕組みが、Authorizer
インターフェースである。
|
|
Authorize
が、上記トークンエンドポイントから認可トークンを取得するメソッドである。
AddResponses
は、既存の Unauthorized(401) レスポインスより、上記認可するための情報を追加するメソッドである。
認可トークンなしで Manifest を取得しようとすると、Docker Hub のエンドポイントより Unauthorized(401) レスポンスが返却されるが、その際のレスポンスに WWW-Authenticate
ヘッダが含まれており、ヘッダ値に scope
、service
などが含まれている。
これらの値を使用することで、トークンエンドポイント の URL を動的に構築できる。
つまり、Unauthorized(401) レスポンスを、AddResponses
に渡すことで、認可トークン URL に必要な情報がパースされ、動的にトークンエンドポイント の URL を構築できる。
Authorize
は、構築したトークンエンドポイント の URL に対して HTTP リクエストを実施し認可トークンを取得する。
また、Authorize
は、Manifest などを取得するための http.Request
型の HTTP クライアントの Authorization ヘッダに取得した認可トークンを設定する。
認可トークン取得後のリクエストでは、Authorization ヘッダに認可トークンが設定されているため Manifest などを取得できる(結果イメージをプルできる)。
Docker Resolver の流れとしては次のようになる。
Resolve
によって ref から Image Index や Manifest の取得(HEAD/GET リクエスト)が実行されるが、認可トークンが設定されていないため Unauthorized(401) エラーとなる。Resolve
内で上記 HTTP リクエストのリトライ判定が行われる。Unauthorized(401) の場合、引数に 1. の HTTP レスポンスを渡してAddResponses
を実行する。そして、WWW-Authenticate
レスポンスヘッダから、認可エンドポイントの URL の情報がパースされる。そしてリトライ対象として判定される。- リトライにより再度 1. の実行を試みる。実際のリクエスト前に
Authorize
が実行され、トークンエンドポイントの URL に対して HTTP リクエストをする。成功すると、1. の HTTP クライアントの Authorization ヘッダに認可トークンが設定される。Authorization ヘッダが設定された HTTP クライアントにより、リトライの HTTP リクエストが実施され Manifest を取得できる(レイヤーなども取得可能)
カスタム Resolver 例
カスタム Resolver の例として、Amazon ECR containerd resolver が挙げられる。 AWS でコンテナを扱う際は、ECR のプライベートレジストリを使うことが多いと思われるが、containerd での ECR のプライベートレジストリの実装である。
ECR プライベートレジストリは、以下のように OCI Registory(Docker Registory V2)に対応している。
Amazon Elastic Container Registry のよくある質問
Q: Amazon ECR では、どのバージョンの Docker Registry API がサポートされていますか?
Amazon ECR でサポートされているのは、Docker Registry V2 API 仕様です。
そのため、ドキュメントの HTTP API 認証を使用するにあるように curl コマンドを使用してアクセスすることができる。
この際に Docker Hub と同じように Authorization
ヘッダに IAM のクレデンシャルをベースとした認可トークンが必要となる。
ECR プライベートレジストリローンチ当時 Docker が主流であり、Docker Hub と同様に(docker login さえすれば) docker コマンドで ECR プライベートレジストリにアクセスできるようにしたためと考えられる。
ECR プライベートレジストリは、上記 Docker Registry V2 の他に AWS API を使用してイメージのプルなど操作可能である。 イメージのプルに関しては、Image Index/Manifest を取得する BatchGetImage API と blobs のダウンロードするための URL を取得する GetDownloadUrlForLayer API である。 ECR Containerd Resolver は、ECR の API を直接実行で実現している。
Resolver インターフェースに照らし合わせると、Resolve
は ref(ecr.aws/arn:aws:ecr:<region>:<account>:repository/<name>:<tag>
フォーマット)をパースして、BatchGetImage API を実行して、Image Index か Manifest を取得し、その Descriptor を返す。
Fetch
の Fetcher
は、Image Index/Manifest や、Layer/Config など MediaType で分岐し、対応した API を実行する。
例えば、Layer/Config であれば、GetDownloadUrlForLayer API を実行し、その Layer/Config が保存されている S3 の署名付き URL を取得する。
取得した S3 の署名付き URL 対してリクエストすることで、レイヤーを取得する形となる。
なお、S3 の署名付き URL に対するリクエストは、並列度を設定することでメモリ使用量が増加する代わりに速度を向上可能である。。
Resolver インターフェース呼び出し流れ
クライアント実装の pull.go の fetch で呼ばれる。 https://github.com/containerd/containerd/blob/v1.5.7/pull.go#L128
まず、ref より Resolve を実行し、Image Index もしくは Manifest の Descriptor を取得する。 その次に Fetcher インターフェースを満たす実装を取得する。 Fetcher インターフェースの Fetch が指定した Descriptor を取得(ダウンロード)する。
|
|
Fetch の詳細
Descriptor に対してそれぞれの処理を実行するハンドラーを作成する。
ハンドラーの一つである images.ChildrenHandler
は Image Index もしくは Manifest の Descriptor の入力を想定している。
入力の Descriptor が Manifest であれば、boltdb から Layer と Config の Descriptor のリストを返し、Image Index であれば、Manifest のリストを返す。
images.FilterPlatforms
は、上記取得した Image Index などをプラットフォームなどでフィルターしている。例えば、Linux 環境であれば Image Index から Linux の Manifest を取得する。
images.FetchHandler
は、Fetcher インターフェースのラッパーである。
images.Handlers(handlers...)
は、個々のハンドラーを順番に同期的に実行する HandlerFunc 型のハンドラー関数を返す(束ねたハンドラーと呼ぶ)。
Image Index もしくは Image Manifest の Descriptor と、束ねたハンドラー関数を Dispatch に渡す。 すると、再起的に Dispatch が実行され、Fetcher の Fetch によりイメージのプルが行われる。
|
|
初回実行時の Dispatch メソッドへの descs
は Resolve で解決された Image Manifest もしくは、Image Index の一つだけである。
つまり、Dispatch 内での for-loop は一度である。
Dispatch では、渡された Descriptor 一つに対して一つの go routine が実行される。
初回では、 Image Manifest もしくは、Image Index に対して go routine が一つ起動する。
一方で、ある一つの Descriptor に対して束ねたハンドラーは同期的に実行される。
初回実行では handler.Handle(ctx2, desc)
によって、Image Manifest や Image Index の Descriptor に対して、 images.FetchHandler
や images.ChildrenHandler
などが実行される。
例えば、
descs
が Image Manifest であれば、FetchHandler(Fetcher の Fetch のラッパー)で、Image Manifest が フェッチされる。(この際に boltdb に保存される)次に、childrenHandler で、Image Manifest より、Image Config と Image Layers の Descriptor が返される(ネットワーク越しにフェッチするのではなく、boltdb の Image Manifest をパースする) childrenHandler の結果の複数の Descriptor が
children
変数になる。children
変数は 0 以上なので、再帰的に Dispatch が実行される。この際に引数のdescs
は Config や Layers となるので複数のリストである。Dispatch 内の for-loop で
descs
の数だけ対応した go routine が一つ実行される。go routine では、束ねたハンドラーがdesc
に対して実行される。例えば、Dispatch の for-loop で
desc
が Image Config となり、対応する go routine が起動する。- Image Config の Descriptor に対して FetchHandler(Fetcher の Fetch のラッパー)が Image Config をネットワーク越しに取得する。
- Image Config は、Image Index でも Image Manifest でもないので、childrenHandler は Descriptor を返さない。
Dispatch の for-loop の desc が Image Layer となり、対応する go routine が起動する。
- Image Layer の Descriptor より FetchHandler が Image Layer をネットワーク越しに取得する
- childrenHandler は Descriptor を返さない。
…..
Dispatch https://github.com/containerd/containerd/blob/v1.5.7/images/handlers.go#L120
|
|
上記により Config や Layer はそれぞれの go routine で実行されるため、並列にダウンロードされるようになっている。
なお、Dispatch では semaphore.Weighted を使用して、並列起動する go routine の数を制限している。
言い換えると、Config や Layer を並列でプルする数を制御できる。
Acquire
はセマフォを取得できるまでブロックされる。