چطور به یک برنامه که تحت Kubernetes اجرا شده است وصل شویم؟
در این مطلب قصد داریم بررسی کنیم که چطور میتوانیم با استفاده از Kubernetes به دنیای بیرون سرویس ارایه کنیم. اگر با مفاهیم اولیه Kubernetes آشنایی ندارید توصیه میکنیم ابتدا به مقاله کوبرنتیس چیست نگاهی بیندازید.
اگر با یکی از روشهای گفته شده در این مقاله یک کلاستر کوبرنتیس راه اندازی کنید و چند سرویس هم بر روی آن اجرا کنید به زودی متوجه میشوید که روش های معمول ارایه سرویسهای وبی و توزیع بار (Load Balancing) مثلا با استفاده از Nginx برای اتصال به این سرویسها مشابه معمول کار نمیکند. علت این است که اجزای موجود در یک کلاستر Kubernetes به صورت پیش فرض از دنیای بیرون جدا هستند و فقط تحت شرایط خاصی میتوانند به درخواستهای دنیای خارج یا همان کاربران نهایی پاسخ بدهند.
در ادامه به بررسی روش های موجود برای این کار بر روی سرورهای مجازی ابری و همچنین سرورهای سخت افزاری (bare-metal) میپردازیم. اما قبل از این کار یک سری مفاهیم ساده و اولیه را با هم مرور میکنیم.
پاد (Pod)
در مجموعه واژگان کوبرنتیس به چند کانتینر که چرخه حیات (Life Cycle) یکسانی دارند و در واقع با هم و روی یک ماشین در کلاستر اجرا میشوند پاد (Pod) میگویند. پاد کوچکترین جزء در سیستم کوبرنتیس محسوب میشود که کانتینرهای موجود در آن به منابع سیستمی یکسانی دسترسی دارند. در اکثر مواقع پاد تنها از یک کانتینر تشکیل میشود.
سرویس (Service)
سرویس در واقع یک لایه منطقی بر روی مجموعه ای از پادهاست. با استفاده از سرویس این امکان داده میشود که چندین پاد را انتخاب کرده و ترافیک ورودی را به یکی از آنها برسانیم. این پادها میتوانند نسخه های یکسانی از یک برنامه (جهت افزایش قابلیت اطمینان) و یا نسخه های متفاوتی از آن (جهت تست A/B) باشند. مثلا شما میتوانید برای تمام پادهای اجرا شده برای یک API خودتان یک سرویس ایجاد کنید که برنامه های دیگر به آن متصل شوند. از دید این برنامه ها فقط یک Endpoint برای این واسط کاربردی وجود دارد و آن سرویس شماست.
سرویسها به طور پیش فرض فقط از داخل کلاستر قابل دسترسی هستند. هدف ما در این مقاله بیان شیوه های رساندن ترافیک به این سرویسها از دنیای خارج است.
روش اول Node Port
در این روش یک پورت شبکه (port) بر روی سرورهای شما که جزئی از کلاستر هستند باز شده و شما میتوانید ترافیک را از طریق این پورت به سرویس برسانید. این پورت به صورت پیش فرض از بازه ۳۲۷۶۷-۳۰۰۰۰ انتخاب میشود که تقریبا در اکثر مواقع چیزی نیست که ما به دنبالش هستیم. چون مثلا ترافیک وب باید بر روی پورت ۸۰ یا ۴۴۳ باشد که این روش برای آن مناسب نیست.
متن زیر نمونه ای از تعریف یک سرویس است که به همین روش پورت ۳۰۰۶۱ را به عنوان nodePort
انتخاب کرده است:
kind: Service
apiVersion: v1
metadata:
name: my-service
spec:
selector:
app: MyApp
type: NodePort
ports:
- protocol: TCP
port: 80
targetPort: 9376
nodePort: 30061
clusterIP: 10.0.171.239
روش دوم Load Balancer
در این روش کوبرنتیس از طریق اتصال به فراهم کننده بستر ابری برای سرویس شما یک IP در نظر میگیرد که با استفاده از این IP میتوانید از خارج کلاستر به سرویس خود متصل شوید. ایراد این روش این است که شما فقط میتوانید در صورتی از آن استفاده کنید که ارایه دهنده خدمات ابری شما این سرویس را ارایه کند.
متن زیر نمونه ای از تعریف یک سرویس است که به این روش آدرس 78.11.24.19
را به عنوان loadBalancerIP
پیشنهاد داده است ولی در نهایت بستر ابری آدرس 146.148.47.155
را در قسمت status.loadBalancer
برای این سرویس انتخاب کرده است:
kind: Service
apiVersion: v1
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
nodePort: 30061
clusterIP: 10.0.171.239
loadBalancerIP: 78.11.24.19
type: LoadBalancer
status:
loadBalancer:
ingress:
- ip: 146.148.47.155
روش سوم Port Proxy
از آنجایی که پادها بر خلاف سرویسها میتوانند به پورت های پایه دسترسی داشته باشند در این روش یک پاد بر روی یک نود (Node) ایجاد میشود که بر روی پورت مورد نظر ترافیک را دریافت و به سرویس مقصد منتقل میکند. برای اینکار از یک تصویر (Image) آماده داکر استفاده میشود که توانایی انتقال ترافیک را در لایه ۴ شبکه (لایه TCP و UDP) داشته باشد.
نمونه این پاد در زیر بر روی پورت ۵۳ سرویس DNS داخلی کوبرنتیس را ارایه میدهد:
apiVersion: v1
kind: Pod
metadata:
name: dns-proxy
spec:
containers:
- name: proxy-udp
image: gcr.io/google_containers/proxy-to-service:v2
args: [ "udp", "53", "kube-dns.default", "1" ]
ports:
- name: udp
protocol: UDP
containerPort: 53
hostPort: 53
- name: proxy-tcp
image: gcr.io/google_containers/proxy-to-service:v2
args: [ "tcp", "53", "kube-dns.default" ]
ports:
- name: tcp
protocol: TCP
containerPort: 53
hostPort: 53
روش چهارم External IP ها
در حالتی که یک آدرس IP خاص از قبل وجود داشته باشد که بتواند به یک سرویس موجود در کلاستر ترافیک را برساند ما میتوانیم با استفاده از آن به سرویس مورد نظر متصل شویم:
kind: Service,
apiVersion: v1,
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- name: http,
protocol: TCP,
port: 80,
targetPort: 9376
externalIPs:
- 80.11.12.10
روش پنجم استفاده از Ingress ها
میتوان گفت پیشرفته ترین روش استفاده از Ingress است. اما ابتدا ببینیم Ingress چیست:
Ingress
Ingress ها یک سری قوانین (rule) هستند که با تعریف آنها میتوانیم ترافیک را از بیرون به سرویس ها نگاشت (map) کنیم. در واقع این قوانین یک لایه منطقی تشکیل میدهند که با استفاده از آن نحوه فرستادن ترافیک خارجی به سرویس ها مشخص میشود.
اما Ingress ها به تنهایی کار نمیکنند. در واقع برای اینکه این قوانین اجرایی شوند نیازمند Ingress Controller ها هستیم. Ingress Controller ها انواع مختلفی دارند. مثلا اکثر بسترهای ابری Ingress Controller های خاص خودشان را دارند که با استفاده از آنها میشود ترافیک را به راحتی بر روی سرویس فرستاد. همچنین ما میتوانیم Ingress Controller هایی را با استفاده از nginx و به صورت چند پاد بر روی خود کلاستر اجرا کنیم.
مثلا به طریق زیر میتوانیم با کمک daemon set ها (یعنی پاد هایی که بر روی تمام نود ها اجرا میشوند) یک Ingress Controller از نوع nginx بر روی هر نود کلاستر اجرا کنیم:
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: nginx-ingress-lb
labels:
name: nginx-ingress-lb
namespace: kube-system
spec:
template:
metadata:
labels:
name: nginx-ingress-lb
annotations:
prometheus.io/port: '10254'
prometheus.io/scrape: 'true'
spec:
terminationGracePeriodSeconds: 60
containers:
- image: gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.3
name: nginx-ingress-lb
readinessProbe:
httpGet:
path: /healthz
port: 10254
scheme: HTTP
livenessProbe:
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
timeoutSeconds: 1
ports:
- containerPort: 80
hostPort: 80
- containerPort: 443
hostPort: 443
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
args:
- /nginx-ingress-controller
- --default-backend-service=$(POD_NAMESPACE)/default-http-backend
دقت کنید که برای اینکار حتما باید یک پاسخ دهنده پیش فرض (default backend) وجود داشته باشد تا در صورتیکه کنترلر سرویسی برای پاسخ دادن به ترافیک وروردی پیدا نکرد از آن استفاده کند. البته نیازی نیست که حتما برای اجرای Ingress Controller ها از Daemon Set استفاده شود و میتوان از بقیه مکانیزمها مثل Deployment یا Replication Controller نیز استفاده کرد.
حال که کنترلر ما اجرا شده است با تعریف Ingress ها میتوانیم ترافیک را بر روی پورت های مشترک بین سرویس ها تقسیم کنیم. مثلا:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test-ingress
spec:
rules:
- http:
paths:
- path: /testpath
backend:
serviceName: test
servicePort: 80
باعث میشود که با استفاده از مسیر /testpath
بتوانیم به سرویس مورد نظر خود وصل شویم.
یا با تعریف:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test
spec:
rules:
- host: foo.bar.com
http:
paths:
- backend:
serviceName: s1
servicePort: 80
- host: bar.foo.com
http:
paths:
- backend:
serviceName: s2
servicePort: 80
میتوان به سرویسهای مختلف بر اساس نام هاست (host) وصل شد. مثلا در نمونه بالا با اتصال به آدرس http://foo.bar.com
میتوان به سرویس s1
درخواست فرستاد.
برای آشنایی بیشتر با این فنآوری میتوانید ارایه ضبط شده کوبرنتیس در همایش داکر تهران را بر روی سایت تاک مشاهده کنید.
نویسنده: میعاد ابرین LinkedIn - Github - Stackoverflow
ویرایش: امیر مقیمی