۲ مرداد ۲۵۸۵ · 7 دقیقه مطالعه
یه رکورد DNS رو دوبار پاک کردم. بار سوم پرسیدم چرا.
یه endpoint عمومی قطع شد. نه یه ۵۰۰، نه یه timeout؛ اسم فقط دیگه resolve نمیشد. پاک کردن یه رکورد یتیم توی DNS تو کمتر از پنج دقیقه درستش کرد، و منم رفتم سراغ کار بعدی. سه روز بعد همون endpoint دقیقاً به همون شکل مرد، و همون fix دوباره جواب داد. تنها دلیلی که این بار تیکت رو نبستم و نگفتم شانس بود، همین تکرار بود: یه fix که باید طبق برنامه تکرار بشه اصلاً fix نیست، یه علامته، و باگ واقعی یه رکورد ownership توی DNS بود که هیچوقت نمیتونه خودش رو بسازه.
قطعی اول شبیه یه بازمونده به نظر میرسه
علامتش این بود که یه سرویس Go هر outbound call به سمت endpoint رو توی حدود ۴.۵ میلیثانیه fail میکرد؛ خیلی سریعتر از یه timeout واقعی، که خودش نشونهی fail شدن resolve DNS هست نه خطای backend. dig روی اسم یه رکورد AAAA یتیم برگردوند که به load balancer ingress اشاره میکرد، و همین. هیچ رکورد Aای نبود.
خود load balancer مشکلی نداشت. قبل از دست زدن به DNS، هدف رو مستقل از چیزی که zone میگفت اثبات کردم:
curl --resolve checkout.example.com:443:<load-balancer-ip> https://checkout.example.com/
یه certificate معتبر و یه جواب واقعی از اپلیکیشن برگشت. پس backend اصلاً زیر سوال نبود، فقط type رکورد اشتباه بود. AAAA وزن اضافه بود، چون load balancer فقط IPv4 حرف میزد و هیچوقت روی اون آدرس جواب نداده بود.
external-dns با --policy=sync و --registry=txt اجرا میشه، یعنی یه اسم رو با نگه داشتن یه رکورد TXT کنارش صاحب میشه، و به چیزی که صاحبش نیست دست نمیزنه. لاگها بقیهی ماجرا رو توضیح دادن:
Skipping endpoint checkout.example.com ... AAAA ... because owner id does not
match for one or more items to create, found: "", required: "eks-prod-cluster"
external-dns میخواست رکورد A رو از روی Ingress منتشر کنه، ولی AAAA یتیم هیچ TXT ownershipی نداشت که external-dns بشناستش، پس کل changeset برای اون اسم رد شد؛ نه فقط AAAA، بلکه A هم همینطور. زیر sync، یه رکورد بدون owner فقط بهعنوان یتیم نمیمونه؛ جلوی هر create دیگهای روی همون اسم رو میگیره تا یه آدم بیاد پاکش کنه. پاک کردن AAAA باعث شد reconcile بعدی A رو منتشر کنه، و endpoint برگشت.
سه روز بعد، همون fix، و تصمیم به واقعاً نگاه کردن
همون endpoint، همون failهای سریع، همون A غایب. پاک کردن AAAA یتیم دوباره درستش کرد. این بار قبل از بستن هر چیزی رفتم سراغ CloudTrail روی hosted zone، چون یه fix که طبق برنامهی خودش تکرار میشه نشونهی اینه که automation داره خودش مشکل رو دوباره تولید میکنه، نه اینکه یکی داره دستی رکورد رو خراب میکنه.
CloudTrail نشون داد که external-dns هر ده دقیقه رکورد A رو UPSERT میکرد، تا یه DELETE تکی روی AAAA، و بعدش سکوت. نه یه اشتباه یهبار، بلکه یه loop که یه مدت تمیز چرخید و بعد خودش رو قفل کرد. سوال دیگه این نبود «چی رکورد A رو پاک کرد»، بلکه شد «چرا external-dns مدام رکوردی میسازه که هیچوقت نمیتونه نگهش داره».
رکوردی که هیچوقت نمیتونه خودش رو صاحب بشه
این endpoint دقیقاً روی apex یه hosted zone دلگیتشدهی خودش نشسته: یه zone مستقل، نه فقط یه اسم توی یه zone بزرگتر، و zone پرنتش توی یه AWS account کاملاً جدا زندگی میکنه؛ همون جور مرز account که peering رو ارزونتر از یه transit hub مشترک میکنه، وقتی داری هزینهی traffic بین accountها رو حساب میکنی نه writeهای DNS رو. دقیقاً همین جدایی مدل ownership رو میشکنه.
provider AWS توی external-dns یه Ingress رو به یه desired state دوتایی باز میکنه: یه A و یه AAAA، حتی وقتی فقط A قراره یه روزی به چیزی resolve بشه. registry TXT مالکیت رو به ازای هر type رکورد با یه اسم TXT که پیشوند نوع رکورد داره از هم جدا میکنه: برای AAAA روی checkout.example.com، این میشه یه TXT که اسمش دقیقاً aaaa-checkout.example.comه. توی یه subdomain معمولی این رکورد توی همون zone بقیه چیزها میشینه. ولی روی apex یه zone، این یه sibling خود اسم apex محسوب میشه، یعنی یه لِوِل بالاتر میره، توی zone پرنت، یعنی example.com، که external-dns هیچ دسترسی بهش نداره. لاگها هم واضح میگن:
Skipping record aaaa-checkout.example.com because no hosted zone matching record DNS Name was detected
external-dns میتونه رکورد AAAA رو بسازه، ولی هیچوقت نمیتونه اون TXT ownershipی رو بنویسه که بذاره ادعا کنه خودش صاحب رکورده. توی reconcile بعدی، خودِ AAAA رو بدون owner متناظر میخونه؛ دقیقاً همون وضعیت «رکورد بدون owner جلوی create رو میگیره» از قطعی اول، با این فرق که این بار رکورد بدون owner همونیه که external-dns خودش نوشته. این همون قفل رو فعال میکنه، که کل changeset رو رد میکنه، A رو هم همینطور. رکوردی که endpoint رو زنده نگه میداره، گروگان رکوردیه که هیچکس هیچوقت نمیتونه صاحبش بشه، توی zoneای که هیچکس هیچوقت نمیتونه توش بنویسه.
این مخصوص یه zone نیست. یه خاصیت کلیه برای registry TXT با پیشوند نوع رکورد: نمیتونه رکوردی روی apex یه zoneای صاحب بشه که پرنتش رو کنترل نمیکنه. فقط فرمت قدیمی TXT بدون پیشوند (یعنی بدون a-/aaaa-/cname-) توی خود zone میشینه، چون sibling apex نیست؛ فقط همون اسم apex با type رکورد فرق داره. یه subdomain چند لِوِل پایینتر هیچوقت به این مشکل نمیخوره، چون هر TXT ownershipی که لازم داره توی همون zone خود رکورد زندگی میکنه.
fixی که واقعاً دووم میاره
نسخهی دائمی اصلاً دست به zone نمیزنه؛ فقط باعث میشه external-dns از همون اول دیگه AAAA نخواد:
# external-dns chart values
managedRecordTypes:
- A
- CNAME
این به --managed-record-types=A --managed-record-types=CNAME رندر میشه و default یعنی [A, AAAA, CNAME] رو جایگزین میکنه. بدون AAAA توی desired state، دیگه هیچ TXT ownershipی روی apex نیست که نتونه نوشته بشه، و دیگه چیزی برای گیر کردن نمیمونه. اینجا مطمئنه چون هر backend پشت این zone یه load balancer تنها-IPv4ه، پس AAAA هیچوقت traffic رو سرویس نمیداد، فقط داشت چیزی رو خراب میکرد که کار میکرد.
دو تا سوال بود که ارزش داشت قبل از اعتماد به این fix جوابشون رو بدونم، نه فقط حدس بزنم:
این باعث میشه TXT registry برای بقیه چیزها خراب بشه؟ نه. managedRecordTypes فقط فیلتر میکنه چه type رکوردهایی از منابع Kubernetes منتشر بشن؛ چیزی دربارهی حسابداری خود registry نمیگه. رکوردهای TXT از اول توی اون لیست نبودن (default هم TXT رو نداره)، و registry دقیقاً مثل قبل برای رکوردهای A و CNAME باقیمونده کار میکنه.
اصلاً external-dns به TXT ownership نیاز داره؟ فقط زیر --registry=txt. جایگزینی که کلاً این دسته مشکل رو حذف میکنه یه registry مبتنیبر DynamoDB هست، که ownership رو out-of-band ذخیره میکنه نه به شکل رکوردهای sibling توی zone. این یه تغییر clusterwide با blast radius بزرگتر از یه مقدار chart تنهاست، پس بهعنوان یه گزینه یادداشتشده موند نه چیزی که همونجا اعمال بشه.
چیزی که باید ازش برداشت کرد
fix اول درست بود و در عین حال غلط. پاک کردن رکورد یتیم هر دو بار حرکت درستی بود، و هیچوقت قرار نبود آخرین بار باشه، چون داشت با اثر برخورد میکرد و علتی که هر reconcile یه اثر تازه تولید میکرد رو دستنخورده میذاشت. علامت لودهنده توی پیام خطا نبود؛ توی این واقعیت بود که همون اکشن پنجدقیقهای به همون دلیل دوبار لازم شد. باگی که خودش برمیگرده، بدون اینکه کسی بینشون سیستم رو دست بزنه، یعنی یه چیزی داره اون رو توی یه loop تولید میکنه، و CloudTrail چیزیه که «دوباره خراب شد» رو تبدیل میکنه به «اینجا loopشه». این دقیقاً همون شکل تلهایه که یه Helm release که موفقیت گزارش میده در حالی که config در حال اجرا کهنه میمونه داره؛ ابزار داره دروغ نمیگه، فقط داره از یه لایه بالاتر از جایی که واقعاً خراب شده گزارش میده.
این یادآوریه هم هست که سیستمهای ownership که روی قرارداد ساخته شدن، یه رکورد TXT که معناش به این وابستهست کجا نشسته و اسمش چه پیشوندی داره، هر مرزی از سلسلهمراتب اسمگذاری زیرش رو به ارث میبرن. توی zone دلگیتشده چیزی اشتباه تنظیم نشده بود. zone دقیقاً همونجوری کار کرد که zoneهای دلگیتشده باید کار کنن؛ این فرض بود که owner هر رکورد همیشه میتونه درست کنارش زندگی کنه که با برخورد به یه apex زنده نموند. Kubernetes بیشتر از این defaultهای مبتنیبر قرارداد ارائه میده که بیشتر آدمها متوجهش نمیشن، دقیقاً مثل enableServiceLinks که بیسروصدا environment variableهایی تزریق میکنه که کسی نخواسته بودشون، تا روزی که با یه چیز واقعی برخورد میکنه.