Photo by Marc-Olivier Jodoin on Unsplash
Fast Local Development of Spring Boot Apps on k8s With Skaffold & Telepresence
Developing applications on Kubernetes can be challenging, especially when setting up a development environment that mirrors the production environment. This process can be complex and time-consuming, and developers often have to make trade-offs between speed and accuracy. Furthermore, Kubernetes applications are packaged as containers, which adds an extra layer of complexity to the development process — developers now have to create and manage container images in addition to writing code.
The complexity of developing applications on Kubernetes is heightened when debugging an issue in a cluster that runs on multiple nodes. Debugging a distributed system can be a challenging task.
In this article, you will learn how to use Skaffold and Telepresence to achieve blazing-fast local development of Spring Boot applications running on Kubernetes.
Prerequisites
For this tutorial, it is required that the following is available on your local setup:
Anatomy of the Spring Boot application
To illustrate how Skaffold and Telepresence operate with Spring Boot applications, let’s begin by imagining a situation where we have a pair of microservices built with Spring Boot.
The Order service is responsible for managing orders.
The Payment service is responsible for processing payments.
The following sequence diagram shows the interaction between Order and Payment microservices.
Here’s a brief explanation of what’s happening in the diagram:
First, the Client sends a POST request to the Order Service to create a new order. The Order Service calls the OrderRepository to save the order and then calls the Payment Service through the PaymentServiceProxy to process the payment.
The PaymentServiceProxy sends a POST request to the Payment Service to create a new payment, which is then saved in the PaymentRepository. The Payment Service returns the saved payment to the PaymentServiceProxy, which returns it to the Order Service.
The Order Service updates the order with the payment status received from the Payment Service and saves the updated order in the Order Repository. The Order Service returns the updated order to the Client.
Creating the Order and Payment microservices
To implement this scenario, we need to create two Spring Boot applications, one for each microservice. Here are the steps to create the microservices.
Creating the Order service
You can use your preferred IDE and the Spring Initializr website to generate the projects with the following dependencies: Spring Web, Spring Data JPA, H2 In-Memory Database, and Spring Cloud OpenFeign. After creating the project, create the OrderRepository.java
and OrderController.java
classes as explained below.
OrderRepository.java: This interface extends the JpaRepository and defines methods to perform CRUD operations on orders.
public interface OrderRepository extends JpaRepository<OrderValueObject, Long> {}
We need to create a Feign client in the Order Service project to call the Payment Service:
@FeignClient(name = "payment-service", url = "http://payment-service:8081")
public interface PaymentServiceFeignClient {
@PostMapping("/payments")
PaymentValueObject processPayment(@RequestBody PaymentValueObject paymentValueObject);
}
We will also need to configure the Order Service to use the Payment Service Feign client by creating a PaymentServiceProxy class:
@Service
public class PaymentServiceProxy {
@Autowired
private PaymentServiceFeignClient paymentServiceFeignClient;
public PaymentValueObject processPayment(PaymentValueObject paymentValueObject) {
return paymentServiceFeignClient.processPayment(paymentValueObject);
}
}
OrderController.java: This class defines REST endpoints to create and retrieve orders. In the code snippet below, you can see that we are calling the Payment Service from Order Service while creating an order.
@RestController
public class OrderController {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentServiceProxy paymentServiceProxy;
@PostMapping("/orders")
public OrderValueObject createOrder(@RequestBody OrderValueObject orderValueObject) {
orderValueObject.setPaymentStatus("pending");
OrderValueObject savedOrderValueObject = orderRepository.save(orderValueObject);
PaymentValueObject paymentValueObject = new PaymentValueObject();
paymentValueObject.setOrderId(savedOrderValueObject.getId());
paymentValueObject.setAmount(savedOrderValueObject.getPrice());
paymentValueObject.setPaymentStatus("pending");
PaymentValueObject savedPaymentValueObject = paymentServiceProxy.processPayment(paymentValueObject);
savedOrderValueObject.setPaymentStatus(savedPaymentValueObject.getPaymentStatus());
return orderRepository.save(savedOrderValueObject);
}
@GetMapping("/orders/{id}")
public OrderValueObject getOrder(@PathVariable Long id) {
return orderRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Order not found"));
}
}
Creating the Payment service
To create the payment service, create a new Spring Boot project with the following dependencies: Spring Web, Spring Data JPA, and H2 In-Memory Database. Then create the PaymentController.java and PaymentRepository.java classes as explained below.
PaymentController.java: This class defines REST endpoints to process payments.
@RestController("/payments")
public class PaymentController {
@Autowired
private PaymentRepository paymentRepository;
@PostMapping("/payments")
public PaymentValueObject processPayment(@RequestBody PaymentValueObject paymentValueObject) {
// perform payment processing logic here
paymentValueObject.setPaymentStatus("success");
return paymentRepository.save(paymentValueObject);
}
}
PaymentRepository.java: This interface extends the JpaRepository and defines methods to perform CRUD operations on payments.
public interface PaymentRepository extends JpaRepository<PaymentValueObject, Long> {}
The entire source code explained above can be found in this GitHub repository.
Containerizing the microservices
The next thing we are going to do is containerize our newly created applications using Docker. To do this, create a Dockerfile in the root directory of each spring boot application (the Order and Payment services), specify the base image, copy the application files, and set the entry point command. Next, find the docker file for each of these microservices below — copy and paste them into the respective docker files.
Dockerfile for the Order microservice:
FROM openjdk:17
COPY target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
Dockerfile for the Payment microservice:
FROM openjdk:17
RUN mkdir /app
COPY target/*SNAPSHOT-exe.jar /app/app.jar
WORKDIR /app
ENTRYPOINT ["java","-jar","/app/app.jar"]
Accelerating inner dev loop with Skaffold
Skaffold is a powerful tool that streamlines the development workflow for Kubernetes-based applications by automating the build, push, and deploy steps of cloud-native applications, simplifying the process of quickly iterating and deploying changes. By utilizing the Dockerfiles provided in the previous section, Skaffold can build and deploy images to the Kubernetes cluster. Refer to the official documentation to download and install Skaffold on your system.
Using the skaffold init
command, we can generate a skaffold.yaml
file in the project’s root directory to set up Skaffold for building and deploying these microservices.
apiVersion: skaffold/v4beta3
kind: Config
metadata:
name: microservices
build:
artifacts:
- image: order-service
context: order-service
docker:
dockerfile: Dockerfile
- image: payment-service
context: payment-service
docker:
dockerfile: Dockerfile
manifests:
rawYaml:
- k8s/order-manifest.yml
- k8s/payment-manifest.yml
Utilizing the Kubernetes manifests accessible under the k8s directory, this configuration file instructs Skaffold to build the Docker images for the Order and Payment microservices and deploy them to the Kubernetes cluster. The code below represents the Kubernetes manifests for the Order Microservice.
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: order-service
app.kubernetes.io/version: 0.0.1-SNAPSHOT
template:
metadata:
labels:
app.kubernetes.io/name: order-service
app.kubernetes.io/version: 0.0.1-SNAPSHOT
spec:
containers:
- name: order-service
image: order-service
imagePullPolicy: IfNotPresent
---
apiVersion: v1
kind: Service
metadata:
name: order-service
namespace: default
spec:
ports:
- name: http
port: 8080
protocol: TCP
targetPort: 8080
selector:
app.kubernetes.io/name: order-service
app.kubernetes.io/version: 0.0.1-SNAPSHOT
type: ClusterIP
The code below represents the Kubernetes manifests for the Payment Microservice.
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: payment-service
app.kubernetes.io/version: 0.0.1-SNAPSHOT
template:
metadata:
labels:
app.kubernetes.io/name: payment-service
app.kubernetes.io/version: 0.0.1-SNAPSHOT
spec:
containers:
- name: payment-service
image: payment-service
imagePullPolicy: IfNotPresent
---
apiVersion: v1
kind: Service
metadata:
name: payment-service
namespace: default
spec:
ports:
- name: http
port: 8081
protocol: TCP
targetPort: 8081
selector:
app.kubernetes.io/name: payment-service
app.kubernetes.io/version: 0.0.1-SNAPSHOT
type: ClusterIP
Finally, run the skaffold dev
command to deploy the microservices. This command tells Skaffold to continuously watch for changes in the code and automatically rebuild and redeploy the microservices. On running this command, you should see the following output:
$ skaffold dev
Generating tags...
- order-service -> order-service:22abffd
- payment-service -> payment-service:22abffd
Checking cache...
- order-service: Not found. Building
- payment-service: Not found. Building
Starting build...
Found [docker-desktop] context, using local docker daemon.
Building [payment-service]...
Target platforms: [linux/amd64]
.....
Build [payment-service] succeeded
Building [order-service]...
.....
Build [order-service] succeeded
Tags used in deployment:
- order-service -> order-service:f1eeffee29324d21a00ef97830ff36538c5060b69827b3f1365a0d9a8de4ba01
- payment-service -> payment-service:fbf01bb6276294a3f9bad25f1ac2c3fbd43813d7bf7549f05c94bb3c3adda0e1
Starting deploy...
- deployment.apps/order-service created
- service/order-service created
- deployment.apps/payment-service created
- service/payment-service created
Waiting for deployments to stabilize...
- deployment/order-service: unable to determine current service state of pod "order-service-6d78fbcc78-m8ncv"
- pod/order-service-6d78fbcc78-m8ncv: unable to determine current service state of pod "order-service-6d78fbcc78-m8ncv"
- deployment/payment-service: unable to determine current service state of pod "payment-service-98f85d574-75f8x"
- pod/payment-service-98f85d574-75f8x: unable to determine current service state of pod "payment-service-98f85d574-75f8x"
- deployment/payment-service is ready. [1/2 deployment(s) still pending]
- deployment/order-service is ready.
Deployments stabilized in 19.534 seconds
Listing files to watch...
- order-service
- payment-service
Press Ctrl+C to exit
Watching for changes...
[payment-service]
[payment-service] . ____ _ __ _ _
[payment-service] /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
[payment-service] ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
[payment-service] \\/ ___)| |_)| | | | | || (_| | ) ) ) )
[payment-service] ' |____| .__|_| |_|_| |_\__, | / / / /
[payment-service] =========|_|==============|___/=/_/_/_/
[payment-service] :: Spring Boot :: (v2.7.9)
[payment-service]
[order-service]
[order-service] . ____ _ __ _ _
[order-service] /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
[order-service] ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
[order-service] \\/ ___)| |_)| | | | | || (_| | ) ) ) )
[order-service] ' |____| .__|_| |_|_| |_\__, | / / / /
[order-service] =========|_|==============|___/=/_/_/_/
[order-service] :: Spring Boot :: (v2.7.9)
Fast local development and debugging with Telepresence
Telepresence is a Cloud Native Computing Foundation project that facilitates local development of Kubernetes applications by providing a “teleportation” of the local development environment into a Kubernetes cluster. Although Telepresence is not a complete solution for Kubernetes application development, it can be seamlessly integrated into the workflow to help developers quickly test and iterate changes locally before deploying them to the cluster.
Compared to Skaffold, Telepresence takes a step further by eliminating the need to build, push, and deploy your application container images. Instead, Telepresence allows developers to run their code directly on a remote Kubernetes cluster without needing a local container registry or deployment process. This approach can save time by speeding up and streamlining development, especially for teams working on complex applications with multiple dependencies.
Installing and configuring Telepresence
If you are using any other operating system other than macOS, then visit Telepresence’s official documentation for installation instructions. If you have a macOS, run this command brew install datawire/blackbird/telepresence
to install Telepresence.
Then run the telepresence version command to verify your installation.
$ telepresence version
Client : v2.11.1
Root Daemon : v2.11.1
User Daemon : v2.11.1
Traffic Manager: not connected
Use Telepresence to connect your local development environment to the remote cluster by running the telepresence connect
command.
$ telepresence connect
Launching Telepresence Root Daemon
Need root privileges to run: /usr/local/bin/telepresence daemon-foreground /Users/ashish/Library/Logs/telepresence '/Users/ashish/Library/Application Support/telepresence'
Password: <insert your password here>
Launching Telepresence User Daemon
telepresence connect: error: connector.Connect: traffic manager not found, if it is not installed, please run 'telepresence helm install'. If it is installed, try connecting with a --manager-namespace to point telepresence to the namespace it's installed in..
As you can see in the above output, there is an issue. We must also install the Traffic Manager using the telepresence helm install command. Let’s do that now.
$ telepresence helm install
Telepresence Daemons disconnecting…done
Traffic Manager installed successfully
Now, if we run the telepresence connect command again, you’ll see it will connect successfully.
$ telepresence connect
Connected to context docker-desktop (https://kubernetes.docker.internal:6443)
Looks like we are all set. Let’s try to access the Order service remotely deployed to the Kubernetes cluster.
$ curl -H 'Content-Type: application/json' \
-d '{"customerName": "mike","productName": "apple","price": 400.00}' \
-X POST \
order-service.default.svc.cluster.local:8080/orders | jq
{
"id": 2,
"customerName": "mike",
"productName": "apple",
"price": 400,
"paymentStatus": "success"
}
We can connect to the remote Kubernetes Service as if our development machine is within the cluster. It is important to remember that the Kubernetes service type is designated as ClusterIP. So it is impossible to access it directly, i.e., outside the cluster. To do so, we require the assistance of Telepresence, which is exactly what we have done in this instance.
Intercepting traffic with Telepresence
In this step, we will establish an intercept, essentially a directive for Telepresence to route all traffic intended for the OrderService to the locally running version of the service.
First, start the local instance of OrderService using the
mvn spring-boot:run
command. In this code version, we will deliberately changepaymentStatus
topending
for all the orders.’For verification, curl the service running locally to confirm
paymentStatus
set to pending.
$ curl -H 'Content-Type: application/json' \
-d '{"customerName": "mike","productName": "apple","price": 400.00}' \
-X POST \
localhost:8080/orders | jq
{
"id": 2,
"customerName": "mike",
"productName": "apple",
"price": 400,
"paymentStatus": "pending"
}
Great. Now let’s start the intercept using the telepresence intercept
command by giving the name of the service and the port.
$ telepresence intercept order-service --port 8080
Using Deployment order-service
intercepted
Intercept name : order-service
State : ACTIVE
Workload kind : Deployment
Destination : 127.0.0.1:8080
Service Port Identifier: http
Volume Mount Error : sshfs is not installed on your local machine
Intercepting : all TCP requests
Before hitting the remote order service, note that paymentStatus
is always returned as a success
in the response in the version deployed to Kubernetes. Now curl the remote service again.
From the following output, it is clear that Telepresence rerouted the traffic to the local running version of the application; hence, the paymentStatus
is set as pending
.
curl -H 'Content-Type: application/json' \
-d '{"customerName": "mike","productName": "apple","price": 400.00}' \
-X POST \
order-service.default.svc.cluster.local:8080/orders | jq
{
"id": 1,
"customerName": "mike",
"productName": "apple",
"price": 400,
"paymentStatus": "pending"
}
If you want to route a subset of the traffic, then you’d have to utilize personal intercept. You can enable personal intercepts by authenticating to Ambassador Cloud using the telepresence login
command.
Our demonstration of Telepresence has now ended. With Telepresence, we don’t have to go through the build, push, deploy, and test cycle. This helps us get feedback faster.
Wrapping Up
The article discusses developing Spring Boot applications quickly and efficiently on Kubernetes using Skaffold and Telepresence. The traditional development workflows can be slow and frustrating, especially when testing and debugging on Kubernetes. Fortunately, many tools and resources are available to help simplify the process.
With the right approach, it is possible to create an efficient and effective local development environment for workloads running on Kubernetes. So don’t let the challenges of developing on Kubernetes hold you back.
Following the steps outlined in the article, developers can streamline their workflow and focus on building and testing their applications without being slowed down by infrastructure concerns.