Modern CI/CD for Microservices: Jenkins, ArgoCD and the GitOps Journey
Managing a few services is easy. Managing 10+ microservices (like in our YAS - Yet Another Shop project) with independent life cycles, different testing requirements, and security standards is a whole different ball game.
In this post, I will walk you through the automated workflow we built to handle everything from code push to production deployment on Kubernetes.
YAS CI/CD Workflow
1. The Architecture Overview
Our workflow follows the GitOps principle: the state of the infrastructure and applications is stored in Git. We use a "Pull-based" deployment model where Kubernetes stays in sync with our repository.
graph LR
subgraph "Source Control"
GH[GitHub Repo]
end
subgraph "Continuous Integration (CI)"
JNK[Jenkins Pipeline]
MVN[Maven Build/Test]
SEC[Security Scan]
DKR[Docker Build]
REG[[Docker Registry]]
end
subgraph "Continuous Delivery (CD)"
GOPS[GitOps Repo - Helm/K8s]
ARGO[ArgoCD]
K8S[Kubernetes Cluster]
end
%% Workflow
GH -->|Webhook Trigger| JNK
JNK --> MVN
MVN --> SEC
SEC --> DKR
DKR -->|Push Image| REG
JNK -->|Update Tag| GOPS
GOPS -->|Auto Sync| ARGO
ARGO -->|Deploy/Rollout| K8S
K8S -->|Pull Image| REG
2. Step-by-Step Workflow
Step 1: Code Push & CI Trigger
Whenever a developer pushes code to the yas repository, GitHub sends a Webhook to our Jenkins instance (running on a public VPS). Jenkins identifies the branch and starts the corresponding Multibranch Pipeline.
Step 2: Jenkins Pipeline (The "Heavy Lifting")
The Jenkinsfile in each service handles the following stages:
- Build & Test: Running
mvn clean packageto ensure the Java code is valid and all Unit/Integration tests pass. - Security Scan: Integrated DevSecOps tool (like SonarQube or Trivy) to check for vulnerabilities.
- Docker Build (Optimized): This is where we shine! Using Multi-stage builds to reduce image size and Layer Caching to speed up builds by up to 70%.
- Base image: Alpine Linux (for security and size).
- Build stage: JDK 21.
- Runtime stage: JRE 21 (Non-root user).
- Push to Registry: The final hardened image is pushed to our Docker Registry with a unique tag (e.g.,
v1.0.0-build.45).
Step 3: GitOps Update
After a successful build, Jenkins doesn't deploy directly. Instead, it updates the image tag in our dedicated GitOps repository (which contains Helm Charts or K8s manifests).
# Example command in Jenkins stage
sed -i "s/tag: .*/tag: ${BUILD_TAG}/g" ./charts/product-service/values.yaml
git commit -am "chore: update product-service image to ${BUILD_TAG}"
git push origin main
Step 4: ArgoCD Sync (Continuous Delivery)
ArgoCD is installed inside our Kubernetes cluster on the Proxmox lab. It constantly monitors the GitOps repository.
- Detection: ArgoCD sees the new commit from Jenkins.
- Comparison: It identifies that the "Desired State" (in Git) is different from the "Live State" (in K8S).
- Synchronization: ArgoCD automatically triggers a rolling update in the cluster.
Step 5: Kubernetes Rollout
Kubernetes nodes pull the new image from the Registry and perform a Zero-downtime deployment. If something goes wrong, we can simply revert the commit in Git, and ArgoCD will roll back the app instantly.
3. Why This Workflow Matters?
- Developer Independence: Each service (Cart, Order, Inventory) can be deployed separately without affecting others.
- Traceability: Every change in production is linked to a specific Git commit.
- Security: By using ArgoCD, we don't need to give Jenkins full access (
kubeconfig) to our cluster. Jenkins only needs permission to push to the GitOps repo. - Performance: Optimized Dockerfiles ensure that even with 10+ services, our build and deploy time remains minimal.
Next Steps
We are currently working on integrating Observability (PLG Stack) to monitor these deployments in real-time. Stay tuned for the next post where I'll talk about how we use Prometheus and Grafana to visualize our microservices' health!
About the author: Ngô Tấn Tài (newnol) is a 3rd-year Computer Networking student at HCMUS, specializing in Cloud Native and DevSecOps.
