باگی که تقصیرشو انداختم گردن provider

cdktf terraform iac typescript devops troubleshooting

هفته‌ها یه stack بود که یه terraform plan داشت که یاد گرفته بودم ازش رد بشم. هر بار، تو هر environment، می‌خواست rule مربوط به server-side-encryption رو از یه S3 bucket برداره و با یه rule خالی جایگزینش کنه. اون bucket که live بود، درست encrypt شده بود. تو AWS هیچی خراب نبود. plan فقط converge نمی‌شد، و هر value ای هم که declare می‌کردم، plan بعدی دوباره همون تغییر رو می‌خواست.

تو ذهنم گذاشتمش تو دسته‌ی «یه گیر عجیب از AWS provider، ارزش درست کردن نداره» و رد شدم. همین اشتباه بود. provider دقیقاً همون کاری رو می‌کرد که بهش گفته بودم. دروغ کاملاً قبل از Terraform بود، تو کد خود من، و کاملاً تو دید بود، تو یه فایلی که هیچ‌وقت بازش نکرده بودم.

اون واکنش غریزی، و اینکه چرا غلطه

این stack با CDKTF و Python نوشته شده. resource ها رو یا به شکل typed class می‌نویسی یا به شکل dict ساده، بعد cdktf synth اونا رو به یه فایل cdk.tf.json تبدیل می‌کنه، و Terraform روی همون plan می‌زنه. وقتی یه plan رفتار عجیب می‌کنه، واکنش غریزی اینه که تقصیر رو بندازی گردن اون لایه‌ای که نمی‌تونی توش رو ببینی: منطق diff خود provider، یه resource که معروفه به دردسر، یه گیر تو state. resource های S3 encryption معروفن به اینکه drift الکی نشون می‌دن، پس این خوب جور درمی‌اومد با اون داستانی که دلم می‌خواست باور کنم.

این واکنش غلطه چون CDKTF یه artifact داره که می‌تونی بخونیش. cdk.tf.json دقیقاً همون JSON ایه که Terraform روش plan می‌زنه. black box نیست؛ output کامپایل‌شده‌ست، بعد از هر synth رو دیسک می‌شینه. من هفته‌ها وقت گذاشته بودم که درباره‌ی رفتار provider فکر کنم، و صفر ثانیه که نگاه کنم واقعاً چی دارم بهش تحویل می‌دم.

وقتی بالاخره فایل رو باز کردم، rule اونجا بود. فقط خالی بود:

"rule": [
  {}
]

Terraform drift نمی‌کرد. داشت با وفاداری تلاش می‌کرد یه bucket که یه rule واقعیِ encryption داشت رو با یه config declare‌شده تطبیق بده که می‌گفت این rule باید هیچ محتوایی نداشته باشه. یه block خالی یه دستور صریحه، نه یه غیبت. provider بی‌گناه بود.

ریشه‌ی اصلی

این همون کدیه که اون block خالی رو تولید کرد. کاملاً عادی به نظر می‌رسه:

S3BucketServerSideEncryptionConfiguration(
    self, "encryption",
    bucket=bucket.id,
    rule=[{
        "apply_server_side_encryption_by_default": {
            "sse_algorithm": "aws:kms",
            "kms_master_key_id": key.arn,
        },
        "bucket_key_enabled": True,
    }],
)

آرگومان rule یه list می‌گیره. هر آیتم یه dict تودرتوئه: یه block که یه block دیگه توش داره. همین تودرتو بودن کل مشکله.

Python binding های CDKTF روی jsii سواره، و jsii خودکار dict های Python رو تبدیل می‌کنه به اون typed struct هایی که TypeScript زیرین انتظار داره. این تبدیل، به شکل recursive توی آیتم‌های list که خودشون dict تودرتو هستن نمی‌ره. وقتی jsii یه Sequence[Union[SomeTypedClass, Dict]] رو طی می‌کنه و یه dict پیدا می‌کنه که value هاش خودشون dict ان، نمی‌تونه ساختار داخلی رو map کنه، و بی‌صدا یه object خالی می‌ده بیرون. نه exception. نه warning. synth با exit code صفر تموم می‌شه.

چیزی که این رو واقعاً کثیف می‌کنه اینه که همون pattern همه‌جای دیگه کار می‌کنه. یه list از dict ها با value های scalar ساده، درست تبدیل می‌شه:

# این درست render می‌شه. آیتم‌های list ساده (flat) ان.
taint=[{
    "key": "dedicated",
    "value": "gpu",
    "effect": "NO_SCHEDULE",
}]

پس حالتی که خراب می‌شه و حالتی که کار می‌کنه، از نظر ظاهری دقیقاً یکی‌ان: یه list از dict که به یه آرگومان resource پاس داده شده. تنها فرقش اینه که value های اون dict، scalar ان یا dict، و نه تو کد، نه تو output خود synth، نه تو assertion هات، هیچی این دوتا رو از هم جدا نمی‌کنه. با خوندن کد Python نمی‌تونی پیداش کنی. فقط با خوندن اون JSON می‌تونی پیداش کنی.

تله‌ی توی تله

اون block خالی از یه no-op بدتره. یه اعلام صریحه که «این rule هیچ config ای نداره»، یعنی plan هیچ‌وقت نمی‌تونه converge کنه. Terraform یه rule واقعی رو live می‌بینه، یه rule که declare شده خالیه رو می‌بینه، و اون delta رو پیشنهاد می‌ده. تو Python رو ویرایش می‌کنی که value بیشتری اضافه کنی، و jsii همون‌جوری می‌ندازتشون دور، پس JSON هنوز [{}] ئه و plan بعدی هم عین همینه. اون تغییر config که قراره درستش کنه، نمی‌تونه درستش کنه، چون value هات اصلاً synth رو زنده رد نمی‌کنن که برسن به Terraform. برای همین بود که مثل یه drift غیرقابل‌حل به نظر می‌رسید. یه جورایی هم واقعاً بود، تا وقتی که داشتم تو لایه‌ی اشتباه سعی می‌کردم درستش کنم.

راه‌حل

بس کن که به jsii یه dict تودرتو بدی که حدس بزنه. اون typed class رو صریح بساز، اون‌وقت تبدیل هیچی برای استنتاج نداره:

S3BucketServerSideEncryptionConfiguration(
    self, "encryption",
    bucket=bucket.id,
    rule=[S3BucketServerSideEncryptionConfigurationRuleA(
        apply_server_side_encryption_by_default=(
            S3BucketServerSideEncryptionConfigurationRuleApplyServerSideEncryptionByDefaultA(
                sse_algorithm="aws:kms",
                kms_master_key_id=key.arn,
            )
        ),
        bucket_key_enabled=True,
    )],
)

اسم class ها طولانی‌ان و اون A آخرش یه artifact از نحوه‌ی generate شدن binding هاست، ولی نکته اینه که jsii حالا یه object کاملاً typed می‌گیره به‌جای یه dict که باید introspect ش کنه. ساختار تودرتو زنده می‌مونه، JSON هر key رو render می‌کنه، و plan تو همون تلاش اول converge می‌کنه. قانون سرانگشتی: هر resource ای که آیتم‌های list ش خودشون یه block تودرتو دارن، typed class می‌خواد، نه dict خام.

درسی که ارزش نگه داشتن داره

راستش این باگ رو قبلاً دیده بودم و ربطش رو نداده بودم. رو Kubernetes provider، ساختن resource از روی dict، بی‌صدا volumeMounts رو انداخته بود دور، یه StatefulSet رو با یه env پسورد خالی رها کرده بود، و readiness probe ها رو حذف کرده بود، همه بدون هیچ error از synth. اون‌موقع گذاشته بودمش پای یه عجیب‌بازی از Kubernetes provider. نیست. یه خاصیت از خود Python binding های CDKTF ئه، و رو هر provider ای سر و کله‌ش پیدا می‌شه، همون لحظه که resource ت یه block تودرتو داخل یه list داشته باشه.

اون درس ماندگار درباره‌ی S3 یا Kubernetes یا هیچ resource خاصی نیست. اینه: یه synth موفق یعنی کدت اجرا شد، نه اینکه چیزی رو که منظورت بود render کرد. این دوتا یکی نیستن، و اون شکاف بینشون دقیقاً همون‌جاست که این باگ زندگی می‌کنه. assertion هایی که بر پایه‌ی grep ان، pass می‌شن، چون اون field هایی که drop می‌شن همون تودرتوهای عمیقی‌ان که هیچ‌کس براشون grep نمی‌نویسه. pass شدن synth هیچی بهت نمی‌گه، چون drop کردن اون field ها دقیقاً همون کاریه که synth می‌کنه.

پس وقتی یه plan converge نمی‌کنه و resource تو console درست به نظر می‌رسه، جلوی این وسوسه رو بگیر که تقصیر رو بندازی گردن provider. اون artifact کامپایل‌شده رو باز کن. اون cdk.tf.json ای که Terraform واقعاً روش plan می‌زنه رو بخون، نه اون Python ای که آرزوشو داری. ابزار تقریباً همیشه دقیقاً همون کاری رو می‌کنه که بهش گفتی. سؤال جالب اینه که واقعاً چی بهش گفتی، و اون جواب نوشته شده، فقط یه synth اون‌طرف‌تر، تو یه فایلی که بیشتر آدما هیچ‌وقت بازش نمی‌کنن.

$ cat KAFKA .md
· 8 دقیقه مطالعه

درباره‌ی یه سیستم live سه تا چیز infer کردم. دوتاش غلط بود.

تو یه هفته سه بار درباره‌ی یه migration زنده یه ادعا کردم، از روی یه config file، یه name prefix، یا یه template، و دوبار اون ادعا غلط بود. artefact و سیستم live دو تا دید از یه چیزن، و می‌تونن به دلایلی با هم match کنن که artefact نمی‌تونه بگه‌شون. فقط یکی‌شون حقیقته.

kafka opentelemetry terraform troubleshooting devops
$ cat TERRAFORM .md
· 8 دقیقه مطالعه

پلن سبز بود چون دو تا از سه دید با هم موافق بودن. اون یکی حقیقت بود.

یه terraform plan برای یه observability stack مدیریت‌شده بعد از یه incident fix دستی clean برگشت. fix زنده بود، fix تو file بود، و فقط state عقب بود. یه plan یه diff بین دو تا دیده، ولی سیستم سه‌تا داره. قبل از plan زدن هر سه‌تا رو خوندن همون چیزیه که diff سبز رو از حدس تبدیل به تصمیم می‌کنه.

terraform helm eks state troubleshooting devops
$ cat OPENTELEMETRY .md
· 7 دقیقه مطالعه

کانفیگ byte به byte عین production بود. همین چیز اشتباهی رو ثابت کرد.

یه config برای collector دوباره ساختیم، byte به byte با نسخه‌ی روی production مقایسه‌ش کردیم، مو نمی‌زد، و با یه عالمه چک سبز پشتش deploy کردیم. سر startup افتاد تو crash-loop. byte-identical یه خاصیت واقعیه؛ فقط جواب سؤالیه که هیچکس نپرسیده بود.

opentelemetry kafka terraform devops troubleshooting