این مقاله تجربه ما را از حذف EC2 instance های همیشه روشن، راه‌اندازی GitLab Runner های مقیاس‌پذیر در AWS و کاهش محسوس هزینه‌های زیرساخت CI روایت می‌کند. همه‌چیز در ابتدا بی‌نقص پیش می‌رفت، اما یک‌دفعه اوضاع تغییر کرد. در مقطعی، Job های GitLab با تأخیرهای ۵، ۱۰ و حتی ۱۵ دقیقه‌ای روبه‌رو شدند. صف pipeline ها عقب افتاد، تیم DevOps نگران شد، توسعه‌دهنده‌ها کلافه شدند و AWS هم بی‌سروصدا هزینه صدها ساعت مصرف EC2 را از ما می‌گرفت. ترفند «فعلاً چند تا EC2 بالا بیاوریم» فقط تا یک جایی جواب می‌دهد. خیلی زود دوباره با همان مشکل‌های قبلی روبه‌رو شدیم: instance های بیکار، هدررفتن پول و نبودِ isolation واقعی برای Job ها. فهمیدیم که وصله‌پینه کردن مداوم این مشکل، بن‌بست است و راه درست، پیاده‌سازی autoscaling واقعی برای GitLab Runner ها بود. 🚀

هدف ما: scaling خودکار و هزینه کمتر بود. می‌خواستیم CI را طوری طراحی کنیم که با workload های متغیر هماهنگ شود و هزینه AWS را پایین بیاورد. به‌جای نگه‌داشتن runner ها روی EC2 های همیشه روشن، ایده این بود که برای Job های مشخص یا گروهی از Job ها، instance ها به‌صورت خودکار بالا بیایند. وقتی Job جدیدی می‌رسید، یک EC2 instance ساخته می‌شد. بعد از تمام شدن Job، همان instance یا به Job بعدی در صف منتقل می‌شد یا اگر کاری باقی نمانده بود، خودکار خاموش می‌شد. در این مدل، تنها EC2 همیشه روشن ما یک managing instance بود که GitLab Runner روی آن اجرا می‌شد و وظیفه راه‌اندازی و هماهنگ‌سازی Job ها را بر عهده داشت. این instance منابع بسیار کمی مصرف می‌کرد، بنابراین هزینه نگه‌داشتن آن در مقایسه با workload های واقعی تقریباً ناچیز بود. خلاصه اهداف ما این‌ها بود: به‌محض ورود GitLab CI Job ها، EC2 instance به‌صورت خودکار بالا بیاید؛ اگر instance برای N دقیقه بیکار ماند، متوقف یا terminate شود؛ Job ها ایزوله بمانند: هر VM برای یک Job یا یک batch کوچک از Job ها؛ ساخت AMI ها به‌صورت خودکار انجام شود تا instance های یکسان و آماده سریع بالا بیایند؛ و مدیریت runner ها با Terraform و در قالب infrastructure as code انجام شود. 🛠️

نمای کلی workflow و setup در GitLab + AWS این‌طور بود: GitLab یک Job را با tag مشخص trigger می‌کند → managing Runner آن را برمی‌دارد و با کمک Fleeting Plugin یک EC2 instance جدید بالا می‌آورد → build روی همان instance اجرا می‌شود → بعد از اتمام Job، instance به‌صورت خودکار stop یا delete می‌شود. ما این پیاده‌سازی را با GitLab Runner نسخه 15.11+ و قابلیت Fleet scaling در یک محیط مبتنی بر Ubuntu Linux انجام دادیم. Fleet scaling قابلیت داخلی GitLab برای مقیاس‌دهی runner ها با استفاده از منابع خارجی است. یعنی Job ها روی خود سرور runner اجرا نمی‌شوند، بلکه روی virtual machine های موقتی در cloud اجرا می‌شوند؛ در این سناریو AWS. Fleeting هم library/plugin ای است که این مدل را عملی می‌کند و runner را به cloud وصل می‌کند. این ابزار مسئول provision کردن EC2 instance ها با درخواست runner، اتصال به آن‌ها معمولاً از طریق SSM، واگذار کردن Job ها به آن‌ها و در نهایت terminate یا delete کردن آن‌ها وقتی دیگر نیازی به‌شان نیست، است. در ادامه، مراحلی را که برای رسیدن به هدف‌مان طی کردیم مرور می‌کنیم:

انتخاب executor. باید مشخص می‌کردیم GitLab Runner دقیقاً Job ها را چطور اجرا کند: مستقیم روی VM، داخل Docker container یا روی EC2 های موقتی.

نصب GitLab Runner روی managing EC2. این runner همیشه روشن، Job ها را از GitLab دریافت می‌کند و با کمک Fleeting، instance های موقتی را برای آن‌ها provision می‌کند.

ساخت IAM user. یک user اختصاصی مثل gitlab-autoscaler تعریف می‌کنیم تا runner با credential های آن بتواند با EC2 و Auto Scaling ارتباط بگیرد.

تنظیم IAM policy. به IAM user فقط همان permission هایی را می‌دهیم که لازم دارد: ساخت و حذف instance ها، تعامل با Auto Scaling Group و خواندن اطلاعات resource ها.

نصب Fleeting Plugin. این plugin runner را به AWS وصل می‌کند و امکان start و stop خودکار EC2 instance ها را فراهم می‌سازد.

تنظیم GitLab Runner. فایل config.toml را برای executor ها، پارامترهای autoscaler، S3 caching، سیاست‌های idle مثل idle_time و حداکثر تعداد instance ها تنظیم می‌کنیم.

آماده‌سازی AMI برای worker instance ها. یک base image می‌سازیم که همه نرم‌افزارهای لازم مثل gitlab-runner، docker، kubectl، helm و موارد مشابه را داشته باشد. بعداً همین AMI را در Launch Template استفاده می‌کنیم.

آماده‌سازی Auto Scaling Group. ASG و Launch Template را می‌سازیم و تنظیم می‌کنیم: نوع instance، AMI، پارامترهای scaling و در صورت نیاز، آپدیت image را هم مشخص می‌کنیم.

در بخش implementation، اول سراغ انتخاب GitLab Runner executor می‌رویم. قبل از ورود به config ها و setup، بد نیست کمی درباره executor های GitLab Runner و تفاوت‌های آن‌ها صحبت کنیم. یک مقایسه فنی کوتاه از executor ها:

shell — isolation ندارد، روی local VM اجرا می‌شود، مناسب برای script های ساده و تست‌های سریع

docker — isolation خوب، روی Docker host اجرا می‌شود، مناسب برای frontend، unit test و microservice ها

instance — isolation و environment اختصاصی دارد، روی EC2 اختصاصی اجرا می‌شود، مناسب برای Terraform، shell job ها، Ansible و ابزارهای مختلف

docker-autoscaler — isolation و مقیاس‌پذیری خوب، Docker روی EC2 اجرا می‌شود، مناسب برای job های containerized، build ها و CI/CD در frontend

kubernetes — isolation و scaling بسیار خوب، روی Kubernetes Pod اجرا می‌شود، مناسب برای زیرساخت‌های CI/CD بزرگ و مقیاس‌پذیر

تمرکز ما روی instance و docker-autoscaler executor است، چون این دو می‌توانند برای Job های مشخص EC2 instance را به‌صورت خودکار بالا و پایین ببرند؛ isolation خیلی خوبی می‌دهند: برای هر Job یک VM یا یک container روی یک instance اختصاصی؛ و از GitLab Fleet Scaling API استفاده می‌کنند که مکانیزم بومی autoscaling برای Runner هاست. درباره kubernetes executor هم باید گفت که هرچند isolation و scaling خوبی دارد، اما نیازمند داشتن و نگهداری یک Kubernetes cluster است. این موضوع خودش بسیار بزرگ است و باید جداگانه بررسی شود، بنابراین برای ساده‌سازی این راهنما از آن عبور می‌کنیم.

در گام بعد، GitLab Runner را روی managing instance نصب می‌کنیم. این instance همیشه روشن مسئول این کارهاست: دریافت Job ها از GitLab؛ تعامل با Fleeting plugin؛ provision کردن EC2 instance های موقتی برای Job ها. یادتان باشد همه مراحل بعدی setup مثل نصب plugin ها، تغییر config.toml و اجرای تست‌ها، دقیقاً روی همین managing instance انجام می‌شود. ⚙️

curl -L “https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh” -o gitlab-repo-add.sh
less gitlab-repo-add.sh # بررسی محتوای script برای اطمینان از امن بودن
sudo bash gitlab-repo-add.sh
sudo apt-get install -y gitlab-runner

قبل از نصب Fleeting Plugin، باید دسترسی به AWS را آماده کنید. این plugin برای کار کردن باید بتواند به instance ها وصل شود، معمولاً از طریق SSM یا SSH؛ EC2 instance ها را create و delete کند؛ و به Auto Scaling Group و Launch Template دسترسی داشته باشد. همه این‌ها از طریق IAM در AWS تنظیم می‌شوند. ساده‌ترین راه این است که یک IAM user جداگانه، مثلاً gitlab-autoscaler، بسازید. GitLab Runner از credential های همین user برای ارتباط با AWS استفاده می‌کند. این credential ها در profile ای که در config.toml آمده، استفاده می‌شوند:

profile = default

روی ماشینی که GitLab Runner روی آن اجرا می‌شود، باید کلیدهای این user را داخل فایل ~/.aws/credentials قرار دهید:

[default]
aws_access_key_id = …
aws_secret_access_key = …

برای اینکه user گیت‌لب‌اتو‌اسکیلر بتواند resource ها را مدیریت کند، باید permission های مناسب را به آن بدهیم. یک IAM policy مثلاً با نام gitlab-runner-autoscaling-policy می‌سازیم و به user وصل می‌کنیم. این policy به user اجازه می‌دهد: EC2 instance ها را create و delete کند؛ description مربوط به instance ها، image ها و tag ها را بخواند؛ از Auto Scaling Group مربوط به gitlab-runner-ao-group استفاده کند، یعنی autoScalingGroupName/gitlab-runner-ao-group. این policy مشخص به Fleeting Plugin اجازه می‌دهد instance ها را در ASG تعیین‌شده بالا و پایین ببرد. 🔐

نمونه policy به شکل JSON را در نظر بگیرید؛ فقط باید $ACCOUNT_ID را با AWS Account ID واقعی خودتان جایگزین کنید:

{ “Version”: “2012-10-17”, “Statement”: [ { “Effect”: “Allow”, “Action”: [ “autoscaling:SetDesiredCapacity”, “autoscaling:TerminateInstanceInAutoScalingGroup” ], “Resource”: “arn:aws:autoscaling:eu-central-1:{$ACCOUNT_ID}:autoScalingGroup:…:autoScalingGroupName/gitlab-runner-ao-group” }, { “Effect”: “Allow”, “Action”: [ “autoscaling:DescribeAutoScalingGroups”, “ec2:DescribeInstances” ], “Resource”: “*” }, { “Effect”: “Allow”, “Action”: [ “ec2:GetPasswordData”, “ec2-instance-connect:SendSSHPublicKey” ], “Resource”: “arn:aws:ec2:eu-central-1:{$ACCOUNT_ID}:instance/*”, “Condition”: { “StringEquals”: { “ec2:ResourceTag/aws:autoscaling:groupName”: “gitlab-runner-ao-group” } } } ] }

حالا که IAM user آماده است و key ها هم تنظیم شده‌اند، وقت نصب Fleeting Plugin است:

# Install Fleeting Plugin for AWS
echo “Installing Fleeting Plugin…”
sudo gitlab-runner fleeting install aws:latest
# Create AWS credentials directory and files
sudo mkdir -p /home/gitlab-runner/.aws
sudo chown gitlab-runner:gitlab-runner /home/gitlab-runner/.aws
# Create AWS credentials file with S3 cache credentials
sudo tee /home/gitlab-runner/.aws/credentials > /dev/null