Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Part 13: The Eight Sub-DSLs HomeLab Needs

"You don't need every Ops.Dsl sub-DSL. You need the eight that cover the verbs of running a homelab."


Why

The Ops DSL Ecosystem series defines 22 sub-DSLs. Some of them are clearly about cloud-scale concerns: Ops.LoadTesting is for k6 distributed across cloud VMs; Ops.SupplyChain is for SBOM and license auditing; Ops.Capacity is for autoscaling and KEDA. These are valuable, but they are not what a homelab needs. A homelab does not autoscale. A homelab does not run distributed load tests. A homelab does not have a supply chain in the SOC2 sense.

A homelab does have:

  • Infrastructure (containers, certs, DNS records, storage)
  • Deployment (the order in which to start services)
  • Observability (health checks, metrics, alerts)
  • Configuration (env files, secrets, per-environment overrides)
  • Resilience (retries, circuit breakers, restart policies)
  • Security (RBAC for the GitLab admin, secret rotation)
  • Networking (Traefik routing, firewall rules between VMs)
  • Data governance (backups, retention, restore tests)

Eight sub-DSLs. Each one maps to a specific set of HomeLab features. This part is the index: which attributes, which generated artifacts, which HomeLab parts later in the series build on each one. By the end, you should know exactly which Ops.Dsl package to import when you want to declare a new operational concern in HomeLab.


1. Ops.Infrastructure — the nouns

Reference: Ops DSL Ecosystem — Part 15.

Attributes used by HomeLab:

[ContainerSpec(Image = "gitlab/gitlab-ce:16.4.2-ce.0", Cpu = "2", Memory = "4Gi")]
[StorageSpec(Mount = "/var/opt/gitlab", Capacity = "20Gi", Persistent = true)]
[CertificateSpec(Domain = "gitlab.frenchexdev.lab", SanList = new[] { "gitlab.frenchexdev.lab" })]
[DnsRecord(Name = "gitlab", Type = "A", Value = "192.168.56.11")]
public sealed class GitLabService { }

HomeLab generates:

  • Compose services.gitlab.image, cpus, mem_limit, volumes
  • TLS cert via the ITlsCertificateProvider plugin (native or mkcert)
  • DNS entry via the IDnsProvider plugin (hosts file or PiHole or Cloudflare)
  • A typed OpsTarget for the Verify stage

Where in the series: Parts 31, 33, 34, 35, 38.


2. Ops.Deployment — the verbs of starting things

Reference: Ops DSL Ecosystem — Part 5.

Attributes:

[DeploymentOrchestrator("devlab")]
public sealed class DevLabDeployment { }

[DeploymentApp(Name = "postgres", Image = "postgres:16-alpine")]
[DeploymentDependency(typeof(PostgresApp))]                       // ← gitlab depends on postgres
public sealed class GitLabApp { }

[DeploymentApp(Name = "gitlab", Image = "gitlab/gitlab-ce:16.4.2-ce.0")]
[DeploymentDependency(typeof(GitLabApp))]                          // ← runner depends on gitlab
public sealed class GitLabRunnerApp { }

HomeLab generates:

  • A topologically-sorted DAG of services that the Apply stage executes in order
  • Health-check waits between dependent services (postgres must be ready before gitlab starts)
  • Compose depends_on clauses (which docker-compose v2 respects with condition: service_healthy)
  • A homelab plan output that shows the DAG as a Mermaid graph

Where in the series: Parts 07, 37, 39.


3. Ops.Observability — the verbs of watching things

Reference: Ops DSL Ecosystem — Part 7.

Attributes:

[HealthCheck(Path = "/api/v4/version", Method = "GET", IntervalSeconds = 30, TimeoutSeconds = 5)]
[Metric(Name = "gitlab_http_requests_total", Type = "counter")]
[Metric(Name = "gitlab_http_request_duration_seconds", Type = "histogram")]
[AlertRule(
    Name = "GitLabDown",
    Expression = "up{job=\"gitlab\"} == 0",
    For = "2m",
    Severity = "critical")]
[Dashboard("GitLab Service")]
public sealed class GitLabObservability { }

HomeLab generates:

  • ASP.NET-style health-check probes the Verify stage runs (in this case, just a curl against the path)
  • Prometheus scrape config that the observability stack picks up
  • Prometheus alert rules in alerts.yaml (mounted into the Alertmanager container)
  • A Grafana dashboard JSON saved to data/grafana/dashboards/gitlab.json

Where in the series: Parts 13 (this part), 44, 49.


4. Ops.Configuration — the verbs of providing settings

Reference: Ops DSL Ecosystem — Part 8.

Attributes:

[ConfigTransform(Source = "gitlab.rb.template", Target = "/etc/gitlab/gitlab.rb")]
[Secret(Name = "GITLAB_ROOT_PASSWORD", Source = "bitwarden://devlab/gitlab-root")]
[Secret(Name = "POSTGRES_PASSWORD",   Source = "bitwarden://devlab/postgres")]
[EnvironmentMatrix(
    Environments = new[] { "single", "multi", "ha" },
    Overrides = "config-overrides.yaml")]
public sealed class DevLabConfiguration { }

HomeLab generates:

  • The gitlab.rb file by templating the gitlab.rb.template with values from the typed config
  • An env file (compose.env) for docker-compose, with secret values injected at apply time
  • The cross-environment matrix evaluation: which values change between single, multi, and ha

Where in the series: Parts 04, 05, 43.


5. Ops.Resilience — the verbs of recovering from failure

Reference: Ops DSL Ecosystem — Part 8.

Attributes:

[CircuitBreaker(FailureThreshold = 5, CooldownSeconds = 30)]
[RetryPolicy(MaxAttempts = 3, BackoffSeconds = 5)]
[Bulkhead(MaxConcurrent = 10)]
[RollbackPlan(Strategy = "compose-down-restore-volume")]
public sealed class GitLabResilience { }

HomeLab generates:

  • [InjectableDecorator] chains around the binary wrappers (so _packer.BuildAsync is wrapped in retry + circuit-breaker)
  • A homelab rollback verb that runs the rollback plan
  • Saga compensation steps (see FrenchExDev.Net.Saga from Part 11)

Where in the series: Parts 09, 11, 49.


6. Ops.Security — the verbs of access and rotation

Reference: Ops DSL Ecosystem — Part 12.

Attributes:

[RbacRule(Subject = "ops-team", Action = "deploy", Resource = "devlab")]
[RbacRule(Subject = "ops-team", Action = "backup",  Resource = "devlab/data")]
[SecretRotation(Secret = "GITLAB_ROOT_PASSWORD", IntervalDays = 90)]
[VulnerabilityScan(Target = "gitlab/gitlab-ce", Tool = "trivy", FailOn = "high")]
public sealed class DevLabSecurity { }

HomeLab generates:

  • A homelab access list that prints who can do what (sourced from RBAC declarations + the GitLab API)
  • A homelab rotate secrets verb that runs the rotation logic via the ISecretStore plugin
  • A scheduled trivy image scan job (via OpsSchedule) that fails the build if a high-severity CVE is found in the GitLab image

Where in the series: Parts 28, 43.


7. Ops.Networking — the verbs of routing and isolation

Reference: Ops DSL Ecosystem — Part 16.

Attributes:

[IngressRule(
    Host = "gitlab.frenchexdev.lab",
    Service = "gitlab",
    Port = 80,
    TlsSecret = "wildcard")]
[FirewallRule(
    From = "192.168.56.0/24",
    To = "192.168.56.10",
    Port = 80,
    Action = "allow")]
[NetworkPolicy(
    From = "platform-vm",
    To = "data-vm",
    Ports = new[] { 5432, 9000 })]
public sealed class DevLabNetworking { }

HomeLab generates:

  • Traefik dynamic config (host-based routing, TLS termination)
  • iptables / nftables rules in the Vagrant provisioning script
  • Compose networks: declarations with explicit driver and subnet
  • For HA topology: HAProxy backend pools

Where in the series: Parts 23, 28, 30, 33.


8. Ops.DataGovernance — the verbs of data lifecycle

Reference: Ops DSL Ecosystem — Part 17.

Attributes:

[BackupPolicy(
    Target = "postgres",
    Provider = "pgbackrest",
    Cron = "0 2 * * *",
    Retention = "30d")]
[BackupPolicy(
    Target = "minio",
    Provider = "restic",
    Cron = "0 3 * * *",
    Retention = "14d")]
[RetentionPolicy(Target = "audit-log", Retention = "365d")]
[RecoveryObjective(Rpo = "1h", Rto = "30m")]
[SeedData(Source = "fixtures/devlab-seed.sql", Target = "postgres")]
public sealed class DevLabDataGovernance { }

HomeLab generates:

  • Cron jobs in the host crontab (or as compose sidecars) that run pgbackrest backup and restic backup
  • A homelab backup run verb (immediate)
  • A homelab backup restore <id> verb
  • A scheduled "restore-test" job that spins up a throwaway lab and verifies the restore (this is dogfood Loop 4 — see Part 06)

Where in the series: Parts 38, 45, 49.


What HomeLab does not consume (and why)

Of the 22 Ops.Dsl sub-DSLs, HomeLab does not consume the other 14 in v1. Quick justifications:

Sub-DSL Why HomeLab skips it
Ops.Migration HomeLab doesn't run app migrations; the apps it hosts (like GitLab) handle their own
Ops.Performance SLI/SLO is overkill for a homelab; covered indirectly by Ops.Observability
Ops.LoadTesting k6 distributed scenarios are cloud-tier; HomeLab is container-tier only
Ops.Chaos Same — interesting future plugin, not v1
Ops.Testing HomeLab itself uses Requirements for traceability; this DSL is for the hosted apps
Ops.Quality Covered by the QualityGate library applied to HomeLab itself, not by an Ops.Dsl
Ops.ApiContract For the hosted apps, not for HomeLab
Ops.EnvironmentParity Topology is a single config field; we don't need a full DSL
Ops.Lifecycle Sunset schedules are out of scope for a homelab
Ops.Cost We do track cost (Part 47), but we use Ops.Cost minimally — only [ResourceBudget]
Ops.Capacity Autoscaling is not a homelab concern
Ops.Incident On-call rotations and PagerDuty are overkill for a single developer
Ops.Compliance SOC2 / GDPR matrices are not what a homelab does
Ops.SupplyChain SBOM is partially covered by vulnerability scan in Ops.Security

We import 8 packages, not 22. The remaining 14 are valuable in their own right and are documented in the Ops DSL Ecosystem series. They are not part of HomeLab's contract.


The wiring

Each Ops.Dsl sub-DSL ships as a separate NuGet:

<PackageReference Include="FrenchExDev.Net.Ops.Dsl.Infrastructure"   Version="1.*" />
<PackageReference Include="FrenchExDev.Net.Ops.Dsl.Deployment"       Version="1.*" />
<PackageReference Include="FrenchExDev.Net.Ops.Dsl.Observability"    Version="1.*" />
<PackageReference Include="FrenchExDev.Net.Ops.Dsl.Configuration"    Version="1.*" />
<PackageReference Include="FrenchExDev.Net.Ops.Dsl.Resilience"       Version="1.*" />
<PackageReference Include="FrenchExDev.Net.Ops.Dsl.Security"         Version="1.*" />
<PackageReference Include="FrenchExDev.Net.Ops.Dsl.Networking"       Version="1.*" />
<PackageReference Include="FrenchExDev.Net.Ops.Dsl.DataGovernance"   Version="1.*" />

Each NuGet declares its concepts via [MetaConcept]. The MetamodelRegistry discovers them at startup. HomeLab's IPlanProjector knows how to project from HomeLabConfig into the IR for each one — see the projector code in Part 12.


What this gives you that bash doesn't

A bash script does not have a vocabulary. Every operational concern is whatever string the script's author typed. Two scripts that "do health checks" have nothing in common except the verb. There is no analyzer that understands what a health check is. There is no generator that emits Prometheus alerts from a health check. There is no validator that catches "this alert references a metric that no service emits".

Eight typed Ops.Dsl sub-DSLs give you, for the same surface area:

  • A shared vocabulary that other Ops.Dsl-aware tools (analyzers, doc generators, future plugins) understand
  • Generated artifacts for each declaration (compose YAML, Prometheus rules, Grafana dashboards, iptables rules, cron entries)
  • Cross-DSL validation (an alert rule that references a missing metric becomes a compile error)
  • Plugin extensibility — a plugin can add a new sub-DSL that the existing analyzers automatically recognise
  • Scope discipline — you import the eight packages you need and skip the fourteen you don't

The bargain pays back the moment you write a [HealthCheck] and discover that HomeLab automatically generates a Prometheus scrape config, an alert rule, a Grafana panel, and a Verify probe — all from one attribute.


⬇ Download