Video Thumbnail for Lesson
6.3: Building the App Images

Building the Application Container Images

Transcript:

Before we can go about deploying our application onto Kubernetes, we need to build our container images that Kubernetes is going to use. While I'm not going to do a deep dive on building container images like I did in the Docker course, I'm going to go over them briefly and showcase how we can build and push them to container registries here. As a quick reminder, the general way that we define a container image is via a Dockerfile. This is a text file containing essentially the steps of instructions required to build out the application. You're usually going to start with some base operating system, install maybe your language runtime if you're running in Python or Node.js, you'll install your application dependencies, copy in your source code, and then set up the necessary commands to run the application. Once we've built an image, we then need to store it somewhere that our cluster can use. The place that we do this is called a container registry. Docker Hub is a great example of one, but there's many others including GitHub Container Registry. Many of the major clouds have their own container registries, etc. So you'll build these images either on your laptop or maybe within a continuous integration server. You'll push those to a registry, and then our cluster as a production environment is going to pull an image from the registry with a given configuration. I'll call out here that it is important to think about the build architecture of the images. Your development system may or may not match the architecture of your staging or production servers. For example, if you're building on a Apple Silicon laptop but you're deploying to an x86 server, by default those two would be incompatible. However, there is a way to build multi-architecture images where you're going to emulate these other architectures during the build such that the container image can be run across multiple different architectures. For each of our applications, I've set up one version that's using Docker Build. That's going to be a single architecture that matches the architecture of your development system. I've also set up a buildx command which is going to allow us to build multi-platform images across a variety of architectures. In order to run on Kubernetes, we do need to build all of these into container images. Let me stop all of these processes and we'll take a look at how we can build the container images that we're going to use in our Kubernetes cluster. If you want a deep dive on how we wrote the Docker files and optimized the build process, you should go check out the Docker course. But in this case, I'm just going to quickly run through them and showcase how we can build them and push them to a registry. In the case of Postgres, the database that we're going to run in Kubernetes is going to use that public Postgres image. However, we do need to be able to execute our migrations against that database. To do that, I'm starting from this base image which is using the golang-migrate package. It's a CLI and golang library used for running database migrations. And from that base image, I'm just copying in all of the migrations in my migrations file. So very simple Docker file. In order to build that, I can run t build container image which is going to issue a docker build dash t. Here I'm tagging it with a docker hub container image name. If you wanted to build and push this to your own container image registry, you would need to replace this. And then I'm passing it the context for the build which is my current directory. Looks like that built successfully. In today's world where many people are developing on ARM-based architectures because of Apple Silicon or potentially deploying to ARM-based servers, it can be useful to build multi-architecture container images that can be used against either AMD64 or ARM architectures. In order to do this, we can use docker build x. The first thing that we're going to want to do is bootstrap a build x builder. To do this, we're going to issue the docker build x create command. We're going to give it a name. We're going to tell it to use the docker container driver. We're going to allow it to connect to our host network and then we're going to specify it as the default builder. Looks like that ran successfully and now we're configured to use that when we issue docker build x commands. Now if I go back into my postgres directory, I also have this build container image multi-art task and we can see it's issuing a docker build x build command passing two platforms, both the linux AMD64 as well as the linux ARM64, passing in the image tag and then automatically pushing it to that registry. If we go to that registry on docker hub, you can see it pushed the foobar baz tag just a few seconds ago. Great. And it has both of the architectures that we specified available. So you could pull this image and run it on either type of system. If we go to the go API, I do have a docker file here specified. This is the one that I used in the docker course. However, there's a really cool tool called ko that allows you to build golang applications without needing to build or optimize your own docker file. So in this case, when I issue the build container image task, it calls ko build with a couple of options enabled. In this case, platform all is going to do both arm and AMD64 and I'm passing it the name of the docker repo that I want to use. And then without it even needing a docker file, it will go off and build my application. We can go to docker hub and look at that repo and see in this case, not only did it build the AMD64 and ARM64, it also built an ARMv7, a PPC64LE and an S390X architecture. So with that one simple ko command and no docker file at all, I was able to build for all these different architectures. And you can see it's just an eight megabyte image. So it's fairly optimized. For node.js, this is going to be the same docker file that we used in the docker course. We've got our base image, which is pulling from an official upstream node image. We're setting some environment variables, installing our dependencies, copying in our source code, and then issuing the command to start the application. We have the two tasks again. The first one builds a single architecture image using the docker build command. And the second one uses our buildx builder to build the multi-architecture version. Now it is important to note that you can't use buildx to build the multi-architecture version without this push command. And so if you didn't have a remote registry on docker hub or elsewhere set up, you could run a local registry using the docker run command on the registry image, listening on port 5000 and running in the background. You could then use this registry and tell dockerx to push your multi-architecture images to this local registry. So that can be a good way to validate your process without having to have a remote registry authenticated. Okay, that was the API node one. Within client react, we've got the same deal. Our docker file looks quite similar to our node.js application. We're starting from an official upstream image. We're installing dependencies. We're copying our source. The one difference is that instead of serving this as a node.js application, we run the npm build command, which outputs our static html, javascript, and css. And then we're going to serve those from an nginx container. You could serve them from a container like this, or you could distribute them via cdn. Finally, for our python application, this one did not exist in the docker course, so I wrote this docker file just for this purpose. You can see we have a multi-stage docker file. We're starting from this first stage, where we're going to generate the requirements needed in our application. To do that, we install Poetry. We then use our pyproject.toml and our poetry.loc file to generate a requirements.txt file. Then within our second stage, we copy that requirements.txt file in, install via pip, copy our source code in, and then specify the command that's going to be run. We do it this way so that we don't need to include Poetry and all of that development machinery in our final image. The build command and the multi-architecture build commands are going to be the same. A docker build and a docker buildx command. As you can see, by defining things in a similar way across all these different services, the actual build commands end up being identical, whereas we can use docker build for a single architecture, or we can use buildx to build a multi-architecture image. The Go API was the one slightly different one because we're leveraging that third-party tool Co to handle all of that for us. While I went quickly there, hopefully that gives you an idea of the configuration surface area of the various services and how they all fit together in this puzzle so that you have the context required as we now take all of these services and figure out how we're going to deploy them into Kubernetes.