<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Tim Kang</title>
    <description>The latest articles on Forem by Tim Kang (@selenehyun).</description>
    <link>https://forem.com/selenehyun</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3643144%2Fdbb47c89-e13d-4b94-99d8-69eb3a169112.jpeg</url>
      <title>Forem: Tim Kang</title>
      <link>https://forem.com/selenehyun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/selenehyun"/>
    <language>en</language>
    <item>
      <title>Migrating from Terraform/Helm to Database-Driven Kubernetes Without Deleting Anything</title>
      <dc:creator>Tim Kang</dc:creator>
      <pubDate>Thu, 04 Dec 2025 00:46:12 +0000</pubDate>
      <link>https://forem.com/selenehyun/migrating-from-terraformhelm-to-database-driven-kubernetes-without-deleting-anything-1bgm</link>
      <guid>https://forem.com/selenehyun/migrating-from-terraformhelm-to-database-driven-kubernetes-without-deleting-anything-1bgm</guid>
      <description>&lt;p&gt;So you've seen &lt;a href="https://dev.to/selenehyun/when-terraform-stops-scaling-for-multi-tenant-kubernetes-a-database-driven-approach-3oi5"&gt;how database-driven automation works&lt;/a&gt;, &lt;a href="https://dev.to/selenehyun/how-database-driven-kubernetes-automation-actually-works-242i"&gt;maybe even tried the Killercoda demo&lt;/a&gt;, and now you're thinking: "cool, but I already have a hundred tenants running. I can't just blow everything away and start over."&lt;/p&gt;

&lt;p&gt;Yeah. That's a real problem.&lt;/p&gt;

&lt;p&gt;This post is about how to migrate existing resources managed by Terraform, Helm, Kustomize, or whatever else you're using, over to Lynq without deleting anything. The goal is zero downtime, no data loss, and a safe rollback path if things go sideways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before we start
&lt;/h2&gt;

&lt;p&gt;I'm assuming you've already:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up your MySQL database with the node table&lt;/li&gt;
&lt;li&gt;Created a LynqHub pointing to that database&lt;/li&gt;
&lt;li&gt;Written a basic LynqForm and tested it with a fresh node&lt;/li&gt;
&lt;li&gt;Verified that new nodes provision correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you haven't done that yet, check out the &lt;a href="https://killercoda.com/lynq-operator/course/killercoda/lynq-quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; first. This guide is specifically about taking over existing resources, not creating new ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The strategy
&lt;/h2&gt;

&lt;p&gt;Here's the high-level approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure your LynqForm to generate the exact same resource names as your existing ones&lt;/li&gt;
&lt;li&gt;Use conservative policies as safety nets&lt;/li&gt;
&lt;li&gt;Test with one node first to verify conflict detection works&lt;/li&gt;
&lt;li&gt;Remove ownership from your old tool (Terraform state, Helm release, etc.)&lt;/li&gt;
&lt;li&gt;Let Lynq take over ownership&lt;/li&gt;
&lt;li&gt;Repeat for remaining nodes&lt;/li&gt;
&lt;li&gt;Gradually relax safety policies once stable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's walk through each step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: match your existing resource names
&lt;/h2&gt;

&lt;p&gt;This is crucial. Your LynqForm templates need to produce the exact same resource names that already exist in the cluster.&lt;/p&gt;

&lt;p&gt;Say your existing deployment is named &lt;code&gt;acme-corp-app&lt;/code&gt; in namespace &lt;code&gt;acme-corp&lt;/code&gt;. Your template needs to render to exactly that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deployments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;nameTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.uid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-app"&lt;/span&gt;
    &lt;span class="na"&gt;namespaceTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.uid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
      &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your database has &lt;code&gt;uid = "acme-corp"&lt;/code&gt;, this renders to &lt;code&gt;acme-corp-app&lt;/code&gt; in namespace &lt;code&gt;acme-corp&lt;/code&gt;. Perfect match.&lt;/p&gt;

&lt;p&gt;Double check your naming conventions. If Terraform was using underscores and Lynq templates use dashes, you'll create duplicate resources instead of taking over the existing ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: configure safety-first policies
&lt;/h2&gt;

&lt;p&gt;For migration, start with the most conservative settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deployments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;nameTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.uid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-app"&lt;/span&gt;
    &lt;span class="na"&gt;conflictPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Stuck&lt;/span&gt;      &lt;span class="c1"&gt;# don't force takeover yet&lt;/span&gt;
    &lt;span class="na"&gt;deletionPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Retain&lt;/span&gt;     &lt;span class="c1"&gt;# never auto-delete, even if something goes wrong&lt;/span&gt;
    &lt;span class="na"&gt;creationPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WhenNeeded&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why these settings?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;conflictPolicy: Stuck&lt;/strong&gt; is your early warning system. When Lynq tries to apply a resource that's already owned by something else (like Terraform or Helm), it will stop and emit an event instead of forcing through. This lets you verify that Lynq is actually targeting the right resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;deletionPolicy: Retain&lt;/strong&gt; is your safety net. Even if you accidentally delete a LynqNode or mess up the hub config, the actual kubernetes resources stay in the cluster. You can always recover.&lt;/p&gt;

&lt;p&gt;Apply this to every resource in your template. Yes, all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: test with a single node first
&lt;/h2&gt;

&lt;p&gt;Don't migrate everything at once. Pick one tenant/node and try it first.&lt;/p&gt;

&lt;p&gt;Insert or activate the row in your database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'acme-corp'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now watch what happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get lynqnodes &lt;span class="nt"&gt;-w&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a LynqNode created. Check its events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe lynqnode acme-corp-web-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your existing resources match the template output, you'll see &lt;code&gt;ResourceConflict&lt;/code&gt; events. This is actually what we want at this stage. It confirms Lynq is finding and targeting the right resources.&lt;/p&gt;

&lt;p&gt;The event message tells you exactly what's conflicting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Resource conflict detected for acme-corp/acme-corp-app (Kind: Deployment, Policy: Stuck). 
Another controller or user may be managing this resource. Consider using ConflictPolicy=Force 
to take ownership or resolve the conflict manually. 
Error: Apply failed with 1 conflict: conflict with "helm" using apps/v1: .spec.replicas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which resource: &lt;code&gt;acme-corp/acme-corp-app&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Current owner: &lt;code&gt;helm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Conflicting field: &lt;code&gt;.spec.replicas&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: review and fix template mismatches
&lt;/h2&gt;

&lt;p&gt;Sometimes the conflict message reveals that your LynqForm doesn't quite match the existing resource. Maybe your template sets &lt;code&gt;replicas: 2&lt;/code&gt; but the existing deployment has &lt;code&gt;replicas: 5&lt;/code&gt; because of HPA.&lt;/p&gt;

&lt;p&gt;You have a few options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A: Update your template to match&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the difference is intentional (like HPA managing replicas), don't set that field in your template, or use &lt;code&gt;ignoreFields&lt;/code&gt; to skip it during reconciliation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B: Accept the difference&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want Lynq to enforce a new value, that's fine. Just be aware the resource will change when you force takeover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option C: Update the database&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the value should come from your database, add it to &lt;code&gt;extraValueMappings&lt;/code&gt; and use it in the template.&lt;/p&gt;

&lt;p&gt;The key is understanding what will change before you flip the switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: remove ownership from your old tool
&lt;/h2&gt;

&lt;p&gt;Now comes the actual migration. You need to tell your old tool to stop managing these resources without deleting them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Terraform:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove from state without destroying&lt;/span&gt;
terraform state &lt;span class="nb"&gt;rm &lt;/span&gt;kubernetes_deployment.acme_corp_app
terraform state &lt;span class="nb"&gt;rm &lt;/span&gt;kubernetes_service.acme_corp_svc
&lt;span class="c"&gt;# repeat for all resources&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For Helm:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Uninstall release but keep resources&lt;/span&gt;
helm uninstall acme-corp-release &lt;span class="nt"&gt;--keep-history&lt;/span&gt;

&lt;span class="c"&gt;# Or if you want to be extra safe, just delete the release secret&lt;/span&gt;
kubectl delete secret &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;helm,name&lt;span class="o"&gt;=&lt;/span&gt;acme-corp-release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For Kustomize/kubectl:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you were just applying manifests directly, there's no state to remove. The resources exist, they're just not tracked by anything. Lynq can take over directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For ArgoCD/Flux:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Remove the Application or Kustomization CR, or exclude those resources from sync. The actual resources stay in cluster.&lt;/p&gt;

&lt;p&gt;After this step, the resources exist in kubernetes but nothing is actively managing them. They're orphaned, which is exactly what we want temporarily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: let lynq take ownership
&lt;/h2&gt;

&lt;p&gt;Now update your LynqForm to force takeover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deployments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;nameTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.uid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-app"&lt;/span&gt;
    &lt;span class="na"&gt;conflictPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Force&lt;/span&gt;      &lt;span class="c1"&gt;# changed from Stuck&lt;/span&gt;
    &lt;span class="na"&gt;deletionPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Retain&lt;/span&gt;     &lt;span class="c1"&gt;# keep this for now&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the updated LynqForm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; lynqform.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next reconciliation will use Server-Side Apply with &lt;code&gt;force=true&lt;/code&gt; to take ownership. Check the LynqNode status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get lynqnode acme-corp-web-app &lt;span class="nt"&gt;-o&lt;/span&gt; yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;desiredResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;readyResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;failedResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="na"&gt;appliedResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deployment/acme-corp/acme-corp-app@app"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Service/acme-corp/acme-corp-svc@svc"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more conflicts. Lynq now owns these resources.&lt;/p&gt;

&lt;p&gt;Verify by checking the resource's managedFields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get deployment acme-corp-app &lt;span class="nt"&gt;-n&lt;/span&gt; acme-corp &lt;span class="nt"&gt;-o&lt;/span&gt; yaml | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A5&lt;/span&gt; managedFields
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;manager: lynq&lt;/code&gt; in there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: repeat for remaining nodes
&lt;/h2&gt;

&lt;p&gt;Once you've confirmed the first node works, migrate the rest. You can do this gradually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Migrate in batches&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'us-east-1'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Wait, verify&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'us-west-2'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- And so on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Monitor the LynqHub status to track progress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get lynqhub my-hub &lt;span class="nt"&gt;-o&lt;/span&gt; yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;referencingTemplates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;desired&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;150&lt;/span&gt;
  &lt;span class="na"&gt;ready&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;148&lt;/span&gt;
  &lt;span class="na"&gt;failed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Investigate any failures before continuing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: clean up old tool artifacts
&lt;/h2&gt;

&lt;p&gt;Once everything is migrated and stable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delete old Terraform state files or workspaces&lt;/li&gt;
&lt;li&gt;Remove Helm release history if you used &lt;code&gt;--keep-history&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Archive old Kustomize overlays&lt;/li&gt;
&lt;li&gt;Update CI/CD pipelines to stop running old provisioning&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  step 9: consider relaxing policies
&lt;/h2&gt;

&lt;p&gt;After running stable for a while, you might want to adjust policies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deployments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;conflictPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Stuck&lt;/span&gt;      &lt;span class="c1"&gt;# back to Stuck for safety&lt;/span&gt;
    &lt;span class="na"&gt;deletionPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete&lt;/span&gt;     &lt;span class="c1"&gt;# now safe to auto-cleanup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switching &lt;code&gt;deletionPolicy&lt;/code&gt; back to &lt;code&gt;Delete&lt;/code&gt; means when a node is deactivated, resources get cleaned up automatically. Only do this once you trust the system.&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;conflictPolicy: Stuck&lt;/code&gt; for ongoing safety. Force was just for the migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  troubleshooting common issues
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Resource names don't match&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you see Lynq creating new resources instead of conflict events, your &lt;code&gt;nameTemplate&lt;/code&gt; isn't producing the right names. Check the LynqNode spec to see what names it's trying to create.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stuck on unexpected fields&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The conflict message shows which fields conflict. Common culprits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;replicas&lt;/code&gt; (managed by HPA)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;annotations&lt;/code&gt; (added by other controllers)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;labels&lt;/code&gt; (injected by admission webhooks)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;code&gt;ignoreFields&lt;/code&gt; in your resource definition to skip these during reconciliation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Old tool still trying to manage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If Terraform or Helm is still running somewhere (CI pipeline, cron job), it might fight with Lynq for ownership. Make sure you've fully disabled the old automation before migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LynqNode stuck in progressing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check events: &lt;code&gt;kubectl describe lynqnode &amp;lt;name&amp;gt;&lt;/code&gt;. Usually it's a dependency waiting for readiness or a template rendering error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollback plan
&lt;/h2&gt;

&lt;p&gt;If something goes wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Since you used &lt;code&gt;deletionPolicy: Retain&lt;/code&gt;, resources are safe&lt;/li&gt;
&lt;li&gt;Delete the LynqNode: &lt;code&gt;kubectl delete lynqnode &amp;lt;name&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Resources stay in cluster, just unmanaged&lt;/li&gt;
&lt;li&gt;Re-import into Terraform: &lt;code&gt;terraform import ...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Or re-deploy with Helm: &lt;code&gt;helm upgrade --install ...&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The retain policy gives you this escape hatch. Use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Migrating to database-driven automation doesn't have to be scary. The key is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Match existing resource names exactly&lt;/li&gt;
&lt;li&gt;Use Stuck policy to verify targeting before forcing&lt;/li&gt;
&lt;li&gt;Use Retain policy as a safety net throughout&lt;/li&gt;
&lt;li&gt;Migrate incrementally, not all at once&lt;/li&gt;
&lt;li&gt;Keep your old tool's state around until you're confident&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Take your time. There's no rush. The resources aren't going anywhere.&lt;/p&gt;




&lt;p&gt;Questions? Drop them in the comments or open an issue on GitHub.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://lynq.sh" rel="noopener noreferrer"&gt;https://lynq.sh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/k8s-lynq/lynq" rel="noopener noreferrer"&gt;https://github.com/k8s-lynq/lynq&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>lynq</category>
    </item>
    <item>
      <title>How Database-Driven Kubernetes Automation Actually Works</title>
      <dc:creator>Tim Kang</dc:creator>
      <pubDate>Thu, 04 Dec 2025 00:13:35 +0000</pubDate>
      <link>https://forem.com/selenehyun/how-database-driven-kubernetes-automation-actually-works-242i</link>
      <guid>https://forem.com/selenehyun/how-database-driven-kubernetes-automation-actually-works-242i</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/selenehyun/when-terraform-stops-scaling-for-multi-tenant-kubernetes-a-database-driven-approach-3oi5"&gt;previous post&lt;/a&gt;, I talked about why I built Lynq and the problems it solves. But I didn't really get into how it actually works. So let's fix that.&lt;/p&gt;

&lt;p&gt;If you want to follow along hands-on, there's a Killercoda scenario that walks through everything in about 10 minutes: &lt;a href="https://killercoda.com/lynq-operator/course/killercoda/lynq-quickstart" rel="noopener noreferrer"&gt;https://killercoda.com/lynq-operator/course/killercoda/lynq-quickstart&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic idea
&lt;/h2&gt;

&lt;p&gt;Here's the mental model. You have three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;LynqHub&lt;/strong&gt; connects to your database and watches for changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LynqForm&lt;/strong&gt; defines what kubernetes resources to create (templates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LynqNode&lt;/strong&gt; is the actual instance, one per database row per template&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The flow is simple. A row appears in your database. The hub sees it, creates a LynqNode. The node controller renders your templates and applies resources. Done.&lt;/p&gt;

&lt;p&gt;When the row disappears or gets deactivated, cleanup happens automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting to your database
&lt;/h2&gt;

&lt;p&gt;First you need a LynqHub. This tells Lynq where your data lives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;operator.lynq.sh/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LynqHub&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-saas-hub&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
    &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql.default.svc.cluster.local&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3306&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nodes&lt;/span&gt;
      &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node_data&lt;/span&gt;
      &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node_reader&lt;/span&gt;
      &lt;span class="na"&gt;passwordRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql-secret&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;
  &lt;span class="na"&gt;syncInterval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
  &lt;span class="na"&gt;valueMappings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node_id&lt;/span&gt;
    &lt;span class="na"&gt;activate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;is_active&lt;/span&gt;
  &lt;span class="na"&gt;extraValueMappings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;planId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;subscription_plan&lt;/span&gt;
    &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deployment_region&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;valueMappings&lt;/code&gt; section is important. You're telling Lynq which columns matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;uid&lt;/code&gt; is the unique identifier for each node&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;activate&lt;/code&gt; is a boolean that controls whether resources should exist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then &lt;code&gt;extraValueMappings&lt;/code&gt; lets you pull in whatever custom fields you need. These become variables you can use in your templates.&lt;/p&gt;

&lt;p&gt;The hub polls your database at &lt;code&gt;syncInterval&lt;/code&gt; and syncs changes. If a row has &lt;code&gt;activate=true&lt;/code&gt;, Lynq creates resources. If it changes to &lt;code&gt;false&lt;/code&gt;, cleanup starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining your resource templates
&lt;/h2&gt;

&lt;p&gt;Next is the LynqForm. This is basically your blueprint for what gets created per database row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;operator.lynq.sh/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LynqForm&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web-app&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hubId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-saas-hub&lt;/span&gt;
  &lt;span class="na"&gt;deployments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;nameTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.uid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-app"&lt;/span&gt;
      &lt;span class="na"&gt;labelsTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.uid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.planId&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;basic&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
        &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
        &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
          &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
                  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.deployImage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;nginx:latest&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;svc&lt;/span&gt;
      &lt;span class="na"&gt;nameTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.uid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-svc"&lt;/span&gt;
      &lt;span class="na"&gt;dependIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
        &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
        &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Templates use Go's text/template syntax with Sprig functions. So you get 200+ functions out of the box. The variables come from your database columns via the hub mappings.&lt;/p&gt;

&lt;p&gt;A few things I find myself using constantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;{{ .uid }}&lt;/code&gt; for unique names&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{{ .planId | default "basic" }}&lt;/code&gt; when columns might be null&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{{ .uid | trunc63 }}&lt;/code&gt; to respect kubernetes naming limits&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where policies come in
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. Not every resource should behave the same way.&lt;/p&gt;

&lt;p&gt;Each resource in your template can have its own policies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deployments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;creationPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WhenNeeded&lt;/span&gt;
    &lt;span class="na"&gt;deletionPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete&lt;/span&gt;
    &lt;span class="na"&gt;conflictPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Stuck&lt;/span&gt;
    &lt;span class="na"&gt;patchStrategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apply&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;creationPolicy&lt;/strong&gt; controls when resources get created or updated.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WhenNeeded&lt;/code&gt; (the default) means Lynq continuously syncs. If someone deletes the resource manually, it comes back. If you update the template, changes apply. This is what you want for most things.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Once&lt;/code&gt; means create it once and never touch it again. Perfect for init jobs or migration scripts that should only run on first setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;init-job&lt;/span&gt;
    &lt;span class="na"&gt;creationPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Once&lt;/span&gt;
    &lt;span class="na"&gt;nameTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.uid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-init"&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Job&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;init&lt;/span&gt;
                &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;echo&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'one-time&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;setup'"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;deletionPolicy&lt;/strong&gt; controls what happens when a LynqNode is deleted or the row disappears.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Delete&lt;/code&gt; (default) cleans up the resource. Lynq sets an ownerReference so kubernetes garbage collection handles it automatically.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Retain&lt;/code&gt; keeps the resource around. Lynq tracks it via labels instead of ownerReference. When deleted, it just gets marked as orphaned so you can find it later.&lt;/p&gt;

&lt;p&gt;If you're dealing with PersistentVolumeClaims or anything with data you don't want to lose, use Retain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;persistentVolumeClaims&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data-pvc&lt;/span&gt;
    &lt;span class="na"&gt;deletionPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Retain&lt;/span&gt;
    &lt;span class="na"&gt;nameTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.uid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-data"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;conflictPolicy&lt;/strong&gt; handles what happens when a resource already exists with a different owner.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Stuck&lt;/code&gt; (default) is conservative. If there's a conflict, reconciliation stops and you get an event. Safe but requires manual intervention.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Force&lt;/code&gt; takes ownership using Server-Side Apply with force=true. Useful when you're migrating from another system or when Lynq should be the single source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ordering with dependencies
&lt;/h2&gt;

&lt;p&gt;Sometimes you need resources to come up in order. A deployment needs its configmap first. A service should wait for its deployment.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;dependIds&lt;/code&gt; does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db-creds&lt;/span&gt;
    &lt;span class="na"&gt;nameTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.uid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-creds"&lt;/span&gt;

&lt;span class="na"&gt;deployments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;dependIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db-creds"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;waitForReady&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;dependIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;waitForReady&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lynq builds a DAG (directed acyclic graph) from your dependencies and applies resources in topological order. If you accidentally create a cycle, it fails fast with an error.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;waitForReady: true&lt;/code&gt; flag is important. Without it, &lt;code&gt;dependIds&lt;/code&gt; only guarantees creation order. With it, Lynq actually waits for the dependency to become ready before creating the dependent resource.&lt;/p&gt;

&lt;p&gt;There's also &lt;code&gt;skipOnDependencyFailure&lt;/code&gt; (defaults to true). If a dependency fails, dependent resources get skipped instead of failing too. Sometimes you want the opposite though:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cleanup-job&lt;/span&gt;
    &lt;span class="na"&gt;dependIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main-app"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;skipOnDependencyFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;  &lt;span class="c1"&gt;# run even if main-app fails&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it all ties together
&lt;/h2&gt;

&lt;p&gt;So the full flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hub controller polls database every syncInterval&lt;/li&gt;
&lt;li&gt;For each active row, it creates or updates a LynqNode CR&lt;/li&gt;
&lt;li&gt;Node controller picks up the LynqNode&lt;/li&gt;
&lt;li&gt;It renders all templates with the row's data&lt;/li&gt;
&lt;li&gt;It builds a dependency graph and sorts resources&lt;/li&gt;
&lt;li&gt;It applies each resource in order using Server-Side Apply&lt;/li&gt;
&lt;li&gt;It waits for readiness if configured&lt;/li&gt;
&lt;li&gt;It updates LynqNode status with what got created&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When a row is deactivated or deleted:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hub controller detects the change&lt;/li&gt;
&lt;li&gt;LynqNode CR is deleted&lt;/li&gt;
&lt;li&gt;Finalizer runs cleanup based on each resource's deletionPolicy&lt;/li&gt;
&lt;li&gt;Resources with Delete policy get removed&lt;/li&gt;
&lt;li&gt;Resources with Retain policy get orphan labels added&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can watch this in action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get lynqnodes &lt;span class="nt"&gt;-w&lt;/span&gt;
kubectl describe lynqnode &amp;lt;name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The status shows exactly what's happening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;desiredResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;readyResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;failedResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="na"&gt;appliedResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deployment/default/acme-app@app"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Service/default/acme-svc@svc"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The best way to understand this is to actually run through it. I set up a Killercoda scenario that walks through the whole thing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://killercoda.com/lynq-operator/course/killercoda/lynq-quickstart" rel="noopener noreferrer"&gt;https://killercoda.com/lynq-operator/course/killercoda/lynq-quickstart&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It takes about 10 minutes. You'll set up MySQL, deploy the operator, create a hub and template, and then insert/update/delete rows to see how resources respond.&lt;/p&gt;

&lt;h2&gt;
  
  
  When would you actually use this
&lt;/h2&gt;

&lt;p&gt;This pattern works well when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You already have business data in a database (users, orgs, tenants)&lt;/li&gt;
&lt;li&gt;You need fast provisioning, not commit-sync-reconcile loops&lt;/li&gt;
&lt;li&gt;You want to replicate the same resources many times with different values&lt;/li&gt;
&lt;li&gt;Template versioning matters more than instance versioning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not for everything. If you have a small number of snowflake environments, traditional IaC is probably fine. But if you're replicating the same pattern hundreds of times based on database records, this approach is worth considering.&lt;/p&gt;




&lt;p&gt;Docs: &lt;a href="https://lynq.sh" rel="noopener noreferrer"&gt;https://lynq.sh&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/k8s-lynq/lynq" rel="noopener noreferrer"&gt;https://github.com/k8s-lynq/lynq&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer questions in the comments.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>database</category>
      <category>lynq</category>
    </item>
    <item>
      <title>RecordOps: What if Your Database Records Could Provision Infrastructure?</title>
      <dc:creator>Tim Kang</dc:creator>
      <pubDate>Wed, 03 Dec 2025 10:18:25 +0000</pubDate>
      <link>https://forem.com/selenehyun/when-terraform-stops-scaling-for-multi-tenant-kubernetes-a-database-driven-approach-3oi5</link>
      <guid>https://forem.com/selenehyun/when-terraform-stops-scaling-for-multi-tenant-kubernetes-a-database-driven-approach-3oi5</guid>
      <description>&lt;p&gt;Here's a scenario you might recognize: You're building a multi-tenant SaaS platform. A new customer signs up, and their data gets inserted into your database. Perfect. Now you need to provision their infrastructure—namespace, deployment, service, ingress. So you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write YAML manifests&lt;/li&gt;
&lt;li&gt;Commit to Git&lt;/li&gt;
&lt;li&gt;Wait for PR approval&lt;/li&gt;
&lt;li&gt;Wait for CI/CD&lt;/li&gt;
&lt;li&gt;Hope nothing breaks&lt;/li&gt;
&lt;li&gt;Update the customer record with their URL&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It works, but doesn't it feel... disconnected? Your application already knows everything about this customer through the database. Why are you manually coordinating with a completely separate infrastructure system?&lt;/p&gt;

&lt;h2&gt;
  
  
  What if Infrastructure Just Read From the Same Database?
&lt;/h2&gt;

&lt;p&gt;That's the idea behind RecordOps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RecordOps&lt;/strong&gt; (Record Operations) is a pattern where your database records define infrastructure state. Instead of maintaining YAML files or Terraform code, you define infrastructure parameters as columns in your database. (I'm coining this term to describe a pattern I've been using—maybe it resonates with you too.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INSERT a row     -&amp;gt;  Infrastructure provisions
UPDATE a column  -&amp;gt;  Resources reconfigure
DELETE a record  -&amp;gt;  Everything cleans up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every active row in your database represents a running stack in your cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Concrete Example
&lt;/h2&gt;

&lt;p&gt;Let's say you have a &lt;code&gt;customers&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;domain&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;replicas&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With RecordOps, you define a template once: "For each active customer, create a namespace, deployment (with N replicas), service, and ingress (pointing to their domain)."&lt;/p&gt;

&lt;p&gt;Now when you onboard a customer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'acme-corp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'acme.example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'enterprise'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within 30 seconds, infrastructure provisions automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Namespace: &lt;code&gt;acme-corp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Deployment: 5 replicas&lt;/li&gt;
&lt;li&gt;Service: &lt;code&gt;acme-corp-app&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ingress: &lt;code&gt;acme.example.com&lt;/code&gt; -&amp;gt; service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No YAML. No Git. No manual steps. Just a database transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Feels Different
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Your Database Already Has All the Answers
&lt;/h3&gt;

&lt;p&gt;Think about what information you need to provision infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer ID&lt;/li&gt;
&lt;li&gt;Domain name&lt;/li&gt;
&lt;li&gt;Plan/tier&lt;/li&gt;
&lt;li&gt;Region&lt;/li&gt;
&lt;li&gt;Resource limits&lt;/li&gt;
&lt;li&gt;Feature flags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this is already in your database. You're just duplicating it in YAML files or Terraform variables.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operations Become Data Changes
&lt;/h3&gt;

&lt;p&gt;Common operational tasks are just database operations you already know:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scale a customer:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;replicas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'acme-corp'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Enable a feature flag:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;feature_flags&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'acme-corp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ai-assistant'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Blue-green deployment:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;deployments&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;active_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'green'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'acme-corp'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No new tooling. No context switching. Just SQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing Becomes Trivial
&lt;/h3&gt;

&lt;p&gt;Want to clone your production environment to staging? With traditional infrastructure, that's a project. You're exporting state, modifying variables, coordinating across systems.&lt;/p&gt;

&lt;p&gt;With RecordOps, it's just cloning database rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'prod'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'.staging'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;30 seconds later, you have a perfect staging environment. Every service, every configuration, every dependency—recreated automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Does This Compare to GitOps?
&lt;/h2&gt;

&lt;p&gt;GitOps is excellent for cluster-level infrastructure. Your operators, CRDs, system services—these absolutely should be in Git with proper review.&lt;/p&gt;

&lt;p&gt;But for per-customer infrastructure? Git becomes tedious. You're creating YAML files for each customer, managing merge conflicts, waiting for pipelines. Meanwhile, your application already knows about these customers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They work well together:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitOps&lt;/strong&gt;: Cluster-level config (changes rarely, needs review)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RecordOps&lt;/strong&gt;: Customer-level stacks (changes frequently, follows data)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What About Infrastructure-as-Code?
&lt;/h2&gt;

&lt;p&gt;Terraform and Pulumi are great for cloud infrastructure. If you're provisioning AWS resources or managing your cluster itself, absolutely use them.&lt;/p&gt;

&lt;p&gt;But if you're provisioning the same pattern repeatedly—one stack per customer, one environment per project—you might not need infrastructure-as-code. You might just need infrastructure-as-data.&lt;/p&gt;

&lt;p&gt;Instead of writing code to describe infrastructure, you're adding rows to describe state. It's a different mental model that maps naturally to database-driven applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Feature Flags Control Infrastructure
&lt;/h3&gt;

&lt;p&gt;Instead of deploying optional features for everyone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;feature_flags&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;feature&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- AI assistant appears only for this customer&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;feature_flags&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'acme-corp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ai-assistant'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your template includes conditional logic. If the flag exists and is enabled, the AI service deploys. Otherwise, it skips it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blue-Green as a Column
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;deployments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;active_version&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;-- 'blue' or 'green'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Switch traffic&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;deployments&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;active_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'green'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your service selector updates to point to green. Traffic switches in seconds. Roll back by changing it back to 'blue'.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ephemeral Environments with TTL
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;environments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;domain&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;environments&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'demo-123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'demo-123.example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Database trigger cleans up expired environments&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;cleanup_expired&lt;/span&gt;
&lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;environments&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;environments&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Demo environments provision on insert and auto-cleanup after their TTL.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Does RecordOps Make Sense?
&lt;/h2&gt;

&lt;p&gt;This pattern works well when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're building multi-tenant platforms where each customer needs isolated infrastructure&lt;/li&gt;
&lt;li&gt;You provision infrastructure frequently (multiple times per day)&lt;/li&gt;
&lt;li&gt;Your infrastructure closely follows your data model&lt;/li&gt;
&lt;li&gt;You want less coordination between application and infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's probably not right if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You rarely provision infrastructure (once a month or less)&lt;/li&gt;
&lt;li&gt;Every change requires manual approval&lt;/li&gt;
&lt;li&gt;You need deep cloud provider integrations beyond Kubernetes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Honestly, you can mix approaches. GitOps for cluster-level, RecordOps for tenant-level, manual for critical changes. They complement each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to Keep in Mind
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Your Database Becomes Critical Infrastructure
&lt;/h3&gt;

&lt;p&gt;It's not just storing application data anymore—it's controlling infrastructure. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database availability matters more (though existing infrastructure keeps running if DB goes down)&lt;/li&gt;
&lt;li&gt;Schema migrations affect infrastructure (test carefully)&lt;/li&gt;
&lt;li&gt;Database permissions become infrastructure permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Security Model Shifts
&lt;/h3&gt;

&lt;p&gt;SQL injection vulnerabilities can become infrastructure vulnerabilities. If user input can manipulate your queries, they could trigger unwanted infrastructure changes. Use parameterized queries and validate inputs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sync Delays Exist
&lt;/h3&gt;

&lt;p&gt;With RecordOps, there's typically a sync interval (e.g., 30 seconds) between database changes and infrastructure updates. For most cases this is fine, but if you need instant provisioning, you'll need to tune this or reconsider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm Exploring This
&lt;/h2&gt;

&lt;p&gt;I kept running into the same problem: syncing my application's database state with infrastructure state. I'd add a customer to the database, then manually coordinate with my infrastructure tooling. Eventually I realized—they could just be the same thing.&lt;/p&gt;

&lt;p&gt;RecordOps isn't revolutionary. It's actually pretty obvious once you see it. If your infrastructure maps to your data, why not let your data drive your infrastructure?&lt;/p&gt;

&lt;p&gt;This pattern won't replace every tool in your stack. But for the specific problem of provisioning repeated patterns (per-customer stacks, per-project environments), it might simplify your life.&lt;/p&gt;

&lt;h2&gt;
  
  
  If You Want to Try It
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://lynq.sh" rel="noopener noreferrer"&gt;Lynq&lt;/a&gt;, an open-source operator that implements RecordOps for Kubernetes. You point it at your database, define your templates, and it handles the rest.&lt;/p&gt;

&lt;p&gt;But the pattern itself is tool-agnostic. You could build your own implementation, use a different tool, or just take the concepts and apply them however makes sense for your stack.&lt;/p&gt;




&lt;p&gt;What do you think? Have you felt this pain before? How are you handling per-customer infrastructure provisioning today?&lt;/p&gt;

&lt;p&gt;I'd love to hear your thoughts and experiences in the comments. 👇&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>database</category>
      <category>lynq</category>
    </item>
  </channel>
</rss>
