העלאת סרביס מ docker-compose ל Kubernetes

07/09/2021

פוסט זה כולל טיפ קצר על Docker. אם אתם רוצים ללמוד יותר לעומק על פיתוח עם Docker, Docker Compose או Kubernetes תשמחו לשמוע שבניתי קורס וידאו מקיף בנושא זה.
למידע נוסף והצטרפות לקורס בקרו בדף קורס Docker כאן באתר.
 

דרך אחת ללמוד קוברנטס היא לקרוא את כל דפי התיעוד שלהם, אבל זה לוקח המון זמן ולא תמיד מבטיח שגם נבין את מה שנקרא. דרך יותר מעשית היא לקחת פרויקט פשוט ולהעלות אותו על קלאסטר. בפוסט זה אני אראה את הדרך השניה ומקווה שהיא תתן לכם נקודת התחלה טובה לעבודה עם k8s.

1. תיאור הפרויקט

רציתי למצוא פרויקט פשוט אבל עדיין שיכיל היבטים מעניינים של Deployment ולכן בחרתי בשרת Node.JS שמשתמש בבסיס נתונים Redis. קובץ ה docker-compose.yml שאני מתחיל איתו נראה כך:

version: "3"
services:
  web:
    image: ynonp/kubedemo:1.0
    ports:
      - "3000:3000"
    deploy:
      restart_policy:
        condition: any

  redis:
    image: "redis:alpine"
    volumes:
      - data:/data

volumes:
  data:

האימג' kubedemo נוצר מה Dockerfile הבא:

FROM node:16

# Create app directory
WORKDIR /app

# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./

RUN npm install
# If you are building your code for production
# RUN npm ci --only=production

# Bundle app source
COPY . .

EXPOSE 3000
CMD [ "node", "server.js" ]

ותוכן הקובץ server.js באותה תיקיה הוא:

const express = require('express')
const app = express()
const port = 3000

let
  redis     = require('redis'),
  /* Values are hard-coded for this example, it's usually best to bring these in via file or environment variable for production */
  client    = redis.createClient({
    port      : 6379,
    host      : 'redis',        // replace with your hostanme or IP address
  });

app.get('/', (req, res) => {
  client.get(req.hostname, (err, count) => {
    if (err) return next(err);
    res.send({ count });
  });
});

app.post('/', (req, res, next) => {
  client.incr(req.hostname, (err, count) => {
    if (err) return next(err);

    res.send({ count });
  });
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

הקובץ מתחבר לרדיס ויודע לקבל שני סוגי בקשות: בקשת GET לנתיב הראשי מחזירה ערך מונה מסוים, ובקשת POST לנתיב הראשי מגדילה את המונה ב-1. מה שחשוב מבחינת ה Deployment זה שיש רק שרת אחת שמקשיב על פורט 3000 ומתחבר למכונת רדיס אחת. מספיק פשוט בשביל פרויקט קוברנטס ראשון.

2. התקנה ויצירת קבצי yaml של Kubernetes

יש המון דרכים לקבל גישה לקלאסטר קוברנטס: אתר https://labs.play-with-k8s.com/ ייתן לכם קלאסטר בחינם ל-4 שעות. כל ספקי הענן הגדולים יתנו לכם קלאסטר למשחקים בתוכנית החינמית שלהם וספקי ענן קטנים יותר יתנו לכם קלאסטר בתשלום סמלי. אבל מכל האפשרויות אני מוצא שהכי נוחה היא התקנת minikube אצלכם על הלפטופ. זה פשוט עובד הכי מהר והכי קל למצוא בעיות.

כאן יש הוראות התקנה לכל מערכות ההפעלה: https://minikube.sigs.k8s.io/docs/start/

אתם יודעים שהצלחתם אם אתם יכולים להפעיל:

$ kubectl config current-context

והמחשב מדפיס את מילת הקסם minikube.

הכלי השני שאשתמש בו בדוגמה נקרא kompose והוא יודע להפוך קובץ docker-compose.yml לקבצי הגדרות של kubernetes. מתקינים אותו מכאן: https://kompose.io/.

אחרי התקנה מוצלחת אתם צריכים להיות מסוגלים להריץ משורת הפקודה:

$ kompose version

ולקבל משהו כמו 1.22.0.

אחרי שכל הכלים מותקנים נצא לדרך עם הפרויקט.

3. המרת docker-compose.yml לקבצי k8s

אני יוצר תיקיה חדשה ליד קובץ docker-compose.yml וקורא לה k8s. אחר כך מפעיל:

$ kompose convert -f docker-compose.yml -o k8s

ומקבל את ההדפסה:

INFO Kubernetes file "k8s/web-service.yaml" created 
INFO Kubernetes file "k8s/redis-deployment.yaml" created 
INFO Kubernetes file "k8s/data-persistentvolumeclaim.yaml" created 
INFO Kubernetes file "k8s/web-deployment.yaml" created 

עכשיו האופטימיים מוזמנים להריץ את הפקודה הבאה כדי לנסות להפעיל את הסרביס על הקלאסטר שלנו:

$ kubectl apply -f k8s

אבל הריאליסטיים מבין הקוראים יודעים שזה לא הולך לעבוד כל כך בקלות. הפקודה kubectl apply פשוט לוקחת קובץ הגדרות או תיקיה של קבצי הגדרות ומנסה "להתקין" אותם על הקלאסטר, כלומר לשנות את מצב הקלאסטר כדי שמה שכתוב בהגדרות זה גם מה שיהיה עליו. הבעיה שמה שכתוב בהגדרות ש kompose יצר עדיין לא מספיק מוכן.

את הנקודה הבעייתית הראשונה אנחנו יכולים לראות במבט חטוף על הקבצים שנוצרו: קומפוז יצר שני קבצים בשביל הסרביס web אבל רק קובץ אחד בשביל הסרביס redis. הקבצים שנוצרו ל web הם מסוג Deployment ו Service, ול redis נוצר רק קובץ Deployment.

אתם יכולים לראות מה ההבדל בין שני הבלוקים בקובץ docker-compose.yml המקורי שלנו?

  web:
    image: ynonp/kubedemo:1.0
    ports:
      - "3000:3000"
    deploy:
      restart_policy:
        condition: any

  redis:
    image: "redis:alpine"
    volumes:
      - data:/data

אז נכון יש כמה הבדלים אבל ההבדל המרכזי שגרם לקומפוז להתנהג אחרת הוא ש web הגדיר מפתח ports ו redis לא הגדיר. מבחינת קומפוז אם אין לך ports אי אפשר יהיה להתחבר אליך (למרות שב docker-compose אין בעיה להתחבר לסרביס שלא הגדיר מיפוי פורטים). בכל מקרה ובשביל להיות נחמדים לקומפוז נעדכן את הקובץ לגירסה הזו:

version: "3"
services:
  web:
    image: ynonp/kubedemo:1.0
    ports:
      - "3000:3000"
    deploy:
      restart_policy:
        condition: any

  redis:
    image: "redis:alpine"
    ports:
      - "6379:6379"
    volumes:
      - data:/data

volumes:
  data:

ונריץ מחדש את קומפוז:

$ kompose convert -f docker-compose.yml -o k8s
INFO Kubernetes file "k8s/redis-service.yaml" created 
INFO Kubernetes file "k8s/web-service.yaml" created 
INFO Kubernetes file "k8s/redis-deployment.yaml" created 
INFO Kubernetes file "k8s/data-persistentvolumeclaim.yaml" created 
INFO Kubernetes file "k8s/web-deployment.yaml" created 

ניסיון שני להתקין את הקבצים על הקלאסטר ובדיקה של התוצאה מביא לנו:

$ kubectl apply -f k8s
$ kubectl get pods
NAME                     READY   STATUS             RESTARTS   AGE
redis-784899f6c7-mtwzs   1/1     Running            0          5s
web-675ddc94f5-gcz8s     0/1     CrashLoopBackOff   1          5s

כמה שתריצו יותר פעמים את kubectl get pods תוכלו לראות שבסך הכל מונה ה Restarts יעלה אבל הסרביס שלנו לא יעבוד. בדיקה של הלוגים תראה לכם שסרביס web לא מוצא את הסרביס redis. המשך מחקר יספר לכם שהסיבה היא שלא הגדרנו hostname ב docker-compose.yml. במעבר מדוקר קומפוז לקוברנטס הכלי kompose לא יגדיר בשבילכם את ה hostname בצורה אוטומטית אם לא הגדרתם אותו בצורה מפורשת בקובץ.

העדכון הבא ל docker-compose.yml מביא אותנו לגירסה הזו:

version: "3"
services:
  web:
    hostname: web
    image: ynonp/kubedemo:1.0
    ports:
      - "3000:3000"
    deploy:
      restart_policy:
        condition: any

  redis:
    hostname: redis
    image: "redis:alpine"
    ports:
      - "6379:6379"
    volumes:
      - data:/data

volumes:
  data:

והפעל התקנה והפעלה מחדש והכל כבר עובד:

$ kompose convert -f docker-compose.yml -o k8s
$ kubectl apply -f k8s
$ kubectl get pods

NAME                    READY   STATUS    RESTARTS   AGE
redis-f7f66c7cb-zd9r9   1/1     Running   0          68s
web-6b56cc97d7-mclw2    1/1     Running   2          74s

עכשיו אני יכול להיכנס לשרת ה web ולבצע ממנו curl כדי לראות שהכל עובד:

$ kubectl exec -it web-6b56cc97d7-mclw2 -- /bin/bash
$ curl web:3000/
{"count":null}

אבל עדיין לא יכול להתחבר למערכת מבחוץ. בשביל זה צריך Ingress Controller.

4. הוספת ingress

ב Kubernetes יש מנגנון אוטומטי שמפעיל שרת Nginx בכניסה לקלאסטר שלכם ומנהל את כל החיבורים פנימה. נכון, אפשר היה לחבר את הסרביס web שכתבתי ישירות לפורט 3000 של הקלאסטר אבל זה היה משעמם ולא מתאים למצב פרודקשן.

בשביל להגדיר את אותו Nginx אוטומטי על minikube אני מפעיל את הפקודה:

$ minikube addons enable ingress

ואם הכל עבד כמו שצריך אתם תוכלו להריץ:

$ kubectl get pods -n ingress-nginx

ולראות את הפלט:

NAME                                        READY   STATUS      RESTARTS   AGE
ingress-nginx-admission-create-2pz8l        0/1     Completed   0          68m
ingress-nginx-admission-patch-kjrv2         0/1     Completed   0          68m
ingress-nginx-controller-59b45fb494-nvkbg   1/1     Running     0          68m

אחרי שאנחנו יודעים ש nginx שלנו באוויר אנחנו צריכים לכתוב לו קובץ הגדרות. בגלל קוברנטס זה לא יהיה קובץ הגדרות ספציפי של nginx אלא קובץ הגדרות של קוברנטס וקוברנטס יתרגם אותו להגדרות nginx (אולי כדי שיום אחד בעתיד נוכל להחליף את ה Gateway בכניסה לקלאסטר בצורה אוטומטית).

בכל מקרה כתבו קובץ חדש בתיקיית k8s בשם ingress.yaml עם התוכן הבא:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress
spec:
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 3000

בעברית זה אומר שכל פעם שמישהו נכנס לקלאסטר שלנו לנתיב הראשי אנחנו פשוט נשלח אותו לסרביס web בפורט 3000. בעתיד כשיהיו לנו יותר סרביסים נוכל לבנות מערכת ניתוב יותר מתוחכמת, אבל זה כבר נושא לפוסט אחר.

הפעילו שוב:

$ kubectl apply -f k8s

ואז:

$ kubectl get ingress

ואם הכל עבד כמו שצריך תקבלו על המסך שורה שנראית בערך כך עם כתובת IP של הקלאסטר שלכם:

NAME      CLASS    HOSTS   ADDRESS        PORTS   AGE
ingress   <none>   *       192.168.49.2   80      46m

כניסה דרך הדפדפן או עם curl לכתובת ה IP שקיבלתם תחזיר את אוביקט ה JSON הריק:

$ curl 192.168.49.2
{"count":null}

ושליחת בקשת POST תעלה את המונה ב-1:

$ curl -X POST 192.168.49.2
{"count":1}

$ curl -X POST 192.168.49.2
{"count":2}

$ curl -X POST 192.168.49.2
{"count":3}

5. מה הלאה

מה הלאה? יש לכם קלאסטר קוברנטס שמריץ אפליקציה דרך NGINX, שמתחבר ל Redis ושומר את המידע ב Persistent Volume. מה עוד אפשר לרצות!?

נו, לא יותר מדי.

השלב הבא יהיה לעבור על קבצי ה yaml בתוך תיקיית k8s, לקרוא אותם ולחפש בתיעוד מה כל שורה בהם אומרת. אחרי זה אפשר לנסות להוסיף עוד סרביסים ל docker-compose.yml ולראות איך kompose מתרגם אותם ולנסות להפעיל גם אותם. אחרי שתעשו את זה מספיק פעמים אולי התיעוד של קוברנטס כבר לא ירגיש כל כך משעמם ותוכלו אפילו לקרוא אותו מקצה לקצה.

נ.ב. שימו לב רק לשורת הגירסה ב docker-compose.yml שלכם. ברוב המקרים בכתיבת docker-compose.yml אנחנו מציינים גירסה כמה שיותר מתקדמת, אבל בשביל kompose צריך לוותר על החלק שאחרי הנקודה ולציין פשוט:

version: "3"

אם תשכחו את זה אז kompose פשוט ידפיס שגיאה למסך, וזה דווקא נחמד מצידו כי על רוב הדברים הוא לא טורח לדווח.