Chapter Details | |
---|---|
Chapter Goal | Learn how Docker Overlay2 storage driver works |
Chapter Sections |
In this chapter, we will first experiment with Linux Overlay mount type to understand how Union Filesystem in Linux works. Then we will take a look at how Docker leverages this feature under the hood.
Let’s create a fictitious tree of directories which we will mount using Overlay mount type.
Step 1 Create a overlayfs directory at /home/stack/:
$ mkdir /home/stack/overlayfs
Step 2 Create the following directories and files in /home/stack/overlayfs directory:
/home/stack/overlayfs/
|_____lower1/
|_____lower2/
|_____merged/
|_____upper/
|_____work/
$ mkdir -p /home/stack/overlayfs/{lower1,lower2,merged,upper,work}
Step 3 Create the corresponding files which identify the directories it belongs to. The naming will be helpful once we mount and view the merged overlay:
/home/stack/overlayfs/
|_____lower1/
|_______lower1_file
|_____lower2/
|_______lower2_file
|_____merged/
|_____upper/
|_______upper_file
|_____work/
$ touch /home/stack/overlayfs/lower1/lower1_file \
/home/stack/overlayfs/lower2/lower2_file \
/home/stack/overlayfs/upper/upper_file
The /home/stack/overlayfs/merged and /home/stack/overlayfs/work directories are pre-emptively created to be passed in to the mount command.
Step 4 Become sudo before executing any mount commands:
$ sudo -i
#
Step 5 Change directory to /home/stack/overlayfs:
# cd /home/stack/overlayfs
/home/stack/overlayfs#
Step 6 Mount the two lowerdir to the upperdir using the overlay type:
/home/stack/overlayfs# mount -t overlay -o \
lowerdir=./lower1:./lower2,upperdir=./upper,workdir=./work overlay ./merged
Step 7 View the contents of the /home/stack/overlayfs/merged directory:
/home/stack/overlayfs# ls merged
lower1_file lower2_file upper_file
As we expected, the merged directory shows all three files we created in our different filesystems. This directory is synonymous to the Container layer that Docker mounts to a running container.
Step 8 View the contents of the /home/stack/overlayfs/upper directory:
/home/stack/overlayfs# ls upper
upper_file
This is the top-most layer; any changes we make in the merged directory will be written here. Let’s demonstrate that in the next steps.
Step 9 Edit the content of the lower1_file from the merged directory:
/home/stack/overlayfs# echo "from_merged" >> merged/lower1_file
Step 10 View the content of the lower1_file from the upper directory:
/home/stack/overlayfs# cat upper/lower1_file
from_merged
Even though we only changed the content in the merged/lower1_file, Overlay Filesystem copied the lower1_file from the lowerdir and wrote the contents in the upperdir writable layer. upper directory is where the ‘physical’ copy of the text from_merged exists.
Let’s check the lower1 directory to see if its content has been changed as well.
Step 11 View the content of the lower1_file from the lower1 directory:
/home/stack/overlayfs# cat lower1/lower1_file
This file is empty. No changes are written to the Read-only lower directories. This behavior is desirable because multiple mounts can reference this lower1 directory and have a consistent view of its contents. If a different Overlay mount wants to make changes to its content, it will be written in their own ‘upper’ layer.
Step 12 Create a new file in the merged directory:
/home/stack/overlayfs# touch merged/merged_file
New files are also persisted in the upper directory.
Step 13 View the contents of the upper directory:
/home/stack/overlayfs# ls upper/
lower1_file merged_file upper_file
If you are curious as to why we don’t see the lower2_file in the upper directory, it is because nothing has been changed between the lower2_file in the lower2 directory and the lower2_file in the merged directory. Therefore, any read requests to the lower2_file will directly go to the lower2 directory.
Keep in mind that any file or directory created in the upper directory which correspond to the names of the lower directories will be obscured and only the upper directory files will be presented to the merged overlay view.
Our current filesystem looks like the following:
/home/stack/overlayfs/
|_____lower1/
|_______lower1_file
|_____lower2/
|_______lower2_file
|_____upper/
|_______lower1_file
|_______merged_file
|_______upper_file
|_____merged/
|_______lower1_file
|_______lower2_file
|_______merged_file
|_______upper_file
|_____work/
Notes
You may spend some time to issue a few more tests on this filesystem before we unmount it in the next step.
Step 14 View the current mount in the /home/stack/overlayfs directory:
# mount | grep 'overlayfs'
overlay on /home/stack/overlayfs/merged type overlay ...
Step 15 Unmount the /home/stack/overlayfs/merged mount:
# umount /home/stack/overlayfs/merged
Step 16 View the /home/stack/overlayfs/merged directory:
# ls /home/stack/overlayfs/merged
Step 17 View the /home/stack/overlayfs/upper directory:
# ls /home/stack/overlayfs/upper
lower1_file merged_file upper_file
Step 18 Exit sudo:
# exit
$
Great! we are now ready to take a look at Docker’s point of view. The next sections are overlay2 storage driver specific exercise. This is the recommended driver in the latest Docker release. Keep in mind that your choice of storage driver will affect how Docker manages the image layers.
Before you proceed with this section, ensure that you have created the flask-app directory and the docker image <user-name>/myfirstapp from 3.1.2. Build a Docker image
We will look at the image history of the myfirstapp we created. Then we will make a small change in one of the files, build a new image, and identify the changes to our image layers.
Step 1 Issue the docker history command on your image <user-name>/myfirstapp. Note that you should replace the field <user-name> with the one you have used to create the image:
$ docker history <user-name>/myfirstapp
IMAGE CREATED CREATED BY
7e73fa752f06 32 minutes ago /bin/sh -c #(nop) CMD ["python3" "/us
0024be96aad8 36 minutes ago /bin/sh -c #(nop) EXPOSE 5000
592539f8bfd4 36 minutes ago /bin/sh -c #(nop) COPY file:7ff2405195
c2c2a169ccda 36 minutes ago /bin/sh -c #(nop) COPY file:2ee7f03cef
cba61ad1336c 36 minutes ago /bin/sh -c pip install --no-cache-dir
0560a450eb0f 36 minutes ago /bin/sh -c #(nop) COPY file:1b30cce28c
dfa92f9a4f27 36 minutes ago /bin/sh -c pip3 install --upgrade pip
5350c4c0e309 37 minutes ago /bin/sh -c apk add --update python3
055936d39205 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"]
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:a86aea1f3a7
There are nine images with the IMAGE-ID field and one image which is noted as <missing> (we will get back to that later). The output is a summary of how our docker image <user-name>/myfirstapp was built.
Step 2 View the Dockerfile for the flask-app and compare the image history listing from above:
$ cat /home/stack/flask-app/Dockerfile
# our base image
FROM alpine:3.9
# Install python and pip
RUN apk add --update python3
# upgrade pip
RUN pip3 install --upgrade pip
# install Python modules needed by the Python app
COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt
# copy files required for the app to run
COPY app.py /usr/src/app/
COPY templates/index.html /usr/src/app/templates/
# tell the port number the container should expose
EXPOSE 5000
# run the application
CMD ["python3", "/usr/src/app/app.py"]
As we know, each line of Dockerfile corresponds to an image layer. However, the docker history command suggests that there is an extra layer with the ID ‘<missing>’:
055936d39205 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"]
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:a86aea1f3a7
These two lines come from the statement FROM alpine:3.9. The <missing> tag is utilized by Docker’s build cache, and is required as an intermediary copy of the parent image; denoted by ADD file:a86aea1f3a7.. The <missing> tag may seem erroneous, but it is just an implementation detail related to local image buliding - which may be ignored (but it’s fun to see where it came from, right?)
Let’s find the alpine:3.9 image on Docker Hub and track down the commands from the parent image.
Step 3 Find the alpine:3.9 image on Docker Hub and locate its Image Artifact Details page listed in the Quick reference. For your convenience, we have copied the link here:
https://github.com/docker-library/repo-info/blob/master/repos/alpine/tag-details.md#alpine39
This page shows us the following:
We can see that the ADD and CMD [“/bin/sh”] are the default specified commands in our parent image which is part of our docker history. (Your exact file character may be different than the screenshot depending on when you are running this lab).
Step 4 Open the /home/stack/flask-app/templates/index.html file and replace Cat Gif of the day with Cat Gif of the week. Save and quit the file:
$ vim /home/stack/flask-app/templates/index.html
...
<div class="container">
<h4>Cat Gif of the week</h4>
<img src="{{url}}" />
...
Step 5 Build a new docker image based on this change:
$ docker build -t <user-name>/flaskapp2 /home/stack/flask-app/
Step 6 Compare the image history between the old image and the new image we created:
$ docker history <user-name>/myfirstapp
IMAGE CREATED CREATED BY
7e73fa752f06 32 minutes ago /bin/sh -c #(nop) CMD ["python3" "/us
0024be96aad8 36 minutes ago /bin/sh -c #(nop) EXPOSE 5000
592539f8bfd4 36 minutes ago /bin/sh -c #(nop) COPY file:7ff2405195
c2c2a169ccda 36 minutes ago /bin/sh -c #(nop) COPY file:2ee7f03cef
cba61ad1336c 36 minutes ago /bin/sh -c pip install --no-cache-dir
0560a450eb0f 36 minutes ago /bin/sh -c #(nop) COPY file:1b30cce28c
dfa92f9a4f27 36 minutes ago /bin/sh -c pip3 install --upgrade pip
5350c4c0e309 37 minutes ago /bin/sh -c apk add --update python3
055936d39205 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"]
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:a86aea1f3a7
$ docker history <user-name>/flaskapp2
IMAGE CREATED CREATED BY
6fc307b6cead 11 seconds ago /bin/sh -c #(nop) CMD ["python3" "/u
83c26f40583f 12 seconds ago /bin/sh -c #(nop) EXPOSE 5000
d752ef838ccd 12 seconds ago /bin/sh -c #(nop) COPY file:6b70bb374
c2c2a169ccda About an hour ago /bin/sh -c #(nop) COPY file:2ee7f03ce
cba61ad1336c About an hour ago /bin/sh -c pip install --no-cache-dir
0560a450eb0f About an hour ago /bin/sh -c #(nop) COPY file:1b30cce28
dfa92f9a4f27 About an hour ago /bin/sh -c pip3 install --upgrade pip
5350c4c0e309 About an hour ago /bin/sh -c apk add --update python3
055936d39205 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"]
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:a86aea1f3a
As you can see, three layers have changed and new image layers have been created starting with the COPY command which was for the templates/index.html file. This is why it is important to order the Dockerfile directives such that frequently changing artifacts should be towards the bottom of the file.
Notes
‘docker manifest’ command is in experimental stages in version 18.02+ which allows to inspect, pull, and push image manifests from the registry. For more information, see: https://github.com/docker/cli/pull/138
Lastly, let’s run our newly created image and utilize Docker utilities to view the filesystems locations.
Step 1 Run the flaskapp2 we created in the previous section:
$ docker run -dP <user-name>/flaskapp2
13304e9fc3757f3752ddf843...
Step 2 Inspect the running container (Copy and paste the container ID from the output of the first command) and scroll down to view the “GraphDriver” key:
$ docker inspect <CONTAINER ID>
"GraphDriver": {
"Name": "overlay2",
"Data": {
"LowerDir": "/var/lib/docker/overlay2/407ad1f1c9cdb5a947d97c09ed4cb8b924ba265df87f10533d898...
"MergedDir": "/var/lib/docker/overlay2/407ad1f1c9cdb5a947d97c09ed4cb8b924ba265df87f10433d898/merged",
"UpperDir": "/var/lib/docker/overlay2/407ad1f1c9cdb5a947d97c09ed4cb8b924ba265df87f10533d898/diff",
"WorkDir": "/var/lib/docker/overlay2/407ad1f1c9cdb5a947d97c09ed4cb8b924ba265df87f10513d898/work"
}
}
The concepts we have learned in 4.1. Linux Overlay Filesystem apply to this output, where:
- LowerDir: Read-only image layers separated by colons from top most layer -> bottom most layer
- MergedDir: Merged view of all the layers
- UpperDir: Read-write layer where changes are written
- WorkDir: Used specifically by the underlying Linux OverlayFS
Keep these locations handy by copying it over to a text file!
Step 3 Log-in to the container and create a file in the root directory, then exit from the container:
$ docker exec -it <CONTAINER ID> /bin/sh
# touch /hello-from-container
# exit
Where do you think the new file was created? If you said in the UpperDir, you are correct! Let’s take a look.
Step 4 Become sudo, then list the contents of the UpperDir directory specific to your running container:
$ sudo -i
# ls /var/lib/docker/overlay2/<your-location>/diff
hello-from-container root
As you can see, changes made to your container filesystem is stored in the “UpperDir”. Of course, it is also available in the “MergedDir”.
Step 5 Lastly, view the /var/lib/docker/overlay2 directory:
# ls /var/lib/docker/overlay2/
04c25422e8328ea076a55ed2f31d3fe51dec68225afc6632cf40d4fa3531fc6c
53f8d3506b1d3b136494adafa5500c6c50c65ec88c13c7f2945ee20f5a6f36ef
b2595ac081610fe2d195e550bff7afd3bc832d4e7fbc43462b659674c80d4f0c-init
04c25422e8328ea076a55ed2f31d3fe51dec68225afc6632cf40d4fa3531fc6c-init
53f8d3506b1d3b136494adafa5500c6c50c65ec88c13c7f2945ee20f5a6f36ef-init
d99085c36ac8163c8b69672f7aa3b9afcd33cf502d1792a3370abedb668b4f0d
059ae1a27495246eccbc1f64c27a881483fc81a8696a5269a6a5a897facd8436
5618685db218647f445527a50262b2942bb562f61b240686cbf921b50659af43
dcf62de6d364da88df6e072b553995dfc3faffadee63d88553f41c2cc69fd7db
l
As you can see, your Docker journey will leave you with many artifacts and gifts in your /var/lib/docker/overlay2 directory. You will need to remove any unused containers and its “dangling” images to clean-up after yourself.
Step 6 Take a look at the /var/lib/docker/overlay2/l directory:
# ls -lha /var/lib/docker/overlay2/l
lrwxrwxrwx 1 root root 72 Feb 5 21:34 27P43I4L5QQ46C3NMS33PR3EOT -> ../5618685db218640659af43/diff
lrwxrwxrwx 1 root root 72 Feb 8 20:58 2G3FG4AEOXZGO2BLBY5AXOEXRH -> ../2d0507a0a64dca7e588722/diff
lrwxrwxrwx 1 root root 77 Feb 8 21:18 3H6UK2OWJVMDKQ7RADZI7WZLYF -> ../b2595ac081610fe20d4f0c-init/diff
This is another implementation detail and not an accidental ‘l’ directory. These “shortened” values link to the longer names of the actual directories in the same directories. These symbolic links help avoid the Linux ‘mount’ command from exceeding page size limitation.
Step 7 Exit from sudo, and cleanup!:
# exit
$ docker rm -f <CONTAINER ID>
Checkpoint
Historically dockerd was a monolithic system, but in 2015 under the pressure from CoreOS rkt and AppC it was rewritten as part of the Open Container Initiative (OCI) effort, and was refactored into reusable components:
Step 1 Cleanup your environment and make sure there are no running containers:
$ docker rm -f $(docker ps -qa)
Step 2 Find dockerd and containerd daemons:
$ ps axf | grep containerd
5266 pts/1 S+ 0:00 \_ grep --color=auto containerd
4484 ? Ssl 0:02 /usr/bin/containerd
4995 ? Ssl 0:00 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
Run a container:
$ docker run -d -P --name hello-world-app training/webapp python app.py
1ce6630cc72a....
List the processes again:
$ ps axf | grep containerd
5587 pts/1 S+ 0:00 \_ grep --color=auto containerd
4484 ? Ssl 0:03 /usr/bin/containerd
5522 ? Sl 0:00 \_ containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/1ce6630cc72a.... -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc
4995 ? Ssl 0:06 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
We are interested in the parameters passed to containerd-shim:
containerd-shim
-namespace moby
-workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/1ce6630cc72a....
-address /run/containerd/containerd.sock
-containerd-binary /usr/bin/containerd
-runtime-root /var/run/docker/runtime-runc
Step 3 Use containerd command-line tool ctr to inspect the above:
$ sudo ctr --namespace moby containers list
CONTAINER IMAGE RUNTIME
1ce6630cc72a.... - io.containerd.runtime.v1.linux
containerd has the concept of a namespace. Namespaces allow the ability for one client to create, edit, and delete resources without affecting another client. A resource can be anything from an image, container, task, or snapshot. Docker uses the namespace moby. Check existing namesapces:
$ sudo ctr namespaces ls
NAME LABELS
moby
Step 4 Docker manages images in /var/lib/docker, so containerd has no knowledge of the container images:
$ sudo ctr -namespace moby image ls
REF TYPE DIGEST SIZE PLATFORMS LABELS
You can pull images using ctr but Docker will not know about them because they are places in /var/lib/containerd.io/:
$ sudo ctr -n moby image pull docker.io/library/hello-world:latest
docker.io/library/hello-world:latest: resolved |++++++++++++++++++++++++++++|
index-sha256:c3b4ada4687bbaa170745b3e4dd8: done |++++++++++++++++++++++++++++|
manifest-sha256:92c7f9c92844bbbb5d0a101b2: done |++++++++++++++++++++++++++++|
layer-sha256:1b930d010525941c1d56ec53b97b: done |++++++++++++++++++++++++++++|
config-sha256:fce289e99eb9bca977dae136fbe: done |++++++++++++++++++++++++++++|
elapsed: 1.2 s total: 3.5 Ki (3.0 KiB/s)
unpacking linux/amd64 sha256:c3b4ada4687bbaa170745b3e4dd...
done
Step 5 Run the a container based on the downloaded image. ctr requires that we use the fully qualified name of the image:
$ sudo ctr -n moby image ls -q
docker.io/library/hello-world:latest
$ sudo ctr -n moby run docker.io/library/hello-world:latest demo
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
....
Docker bridge networks is not used by Kuberenetes, so it’s no longer needed. In this lab we will configure Docker to disable the legacy bridge network docker0.
Step 1 Become root to edit /etc/docker/daemon.json. This file is optional and may not always be there:
$ sudo -i
# cat /etc/docker/daemon.json
{
"live-restore": true,
"storage-driver": "overlay2"
}
Step 2 As you can see, in our lab enviroment it’s already created and initialized to allow dockerd to be restarted without killing all the containers started by docker. Edit the file and add "bridge": "none" to disable the legacy bridge network. Make sure the file remains in valid json format. End result should be similar to:
# cat /etc/docker/daemon.json
{
"live-restore": true,
"bridge": "none",
"storage-driver": "overlay2"
}
Step 3 Restart the Docker daemon for the change to take effect:
# systemctl restart docker.service
Step 4 Check the available networks:
# exit
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
97b103fb9417 host host local
4fce902f62b1 none null local
We are now ready for Kubernetes installation.