Skip to content

Dependency Graph

The dependency graph is the backbone of Kyku’s execution ordering. It ensures resources are created, updated, and destroyed in the correct sequence.

The GraphBuilder class in @kykucloud/core scans every resource property for BaseResource instances and builds a Directed Acyclic Graph (DAG).

// Object reference → creates edge
vm.network = myVpc;
// Result: Vm → Vpc edge
// Array of objects → creates edges
vm.securityGroups = [sg1, sg2];
// Result: Vm → sg1, Vm → sg2 edges
// String reference → NO edge
vm.network = "vpc-abc123";
// Result: no dependency created (use object references!)
Reference Type Edge Created? Example
BaseResource property ✅ Yes vm.network = myVpc
BaseResource[] property ✅ Yes vm.securityGroups = [sg1, sg2]
String (ID) ❌ No vm.network = "vpc-id"
Tags/labels (Record<string, string>) ❌ No tags: { env: "prod" }
Output references ✅ Yes vm.env.DATABASE_URL = db.outputs.endpoint

Important: Use object references (not string IDs) to ensure the dependency graph captures all edges.

Resources are assigned levels via topological sort:

Level 0: Vpc, SshKey (no dependencies)
Level 1: SecurityGroup, Subnet (depends on Vpc)
Level 2: Vm, Database (depends on Vpc, SG, Subnet)
Level 3: LoadBalancer (depends on Vm, Vpc, SG)

Resources at the same level execute in parallel. Level N waits for all levels < N to complete.

GraphBuilder.hasCycles() detects circular dependencies and throws a CycleError:

// Bad: circular reference
sgA.ingress = [{ sources: [sgB] }];
sgB.ingress = [{ sources: [sgA] }];
// CycleError: Circular dependency detected

Circuits with TargetGroup ↔ LoadBalancer are avoided by making only one direction reference the other.

Destroy follows the reverse of creation order. Resources at higher levels are destroyed first:

Destroy order: LoadBalancer → Vm → SecurityGroup → Vpc

The destroyOrder array in the CLI must list resources in this reverse order.

Use kyku graph to inspect and verify resource connectivity:

Terminal window
kyku graph # ASCII tree view with nodes, edges, and levels
kyku graph --dot # Graphviz DOT output
kyku graph -c ./my-config.ts # Inspect a specific config
Check How
No dangling references Every depends on: ID in the tree output refers to a node that appears elsewhere in the graph
No orphan resources Every node has at least one dependency OR at least one dependent (unless standalone like SshKey)
Create order correct Roots (level 0) appear first, leaves appear last
Destroy order matches levels Compare destroyOrder against graph output
No unintended cycles Detected automatically — CycleError thrown at validation time
Auto-subnet injection After injectAutoSubnets(), graph shows synthetic Subnet nodes under each Vpc
Terminal window
kyku graph --dot > graph.dot
dot -Tsvg graph.dot > graph.svg # Color-coded SVG
dot -Tpng graph.dot > graph.png # PNG for docs

Dot output assigns distinct shapes and colors per resource type:

  • Vpc = folder shape, blue
  • Vm = box shape, green
  • SecurityGroup = hexagon, orange
  • Subnet = ellipse, purple
  • LoadBalancer = diamond, red
  • Database = cylinder, yellow

After the user’s resources are collected, the engine injects synthetic Subnet resources for each Vpc with distributeAcrossAzs > 0:

// Vpc with distributeAcrossAzs: 2
// Creates 4 synthetic Subnet resources:
// vpc-main-subnet-public-0 (level 1, depends on Vpc)
// vpc-main-subnet-public-1 (level 1, depends on Vpc)
// vpc-main-subnet-private-0 (level 1, depends on Vpc)
// vpc-main-subnet-private-1 (level 1, depends on Vpc)

These subnets are first-class resources in the graph and state file. VMs are automatically wired to private subnets (or public if they have public SG rules).

class GraphBuilder {
// Entry point
build(resources: BaseResource[]): Graph
// Edge detection
private scanForReferences(resource: BaseResource): string[]
private isBaseResource(value: unknown): boolean
// Topological sort
private assignLevels(edges: Edge[]): Map<string, number>
// Cycle detection
hasCycles(): boolean
// Output
toAscii(): string
toDot(): string
}
interface Graph {
nodes: Node[]
edges: Edge[]
levels: Map<string, number>
}
interface Node {
id: string
type: ResourceType
name: string
}
interface Edge {
from: string // dependent
to: string // dependency
}