Foundationপ্রথম নীতি থেকে
LEVEL 12লেসন ১৪/২২মাঝারি৪০ মিনিট

Service Discovery — Kubernetes-এ DNS কীভাবে নাম থেকে Pod-এ পৌঁছায়

Service Discovery: DNS Inside Kubernetes

একটা Pod থেকে অন্য একটা Service-এ পৌঁছানোর প্রথম ধাপ কোনো routing rule না — একটা DNS query। CoreDNS, নিজেই ক্লাস্টারের ভেতরে চলা কিছু সাধারণ Pod, API server watch করে লাইভ রেকর্ড বানায়; সেই রেকর্ড থেকে পাওয়া ClusterIP-টাই তারপর kube-proxy-র হাতে তুলে দেওয়া হয় আসল প্যাকেট রাউট করার জন্য।

এই লেসন শেষে আপনি পারবেন

  • কেন Kubernetes-এ Pod IP হার্ডকোড করা যায় না তা ব্যাখ্যা করে বলতে পারবেন কেন একটা নাম-ভিত্তিক ঠিকানা ব্যবস্থা অপরিহার্য
  • CoreDNS নিজেই ক্লাস্টারের ভেতরে চলা কয়েকটা সাধারণ Pod — এই সত্যিটা ব্যাখ্যা করে বলতে পারবেন এটা কীভাবে reconciliation-loop প্যাটার্নের আরেকটা উদাহরণ
  • `<service-name>.<namespace>.svc.cluster.local` নামকরণ ব্যবস্থা আর search-domain-নির্ভর সংক্ষিপ্ত রূপগুলো নির্ভুলভাবে ব্যাখ্যা করতে পারবেন
  • কেন cluster DNS record স্ট্যাটিক zone file না, বরং etcd-র বর্তমান অবস্থা থেকে live তৈরি হয় তা যুক্তি দিয়ে দেখাতে পারবেন
  • DNS resolution আর kube-proxy-র ClusterIP routing — এই দুইটা আলাদা, composed স্তর কীভাবে একসাথে কাজ করে সংযোগ সম্পূর্ণ করে তা ট্রেস করে দেখাতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

virtualization-cloud/pods-controllers-services-ingress লেসন একটা কড়া সিদ্ধান্তে থেমেছিল: Pod-রা মরে, নতুন IP নিয়ে জন্মায়, Service তাদের ওপর একটা স্থিতিশীল ClusterIP বসিয়ে দেয় যাতে caller-দের আর Pod IP নিয়ে মাথা ঘামাতে না হয়। কিন্তু সেই ClusterIP-টা নিজেও তো একটা সংখ্যা — 10.96.10.11-জাতীয় কিছু। প্রশ্নটা তাই এক ধাপ পিছিয়ে গেল, মিটল না: কেউ কি সেই ClusterIP-টা YAML-এ, কনফিগ ফাইলে, environment variable-এ হার্ডকোড করবে?

এক কথায় — না। একটা মাইক্রোসার্ভিস আর্কিটেকচারে দশ-বিশটা Service একে অপরকে কল করে; প্রতিটা redeploy-তে (বা Service মুছে নতুন করে বানালে) ClusterIP বদলে যেতে পারে। IP হার্ডকোড করা মানে প্রতিটা caller-কে প্রতিটা পরিবর্তনে জানানো — ঠিক সেই সমস্যা যা networking/dns লেসন ইতিমধ্যে সমাধান করে দেখিয়েছে গোটা ইন্টারনেটের জন্য: নাম মনে রাখো, ঠিকানা না — ঠিকানা বদলালেও নামটা একই থাকুক।

Kubernetes এই একই সমাধান পুনরাবিষ্কার করেনি — সরাসরি DNS-কেই ভেতরে বসিয়ে দিয়েছে। প্রতিটা ক্লাস্টারে একটা ভেতরের, নিজস্ব DNS সার্ভার চলে যার কাজ একটাই: Service নামকে ClusterIP-তে বদলানো। এই লেসনে আমরা দেখব সেই সার্ভারটা — CoreDNS — আসলে কী, নামকরণের নিয়মটা ঠিক কেমন, আর সবচেয়ে গুরুত্বপূর্ণভাবে — DNS resolution শেষ হওয়ার পরে ঠিক কোথায় থেমে যায়, আর prerequisite লেসনের kube-proxy মেশিনারি কোথা থেকে শুরু করে।

মূল ধারণা

CoreDNS — ক্লাস্টারের নিজস্ব phonebook, নিজেই ক্লাস্টারের ভেতরে বসবাস করে

networking/dns লেসন যে stub resolver / recursive resolver ভোকাবুলারি শিখিয়েছে, সেটাই এখানে সরাসরি প্রযোজ্য — শুধু গাছের চূড়াটা বদলে গেছে। প্রতিটা Kubernetes ক্লাস্টারে একটা ডিফল্ট cluster DNS সার্ভার থাকে, নাম CoreDNS — একটা CNCF প্রজেক্ট যা kube-dns-কে (পুরনো ডিফল্ট) প্রতিস্থাপন করেছে। CoreDNS-এর ব্যাপারে সবচেয়ে চমকপ্রদ ব্যাপারটা তার স্থাপত্যে না, তার অবস্থানে:

CoreDNS নিজেই একগুচ্ছ সাধারণ Pod, যারা সেই একই ক্লাস্টারের ভেতরে চলে যার জন্য তারা DNS সার্ভিস দেয়।

এখানে কোনো জাদু নেই — CoreDNS একটা Deployment হিসেবে kube-system namespace-এ চলে (সাধারণত ২টা Replica, high-availability-র জন্য), তার সামনে একটা সাধারণ Service (নাম প্রায়ই kube-dns, ঐতিহাসিক কারণে) থাকে, আর সেই Service-এরও একটা ClusterIP আছে — ঠিক অন্য যেকোনো Service-এর মতো। প্রতিটা Pod-এর ভেতরের /etc/resolv.conf সেই ClusterIP-কে nameserver হিসেবে পায় (kubelet নিজেই এটা Pod তৈরির সময় বসিয়ে দেয়)। অর্থাৎ DNS সার্ভিস নিজেও Service abstraction-এর ওপর দাঁড়িয়ে আছে — যে সমস্যাটা এই লেসনের বিষয়, CoreDNS নিজেই সেই সমাধানের একজন ব্যবহারকারী।

কীভাবে রেকর্ড পায় — watch, static zone file না

একটা প্রথাগত authoritative DNS সার্ভার (যেমন networking/dns লেসনের example.com-এর সার্ভার) তার রেকর্ড পড়ে একটা zone file থেকে — ডিস্কে বসানো, admin হাতে এডিট করা টেক্সট। CoreDNS সম্পূর্ণ ভিন্ন উৎস থেকে রেকর্ড পায়: তার kubernetes plugin সরাসরি Kubernetes API server-এর সাথে একটা watch সংযোগ ধরে রাখে — Service ও Pod object-এর যেকোনো change (create/update/delete) সাথে সাথে event হিসেবে পৌঁছায়। প্রতিটা event-এ CoreDNS তার in-memory রেকর্ড সেট নতুন করে গণনা করে, বর্তমান cluster state থেকে।

এই প্যাটার্নটা চেনা লাগার কথা — pods-controllers-services-ingress লেসনের controller/reconciliation-loop প্যাটার্নেরই আরেকটা উদাহরণ, শুধু এবার controller-টা DNS রেকর্ড লেখে, Pod তৈরি করে না।

ভেতরে কী ঘটছে

নামকরণের নিয়ম — precise, predictable, hierarchy-নির্ভর

Kubernetes cluster DNS-এর namespace cluster.local-এ শুরু হয় (এটা প্রায়ই ডিফল্ট, ভিন্ন করা যায় কিন্তু কম দেখা যায়)। একটা Service-এর জন্য পূর্ণাঙ্গ, নির্ভুল নাম:

<service-name>.<namespace>.svc.cluster.local

networking/dns লেসনের FQDN আর zone/delegation-এর ভাষায় বললে — cluster.local হলো ক্লাস্টারের নিজস্ব root-এর মতো একটা internal zone, svc একটা fixed sub-zone (Service-দের জন্য সংরক্ষিত), <namespace> তার নিচে, আর <service-name> সবচেয়ে নিচের পাতা। CoreDNS-ই এই পুরো zone-এর জন্য authoritative — কোনো root/TLD/delegation chain হাঁটার দরকার নেই, কারণ পুরো namespace-টাই একটামাত্র সার্ভারের হাতে।

একটা concrete নাম, ধাপে ধাপে

ধরা যাক billing namespace-এ একটা Service আছে, নাম ledger। তার পূর্ণ নাম:

ledger.billing.svc.cluster.local

default namespace-এর একটা Pod থেকে এই নামে একটা DNS query পাঠালে যা ঘটে:

  1. Pod-এর ভেতরের অ্যাপ্লিকেশন getaddrinfo("ledger.billing.svc.cluster.local") কল করে — ঠিক networking/dns লেসনের stub resolver ধাপ।
  2. Query যায় /etc/resolv.conf-এ বসানো nameserver-এ — যেটা CoreDNS Service-এর ClusterIP।
  3. CoreDNS তার in-memory রেকর্ড সেটে (আগের section-এ বর্ণিত watch-নির্মিত রেকর্ড) ledger.billing.svc.cluster.local-এর জন্য একটা A রেকর্ড খুঁজে পায় — মান: ledger Service-এর ClusterIP।
  4. উত্তর ফেরত আসে; Pod-এর ভেতরের অ্যাপ্লিকেশন এখন সেই ClusterIP-তে connect করে।

সংক্ষিপ্ত রূপ — search domain-এর কারসাজি

প্রতিদিনের কোডে কেউ পূর্ণাঙ্গ চার-অংশের নাম লেখে না — সাধারণত লেখা হয় শুধু ledger.billing, নিজের namespace থেকে কল করলে তো শুধু ledger-ও চলে। এটা জাদু না, এটা DNS resolver-এর search domain ফিচার (networking/dns লেসনের stub resolver আচরণের অংশ, যা সেই লেসন বিস্তারিত করেনি)। kubelet প্রতিটা Pod-এর /etc/resolv.conf-এ একটা search লাইন বসিয়ে দেয়:

search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5

Resolver যখন একটা অসম্পূর্ণ নাম পায় (ডটের সংখ্যা ndots থ্রেশহোল্ডের কম), সে একে একে প্রতিটা search suffix জুড়ে চেষ্টা করে যতক্ষণ না উত্তর পায়। তাই ledger লিখলে resolver নিজে থেকে ledger.default.svc.cluster.local বানিয়ে query পাঠায় (যদি Pod নিজেই default namespace-এ থাকে) — এটাই কেন একই-namespace-এর ভেতর শুধু short name যথেষ্ট, কিন্তু ভিন্ন namespace-এ থাকা Service-এর জন্য অন্তত <name>.<namespace> লিখতে হয়।

Pod DNS — কম ব্যবহৃত, কিন্তু StatefulSet-এর জন্য জরুরি

Service-এর পাশাপাশি Kubernetes প্রতিটা Pod-কেও একটা DNS নাম দিতে পারে, যদিও এটা দৈনন্দিন ব্যবহারে অনেক কম দেখা যায়। একটা Pod-এর নাম তৈরি হয় তার IP থেকে, ড্যাশ দিয়ে বসানো:

<pod-ip-with-dashes>.<namespace>.pod.cluster.local

সাধারণ, stateless Deployment-এর Pod-দের জন্য এটা প্রায় অপ্রয়োজনীয় — যেহেতু Pod-রা এমনিতেই replaceable আর caller সবসময় Service-এর মাধ্যমেই পৌঁছায়। কিন্তু StatefulSet-এর ক্ষেত্রে এটা গুরুত্বপূর্ণ হয়ে ওঠে: একটা database cluster-এর (যেমন Kafka broker বা etcd node) প্রতিটা instance-এর একটা স্থিতিশীল, পূর্বনির্ধারিত পরিচয় দরকার — pod-0, pod-1, pod-2 — যাতে বাকিরা ঠিক কোন instance-এর সাথে কথা বলছে তা জানতে পারে, শুধু “যেকোনো একটা replica” না। StatefulSet একটা headless Service (ClusterIP ছাড়া Service) ব্যবহার করে এই স্থিতিশীল per-Pod DNS নাম বানায় — এই প্যাটার্নের বিস্তারিত একটা পরের লেসনের বিষয়, আজ শুধু এটুকু মনে রাখা যথেষ্ট যে Pod DNS-এর মূল কারণ পরিচয়, ClusterIP-র মতো load-balancing না।

DNS আর kube-proxy — দুইটা আলাদা, composed স্তর

এই লেসনের সবচেয়ে গুরুত্বপূর্ণ বিভাজনটা এখানে স্পষ্ট করা দরকার, কারণ এটা প্রায়ই একটা একক মেকানিজম বলে ভুল বোঝা হয়:

DNS resolution আপনাকে একটা ঠিকানা দেয়। Kube-proxy আপনাকে সেই ঠিকানায় সংযুক্ত করে। এই দুইটা সম্পূর্ণ ভিন্ন, ক্রমিক স্তর — একটা মেকানিজম না, দুইটা।

ledger.billing.svc.cluster.local resolve করার পুরো কাজ CoreDNS-এর — উত্তরে একটা ClusterIP আসে, ব্যস, DNS-এর কাজ এখানেই শেষ। সেই ClusterIP-তে TCP connect() করার মুহূর্তে DNS আর কোনো ভূমিকা রাখে না — এখন থেকে দায়িত্ব pods-controllers-services-ingress লেসনের বর্ণিত kube-proxy মেশিনারির: প্রতিটা node-এ বসানো iptables/IPVS রুল, যেগুলো ClusterIP-কে destination দেখে প্যাকেটকে একটা জীবিত, মিলে-যাওয়া Pod-এর আসল IP-তে rewrite করে দেয়। ClusterIP নিজে কোনো real network interface-এ bind হয়ে থাকা কিছু না — এটা শুধু একটা virtual ঠিকানা যা kube-proxy-র রুলসেট চিনে রাউট করে।

একটা নাম থেকে একটা জীবিত Pod পর্যন্ত — দুইটা আলাদা স্তর
  1. অ্যাপ্লিকেশন কোডconnect("ledger.billing.svc.cluster.local", 5432)
  2. stub resolver → CoreDNSsearch-domain suffixing, A রেকর্ড lookup — networking/dns লেসনের resolution vocabulary
  3. CoreDNS-এর উত্তর: ClusterIPযেমন 10.96.20.5 — এখানে DNS-এর কাজ সম্পূর্ণ শেষ
  4. kernel TCP connect(10.96.20.5:5432)প্যাকেট node-এর network stack-এ ঢোকে
  5. kube-proxy iptables/IPVS রুলClusterIP destination match করে, একটা জীবিত Pod IP-তে DNAT rewrite করে (pods-controllers-services-ingress লেসনের বিষয়)
  6. প্যাকেট আসল Pod-এ পৌঁছায়সংযোগ সম্পূর্ণ

এই বিভাজনটা বোঝার সবচেয়ে বড় সুবিধা: dnsutils-জাতীয় টুল দিয়ে ClusterIP resolve হয় কিনা টেস্ট করা যায় network connectivity সম্পূর্ণ আলাদা রেখে — একটা DNS সমস্যা (nslookup ব্যর্থ) আর একটা routing সমস্যা (nslookup সফল কিন্তু curl timeout) সম্পূর্ণ ভিন্ন জায়গায় debug করতে হয়, ভিন্ন টুল দিয়ে।

উদাহরণ

nslookup থেকে ClusterIP পর্যন্ত — একটা বাস্তব ট্রেস

একটা default namespace-এর Pod থেকে billing namespace-এর ledger Service resolve করার একটা টিপিক্যাল সেশন (প্রায়ই debugging-এর জন্য ব্যবহৃত busybox/dnsutils image দিয়ে):

kubectl run -it --rm debug --image=busybox:1.36 -- sh
/ # cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5

/ # nslookup ledger.billing
Server:    10.96.0.10
Address:   10.96.0.10:53

Name:      ledger.billing.svc.cluster.local
Address:   10.96.20.5

তিনটা জিনিস লক্ষ্য করার মতো, networking/dns লেসনের dig output-এর সাথে সরাসরি তুলনা করে। এক — Server: 10.96.0.10 মানে query resolver হিসেবে ক্লাস্টারের নিজস্ব CoreDNS Service-এর ClusterIP-তে গেছে, কোনো external 1.1.1.1/8.8.8.8 না। দুই — লেখা হয়েছিল শুধু ledger.billing, কিন্তু উত্তরে পূর্ণাঙ্গ ledger.billing.svc.cluster.local — search-domain suffixing চোখের সামনে ঘটেছে। তিন — এই resolution-এর dig-স্টাইলে কোনো referral chain নেই (কোনো root/TLD হপ না) কারণ CoreDNS নিজেই cluster.local-এর জন্য authoritative — পুরো উত্তর এক hop-এ।

ছবিতে

পুরো ছবিটা এক ফ্রেমে

┌─────────────────────────────┐
│  Pod (default namespace)    │
│  app: connect(              │
│   "ledger.billing.svc...")  │
└──────────────┬───────────────┘
               │ ১. DNS query (/etc/resolv.conf → nameserver)
               ▼
┌───────────────────────────────────────────┐
│  CoreDNS Pod-রা (kube-system namespace)     │
│  - kubernetes plugin: API server watch করে  │
│  - Service/Pod object → in-memory রেকর্ড    │
│  - উত্তর: ledger.billing... = 10.96.20.5    │  ← DNS-এর কাজ এখানেই শেষ
└──────────────┬───────────────────────────────┘
               │ ২. ClusterIP নিয়ে connect()
               ▼
┌───────────────────────────────────────────┐
│  kube-proxy রুলসেট (প্রতিটা node-এ)          │
│  iptables/IPVS: dst=10.96.20.5 ধরা,          │
│  একটা জীবিত Pod IP-তে DNAT rewrite            │  ← routing শুরু
└──────────────┬───────────────────────────────┘
               │ ৩. rewritten প্যাকেট
               ▼
        আসল ledger Pod (10.244.x.y)
সংযোগ চিত্রএকটা Pod-এর দৃষ্টিকোণ থেকে নাম-থেকে-সংযোগ পর্যন্ত — DNS আর data-plane routing দুইটা আলাদা বাক্সে।

নিজে চালিয়ে দেখুন

EXPERIMENT

CoreDNS-এর মতো Kubernetes cluster DNS resolve করে দেখা

যেকোনো চলমান Kubernetes cluster (minikube/kind/managed) — kubectl দরকার· ১৫ মিনিট

ধাপ ১ — CoreDNS নিজেই কয়েকটা Pod, তা যাচাই করুন:

kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl get svc -n kube-system kube-dns

প্রত্যাশিত (সংক্ষেপে):

NAME                       READY   STATUS    RESTARTS   AGE
coredns-5d78c9869d-4x2pl   1/1     Running   0          2d
coredns-5d78c9869d-9k7wq   1/1     Running   0          2d

NAME       TYPE        CLUSTER-IP    PORT(S)
kube-dns   ClusterIP   10.96.0.10    53/UDP,53/TCP,9153/TCP

CoreDNS নিজেই একটা ClusterIP-ওয়ালা Service, ২টা Running Pod — এই লেসনের কেন্দ্রীয় দাবির সরাসরি প্রমাণ।

ধাপ ২ — একটা Service বানিয়ে সংক্ষিপ্ত নামে resolve করুন:

kubectl create namespace billing
kubectl create service clusterip ledger --tcp=5432:5432 -n billing
kubectl run -it --rm dbg --image=busybox:1.36 -n default -- \
  nslookup ledger.billing

ধাপ ৩ — মুছে নতুন করে বানিয়ে live update দেখুন:

kubectl delete service ledger -n billing
kubectl run -it --rm dbg2 --image=busybox:1.36 -n default -- \
  nslookup ledger.billing   # এখন NXDOMAIN

kubectl create service clusterip ledger --tcp=5432:5432 -n billing
kubectl run -it --rm dbg3 --image=busybox:1.36 -n default -- \
  nslookup ledger.billing   # নতুন ClusterIP সাথে সাথে resolve হয়

তৃতীয় ধাপটাই সবচেয়ে গুরুত্বপূর্ণ পর্যবেক্ষণ — কোনো reload, কোনো propagation delay নেই। Service মোছার সাথে সাথে সেই নাম NXDOMAIN হয়ে যায়, নতুন Service বানানোর সাথে সাথে নতুন ClusterIP resolve হতে শুরু করে — কারণ CoreDNS static file পড়ে না, প্রতিটা watch event-এ live reconcile করে (build অংশে এই একই আচরণ কোড দিয়ে সিমুলেট করা হয়েছে)।

এটা কী প্রমাণ করে

Service DNS নাম সত্যিই search-domain suffixing দিয়ে সংক্ষিপ্ত হতে পারে, CoreDNS সত্যিই কয়েকটা সাধারণ Pod হিসেবে চলে, আর একটা Service মুছে নতুন Service বানালে DNS উত্তর সাথে সাথে বদলে যায় — কোনো caching delay ছাড়া যা zone-file-ভিত্তিক DNS-এ প্রত্যাশিত হতো।

নিজে বানান

BUILD IT

CoreDNSSimulator — reconciliation থেকে DNS রেকর্ড, লাইভ

Python · ●●○○○
  1. একটা ClusterState ক্লাস বানান যা (namespace, name) -> ClusterIP ম্যাপ ধরে রাখে — API server/etcd-র সরলীকৃত মডেল
  2. একটা reconcile() মেথড লিখুন যা বর্তমান state থেকে name.namespace.svc.cluster.local -> IP রেকর্ড regenerate করে
  3. একটা Service create করে initial রেকর্ড সেট দেখান
  4. একটা Service delete করে দেখান রেকর্ড সাথে সাথে হারিয়ে যায়
  5. একই short name-এ ভিন্ন namespace-এ নতুন Service create করে দেখান রেকর্ড সেট আবার update হয়ে ভিন্ন IP দেয়

লক্ষ্য: CoreDNS-এর kubernetes plugin আসলে কী করে তা একটা ছোট, সৎ সিমুলেশনে নামানো — কোনো library ছাড়া, শুধু একটা dict আর একটা reconcile ফাংশন। এটাই এই লেসনের কেন্দ্রীয় দাবি — “রেকর্ড static zone file থেকে না, live cluster state থেকে আসে” — কোডে সরাসরি দেখানো।

# coredns_sim.py -- a tiny simulation of CoreDNS's core mechanism: watch
# cluster state (here, a plain dict standing in for the API server /
# etcd) and regenerate DNS records from CURRENT state every time
# something changes -- the same reconciliation-loop pattern used by
# every other Kubernetes controller.

class ClusterState:
    """Stand-in for etcd + the API server's Service objects."""
    def __init__(self):
        self.services = {}  # (namespace, name) -> clusterIP

    def create_service(self, namespace, name, cluster_ip):
        self.services[(namespace, name)] = cluster_ip

    def delete_service(self, namespace, name):
        del self.services[(namespace, name)]


class CoreDNSSimulator:
    """Mimics CoreDNS's kubernetes plugin: on every watch event,
    regenerate the DNS record set from the CURRENT state -- never a
    static zone file on disk."""
    def __init__(self, cluster):
        self.cluster = cluster
        self.records = {}
        self.reconcile()

    def reconcile(self):
        """The one thing CoreDNS's kubernetes plugin actually does on
        every watch event: rebuild name.namespace.svc.cluster.local ->
        ClusterIP from whatever the API server currently reports."""
        self.records = {
            f"{name}.{namespace}.svc.cluster.local": ip
            for (namespace, name), ip in self.cluster.services.items()
        }

    def resolve(self, query):
        return self.records.get(query, "NXDOMAIN")

    def dump(self, title):
        print(f"--- {title} ---")
        for fqdn in sorted(self.records):
            print(f"{fqdn:42s} -> {self.records[fqdn]}")
        print()


cluster = ClusterState()
cluster.create_service("default", "web", "10.96.10.11")
cluster.create_service("default", "api", "10.96.10.12")
cluster.create_service("billing", "ledger", "10.96.20.5")

dns = CoreDNSSimulator(cluster)
dns.dump("watch event: 3 Services exist -- initial record set")

print("resolve('api.default.svc.cluster.local') ->", dns.resolve("api.default.svc.cluster.local"))
print()

# --- event: Service billing/ledger deleted ---
cluster.delete_service("billing", "ledger")
dns.reconcile()
dns.dump("watch event: DELETE billing/ledger -- reconciled")

# --- event: a NEW Service named 'ledger' created in namespace 'default' ---
cluster.create_service("default", "ledger", "10.96.10.99")
dns.reconcile()
dns.dump("watch event: CREATE default/ledger (same short name, different namespace)")

print("resolve('ledger.billing.svc.cluster.local') ->", dns.resolve("ledger.billing.svc.cluster.local"))
print("resolve('ledger.default.svc.cluster.local')  ->", dns.resolve("ledger.default.svc.cluster.local"))

প্রকৃত চালিয়ে পাওয়া output (deterministic — হুবহু এটাই আসবে):

--- watch event: 3 Services exist -- initial record set ---
api.default.svc.cluster.local              -> 10.96.10.12
ledger.billing.svc.cluster.local           -> 10.96.20.5
web.default.svc.cluster.local              -> 10.96.10.11

resolve('api.default.svc.cluster.local') -> 10.96.10.12

--- watch event: DELETE billing/ledger -- reconciled ---
api.default.svc.cluster.local              -> 10.96.10.12
web.default.svc.cluster.local              -> 10.96.10.11

--- watch event: CREATE default/ledger (same short name, different namespace) ---
api.default.svc.cluster.local              -> 10.96.10.12
ledger.default.svc.cluster.local           -> 10.96.10.99
web.default.svc.cluster.local              -> 10.96.10.11

resolve('ledger.billing.svc.cluster.local') -> NXDOMAIN
resolve('ledger.default.svc.cluster.local')  -> 10.96.10.99

শেষ দুইটা লাইন এই লেসনের পুরো যুক্তি এক জায়গায় ধরে ফেলে। ledger.billing.svc.cluster.local এখন NXDOMAIN — পুরনো রেকর্ড কোনো TTL countdown-এর অপেক্ষা না করেই সম্পূর্ণ হারিয়ে গেছে, কারণ সেটা কখনো “cache” হয়নি, reconcile()-এর প্রতিটা কলে পুরো সেটটাই নতুন করে গণনা হয়। আর ledger.default.svc.cluster.local একটা সম্পূর্ণ নতুন IP দেয় — একই short name (ledger), সম্পূর্ণ ভিন্ন namespace, সম্পূর্ণ ভিন্ন ClusterIP। এটাই প্রমাণ করে কেন <namespace> অংশটা নামকরণে ঐচ্ছিক না — এটাই দুইটা সম্পূর্ণ ভিন্ন Service-কে একই short name থাকা সত্ত্বেও আলাদা রাখার একমাত্র উপায়।

নিজে বাড়ান:

  1. reconcile()-এ একটা history list যোগ করুন যা প্রতিটা event-এর টাইমস্ট্যাম্প ও diff (কোন রেকর্ড যোগ/বাদ হলো) রেকর্ড করে — CoreDNS-এর নিজস্ব kubernetes plugin-এর মেট্রিক্সের একটা সরলীকৃত সংস্করণ
  2. Pod DNS সিমুলেট করুন — ClusterState-এ একটা pods dict যোগ করে <ip-with-dashes>.<namespace>.pod.cluster.local রেকর্ড বানান
  3. একটা search() ফাংশন লিখুন যা ledger-এর মতো short name নিয়ে search default.svc.cluster.local svc.cluster.local cluster.local তালিকা অনুযায়ী প্রতিটা suffix চেষ্টা করে প্রথম hit ফেরত দেয় — /etc/resolv.conf-এর search লাইনের আচরণ হাতে বাস্তবায়ন

বাস্তব সিস্টেমে

বাস্তব জগতে — কেন এই বিভাজনটা debugging-এ সবচেয়ে বেশি কাজে লাগে

“আমার Service-এ পৌঁছাতে পারছি না” — প্রথম প্রশ্নটাই হওয়া উচিত “কোন স্তর ব্যর্থ হচ্ছে?” DNS resolution আর data-plane routing আলাদা রাখার সবচেয়ে বড় বাস্তব সুবিধা troubleshooting-এ। nslookup ledger.billing ব্যর্থ হলে সমস্যাটা CoreDNS-এ বা নামকরণে (namespace ভুল লেখা, Service নেই, বা এমনকি CoreDNS Pod-ই crash করেছে)। nslookup সফল কিন্তু curl/telnet টাইমআউট করলে সমস্যাটা সম্পূর্ণ ভিন্ন জায়গায় — kube-proxy রুল ভুল, NetworkPolicy ব্লক করছে, বা target Pod-ই crash-looping। দুইটা উপসর্গ একই রকম দেখায় (“connect করতে পারছি না”) কিন্তু ভিন্ন টুল, ভিন্ন টিম, ভিন্ন fix লাগে — আর এই লেসনের দুই-স্তরের মডেলটাই সেই পার্থক্য প্রথম ধাপেই স্পষ্ট করে দেয়।

CoreDNS নিজেই একটা single point of failure হতে পারে — এবং হয়েছে, বাস্তবে। যেহেতু প্রতিটা Service-নাম resolution CoreDNS-এর মধ্য দিয়ে যায়, CoreDNS Pod-রা crash-loop বা resource-starved হলে গোটা ক্লাস্টারের সব inter-service যোগাযোগ ভেঙে পড়ে — এমনকি সব Pod আর Service নিজে সম্পূর্ণ সুস্থ থাকলেও। এই কারণেই CoreDNS ডিফল্টভাবে একাধিক Replica নিয়ে চলে, আর production ক্লাস্টারে CoreDNS-এর নিজস্ব CPU/memory limit ও autoscaling (cluster-proportional-autoscaler-জাতীয় টুল দিয়ে) আলাদাভাবে মনিটর করা হয় — এটা “শুধু আরেকটা Service” না, এটা গোটা ক্লাস্টারের নামকরণ ব্যবস্থার মেরুদণ্ড।

ndots:5-এর লুকানো খরচ। ডিফল্ট ndots:5 মানে ৫টার কম ডট থাকা যেকোনো নাম (যা প্রায় সব internal Service নাম) প্রথমে সবগুলো search-suffix দিয়ে চেষ্টা হয়, তারপর বাইরের internet-এর নামের মতো resolve হয়। একটা external API-র নাম (যেমন api.stripe.com) কল করলে resolver প্রথমে api.stripe.com.default.svc.cluster.local, তারপর আরও কয়েকটা ভুল suffix চেষ্টা করে, শেষে আসল বাইরের নাম resolve করে — প্রতিটা লুকানো, ব্যর্থ query একটা বাড়তি network round-trip। উচ্চ-throughput সার্ভিসে এই বাড়তি latency জমতে জমতে চোখে পড়ার মতো হয়ে যায় — এটাই সবচেয়ে সাধারণ, কম-জানা Kubernetes DNS পারফরম্যান্স ফাঁদ।

যে ভুলগুলো সবাই করে

“ClusterIP resolve হওয়া মানেই Service-টা reachable — DNS সফল হলে connectivity নিয়ে চিন্তা করার দরকার নেই”

সম্পূর্ণ ভুল সংযোগ। DNS resolution শুধু প্রমাণ করে CoreDNS নামটা চেনে আর একটা ClusterIP ফেরত দিয়েছে — এটা প্যাকেট আসলে কোনো জীবিত Pod-এ পৌঁছাবে কিনা তার কোনো নিশ্চয়তা দেয় না। সেই দায়িত্ব সম্পূর্ণ আলাদা স্তরের — kube-proxy-র রুটিং রুল, matching Pod জীবিত ও Ready কিনা, NetworkPolicy অনুমতি দিচ্ছে কিনা। nslookup সফল হওয়া মানে শুধু “নাম-থেকে-ঠিকানা” ধাপ পেরিয়েছে, “ঠিকানা-থেকে-সংযোগ” ধাপ একদমই আলাদা পরীক্ষা দাবি করে (curl/telnet)।

“CoreDNS একটা বিশেষ, cluster-এর বাইরে চলা infrastructure component — যেমন cloud provider-এর একটা managed সার্ভিস”

উল্টো — CoreDNS নিজেই কয়েকটা সাধারণ Pod, cluster-এর ভেতরে, kube-system namespace-এ, একটা সাধারণ Deployment হিসেবে চলে, একটা সাধারণ Service-এর পেছনে বসে। এটা অন্য যেকোনো অ্যাপ্লিকেশন Pod-এর মতোই kubectl get pods দিয়ে দেখা যায়, crash করতে পারে, resource limit ছাড়িয়ে যেতে পারে, restart হতে পারে। “cluster নিজেই DNS সার্ভার চালায়” কথাটার মানে আক্ষরিক — কোনো বাইরের managed magic না।

“Service মোছার পরে পুরনো নাম কিছুক্ষণ (TTL-এর মতো) কাজ করবে, কারণ DNS তো cache করে”

Cluster DNS-এর ক্ষেত্রে ভুল। Public internet DNS-এ TTL-ভিত্তিক caching থাকে কারণ রেকর্ড আসে একটা ধীরে-বদলানো zone file থেকে, আর caching root/TLD সার্ভারদের ওপর চাপ কমায়। CoreDNS-এর kubernetes plugin ভিন্নভাবে কাজ করে — রেকর্ড API server watch থেকে সরাসরি, লাইভ regenerate হয়; একটা Service মোছা মাত্রই সেই watch event পৌঁছায় আর রেকর্ড অবিলম্বে সেট থেকে বাদ পড়ে। কিছু ক্ষেত্রে client-side (Pod-এর নিজস্ব stub resolver-এ) সংক্ষিপ্ত caching থাকতে পারে, কিন্তু CoreDNS-এর নিজের রেকর্ড সেটে কোনো “পুরনো কিন্তু এখনো valid” জানালা নেই।

“`ledger.billing.svc.cluster.local` লেখার বদলে সবসময় শুধু `ledger` লিখলেই চলে, namespace-এর কথা ভাবতে হয় না”

শুধু তখনই চলে যখন caller নিজেও ঠিক সেই একই namespace-এ থাকে — search-domain suffixing-এর প্রথম চেষ্টাটাই caller-এর নিজের namespace ধরে নেয়। ভিন্ন namespace থেকে কল করলে শুধু ledger লিখলে ভুল Service-এ (বা কোনো Service-েই না) পৌঁছাতে পারে, কারণ প্রতিটা namespace-এ একই short name-এর আলাদা Service থাকতে পারে (এই লেসনের build অংশের ledger.billing বনাম ledger.default উদাহরণ)। Cross-namespace কল করলে অন্তত <name>.<namespace> লেখাই নিরাপদ অভ্যাস।

বুঝেছেন কি না দেখুন

1একটা Service DNS নামের পূর্ণাঙ্গ ফর্ম কী, আর প্রতিটা অংশ কী বোঝায়?স্মরণ
<service-name>.<namespace>.svc.cluster.local। <service-name> হলো Service-এর নাম, <namespace> যে namespace-এ সেটা আছে, svc একটা fixed sub-zone যা Service-দের জন্য সংরক্ষিত (Pod-দের জন্য pod ব্যবহৃত হয়), আর cluster.local পুরো ক্লাস্টারের internal DNS root — public internet-এর root zone-এর সমতুল্য কিন্তু শুধু এই cluster-এর ভেতরে বৈধ।
2CoreDNS কোথায় চলে, আর সেই অবস্থানটা নিয়ে অবাক হওয়ার মতো কী আছে?স্মরণ
CoreDNS নিজেই কয়েকটা সাধারণ Pod, সেই একই ক্লাস্টারের kube-system namespace-এ চলে যার জন্য সে DNS সার্ভিস দেয়। অবাক করার বিষয়টা হলো — DNS সার্ভিসটাই নিজে Service abstraction-এর ওপর নির্ভরশীল (তার নিজেরও একটা ClusterIP আছে), যেটা এমন একটা সমস্যা যার সমাধানের জন্যই CoreDNS তৈরি। এটা reconciliation-loop প্যাটার্নের একটা নিজেকে-ব্যবহার-করা উদাহরণ।
3Cluster DNS-এর রেকর্ড কেন static zone file থেকে না এসে API server watch থেকে সরাসরি লাইভ তৈরি হয়? এই পার্থক্যটা public internet DNS-এর সাথে কীভাবে বিপরীত?যুক্তি
Kubernetes-এ Service প্রায়ই মিনিটের পর মিনিটে তৈরি-মোছা হয় (CI/CD, autoscaling, rolling deploy) — এই গতির সাথে একটা মানুষ-এডিট-করা zone file তাল মেলাতে পারত না। তাই CoreDNS-এর kubernetes plugin API server-এর সাথে watch সংযোগ রাখে, প্রতিটা change-event-এ রেকর্ড regenerate করে। Public internet DNS-এ (networking/dns লেসন) রেকর্ড আসে ধীরে-বদলানো zone file থেকে, আর TTL-ভিত্তিক caching সেই স্থিতিশীলতার ওপর নির্ভর করে বছরের পর বছর ধরে কাজ করছে — সেই মডেল Kubernetes-এর গতিতে অচল হয়ে যেত।
4payments namespace-এর একটা Pod থেকে শুধু ledger লিখে DNS query করা হলো, অথচ ledger নামের Service আসলে billing namespace-এ আছে। কী ঘটবে?প্রয়োগ
Search-domain suffixing প্রথমে caller-এর নিজের namespace ধরে চেষ্টা করবে — ledger.payments.svc.cluster.local — যেটা resolve হবে না যদি payments namespace-এ ledger নামের কোনো Service না থাকে। এরপর তালিকার বাকি suffix (svc.cluster.local, cluster.local) চেষ্টা হবে, কোনোটাই মিলবে না, শেষে NXDOMAIN। billing namespace-এর ledger-এ পৌঁছাতে হলে caller-কে অন্তত ledger.billing লিখতে হতো।
5একটা টিম রিপোর্ট করছে “আমাদের Pod অন্য একটা Service-এ কানেক্ট করতে পারছে না — timeout হচ্ছে।” nslookup <service-name> চালালে সঠিক ClusterIP ফেরত আসে। সমস্যাটা কোন স্তরে খোঁজা উচিত, আর কোন স্তরে না?প্রয়োগ
যেহেতু nslookup সঠিক ClusterIP ফেরত দিচ্ছে, DNS resolution স্তর (CoreDNS, নামকরণ) সম্পূর্ণ ঠিক আছে — এখানে সময় নষ্ট করার দরকার নেই। সমস্যাটা অবশ্যই এর পরের স্তরে: kube-proxy-র routing রুল, target Pod জীবিত ও Ready কিনা, বা NetworkPolicy ব্লক করছে কিনা — এই লেসনের “DNS আর kube-proxy দুইটা আলাদা স্তর” যুক্তিটার সরাসরি প্রয়োগ।
6আপনি একটা high-throughput payment-processing সার্ভিস ডিজাইন করছেন যা প্রতি সেকেন্ডে বহু বার একটা বাইরের third-party API (যেমন api.paymentgateway.com) কল করে, ক্লাস্টারের ভেতর থেকে। মনিটরিং-এ দেখা যাচ্ছে প্রতিটা কলের সাথে একটা রহস্যময় বাড়তি latency যুক্ত হচ্ছে যা internal Service কলে নেই। এই লেসনের কোন concept দিয়ে এটা ব্যাখ্যা করবেন, আর কী ঠিক করবেন?ডিজাইন
সন্দেহভাজন কারণ ndots:5-এর ডিফল্ট আচরণ — api.paymentgateway.com-এ ডট আছে ২টা, যা ৫-এর কম, তাই resolver প্রথমে ভুলভাবে সবগুলো internal search-suffix (api.paymentgateway.com.default.svc.cluster.local ইত্যাদি) চেষ্টা করে, প্রতিটাই CoreDNS-এ একটা ব্যর্থ query আর NXDOMAIN হিসেবে ফেরত আসে, তারপরই আসল বাইরের নাম resolve হয় — প্রতিটা কলে কয়েকটা বাড়তি, অকেজো round-trip। সমাধান: এই নামের শেষে একটা ডট যোগ করে FQDN হিসেবে লেখা (api.paymentgateway.com., যা suffix-চেষ্টা বাইপাস করে), অথবা Pod-এর DNS নীতিতে ndots কমিয়ে দেওয়া, অথবা DNS response cache (NodeLocal DNSCache-জাতীয় প্যাটার্ন) বসানো যাতে বারবার একই ব্যর্থ suffix-চেষ্টার খরচ কমে।

এরপর কী

আজ দেখলাম DNS কীভাবে একটা নামকে একটা ClusterIP-তে বদলায়, আর kube-proxy কীভাবে সেই একটা ClusterIP-কে একটা মিলে-যাওয়া Pod-এর কাছে পৌঁছে দেয় — কিন্তু একটা প্রশ্ন এড়িয়ে গেছি ইচ্ছাকৃতভাবে। একটা Service-এর পেছনে যদি একসাথে পাঁচটা, পঞ্চাশটা Pod থাকে, kube-proxy ঠিক কোনটা বেছে নেয়? আর যখন traffic-এর পরিমাণ এতটাই বেশি হয়ে যায় যে কোনো একক routing rule পরিষ্কারভাবে সামলাতে পারে না, তখন কী হয়?

পরের লেসন, virtualization-cloud/load-balancing-l4-l7, ঠিক এই দুইটা প্রশ্নের উত্তর দেবে — আসল load-balancing algorithm (round-robin, least-connection, consistent hashing), আর সবচেয়ে গুরুত্বপূর্ণভাবে L4-বনাম-L7 load balancing-এর মৌলিক পার্থক্য, যেটা শুধু Kubernetes-এ না, নেটওয়ার্কিং-এর প্রায় প্রতিটা কোণে প্রযোজ্য।

আরও পড়ুন

  • DNS for Services and Pods — Kubernetes documentation · Service ও Pod DNS নামকরণের নির্ভুল স্পেসিফিকেশন — A/AAAA/SRV record কীভাবে তৈরি হয়, search domain কীভাবে কাজ করে তার প্রামাণ্য উৎস
  • CoreDNS: DNS and Service Discovery for the Cloud Native Era — CoreDNS project (CNCF) · CoreDNS-এর plugin-চেইন স্থাপত্য আর `kubernetes` plugin কীভাবে API server watch করে রেকর্ড বানায় তার সরকারি documentation
  • Customizing DNS Service — Kubernetes documentation · `/etc/resolv.conf`-এ বসা search domain তালিকা আর ndots সেটিং-এর ব্যাখ্যা — সংক্ষিপ্ত নাম কেন কাজ করে তার ভিত্তি
  • kube-proxy — Kubernetes documentation · ClusterIP পাওয়ার পর প্যাকেট আসলে কীভাবে একটা Pod-এ রুট হয় তার প্রামাণ্য উৎস — এই লেসন যেখানে থেমে prerequisite লেসনের দিকে ফিরিয়ে দেয়