惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Microsoft Azure Blog
Microsoft Azure Blog
有赞技术团队
有赞技术团队
IT之家
IT之家
博客园 - 聂微东
Jina AI
Jina AI
Hugging Face - Blog
Hugging Face - Blog
Last Week in AI
Last Week in AI
Apple Machine Learning Research
Apple Machine Learning Research
WordPress大学
WordPress大学
小众软件
小众软件
爱范儿
爱范儿
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
Visual Studio Blog
雷峰网
雷峰网
酷 壳 – CoolShell
酷 壳 – CoolShell
阮一峰的网络日志
阮一峰的网络日志
宝玉的分享
宝玉的分享
博客园 - 三生石上(FineUI控件)
大猫的无限游戏
大猫的无限游戏
博客园 - Franky
量子位
月光博客
月光博客
博客园 - 【当耐特】
博客园 - 叶小钗

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
The bug that made my Terraform drift detector cry wolf (a...
ひとし 田畑 · 2026-06-15 · via DEV Community

ひとし 田畑

terraform plan tells you what Terraform changed. It says nothing about the
RDS parameter someone tweaked in the console at 2am, or the security group rule
added by hand during an incident. To catch that kind of drift, you have to
compare your tfstate against what AWS actually returns from the API — yourself.

I built exactly that, and the first version was useless. Not because the diff
was wrong, but because it screamed about drift on every single resource, every
single time. Here's the bug, and why the fix is one line.

The naive approach

The idea is simple. For each resource you have two dicts:

  • old — the attributes from your tfstate import
  • new — the attributes from a live AWS scan (boto3)

Diff them, report the fields that changed:

def compute_diff(old: dict, new: dict) -> list:
    changes = []
    for key in old.keys() | new.keys():   # union of all keys
        if old.get(key) != new.get(key):
            changes.append({"field": key, "old": old.get(key), "new": new.get(key)})
    return changes

Looks correct. Ship it.

Why it cried wolf
Every resource came back as massively "drifted" — even ones nobody had touched.

The cause is a schema asymmetry between the two sources:

A tfstate import stores everything Terraform knows about a resource — 50+ attributes per instance, including provider-internal fields, computed values, timeouts, tags-all, and so on.
A live scan only emits the handful of attributes you can reliably and comparably fetch from the AWS API — in my case ~11 carefully chosen keys.
So old has 50+ keys, new has ~11. Take the union of keys and every
tfstate-only field has new.get(key) == None, which never equals the stored
value. Result: ~40 phantom "deletions" on every resource. The signal drowns in
noise, and people stop trusting the tool on day one.

The fix: compare the intersection, not the union
Real drift can only happen on a key that both sides actually report. A field
that the live scanner never emits isn't "deleted" — it's simply out of scope
for the comparison. So you intersect:

def compute_diff(old: dict, new: dict) -> list:
    changes = []
    keys = (set(old.keys()) & set(new.keys())) - EXCLUDE   # intersection
    for key in sorted(keys):
        ov, nv = old.get(key), new.get(key)
        if ov != nv:
            changes.append({
                "field": key,
                "old": str(ov) if ov is not None else "",
                "new": str(nv) if nv is not None else "",
            })
    return changes

& instead of |. That's the whole fix. False positives went to zero, and the
fields that do differ are now real, every time.

"But then you miss added and removed resources"
Right — and that's the point. Intersecting keys is the correct tool for
attribute-level drift on a resource that exists in both worlds. Detecting
whole resources that were added or removed is a different question, and it
deserves a different layer.

I keep a raw_data_prev snapshot per asset. New resource → no previous snapshot.
Removed resource → present before, absent now. That detection lives separately
from compute_diff, so each layer stays simple and honest about what it's
actually measuring. Trying to make one function answer both questions is exactly
how you end up with the cry-wolf bug.

One more consistency trap: the dashboard badge that counts "N changed" must use
the same diff function, not a raw !=. The first version used raw_data != raw_data_prev for the badge — which is true on every resource thanks to the same
schema asymmetry — so the badge counts ballooned while the detail view said
"no drift." Use one source of truth for "did this drift?".

Don't store secrets in your diffs
tfstate is full of plaintext secrets — DB passwords, access keys, tokens. The
moment you persist a diff or render it in a UI, you can leak them. Before storing
anything, scrub by key name:

SECRET_PATTERNS = ("password", "secret", "token", "private_key",
                   "access_key", "credential", "auth")

def scrub(attrs: dict) -> dict:
    return {
        k: "***" if any(p in k.lower() for p in SECRET_PATTERNS) else v
        for k, v in attrs.items()
    }

Partial match on the lowercased key, applied on both the tfstate and the boto3
side, before the data ever hits the database. Self-hosted or not, your own
diff history shouldn't become a secrets store.

Takeaways

  • Comparing two data sources? Check whether they share a schema. If not, intersect the keys — a union diff turns missing-by-design into false drift.
  • Keep "attribute changed" and "resource added/removed" as separate layers.
  • Any aggregate count (badges, summaries) must reuse the same diff logic, or it will disagree with the detail view.
  • Scrub secrets before persistence, by key name, on every source.

I packaged all of this into a small self-hosted web app — an AWS asset ledger
with tfstate-vs-live drift detection and middleware EOL tracking. It's open
source (MIT) and runs with one docker compose up: syncvey.com.
I'd genuinely like to hear how others handle the added/removed-resource layer —
do you snapshot, or diff against a fresh terraform plan?