In the world of containerization and Kubernetes, there are not only so many levels of abstraction but so many ways to build out your production environments. Before using Kubernetes, you must have a container image, and there are several ways to build a container image.
All (or hopefully all) container images are built using the Open Container Initiative (OCI), which is a standard that was set in place by CNCF to ensure all container images follow the same protocol. That way, they can be used everywhere. If we didn’t have OCI, we would most likely have different container image configurations across our Kubernetes clusters.
You may be thinking to yourself well, Dockerfiles are the standard way of building container images and you’re absolutely right. However, there’s another (and sometimes more effective) way - Cloud Native Buildpacks.
In this blog post, you’ll learn what exactly Dockerfiles are, what Cloud Native Buildpacks are, and how to create your very own Cloud Native Buildpack in under 5 minutes.
Quick Rant On Containers And Docker
Containers have been around for a long time. Before Docker, there were things like Linux Containers (LXC). However, they were a bit different than what we know as containers today. In the Linux world before Docker, cgroups
and namespaces
were the primary way to build containers. Both of these have been in the Linux kernel since 2008. Without going too far down the rabbit hole, a lot of engineers found working with containers this way to be incredibly complex.
Docker didn’t invent anything new, it simply made containers far more accessible to everyone. Docker abstracted away all of the nuances with an easy-to-use CLI and fewer scalability headaches.
What Are Dockerfiles
Dockerfiles, simply put, are a way to build a container image. Most container images are built with a Dockerfile. It consists of:
- A base image that you’re building your container image from (think underlying OS)
- What application you want to run on the container image
- The working directory of the app
- What commands are needed to run the application
- What ports are needed for the application
You’re essentially taking an application and compressing it into the smallest form factor possible.
With Dockerfiles, there are several layers needed. The base image, commands used to run the app, ports used, directories that the application will be stored in, and any other commands needed. Depending on the complexity of your app and the expertise of members of your team, Dockerfiles can get complex.
Here’s an example of a Dockerfile, which is building a Go app. The Dockerfile is pulling from a Go base image, which is on Docker Hub. It then specifies what application will be pulled into the container image, commands that the application needs to run, and what port the application listens on.
FROM golang:latest
RUN mkdir /app
ADD . /app
WORKDIR /app
RUN go build -o main .
EXPOSE 8080
CMD [ "/app/main" ]
What Are Cloud Native Buildpacks
Cloud Native Buildpacks take your raw code and turn it into OCI-compliant container images, all without you having to write a Dockerfile or any other configuration file. They look at your source code, determine what base container image it should use, and builds the container image with all of the needed dependencies.
Cloud Native Buildpacks are able to abstract away the need to define a Dockerfile by:
- Automatically detecting buildpacks to use
- Creates the artifact (container image) from the source of your application code
- Automatically exports an OCI-compliant container image
Just like with Dockerfiles having multiple layers, which could be looked at as a negative/con, Cloud Native Buildpacks have cons too. For example, the base image, which you’ll learn about in the Getting Started with Cloud Native Buildpacks section, is set and configured by outside parties. That means you have to have trust in those parties to ensure the base images are kept up to date. The other reality is a lot of the configurations for container images are abstracted away with Cloud Native Buildpacks, and teams may not like that.
Which is Better?
There’s no one-size-fits-all answer, but the best way to put it is…
Dockerfiles:
- Can be complex
- Take certain skills on a team
- Require multiple configurations
- Allows teams to truly customize the way their app interacts with the world
- Full control over what’s being used in the container image
Cloud Native Buildpacks:
- Removes a lot of the complexity from your team
- Builds a container image right from source code, abstracting away everything else
- The automatically built container image is OCI-compliant
- Because of the abstraction, teams may not like Cloud Native Buildpacks because of the level of abstraction. They lose a lot of configuration capabilities
- Debugging can be a bit more of a pain
There’s truly no right or wrong blanket answer. It’s all about what a team's comfort level is with Docker.
Getting Started with Cloud Native Buildpacks
Now that you know what Dockerfiles and Cloud Native Buildpacks are, it’s time to get hands-on and build your own Cloud Native Buildpack. As discussed earlier, one isn’t necessarily better than the other. It all depends on your workflow and the expertise of your team.
To follow along with this section, you can use the GoWebAPI
project found here.
First, let’s start with the installation. The installation will vary based on Operating System, and you can find the installation guide here.
As an example, if you’re on macOS, you can install Pack
, the command-line tool with the following Brew package:
brew install buildpacks/tap/pack
Once Pack
is installed, open up the GoWebAPI
project in the editor of your choosing. For the purposes of this blog post, VS Code will be used.
Open up the terminal in your code editor and type the following command, which shows you the suggested base container images for your project. One of the cool things about Cloud Native Buildpacks with the builder
is that it’ll scan what’s in your code project and tell you what image would be best to use for your code.
pack builder suggest
In this case, you’ll use the [gcr.io/buildpacks/builder:v1](http://gcr.io/buildpacks/builder:v1)
container image, which is essentially a pretty vanilla base image that supports .NET, Go, Java, Node.js, and Python.
The pack build
command is to build the base image and consists of:
- The
build
flag - The name of the container image, which in this case is
gocloudpack
- The
--builder
flag which specifies the container base image - The
--path
which specifies where your code is. Because you have VS Code open with theGoWebAPI
, you can specify the dot (.
) which means current directory
pack build gocloudpack --builder [gcr.io/buildpacks/builder:v1](http://gcr.io/buildpacks/builder:v1) --path .
Once you run the preceding command, you’ll see an output similar to the screenshot below.
To check that the container image was built, you can use Docker commands. For example, docker image ls
should show you a container image called gocloudpack
.
You can test that the container image works by running the following command:
docker run -p 8080:8080 -tid gocloudpack
If you open up a web browser and go to localhost:8080
, you’ll see that the application is running.
Congrats! You’ve officially (and successfully) set up your very own Cloud Native Buildpack
Top comments (2)
did you notice any build size and time difference among these?
Thanks for this post! It's so well written and I finally get the differences :)