openshift

How to Use S2I to Deploy Dockerfile to Openshift Container Platform

Lets say i have a Java application,

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.edw</groupId>
    <artifactId>hello-world-containerized-quarkus-on-jvm</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <compiler-plugin.version>3.15.0</compiler-plugin.version>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
        <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
        <quarkus.platform.version>3.39.2</quarkus.platform.version>
        <skipITs>true</skipITs>
        <surefire-plugin.version>3.5.6</surefire-plugin.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>${quarkus.platform.group-id}</groupId>
                <artifactId>${quarkus.platform.artifact-id}</artifactId>
                <version>${quarkus.platform.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-arc</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>${quarkus.platform.group-id}</groupId>
                <artifactId>quarkus-maven-plugin</artifactId>
                <version>${quarkus.platform.version}</version>
                <extensions>true</extensions>
                <executions>
                    <execution>
                        <goals>
                            <goal>build</goal>
                            <goal>generate-code</goal>
                            <goal>generate-code-tests</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${compiler-plugin.version}</version>
                <configuration>
                    <parameters>true</parameters>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${surefire-plugin.version}</version>
                <configuration>
                    <argLine>@{argLine}</argLine>
                    <systemPropertyVariables>
                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                        <maven.home>${maven.home}</maven.home>
                    </systemPropertyVariables>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>${surefire-plugin.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <argLine>@{argLine}</argLine>
                    <systemPropertyVariables>
                        <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                        <maven.home>${maven.home}</maven.home>
                    </systemPropertyVariables>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>native</id>
            <activation>
                <property>
                    <name>native</name>
                </property>
            </activation>
            <properties>
                <quarkus.package.jar.enabled>false</quarkus.package.jar.enabled>
                <skipITs>false</skipITs>
                <quarkus.native.enabled>true</quarkus.native.enabled>
            </properties>
        </profile>
    </profiles>

</project>
package com.edw.controller;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

import java.util.HashMap;

@Path("/")
public class IndexController {
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response index() {
        return Response.ok(new HashMap(){{
                    put("hello", "world");
                }})
                .build();
    }
}

And a very simple Dockerfile for a multistage build, where we do a maven build there

## Stage 1 : build with maven builder image with native capabilities
FROM registry.redhat.io/ubi9/openjdk-21:1.24 AS build
COPY --chown=185 --chmod=0755 mvnw /code/mvnw
COPY --chown=185 .mvn /code/.mvn
COPY --chown=185 pom.xml /code/
USER 185
WORKDIR /code
COPY src /code/src
RUN ./mvnw clean package


## Stage 2 : create the docker final image
FROM registry.access.redhat.com/ubi9/openjdk-21-runtime:1.24

LABEL BASE_IMAGE="registry.access.redhat.com/ubi9/openjdk-21-runtime:1.24"
LABEL JAVA_VERSION="21"

ENV LANGUAGE='en_US:en'
ENV TZ='Asia/Jakarta'

ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -XX:TieredStopAtLevel=1 -noverify -XX:+AlwaysPreTouch -XX:+UseNUMA -Xlog:gc*,safepoint=debug:file=/tmp/gc.log.%p:time,uptime:filecount=5,filesize=50M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENV GC_CONTAINER_OPTIONS="-XX:+UseShenandoahGC"

COPY --from=build --chown=185 /code/target/quarkus-app/lib/ /deployments/lib/
COPY --from=build --chown=185 /code/target/quarkus-app/*.jar /deployments/
COPY --from=build --chown=185 /code/target/quarkus-app/app/ /deployments/app/
COPY --from=build --chown=185 /code/target/quarkus-app/quarkus/ /deployments/quarkus/

EXPOSE 8080
USER 185

ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]

This is the project structure,

$ tree
.
- Dockerfile
- mvnw
- pom.xml
- readme.md
- src
    + main
        + java
        |   + com
        |       + edw
        |           + controller
        |               + IndexController.java
        + resources
            + application.properties

We can use the below command to build this Dockerfile inside Openshift,

$ oc new-build --strategy docker --binary \ 
	--name hello-world-containerized-quarkus-on-jvm
    
	* A Docker build using binary input will be created
      * The resulting image will be pushed to image stream tag "hello-world-containerized-quarkus-on-jvm:latest"
      * A binary build was created, use 'oc start-build --from-dir' to trigger a new build

--> Creating resources with label build=hello-world-containerized-quarkus-on-jvm ...
    imagestream.image.openshift.io "hello-world-containerized-quarkus-on-jvm" created
    buildconfig.build.openshift.io "hello-world-containerized-quarkus-on-jvm" created
--> Success


$ oc start-build hello-world-containerized-quarkus-on-jvm \
	--from-dir . --follow
	
Uploading directory "." as binary input for the build ...
..........
Uploading finished
build.build.openshift.io/hello-world-containerized-quarkus-on-jvm-1 started
Receiving source from STDIN as archive ...
time="2026-09-12T08:10:52Z" level=info msg="Not using native diff for overlay, this may cause degraded performance for building images: kernel has CONFIG_OVERLAY_FS_REDIRECT_DIR enabled"
I0912 08:10:52.582538       1 defaults.go:112] Defaulting to storage driver "overlay" with options [mountopt=metacopy=on].
Caching blobs under "/var/cache/blobs".

.....

Pushing image image-registry.openshift-image-registry.svc:5000/api/hello-world-containerized-quarkus-on-jvm:latest ...
Getting image source signatures
Copying blob sha256:b8969f72c7c3e136de5b0da7xx354ab8c17d19792f4149091e7141cfad15xxxx
Copying config sha256:132e66ed9a99d36f7cd493xxc7158a19235665e71914157ec7d4f2cfc9xxxx
Writing manifest to image destination
Successfully pushed image-registry.openshift-image-registry.svc:5000/api/hello-world-containerized-quarkus-on-jvm@sha256:f466627c0e10aad58037e3059b5f8d15bf9d8e26d9c2c2a60f09be9a98999e39
Push successful


$ oc new-app --name hello-world-containerized-quarkus-on-jvm \ 
	--image-stream=api/hello-world-containerized-quarkus-on-jvm:latest
	
--> Found image 132e66e (28 minutes old) in image stream "api/hello-world-containerized-quarkus-on-jvm" under tag "latest" for "api/hello-world-containerized-quarkus-on-jvm:latest"

    Java Applications
    -----------------
    Platform for running plain Java applications (fat-jar and flat classpath)

    Tags: java


--> Creating resources ...
    deployment.apps "hello-world-containerized-quarkus-on-jvm" created
    service "hello-world-containerized-quarkus-on-jvm" created
--> Success
    Application is not exposed. You can expose services to the outside world by executing one or more of the commands below:
     'oc expose service/hello-world-containerized-quarkus-on-jvm'
    Run 'oc status' to view your app.

And we can see that our application is deployed successfully,

Source code for this project can be accessed here,

https://github.com/edwin/hello-world-containerized-quarkus-on-jvm

Infinispan Deployment.YAML on OpenShift Container Platform

Deploying Infinispan to OpenShift can be done easily by using either Operator or Helm chart. However, there is an even easier way to deploy it, and that is by using a single YAML file.

---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: infinispan
  namespace: cache
  labels:
    app: infinispan
spec:
  replicas: 1
  selector:
    matchLabels:
      app: infinispan
  template:
    metadata:
      labels:
        app: infinispan
    spec:
      volumes:
        - name: data-volume
          persistentVolumeClaim:
            claimName: ispn-pv
      containers:
        - name: infinispan
          image: 'quay.io/infinispan/server:16.1'
          imagePullPolicy: IfNotPresent
          resources:
            limits:
              memory: 1Gi
            requests:
              memory: 1Gi
          env:
            - name: USER
              value: admin
            - name: PASS
              value: password
          ports:
            - name: infinispan
              containerPort: 11222
              protocol: TCP
          volumeMounts:
            - name: data-volume
              mountPath: /opt/infinispan/server/data
---
kind: Service
apiVersion: v1
metadata:
  name: infinispan
  namespace: cache
spec:
  ports:
    - protocol: TCP
      port: 11222
      targetPort: 11222
  selector:
    app: infinispan
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: ispn-pv
  namespace: cache
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  volumeMode: Filesystem

We can deploy this single file directly to OpenShift and have it up and running in a few minutes. However this approach is perfect for local development, testing, or a quick proof-of-concept. If you intend to scale Infinispan into a multi-node distributed cluster for production, you should migrate this configuration to a StatefulSet or leverage the official Infinispan Operator to handle cluster discovery and data replication.

Creating a Service Account to Access OpenShift Container Registry

Let’s say you want to create an OpenShift Container Registry account to be used by your CI/CD tooling. The recommended approach is to use a ServiceAccount instead of a regular user account. Here’s how you can do it.

First, create a ServiceAccount,

$ oc create serviceaccount david-susugigi-sa

Next, generate a token for this ServiceAccount. In this example, we create a long-lived token with a lifespan of two years

$ oc create token david-susugigi-sa --duration=16760h

eyJhbGciOiJ.....Gog8tY

Then, assign the appropriate role to the ServiceAccount

$ oc policy add-role-to-user system:image-builder -z david-susugigi-sa

Finally, use the ServiceAccount to log in to the registry, using the token as the password

$ podman login default-route-openshift-image-registry.apps-crc.testing \
      --tls-verify=false \ 
      -u david-susugigi-sa \ 
      -p eyJhbGciOiJ.....Gog8tY

Login Succeeded!

Deploying Python App using S2I to Openshift Container Platform

For this sample, we are trying to deploy a Flask application to Openshift without any CI/CD tools involved. Means we are deploying directly by using S2I functionality thru OCP Dashboard.

Lets start with a simple hello world Python application, it is basically one simple py file

from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return "<h1>Hello, World!</h1>"

app.run(host="0.0.0.0", port=8080)

and one requirements.txt file,

flask

And after that, we can push our complete Python code to a Git repository. For example, im using below Git repo,

https://github.com/edwin/hello-world-flask

We can deploy to Openshift from the Developer dashboard,

Put the Git repo URL there

It will trigger a build process

Would took a while

But it will deployed successfully after a while

We can access the deployed apps directly after we expose a Route to our app.

Debugging HTTP Request and Responses in Red Hat Single Sign On

Red Hat Single Sign On (RHSSO) or its opensource project, which is Keycloak, is an open-source software product to allow single sign-on with identity and access management which can be deployed as a cloud service or containerized application. For this sample, we are trying to debug and print all http requests and responses that comes to RHSSO 7.4.6 which is being deployed on Openshift, for debugging purpose. But we also need to be very careful since it will print all http content which might contains sensitive values.

Okay, so lets start with creating a file “sso.cli” which have below content,

/subsystem=undertow/configuration=filter/expression-filter=requestDumperExpression:add(expression="dump-request")
/subsystem=undertow/server=default-server/host=default-host/filter-ref=requestDumperExpression:add

And deploy it as a ConfigMap,

$ oc create configmap jboss-cli --from-file=sso-extensions.cli=sso.cli

Next is mount it as a volume to RHSSO DeploymentConfig

$ oc set volume dc/sso --add --name=jboss-cli \
		-m /opt/eap/extensions -t configmap --configmap-name=jboss-cli \ 
		--default-mode='0755' --overwrite

Rollout the corresponding DeploymentConfig and we can observe that http request-response logs now is showing, we can use this curl command to test

$ curl --location --request POST 'https://sso.url/auth/realms/realm/protocol/openid-connect/userinfo' \
--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiw......YXNzPlSVE2Oj0ImIQd6zQkw2UEMiEyJz8FrsVaS7x2M8mQjy-xQrSTGZVXKWR7KLHa-MCRx4S33Ja5nQuD3K_VVihKTyn4cOHnQ'

with below logs as the result

21:46:28,071 INFO  [io.undertow.request.dump] (default task-1) 
----------------------------REQUEST---------------------------
               URI=/auth/realms/realm/protocol/openid-connect/userinfo
 characterEncoding=null
     contentLength=0
       contentType=null
            header=accept=*/*
            header=accept-encoding=gzip, deflate, br
            header=forwarded=for=10.161.5.3;host=sso.url;proto=https
            header=authorization=Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiw......YXNzPlSVE2Oj0ImIQd6zQkw2UEMiEyJz8FrsVaS7x2M8mQjy-xQrSTGZVXKWR7KLHa-MCRx4S33Ja5nQuD3K_VVihKTyn4cOHnQ
            header=x-forwarded-proto=https
            header=x-forwarded-port=443
            header=x-forwarded-for=10.161.5.3
            header=content-length=0
            header=host=sso.url
            header=x-forwarded-host=sso.url
            locale=[]
            method=POST
          protocol=HTTP/1.1
       queryString=
        remoteAddr=/10.161.5.3:0
        remoteHost=10.161.5.3
            scheme=https
              host=sso.url
        serverPort=8443
          isSecure=true
--------------------------RESPONSE--------------------------
     contentLength=73
       contentType=application/json
            header=X-XSS-Protection=1; mode=block
            header=X-Frame-Options=SAMEORIGIN
            header=Referrer-Policy=no-referrer
            header=Date=Wed, 06 Nov 2024 14:46:28 GMT
            header=Connection=keep-alive
            header=WWW-Authenticate=Bearer realm="realm", error="invalid_token", error_description="Token verification failed"
            header=Strict-Transport-Security=max-age=31536000; includeSubDomains
            header=X-Content-Type-Options=nosniff
            header=Content-Type=application/json
            header=Content-Length=73
            status=401

==============================================================