دام های Terraform

دام های Terraform
بیایید چند مشکل را برجسته کنیم، از جمله موارد مربوط به حلقه‌ها، دستورات if و تکنیک‌های استقرار، و همچنین مسائل کلی‌تری که به‌طور کلی بر Terraform تأثیر می‌گذارند:

  • پارامترهای count و for_each دارای محدودیت هستند.
  • عدم استقرار زمان خرابی را محدود کنید.
  • حتی یک طرح خوب نیز ممکن است شکست بخورد.
  • refactoring می تواند مشکلات خود را داشته باشد.
  • انسجام معوق سازگار است... با تعویق.

پارامترهای count و for_each محدودیت هایی دارند

مثال های این فصل به طور گسترده از پارامتر count و عبارت for_each در حلقه ها و منطق شرطی استفاده می کنند. آنها عملکرد خوبی دارند، اما دو محدودیت مهم دارند که باید از آنها آگاه باشید.

  • Count و for_each نمی توانند به هیچ یک از متغیرهای خروجی منبع ارجاع دهند.
  • count و for_each را نمی توان در پیکربندی ماژول استفاده کرد.

count و for_each نمی توانند به هیچ کدام از متغیرهای خروجی منبع ارجاع دهند

تصور کنید باید چندین سرور EC2 را مستقر کنید و به دلایلی نمی خواهید از ASG استفاده کنید. کد شما می تواند اینگونه باشد:

resource "aws_instance" "example_1" {
   count             = 3
   ami                = "ami-0c55b159cbfafe1f0"
   instance_type = "t2.micro"
}

بیایید یک به یک آنها را بررسی کنیم.

از آنجایی که پارامتر count روی یک مقدار ثابت تنظیم شده است، این کد بدون مشکل کار می کند: هنگامی که فرمان application را اجرا می کنید، سه سرور EC2 ایجاد می کند. اما اگر بخواهید یک سرور را در هر منطقه در دسترس (AZ) در منطقه AWS فعلی خود مستقر کنید، چه؟ می‌توانید از کدتان بخواهید فهرستی از مناطق را از منبع داده aws_availability_zones بارگیری کند و سپس هر یک را حلقه بزنید و با استفاده از پارامتر شمارش و دسترسی به فهرست آرایه، یک سرور EC2 در آن ایجاد کنید:

resource "aws_instance" "example_2" {
   count                   = length(data.aws_availability_zones.all.names)
   availability_zone   = data.aws_availability_zones.all.names[count.index]
   ami                     = "ami-0c55b159cbfafe1f0"
   instance_type       = "t2.micro"
}

data "aws_availability_zones" "all" {}

این کد نیز به خوبی کار خواهد کرد، زیرا پارامتر شمارش می تواند بدون هیچ مشکلی به منابع داده ارجاع دهد. اما چه اتفاقی می افتد اگر تعداد سرورهایی که باید ایجاد کنید به خروجی برخی از منابع بستگی دارد؟ برای نشان دادن این موضوع، ساده ترین راه استفاده از منبع random_integer است، که همانطور که از نام آن پیداست، یک عدد صحیح تصادفی را برمی گرداند:

resource "random_integer" "num_instances" {
  min = 1
  max = 3
}

این کد یک عدد تصادفی بین 1 و 3 ایجاد می کند. بیایید ببینیم اگر بخواهیم از خروجی این منبع در پارامتر count منبع aws_instance استفاده کنیم چه اتفاقی می افتد:

resource "aws_instance" "example_3" {
   count             = random_integer.num_instances.result
   ami                = "ami-0c55b159cbfafe1f0"
   instance_type = "t2.micro"
}

اگر برنامه terraform را روی این کد اجرا کنید، با خطای زیر مواجه خواهید شد:

Error: Invalid count argument

   on main.tf line 30, in resource "aws_instance" "example_3":
   30: count = random_integer.num_instances.result

The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the count depends on.

Terraform مستلزم آن است که count و for_each در مرحله برنامه ریزی، قبل از ایجاد یا اصلاح هر گونه منبعی، محاسبه شود. این بدان معناست که count و for_each می‌توانند به حرف‌ها، متغیرها، منابع داده‌ها و حتی فهرست‌های منابع اشاره کنند (تا زمانی که بتوان طول آنها را در زمان‌بندی تعیین کرد)، اما نه به متغیرهای خروجی منبع محاسبه‌شده.

count و for_each را نمی توان در پیکربندی ماژول استفاده کرد

روزی ممکن است وسوسه شوید که یک پارامتر شمارش را به پیکربندی ماژول خود اضافه کنید:

module "count_example" {
     source = "../../../../modules/services/webserver-cluster"

     count = 3

     cluster_name = "terraform-up-and-running-example"
     server_port = 8080
     instance_type = "t2.micro"
}

این کد سعی می کند از تعداد در داخل یک ماژول برای ایجاد سه نسخه از منبع وب سرور-خوشه استفاده کند. یا ممکن است بخواهید اتصال یک ماژول را بر اساس برخی شرایط بولی با تنظیم پارامتر شمارش آن بر روی 0 اختیاری کنید. ممکن است به نظر یک کد معقول به نظر برسد، اما هنگام اجرای terraform plan با این خطا مواجه خواهید شد:

Error: Reserved argument name in module block

   on main.tf line 13, in module "count_example":
   13: count = 3

The name "count" is reserved for use in a future version of Terraform.

متأسفانه، از Terraform 0.12.6، استفاده از count یا for_each در یک منبع ماژول پشتیبانی نمی شود. طبق یادداشت‌های انتشار Terraform 0.12 (http://bit.ly/3257bv4)، HashiCorp قصد دارد این قابلیت را در آینده اضافه کند، بنابراین بسته به زمان خواندن این کتاب، ممکن است از قبل در دسترس باشد. برای اینکه مطمئن شوید، تغییرات Terraform را اینجا بخوانید.

محدودیت‌های استقرار زمان توقف صفر

استفاده از بلوک create_before_destroy در ترکیب با ASG یک راه حل عالی برای ایجاد استقرارهای بدون توقف است، به جز یک نکته: قوانین مقیاس خودکار پشتیبانی نمی شوند. یا به عبارت دقیق‌تر، این کار اندازه ASG را به min_size در هر استقرار بازنشانی می‌کند، که اگر از قوانین مقیاس خودکار برای افزایش تعداد سرورهای در حال اجرا استفاده می‌کنید، می‌تواند مشکل ساز باشد.

به عنوان مثال، ماژول webserver-cluster حاوی یک جفت منبع aws_autoscaling_schedule است که در ساعت 9 صبح تعداد سرورهای خوشه را از 11 به 9 افزایش می دهد. اگر مثلاً ساعت XNUMX صبح مستقر شوید، ASG جدید به جای ده سرور فقط با دو سرور راه اندازی می شود و تا ساعت XNUMX صبح روز بعد به همین صورت باقی می ماند.

این محدودیت را می توان به روش های مختلفی دور زد.

  • پارامتر تکرار در aws_autoscaling_schedule را از 0 9 * * * («ساعت 9 صبح اجرا کنید») به چیزی شبیه به 0-59 9-17 * * * («هر دقیقه از 9 صبح تا 5 بعدازظهر اجرا کنید») تغییر دهید. اگر ASG از قبل ده سرور داشته باشد، اجرای مجدد این قانون مقیاس خودکار چیزی را تغییر نمی دهد، چیزی که ما می خواهیم. اما اگر ASG اخیراً مستقر شده باشد، این قانون تضمین می کند که حداکثر در یک دقیقه تعداد سرورهای آن به XNUMX برسد. این یک رویکرد کاملاً زیبا نیست و جهش های بزرگ از ده سرور به دو سرور و برگشت نیز می تواند برای کاربران مشکلاتی ایجاد کند.
  • یک اسکریپت سفارشی ایجاد کنید که از API AWS برای تعیین تعداد سرورهای فعال در ASG استفاده کند، آن را با استفاده از یک منبع داده خارجی فراخوانی کنید (به "منبع داده خارجی" در صفحه 249 مراجعه کنید)، و پارامتر ASG's want_capacity را روی مقدار بازگردانده شده تنظیم کنید. فیلمنامه به این ترتیب، هر نمونه ASG جدید همیشه با همان ظرفیت کد Terraform موجود اجرا می شود و نگهداری آن را دشوارتر می کند.

البته، Terraform در حالت ایده‌آل می‌تواند پشتیبانی داخلی برای استقرار زمان خاموشی صفر داشته باشد، اما از می 2019، تیم HashiCorp هیچ برنامه‌ای برای افزودن این قابلیت نداشت.جزئیات - اینجا).

برنامه صحیح ممکن است با موفقیت اجرا نشود

گاهی اوقات دستور plan یک طرح استقرار کاملاً صحیح تولید می کند، اما دستور application یک خطا برمی گرداند. برای مثال، سعی کنید منبع aws_iam_user را با همان نامی که برای کاربر IAM که قبلاً در فصل 2 ایجاد کردید، اضافه کنید:

resource "aws_iam_user" "existing_user" {
   # Подставьте сюда имя уже существующего пользователя IAM,
   # чтобы попрактиковаться в использовании команды terraform import
   name = "yevgeniy.brikman"
}

اکنون، اگر دستور plan را اجرا کنید، Terraform یک برنامه استقرار به ظاهر معقول را خروجی خواهد داد:

Terraform will perform the following actions:

   # aws_iam_user.existing_user will be created
   + resource "aws_iam_user" "existing_user" {
         + arn                  = (known after apply)
         + force_destroy   = false
         + id                    = (known after apply)
         + name               = "yevgeniy.brikman"
         + path                 = "/"
         + unique_id         = (known after apply)
      }

Plan: 1 to add, 0 to change, 0 to destroy.

اگر دستور application را اجرا کنید با خطای زیر مواجه خواهید شد:

Error: Error creating IAM User yevgeniy.brikman: EntityAlreadyExists:
User with name yevgeniy.brikman already exists.

   on main.tf line 10, in resource "aws_iam_user" "existing_user":
   10: resource "aws_iam_user" "existing_user" {

البته مشکل این است که یک کاربر IAM با آن نام از قبل وجود دارد. و این می تواند نه تنها برای کاربران IAM، بلکه برای تقریباً هر منبعی اتفاق بیفتد. ممکن است شخصی این منبع را به صورت دستی یا با استفاده از خط فرمان ایجاد کرده باشد، اما در هر صورت، تطبیق شناسه ها منجر به تداخل می شود. تغییرات زیادی از این خطا وجود دارد که اغلب افراد تازه وارد به Terraform را غافلگیر می کند.

نکته کلیدی این است که دستور terraform plan فقط منابعی را در نظر می گیرد که در فایل State Terraform مشخص شده اند. اگر منابع به روش دیگری ایجاد شوند (مثلاً به صورت دستی با کلیک بر روی کنسول AWS)، در فایل حالت قرار نمی گیرند و بنابراین Terraform آنها را هنگام اجرای دستور plan در نظر نمی گیرد. در نتیجه، طرحی که در نگاه اول درست به نظر می رسد ناموفق خواهد بود.

از این موضوع دو درس می توان آموخت.

  • اگر قبلاً کار با Terraform را شروع کرده اید، از هیچ چیز دیگری استفاده نکنید. اگر بخشی از زیرساخت شما با استفاده از Terraform مدیریت می شود، دیگر نمی توانید آن را به صورت دستی تغییر دهید. در غیر این صورت، نه تنها خطر خطاهای عجیب Terraform را دارید، بلکه بسیاری از مزایای IaC را نیز نفی می کنید، زیرا کد دیگر نشان دهنده دقیق زیرساخت شما نخواهد بود.
  • اگر از قبل زیرساختی دارید، از دستور import استفاده کنید. اگر شروع به استفاده از Terraform با زیرساخت های موجود کرده اید، می توانید آن را با استفاده از دستور terraform import به فایل state اضافه کنید. به این ترتیب Terraform می داند که چه زیرساختی باید مدیریت شود. دستور import دو آرگومان می گیرد. اولین مورد آدرس منبع در فایل های پیکربندی شما است. نحو در اینجا مانند پیوندهای منبع است: _. (مانند aws_iam_user.existing_user). آرگومان دوم شناسه منبعی است که باید وارد شود. فرض کنید شناسه منبع aws_iam_user نام کاربری است (به عنوان مثال yevgeniy.brikman) و شناسه منبع aws_instance شناسه سرور EC2 است (مانند i-190e22e5). نحوه وارد کردن یک منبع معمولاً در اسناد در پایین صفحه آن نشان داده شده است.

    در زیر یک دستور import وجود دارد که منبع aws_iam_user را که به پیکربندی Terraform خود به همراه کاربر IAM در فصل 2 اضافه کرده اید همگام می کند (البته نام شما را جایگزین yevgeniy.brikman می کند):

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    Terraform API AWS را فراخوانی می کند تا کاربر IAM شما را پیدا کند و یک ارتباط فایل حالت بین آن و منبع aws_iam_user.existing_user در پیکربندی Terraform شما ایجاد کند. از این پس، وقتی دستور plan را اجرا می کنید، Terraform متوجه می شود که کاربر IAM از قبل وجود دارد و دیگر سعی نمی کند آن را ایجاد کند.

    شایان ذکر است که اگر منابع زیادی دارید که می‌خواهید به Terraform وارد کنید، نوشتن دستی کد و وارد کردن هر یک در یک زمان ممکن است دردسرساز باشد. بنابراین ارزش آن را دارد که ابزاری مانند Terraforming (http://terraforming.dtan4.net/) را جستجو کنید، که می تواند به طور خودکار کد و حالت را از حساب AWS شما وارد کند.

    Refactoring می تواند مشکلات خود را داشته باشد

    Refactoring یک روش معمول در برنامه نویسی است که در آن ساختار داخلی کد را تغییر می دهید در حالی که رفتار خارجی را بدون تغییر می گذارید. این کار برای شفاف‌تر، منظم‌تر کردن و نگهداری کدها است. Refactoring یک تکنیک ضروری است که باید به طور منظم استفاده شود. اما وقتی صحبت از Terraform یا هر ابزار IaC دیگری به میان می‌آید، باید بسیار مراقب باشید که منظورتان از «رفتار خارجی» یک قطعه کد چیست، در غیر این صورت مشکلات غیرمنتظره‌ای پیش خواهد آمد.

    به عنوان مثال، یک نوع متداول refactoring جایگزینی نام متغیرها یا توابع با موارد قابل درک تر است. بسیاری از IDE ها دارای پشتیبانی داخلی برای refactoring هستند و می توانند به طور خودکار متغیرها و توابع را در طول پروژه تغییر نام دهند. در زبان های برنامه نویسی همه منظوره، این یک روش پیش پا افتاده است که ممکن است به آن فکر نکنید، اما در Terraform باید بسیار مراقب این موضوع باشید، در غیر این صورت ممکن است با قطعی مواجه شوید.

    به عنوان مثال، ماژول webserver-cluster دارای یک متغیر ورودی cluster_name است:

    variable "cluster_name" {
       description = "The name to use for all the cluster resources"
       type          = string
    }

    تصور کنید که شما شروع به استفاده از این ماژول برای استقرار یک میکروسرویس به نام foo کرده اید. بعداً می خواهید نام سرویس خود را به نوار تغییر دهید. این تغییر ممکن است بی اهمیت به نظر برسد، اما در واقعیت می تواند باعث اختلال در خدمات شود.

    واقعیت این است که ماژول webserver-cluster از متغیر cluster_name در تعدادی از منابع از جمله پارامتر نام دو گروه امنیتی و ALB استفاده می کند:

    resource "aws_lb" "example" {
       name                    = var.cluster_name
       load_balancer_type = "application"
       subnets = data.aws_subnet_ids.default.ids
       security_groups      = [aws_security_group.alb.id]
    }

    اگر پارامتر نام یک منبع را تغییر دهید، Terraform نسخه قدیمی آن منبع را حذف می کند و یک نسخه جدید به جای آن ایجاد می کند. اما اگر آن منبع یک ALB باشد، بین حذف آن و دانلود نسخه جدید، مکانیزمی برای هدایت ترافیک به سرور وب خود نخواهید داشت. به همین ترتیب، اگر یک گروه امنیتی حذف شود، سرورهای شما شروع به رد هر گونه ترافیک شبکه می کنند تا زمانی که یک گروه جدید ایجاد شود.

    نوع دیگری از refactoring که ممکن است به آن علاقه مند باشید، تغییر ID Terraform است. بیایید منبع aws_security_group را در ماژول webserver-cluster به عنوان مثال در نظر بگیریم:

    resource "aws_security_group" "instance" {
      # (...)
    }

    شناسه این منبع را نمونه می گویند. تصور کنید که در طول بازسازی تصمیم گرفتید آن را به یک نام قابل فهم تر (به نظر شما) تغییر دهید cluster_instance:

    resource "aws_security_group" "cluster_instance" {
       # (...)
    }

    در نهایت چه اتفاقی خواهد افتاد؟ درست است: یک اختلال.

    Terraform هر شناسه منبع را با شناسه ارائه دهنده ابر مرتبط می کند. به عنوان مثال، iam_user با شناسه کاربری AWS IAM و aws_instance با شناسه سرور AWS EC2 مرتبط است. اگر شناسه منبع را تغییر دهید (مثلاً از نمونه به cluster_instance، همانطور که در مورد aws_security_group است)، به Terraform به نظر می رسد که گویی منبع قدیمی را حذف کرده اید و منبع جدیدی اضافه کرده اید. اگر این تغییرات را اعمال کنید، Terraform گروه امنیتی قدیمی را حذف می‌کند و یک گروه جدید ایجاد می‌کند، در حالی که سرورهای شما شروع به رد هر گونه ترافیک شبکه می‌کنند.

    در اینجا چهار درس کلیدی وجود دارد که باید از این بحث بردارید.

    • همیشه از دستور plan استفاده کنید. می تواند همه این مشکلات را آشکار کند. خروجی آن را به دقت بررسی کنید و به موقعیت هایی توجه کنید که Terraform قصد دارد منابعی را حذف کند که به احتمال زیاد نباید حذف شوند.
    • قبل از حذف ایجاد کنید اگر می‌خواهید منبعی را جایگزین کنید، قبل از حذف منبع اصلی به دقت در مورد اینکه آیا نیاز به ایجاد جایگزین دارید یا خیر فکر کنید. اگر پاسخ مثبت است، create_before_destroy می تواند کمک کند. همین نتیجه را می توان به صورت دستی با انجام دو مرحله به دست آورد: ابتدا یک منبع جدید به پیکربندی اضافه کنید و دستور application را اجرا کنید و سپس منبع قدیمی را از پیکربندی حذف کنید و دوباره از دستور application استفاده کنید.
    • تغییر شناسه ها نیاز به تغییر حالت دارد. اگر می‌خواهید شناسه مرتبط با یک منبع را تغییر دهید (مثلاً نام aws_security_group را از نمونه به cluster_instance تغییر دهید) بدون حذف منبع و ایجاد نسخه جدیدی از آن، باید فایل State Terraform را متناسب با آن به‌روزرسانی کنید. هرگز این کار را به صورت دستی انجام ندهید - به جای آن از دستور terraform state استفاده کنید. هنگام تغییر نام شناسه ها، باید دستور terraform state mv را اجرا کنید که دستور زیر را دارد:
      terraform state mv <ORIGINAL_REFERENCE> <NEW_REFERENCE>

      ORIGINAL_REFERENCE عبارتی است که به منبع به شکل فعلی اش اشاره دارد و NEW_REFERENCE جایی است که می خواهید آن را منتقل کنید. به عنوان مثال، هنگام تغییر نام گروه aws_security_group از نمونه به cluster_instance، باید دستور زیر را اجرا کنید:

      $ terraform state mv 
         aws_security_group.instance 
         aws_security_group.cluster_instance

      این به Terraform می‌گوید که حالتی که قبلاً با aws_security_group.instance مرتبط بود، اکنون باید با aws_security_group.cluster_instance مرتبط شود. اگر پس از تغییر نام و اجرای این دستور، پلان terraform هیچ تغییری را نشان نمی دهد، پس همه چیز را به درستی انجام داده اید.

    • برخی از تنظیمات را نمی توان تغییر داد. پارامترهای بسیاری از منابع تغییر ناپذیر است. اگر بخواهید آنها را تغییر دهید، Terraform منبع قدیمی را حذف می کند و منبع جدیدی را به جای آن ایجاد می کند. هر صفحه منبع معمولاً نشان می دهد که وقتی یک تنظیم خاص را تغییر می دهید چه اتفاقی می افتد، بنابراین حتماً مستندات را بررسی کنید. همیشه از دستور plan استفاده کنید و از استراتژی create_before_destroy استفاده کنید.

    قوام معوق منطبق است... با تعویق

    برخی از APIهای ارائه دهندگان ابر، مانند AWS، ناهمزمان هستند و سازگاری با تاخیر دارند. ناهمزمانی به این معنی است که رابط می‌تواند فوراً پاسخی را بدون منتظر ماندن برای تکمیل عملکرد درخواستی، بازگرداند. سازگاری با تاخیر به این معنی است که انتشار تغییرات در سراسر سیستم ممکن است زمان ببرد. در حالی که این اتفاق می افتد، پاسخ های شما ممکن است متناقض باشد و بستگی به این دارد که کدام منبع داده به تماس های API شما پاسخ می دهد.

    به عنوان مثال، تصور کنید که یک تماس API با AWS برقرار می کنید و از آن می خواهید یک سرور EC2 ایجاد کند. API یک پاسخ «موفقیت‌آمیز» (201 ایجاد شده) را تقریباً فوراً بدون منتظر ماندن برای ایجاد خود سرور برمی‌گرداند. اگر بخواهید فوراً به آن وصل شوید، تقریباً مطمئناً با شکست مواجه می شود زیرا در آن نقطه AWS هنوز در حال آماده سازی منابع است یا، در عوض، سرور هنوز راه اندازی نشده است. علاوه بر این، اگر تماس دیگری برای دریافت اطلاعات در مورد این سرور برقرار کنید، ممکن است با خطای (404 Not Found) مواجه شوید. نکته این است که اطلاعات مربوط به این سرور EC2 ممکن است هنوز در سراسر AWS منتشر شود قبل از اینکه در همه جا در دسترس باشد، باید چند ثانیه صبر کنید.

    هر زمان که از یک API ناهمزمان با ثبات تنبل استفاده می‌کنید، باید به‌طور دوره‌ای درخواست خود را دوباره امتحان کنید تا زمانی که عملکرد کامل شود و در سیستم منتشر شود. متأسفانه، AWS SDK هیچ ابزار خوبی برای این کار ارائه نمی دهد، و پروژه Terraform از مشکلات زیادی مانند 6813 رنج می برد (https://github.com/hashicorp/terraform/issues/6813):

    $ terraform apply
    aws_subnet.private-persistence.2: InvalidSubnetID.NotFound:
    The subnet ID 'subnet-xxxxxxx' does not exist

    به عبارت دیگر، شما یک منبع (مانند یک زیر شبکه) ایجاد می کنید و سپس سعی می کنید اطلاعاتی در مورد آن به دست آورید (مثل شناسه زیرشبکه تازه ایجاد شده)، و Terraform نمی تواند آن را پیدا کند. بسیاری از این اشکالات (از جمله 6813) برطرف شده اند، اما هنوز هم هر از گاهی ظاهر می شوند، به خصوص زمانی که Terraform از یک نوع منبع جدید پشتیبانی می کند. این آزار دهنده است، اما در بیشتر موارد هیچ آسیبی ایجاد نمی کند. وقتی دوباره terraform application را اجرا می کنید، همه چیز باید کار کند، زیرا در این زمان اطلاعات در سراسر سیستم پخش شده است.

    این گزیده از کتاب اوگنی بریکمن ارائه شده است "Terraform: زیرساخت در سطح کد".

منبع: www.habr.com

اضافه کردن نظر