Using Podman to deploy an image directly to OpenShift 4

OpenShift logo A standard way to deploy an image to OpenShift (3 or 4) is to build the image on a development workstation, then push it to a public image registry like Quay (quay.io). We can use an OpenShift deployment configuration (DC), or just a command-line operation, to instantiate the image in an OpenShift Pod, given its registry URI.

While the use of a public registry makes it easy to share images, and to deploy on multiple clusters, it can be inconvenient during the early stages of development and testing. It's often easier simply to push an image from the development workstation directly to the OpenShift cluster. This way it's generally easier to be certain that you're running the latest build of an image, and there are fewer steps in each development cycle.

OpenShift requires some repository to store its images but, fortunately, there is a Docker registry built in. It's not always very visible -- its pods and configuration are tucked away in the openshift-image-registry namespace.

If we can get access to the built-in repository from outside the OpenShift cluster, then we can deploy to OpenShift directly, without needing any other remote repository.

In this article I will demonstrate how to carry out an image deployment this way, and explain at each step what exactly is happening. However, I won't explain how to build an image, and I certainly won't be explaining how to write code -- I'm assuming the user is familiar with Podman or Docker, and knows how to code the application in a way that will be appropriate to run in a container. There are other articles on my website about these subjects.

Prerequisites

To follow the steps in this article yourself, you'll need the following:

Expose the OpenShift image registry

Before proceeding further, we need to ensure that the built-in registry is accessible outside the cluster. If it is accessible, it will be assigned a secured route for encrypted access. You can find out as follows:

$ oc get route default-route -n openshift-image-registry

You don't need to be a privileged user to run this command. If the registry is not exposed, follow the instructions in the OpenShift documentation.

Create the image

For illustration, I will use a simple webservice called solunar_ws. This service reports sunrise and sunset times for specific dates and locations. Neither the function of the application nor its implementation are important in the context of this article -- it's just an application written in C that can be built into a container image from a Dockerfile using Podman.

$ git clone https://github.com/kevinboone/solunar_ws.git

There's no particular reason to download the complete source -- all you actually need is the Dockerfile. However, there are example YAML files in the source that might be useful later (but aren't used in this article).

Build the image:

$ podman build .

Assuming the build is successful (please let me know if it isn't), you'll have a new image in your local repository. You'll need to tag this image with a useful name, or just make a note of the numeric image ID:

$ podman image list
REPOSITORY                 TAG      IMAGE ID       CREATED          SIZE
<none>                     <none>   11797b801cb9   27 seconds ago   10.4 MB
...

10.4Mb is the correct size for this image. The build will also generate a first-stage image which is much larger, but does not need to be deployed.

Prepare the OpenShift imagestream

Ensure you have an OpenShift namespace (or project) to deploy to. I'm using the name "foo" in the following steps, because I have absolutely no imagination. If necessary, create the namespace:

$ oc new-project foo

Create an imagestream to hold the image that will deployed. You don't need to use a privileged user on OpenShift to do this, if you're creating the imagestream in a namespace your user created. If there is a simpler way to do this than creating a YAML file, I don't know what it is.

Create the file is.yaml with these contents:

kind: ImageStream
apiVersion: image.openshift.io/v1
metadata:
  name: solunar-ws
spec: {}

As always with YAML, be careful about the indentation. The only entry in the YML file that will be important later is the name, "solunar-ws". This is the name of the new imagestream.

Deploy the YAML and check the full URI of the new imagestream:

$ oc create -f is.yaml 
$ oc get is
solunar_ws   default-route-openshift-image-registry.apps-crc.testing/foo/solunar-ws   
  latest    2 minutes ago

The URI contains the hostname of the route that maps onto the image registry's service, and the project name -- "foo" in this case. This is the URI that we'll use as the target of the podman push operation later.

Assign the registry hostname to an environment variable for convenience; use oc get route like this:

REGHOST=`oc get route default-route -n openshift-image-registry \
  --template='{{ .spec.host }}'`

or just cut-and-paste the URI it from the output of oc get is, whatever's easier.

Push the image

Now we have an imagestream called solunar-ws in the namespace foo -- but it's empty. We need to push the image to the imagestream.

The following steps need to be carried out by a user with permissions to push images to the OpenShift registry. This could be the cluster admin user, but need not be, if you set appropriate roles on another user.

Authenticate Podman against the OpenShift image registry:

$ oc login -u [my_admin_user]
$ podman login -u bogus -p $(oc whoami -t) ${REGHOST} 
Login Succeeded!

The username bogus here is not an accident. The registry interprets the password argument as an authentication token (the output of oc whoami -t), not a real password. With a token, we don't need a username. However, the protocol requires a username to be supplied. Although it's neither documented nor intuitive, any username that contains a colon (like 'kube:admin') will be rejected -- even though the username is not actually used. If there's any logic to this behaviour, I don't know what it is. In any event, you can use any username that does not contain a colon.

If podman login shows a certificate error, like this:

Error: error authenticating creds for 
"default-route-openshift-image-registry.apps-crc.testing": 
pinging docker registry returned: 
Get https://default-route-openshift-image-registry.apps-crc.testing/v2/: x509: 
certificate signed by unknown authority

it may be beause the server certificate from the OpenShift router is not recognized by Podman. The proper solution is probably to extract the server certificate and pass its location to podman. In a development context, though, you can probably use --tls-verify=false and disable certificate verification. Other Podman commands in this article may also encounter the same problem.

Once you've successfully authenticated, you should be able to push the image. There are (at least) two ways to specify the remote image stream: you can specify it directly as an argument to podman push, or you can tag the image in your local repository. Here's the first method, which references the local image by its image ID (obtained from podman image list):

$ podman push 11797b801cb9 $REGHOST/foo/solunar-ws:latest --tls-verify=false
Getting image source signatures
Copying blob 50644c29ef5a [======================================] 5.6MiB / 5.6MiB
...
Writing manifest to image destination
Copying config fed7dc7576 [======================================] 1.5KiB / 1.5KiB
Writing manifest to image destination
Storing signatures

The remote URI in the podman push command is the OpenShift imagestream URI, which you can get from oc get is, as I showed earlier.

Note:
Unhelpfully, if you deploy to an incorrect or non-existent imagestream, you'll get an authentication error, which does not help to identify the source of the problem at all.

The second method is to tag the local image with the full URI of the remote repository. You can do this at build time, like this:

$ podman build -t ${REGHOST}/foo/solunar-ws:latest .

Alternatively, you can use podman tag after the build has finished. In either case, you can push the image to OpenShift using the tag:

$ podman push $REGHOST/foo/solunar-ws:latest --tls-verify=false

Both methods have exactly the same effect. Tagging the local image avoids the need to use hexadecimal image IDs, but it can be awkward to deploy to multiple registries, should the need arise.

Create the pod

There's usually no need to work as a privileged user beyond this point.

Now that the image is in the OpenShift registry, you can instantiate it in a pod. There are various ways to do this. The simplest is probably to let OpenShift create all the configuration using defaults.

Within the cluster, the image repository can be addressed by its service name, which by default is image-registry.openshift-image-registry.svc. The default port is 5000, as is conventional for Docker.

With this information in hand, you can do a default deployment like this:

$ oc new-app --docker-image=\
image-registry.openshift-image-registry.svc:5000/foo/solunar-ws:latest \ 
  --name=solunar-ws -l app=solunar-ws

The image URI here is the same as the one we used before for podman push, except that the hostname in the URI is the service name, not the external route name. The route name will usually also work -- so long as the registry continues to be exposed as a route.

This oc new-app command creates a default OpenShift deployment configuration called solunar-ws, with an app tag with the same name. This tag will be useful if you later want to create a service to expose the application to clients.

A more sophisticated method of deployment is to define the deployment configration as, say, a YAML file. You could if you wished create a deployment configuration, a service, and a route for the application all in the same YAML file. You can also define readiness and liveness probes to suit the application, if required. However, all these steps are beyond the scope of this article, which deals only with deployment.

The development cycle

The process I've described above might look complicated at first, but it's all just set-up. Thereafter, deploying a new image consists of podman build, podman push, and oc rollout latest solunar-ws.

Closing remarks

In short, the deployment process consists of authenticating Podmand (or Docker) against the OpenShift internal image registry, and pushing the image, either by its ID or its tag. Simple in principle, but there are a few oddities that can cause problems, some of which I've demonstrated in this article.