Staged Rollout Strategies for Multi-Tenant Schema Migrations
In the previous sections, we learned how to define target groups and deploy migrations to multiple tenant databases. In this section, we will explore deployment rollout strategies - a powerful feature that gives you fine-grained control over how migrations are applied across your tenant databases.
Overview
When deploying schema migrations to multiple tenant databases, you often need more control than simply applying migrations to all targets at once. Common requirements include:
- Canary deployments - Validate changes on a small subset of tenants before a broader rollout
- Regional rollouts - Deploy to US-West first, then US-East, then EU, minimizing blast radius
- Priority ordering - Migrate enterprise customers before SMB, or process tenants alphabetically
- Parallel execution - Speed up deployments by running migrations concurrently within each stage
- Error resilience - Log failures and continue with remaining tenants instead of stopping entirely
Atlas's deployment block addresses all these needs by organizing targets into groups with configurable
execution order, parallelism, and error handling.
The deployment Block
The deployment block defines a rollout strategy that can be referenced by one or more environments.
Basic Syntax
deployment "<name>" {
// Variables passed from the env block
variable "<var_name>" {
type = <type> // string, bool, number, etc.
default = <value> // Optional default value
}
// Groups define execution stages
group "<group_name>" {
match = <expr> // Boolean expression to filter targets
order_by = <expr> // Expression to sort targets within group
parallel = <number> // Max concurrent executions (default: 1)
on_error = FAIL | CONTINUE // Error handling mode
depends_on = [group.<other_group>] // Groups that must complete first
}
}
Connecting to an Environment
To use a deployment strategy, reference it in your env block using the rollout block:
env "prod" {
for_each = toset(var.tenants)
url = urlsetpath(var.url, each.value)
rollout {
deployment = deployment.staged
vars = {
name = each.value
}
}
}
Group Matching Behavior
By default, groups are evaluated in the order they appear in the configuration file. When a target matches multiple groups,
the first matching group wins - the target is assigned to it and skipped by subsequent groups. You can override
the execution order using the depends_on attribute.
This allows you to define specific groups first (e.g., canary, internal) followed by a catch-all group for remaining targets:
deployment "staged" {
variable "name" {
type = string
}
// First: Internal tenants (matched first by position)
group "internal" {
match = startswith(var.name, "internal-")
}
// Second: Canary tenants
group "canary" {
match = startswith(var.name, "canary-")
parallel = 10
depends_on = [group.internal]
}
// Last: Catch-all for remaining targets (no match = all unmatched)
group "rest" {
depends_on = [group.canary]
}
}