Guides

Creating Your First App

A complete walkthrough — from empty cluster to running application

Creating Your First App

This tutorial walks you through deploying a real application with Wardn — from an empty cluster to a running, healthy application with automated sync.

Needs a Kubernetes cluster. The App CR + operator only run against a real cluster — use docker-compose.k8s.yml (k3s) from Getting Started before starting this tutorial.

What You'll Build

A simple web application with:

  • A Deployment running 3 replicas
  • A Service exposing it internally
  • A ConfigMap with application configuration
  • Automated sync with pruning enabled

Prerequisites

  • Wardn server + operator running (Getting Started)
  • kubectl configured against the cluster (k3s.yaml if using the compose layer)
  • A Git repository you can push to — public for now, since the operator's clone has no credential support

Tutorial

Step 1: Create the Application Repo

Create a new Git repository with the following structure:

my-first-app/
└── manifests/
    ├── deployment.yaml
    ├── service.yaml
    └── configmap.yaml

manifests/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-first-app
  labels:
    app: my-first-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-first-app
  template:
    metadata:
      labels:
        app: my-first-app
    spec:
      containers:
        - name: web
          image: nginx:1.25
          ports:
            - containerPort: 80
          envFrom:
            - configMapRef:
                name: my-first-app-config

manifests/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-first-app
spec:
  selector:
    app: my-first-app
  ports:
    - port: 80
      targetPort: 80

manifests/configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-first-app-config
data:
  APP_ENV: "production"
  LOG_LEVEL: "info"

Step 2: Push to Git

cd my-first-app
git init
git add .
git commit -m "Initial manifests"
git remote add origin https://github.com/yourorg/my-first-app.git
git push -u origin main

Using the bundled Gitea instead of GitHub:

docker compose -f docker-compose.k8s.yml exec gitea \
  gitea admin user create --username wardn --password wardn1234 --email dev@wardn.local --admin
# Create a repo in the Gitea UI at http://localhost:3000's gitea port, then
git remote add origin http://localhost:3000/wardn/my-first-app.git

Step 3: Create the App

Create app.yaml:

apiVersion: cd.wardn.space/v1alpha1
kind: App
metadata:
  name: my-first-app
  namespace: org-my-org          # the org's management namespace
spec:
  source:
    type: Git
    git:
      repoURL: https://github.com/yourorg/my-first-app
      branch: main
      path: manifests
      manifestType: Auto
  destination:
    namespace: default
  syncPolicy:
    automated: true
    prune: true

Apply it:

kubectl --kubeconfig k3s.yaml apply -f app.yaml

Step 4: Watch the Sync

kubectl --kubeconfig k3s.yaml get app my-first-app -n org-my-org -w

Within ~60 seconds (or immediately if a webhook is wired), you should see:

NAME            SYNC STATUS   HEALTH
my-first-app    OutOfSync     Unknown
my-first-app    Synced        Healthy

Step 5: Verify the Deployment

kubectl --kubeconfig k3s.yaml get pods -n default -l app=my-first-app
kubectl --kubeconfig k3s.yaml get svc -n default my-first-app

Expected output:

NAME                          READY   STATUS    RESTARTS   AGE
my-first-app-7d6f8c5b-x2k9m   1/1     Running   0          30s
my-first-app-7d6f8c5b-p3rlq   1/1     Running   0          30s
my-first-app-7d6f8c5b-n8w2x   1/1     Running   0          30s

Step 6: Deploy a Change

Update the image tag in manifests/deployment.yaml:

image: nginx:1.26

Commit and push:

git add manifests/deployment.yaml
git commit -m "Upgrade nginx to 1.26"
git push

With automated: true, the next reconcile (≤ ~60s) picks up the new sha and applies it:

kubectl --kubeconfig k3s.yaml get app my-first-app -n org-my-org -w

Step 7: Trigger a Manual Sync (optional)

curl -X POST http://localhost:8080/api/v1/apps/default/my-first-app/sync -b cookies.txt
# {"message":"sync triggered","name":"my-first-app","operation_id":"…"}

What Just Happened?

  1. You pushed to Git — the single source of truth changed
  2. The operator detected the new revision — on its ~60s reconcile (or instantly via webhook)
  3. Manifests were rendered — .yaml files under path parsed to objects
  4. Server-side apply — objects applied with field manager wardn-operator
  5. Health checks ran — Deployment readiness drives healthStatus
  6. Status updated — syncStatus/revision/objectHash written back to the App CR

About Self-Heal

The syncPolicy.selfHeal field exists in the schema but isn't wired in the reconciler yet. If you kubectl scale the deployment down, Wardn won't restore it until the next upstream change (or a manual sync). This is one of the biggest gaps vs ArgoCD — track it on the Roadmap.

Troubleshooting

Sync Status is Unknown / OutOfSync forever

Check the operator logs:

kubectl --kubeconfig k3s.yaml logs -n <operator-namespace> deployment/wardn-operator --tail=50

Common causes:

  • Repo URL unreachable or private (no credential support — public repos only)
  • Branch doesn't exist (main vs master)
  • path doesn't contain any manifests
  • manifestType: Helm / HelmChart/Catalog source — not implemented, the reconcile fails by design

Health Status is Degraded or stuck Progressing

Check per-resource status:

kubectl --kubeconfig k3s.yaml get app my-first-app -n org-my-org \
  -o jsonpath='{.status.resources}' | jq

Common causes:

  • Image pull errors (wrong name, missing pullSecretRef)
  • Crash loops (kubectl logs on the pod)
  • readyReplicas not reaching spec.replicas within the Deployment's progress deadline

Sync Doesn't Trigger

  • Verify syncPolicy.automated is true (or trigger manually via POST /sync)
  • Check spec.freeze isn't true
  • Check the operator is running and can reach the source cluster

Next Steps

On this page