Java Development with Odo on Podman, Kubernetes and OpenShift
In this article, you will learn how to develop and deploy Java apps on Podman, Kubernetes, and OpenShift with odo. Odo is a fast and iterative CLI tool for developers who want to write, build, and deploy applications on Kubernetes-native environments. Thanks to odo
you can focus on the most important aspect of programming – code. I have already written an article about that tool in my blog some years ago. However, a lot has changed during that time.
Today, we will also focus more on Podman, and especially Podman Desktop, as an alternative to the Docker Desktop for local development. You will learn how to integrate the odo
CLI with Podman. We will also use Podman for creating clusters and switching between several Kubernetes contexts. Our sample Java app is written in Spring Boot, exposes some REST endpoints over HTTP, and connects to the Postgres database. Let’s begin!
Source Code
If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. The sample Spring Boot app is located inside the micro-springboot/person-service
directory. Once you clone the repo and go to that directory, you should just follow my further instructions.
Create Sample Spring Boot App
The app source code is not the most important thing in our exercise. However, let’s do a quick recap of its main parts. Here’s the Maven pom.xml
with a list of dependencies. It includes standard Spring Boot starters for exposing REST endpoints and integrating with the Postgres database through JPA. It also uses additional libraries for generating OpenAPI docs (Springdoc) and creating entity views (Blaze Persistence).
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>com.blazebit</groupId>
<artifactId>blaze-persistence-integration-spring-data-3.1</artifactId>
<version>${blaze.version}</version>
</dependency>
<dependency>
<groupId>com.blazebit</groupId>
<artifactId>blaze-persistence-integration-hibernate-6.2</artifactId>
<version>${blaze.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.blazebit</groupId>
<artifactId>blaze-persistence-entity-view-processor</artifactId>
<version>${blaze.version}</version>
</dependency>
Here’s our @Entity
model class:
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String firstName;
private String lastName;
private int age;
@Enumerated(EnumType.STRING)
private Gender gender;
private Integer externalId;
}
Here’s the entity view interface used for returning persons in the REST endpoint. We can leverage the Blaze Persistence library to map between the JPA entity and a DTO view object.
@EntityView(Person.class)
public interface PersonView {
@IdMapping
Integer getId();
void setId(Integer id);
@Mapping("CONCAT(firstName,' ',lastName)")
String getName();
void setName(String name);
}
There are two repository interfaces. The first one is used for the modifications. It extends the standard Spring Data JPA CrudRepository
.
public interface PersonRepository extends CrudRepository<Person, Integer> {}
The second one is dedicated just for read operation. It extends the Blaze Persistence EntityViewRepository
interface.
@Transactional(readOnly = true)
public interface PersonViewRepository extends EntityViewRepository<PersonView, Integer> {
PersonView findByAgeGreaterThan(int age);
}
In the @RestController implementation, we use both repository beans. Depending on the operation type, the API method uses the Spring Data JPA PersonRepository
or the Blaze Persistence PersonViewRepository
.
@RestController
@RequestMapping("/persons")
public class PersonController {
private static final Logger LOG = LoggerFactory
.getLogger(PersonController.class);
private final PersonRepository repository;
private final PersonViewRepository viewRepository;
public PersonController(PersonRepository repository,
PersonViewRepository viewRepository) {
this.repository = repository;
this.viewRepository = viewRepository;
}
@GetMapping
public List<PersonView> getAll() {
LOG.info("Get all persons");
return (List<PersonView>) viewRepository.findAll();
}
@GetMapping("/{id}")
public PersonView getById(@PathVariable("id") Integer id) {
LOG.info("Get person by id={}", id);
return viewRepository.findOne(id);
}
@GetMapping("/age/{age}")
public PersonView getByAgeGreaterThan(@PathVariable("age") int age) {
LOG.info("Get person by age={}", age);
return viewRepository.findByAgeGreaterThan(age);
}
@DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") Integer id) {
LOG.info("Delete person by id={}", id);
repository.deleteById(id);
}
@PostMapping
public Person addNew(@RequestBody Person person) {
LOG.info("Add new person: {}", person);
return repository.save(person);
}
@PutMapping
public void update(@RequestBody Person person) {
repository.save(person);
}
}
Here are the app configuration settings in the Spring Boot application.yml
file. The app creates the database schema on startup and uses environment variables to establish a connection with the target database.
spring:
application:
name: person-service
datasource:
url: jdbc:postgresql://${DATABASE_HOST}:5432/${DATABASE_NAME}
username: ${DATABASE_USER}
password: ${DATABASE_PASS}
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
show_sql: true
format_sql: true
Now, our goal is to build the app container image and then run it on the target environment for further development. In our case, it is the Kubernetes or OpensShift cluster. We need a tool that doesn’t require a deep understanding of Kubernetes or even Podman. Here comes odo
.
Create and Manage Devfiles
In the first step, we need to configure our project. Odo is based on the open standard for defining containerized environments called Devfile. Once we execute the odo init
command inside the app root directory and choose the app type, it generates the devfile.yaml
automatically. Of course, we can already keep the devfile.yaml
in the Git repo. Thanks to that, we don’t need to initialize the odo
configuration after cloning the repo. That’s what it is in my case.
After generating the “devfile”, we need to change it a little bit. First of all, I switched to the Java 21 base image, instead of Java 17 generated by the odo init
command. We will also add the environment variables used by the Spring Boot app to establish connection with the Postgres database. Here’s the udpated fragment of the devfile.yaml
responsible for running app container.
components:
- container:
command:
- tail
- -f
- /dev/null
endpoints:
- name: http-springboot
targetPort: 8080
- exposure: none
name: debug
targetPort: 5858
env:
- name: DEBUG_PORT
value: "5858"
- name: DATABASE_HOST
value: localhost
- name: DATABASE_USER
value: springboot
- name: DATABASE_PASS
value: springboot123
- name: DATABASE_NAME
value: sampledb
image: registry.access.redhat.com/ubi9/openjdk-21:latest
I also included an additional container with the Postgres database. Thanks to that, odo
will not only build and run the app container but also the container with a database required by that app. We use the registry.redhat.io/rhel9/postgresql-15
Postgres image from Red Hat official registry. We can set a default username, password and database using the POSTGRESQL_*
envs supported by the Red Hat Postgres image.
- name: postgresql
container:
image: registry.redhat.io/rhel9/postgresql-15
env:
- name: POSTGRESQL_USER
value: springboot
- name: POSTGRESQL_PASSWORD
value: springboot123
- name: POSTGRESQL_DATABASE
value: sampledb
endpoints:
- name: postgresql
exposure: internal
targetPort: 5432
attributes:
discoverable: 'true'
memoryLimit: 512Mi
mountSources: true
volumeMounts:
- name: postgresql-storage
path: /var/lib/postgresql/data
- name: postgresql-storage
volume:
size: 256Mi
Here’s the whole devfile.yaml
after our customizations. Of course, you can find it inside the GitHub repository.
commands:
- exec:
commandLine: mvn clean -Dmaven.repo.local=/home/user/.m2/repository package -Dmaven.test.skip=true
component: tools
group:
isDefault: true
kind: build
workingDir: ${PROJECT_SOURCE}
id: build
- exec:
commandLine: mvn -Dmaven.repo.local=/home/user/.m2/repository spring-boot:run
component: tools
group:
isDefault: true
kind: run
workingDir: ${PROJECT_SOURCE}
id: run
- exec:
commandLine: java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=${DEBUG_PORT},suspend=n -jar target/*.jar
component: tools
group:
isDefault: true
kind: debug
workingDir: ${PROJECT_SOURCE}
id: debug
components:
- container:
command:
- tail
- -f
- /dev/null
endpoints:
- name: http-springboot
targetPort: 8080
- exposure: none
name: debug
targetPort: 5858
env:
- name: DEBUG_PORT
value: "5858"
- name: DATABASE_HOST
value: localhost
- name: DATABASE_USER
value: springboot
- name: DATABASE_PASS
value: springboot123
- name: DATABASE_NAME
value: sampledb
image: registry.access.redhat.com/ubi9/openjdk-21:latest
memoryLimit: 768Mi
mountSources: true
volumeMounts:
- name: m2
path: /home/user/.m2
name: tools
- name: postgresql
container:
# uncomment for Kubernetes
# image: postgres:15
# env:
# - name: POSTGRES_USER
# value: springboot
# - name: POSTGRES_PASSWORD
# value: springboot123
# - name: POSTGRES_DB
# value: sampledb
image: registry.redhat.io/rhel9/postgresql-15
env:
- name: POSTGRESQL_USER
value: springboot
- name: POSTGRESQL_PASSWORD
value: springboot123
- name: POSTGRESQL_DATABASE
value: sampledb
endpoints:
- name: postgresql
exposure: internal
targetPort: 5432
attributes:
discoverable: 'true'
memoryLimit: 512Mi
mountSources: true
volumeMounts:
- name: postgresql-storage
path: /var/lib/postgresql/data
- name: postgresql-storage
volume:
size: 256Mi
- name: m2
volume:
size: 3Gi
metadata:
description: Java application using Spring Boot® and OpenJDK 21
displayName: Spring Boot®
globalMemoryLimit: 2674Mi
icon: https://raw.githubusercontent.com/devfile-samples/devfile-stack-icons/main/spring.svg
language: Java
name: person-service
projectType: springboot
tags:
- Java
- Spring
version: 1.3.0
schemaVersion: 2.1.0
Prepare Development Environment with Podman
We have everything ready on the application side. Now, it is time to prepare a dev environment. Let’s run Podman Desktop. Podman Desktop comes with several useful features that simplify interaction with Kubernetes or OpenShift clusters for developers. It provides plugins that allow us to install Openshift Local, Minikube, or Kind on the laptop. We can also leverage the remote instance of OpenShift on the Sandbox Developer portal. It is active for 30 days and may be renewed.
Podman has one important advantage over Docker. It supports the Pod concept. So, for example, suppose we have a container that requires the Postgres container, with Podman we do not need to bind that database to a routable network. With a pod, we are just binding to the localhost address and all containers in that pod can connect to it because of the shared network. Thanks to that, switching between Podman and Kubernetes is very simple with odo. In fact, we don’t need to change anything in the configuration.
For running the Kubernetes cluster on the local machine, we will Minikube. You can install and run Minikube by yourself or create it using the Podman Desktop. With Podman Desktop, we need to go to the Settings -> Resources section. Then find the Minikube tile as shown below and click the “Create new …” button.
Podman Desktop redirects us to the window with the creation form. Then, choose your preferred settings and click Create.
In order to create an OpenShift cluster we use a managed service available on the Red Hat Developer Sandbox website. Then, let’s choose “Start your Sandbox for free”.
You need to create an account on Red Hat Developer or sign in if you already did that. After that, you will be redirected to the Red Hat Hybrid Cloud Console, where you should choose Launch option on the “Red Hat OpenShift” tile as shown below.
In the OpenShift Console click on your username in the top-right corner and choose “Copy login command”.
Then, we can back to the Podman Desktop and paste the copied command in the Settings -> Resources -> Developer Sandbox section.
Deploy App with Database on Podman
Finally, let’s move from theory to practice. Assuming we have Podman running on our laptop, we can deploy our app with odo there by executing the following command:
$ odo dev --platform podman
The odo dev
command deploys the app on the target environment and waits for any changes in the source files. Once it occurs, it redeploys the app.
The odo command exposes both the app HTTP port and the Postgres port outside Podman with port forwarding. For example, our Spring Boot app is available under the 20002
port. You can run the Swagger UI and call some endpoints to test the functionality.
Our app container can connect to the database over localhost
, because both containers are running inside a single pod.
Deploy App with a Database on Kubernetes
Then, we can switch to the Kubernetes cluster with our app. In this exercise, we will use Minikube. We can easily create and run the Minikube instance using the Podman Desktop plugin.
Once we create such an instance, the Kubernetes content is switched automatically. We can check it out with Podman Desktop as shown below.
Let’s deploy our app on Minikube. If we don’t activate the platform option, odo deploys on the default cluster context.
$ odo create namespace springboot
$ ode dev
Here’s the command result for my Minikube instance. Thanks to automatic port-forwarding we can access the app exactly in the same as before with Podman.
We can display a list of running with the following command:
kubectl get po
NAME READY STATUS RESTARTS AGE
person-service-app-b69bf8f7d-hk555 2/2 Running 0 2m50s
The full required YAML is generated automatically based on the devfile (e.g. environment variables). The only thing I changed for Kubernetes is the Postgres-based image used by odo. Instead of the image from the Red Hat registry, we are just using the official Postgres image from Docker Hub.
- name: postgresql
container:
image: postgres:15
env:
- name: POSTGRES_USER
value: springboot
- name: POSTGRES_PASSWORD
value: springboot123
- name: POSTGRES_DB
value: sampledb
Deploy App with a Database on OpenShift
We can deploy the sample Spring Boot app on OpenShift exactly in the same way as in OpenShift. The only thing that can be changed is the Postgres image. This time we will back to the configuration used when deploying to podman. Instead of the image from the Docker Hub, we will use the registry.redhat.io/rhel9/postgresql-15
image.
As I mentioned before, we will use the remote OpenShift cluster on the Developer Sandbox. Podman Desktop provides the plugin for Developer Sandbox. With that plugin, we can map the OpenShift context to a specific name like dev-sandbox-context
.
Then we can switch to the Kubernetes context related to Developer Sandbox using the Podman Desktop.
Finally, let’s run the app on the cluster with the following command:
$ odo dev
Here’s the output after running the odo dev
command:
We can verify that a pod is running on the OpenShift cluster. Just go to the Workloads -> Pods section in the OpenShift Console.
Thanks to automatic port-forwarding we can access the app on the local port. However, we can expose the service outside OpenShift with the Route object. Firstly, let’s display a list of Kubernetes services using Podman Desktop.
Then, we need to create the Route
object with the following command:
$ oc expose svc/person-service-app
Here’s our Route
visible in the OpenShift Console. To access it we need to open the following address in the web browser:
Finally, let’s access the app Swagger UI using the exposed URL address:
Final Thoughts
With Podman and the odo CLI, we can configure our development space and easily run apps across different containerized and Kubernetes-native environments. Odo with Devfile standard can similarly run the app on Podman, Kubernetes, and OpenShift. You can control the whole process using Podman Desktop.
Leave a Reply