GitOps on K3s with ArgoCD

Series: K3s on Raspberry Pi

  1. Build a K3s Cluster on Raspberry Pi
  2. Deploy a Go Service to K3s
  3. GitOps on K3s with ArgoCD ← this article
  4. Multi-Environment K3s with Kustomize

Continuation of Deploy a Go Service to K3s. The service is running - now replace the manual kubectl apply workflow with ArgoCD so every git push deploys automatically.

1. Install ArgoCD

sudo kubectl create namespace argocd
sudo kubectl config set-context --current --namespace=argocd

kubectl apply -n argocd --server-side --force-conflicts \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

Wait until all pods are running:

sudo kubectl get pods -n argocd
# NAME                                               READY   STATUS
# argocd-application-controller-0                    1/1     Running
# argocd-applicationset-controller-b7669f646-wnhn8   1/1     Running
# argocd-dex-server-569b757-phff9                    1/1     Running
# argocd-notifications-controller-58ff87546-dmr7r    1/1     Running
# argocd-redis-b9496d8bf-2w84b                       1/1     Running
# argocd-repo-server-75ffcfc9df-86rnf                1/1     Running
# argocd-server-76755b46f8-792bw                     1/1     Running

2. Fix the Traefik TLS conflict

ArgoCD serves its own HTTPS by default. When Traefik terminates TLS at the ingress and forwards HTTP to ArgoCD, ArgoCD redirects back to HTTPS - creating a redirect loop.

Fix: tell ArgoCD to run in insecure mode so Traefik owns TLS end-to-end.

sudo kubectl -n argocd patch deployment argocd-server \
  --type='json' \
  -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--insecure"}]'

Now create the ingress so the UI is reachable at https://argocd.local:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-ingress
  namespace: argocd
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: 'true'
spec:
  ingressClassName: traefik
  rules:
    - host: argocd.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: argocd-server
                port:
                  number: 80
sudo kubectl apply -f ingress.yaml

Add to your Windows hosts file (C:\Windows\System32\drivers\etc\hosts):

192.168.0.51 argocd.local

Open https://argocd.local (note: https, not http) and get the initial password:

sudo kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d; echo

Login with admin + the password above.

3. Remove manually deployed apps

The REST API was applied with kubectl apply earlier. Delete it - ArgoCD will redeploy it from Git.

sudo kubectl delete -f ingress.yaml -n pi
sudo kubectl delete -f service.yaml -n pi
sudo kubectl delete -f simple-http.yaml -n pi

4. The App-of-Apps pattern

The problem with a plain ArgoCD setup: every new service needs a manual kubectl apply to register it with ArgoCD. That doesn’t scale - and it means your cluster state is only half in Git.

App-of-Apps solves this: one root ArgoCD Application watches a folder in your Git repo. Every YAML file in that folder is itself an ArgoCD Application. Add a file → ArgoCD picks it up and deploys it. No manual cluster commands ever again.

Git repo
└── argocd/
    ├── root-app.yaml        ← apply this ONCE to bootstrap
    └── apps/
        ├── simple-rest-api.yaml   ← ArgoCD App → deploys projects/simple-rest-api/
        └── (new-service.yaml)     ← add a file, get a deployment
└── projects/
    └── simple-rest-api/
        ├── deployment.yaml
        ├── service.yaml
        └── ingress.yaml

The infra repo is at github.com/mk48/k3s-infra.

5. The infra repo structure

k3s-infra/
├── argocd/
│   ├── root-app.yaml          # Bootstrap - the only manual apply
│   ├── ingress.yaml           # ArgoCD UI ingress
│   └── apps/
│       └── simple-rest-api.yaml   # one file per deployed app
└── projects/
    └── simple-rest-api/       # one folder per deployed app
        ├── deployment.yaml
        ├── service.yaml
        └── ingress.yaml

Why this split? argocd/apps/ contains ArgoCD-level objects (what to deploy and where to watch). projects/ contains the actual Kubernetes manifests. Keeping them separate means you can look in argocd/apps/ to see every running app at a glance, and in projects/ to see what each app actually runs.

root-app.yaml

The only file you ever kubectl apply manually. After this, Git drives everything.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/mk48/k3s-infra
    targetRevision: HEAD
    path: argocd/apps # watches this folder
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd # child Applications land in argocd namespace
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
  • path: argocd/apps - ArgoCD treats every YAML in this folder as a child Application to create
  • prune: true - delete resources from the cluster when you delete them from Git
  • selfHeal: true - revert manual kubectl changes so Git is always the source of truth

argocd/apps/simple-rest-api.yaml

One file per app. This is how ArgoCD learns about a new service - no kubectl needed.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: simple-rest-api
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io # cleans up on delete
spec:
  project: default
  source:
    repoURL: https://github.com/mk48/k3s-infra
    targetRevision: HEAD
    path: projects/simple-rest-api
  destination:
    server: https://kubernetes.default.svc
    namespace: pi
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
  • path: projects/simple-rest-api - ArgoCD syncs all manifests in this folder to the cluster
  • CreateNamespace=true - ArgoCD creates the pi namespace if it doesn’t exist
  • finalizers - when you delete this Application from ArgoCD, it also removes the Deployment, Service, and Ingress from the cluster

projects/simple-rest-api/

The actual Kubernetes manifests. ArgoCD watches this folder and applies any changes automatically.

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-world
  namespace: pi
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-world
  template:
    metadata:
      labels:
        app: hello-world
    spec:
      containers:
        - name: hello-world
          image: ghcr.io/mk48/simple-http:latest
          imagePullPolicy: Always
          resources:
            requests:
              memory: '12Mi'
              cpu: '2m'
          ports:
            - containerPort: 8080
              name: web
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: web
            initialDelaySeconds: 3
            periodSeconds: 30

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: hello-world-service
  namespace: pi
spec:
  selector:
    app: hello-world
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 8080

ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hello-world-ingress
  namespace: pi
spec:
  ingressClassName: traefik
  rules:
    - host: hello.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: hello-world-service
                port:
                  number: 80

6. Bootstrap

Copy argocd/root-app.yaml from the repo to pi01 and apply it - this is the only manual step:

sudo kubectl apply -f root-app.yaml -n argocd

ArgoCD picks up the root app, scans argocd/apps/, finds simple-rest-api.yaml, and deploys everything in projects/simple-rest-api/ automatically.

sudo kubectl get applications -n argocd
# NAME              SYNC STATUS   HEALTH STATUS
# root              Synced        Healthy
# simple-rest-api   Synced        Healthy

ArgoCD all apps Simple rest app on ArgoCD

7. Adding a new app

To deploy a new service, no kubectl commands required:

  1. Add your manifests to projects/new-service/
  2. Add an Application file argocd/apps/new-service.yaml (copy simple-rest-api.yaml, change name + path)
  3. git push

ArgoCD detects the new file in argocd/apps/ within seconds and starts syncing projects/new-service/ to the cluster.