Why Snap CD: Modular Deployments
Terraform manages dependencies between resources within a single state. The moment your infrastructure outgrows one state file — slow plans, wide blast radius, team contention — you need to split. But the pieces still depend on each other: compute needs the VPC ID from networking, application infrastructure needs the cluster endpoint from compute.
The usual approaches — terraform_remote_state, parameter stores, wrapper scripts, Terragrunt — each solve part of the problem but leave gaps in change detection, ordering enforcement, and visibility. For a walkthrough of these approaches and how to perform the split itself, see Splitting a Terraform Monolith.
Snap CD's module system was built specifically for what comes after the split: declaring the dependency graph as code, enforcing apply ordering, and cascading changes automatically.
Managing Snap CD with Terraform
The HCL examples throughout this guide use the Snap CD Terraform provider. This is the canonical way to configure Snap CD — you use Terraform to manage the system that manages your Terraform modules. Everything you see in the examples below — the hierarchy, the dependency wiring, the secret bindings — is declared as standard Terraform resources via this provider.
This means your Snap CD configuration is version-controlled, reviewable, and reproducible — the same properties you expect from the infrastructure it orchestrates. For more on the provider and the broader toolset, see An Extensive Supporting Toolset. For how runners provide credential isolation between modules, see Self-Hosted Terraform Runners with Credential Isolation.
Stacks, Namespaces, and Modules
Snap CD organises infrastructure in a three-level hierarchy:
- Stack — a top-level grouping, typically an environment or a product. Examples:
production,staging,platform-services. - Namespace — a logical grouping within a Stack, typically a team or an infrastructure layer. Examples:
networking,data-platform,frontend. - Module — a single Terraform root within a Namespace. This is the unit of deployment — each Module has its own state, its own Runner, and its own lifecycle.
Permissions, secrets, and default inputs can be set at any level and inherited downward. A secret defined at the namespace level is available to all modules in that namespace. A permission granted at the stack level applies to all namespaces and modules within it.
resource "snapcd_stack" "production" {
name = "production"
organization_id = snapcd_organization.main.id
}
resource "snapcd_namespace" "platform" {
name = "platform"
stack_id = snapcd_stack.production.id
}
Modules
A module is an independent Terraform root — its own source repository, its own state, its own runner, its own credentials. Modules are defined within a namespace:
resource "snapcd_module" "networking" {
name = "networking"
namespace_id = snapcd_namespace.platform.id
source_url = "https://github.com/myorg/infra-networking.git"
runner_id = snapcd_runner.platform.id
}
resource "snapcd_module" "compute" {
name = "compute"
namespace_id = snapcd_namespace.platform.id
source_url = "https://github.com/myorg/infra-compute.git"
runner_id = snapcd_runner.platform.id
}
resource "snapcd_module" "database" {
name = "database"
namespace_id = snapcd_namespace.platform.id
source_url = "https://github.com/myorg/infra-database.git"
runner_id = snapcd_runner.platform.id
}
Each module is fully independent — its own state, its own credentials (scoped via the runner), its own lifecycle.
Inputs
Modules receive values through inputs. Snap CD supports several input types, each suited to a different use case.
Outputs from other modules
The most common input type. A module consumes an output from another module, and Snap CD builds the dependency graph from these declarations:
resource "snapcd_module_input_from_output" "vpc_id" {
module_id = snapcd_module.compute.id
input_kind = "Param"
name = "vpc_id"
output_module_id = snapcd_module.networking.id
output_name = "vpc_id"
}
resource "snapcd_module_input_from_output" "private_subnet_ids" {
module_id = snapcd_module.compute.id
input_kind = "Param"
name = "private_subnet_ids"
output_module_id = snapcd_module.networking.id
output_name = "private_subnet_ids"
}
The input_kind controls how the value is delivered to Terraform:
Param— injected as a Terraform variable (written to a.tfvarsfile). Use this when your Terraform code declares a matchingvariableblock.EnvVar— injected as an environment variable. Use this for values that configure provider authentication or backend settings (e.g.,ARM_SUBSCRIPTION_ID).
This replaces terraform_remote_state entirely. Modules don't need to know each other's backend configuration. They declare what they produce and what they consume — Snap CD handles the wiring.
When two modules share many outputs, snapcd_module_input_from_output_set wires all outputs from the producer in a single resource — no need to declare each one individually.
Literal values
Static configuration values that don't come from another module or a secret:
resource "snapcd_module_input_from_literal" "environment" {
module_id = snapcd_module.compute.id
input_kind = "Param"
name = "environment"
literal_value = "production"
}
resource "snapcd_module_input_from_literal" "instance_count" {
module_id = snapcd_module.compute.id
input_kind = "Param"
name = "instance_count"
literal_value = "3"
type = "NotString"
}
The type attribute defaults to "String". Set it to "NotString" for numbers, booleans, lists, or maps — this tells Snap CD to pass the value unquoted so Terraform interprets it as the correct type.
Secrets
Secrets are stored encrypted in Snap CD's secret store and injected at runtime. See Managing Secrets in Terraform for the full picture. The binding looks like:
resource "snapcd_module_input_from_secret" "db_password" {
module_id = snapcd_module.database.id
name = "db_password"
secret_id = data.snapcd_module_secret.db_password.id
input_kind = "Param"
}
Secrets are scoped — a secret bound to the database module is never visible to the networking or compute modules.
Namespace-level inputs
When multiple modules in a namespace share common inputs — a subscription ID, a region, a shared tag set — you can define inputs at the namespace level:
resource "snapcd_namespace_input_from_secret" "arm_subscription_id" {
namespace_id = snapcd_namespace.platform.id
name = "ARM_SUBSCRIPTION_ID"
secret_id = data.snapcd_namespace_secret.subscription_id.id
input_kind = "EnvVar"
usage_mode = "UseByDefault"
}
The usage_mode controls inheritance:
UseByDefault— every module in the namespace receives this input automatically unless it declares its own override.UseIfSelected— the input is available but only applied to modules that explicitly opt in.
This eliminates the need to repeat the same credential or configuration binding on every module in a namespace. Namespace inputs also support literals (snapcd_namespace_input_from_literal) and definition values (snapcd_namespace_input_from_definition).
Definition values
Snap CD can inject its own metadata — module IDs, namespace names, source revisions — as inputs to your Terraform code:
resource "snapcd_module_input_from_definition" "module_name" {
module_id = snapcd_module.compute.id
input_kind = "Param"
name = "snapcd_module_name"
definition_name = "ModuleName"
}
Available definition values include ModuleId, ModuleName, NamespaceId, NamespaceName, StackId, StackName, SourceUrl, SourceRevision, and SourceSubdirectory. This is useful for tagging resources with their Snap CD provenance or for conditional logic based on which module is being deployed.
Change propagation
Once modules and their inputs are defined, Snap CD handles the deployment lifecycle automatically:
Automatic ordering. Snap CD knows that compute and database depend on networking. It will never apply compute before networking has successfully completed.
Parallel execution. Compute and database both depend on networking but not on each other. Snap CD runs them in parallel once networking completes.
Cascading changes. When networking's outputs change — say you add a subnet — Snap CD automatically queues compute and database for re-planning. If the new plan has changes, it either auto-applies (if configured) or waits for approval. If compute's outputs are unchanged, downstream modules that depend on compute are skipped entirely.
Source-triggered plans. When a commit is pushed to a module's source repository, Snap CD detects the change and triggers a new plan. If the plan produces output changes, dependents cascade as above.
networking
├──► compute ──► application
└──► database
A commit to infra-networking that changes a subnet triggers this cascade:
- Networking re-plans and applies.
- Compute and database re-plan in parallel.
- If compute's outputs change, application re-plans after compute finishes.
- If compute's outputs are unchanged, application is skipped.
No scripts. No parameter stores. No terraform_remote_state.
Compared to the alternatives
terraform_remote_state |
Parameter store | Wrapper scripts | Terragrunt | Snap CD | |
|---|---|---|---|---|---|
| Backend coupling | Yes | No | No | Partial | No |
| Change detection | No | No | No | No | Automatic |
| Ordering enforcement | No | No | Manual | Automatic | Automatic |
| Parallelism | N/A | N/A | Manual | Automatic | Automatic |
| Approval gates | No | No | DIY | No | Built-in |
| Scoped permissions | No | No | No | No | Built-in |
| Persistent visibility | No | No | CI logs | No | Dashboard |
| Extra infrastructure | No | Yes | No | No | Yes |
Getting started
If you have a monolithic Terraform state today, the path to modular deployments is:
- Identify boundaries — group resources by team, lifecycle, and credential scope.
- Split the state — use
terraform state mvto migrate resources to new roots (see Splitting a Terraform Monolith). - Define modules in Snap CD — one
snapcd_moduleper root. - Wire the dependencies —
snapcd_module_input_from_outputfor cross-module values,snapcd_module_input_from_secretfor credentials. - Remove the glue — delete
terraform_remote_stateblocks, wrapper scripts, and CI pipeline steps.
From that point on, Snap CD manages the dependency graph, propagates changes, and keeps your infrastructure in sync.
See also
- The Problem with Large Terraform States — diagnosing when it's time to split
- Splitting a Terraform Monolith — approaches to breaking a monolith into smaller states
- Self-Hosted Terraform Runners with Credential Isolation — how Runners provide per-Module credential isolation
- Managing Secrets in Terraform — scoping and injecting secrets per Module
- A Permission System Built for Infrastructure — granular RBAC across the Stack/Namespace/Module hierarchy
- An Extensive Supporting Toolset — the Terraform provider and broader tooling