CKAD - Define, build and modify container images

How to build and alter images for basic purposes and to a level needed for the CKAD Exam.

Requirements

Podman installed on local machine.
Basic Image and Container knowledge

Building a new Image

The first thing needed is a file specifying an image to build an image. In Docker terminology, that is usually called the DOCKERFILE or Dockerfile; Podman will call this the more generic Containerfile; the file can be called anything and specified with a flag on the image build. If the flag is not used, the file name must match the default expected for the build system and be in a location relative to where the command is run.

Simple Image File

All images start with a base object; sometimes, that beginning point is called 'scratch', and other times, another image, such as Centos or Ubuntu. For most purposes, an image file will start with an existing base Image, not scratch.

Example 1:

FROM scratch

Scratch Example

Example 2:

FROM alpine:3.19.1

OS Example

From this starting point, more changes can be added to the Containerfile.

Building a small server in go

We will build and change a small server written in Golang for the remaining examples. Neither scratch nor alpine will be sufficiently easy; we will use the image golang:1.22.0, which includes the building code we need.
We need two files: our container file, which we will continue to build, and the small snippet of go code to compile in our Image.
Create an empty directory on your machine where we can do work.
We will do all the work in the folder specified below in this article. /Users/codadensys/ckad/build/

Copy and paste the contents below into a file named server.go. Hat tip to hendrikmaus

package main

import (
    "fmt"
    "flag"
    "log"
    "net/http"
)

func main() {
    listen := flag.String("listen", ":8080", "Provide address:port to listen on")
    flag.Parse();

    log.Printf("Starting server on %s", *listen)
    http.HandleFunc("/", httpHandleRoot)
    http.HandleFunc("/ping", httpHandlePing)
    log.Fatal(http.ListenAndServe(*listen, nil))
}

func httpHandleRoot(res http.ResponseWriter, req *http.Request) {
    http.Redirect(res, req, "/ping", http.StatusSeeOther)
}

func httpHandlePing(res http.ResponseWriter, req *http.Request) {
    res.Header().Add("X-Powered-By", "golang")
    res.WriteHeader(http.StatusOK)
    fmt.Fprint(res, "pong")
}

server.go

Let's also create a file for the new Image, and we will call it pingpongimage

Create pingpongimage file and add the below content.

FROM golang:1.22.0
COPY server.go .

pingpongimage

This image file will build on golang:1.22.0 image and copy our file for the server into it.

The local file system should look like the below in the build folder.

build/
├── pingpongimage
└── server.go 

Ensure you are in the build directory and run the command below.

podman image build -f pingpongimage -t pingpongserver:0.0.1

podman build

The output from an image build should look like the below

# OUTPUT
STEP 1/2: FROM golang:1.22.0
--> 23a095adf224
STEP 2/2: COPY server.go .
--> 24a4554dge45
COMMIT pingpongserver:0.0.1
--> 0dad1905a224
Successfully tagged localhost/pingpongserver:0.0.1
0dad1905a224c250f596cb82f70d3ebad888a1767523f7d0f9202072e9247b20

Each line in the Containerfile is a step and "layer" in the image build process.

Now that it is built verify it is stored locally.

podman image list

List the images on local machine with podman

There should now be at least two images on the local machine: our newly built pingpongserver image and the image it built on library/golang image.

#OUTPUT
REPOSITORY                TAG         IMAGE ID      CREATED             SIZE
localhost/pingpongserver  0.0.1       0dad1905a224  About a minute ago  854 MB
docker.io/library/golang  1.22.0      9cbeef2f2690  3 weeks ago         854 MB

Modify an image with more building

Change the Containerfile

Now currently, the built Image doesn't do anything, and it didn't do anything on the we can test other than copy the file. So let's modify it and have the image build do something useful.
Add a line to build the go code and run it on container start-up.

In the pingpongimage file, add a few lines.

FROM golang:1.22.0
COPY server.go .
RUN CGO_ENABLED=0 go build server.go
CMD ["/server"]

Don't worry about the RUN line, this is just golangs way to compile the go code and make an executable file called server. The CMD however is important as this tells the container what to execute on startup.

Update the semantic version of the newly built image and rerun the build process.

podman image build -f pingpongimage -t pingpongserver:0.0.2
## OUTPUT
STEP 1/4: FROM golang:1.22.0
STEP 2/4: COPY server.go .
--> Using cache 0dad1905a224c250f596cb82f70d3ebad888a1767523f7d0f9202072e9247b20
--> 0dad1905a224
STEP 3/4: RUN CGO_ENABLED=0 go build server.go
--> 8ad61ffe0874
STEP 4/4: CMD ["./server"]
COMMIT pingpongimage:0.0.2
--> e9709d1a4119
Successfully tagged localhost/pingpongimage:0.0.2
e9709d1a4119ce976d2333834bbfe7b3b4022b9b9bcdae6636ac6e98d1718cf4

Now a new image is built and it actually does something to test.

With a new image built, we can run the command below.

podman run -d -p 9999:8080 localhost/pingpongimage:0.0.2

This command will run the container, and on the local machine expose port 9999 and forward it to port 8080 in the container. If a current process is running that uses port 9999 this will fail.

When successful the output will look like below.

podman run -d -p 9999:8080 localhost/pingpongimage:0.0.2
# OUTPUT
c2b75362a4678eaf683efccab0c78607ac6bf4f9892d0fad86de204a0c9e5cba

We now have the image on the local machine and the running container.


With the command above executed, we can run a small test to see what the running container does.

It's a ping-pong server, so let's send it a ping.

curl localhost:9999/ping

send the ping

Ping sent, pong received.

curl localhost:9999/ping
# OUTPUT
pong

receive the pong

We have successfully taken our new image and built it up to do something, not that both are valid images, though our first one would not run and do anything.

Create a new Image from an Image.

This method is a different way, but not really.
Take our new image and make a new Contianrefile with it as the base.

FROM localhost/pingpongimage:0.0.2
RUN rm /usr/bin/bash #Secretly advanced CKS knowledge

Build this new Contianerfile with podman commands again.

podman image build -f newpingpongimage -t newpingpongserver:0.0.1

This command will use the exisitng image pingpongimage:0.0.2 and add onemore layer.

podman image build -f newpingpongimage -t newpingpongserver:0.0.1
# OUTPUT
STEP 1/2: FROM localhost/pingpongimage:0.0.2
STEP 2/2: RUN rm /usr/bin/bash #Secretly advanced CKS knowledge
COMMIT newpingpongserver:0.0.1
--> ac02ed877435
Successfully tagged localhost/newpingpongserver:0.0.1
ac02ed8774353fee1e04bc877121afb59c47408761c5a00a1ba31af15b36cc65

This would be the equivalent of a single Containerfile

FROM golang:1.22.0
COPY server.go .
RUN CGO_ENABLED=0 go build server.go
CMD ["/server"]
RUN rm /usr/bin/bash #Secretly advanced CKS knowledge

notional equivalent Containerfile

Since we changed something, we removed the bash shell from the image, lets make sure the container still works. Since last container is still running on port 9999 it will need to be removed first.

podman run -d -p 9999:8080 localhost/newpingpongserver:0.0.1
# OUTPUT
ee84429a87cb8e35bc6f961675c0d4ed8b9e90abc7ddd39b6dcd01fb15544c61
# New curl try
curl localhost:9999/ping
# OUTPUT
pong

rerun and send the ping

What else can we do to create and modify Images?

Create a new Image from a running Container.

We can create and alter an existing image, but sometimes, we may need to make a new image from a running container. Ideally, we would recreate a Container file, but occasionally, we must take an altered running container and create an image.

Let's alter the last running image of the newpingpongserver
Exec into it and inspect the file system

podman exec -it sharp_jemison  /bin/bash

Uh oh error!

Error: crun: executable file `/bin/bash` not found in $PATH: No such file or directory: OCI runtime attempted to invoke a command that was not found

Thankfully we only deleted bash and not shell.

podman exec -it sharp_jemison  /bin/sh

Now that you are successfully in the container look around. Run ls command.

ls
# OUTPUT
bin  server  server.go  src

list files inside newpingpongserver

The file server is running the ping pong server; now, we no longer need the server.go file.


Remove it to test a small change.

rm server.go

Now exit the container.

Export and the running container into an saved tar file of the image layers, and then reimport it into the local file system images.

Here, my container ID is ee84429a87cb.

podman export ee84429a87cb > container.tar
podman import container.tar localhost/newpingpongserver:0.0.2

NOTE: podman commit should do this in one line.

Now check to see the new image listed with a new and different `IMAGE ID`

podman image list
# OUTPUT
# New Image
REPOSITORY                   TAG         IMAGE ID      CREATED             SIZE
localhost/newpingpongserver  0.0.2       d26282904ba1  13 seconds ago      922 MB

#Previous Images
localhost/newpingpongserver  0.0.1       ac02ed877435  3 hours ago         928 MB
localhost/pingpongimage      0.0.2       e9709d1a4119  23 hours ago        928 MB
localhost/pingpongimage      0.0.1       0dad1905a224  23 hours ago        854 MB

Feel free to start the new image up again into a container and test again.

Conclusion


You should successfully understand how to build an image using a few techniques. Using Containerfile is the best approach as this file is now trackable in Git for proper GitOps and change management.


The pingpongserver image will be used again later.