google-site-verification: google61fe8ba583a51912.html
GitOps with ArgoCD for Java Applications

GitOps is a modern approach to continuous delivery that uses Git as the single source of truth for infrastructure and application deployment. ArgoCD is a popular GitOps tool that automates deployment to Kubernetes. Here's how to implement GitOps for Java applications using ArgoCD.


GitOps Principles with ArgoCD

  1. Declarative - Everything described as code
  2. Versioned - All changes tracked in Git
  3. Automated - ArgoCD automatically syncs changes
  4. Auditable - Complete deployment history
  5. Self-healing - Automatic drift detection and correction

Architecture Overview

Git Repository (Source of Truth) ↓ ArgoCD (Controller) ↓ Kubernetes Cluster (Target) ↓ Java Application (Running)

Prerequisites

  • Kubernetes cluster (Minikube, EKS, AKS, GKE)
  • kubectl configured
  • Git repository
  • Java application with Docker image

1. Java Application Setup

Example: Spring Boot Application

pom.xml

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>gitops-java-app</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.0</version> <relativePath/> </parent> <properties> <java.version>17</java.version> <docker.image.prefix>myregistry</docker.image.prefix> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-maven-plugin</artifactId> <version>3.3.1</version> <configuration> <to> <image>${docker.image.prefix}/gitops-java-app:${project.version}</image> </to> </configuration> </plugin> </plugins> </build> </project>

Application.java

package com.example.gitops; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } @RestController class HelloController { @GetMapping("/") public String hello() { return "Hello from GitOps Java App! Version: " + System.getenv().getOrDefault("APP_VERSION", "1.0.0"); } @GetMapping("/health") public String health() { return "OK"; } @GetMapping("/info") public AppInfo info() { return new AppInfo( "gitops-java-app", System.getenv().getOrDefault("APP_VERSION", "1.0.0"), "running" ); } record AppInfo(String name, String version, String status) {} }

2. Docker Configuration

Dockerfile

FROM eclipse-temurin:17-jre-jammy WORKDIR /app COPY target/gitops-java-app-1.0.0.jar app.jar RUN addgroup --system --gid 1000 javauser && \ adduser --system --uid 1000 --gid 1000 javauser && \ chown -R javauser:javauser /app USER javauser EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

3. Kubernetes Manifests (Git Repository Structure)

gitops-repo/ ├── apps/ │ └── java-app/ │ ├── base/ │ │ ├── deployment.yaml │ │ ├── service.yaml │ │ ├── configmap.yaml │ │ └── kustomization.yaml │ └── overlays/ │ ├── development/ │ │ ├── kustomization.yaml │ │ └── patch-deployment.yaml │ ├── staging/ │ │ ├── kustomization.yaml │ │ └── patch-deployment.yaml │ └── production/ │ ├── kustomization.yaml │ └── patch-deployment.yaml └── infrastructure/ └── argocd/ └── application.yaml

Base Kubernetes Manifests

apps/java-app/base/deployment.yaml

apiVersion: apps/v1 kind: Deployment metadata: name: java-app labels: app: java-app spec: replicas: 2 selector: matchLabels: app: java-app template: metadata: labels: app: java-app spec: containers: - name: java-app image: myregistry/gitops-java-app:1.0.0 ports: - containerPort: 8080 env: - name: APP_VERSION value: "1.0.0" - name: JAVA_OPTS value: "-Xmx512m -Xms256m" livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m"

apps/java-app/base/service.yaml

apiVersion: v1 kind: Service metadata: name: java-app-service labels: app: java-app spec: selector: app: java-app ports: - port: 80 targetPort: 8080 protocol: TCP type: ClusterIP

apps/java-app/base/configmap.yaml

apiVersion: v1 kind: ConfigMap metadata: name: java-app-config data: application.properties: | server.port=8080 management.endpoints.web.exposure.include=health,info,metrics logging.level.com.example=INFO

apps/java-app/base/kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - deployment.yaml - service.yaml - configmap.yaml commonLabels: app: java-app version: "1.0.0"

Environment-Specific Overlays

apps/java-app/overlays/development/kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - ../../base namespace: development nameSuffix: -dev images: - name: myregistry/gitops-java-app newTag: latest patchesStrategicMerge: - patch-deployment.yaml commonLabels: environment: development

apps/java-app/overlays/development/patch-deployment.yaml

apiVersion: apps/v1 kind: Deployment metadata: name: java-app spec: replicas: 1 template: spec: containers: - name: java-app env: - name: SPRING_PROFILES_ACTIVE value: "dev" - name: LOGGING_LEVEL value: "DEBUG" resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "250m"

apps/java-app/overlays/production/kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - ../../base namespace: production namePrefix: prod- images: - name: myregistry/gitops-java-app newTag: 1.0.0 patchesStrategicMerge: - patch-deployment.yaml commonLabels: environment: production

apps/java-app/overlays/production/patch-deployment.yaml

apiVersion: apps/v1 kind: Deployment metadata: name: java-app spec: replicas: 3 template: spec: containers: - name: java-app env: - name: SPRING_PROFILES_ACTIVE value: "prod" - name: LOGGING_LEVEL value: "INFO" resources: requests: memory: "1Gi" cpu: "500m" limits: memory: "2Gi" cpu: "1000m"

4. ArgoCD Application Configuration

infrastructure/argocd/java-app.yaml

apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: java-app-production namespace: argocd finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: https://github.com/your-org/gitops-repo.git targetRevision: HEAD path: apps/java-app/overlays/production directory: recurse: true destination: server: https://kubernetes.default.svc namespace: production syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true - PruneLast=true ignoreDifferences: - group: apps kind: Deployment jsonPointers: - /spec/replicas

5. ArgoCD Setup and Configuration

Install ArgoCD in Kubernetes

# Create namespace kubectl create namespace argocd # Install ArgoCD kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml # Get initial admin password kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d # Port forward to access UI kubectl port-forward svc/argocd-server -n argocd 8080:443

Java Application for ArgoCD Management

ArgoCD Java Client Example

package com.example.argocd; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.Configuration; import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.util.Config; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.concurrent.TimeUnit; public class ArgoCDJavaClient { private final String argoCDUrl; private final String authToken; private final OkHttpClient httpClient; private final ObjectMapper objectMapper; public ArgoCDJavaClient(String argoCDUrl, String authToken) { this.argoCDUrl = argoCDUrl; this.authToken = authToken; this.httpClient = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); this.objectMapper = new ObjectMapper(); } public ApplicationStatus getApplicationStatus(String appName) throws Exception { Request request = new Request.Builder() .url(argoCDUrl + "/api/v1/applications/" + appName) .header("Authorization", "Bearer " + authToken) .build(); try (Response response = httpClient.newCall(request).execute()) { if (response.isSuccessful()) { return objectMapper.readValue(response.body().string(), ApplicationStatus.class); } else { throw new RuntimeException("Failed to get application status: " + response.code()); } } } public void syncApplication(String appName) throws Exception { Request request = new Request.Builder() .post(okhttp3.RequestBody.create("", null)) .url(argoCDUrl + "/api/v1/applications/" + appName + "/sync") .header("Authorization", "Bearer " + authToken) .build(); try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new RuntimeException("Failed to sync application: " + response.code()); } } } public static class ApplicationStatus { private Metadata metadata; private Status status; // Getters and setters public Metadata getMetadata() { return metadata; } public void setMetadata(Metadata metadata) { this.metadata = metadata; } public Status getStatus() { return status; } public void setStatus(Status status) { this.status = status; } public static class Metadata { private String name; private String namespace; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getNamespace() { return namespace; } public void setNamespace(String namespace) { this.namespace = namespace; } } public static class Status { private String health; private String sync; public String getHealth() { return health; } public void setHealth(String health) { this.health = health; } public String getSync() { return sync; } public void setSync(String sync) { this.sync = sync; } } } }

6. CI/CD Pipeline Integration

GitHub Actions Workflow

.github/workflows/build-and-deploy.yaml

name: Build and Deploy Java App on: push: branches: [ main, develop ] pull_request: branches: [ main ] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: maven - name: Build with Maven run: mvn -B package -DskipTests - name: Run tests run: mvn test - name: Build Docker image run: | docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} . - name: Log in to Container Registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin - name: Push Docker image run: | docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} update-manifests: needs: build-and-test runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 with: token: ${{ secrets.PAT_TOKEN }} - name: Update Kubernetes manifests run: | # Update image tag in manifests sed -i 's|myregistry/gitops-java-app:.*|${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}|g' apps/java-app/base/deployment.yaml # Update app version sed -i 's/value: ".*"/value: "${{ github.sha }}"/g' apps/java-app/base/deployment.yaml git config user.name "GitHub Actions" git config user.email "[email protected]" git add . git commit -m "Deploy version ${{ github.sha }}" git push

7. Advanced ArgoCD Features

ApplicationSet for Multiple Environments

infrastructure/argocd/applicationset.yaml

apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata: name: java-apps namespace: argocd spec: generators: - list: elements: - cluster: in-cluster url: https://kubernetes.default.svc environment: development branch: develop - cluster: in-cluster url: https://kubernetes.default.svc environment: staging branch: main - cluster: in-cluster url: https://kubernetes.default.svc environment: production branch: main template: metadata: name: 'java-app-{{environment}}' spec: project: default source: repoURL: https://github.com/your-org/gitops-repo.git targetRevision: '{{branch}}' path: apps/java-app/overlays/{{environment}} destination: server: '{{url}}' namespace: '{{environment}}' syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true

Health Checks and Custom Health Assessments

Java Health Check Integration

package com.example.gitops.health; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.stereotype.Component; @Component public class DeploymentHealthIndicator implements HealthIndicator { private final ArgoCDJavaClient argoCDClient; public DeploymentHealthIndicator(ArgoCDJavaClient argoCDClient) { this.argoCDClient = argoCDClient; } @Override public Health health() { try { ArgoCDJavaClient.ApplicationStatus status = argoCDClient.getApplicationStatus("java-app-production"); if ("Healthy".equals(status.getStatus().getHealth()) && "Synced".equals(status.getStatus().getSync())) { return Health.up() .withDetail("application", status.getMetadata().getName()) .withDetail("namespace", status.getMetadata().getNamespace()) .withDetail("status", "deployment_healthy") .build(); } else { return Health.down() .withDetail("application", status.getMetadata().getName()) .withDetail("health", status.getStatus().getHealth()) .withDetail("sync", status.getStatus().getSync()) .build(); } } catch (Exception e) { return Health.down(e).build(); } } }

8. Monitoring and Observability

Custom Metrics for GitOps

package com.example.gitops.metrics; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class GitOpsMetrics { private final Counter deploymentCounter; private final ArgoCDJavaClient argoCDClient; public GitOpsMetrics(MeterRegistry registry, ArgoCDJavaClient argoCDClient) { this.deploymentCounter = Counter.builder("gitops.deployments") .description("Number of deployments triggered") .register(registry); this.argoCDClient = argoCDClient; } @Scheduled(fixedRate = 60000) // Every minute public void trackDeploymentStatus() { try { ArgoCDJavaClient.ApplicationStatus status = argoCDClient.getApplicationStatus("java-app-production"); // Record metrics based on status if ("Healthy".equals(status.getStatus().getHealth())) { deploymentCounter.increment(); } } catch (Exception e) { // Log error but don't fail } } }

Best Practices for Java Apps with ArgoCD

  1. Immutable Deployments - Always use new image tags
  2. Health Checks - Implement comprehensive readiness/liveness probes
  3. Resource Management - Set appropriate resource requests/limits
  4. Configuration Management - Use ConfigMaps and Secrets appropriately
  5. Rollback Strategy - Use Git revert for rollbacks
  6. Security - Implement network policies and security contexts

Benefits for Java Applications

  • Consistent Deployments - Same process across all environments
  • Faster Recovery - Quick rollback through Git history
  • Improved Collaboration - DevOps teams work from single source
  • Audit Trail - Complete deployment history in Git
  • Self-Service - Developers can manage their own deployments

Conclusion

GitOps with ArgoCD provides a robust, declarative approach to deploying Java applications in Kubernetes. By treating everything as code and using Git as the single source of truth, you achieve:

  • Reliability - Consistent, repeatable deployments
  • Visibility - Clear audit trail and change history
  • Automation - Reduced manual intervention
  • Security - Proper access controls and review processes

This approach is particularly well-suited for microservices architectures and cloud-native Java applications, enabling teams to deliver software faster and more reliably.


Next Steps: Implement canary deployments, integrate with service meshes, and explore advanced ArgoCD features like sync waves and hooks for complex deployment scenarios.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper