<?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>DEV Community: Karl Schriek</title>
    <description>The latest articles on DEV Community by Karl Schriek (@karlschriek).</description>
    <link>https://dev.to/karlschriek</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3761645%2Fd85e3ad5-4809-4396-b6e2-f67f3a4b7068.png</url>
      <title>DEV Community: Karl Schriek</title>
      <link>https://dev.to/karlschriek</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/karlschriek"/>
    <language>en</language>
    <item>
      <title>Splitting a Terraform Monolith into Smaller States</title>
      <dc:creator>Karl Schriek</dc:creator>
      <pubDate>Tue, 30 Jun 2026 15:39:06 +0000</pubDate>
      <link>https://dev.to/karlschriek/splitting-a-terraform-monolith-into-smaller-states-4b5a</link>
      <guid>https://dev.to/karlschriek/splitting-a-terraform-monolith-into-smaller-states-4b5a</guid>
      <description>&lt;p&gt;If your Terraform plans are slow, your blast radius is too wide, or multiple teams are stepping on each other's changes, it's time to split your monolith. See &lt;a href="https://snapcd.io/Learn/large-terraform-states" rel="noopener noreferrer"&gt;The Problem with Large Terraform States&lt;/a&gt; for how to diagnose whether you've reached that point.&lt;/p&gt;

&lt;p&gt;This guide walks through the process of breaking a monolithic Terraform state into smaller, focused states — and how Snap CD can manage the dependencies between them so you don't have to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approach
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Identify natural boundaries
&lt;/h3&gt;

&lt;p&gt;Look at your resources and group them by lifecycle and ownership. Common boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Networking&lt;/strong&gt; — VPCs, subnets, route tables, NAT gateways. Changes rarely, underpins everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS&lt;/strong&gt; — Zones, records. Usually owned by a platform team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute&lt;/strong&gt; — Kubernetes clusters, VM scale sets, container services. Changes more often, depends on networking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application infrastructure&lt;/strong&gt; — Databases, caches, queues, storage accounts. Owned by application teams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt; — Dashboards, alerts, log sinks. Changes frequently, depends on everything but nothing depends on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful test: if two resources would never be changed in the same PR by the same person, they probably belong in different states.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Map the dependencies
&lt;/h3&gt;

&lt;p&gt;Before you move anything, draw the dependency graph. Which groups produce values that other groups consume?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;networking          dns
    │                 ▲
    ▼                 │
  compute ──────────►─┘
    │
    ▼
application
    │
    ▼
monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The outputs that cross these boundaries are what you'll need to wire up after the split. Typical examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Networking → Compute: &lt;code&gt;vpc_id&lt;/code&gt;, &lt;code&gt;private_subnet_ids&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Compute → DNS: &lt;code&gt;load_balancer_ip&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Compute → Application: &lt;code&gt;cluster_endpoint&lt;/code&gt;, &lt;code&gt;cluster_ca_certificate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Application → Monitoring: &lt;code&gt;database_id&lt;/code&gt;, &lt;code&gt;cache_name&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Use &lt;code&gt;terraform state mv&lt;/code&gt; to migrate resources
&lt;/h3&gt;

&lt;p&gt;Terraform's &lt;code&gt;state mv&lt;/code&gt; command lets you move resources from one state to another without destroying and recreating them.&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;# Initialize the destination state&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;modules/networking
terraform init

&lt;span class="c"&gt;# Move resources from the monolith to the new state&lt;/span&gt;
terraform state &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;../monolith/terraform.tfstate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-state-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./terraform.tfstate &lt;span class="se"&gt;\&lt;/span&gt;
  aws_vpc.main aws_vpc.main

terraform state &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;../monolith/terraform.tfstate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-state-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./terraform.tfstate &lt;span class="se"&gt;\&lt;/span&gt;
  aws_subnet.private aws_subnet.private
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this methodically, one logical group at a time. After each move:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;terraform plan&lt;/code&gt; on the new state — it should show no changes.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;terraform plan&lt;/code&gt; on the monolith — the moved resources should no longer appear.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  4. Replace hard references with inputs
&lt;/h3&gt;

&lt;p&gt;In the monolith, your compute module might directly reference &lt;code&gt;aws_vpc.main.id&lt;/code&gt;. After the split, that VPC lives in a different state. You need to replace the hard reference with a variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before (monolith)&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_eks_cluster"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;subnet_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private&lt;/span&gt;&lt;span class="p"&gt;[*].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# After (separate compute module)&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"private_subnet_ids"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_eks_cluster"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;subnet_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the networking module, expose the value as an output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"private_subnet_ids"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private&lt;/span&gt;&lt;span class="p"&gt;[*].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Wire up the cross-state dependencies
&lt;/h3&gt;

&lt;p&gt;This is where it gets interesting. You've split the monolith, and now you need the outputs from one state to flow into another. There are a few ways to do this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A: &lt;code&gt;terraform_remote_state&lt;/code&gt; data sources&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The built-in approach. Each consuming module reads the producer's state directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"terraform_remote_state"&lt;/span&gt; &lt;span class="s2"&gt;"networking"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt;
  &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-terraform-state"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"networking/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_eks_cluster"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;subnet_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;terraform_remote_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;networking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works but has significant drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every consumer needs to know the backend configuration of every producer.&lt;/li&gt;
&lt;li&gt;There's no enforcement of the dependency order — you have to manually ensure networking is applied before compute.&lt;/li&gt;
&lt;li&gt;Changes to networking outputs don't automatically trigger a re-plan of compute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option B: Wrapper scripts and CI glue&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You write shell scripts or CI pipeline steps that run &lt;code&gt;terraform output&lt;/code&gt; on one state and feed the values into &lt;code&gt;terraform apply -var&lt;/code&gt; on the next. This is what most teams end up doing, and it's fragile — the dependency graph lives in CI config rather than in code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option C: Terragrunt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Terragrunt adds a dependency layer on top of Terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# compute/terragrunt.hcl&lt;/span&gt;
&lt;span class="nx"&gt;dependency&lt;/span&gt; &lt;span class="s2"&gt;"networking"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;config_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../networking"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dependency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;networking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_id&lt;/span&gt;
  &lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dependency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;networking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a genuine improvement — dependencies are declared in code, ordering is enforced, and &lt;code&gt;terragrunt run-all apply&lt;/code&gt; handles the graph. But Terragrunt is a local CLI tool. It doesn't provide a persistent view of deployment status, approval gates, automatic re-deployment when upstream outputs change, or scoped permissions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option D: Snap CD&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://snapcd.io" rel="noopener noreferrer"&gt;Snap CD&lt;/a&gt; was built for this problem. Each split becomes a Snap CD &lt;a href="https://docs.snapcd.io/resources/stack-namespace-module/" rel="noopener noreferrer"&gt;&lt;strong&gt;Module&lt;/strong&gt;&lt;/a&gt;, and cross-state dependencies are declared as code. Snap CD enforces apply ordering, runs independent Modules in parallel, and automatically cascades changes when upstream outputs change. See &lt;a href="https://snapcd.io/Learn/modular-deployments" rel="noopener noreferrer"&gt;Modular Deployments&lt;/a&gt; for a detailed walkthrough of how the Module and &lt;a href="https://docs.snapcd.io/resources/module-inputs/" rel="noopener noreferrer"&gt;input&lt;/a&gt; system works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Split incrementally.&lt;/strong&gt; Move one logical group at a time. Don't try to split everything in one go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with the layer that changes least.&lt;/strong&gt; Networking is usually the best first candidate — it has many dependents but few dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep shared modules small.&lt;/strong&gt; If a Terraform module (in the &lt;code&gt;module {}&lt;/code&gt; sense) is used by multiple states, keep it focused. A module that provisions "everything for an app" is just a monolith in disguise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with &lt;code&gt;terraform plan&lt;/code&gt; after every move.&lt;/strong&gt; A clean plan (no changes) on both the source and destination states confirms the migration was correct.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  See also
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://snapcd.io/Learn/large-terraform-states" rel="noopener noreferrer"&gt;The Problem with Large Terraform States&lt;/a&gt; — diagnosing when it's time to split&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://snapcd.io/Learn/modular-deployments" rel="noopener noreferrer"&gt;Modular Deployments&lt;/a&gt; — how Snap CD manages cross-state dependencies after the split&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://snapcd.io/Learn/runner-isolation" rel="noopener noreferrer"&gt;Self-Hosted Terraform Runners with Credential Isolation&lt;/a&gt; — scoping credentials per environment with dedicated Runners&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://snapcd.io/Learn/secrets-management-terraform" rel="noopener noreferrer"&gt;Managing Secrets in Terraform&lt;/a&gt; — keeping secrets scoped when splitting states&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>terraform</category>
      <category>cloud</category>
      <category>cicd</category>
      <category>infrastructureascode</category>
    </item>
    <item>
      <title>The Problem with Large Terraform States</title>
      <dc:creator>Karl Schriek</dc:creator>
      <pubDate>Tue, 30 Jun 2026 15:35:47 +0000</pubDate>
      <link>https://dev.to/karlschriek/the-problem-with-large-terraform-states-a2o</link>
      <guid>https://dev.to/karlschriek/the-problem-with-large-terraform-states-a2o</guid>
      <description>&lt;p&gt;At some point every growing Terraform project hits a wall. Plans that used to finish in seconds now take minutes. Applies feel risky because hundreds of resources share a single blast radius. Colleagues avoid running &lt;code&gt;terraform plan&lt;/code&gt; because it hammers cloud APIs hard enough to trigger throttling. The state file itself becomes a liability — large, slow to lock, and one bad write away from corruption.&lt;/p&gt;

&lt;p&gt;This guide covers the symptoms of an oversized state, the band-aids teams reach for, and the structural fix that actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Terraform state works under the hood
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;terraform plan&lt;/code&gt; does two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Refresh&lt;/strong&gt; — for every resource in state, Terraform calls the provider's API to read the current real-world status. A state with 500 resources means 500+ API calls, often more when resources have nested data sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff&lt;/strong&gt; — compare the refreshed state against the desired configuration and produce a change set.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The refresh phase is the bottleneck. It's sequential per provider (parallelism helps across providers, not within one), and every resource pays the cost whether you changed it or not. Adding ten resources to a 500-resource state doesn't make plans 2% slower — it makes the refresh 2% slower on every single plan, for every engineer, forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptoms of a state that's too large
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Slow plans
&lt;/h3&gt;

&lt;p&gt;The most visible symptom. Plan time scales with resource count because every resource is refreshed on every plan, regardless of whether its configuration changed. The exact speed depends on provider — AWS resources with complex nested structures (IAM policies, security group rules) are slower to refresh than simple ones, and Azure resources that require multiple API calls per refresh are worse still. These aren't edge cases — users regularly report &lt;a href="https://github.com/hashicorp/terraform/issues/18981" rel="noopener noreferrer"&gt;2,900-resource states taking 20–25 minutes to plan&lt;/a&gt; and &lt;a href="https://github.com/hashicorp/terraform/issues/16375" rel="noopener noreferrer"&gt;1,600-resource states taking 8+ minutes&lt;/a&gt;. Even starting Terraform with a large state &lt;a href="https://github.com/hashicorp/terraform/issues/35822" rel="noopener noreferrer"&gt;can take minutes before a single API call is made&lt;/a&gt;. There's a &lt;a href="https://github.com/hashicorp/terraform/issues/35290" rel="noopener noreferrer"&gt;long-standing proposal for &lt;code&gt;terraform plan -light&lt;/code&gt;&lt;/a&gt; that would only refresh resources whose configuration changed, but it remains unimplemented. OpenTofu has a similar request to &lt;a href="https://github.com/opentofu/opentofu/issues/1703" rel="noopener noreferrer"&gt;skip refreshing unchanged resources&lt;/a&gt; and a proposal for &lt;a href="https://github.com/opentofu/opentofu/issues/2083" rel="noopener noreferrer"&gt;state compression&lt;/a&gt; to reduce the overhead of large state files.&lt;/p&gt;

&lt;h3&gt;
  
  
  API rate limiting
&lt;/h3&gt;

&lt;p&gt;Cloud providers throttle API calls. When Terraform refreshes hundreds of resources, it can exhaust rate limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS&lt;/strong&gt;: &lt;code&gt;ThrottlingException&lt;/code&gt; or &lt;code&gt;Rate exceeded&lt;/code&gt; errors, especially on IAM, EC2 describe calls, and CloudFormation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure&lt;/strong&gt;: &lt;code&gt;429 Too Many Requests&lt;/code&gt;, particularly on Resource Manager and Key Vault APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GCP&lt;/strong&gt;: &lt;code&gt;rateLimitExceeded&lt;/code&gt; on Compute Engine and IAM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Terraform retries on throttling, which makes plans even slower. In severe cases, retries exhaust their budget and the plan fails entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blast radius
&lt;/h3&gt;

&lt;p&gt;Every resource in a state shares a blast radius. A typo in a DNS record can, in the same plan, sit alongside a database resize. One bad &lt;code&gt;terraform apply&lt;/code&gt; can damage resources the operator didn't intend to touch.&lt;/p&gt;

&lt;p&gt;This isn't theoretical. Common incidents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;for_each&lt;/code&gt; key change causes Terraform to destroy and recreate resources it shouldn't.&lt;/li&gt;
&lt;li&gt;A provider upgrade changes how a resource is read, causing phantom diffs on dozens of resources.&lt;/li&gt;
&lt;li&gt;An engineer runs &lt;code&gt;terraform apply&lt;/code&gt; on a plan that's stale — someone else merged a change to a different resource in the same state, and the apply picks up both.&lt;/li&gt;
&lt;li&gt;A third-party API is down or throttling, so the refresh fails for a resource you weren't even changing — blocking the entire plan. With a smaller state, that resource would be in a different state file and wouldn't affect your work at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With smaller states, each of these incidents affects only the resources in that state. With a monolith, everything is in play.&lt;/p&gt;

&lt;h3&gt;
  
  
  Locking contention
&lt;/h3&gt;

&lt;p&gt;Remote state backends use locking to prevent concurrent writes. The longer a plan or apply takes, the longer the lock is held. With a 10-minute plan, other engineers are blocked for 10 minutes. If an apply follows, that's another stretch of locked state.&lt;/p&gt;

&lt;p&gt;Teams start working around locks — using &lt;code&gt;-lock=false&lt;/code&gt; (dangerous), splitting work by time of day (inefficient), or simply waiting. &lt;a href="https://github.com/hashicorp/terraform/issues/33032" rel="noopener noreferrer"&gt;Concurrent updates to large state files are also significantly slower&lt;/a&gt; because each write serialises the entire state. None of these are real solutions.&lt;/p&gt;

&lt;h3&gt;
  
  
  State file size and corruption risk
&lt;/h3&gt;

&lt;p&gt;State files grow linearly with resource count. A 1,000-resource state file can be several megabytes of JSON. Every plan downloads the full state, and every apply uploads a new version. On slow connections or with large states, this adds latency.&lt;/p&gt;

&lt;p&gt;More critically, large state files are harder to recover from corruption. If a write is interrupted (network failure during apply, process killed), the state can become inconsistent. With a small state, recovery is straightforward — reimport a handful of resources. With a monolith, you're reimporting hundreds. Large state files also compound the secrets problem — Terraform &lt;a href="https://github.com/hashicorp/terraform/issues/9556" rel="noopener noreferrer"&gt;stores sensitive values in plaintext in state&lt;/a&gt;, so a bigger state means more secrets exposed in a single file. OpenTofu &lt;a href="https://github.com/opentofu/opentofu/issues/297" rel="noopener noreferrer"&gt;implemented state encryption&lt;/a&gt;, but Terraform's proposal has been open since 2016.&lt;/p&gt;

&lt;h2&gt;
  
  
  Band-aids that don't fix the problem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;terraform plan -target&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;-target&lt;/code&gt; flag tells Terraform to only refresh and plan specific resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan &lt;span class="nt"&gt;-target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;aws_instance.web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes individual plans fast, but it's a trap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You must know which resources to target. Miss a dependency and the plan is incomplete.&lt;/li&gt;
&lt;li&gt;Targeted plans skip dependency checking. You can apply a change that breaks a resource you didn't target.&lt;/li&gt;
&lt;li&gt;It's manual and error-prone. There's no guardrail preventing someone from running a full plan and waiting 15 minutes.&lt;/li&gt;
&lt;li&gt;Terraform itself warns: "Resource targeting is intended for exceptional use and should not be part of normal workflow."&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;terraform plan -refresh=false&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Skipping refresh makes plans fast because Terraform uses the last-known state instead of querying APIs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan &lt;span class="nt"&gt;-refresh&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is obvious: if the real world has drifted from state, the plan is wrong. An engineer deleted a resource manually, someone changed a security group in the console, a colleague applied from a different branch — none of this shows up. You're planning against fiction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workspaces
&lt;/h3&gt;

&lt;p&gt;Terraform workspaces let you maintain multiple state files from the same configuration. They're designed for deploying the same infrastructure to different environments (dev, staging, prod), not for splitting a large state into smaller pieces.&lt;/p&gt;

&lt;p&gt;Workspaces don't reduce the number of resources per state. If your monolith has 500 resources, each workspace still has 500 resources. They solve a different problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;terraform state rm&lt;/code&gt; and manual state surgery
&lt;/h3&gt;

&lt;p&gt;When a single resource is causing problems, engineers sometimes remove it from state and reimport it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform state &lt;span class="nb"&gt;rm &lt;/span&gt;aws_instance.problematic
terraform import aws_instance.problematic i-0123456789abcdef0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a valid recovery technique but not a scaling strategy. It's manual, risky (removing the wrong resource is destructive), and doesn't address the underlying size problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real fix: smaller states
&lt;/h2&gt;

&lt;p&gt;The only way to permanently fix a large state is to break it into smaller ones. Each state contains a logical group of resources — networking, compute, databases, monitoring — with its own lifecycle, credentials, and blast radius. If your state spans multiple cloud providers, splitting along provider boundaries is one of the most effective first moves. Each provider has its own API rate limits, its own authentication, and its own failure modes — an Azure outage shouldn't block a plan that only touches AWS resources. Separate states per provider also let you scope credentials more tightly and parallelise plans that would otherwise run sequentially through a single refresh cycle.&lt;/p&gt;

&lt;p&gt;The hard part isn't the split itself — it's managing the dependencies between the resulting states. Networking outputs need to flow into compute. Compute outputs need to flow into application infrastructure. Changes to one state need to trigger re-plans in dependent states. &lt;a href="https://snapcd.io" rel="noopener noreferrer"&gt;Snap CD&lt;/a&gt; was built for exactly this workflow — it tracks cross-state dependencies declaratively and cascades changes automatically, so you get the benefits of smaller states without the coordination overhead. For a discussion of approaches to breaking a monolith into smaller states, see &lt;a href="https://snapcd.io/Learn/splitting-terraform-monolith" rel="noopener noreferrer"&gt;Splitting a Terraform Monolith&lt;/a&gt;. To learn more about how Snap CD approaches modular deployments, see &lt;a href="https://snapcd.io/Learn/modular-deployments" rel="noopener noreferrer"&gt;Modular Deployments&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tell when it's time
&lt;/h2&gt;

&lt;p&gt;There's no universal threshold, but if any of these are true, you should start planning a split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;terraform plan&lt;/code&gt; consistently takes more than a few minutes.&lt;/li&gt;
&lt;li&gt;More than one team commits to the same Terraform root module.&lt;/li&gt;
&lt;li&gt;You've had an incident where an apply affected resources the operator didn't intend to change.&lt;/li&gt;
&lt;li&gt;Applies are failing due to issues with unrelated resources in the same state.&lt;/li&gt;
&lt;li&gt;Your state spans multiple cloud providers, and an outage or rate limit on one provider blocks plans for resources on another.&lt;/li&gt;
&lt;li&gt;Engineers routinely use &lt;code&gt;-target&lt;/code&gt; or &lt;code&gt;-refresh=false&lt;/code&gt; to work around slowness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with the layer that changes least (usually networking) and work outward. The &lt;a href="https://snapcd.io/Learn/splitting-terraform-monolith" rel="noopener noreferrer"&gt;Splitting a Terraform Monolith&lt;/a&gt; guide has the step-by-step process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Split by cloud provider early.&lt;/strong&gt; If your state has resources across AWS, Azure, and GCP, separating them into per-provider states is one of the highest-value splits. Each provider has independent rate limits, authentication, and failure modes — keeping them together means a slow Azure API refresh delays your AWS plan for no reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for provider-specific bottlenecks.&lt;/strong&gt; Even within a single cloud, some resource types are slower than others. If most of your plan time is AWS IAM resources, splitting out IAM alone might cut plan time dramatically. This is also a prerequisite if you are serious about not mixing credentials on the workers that are responsible for the deployments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't over-split.&lt;/strong&gt; Five resources that always change together, owned by the same team, with the same credentials, should stay in one state. The goal is fast plans and small blast radius, not one resource per state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;-parallelism&lt;/code&gt; wisely.&lt;/strong&gt; Terraform's &lt;code&gt;-parallelism&lt;/code&gt; flag (default 10) controls concurrent provider operations. Increasing it can speed up plans but also increases the risk of hitting API rate limits. With smaller states, the default is usually fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  See also
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://snapcd.io/Learn/splitting-terraform-monolith" rel="noopener noreferrer"&gt;Splitting a Terraform Monolith&lt;/a&gt; — approaches to breaking a monolith into smaller states&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://snapcd.io/Learn/modular-deployments" rel="noopener noreferrer"&gt;Modular Deployments&lt;/a&gt; — how Snap CD manages cross-state dependencies after the split&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://snapcd.io/Learn/runner-isolation" rel="noopener noreferrer"&gt;Self-Hosted Terraform Runners with Credential Isolation&lt;/a&gt; — scoping credentials per environment with dedicated Runners&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://snapcd.io/Learn/secrets-management-terraform" rel="noopener noreferrer"&gt;Managing Secrets in Terraform&lt;/a&gt; — why smaller states reduce secret exposure&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>terraform</category>
      <category>cicd</category>
      <category>infrastructureascode</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Introducing Snap CD: Why I Built a New Terraform Orchestrator</title>
      <dc:creator>Karl Schriek</dc:creator>
      <pubDate>Tue, 10 Feb 2026 11:45:35 +0000</pubDate>
      <link>https://dev.to/karlschriek/introducing-snap-cd-why-i-built-a-new-terraform-orchestrator-23ll</link>
      <guid>https://dev.to/karlschriek/introducing-snap-cd-why-i-built-a-new-terraform-orchestrator-23ll</guid>
      <description>&lt;p&gt;Anyone who has operated Terraform/OpenTofu at scale knows the pattern:&lt;/p&gt;

&lt;p&gt;You start with one state file. It works great. Then it grows. And grows. Maybe your company also grows so that now multiple teams are deploying infrastructure. &lt;code&gt;terraform plan&lt;/code&gt; used to take seconds — now it takes many minutes. A single change triggers a refresh of hundreds of resources. One team's DNS change blocks another team's application deployment. You start sweating every &lt;code&gt;terraform apply&lt;/code&gt;. All you want to do is add a tag to an Azure storage account, but the &lt;code&gt;plan&lt;/code&gt; won't run through because that one fringe resource where credentials have gone stale is causing the refresh to fail! Or it &lt;em&gt;does&lt;/em&gt; run through but now there are multiple resources you have no knowledge of, all of which will be modified by the &lt;code&gt;apply&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The answer everyone arrives at is the same: break it up. Split your monolith into smaller, focused state files. Networking in one. DNS in another. Application infrastructure in a third. Give different teams the responsibility to manage different pieces.&lt;/p&gt;

&lt;p&gt;But the moment you do that, you inherit a new problem: &lt;strong&gt;the dependencies between those pieces are no longer enforced, and no longer visible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your application module needs the &lt;code&gt;vpc_id&lt;/code&gt; from your networking module. Your DNS module needs the &lt;code&gt;load_balancer_arn&lt;/code&gt; from your application module. Suddenly you're stitching together &lt;code&gt;terraform_remote_state&lt;/code&gt; data sources, writing wrapper scripts, building CI/CD pipelines with hard-coded dependency chains, and praying that someone doesn't deploy a networking change that deletes resources your application deployments depend on.&lt;/p&gt;

&lt;p&gt;The dependency graph that Terraform/OpenTofu handles beautifully &lt;em&gt;within&lt;/em&gt; a single state becomes your manual responsibility &lt;em&gt;across&lt;/em&gt; states.&lt;/p&gt;

&lt;h1&gt;
  
  
  What I wanted
&lt;/h1&gt;

&lt;p&gt;I wanted a system where I could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Break infrastructure into small, focused modules&lt;/strong&gt;, each with its own state file, its own lifecycle, its own blast radius. Outputs from any module automatically become available as inputs to other modules, creating a declarative dependency system across my entire infrastructure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Have changes propagate automatically.&lt;/strong&gt; When my "vpc" module produces a new &lt;code&gt;private_subnet_id&lt;/code&gt;, downstream modules that consume it should re-plan and re-apply without manual intervention. It should also be a true GitOps orchestrator, meaning new commits or updated configuration should automatically trigger deployment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep my cloud credentials out of the control plane.&lt;/strong&gt; The orchestrator should coordinate work, not execute it. Execution should happen on runners I deploy in my own infrastructure. I decide where they run, what access they have, and which modules are allowed to use them. My state files I manage in whatever remote location I am most comfortable with.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Control access granularly.&lt;/strong&gt; Infrastructure is organized into stacks (hard boundaries like "prod" and "dev"), then namespaces (logical groupings like "networking" or "storage"), then modules (individual deployments). I need role-based permissions assignable at every one of these levels, whether for service principals or users.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stay non-invasive.&lt;/strong&gt; No proprietary runtimes, no lock-in at the execution layer. Runners should execute standard commands like &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt; in a normal shell. I should be able to SSH into a runner's working directory and run commands manually if I need to.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Manage everything as code.&lt;/strong&gt; A Terraform Provider for the orchestrator itself, so that stacks, namespaces, modules, runners, secrets, role assignments etc. are all defined in HCL.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Let AI agents participate safely.&lt;/strong&gt; AI coding agents are transforming how infrastructure is managed, but handing an agent unrestricted &lt;code&gt;terraform apply&lt;/code&gt; access is a recipe for outages. The orchestrator should let me treat an AI agent as just another principal — with scoped permissions, runner restrictions, and approval gates.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of the existing tools delivered on all of these and eventually I realized that if I wanted a system like this I would have to start building it myself.&lt;/p&gt;

&lt;h1&gt;
  
  
  How I solved it
&lt;/h1&gt;

&lt;p&gt;This probably warrants a dedicated article by itself, but suffice it to say that for a software engineer with the interests I have (building cohesive solutions that consist of various interlocking systems) this was a &lt;em&gt;wonderful&lt;/em&gt; project to work on. Few things I have done in my career have brought me as much satisfaction as this. It tapped into all the skills I had learnt over 20 years of software engineering and also demanded that I learn quite a few more!&lt;/p&gt;

&lt;p&gt;It took me about six months to lay down the bare bones, then about another 12 months of iteration, testing (with pretty serious production infrastructure) and feature expansion. During this time I completely rewrote some of the core systems multiple times until I was happy with them.&lt;/p&gt;

&lt;p&gt;With that being said, allow me to introduce &lt;a href="https://snapcd.io" rel="noopener noreferrer"&gt;Snap CD&lt;/a&gt; and explain how it addresses each of the requirements above:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Modular deployments
&lt;/h2&gt;

&lt;p&gt;Snap CD organizes infrastructure into three levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;module&lt;/strong&gt; is a single Terraform/OpenTofu deployment. It points to code in a Git repo, has its own state file, and defines inputs and outputs.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;namespace&lt;/strong&gt; groups related modules. Think "networking", "storage", "applications". Typically only one team would be responsible for a single namespace.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;stack&lt;/strong&gt; is a hard boundary, such "prod", "dev" or "staging". Namespaces are organized into stacks. Modules in different stacks don't influence each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below is some very simple sample code using the &lt;a href="https://registry.terraform.io/providers/schrieksoft/snapcd/latest/docs" rel="noopener noreferrer"&gt;Snap CD Terraform Provider&lt;/a&gt; to deploy a new namespace into an existing stack. Into the namespace we deploy two modules, "vpc" and "cluster", where the latter requires an output from the former as one of its inputs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Stack&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"snapcd_stack"&lt;/span&gt; &lt;span class="s2"&gt;"mystack"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-stack"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Namespace&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"snapcd_namespace"&lt;/span&gt; &lt;span class="s2"&gt;"mynamespace"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-namespace"&lt;/span&gt;
  &lt;span class="nx"&gt;stack_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snapcd_stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mystack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;## Module 1 (VPC)&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"snapcd_module"&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt;
  &lt;span class="nx"&gt;namespace_id&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snapcd_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mynamespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;source_revision&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt;
  &lt;span class="nx"&gt;source_url&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/snapcd-samples/mock-module-vpc.git"&lt;/span&gt;
  &lt;span class="nx"&gt;source_subdirectory&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="nx"&gt;runner_id&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snapcd_runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;## Module 2 (Cluster)&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"snapcd_module"&lt;/span&gt; &lt;span class="s2"&gt;"cluster"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cluster"&lt;/span&gt;
  &lt;span class="nx"&gt;namespace_id&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snapcd_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mynamespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;source_revision&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt;
  &lt;span class="nx"&gt;source_url&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/snapcd-samples/mock-module-kubernetes-cluster.git"&lt;/span&gt;
  &lt;span class="nx"&gt;source_subdirectory&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="nx"&gt;runner_id&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snapcd_runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"snapcd_module_input_from_output"&lt;/span&gt; &lt;span class="s2"&gt;"private_subnet_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;input_kind&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Param"&lt;/span&gt;
  &lt;span class="nx"&gt;module_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snapcd_module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"deploy_to_subnet_id"&lt;/span&gt; &lt;span class="c1"&gt;// The "cluster" module expects a variable called "deploy_to_subnet_id"&lt;/span&gt;
  &lt;span class="nx"&gt;output_module_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snapcd_module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;output_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"private_subnet_id"&lt;/span&gt; &lt;span class="c1"&gt;// The "vpc" module produces an output called "private_subnet_id", which we map to "deploy_to_subnet_id"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;NOTE that these are "mock" deployments, meant for illustration only. You can find their code &lt;a href="https://github.com/snapcd-samples/mock-module-vpc" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://github.com/snapcd-samples/mock-module-kubernetes-cluster" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The dependency graph is the core of Snap CD. Since the "cluster" module has a &lt;code&gt;snapcd_module_input_from_output&lt;/code&gt; that references an output from the "vpc" module, Snap CD knows that a dependency exists. No scripts. No CI/CD glue. The dependency graph is derived from the configuration itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqatpji1eicqu4qu1wefq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqatpji1eicqu4qu1wefq.gif" alt="dag" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Event-driven CD
&lt;/h2&gt;

&lt;p&gt;Modules can trigger automatically based on multiple events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source changes&lt;/strong&gt;: A new commit lands on a branch, or a new semantic version tag appears. Snap CD detects this (typically via polling jobs pushed to a runner, but manual notification webhooks are also supported) and triggers a deployment job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upstream output changes&lt;/strong&gt;: When a dependency's outputs change, downstream modules re-deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Definition changes&lt;/strong&gt;: When you modify a module's configuration (e.g. via the Terraform Provider, or manually via the Portal), it triggers a sync.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also require &lt;strong&gt;manual approval&lt;/strong&gt; before applies go through, with configurable approval thresholds. This lets you build workflows where plans run automatically but &lt;code&gt;apply&lt;/code&gt; waits for human sign-off.&lt;/p&gt;

&lt;p&gt;Let's consider again the code for the "cluster" module above. That module points to the "main" branch of the repo at "&lt;a href="https://github.com/snapcd-samples/mock-module-kubernetes-cluster.git" rel="noopener noreferrer"&gt;https://github.com/snapcd-samples/mock-module-kubernetes-cluster.git&lt;/a&gt;". Whenever new commits are pushed to this branch, Snap CD will automatically trigger a deployment.&lt;/p&gt;

&lt;p&gt;Similarly if the "vpc" module outputs a new value for &lt;code&gt;private_subnet_id&lt;/code&gt;, then the "cluster" module deployment will trigger.&lt;/p&gt;

&lt;p&gt;Lastly, a change to the definition as follows would automatically trigger a deployment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"snapcd_module"&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt;
  &lt;span class="nx"&gt;namespace_id&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snapcd_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mynamespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;source_revision&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt;
  &lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;source_url&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/snapcd-samples/mock-module-vpc.git"&lt;/span&gt;
  &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;source_url&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/snapcd-samples/mock-module-another-vpc.git"&lt;/span&gt;
  &lt;span class="nx"&gt;source_subdirectory&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="nx"&gt;runner_id&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snapcd_runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we are changing the &lt;code&gt;source_url&lt;/code&gt; but any changes to the &lt;code&gt;snapcd_module&lt;/code&gt; itself or to any child resources such as &lt;code&gt;snapcd_module_input...&lt;/code&gt;, &lt;code&gt;snapcd_extra_file&lt;/code&gt;, &lt;code&gt;snapcd_backend_config&lt;/code&gt; and so forth would also automatically trigger a deployment!&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Runner isolation
&lt;/h2&gt;

&lt;p&gt;Snap CD's architecture cleanly separates orchestration from execution. The &lt;strong&gt;Server&lt;/strong&gt; (&lt;a href="https://snapcd.io" rel="noopener noreferrer"&gt;snapcd.io&lt;/a&gt;) is the control plane — it handles configuration, dependency tracking, job management, and log/output storage. It never touches your cloud infrastructure directly. No AWS credentials, no Azure service principals, no GCP service accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runners&lt;/strong&gt; are self-hosted agents that you deploy in a manner and location of your choosing. They connect to the Server over an authenticated WebSocket, pick up jobs, execute standard &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt; etc., and report back with logs and outputs.&lt;/p&gt;

&lt;p&gt;The Runner is a source-available component, maintained as part of the main &lt;a href="https://github.com/schrieksoft/snapcd" rel="noopener noreferrer"&gt;Snap CD monorepo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You configure your runners with whatever cloud credentials they need, and then you dictate which Snap CD modules are allowed to use them. For example, you may want to separate runners for "dev" and "prod", and/or for different cloud providers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fltxb4anwrip5zmi3i00m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fltxb4anwrip5zmi3i00m.png" alt="runners" width="800" height="309"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Below is an example of how you would register a runner and assign it for use by modules within the namespace we created above.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"snapcd_service_principal"&lt;/span&gt; &lt;span class="s2"&gt;"my_service_principal"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// fetch a pre-existing Service Principal (this must be created manually via the snapcd.io portal)&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MyServicePrincipal"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"snapcd_runner"&lt;/span&gt; &lt;span class="s2"&gt;"my_runner"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"myrunner"&lt;/span&gt;
  &lt;span class="nx"&gt;service_principal_id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snapcd_service_principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_service_principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;is_assigned_to_all_modules&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"snapcd_runner_namespace_assignment"&lt;/span&gt; &lt;span class="s2"&gt;"myrunner_mynamespace"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;runner_id&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snapcd_runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;namespace_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snapcd_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mynamespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runners can be assigned to a single &lt;a href="https://registry.terraform.io/providers/schrieksoft/snapcd/latest/docs/resources/runner_module_assignment" rel="noopener noreferrer"&gt;module&lt;/a&gt;, to an entire &lt;a href="https://registry.terraform.io/providers/schrieksoft/snapcd/latest/docs/resources/runner_namespace_assignment" rel="noopener noreferrer"&gt;namespace&lt;/a&gt;, an entire &lt;a href="https://registry.terraform.io/providers/schrieksoft/snapcd/latest/docs/resources/runner_stack_assignment" rel="noopener noreferrer"&gt;stack&lt;/a&gt; or (by setting the &lt;code&gt;is_assigned_to_all_modules&lt;/code&gt; flag to &lt;code&gt;true&lt;/code&gt;) to an entire organization.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Permission system
&lt;/h2&gt;

&lt;p&gt;Role-based access control is assignable at every level of the hierarchy: organization, stack, namespace, module, as well as to runners.&lt;/p&gt;

&lt;p&gt;Users, service principals, and groups can all be scoped precisely.&lt;/p&gt;

&lt;p&gt;In the below example code, we set a User to &lt;code&gt;Contributor&lt;/code&gt; on the namespace shown in the example code above&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"snapcd_user"&lt;/span&gt; &lt;span class="s2"&gt;"myuser"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;user_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"myuser@somedomain.com"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"snapcd_namespace_role_assignment"&lt;/span&gt; &lt;span class="s2"&gt;"myuser_contributor"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;namespace_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snapcd_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mynamespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;principal_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snapcd_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;myuser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;principal_discriminator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"User"&lt;/span&gt; &lt;span class="c1"&gt;// Can be one of "User", "ServicePrincipal" or "Group"&lt;/span&gt;
  &lt;span class="nx"&gt;role_name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Contributor"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Non-invasive orchestration
&lt;/h2&gt;

&lt;p&gt;Snap CD is not a Terraform/OpenTofu replacement. It doesn't parse HCL. It doesn't have its own resource model. Your modules are regular Terraform/OpenTofu modules. Your providers are regular Terraform/OpenTofu providers. If you stopped using Snap CD tomorrow, your infrastructure and state files would still be perfectly valid.&lt;/p&gt;

&lt;p&gt;Snap CD also doesn't force proprietary tooling into your deployment process. Runners execute standard &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt; in a normal shell. Snap CD provides the inputs — &lt;code&gt;.env&lt;/code&gt; files, &lt;code&gt;.tfvars&lt;/code&gt; files, scripts — and the runner executes them. If you needed to, you could navigate directly to a runner's working directory and run those commands manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Everything as code
&lt;/h2&gt;

&lt;p&gt;Almost everything in Snap CD is managed via its own &lt;a href="https://registry.terraform.io/providers/schrieksoft/snapcd/latest/docs" rel="noopener noreferrer"&gt;Terraform Provider&lt;/a&gt;. Stacks, namespaces, modules, runners, secrets, role assignments, etc. — all defined in HCL.&lt;/p&gt;

&lt;p&gt;For a more complete tutorial see the &lt;a href="https://docs.snapcd.io/quickstart/" rel="noopener noreferrer"&gt;quickstart guide&lt;/a&gt; or go directly to the &lt;a href="https://github.com/snapcd-samples/sample-deployment" rel="noopener noreferrer"&gt;sample deployment&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One of the interesting patterns that is made possible by the Terraform Provider is a module-within-module pattern. In other words, you could instruct Snap CD to deploy Snap CD modules, which then instruct Snap CD to deploy your actual resources!&lt;/p&gt;

&lt;h2&gt;
  
  
  7. AI on a leash
&lt;/h2&gt;

&lt;p&gt;AI coding agents are transforming software development, and infrastructure is no exception. But letting an AI run &lt;code&gt;terraform apply&lt;/code&gt; with broad credentials and no guardrails is a recipe for outages.&lt;/p&gt;

&lt;p&gt;Snap CD lets you treat an AI agent as just another &lt;strong&gt;principal&lt;/strong&gt; — with scoped permissions, runner restrictions, and approval gates. Give an agent &lt;code&gt;Contributor&lt;/code&gt; on a test namespace but &lt;code&gt;Reader&lt;/code&gt; on production. Let it trigger plans and diagnose drift, but require a human to approve before anything is applied.&lt;/p&gt;

&lt;p&gt;The same RBAC, approval thresholds, and audit trail that govern human operators govern AI agents. Move faster with AI assistance, on a leash you control.&lt;/p&gt;

&lt;h1&gt;
  
  
  Getting started
&lt;/h1&gt;

&lt;p&gt;The entire project is source available on &lt;a href="https://github.com/schrieksoft/snapcd" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, with a free community edition for self-hosting and advanced features available at reasonable prices. We provide deployment instructions for &lt;a href="https://github.com/schrieksoft/snapcd-deployment-local" rel="noopener noreferrer"&gt;local use&lt;/a&gt;, with &lt;a href="https://github.com/schrieksoft/snapcd-deployment-docker" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt;, or on &lt;a href="https://github.com/schrieksoft/snapcd-deployment-kubernetes" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A cloud-hosted edition is also available at &lt;a href="https://snapcd.io/pricing" rel="noopener noreferrer"&gt;snapcd.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you've ever stared at a sprawling Terraform monolith and thought "there has to be a better way to split this up" — that's exactly the problem Snap CD was built to solve. If you would like to try it out, here is a &lt;a href="https://docs.snapcd.io/quickstart/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; guide.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>tooling</category>
      <category>cicd</category>
    </item>
  </channel>
</rss>
