目录

OCI Image Format Spec探索与实践

OCI Image Format定义了content addressable与location addressable结合的分层树状结构。

基本组成

  • Image manifest:用于对镜像的内容寻址。
  • Image index:指向多个manifest的更高级别manifest,一般用于区分多平台。
  • Filesystem layers:用于描述容器文件系统内容变化。
  • Configuration:用于记录镜像配置及运行时信息等元信息。

引用go-containerregistry项目中结构图来宏观描述一下上述组成的关系:

/posts/oci-image/remote.dot.svg
镜像层级结构

OCI Image Spec中有更细化的描述:

/posts/oci-image/build-diagram.png

Content Descriptor

Content descriptor用于描述对象内容的位置,组件内的descriptor可以描述当前组件对其他组件的引用关系,其应包含如下核心元素:

  • 内容的类型
  • 内容唯一标识
  • 内容的大小

属性描述

属性类型作用
mediaTypestring对象内容的类型
digeststring对象内容的唯一标识,常使用sha256算法加密
sizeint64对象内容的字节数
urls[]string对象可以被下载的url列表(optional)
annotationsmap[string]string携带额外信息的键值对集合
mediaType的作用

mediaType用于唯一地标识当前blob的类型,通过此类型可以标准化对blob的处理。以containerd的image.ChildrenHandler获取当前descriptor所有直接子引用为例。可以看出对不同blob的处理依据就是mediaType

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Children returns the immediate children of content described by the descriptor.
func Children(ctx context.Context, provider content.Provider, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
	var descs []ocispec.Descriptor
	switch desc.MediaType {
	case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
		p, err := content.ReadBlob(ctx, provider, desc)
		if err != nil {
			return nil, err
		}

		// TODO(stevvooe): We just assume oci manifest, for now. There may be
		// subtle differences from the docker version.
		var manifest ocispec.Manifest
		if err := json.Unmarshal(p, &manifest); err != nil {
			return nil, err
		}

		descs = append(descs, manifest.Config)
		descs = append(descs, manifest.Layers...)
	case MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
		p, err := content.ReadBlob(ctx, provider, desc)
		if err != nil {
			return nil, err
		}

		var index ocispec.Index
		if err := json.Unmarshal(p, &index); err != nil {
			return nil, err
		}

		descs = append(descs, index.Manifests...)
	default:
		if IsLayerType(desc.MediaType) || IsKnownConfig(desc.MediaType) {
			// childless data types.
			return nil, nil
		}
		log.G(ctx).Debugf("encountered unknown type %v; children may not be fetched", desc.MediaType)
	}

	return descs, nil
}

Image Manifest

Manifest用于定位镜像内容,可以认为是一个镜像的实际入口,包含一个特定platform下image所需的全部信息:

  • 若干个layers
  • 一个configuration

MediaType

  • application/vnd.oci.image.manifest.v1+json OCI Image Format Spec
  • application/vnd.docker.distribution.manifest.v2+json 兼容Docker Image Format Spec

属性描述

属性类型作用
schemaVersionint指定manifest schema,为确保与旧版本docker兼容,此Spec下固定值为2
mediaTypestring内容的类型
configdescriptor与容器运行时相关的配置信息
layers[]descriptor用于构建镜像内文件系统布局,其中layers[0]描述base layer
annotationsmap[string]string携带额外信息的键值对集合

实践探索

以linux/amd64下的ubuntu:21.04为例,我们看一下其manifest:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
  "config": {
    "mediaType": "application/vnd.docker.container.image.v1+json",
    "size": 1462,
    "digest": "sha256:bf70ebd2c444440ae068c5ccea80e2087906a825ff1019a9f6d6cbb229e33481"
  },
  "layers": [
    {
      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
      "size": 31837572,
      "digest": "sha256:4451f5c7eb7af74432585f5ebfbeb01bbfc87ec4a74dc93703bdd89330559cd1"
    }
  ]
}

可以看到,其mediaType为application/vnd.docker.distribution.manifest.v2+json,包含一个config blob与一个layer blob。

Image Index

Index又被称为fat manifest,manifest可以视为layer的索引,而index是在manifest上又加了一层的索引。有了index,这种两层树状结构变成了多层,提供了多描述符入口点。

在docker image中,index的主要作用是区分多平台(OS/ORCH)。

MediaType

  • application/vnd.oci.image.index.v1+json OCI Image Format Spec
  • application/vnd.docker.distribution.manifest.list.v2+json 兼容Docker Image Format Spec

属性描述

属性类型作用
schemaVersionint指定manifest schema,为确保与旧版本docker兼容,此spec下固定值为2
mediaTypestring内容的类型
manifests[]object描述运行时要求的最小集,主要是操作系统/架构等平台相关,列表中有多个manifest,提供平台相关的属性用以进行filter。
annotationsmap[string]string携带额外信息的键值对集合

实践探索

我们看一下dockerhub上ubuntu:21.04的index。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
   "manifests": [
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 529,
         "digest": "sha256:ef8ee90cfa9cfc7c218586dea9daa6a8d1d191b3c73be143f4120fe140dae3d0",
         "platform": {
            "architecture": "amd64",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 529,
         "digest": "sha256:b7de3b708ddbdb5ca7d0a6a81f6d9df450276fc4794174a7b7a3441b00281a61",
         "platform": {
            "architecture": "arm",
            "os": "linux",
            "variant": "v7"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 529,
         "digest": "sha256:ca763e1a382a5b23f91abaf1c36a84be33da2d657f45746112f28ae010571041",
         "platform": {
            "architecture": "arm64",
            "os": "linux",
            "variant": "v8"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 529,
         "digest": "sha256:54b3fc49fc1949bcedbafbf1f18393920545ba934331cf72176cb14087962879",
         "platform": {
            "architecture": "ppc64le",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 529,
         "digest": "sha256:9c389f10c2b192dd01e87188c7cf1591dc830370046085190dd3ecfdaa1f2cfb",
         "platform": {
            "architecture": "riscv64",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 529,
         "digest": "sha256:13532df2f7a272c2c973268db5264059be5ba9882962d30db3d86ca38db3a737",
         "platform": {
            "architecture": "s390x",
            "os": "linux"
         }
      }
   ]
}

从中不难看出,该镜像提供了六种CPU架构下编译的ubuntu镜像,当某个client发出docker pull命令时,registry会index到对应的架构平台,找到合适的manifest。

Filesystem layer

Layer是镜像内文件系统的组成成分,每一层都在描述一系列文件系统变化。

MediaType

  • application/vnd.oci.image.layer.v1.tar+gzip OCI Image Format Spec
  • application/vnd.docker.image.rootfs.diff.tar.gzip 兼容Docker Image Format Spec

实践探索

我们copy出ubuntu:21.04的layer并解压,看一下base image的样式,tar内文件太多,仅列出前10行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ cp blobs/sha256/44/4451f5c7eb7af74432585f5ebfbeb01bbfc87ec4a74dc93703bdd89330559cd1/data ~/ubuntu.tar.gz && gzip -d ~/ubuntu.tar.gz -c | tar tv | head -10

# output
lrwxrwxrwx 0/0               0 2021-07-24 01:47 bin -> usr/bin
drwxr-xr-x 0/0               0 2021-04-19 15:26 boot/
drwxr-xr-x 0/0               0 2021-07-24 01:50 dev/
drwxr-xr-x 0/0               0 2021-07-24 01:50 etc/
-rw------- 0/0               0 2021-07-24 01:47 etc/.pwd.lock
-rw-r--r-- 0/0            3028 2021-07-24 01:47 etc/adduser.conf
drwxr-xr-x 0/0               0 2021-07-24 01:50 etc/alternatives/
-rw-r--r-- 0/0             100 2021-04-14 18:32 etc/alternatives/README
lrwxrwxrwx 0/0               0 2021-07-24 01:50 etc/alternatives/awk -> /usr/bin/mawk
lrwxrwxrwx 0/0               0 2021-07-24 01:50 etc/alternatives/nawk -> /usr/bin/mawk
技巧
可以通过... | awk '{print $6}' | awk -F/ '{print $1}'| sort | uniq 对上述输出结果进行聚合获取第一层目录,结果可以看到就是标准的ubuntu root filesystem。

再探索一下filesystem changeset的内容,新创建一个镜像,修改镜像内的文件系统

1
2
3
FROM ubuntu:21.04
RUN echo "hello world" > /tmp/hello.txt
COPY ccc .

分别将第二层与第三层的内容拷贝出来并解压

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 略去拷贝过程

######Layer2######
$ tar zxvf layer2.tar.gz
tmp/
tmp/hello.txt

######Layer3######
$ tar zxvf layer3.tar.gz
ccc

上述结果验证了之前踩过的一个坑:写Dockerfile构建镜像时,使用COPY将宿主机上的文件复制到镜像里时,如果源文件变化了,docker缓存会失效。之前误以为一个dockerfile中的一条语句对应一个layer,只要语句不变,layer就不变,就可以使用cache。此例清晰地描述出layer会与文件系统的changeset密切相关。

Configuration

用于描述镜像的一些元信息及容器运行时所需的信息。

MediaType

  • application/vnd.oci.image.config.v1+json OCI Image Format Spec
  • application/vnd.docker.container.image.v1+json 兼容Docker Image Format Spec

属性描述

属性类型作用
createdstring描述镜像创建日期
authorstring描述镜像创建的作者
architecturestring描述编译镜像中二进制包的节点CPU架构
osstring描述构建镜像的节点的操作系统
configobject容器运行时所需要的执行参数(docker run中所能指定的参数),如Volumes,Env,ExposedPort等
rootfsobject描述image各层DiffID
historyobject描述每一层的历史信息

上述大部分属性可以通过docker inspect [IMAGE]获取到。

DiffID

DiffID是layer未压缩时的tar包hash后的digest,可用于解压后内容验证。

由于DiffID仅能描述某个layer的信息,无法描述整个layer布局的信息,因此又引入ChainID来校验image的布局,主要思想是引入与之前layer的相关性来生成对应layer的ID。从定义上看,第一层base layer的DiffIDChainID一致。

实践探索

展示一下ubuntu:21.04的configuration,部分与container相关的字段超出此spec范围。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
{
  "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": [
      "bash"
    ],
    "Image": "sha256:2a1126c0612fcbe61f0acaa6b1f2caf3a156b31684219de8bbb763ee3e99940c",
    "Volumes": null,
    "WorkingDir": "",
    "Entrypoint": null,
    "OnBuild": null,
    "Labels": null
  },
  "container": "acac01451c096428e536623ecd3887aa7c79f8377ac8a94885b6ceae8971dfcf",
  "container_config": {
    "Hostname": "acac01451c09",
    "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 [\"bash\"]"
    ],
    "Image": "sha256:2a1126c0612fcbe61f0acaa6b1f2caf3a156b31684219de8bbb763ee3e99940c",
    "Volumes": null,
    "WorkingDir": "",
    "Entrypoint": null,
    "OnBuild": null,
    "Labels": {}
  },
  "created": "2021-07-26T21:21:54.791192114Z",
  "docker_version": "20.10.7",
  "history": [
    {
      "created": "2021-07-26T21:21:54.424131139Z",
      "created_by": "/bin/sh -c #(nop) ADD file:6ae44786caae9af1c6b70dc9cc244e7d4e06fffc0696f68877527d69aa3fc735 in / "
    },
    {
      "created": "2021-07-26T21:21:54.791192114Z",
      "created_by": "/bin/sh -c #(nop)  CMD [\"bash\"]",
      "empty_layer": true
    }
  ],
  "os": "linux",
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:ce91b7d7ac5b2c288515e8eee3a83720d6855e7f1cf8dfa6e9b524453956175f"
    ]
  }
}

Image Layout

用于描述OCI内容寻址(content-addressable)blob与位置寻址(location-addressable)reference的目录结构。

content-addressable vs location-addressable

location-addressed存储中,每个数据元素存储在特定的物理媒介中,并且它的(物理媒介)location会被记录下来以供后续访问。当想要访问到对应数据内容时,只需要在request中使用这个location即可。位置寻址不关心存储的具体内容是什么,只关心内容存储在什么位置,内容的大小多少(与盘空间占用相关),location所标识的内容可以被灵活地修改/覆盖/删除。

与之相比,content-addressed存储通过与内容相关的唯一ID来定位,通过存储系统找到对应的内容。内容一旦发生变化,这个标识ID也会发生变化,即寻址地址也会发生变化。由于这个特点,一般的content-addressed系统不允许修改原有的内容,并且删除操作也会通过严格的策略进行控制。

参考wikipedia: Content-addressed vs. location-addressed

结构组成

  • blobs目录:包含内容寻址的blob。
    • 子目录是hash算法名称
    • 内容索引形式为blobs/<alg>/<encoded>
  • oci-layout 文件:声明OCI image-layout的版本。
  • Index.json文件:image-layout中reference的入口点。
    • 一般使用org.opencontainers.image.ref.name annotation声明引用。
    • 组织形式与image index形式十分相似。

示例

比较完善的示例是helm对OCI支持,chart的发布支持OCI规范,实现上依赖于本地的OCI Image Layout与OCI registry交互完成chart的发布。

/posts/oci-image/helm-oci.png
helm支持OCI的交互方式

使用save cmd保存两个版本的chart。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ helm chart save mychart-0.1.0 myregistry:5000/mychart:v0.1.0

ref:     myregistry:5000/mychart:v0.1.0
digest:  dfec110f2b7aecb1d8604d64f7f32026b0af51aa1286627c6520ff2cf1576337
size:    3.7 KiB
name:    mychart
version: 0.1.0
v0.1.0: saved

$ helm chart save mychart-0.2.0 myregistry:5000/mychart:v0.2.0

ref:     myregistry:5000/mychart:v0.2.0
digest:  fe45ba098c1f8bc61e19245c6123f47d7c51f78cec016834e2c0c26c28901e24
size:    3.7 KiB
name:    mychart
version: 0.2.0
v0.2.0: saved

local OCI image layout的结构为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ tree ~/.cache/helm/registry/cache

├── blobs
│   └── sha256
│       ├── 573f8b72a735d3f6e5919acb325d365aeddf69edee1e4840c59a5d741179da97
│       ├── 65a07b841ece031e6d0ec5eb948eacb17aa6d7294cdeb01d5348e86242951487
│       ├── 98ddb183b4658761a6e431fbbde4c6c15863b0c3597b74b519c67776830de282
│       ├── dfec110f2b7aecb1d8604d64f7f32026b0af51aa1286627c6520ff2cf1576337
│       ├── e76837ca35eb2e8f22ce8a78f14a1275511eafed58b76955b2ac7ddd0211c965
│       └── fe45ba098c1f8bc61e19245c6123f47d7c51f78cec016834e2c0c26c28901e24
├── index.json
├── ingest
└── oci-layout

查看一下index.json中的内容cat ~/.cache/helm/registry/cache/index.json | jq .

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:dfec110f2b7aecb1d8604d64f7f32026b0af51aa1286627c6520ff2cf1576337",
      "size": 322,
      "annotations": {
        "org.opencontainers.image.ref.name": "myregistry:5000/mychart:v0.1.0"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:fe45ba098c1f8bc61e19245c6123f47d7c51f78cec016834e2c0c26c28901e24",
      "size": 322,
      "annotations": {
        "org.opencontainers.image.ref.name": "myregistry:5000/mychart:v0.2.0"
      }
    }
  ]
}

index.json中很容易看出来通过descriptor中的digest与org.opencontainers.image.ref.name annotion将location(chart reference)与content(OCI manifest)关联了起来。

总结与延伸

本文介绍了OCI Image Format Spec的组成,其对mediaType做了兼容,可以说是Docker Image Format Spec的一个超集。但OCI Image Format Spec要更通用些,体现在layer content可以更加多样,并且index不局限于一层。在此通用规范下可以做一些更cool的事情,比如OCI Artifacts

此外,OCI Image Format Spec与OCI Runtime SpecOCI Distribution Spec密切相关,比如image config如何转换成runtime bundle;image如何存储到registry。可见OCI Image Format Spec是OCI 规范中关键纽带。

后续会写一篇文章来描述一个私有化交付下的应用打包方案,从中可以看到OCI Artifacts,OCI Image Format Spec与OCI Distribution结合起来释放的强大力量。