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
- Declarative - Everything described as code
- Versioned - All changes tracked in Git
- Automated - ArgoCD automatically syncs changes
- Auditable - Complete deployment history
- 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
- Immutable Deployments - Always use new image tags
- Health Checks - Implement comprehensive readiness/liveness probes
- Resource Management - Set appropriate resource requests/limits
- Configuration Management - Use ConfigMaps and Secrets appropriately
- Rollback Strategy - Use Git revert for rollbacks
- 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.