前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Docker容器Registry私有镜像仓库安全配置与GC回收实践

Docker容器Registry私有镜像仓库安全配置与GC回收实践

作者头像
全栈工程师修炼指南
发布2022-09-29 17:14:36
2.1K0
发布2022-09-29 17:14:36
举报
文章被收录于专栏:全栈工程师修炼之路

[TOC]

0x00 前言简述

描述:本来我想直接写Harbor的Docker镜像仓库搭建配置与使用,但是觉得还是应该从基础的Docker的Registry镜像讲起从安全构建到GC回收同时加深学习映像;

官网介绍:Registry是一个无状态的,高度可扩展的服务器端应用程序,存储,让你分发图像。它是开源的根据许可Apache许可证。

此时就不在详细讲解Registry的介绍了,有兴趣的朋友可以参见官网文档或者前面我笔记中的介绍; 以下是一些知识点复习:

  • (1) Registry是一个几种存放image并对外提供上传下载以及一系列API的服务,Registry与镜像的关系可以想象成自己机器上的源码和远端SVN或者Git服务的关系,可以很容易和本地源代码以及远端Git服务的关系相对应。
  • (2) Registry开源常用于构建私有镜像仓库;

Q:为什么不直接采用Docker官网Hub作为存储镜像的地方?

答:我认为主要是以下几个方面的影响 1.存储空间有限 2.上传/拉取速度有限 3.企业内部敏感开发项目(如果是您肯定不会上传到别人的服务器中) 4.免费开源

反之使用Registey好处:

  • 镜像存储位置由您掌握
  • 全面管理控制自己的镜像
  • 镜像存储和分配紧密集成到您的内部开发流程

Registry 版本说明:

  • Docker Registry 1.0版本(hub/docker.io等公共的镜像仓库还支持,安全性以及兼容性不如V2.0)
  • Docker Registry 2.0版本在安全性和性能上做了诸多优化,并重新设计了镜像的存储的格式;(Docker目前1.6之后支持V2)

名词解释:

1.repository name(存储库名词) 存储库指在库中存储的镜像。/project/redis:latest

  • 经典存储库名称由2级路径构成,每级路径小于30个字符,V2的api不强制要求这样的格式。
  • 每级路径名至少有一个小写字母或者数字,使用句号,破折号和下划线分割。更严格来说,它必须符合正则表达式:[a-z0-9]+[._-][a-z0-9]+)
  • 多级路径用/分隔
  • 存储库名称总长度(包括/)不能超过256个字符

2.digest(摘要) 摘要是镜像每个层的唯一标示。虽然算法允许使用任意算法,但是为了兼容性应该使用sha256。

代码语言:javascript
复制
例如 - sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b
# 用一个简单的例子,在伪代码来演示摘要计算
let C = 'a small string'
let B = sha256(C)
let D = 'sha256:' + EncodeHex(B)
let ID(C) = D

# python伪代码
import hashlib
C = 'a small string'
B = hashlib.sha256(C)
D = 'sha256:' + B.hexdigest()

0x01 基础安装

测试环境:

代码语言:javascript
复制
# OS
CentOS Linux release 7.8.2003 (Core)

# Linux
Server Version: 19.03.9
Storage Driver: overlay2
1) 基础命令

基础实例:

代码语言:javascript
复制
# 1.Start your registry
docker run -d -p 5000:5000 --restart=always --name registry registry:2

# 2.拉取镜像并修改
docker pull ubuntu
docker run --name ubuntu-test -d ubuntu 
docker commit -a "Weiyigeek" -m "镜像描述" ubuntu-test ubuntu-custom:1.0

# 3.Tag the image so that it points to your registry
docker image tag ubuntu-custom:1.0 127.0.0.1:5000/ubuntu-custom:1.0

# 4.从镜像仓储上传与下载镜像
docker push localhost:5000/ubuntu-custom:1.0
docker pull localhost:5000/ubuntu-custom:1.0

# 5.删除本地/远程的ubuntu-custom:1.0
docker image remove ubuntu-custom:1.0
docker image remove localhost:5000/ubuntu-custom:1.0

# 6.registry仓库删除与清除卷;
docker container stop registry && docker container rm -v registry
2) 基础配置

Registry 镜像环境变量:

代码语言:javascript
复制
# Environment variable
# 自定义Registry仓库镜像存放的物理地址
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/var/lib/registry
# 启用DELETE操作否则不能执行 -X DELETE
REGISTRY_STORAGE_DELETE_ENABLED=true

# 绑定Registry地址与端口
REGISTRY_HTTP_ADDR=0.0.0.0:5000

# 证书Certificate设置
REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt
REGISTRY_HTTP_TLS_KEY=/certs/domain.key

# 本地基础认证
REGISTRY_AUTH=htpasswd
REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm
REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd

指定环境变量运行Registry仓库:

代码语言:javascript
复制
# (1) docker run 时指定环境变量
$ docker run -d \
  --restart=always \
  --name registry \
  -v "$(pwd)"/auth:/auth \
  -v /opt/docker/registry:/var/lib/registry \
  -v "$(pwd)"/certs:/certs \
  -e REGISTRY_HTTP_ADDR=0.0.0.0:443 \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
  -e REGISTRY_STORAGE_DELETE_ENABLED=true \
  -p 443:443 \
  registry:2
代码语言:javascript
复制
# (2) Yaml 配置
docker run -d -p 5000:5000 --restart=always --name registry \
  -v `pwd`/config.yml:/etc/docker/registry/config.yml \
  registry:2

# 例如:开发配置的config.yaml
version: 0.1
log:
  level: debug
storage:
  filesystem:
    rootdirectory: /var/lib/registry
http:
  addr: localhost:5000
  host: https://registry.weiyigeek.top:5000
  secret: asecretforlocaldevelopment
  debug:
    addr: localhost:5001

auth:
  htpasswd:
    realm: basic-realm
    path: /path/to/htpasswd

参考地址:https://github.com/docker/distribution/blob/master/cmd/registry/config-example.yml

代码语言:javascript
复制
# (3) docker-compose 构建镜像仓库
# docker-compose.yml 
registry:
  restart: always
  image: registry:2
  ports:
    - 5000:5000
  environment:
    REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
    REGISTRY_HTTP_TLS_KEY: /certs/domain.key
    REGISTRY_AUTH: htpasswd
    REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
    REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
    REGISTRY_STORAGE_DELETE_ENABLED=true
  volumes:
    - /opt/docker/registry:/var/lib/registry
    - /opt/docker/certs:/certs
    - /opt/docker/auth:/auth
3) 生产实例

描述:此处以实际生产环境为例进行Docker Registry私有仓库搭建;

3.1) 服务器拉取Docker Registry镜像并创建Autho认证的.htpasswd文件;

代码语言:javascript
复制
$ docker pull registry:2
# Digest: sha256:8be26f81ffea54106bae012c6f349df70f4d5e7e2ec01b143c46e2c03b9e551d
# Status: Downloaded newer image for registry:2
# docker.io/library/registry:2

$ mkdir -vp /opt/registry/ && cd $_
htpasswd -Bbn weiyigeek 123456                  # -n 不更新文件输入到终端标准输出
htpasswd -bB -c auth.htpasswd weiyigeek 123456  # -c 创建存储认证字符串,-b 强制加密密码,-B 在命令行中接收密码

Tips:在Push或者Delete镜像是通过HTTP请求Registry的API完成的,每个请求都需要一个Token才能完成操作,而此Token需要使用auth文件(明文用户/密码编码)来进行鉴权;

3.2) Docker Registry 自签证书 描述:如果Registry仓库使用TLS认证时必须带有证书,当外部访问该Registry仓库时候提供安全通道,我们可以在认证机构购买签名证书或者自签证书也可以; 使用 OpenSSL 来生成 CA (证书授权中心,certificate authority)、 中级 CA(intermediate CA) 和末端证书(end certificate)。包括 OCSP、CRL 和 CA颁发者Issuer信息、具体颁发和失效日期。

代码语言:javascript
复制
# 方式1:交互式证书生成
$openssl req -newkey rsa:4096 -nodes -sha256 -keyout ./certs/domain.key -x509 -days 365 -out ./certs/domain.crt

# 方式2:配置文件方式生成
cat >ca.conf <<EOF
[ req ]
default_bits  = 2048
distinguished_name = req_distinguished_name
prompt   = no
encrypt_key  = no
x509_extensions  = v3_ca
[ req_distinguished_name ]
CN = localhost
[ CA_default ]
copy_extensions = copy
[ alternate_names ]
DNS.2=localhost
[ v3_ca ]
subjectAltName=@alternate_names
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints = critical,CA:true
keyUsage=keyCertSign,cRLSign,digitalSignature,keyEncipherment,nonRepudiation
EOF
openssl req -days 365 -x509 -config ca.conf -new -keyout certs/domain.key -out certs/domain.crt

3.3) 挂载安全认证与自签证书并启动registry容器

代码语言:javascript
复制
docker run -d -p 0.0.0.0:8443:5000 --name registry-net \
    -v /var/lib/registry-net:/var/lib/registry \
    -v /opt/registry/certs:/certs \
    -v /opt/registry/auth.htpasswd:/etc/docker/registry/auth.htpasswd \
    -e REGISTRY_AUTH="{htpasswd: {realm: localhost, path: /etc/docker/registry/auth.htpasswd}}" \
    -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 \
    -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
    -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
    -e REGISTRY_STORAGE_DELETE_ENABLED=true \
    registry

# 查看容器(只能本地访问)
$docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                     NAMES
86bc983e8183        registry            "/entrypoint.sh /etc…"   About a minute ago   Up About a minute   127.0.0.1:443->5000/tcp   registry

3.4) 验证registry是否可登录,登录后账号密码将会base64存储在 /root/.docker/config.json 之中,为后续使用skopeo的时候做准备;

代码语言:javascript
复制
$docker login localhost -u weiyigeek -p 123456
# WARNING! Using --password via the CLI is insecure. Use --password-stdin.
# WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Login Succeeded

cat /root/.docker/config.json
{
  "auths": {
    "https://index.docker.io/v1/": { "auth": "d2VpxOQ=="},
    "localhost": {"auth": "d2VpeWlnZWVrOjEyMzQ1Ng==" }
  },
  "HttpHeaders": {"User-Agent": "Docker-Client/19.03.9 (linux)"}
}

# base64 编码
$echo "d2VpeWlnZWVrOjEyMzQ1Ng==" | base64 -d
weiyigeek:123456[

3.5) 上传一个镜像到registry之中

代码语言:javascript
复制
$docker tag alpine localhost/alpine:latest
$docker images
localhost/alpine    latest              a24bb4013296        2 months ago        5.57MB
$docker push localhost/alpine:latest
# The push refers to repository [localhost/alpine]
# 50644c29ef5a: Pushed
# latest: digest: sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 size: 528

3.6)registry 查看上传的镜像

代码语言:javascript
复制
# (1) 查看registry中存储的镜像仓库名称 (注意--cacert参数如果证书未导入到系统中则必须加上)
$curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET https://localhost/v2/_catalog
{"repositories":["alpine"]}

3.7) 利用skopeo转储镜像到registry之中和操作镜像:

代码语言:javascript
复制
# (1) 首先信任CA证书,根据不同的发行版选择相应的路径和命令行即可,为后面skopeo命令使用做准备。
# CentOS
update-ca-trust force-enable
cp certs/domain.crt /etc/pki/ca-trust/source/anchors/localhost.crt
update-ca-trust
# Ubuntu
cp certs/domain.crt /usr/local/share/ca-certificates/localhost.crt
$ update-ca-certificates
# Debian
cp certs/domain.crt /usr/share/ca-certificates/localhost.crt
echo localhost.crt >> /etc/ca-certificates.conf
update-ca-certificates

# (2) COPY 镜像到 registry
skopeo inspect docker://docker.io/alpine
# 以 localhost/library/alpine:3.10 为例
# localhost   就是该 registry 的域名或者 URL 
# library     就是项目名称 project
# alpine:3.12 就是镜像名和镜像的 tag
skopeo copy docker://alpine:3.12 docker://localhost/library/alpine:3.12
# Getting image source signatures
# Copying blob df20fa9351a1 done
# Copying config a24bb40132 done
# Writing manifest to image destination
# Storing signatures

# (3) 删除registry仓库中的镜像(删除后并不彻底)
skopeo delete docker://localhost/alpine

# (4) 查看仓库里的镜像
curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET https://localhost/v2/_catalog
{"repositories":["library/alpine"]}
skopeo inspect docker://localhost/library/alpine:3.12
{
    "Name": "localhost/library/alpine",
    "Digest": "sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65",
    "RepoTags": ["3.12"],
    "Created": "2020-05-29T21:19:46.363518345Z",
    "DockerVersion": "18.09.7",
    "Labels": null,
    "Architecture": "amd64",
    "Os": "linux",
    "Layers": ["sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c"],
    "Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]
}

注意事项:

1.证书颁发者可以用中间证书提供给您。在这种情况下,你必须用中间证书串连您的证书形成的证书捆绑。

代码语言:javascript
复制
cat domain.crt intermediate-certificates.pem > certs/domain.crt

2.Let’s Encrypt:https://letsencrypt.org/how-it-works/


0x02 Registry 目录结构

描述: 此时以上传的library/alpine:3.12镜像为例查看registry目录中文件变化;

registry 仓库结构目录如下:

代码语言:javascript
复制
# registry 持久化位置
$ls /var/lib/registry/docker/registry/v2/
blobs  repositories

# registry 树形结构
tree -h /var/lib/registry/docker/registry/v2/
/var/lib/registry/docker/registry/v2/
├── [  20]  blobs
│   └── [  36]  sha256
│       ├── [  78]  a1
│       │   └── [  18]  a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65  # 镜像data manifest (存储了data config与data layer 的 Digest摘要信息)
│       │       └── [ 528]  data
│       ├── [  78]  a2
│       │   └── [  18]  a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e  # 镜像data config
│       │       └── [1.5K]  data #  "mediaType": "application/vnd.docker.container.image.v1+json",
│       └── [  78]  df
│           └── [  18]  df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c  # 镜像data layer (通常体积最大)
│               └── [2.7M]  data # "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
└── [  21]  repositories
    └── [  20]  library
        └── [  55]  alpine
            ├── [  20]  _layers
            │   └── [ 150]  sha256
            │       ├── [  18]  a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e   # 指向 > blobs/sha256/a24bb401......6b63d83e
            │       │   └── [  71]  link
            │       └── [  18]  df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c   # 指向 > blobs/sha256/df20fa9.......9a752eb4c
            │           └── [  71]  link
            ├── [  35]  _manifests
            │   ├── [  20]  revisions  # 修订记录
            │   │   └── [  78]  sha256  
            │   │       └── [  18]  a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65  # 修订指向 manifest  >  blobs/sha256/a15790640......c90fd65
            │   │           └── [  71]  link
            │   └── [  18]  tags
            │       └── [  34]  3.12
            │           ├── [  18]  current
            │           │   └── [  71]  link  # 指向 blobs 中 data manifest digest a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
            │           └── [  20]  index
            │               └── [  78]  sha256
            │                   └── [  18]  a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 
            │                       └── [  71]  link  # 同上
            └── [   6]  _uploads  # 镜像上传过程中的临时目录
26 directories, 8 files

Q: 那 registry 存储目录到底长什么样? 🤔

答: 结合下面这张图可以看见,registry 存储目录下只有两种文件名的文件即data与link文件 (1) link 文件: 是普通的文本文件存放在 repositories 目录下,其内容是指向 data 文件的 sha256 digest值, 从字面意义上就很好理解它; (2) data 文件: 存放在 blobs 目录下文件且分为三个文件(即镜像的layer/config/manifest等文件)

代码语言:javascript
复制
# (1) Repositories
$ls /var/lib/registry/docker/registry/v2/repositories/alpine/
_layers  _manifests  _uploads
# - _layers/sha256 目录下的文件夹名是镜像的Layer和Config的Digest,通该目录下的link文件找到对应 blobs 目录下的 data 文件,实际上当我们 pull 一个镜像的 layer 时,是通过 link 文件找到 layer 在 registry 中实际的存储位置的。
# - _manifests 文件夹下的 tags 和 revisions 目录下的 link 文件则指向该镜像的 manifest 文件,保存在所有历史镜像tag的manifest文件的link。当删除一个镜像时,只会删除该镜像最新的 tag 的 link 文件。
  # - revisions 目录记录了镜像修订版本
  $ls /var/lib/registry/docker/registry/v2/repositories/alpine(镜像名称)/_manifests/revisions/sha256/
  a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
  # - tags 目录下的文件夹名例如3.10就是镜像的Tag,在它的子目录下的 current/link 文件则记录了当前 tag 指向的 manifest 文件的位置;
    # 比如我们的 alpine:latest 镜像,每次 push 新的 latest 镜像时current/link 都会更新成指向最新镜像的 manifest 文件。
  $ ls /var/lib/registry/docker/registry/v2/repositories/alpine(镜像名称)/_manifests/tags/3.12(镜像标记)/
  current  index  # 目前都指向 a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 即 镜像 data manifest


# (2) Blobs
$ls /var/lib/registry/docker/registry/v2/blobs/sha256/
a1/ a2/ df/ 

$find /var/lib/registry/docker/registry/v2/ -name "data" -exec ls -sh {} \;
# image layer 文件是 gzip 格式的 tar 包,是镜像层真实内容的 tar.gzip 格式存储形式。
2.7M ./blobs/sha256/df/df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c/data

# image config 文件是 json 格式的,它是在构建时候生成的根据DockerFile和宿主机的一些信息; (记录了"rootfs"."diff_ids")
4.0K ./blobs/sha256/a2/a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e/data

# image manifest 文件json 文件格式的,存放该镜像 layer 和 image config 文件的索引。(镜像拉取首先需拉取此文件)
4.0K ./blobs/sha256/a1/a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65/data
WeiyiGeek.registry存储结构
WeiyiGeek.registry存储结构

WeiyiGeek.registry存储结构

此时我们可以再往Registry中COPY一个镜像方便后面进行对比分析:

代码语言:javascript
复制
$skopeo copy docker://debian:buster-slim docker://localhost/library/debian:buster-slim
$curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET https://localhost/v2/_catalog
{"repositories":["library/alpine","library/debian"]}

# 对比分析 registry 中 alpine:3.12 和 debian:buster-slim 两个基础镜像,此时在registry 存储目录的结构如下:
$tree -h /var/lib/registry/docker/registry/v2/
/var/lib/registry/docker/registry/v2/
├── [  20]  blobs
│   └── [  66]  sha256
│       ├── [  78]  a1
│       │   └── [  18]  a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
│       │       └── [ 528]  data
│       ├── [  78]  a2
│       │   └── [  18]  a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e
│       │       └── [1.5K]  data
│       ├── [  78]  bf
│       │   └── [  18]  bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb  # debian:buster-slim 的 Data Layer
│       │       └── [ 26M]  data
│       ├── [  78]  c7
│       │   └── [  18]  c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773  # debian:buster-slim 的 Data Config
│       │       └── [1.5K]  data
│       ├── [  78]  df
│       │   └── [  18]  df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c
│       │       └── [2.7M]  data
│       └── [  78]  e0
│           └── [  18]  e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02  # debian:buster-slim 的 Data Manifest
│               └── [ 529]  data
└── [  21]  repositories
    └── [  34]  library
        ├── [  55]  alpine  # 此处不做过多说明
        │   ├── [  20]  _layers
        │   │   └── [ 150]  sha256
        │   │       ├── [  18]  a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e
        │   │       │   └── [  71]  link
        │   │       └── [  18]  df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c
        │   │           └── [  71]  link
        │   ├── [  35]  _manifests
        │   │   ├── [  20]  revisions
        │   │   │   └── [  78]  sha256
        │   │   │       └── [  18]  a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
        │   │   │           └── [  71]  link
        │   │   └── [  18]  tags
        │   │       └── [  34]  3.12
        │   │           ├── [  18]  current
        │   │           │   └── [  71]  link
        │   │           └── [  20]  index
        │   │               └── [  78]  sha256
        │   │                   └── [  18]  a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
        │   │                       └── [  71]  link
        │   └── [   6]  _uploads
        └── [  55]  debian
            ├── [  20]  _layers
            │   └── [ 150]  sha256
            │       ├── [  18]  bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb
            │       │   └── [  71]  link  # 摘要指向 blobs 中debian:buster-slim 的 Data Layer 
            │       └── [  18]  c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773
            │           └── [  71]  link  # 摘要指向 blobs 中debian:buster-slim 的 Data Config
            ├── [  35]  _manifests
            │   ├── [  20]  revisions
            │   │   └── [  78]  sha256
            │   │       └── [  18]  e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02
            │   │           └── [  71]  link # 摘要指向 blobs 中 debian:buster-slim 的 Data Manifest
            │   └── [  25]  tags
            │       └── [  34]  buster-slim # 以下摘要都是指向 blobs 中 debian:buster-slim 的 Data Manifest
            │           ├── [  18]  current
            │           │   └── [  71]  link
            │           └── [  20]  index
            │               └── [  78]  sha256
            │                   └── [  18]  e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02
            │                       └── [  71]  link
            └── [   6]  _uploads
48 directories, 16 files

然后我们再采用skopeo删除我们上传到Registry仓库中的镜像,再进行目录的对比:

代码语言:javascript
复制
$skopeo delete docker://localhost/library/alpine:3.12 --debug

# 列出变化的目录结构部分
repositories
    └── library
        ├── alpine
        │   ├── _layers
        │   │   └── sha256
        │   │       ├── a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e
        │   │       │   └── link
        │   │       └── df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c
        │   │           └── link
        │   ├── _manifests
        │   │   ├── revisions
        │   │   │   └── sha256
        │   │   │       └── a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
        │   │   └── tags  # tags 下面的目录与文件被删除
        │   └── _uploads

总结上述:

1.上述可以看见当skopeo delete删除一个镜像时,只是对_manifests下的文件revisions/sha256/a15790640a6690...ka9f2b0d7cc90fd65/linktags即其子目录文件进行删除;实际上两者删除的是同一个内容,即对记录了该镜像 manifests 文件 digest摘要 的 link 文件。

2.运行后从–debug参数中得到DEBU[0000] DELETE https://localhost/v2/library/alpine/manifests/sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65可以得出通过 registry API 来 DELETE 一个镜像实质上是删除 repositories 元数据文件夹下的 tag 名文件夹和该 tag 的 revisions 下的 link 文件。

代码语言:javascript
复制
# 我们也可以采用Registry API 进行操作达到同样的效率
我们定义摘要字符串匹配以下语法:
digest      := algorithm ":" hex
algorithm   := /[A-Fa-f0-9_+.-]+/
hex         := /[A-Fa-f0-9]+/
# digest = sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b

# (1) 获取到镜像 Docker-Content-Digest:
$curl -I -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12
# HTTP/1.1 200 OK
# Content-Length: 528
# Content-Type: application/vnd.docker.distribution.manifest.v2+json
# Docker-Content-Digest: sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
# Docker-Distribution-Api-Version: registry/2.0
# Etag: "sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65"
# X-Content-Type-Options: nosniff
# Date: Fri, 21 Aug 2020 02:30:06 GMT

# (2) 删除registry仓库中的指定Docker-Content-Digest的镜像tags 与 links 文件
$curl -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X DELETE https://localhost/v2/library/alpine/manifests/sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65

可以看出删除仓库中的镜像,只是把在registry仓库中镜像的tags子目录与revisions/sha256/…/link文件进行删除,而blobs中的镜像 data 与 repositories 中镜像目录library/alpine/并未被删除,这样导致的后果是registry仓库的服务器将会占用一部分存储资源导致资源的浪费,那如何解决这个问题就是我们下面提到的Registry GC 机制;


0x03 Registry API

描述:可以通过registry API操作管理镜像或者获取镜像manifest相关信息; 官方参考地址: https://docs.docker.com/registry/spec/api/

API 一览

描述:通过API遇到的错误代码如下表所示: https://docs.docker.com/registry/spec/api/#errors-2

API方法和URI列表涵盖如下表:

Method

Path

Entity

Description

GET

/v2/

Base

Check that the endpoint implements Docker Registry API V2.

GET

/v2/_catalog /v2/_catalog?n=<integer>

Catalog

检索注册中心中可用的存储库的排序json列表

GET

/v2/<name>/tags/list

Tags

获取存储库下由“name”标识的标记。

GET

/v2/manifests/

Manifest

获取由“name”和“reference”标识的清单,其中“reference”可以是标记或摘要。还可以向这个端点发出一个’ HEAD ‘请求,在不接收所有数据的情况下获取资源信息。

PUT

/v2/manifests/

Manifest

把由“name”和“reference”标识的清单放在“reference”可以是标签或摘要的地方。

DELETE

/v2/manifests/

Manifest

删除由“name”和“reference”标识的清单。注意,一个清单只能被“摘要”删除。

GET

/v2/blobs/

Blob

从由“摘要”标识的注册表中检索blob。还可以向这个端点发出一个’ HEAD ‘请求,在不接收所有数据的情况下获取资源信息。

DELETE

/v2/blobs/

Blob

删除由“name”和“digest”标识的blob

POST

/v2/blobs/uploads/

Initiate Blob Upload

如果成功,将提供一个上传位置来完成上传。可选地,如果“digest”参数存在,请求主体将用于在单个请求中完成上传。

GET

/v2/blobs/uploads/

Blob Upload

此端点的主要目的是解决可恢复上传的当前状态利用uuid。

PATCH

/v2/blobs/uploads/

Blob Upload

上传指定上传的数据块。

PUT

/v2/blobs/uploads/

Blob Upload

完成’ uuid ‘指定的上传,可选附加主体作为最后块

DELETE

/v2/blobs/uploads/

Blob Upload

取消未完成的上传进程,释放相关资源。如果没有调用此操作,未完成的上传最终将超时。

(Important)结合registry仓库解释镜像PULL与PUSH过程:

(1) PULL 镜像: 镜像由一个json清单和层叠文件组成,pull镜像的过程就是检索这两个组件的过程。

代码语言:javascript
复制
- 第一步就是获取清单,清单由下面几个字段组成:
registry:5000/v2/redis/manifests/latest(获取redis:latest清单文件)
# 字段	描述
# name	镜像名称
# tag	镜像当前版本的tag
# fsLayers	层描述列表(包括摘要)
# signature	一个JWS签名,用来验证清单内容

- 第二步当获取清单之后,客户端需要验证前面(signature),以确保名称和fsLayers层是有效的。确认后客户端可以使用digest去下载各个fs层。在V2api中层存储在blobs中已digest作为键值.
1.首先拉取镜像清单(pulling an Image Manifest)
  $ HEAD /v2/<image/manifests/<reference>#检查镜像清单是否存在
  $ GET /v2/<image>/manifests/<reference>#拉取镜像清单
  提示:reference可是是tag或者是digest  
2.开始拉取每个层(pulling a Layer)
  $ GET /v2/<image>/blobs/<digest>
  提示:digest是镜像每个fsLayer层的唯一标识,存在于清单的fsLayers里面。

(2) PUSH 镜像: 推送镜像和拉取镜像过程相反,先推各个层到registry仓库,然后上传清单.(Pushing a Layer(上传层)分为2步)

代码语言:javascript
复制
# 2.1) 使用post请求在registry仓库启动上传服务,返回一个url这个url用来上传数据和检查状态。
# 首先Existing Layers(检查层是否存在),若返回200 OK 则表示存在,不用上传
$ HEAD /v2/image/blobs/<digest>

# 开始上传服务(Starting An Upload),如果post请求返回202 accepted,一个url会在location字段返回.
$ POST /v2/image/blobs/uploads/
# 202 Accepted
# Location: /v2/\<image>/blobs/uploads/\<uuid>
# Range: bytes=0-<offset>
# Content-Length: 0
# Docker-Upload-UUID: <uuid> # 可以用来查看上传状态和实现断点续传

# 2.2) 开始上传层(Uploging the Layer)
> PUT /v2/<name>/blobs/uploads/<uuid>?digest=<digest> 
Content-Length: <size of layer>>
Content-Type: application/octet-stream

# 上传进度(Upload Progress)
$ GET /v2/<image>/blobs/uploads/<uuid>
# 204 No Content
# Location: /v2/<name>/blobs/uploads/<uuid>
# Range: bytes=0-<offset>
# Docker-Upload-UUID: <uuid>

# 重点-整块上传(Monolithic Upload)
> PUT /v2/<name>/blobs/uploads/<uuid>?digest=<digest> 
> Content-Length: <size of layer>
> Content-Type: application/octet-stream

<Layer Binary Data>

# 重点-分块上传(Chunked Upload)
> PATCH /v2/\<name>/blobs/uploads/\<uuid>
> Content-Length: \<size of chunk>
> Content-Range: \<start of range>-\<end of range>

> Content-Type: application/octet-stream

\<Layer Chunk Binary Data>

# 如果服务器不接受这个块,则返回:
#   416 Requested Range Not Satisfiable
#   Location: /v2/<name>/blobs/uploads/<uuid>
#   Range: 0-<last valid range>
#   Content-Length: 0
#   Docker-Upload-UUID: <uuid>
             
# 成功则返回:
#   202 Accepted
#   Location: /v2/<name>/blobs/uploads/<uuid>
#   Range: bytes=0-<offset>
#   Content-Length: 0
#   Docker-Upload-UUID: <uuid>

# 重点-交叉上传(Cross Repository Blob Mount)可以把客户端有访问权限的已有存储库中的层挂载到当前存储库中
POST /v2/<name>/blobs/uploads/?mount=<digest>&from=<repository name>
Content-Length: 0
# 成功返回:
#   201 Created
#   Location: /v2/<name>/blobs/<digest>
#   Content-Length: 0
#   Docker-Content-Digest: <digest>

# 失败返回:
#   202 Accepted
#   Location: /v2/<name>/blobs/uploads/<uuid>
#   Range: bytes=0-<offset>
#   Content-Length: 0
#   Docker-Upload-UUID: <uuid>


# 3.3) 上传完成(Completed Upload),但是注意分块上传在最后一块上传完毕后,需要提交一个上传完成的请求
> PUT /v2/<name>/blob/uploads/<uuid>?digest=<digest>
> Content-Length: <size of chunk>
> Content-Range: <start of range>-<end of range>
> Content-Type: application/octet-stream   

<Last Layer Chunk Binary Data>

# 成功返回:
# 201 Created
# Location: /v2/<name>/blobs/<digest>
# Content-Length: 0
# Docker-Content-Digest: <digest>
实际示例

Tips: 后续不再加--cacert /opt/registry/certs/domain.crt参数,默认大家都已经把证书导入带系统本地;

0.Registry V2协议及其认证请求验证

代码语言:javascript
复制
# Registry 仓库协议
$curl -I -u 'weiyigeek:123456' -X GET https://localhost/v2/
HTTP/1.1 200 OK
# 采用一个RFC7235兼容的授权头进行认证
$curl -I -H 'Authorization: Basic d2VpeWlnZWVrOjEyMzQ1Ng==' -X GET https://localhost/v2/
HTTP/1.1 200 OK

1.查看Registry仓库中有那些镜像(不精确-当通过delete删除镜像时候此处并未删除需要手动到repositories文件夹中删除)

代码语言:javascript
复制
# Registry 仓库中所有镜像
curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET https://localhost/v2/_catalog
{"repositories":["library/alpine","library/debian"]}

# 返回仓库中指定条目的镜像(通过-v 参数可看出last的不同)
curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET "https://localhost/v2/_catalog?n=1&last=a"

2.获取某个镜像的标签列表 (注意加或者未加Project的区别)

代码语言:javascript
复制
curl -u 'weiyigeek:123456' -X GET https://localhost/v2/alpine/tags/list
# {"name":"alpine","tags":["latest"]}
curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/tags/list  # 'library/alpine'
# {"name":"library/alpine","tags":["3.12"]}
# 列出镜像部分tags(Pagination)
curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/tags/list?n=<integer>

3.拉取Registry 仓库镜像中Manifests(清单)文件

代码语言:javascript
复制
# 判断指定镜像与tags的Manifests(清单)是否存在
curl -I -u 'weiyigeek:123456' -X HEAD https://localhost/v2/library/alpine/manifests/3.12
# HTTP/1.1 200 OK
# Content-Length: 2783
# Content-Type: application/vnd.docker.distribution.manifest.v1+prettyjws


# 仓库中`Manifests`清单
$ curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/manifests/3.12
# {
#    "schemaVersion": 1,
#    "name": "library/alpine",
#    "tag": "3.12",
#    "architecture": "amd64",
#    "fsLayers": [{
#          "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"},{
#          "blobSum": "sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c"
#       }
#    ],
#    "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:64771e4514cb653a0fe68e1ceed5bd16640ebf3bd859dc3333efe87dc4709a5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"container\":\"ce1874fa1fc1eb128516899352f185645f492c443b5a80d9a3fae8b09d1b6b16\",\"container_config\":{\"Hostname\":\"ce1874fa1fc1\",\"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:64771e4514cb653a0fe68e1ceed5bd16640ebf3bd859dc3333efe87dc4709a5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"created\":\"2020-05-29T21:19:46.363518345Z\",\"docker_version\":\"18.09.7\",\"id\":\"4a28aef4f8a9e13b1df98eaf8e651db2857161cea27134dad697ad0c7a7de12d\",\"os\":\"linux\",\"parent\":\"a5213fa3ad8fa7a42f88213945845ef49dcf11328d51576b8f076142ce75bdf8\",\"throwaway\":true}"
#       },
#       {
#          "v1Compatibility": "{\"id\":\"a5213fa3ad8fa7a42f88213945845ef49dcf11328d51576b8f076142ce75bdf8\",\"created\":\"2020-05-29T21:19:46.192045972Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD file:c92c248239f8c7b9b3c067650954815f391b7bcb09023f984972c082ace2a8d0 in / \"]}}"
#       }
#    ],
#    "signatures": [
#       {
#          "header": {
#             "jwk": {
#                "crv": "P-256",
#                "kid": "P6TV:UOU3:V564:FNEL:DQG2:WQX5:6Z5P:NQF6:XZOR:JTMI:Q2QI:AQZ3",
#                "kty": "EC",
#                "x": "n70C5idlCOFB4ubdg5K6MCvRBIH6d5YzhTRumV1i6D8",
#                "y": "OmZn6AyifVg3kZ67ICPViHTHBXvMui8fPwqXzbTnWw0"
#             },
#             "alg": "ES256"
#          },
#          "signature": "S4Tvfqx0nA7hULgyKdKKdoYpgMsTqxlbQ6JDeQv1HXZie1zMCsafNZfLI59kivzHb7IV8hwEnvxehL0cKuoZ4w",
#          "protected": "eyJmb3JtYXRMZW5ndGgiOjIxMzYsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAyMC0wOC0yMlQwNzoyNDozM1oifQ"
#       }
#    ]
# }

4.获取仓库镜像的manifests内容 (go-hello:scratch)

代码语言:javascript
复制
curl -v -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/go-hello/manifests/scratch 
# {
#    "schemaVersion": 2,
#    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
#    "config": {
#       "mediaType": "application/vnd.docker.container.image.v1+json",
#       "size": 1472,
#       "digest": "sha256:cb05b87d001253772ae9a212200de5eb8304ab9691c61589332a2f57e7059209"
#    },
#    "layers": [
#       {
#          "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
#          "size": 1106793,
#          "digest": "sha256:5395625ce01dee311e2f7c879b0b148ac7525de7aad5080a518d7f7e5a99d368"
#       }
#    ]
# }

5.获取(镜像:版本)标识的data manifests的 digest

代码语言:javascript
复制
curl -I --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/go-hello/manifests/scratch
# HTTP/1.1 200 OK
# Content-Length: 528
# Content-Type: application/vnd.docker.distribution.manifest.v2+json
# 注意Docker-Content-Digest中的内容: 在registry2.3或更高版本删除清单时,必须在HEAD或GET获取清单以获取要删除的正确digest携带以下头;
# Docker-Content-Digest: sha256:8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e
# Docker-Distribution-Api-Version: registry/2.0
# Etag: "sha256:8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e"
# X-Content-Type-Options: nosniff
# Date: Fri, 21 Aug 2020 02:30:06 GMT

# 【简约版本:直接提取Docker-Content-Digest头内容】
curl -Is -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12 | grep "Docker-Content-Digest:" | cut -f 2 -d " "
sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65

# 【如不指定Accept则默认为 application/vnd.docker.distribution.manifest.v1+prettyjws 】
curl -I -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/manifests/3.12                                     HTTP/1.1 200 OK
# Content-Length: 2783
# Content-Type: application/vnd.docker.distribution.manifest.v1+prettyjws
# Docker-Content-Digest: sha256:cae82a43ba96214acd380f3d4ed043445f56f80f0fc99f3f927d5e6eaee40791
# Docker-Distribution-Api-Version: registry/2.0
# Etag: "sha256:cae82a43ba96214acd380f3d4ed043445f56f80f0fc99f3f927d5e6eaee40791"
# X-Content-Type-Options: nosniff
# Date: Sat, 22 Aug 2020 03:01:16 GMT

6.删除仓库中的镜像即删除(repositories下面的 _manifests 中的Tags 与 revisions 下的link)

代码语言:javascript
复制
# 加入 -v 参数 查看请求返回流程  
$curl -v --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X DELETE https://localhost/v2/go-hello/manifests/sha256:8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e
#  202 Accepted
#  Content-Length: None
# 失败 返回404错误

注意:默认情况下,registry不允许删除镜像操作,需要在启动registry时指定环境变量REGISTRY_STORAGE_DELETE_ENABLED=true,或者修改其配置文件即可。reference必须是digest,否则删除将失败。在registry2.3或更高版本删除清单时,必须在HEAD或GET获取清单以获取要删除的正确digest携带以下头: Accept: application/vnd.docker.distribution.manifest.v2+json

7.拉取镜像,由于层被存储在注册表中的blobs中所以是需要通过一个标准的HTTP请求来进行拉取一个层的信息

代码语言:javascript
复制
# (1) 先查看镜像 data 相关的 Digest 码
curl -s -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12 | jq '"Data Config - " + .config.digest','"Data Layer - " + .layers[0].digest'
# "Data Config - sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e"
# "Data Layer - sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c"

# (2) 获取拉取镜像的data config 与 data layer 文件
curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/blobs/sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e | jq 

curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/blobs/sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c -o /tmp/alpine-3.12.tar.gz
[root@k8s tmp]$ ls -lh alpine-3.12.tar.gz
-rw-r--r--.  1 root root 2.7M 8月  22 16:27 alpine-3.12.tar.gz
[root@k8s tmp]$ tar -zxvf alpine-3.12.tar.gz  # 实际上该压缩文件中存放的是rootfs文件系统
# bin/
# bin/arch
# bin/ash
# bin/base64
# bin/bbconfig

8.镜像通过Registry API上传到仓库中

代码语言:javascript
复制
# 所有层上传使用两个步骤来管理上传过程。
* 第一步开始在注册表中的服务上传,返回一个URL来进行第二步。
* 第二步使用上载URL传递的实际数据。上传都开始返回,可用于将数据推和检查上传状态URL的POST请求。
# Location头将用于每个请求后进行通信的上载位置。虽然它不会在本技术规格改变,客户应使用API​​返回的最新值。

# (1) 开始上载一个POST请求 
# POST /v2/<name>/blobs/uploads/

curl -I -u 'weiyigeek:123456' -X POST https://localhost/v2/test-images/blobs/uploads/
# 如果POST请求是成功的它将返回 202响应 将与Location头上传的URL返回:
# HTTP/1.1 202 Accepted
# Content-Length: 0
# Docker-Distribution-Api-Version: registry/2.0
# Docker-Upload-Uuid: 9efa5ca7-d009-4532-88de-695c1f945e59  # 必须额
# Location: https://localhost/v2/test-images/blobs/uploads/9efa5ca7-d009-4532-88de-695c1f945e59?_state=XqlcahxfSzts1a43SgEs_MQ9-GAczgadg-Ra3vayoh57Ik5hbWUiOiJ0ZXN0LWltYWdlcyIsIlVVSUQiOiI5ZWZhNWNhNy1kMDA5LTQ1MzItODhkZS02OTVjMWY5NDVlNTkiLCJPZmZzZXQiOjAsIlN0YXJ0ZWRBdCI6IjIwMjAtMDgtMjJUMDg6NDA6NDguMDkwNTk0NzQ4WiJ9  # 指定了位置标头
# Range: 0-0
# X-Content-Type-Options: nosniff
# Date: Sat, 22 Aug 2020 08:40:48 GMT


# (2) 通过HEAD请求到BLOB存储API进行检查镜像相关层是否存在(可用返回200 OK)
# HEAD /v2/<name>/blobs/<digest>

curl -I -u 'weiyigeek:123456' -X HEAD https://localhost/v2/test-images/blobs/sha256:a14....jk5


# (3) 上传进度查看此时需要第一步中的Docker-Upload-Uuid之进行请求
# GET /v2/<name>/blobs/uploads/<uuid>
curl -I -u 'weiyigeek:123456' -X GET https://localhost/v2/test-images/blobs/uploads/9efa5ca7-d009-4532-88de-695c1f945e59


# (4) Monolithic Upload 简单的单块上传,并可以通过想避免分块的复杂性的
# PUT /v2/<name>/blobs/uploads/<uuid>?digest=<digest>
# Content-Length: <size of layer>
# Content-Type: application/octet-stream

# <Layer Binary Data>


# (5) Chunked Upload 进行组块的上载,该客户机可以指定一个范围报头和仅包括层文件的一部分:
# PATCH /v2/<name>/blobs/uploads/<uuid>
# Content-Length: <size of chunk>
# Content-Range: <start of range>-<end of range>
# Content-Type: application/octet-stream

# <Layer Chunk Binary Data>

# (6) 跨存储库Blob挂载,可以从客户机具有读访问权的另一个存储库挂载blob,从而不需要将已知的blob上传到注册中心,要发出一个blob挂载而不是一个upload, POST请求应该以以下格式发出(成功将返回202 Accepted):
POST /v2/<name>/blobs/uploads/?mount=<digest>&from=<repository name>
Content-Length: 0


# (7) Completed Upload 当镜像上传完毕必须进行以下请求否则仓库不认为镜像个层全部上传,当接收到最后一个块和层已被验证时候将返回201 Create 并且返回该镜像的Docker-Content-Digest值;
# PUT /v2/<name>/blobs/uploads/<uuid>?digest=<digest>
# Content-Length: <size of chunk>
# Content-Range: <start of range>-<end of range>
# Content-Type: application/octet-stream

# <Last Layer Chunk Binary Data>


# (8) 取消上传镜像到仓库,可通过发出DELETE请求到registry之中;
# DELETE /v2/<name>/blobs/uploads/<uuid>


# (9) 删除层(Deleting a Layer)
# DELETE /v2/<image>/blobs/<digest>

# 成功返回:
# 202 Accepted
# Content-Length: None


# (10) 上传镜像清单(Pushing an Image Manifest),我们上传完镜像层之后,就开始上传镜像清单
# PUT /v2/<name>/manifests/<reference>
# Content-Type: <manifest media type>
# {
# "name": <name>,
# "tag": <tag>,
# "fsLayers": [
#   {
#     "blobSum": <digest>
#   },
#   ...
# ]
# ],
# "history": <v1 images>,
# "signature": <JWS>,
# ...
# }

# 如果清单中有层("blobSum":<digest>)是未知的,则返回
# {  "errors:" [{          "code": "BLOB_UNKNOWN",          "message": "blob unknown to registry",          "detail": {              "digest": <digest>
#         }
#     },
#     ...
#   ]
# }
#
代码语言:javascript
复制


删除镜像Manifests与镜像层信息:
```bash
curl -Is -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12 | grep "Docker-Content-Digest:" | cut -f 2 -d " "
sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65

curl -s -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12 | grep "digest"
"digest": "sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e"
"digest": "sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c"

# 删除 镜像  _Manifests Tags
curl -v --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X DELETE https://localhost/v2/library/alpine/manifests/sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65

# 删除 镜像 _Layer
curl -v -u 'weiyigeek:123456' -X DELETE https://localhost/v2/library/alpine/blobs/sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e

curl -v -u 'weiyigeek:123456' -X DELETE https://localhost/v2/library/alpine/blobs/sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c

curl -v -u 'weiyigeek:123456' -X DELETE "https://localhost/v2/library/alpine/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"

curl -v -u 'weiyigeek:123456' -X DELETE https://localhost/v2/library/alpine/blobs/sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65

# 清空后效果
repositories
    |
   library
    │   ├── alpine
    │   │   ├── _layers
    │   │   │   └── sha256
    │   │   │       ├── a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e
    │   │   │       ├── a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    │   │   │       └── df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c
    │   │   ├── _manifests
    │   │   │   ├── revisions
    │   │   │   │   └── sha256
    │   │   │   │       └── a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
    │   │   │   └── tags
    │   │   └── _uploads

简单粗暴清空 Registry 仓库:

代码语言:javascript
复制
$tree /var/lib/registry/docker/registry/v2/blobs/sha256/
/var/lib/registry/docker/registry/v2/blobs/sha256/
├── 0e
├── 32
├── 3f
├── 53
├── 59
├── 8d
├── a1
├── a2
├── b7
├── b9
├── cb
├── df
└── e7
rm -rf /var/lib/registry/docker/registry/v2/blobs/sha256/*
rm -rf /var/lib/registry/docker/registry/v2/repositories/*

0x04 Registry GC

描述:在上一章节中我们阐述了为什么要引入Registry GC机制,再说其目的用处前我们先对其进行一个简单的介绍;

Q: 什么是Registry GC ?

答 :GC英文全称Garbage collection就是垃圾回收的意思,从 docker 官方文档关于GC偷来的 example 来解释一下吧。 官网文档:https://docs.docker.com/registry/garbage-collection/

代码语言:javascript
复制
#假如镜像 A 和镜像 B 它们分别引用了layer a,b和 a,c。
-  A  -
|     |
a  -  b  -  c 
|           |
----  B ----

# 通过 registry API 删除镜像 B 之后,layer c 并没有删掉,只是删掉了对它的引用所以 c 是多余的(就是我们上诉提到那种清情况)。
-  A  -
|     |
a  -  b  -  c 

#此时通过GC机制之后 Layer C 被删除掉了即没有被引用的层将被彻底删除;
-  A  -
|     |
a  -  b

此处可以借鉴registry GC 的源码文件 garbagecollect.go 可以看到 GC 的主要分两个阶段:

(1) marking 阶段:根据上文我们提到的 link 文件,通过扫描所有镜像 tags 目录下的 link 文件就可以得到这些镜像的 manifest,在 manifest 中保存在该镜像所有的 layer 和 config 文件的 digest 值,把这些值标记为不能清除。

代码语言:javascript
复制
markSet := make(map[digest.Digest]struct{})
manifestArr := make([]ManifestDel, 0)
err := repositoryEnumerator.Enumerate(ctx, func(repoName string) error {
  emit(repoName)

  var err error
  named, err := reference.WithName(repoName)
  if err != nil {
    return fmt.Errorf("failed to parse repo name %s: %v", repoName, err)
  }
  repository, err := registry.Repository(ctx, named)
  if err != nil {
    return fmt.Errorf("failed to construct repository: %v", err)
  }

  manifestService, err := repository.Manifests(ctx)
  if err != nil {
    return fmt.Errorf("failed to construct manifest service: %v", err)
  }

  manifestEnumerator, ok := manifestService.(distribution.ManifestEnumerator)
  if !ok {
    return fmt.Errorf("unable to convert ManifestService into ManifestEnumerator")
  }

(2) sweep 阶段:删除操作当marking完成之后没有标记blobs(layer 和 config)就会被清理掉;

代码语言:javascript
复制
// sweep
vacuum := NewVacuum(ctx, storageDriver)
if !opts.DryRun {
  for _, obj := range manifestArr {
    err = vacuum.RemoveManifest(obj.Name, obj.Digest, obj.Tags)
    if err != nil {
      return fmt.Errorf("failed to delete manifest %s: %v", obj.Digest, err)
    }
  }
}
blobService := registry.Blobs()
deleteSet := make(map[digest.Digest]struct{})
err = blobService.Enumerate(ctx, func(dgst digest.Digest) error {
  // check if digest is in markSet. If not, delete it!
  if _, ok := markSet[dgst]; !ok {
    deleteSet[dgst] = struct{}{}
  }
  return nil
})
WeiyiGeek.marking and sweep
WeiyiGeek.marking and sweep

WeiyiGeek.marking and sweep

Q:那 GC 都干了啥?

答: 我们可以利用registry容器中的registry garbage-collect命令进行GC回收操作;

代码语言:javascript
复制
$ docker exec -it registry sh -c "/bin/registry garbage-collect -m --delete-untagged=true /etc/docker/registry/config.yml"
/ # cat /etc/docker/registry/config.yml 
version: 0.1
log:
  fields:
    service: registry
storage:
  cache:
    blobdescriptor: inmemory
  filesystem:
    rootdirectory: /var/lib/registry  # 关键点
http:
  addr: :5000
  headers:
    X-Content-Type-Options: [nosniff]
health:
  storagedriver:
    enabled: true
    interval: 10s
    threshold: 3

# marking 阶段
library/alpine # Registry中的镜像未标记将会被删除 
library/debian 
library/debian: marking manifest sha256:e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02
library/debian: marking blob sha256:c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773
library/debian: marking blob sha256:bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb

# sweep 阶段
3 blobs marked, 3 blobs and 0 manifests eligible for deletion
blob eligible for deletion: sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/a1/a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65  go.version=go1.11.2 instance.id=fe470a94-4f51-4764-93e5-0f1d5d6172bf service=registry
blob eligible for deletion: sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/a2/a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e  go.version=go1.11.2 instance.id=fe470a94-4f51-4764-93e5-0f1d5d6172bf service=registry
blob eligible for deletion: sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/df/df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c  go.version=go1.11.2 instance.id=fe470a94-4f51-4764-93e5-0f1d5d6172bf service=registry

此时经过GC之后的Registry 存储目录长什么样子?

代码语言:javascript
复制
$ tree /var/lib/registry/docker/registry/v2
/var/lib/registry/docker/registry/v2
├── blobs
│   └── sha256
│       ├── a1
│       ├── a2
│       ├── bf
│       │   └── bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb
│       │       └── data
│       ├── c7
│       │   └── c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773
│       │       └── data
│       ├── df
│       └── e0
│           └── e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02
│               └── data
└── repositories
    └── library
        ├── alpine  # 可以看见镜像名称并未删除
        │   ├── _layers
        │   │   └── sha256
        │   │       ├── a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e
        │   │       │   └── link
        │   │       └── df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c
        │   │           └── link
        │   ├── _manifests
        │   │   ├── revisions
        │   │   │   └── sha256
        │   │   │       └── a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
        │   │   └── tags
        │   └── _uploads
        └── debian
            ├── _layers
            │   └── sha256
            │       ├── bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb
            │       │   └── link
            │       └── c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773
            │           └── link
            ├── _manifests
            │   ├── revisions
            │   │   └── sha256
            │   │       └── e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02
            │   │           └── link
            │   └── tags
            │       └── buster-slim
            │           ├── current
            │           │   └── link
            │           └── index
            │               └── sha256
            │                   └── e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02
            │                       └── link
            └── _uploads
40 directories, 10 files
Shell 脚本

示例1:查看Docker官方镜像仓库中镜像的所有标签

方式1:

代码语言:javascript
复制
#!/bin/sh
# set -xe 
# 其实实现方法就是通过镜像仓库的 restful API,来查询,然后把返回的 json 结果简单处理一下,然后打印出来。
image_name=$1
repo_url=https://registry.hub.docker.com/v1/repositories
curl -s ${repo_url}/${image_name}/tags | jq | grep name | awk '{print $2}' | sed -e 's/"//g'

方式2:一条命令搞定

代码语言:javascript
复制
skopeo inspect docker://docker.io/alpine | jq ".RepoTags"

示例2.registry信息查看脚本与RegistryGC回收脚本

代码语言:javascript
复制
#!/bin/bash
# Description:查看Registry仓库中的镜像信息并从仓库中删除指定镜像,然后进行垃圾回收
# Author:WeiyiGeek
# createTime:2020年8月23日 16:55:57
set -x

# [+ Defined]
PARM=$1
IMAGE_NAME=${2}
ACTION=${PARM:="NONE"}

REGISTRY_URL="https://localhost/v2"
REGISTRY_NAME="registry"
REGISTRY_HOME="/var/lib/registry/docker/registry/v2"
MANIFESTS_DIGEST=""
AUTH="Authorization: Basic d2VpeWlnZWVrOjEyMzQ1Ng=="

function Usage(){
  echo -e "\e[32mUsage: $0 {view} \e[0m"
  echo -e "\e[32m       $0 {tags} <image-name> \e[0m"
  echo -e "\e[32m       $0 {gc} <registry-container-name|container-id> \e[0m"
  echo -e "\e[32m       $0 {delete} <image-name> <reference> \e[0m"
  echo -e "\e[32m #查看仓库中的镜像信息并从仓库中删除指定镜像,然后进行垃圾回收 \e[0m"

  exit;
}

# [+ 显示仓库中的镜像]
function ViewRegistry(){
  curl -s -H "${AUTH}" "${REGISTRY_URL}/_catalog" | jq ".repositories"
}

# [+ 显示仓库中镜像标记]
function ViewTags(){
  local FLAG=0
  local IMAGE_NAME=$1
  curl -s -H "${AUTH}" "${REGISTRY_URL}/_catalog" | jq ".repositories" > registry.repo
  sed -i "s#\[##g;s#]##g;s# ##g;s#\"##g;s#,##g;/^\s*$/d" registry.repo
  
  for i in $(cat registry.repo)
  do
    if [[ "$i" == "${IMAGE_NAME}" ]];then
      FLAG=1
      break
    fi
  done

  if [[ $FLAG -eq 1 ]];then
    curl -s -H "${AUTH}" "${REGISTRY_URL}/${IMAGE_NAME}/tags/list" | jq ".tags"
  else
    echo -e "\e[31m[ERROR]: Registry 不存在 ${IMAGE_NAME} 该镜像\e[0m"
    exit
  fi
}

# [+ 仓库废弃镜像回收] 
function GcRegistry(){
  docker exec -it $1 sh -c "/bin/registry garbage-collect -m --delete-untagged=true /etc/docker/registry/config.yml"
  if [[ $? -ne 0 ]];then
    echo -e "\e[31m[ERROR]:GC Failed! \e[0m"
    exit
  fi

  # 删除 blobs/sha256中的空目录
  for i in $(find ${REGISTRY_HOME}/blobs/sha256/ | grep -v "data");do 
    if [[ $(ls -A $i|wc -c) -eq 0 ]];then 
      echo -e "[info]delete empty directory : ${i}"
      rm -rf ${i}
    fi
  done

  echo -e "[+ Registry restart ....]"
  docker restart $1
}


# [+ 删除仓库中的镜像]
function Del() {
  local IMAGE_NAME=$1
  local TAGS=$2

  if [[ "$TAGS" != "" ]];then
    # 验证删除的镜像是否存在
    curl -s -H "${AUTH}" -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' "${REGISTRY_URL}/${IMAGE_NAME}/manifests/${TAGS}" > images.mainfests
    
    err_flag=$(grep -c '"errors"' images.mainfests)
    if [[ $err_flag -ne 0 ]];then
      echo -e "\e[31m[ERROR]:$(cat images.mainfests) \e[0m"
      exit
    fi

    # 获取要删除镜像的digest摘要
    MANIFESTS_DIGEST=$(curl -s -H "${AUTH}" -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' "${REGISTRY_URL}/${IMAGE_NAME}/manifests/${TAGS}" | grep "Docker-Content-Digest:" | cut -f 2 -d " ")

    grep "digest" images.mainfests | sed 's# ##g;s#"##g;s#digest:##g' > images.digest
    echo ${MANIFESTS_DIGEST} >> images.digest

    # 删除 镜像 _Manifests目录中的Tags相关目录
    curl -v -H "${AUTH}" -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X DELETE "${REGISTRY_URL}/${IMAGE_NAME}/manifests/${MANIFESTS_DIGEST}"
    
    # 删除 镜像 _Layer 目录下的link
    for digest in $(cat images.digest);do
      curl -v -H "${AUTH}" -X DELETE "${REGISTRY_URL}/${IMAGE_NAME}/blobs/${digest}"
    done
  fi

  # GC 回收(注意参数为容器镜像名称)
  GcRegistry ${REGISTRY_NAME}

  # 判断 镜像 是否存在其它 tags 不存在时候直接删除其目录
  $flag_tags=$(curl -s -H "${AUTH}" "${REGISTRY_URL}/${IMAGE_NAME}/tags/list" | jq ".tags") 
  if [[ -z $flag_tags ]];then
    rm -rf "${REGISTRY_HOME}/repositories/${IMAGE_NAME}"
  fi
  # 删除 _layers 目录下的digest文件空的目录
  for i in $(find ${REGISTRY_HOME}/repositories/${IMAGE_NAME}/_layers/sha256/ | grep -v "link");do 
      if [[ $(ls -A $i|wc -c) -eq 0 ]];then 
        echo -e "[info]delete empty directory : ${i}"
        rm -rf ${i}
      fi
  done

  # 删除 manifests 目录下的digest文件空的目录
  for i in $(find ${REGISTRY_HOME}/repositories/${IMAGE_NAME}/_manifests/revisions/sha256/ | grep -v "link");do 
      if [[ $(ls -A $i|wc -c) -eq 0 ]];then 
        echo -e "[info]delete empty directory : ${i}"
        rm -rf ${i}
      fi
  done
}


# [Main]
if [[ "$ACTION" = "NONE" ]];then
  Usage
elif [[ "$ACTION" = "view" ]];then
  ViewRegistry
elif  [[ "$ACTION" = "tags" ]];then
  ViewTags $2
elif  [[ "$ACTION" = "delete" ]];then
  Del $2 $3
elif  [[ "$ACTION" = "gc" ]];then
  GcRegistry $2
else
  Usage
fi

EOF

章节总结:

  • (1) 在GC之后registry存储目录我们可以看到,原本blobs目录下有6个data文件现在已经变成3个,而alpine:3.12镜像相关的Layer、Config、Manifest文件已经被GC清理掉了; 但是在 repositories 目录下,该镜像的 _layers 下的 link 文件依旧存在🤔。
  • (2) 总结以上,用下面这三张图片就能直观地理解这些过程啦
    • 2.1 delete 镜像之前的 registry 存储目录结构
    WeiyiGeek.1
    WeiyiGeek.1

    WeiyiGeek.1

    • 2.2 delete 镜像之后的 registry 存储目录结构
    WeiyiGeek.2
    WeiyiGeek.2

    WeiyiGeek.2

    • 2.3 GC 之后的 registry 存储目录结构
    WeiyiGeek.
    WeiyiGeek.

    WeiyiGeek.

  • (3) GC 之后一定要重启,因为 registry 容器缓存了镜像 layer 的信息当删除掉一个镜像 A 后边 GC 掉该镜像的 layer 之后,如果不重启 registry 容器,当重新 PUSH 镜像 A 的时候就会提示镜像 layer 已经存在,不会重新上传 layer 但实际上已经被 GC 掉了,最终会导致镜像 A 不完整无法 pull 到该镜像
  • (4) GC 不是事务性操作,所以在进行 GC 的时候最好暂停 PUSH 镜像,以免把正在上传的镜像 layer 给 GC 掉。

0x05 配置文件解析
config.yaml 文件一览

Registry 仓库的 config.yaml 文件

代码语言:javascript
复制
# 配置版本
version: 0.1
# 日志配置日志系统的行为
log:
  # 访问日志系统的ACCESSLOG配置行为
  accesslog:
    disabled: true 
  # 日志输出的格式默认info.Permitted values are error, warn, info, debug
  level: debug 
  # 日志输出的格式默认 text , json, and logstash
  formatter: text
  # 字段名称的地图
  fields:
    service: registry
    environment: staging
  # 配置日志记录挂钩的行为
  hooks:
    - type: mail
      disabled: true
      levels:
        - panic
      options:
        smtp:
          addr: mail.example.com:25
          username: mailuser
          password: password
          insecure: true
        from: sender@example.com
        to:
          - errors@example.com
# 日志等级(error, warn, info, debug):deprecated: use "log"
loglevel: debug
# 存储后端配置必须
storage:
  # 使用本地磁盘存储注册表文件
  filesystem:
    rootdirectory: /var/lib/registry
    maxthreads: 100
  azure:
    accountname: accountname
    accountkey: base64encodedaccountkey
    container: containername
  # Google Cloud Storage
  gcs:
    bucket: bucketname
    keyfile: /path/to/keyfile
    credentials:
      type: service_account
      project_id: project_id_string
      private_key_id: private_key_id_string
      private_key: private_key_string
      client_email: client@example.com
      client_id: client_id_string
      auth_uri: http://example.com/auth_uri
      token_uri: http://example.com/token_uri
      auth_provider_x509_cert_url: http://example.com/provider_cert_url
      client_x509_cert_url: http://example.com/client_cert_url
    rootdirectory: /gcs/object/name/prefix
    chunksize: 5242880
  # Amazon Simple Storage Service (S3)
  s3:
    accesskey: awsaccesskey
    secretkey: awssecretkey
    region: us-west-1
    regionendpoint: http://myobjects.local
    bucket: bucketname
    encrypt: true
    keyid: mykeyid
    secure: true
    v4auth: true
    chunksize: 5242880
    multipartcopychunksize: 33554432
    multipartcopymaxconcurrency: 100
    multipartcopythresholdsize: 33554432
    rootdirectory: /s3/object/name/prefix
  # Openstack Swift object storage. 
  swift:
    username: username
    password: password
    authurl: https://storage.myprovider.com/auth/v1.0 or https://storage.myprovider.com/v2.0 or https://storage.myprovider.com/v3/auth
    tenant: tenantname
    tenantid: tenantid
    domain: domain name for Openstack Identity v3 API
    domainid: domain id for Openstack Identity v3 API
    insecureskipverify: true
    region: fr
    container: containername
    rootdirectory: /swift/object/name/prefix
  # Aliyun OSS for object storage
  oss:
    accesskeyid: accesskeyid
    accesskeysecret: accesskeysecret
    region: OSS region name
    endpoint: optional endpoints
    internal: optional internal endpoint
    bucket: OSS bucket
    encrypt: optional data encryption setting
    secure: optional ssl setting
    chunksize: optional size valye
    rootdirectory: optional root directory
  inmemory:  # This driver takes no parameters
  # 使用delete结构允许通过摘要删除映像blob和清单
  delete:
    enabled: false
  redirect:
    disable: false
  # 使用高速缓存结构,以便能够在存储后端访问的数据缓存。目前唯一可用的高速缓存提供对层的元数据,它使用blobdescriptor字段如果配置的快速访问。
  cache:
    # 如果设置为Redis的,一个Redis的缓存池元数据层。如果设置为inmemory,一个inmemory地图缓存层的元数据。
    blobdescriptor: redis
  # 维护维修
  maintenance:
    # 上传清除
    uploadpurging:
      enabled: true
      age: 168h
      interval: 24h
      dryrun: false
      # 如果在维护只读部分,使设置为true,客户端将不会被允许写入注册表。
    readonly:
      enabled: false
# 认证相关(只能配置一个身份验证提供者。)
auth:
  silly:
    # 使用范围
    realm: silly-realm
    service: silly-service
  # 令牌的认证您可以从registry中分离认证系统
  token:
    autoredirect: true
    realm: token-realm
    service: token-service
    issuer: registry-token-issuer
    rootcertbundle: /root/certs/bundle
  # 支持htpasswd的认证允许您使用的是Apache的htpasswd文件来配置基本身份验证
  htpasswd:
    realm: basic-realm
    # 唯一支持的密码格式是bcrypt。
    path: /path/to/htpasswd
  
middleware:
  registry:
    - name: ARegistryMiddleware
      options:
        foo: bar
  repository:
    - name: ARepositoryMiddleware
      options:
        foo: bar
  storage:
    - name: cloudfront
      options:
        baseurl: https://my.cloudfronted.domain.com/
        privatekey: /path/to/pem
        keypairid: cloudfrontkeypairid
        duration: 3000s
        ipfilteredby: awsregion
        awsregion: us-east-1, use-east-2
        updatefrenquency: 12h
        iprangesurl: https://ip-ranges.amazonaws.com/ip-ranges.json
  storage:
    - name: redirect
      options:
        baseurl: https://example.com/
# 报表
reporting:
  bugsnag:
    apikey: bugsnagapikey
    releasestage: bugsnagreleasestage
    endpoint: bugsnagendpoint
  newrelic:
    licensekey: newreliclicensekey
    name: newrelicname
    verbose: true
# HTTP服务器主机上的注册表中的配置
http:
  addr: localhost:5000
  prefix: /my/nested/registry/
  host: https://myregistryaddress.org:5000
  secret: asecretforlocaldevelopment
  relativeurls: false
  draintimeout: 60s
  tls:
    certificate: /path/to/x509/public
    key: /path/to/x509/private
    clientcas:
      - /path/to/ca.pem
      - /path/to/another/ca.pem
    letsencrypt:
      cachefile: /path/to/cache-file
      email: emailused@letsencrypt.com
      hosts: [myregistryaddress.org]
  debug:
    addr: localhost:5001
    # 监控
    prometheus:
      enabled: true
      path: /metrics
  # 它来指定报头的HTTP服务器应该在响应;
  headers:
    X-Content-Type-Options: [nosniff]
  http2:
    disabled: false
# 通知公告
notifications:
  events:
    includereferences: true
  # 可以接受的事件通知的清单
  endpoints:
    - name: alistener
      disabled: false
      url: https://my.listener.com/event
      headers: <http.Header>
      timeout: 1s
      threshold: 10
      backoff: 1s
      ignoredmediatypes:
        - application/octet-stream
      ignore:
        mediatypes:
           - application/octet-stream
        actions:
           - pull
# redis 相关配置
redis:
  addr: localhost:6379
  password: asecret
  db: 0
  dialtimeout: 10ms
  readtimeout: 10ms
  writetimeout: 10ms
  pool:
    maxidle: 16
    maxactive: 64
    idletimeout: 300s
# 健康检查
health:
  storagedriver:
    enabled: true
    interval: 10s
    threshold: 3
  # 文件结构包括要定期检查的路径的列表为一个文件的\存在。如果文件存在于指定的路径,健康检查将失败。您可以使用这一机制通过创建一个文件,使注册表进行旋转。
  file:
    - file: /path/to/checked/file
      interval: 10s
  http:
    - uri: http://server.to.check/must/return/200
      headers:
        Authorization: [Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==]
      statuscode: 200
      timeout: 3s
      interval: 10s
      threshold: 3
  tcp:
    - addr: redis-server.domain.com:6379
      timeout: 3s
      interval: 10s
      threshold: 3

proxy:
  remoteurl: https://registry-1.docker.io
  username: [username]
  password: [password]

compatibility:
  schema1:
    signingkeyfile: /etc/registry/key.json
    enabled: true
    
# 验证
validation:
  manifests:
    urls:
      allow:
        - ^https?://([^/]+\.)*example\.com/
      deny:
        - ^https?://www\.example\.com/
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-05-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0x00 前言简述
  • 0x01 基础安装
    • 1) 基础命令
      • 2) 基础配置
        • 3) 生产实例
        • 0x02 Registry 目录结构
        • 0x03 Registry API
          • API 一览
            • 实际示例
            • 0x04 Registry GC
              • Shell 脚本
              • 0x05 配置文件解析
                • config.yaml 文件一览
                相关产品与服务
                容器镜像服务
                容器镜像服务(Tencent Container Registry,TCR)为您提供安全独享、高性能的容器镜像托管分发服务。您可同时在全球多个地域创建独享实例,以实现容器镜像的就近拉取,降低拉取时间,节约带宽成本。TCR 提供细颗粒度的权限管理及访问控制,保障您的数据安全。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档