はじめに
社内用イメージをタグを切りながら頻繁に更新していたらPrivateRegitryが明らかに肥大し始めたので何とかしたいなぁと考えておりました。いらなくなったイメージもあったりするし。
Registryのバージョン2.3以降の標準APIにDELETEメソッド(https://docs.docker.com/registry/spec/api/#deleting-an-image)があるんですが、これには下記の手順が必要となります。
- ヘッダーに
Accept: application/vnd.docker.distribution.manifest.v2+json
を追加して、Get /v2/<イメージ名>/manifests/<タグ>
でマニフェストを取得(https://docs.docker.com/registry/spec/api/#pulling-an-image)。 - レスポンスのヘッダーに付いてくる[Docker-Content-Digest]を確認。
-
Delete /v2/<イメージ名>/manifests/<Docker-Content-Digest>
の実行。202が返ってきたら成功です。 - 最後にregistryコンテナ本体で
bin/registry garbage-collect /path/to/config.yml
(https://docs.docker.com/registry/garbage-collection/)を実行してCG(ガーベッジコレクション)を起動させて終了。
以上の1~3までをタグ数分手打ちで繰り返すのは面倒です。
とりあえず自分で書こう
世の中には自動処理のスクリプトやPortusなんてのがあったりしますが、とりあえず自分でGoを使ってコマンドラインアプリを書いてみようかと思います。
用意する物
- Go 1.9
- urfave/cli コマンドラインアプリを簡単に作れるライブラリ(解説しません)
以上。
アプリのイメージ
command -tags=v0.0.1,v0.0.2 <image name>
ってしたら対象イメージのv0.0.1とv0.0.2のタグが削除されるって感じでいければなぁってところです。
とりあえずコマンド名は「drcleaner」とでもします(これできれいになるわけではありませんが…)。
Docker-Content-Digestを取得する
これが無いとDeleteを呼べないのでまずはこれから。
package main
import(
"errors"
"fmt"
"net/http"
)
var client http.Client
func getDigest(u, i, t string) (string, error) {
url := fmt.Sprintf("%s/v2/%s/manifests/%s", u, i, t)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json")
res, err := client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode < 200 || 300 < res.StatusCode {
return "", errors.New(res.Status)
}
return res.Header.Get("Docker-Content-Digest"), nil
}
標準のhttpライブラリの使い方ってこれであってるのか?って感じもしますが、中身自体は簡単なので説明することもないです。
タグを削除する
Docker-Content-Digestを得たので削除メソッドを呼びます。
// 上記の続き
func deleteTag(u, i, d string) error {
url := fmt.Sprintf("%s/v2/%s/manifests/%s", u, i, d)
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return err
}
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode < 200 || 300 < res.StatusCode {
return errors.New(res.Status)
}
return nil
}
わざわざ書く必要あったのかって気がしてきましたが、とりあえずこれで削除することができるはず。
コマンドラインアプリ化
cliライブラリを使ってコマンドラインアプリ化します。
package main
import (
// 省略
"github.com/urfave/cli"
)
func main() {
app := cli.NewApp()
app.Name = "drcleaner"
app.Usage = "Docker Registry Cleaner"
app.Version = "0.0.1"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "url, U",
Value: "localhost:5000",
},
cli.StringSliceFlag{
Name: "tags, T",
},
}
app.Action = func(c *cli.Context) error {
fmt.Println("start")
i := c.Args().First()
u := c.String("U")
ts := c.StringSlice("T")
for _, t := range ts {
d, err := getDigest(u, i, t)
if err != nil {
return cli.NewExitError(err.Error(), 86)
}
if err := deleteTag(u, i, d); err != nil {
return cli.NewExitError(err.Error(), 86)
}
}
fmt.Println("finish")
return nil
}
app.Run(os.Args)
}
// getDigest
// deleteTag
タグの配列指定がGoっぽくになってしまいましたが(-tags={v0.0.1,v0.0.2}
)、だいたいイメージ通りに実行できるはずです。
実行
まずは、テスト用にDeleteを実行可能にしたレジストリ(https://docs.docker.com/registry/configuration/#delete)を立てて、何でもいいのでpush。
$: docker run -d -p 5000:5000 -v `pwd`/config.yml:/etc/docker/registry/config.yml --name registry registry:2
$: docker tag busybox localhost:5000/busybox:v0.0.1
$: docker push localhost:5000/busybox:v0.0.1
drcleanerを実行してみる。
$: go get -u github.com/lightstaff/drcleaner
$: drcleaner -T=v0.0.1 busybox
で無事、タグが削除されていれば成功です。
$: curl http://localhost:5000/v2/busybox/tags/list
{"name":"busybox","tags":null}
使ってみて
- 複数指定したタグ(例えばv1とかv2)に差分が無い場合、2つめの実行時にNotFoundが返ってきて、実際にそのタグが消えている。レイヤーが一緒だからまとめて削除されてんのか?
- 結局、RegitryのGCが呼ばれるまではきれいになるわけではない。
- GC後も、イメージ名だけは残ってしまう。これは手動でやっても同じ現象が発生するのでRegistry側の仕様なのか?
結論
Registry側のDelete後の動作に関しては、よく分かんない部分があるが、とりあえずタグに絞って削除することは出来るようになったということにしておこう。
ちょっと機能を足した成果物はこちらhttps://github.com/lightstaff/drcleaner