بلاگ/اتوماسیون ساخت و حذف سرور مجازی ساعتی با API آریانت برای CI/CD و تست موقت
به‌روز شده

اتوماسیون ساخت و حذف سرور مجازی ساعتی با API آریانت برای CI/CD و تست موقت

1405/06/232 بازدید
اتوماسیون ساخت و حذف سرور مجازی ساعتی با API آریانت برای CI/CD و تست موقت

اتوماسیون ساخت و حذف سرور مجازی ساعتی با API آریانت برای CI/CD و تست موقت

نمای شماتیک ساخت و حذف خودکار سرور مجازی ساعتی در یک پایپ‌لاین CI/CD بعضی کارهای فنی به یک سرور دائمی نیاز ندارند. اجرای تست‌های یکپارچه، ساخت ایمیج، بررسی اسکریپت نصب، آزمایش مهاجرت پایگاه داده یا ایجاد یک محیط نمایشی برای Pull Request ممکن است فقط چند دقیقه یا چند ساعت طول بکشد. نگه‌داشتن یک ماشین روشن برای چنین کارهایی هم هزینه را بالا می‌برد و هم مدیریت زیرساخت را دشوارتر می‌کند. در این سناریو می‌توان یک سرور مجازی ساعتی با API ساخت، وظیفه موردنظر را روی آن اجرا کرد و سرور را حتی در صورت شکست پایپ‌لاین حذف کرد. نتیجه، محیطی تمیز و تکرارپذیر است که فقط هنگام نیاز وجود دارد. این مقاله معماری و الگوی پیاده‌سازی چنین فرایندی را با API و ابزارهای خط فرمان توضیح می‌دهد. نام دقیق endpointها و فیلدهای درخواست ممکن است با نسخه API یا تنظیمات حساب شما تفاوت داشته باشد؛ بنابراین مقادیر نمونه را با مستندات API و اطلاعات پنل آریانت تطبیق دهید.

سرور موقت در CI/CD چه مسئله‌ای را حل می‌کند؟

رانرهای اشتراکی سرویس‌های CI برای بسیاری از پروژه‌ها کافی‌اند، اما همیشه محیط مناسبی فراهم نمی‌کنند. شاید تست شما به دسترسی ریشه، کرنل مشخص، شبکه اختصاصی، Docker-in-Docker یا منابعی بیشتر از رانر پیش‌فرض نیاز داشته باشد. گاهی نیز باید نصب نرم‌افزار را روی یک سیستم‌عامل کاملاً تازه آزمایش کنید. سرور موقت برای این موارد مناسب است: - اجرای تست‌های یکپارچه یا End-to-End روی ماشین مستقل - ساخت بسته، ایمیج کانتینر یا آرتیفکت‌های حجیم - آزمایش اسکریپت‌های Ansible، cloud-init و نصب خودکار - ایجاد محیط آزمایشی جداگانه برای هر Pull Request - بررسی ارتقا یا مهاجرت سرویس بدون دست‌کاری محیط اصلی - اجرای کارهای زمان‌بندی‌شده‌ای که منابع زیادی می‌خواهند مزیت اصلی این روش، جداسازی هر اجراست. پایپ‌لاین با یک سیستم تازه شروع می‌شود و فایل‌ها، پردازه‌ها یا تنظیمات اجرای قبلی روی نتیجه اثر نمی‌گذارند. در پایان نیز با حذف ماشین، منابع آزاد می‌شوند. برای آشنایی بیشتر با مدل صورت‌حساب ساعتی و تفاوت آن با پلن‌های ماهانه، سرور مجازی ساعتی چیست و چه زمانی به آن نیاز دارید؟ را ببینید.

معماری چرخه عمر سرور مجازی ساعتی

چرخه عمر پیشنهادی پنج مرحله دارد: 1. پایپ‌لاین با توکن ذخیره‌شده در Secret Store به API متصل می‌شود. 2. یک سرور با ایمیج، پلن، منطقه و کلید SSH مشخص ساخته می‌شود. 3. اسکریپت تا آماده‌شدن سرور و قابل‌دسترسی‌شدن SSH صبر می‌کند. 4. کد و تست‌ها روی سرور اجرا و خروجی لازم جمع‌آوری می‌شود. 5. یک مرحله پاک‌سازی، سرور را مستقل از موفقیت یا شکست کار حذف می‌کند. ساخت و حذف سرور مجازی از طریق فراخوانی API نکته مهم این است که حذف سرور نباید فقط آخرین دستور مسیر موفقیت باشد. اگر نصب وابستگی شکست بخورد، تست Timeout شود یا کاربر اجرای پایپ‌لاین را لغو کند، مرحله پاک‌سازی همچنان باید اجرا شود. بیشتر سامانه‌های CI قابلیتی مانند always، after_script یا trap شل برای این کار دارند.

پیش‌نیازهای امن برای اتصال به API

پیش از نوشتن اسکریپت، این اطلاعات را آماده کنید: - نشانی پایه API و مسیرهای ساخت، مشاهده وضعیت و حذف سرور - توکن API با حداقل دسترسی لازم - شناسه ایمیج سیستم‌عامل، پلن و منطقه - کلید SSH مخصوص اتوماسیون - محدودیت زمانی برای انتظار آماده‌شدن ماشین - یک الگوی نام‌گذاری قابل ردیابی توکن را داخل مخزن، فایل YAML یا خروجی لاگ قرار ندهید. آن را در Secret Store سرویس CI ذخیره و از طریق متغیر محیطی دریافت کنید. همچنین نمایش دستورات با set -x می‌تواند هدر Authorization را وارد لاگ کند؛ هنگام کار با توکن این قابلیت را فعال نکنید. برای نام ماشین می‌توان شناسه پروژه و اجرای پایپ‌لاین را ترکیب کرد:

ci-<project>-<pipeline-id>-<job-id>

این نام به شناسایی منابع رهاشده کمک می‌کند. افزودن برچسب‌هایی مانند managed-by=ci، نام پروژه و زمان انقضا نیز پاک‌سازی دوره‌ای را ساده‌تر می‌کند، البته اگر API حساب شما از برچسب پشتیبانی کند.

الگوی ساخت سرور با curl و jq

نمونه زیر عمداً مسیر endpoint و نام فیلدهای وابسته به سرویس را در متغیرها نگه می‌دارد. آن‌ها را طبق مستندات API آریانت تنظیم کنید. ابزار jq نیز برای ساخت JSON و خواندن پاسخ لازم است.

#!/usr/bin/env bash
set -Eeuo pipefail
: "${ARIANET_API_BASE:?ARIANET_API_BASE is required}"
: "${ARIANET_API_TOKEN:?ARIANET_API_TOKEN is required}"
: "${ARIANET_CREATE_PATH:?ARIANET_CREATE_PATH is required}"
: "${ARIANET_DELETE_PATH:?ARIANET_DELETE_PATH is required}"
: "${ARIANET_IMAGE_ID:?ARIANET_IMAGE_ID is required}"
: "${ARIANET_PLAN_ID:?ARIANET_PLAN_ID is required}"
: "${ARIANET_REGION_ID:?ARIANET_REGION_ID is required}"
: "${ARIANET_SSH_KEY_ID:?ARIANET_SSH_KEY_ID is required}"
SERVER_ID=""
SERVER_NAME="ci-${CI_PROJECT_NAME:-project}-${CI_PIPELINE_ID:-local}"
api_request() {
  local method="$1"
  local path="$2"
  local body="${3:-}"
  if [[ -n "$body" ]]; then
    curl --fail-with-body --silent --show-error \
      --request "$method" \
      --header "Authorization: Bearer ${ARIANET_API_TOKEN}" \
      --header "Content-Type: application/json" \
      --data "$body" \
      "${ARIANET_API_BASE}${path}"
  else
    curl --fail-with-body --silent --show-error \
      --request "$method" \
      --header "Authorization: Bearer ${ARIANET_API_TOKEN}" \
      "${ARIANET_API_BASE}${path}"
  fi
}

بهتر است JSON را با jq بسازید تا کاراکترهای خاص نام پروژه باعث خراب‌شدن درخواست نشوند:

CREATE_PAYLOAD="$(
  jq -n \
    --arg name "$SERVER_NAME" \
    --arg image_id "$ARIANET_IMAGE_ID" \
    --arg plan_id "$ARIANET_PLAN_ID" \
    --arg region_id "$ARIANET_REGION_ID" \
    --arg ssh_key_id "$ARIANET_SSH_KEY_ID" \
    '{
      name: $name,
      image_id: $image_id,
      plan_id: $plan_id,
      region_id: $region_id,
      ssh_key_id: $ssh_key_id
    }'
)"
CREATE_RESPONSE="$(
  api_request POST "$ARIANET_CREATE_PATH" "$CREATE_PAYLOAD"
)"
SERVER_ID="$(jq -er '.id' <<<"$CREATE_RESPONSE")"
SERVER_IP="$(jq -er '.ip // .ipv4 // empty' <<<"$CREATE_RESPONSE")"
printf 'Temporary server created: %s\n' "$SERVER_ID"

ساختار واقعی پاسخ ممکن است شناسه یا IP را در یک شیء تو‌در‌تو برگرداند. عبارت‌های jq را پس از بررسی یک پاسخ واقعی تنظیم کنید. بهتر است پاسخ کامل را در لاگ عمومی چاپ نکنید، زیرا ممکن است اطلاعات عملیاتی یا محرمانه داشته باشد.

منتظر آماده‌شدن واقعی سرور بمانید

دریافت پاسخ موفق از درخواست ساخت لزوماً به معنای آماده‌بودن سیستم‌عامل نیست. ممکن است ماشین ایجاد شده باشد، اما شبکه، cloud-init یا سرویس SSH هنوز آماده نباشد. برای انتظار دو مرحله در نظر بگیرید: - وضعیت سرور را از API تا رسیدن به حالت فعال بررسی کنید. - سپس با یک Timeout محدود، اتصال SSH را آزمایش کنید. نمونه ساده برای بررسی SSH:

SSH_WAIT_SECONDS="${SSH_WAIT_SECONDS:-300}"
STARTED_AT="$(date +%s)"
until ssh \
  -o BatchMode=yes \
  -o ConnectTimeout=5 \
  -o StrictHostKeyChecking=accept-new \
  "ci-user@${SERVER_IP}" true 2>/dev/null
do
  NOW="$(date +%s)"
  if (( NOW - STARTED_AT >= SSH_WAIT_SECONDS )); then
    echo "SSH did not become ready before timeout" >&2
    exit 1
  fi
  sleep 5
done

در یک محیط حساس، کلید میزبان را از منبع قابل‌اعتماد دریافت و اعتبارسنجی کنید. گزینه accept-new برای محیط‌های کوتاه‌عمر ساده است، اما جلوی همه حملات جعل میزبان را نمی‌گیرد. اگر برای راه‌اندازی اولیه سرور از cloud-init استفاده می‌کنید، پس از برقراری SSH نیز پایان آن را بررسی کنید:

ssh "ci-user@${SERVER_IP}" \
  'sudo cloud-init status --wait'

اجرای تست و برگرداندن نتیجه

پس از آماده‌شدن ماشین می‌توانید کد را با rsync منتقل کنید یا از داخل سرور مخزن را با یک توکن کوتاه‌عمر دریافت کنید. انتقال آرتیفکت آماده معمولاً کنترل بیشتری بر نسخه دقیق کد دارد.

rsync -az --delete \
  -e "ssh -o BatchMode=yes" \
  ./ "ci-user@${SERVER_IP}:/opt/test-workspace/"
ssh "ci-user@${SERVER_IP}" '
  set -Eeuo pipefail
  cd /opt/test-workspace
  ./scripts/install-dependencies.sh
  ./scripts/run-integration-tests.sh
'

خروجی تست، گزارش پوشش و لاگ‌های لازم را پیش از حذف سرور به فضای آرتیفکت CI برگردانید:

mkdir -p artifacts
rsync -az \
  -e "ssh -o BatchMode=yes" \
  "ci-user@${SERVER_IP}:/opt/test-workspace/test-results/" \
  artifacts/

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

حذف سرور حتی پس از شکست پایپ‌لاین

تابع پاک‌سازی باید بلافاصله پس از تعریف متغیر شناسه سرور ثبت شود. در Bash می‌توان از trap استفاده کرد:

cleanup() {
  local original_exit="$?"
  if [[ -n "${SERVER_ID:-}" ]]; then
    local delete_path
    delete_path="${ARIANET_DELETE_PATH/\{id\}/$SERVER_ID}"
    if api_request DELETE "$delete_path" >/dev/null; then
      printf 'Temporary server deleted: %s\n' "$SERVER_ID"
    else
      printf 'WARNING: failed to delete server %s\n' "$SERVER_ID" >&2
    fi
  fi
  exit "$original_exit"
}
trap cleanup EXIT INT TERM

در این نمونه، مقدار ARIANET_DELETE_PATH می‌تواند الگویی مانند مسیر مستندشده API با بخش {id} باشد. حذف باید idempotent طراحی شود؛ یعنی اگر ماشین قبلاً حذف شده بود، اجرای دوباره پاک‌سازی وضعیت خطرناکی ایجاد نکند. پس از درخواست DELETE بهتر است وضعیت نهایی را نیز بررسی کنید. برخی APIها ابتدا عملیات حذف را در صف قرار می‌دهند و پاسخ موفق تنها پذیرش درخواست را نشان می‌دهد. اگر پایان قطعی حذف برای کنترل هزینه مهم است، تا ناپدیدشدن منبع یا رسیدن آن به وضعیت حذف‌شده Poll کنید.

برای پروژه‌هایی که مدیریت زیرساخت پیچیده‌تری دارند، رویکردهای Infrastructure as Code مانند Terraform نیز می‌تواند همین چرخهٔ ساخت و حذف را با تعریف اعلانی منابع پیاده‌سازی کند.

مقایسه رانر دائمی و سرور ساعتی موقت

انتخاب مناسب به تعداد اجراها، مدت هر اجرا و هزینه نگهداری بستگی دارد.

معیاررانر دائمیسرور ساعتی موقت
زمان شروع کارمعمولاً فورینیازمند زمان ساخت و آماده‌شدن
جداسازی اجراهانیازمند پاک‌سازی دقیقهر اجرا با ماشین تازه شروع می‌شود
هزینه در زمان بیکاریادامه داردپس از حذف متوقف می‌شود
نگهداری سیستم‌عاملبر عهده تیمایمیج پایه باید نگهداری شود
مناسب برای بار پیوستهبلهمعمولاً خیر
مناسب برای تست‌های مقطعیقابل استفادهگزینه مناسب‌تر
خطر باقی‌ماندن منابعکمترنیازمند حذف تضمین‌شده و پایش

اگر پایپ‌لاین در بیشتر ساعات شبانه‌روز فعال است، رانر دائمی ممکن است اقتصادی‌تر و سریع‌تر باشد. برای اجراهای نامنظم، تست نسخه‌های مختلف سیستم‌عامل یا محیط‌های جداگانه Pull Request، مدل موقت انعطاف بیشتری دارد.

نمونه ساختار در GitLab CI و GitHub Actions

در GitLab CI می‌توان ساخت، اجرا و حذف را در یک Job نگه داشت تا trap شل پاک‌سازی را انجام دهد. after_script نیز یک لایه حفاظتی دیگر است. برای راه‌اندازی Runner خودمیزبان به‌جای رانرهای اشتراکی، راه‌اندازی GitLab Runner خودمیزبان روی سرور مجازی و اختصاصی برای CI/CD را ببینید:

integration-test:
  image: alpine:latest
  script:
    - apk add --no-cache bash curl jq openssh-client rsync
    - bash ci/run-on-temporary-server.sh
  artifacts:
    when: always
    paths:
      - artifacts/
  after_script:
    - bash ci/delete-orphan-server.sh || true

در GitHub Actions مرحله حذف باید با if: always() اجرا شود:

- name: Create temporary server and run tests
  id: temporary_server
  run: bash ci/create-and-test.sh
- name: Upload test results
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: integration-test-results
    path: artifacts/
- name: Delete temporary server
  if: always()
  run: bash ci/delete-server.sh

شناسه سرور را بین Stepها در خروجی امن Job یا یک فایل موقت نگه دارید. اگر فایل را به‌عنوان آرتیفکت منتشر می‌کنید، مطمئن شوید حاوی توکن، کلید خصوصی یا داده حساس نیست.

جلوگیری از هزینه منابع رهاشده

حتی یک trap درست هم همه حالت‌ها را پوشش نمی‌دهد. ممکن است رانر ناگهان خاموش شود، سرویس CI قطع شود یا فرایند با توقف اجباری پایان یابد. به همین دلیل یک پاک‌ساز دوره‌ای لازم است. این Job می‌تواند هر ساعت فهرست سرورهای دارای برچسب managed-by=ci را دریافت کند و موارد قدیمی‌تر از سقف تعیین‌شده را حذف یا برای بررسی گزارش کند. برای کاهش خطر، پاک‌ساز باید فقط منابعی را ببیند که هم نام و هم برچسب مورد انتظار را دارند. چند محافظ عملی دیگر عبارت‌اند از: - تعیین سقف تعداد سرورهای هم‌زمان برای هر پروژه - ثبت زمان انقضا هنگام ساخت، در صورت پشتیبانی API - هشدار برای ماشین‌های CI قدیمی‌تر از چند ساعت - محدودکردن پلن‌ها و مناطق مجاز در اسکریپت - ثبت شناسه سرور و شناسه پایپ‌لاین در سامانه لاگ - محاسبه هزینه هر اجرا از زمان ساخت تا حذف پاک‌سازی دوره‌ای نباید جای حذف داخل پایپ‌لاین را بگیرد. این دو سازوکار مکمل یکدیگرند: اولی مسیر معمول را پوشش می‌دهد و دومی برای خرابی‌های غیرمنتظره است.

کنترل هم‌زمانی و خطاهای API

در پروژه‌های شلوغ ممکن است چند Pull Request هم‌زمان سرور بسازند. محدودیت حساب، موجودی منطقه یا Rate Limit API می‌تواند بعضی درخواست‌ها را ناموفق کند. برای خطاهای موقت از Retry با فاصله افزایشی استفاده کنید، اما خطاهای اعتبارسنجی مانند شناسه پلن اشتباه را بدون اصلاح درخواست تکرار نکنید. برای درخواست ساخت نیز یک کلید idempotency مفید است، اگر API آن را پشتیبانی کند. این کلید مانع ایجاد دو سرور می‌شود وقتی پاسخ درخواست اول در شبکه گم شده، اما خود عملیات با موفقیت انجام شده است. همچنین برای هر مرحله Timeout مشخص بگذارید. پایپ‌لاینی که بدون محدودیت منتظر وضعیت سرور می‌ماند، هم رانر CI را اشغال می‌کند و هم تشخیص خرابی را عقب می‌اندازد.

انتخاب پلن مناسب برای محیط موقت

سرور کوچک همیشه ارزان‌ترین انتخاب عملی نیست. اگر کمبود RAM یا CPU باعث شود تست چند برابر بیشتر طول بکشد، پلن کمی بزرگ‌تر می‌تواند زمان و هزینه نهایی را کاهش دهد. تصمیم را بر اساس اندازه پروژه و داده‌های اجرای واقعی بگیرید. برای شروع، زمان ساخت، مدت آماده‌سازی، زمان تست و زمان حذف را جداگانه ثبت کنید. بعد از چند اجرا مشخص می‌شود کدام بخش کند است و آیا تغییر ایمیج، پلن یا روش نصب وابستگی‌ها نتیجه بهتری می‌دهد. برای مقایسه دقیق‌تر گزینه‌های پلن، راهنمای انتخاب پلن سرور مجازی آریانت را نیز ببینید. اگر برای پیاده‌سازی این الگو به یک زیرساخت موقت نیاز دارید، می‌توانید مشخصات سرور مجازی ابری آریانت را بررسی و پلن را متناسب با مصرف CPU، حافظه و مدت اجرای پایپ‌لاین انتخاب کنید.

جمع‌بندی

اتوماسیون یک سرور مجازی ساعتی با API سه بخش اصلی دارد: ساخت منبع با تنظیمات مشخص، انتظار برای آماده‌شدن واقعی ماشین و حذف تضمین‌شده پس از جمع‌آوری نتایج. بخش حذف از خود عملیات ساخت مهم‌تر است، چون شکست یا لغو پایپ‌لاین نباید یک سرور روشن و بدون مالک باقی بگذارد. توکن محدود، Timeout، نام‌گذاری قابل ردیابی، trap یا مرحله always و پاک‌سازی دوره‌ای، این فرایند را برای استفاده مداوم قابل‌اعتماد می‌کنند. پس از راه‌اندازی نسخه اولیه، زمان و هزینه هر اجرا را اندازه بگیرید تا درباره پلن، ایمیج پایه و صرفه اقتصادی رانر موقت بر اساس داده واقعی تصمیم بگیرید.