This guide demonstrates how to create and configure new microservices that integrate with the Linqra platform. We’ll use the “Inventory Service” as a reference implementation.
After starting your service, verify that it has registered with Eureka by accessing the Eureka dashboard at https://localhost:8761/.
You should see your service listed in the “Instances currently registered with Eureka” section:
A successful registration means:
Your service shows as “UP” in the Status column
It appears with a unique instance ID
The API-GATEWAY service is also registered and running
If you see an emergency message about renewals, don’t be alarmed. This often appears during development when services are frequently started and stopped. As long as your service shows “UP”, it is properly registered.
The API Gateway will automatically communicate with your service’s health endpoint if health checking is enabled. The /api/health endpoint will return detailed information about your service’s health status, including:
Service identifier
Current status (UP/DOWN)
Service uptime
Current timestamp
Performance metrics like heap usage and CPU information
This allows the API Gateway to make intelligent routing decisions and implement circuit breaking if your service experiences issues.
Since we’re using dynamic port allocation (server.port: 0), you’ll need to check the Eureka dashboard or your service logs to determine the assigned port.
Create a filter to validate JWT tokens and check for required roles:
Copy
package org.lite.inventory.filter;import jakarta.servlet.FilterChain;import jakarta.servlet.ServletException;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import lombok.extern.slf4j.Slf4j;import org.springframework.lang.NonNull;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.security.oauth2.jwt.Jwt;import org.springframework.stereotype.Component;import org.springframework.web.filter.OncePerRequestFilter;import java.io.IOException;import java.util.List;import java.util.Map;@Component@Slf4jpublic class JwtRoleValidationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { // Retrieve the JWT token from the security context var authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.getPrincipal() instanceof Jwt jwt) { // Log the JWT token for debugging purposes log.info("JWT Token: {}", jwt.getTokenValue()); // Log roles for debugging List<String> realmRoles = (List<String>) jwt.getClaimAsMap("realm_access").get("roles"); log.info("Realm Roles: {}", realmRoles); Map<String, Object> resourceAccess = jwt.getClaimAsMap("resource_access"); log.info("Client Roles: {}", resourceAccess); if (hasRequiredRole(jwt)) { filterChain.doFilter(request, response); // Continue the request processing } else { response.setStatus(HttpServletResponse.SC_FORBIDDEN); // Return 403 Forbidden if role check fails } } else { //It's up to you which line do you want to enable, the latter one is more secure, as you don't force the call to use a jwt token //filterChain.doFilter(request, response); // No JWT token, continue request processing, i.e. calling the GET from browser response.setStatus(HttpServletResponse.SC_FORBIDDEN); // Return 403 Forbidden if role check fails, you are forcing to use the token } } //We force both realm and resource roles to exist in the token private boolean hasRequiredRole(Jwt jwt) { // Check realm roles Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access"); boolean hasRealmRole = false; if (realmAccess != null) { List<String> realmRoles = (List<String>) realmAccess.get("roles"); if (realmRoles != null && realmRoles.contains("gateway_admin_realm")) { hasRealmRole = true; } } // Check client roles for lite-mesh-gateway-client boolean hasClientRole = false; Map<String, Object> resourceAccess = jwt.getClaimAsMap("resource_access"); if (resourceAccess != null && resourceAccess.containsKey("linqra-gateway-client")) { Map<String, List<String>> clientRoles = (Map<String, List<String>>) resourceAccess.get("linqra-gateway-client"); if (clientRoles.get("roles").contains("gateway_admin")) { hasClientRole = true; } } // Both roles must be present return hasRealmRole && hasClientRole; }}
Create the SecurityConfig.java class to configure Spring Security:
Copy
package org.lite.inventory.config;import lombok.AllArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.lite.inventory.filter.JwtRoleValidationFilter;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.config.annotation.web.configurers.AbstractHttpConfigurer;import org.springframework.security.web.SecurityFilterChain;@Configuration@EnableWebSecurity@AllArgsConstructor@Slf4jpublic class SecurityConfig { //It will be called even though you don't use it here, so don't remove it private final JwtRoleValidationFilter jwtRoleValidationFilter; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .x509(x509 -> x509 .x509PrincipalExtractor((principal -> { //Enable mTLS (client certificate authentication) // Extract the CN from the certificate (adjust this logic as needed) String dn = principal.getSubjectX500Principal().getName(); log.info("dn: {}", dn); String cn = dn.split(",")[0].replace("CN=", ""); return cn; // Return the Common Name (CN) as the principal }) )) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/inventory-service/**")//no matter what you put here, if we have the gateway token from oauth2ResourceServer, we'll be authenticated .permitAll() // Public endpoints (if any) .anyRequest() .authenticated() ) .oauth2ResourceServer(oauth2-> { // Enable OAuth2-based authentication (via JWT tokens) oauth2.jwt(Customizer.withDefaults()); }); return http.build(); }}
Obtain a token from Keycloak using the client credentials grant type
Include the token in the Authorization header of your requests:
Copy
Authorization: Bearer <your-jwt-token>
The security configuration demands both proper JWT tokens and valid certificates. Make sure your API Gateway is correctly configured to pass these credentials to your microservice.
Create a skeleton for the InventoryController that will house our business logic endpoints:
Copy
package org.lite.inventory.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;@RestController@RequestMapping("/api/inventory")public class InventoryController { private final RestTemplate restTemplate; @Autowired public InventoryController(RestTemplate restTemplate) { this.restTemplate = restTemplate; } // API endpoints will be implemented in subsequent sections}
Delete an item: DELETE https://localhost:{port}/inventory-service/api/inventory/1
Product availability: GET https://localhost:{port}/inventory-service/api/inventory/product-availability
Remember that the actual port will be dynamically assigned since we’re using server.port: 0. Check the Eureka dashboard or service logs to find the assigned port.
The inter-service communication will only work if the Product Service is also running and registered with Eureka. If it’s not running, the /product-availability endpoint will return an error.
Full implementation of the InventoryController with CRUD operations
Models for inventory items and product availability
Service-to-service communication with the Product Service
Mock data for testing purposes
This reference implementation demonstrates best practices for creating microservices that integrate with the Linqra platform and can serve as a starting point for your own services.
By following this guide and referencing the sample implementation, you can create secure, cloud-native microservices that integrate seamlessly with the Linqra platform. Your services will be discoverable through Eureka, secured with both JWT tokens and mTLS, and ready for extension with your specific business logic.
The Inventory Service uses GitHub Actions for continuous integration and deployment. When code is merged into the master branch, it automatically deploys to EC2. Here’s the complete CI/CD configuration:
Copy
name: Linqra Inventory Service CIon: push: branches: [ "master" ] pull_request: branches: [ "master" ]permissions: contents: writejobs: java-app: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Upload Inventory Service source files - name: Upload Inventory Service source uses: actions/upload-artifact@v4 with: name: inventory-service-source path: | . # Upload docker-compose.yml - name: Upload docker-compose file uses: actions/upload-artifact@v4 with: name: docker-compose path: docker-compose-ec2.yml # Upload root pom.xml - name: Upload root pom file uses: actions/upload-artifact@v4 with: name: root-pom path: pom.xml # Debug and upload .kube directory with hidden files - name: Debug .kube directory run: | echo "Checking .kube directory contents:" ls -la .kube/ - name: Copy .kube to temporary directory run: | cp -r .kube kube-config - name: Upload kube directory uses: actions/upload-artifact@v4 with: name: kube-config path: kube-config/** # Add this new step to upload keys directory - name: Upload keys directory uses: actions/upload-artifact@v4 with: name: keys-config path: keys/ if-no-files-found: error deploy: needs: [java-app] runs-on: ubuntu-latest if: github.ref == 'refs/heads/master' steps: - name: Download all artifacts uses: actions/download-artifact@v4 with: path: artifacts - name: Install SSH key uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.EC2_SSH_KEY_PROD }} - name: Deploy to EC2 env: EC2_HOST: ${{ secrets.HOST_DNS_PROD }} EC2_USERNAME: ${{ secrets.USERNAME_PROD }} TARGET_DIR: ${{ secrets.TARGET_DIR_PROD }} run: | # Disable strict host key checking mkdir -p ~/.ssh echo "StrictHostKeyChecking no" >> ~/.ssh/config # Ensure the target directory exists ssh $EC2_USERNAME@$EC2_HOST "sudo mkdir -p /var/www/inventory-service && sudo chown -R $EC2_USERNAME:$EC2_USERNAME /var/www/inventory-service && sudo chmod -R 755 /var/www/inventory-service" # Rsync main directories rsync -avz --delete artifacts/inventory-service-source/ $EC2_USERNAME@$EC2_HOST:/var/www/inventory-service # Use rsync for sensitive files rsync -avz artifacts/docker-compose/docker-compose-ec2.yml $EC2_USERNAME@$EC2_HOST:/var/www/inventory-service/docker-compose.yml rsync -avz artifacts/root-pom/pom.xml $EC2_USERNAME@$EC2_HOST:/var/www/inventory-service/pom.xml # .kube and keys (if needed, use scp or rsync as appropriate) if [ -d "artifacts/kube-config" ]; then rsync -avz --delete --rsync-path="sudo rsync" artifacts/kube-config/ $EC2_USERNAME@$EC2_HOST:/var/www/inventory-service/.kube/ fi if [ -d "artifacts/keys-config" ]; then rsync -avz --delete --rsync-path="sudo rsync" artifacts/keys-config/ $EC2_USERNAME@$EC2_HOST:/var/www/inventory-service/keys/ fi ssh $EC2_USERNAME@$EC2_HOST " # Set permissions sudo chown -R ubuntu:ubuntu /var/www/inventory-service sudo chmod -R 600 /var/www/inventory-service/keys/* || echo 'No keys to set permissions for' sudo find /var/www/inventory-service/keys -type d -exec chmod 755 {} \; sudo find /var/www/inventory-service/keys -type f -exec chmod 600 {} \; # Move to the deployment directory cd /var/www/inventory-service # Prune unused images/containers/volumes/networks echo 'Pruning unused images/containers/volumes/networks' sudo docker system prune -a -f # Check disk usage echo 'Checking disk usage' df -hT # Ensure the Docker network exists echo 'Ensuring the Docker network exists' sudo docker network create linqra-network || true # Build and start containers echo 'Building and starting containers' sudo docker compose -f docker-compose.yml -p linqra up -d --build inventory-service # Check if containers are running echo 'Checking if containers are running' docker ps " dependency-review: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Dependency Review uses: actions/dependency-review-action@v4 with: fail-on-severity: high