تهيئة DNS لمواقع الويب على Kubernetes: دليل شامل باستخدام EKS و NGINX
مقدمة: تحديات DNS في بيئات Kubernetes المعقدة
كمطور لمنصة Foo لمراقبة جودة المواقع الإلكترونية، خضت مؤخرًا تجربة هجرة أنظمتنا إلى بيئة Kubernetes وخدمة EKS من AWS. يوفر Kubernetes دعمًا قويًا وموثوقًا لنظام أسماء النطاقات (DNS) داخل الكتلة. لحسن الحظ، يمكننا الإشارة إلى الحاويات (Pods) باستخدام أسماء المضيفين (hostnames) كما هي محددة في المواصفات (spec) داخل الكتلة الواحدة.
لكن التحدي الحقيقي يكمن في كيفية تعريض تطبيق للعالم الخارجي كموقع ويب تحت نطاق ثابت ومستقر. قد يظن البعض أن هذا السيناريو شائع وموثق بشكل جيد، لكن الواقع كان مختلفًا تمامًا. على سبيل المثال، إذا كان لدينا خدمة (Service) باسم foo في مساحة أسماء (namespace) تُدعى bar ضمن Kubernetes، فإن أي حاوية (Pod) تعمل في نفس مساحة الأسماء bar يمكنها البحث عن هذه الخدمة ببساطة عن طريق استعلام DNS لـ foo. أما إذا كانت الحاوية تعمل في مساحة أسماء quux، فيمكنها البحث عن الخدمة باستخدام foo.bar، كما هو موضح في وثائق Kubernetes حول DNS للخدمات والحاويات.
هذا الدعم الداخلي رائع، لكنه لا يحل لغز كيفية ربط تطبيقنا بنطاق خارجي ثابت. سيتناول هذا المقال الخطوات التالية لحل هذه المعضلة:
- كيفية تعريف الخدمات (Services).
- كيفية تعريض خدمات متعددة تحت خادم NGINX واحد، دون الحاجة إلى Ingress المعقدة.
- كيفية إنشاء خدمة DNS خارجية وربطها بنطاق قمت بشرائه من أي مسجل معتمد مثل GoDaddy أو Google Domains، وسنستخدم Route 53 و ExternalDNS لإنجاز المهمة الصعبة.
يفترض هذا الدليل أن لديك إعدادًا مسبقًا لـ EKS باستخدام أداة eksctl، كما هو موثق في “البدء مع eksctl”، ولكن العديد من المفاهيم والأمثلة المطروحة هنا يمكن تطبيقها في تكوينات مختلفة.
الخطوة 1: تعريف الخدمات (Services)
توضح وثائق ربط التطبيقات بالخدمات كيفية تعريض تطبيق NGINX عن طريق تعريف Deployment و Service. دعنا نقم بإنشاء ثلاثة تطبيقات بنفس الطريقة: تطبيق ويب يواجه المستخدم، واجهة برمجة تطبيقات (API)، وخادم NGINX كوكيل عكسي لتعريض التطبيقين تحت مضيف واحد.
نشر تطبيق الويب (Web Application Deployment)
ملف web-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
# ...إلخ، تفاصيل الحاوية
ملف web-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: web
labels:
app: web
spec:
ports:
- name: "3000"
port: 3000
targetPort: 3000
selector:
app: web
نشر واجهة برمجة التطبيقات (API Deployment)
ملف api-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
# ...إلخ، تفاصيل الحاوية
ملف api-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: api
labels:
app: api
spec:
ports:
- name: "3000"
port: 3000
targetPort: 3000
selector:
app: api
هذه هي الخطوات الأساسية، دعنا ننتقل إلى الجزء التالي!
الخطوة 2: تعريض خدمات متعددة تحت خادم NGINX واحد
يعمل NGINX كوكيل عكسي (reverse proxy)، حيث يقوم بتوجيه الطلبات إلى مصدر محدد، ثم يجلب الاستجابة ويعيدها إلى العميل. بالعودة إلى فكرة أن أسماء الخدمات يمكن الوصول إليها من قبل الحاويات الأخرى داخل الكتلة، يمكننا إعداد تهيئة NGINX لتبدو كالتالي:
تهيئة NGINX للوكيل العكسي
ملف sites-enabled/www.example.com.conf:
upstream api {
server api:3000;
}
upstream web {
server web:3000;
}
server {
listen 80;
server_name www.example.com;
location / {
proxy_pass http://web;
}
location /api {
proxy_pass http://api;
}
}
لاحظ كيف يمكننا الإشارة إلى المضيفين الأصليين مثل web:3000 و api:3000. هذه ميزة قوية!
نشر NGINX كخدمة
ملف nginx-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
ملف nginx-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
# هذا الجزء سيتضح لاحقًا
external-dns.alpha.kubernetes.io/hostname: www.example.com
labels:
app: nginx
spec:
type: LoadBalancer
ports:
- name: "80"
port: 80
targetPort: 80
selector:
app: nginx
هل انتهينا الآن؟ في تجربتي الأولية، اعتقدت ذلك. يوفر LoadBalancer عنوان IP يمكن الوصول إليه خارجيًا. يمكنك التأكد من ذلك بتشغيل الأمر kubectl get svc، وستجد اسم مضيف مدرجًا في عمود EXTERNAL-IP. بافتراض أنك حصلت على نطاق من مزود يوفر واجهة لإدارة إعدادات DNS، يمكنك ببساطة إضافة عنوان URL هذا كسجل CNAME، وهكذا تكون جاهزًا، أليس كذلك؟
حسنًا، نوعًا ما… ولكن ليس تمامًا. تعتبر حاويات Kubernetes كيانات مؤقتة نسبيًا (ephemeral) وليست دائمة. يمكنك معرفة المزيد عن هذا في “دورة حياة الحاوية (Pod Lifecycle) – Kubernetes”. وهذا يعني أنه في أي وقت يتم فيه إجراء تغيير كبير في دورة حياة خدمة ما، مثل تطبيق NGINX في حالتنا، سنحصل على عنوان IP مختلف، مما سيؤدي بدوره إلى توقف كبير في تطبيقنا. وهذا يتعارض مع أحد الأهداف الرئيسية لـ Kubernetes: المساعدة في إنشاء تطبيق “عالي التوفر” (highly available). لا داعي للذعر، سنتجاوز هذا التحدي!
الخطوة 3: إنشاء خدمة DNS خارجية لتوجيه NGINX ديناميكيًا
في الخطوة السابقة، من خلال مواصفات LoadBalancer الخاصة بنا المقترنة بـ EKS، قمنا بالفعل بإنشاء Elastic Load Balancer (سواء كان ذلك للأفضل أو للأسوأ). في هذا القسم، سنقوم بإنشاء خدمة DNS توجه موازن التحميل الخاص بنا عبر “سجل ALIAS”. هذا السجل ALIAS ديناميكي بطبيعته، حيث يتم إنشاء سجل جديد كلما تغيرت خدمتنا. يتم تحقيق الاستقرار في سجلات خادم الأسماء (name server records).
باختصار، الجزء المتبقي هو ببساطة اتباع وثائق استخدام ExternalDNS مع Route 53. يُعرف Route 53 بأنه “خدمة ويب لنظام أسماء النطاقات (DNS) السحابية”. فيما يلي بعض الأمور التي كان علي القيام بها ولم تكن واضحة من الوثائق:
- نفذ الأمر
eksctl utils associate-iam-oidc-provider --cluster=your-cluster-nameوفقًا لوثائق حسابات خدمة eksctl. - عند إنشاء وثيقة سياسة IAM وفقًا لوثائق ExternalDNS، اضطررت فعليًا للقيام بذلك عبر واجهة سطر الأوامر (CLI) بدلاً من الواجهة عبر الإنترنت في حسابي. كنت أتلقى هذا الخطأ باستمرار:
WebIdentityErr: failed to retrieve credentials. عندما أنشأت السياسة عبر CLI، اختفت المشكلة. أدناه هو الأمر الكامل الذي يجب أن تتمكن من نسخه وتنفيذه حرفيًا إذا كان لديك AWS CLI مثبتًا:
caused by: AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity
status code: 403
aws iam create-policy \
--policy-name AllowExternalDNSUpdates \
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["route53:ChangeResourceRecordSets"],"Resource":["arn:aws:route53:::hostedzone/*"]},{"Effect":"Allow","Action":["route53:ListHostedZones","route53:ListResourceRecordSets"],"Resource":["*"]}]}'
- استخدم مخرجات ARN للسياسة أعلاه لإنشاء دور IAM مرتبط بحساب خدمة ExternalDNS باستخدام أمر سيبدو كالتالي:
eksctl create iamserviceaccount --cluster=your-cluster-name --name=external-dns --namespace=default --attach-policy-arn=arn:aws:iam::123456789:policy/AllowExternalDNSUpdates. - يجب أن يكون لدينا الآن دور جديد من الأمر أعلاه يمكننا رؤيته في وحدة تحكم IAM، وسيكون له اسم مثل
eksctl-foo-addon-iamserviceaccount-Role1-abcdefg. انقر على الدور من القائمة وفي الجزء العلوي من الشاشة التالية، لاحظ “Role ARN” كشيء مثلarn:aws:iam::123456789:role/eksctl-foo-addon-iamserviceaccount-Role1-abcdefg. - اتبع هذه الخطوات لإنشاء “منطقة مستضافة” (hosted zone) في Route 53. يمكنك تأكيد الإعدادات في وحدة تحكم Route 53.
- إذا كان مزود النطاق الخاص بك يسمح لك بإدارة إعدادات DNS، أضف سجلات خادم الأسماء الأربعة من مخرجات الأمر الذي قمت بتشغيله لإنشاء “منطقة مستضافة”.
- انشر ExternalDNS باتباع التعليمات. بعد ذلك، يمكنك تتبع السجلات باستخدام
kubectl logs -f name-of-external-dns-pod. يجب أن ترى سطرًا مثل هذا في النهاية:time="2020-05-05T02:57:31Z" level=info msg="All records are already up to date".
سهل، أليس كذلك؟! ربما لا… لكن على الأقل لم تضطر إلى اكتشاف كل ذلك بمفردك! قد تكون هناك بعض الفجوات أعلاه، ولكن نأمل أن يساعدك هذا في توجيهك خلال عمليتك.
الخلاصة التقنية
يُعد إعداد نظام DNS ديناميكيًا لتطبيقات الويب المستضافة على Kubernetes، خاصةً في بيئات مثل EKS، خطوة حاسمة نحو تحقيق التوفرية العالية والاستقرار. على الرغم من أن Kubernetes يوفر دعمًا داخليًا ممتازًا لـ DNS، إلا أن تحدي ربط الخدمات الداخلية بنطاقات خارجية ثابتة يتطلب حلولًا متقدمة مثل ExternalDNS و AWS Route 53. من خلال التغلب على تعقيدات IAM وإدارة سجلات DNS، يمكن للمطورين ضمان أن تطبيقاتهم تظل متاحة باستمرار، حتى مع الطبيعة المؤقتة للحاويات وعناوين IP المتغيرة لموازنات التحميل. هذا النهج لا يعزز فقط موثوقية التطبيق ولكنه يقلل أيضًا من التدخل اليدوي، مما يجعله مكونًا أساسيًا لأي بنية تحتية حديثة تعتمد على Kubernetes.