Cloud Native 21 min read

Why I Dropped Jenkins for GitHub Actions & ArgoCD: A Complete GitOps Migration Guide

After years of using Jenkins, the author explains why moving to a GitOps workflow with GitHub Actions for CI and ArgoCD for CD offers lower maintenance, tighter integration with Kubernetes, declarative configurations, and automated deployments, and provides a step‑by‑step guide covering environment requirements, repository layout, CI pipeline, ArgoCD application setup, multi‑environment strategies, secret management, RBAC, monitoring, troubleshooting, and migration best practices.

Ops Community
Ops Community
Ops Community
Why I Dropped Jenkins for GitHub Actions & ArgoCD: A Complete GitOps Migration Guide

Overview

Jenkins was once the de‑facto CI/CD tool, but as codebases migrated to GitHub and Kubernetes became the standard deployment platform, the author found Jenkins increasingly costly to maintain and poorly aligned with cloud‑native practices. The article explains the decision to replace Jenkins with a GitOps stack built around GitHub Actions for CI and ArgoCD for CD.

What is GitOps?

GitOps treats a Git repository as the single source of truth . Instead of a push‑based CI/CD flow that directly applies manifests, a pull‑based model stores all declarative configuration in Git. ArgoCD continuously watches the repository and reconciles the cluster state to match the desired state defined in Git, providing built‑in auditability.

Technology Stack Selection

CI : GitHub Actions (build, test, image push)

CD : ArgoCD (application deployment, state sync)

Image Registry : Harbor or GitHub Container Registry (ghcr.io)

Configuration Management : Kustomize or Helm

Secret Management : Sealed Secrets or External Secrets

This combination leverages native GitHub integration, clear separation of responsibilities, and strong Kubernetes support.

Environment Requirements

Kubernetes 1.20+ (tested on 1.27)

kubectl with cluster access

GitHub repository with Actions permissions

Accessible container image registry

Step‑by‑Step Implementation

2.1 Install and Configure ArgoCD

# Create namespace
kubectl create namespace argocd

# Install HA version
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/ha/install.yaml

# Wait for all Pods to be ready
kubectl wait --for=condition=Ready pods --all -n argocd --timeout=300s

# Retrieve initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

# Expose service (testing)
kubectl port-forward svc/argocd-server -n argocd 8080:443

2.2 Repository Structure Design

Separate the application code repository from the deployment configuration repository to avoid triggering full CI pipelines on config changes.

# Application code repo (app-repo)
app-repo/
├── src/
├── tests/
├── Dockerfile
├── .github/workflows/ci.yaml
└── README.md

# Deployment config repo (deploy-repo)
deploy-repo/
├── apps/
│   ├── frontend/
│   │   ├── base/
│   │   └── overlays/
│   └── backend/
├── argocd/applications/
└── README.md

2.3 GitHub Actions CI Pipeline

name: CI Pipeline
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.21'
          cache: true
      - name: Run tests
        run: |
          go test -v -race -coverprofile=coverage.out ./...
          go tool cover -func=coverage.out
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage.out

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'push'
    steps:
      - uses: actions/checkout@v4
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=
            type=ref,event=branch
            type=semver,pattern={{version}}
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64

  update-manifest:
    needs: build-and-push
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Checkout deploy repo
        uses: actions/checkout@v4
        with:
          repository: myorg/deploy-repo
          token: ${{ secrets.DEPLOY_REPO_TOKEN }}
          path: deploy-repo
      - name: Update image tag
        run: |
          cd deploy-repo/apps/myapp/overlays/staging
          kustomize edit set image myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }}
      - name: Commit and push
        run: |
          cd deploy-repo
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add .
          git commit -m "chore: update myapp image to ${{ needs.build-and-push.outputs.image_tag }}"
          git push

2.4 ArgoCD Application Definition

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp-staging
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/deploy-repo.git
    targetRevision: main
    path: apps/myapp/overlays/staging
  destination:
    server: https://kubernetes.default.svc
    namespace: myapp-staging
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

The syncPolicy enables automatic synchronization, pruning of removed resources, self‑healing of drift, and retry logic for transient failures.

2.5 Multi‑Environment Deployment with Kustomize

# Base deployment (apps/myapp/base/deployment.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myapp:latest
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 512Mi
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5

# Production overlay (apps/myapp/overlays/prod/kustomization.yaml)
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
namespace: myapp-prod
replicas:
  - name: myapp
    count: 3
patches:
  - patch: |-
      - op: replace
        path: /spec/template/spec/containers/0/resources/requests/cpu
        value: 500m
      - op: replace
        path: /spec/template/spec/containers/0/resources/requests/memory
        value: 512Mi
      - op: replace
        path: /spec/template/spec/containers/0/resources/limits/cpu
        value: 2000m
      - op: replace
        path: /spec/template/spec/containers/0/resources/limits/memory
        value: 2Gi
    target:
      kind: Deployment
      name: myapp
images:
  - name: myapp
    newName: ghcr.io/myorg/myapp
    newTag: v1.2.3

2.6 Rollback Strategies

Three rollback options are provided:

Use the ArgoCD UI history view to sync to a previous revision.

Run argocd app rollback myapp-prod from the CLI.

Perform a Git revert (recommended) to keep a full history, then push.

2.7 Canary Releases with Argo Rollouts

# Install Argo Rollouts
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml

# Convert Deployment to Rollout
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: myapp
spec:
  replicas: 5
  strategy:
    canary:
      steps:
        - setWeight: 20
        - pause: {duration: 5m}
        - setWeight: 40
        - pause: {duration: 5m}
        - setWeight: 60
        - pause: {duration: 5m}
        - setWeight: 80
        - pause: {duration: 5m}
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: ghcr.io/myorg/myapp:v1.2.3
          ports:
            - containerPort: 8080

Best Practices and Caveats

Secret Management

Two approaches are highlighted:

Sealed Secrets : Encrypt secrets with a cluster‑side key; only the cluster can decrypt.

External Secrets : Sync secrets from external stores such as AWS Secrets Manager or HashiCorp Vault.

# Install Sealed Secrets controller
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml

# Encrypt a secret
kubeseal --format=yaml < secret.yaml > sealed-secret.yaml

RBAC Configuration

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: argocd
data:
  policy.csv: |
    # Developers can view all apps and sync non‑prod environments
    p, role:developer, applications, get, */*, allow
    p, role:developer, applications, sync, */*-dev, allow
    p, role:developer, applications, sync, */*-staging, allow
    # SREs have full access
    p, role:sre, applications, *, */*, allow
    p, role:sre, clusters, *, *, allow
  # Bind GitHub teams to roles
  g, myorg:developers, role:developer
  g, myorg:sre-team, role:sre
  policy.default: role:readonly

Performance Tuning

ArgoCD polls Git every 3 minutes by default; for high‑frequency changes, enable a GitHub webhook.

# Webhook configuration (GitHub Settings → Webhooks)
# URL: https://argocd.example.com/api/webhook
# Content type: application/json
# Secret stored in argocd-secret

apiVersion: v1
kind: Secret
metadata:
  name: argocd-secret
  namespace: argocd
stringData:
  webhook.github.secret: your-webhook-secret

Common Issues and Fixes

Application stuck OutOfSync : Verify the latest commit, check webhook delivery, or manually refresh with argocd app get myapp --refresh. Ignoring replica count differences can also help.

Sync failure due to existing resources : Add the missing app.kubernetes.io/instance label so ArgoCD can adopt the resource.

Image pull errors : Ensure imagePullSecrets reference a secret that contains credentials for the registry.

Observability

ArgoCD exposes Prometheus metrics. Example alert rules:

# Alert when an application is out of sync for >15m
alert: ArgocdAppOutOfSync
expr: argocd_app_info{sync_status!="Synced"} == 1
for: 15m
labels:
  severity: warning
annotations:
  summary: "Application {{ $labels.name }} is out of sync"

# Alert when health status is degraded for >5m
alert: ArgocdAppHealthDegraded
expr: argocd_app_info{health_status!="Healthy"} == 1
for: 5m
labels:
  severity: critical
annotations:
  summary: "Application {{ $labels.name }} health is degraded"

Jenkins vs. GitHub Actions + ArgoCD Comparison

Maintenance Cost : Jenkins requires dedicated servers and plugins; the GitHub‑ArgoCD combo is SaaS‑based with near‑zero ops.

Configuration Model : Jenkins mixes UI and Groovy scripts; GitHub Actions + ArgoCD are fully declarative YAML.

Kubernetes Integration : Jenkins needs plugins; ArgoCD natively syncs with Kubernetes.

Extensibility : Jenkins offers many plugins but can cause version conflicts; GitHub Actions allow custom actions and flexible composition.

Auditability : Jenkins needs extra setup; Git history provides complete audit trails.

Learning Curve : Jenkins Groovy is steep; GitHub Actions YAML is more approachable.

Cost : Self‑hosted Jenkins incurs hardware costs; GitHub Actions has generous free tiers.

Conclusion and Migration Advice

For teams already operating in Kubernetes, the GitHub Actions + ArgoCD stack offers a modern, cloud‑native CI/CD pipeline with clear separation of concerns, built‑in auditability, and easier maintenance. Migration can be incremental: start with GitHub Actions for CI on new projects, then gradually replace Jenkins‑driven pipelines in existing projects, and finally adopt ArgoCD for CD.

References

ArgoCD official documentation

GitHub Actions documentation

GitOps principles – Weaveworks

Argo Rollouts progressive delivery guide

CI/CDKubernetesDevOpsGitOpsGitHub ActionsArgoCD
Ops Community
Written by

Ops Community

A leading IT operations community where professionals share and grow together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.