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