زیرساخت ابری و IaCمهندسانسطح متوسط

قفل state ترافورم گیر کرده؛ آزادسازی امن بعد از کیل شدن apply

وقتی پایپلاین CI وسط اجرای apply کشته می‌شود، قفل state ترافورم در جدول DynamoDB باقی می‌ماند و اجرای بعدی با خطای Error acquiring the state lock متوقف می‌شود. در این مقاله علت ریشه‌ای، روش آزادسازی امن و راه‌های پیشگیری را بررسی می‌کنیم.

تحریریه کلودیکپ ۶ دقیقه مطالعه
قفل state ترافورم گیر کرده؛ آزادسازی امن بعد از کیل شدن apply

شرح مسئله

ساعت ۹ صبح است و می‌خواهی تغییرات تیم زیرساخت را روی محیط production اعمال کنی. terraform apply را می‌زنی و به‌جای خروجی معمول، این را می‌بینی:

$ terraform apply
Acquiring state lock. This may take a few moments...

Error: Error acquiring the state lock

Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
  ID:        a1b2c3d4-5678-90ab-cdef-1234567890ab
  Path:      prod-network/terraform.tfstate
  Operation: OperationTypeApply
  Who:       runner@gitlab-ci-runner-7f9c
  Version:   1.6.2
  Created:   2024-03-11 09:14:22.123456789 +0000 UTC
  Info:

نکته مهم در لاگ، فیلدهای ID، Who و Created است که به زمان حال مربوط نیستند. یعنی قفل قبلاً توسط یک اجرای دیگر گرفته شده و آزاد نشده است. اگر همان لحظه در ترمینال دیگری یا در رابط کاربری GitLab/GitHub Actions نگاه کنی، می‌بینی هیچ اجرای فعالی در جریان نیست. قفل «روح» شده است.

علت ریشه‌ای

بک‌اند S3 در ترافورم به‌تنهایی قفل ندارد. برای همین در کنارش از یک جدول DynamoDB با کلید LockID استفاده می‌شود. جریان کار این‌طور است:

  1. پیش از هر عملیات، ترافورم تلاش می‌کند یک آیتم با کلید مسیر state را در جدول قفل درج کند.
  2. درج با شرط attribute_not_exists(LockID) انجام می‌شود؛ اگر آیتم از قبل موجود باشد، خطای ConditionalCheckFailedException برمی‌گردد و ترافورم با همان پیام بالا متوقف می‌شود.
  3. در پایان موفقیت‌آمیز اجرا، ترافورم آیتم را حذف می‌کند.

مشکل جایی است که فرایند ترافورم به شکل تمیز خاتمه پیدا نکند. ترافورم سیگنال SIGINT (Ctrl+C) را مدیریت می‌کند و قفل را آزاد می‌کند، اما روی SIGKILL هیچ فرصتی برای این کار ندارد. سناریوهای رایج:

  • تمام‌شدن timeout جاب CI و کشته‌شدن runner.
  • OOM Kill شدن پاد یا کانتینر runner.
  • قطع‌ومنت شبکه هنگام نوشتن state در S3.
  • پاک‌شدن ناگهانی runner اسپات در میان اجرا.
  • بسته‌شدن ناگهانی لپ‌تاپ یا VPN در حال اجرا.

در همه این حالت‌ها آیتم قفل در جدول DynamoDB باقی می‌ماند و ترافورم دیگر نمی‌تواند وارد شود.

راه‌حل گام‌به‌گام

قدم ۱: مطمئن شو هیچ اجرایی در حال انجام نیست

پیش از هر کاری، بررسی کن که کسی در حال apply نیست. آزادسازی نسنجیده می‌تواند دو اجرای همزمان را روی یک state راه بیندازد و state را خراب کند.

# دیدن آیتم قفل فعلی
aws dynamodb get-item \
  --table-name terraform-locks \
  --key '{"LockID": {"S": "my-terraform-state/prod-network/terraform.tfstate"}}'

# دیدن همه قفل‌های فعلی جدول
aws dynamodb scan \
  --table-name terraform-locks \
  --projection-expression "LockID, Info"

در خروجی به فیلد Info نگاه کن؛ همان چیزی است که در پیام خطا دیدی. اگر Created مربوط به ساعت‌ها یا روزها قبل است و در CI هم جاب فعالی در جریان نیست، قفل متروک است. بهتر است تاریخچه جاب‌های اخیر را هم چک کنی؛ معمولاً آخرین اجرای نیمه‌کاره، همان قفل‌گیرنده است.

قدم ۲: آزادسازی با force-unlock

ترافورم برای همین حالت یک دستور دارد که همان ID را می‌گیرد:

terraform force-unlock a1b2c3d4-5678-90ab-cdef-1234567890ab

اگر می‌خواهی بدون تأییدیه تعاملی اجرا شود:

terraform force-unlock -force a1b2c3d4-5678-90ab-cdef-1234567890ab
مهم: این دستور آیتم قفل را حذف می‌کند، ولی هیچ کاری با خود state نمی‌کند. اگر اجرای نیمه‌کاره بخشی از تغییرات را در S3 نوشته باشد، آن نوشته سر جایش می‌ماند و باید جداگانه بررسی شود.

قدم ۳: اگر force-unlock جواب نداد

گاهی دسترسی به بک‌اند از ماشین محلی درست پیکربندی نشده و ترافورم نمی‌تواند قفل را ببیند. در این حالت مستقیم آیتم را حذف کن:

aws dynamodb delete-item \
  --table-name terraform-locks \
  --key '{"LockID": {"S": "my-terraform-state/prod-network/terraform.tfstate"}}'

سپس با aws dynamodb get-item تأیید کن که آیتم حذف شده است.

قدم ۴: بررسی سازگاری state

پس از آزادسازی، اول یک plan بگیر:

terraform init -reconfigure
terraform plan -out=tfplan

اگر در plan تعداد زیادی resource به‌شکل create/update غیرمنتظره ظاهر شد که قبلاً اعمال شده بودند، احتمالاً اجرای نیمه‌کاره بخشی از state را نوشته و بخشی را نه. در این حالت به‌جای apply کورکورانه، ابتدا با تیم بررسی کن و در صورت نیاز از نسخه‌های پشتیبان state استفاده کن.

روش بررسی اینکه مشکل واقعاً حل شده است

پس از آزادسازی، سه معیار را چک کن:

  1. terraform plan بدون خطای قفل اجرا شود و عملیات را کامل کند.
  2. در جدول DynamoDB هیچ آیتمی با آن LockID باقی نمانده باشد.
  3. خروجی plan دقیقاً انتظار تیم را منعکس کند، نه مجموعه‌ای از createهای عجیب.
aws dynamodb scan --table-name terraform-locks --select COUNT --query "Count"

اگر عدد صفر برگشت و هیچ اجرای فعالی در CI وجود ندارد، قفل تمیز است. دستور terraform plan را یک بار دیگر اجرا کن و ببین که بدون هیچ تأخیری قفل را می‌گیرد.

پیشگیری و بهترین روش‌ها

  • زمان انتظار قفل را تعیین کن: در CI از -lock-timeout استفاده کن تا اجرای جدید چند دقیقه صبر کند و بعد خطا بدهد، نه اینکه فوراً بمیرد:
    terraform apply -lock-timeout=10m
  • برای عملیات فقط-خواندنی قفل نگیر: برای plan در PRها می‌توانی از -lock=false استفاده کنی، ولی هرگز برای apply:
    terraform plan -lock=false
  • timeout جاب CI را واقع‌گرایانه تنظیم کن: اگر apply معمولاً ۱۵ دقیقه طول می‌کشد، timeout را ۱۰ دقیقه نگذار. مقدار را بر اساس P95 اندازه‌گیری‌های واقعی انتخاب کن.
  • از SIGKILL پرهیز کن: در اسکریپت CI از timeout --signal=INT استفاده کن تا ترافورم فرصت آزادسازی قفل داشته باشد:
    timeout --signal=INT 30m terraform apply -auto-approve
  • جدول قفل را مانیتور کن: روی جدول DynamoDB یک متریک CloudWatch برای تعداد آیتم و سن آن بساز و اگر قفلی بیشتر از حد معمول باقی ماند هشدار بگیر.
  • state را قفل و نسخه‌بندی کن: در بک‌اند S3 گزینه encrypt و bucket versioning را فعال نگه دار تا بازگردانی آسان باشد.
  • پیکربندی مرجع بک‌اند:
    terraform {
      backend "s3" {
        bucket         = "my-terraform-state"
        key            = "prod-network/terraform.tfstate"
        region         = "us-east-1"
        dynamodb_table = "terraform-locks"
        encrypt        = true
      }
    }
    
  • مهاجرت به قفل بومی S3: از ترافورم 1.10 به بعد، بک‌اند S3 خودش می‌تواند قفل را با use_lockfile = true روی خود state مدیریت کند و دیگر نیازی به DynamoDB نداشته باشی:
    terraform {
      backend "s3" {
        bucket       = "my-terraform-state"
        key          = "prod-network/terraform.tfstate"
        region       = "us-east-1"
        encrypt      = true
        use_lockfile = true
      }
    }
    
    در این حالت دیگر لازم نیست جدول DynamoDB بسازی. اگر همچنان از DynamoDB استفاده می‌کنی، جدول را با همین اسکیمای ساده ایجاد کن:
    aws dynamodb create-table \
      --table-name terraform-locks \
      --attribute-definitions AttributeName=LockID,AttributeType=S \
      --key-schema AttributeName=LockID,KeyType=HASH \
      --billing-mode PAY_PER_REQUEST
    

جمع‌بندی: قفل state یک مکانیزم محافظ است، نه دشمن. هر بار که مجبور می‌شوی force-unlock بزنی، یک فرایند در CI یا اسکریپت محلی وجود دارد که به‌درستی خاتمه پیدا نمی‌کند. با تنظیم timeouts، مدیریت سیگنال‌ها و مانیتورینگ جدول قفل، تعداد این اتفاق‌ها به‌سرعت به صفر نزدیک می‌شود.

اشتراک‌گذاری:

نویسنده

تحریریه کلودیکپ

تیم محتوای کلودیکپ

تیم فنی و محتوای کلودیکپ؛ مهندسانی که هر روز با DevOps، Kubernetes و زیرساخت ابری کار می‌کنند و تجربه‌هایشان را اینجا می‌نویسند.

سوالات متداول

سوالات متداول این مقاله

بله. اگر واقعاً یک اجرای دیگر در حال apply باشد و تو قفل را به‌زور آزاد کنی، دو ترافورم همزمان روی یک state کار می‌کنند و احتمال خرابی state بالا می‌رود. پس همیشه اول با بررسی جدول DynamoDB و تاریخچه جاب‌های CI مطمئن شو که اجرای فعالی وجود ندارد، بعد آزاد کن.

فیلد Created در پیام خطا و در آیتم DynamoDB را ببین. اگر زمان آن مربوط به ساعت‌ها پیش است، در CI هیچ جاب فعالی در حال اجرا نیست و در لاگ‌های runner اثری از فرایند ترافورم نیست، قفل متروک است.

بله. از ترافورم 1.10 به بعد بک‌اند S3 گزینه use_lockfile را پشتیبانی می‌کند و قفل روی خود فایل state در S3 نگه داشته می‌شود. در این حالت دیگر نیازی به ساخت و نگهداری جدول DynamoDB نداری، ولی مهاجرت نیاز به دقت دارد و باید پس از تغییر، یک plan سالم بگیر.

ادامه مطالعه

مقالات مرتبط

حلقه بی‌پایان Jira Automation: وقتی قانون خودش را صدا می‌زند
CI/CD و اتوماسیونمتوسط

حلقه بی‌پایان Jira Automation: وقتی قانون خودش را صدا می‌زند

اگر trigger و action یک قانون Jira Automation روی یک رویداد بیفتند، قانون خودش را دوباره اجرا می‌کند و issue با صدها کامنت تکراری پر می‌شود. این مقاله علت، پاک‌سازی و راه‌حل پایدار را نشان می‌دهد.

تحریریه کلودیکپ ۷ دقیقه مطالعه
قطع شدن WebSocket در HAProxy هر ۵۰ ثانیه؛ علت و راه‌حل
Linux و مدیریت سرورمتوسط

قطع شدن WebSocket در HAProxy هر ۵۰ ثانیه؛ علت و راه‌حل

اگر WebSocket یا SSE پشت HAProxy هر ۵۰ ثانیه قطع می‌شود، مقصر تایماوت‌های پیش‌فرض فایل نمونه است. در این مقاله علت ریشه‌ای و پیکربندی درست timeout tunnel را بررسی می‌کنیم.

تحریریه کلودیکپ ۷ دقیقه مطالعه
پرشدن دیسک PostgreSQL با WAL اسلات replication غیرفعال
مانیتورینگ و پایداریمتوسط

پرشدن دیسک PostgreSQL با WAL اسلات replication غیرفعال

یک replication slot غیرفعال می‌تواند pg_wal را تا پر شدن کامل دیسک نگه دارد و دیتابیس را عملاً از کار بیندازد. در این مقاله ریشه‌یابی، رفع و پیشگیری از آن را گام‌به‌گام بررسی می‌کنیم.

تحریریه کلودیکپ ۶ دقیقه مطالعه

در پیاده‌سازی به کمک نیاز دارید؟

تیم کلودیکپ همین کار را هر روز برای تیم‌های دیگر انجام می‌دهد. اگر جایی گیر کرده‌اید، با ما صحبت کنید.