Container Bundle Deep Dive


Let's re-cap what container bundle is: it's just a filesystem with all the files and directories that container needs, plus a config.json file - a specification, compliant with runtime-spec, on how to run this container. Config file just points to a directory and expects this directory to contain the complete root filesystem. Let’s create a container bundle and examine it.
One way to create it is to do it by hand. We would need to create every file and directory required for the container process to run. We could certainly try to go this way, it’s certainly not the easiest path, and we might not even learn too much while doing this.
Another way to get the container bundle is to unpack a container image. As you already know, container image is just a way to create, package and distribute container bundles - so we can take an existing image and simply unpack it.
We already have a container image in an oci format - we copied it with the help of Skopeo. To unpack this image into a container bundle, we will need umoci.
To unpack the image into a container bundle, we need to use umoci unpack command:
umoci unpack --rootless --image httpd:2.4.47 bundle
umoci unpack command will take an image, from the local directory and unpack it into a new directory named bundle. We need to pass the “rootless” flag to use the rootless mode - or we could also just run “sudo umoci”, of course. We will talk about rootless approach in the latter lessons.
Let’s look inside the bundle directory.
Two files there do not belong to the bundle: umoci.json and the mtree file. Those are coming from umoci. mtree file can be used to verify that the contents of the rootfs are matching the manifest inside this mtree file, and umoci.json file contains some umoci-specific configuration. Let’s remove both files, so that we can focus on the bundle.
The rootfs directory contains the complete file system required to run the container. When umoci is running, it merges the image layers together to build this rootfs. We’ve discussed those layers before, but it’s worth revisting that merging layers is not about simply putting them on top of each other - there are more things to do, like, for example, figuring out which files to remove.
The config.json file is the runtime spec config file. Let’s look inside:
{
"ociVersion": "1.0.0",
"process": {
"terminal": true,
"user": {
"uid": 0,
"gid": 0
},
"args": [
"httpd-foreground"
],
"env": [
"PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm",
"HTTPD_PREFIX=/usr/local/apache2",
"HTTPD_VERSION=2.4.47",
"HTTPD_SHA256=23d006dbc8e578116a1138fa457eea824048458e89c84087219f0372880c03ca",
"HTTPD_PATCHES=",
"HOME=/root"
],
"cwd": "/usr/local/apache2",
"capabilities": {
"bounding": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"effective": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"inheritable": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"permitted": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"ambient": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
]
},
"rlimits": [
{
"type": "RLIMIT_NOFILE",
"hard": 1024,
"soft": 1024
}
],
"noNewPrivileges": true
},
"root": {
"path": "rootfs"
},
"hostname": "umoci-default",
"mounts": [
{
"destination": "/proc",
"type": "proc",
"source": "proc"
},
{
"destination": "/dev",
"type": "tmpfs",
"source": "tmpfs",
"options": [
"nosuid",
"strictatime",
"mode=755",
"size=65536k"
]
},
{
"destination": "/dev/pts",
"type": "devpts",
"source": "devpts",
"options": [
"nosuid",
"noexec",
"newinstance",
"ptmxmode=0666",
"mode=0620"
]
},
{
"destination": "/dev/shm",
"type": "tmpfs",
"source": "shm",
"options": [
"nosuid",
"noexec",
"nodev",
"mode=1777",
"size=65536k"
]
},
{
"destination": "/dev/mqueue",
"type": "mqueue",
"source": "mqueue",
"options": [
"nosuid",
"noexec",
"nodev"
]
},
{
"destination": "/sys",
"type": "bind",
"source": "/sys",
"options": [
"rbind",
"nosuid",
"noexec",
"nodev",
"ro"
]
},
{
"destination": "/etc/resolv.conf",
"type": "bind",
"source": "/etc/resolv.conf",
"options": [
"nodev",
"nosuid",
"rbind",
"ro"
]
}
],
"annotations": {
"org.opencontainers.image.architecture": "amd64",
"org.opencontainers.image.author": "",
"org.opencontainers.image.created": "2021-05-03T23:47:09.946682701Z",
"org.opencontainers.image.exposedPorts": "80/tcp",
"org.opencontainers.image.os": "linux",
"org.opencontainers.image.stopSignal": "SIGWINCH"
},
"linux": {
"uidMappings": [
{
"containerID": 0,
"hostID": 1000,
"size": 1
}
],
"gidMappings": [
{
"containerID": 0,
"hostID": 1000,
"size": 1
}
],
"namespaces": [
{
"type": "pid"
},
{
"type": "ipc"
},
{
"type": "uts"
},
{
"type": "mount"
},
{
"type": "user"
}
],
"maskedPaths": [
"/proc/kcore",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/sys/firmware",
"/proc/scsi"
],
"readonlyPaths": [
"/proc/asound",
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
}
}
First thing that we see is the ociVersion - that's the version of the actual runtime-spec. If we go to github and check the list of tags, we will see that this version is not the latest one - but that doesn’t matter, as long the container runtime supports the version we have here.
Next comes the process section - that's where we can specify which process to run inside the container. In this generated file, the process is an httpd server - remember, we unpacked the httpd image. Also inside this block, we can see the user and the group used to run this process, environment variables for this process, starting directory, various limits and Linux capabilities.
Besides process, there is a self explanatory hostname section, populated by umoci and a rather curious "root" section. What does this one do? To answer this question, we can simply go back to github and inspect the related part of the runtime-spec. Here we see that root is "container's root filesystem.", with a path pointing to this filesystem. This simply means, that container runtime will expect a "rootfs" directory to exist next to this config file, and this directory is the whole filesystem needed to run this container.
After the root section, there are mounts - container will need a number of directories from the host to operate.
Finally, there is a linux section, with Linux-specific options. The cool part about runtime-spec is that it aims to be platform agnostic. There are windows containers, solaris containers and you can even run virtual machines from runtime-spec, which I won't talk about in depth now.
Because Linux namespaces are a linux specific feature, they are specified in the Linux block.
From the discussion of the image config file, you can remember that it looked very similar to this config file. And indeed, the way umoci got the runtime-spec file is by converting the image-spec config file. This conversion is required, because image-spec and runtime-spec are, in fact, two independant specifications.
When I said, that you don’t need an image to run a container, I meant it - you can come up with your own way to create container bundles with runtime-spec inside. And it works in reverse - you can use images as a packaging format, but come up with a different way to run them, you are not locked to runtime-spec. Naturally, in majority of scenarious, those two specifications work together, and that's why the tools that implement OCI speficiations need to do this kind of conversion.
With this unpacked rootfs and the runtime spec file we’ve created, we have a complete container bundle, which means we have everything we need to run a container! In the next article, we will install the container runtime and then run the image.