Skip to content

Stacks & Components

IaC organizes resources into a hierarchy of component types - from individual cloud primitives up to full multi-stack compositions. Understanding this hierarchy clarifies what a Terraform project, a module, and a “stack” actually are, and how they relate to each other.


IaC components are organized into four levels of scope, each aggregating the level below it:

IaC Component Hierarchy
LevelComponentWhat it isTool examples
4 - HighestCompositionA collection of stacks organized around a workload concern; defines inter-stack dependencies and configPulumi projects, Terraform stacks, Terragrunt services, Atmos stacks
3Deployment StackA complete, independently deployable unit of infrastructure resourcesCloudFormation stacks, CDK stacks, Pulumi stacks, Terraform projects/workspaces
2Code LibraryReusable code patterns projectsTerraform modules, Bicep modules, CDK L3 constructs, CloudFormation modules
1 - LowestPrimitive ResourceThe smallest independently provisionable unitaws_instance, google_compute_instance, CDK L1/L2 constructs

A composition is a collection of deployment stacks organized around a workload-relevant concern. It defines:

  • Configuration values for the stacks it contains
  • Integration points and dependencies between those stacks
  • Lifecycle orchestration - how stacks are deployed in relation to each other

Compositions should be structured to make sense to the teams configuring and managing the applications - not to the infrastructure engineers who built the underlying primitives.


A stack is the architectural quantum of infrastructure - a highly cohesive, independently deployable collection of resources. How resources are grouped and sized within a stack directly determines how easily the system can be delivered, updated, and operated.

Stack Lifecycle

Every stack progresses through four stages from code to live infrastructure:

StageWhat happensArtifact
Stack ProjectEngineers write the raw IaC codeSource files in a repo
Assemble (Build)Tool imports libraries, plugins, and dependenciesBuild artifact - directory, branch, or archive
Compile (Model)Build is compiled with instance-specific config into a desired stateIn-memory state model
Execute (Instance)Tool calls IaaS API to provision or modify real resourcesLive infrastructure used by workloads

This maps directly to the terraform initterraform planterraform apply flow:

  • init = Assemble
  • plan = Compile (generates desired state, compares to current)
  • apply = Execute

Code libraries group infrastructure resources so they can be shared and reused across multiple stack projects. In Terraform, these are modules.

Libraries are resolved during the Assemble step - they can live alongside the stack project or be imported from an external registry (Terraform Registry, Git, S3, GCS).

In tools like Terraform and OpenTofu - which have strong library support but no native composition or deployment-unit abstraction - teams often implement an entire deployable stack as a code library. This is the Stack Module pattern:

  1. A stack module contains all the resource definitions for a complete deployable unit
  2. A wrapper stack project imports the module and provides instance-specific configuration
  3. The wrapper’s only job is to call the module and pass variables - it contains no resources of its own

This pattern separates reusable infrastructure logic from environment-specific deployment config.


Sharing infrastructure code reduces maintenance surface area but creates coupling. Every reuse decision is a trade-off.

The primary mechanism for code sharing is the Don’t Repeat Yourself (DRY) principle, implemented through libraries (Terraform modules).

BenefitTrade-off
Single source of truth for shared logicChanges to a library impact every stack that uses it
Less code to maintainBreaking changes cascade across all consumers
Enforces consistent patternsRequires versioning discipline and retesting
Avoid Hasty Abstractions

Avoid “hasty abstractions.” The DRY principle applies to higher-level knowledge, not just raw code. Wrapping an aws_instance in a generic virtual_server module that still requires all the original configuration options adds complexity without value. Instead:

  • Create purpose-built modules (like java_application_server) only where configurations are genuinely identical across consumers
  • Use raw resources for unique or highly varied infrastructure

Beyond sharing libraries, teams can share entire stack projects - a single codebase deployed multiple times to create separate live instances:

  • Write one stack project for a Cloud SQL database
  • Deploy it once for the menu service, once for the ordering service
  • Inject different configuration (size, tuning, region) per instance via variables

This is also how multi-environment deployments work: the same stack project produces dev, staging, and production instances with different config.

Beyond sharing code, teams must decide whether to share live infrastructure instances across workloads (e.g., multiple services using one shared VPC).

ApproachWhen to use
Shared instanceOnly when sharing actively enables services to interact (e.g., a common network for service-to-service communication)
Dedicated instances (shared-nothing)Default choice - avoids coordination friction, scaling contention, and deployment coupling

Rule of thumb: Reuse stack projects freely to stamp out independent instances. Only share a live instance if it enables the consumers to communicate with each other.


Borrowed from distributed computing, shared-nothing is a design strategy where new nodes can be added without creating contention for shared resources. Applied to IaC, it means provisioning dedicated infrastructure instances for each service rather than routing everything through shared components.

Shared Nothing

Traditional “Iron Age” infrastructure made sharing necessary - duplicating physical hardware was expensive and led to waste. Cloud IaaS eliminates this constraint:

  • Virtual infrastructure is cheap and fast to duplicate
  • Auto-scaling resizes resources to match actual usage
  • No physical waste from provisioning dedicated instances

The result: systems of all sizes benefit from removing shared infrastructure. This is especially critical at scale - a telecommunications provider adding dozens of service instances per minute cannot afford contention on shared resources.


The primary purpose of infrastructure is to run workloads. This makes the workload - not the technology tier - the starting point for design.

ApproachHow it’s structuredThe problem
Horizontal (antipattern)One stack for all compute, one for all databases, one for all networkingA single team’s DB config change requires modifying a stack shared with every other team → central bottleneck
Vertical (recommended)Each service’s compute, database, and networking provisioned together in a dedicated stackTeams change, test, and deploy their infrastructure independently - no cross-team coordination required
Horizontal Antipattern

Horizontal design doesn’t just create code duplication - it creates scope-of-risk and ownership problems. When a shared database stack breaks during one team’s deployment, every team’s service is affected.

Vertical Recommended

When applying workload-centric design across multiple environments, infrastructure naturally organizes into layers:

LayerWhat it containsExample
Dedicated workloadInfrastructure for a single specific serviceApp-specific Cloud SQL, GKE deployment, service account
Shared infrastructureComponents used by multiple workloadsShared GKE cluster, shared VPC
EnvironmentOverarching environment-wide resourcesEnvironment-wide network routing, DNS zones
GlobalCross-environment resourcesOrganization-wide IAM policies, governance controls

Two design principles govern where resources belong:

  1. Implement at the most specific level - a workload’s load-balancer rules belong in its dedicated stack, not in the environment stack
  2. Lower levels must not know about higher levels - an environment stack should never contain configuration that references specific workloads (this would create circular provider→consumer dependencies)
6 Step Design Workflow

Start at the runtime context and work backward to the code:

  1. Workload - understand the software that will run on this infrastructure
  2. Compositions - design how stacks integrate to support the workload
  3. Stacks - organize resources into independently deployable units
  4. Stack project code - define the code projects to build and deploy the stacks
  5. Code libraries - identify reusable modules to share across projects
  6. Resources - write the underlying IaaS resource definitions

The goal of sizing stacks is not to make them as small as possible - it is to balance design forces like cohesion and coupling so that each stack aligns cleanly with the workloads it supports.

PatternScopeTypeBest for
Full System StackAll infrastructure in one stackPatternSimple systems, tightly coupled resources
Monolithic StackToo many loosely related resources in one stackAntipatternNothing - split it
Application Group StackAll infrastructure for a team’s service groupPatternSmall teams owning related services
Single Service StackOne stack per deployable servicePatternMicroservice architectures
Micro StacksOne service split across multiple stacksPatternIsolating compute from stateful resources
Shared StackInfrastructure used by multiple workloadsPatternNetworking, clusters, platform services

All infrastructure for a complete system lives in a single deployment stack. This avoids the overhead of orchestrating multi-stack deployments and works well when:

  • The infrastructure estate is simple (single application, straightforward resources)

  • The entire stack deploys fast

  • Resources within it change together frequently

  • Infrastructure is entirely shared by all workloads (e.g., one network topology serving everything)

    Full System Stack

A full system stack that has grown beyond its useful scope - containing an unmanageably large number of loosely related resources with low cohesion.

Symptoms:

  • Codebase is hard to understand or debug

  • Fixes cause new, unrelated problems

  • Frequent deployment conflicts from multiple people or teams working simultaneously

  • CI builds take too long to run

    Monolithic Stack

Why it’s dangerous: combining resources for different workloads creates high coupling. A change for one workload risks breaking another. Slow test-fix-test cycles lead to poorer code quality and less frequent, riskier deployments.

Resolution: look for natural “seams” - boundaries between workloads, ownership domains, or change frequencies - and split the monolith into multiple smaller stacks or an infrastructure composition.

All infrastructure for a group of related applications managed by a single team lives in one stack. This pattern aligns infrastructure ownership with organizational structure.

Application Group Stack
AdvantageRisk
Simplifies tooling (fewer stacks to manage)Designing around team ownership makes reorganization difficult
Single team can deploy and iterate quicklyIf a service transfers to another team, the stack must be split
Useful stepping stone away from a monolithCan organically grow into a monolithic stack

Each deployable application component gets its own dedicated stack - the strictest workload-aligned boundary.

  • Blast radius: limited to exactly one service

  • Autonomy: teams fully own the infrastructure for their software

  • Best for: microservice architectures

    Single Service Stack

The infrastructure for a single service is split across multiple deployment stacks - for example, separate compute, storage, and networking stacks for one application.

When to use: deployment and runtime concerns demand different change frequencies or risk profiles. A team might want to frequently patch application servers (compute) without risking accidental data loss (storage).

Micro Stacks
BenefitTrade-off
Individual stacks are smaller and simplerMore moving parts to test, deliver, and integrate
Change isolation between tiersRequires cross-stack integration and orchestration
Stateful resources protected from compute churnHigher total operational complexity

If multiple workloads use similar micro stacks (e.g., standard databases), a single reusable project can provision all of them.

A stack that provisions infrastructure exclusively for use by multiple workloads - containing no resources specific to any single workload.

Shared Stack

Ideal for:

  • Connectivity: networking, VPCs, messaging infrastructure
  • Platform services: monitoring, logging, observability
  • Multi-tenant hosting: container clusters, serverless platforms

Organizations frequently need to deploy multiple instances of the same infrastructure - separate environments (dev, test, prod), geographic replicas, or per-customer instances. Three strategies exist; two are antipatterns.

StrategyTypeKey characteristic
Multi-Environment StackAntipatternAll environments in one stack
Snowflakes as CodeAntipatternSeparate codebase per environment
Reusable StackPatternOne codebase, many instances

All environments (dev, test, production) are defined and managed within a single deployment stack.

Why teams do it: for teams starting with IaC, putting everything in one project feels like the most natural first step.

Multi Environment Stack

Why it’s dangerous: deployment tools operate at the stack level - the scope of any change encompasses everything within the stack. A coding error, an unexpected dependency, or a tool bug intended only for test can bring down production.

A completely separate stack source code project is created for every instance, even when those instances are meant to be identical replicas.

Snowflakes as Code

Why teams do it: it successfully isolates environments - changes to one project can’t break another. Copying and customizing an existing codebase is also the fastest way to spin up a new environment initially.

Why it fails at scale: the per-environment cost of ownership escalates rapidly. Applying a bug fix across all environments is time-consuming. Because of the manual effort, organizations fail to update all environments simultaneously, leading to configuration drift - dangerous inconsistencies that multiply between environments over time.

A single stack project codebase is used to create and update multiple distinct stack instances.

Reusable Stack
AspectHow it works
ConfigurationVariations (resource sizes, tuning, region) are injected via parameters - not by changing core code
ToolingTerraform workspaces, separate state files, or CloudFormation stack IDs
ConsistencyUpdating the core project rolls fixes and improvements to all instances
PipelinesAutomated infrastructure pipelines test and deploy changes to all instances within a short time frame, preventing drift
Complete Architecture