Bitnami Content

Wednesday, March 6, 2024

Spring Boot 3.3.0-M2’s Support for Bitnami Container Images: Developer’s Guide

Author: Agustin Ventura


As you may already know, starting from version 3.1, Spring Boot has provided Docker Compose support for our projects.


What does that mean for us as developers? Simply put, it means that we can easily bootstrap our application infrastructure from an existing docker-compose.yaml or compose.yaml in our source folder root, and Spring Boot will automatically wire this infrastructure with our application at runtime. Hmmm...does that still sound a bit abstract? Let’s clarify. It means that if we have a docker-compose.yaml in our source folder, which defines a PostgreSQL database, Spring Boot will run the docker-compose.yaml when starting the app and automatically create a DataSource in our application linked to the said database. It’s as easy as that. But, it sounds too good to be true, doesn’t it? If you are wondering if there’s a caveat, we wouldn’t blame you.


It turns out there is one—the container images used must be DockerHub’s official ones. So, if you want to run, say, a Cassandra instance, you are limited to DockerHub's official Cassandra image. The same goes for MySQL, PostgreSQL, Apache Pulsar, and all the supported Service Connections. But the good news is that this limitation no longer exists.


Spring Boot 3.3.0-M2 added support for Bitnami images for a number of Service Connections. For several years now, Bitnami has been a trusted provider of hundreds of packaged applications, for the cloud as well as containerized and on your local machine. Bitnami offers you automatic packaging and verification of up-to-date versions of hundreds of open source software (OSS) applications. Now, let's take a look at a practical use case.

A practical use case

If you’re currently working on a cloud native application powered by Kubernetes and using the latest tools such as ArgoCD and Helm charts, great! Now, you want to create a local dev environment that takes advantage of this Docker Compose support, however, there’s a very small caveat: Your Helm chart in production is using a very specific version of PostgreSQL: 13.14.0 on Debian 11 r5. That’s not a big deal! You have a quick glance at bitnami/postgresql, and notice that you have exactly that very same container image at your disposal: bitnami/postgresql:13.14.0-debian-11-r5. In case you didn’t know earlier, you can avail of enterprise-grade Bitnami packages from Tanzu Application Catalog, where you can get the latest versions of hundreds of OSS built and delivered to you on an SLSA L3-compliant pipeline, along with comprehensive metadata in industry standard formats, and also the ability to custom configure them per your enterprise policies. Problem solved!


So, now, you can go ahead and work with the exact same version you have at production with guaranteed updates on Bitnami’s side and maximum compatibility with your Helm charts. It is always nice to have a choice, isn’t it?


All of this information is great, but you still might wonder what it means to you as a developer and how can you leverage Docker Compose and Bitnami image support to build awesome applications. To get answers to these questions, let's code a bit and build another Todo List REST API to showcase how we can use it (yes, another to-do list example, I know).

To-do list example

As we are going to build a Spring application, our first stop must be the great Spring Initilizr or maybe you can use the wonderful Intellij wizard.


For this example, we will choose a Gradle with Groovy project with Java and Spring Boot 3.3.0 (M2), packaging jar, and Java 21 (because we are already using the latest LTS version in production, aren't we?).


For dependencies, we are choosing:

  • Spring Web: To be able to create the REST resources for our application
  • Spring Data JDBC: In order to abstract our database operations
  • PostgreSQL Driver: To communicate with our PostgreSQL database
  • Docker Compose Support: Which is what we really want to test
  • Flyway: To initialize our database schema

Now we are ready to go! We can click Generate and download a zip file with our bootstrapped project ready to code.


Upon unzipping it and opening it with our favorite Integrated Development Environment (IDE), we can notice a few interesting things.


First, let’s look at the build.gradle:


plugins {  

  id 'java'  

  id 'org.springframework.boot' version '3.3.0-M2'  

  id 'io.spring.dependency-management' version '1.1.4'  

}  

  

group = 'com.bitnami'  

version = '0.0.1-SNAPSHOT'  

  

java {  

  sourceCompatibility = '21'  

}  

  

repositories {  

  mavenCentral()  

  maven { url 'https://repo.spring.io/milestone' }  

}  

  

dependencies {  

  implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'  

  implementation 'org.springframework.boot:spring-boot-starter-web'

  implementation 'org.flywaydb:flyway-core'  

  developmentOnly 'org.springframework.boot:spring-boot-docker-compose'  

  runtimeOnly 'org.postgresql:postgresql'  

  testImplementation 'org.springframework.boot:spring-boot-starter-test'  

}  

  

tasks.named('test') {  

  useJUnitPlatform()  

}


We have all of our selected dependencies, and as Spring Initilizr warned us (see above image), Docker Compose support is developmentOnly, therefore it is only available in development time and not production. This makes sense, as you will probably already have a database in production and, no matter which way you are using it to spin it up, you don't want to get a new database running every time you start your application. Just imagine having a new database running every time a Kubernetes pod starts your application, that’s definitely not the way to go. 


Next, if we have a look at the project tree, we will notice a compose.yaml sitting there.



And if we have a look at it:


services:  

  postgres:  

    image: 'postgres:latest'  

    environment:  

      - 'POSTGRES_DB=mydatabase'  

      - 'POSTGRES_PASSWORD=secret'  

      - 'POSTGRES_USER=myuser'  

    ports:  

      - '5432'


Spring Initilizr has recognized we are using PostgreSQL and has added a sensible configuration for us, now let's change this to use our Bitnami image:


services:  

  postgres:  

    image: 'bitnami/postgresql:13.14.0-debian-11-r15'

    environment:  

      - 'POSTGRES_DB=mydatabase'  

      - 'POSTGRES_PASSWORD=secret'  

      - 'POSTGRES_USER=myuser'  

    ports:  

      - '5432:5432'


It’s as easy as that!


Now, for the rest of the application, we can use Spring's RestController and Data JDBC support to easily create new to-do items and load them. In our controller we can create new todo items, returning a 201 CREATED response with a header containing the URI of the created item, and we can get a todo item by its id:


@RestController  

@RequestMapping("/items")  

public class TodoItemController {  

  

  private final TodoItemRepository todoItemRepository;  

  

  public TodoItemController(TodoItemRepository todoItemRepository) {  

    this.todoItemRepository = todoItemRepository;  

  }  

  

  @PostMapping  

  public ResponseEntity<Void> create(@RequestBody TodoItem todoItem, UriComponentsBuilder ucb) {  

    TodoItem createdItem = todoItemRepository.save(todoItem);  

    URI locationOfCreatedItem = ucb  

        .path("items/{id}")  

        .buildAndExpand(createdItem.id())  

        .toUri();  

    return ResponseEntity.created(locationOfCreatedItem).build();  

  }  

  

  @GetMapping("/{id}")  

  public ResponseEntity<TodoItem> get(@PathVariable UUID id) {  

    Optional<TodoItem> todoItem = todoItemRepository.findById(id);  

    return todoItem.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());  

  }  

}


The repository is as easy as this:


public interface TodoItemRepository extends CrudRepository<TodoItem, UUID> {  

  

}


For the sake of completeness, here's our TodoItem:


public record TodoItem(@Id UUID id, String title, String description) {  

  

}


Sorry to disappoint you if you were expecting a fancier approach using DDD or the like, but a good old record will serve our demonstration purposes.


Finally, we need a migration script to create our table in the database, so let's create it in src/main/resource/db/migrations (which is where Flyway expects to find it) and let's call it V1_0_0__schema_creation.sql:


CREATE TABLE todo_item  

(  

    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),  

    title VARCHAR(255) NOT NULL,  

    description VARCHAR(255) NOT NULL  

);


There is nothing special here, if we receive a TodoItem to insert without id, it will generate a random one.


Our whole app has six files, of which we only had to edit four. Not bad, right?



So far so good. Let’s, test it from a terminal run.


./gradlew bootRun


After a few info messages and about two seconds (I know, I know, you want under second startup time, but please consider that we are creating the schema and this is a JIT application, not a native one, we are ready to test our new todo implementation:


curl -i --location 'localhost:8080/items' \

--header 'Content-Type: application/json' \

--data '{

"title": "Groceries",

"description": "Don'\''t forget zucchini"

}'


You'll get a 201 response and you can query your new item using the URI provided in the location header:

curl --location 'http://localhost:8080/items/5480cb38-1a0d-4498-a2de-a934e7994882'


At this point, you may be wondering, what's so special here? Isn't this just another REST CRUD tutorial using Spring Web and Spring Data JDBC? Why am I even reading this?

And, you’re right, this is no more than a REST CRUD tutorial. But, if you’ve done a few of these, you may have noticed that we skipped a mandatory and tedious step...we didn’t configure the datasource! In fact, we didn't even edit application.properties. We simply added the pieces that we want to use to create our application and it’s working, no configuration needed.


Thanks to the integration of Spring Boot's Service Connection and Bitnami images, you can enjoy a zero configuration local environment with version and operating system compatibility, the latest bug fixes and distribution packages, and a standardized configuration approach. And, that means you can easily move your configuration between your environments. Isn’t that just what you wanted?