blumeops/docs/tutorials/adding-a-service.md
Erich Blume b197bd5f58 Adopt Dagger CI for docs build (Phase 2) (#157)
## Summary

Migrates the docs build pipeline to Dagger (Phase 2 of the Dagger CI adoption plan).

- **Backfill `date-modified` frontmatter** on all 80 docs — Dagger's `--src=.` excludes `.git`, so Quartz can't use git history for page dates. Frontmatter dates work with or without git.
- **New `docs-check-frontmatter` mise task + pre-commit hook** — validates all docs have `title`, `tags`, and `date-modified`
- **New Dagger functions** — `build_changelog` (towncrier in Python container) and `build_docs` (chains changelog → Quartz build in Node container, returns tarball)
- **Simplified CI workflow** — the ~44-line inline Quartz build (clone, npm ci, build, tar, cleanup) is replaced by `dagger call build-docs`. Changelog step remains local on the runner since towncrier needs to modify the host working tree for the git commit.

### Design decisions

- **Towncrier runs twice in CI**: once inside Dagger (for the docs tarball) and once on the runner (for the git commit). This is intentional — Dagger's directory export is additive and can't delete the consumed changelog fragments from the host.
- **Artifact hosting stays on Forgejo Releases** (not migrated to Forgejo Packages as the plan doc originally suggested). That migration can happen independently.
- **`date-modified` frontmatter** preserved even though `build_changelog` installs git — the git there is only for towncrier's `git add` call, not for history. The local iteration story (`dagger call build-docs --src=. --version=dev` with uncommitted changes) depends on frontmatter dates.

### Local iteration

```bash
dagger call build-docs --src=. --version=dev export --path=./docs-dev.tar.gz
tar tf docs-dev.tar.gz | head -20
```

## Deployment and Testing

- [x] `dagger call build-docs --src=. --version=dev` produces valid 1.1MB tarball (149 HTML pages)
- [x] Pre-commit hooks pass (including new `docs-check-frontmatter`)
- [ ] Full `workflow_dispatch` run after merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/157
2026-02-11 16:33:16 -08:00

5.6 KiB

title date-modified tags
Adding a Service 2026-02-07
tutorials
argocd
kubernetes

Adding an ArgoCD-Managed Service

Audiences: Contributor, Replicator

This tutorial walks through deploying a new service to BlumeOps via ArgoCD, including ingress configuration, homepage integration, and observability setup.

Prerequisites

  • Access to the tailscale network
  • kubectl configured with minikube-indri context
  • argocd CLI installed (via Brewfile: brew bundle)

Overview

Adding a service involves:

  1. Creating Kubernetes manifests
  2. Creating an ArgoCD Application
  3. Configuring Tailscale ingress
  4. Adding Homepage dashboard entry
  5. Setting up Grafana dashboards (optional)

Step 1: Create Manifests Directory

Create a directory for your service's Kubernetes manifests:

argocd/manifests/<service-name>/
├── deployment.yaml
├── service.yaml
├── ingress-tailscale.yaml
└── configmap.yaml  # if needed

Example Deployment

# argocd/manifests/myservice/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myservice
  namespace: myservice
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myservice
  template:
    metadata:
      labels:
        app: myservice
    spec:
      containers:
      - name: myservice
        image: registry.ops.eblu.me/myservice:v1.0.0
        ports:
        - containerPort: 8080

Example Service

# argocd/manifests/myservice/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: myservice
  namespace: myservice
spec:
  selector:
    app: myservice
  ports:
  - port: 80
    targetPort: 8080

Step 2: Configure Tailscale Ingress

Create an Ingress to expose the service via Tailscale. See tailscale-operator for details.

# argocd/manifests/myservice/ingress-tailscale.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myservice
  namespace: myservice
spec:
  ingressClassName: tailscale
  rules:
  - host: myservice
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myservice
            port:
              number: 80

This exposes the service at https://myservice.tail8d86e.ts.net.

Step 3: Add Homepage Annotations

Add annotations to the Ingress for automatic Homepage dashboard discovery:

metadata:
  annotations:
    gethomepage.dev/enabled: "true"
    gethomepage.dev/name: "My Service"
    gethomepage.dev/group: "Apps"
    gethomepage.dev/icon: "myservice.png"
    gethomepage.dev/description: "Short description"
    gethomepage.dev/href: "https://myservice.ops.eblu.me"
    gethomepage.dev/pod-selector: "app=myservice"

Icons use Dashboard Icons format.

Step 4: Create ArgoCD Application

Create an Application manifest to tell ArgoCD about your service:

# argocd/apps/myservice.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myservice
  namespace: argocd
spec:
  project: default
  source:
    repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
    targetRevision: main
    path: argocd/manifests/myservice
  destination:
    server: https://kubernetes.default.svc
    namespace: myservice
  syncPolicy:
    syncOptions:
    - CreateNamespace=true

Step 5: Add Caddy Route (Optional)

If the service needs to be accessible from other pods or containers, add a Caddy route in ansible/roles/caddy/defaults/main.yml:

caddy_services:
  # ... existing services ...
  - name: myservice
    upstream: "https://myservice.tail8d86e.ts.net"

Then run mise run provision-indri -- --tags caddy to apply.

This enables access via https://myservice.ops.eblu.me. See routing for details on when this is needed.

Step 6: Deploy

Testing on a Feature Branch

For new services, point ArgoCD at your feature branch first:

# Sync the apps application to pick up your new Application
argocd app sync apps

# Point your app at the feature branch
argocd app set myservice --revision feature/your-branch
argocd app sync myservice

Verify Deployment

kubectl --context=minikube-indri -n myservice get pods
kubectl --context=minikube-indri -n myservice logs -f deployment/myservice

After PR Merge

Reset to main branch:

argocd app set myservice --revision main
argocd app sync myservice

Step 7: Add Observability (Optional)

Prometheus Metrics

If your service exposes Prometheus metrics, add scrape annotations:

# In deployment.yaml pod template
metadata:
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "8080"
    prometheus.io/path: "/metrics"

Grafana Dashboard

Create a ConfigMap in argocd/manifests/grafana-config/dashboards/:

apiVersion: v1
kind: ConfigMap
metadata:
  name: myservice-dashboard
  namespace: monitoring
  labels:
    grafana_dashboard: "1"
  annotations:
    grafana_folder: "Services"
data:
  myservice.json: |
    { ... dashboard JSON ... }

See grafana for dashboard provisioning details.

Checklist

  • Manifests created in argocd/manifests/<service>/
  • ArgoCD Application created in argocd/apps/
  • Tailscale Ingress configured
  • Homepage annotations added
  • Caddy route added (if needed for pod access)
  • Feature branch tested via ArgoCD
  • Metrics/dashboard configured (if applicable)
  • PR created and reviewed
  • Reset to main after merge