Route 53 sync + drift detection plan complete: 3 tasks, 4 files, all verification passing (22 new tests, tsc clean, full suite green aside from pre-existing unrelated itglue-search failures).
7.6 KiB
| phase | plan | subsystem | tags | dependency-graph | tech-stack | key-files | decisions | metrics | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud | 02 | aws-route53 |
|
|
|
|
|
|
Phase 24 Plan 2: Route 53 Sync + Drift Detection Summary
Built the Route 53 → Postgres mirror sync (Route53SyncService) with paginated
zone/record ingestion, soft-delete reconciliation, and whole-recordset drift
detection that writes sync_detected_drift history rows whenever a mirrored
record's live AWS value no longer matches Postgres.
What Was Built
lib/services/route53-record-key.ts — pure, dependency-free helpers:
buildRecordKey({ zoneId, name, type, setIdentifier })→${zoneId}:${name}:${type}:${setIdentifier ?? ''}, theroute53_records.record_keyprimary keynormalizeRecordSet(rs, zoneId)— lowercases name (preserving trailing dot), uppercases type, coerces missing TTL tonull, sortsresourceRecordsby value (so AWS's unordered list never produces phantom drift), passes throughaliasTargetrecordSetsEqual(a, b)— TTL + serialized resourceRecords + serialized aliasTarget comparison; both-null is equal, one-null is notclassifyDrift(prev, next)→'create' | 'update' | 'delete' | null, returningnullfor no material difference (no-op history row avoided)toHistoryPayload(ns)— whole-recordset JSONB snapshot forbefore_value/after_value
lib/services/route53-sync-service.ts — Route53SyncService class:
fullSync()/incrementalSync()both delegate toexecuteSync(), which runs the same two steps in both cases (Route 53 exposes no modification cursor, so "incremental" is the same full diff run more often — documented in a code comment per D-11)syncZones()— paginatesListHostedZonesCommand(Marker/IsTruncated/NextMarker), strips the/hostedzone/prefix from each zone id, fetchesGetHostedZoneCommandper zone forDelegationSet.NameServers(backs plan 24-04's D-12 NS-delegation check), upserts intoroute53_zones, then soft-deletes zones no longer returned by AWSsyncRecords()— for each live zone, paginatesListResourceRecordSetsCommand(StartRecordName/Type/Identifier), normalizes each recordset, loads the zone's current mirror rows, computes drift viabuildDriftHistoryRows()before upserting (so the pre-sync state is still available), inserts drift history rows in a transaction, then upserts records and soft-deletes anything no longer presentbuildDriftHistoryRows(prevByKey, nextByKey, zoneId)— exported pure function; unions both keyed maps, classifies drift per key viaclassifyDrift, skipsnull(no-change) keys, buildsDriftHistoryRow[]with whole-recordset before/after payloads- Initial-import guard: if a zone's mirror had zero rows before this sync, drift history generation is skipped entirely for that zone (logged once) — prevents flooding the append-only ledger with meaningless
createrows on first import sync_historybookkeeping:entity_type='route53',sync_typewritten as the literal'full'/'incremental'(matching the table's CHECK constraint), catastrophic-failure catch marks the rowstatus='failed'- All error logging uses
error instanceof Error ? error.message : String(error)— never the raw AWS SDK error object (T-24-03: avoids leaking$metadata/request headers into logs) - No
integration_settingscheck anywhere in the file (D-10: Route 53's disable toggle is display-only, must never gate sync) getRoute53SyncService()module-level singleton, followingpax8-sync-service.ts's lazy-instance pattern, exported for plans 24-05/24-06
Deviations from Plan
None — plan executed as written. Tasks 2 and 3 were combined into a single write of route53-sync-service.ts (see Decisions above) since the file only exists once either way; both tasks' acceptance criteria are independently verified below.
Verification
npx vitest run lib/services/route53-record-key.test.ts— 16 tests passednpx vitest run lib/services/route53-sync-service.test.ts— 6 tests passednpx tsc --noEmit --pretty— exits 0npm test(full suite) — 489 passed, 2 pre-existing failures inlib/services/analyzer/itglue-search.test.ts, unrelated to this plan's files and already logged indeferred-items.mdfrom plan 24-01 (neitheritglue-search.tsnor its test were touched by this plan)- Acceptance-criteria greps all confirmed:
integration_settingscount 0,entity_typepresent, both pagination loops present (IsTruncatedcount 2), both soft-delete statements present (is_deleted = truecount 2),sync_detected_driftliteral present,changed_by_user_idNULL for drift rows, initial-import guard present (prevByKey.size === 0),buildDriftHistoryRowscall precedes theINSERT INTO route53_recordsstatement in source order
Environment Note (not a code deviation)
This worktree had no node_modules directory. Symlinked it from the main
repo checkout (ln -s /opt/stacks/pulse/node_modules ./node_modules) so
@aws-sdk/client-route-53 types and vitest/tsc were available. This is a
local filesystem convenience, not a git-tracked change — node_modules is
.gitignored and no commit references it.
Self-Check: PASSED
lib/services/route53-record-key.ts— FOUNDlib/services/route53-record-key.test.ts— FOUNDlib/services/route53-sync-service.ts— FOUNDlib/services/route53-sync-service.test.ts— FOUND- Commit
c18271d(Task 1) — FOUND ingit log --oneline --all - Commit
d8c0912(Task 2) — FOUND ingit log --oneline --all - Commit
0acf1fa(Task 3) — FOUND ingit log --oneline --all