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

شرح مسئله
ساعت ۹ صبح است و میخواهی تغییرات تیم زیرساخت را روی محیط 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 استفاده میشود. جریان کار اینطور است:
- پیش از هر عملیات، ترافورم تلاش میکند یک آیتم با کلید مسیر state را در جدول قفل درج کند.
- درج با شرط
attribute_not_exists(LockID)انجام میشود؛ اگر آیتم از قبل موجود باشد، خطایConditionalCheckFailedExceptionبرمیگردد و ترافورم با همان پیام بالا متوقف میشود. - در پایان موفقیتآمیز اجرا، ترافورم آیتم را حذف میکند.
مشکل جایی است که فرایند ترافورم به شکل تمیز خاتمه پیدا نکند. ترافورم سیگنال 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 استفاده کن.
روش بررسی اینکه مشکل واقعاً حل شده است
پس از آزادسازی، سه معیار را چک کن:
terraform planبدون خطای قفل اجرا شود و عملیات را کامل کند.- در جدول DynamoDB هیچ آیتمی با آن
LockIDباقی نمانده باشد. - خروجی 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 نداشته باشی:
در این حالت دیگر لازم نیست جدول DynamoDB بسازی. اگر همچنان از DynamoDB استفاده میکنی، جدول را با همین اسکیمای ساده ایجاد کن:terraform { backend "s3" { bucket = "my-terraform-state" key = "prod-network/terraform.tfstate" region = "us-east-1" encrypt = true use_lockfile = true } }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 سالم بگیر.
کلودیکپ در این زمینه چه کمکی میکند؟
زیرساخت بهعنوان کد (IaC)
مدیریت زیرساخت بهصورت کد نسخهدار، قابل بازتولید و قابل بازبینی.
مشاهده جزئیات خدمتمعماری زیرساخت ابری
طراحی زیرساخت مقیاسپذیر، امن و مقرونبهصرفه روی AWS، Azure یا Google Cloud.
مشاهده جزئیات خدمتمهاجرت به ابر
انتقال ایمن و برنامهریزیشده زیرساخت و برنامهها از سرورهای سنتی به فضای ابری.
مشاهده جزئیات خدمت
حلقه بیپایان Jira Automation: وقتی قانون خودش را صدا میزند
اگر trigger و action یک قانون Jira Automation روی یک رویداد بیفتند، قانون خودش را دوباره اجرا میکند و issue با صدها کامنت تکراری پر میشود. این مقاله علت، پاکسازی و راهحل پایدار را نشان میدهد.

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

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