Grouping Infrastructure with Stacks
Stacks enable the grouping of related infrastructure components and the ability to orchestrate their deployment across multiple environments with sophisticated dependency management. This guide walks through building a production-grade deployment pipeline using Stacks.
Why Use Stacks?
Stacks allow for giving a name to a group of directories and workspaces and defining rules around when those groups may be operated on relative to other groupings.
Some example use cases:
- You have a development and production environment and these environments may be planned together but production can only be applied after development has been applied.
- You want to automatically perform an operation after applying a change, for example automatically running an Ansible script.
- Directory B depends on directory A, and B should be planned and applied if directory A changes.
Core Concepts
Stacks are defined in the stacks
section of the configuration file.
- There are two types of stacks:
- A stack which is a collection of directories and workspaces, these are defined using a tag query.
- A stack which is a collection of other stacks, these are defined by specifying a list of stacks.
- A directory and workspace can be part of one, and only one stack.
- Stack rules are transitive. That is, if stack C requires B to be applied before planning, and B requires A to be applied before planning, if a change modifies A and C but not B, then the relationship between C and A is maintained: C cannot be planned until A is applied.
- Stacks can define variables.
- These variables can be accessed in the
workflows
section of the configuration file. - These variables are available in the pipeline run as environment variables.
Building Your First Stack Architecture
Let’s build a real-world deployment pipeline for a typical three-tier application deployed across multiple environments.
Step 1: Define Your Infrastructure Components
Start by organizing your Terraform modules into logical groups:
infrastructure/├── ansible/ # Perform operations on infrastructure├── base/│ └── networking/ # VPCs, subnets, security groups├── dev/│ ├── database/ # RDS instances│ ├── compute/ # ECS/EKS clusters│ └── application/ # Application deployments├── staging/│ ├── database/│ ├── compute/│ └── application/└── production/ ├── database/ ├── compute/ └── application/
Step 2: Create Environment Stacks
Begin with basic environment separation. Each environment becomes a stack with its own configuration. Importantly, these rules only take effect when multiple stacks are modified. For example, modifying dev
does not force staging
to be run, however if dev
and staging
are modified in the same change, then staging
can only be applied after dev
.
dirs: 'dev/**': tags: [dev] 'staging/**': tags: [staging] 'production/**': tags: [production]
stacks: names: dev: tag_query: 'dev' variables: environment: development rules: auto_apply: true # Auto-apply for rapid development
staging: tag_query: 'staging' variables: environment: staging rules: apply_after: - dev # Can only apply after dev succeeds
production: tag_query: 'production' variables: environment: production rules: apply_after: - staging # Requires staging to be applied first
workflows: - tag_query: '' environment: '${environment}'
This configuration ensures changes flow from dev → staging → production, but only when a change is made to each environment. It also makes use of stack variables for specifying the execution environment.
Step 3: Add Infrastructure Layers
The above configuration allows each component within the dev
, staging
, and production
stacks to be run at the same time. In reality, we likely want some ordering of operations within a stack, such as the database component should be run before the compute and compute component should be run before the application.
Now let’s add dependency management within each environment. This creates one stack per layer, and dev
, staging
, and production
stacks which nest the layers. It also moves the environment
variable to the nested stack, rather than defining it in each nested stack. Finally, it also moves the rules which define when the dev
, staging
, and production
stacks can be applied relative to each other into each stack’s definition.
stacks: names: # Development layers dev-database: tag_query: 'dir:dev/database' variables: layer: database
dev-compute: tag_query: 'dir:dev/compute' variables: layer: compute rules: plan_after: - dev-database # Must wait for database
dev-application: tag_query: 'dir:dev/application' variables: layer: application rules: plan_after: - dev-compute # Must wait for compute
dev: stacks: - dev-database - dev-compute - dev-application
variables: environment: development
rules: auto_apply: true
# Staging layers staging-database: tag_query: 'dir:staging/database' variables: layer: database
staging-compute: tag_query: 'dir:staging/compute' variables: layer: compute rules: plan_after: - staging-database # Must wait for database
staging-application: tag_query: 'dir:staging/application' variables: layer: application rules: plan_after: - staging-compute # Must wait for compute
staging: stacks: - staging-database - staging-compute - staging-application
variables: environment: staging
rules: apply_after: - dev
# Production layers production-database: tag_query: 'dir:production/database' variables: layer: database
production-compute: tag_query: 'dir:production/compute' variables: layer: compute rules: plan_after: - production-database # Must wait for database
production-application: tag_query: 'dir:production/application' variables: layer: application rules: plan_after: - production-compute # Must wait for compute
production: stacks: - production-database - production-compute - production-application
variables: environment: production
rules: apply_after: - staging
workflows: - tag_query: '' environment: '${environment}'
Step 4: Shared infrastructure
Finally, there is some shared infrastructure, in the shared
directory, which impacts every stack. When the shared infrastructure changes, want to trigger runs on all of our downstream stacks, even if they do not have an explicit change. This is achieved using the modified_by
rule, which has defined in each dependent stack.
stacks: names: # Base infrastructure base-networking: tag_query: 'dir:base/networking'
base: stacks: - base-networking
# Development layers dev-database: tag_query: 'dir:dev/database' variables: layer: database
dev-compute: tag_query: 'dir:dev/compute' variables: layer: compute rules: plan_after: - dev-database # Must wait for database
dev-application: tag_query: 'dir:dev/application' variables: layer: application rules: plan_after: - dev-compute # Must wait for compute
dev: stacks: - dev-database - dev-compute - dev-application
variables: environment: development
rules: auto_apply: true modified_by: - base
# Staging layers staging-database: tag_query: 'dir:staging/database' variables: layer: database
staging-compute: tag_query: 'dir:staging/compute' variables: layer: compute rules: plan_after: - staging-database # Must wait for database
staging-application: tag_query: 'dir:staging/application' variables: layer: application rules: plan_after: - staging-compute # Must wait for compute
staging: stacks: - staging-database - staging-compute - staging-application
variables: environment: staging
rules: apply_after: - dev modified_by: - base
# Production layers production-database: tag_query: 'dir:production/database' variables: layer: database
production-compute: tag_query: 'dir:production/compute' variables: layer: compute rules: plan_after: - production-database # Must wait for database
production-application: tag_query: 'dir:production/application' variables: layer: application rules: plan_after: - production-compute # Must wait for compute
production: stacks: - production-database - production-compute - production-application
variables: environment: production
rules: apply_after: - staging modified_by: - base
workflows: - tag_query: 'stack_name:base' - tag_query: '' environment: '${environment}'
Step 5: Automatically perform Ansible operations on production after applying
If we have some post-apply operations to perform we can use modified_by
combined with auto_apply
to initiate the run of a stack automatically whenever another stack is modified. The auto_apply
configuration only performs an apply automatically if all apply requirements for that stack have passed, otherwise it waits for human input before continuing.
stacks: names: # Ansible ansible: tag_query: 'dir:ansible'
rules: auto_apply: true # Automatically apply if all apply requirements pass modified_by: - production
# Base infrastructure base-networking: tag_query: 'dir:base/networking'
base: stacks: - base-networking
# Development layers dev-database: tag_query: 'dir:dev/database' variables: layer: database
dev-compute: tag_query: 'dir:dev/compute' variables: layer: compute rules: plan_after: - dev-database # Must wait for database
dev-application: tag_query: 'dir:dev/application' variables: layer: application rules: plan_after: - dev-compute # Must wait for compute
dev: stacks: - dev-database - dev-compute - dev-application
variables: environment: development
rules: auto_apply: true modified_by: - base
# Staging layers staging-database: tag_query: 'dir:staging/database' variables: layer: database
staging-compute: tag_query: 'dir:staging/compute' variables: layer: compute rules: plan_after: - staging-database # Must wait for database
staging-application: tag_query: 'dir:staging/application' variables: layer: application rules: plan_after: - staging-compute # Must wait for compute
staging: stacks: - staging-database - staging-compute - staging-application
variables: environment: staging
rules: apply_after: - dev modified_by: - base
# Production layers production-database: tag_query: 'dir:production/database' variables: layer: database
production-compute: tag_query: 'dir:production/compute' variables: layer: compute rules: plan_after: - production-database # Must wait for database
production-application: tag_query: 'dir:production/application' variables: layer: application rules: plan_after: - production-compute # Must wait for compute
production: stacks: - production-database - production-compute - production-application
variables: environment: production
rules: apply_after: - staging modified_by: - base
workflows: - tag_query: 'stack_name:ansible' engine: name: custom plan: ['${TERRATEAM_ROOT}/bin/ansible-plan'] apply: ['${TERRATEAM_ROOT}/bin/ansible-apply'] - tag_query: 'stack_name:base' - tag_query: '' environment: '${environment}'
The ‘default’ Stack
All workspaces must map to exactly one stack. Terrateam will produce an error if a workspace matches more than one stack or zero stacks.
An special, implicit, stack called default
is defined where all workspaces that do not match an existing stack are assigned to. However, this behavior is overridden if a stack is explicitly defined named default
. In that case, the default
stack has the same properties as any other stack.