DNS seed provider
이 콘텐츠는 아직 번역되지 않았습니다.
DnsSeedProvider resolves seeds via DNS at startup. Two modes:
| Mode | Lookup | Result |
|---|---|---|
| SRV records | _actor-ts._tcp.example.com | Service + port from records |
| A records | actor-ts.example.com | IPs only; port comes from config |
SRV is the more flexible choice (service-discovery-style); A records work for simpler setups.
import { Cluster, ClusterOptions, DnsSeedProvider, DnsSeedProviderOptions } from 'actor-ts';
const dnsSeedProviderOptions = DnsSeedProviderOptions.create() .withHostname('_actor-ts._tcp.example.com') .withSystemName('my-app') .withUseSrv();const provider = new DnsSeedProvider( dnsSeedProviderOptions,);
const seeds = await provider.lookup();const clusterOptions = ClusterOptions.create() .withHost(host) .withPort(port) .withSeeds(seeds);await Cluster.join(system, clusterOptions);Configuration
Section titled “Configuration”type DnsSeedProviderOptionsType = { hostname: string; // SRV record name (or A-record hostname) systemName: string; // stamped on discovered node addresses port: number; // paired with each IP in A-record mode useSrv?: boolean; // prefer SRV records (which carry a port) over A cacheTtlMs?: number; // in-process TTL cache; default 60_000 ms, 0 disables pinnedAddresses?: readonly string[]; // accept only these; unset = accept all log?: (message: string, error?: unknown) => void; // reports pinned-out addresses // Override the DNS functions (default: node:dns/promises) — mainly for tests: resolve?: (hostname: string) => Promise<string[]>; resolveSrv?: (hostname: string) => Promise<Array<{ name: string; port: number }>>;};SRV mode (recommended)
Section titled “SRV mode (recommended)”const dnsSeedProviderOptions = DnsSeedProviderOptions.create() .withHostname('_actor-ts._tcp.my-app.example.com') .withSystemName('my-app') .withUseSrv();new DnsSeedProvider( dnsSeedProviderOptions,);SRV records carry host, port, weight, priority per entry.
The provider returns host:port for every entry the DNS query
returned.
To create the SRV records:
_actor-ts._tcp.my-app.example.com. IN SRV 10 100 2552 node-1.my-app.example.com._actor-ts._tcp.my-app.example.com. IN SRV 10 100 2552 node-2.my-app.example.com._actor-ts._tcp.my-app.example.com. IN SRV 10 100 2552 node-3.my-app.example.com.Plus matching A records for the hostnames. Most DNS-SD service-registration tools (Consul, Eureka with DNS plugin) publish SRV records automatically.
A-record mode
Section titled “A-record mode”const dnsSeedProviderOptions = DnsSeedProviderOptions.create() .withHostname('actor-ts.example.com') .withSystemName('my-app') .withPort(2552);new DnsSeedProvider( dnsSeedProviderOptions,);For setups without SRV — DNS only carries IPs. The provider queries A records and pairs each with the configured port.
A records typically point at a load balancer or a round-robin DNS list; multiple A entries mean multiple seed candidates.
Pinning resolved addresses
Section titled “Pinning resolved addresses”Whatever DNS returns is an answer from a party the node never
authenticated. pinnedAddresses bounds what that answer is
allowed to say: addresses outside the list are discarded before
they are offered to the cluster as seeds.
const dnsSeedProviderOptions = DnsSeedProviderOptions.create() .withHostname('actor-ts.example.com') .withSystemName('my-app') .withPort(2552) .withPinnedAddresses(['10.0.0.0/8']) .withLog((message) => logger.warn(message));Entry shape depends on the mode, because the two modes resolve to different things:
| Mode | Resolves to | Pin with |
|---|---|---|
| A records | IP addresses | CIDRs — '10.0.0.0/8', '2001:db8::/32' |
| SRV records | Target hostnames | Host suffixes — 'svc.cluster.local' |
A suffix matches on a label boundary, so svc.cluster.local
admits pod-1.svc.cluster.local and the apex itself, but not
evilsvc.cluster.local. A CIDR is never consulted for a
hostname and a suffix is never consulted for an IP, so mixing
both shapes in one list is fine — useful when the same config
serves both modes.
A list that cannot match anything in the configured mode is rejected at construction rather than silently discarding every record:
const dnsSeedProviderOptions = DnsSeedProviderOptions.create() .withHostname('_actor-ts._tcp.example.com') .withSystemName('my-app') .withUseSrv() .withPinnedAddresses(['10.0.0.0/8']);// Throws OptionsError — SRV targets are hostnames, so this list// would drop every record and look like an empty DNS answer.new DnsSeedProvider(dnsSeedProviderOptions);The same guard rejects a bare '10.0.0.1' (write '10.0.0.1/32'),
an empty list, and a malformed CIDR.
Addresses are compared in canonical form only
Section titled “Addresses are compared in canonical form only”Both sides of the comparison — the CIDRs you pin and the addresses
the resolver hands back — must be canonical: decimal octets
with no leading zero, no 0x, no exponent, no whitespace, and a
plain decimal prefix length.
This is not pedantry. 1e1.0.0.1, 010.0.0.1 and 0x0a.0.0.1
all read as 10.0.0.1 to a lenient number parser, but the socket
layer scores them as not an IP and resolves them through DNS
instead — so an answer in one of those spellings would clear a
10.0.0.0/8 pin and then connect wherever the attacker’s resolver
pointed. Anything non-canonical is now treated as a hostname, and
a hostname is never matched against a CIDR pin.
Two consequences worth knowing when upgrading:
- A pin entry written non-canonically (
'010.0.0.0/8') throws at construction instead of pinning a network you did not write. - A trailing-slash typo (
'10.0.0.0/') throws as well. It used to parse as/0— a pin meant to admit one network admitting the entire address space.
An all-numeric host suffix ('0.1') is rejected too: suffix
matching is string comparison, so such an entry would match the
tail of unrelated addresses. Pin a CIDR instead.
Dropped addresses are filtered, not fatal — one stale record
costs that record, not the whole bootstrap. Each drop goes to
log, which is worth wiring up: an over-tight pin list and an
empty DNS answer look identical from the outside, and an empty
seed list means the node forms its own single-node cluster.
When to use it
Section titled “When to use it”| Environment | Use |
|---|---|
| Consul-managed deployments | SRV mode — Consul writes them automatically. |
| Eureka with DNS plugin | SRV mode. |
| Manual deployments with DNS server | Either mode; SRV preferred. |
| Cloud with DNS-based service discovery | SRV mode. |
| K8s | Use KubernetesApiSeedProvider — K8s’s headless services do produce A records but the K8s API provider is more reliable. |
DNS caching
Section titled “DNS caching”lookup() isn’t one-shot — the bootstrap layer calls it at
startup, and higher-level retry logic may call it again. To keep
repeated calls from hammering DNS, the provider keeps a
per-instance in-memory TTL cache:
const dnsSeedProviderOptions = DnsSeedProviderOptions.create() .withHostname('_actor-ts._tcp.example.com') .withSystemName('my-app') .withUseSrv() .withCacheTtlMs(30_000); // default 60_000 ms; 0 disables the cache- Calls within the
cacheTtlMswindow (default 60 000 ms) are served from cache without touching DNS. - The first call after the TTL expires re-queries DNS.
- A failed lookup is not cached — it retries on the next call.
Once Cluster.join completes, the cluster’s gossip layer takes
over membership tracking, so DNS is off the hot path. This
means:
- A node added after startup isn’t visible to existing peers via DNS. It joins via its own DNS lookup, contacts an existing seed, gossip propagates.
- DNS record TTLs matter only when a lookup actually hits DNS (startup and post-expiry re-queries), not for the cluster’s steady-state runtime.
When NOT
Section titled “When NOT”Where to next
Section titled “Where to next”- Discovery overview — the bigger picture.
- Config seed provider — the simpler alternative.
- Kubernetes API seed provider — preferred on K8s.
- Aggregate seed provider — combine DNS with a static fallback.
