In this post I’d like to share some tools from my toolbox for working with container images (Docker/OCI).
I specifically excluded tools for building container images (Buildkit, buildah, kaniko & friends).
#
What is a container image?
Before we get to the tools, let’s first take a minute to understand what a container image actually is.
This will allow us to better understand what these tools do and how they can help us.
In a nutshell, a container image is a collection of layers alongside some configuration metadata which are tied together by an image manifest.
The layers contain files and directories, which are stacked on top of each other to form the final filesystem for the container.
The principal is similar to a Git repository: each repository is made up of one or more commits (= layers), a j.git/config
(= configuration) and an index (= manifest).
The container image format was initially developed by Docker, but has been spun out into the dedicated Open Container Initiative project to help with standardization and interoperability.
These days the OCI image specification formally defines what an image consists of and is supported by many tools in the cloud native ecosystem.
Sidenote: the OCI also defines the Distribution Specification (which API clients and servers need to implement for uploading and downloading container images) as well as the Runtime Specification (how a container should be run).
Let’s look at a practical example (all subsequent podman
command can be substituted with docker
):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| $ mkdir /tmp/container-tools
$ cd /tmp/container-tools
$ podman image pull docker.io/library/httpd:latest
Trying to pull docker.io/library/httpd:latest...
Getting image source signatures
Copying blob ea83e81966d6 ... done
Copying blob b0a0cf830b12 ... done
Copying blob 4f4fb700ef54 ... done
Copying blob 943a2b3cf551 ... done
Copying blob 851c52adaa9b ... done
Copying blob 39d9f60535a6 ... done
Copying config 67c2fc9e3d ... done
Writing manifest to image destination
67c2fc9e3d849b21a35b4c96f5ad8ec4dc9b73ca44c56537039e8c6f3054db0a
|
The previous command downloaded the most recent version of the Apache httpd container image from DockerHub.
Based on the progressive download output we can already see that the container image is not “one file”, but is made up of multiple parts (referred to as blobs in the output above).
The image is now stored locally on the machine, but the exact location will vary depending on the container manager (Docker, Podman, …) and its configuration.
Let’s extract the contents of the container image into an empty directory:
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
| $ mkdir /tmp/httpd && cd /tmp/httpd
$ podman image save docker.io/library/httpd:latest | tar x
$ tree
.
├── 30bc0a59d494d75238f1f9d26e3851eb33debbfca69ed5dac18588bdcb58d9d4
│ ├── json
│ ├── layer.tar -> ../53920c8b9c11c3f431a7f096a6a496f587eec1ccbc3b9e9649470db799c96758.tar
│ └── VERSION
├── 46176a0cbe9f3e95a08e3914ef9f4ac33fb1d412c9bacf2d1217616f76077fad.tar
├── 52ec5a4316fadc09a4a51f82b8d7b66ead0d71bea4f75e81e25b4094c4219061.tar
├── 53920c8b9c11c3f431a7f096a6a496f587eec1ccbc3b9e9649470db799c96758.tar
├── 5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef.tar
├── 67c2fc9e3d849b21a35b4c96f5ad8ec4dc9b73ca44c56537039e8c6f3054db0a.json
├── 7b341f868c2e467ddb56f7dfcbe82578403faa3da4ae0cb264048f3fa7a9bacd
│ ├── json
│ ├── layer.tar -> ../5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef.tar
│ └── VERSION
├── a1f21c4adbc031095cd0bf494d36e6d2f7c8c0a4fb0e8f22ff559f3e4b436e66
│ ├── json
│ ├── layer.tar -> ../dba1169a4ef830544ed14026d745670ee754a929e251440482009a5e89d4f20c.tar
│ └── VERSION
├── a2ba897b57547df4c74318915ba74bac4c5603d3ed56ea70eb7397914479d033
│ ├── json
│ ├── layer.tar -> ../ed8b961e754fdc176a3307cc94471a8c7db19edfcbce503deeb40d0043af2f44.tar
│ └── VERSION
├── d4112ecc8ff8a228860a3851d8cdf391987fc47b0c1ffa9e3ddbc1c744da4b16
│ ├── json
│ ├── layer.tar -> ../46176a0cbe9f3e95a08e3914ef9f4ac33fb1d412c9bacf2d1217616f76077fad.tar
│ └── VERSION
├── dba1169a4ef830544ed14026d745670ee754a929e251440482009a5e89d4f20c.tar
├── e6fa7777a000792ac871b6ab8c18b5dd6778dbe61537b3864e4aa50445c45d2c
│ ├── json
│ ├── layer.tar -> ../52ec5a4316fadc09a4a51f82b8d7b66ead0d71bea4f75e81e25b4094c4219061.tar
│ └── VERSION
├── ed8b961e754fdc176a3307cc94471a8c7db19edfcbce503deeb40d0043af2f44.tar
├── manifest.json
└── repositories
7 directories, 27 files
|
The image manifest (manifest.json
) is the file that ties everything together, let’s take a look at it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| $ jq . manifest.json
[
{
"Config": "67c2fc9e3d849b21a35b4c96f5ad8ec4dc9b73ca44c56537039e8c6f3054db0a.json",
"RepoTags": [
"docker.io/library/httpd:latest"
],
"Layers": [
"52ec5a4316fadc09a4a51f82b8d7b66ead0d71bea4f75e81e25b4094c4219061.tar",
"46176a0cbe9f3e95a08e3914ef9f4ac33fb1d412c9bacf2d1217616f76077fad.tar",
"5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef.tar",
"53920c8b9c11c3f431a7f096a6a496f587eec1ccbc3b9e9649470db799c96758.tar",
"ed8b961e754fdc176a3307cc94471a8c7db19edfcbce503deeb40d0043af2f44.tar",
"dba1169a4ef830544ed14026d745670ee754a929e251440482009a5e89d4f20c.tar"
]
}
]
|
It tells us where we can find the configuration (Config
) of the container image and where we can find the layers.
Notice that layers
is an array: the layers need to be assembled in the correct order to get the correct result.
It’s also interesting to observe that all referenced files use content-addressable storage, i.e. the name of the files corresponds to their SHA256 checksum.
The first advantage is that we can easily check if any errors occurred during the download (if that was the cause, the filename and checksum would not match):
1
2
3
4
5
6
7
8
| $ sha256sum *.json *.tar
67c2fc9e3d849b21a35b4c96f5ad8ec4dc9b73ca44c56537039e8c6f3054db0a 67c2fc9e3d849b21a35b4c96f5ad8ec4dc9b73ca44c56537039e8c6f3054db0a.json
46176a0cbe9f3e95a08e3914ef9f4ac33fb1d412c9bacf2d1217616f76077fad 46176a0cbe9f3e95a08e3914ef9f4ac33fb1d412c9bacf2d1217616f76077fad.tar
52ec5a4316fadc09a4a51f82b8d7b66ead0d71bea4f75e81e25b4094c4219061 52ec5a4316fadc09a4a51f82b8d7b66ead0d71bea4f75e81e25b4094c4219061.tar
53920c8b9c11c3f431a7f096a6a496f587eec1ccbc3b9e9649470db799c96758 53920c8b9c11c3f431a7f096a6a496f587eec1ccbc3b9e9649470db799c96758.tar
5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef 5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef.tar
dba1169a4ef830544ed14026d745670ee754a929e251440482009a5e89d4f20c dba1169a4ef830544ed14026d745670ee754a929e251440482009a5e89d4f20c.tar
ed8b961e754fdc176a3307cc94471a8c7db19edfcbce503deeb40d0043af2f44 ed8b961e754fdc176a3307cc94471a8c7db19edfcbce503deeb40d0043af2f44.tar
|
The second advantage is that it allows sharing layers between container images.
For example if two container images are built from the same base image, that particular base layer only needs to be stored once (both in the container registry and on the local machine) as well transmitted once (the check can be carried out before downloading the layers).
The configuration file (in this case 67c2fc9e3d849b21a35b4c96f5ad8ec4dc9b73ca44c56537039e8c6f3054db0a.json
) contains information about how and when the image was built (history
), which platform it is compatible with (architecture
, os
) and how the container should be run (elements under config
).
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
| $ jq . 67c2fc9e3d849b21a35b4c96f5ad8ec4dc9b73ca44c56537039e8c6f3054db0a.json
{
"architecture": "amd64",
"config": {
"ExposedPorts": {
"80/tcp": {}
},
"Env": [
"PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HTTPD_PREFIX=/usr/local/apache2",
"HTTPD_VERSION=2.4.59",
"HTTPD_SHA256=ec51501ec480284ff52f637258135d333230a7d229c3afa6f6c2f9040e321323",
"HTTPD_PATCHES="
],
"Cmd": [
"httpd-foreground"
],
"WorkingDir": "/usr/local/apache2",
"StopSignal": "SIGWINCH",
"ArgsEscaped": true
},
"created": "2024-04-05T21:49:16Z",
"history": [
{
"created": "2024-04-05T21:49:16Z",
"created_by": "/bin/sh -c #(nop) ADD file:4b1be1de1a1e5aa608c688cad2824587262081866180d7368feb79d33ca05953 in / "
},
{
"created": "2024-04-05T21:49:16Z",
"created_by": "/bin/sh -c #(nop) CMD [\"bash\"]",
"empty_layer": true
},
{
"created": "2024-04-05T21:49:16Z",
"created_by": "ENV HTTPD_PREFIX=/usr/local/apache2",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
},
{
"created": "2024-04-05T21:49:16Z",
"created_by": "ENV PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
},
{
"created": "2024-04-05T21:49:16Z",
"created_by": "RUN /bin/sh -c mkdir -p \"$HTTPD_PREFIX\" \t&& chown www-data:www-data \"$HTTPD_PREFIX\" # buildkit",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2024-04-05T21:49:16Z",
"created_by": "WORKDIR /usr/local/apache2",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2024-04-05T21:49:16Z",
"created_by": "RUN /bin/sh -c set -eux; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tca-certificates \t\tlibaprutil1-ldap \t\tlibldap-common \t; \trm -rf /var/lib/apt/lists/* # buildkit",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2024-04-05T21:49:16Z",
"created_by": "ENV HTTPD_VERSION=2.4.59",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
},
{
"created": "2024-04-05T21:49:16Z",
"created_by": "ENV HTTPD_SHA256=ec51501ec480284ff52f637258135d333230a7d229c3afa6f6c2f9040e321323",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
},
{
"created": "2024-04-05T21:49:16Z",
"created_by": "ENV HTTPD_PATCHES=",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
},
{
"created": "2024-04-05T21:49:16Z",
"created_by": "RUN /bin/sh -c set -eux; \t\tsavedAptMark=\"$(apt-mark showmanual)\"; \tapt-get update; [...] ",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2024-04-05T21:49:16Z",
"created_by": "STOPSIGNAL SIGWINCH",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
},
{
"created": "2024-04-05T21:49:16Z",
"created_by": "COPY httpd-foreground /usr/local/bin/ # buildkit",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2024-04-05T21:49:16Z",
"created_by": "EXPOSE map[80/tcp:{}]",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
},
{
"created": "2024-04-05T21:49:16Z",
"created_by": "CMD [\"httpd-foreground\"]",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
}
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:52ec5a4316fadc09a4a51f82b8d7b66ead0d71bea4f75e81e25b4094c4219061",
"sha256:46176a0cbe9f3e95a08e3914ef9f4ac33fb1d412c9bacf2d1217616f76077fad",
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
"sha256:53920c8b9c11c3f431a7f096a6a496f587eec1ccbc3b9e9649470db799c96758",
"sha256:ed8b961e754fdc176a3307cc94471a8c7db19edfcbce503deeb40d0043af2f44",
"sha256:dba1169a4ef830544ed14026d745670ee754a929e251440482009a5e89d4f20c"
]
}
}
|
Finally, let’s take a look at the layers, which are individual archives (.tar
).
Sidenote: usually these layers are compressed with gzip
or zstd
to save space (and time!).
This is not the case here because we locally exported the image, i.e. they have already been decompressed in the process.
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
| # the base layer (`FROM debian:bookworm-slim`)
$ tar tvf 52ec5a4316fadc09a4a51f82b8d7b66ead0d71bea4f75e81e25b4094c4219061.tar | wc -l
4222
# second layer only creates the apache directory
$ tar tvf 46176a0cbe9f3e95a08e3914ef9f4ac33fb1d412c9bacf2d1217616f76077fad.tar
drwxr-xr-x 0/0 0 2024-04-23 17:00 usr/
drwxr-xr-x 0/0 0 2024-04-24 06:54 usr/local/
drwxr-xr-x 33/33 0 2024-04-24 06:54 usr/local/apache2/
# third layer is empty (changing WORKDIR)
$ tar tvf 5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef.tar | wc -l
0
# fourth layer adds runtime dependencies, certificates etc.
$ tar tvf 53920c8b9c11c3f431a7f096a6a496f587eec1ccbc3b9e9649470db799c96758.tar | wc -l
608
# fifth layer contains the application (apache httpd)
$ tar tvf ed8b961e754fdc176a3307cc94471a8c7db19edfcbce503deeb40d0043af2f44.tar | wc -l
735
# final layer adds a script for running apache httpd in the foreground
$ tar tvf dba1169a4ef830544ed14026d745670ee754a929e251440482009a5e89d4f20c.tar
drwxr-xr-x 0/0 0 2024-04-23 17:00 usr/
drwxr-xr-x 0/0 0 2024-04-24 06:54 usr/local/
drwxr-xr-x 0/0 0 2024-04-24 06:57 usr/local/bin/
-rwxr-xr-x 0/0 138 2024-04-24 06:54 usr/local/bin/httpd-foreground
|
For reference you can compare these layers to the upstream Dockerfile.
Notice that not all instructions result in the creation of a new layer.
Instructions like ENV
and STOPSIGNAL
are recorded in the configuration manifest instead, which we looked at before.
Now we have a basic understanding of the contents of a container image, let’s take a look at the toolbox.
#
dive
The first tool is called dive: an interactive TUI (terminal user interface) tool that effectively automates the process we did above: looking at individual layers of an image and understanding which files were added/removed/modified.
This is super helpful for learning how to write efficient Dockerfiles (i.e. minimal size) and developing confidence on what exactly is part of the image being built.
Diffing and seeing what files/directories go into each layer is handy to reduce the size of the container image, which in return makes it faster to build, upload and download since less data needs to transferred.
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
| $ dive podman://docker.io/library/httpd:latest
┃ ● Current Layer Contents ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
│ Layers ├──────────────────────────────────────────────────────────────────────────── Permission UID:GID Size Filetree
Cmp Size Command -rwxrwxrwx 0:0 0 B ├── bin → usr/bin
75 MB FROM 52ec5a4316fadc0 drwxr-xr-x 0:0 0 B ├── boot
0 B RUN /bin/sh -c mkdir -p "$HTTPD_PREFIX" && chown www-data:www-data "$H drwxr-xr-x 0:0 0 B ├── dev
0 B WORKDIR /usr/local/apache2 drwxr-xr-x 0:0 172 kB ├─⊕ etc
10 MB RUN /bin/sh -c set -eux; apt-get update; apt-get install -y --no-i drwxr-xr-x 0:0 0 B ├── home
62 MB RUN /bin/sh -c set -eux; savedAptMark="$(apt-mark showmanual)"; -rwxrwxrwx 0:0 0 B ├── lib → usr/lib
138 B COPY httpd-foreground /usr/local/bin/ # buildkit -rwxrwxrwx 0:0 0 B ├── lib64 → usr/lib64
drwxr-xr-x 0:0 0 B ├── media
drwxr-xr-x 0:0 0 B ├── mnt
drwxr-xr-x 0:0 0 B ├── opt
drwxr-xr-x 0:0 0 B ├── proc
drwx------ 0:0 732 B ├─⊕ root
drwxr-xr-x 0:0 0 B ├─⊕ run
-rwxrwxrwx 0:0 0 B ├── sbin → usr/sbin
│ Layer Details ├───────────────────────────────────────────────────────────────────── drwxr-xr-x 0:0 0 B ├── srv
drwxr-xr-x 0:0 0 B ├── sys
Tags: (unavailable) drwxrwxrwx 0:0 0 B ├── tmp
Id: 46176a0cbe9f3e95a08e3914ef9f4ac33fb1d412c9bacf2d1217616f76077fad.tar drwxr-xr-x 0:0 70 MB ├── usr
Digest: sha256:46176a0cbe9f3e95a08e3914ef9f4ac33fb1d412c9bacf2d1217616f76077fad drwxr-xr-x 0:0 21 MB │ ├─⊕ bin
Command: drwxr-xr-x 0:0 0 B │ ├── games
RUN /bin/sh -c mkdir -p "$HTTPD_PREFIX" && chown www-data:www-data "$HTTPD_PREFIX" drwxr-xr-x 0:0 0 B │ ├── include
# buildkit drwxr-xr-x 0:0 40 MB │ ├─⊕ lib
drwxr-xr-x 0:0 0 B │ ├─⊕ lib64
drwxr-xr-x 0:0 17 kB │ ├─⊕ libexec
drwxr-xr-x 0:0 0 B │ ├── local
drwxr-xr-x 33:33 0 B │ │ ├── apache2
drwxr-xr-x 0:0 0 B │ │ ├── bin
drwxr-xr-x 0:0 0 B │ │ ├── etc
drwxr-xr-x 0:0 0 B │ │ ├── games
│ Image Details ├───────────────────────────────────────────────────────────────────── drwxr-xr-x 0:0 0 B │ │ ├── include
drwxr-xr-x 0:0 0 B │ │ ├── lib
Image name: docker.io/library/httpd:latest -rwxrwxrwx 0:0 0 B │ │ ├── man → share/man
Total Image size: 147 MB drwxr-xr-x 0:0 0 B │ │ ├── sbin
Potential wasted space: 2.4 MB drwxr-xr-x 0:0 0 B │ │ ├─⊕ share
Image efficiency score: 99 % drwxr-xr-x 0:0 0 B │ │ └── src
drwxr-xr-x 0:0 6.3 MB │ ├─⊕ sbin
Count Total Space Path drwxr-xr-x 0:0 3.2 MB │ ├─⊕ share
2 1.5 MB /var/cache/debconf/templates.dat drwxr-xr-x 0:0 0 B │ └── src
3 262 kB /var/lib/dpkg/status-old drwxr-xr-x 0:0 4.2 MB └─⊕ var
3 262 kB /var/lib/dpkg/status
2 109 kB /var/log/dpkg.log
2 82 kB /var/lib/dpkg/info/perl-base.list
2 43 kB /var/log/apt/term.log
|
dive
also calculates an efficiency score by determining how many bytes of the final image (sum of all layers) are “wasted”.
An ideal score is 100%.
If a file is added in the first layer and then removed again in a following layer, this space is considered wasted and the efficiency score will be lower.
This can be used in CI tests to prevent needlessly big container images.
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
| $ CI=true dive podman://docker.io/library/httpd:latest
Image Source: podman://docker.io/library/httpd:latest
Fetching image... (this can take a while for large images)
Analyzing image...
efficiency: 99.0433 %
wastedBytes: 2399436 bytes (2.4 MB)
userWastedPercent: 3.3051 %
Inefficient Files:
Count Wasted Space File Path
2 1.5 MB /var/cache/debconf/templates.dat
3 262 kB /var/lib/dpkg/status-old
3 262 kB /var/lib/dpkg/status
2 109 kB /var/log/dpkg.log
2 82 kB /var/lib/dpkg/info/perl-base.list
2 43 kB /var/log/apt/term.log
2 20 kB /var/cache/debconf/config.dat
3 20 kB /var/log/apt/eipp.log.xz
3 16 kB /etc/ld.so.cache
3 15 kB /var/lib/apt/extended_states
2 12 kB /var/cache/ldconfig/aux-cache
2 9.6 kB /var/log/apt/history.log
2 8.2 kB /var/cache/debconf/config.dat-old
3 0 B /var/lib/dpkg/lock
2 0 B /var/lib/dpkg/triggers/Unincorp
3 0 B /var/lib/dpkg/triggers/Lock
Results:
PASS: highestUserWastedPercent
SKIP: highestWastedBytes: rule disabled
PASS: lowestEfficiency
Result:PASS [Total:3] [Passed:2] [Failed:0] [Warn:0] [Skipped:1]
|
#
diffoci
The next tool serves a similar, yet slightly different purpose: diffoci compares container images and shows the differences between them (unlike dive
which shows differences between layers).
It is the successor to the deprecated container-diff tool.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| $ diffoci diff --semantic docker.io/library/httpd:alpine3.17 docker.io/library/httpd:alpine3.18
TYPE NAME INPUT-0 INPUT-1
File etc/alpine-release 44d3bbd769620a3aa2fd5403d0755f6a4afba3c14e55c156f1c88905866ff04e aac1e8712e84121ed6a270d8003e4b52d6cb474b7d1709cd80a4bc5b6c82aa6d
File lib/libssl.so.3 7c6e060a33d5e0969aced1645e669666b58f223f27d783b8e49471088e3f756a c751d77b54c8ec62103a7052bbf6efdd4f05fa0df1168153ca88e7a81d27d9a6
File bin/busybox 36d96947f81bee3a5e1d436a333a52209f051bb3556028352d4273a748e2d136 8c6864b97c2b9ff892c859c59363482cb19a546a33a204b83a24f29048b2118f
File etc/os-release e72f89989f15487206f906fe8fb12a6d4363273c0ec886e66e66222a301e42f6 a001686cc7651f44cbbeae86e783d42dc05dce05940c55f6e394b6f9bea4c631
File etc/ssl/certs/ca-certificates.crt 92dd040a840947f00a14f66e9e17a799051ad7f88ddcde851b3711ee0a78a7ac 824cefcee69de918c76b7b92776f304c3a4b7f6281539118bc1d41a9dd8476d9
Layer ctx:/manifests-0/layers-0/layer name "lib/libapk.so.3.12.0" only appears in input 0
File etc/issue c2dc1872792bfb7e7af7467b82cf136ad468acab74d99a97d0e479dbf4665dab 774e6ac5abc275013a07cba05be43f0953cf759f852d177aa25f27ff62ab2eb4
File etc/ssl/openssl.cnf.dist 15c6ec001241f54bed98c135f0dab42f4a5e489f22544613b0671198f8ad8318 f6045e326b439e8ee31d4efd020ddf660d616c67d03e0e8e7a927eb14cbb5d1f
Layer ctx:/manifests-0/layers-0/layer name "etc/profile.d/locale.sh" only appears in input 0
File lib/libz.so.1.2.13 a7df4375fc9f37d5c284ef74e1d782d0100ce3a907ad2cbc6287f769cb90aac4 e0df969fd057eeaeb3064663b0f0af0f776fa42c697b56726a3eec6b13ca6986
File lib/libcrypto.so.3 4b061badaa55a61bcb65a05445f84d3003e3d554c10a56a44d7e6f0c0e349e6e 169037db2377a8227ddba422ca6e03dfb61c6660a2594ecf3b306bc3323ff80c
File etc/apk/repositories e23f0c9897bd5f21f1451995f7e05470026012288d7ec811ed6e5567cd521c01 293ab007ea57d023c3418a8063966a40407ceb90517e60ad710772c70694f9c5
...
|
#
container-structure-test
If you’d like to make sure that the container images you’re building always contain certain configuration files, have dependencies installed etc., then container-structure-test is the right tool.
Especially when you have to deal with a large number of (base) images and complex multi-stage builds, it is invaluable to run quick unit tests on them (after building) instead of having to go through a full integration test cycle (which usually involves uploading the container image to a registry, downloading it on the target host and then spinning up the application alongside all its dependencies).
container-structure-test
takes a container image and a simple YAML configuration file as input:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| schemaVersion: "2.0.0"
commandTests:
- name: 'helm is installed'
command: 'helm'
args: ['version']
exitCode: 0
fileContentTests:
- name: 'Velero is configured'
path: '/root/.config/velero/config.json'
expectedContents:
- '{"namespace":"kube-system"}'
fileExistenceTests:
- name: 'kubectl bash completions are installed'
path: '/etc/bash_completion.d/kubectl_completion'
shouldExist: true
|
and will then validate that all conditions are met:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| $ container-structure-test test --image "registry.example.com/kube-tools" --config container-structure-test.yaml
======================================================
====== Test file: container-structure-test.yaml ======
======================================================
=== RUN: Command Test: helm is installed
--- PASS
duration: 500.813594ms
stdout: version.BuildInfo{Version:"v3.11.2", GitCommit:"912ebc1cd10d38d340f048efaf0abda047c3468e", GitTreeState:"clean", GoVersion:"go1.18.10"}
=== RUN: File Content Test: Velero is configured
--- PASS
duration: 0s
=== RUN: File Existence Test: kubectl bash completions are installed
--- PASS
duration: 0s
=======================================================
======================= RESULTS =======================
=======================================================
Passes: 3
Failures: 0
Duration: 0.519310751s
Total tests: 3
PASS
|
Note that container-structure-test
works best with an actual Docker daemon, though with some workarounds it is possible to use Podman.
I previously gave a lightning talk about this testing tool, you can watch the recording here:
#
container-explorer
Given the disk image of a virtual (or physical) machine, container-explorer is a neat tool that allows you to explore the containers and container images on the host.
It supports containers run by containerd
as well as Docker
.
Since OpenShift uses the CRI-O container runtime, I haven’t used this tool myself but I imagine it can be quite helpful for post-mortem troubleshooting and forensic analysis.
#
skopeo
skopeo is a tool for working with container images in remote image registries.
It can show information about an image repository (list tags, digests and configuration) as well as copy/delete/sync images from one registry to another (without having to store the images locally).
This can be extremely handy when you are in an environment where you cannot run containers (e.g. inside a container!) or you don’t have root access.
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
74
75
76
77
| $ skopeo list-tags docker://docker.io/library/httpd
{
"Repository": "docker.io/library/httpd",
"Tags": [
"2",
"2-alpine",
"2-alpine3.13",
"2-alpine3.14",
"2-alpine3.15",
"2-alpine3.16",
"2-alpine3.17",
...
$ skopeo inspect docker://docker.io/library/httpd:latest
{
"Name": "docker.io/library/httpd",
"Digest": "sha256:36c8c79f900108f0f09fd4148ad35ade57cba0dc19d13f3d15be24ce94e6a639",
"RepoTags": [ ... ],
"Created": "2024-04-05T21:49:16Z",
"DockerVersion": "",
"Labels": null,
"Architecture": "amd64",
"Os": "linux",
"Layers": [
"sha256:b0a0cf830b12453b7e15359a804215a7bcccd3788e2bcecff2a03af64bbd4df7",
"sha256:851c52adaa9bf680b2eaa45164baa37665418fb02666fb36c268002edd853994",
"sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1",
"sha256:39d9f60535a61833df2e57cc08221031d3ae50f3955a6d6b0153214619d357fb",
"sha256:943a2b3cf551375517593790b58407ffc7028b4b34bca6c0756b76708ea4b3ce",
"sha256:ea83e81966d64f34a57ffcc118b75ad19bc9321f9809cf9b0df25548a2fa695e"
],
"LayersData": [
{
"MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
"Digest": "sha256:b0a0cf830b12453b7e15359a804215a7bcccd3788e2bcecff2a03af64bbd4df7",
"Size": 29150479,
"Annotations": null
},
{
"MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
"Digest": "sha256:851c52adaa9bf680b2eaa45164baa37665418fb02666fb36c268002edd853994",
"Size": 146,
"Annotations": null
},
{
"MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
"Digest": "sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1",
"Size": 32,
"Annotations": null
},
{
"MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
"Digest": "sha256:39d9f60535a61833df2e57cc08221031d3ae50f3955a6d6b0153214619d357fb",
"Size": 4006988,
"Annotations": null
},
{
"MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
"Digest": "sha256:943a2b3cf551375517593790b58407ffc7028b4b34bca6c0756b76708ea4b3ce",
"Size": 26027444,
"Annotations": null
},
{
"MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
"Digest": "sha256:ea83e81966d64f34a57ffcc118b75ad19bc9321f9809cf9b0df25548a2fa695e",
"Size": 293,
"Annotations": null
}
],
"Env": [
"PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HTTPD_PREFIX=/usr/local/apache2",
"HTTPD_VERSION=2.4.59",
"HTTPD_SHA256=ec51501ec480284ff52f637258135d333230a7d229c3afa6f6c2f9040e321323",
"HTTPD_PATCHES="
]
}
|
#
crane
The use case for crane is very similar to skopeo
, but it goes a step further: crane
can also modify existing container images.
Adding or changing layers, flattening (“squashing”) images, modifying metadata (environment variables, entrypoint, labels etc.).
It does this all directly in the registry, meaning no local container runtime is needed.
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
| # list files in an image
$ crane export docker.io/library/httpd:latest - | tar -tvf - | head
drwxr-xr-x 0/0 0 2024-04-23 17:00 usr
drwxr-xr-x 0/0 0 2024-04-24 06:54 usr/local
drwxr-xr-x 0/0 0 2024-04-24 06:57 usr/local/bin
-rwxr-xr-x 0/0 138 2024-04-24 06:54 usr/local/bin/httpd-foreground
drwxr-xr-x 0/0 0 2024-04-24 06:57 etc
drwxr-xr-x 0/0 0 2024-04-24 06:54 etc/gss
drwxr-xr-x 0/0 0 2023-08-14 22:06 etc/gss/mech.d
-rw-r--r-- 0/0 6682 2024-04-24 06:57 etc/ld.so.cache
drwxr-xr-x 0/0 0 2024-04-24 06:57 usr/lib
drwxr-xr-x 0/0 0 2024-04-24 06:57 usr/lib/x86_64-linux-gnu
# squash an image into a single layer and push it to a different registry
$ crane flatten docker.io/library/httpd:latest --tag registry.example.com/jack/httpd:flat --platform linux/amd64
...
# check that the new image only has one layer
$ crane config registry.example.com/jack/httpd:flat | jq .
{
"architecture": "amd64",
"created": "2024-04-05T21:49:16Z",
"history": [
{
"created": "0001-01-01T00:00:00Z",
"created_by": "crane flatten sha256:518e9447a236de47cc2fb4f2dbf06466b6cd5cf50f9951742f9a20af76e8118d"
}
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:3da7dd597070d0a53bef421a06dc2b7e3e6e937449c6f78e144ed4095c07a52a"
]
},
"config": {
"Cmd": [
"httpd-foreground"
],
"Env": [
"PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HTTPD_PREFIX=/usr/local/apache2",
"HTTPD_VERSION=2.4.59",
"HTTPD_SHA256=ec51501ec480284ff52f637258135d333230a7d229c3afa6f6c2f9040e321323",
"HTTPD_PATCHES="
],
"WorkingDir": "/usr/local/apache2",
"ExposedPorts": {
"80/tcp": {}
},
"ArgsEscaped": true,
"StopSignal": "SIGWINCH"
}
}
|
Check out their recipes page for some more examples and take a look at the go-containerregistry library (which exposes the primitives crane
is built on through a Go API) as well.
#
dredge
The final item in the toolbox is dredge.
Much like skopeo
and crane
it allows you to inspect repositories and images from remote registries.
One particularly neat feature is the diffing capability: it can show differences in files between two images.
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
| $ dredge image compare layers httpd:2.2 httpd:2.4 --os linux --arch amd64 --output=inline --compressed-size
- sha256:f49cf87b52c10aa83b4f4405800527a74400fb19ea1821d209293bc4d53966aa
- Size (compressed): 50.2 MB
+ sha256:b0a0cf830b12453b7e15359a804215a7bcccd3788e2bcecff2a03af64bbd4df7
+ Size (compressed): 27.8 MB
- sha256:24b1e09cbcb7a380a9c98956c6131125b3e0f0a7965d24db460f3548bf300eae
- Size (compressed): 155 bytes
+ sha256:851c52adaa9bf680b2eaa45164baa37665418fb02666fb36c268002edd853994
+ Size (compressed): 146 bytes
- sha256:8a4e0d64e915177206a8c2729a60e6b5001ae5bf47d598ad79a626e5cf983309
- Size (compressed): 11.4 MB
+ sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1
+ Size (compressed): 32 bytes
- sha256:bcbe0eb4ca5196217c6368138d5e1feabdb83272dc52859a6e5f2e81aec0c939
- Size (compressed): 1.9 MB
+ sha256:39d9f60535a61833df2e57cc08221031d3ae50f3955a6d6b0153214619d357fb
+ Size (compressed): 3.8 MB
- sha256:16e370c15d382f71f43248107f9524085de33a42809f33034e4571e503f358bf
- Size (compressed): 304 bytes
+ sha256:943a2b3cf551375517593790b58407ffc7028b4b34bca6c0756b76708ea4b3ce
+ Size (compressed): 24.8 MB
+ sha256:ea83e81966d64f34a57ffcc118b75ad19bc9321f9809cf9b0df25548a2fa695e
+ Size (compressed): 293 bytes
$ dredge image compare files httpd:2.2 httpd:2.4 --os linux --arch amd64 --base-layer-index 4 --target-layer-index 4
...
$ dredge image save-layers httpd:2.2 /tmp/httpd-2.2 --layer-index 4 --arch amd64 --os linux --no-squash
Getting layers for httpd:2.2
Layer sha256:16e370c15d382f71f43248107f9524085de33a42809f33034e4571e503f358bf
Downloading layer...
Extracting layer...
$ tree /tmp/httpd-2.2
/tmp/httpd-2.2
└── layer4-16e370c15d382f71f43248107f9524085de33a42809f33034e4571e503f358bf
└── usr
└── local
└── bin
└── httpd-foreground
|
It’s also notable that dredge
is written in C# with the .NET framework (unlike all other tools which are written in Go).
That’s all for today! I hope you found some new tools for your toolbox.
Some of them have overlapping functionality, so you can pick and choose the appropriate tool for the task at hand.
Also make sure to check out the awesome-docker and awesome-linux-containers lists.