Skip to content
GitHub stars

Architecture ​

This document provides a detailed overview of Lynq's architecture as a RecordOps platform, including system components, reconciliation flow, and key design decisions that enable Infrastructure as Data.

Infrastructure as Data Architecture

Lynq implements Infrastructure as Data through the RecordOps pattern. Database records control infrastructure state, enabling real-time provisioning without YAML files or CI/CD pipelines.

Learn more about Infrastructure as Data β†’

System Overview ​

Database Support

  • MySQL: Fully supported (v1.0+)
  • PostgreSQL: Planned for v1.2

Architecture at a Glance ​

Quick reference for the three main components:

ComponentPurposeExample
LynqHubConnects to database, syncs node rowsMySQL every 30s β†’ Creates LynqNode CRs
LynqFormDefines resource blueprintDeployment + Service per node
LynqNodeInstance of a single nodeacme-corp-web-app β†’ 5 K8s resources

Infrastructure as Data Workflow: Database row becomes active β†’ LynqHub controller syncs and multiplies rows by all referencing LynqForms β†’ A LynqNode CR is created per {row Γ— template} combination β†’ LynqNode controller renders the LynqForm snapshot for that node and applies it via the SSA engine β†’ Kubernetes resources are created/updated and kept in sync. Your data defines infrastructure, Lynq provisions it.

Reconciliation Flow ​

Three-Controller Design ​

The operator uses a three-controller architecture to separate concerns and optimize reconciliation:

1. LynqHub Controller ​

Purpose: Syncs database (e.g., 1m interval) β†’ Creates/Updates/Deletes LynqNode CRs

Responsibilities:

  • Periodically queries external datasource at spec.source.syncInterval
  • Filters active rows where activate field is truthy
  • Calculates desired LynqNode set: referencingTemplates Γ— activeRows
  • Creates missing LynqNode CRs (naming: {uid}-{template-name})
  • Updates existing LynqNode CRs with fresh data
  • Deletes LynqNode CRs for inactive/removed rows (garbage collection)
  • Updates Hub status with counts

Key Status Fields:

yaml
status:
  referencingTemplates: 2 # Number of templates using this hub
  desired: 6 # referencingTemplates Γ— activeRows
  ready: 5 # Ready LynqNodes across all templates
  failed: 1 # Failed LynqNodes across all templates

2. LynqForm Controller ​

Purpose: Validates template-registry linkage and invariants

Responsibilities:

  • Validates that spec.hubId references an existing LynqHub
  • Ensures template syntax is valid (Go text/template)
  • Validates resource IDs are unique within template
  • Detects dependency cycles in dependIds
  • Updates template status

3. LynqNode Controller ​

Purpose: Renders templates β†’ Resolves dependencies β†’ Applies resources via SSA

Responsibilities:

  • Builds template variables from LynqNode spec
  • Resolves resource dependencies (DAG + topological sort)
  • Renders all templates (names, namespaces, specs)
  • Applies resources using Server-Side Apply
  • Waits for resource readiness (if waitForReady=true)
  • Updates LynqNode status with resource counts and conditions
  • Handles conflicts according to ConflictPolicy
  • Manages finalizers for proper cleanup

CRD Architecture ​

LynqHub ​

Defines external datasource configuration and sync behavior:

yaml
apiVersion: operator.lynq.sh/v1
kind: LynqHub
metadata:
  name: my-saas-hub
spec:
  source:
    type: mysql
    mysql:
      host: mysql.default.svc.cluster.local
      port: 3306
      database: nodes
      table: node_data
      username: node_reader
      passwordRef:
        name: mysql-secret
        key: password
    syncInterval: 30s
  valueMappings:
    uid: node_id # Required
    hostOrUrl: domain # Required
    activate: is_active # Required
  extraValueMappings:
    planId: subscription_plan
    deployImage: container_image

Multi-Form Support: One hub can be referenced by multiple LynqForms, creating separate LynqNode CRs for each form-row combination.

LynqForm ​

Blueprint for resources to create per node:

yaml
apiVersion: operator.lynq.sh/v1
kind: LynqForm
metadata:
  name: web-app
spec:
  hubId: my-saas-hub
  deployments:
    - id: app-deployment
      nameTemplate: "{{ .uid }}-app"
      spec:
        apiVersion: apps/v1
        kind: Deployment
        spec:
          replicas: 2
          # ... deployment spec

Supported Resource Types:

  • serviceAccounts
  • deployments, statefulSets, daemonSets
  • services
  • configMaps, secrets
  • persistentVolumeClaims
  • jobs, cronJobs
  • ingresses
  • namespaces
  • manifests (raw resources)

LynqNode ​

Instance representing a single node:

yaml
apiVersion: operator.lynq.sh/v1
kind: LynqNode
metadata:
  name: acme-web-app
spec:
  uid: acme
  templateRef: web-app
  hubId: my-saas-hub
  # ... resolved resource arrays
status:
  desiredResources: 10
  readyResources: 10
  failedResources: 0
  appliedResources:
    - "Deployment/default/acme-app@app-deployment"
    - "Service/default/acme-svc@app-service"
  conditions:
    - type: Ready
      status: "True"
      lastTransitionTime: "2024-01-15T10:30:00Z"

Key Design Patterns ​

Server-Side Apply (SSA) ​

All resources are applied using Kubernetes Server-Side Apply with fieldManager: lynq. This provides:

  • Conflict-free updates: Multiple controllers can manage different fields
  • Declarative management: Operator owns only fields it sets
  • Drift detection: Automatic detection of manual changes
  • Force mode: Optional force ownership with ConflictPolicy: Force

Resource Tracking ​

Two mechanisms based on namespace and deletion policy:

  1. OwnerReference-based (automatic GC):

    • Same-namespace resources with DeletionPolicy=Delete
    • Kubernetes garbage collector handles cleanup
  2. Label-based (manual lifecycle):

    • Cross-namespace resources
    • Namespace resources
    • Resources with DeletionPolicy=Retain
    • Labels: lynq.sh/node, lynq.sh/node-namespace

Dependency Management ​

Resources are applied in order based on dependIds:

yaml
deployments:
  - id: app-deployment
    # ...

services:
  - id: app-service
    dependIds: ["app-deployment"]
    waitForReady: true
    # ...

The operator:

  1. Builds a Directed Acyclic Graph (DAG)
  2. Detects cycles (fails fast if found)
  3. Performs topological sort
  4. Applies resources in dependency order

Drift Detection & Auto-Correction ​

The operator continuously monitors managed resources through:

  • Event-driven watches: Immediate reconciliation on resource changes
  • Watch predicates: Only trigger on meaningful changes (Generation/Annotation)
  • Fast requeue: 30-second periodic requeue for status reflection
  • Auto-correction: Reverts manual changes to maintain desired state

Garbage Collection ​

Automatic cleanup when:

  • Database row is deleted
  • Row's activate field becomes false
  • Template no longer references the hub
  • LynqNode CR is deleted (with finalizer-based cleanup)

Resources respect DeletionPolicy:

  • Delete: Removed from cluster
  • Retain: Orphaned with labels for manual cleanup

Orphan Resource Management ​

When resources are removed from templates:

  • Detected via comparison of status.appliedResources
  • Resource key format: kind/namespace/name@id
  • DeletionPolicy preserved in annotation: lynq.sh/deletion-policy
  • Orphaned resources marked with:
    • Label: lynq.sh/orphaned: "true"
    • Annotations: orphaned-at, orphaned-reason
  • Re-adoption: Orphan markers removed when resource re-added to template

Performance Considerations ​

Controller Concurrency ​

Configurable worker pools for each controller:

  • --hub-concurrency=N (default: 3)
  • --form-concurrency=N (default: 5)
  • --node-concurrency=N (default: 10)

Reconciliation Optimization ​

  • Fast status reflection: 30-second requeue interval
  • Smart watch predicates: Filter status-only updates
  • Event-driven architecture: Immediate reaction to changes
  • Resource caching: Frequently accessed resources cached

Scalability ​

The operator is designed to scale horizontally:

  • Leader election for single-writer pattern
  • Optional sharding by namespace or node ID
  • Resource-type worker pools for parallel processing
  • SSA batching for bulk applies

Infrastructure as Data in Practice ​

Lynq's architecture implements Infrastructure as Data through RecordOps:

  1. Database as Infrastructure API: Your database schema defines your infrastructure specifications
  2. Continuous Reconciliation: Controllers ensure infrastructure state matches database state
  3. Template-Based Provisioning: One template definition applies to all records
  4. Automatic Lifecycle: Database operations (INSERT/UPDATE/DELETE) trigger infrastructure changes

This architecture enables Infrastructure as Data where operational changes are simply database transactions.

See Also ​

Released under the Apache 2.0 License.
Built with ❀️ using Kubebuilder, Controller-Runtime, and VitePress.