
An image contains the code used to create a deployment. Then, a deployment can be created from an image, which should then create a replica set (which is the number of pods that should be created), and then the pods should be created.
There are various ways to deploy an app on OpenShift.
- From GitHub (https://github.com)
- From Docker Hub (https://hub.docker.com)
- From an image
- From a build
- From a template
- From a JSON or YAML file (templates)
Let's create a Python script named app.py that contains the following. This is a very simple Flask app that returns "Hello World". Check out my article FreeKB - Flask - Getting Started with Flask on Linux for more details on Flask.
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World"
if __name__ == "__main__":
app.run(host='0.0.0.0', port=8080)
Let's also create the requirements.txt file which will be used by pip to install the required modules that our Flask app needs. Let's say requirements.txt contains the following.
flask==3.0.3
And then let's create a config map that contains app.py and requirements.txt
oc create configmap app-and-requirements --from-file app.py --from-file requirements.txt
Let's use the oc get images command to get the list of Red Hat Python images that can be used. Let's say we want to create our Python Flask app using the registry.redhat.io/ubi9/python-39@sha256:0c2f708b4977469d090719d939778eb95b42c02c1da6476aa95f2e875920652b image, which will create the container using Python version 3.9.
~]$ oc get images | grep -i python
sha256:0c2f708b4977469d090719d939778eb95b42c02c1da6476aa95f2e875920652b registry.redhat.io/ubi9/python-39@sha256:0c2f708b4977469d090719d939778eb95b42c02c1da6476aa95f2e875920652b
sha256:190ea81f2f64ccf7f7c8cb9dc4612eda59eb9e3d2e17f71727a270f078f5114a registry.redhat.io/ubi8/python-27@sha256:190ea81f2f64ccf7f7c8cb9dc4612eda59eb9e3d2e17f71727a270f078f5114a
sha256:4a1d451e1d513115ff54c6e80299e761f60454b5f2f091f3c9ddb9fc1d61f5c4 registry.redhat.io/ubi8/python-38@sha256:4a1d451e1d513115ff54c6e80299e761f60454b5f2f091f3c9ddb9fc1d61f5c4
sha256:971dcd27c3d53f58eb59c946f123223b95662841c1214a394a445380beb75f59 registry.redhat.io/ubi8/python-36@sha256:971dcd27c3d53f58eb59c946f123223b95662841c1214a394a445380beb75f59
sha256:d4e20aa826660f635fad77837b9c6aab8248f0560cd8c3c2283c12704359e9bb registry.redhat.io/rhscl/python-38-rhel7@sha256:d4e20aa826660f635fad77837b9c6aab8248f0560cd8c3c2283c12704359e9bb
sha256:e2a461928e82d7da8991f4fdf5496219f013a6e70c4ef30cf5fb93a4cc450eac registry.redhat.io/ubi8/python-39@sha256:e2a461928e82d7da8991f4fdf5496219f013a6e70c4ef30cf5fb93a4cc450eac
Let's create a YAML file named flask_deployment.yaml with the following, using "registry.redhat.io/ubi9/python-39@sha256:0c2f708b4977469d090719d939778eb95b42c02c1da6476aa95f2e875920652b" as the image.
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
openshift.io/generated-by: OpenShiftNewApp
labels:
app: flask
app.kubernetes.io/component: flask
app.kubernetes.io/instance: flask
name: flask
namespace: my-project
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
deployment: flask
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
annotations:
openshift.io/generated-by: OpenShiftNewApp
labels:
deployment: flask
spec:
containers:
- command:
- /bin/sh
- -c
- pip install --upgrade pip; pip install --requirement /opt/app-root/src/requirements.txt;
export FLASK_APP=app.py; python /opt/app-root/src/app.py
image: registry.redhat.io/ubi9/python-39@sha256:0c2f708b4977469d090719d939778eb95b42c02c1da6476aa95f2e875920652b
imagePullPolicy: IfNotPresent
name: flask
ports:
- containerPort: 8080
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /opt/app-root/src/app.py
name: my-config-map
subPath: app.py
- mountPath: /opt/app-root/src/requirements.txt
name: my-config-map
subPath: requirements.txt
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
volumes:
- configMap:
defaultMode: 420
name: app-and-requirements
name: my-config-map
And then use the oc apply command to create the deployment. If you are not familiar with the oc command, check out my article OpenShift - Getting Started with the oc command
oc apply --filename flask_deployment.py
At this point the oc get all command should return the following, where there is a deployment, a replica set, and a Running pod. So far, so good!
~]$ oc get all
NAME READY STATUS RESTARTS AGE
pod/flask-56c98b85f4-fqtc7 1/1 Running 0 6m28s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/flask 1/1 1 1 6m28s
NAME DESIRED CURRENT READY AGE
replicaset.apps/flask-56c98b85f4 1 1 1 6m28s
And the pod log should have something like this, show that pip install the Flask dependencies and the Flask app is listening for connections on port 8080.
~]$ oc logs pod/flask-56c98b85f4-fqtc7
Requirement already satisfied: pip in /opt/app-root/lib/python3.9/site-packages (22.2.2)
Collecting pip
Downloading pip-24.2-py3-none-any.whl (1.8 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.8/1.8 MB 12.5 MB/s eta 0:00:00
Installing collected packages: pip
Attempting uninstall: pip
Found existing installation: pip 22.2.2
Uninstalling pip-22.2.2:
Successfully uninstalled pip-22.2.2
Successfully installed pip-24.2
Collecting flask==3.0.3 (from -r /opt/app-root/src/requirements.txt (line 1))
Downloading flask-3.0.3-py3-none-any.whl.metadata (3.2 kB)
Collecting ldap3==2.9.1 (from -r /opt/app-root/src/requirements.txt (line 2))
Downloading ldap3-2.9.1-py2.py3-none-any.whl.metadata (5.4 kB)
Collecting paramiko==3.4.0 (from -r /opt/app-root/src/requirements.txt (line 3))
Downloading paramiko-3.4.0-py3-none-any.whl.metadata (4.4 kB)
Collecting requests==2.31.0 (from -r /opt/app-root/src/requirements.txt (line 4))
Downloading requests-2.31.0-py3-none-any.whl.metadata (4.6 kB)
Collecting Werkzeug>=3.0.0 (from flask==3.0.3->-r /opt/app-root/src/requirements.txt (line 1))
Downloading werkzeug-3.0.4-py3-none-any.whl.metadata (3.7 kB)
Collecting Jinja2>=3.1.2 (from flask==3.0.3->-r /opt/app-root/src/requirements.txt (line 1))
Downloading jinja2-3.1.4-py3-none-any.whl.metadata (2.6 kB)
Collecting itsdangerous>=2.1.2 (from flask==3.0.3->-r /opt/app-root/src/requirements.txt (line 1))
Downloading itsdangerous-2.2.0-py3-none-any.whl.metadata (1.9 kB)
Collecting click>=8.1.3 (from flask==3.0.3->-r /opt/app-root/src/requirements.txt (line 1))
Downloading click-8.1.7-py3-none-any.whl.metadata (3.0 kB)
Collecting blinker>=1.6.2 (from flask==3.0.3->-r /opt/app-root/src/requirements.txt (line 1))
Downloading blinker-1.8.2-py3-none-any.whl.metadata (1.6 kB)
Collecting importlib-metadata>=3.6.0 (from flask==3.0.3->-r /opt/app-root/src/requirements.txt (line 1))
Downloading importlib_metadata-8.5.0-py3-none-any.whl.metadata (4.8 kB)
Collecting pyasn1>=0.4.6 (from ldap3==2.9.1->-r /opt/app-root/src/requirements.txt (line 2))
Downloading pyasn1-0.6.1-py3-none-any.whl.metadata (8.4 kB)
Collecting bcrypt>=3.2 (from paramiko==3.4.0->-r /opt/app-root/src/requirements.txt (line 3))
Downloading bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl.metadata (9.6 kB)
Collecting cryptography>=3.3 (from paramiko==3.4.0->-r /opt/app-root/src/requirements.txt (line 3))
Downloading cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl.metadata (5.4 kB)
Collecting pynacl>=1.5 (from paramiko==3.4.0->-r /opt/app-root/src/requirements.txt (line 3))
Downloading PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl.metadata (8.6 kB)
Collecting charset-normalizer<4,>=2 (from requests==2.31.0->-r /opt/app-root/src/requirements.txt (line 4))
Downloading charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (34 kB)
Collecting idna<4,>=2.5 (from requests==2.31.0->-r /opt/app-root/src/requirements.txt (line 4))
Downloading idna-3.10-py3-none-any.whl.metadata (10 kB)
Collecting urllib3<3,>=1.21.1 (from requests==2.31.0->-r /opt/app-root/src/requirements.txt (line 4))
Downloading urllib3-2.2.3-py3-none-any.whl.metadata (6.5 kB)
Collecting certifi>=2017.4.17 (from requests==2.31.0->-r /opt/app-root/src/requirements.txt (line 4))
Downloading certifi-2024.8.30-py3-none-any.whl.metadata (2.2 kB)
Collecting cffi>=1.12 (from cryptography>=3.3->paramiko==3.4.0->-r /opt/app-root/src/requirements.txt (line 3))
Downloading cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (1.5 kB)
Collecting zipp>=3.20 (from importlib-metadata>=3.6.0->flask==3.0.3->-r /opt/app-root/src/requirements.txt (line 1))
Downloading zipp-3.20.2-py3-none-any.whl.metadata (3.7 kB)
Collecting MarkupSafe>=2.0 (from Jinja2>=3.1.2->flask==3.0.3->-r /opt/app-root/src/requirements.txt (line 1))
Downloading MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.0 kB)
Collecting pycparser (from cffi>=1.12->cryptography>=3.3->paramiko==3.4.0->-r /opt/app-root/src/requirements.txt (line 3))
Downloading pycparser-2.22-py3-none-any.whl.metadata (943 bytes)
Downloading flask-3.0.3-py3-none-any.whl (101 kB)
Downloading ldap3-2.9.1-py2.py3-none-any.whl (432 kB)
Downloading paramiko-3.4.0-py3-none-any.whl (225 kB)
Downloading requests-2.31.0-py3-none-any.whl (62 kB)
Downloading bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl (273 kB)
Downloading blinker-1.8.2-py3-none-any.whl (9.5 kB)
Downloading certifi-2024.8.30-py3-none-any.whl (167 kB)
Downloading charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (144 kB)
Downloading click-8.1.7-py3-none-any.whl (97 kB)
Downloading cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl (4.0 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.0/4.0 MB 30.8 MB/s eta 0:00:00
Downloading idna-3.10-py3-none-any.whl (70 kB)
Downloading importlib_metadata-8.5.0-py3-none-any.whl (26 kB)
Downloading itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Downloading jinja2-3.1.4-py3-none-any.whl (133 kB)
Downloading pyasn1-0.6.1-py3-none-any.whl (83 kB)
Downloading PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl (856 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 856.7/856.7 kB 14.1 MB/s eta 0:00:00
Downloading urllib3-2.2.3-py3-none-any.whl (126 kB)
Downloading werkzeug-3.0.4-py3-none-any.whl (227 kB)
Downloading cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (445 kB)
Downloading MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (20 kB)
Downloading zipp-3.20.2-py3-none-any.whl (9.2 kB)
Downloading pycparser-2.22-py3-none-any.whl (117 kB)
Installing collected packages: zipp, urllib3, pycparser, pyasn1, MarkupSafe, itsdangerous, idna, click, charset-normalizer, certifi, blinker, bcrypt, Werkzeug, requests, ldap3, Jinja2, importlib-metadata, cffi, pynacl, flask, cryptography, paramiko
Successfully installed Jinja2-3.1.4 MarkupSafe-3.0.2 Werkzeug-3.0.4 bcrypt-4.2.0 blinker-1.8.2 certifi-2024.8.30 cffi-1.17.1 charset-normalizer-3.4.0 click-8.1.7 cryptography-43.0.3 flask-3.0.3 idna-3.10 importlib-metadata-8.5.0 itsdangerous-2.2.0 ldap3-2.9.1 paramiko-3.4.0 pyasn1-0.6.1 pycparser-2.22 pynacl-1.5.0 requests-2.31.0 urllib3-2.2.3 zipp-3.20.2
/opt/app-root/lib64/python3.9/site-packages/paramiko/pkey.py:100: CryptographyDeprecationWarning: TripleDES has been moved to cryptography.hazmat.decrepit.ciphers.algorithms.TripleDES and will be removed from this module in 48.0.0.
"cipher": algorithms.TripleDES,
/opt/app-root/lib64/python3.9/site-packages/paramiko/transport.py:259: CryptographyDeprecationWarning: TripleDES has been moved to cryptography.hazmat.decrepit.ciphers.algorithms.TripleDES and will be removed from this module in 48.0.0.
"class": algorithms.TripleDES,
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:8080
* Running on http://10.128.3.218:8080
Press CTRL+C to quit
Curl from Pod to Pod
Notice in the prior output that the Flask app in the pod is listening for connections on http://10.128.3.218:8080. If find it makes the most sense to first try to submit a request using the socket (IP address and port) of the pod, because if the request isn't working using the IP address and port of the pod, then likewise the request using the Service or Route will probably also not work either.
Be aware that many pods use /bin/busybox and /bin/busybox does not contain curl. The hello-openshift image contains curl. Check out my article Check out my article FreeKB - OpenShift - Deploy Hello Openshift.
This also will be useful to determine if communication can occur across different projects / namespaces.
~]# oc new-project hello-openshift
~]# oc import-image openshift4/ose-hello-openshift-rhel8:v4.7.0-202205312157.p0.g7706ed4.assembly.stream --from=registry.redhat.io/openshift4/ose-hello-openshift-rhel8:v4.7.0-202205312157.p0.g7706ed4.assembly.stream --confirm
~]# oc new-app registry.example.com/openshift4/ose-hello-openshift-rhel8
~]$ oc exec pod/ose-hello-openshift-rhel8-5959c4fb77-zss6g -- curl --silent localhost:8080
Hello OpenShift!
Assuming there are no ingress network polices in the project / namespace that contains the Python Flask pod, let's use the hello-openshift pod to submit a request to the Python Flask pod using curl in the hello-openshift pod and using the socket (IP address and port) of the Python Flask pod.
In this example I get response "Hello World" to the GET request. Good - we know the application in the Python Flask pod is able to respond to GET requests.
~]$ oc exec pod/ose-hello-openshift-rhel8-5959c4fb77-zss6g --namespace hello-openshift -- curl 10.128.3.218:8080 -v
> GET / HTTP/1.1
> Host: 10.128.3.218:8080
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200
< Cache-Control: private, no-cache, no-store, must-revalidate, max-age=0
< Content-Type: application/json
< Date: Thu, 03 Oct 2024 01:08:04 GMT
<
Hello World
Curl from Pod to Service
If you are able to get a response from the Python Flask pod, let's create a service that will forward requests on the pod.
~]$ oc expose pod flask-56c98b85f4-fqtc7 --name flask --port 8080
service/flask exposed
The oc get services command can be used to list the IP address of the Service (172.30.175.102 in this example).
~]$ oc get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
flask ClusterIP 172.30.175.102 <none> 8080/TCP 51s
I would then next see if I can get a response using the Service that forwards requests onto the pod.
Let's use the hello-openshift pod to submit a request to the Service that is forwarding requests onto the Python Flask pod using curl in the hello-openshift pod and using the socket (IP address and port) of the Service.
In this example I get a response to the GET request. This means I am able to connect to the Python Flask pod using the Service. Good - we know the Service is able to forward requests onto the pod and the application in the pod is able to response to GET requests.
~]$ oc exec pod/ose-hello-openshift-rhel8-5959c4fb77-zss6g --namespace hello-openshift -- curl --silent 172.30.175.102:8080
> GET / HTTP/1.1
> Host: 172.30.175.102:8080
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200
< Cache-Control: private, no-cache, no-store, must-revalidate, max-age=0
< Content-Type: application/json
< Date: Thu, 03 Oct 2024 01:08:04 GMT
<
Hello World
Create a Route
The oc create route command can be used to create a secured HTTPS edge, passthrough or reencrypt route. In this example, I create an edge route that will foward the request onto the service/pod.
oc create route edge my-edge-route --service s2i-python-container --hostname my-route.apps.openshift.example.com
And going to the route in your web browser should return whatever it is that the Flask app in the pod is returning, which is just "Hello World" in this example. Nice!

Did you find this article helpful?
If so, consider buying me a coffee over at