“Your IDE’s schema validation is only as reliable as the endpoint serving it.”
The Problem: Schema Sprawl
When I audited the yaml-language-server schema references across my home-ops repository, I found chaos:
|
|
678 YAML files with schemas, pulling from six different sources. All external. All outside my control.
The problems:
- Reliability: If any of these pages.dev sites go down, my IDE validation breaks
- Consistency: Different sources have slightly different schema versions
- Staleness: External schemas might not match my actual cluster CRDs
- Version drift: After upgrading a CRD, schemas lag until someone updates them
The solution: extract schemas directly from my cluster and host them myself.
The Architecture
|
|
The workflow:
- Self-hosted runner has
kubectlaccess to the cluster - Daily cron job extracts all CRDs and converts to JSON Schema
- Schemas deploy to Cloudflare Pages at
kubernetes-schemas.nerdz.cloud - All YAML files reference the self-hosted endpoint
Step 1: GitHub App for Actions Runner Controller
The self-hosted runner needs to authenticate to GitHub. Following the onedr0p pattern, I created a GitHub App rather than using a PAT.
Creating the App
-
Go to GitHub Settings → Developer settings → GitHub Apps → New GitHub App
-
Configure:
- Name:
Nerdz-Action Runner - Homepage URL: Your repo URL
- Webhook: Disable (unchecked)
- Permissions:
- Repository: Administration (Read and write), Metadata (Read-only)
- Where can this app be installed?: Only on this account
- Name:
-
After creation, note the App ID
-
Generate a Private Key (downloads a .pem file)
-
Install the app on your repository and note the Installation ID (from the URL)
Storing Credentials in 1Password
The private key is multi-line PEM, which 1Password doesn’t handle well in text fields. The workaround: base64 encode it.
|
|
Store in 1Password under a github-bots item:
ACTIONS_RUNNER_APP_ID: The App IDACTIONS_RUNNER_INSTALLATION_ID: The Installation IDACTIONS_RUNNER_PRIVATE_KEY: Base64-encoded private key
Step 2: Deploy Actions Runner Controller
The controller manages ephemeral runner pods that scale based on workflow demand.
The Controller HelmRelease
|
|
The Runner Scale Set
Each repository gets its own runner scale set. The key insight: the runner needs kubectl access to extract CRDs, so it gets a ServiceAccount with cluster-admin.
|
|
ExternalSecret with Base64 Decode
The private key needs decoding from base64:
|
|
The | b64dec template function handles the base64 decoding.
RBAC for kubectl Access
|
|
Step 3: The Schemas Workflow
The workflow runs daily, extracts CRDs, and deploys to Cloudflare Pages.
|
|
The datreeio/CRDs-catalog crd-extractor script handles the heavy lifting:
- Runs
kubectl get crds -o yaml - Converts OpenAPI v3 schemas to JSON Schema format
- Organizes by API group (
helm.toolkit.fluxcd.io/,external-secrets.io/, etc.)
Step 4: Cloudflare Pages Setup
Cloudflare Pages is deprecated in favor of Workers, but still works for static hosting.
- Create a Pages project named
kubernetes-schemas - Add custom domain
kubernetes-schemas.nerdz.cloud - Add GitHub secrets:
CLOUDFLARE_API_TOKEN(with Pages:Edit permission)CLOUDFLARE_ACCOUNT_ID
The first workflow run populates the site. After that, it updates daily.
The Index Page
I added a styled index.html that makes the schemas browsable:
|
|
The UI shows stats (API groups, schema count, last update) and lets you search/filter.
Step 5: Migrate All YAML Files
With schemas hosted, I migrated 357 files:
|
|
I also added schemas to ~75 files that were missing them entirely.
Schema Version Mismatches
After migration, my IDE showed warnings on many files. The cause: schema URLs didn’t match apiVersions.
|
|
Fixed 60 files with version mismatches:
externalsecret_v1beta1→externalsecret_v1(51 files)clustersecretstore_v1beta1→clustersecretstore_v1(1 file)helmrepository_v1beta2→helmrepository_v1(8 files)
What Didn’t Work: Flux Variable Patterns
One schema validation error I couldn’t fix cleanly:
|
|
The Flux variable ${SECRET_DOMAIN} gets substituted at reconciliation time, but the schema validator sees the literal string and fails the hostname pattern.
Options considered:
- Patch schemas to allow Flux patterns - Over-permissive, masks real errors
- Accept the warnings - Harmless, Flux still works
- Remove schemas from files with variables - Loses validation
I went with option 2. The warnings are cosmetic—kubeconform passes, Flux reconciles correctly, and the IDE just shows a squiggle on variable-heavy files.
The End Result
Before:
- 6 different external schema sources
- No control over availability or freshness
- Schema versions lagging behind CRD upgrades
After:
- Single self-hosted endpoint:
kubernetes-schemas.nerdz.cloud - Schemas extracted daily from actual cluster CRDs
- Browsable index with search
- Full control over the infrastructure
|
|
The schemas update automatically when I upgrade CRDs. No more waiting for upstream schema repos to catch up.
Costs
- Cloudflare Pages: Free tier
- GitHub Actions: Free for public repos (self-hosted runner avoids minute limits anyway)
- Complexity: One more thing to maintain, but it’s fully GitOps-managed
Summary
| Component | Purpose |
|---|---|
| GitHub App | Authentication for self-hosted runner |
| Actions Runner Controller | Manages ephemeral runner pods |
| home-ops-runner | Runner scale set with kubectl access |
| crd-extractor.sh | Converts CRDs to JSON Schema |
| Cloudflare Pages | Hosts schemas at custom domain |
| schemas-index.html | Browsable UI for schema discovery |
The self-hosted runner pattern enables more than just schemas—any workflow that needs cluster access (integration tests, deployments, monitoring) can now run on infrastructure I control.
This post documents work on my home-ops repository. The runner pattern is adapted from onedr0p’s home-ops and the broader Kubernetes@Home community.