
This post records how I installed ArgoCD into a k3s cluster, created an ArgoCD Application, and switched the whole CD flow from "GitHub Actions directly running kubectl apply" to "git is the source of truth and ArgoCD performs sync."

## Control Flow Shift

```mermaid
flowchart TD
    subgraph Push-based
        P1[Developer push] --> P2[GitHub Actions]
        P2 -->|kubectl apply + K8s credentials| P3[K8s Cluster]
    end

    subgraph Pull-based GitOps
        G1[Developer push] --> G2[GitHub Actions]
        G2 -->|push image + update YAML| G3[Git Repo]
        G3 -->|ArgoCD polls every 3min| G4[ArgoCD in Cluster]
        G4 -->|kubectl apply internal| G5[K8s Cluster]
    end
```

| | Push-based | Pull-based (ArgoCD) |
|---|---|---|
| CI needs K8s permission | yes | no |
| Drift detection | none | automatic detect and repair |
| Rollback | manually run old workflow | one-click in UI or git revert |
| Visibility | CI log | complete sync status in ArgoCD UI |

The root issue in push-based CD is a blurry security boundary: the CI runner holds K8s credentials, and if those credentials leak, an attacker can operate the cluster directly. Pull-based keeps control inside the cluster, and CI only needs permission to write git.

## Internal Control Plane

```mermaid
flowchart LR
    Git["Git Repo<br/>k8s/base/"] -->|clone + render| RS[argocd-repo-server]
    RS -->|desired state| AC[argocd-application-controller]
    AC <-->|compare| K8s["k3s Cluster<br/>actual state"]
    AC -->|diff found: sync| K8s
    Redis["argocd-redis<br/>cache"] --- AC
    Server["argocd-server<br/>UI + API"] --- AC
```

Responsibilities of each component:

| Component | Responsibility |
|---|---|
| `argocd-server` | UI + API, operation entry point |
| `argocd-repo-server` | clone git repo, render YAML (Helm/Kustomize/plain) |
| `argocd-application-controller` | core loop, compares desired vs actual state every 3 minutes |
| `argocd-applicationset-controller` | batch management for multiple Applications |
| `argocd-dex-server` | SSO authentication (GitHub OAuth, LDAP) |
| `argocd-redis` | cache application state to accelerate comparisons |
| `argocd-notifications-controller` | send notifications (Slack, email) |

Render YAML means ArgoCD expands Helm templates or Kustomize overlays into valid Kubernetes YAML. Plain YAML does not need render; what you see is what gets applied.

## Initial Setup Steps

```bash
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
kubectl get pods -n argocd -w
```

After all pods are Running, access UI:

```bash
kubectl port-forward svc/argocd-server -n argocd 8080:443

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

Open `https://localhost:8080`, in browser, account `admin`.

## Repo Directory Boundaries

The monitored path in ArgoCD should contain only YAML that ArgoCD is supposed to manage. If test resources and production resources are mixed in the same directory, ArgoCD cannot distinguish them:

```text
k8s/
  base/            <- ArgoCD managed
    deployment.yaml
    service.yaml
    configmap.yaml
    hpa.yaml
    secret.yaml
  argocd/          <- Application manifest
    application.yaml
  test/            <- excluded from ArgoCD
    test-namespace.yaml
    test-liveness.yaml
    test-probe.yaml
```

`base/` is the source of truth for ArgoCD. `test/` is completely excluded from the GitOps flow.

## Declarative App Registration

ArgoCD Application should live in git, not only created from UI. UI-only config has no version control and violates GitOps principles:

```yaml
# k8s/argocd/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: go-api
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/your-repo
    targetRevision: HEAD
    path: k8s/base
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
```

| Field | Meaning |
|---|---|
| `namespace: argocd` | the Application object itself lives in argocd namespace |
| `project: default` | ArgoCD Project, used for RBAC and resource isolation |
| `targetRevision: HEAD` | always tracks latest commit |
| `path: k8s/base` | monitor all YAML under this directory |
| `server: kubernetes.default.svc` | the same cluster where ArgoCD runs |
| `automated.prune: true` | resources deleted in git are also deleted in cluster |
| `automated.selfHeal: true` | if someone changes cluster manually, ArgoCD restores git state |

```bash
kubectl apply -f k8s/argocd/application.yaml
```

If the Application already exists from UI, `kubectl apply` updates the existing object and YAML becomes the source. After that, UI is used for observation only.

## Credential Secret Wiring for Private Images

k3s is a local cluster and has no GCP Workload Identity. It needs explicit credentials to pull images from a private registry.

Create a non-expiring imagePullSecret from a Service Account key:

```bash
gcloud iam service-accounts keys create /tmp/ar-pull-key.json \
  --iam-account=YOUR_SA@YOUR_PROJECT.iam.gserviceaccount.com

kubectl create secret docker-registry ar-secret \
  --docker-server=us-east1-docker.pkg.dev \
  --docker-username=_json_key \
  --docker-password="$(cat /tmp/ar-pull-key.json)" \
  --namespace=default

rm /tmp/ar-pull-key.json
```

Delete SA key right after creation — the full content is already in Kubernetes Secret, and keeping it on local disk is unnecessary risk.

Add this into `deployment.yaml`, including `imagePullSecrets`:

```yaml
spec:
  imagePullSecrets:
    - name: ar-secret
  containers:
    - name: go-api
      image: us-east1-docker.pkg.dev/YOUR_PROJECT/go-api/go-api:prod-7639a24
```

GKE does not need this because nodes can pull Artifact Registry through Workload Identity. `ar-secret` is a k3s local-environment constraint.

## Deployment Timeline Loop

```mermaid
sequenceDiagram
    participant Dev as Developer
    participant GH as GitHub Actions
    participant AR as Artifact Registry
    participant Git as Git Repo
    participant Argo as ArgoCD
    participant K8s as k3s Cluster

    Dev->>GH: push to main
    GH->>GH: go test + go build
    GH->>AR: docker push prod-{sha}
    GH->>Git: update deployment.yaml image tag
    GH->>Git: push gitops/update-image-{sha} branch
    Dev->>Git: merge PR to main
    loop every 3 minutes
        Argo->>Git: poll for changes
        Git-->>Argo: deployment.yaml changed
    end
    Argo->>K8s: kubectl apply k8s/base/
    K8s->>K8s: rolling update
```

Why CI should not push main directly: main has branch protection rules and requires PRs. CI pushes to a gitops branch to bypass that safely while keeping code review flow.

Drift detection: if someone manually runs `kubectl edit deployment go-api` and changes replicas, ArgoCD selfHeal automatically restores the value from git on the next poll. Cluster state is always decided by git.

## Local and Managed Runtime Notes

| | k3s (local) | GKE |
|---|---|---|
| imagePullSecret | must create manually | not needed, node has Workload Identity |
| Load Balancer | none, need NodePort | native L4/L7 LB support |
| Workload Identity | unsupported | natively supported |
| Cost | free | e2-small ~$38/80 days |

k3s is good for local learning; GKE is closer to production. The next phase will run Prometheus + Grafana on GKE and remove `ar-secret` constraints.

## Further Reading Links

- [ArgoCD Getting Started](https://argo-cd.readthedocs.io/en/stable/getting_started/)
- [ArgoCD Application Specification](https://argo-cd.readthedocs.io/en/stable/operator-manual/application.yaml/)
- [ArgoCD Auto Sync](https://argo-cd.readthedocs.io/en/stable/user-guide/auto_sync/)
- [GitOps Principles](https://opengitops.dev/)
- [GCP Workload Identity for GKE](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity)
- [Kubernetes imagePullSecrets](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/)
- [Artifact Registry Authentication](https://cloud.google.com/artifact-registry/docs/docker/authentication)
