This article is not for those who are easily afraid by the low-level specifics of container images and rely on high-level tools (buildkit, docker, buildah etc.) to package the application and its runtime dependencies into a container image.
A container image, in its most basic form, is a collection of tarball files in charge of the following tasks:
That's nice, but …
…
The Open Container Initiative is a non-profit open governance organization whose sole goal is to create open industry standards for container formats and runtimes. With that industry aligning around a set of open, universal container technology standards.
This specification defines an OCI Image as a manifest, an image index (optional), a set of filesystem layers, and a configuration. The purpose of this standard is to make it possible to develop compatible tools for different container runtimes (docker, container, rkt, runc, crun, etc.)
At the top level, a OCI Image is just a tarball, and it has the layout as below.
# OCI container image layout
├── blobs
│ └── sha256
│ ├── DIFFID (image.manifest)
│ └── DIFFID (image.config)
│ └── DIFFID (image.rootfs)
│ └── DIFFID (image.application)
│ └── DIFFID (image .... )
│ └── DIFFID (image .... +n)
├── index.json
└── oci-layout
The image index is a collection of manifests for one or more platforms and is the starting point. It effectively acts as a manifest, listing all of the "resources" that a single container image requires.
Name |
Description |
---|---|
architecture |
Name of the compilation/CPU architecture. |
os |
Name of the OS. (Linux, windows) |
os version |
Version of the operating system targeted by the referenced blob. |
manifests |
List of manifests for specific platforms. |
~$ skopeo inspect --raw docker://yorek/multiarch-hello-world
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 428,
"digest": "sha256:9b874ccdb73e1aaf29e1c0fd3a550ddfa486d73525f2d77b452e30a09d3cb417",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
.....
]
}
An image manifest specifies the layers and configuration of a single container image for a certain architecture and operating system.
~$ cat blobs/sha256/dfddba39ed871b3994726d34b0f33b313101c8262567590c02d3763f9f5a27f6 | jq
{
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:97895c15ee91c7d4957334444c1a449c52fc6c2f90a7113b49d77c8718f7d8c4",
"size": 3058
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:45337b3433fa437664b13287291679cf9edca3230f1f37043cc61dab03199d6d",
"size": 26692286
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:36d0aa647669adaa137c59f52b7e91018ef63c75a0daee690963133217ee47b3",
"size": 54120976
},
.......
]
}
You'll notice that the image manifest is merely a text file with no raw data streams from any of the filesystem layers. The manifest format provides a list of distribution artifact hashes.
This list of hashes is effectively a set of instructions on what to download, in what order, how to validate against the content hashes after decompression.
These hashes are represented in Digests format to enable content addressability and uniquely identify image resources.
Digest format is
digest ::= algorithm ":" encoded
sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b
Since we're talking about hashes, we need to distinguish between content hashes and distribution hashes.
Content hashes |
Distribution hashes |
---|---|
A content hash is used for uncompressed data present in the layers. |
A distribution artifact hash is simply a hash of the compressed blob. |
|
To hash a gzipped item, compression must first occur. |
|
Check the downloaded layers' integrity without first uncompressing them. |
|
The use of distribution artifact hashes eliminates the requirement to maintain a mapping of content hashes. |
The root file system is the hierarchical file tree. It contains the files and directories critical for system operation, including the mounting device directory and programs for booting the container processes.
But how to build this hierarchical file order?
Well, the answer is image layers, also known as achieve files. We record file system changes (addition of files, removal of files, or updates) as achieve files. The first achieve file is the base layer; all the other achieve files contain only the changes to its base. Together these achieve files, in chronological order, are what make up the final Root Filesystem.
But ….. I still have questions
{
"created": "2021-01-07T01:29:27.650294696Z",
"architecture": "amd64",
"os": "linux",
"config": {
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/hello"
]
},
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
],
.....
},
"history": [
.....
],
.....
.....
}
$ ALGORITHM="sha256"
$ BUILD_DIR="build"
$ BLOBS_DIR="$BUILD_DIR/blobs/$ALGORITHM"
$ mkdir -p $BLOBS_DIR
# GET AUTH TOKEN
$ TOKEN=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/busybox:pull" | jq -r .token)
# PULL LATEST MANIFEST (DOCKER V2) OF BUSYBOX FROM THE REGISTRY
$ curl https://registry.hub.docker.com/v2/library/busybox/manifests/latest \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
-H "Authorization: Bearer $TOKEN" \
-L \
-o manifest.json
# PARSE LAYERS REFs FROM THE MANIFEST
$ CONFIG_LAYER=$(cat manifest.json | jq -r '.config.digest')
$ ROOTFS_LAYER_DIGEST=$(cat manifest.json | jq -r '.layers[0].digest')
$ ROOTFS_LAYER_SIZE=$(cat manifest.json | jq -r '.layers[0].size')
# PULL CONFIG LAYER OF BUSYBOX FROM THE REGISTRY
$ curl https://registry.hub.docker.com/v2/library/busybox/blobs/$CONFIG_LAYER \
-H "Authorization: Bearer $TOKEN" \
-L \
-o config.json
$ ROOTFS_LAYER_DIFF=$(cat config.json | jq -r '.rootfs.diff_ids[0]')
# PULL ROOT FILE SYTEM LAYER OF BUSYBOX FROM THE REGISTRY
$ curl https://registry.hub.docker.com/v2/library/busybox/blobs/$ROOTFS_LAYER_DIGEST \
-H "Authorization: Bearer $TOKEN" \
-L \
-o $BLOBS_DIR/${ROOTFS_LAYER_DIGEST//$ALGORITHM:/}
# CREATE APPLICATION LAYER LOCALLY
$ APP_LAYER="app-layer.tar"
$ GZIP_APP_LAYER="$APP_LAYER.gz"
$ echo 'echo "Hello World"' > hello.sh && chmod +x hello.sh
$ tar -cvf $APP_LAYER hello.sh
# CALCULATE LAYER'S DIGEST, DIFFID AND SIZE
$ APP_LAYER_DIFF="$(sha256sum < $APP_LAYER | sed 's/\s*-//g')"
$ APP_LAYER_DIGEST="$(gzip < $APP_LAYER > $GZIP_APP_LAYER | sha256sum | sed 's/\s*-//g')"
$ APP_LAYER_SIZE="$(stat -c%s $GZIP_APP_LAYER)"
# MOVE APPLICATION LAYER TO BLOBS FOLDER
$ cp $GZIP_APP_LAYER "$BLOBS_DIR/$APP_LAYER_DIGEST"
# OCI container image layout
# ├── blobs
# │ └── sha256
# │ ├── DIFFID (image.manifest)
# │ └── DIFFID (image.config)
# │ └── DIFFID (image.rootfs)
# │ └── DIFFID (image.application)
# ├── index.json
# └── oci-layout
# CREATE OCI-LAYOUT FILE
$ echo > "$BUILD_DIR/oci-layout" << EOF
{
"imageLayoutVersion": "1.0.0"
} EOF
##
## CREATE OCI-CONFIG LAYER
$ CONFIG_LAYER="image-config.json"
$ echo > $CONFIG_LAYER << EOF
{
"created": "2020-04-07T01:29:27.650294696Z",
"architecture": "amd64",
"os": "linux",
"config": {
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/hello"
]
},
"rootfs": {
"type": "layers",
"diff_ids": [
"$ROOTFS_LAYER_DIFF",
"$APP_LAYER_DIFF"
]
},
"history": []
}
EOF
$ CONFIG_LAYER_DIGEST="$(sha256sum < $CONFIG_LAYER | sed 's/\s*-//g')"
$ CONFIG_LAYER_SIZE="$(stat -c%s $CONFIG_LAYER)"
$ cp $CONFIG_LAYER "$BLOBS_DIR/$CONFIG_LAYER_DIGEST"
# CREATE OCI-CONFIG LAYER
$ MANIFEST_LAYER="image-manifest.json"
$ cat > $MANIFEST_LAYER << EOF
{
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "$CONFIG_LAYER_DIGEST",
"size": $CONFIG_LAYER_SIZE
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "$ROOTFS_LAYER_DIGEST",
"size": $ROOTFS_LAYER_SIZE
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "$APP_LAYER_DIGEST",
"size": $APP_LAYER_SIZE
}
]
}
EOF
$ MANIFEST_LAYER_DIGEST="$(sha256sum < $MANIFEST_LAYER | sed 's/\s*-//g')"
$ MANIFEST_LAYER_SIZE="$(stat -c%s $MANIFEST_LAYER)"
$ cp $MANIFEST_LAYER "$BLOBS_DIR/$MANIFEST_LAYER_DIGEST"
# CREATE INDEX FILE
$ MANIFESTS_INDEX="index.json"
$ cat > "$BUILD_DIR/$MANIFESTS_INDEX" << EOF
{
"schemaVersion": 2,
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "$MANIFEST_LAYER_DIGEST",
"size": $MANIFEST_LAYER_SIZE,
"annotations": {
"org.opencontainers.image.ref.name": "latest"
}
}
]
}
EOF
# CREATE OCI-LAYOUT FOLDER STRUCTURE
$ tree .
├── blobs
│ └── sha256
│ ├── 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
│ ├── 5cc84ad355aaa64f46ea9c7bbcc319a9d808ab15088a27209c9e70ef86e5a2aa
│ ├── 9710b9c0c7c956ca8d2884ee73d7fb578921f127f1eae05078965b2e3125b8de
│ └── e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
├── index.json
└── oci-layout
2 directories, 6 files
# $
# ├── blobs
# │ └── sha256
# │ ├── 5cc84ad355aaa64f46ea9c7bbcc319a9d808ab15088a27209c9e70ef86e5a2aa
# │ └── e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
# STEP0: CREATE ROOTFS FOLDER
cd $BUNDLE_DIR
BUNDLE_DIR="runtime_bundle"
mkdir $BUNDLE_DIR && cd $BUNDLE_DIR
# STEP1: EXTRACT BUSYBOX LAYER INTO ROOTFS FOLDER
$ tar -vxzf ../$BUNDLE_DIR/$BLOBS_DIR/5cc84ad355aaa64f46ea9c7bbcc319a9d808ab15088a27209c9e70ef86e5a2aa -C rootfs
# STEP2: EXTRACT APPLICATION LAYER INTO ROOTFS FOLDER
$ tar -vxzf ../$BUNDLE_DIR/$BLOBS_DIR/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -C rootfs
# STEP3: ALLOW RUNC TO CREATE A DUMMY CONFIGURE FILE FOR YOU AND RUN
# runc spec
# runc run test
# ls
bin dev etc hello.sh home proc root sys tmp usr var
# sh hello.sh
Hello World