Spring Boot 3 Buildpacks with Testcontainers Cloud
Using buildpacks with Testcontainers Cloud makes my demo so much better
Background
I have a modest laptop. It checks the boxes, for just about everything I need to do for work. Just about. AOT processing, with GraalVM, is not a great experience on this machine.
The origin story
Back in June 2022, I was presenting at SpringOne Tour NYC. As part of the demo, I built a native
image using Spring Boot 2.7 with the spring-native
experimental dependency. The session went well, except for the building the native image part of it. During my 50-minute session, my laptop was busy building the native image for over 8-minutes, using the buildpack. It was a humbling experience. After the session, I hung out with Sergei Egorov, and we worked on something very special. With help from one of his previous blog posts, we created a proof of concept. We used Testcontainers Cloud to build the native OCI image, in the cloud, in a little over 3-minutes. Pairing with Sergei is amazing, if you get the chance, I highly recommend it.
It sat on the back-burner and fire is hot
I had this code sitting in my repository, waiting to be used. When it was time for SpringOne Tour Tel Aviv, Nov 2022, I was ready to show it off. Unfortunately for me, my session was shortened, so I pulled it out of the presentation. I shouldn’t have pulled it out, I made the exact same mistake that I made in NYC. When I built the native image using buildpacks, it took way too long on my laptop.
Momentum
My adventures with buildpacks have been top of mind for a couple of weeks now. Oleg Šelajev and Cora Iberklied also presented about testcontainers in Tel Aviv. The stage was set for me to take this use case further.
The Goal
Build Spring Boot 3, native OCI images, with buildpacks, during demos, faster, with Testcontainers Cloud
Prerequisites
You need to have an account at https://testcontainers.cloud.
Login to your account
I’m logging in with v1.3.11
of the cloud desktop app for Mac, this is still in private beta at the time of this writing.
docker context list
You should see at least one context named ’tcc'
docker context use tcc
Create an application example
Create a Spring Boot 3 application with web
, actuator
, and testcontainers
for a simple test.
curl https://start.spring.io/starter.tgz -d dependencies=web,actuator,testcontainers -d javaVersion=17 -d bootVersion=3.0.0-RC2 -d type=maven-project | tar -xzf -
Spring Boot 3 goes GA later this week, but I can’t wait that long
In the pom.xml add this dependency:
<dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-invoker</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>
Add this property:
<testcontainers.version>1.17.6</testcontainers.version>
Add this dependency management section:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>${testcontainers.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Those pieces will allow you to write a test, that creates an OCI image, using buildpacks, with Testcontainers Cloud.
Write the test class
Here is the test class that I’ve been reusing since June.
package com.example.demo;
import org.apache.maven.shared.invoker.DefaultInvocationRequest;
import org.apache.maven.shared.invoker.DefaultInvoker;
import org.apache.maven.shared.invoker.InvocationResult;
import org.apache.maven.shared.invoker.MavenInvocationException;
import org.junit.jupiter.api.Test;
import org.springframework.util.DigestUtils;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.LazyFuture;
import java.io.File;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Future;
public class TccTest {
private static final Future<String> IMAGE_FUTURE = new LazyFuture<>() {
@Override
protected String resolve() {
// Find project's root dir
File cwd;
for (
cwd = new File(".");
!new File(cwd, "mvnw").isFile();
cwd = cwd.getParentFile()
);
// Make it unique per folder (for caching)
var imageName = String.format(
"local/app-%s:%s",
DigestUtils.md5DigestAsHex(cwd.getAbsolutePath().getBytes()),
System.currentTimeMillis()
);
var properties = new Properties();
properties.put("spring-boot.build-image.imageName", imageName);
properties.put("skipTests", "true");
var request = new DefaultInvocationRequest()
.addShellEnvironment("DOCKER_HOST", DockerClientFactory.instance().getTransportConfig().getDockerHost().toString())
.setPomFile(new File(cwd, "pom.xml"))
.setGoals(List.of("spring-boot:build-image"))
.setMavenExecutable(new File(cwd, "mvnw"))
.setProfiles(List.of("native"))
.setProperties(properties);
InvocationResult invocationResult = null;
try {
invocationResult = new DefaultInvoker().execute(request);
} catch (MavenInvocationException e) {
throw new RuntimeException(e);
}
if (invocationResult.getExitCode() != 0) {
throw new RuntimeException(invocationResult.getExecutionException());
}
return imageName;
}
};
static final GenericContainer<?> APP = new GenericContainer<>(IMAGE_FUTURE)
.withExposedPorts(8080);
@Test
void letsGo() throws Exception {
APP.start();
}
}
In my example repository, I actually create test classes for a regular OCI image and a native
OCI image.
Verify
./mvnw clean test
Boom! You get an OCI image, or two if you use my example repository.
docker images | grep "local"
Enjoy!
Summary
This is slick. It brings me joy. When I am presenting this topic, I no longer need to have Docker Desktop
running on my laptop. Therefore, my laptop battery will last longer. My builds will be consistently faster, using Testcontainers Cloud, and my demos will be smoother.
I am completely rethinking a few of my own use cases. I will definitely create more Testcontainers Cloud content in the future.
Please let me know what you think!