Cloud Infrastructure · Cloud Migration

Dependency Discovery from Observed Traffic and Configuration Graphs

Why declared configuration undercounts a system's real dependencies, what a flow record can prove, and how sampling and timeouts corrupt an inferred call graph.

A migration plan begins with an inventory, and the inventory is almost always wrong in the same direction. Configuration files, deployment manifests, and service registries record what someone intended a system to talk to. They do not record what it actually talked to. A service that resolves a hostname written into a properties file at startup, then caches the answer forever, may be reaching a database that appears nowhere in its declared dependencies. A batch job that shells out to a command-line client inherits that client’s configuration, which lives in a third place. The gap between declared and observed edges is where migrations fail, and closing it is a measurement problem rather than a documentation problem.

What a configuration graph can and cannot establish

A configuration graph is cheap to build and gives strong evidence for a narrow claim: this artifact was configured to use that endpoint. Its edges are typed by declaration, so a manifest naming a queue and a manifest naming a credential store look different, which is useful. The graph is also stable and readable, and it can be built before anything runs.

The weakness is that configuration describes capability, not use. Three cases account for most of the missing edges. First, indirection: a service reads a base URL from a config file, and the value of that file differs between environments, so the parsed edge points at a hostname that resolves differently in each. Second, inheritance: a process launched by a supervisor, scheduler, or init system picks up environment variables and network namespaces from its parent rather than from its own configuration. Third, transitive reach: a service calls a proxy, the proxy calls four backends, and only the proxy’s configuration mentions any of them. The service’s declared dependency list is correct and complete with respect to the service, and useless with respect to the migration.

A flow record is an assertion about packets, not about intent

Flow export protocols replace per-packet capture with a summarised record. In the IPFIX model defined by RFC 7011, an exporting process observes packets, aggregates them into flows keyed on selected fields, and sends templates describing the record layout followed by data records conforming to those templates. The key is configurable, which matters more than it first appears: the default five-tuple of source address, destination address, source port, destination port, and protocol identifies a conversation, not a dependency. Two different clients hitting the same server port produce two flows; one client hitting two replicas of the same service produces two flows that a naive reader will treat as two distinct dependencies.

The fixed-size NetFlow version 5 format carries a 24-byte header and 48-byte records, which bounds what each record can express: addresses, ports, protocol, TCP flags, counters, and timestamps, with no room for anything application-level. Version 9 and IPFIX moved to templates precisely so that exporters could add fields such as ingress interface, autonomous system numbers, and application identifiers. When the aggregation key includes an application identifier, the graph becomes far more useful, because the difference between a health check and a bulk data transfer is the difference between an edge that can be dropped and one that cannot.

Sampling breaks the tail of the distribution

Exporters sample because unsampled aggregation does not scale to high packet rates. Systematic sampling keeps one packet in every N; random sampling keeps each packet with probability 1/N. Either way the counters are scaled back up, and either way the correction is unbiased for volume and biased for presence.

Presence is what a dependency graph needs. A backup job that transfers a few hundred packets once a night will very likely be missed entirely by a 1-in-1000 sampler. The graph will then show no edge to the backup target, and the migration plan will omit a dependency that runs daily. The failure is silent, because a sampled flow record carries no marker distinguishing “observed zero packets” from “not observed”. This is the single most common way an inferred graph is confidently wrong: rare but essential traffic disappears, while chatty but optional traffic dominates the edge set.

Countermeasures are structural rather than statistical. Sample at the host rather than at the switch, since host-level capture sees every packet the workload emits and the volume is bounded by that workload. Run discovery over a window long enough to contain every periodic job at least twice, which means at minimum a week and, for monthly reports, a month. Accept that any observation window shorter than the longest period in the system cannot prove absence.

Timeouts decide what counts as one flow

A flow is not closed when the conversation ends; it is closed when the exporter decides it has ended. Two timers govern this. The inactive timeout closes a flow after no packets have been seen for some interval, commonly on the order of seconds to a minute. The active timeout forces a record out for a long-lived flow regardless of activity, commonly on the order of minutes, so that counters do not grow unbounded and consumers see progress.

Both timers corrupt inference in the same direction: one logical conversation can produce many records. A database connection held open for hours with intermittent queries will emit a record per active timeout period, and each record has the same five-tuple. Deduplicating on the tuple recovers the conversation; counting records instead inflates both the edge count and the apparent request rate. Worse, a connection that is idle longer than the inactive timeout is closed as a flow and then reopens, so a single persistent client-server pair can appear as hundreds of short-lived edges, which reads as churn that does not exist.

Turning edges into a directed graph with direction

The useful artifact is a directed graph whose vertices are workloads and whose edges are labelled with protocol, port, and an estimate of criticality. Building it requires three joins that are individually easy and jointly fiddly.

The first join maps addresses to workloads. Addresses change; a workload identity does not. In an environment with dynamic addressing, the mapping must be resolved as of the timestamp on the flow record, not as of the time of analysis, or the graph will attribute last week’s traffic to this week’s owner. The second join maps ports to services, which works when ports are registered and fails when they are ephemeral, so the server side of an edge should be identified by the destination port only when that port is below the ephemeral range. The third join collapses the graph to the granularity of the migration unit, which is rarely the host: two processes on one host that talk to each other move together, and the edge between them is irrelevant to the plan.

Direction matters for a reason that is easy to miss. An inbound edge and an outbound edge are not symmetric obligations. If service A calls service B, then moving A requires that A can still reach B, which may be as simple as routing. Moving B requires that every caller of B can still reach it, which requires enumerating the callers. The second obligation is strictly harder, and it is the one a graph built from the caller’s configuration will systematically under-report. This asymmetry is the reason discovery has to observe traffic at the destination as well as the source, and it feeds directly into how a workload’s cutover boundary is drawn.

Where the inferred graph is wrong even when capture is complete

Complete capture removes the sampling and timeout problems and leaves three sources of error that no amount of observation fixes.

Encrypted traffic hides the application protocol. A flow record for TLS on port 443 proves that two addresses exchanged bytes and proves nothing about which API was called. Reconstructing that requires either terminating TLS at the observation point, which changes the system being observed, or reading the server’s own logs, which reintroduces the configuration problem in a different form.

Control-plane traffic masquerades as dependency. Service discovery, metrics shipping, and log forwarding produce dense, regular edges between every workload and a small number of infrastructure endpoints. These edges are real and they are usually not migration dependencies, because the target environment supplies its own equivalents. Treating them as dependencies produces a graph where everything depends on everything, which is the same as no graph at all.

Finally, the observation window defines the graph’s validity, and the system keeps changing. An edge that existed during capture and has since been removed will still appear; an edge added after capture will not. The graph should therefore carry its window explicitly, and any plan derived from it should be re-measured rather than trusted once. Where the observation and the declared configuration disagree, the disagreement is the finding: it names a place where the system’s behaviour is not described by its artifacts, and those are exactly the places a cutover will surprise its authors. Recording the disagreement, rather than resolving it in favour of either source, is what makes the inventory usable when the plan reaches the point of copying data.

References

  1. RFC 7011: Specification of the IP Flow Information Export (IPFIX) Protocol — IETF
  2. RFC 3954: Cisco Systems NetFlow Services Export Version 9 — IETF
  3. pcap-savefile(5): PCAP capture file format — The Tcpdump Group, libpcap man pages