Container Image Structuring for container runtimes

Container Image Structuring for container runtimes

While you might have read posts about docker being dead, but given its adoption. That’s not really the case.

While we have other container runtimes like runc, containerd, rkt and some others. Docker is still something which a lot of folks running containers use as their container runtime.

What this post will describe is one of the many approaches of structuring your container images, keeping in mind reusability, security and best practices in mind and keeping them as lightweight as possible. At the time of writing this, this is something which is still used to run production container workloads in my last company.

Prelude

Before going ahead, just so that we are on the same page.

A Container image is a filesystem tree that includes all of the requirements for running a container, as well as metadata describing the content. You can think of it as a packaging technology.

A container is composed of two things: a writable filesystem layer on top of a container image, and a traditional linux process. Multiple containers can run on the same machine and share the OS kernel with other containers, each running as an isolated processes in the user space. Containers take up less space than VMs (application container images are typically tens of MBs in size), and start almost instantly.

Source: project atomic, container best practices

Introduction

Immutable Server pattern is something which we used to follow in my last company. Netflix has written at length on how they do it. More on how we used to do it in another post.

I will not go into the how and why of immutable infra in this blog post, as that is something which deserves its own post.

Docker presents fit’s right in if you follow the above pattern for your infrastructure.

Which is, if you are baking the whole application using packer or something similar, including config inside the AMI,and then adding that AMI in the launch config for the ASG so that the newer instance which comes up when the ASG scales up, is an exact copy of the instances already present in the ASG.

What you have is repeatable infra in short, with the above pattern. And you start treating servers as cattle and not pets.

The layering of container images

We used to follow a layered approach of immutable infrastructure, where we would have a base layer.

Base Layer

contains a fresh copy of an operating system (alpine Linux in this case) and would include core system tools, (eg: such as bash or coreutils, curl, dumb-init et al) and tools necessary to install packages and make updates to the image over time.

As for the Intermediate container images, each would use the base layer, hence inheriting from the base image.

Intermediate Layers

Note: The above intermediate layers are just to show you an example, you can replace it with your use case.

Dependency managers like pip/composer/golang-dep would go in this layer for the next layer to make use of it and after their use we can clear their cache.

For example in the case of

An example of such a setup

FROM gliderlabs/alpine:3.4

ENV ALPINE_VER=3.4
ENV ALPINE_SHA=45ba65c1116aaf668f7ab5f2b3ae2ef4b00738be

RUN apk update && \
    apk add xorriso git xz curl tar iptables cpio bash && \
    rm -rf /var/cache/apk/*

RUN apk add -U --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing aufs-util

RUN addgroup -g 2999 docker

after which you would create the container image from this Dockerfile. And for the sake of this example, you would name it as base-image

To create a JAVA based intermediate layer

FROM tasdikrahman/base-layer:0.1.0

ENV LANG=C.UTF-8

RUN curl -LO 'http://download.oracle.com/otn-pub/java/jdk/8u131-b11/d54c1d3a095b4ff2b6607d096fa80163/jre-8u131-linux-x64.tar.gz' -H 'Cookie: oraclelicense=accept-securebackup-cookie' \
	&& chown root:root jre-8u131-linux-x64.tar.gz \
	&& tar -xzf jre-8u131-linux-x64.tar.gz \
	&& rm jre-8u131-linux-x64.tar.gz \
	&& mv jre1.8.0_131 /usr/local/lib

WORKDIR /usr/local/lib/jre1.8.0_131

ENV JAVA_HOME /usr/local/lib/jre1.8.0_131
ENV PATH $JAVA_HOME/bin:$PATH

RUN apk del --no-cache curl tar # wget ca-certificates
FROM tasdikrahman/java-base:0.1.0

# Your application specific requirements etc.

Application Image layer

This is where the container image would contain dependencies specific to the application and other required tooling, inheriting other things from the previous layers.

Security

RUN groupadd -r myapp && useradd -r -g myapp myapp
USER myapp

Keeping the size of the docker image small

At each layer

The above division ideally, should always be maintained and any new requirement should always go into the layer most appropriate for it

Example:

RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion

Good to have

References