Sunday, November 12, 2023

Deploying Spring Boot Apps Locally with Skaffold, Helm and Docker for Kubernetes

Introduction:

In the ever-evolving landscape of software development and deployment, the integration of Spring Boot,Skaffold, Helm, Docker, and local Kubernetes enviroment has become a powerful combination for building scalable and containerized applications.




Skaffold: Automating Kubernetes Development Workflows

Skaffold is a powerful command-line tool designed to streamline the development and deployment workflows for Kubernetes applications. It acts as the orchestrator, automating tasks such as building container images, deploying to Kubernetes clusters, and monitoring code changes in real-time.

Helm: Simplifying Kubernetes Application Management


Helm is a package manager for Kubernetes applications, making it easier to define, install, and upgrade applications. Helm uses charts, which encapsulate pre-configured Kubernetes resources, allowing developers to share and deploy applications with ease.

Kubernetes: The Backbone of Container Orchestration


Kubernetes is an open-source container orchestration platform that automates the deployment, scaling, and management of containerized applications. It provides a robust framework for running and coordinating applications in a distributed environment.

Docker Desktop: Localized Development Environment for Containers


Docker Desktop is an application that simplifies the development and deployment of containerized applications. It includes a local Kubernetes cluster, providing developers with a consistent environment for building, testing, and deploying containers.

Prerequisites

Before we dive into the world of streamlined Spring Boot development with Skaffold, Helm, Docker, and Kubernetes, ensure you have the following prerequisites installed on your machine:

Skaffold:

Follow the official Skaffold installation guide based on your operating system: Skaffold Installation Guide.

Helm:

Install Helm by following the Helm installation instructions: Helm Installation Guide.

Kubernetes:

Choose one of the following options based on your preference:

Docker Desktop:

Install Docker Desktop, which includes Kubernetes support. Download it from the official Docker website: Docker Desktop.

Minikube:

If you prefer using Minikube, follow the installation instructions: Minikube Installation Guide.

Ingress-Nginx Controller:


Install the Ingress-Nginx Controller for routing external HTTP/S traffic to services in the Kubernetes cluster. Use one of the following methods:

Using kubectl:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.41.2/deploy/static/provider/cloud/deploy.yaml
Using helm:

helm install --namespace default nginx ingress-nginx --repo https://kubernetes.github.io/ingress-nginxSpring Boot 3:

For more details, refer to the official Ingress-Nginx deployment guide: Ingress-Nginx Deployment Guide.

If still not work after install then stop and start Docker Desktop Kubernetes.

Directory etc/folder:


Adding the IP-to-hostname mapping in /etc/hosts: in this post
127.0.0.1       myapp.local

Spring Boot 3:

Create a Spring Boot 3 application. 

Environment Setup

Helm Version:

helm version
version.BuildInfo{Version:"v3.13.0", GitCommit:"825e86f6a7a38cef1112bfa606e4127a706749b1", GitTreeState:"clean", GoVersion:"go1.20.8"}
Skaffold Version:

skaffold version
v2.9.0
Kubernetes Nodes:

kubectl get nodes
NAME             STATUS   ROLES           AGE    VERSION
docker-desktop   Ready    control-plane   7d4h   v1.28.2


Project Structure: 


skaffold-dockerfile-demo
|-- myapp-chart
|   |-- templates
|   |   |-- deployment.yaml
|   |   |-- ingress.yaml
|   |   |-- service.yaml
|   |-- Chart.yaml
|   |-- values.yaml
|-- Dockerfile
|-- skaffold.yaml
|-- src
|-- ...

myapp-chart:

This directory encapsulates the Helm chart for your application. Helm charts provide a way to package, distribute, and manage Kubernetes applications.

templates: Contains YAML templates for Kubernetes resources like Deployment, Ingress, and Service.
  • deployment.yaml: Defines the deployment configuration for your Spring Boot application.
  • apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: {{ .Chart.Name }}
    spec:
      replicas: {{ .Values.replicaCount }}
      selector:
        matchLabels:
          app: {{ .Chart.Name }}
      template:
        metadata:
          labels:
            app: {{ .Chart.Name }}
        spec:
          containers:
            - name: {{ .Chart.Name }}
              image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
              ports:
                - name: http
                  containerPort: {{ .Values.service.port }}
  • ingress.yaml: Configures the Ingress resource for routing external traffic.
  • apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: {{ .Chart.Name }}-ingress
    spec:
      defaultBackend:
        service:
          name: {{ .Chart.Name }}-service
          port:
            number: {{ .Values.service.port }}
      ingressClassName: nginx
      rules:
        - host: myapp.local
          http:
            paths:
              - pathType: Prefix
                backend:
                  service:
                    name: {{ .Chart.Name }}-service
                    port:
                      number: {{ .Values.service.port }}
                path: /
  • service.yaml: Specifies the Kubernetes Service for your application.
  • apiVersion: v1
    kind: Service
    metadata:
      name: {{ .Chart.Name }}-service
    spec:
      ports:
        - name: http-web
          port: {{ .Values.service.port }}
          protocol: TCP
          #targetPort: 8080
          nodePort: 30090
      selector:
        app: {{ .Chart.Name }}
      type: NodePort
      #type: LoadBalancer
Chart.yaml: Describes the metadata and configuration of the Helm chart.

apiVersion: v2
name: myapp
description: A Helm chart for my Spring Boot application
version: 1.0.0
appVersion: 1.0.0

values.yaml: Contains default configuration values for your Helm chart.

replicaCount: 1

image:
  repository: skaffold-demo-docker
  tag: latest

service:
  port: 8080

ingress:
  enabled: true


Dockerfile: Check the reference help me for created Dockerfile 

This file instructs Docker on how to build the Docker image for your Spring Boot application.
FROM eclipse-temurin:17-jdk-focal

WORKDIR /app

COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline

COPY src ./src

CMD ["./mvnw", "spring-boot:run"]


skaffold.yaml:

Skaffold configuration file that automates the development workflow. It defines how to build and deploy your application. In this post the skaffokd build by Dockerfile, however in the GitHub also I added by Jib Maven.

apiVersion: skaffold/v2beta10
kind: Config
build:
  artifacts:
    - image: skaffold-demo-docker
      context: .
      docker:
        dockerfile: Dockerfile

deploy:
  helm:
    releases:
      - name: myapp
        chartPath: ./myapp-chart
        valuesFiles:
          - ./myapp-chart/values.yaml

Spring Boot App


UserController:
 
import com.henry.dockerfiledemo.record.UserRecord;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @GetMapping("/hello")
    public String hello() {
        return "hello world";
    }

    @GetMapping("/user")
    public UserRecord helloUser() {
        return new UserRecord("Henry", "Xiloj");
    }
}


Test:

Run next command: we can check in dev mode our changes. Otherwise try with skaffold run. For more command check official documentation

skaffold dev














Curl:
 curl http://myapp.local/hello








curl http://myapp.local/user








Helm Commands:

helm list




Kubectl commands: for check deployement, sevices, ingress etc


kubectl get all
kubectl get deployment
kubectl get svc
kubectl get ingress


Source Code


Here on GitHub.





References


https://skaffold.dev/
https://helm.sh/
https://www.docker.com/blog/kickstart-your-spring-boot-application-development/
https://stackoverflow.com/questions/65193758/enable-ingress-controller-on-docker-desktop-with-wls2
https://kubernetes.github.io/ingress-nginx/deploy/



Monday, September 25, 2023

GraphQL Error Handling with Spring Boot 3

 Why HTTP 200 is Different from REST

GraphQL servers typically return an HTTP 200 status code for all responses, regardless of whether the request was successful or encountered an error. This behavior is by design and is different from RESTful APIs, where different HTTP status codes are used to indicate the success or failure of a request.

In GraphQL, errors are typically handled differently than in RESTful APIs. Instead of using HTTP status codes, GraphQL responses include an "errors" field in the JSON response object when there are issues with the request. This "errors" field contains an array of error objects, each of which includes information about the specific error, such as a message and location within the query.



GraphQL response might look like when there are errors:

{
  "data": {
    "user": null
  },
  "errors": [
    {
      "message": "User not found",
      "locations": [
        {
          "line": 4,
          "column": 3
        }
      ],
      "path": ["user"]
    }
  ]
}

The request to fetch a user encountered an error, and the server responds with an HTTP 200 status code but includes an "errors" field in the response to provide details about the error.

If you're working with a GraphQL client or library, it should be designed to check for errors in the response and handle them accordingly. Typically, successful responses will have a "data" field with the requested data, and errors will be present in the "errors" field.

However we can send customized error messages in the "errors" field in the response and we can send something like BAD_REQUEST or NOT_FOUND etc.

GraphQL Error Handling

DataFetcherExceptionResolverAdapter is a Spring for GraphQL class that allows you to implement custom error handling for your GraphQL API. It is an extension of the DataFetcherExceptionResolver interface, which is used to resolve exceptions that occur during the execution of data fetchers.

pom.xml

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>5.2.4</version>
</dependency>

Config

public class CustomGraphQLException extends RuntimeException {

    private final int statusCode;
    public CustomGraphQLException(int statusCode, String message) {
        super(message);
        this.statusCode = statusCode;
    }
    public int getStatusCode() {
        return statusCode;
    }

}

DataFetcherExceptionResolverAdapter

Sample example HTTP 404 Not Found and HTTP 400 Bad Request

@Component
public class CustomGraphQLExceptionHandler extends DataFetcherExceptionResolverAdapter {

    @Override
    protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
        if (ex instanceof CustomGraphQLException) {
            ErrorType errorType;
            if(((CustomGraphQLException) ex).getStatusCode()==400){
                errorType = ErrorType.BAD_REQUEST;
                return graphQLError(errorType, (CustomGraphQLException) ex, env);
            }
            if(((CustomGraphQLException) ex).getStatusCode()==404){
                errorType = ErrorType.NOT_FOUND;
                return graphQLError(errorType, (CustomGraphQLException) ex, env);
            }
            else {
                return GraphqlErrorBuilder.newError().build();
            }

        } else {
            return GraphqlErrorBuilder.newError().build();
        }
    }

    private GraphQLError graphQLError(ErrorType errorType, CustomGraphQLException ex,DataFetchingEnvironment env){
        return GraphqlErrorBuilder.newError()
                .errorType(errorType)
                .message(ex.getMessage())
                .path(env.getExecutionStepInfo().getPath())
                .location(env.getField().getSourceLocation())
                .build();
    }
}

Resolver

@QueryMapping
    public Iterable<Author> getAllAuthors(@ContextValue Map<String, List<String>> headers){
        throw new CustomGraphQLException(400, "An error occurred while processing your request.");
    }

GraphQLError with the appropriate classification:

{
    "errors": [
        {
            "message": "An error occurred while processing your request.",
            "locations": [
                {
                    "line": 2,
                    "column": 3
                }
            ],
            "path": [
                "getAllAuthors"
            ],
            "extensions": {
                "classification": "BAD_REQUEST"
            }
        }
    ],
    "data": {
        "getAllAuthors": null
    }
}

GraphQLError without classification:

@QueryMapping
    public Iterable<Author> getAllAuthors(@ContextValue Map<String, List<String>> headers){
        throw new CustomGraphQLException(401, "Unauthorized");
    }
{
    "errors": [
        {
            "message": "INTERNAL_ERROR for 54ab7ea3-3bc1-a5b1-0c1c-e45fbda75659",
            "locations": [
                {
                    "line": 2,
                    "column": 3
                }
            ],
            "path": [
                "getAllAuthors"
            ],
            "extensions": {
                "classification": "INTERNAL_ERROR"
            }
        }
    ],
    "data": {
        "getAllAuthors": null
    }
}

Complete exist CRUD example:

Here on GitHub.















Thursday, July 27, 2023

Single Sign-On with Angular 16 Keycloak 22 Spring Boot 3


Single Sign-On (SSO) is an authentication process that allows users to access multiple applications or services with a single set of login credentials. In traditional authentication systems, users need to enter their username and password separately for each application they want to use, which can become cumbersome and lead to security risks. SSO streamlines this process by providing a centralized authentication mechanism.

Single Sign-On typically works:

  1. User logs in: The user enters their login credentials (username and password) into a centralized authentication system, which is often referred to as the Identity Provider (IdP).
  2. Authentication: The IdP verifies the user's credentials and authenticates their identity.
  3. Token issuance: Upon successful authentication, the IdP generates a secure token that contains information about the user's identity and authentication status.
  4. Access to other services: When the user tries to access a different application or service (referred to as the Service Provider or SP), the application asks for authentication.
  5. Token validation: Instead of prompting the user for credentials again, the application accepts the token issued by the IdP and validates it.
  6. Access granted: If the token is valid and the user is authorized to use the service, access is granted without the need for additional login credentials.

Advantages of Single Sign-On:

  1. Improved user experience: Users only need to remember one set of login credentials, making it more convenient and reducing the risk of forgotten passwords.
  2. Enhanced security: Since users don't need to enter credentials into each service, the likelihood of them using weak or easily guessable passwords is reduced. Additionally, SSO allows for centralized security policies and better monitoring of user access.
  3. Easier administration: IT administrators can manage user access and permissions from a central location, making it simpler to grant or revoke access to various applications.
  4. Cost savings: SSO can reduce support costs related to password resets and account management issues.
Single Sign-On is widely used in various industries and organizations, especially when employees need access to multiple internal systems or when a company offers services to external users across different platforms. It promotes both security and user convenience, striking a balance between these two critical aspects.



Overview:

  • Configuration Angular Application
  • Configuration Spring Boot  project  
  • Configuration and installing Keycloak 22 by docker compose 
  • Run & Test

Configuration  Angular App:


Technology

  • Visual Studio Code 
  • Node 18
  • Npm 9
  • Angular Cli 16 

Check the official documentation  keycloak-angular

By default, all HttpClient requests will add the Authorization header in the format of: Authorization: Bearer **_TOKEN_**.

  • Configuration Angular and keycloack.

npm install keycloak-angular keycloak-js


Setup


import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular';
import { HttpClientModule } from '@angular/common/http';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

function initializeKeycloak(keycloak: KeycloakService) {
  return () =>
    keycloak.init({
      config: {
        realm: 'demo',
        url: 'http://localhost:8080',
        clientId: 'spring-boot-angular-keycloak'
      },
      initOptions: {
        onLoad: 'check-sso',
        silentCheckSsoRedirectUri:
          window.location.origin + '/assets/silent-check-sso.html'
      }
    });
}

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppRoutingModule, KeycloakAngularModule, HttpClientModule],
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: initializeKeycloak,
      multi: true,
      deps: [KeycloakService]
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Create a file called silent-check-sso.html in the assets directory of your application and paste in the contents as seen below.


<html>
  <body>
    <script>
      parent.postMessage(location.href, location.origin);
    </script>
  </body>
</html>


Configuration Spring Boot project  :


Technology

  • Spring Boot 3.1.2
  • Spring Security 6.1
  • OAuth2 Resource Server
  • Java 17
  • Maven 
  • IntelliJ IDEA Community

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.1.2</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.henry</groupId>
	<artifactId>sso-spring-boot3-angular16-keycloak22</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>sso-spring-boot3-angular16-keycloak22</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>

		<!-- oauth2 resourcer server -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

application.yml


server:
  port: 8081
  servlet:
    context-path: /

spring:
  security:
    oauth2:
      resource-server:
        jwt:
          issuer-uri: http://localhost:8080/realms/demo
          jwk-set-uri: http://localhost:8080/realms/demo/protocol/openid-connect/certs


Configuration class

  • Configure the CORS policies. You can do this by defining a bean for CorsConfigurationSource.
package com.henry.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;

@Configuration
@EnableWebSecurity
public class SecurityConfig {


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {

        httpSecurity
                .authorizeHttpRequests(registry -> {
                            registry.requestMatchers("/api/**");
                            registry.anyRequest().authenticated();
                        }
                )
                .oauth2ResourceServer(oauth2Configure -> oauth2Configure.jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwt -> {
                    Map<String, Collection<String>> realmAccess = jwt.getClaim("realm_access");
                    Collection<String> roles = realmAccess.get("roles");
                    var grantedAuthorities = roles.stream()
                            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                            .collect(Collectors.toList());
                    return new JwtAuthenticationToken(jwt, grantedAuthorities);
                })))
                .cors(Customizer.withDefaults())

        ;

        return httpSecurity.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedOrigin("*"); // Set allowed origins here (you can specify specific origins instead of "*")
        configuration.addAllowedMethod("*"); // Set allowed HTTP methods (e.g., GET, POST, etc.)
        configuration.addAllowedHeader("*"); // Set allowed headers (e.g., Content-Type, Authorization, etc.)

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Bean
    public CorsFilter corsFilter() {
        return new CorsFilter(corsConfigurationSource());
    }


}


package com.henry.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class WebConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer()
    {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**").allowedOrigins("*");
            }
        };
    }
}

Pojo


package com.henry.model;

public class User {

    private Long id;
    private String username;
    private String email;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

Controller


package com.henry.controller;

import com.henry.model.User;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private List<User> users = new ArrayList<>();
    private Long currentUserId = 1L; // Used to assign incremental IDs to users

    @PostMapping
    public User addUser(@RequestBody User user) {
        // Assign an incremental ID to the user before saving
        user.setId(currentUserId++);

        // Save the user to the in-memory list
        users.add(user);
        return user;
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        // Search for the user in the in-memory list based on the provided ID
        User user = users.stream()
                .filter(u -> u.getId().equals(id))
                .findFirst()
                .orElse(null);

        if (user != null) {
            return ResponseEntity.ok(user);
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}

Configuration  and installing Keycloak 22 by docker compose:


version: '3'
services:
  keycloak:
    image: quay.io/keycloak/keycloak:22.0.0
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    ports:
      - 8080:8080
    volumes:
      - ./config/:/opt/keycloak/data/import:ro
    entrypoint: '/opt/keycloak/bin/kc.sh start-dev --import-realm'


Go to root folder in angular project run next commad:






 docker-compose -f docker-compose-keycloak.yml up -d

Add Valid Redirect URIs.

  • http://localhost:4200/*

  • http://localhost:8081/*

Web origings (*).
















Run & Test

Run Spring Boot application with command: mvn spring-boot:run. by console, IntelliJ etc.





Run angular app ng serve.






User: admin
Passwd: admin

Hint: To access keycloak admin, please use: http://localhost:8080. Same user and password as above.

1.






2.



























3. 















Source Code


Here on GitHub:




References


https://www.npmjs.com/package/keycloak-angular#example-project










Deploying Spring Boot Apps Locally with Skaffold, Helm and Docker for Kubernetes

Introduction: In the ever-evolving landscape of software development and deployment, the integration of Spring Boot,Skaffold, Helm, Docker, ...