Kubernetes Best Practices. Creating Small Containers

Kubernetes Best Practices. Creating Small Containers

The first step in deploying to Kubernetes is hosting your application in a container. In this series, we'll look at how you can create a small, secure container image.
Thanks to Docker, creating container images has never been easier. Specify a base image, add your changes, and create a container.

Kubernetes Best Practices. Creating Small Containers

While this technique is great for getting started, using the default base images can lead to insecure working with large images full of vulnerabilities.

In addition, most images in Docker use either Debian or Ubuntu for the base image, and while this provides excellent compatibility and ease of adaptation (the Docker file is only two lines of code), base images can add hundreds of megabytes of extra load to your container. For example, a simple node.js file for a Go "hello-world" application is about 700 megabytes, while your application itself is only a few megabytes in size.

Kubernetes Best Practices. Creating Small Containers

So all this extra workload is a waste of digital space and a great hiding place for security vulnerabilities and bugs. Therefore, let's look at two ways to reduce the size of a container image.

The first is the use of small base images, the second is the use of the Builder Pattern design pattern. Using smaller base images is probably the easiest way to reduce the size of your container. Most likely, your language or the stack you are using provides a much smaller original application image than the default image. Let's take a look at our node.js container.

Kubernetes Best Practices. Creating Small Containers

By default, in Docker, the size of the node:8 base image is 670MB, while the size of node:8-alpine is only 65MB, which is 10 times smaller. By using the smaller Alpine base image, you will significantly reduce the size of your container. Alpine is a small and lightweight Linux distribution that is very popular with Docker users because it is compatible with many applications while keeping containers small. Unlike the default "node" Docker image, "node:alpine" removes a lot of utility files and programs, leaving only those that are enough to run your application.

To move to a smaller base image, simply update your Dockerfile to start with the new base image:

Kubernetes Best Practices. Creating Small Containers

Now, unlike the old onbuild image, you need to copy your code into the container and install any dependencies. In the new Dockerfile, the container starts with a node:alpine image, then creates a code directory, installs dependencies using the NPM package manager, and finally starts server.js.

Kubernetes Best Practices. Creating Small Containers

This upgrade results in a 10x smaller container. If your programming language or stack does not have a base image reduction feature, use Alpine Linux. It will also provide the ability to fully manage the contents of the container. Using small base images is a great way to quickly create small containers. But even more reduction can be achieved using the Pattern Builder.

Kubernetes Best Practices. Creating Small Containers

In interpreted languages, the source code is first passed to the interpreter and then directly executed. In compiled languages, source code is first converted into compiled code. However, compilation often uses tools that are not really needed to run the code. This means that you can completely remove these tools from the final container. You can use Pattern Builder for this.

Kubernetes Best Practices. Creating Small Containers

The code is generated in the first container and compiled. The compiled code is then packaged into a final container without the compilers and tools needed to compile that code. Let's run a Go application through this process. First, we will move from the onbuild image to Alpine Linux.

Kubernetes Best Practices. Creating Small Containers

In the new Dockerfile, the container starts with the golang:alpine image. It then creates a directory for the code, copies it into source code, builds that source code, and runs the application. This container is much smaller than the onbuild container, but it still contains the compiler and other Go tools that we don't really need. So let's just extract the compiled program and put it in our own container.

Kubernetes Best Practices. Creating Small Containers

You may notice something strange about this Dockerfile: it contains two FROM lines. The first 4-line section looks exactly like the previous Dockerfile except that it uses the AS keyword to name this step. The next section has a new FROM line that allows you to start a new image, and instead of the golang:alpine image, we will use Raw alpine as the base image.

Raw Alpine Linux does not have any SSL certificates installed, which will fail most API calls over HTTPS, so let's install some root CA certificates.

And now the fun part: to copy the compiled code from the first container to the second, you can simply use the COPY command, located on the 5th line of the second section. It will copy only one application file and will not affect the Go utility tools. The new multi-stage Dockerfile will contain a container image of only 12 megabytes compared to the original container image of 700 megabytes, which is a big difference!
So using small base images and a Builder Pattern are great ways to create much smaller containers without a lot of work.
It's possible that, depending on the application stack, there are additional ways to reduce the size of the image and container, but do small containers really have a measurable benefit? Let's look at two areas where small containers are extremely effective: performance and security.

To evaluate the increase in performance, consider the duration of the process of creating a container, inserting it into the registry (push) and then extracting it from there (pull). You can see that a smaller container has a distinct advantage over a larger container.

Kubernetes Best Practices. Creating Small Containers

Docker will cache the layers so subsequent builds will be very fast. However, in many of the CI systems that are used to build and test containers, the layers are not cached, so there are significant time savings here. As you can see, the time to build a large container, depending on the power of your machine, is from 34 to 54 seconds, and when using a container reduced using the Builder Pattern, it is from 23 to 28 seconds. For operations of this kind, the performance increase will be 40-50%. So just think about how many times you build and test your code.

After the container is built, you need to push its image (push container image) into the container registry in order to use it in your Kubernetes cluster. I recommend using the Google Container Registry.

Kubernetes Best Practices. Creating Small Containers

With Google Container Registry (GCR), you only pay for raw storage and networking, and there are no additional container management fees. It's private, secure and very fast. GCR uses many tricks to speed up the pull operation. As you can see, inserting a Docker Container Image using go:onbuild will take from 15 to 48s depending on the performance of the computer, and the same operation with a smaller container will take from 14 to 16s, and for less productive machines, the operation speed advantage increases by 3 times. For larger machines, the times are about the same, since GCR uses a global cache for a shared database of images, meaning you don't need to download them at all. In a low-power computer, the CPU is the bottleneck, so the advantage of using small containers is much more noticeable here.

If you are using GCR, I highly recommend using the Google Container Builder (GCB) as part of your build system.

Kubernetes Best Practices. Creating Small Containers

As you can see, using it allows you to achieve much better results in reducing the duration of the Build + Push operation than even on a productive machine - in this case, the process of building and sending containers to the host is almost 2 times faster. In addition, you get 120 minutes of assembly every day for free, which in most cases satisfies the needs of creating containers.

Next comes the most important performance metric - the speed of retrieving or downloading Pull containers. And if you don't really care about the time spent on the push operation, then the length of the pull process seriously affects the overall performance of the system. Suppose you have a cluster of three nodes and one of them fails. If you are using a management system such as Google Kubernetes Engine, then it will automatically replace the non-working node with a new one. However, this new node will be completely empty and you will have to drag all your containers into it to make it work. If the pull operation is long enough, then your cluster will run at a lower performance all this time.

There are many cases where this can happen, whether it's adding a new node to the cluster, updating nodes, or even switching to a new deployment container. Thus, minimizing pull time becomes a key factor. It is undeniable that a small container downloads much faster than a large one. If you are running multiple containers in a Kubernetes cluster, the time savings can be significant.

Kubernetes Best Practices. Creating Small Containers

Take a look at the above comparison: a pull operation with small containers takes 4-9 times less time, depending on the power of the machine, than the same operation using go:onbuild. The use of shared, small container base images greatly speeds up the time and speed at which new Kubernetes nodes can be deployed and brought online.

Let's look at the issue of security. It is believed that smaller containers are much safer than larger ones because they have a smaller attack surface. Is it really? One of the most useful features of the Google Container Registry is the ability to automatically scan your containers for vulnerabilities. A few months ago I created both onbuild and multi-stage containers, so let's see if there are any vulnerabilities there.

Kubernetes Best Practices. Creating Small Containers

The result is amazing: in a small container, only 3 medium vulnerabilities were found, and in a large container, 16 critical and 376 other vulnerabilities. If we look at the contents of a large container, we can see that most of the security problems have nothing to do with our application, but are related to programs that we do not even use. So when people talk about a large attack surface, that's what they mean.

Kubernetes Best Practices. Creating Small Containers

The conclusion is clear: create small containers because they provide real performance and security benefits to your system.

Kubernetes Best Practices. Kubernetes organization with namespace

Some ads πŸ™‚

Thank you for staying with us. Do you like our articles? Want to see more interesting content? Support us by placing an order or recommending to friends, cloud VPS for developers from $4.99, a unique analogue of entry-level servers, which was invented by us for you: The whole truth about VPS (KVM) E5-2697 v3 (6 Cores) 10GB DDR4 480GB SSD 1Gbps from $19 or how to share a server? (available with RAID1 and RAID10, up to 24 cores and up to 40GB DDR4).

Dell R730xd 2 times cheaper in Equinix Tier IV data center in Amsterdam? Only here 2 x Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 TV from $199 in the Netherlands! Dell R420 - 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB - from $99! Read about How to build infrastructure corp. class with the use of Dell R730xd E5-2650 v4 servers worth 9000 euros for a penny?

Source: habr.com

Add a comment