DEPENDENCIES → STACKIT · ARCHITECTURE
How to build a migration dependency graph
Start with the references your Terraform already states, let them fix the order of the target, then add by hand the links that live in application configuration. StackToIT reads the references between resources as edges, sorts the units into waves so that nothing moves before what it depends on, and says where the graph is blind. This page shows that on a web shop with eleven units, including one explicit depends_on that moves three units by two waves.
Three edge types, one of them explicit
A small estate written for this page and assessed by StackToIT on 4 November 2026. Left: the web server, its elastic IP and the DNS record of the input (lines 83 to 105; the comment sits in the input file). Right: the four edges of the graph in assessment.json that start at the server and at the DNS record, re-indented for reading.
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "m6i.large"
subnet_id = aws_subnet.web.id
vpc_security_group_ids = [aws_security_group.web.id]
# Nothing in the arguments refers to the database or the bucket:
# the application reads both from its configuration at run time.
depends_on = [aws_db_instance.shop]
}
resource "aws_eip" "web" {
instance = aws_instance.web.id
domain = "vpc"
}
resource "aws_route53_record" "www" {
zone_id = "Z0123456789EXAMPLE"
name = "www.shop.example.com"
type = "A"
ttl = 300
records = [aws_eip.web.public_ip]
}[
{
"from": "aws_instance.web",
"to": "aws_db_instance.shop",
"type": "DEPENDS_ON",
"via": "depends_on",
"explicit": true
},
{
"from": "aws_instance.web",
"to": "aws_security_group.web",
"type": "ATTACHED_TO",
"via": "vpc_security_group_ids"
},
{
"from": "aws_instance.web",
"to": "aws_subnet.web",
"type": "DEPLOYED_IN",
"via": "subnet_id"
},
{
"from": "aws_route53_record.www",
"to": "aws_eip.web",
"type": "DEPENDS_ON",
"via": "records"
}
]Three edge types
DEPLOYED_IN says where a resource lives (a subnet, a VPC). ATTACHED_TO says what it carries (a security group). DEPENDS_ON says it needs another resource to exist first. Each edge names the argument it was read from.
Explicit beats silent
The server refers to the database in no argument. The edge to the database exists only because depends_on says so, and the edge carries the flag explicit.
The record follows the address
The DNS record reads the public address of the elastic IP, so it sits behind it, which sits behind the server.
What the explicit link does to the waves
Two runs of the same estate, one with the depends_on line and one without. Waves are computed from the dependencies: a unit moves only after everything it depends on, and units in one wave can move in parallel.
| Unit | Wave with depends_on | Wave without |
|---|---|---|
| aws_s3_bucket.media | 1 | 1 |
| aws_vpc.shop | 1 | 1 |
| aws_security_group.web | 2 | 2 |
| aws_subnet.data_a | 2 | 2 |
| aws_subnet.data_b | 2 | 2 |
| aws_subnet.web | 2 | 2 |
| aws_security_group.db | 3 | 3 |
| aws_db_instance.shop | 4 | 4 |
| aws_instance.web | 5 | 3 |
| aws_eip.web | 6 | 4 |
| aws_route53_record.www | 7 | 5 |
The summary counts 14 dependencies between units with the line and 13 without. Without it the server shares wave 3 with the database security group and no longer waits for the database. The graph itself lists 15 edges for the first run because the database subnet group is part of the database unit, not a unit of its own.
Where each edge type comes from
| Edge type | Terraform argument that makes it | Seen in this run |
|---|---|---|
| DEPLOYED_IN | vpc_id, subnet_id, subnet_ids, db_subnet_group_name | Subnets and security groups in the VPC; the server in its subnet; the database in its subnet group (9 edges) |
| ATTACHED_TO | vpc_security_group_ids, ingress.security_groups | Both security groups on their resources; the database group admitting the web group (3 edges) |
| DEPENDS_ON | instance, records, depends_on | Elastic IP on the server, record on the elastic IP, server on the database (3 edges) |
Counts are from the first run. Which arguments count as references is decided per resource type by the engine.
The place the graph is blind
Same run. The DNS record names its zone with a literal identifier, so there is no reference to follow. Left: the record in the input. Right: the diagnostic the run wrote for it (its provenance field left out).
resource "aws_route53_record" "www" {
zone_id = "Z0123456789EXAMPLE"
name = "www.shop.example.com"
type = "A"
ttl = 300
records = [aws_eip.web.public_ip]
}[
{
"severity": "warning",
"message": "could not find the parent of aws_route53_record.www through attribute zone_id; assessed on its own",
"node_id": "aws_route53_record.www"
}
]Assessed on its own
The record is still classified, and its address change is listed as a cutover step. What is missing is its link to the zone, which the plan asks you to bind to a STACKIT DNS zone by variable.
The explicit link reaches the cutover step
With the depends_on line, the step that switches the database connection names the web server as a client: the source references the database from aws_instance.web. In the run without that line it says that no resource of the source references the database, that the applications are outside the source and that their owner lists them. The media bucket gets the second wording in both runs.
Building the graph, step by step
1. Run on the Terraform you have
Let the engine read the references. The result is the graph, the waves in migration/waves.csv and migration/technical-sequencing.md, and a drawing in architecture/dependencies.mmd.
2. Read the diagnostics
Open the graph diagnostics in assessment.json. A literal identifier, a data source or a value the run could not resolve means a missing edge; note each one.
3. Confirm the application groups
The run proposes groups from references and ingress rules, here one group of five units. The proposal changes nothing until you set review: confirmed in applications-template.yaml and pass the file back with --applications.
4. Add the runtime links
Ask each application owner which databases, buckets, queues and APIs the application reads from configuration. In this run the media bucket belongs to no group and sits in wave 1: the Terraform links it to nothing, so whether the shop reads it is a question only its owner can answer.
5. Re-run and compare
After adding explicit links, run again and compare waves.csv. A link that moves nothing is either redundant or on the wrong unit.
6. Turn waves into a plan
The waves are a starting point for the architect's wave plan, not a schedule. Add data sizes, freeze windows and the cutover order on top.
Questions about dependency mapping
Does StackToIT discover dependencies from running systems?
No. It reads Terraform, OpenTofu and RVTools input. Dependencies that exist only at run time, such as a connection string in a configuration file, are not in the graph; you add them through the application groups and the decisions.
What does an explicit depends_on change besides the waves?
In this run it also changed the cutover plan. With the line, the database connection step names the web server as a known client; without it, the step says the applications are outside the source. A depends_on that mirrors a real runtime link therefore documents a client in the plan, not only an order.
Are waves the order of the cutover?
No. They are the order in which units can be built or moved because of their dependencies. The cutover facts, such as DNS and address re-binding, are listed separately in technical-sequencing.md.
Why does a unit with no references land in wave 1?
It has nothing to wait for. The media bucket is in wave 1 with the VPC. That says the Terraform links it to nothing, not that nothing uses it.
Sources
Your next move: STACKIT.
Tell us about your estate. We show you what StackToIT delivers for it: target architecture, Terraform artifacts and a structured migration plan.