السحابة المتعددة: Azure وGCP

مشروع: معمارية قابلة للنقل بين السحب

18 دقيقة الدرس 10 من 28

مشروع: معمارية قابلة للنقل بين السحب

هذا الدرس هو التتويج النهائي لبرنامج السحب المتعددة. ستقوم بتصميم حمل عمل واحد في بيئة إنتاج — خدمة API عديمة الحالة مدعومة بقاعدة بيانات مُدارة ومخزن كائنات — وستنتج إعدادات ملموسة وقابلة للنشر على كل من Azure وGCP. الهدف ليس كتابة معماريتين منفصلتين؛ بل كتابة معمارية واحدة لها تعبيران سحابيان، مدفوعة بوحدة Terraform جذرية مشتركة وملف vars خاص بكل سحابة. هذا هو الانضباط الذي يُفرّق بين الكفاءة في السحب المتعددة والفوضى فيها.

تعريف حمل العمل: النظام المستهدف

حمل العمل هو REST API يعالج طلبات المستخدمين، ويقرأ ويكتب إلى قاعدة بيانات علائقية مُدارة، ويخزن الملفات المرفوعة في مخزن الكائنات، ويُصدر سجلات منظّمة. وهو محجّب في حاويات ويُدار بواسطة Kubernetes. عقد قابلية النقل هي:

  • الحوسبة: Kubernetes (AKS على Azure، GKE على GCP) — نفس مخطط Helm، ونفس ملفي تعريف Deployment وService.
  • قاعدة البيانات: PostgreSQL المُدار (Azure Database for PostgreSQL Flexible Server / Cloud SQL for PostgreSQL) — نفس المخطط، ونفس تنسيق سلسلة الاتصال.
  • مخزن الكائنات: Azure Blob Storage (عبر ADLS Gen2) / GCS — مجرّد خلف مكتبة عميل للتخزين.
  • الأسرار: Azure Key Vault / GCP Secret Manager — يُحقن عند بدء تشغيل الحاوية عبر Secrets Store CSI Driver.
  • الشبكات: نقاط النهاية الخاصة لقاعدة البيانات؛ لا IP عام على قاعدة البيانات في أي سحابة.
حدود قابلية النقل عند طبقة البنية التحتية، وليس طبقة التطبيق. صورة حاوية API متطابقة — تُبنى مرة واحدة في CI وتُدفع إلى سجل مشترك. التفاصيل الخاصة بكل سحابة (سلاسل الاتصال، أسماء الحاويات، مسارات الأسرار) تُحقن كمتغيرات بيئية. شفرة التطبيق لا تحتوي أبدًا على فرع if cloud == azure.

مخططات المعمارية: Azure وGCP جنبًا إلى جنب

Cloud-portable architecture deployed on Azure (AKS) Azure — VNet: 10.0.0.0/16 Public Subnet Application Gateway (WAF) NAT Gateway AKS Node Subnet API Pods (Deployment) HPA: 2-20 replicas Workload Identity (OIDC) ClusterIP Service Private Subnet Private Endpoint PostgreSQL Flexible Server Private Endpoint Azure Key Vault Service Endpoint Blob Storage (ADLS Gen2) Internet Azure target architecture
المعمارية المستهدفة على Azure — مجموعة AKS في شبكة فرعية مخصصة لعُقد المجموعة، جميع خدمات البيانات على نقاط نهاية خاصة، والوصول الخارجي عبر Application Gateway مع WAF.
Cloud-portable architecture deployed on GCP (GKE) GCP — VPC: 10.1.0.0/16 GKE Node Subnet API Pods (Deployment) HPA: 2-20 replicas Workload Identity (GSA) ClusterIP Service Ingress / LB GCP L7 Load Balancer (Cloud Armor WAF) Cloud NAT Managed Services (PSA) Private Service Access Cloud SQL (PostgreSQL) VPC Service Controls Secret Manager VPC Service Controls Cloud Storage (GCS) Internet GCP target architecture
المعمارية المستهدفة على GCP — مجموعة GKE في شبكة فرعية مخصصة لعُقد المجموعة، Cloud SQL وSecret Manager يُوصَل إليهما عبر Private Service Access، والوصول الخارجي عبر موازن حمل GCP L7 مع Cloud Armor WAF.

وحدة Terraform الجذرية المشتركة

تعيش البنية التحتية كرمز في مستودع Git واحد. يفصل هيكل المجلدات بين ملفات Kubernetes غير المرتبطة بسحابة بعينها (مخطط Helm) وإعدادات Terraform الخاصة بكل سحابة. يُشير ملف backend.tf لكل بيئة إلى Azure Blob أو GCS لتخزين الحالة. يُعرّف ملف variables.tf العقد؛ ويُزوّد ملفا azure.tfvars وgcp.tfvars القيم الخاصة بكل سحابة.

## هيكل المستودع ## infra/ ## modules/ ## k8s-api/ <-- استدعاء Helm غير مرتبط بسحابة ## azure-network/ <-- AKS, VNet, Private Endpoints ## gcp-network/ <-- GKE, VPC, PSA, Cloud NAT ## envs/ ## prod-azure/ ## main.tf backend: azurerm ## azure.tfvars ## prod-gcp/ ## main.tf backend: gcs ## gcp.tfvars ## envs/prod-azure/main.tf terraform { required_providers { azurerm = { source = "hashicorp/azurerm", version = "~> 3.100" } helm = { source = "hashicorp/helm", version = "~> 2.13" } } backend "azurerm" { resource_group_name = "rg-tfstate" storage_account_name = "tfstateprodXXXX" container_name = "tfstate" key = "prod-azure.tfstate" } } module "network" { source = "../../modules/azure-network" vnet_address_space = ["10.0.0.0/16"] aks_subnet_cidr = "10.0.1.0/24" private_subnet_cidr = "10.0.2.0/24" location = var.azure_location resource_group = var.resource_group } module "api" { source = "../../modules/k8s-api" kubeconfig = module.network.kubeconfig image_tag = var.image_tag db_host = module.network.db_private_fqdn secret_provider = "azure" vault_name = module.network.key_vault_name storage_bucket = module.network.blob_container_url }
## envs/prod-gcp/main.tf terraform { required_providers { google = { source = "hashicorp/google", version = "~> 5.30" } helm = { source = "hashicorp/helm", version = "~> 2.13" } } backend "gcs" { bucket = "tf-state-prod-gcp-XXXX" prefix = "prod-gcp" } } module "network" { source = "../../modules/gcp-network" project_id = var.gcp_project_id region = var.gcp_region vpc_cidr = "10.1.0.0/16" gke_subnet_cidr = "10.1.1.0/24" pods_cidr = "10.100.0.0/14" services_cidr = "10.104.0.0/20" } module "api" { source = "../../modules/k8s-api" kubeconfig = module.network.kubeconfig image_tag = var.image_tag db_host = module.network.cloudsql_private_ip secret_provider = "gcp" project_id = var.gcp_project_id storage_bucket = module.network.gcs_bucket_name }

مخطط Helm القابل للنقل

مخطط Helm في modules/k8s-api/ يُصيَّر بشكل متطابق على كلتا المجموعتين. القطعة الوحيدة الخاصة بالسحابة هي ملف تعريف SecretProviderClass، الذي يُولَّد بناءً على متغير secret_provider المُمرَّر من Terraform. يقرأ Secrets Store CSI Driver (المثبَّت على AKS وGKE كليهما) من Key Vault أو Secret Manager ويُثبّت بيانات الاعتماد كمتغيرات بيئية — الحاويات نفسها لا تعرف أبدًا على أي سحابة تعمل.

## helm/templates/secret-provider-class.yaml {{- if eq .Values.secretProvider "azure" }} apiVersion: secrets-store.csi.x-k8s.io/v1 kind: SecretProviderClass metadata: name: api-secrets spec: provider: azure parameters: usePodIdentity: "false" useVMManagedIdentity: "true" userAssignedIdentityID: {{ .Values.azure.managedIdentityClientId }} keyvaultName: {{ .Values.azure.vaultName }} objects: | array: - | objectName: db-password objectType: secret - | objectName: api-secret-key objectType: secret tenantId: {{ .Values.azure.tenantId }} {{- else if eq .Values.secretProvider "gcp" }} apiVersion: secrets-store.csi.x-k8s.io/v1 kind: SecretProviderClass metadata: name: api-secrets spec: provider: gcp parameters: secrets: | - resourceName: "projects/{{ .Values.gcp.projectId }}/secrets/db-password/versions/latest" fileName: "db-password" - resourceName: "projects/{{ .Values.gcp.projectId }}/secrets/api-secret-key/versions/latest" fileName: "api-secret-key" {{- end }}
ثبّت إصدار CSI Driver على كلتا المجموعتين. يجب أن يتطابق إصدار مخطط Helm لـSecrets Store CSI Driver بين عمليات نشر Azure وGCP — استخدم نفس إصدار المخطط في كلا موردَي helm_release في Terraform. الاختلاف في الإصدار بين المجموعتين مصدر شائع لإخفاقات تثبيت الأسرار الدقيقة التي تظهر فقط في ظل إصدارات Kubernetes معينة ويصعب تشخيصها.

CI/CD: خط أنابيب واحد، هدفان

يبني خط أنابيب GitHub Actions الصورة مرة واحدة، يدفعها إلى سجل مشترك (Azure Container Registry أو Artifact Registry حسب السحابة الأساسية)، ثم يُشغّل terraform apply بشكل متسلسل — أولًا لـprod-azure، ثم لـprod-gcp. يتم تقسيم حركة Canary على مستوى الـingress لكل سحابة بشكل مستقل: يستخدم Application Gateway قواعد أوزان مجموعة الخلفية؛ ويستخدم GCP تعليقات أوزان NEG على GKE Ingress.

ترحيلات قواعد البيانات هي أصعب مشكلة في قابلية النقل. تشغيل alembic upgrade head أو rails db:migrate على مثيلَي PostgreSQL منفصلَين (أحدهما على Azure والآخر على GCP) يتطلب إما مهمة ترحيل مشتركة بسلاسل اتصال كلتيهما، أو خطوات ترحيل منفصلة لكل سحابة في خط الأنابيب — مع منطق لإيقاف السحابة الثانية إذا فشل الترحيل الأول. لا تطبّق الترحيلات على كلتا السحابتين في وقت واحد إن لم تكنا متزامنتين. النمط الأكثر أمانًا هو: رحّل Azure، تحقق من النجاح، رحّل GCP؛ واستخدم علامة ميزة لحجب مسارات الكود الجديدة حتى تُرحَّل كلتا قاعدتَي البيانات.

أنماط الإخفاق في الإنتاج والدروس المستفادة

تشغيل هذه المعمارية على نطاق واسع يكشف عن ثلاثة أنماط إخفاق متكررة. أولًا، عدم تماثل الشبكات: نموذج نقطة النهاية الخاصة على Azure يُمرّر DNS عبر منطقة خاصة يجب ربطها بالـVNet؛ إغفال هذا الربط يجعل الحاويات تحل FQDN قاعدة البيانات إلى IP العام، متجاوزةً المسار الخاص تمامًا وتفعيل حجب جدار الحماية. يتجنب GCP PSA هذا بحقن IP الخاص مباشرةً في VPC، لكنه يتطلب تطبيق مورد google_service_networking_connection قبل بدء أي أحمال عمل GKE. ثانيًا، انحراف تدوير الأسرار: عند تدوير كلمة مرور قاعدة البيانات في Key Vault، يُحدَّث Azure تلقائيًا إن كان موفر CSI مُعدًّا بـautoRotationPollInterval؛ يتطلب Secret Manager في GCP ترقية نسخة صريحة. التدوير غير المنسَّق يترك إحدى السحابتين ببيانات اعتماد قديمة. ثالثًا، تباين نموذج التكلفة: يُحاسب Azure على واجهات NIC لنقاط النهاية الخاصة في الساعة؛ يُحاسب GCP على استخدام بوابة Cloud NAT بالجيجابايت. قد يكلّف نفس حمل العمل مبالغ مختلفة بشكل ملحوظ على كل سحابة — تتبّع التكلفة لكل سحابة في منظومة الرصد لديك منذ اليوم الأول حتى تُكتشف الشذوذات قبل الفاتورة الشهرية.

قابلية النقل عبر السحب ليست مجانية — لها ضريبة تعقيد. نمط وحدة Terraform المشتركة، وقوالب أسرار CSI، وتشغيلات خط الأنابيب المزدوجة — كلها تضيف تكاليف عامة. العائد هو القدرة على التفاوض في العقود، والتعافي من الكوارث عبر الموفّرين، وإمكانية وضع أحمال العمل أقرب إلى العملاء في مناطق لا يغطيها موفّر واحد. قرّر بوعي ما إذا كان هذا العائد يبرر التكلفة لمؤسستك قبل تبنّي هذا النمط على نطاق واسع.