2019-02-16

לקבל מושג ירוק על קוברנטיס (Kubernetes)


הפוסט הזה נכתב לאנשי-תוכנה מנוסים, המעוניינים להבין את Kubernetes - בהשקעת זמן קצרה.
הבאזז מסביב ל Docker ו Docker Orchestration הוא כרגע רב מאוד. דברי שבח רבים מסופרים על הטכנולוגיות הללו, מבלתי להתייחס לפרטים ועם מעט מאוד ראיה עניינית וביקורתית. כאנשי-תוכנה ותיקים אתם בוודאי מבינים שהעולם הטכנולוגיה מלא Trade-offs, וכדאי לגשת לטכנולוגיות חדשות עם מעט פחות התלהבות עיוורת - וקצת יותר הבנה.

אני הולך לספק את הידע בפוסט לא בפורמט ה "From A to Z" כפי שדיי מקובל - אבל ע"פ סדר שנראה לי יותר נכון והגיוני. אני רוצה לדלג על דיון ארוך על ההתקנה, ועוד דיון ארוך על הארכיטקטורה - עוד לפני שאנחנו מבינים מהי קוברנטיס. אני הולך לדלג על כמה פרטים - היכן שנראה לי שהדבר יתרום יותר להבנה הכללית.


בואו נתחיל.


אז מהי בעצם קוברנטיס?


פורמלית קוברנטיס היא Container Orchestration Framework (הנה פוסט שלי על הנושא) שהפכה בשנתיים האחרונות לסטנדרט דה-פאקטו (הנה פוסט אחר שלי שמנתח את העניין).

בכדי לפשט את הדברים, ניתן פשוט לומר שקוברנטיס היא סוג של סביבת ענן: 
  • אנו אומרים לה מה להריץ - והיא מריצה. אנו ״מזינים״ אותה ב Containers (מסוג Docker או rkt) והגדרות - והיא ״דואגת לשאר״.
  • אנו מקצים לקוברנטיס כמה שרתים (להלן ״worker nodes״ או פשוט "nodes") שהם השרתים שעליהם ירוצו הקונטיינרים שלנו. בנוסף יש להקצות עוד כמה שרתים (בד״כ - קטנים יותר) בכדי להריץ את ה master nodes - ה״מוח״ מאחורי ה cluster.
  • קוברנטיס תדאג ל containers שלנו:
    • היא תדאג להרים כמה containers בכדי לאפשר high availability - ולהציב אותם על worker nodes שונים.
    • אם יש עומס עבודה, היא תדאג להריץ עוד עותקים של ה containers, וכשהעומס יחלוף - לצמצם את המספר. מה שנקרא auto-scaling.
    • אם container קורס, קוברנטיס תדאג להחליף אותו ב container תקין, מה שנקרא גם auto-healing.
    • קוברנטיס מספקת כלים נוחים לעדכון ה containers לגרסה חדשה יותר, בצורה שתצמצם למינימום את הפגיעה בעבודה השוטפת - מה שנקרא deployment
      • כפי שראינו בפוסט על Docker - פעולת restart של Container תהיה מהירה משמעותית מ VM, שזה גם אומר לרוב deployments מהירים יותר.
    • לשימוש בקוברנטיס יש יתרון בצמצום משמעותי של ה Lock-In ל Cloud Vendor [א], והיכולת להריץ את אותה תצורת ״הענן״ גם On-Premises.
      • הסתמכות על קוד פתוח, ולא קוד של ספק ספציפי - הוא גם יתרון, לאורך זמן, וכאשר הספק עשוי להיקלע לקשיים או לשנות מדיניות כלפי הלקוחות.
  • קוברנטיס גם מספקת לנו יכולות ליבה של ניהול Infrastructure as Code, היכולת להגדיר תצורה רצויה לתשתיות רשת, אבטחה בצורה הצהרתית ופשוטה - מה שמייתר כלי ניהול תצורה (Provisioning) כגון Chef, Puppet או Ansible - ויכול לחסוך עבודה משמעותית.




יעילות


עם כל היעילות המוגברת שהתרגלנו אליה מריצה בענן בעזרת שירותים כמו AWS EC2 או Azure Virtual Machines (להלן ״ענן של מכונות וירטואליות״) - קוברנטיס מאפשרת רמה חדשה וגבוהה יותר של יעילות בניצול משאבי-חומרה.

בתצורה קלאסית של מיקרו-שירותים, הפופולרית כיום - ייתכן ומדובר בניצולת חומרה טובה בכמה מונים. הכל תלוי בתצורה, אבל דיי טיפוסי להריץ את אותו ה workload של מיקרו-שירותים בעזרת קוברנטיס על גבי 20-50% בלבד מהחומרה שהייתה נדרשת על גבי ״ענן ציבורי של מכונות וירטואליות״.

איך זה קורה? למכונה וירטואלית יש overhead גבוה של זיכרון (הרצת מערכת ההפעלה + hypervisor) על כל VM שאנו מריצים. זה לא כ״כ משמעותי כשמריצים שרת גדול (כיום נקרא בבוז: Monolith) - אך זה מאוד משמעותי כאשר מריצים שרתים קטנים (להלן: מיקרו-שירותים).

מעבר לתקורה הישירה שעתה ציינו, יש תקורה עקיפה וגדולה יותר: כאשר אני מריץ על שרת 4 מיקרו-שירותים בעזרת VMs ומקצה לכל אחד מהמיקרו-שירותים 25% מהזיכרון וה CPU ההגבלה היא קשיחה. אם בזמן נתון שלושה מיקרו-שירותים משתמשים ב 10% ממשאבי המכונה כ״א, אבל המיקרו-שירות הרביעי זקוק ל 50% ממשאבי המכונה - הוא לא יכול לקבל אותם. ההקצאה של 25% היא קשיחה ואינה ניתנת להתגמשות, אפילו זמנית [ב].

בסביבת קוברנטיס ההגבלה היא לא קשיחה: ניתן לקבוע גבולות מינימום / מקסימום ולאפשר מצב בו 3 מיקרו-שירותים משתמשים ב 10% CPU ו/או זיכרון כ״א, והרביעי משתמש ב 50%. אפשר שגם 10 דקות אח״כ המיקרו-שירות הרביעי יהיה idle - ומיקרו-שירות אחר ישתמש ב 50% מהמשאבים.





הכרה חברתית


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

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

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


מחירים


כמובן שיש לכל הטוב הזה גם מחירים:
  • קוברנטיס היא טכנולוגיה חדשה שיש ללמוד - וכמות / מאמץ הלמידה הנדרש הוא לרוב גבוה ממה שאנשים מצפים לו.
    • התסריטים הפשוטים על גבי קוברנטיס נראים דיי פשוטים ואוטומטים. כאשר נכנסת לתמונה גם אבטחה, הגדרות רשת, ותעדוף בין מיקרו-שירות אחד על האחר - הדברים הופכים למורכבים יותר!
      Troubleshooting - עשוי להיות גם דבר לא פשוט, מכיוון ש"מתחת למכסה המנוע" של קוברנטיס - יש מנגנונים רבים.
    • ברוב המקרים נרצה להריץ את קוברנטיס על שירות ענן, ולכן נידרש עדיין לשמר מידה של מומחיות כפולה בשני השירותים: לשירות הענן ולקוברנטיס יש שירותים חופפים כמו Auto-Scaling, הרשאות ו Service Discovery (בד"כ: DNS). 
  • הטכנולוגיה אמנם לא ממש ״צעירה״, והיא בהחלט מוכחת ב Production - אך עדיין בסיסי הידע והקהילה התומכת עדיין לא גדולה כמו פתרונות ענן מסחריים אחרים. יש הרבה מאוד אינטגרציות, אך מעט פחות תיעוד איכותי וקל להבנה.
  • כמו פעמים רבות בשימוש ב Open Source - אין תמיכה מוסדרת. יש קהילה משמעותית ופעילה - אבל עדיין הדרך לפתרון בעיות עשויה להיות קשה יותר מהתבססות על פתרון מסחרי. 
    • גם בשימוש ב״קוברנטיס מנוהל״ (EKS, AKS, ו GKE), החלק המנוהל הוא החלק הקטן, והשאר - באחריותנו.
    • האם החיסכון הצפוי מניהול משאבים יעיל יותר, יצדיק במקרה שלכם שימוש בסביבה שדורשת מכם יותר תפעול והבנה?
      • במקרה של ניהול מאות או אלפי שרתים - קרוב לוודאי שזה ישתלם.
      • שימוש בקוברנטיס עשוי לפשט את סביבת התפעול, וה Deployment Pipeline. ההשקעה הנדרשת היא מיידית - בעוד התשואה עשויה להגיע רק לאחר זמן ניכר, כאשר היישום הספציפי באמת הגיע לבגרות.
      • במקרים לא מעטים, ארגונים נקלעים לשרשרת של החלטות שנגזרות מצו האופנה ובניגוד לאינטרס הישיר שלהם: עוברים למיקרו-שירותים כי ״כך כולם עושים״ / ״סיפורי הצלחה״ שנשמעים, משם נגררים לקוברנטיס - כי יש להם הרבה מאוד שרתים לנהל, שכבר נהיה דיי יקר.
        לו היינו עושים שיקולי עלות/תועלת מול המצב הסופי - כנראה שהרבה פעמים היה נכון לחלק את המערכת למודולים פשוטים, או להשתמש במיקרו-שירותים גדולים ("midi-services") - וכך לשלוט טוב יותר בעלויות והמורכבויות האחרות.





קוברנטיס בפועל



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

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

לצורך הדיון, נניח שאנו עובדים על AWS ו EKS ואמזון מנהלים עבורנו את ה Masters nodes. ה Worker nodes שלנו נמצאים ב Auto-Scaling Group - מה שאומר שאמזון תנהל עבורנו את ה nodes מבחינת עומס (תוסיף ותוריד מכונות ע״פ הצורך) והחלפת שרתים שכשלו. זה חשוב!

אנחנו גם משתמשים ב ECR (קרי Container Registry מנוהל) , ואנו משתמשים בכל האינטגרציות האפשריות של קוברנטיס לענן של אמזון (VPC, IAM, ELB, וכו׳). התצורה מקונפגת ועובדת היטב - ונותר רק להשתמש בה.

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






יצירת Pod


הפעולה הבסיסית ביותר היא הרצה של Container. בקוברנטיס היחידה האטומית הקטנה ביותר שניתן להריץ נקראת Pod והיא מכילה Container אחד או יותר. כרגע - נתמקד ב Pod עם Container יחיד, זה יהיה המצב ברבים מהמקרים.

אני מניח שאנחנו מבינים מהו Container (אם לא - שווה לחזור צעד אחורה, ולהבין. למשל: הפוסט שלי בנושא), ויש לנו כבר Image שאנו רוצים להריץ ב ECR.

בכדי להריץ Container, עלינו לעדכן את קוברנטיס ב manifest file המתאר Pod חדש מצביע ל container image. קוברנטיס ירשום את ה Pod ויתזמן אותו לרוץ על אחד מה nodes שזמינים לו.

כאשר ה node מקבל הוראה להריץ Pod עליו להוריד את ה container image - אם אין לו אותו כבר. כל node מחזיק עותקים עצמאיים של ה container images משיקולים של high availability.

הנה קובץ ה manifest שהרכבנו:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  labels:
    env: dev
    version: v1
spec:
  containers:
  - name: hello-world-ctr
    image: hello-world:latest
    ports:
    - containerPort: 8080
      protocol: TCP

קובץ ה manifest בקוברנטיס מורכב מ 4 חלקים סטנדרטיים:
  1. גרסת ה API
    1. הפורמט הוא לרוב <api group>/<version> אבל כמה הפקודות הבסיסיות ביותר בקוברנטיס נמצאות תחת API Group שנקרא core - ולא צריך לציין אותו.
    2. ה API של קוברנטיס נמצא (בעת כתיבת הפוסט) בגרסה 1.13 - אז למה גרסה 1?
      ניהול הגרסאות בקוברנטיס הוא ברזולוציה של משאב. הקריאה לייצור pod היא עדיין בגרסה 1 (כמו כמעט כל ה APIs. בעת כתיבת הפוסט אין עדיין גרסת v2 לשום API, מלבד v2alpha או v2beta - כלומר גרסאות v2 שעדיין אינן GA).
  2. סוג (kind) - הצהרה על סוג האובייקט המדובר. במקרה שלנו: Pod.
  3. metadata - הכולל שם ו labels שיעזרו לנו לזהות את ה pod שיצרנו.
    1. ה labels הם פשוט זוגות key/value שאנחנו בוחרים. הם חשובים מאוד לצורך ניהול Cattle של אובייקטים, והם בלב העבודה בקוברנטיס.
  4. spec - החלק המכיל הגדרות ספציפיות של המשאב שהוגדר כ "Type". 
    1. name - השם שניתן ל container בתוך ה Pod, וצריך להיות ייחודי. במקרה של container יחיד בתוך ה pod - אין בעיה כזו. 
    2. image - כמו בפקודת docker run...
    3. ports - ה port שיהיה published.
      TCP הוא ערך ברירת-המחדל, אך הוספתי אותו בכדי לעשות את ה Yaml לקריא יותר.



תזכורת קצרה על Yaml:

פה ייתכן וצריך לעצור שנייה ולחדד כמה מחוקי-הפורמט של Yaml.
ייתכן ונדמה לכם ש Yaml הוא פורמט פשוט יותר מ JSON - אבל זה ממש לא נכון. זה פורמט "נקי לעין" - אבל מעט מורכב.

רשימה ב Yaml נראית כך:
mylist:
    - 100
    - 200
"mylist" הוא ה key, והערך שלו הוא רשימה של הערכים 100 ו 200.
כל האיברים שלפניהם הסימן " - " ובעלי עימוד זהה - הם חברים ברשימה.
סימן ה Tab ברוב ה Editors מתתרגם ל 2 או 4 רווחים. ב Yaml הוא שקול לרווח אחד, ולכן שימוש בו הוא מקור לבעיות ובלבולים. הימנעו משימוש ב Tab בתוך קבצי Yaml!

המבנה הבא, שגם מופיע ב manifest (ועשוי לבלבל) הוא בעצם רשימה של Maps:
channels:
  - name: '#mychannel'
    password: ''
  - name: '#myprivatechannel'
    password: 'mypassword'
"channel" הוא המפתח הראשי, הכולל רשימה.
כאשר יש "מפתח: ערך" מתחת ל"מפתח: ערך" באותו העימוד - משמע שמדובר ב Map.
כלומר, המפתח "channel" מחזיק ברשימה של שני איברים, כל אחד מהם הוא מפה הכוללת שני מפתחות "name" ו "password".

אם נחזור לדוגמה של הגדרת ה container ב manifest למעלה, בעצם מדובר במפתח "containers" המכיל רשימה של איבר אחד.
בתוך הרשימה יש מפה עם 3 מפתחות ("image", "name", ו "ports") כאשר המפתח האחרון "ports" מכיל רשימה עם ערך יחיד, ובה מפה בעלת 2 entries.

הנה מדריך המתאר את ה artifacts הבסיסיים ב Yaml ממנו לקחתי את הדוגמאות.
חשוב להזכיר שיש עוד כמה וכמה artifacts ב Yaml - אם כי כנראה שלא נזדקק להם בזמן הקרוב.





עכשיו כשיש לנו manifest, אנחנו יכולים להריץ את ה Pod:
$ kubectl apply -f my-manifest-file.yml

kubectl הוא כלי ה command line של קוברנטיס. פקודות מסוימות בו יזכירו לכם את ה command line של docker.
במקרה הזה אנו במקרה הזה אנו מורים לקוברנטיס להחיל קונפיגורציה. הפרמטר f- מציין שאנו מספקים שם של קובץ.

תוך כמה עשרות שניות, לכל היות, ה pod שהגדרנו אמור כבר לרוץ על אחד ה nodes של ה cluster של קוברנטיס.

אנו יכולים לבדוק אלו Pods רצים בעזרת הפקודה הבאה:
$ kubectl get pods
עמודה חשובה שמוצגת כתוצאה, היא עמודת הסטטוס - המציגה את הסטטוס הנוכחי של ה pod. אמנם יש רשימה סגורה של מצבים בו עשוי להיות pod, אולי עדיין הסטטוס המדווח יכול להיות שונה.
למשל: הסטטוס ContainerCreating יופיע בזמן שה docker image יורד ל node. זה מצב נפוץ - אך לא מתועד היטב. את הסטטוס ניתן למצוא בעיקר... בקוד המקור של קוברנטיס.

הפקודה הבאה (וריאציות), בדומה לפקודת ה Docker המקבילה - תציג את הלוגים של ה Container ב Pod :
$ kubectl logs my-pod 
אם ב Pod יש יותר מ-2 containers (מצב שלא אכסה בפוסט), הפקודה תציג לוגים של ה container הראשון שהוגדר ב manifest. אפשר לציין את שם ה container כפי שצוין ב manifest - וכך להגיע ל container נתון בתוך Pod-מרובה containers.

עבור תקלות יותר בסיסיות (למשל: ה pod תקוע על מצב ContainerCreating וכנראה שה node לא מצליח להוריד את container image) - כדאי להשתמש בפקודה:
$ kubectl describe pods my-pod
התוצאה תהיה ססטוס מפורט שיכיל את הפרטים העיקריים מתוך ה manifest, רשימה של conditions של ה pod, ורשימת כל אירועי-המערכת שעברו על ה pod מרגע שהורנו על יצירתו. הנה דוגמה להפעלה הפקודה (מקור):


Name:  nginx-deployment-1006230814-6winp
Node:  kubernetes-node-wul5/10.240.0.9
Start Time: Thu, 24 Mar 2016 01:39:49 +0000
...
Status:  Running
IP:  10.244.0.6
Controllers: ReplicaSet/nginx-deployment-1006230814
Containers:
  nginx:
    Container ID: docker://90315cc9f513c750f244a355eb1149
    Image:  nginx
    Image ID:  docker://6f623fa05180298c351cce53963707
    Port:  80/TCP
    Limits:
      cpu: 500m
      memory: 128Mi
    State:  Running
      Started:  Thu, 24 Mar 2016 01:39:51 +0000
    Ready:  True
    Restart Count: 0
    Environment:        <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-5kdvl (ro)
Conditions:
  Type          Status
  Initialized   True
  Ready         True
  PodScheduled  True
Volumes:
  default-token-4bcbi:
    Type: Secret (a volume populated by a Secret)
    SecretName: default-token-4bcbi
    Optional:   false
...
Events:
  FirstSeen LastSeen Count From     SubobjectPath  Type  Reason  Message
  --------- -------- ----- ----     -------------  -------- ------  -------
  54s  54s  1 {default-scheduler }      Normal  Scheduled Successfully assigned nginx-deployment-1006230814-6winp to kubernetes-node-wul5
  54s  54s  1 {kubelet kubernetes-node-wul5} spec.containers{nginx} Normal  Pulling  pulling image "nginx"
  53s  53s  1 {kubelet kubernetes-node-wul5} spec.containers{nginx} Normal  Pulled  Successfully pulled image "nginx"
  53s  53s  1 {kubelet kubernetes-node-wul5} spec.containers{nginx} Normal  Created  Created container with docker id 90315cc9f513
  53s  53s  1 {kubelet kubernetes-node-wul5} spec.containers{nginx} Normal  Started  Started container with docker id 90315cc9f513

ניתן גם, בדומה ל Docker, לגשת ישירות ל console של ה container שרץ ב pod שלנו בעזרת הפקודה:
$ kubectl exec my-pod -c hello-world-ctr -it -- bash
במקרה הזה ציינתי את שם ה container, אם כי לא הייתי חייב.


נראה לי שזה מספיק, לבינתיים. בואו נסגור את העניינים:
$ kubectl delete -f my-manifest-file.yml
הפעולה הזו עלולה להיראות מוזרה ברגע ראשון. הסרנו את הקונפיגורציה - ולכן גם ה Pod ייסגר?

לשימוש בקוברנטיס יש שתי גישות עיקריות:

  • גישה אימפרטיבית - בה מורים לקוברנטיס איזה שינוי לבצע: להוסיף משאב, לשנות משאב, או להוריד משאב.
    • פקודות כגון kube ctl create או kubectl replace הן בבסיס הגישה האימפרטיבית.
  • גישה דקלרטיבית - בה מורים לקוברנטיס מה המצב הרצוי - והוא יגיע עליו בעצמו.
    • פקודת kubectl apply - היא בבסיס הגישה הדקלרטיבית. אפשר להגדיר כמעט הכל, רק באמצעותה.
    • החלת patch על גבי קונפיגורציה קיימת הוא משהו באמצע: זו פקודה דקלרטיבית, אך מעט חורגת מה lifecycle המסודר של הגישה הדקלרטיבית הקלאסית. סוג של תרגיל נינג'ה.
כמובן שהגישה הדקלרטיבית נחשבת קלה יותר לשימוש ולתחזוקה לאורך זמן - והיא הגישה הנפוצה והשלטת.

מכיוון שאין לנו הגדרה אלו Pods קיימים סה"כ במערכת - לא יכולנו לעדכן את המערכת בהגדרה הכוללת - ולכן הסרנו את המניפסט.



סיכום


ניסינו להגדיר בקצרה, ובצורה עניינית מהם היתרונות הצפויים לנו משימוש בקוברנטיס - מול הצפה של החסרונות והעלויות.

לא פעם קראתי מאמרים שמתארים את קוברנטיס כמעבר מהובלת מסע על גבי פרד - לנסיעה ברכב שטח יעיל!
אני חושב שההנחה הסמויה ברוב התיאורים הללו היא שהקוראים עדיין עובדים על גבי מערכות On-Premises ללא טכנולוגיות ענן. מעבר משם לקוברנטיס - היא באמת התקדמות אדירה.

המעבר משימוש ב"ענן של מכונות וירטואליות" לקוברנטיס - הוא פחות דרמטי. קוברנטיס תספק בעיקר יעילות גבוהה יותר בהרצת מיקרו-שירותים, וסביבה אחידה וקוהרנטית יותר למגוון פעולות ה deployment pipeline.
עדיין, נראה שקוברנטיס היא סביבה מתקדמת יותר - ואין סיבה שלא תהפוך לאופציה הנפוצה והמקובלת, בתוך מספר שנים.


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

חשוב לציין שה Pod שהגדרנו הוא עצמאי ו"חסר גיבוי" - מה שנקרא "naked pod".
אם naked pod כשל מסיבה כלשהי (הקוד שהוא מריץ קרס, או ה node שעליו הוא רץ קרס/נסגר) - הוא לא יתוזמן לרוץ מחדש. מנגנון ה auto-healing של קוברנטיס שייך לאובייקט / אבסטרקציה גבוהה יותר בשם ReplicaSet. אבסטרקציה מעט יותר גבוהה, שבה בדרך כלל משתמשים - נקראת Deployment.

כיסינו דיי הרבה לפוסט אחד. הנושאים הללו מצדיקים פוסט משלהם.


שיהיה בהצלחה!




----

לינקים רלוונטיים

רפרנס של פקודות ה kubectl


----

[א] אל דאגה! לספקי הענן יש אינטרס עליון לגרום לנו ל Lock-In גם על סביבת קוברנטיס. לאחר שהניסיונות להציע חלופות ״מקומיות״ לקוברנטיס כשלו - רק טבעי שהם יתמקדו בלהציע יכולות שיפשטו את השימוש בקוברנטיס, אך גם יוסיפו סוגים חדשים של Lock-In. בכל מקרה, ברוב הפעמים אנו כבר תלויים בתשתיות כמו RDS, Athena, S3 ועוד - ולכן כבר יש Lock-In מסוים גם בלי קשר לקוברנטיס.
"Cloud Agnostic Architecture״ הוא בסה״כ מיתוס, השאלה היא רק מידת התלות.

[ב] שווה לציין שזה המצב בענן ציבורי. כששכן שלנו למכונה ב AWS רוצה יותר CPU - למה שנסכים לתת לו? אנחנו משלמים על ה ״slice״ שלנו במכונה - שהוגדר בתנאי השירות.
בפתרונות של ענן פרטי (כמו VMWare) ישנן יכולות ״ללמוד״ על brusts של שימוש בקרב VM ולהתאים את המשאבים בצורה יעילה יותר. כלומר: המערכת רואה ש VM מספר 4 דורש יותר CPU אז היא משנה, בצורה מנוהלת, את ההגדרות הקשיחות כך שלזמן מסוים - הוא יקבל יותר CPU מה VMs האחרים הרצים על אותה המכונה. טכנולוגית ה VM עדיין מקצה משאבים בצורה קשיחה - אך תכנון דינמי יכול להגביר יעילות השימוש בהם. זה יכול לעבוד רק כאשר כל ה VMs על המכונה שייכים לאותו הארגון / יש ביניהם הסכמה.
T3/T2 instances ב EC2 הם VMs שעובדים על עיקרון דומה: ב״חוזה״ שלנו רשום שה instance יכול לעבוד ב burst ולקבל יותר משאבים - אך עדיין יש פה עבודה לפי חוזה, ולא אופטימיזציה גלובלית של המשאבים על המכונה (מכיוון שה VMs שייכים לארגונים שונים).  



2019-02-03

קפיצה למים עם Docker [למביני-ענין]

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

מה אני הולך לעשות אחרת?
  • אני לא הולך להתחיל מ"היסודות" (גישה סיסטמטית, אך לא תמיד הכי מעניינת) - אלא מצורך ממשי, ולהתקדם לפיו.
  • אני לא הולך לעשות מדריך סופר-נקי. יהיו לנו תקלות, דברים לא יעבדו. נחשוב למה (נוסיף מידע) - ונפתור אותם. תקלות וכישלונות הוא דבר שקל יותר לזכור (יש פה סיפור...), והוא מאוד טיפוסי במהלך העבודה עם infrastructure.


הפוסט מניח שאתם יודעים קצת על לינוקס ובכלל, ועל Docker מספיק להכיר ש:
  • Container הוא ״כמו lightweight VM״ (הגדרה דיי נכונה, אך ממש לא מדויקת). הקונטיינר צורך הרבה פחות משאבים ומציב תקורת פעולה נמוכה מ VM - אבל גם מספק רמת הפרדה נמוכה יותר = פחות אבטחה, פחות הגנה על המערכת מהתרסקויות.
    • כל עוד המחשב מריץ רק תהליכים שאתם סומכים עליהם + אתם ערוכים לספוק קריסה של מכונה בודדת (לא יקרה המון, יקרה מעט יותר) - אז הרצת קונטיינרים היא דבר הגיוני.
    • מכיוון שה Container הוא לא ״מכונה״ אלא תהליך שרץ תחת הגבלות / בצורה חצי-מבודדת, הוא עדיין עשוי להיות מושפע מתהליכים אחרים / Containers אחרים הרצים על אותה המכונה ו/או הגדרות משותפות של מערכת ההפעלה.
  • Docker הוא דבר מגניב. חבל להאריך במלים.
  • אתם יודעים את ההבדל בין Container (מופע ההרצה) ל Image (התוכן שעל בסיסו רץ ה container).
    • אולי שמעתם משהו על Layers... ועל Dockerfile.... זה מספיק.


אז יאללה, הנה ה Use-case הבסיסי ומעשי שלנו:

אני רוצה לנסות איזו ספריה / כלי בגרסה חדשה יותר ממה שיש לי, מבלי להתקין על המחשב המקומי (גם לחסוך התקנה מורכבת יותר, וגם להימנע מ״לכלוך״ שיישאר אח״כ.).

אם זו ספריה נפוצה, בטח נמצא לה docker image מוכן. במקרה שלי, אני רוצה להתקין את MySQL 8 על המחשב ולנסות אותו.

אני מבטיח שלא הכל ילך חלק... ויהיו לנו כמה תקלות להתמודד איתן - וללמוד מהן, ממש כמו שקורה במציאות.



הנה מתחילים


אני מניח שאתם רצים על מק או Windows - ויש לכם את חבילת ה Docker Desktop (לשעבר/שיפור של Docker Toolbox) מותקנת. 

Docker מתבסס על יכולות קרנל של לינוקס, ופעם להתקין אותו על מק היה קצת מסובך. היום חבילת ה Docker Desktop מתקינה בקלות את כל, (או כמעט כל) הכלים של Docker:
Docker CLI, Docker Daemon, Docker Machine, Kitematic (docker GUI), MiniKube, ועוד...

התמנון הוא compose, רובוט עם אקווריום הוא docker-machine, הדגים שנושאים קונטיינר הם docker swarm (אבל מאז הלוגו הפך לחבורת לווייתנים הנושאים את הקונטיינר). לא הצלחתי לזהות את הבחור האחרון בתיבה.

וודאו שכאשר אתם מקלידים ב console את הפקודה docker --version אכן מוצגת מספר גרסה (18 בעת כתיבת הפוסט).

בכדי להתחיל את התסריט שדיברנו עליו, אלך ל DockerHub ואחפש אחר ״MySQL״. 
DockerHub הוא רפוזיטורי הציבורי הגדול לשיתוף של Docker Images.

הנה תוצאות החיפוש:



ישנם שלושה סימונים ששווים התייחסות קצרה:
  1. Official Image - מכיוון שההצלחה של Docker מבוססת על מגוון של Images איכותיים שזמינים, החברה שמאחורי Docker בחרה לתת חסות לחלק מה images הפופולארים ב dockerHub, ואלו מסומנים כ "Official images".
    1. המשמעות היא שצוות של החברה עושה Review ל Dockerfiles ותוכן ה images, מוודא שיהיו עדכונים תכופים ל image, ומבצע סריקות אבטחה ל images הללו (ניתן לראות את התוצאות ב tab ה TAGS של ה image הספציפי.
    2. אחת מסכנות האבטחה הקשורות ל Docker הוא מנגנון השכבות, שעושה caching לשכבות ועלול לעצור אותנו מלקבל עדכוני-אבטחה חשובים לאורך זמן. הפתרון לסיכון הזה הוא לרענן את ה images שלנו, גם בשכבות הנמוכות - מדי פעם.
    3. אם אתם מתכננים להשתמש ב Image בפרודקשיין ויש Official image שמתאים לכם - מומלץ מאוד לבחור בו, או ב image המבוסס עליו, שלא נראה שמוסיף סיכוני אבטחה.
  2. Verified Publisher - למרות תווית הזהב, שעשויה להראות יוקרתית יותר, מדובר בסה"כ ב image שהועלה מחשבון שאומת כשייך לחברה שטוענת שהוא ברשותה. בדף ה image יהיה קישור לפרופיל החברה שיעזרו למשתמש לוודא במי מדובר. זה חשוב בכדי לא להוריד malicious images, אבל זה לא אומר שום-דבר על איכות התוכן עצמו. להזכיר: גם חברות מוכרות עלולות להוציא תוצרים מביכים.
  3. תווית ה "Docker Certified" (שאיננה נגזרת מתווית ה Verified Publisher) אומרת ש:
    1. ה Image המדובר מבוסס על Official Image.
    2. ה Image עבר כלי של Docker לבדיקת כמה היבטים של אבטחה ופעולה בסיסית.
    3. למרות שזה לא טוב כמו Official Image שעובר review ידני, זה עדיין סימן טוב שכדאי להתייחס אליו בחיוב - במיוחד עם השינויים מה Official image מובנים לכם.

ניכנס להפצה הרשמית של MySQL:



כיאה ל Official Image, יש די מידע בדף ה image:
  1. הנה שורת הפקודה על מנת להוריד את ה image מקומית למחשב. פשוט!
  2. הנה גרסאות עיקריות של ה Image, וה Dockerfiles שאיתן נבנו. ה Dockerfile הוא מעניין מאוד ומלמד אותנו מה יש ב Image.
    1. בהמשך נכתוב גם Dockerfiles בעצמנו - ולכן השפה הזו תהיה ברורה וטבעית לנו.
  3. אני יכול לקרוא ביקורות על ה Image - לוודא שהוא לא #פח. לשמחתי: הביקורות מצוינות!
  4. אני יכול לראות את ה Tags השונים. Tags, בניגוד למה שניתן לחשוב ע״פ השם, הן לא מילות מפתח - אלא גרסאות שונות של ה image. היה נכון יותר לקרוא ל Tags בשם "Versions".


ה Dockerfile



לפני שאני מתקין מקומית את ה MySQL 8 image, בעזרת הפקודה docker pull mysql - אני רוצה להתעכב מעט ולצלול לשנייה ל Dockerfile של MySQL 8 (הנה התיעוד הרשמי והמוצלח של ה Dockerfile), וננסה לקלוט כמה תובנות חשובות.

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

הנה תחילת הקובץ:


  1. פקודת ה FROM מתחילה build חדש ומציינת על איזה image בסיס אנחנו מתבססים: זה יכול להיות image של "מערכת הפעלה" או image שבניתם ואתם רוצים להרחיב.
    1. אם יש בקובץ כמה פקודות FROM - כל אחת תייצר image אחר. השימוש העיקרי לכך הוא multi-state build (נושא מתקדם).
    2. כדי שה image יהיה קטן ככל האפשר (פחות זמן/נפח תעבורה בהורדה, לפעמים גם פחות צריכת זיכרון בהרצה) - משתמשים לרוב בגרסאות מצומצמות של הפצות לינוקס.
      1. חשוב להתרגל: בהפצה מינימלית לא מותקן כמעט שום דבר. כאשר נרצה לעבוד ב shell של container שמריץ את ה image - לרוב נצטרך להתקין את ה "utilities" שאנו רגילים להתייחס לקיומם כמובן מאליו.
    3. השימוש ב capital letters לכתיבת פקודות ה Dockerfile (כמו FROM) איננה חובה, אבל היא קונבנציה שימושית - כמו בשפת SQL.
    4. הפצה נפוצה במיוחד של לינוקס לשימוש ב Docker היא Alpine, אשר קטנה מ 5MB (קטנה פי 20 מהפצת אובונטו סטנדרטית), נחשבת מאובטחת היטב ואמינה. הנה סיקור קצר ומעניין שלה.
  2. פקודת RUN היא פקודת-מפתח, המבצעת שינוי ב Image הבסיס, ויוצרת עליו Layer חדש.
    1. כל פקודת RUN מייצרת Layer, ולכן, על מנת לחסוך ב Layers -נוהגים לשרשר פקודות כאשר יש להן משמעות דומה. זו הסיבה שיש כ"כ הרבה שרשורים (&&) על גבי פקודה בודדת.
    2. לסדר פקודות ה RUN בקובץ יש משמעות בסדר בניית ה layers. עוד פרטים - בהמשך.

והנה סוף הקובץ:


  1. דרך נוספת להוסיף layer ל image הוא פקודת ADD, או הגרסה הפשוטה והשימושית יותר שלה: פקודת COPY - המוסיפה קבצים ל image מתוך פעולות העתקה.
  2. לכל קובץ Dockerfile יש פקודת CMD יחידה, שהיא הפקודה-ברירת המחדל שתרוץ בעת הרצת ה container.
    1. אם הפקודה הזו מורכבת - משתמשים ב shellscript שהועתק לתוך ה image.
  3. [נושא מתקדם] כאשר מריצים את ה container - יש אפשרות לשלוח כפרמטר פעולה אחרת שתרוץ, במקום הפעולה שצוינה CMD. 
    1. אם זה לא מצב טיפוסי ל image (ניתן להפעיל פעולות גם על container לאחר שרץ) - אזי משתמשים בפקודת ENTRYPOINT לציין פקודת בסיס, כאשר ה command (אם הוגדר ע"י CMD או קלט חיצוני) - ישורשר אליה כפרמטר/פרמטרים.
    2. רק פקודת ה ENTRYPOINT האחרונה בקובץ - תופסת.
  4. לפקודות CMD ו ENTRYPOINT יש שתי צורות כתיבה עיקריות:
    1. shell form - שורת טקסט רגילה - תפעיל את ה shell בכדי לפענח אותו
      1. מאבדים את היכולת של הקונטיינר לקבל סיגנלים מהמערכת שמארחת אותו
      2. נחשב פורמט פחות אמין מבחינת אבטחה, כי אפשר לעשות כל מיני תרגילי shell תוקפניים
      3. פחות אמין מבחינת ביצוע, כי סיכוי טוב שכל מיני פקודות שאנו מנסים להשתמש בהן (למשל tr או xargs) - פשוט לא זמינות בהפצה המינימלית של הלינוקס שאנו משתמשים בה.
    2. execution form - קלט כמערך JSON: פקודה ואז פרמטרים - כמו שתי הדוגמאות בקובץ הנ"ל, כאשר המערך כולל רק איבר יחיד, ולכן אינו כולל פרמטרים.
      1. זהו הפורמט המומלץ והנפוץ לשימוש.
      2. אפשר בפרמטרים להתייחס למשתני סביבה של לינוקס. אין בעיה.
      3. חסרון נפוץ: לא ניתן לשרשר פקודות בעזרת && (פונקציה של ה shell).
  5. פקודת EXPOSE מציינת לאילו ports ה container יאזין. זה עדיין לא מספיק על מנת לקבל תקשורת, כי בנוסף יש להפעיל (לרוב: בעת הרצת הקונטיינר) פקודה בשם publish (בעזרת הארגומנט p-) שתחליט מהיכן אנו יכולים לקבל את התקשורת.
    1. בגדול אפשר לתאר 3 מצבים:
      1. ללא expose - הקונטיינר לא יוכל לקבל תקשורת. עדיין יש לכך שימושים.
      2. עם expose, אך ללא publish - הקונטיינר יוכל לקבל תקשורת רק מ containers אחרים.
      3. עם expose ועם publish - הקונטיינר יוכל לקבל תקשורת מטווח כתובות ה ip שהוגדר. 
    2. הפורטים אליהם מתייחסים הם מסוג tcp (ברירת המחדל) או udp, כאשר tcp הוא הבסיס גם ל HTTP, כלומר: על מנת לאפשר תקשורת HTTP די לחשוף port מסוג tcp.



Docker Image Layers


אוקי. בהנחה שהצלחנו לקלוט משהו מה Dockerfile, בואו נמשיך בתסריט המרכזי שלנו.
נוריד את ה image של MySQL, בעזרת פקודת docker pull (סט הפקודות מול ה docker registry דיי מזכיר את git):


אנחנו יכולים ממש לראות כיצד יורדים בנפרד/במקביל ה Layers השונים.

אם ל Docker יש כבר layers מסוימים ב "repository המקומי" - הוא לא יוריד אותם, וכך יחסוך זמן.


כדי להדגים את זה, הנה אני אוריד image נוסף של mysql, הפעם עם tag של "5.7.24" (כאשר לא מציינים תג, ברירת המחדל היא latest:):


כפי שאתם יכולים לראות - חסכנו הורדה של רוב ה layers (הכוללים הרבה MBs).

Docker מסוגל לעשות שימוש חוזר ב layers רק "מלמטה - למעלה". ה Layer הראשון שאנו נתקלים בו שאיננו כבר ב repository יגרום להורדה מחדש של כל ה layers מעליו. מסיבה זו לפעמים שווה לנסות ולסדר את פקודות ה RUN/COPY/ADD ב Dockerfile כך שהשוני בין images שונים יהיה מאוחר ככל האפשר (ולכן - קטן ככל האפשר, ורק ה layers החסרים יעודכנו).



מימוש ה Layers ב Docker נועד לא רק עבור הורדת images - אלא גם עבור זמן-הריצה.

בעזרת מנגנון הנקרא union mount (והמימוש שלו: aufs או overlayFS) יכול Docker לבנות את "מערכת הקבצים" הזמינה לכל container בעזרת הרכבת סדרה של mounts בזה על גבי זה.

לדוגמה (בהתבסס על התרשים הנ"ל): עבור "container 1", אנו עושים mount של ה image של Debian ואז mount ל layer שמוסיף את הקבצים של vim, ואז mount של layer המוסיף את הקבצים של nginx ואז layer אחרון (שהוא היחידי שאינו read-only) עבור כתיבות לדיסק שנעשות ע"י container 1 עצמו.

באופן זה אין צורך "להעתיק", אפילו מקומית, את הקבצים אותם דורש ה container. כל ה containers בתמונה הנ"ל באמת עושים שיתוף של עותק יחיד של Debian layer ושל ה vim layer - המגיעים ישירות מה "repository המקומי".

אם אחד מה containers מוחק את ה binaries של vim - זה קורה רק ב layer שלו (בה מותרת כתיבה), מבלי להשפיע על אף אחד מה containers האחרים.
הארכיטקטורה הזו, שאינה דורשת העתקות - תומכת היטב באתחול מהיר במיוחד של containers, ובצמצום משאבים (למשל caches ברמת ה kernel). זה אחד ה"שוסים" של Docker.

אם כבר הזכרנו את העובדה, שווה לציין שגם בבניית docker image, מחיקה של קבצים בעצם רק מוסיפה layer נוסף בו הקבצים לא נמצאים - ולכן לא מקטינה את גודל ה image הכולל. אם רוצים לחסוך בגודל, יש לבנות את ה Layer המדובר בלי אותם קבצים מלכתחילה.


נמשיך בתסריט שלנו. הפעולה האחרונה שלנו הייתה להוריד שני images (אסופות של layers) ל repository המקומי.

בואו נקרא לפקודת docker image list - בכדי לראות את המציאות הזו בשטח. הנה רשימת כל ה images ב "repository המקומי" (לא מונח רשמי, אך מונח שקל להבין):


  1. גודל ה image הוא פרטמר שיש לשים לב אליו, במיוחד כאשר אנחנו בונים images בעצמנו. נרצה לצמצם את הגודל ככל האפשר.
  2. ה image id הוא בעצם תחילית של ה hash (הייחודי) של ה image. 
    1. אפשר לזהות image ע"י name:tag - אבל לא תמיד הזיהוי יהיה "יציב" (למשל: mysql:latest נדרס ע"י עותק חדש יותר של latest)
    2. דרך זיהוי יותר יציבה היא בעזרת ה image ID החלקי (תחילית של 12 סימנים ראשונים ב hash) או ה image ID המלא (GUID באורך 256 ביט).
  3. כבר הזכרנו שה Tag הוא בעצם מספר גרסה. שימו לב ש `latest` הוא ערך ברירת המחדל של tags ב Docker - ואין שום מחויבות שזו באמת הגרסה האחרונה שזמינה.
    1. עבור image מתוחזק-היטב כמו MySQL - זו כנראה באמת הגרסה האחרונה (בעת ההורדה) - אם כי אין מנגנונים של Docker שעוזרים לתחזק זאת (זו עבודה "ידנית" של מי שמנהל את ה images).
    2. מסיבה זו - ההמלצה המקובלת היא להימנע משימוש ב tag בשם latest ו/או להימנע מאי-ציון tag ל images שאנחנו בונים - אלא אם אתם מנהלים בדייקנות ש latest תמיד תהיה הגרסה האחרונה. ייתכן ובעתיד ה ecosystem של docker יספק פתרון אמין לניהול גרסאות - אבל בינתיים זה בידכם.


אני רוצה לצלול לרגע, ולהראות את הקשר (הישיר) בין ה Dockerfile של Mysql8 שראינו למעלה, וה image שירד אלינו למחשב. נעשה זאת בעזרת הפקודה docker history המציגה את ההיסטוריה של image נתון:


מכיוון שלא ציינתי tag, אני מקבל את ה latest - במקרה שלנו: MySQL 8.
  1. אם נשווה את ה layers קל מאוד למצוא את ההתאמה ל Dockerfile של MySQL8 שראינו למעלה. הקדישו דקה ונסו!
    כפי שאמרנו, כל פעולת RUN / ADD / COPY מתרגמת ל Layer פיסי חדש.
    כל פעולה אחרת יוצרת מה שנקרא temporary intermediate image (שלב טכני בזמן היצירה) - וההשפעה "תמוזג" לתוך ה Layer הפיסי הבא.
  2. אני יכול לזהות intermediate layers ע"פ כך שיש להם גודל של 0B.
  3. Docker מפעיל shell בבניית ה image על כל פעולת RUN, וכל פעולה אחרת (כולל COPY/ADD - המבוצעות ע"י Docker עצמו) - מסומנות כ (nop)# - קיצור של no operation.
  4. אם בעבר בעמודת ה IMAGE היה מופיע ה id של ה layer (מלבד intermediate layers שאין להן id), מסיבות הקשורות לאבטחה החליטו להוריד את העמודה הזו - ולכתוב שם <missing> - תכתובת מיותרת ומבלבלת. היה עדיף להסיר את העמודה וזהו.


בואו נריץ את ה Container


עכשיו שיש לנו את ה Image, אנו יכולים להריץ אותו - בדמות container (שהוא ה"מופע"). הרצה של container היא דיי פשוטה:


אופס!
מה קרה כאן?

השגיאה הזו היא לא שגיאה של Docker, אלא של ה image שאנחנו מריצים. הוא מצפה ל Environment Variable מסוים. מקרה נפוץ. כשאתם כותבים Dockerfile בעצמכם - נסו להקפיד ולספק הודעות שגיאה ברורות, אם חסר משתנה סביבה, למשל.


הפרמטר e- מאפשר לי לקבע ב container את ה env. variable שאני רוצה. ניתן לשרשר כמה env. variables שאני רוצה. שימו לב שעל שם ה image תמיד להופיע כפרמטר האחרון.

אפשר לראות שהפעם השרת מתחיל לרוץ... יוהו! הנה הפוסט אוטוטו נגמר.

בואו נאמת שאנו רואים שהקונטיינר רץ. רק עוד שניה אחת...
שניה, יש לי בעיה! הרצת השרת "תפסה" לי את ה console. אני מנסה להקיש על Z^ - ללא הצלחה. גם C^ לא עוזר.

יש שרתים שלא מאזינים ל C^ (סיגנל INT) אבל מאזינים ל \^ (סיגנל QUIT) - אני יכול להרוג כך את הקונטיינר, אבל יש צורה יותר ״מנוהלת״ לעשות זאת.


כמו שחלק מסט הפקודות של ה Docker API מזכיר את git, חלק אחר שלו - מזכיר ניהול תהליכים בלינוקס. אני פותח חלון נוסף של Terminal (הנוכחי "תפוס") ומתחיל להקליד:

(לחצו להגדלה)
  1. הפקודה docker ps דומה לפקודה ps - ומציגה את רשימת ה containers הרצים.
    1. אני יכול לראות את ה container id (גם הוא hash באורך 256 בתים, כאן אנו רואים רק את התחילית)
    2. אני רואה כמה זמן ה container חי, וכמה זמן הוא רץ (לא תמיד זה אותו הדבר).
    3. לכל container שלא נתתי לו שם, Docker בוחר שם מחיבור אקראי של שתי מילים, לפעמים זה יוצא מצחיק. אם הייתי מריץ הרבה containers מאותו סוג על אותה המכונה - כנראה היה לי חשוב לתת שם בעצמי - אבל בינתיים ה container id מספיק לי.
    4. אם אני רוצה לראות את הפקודה המלאה שהורצה (במקרה זה: docker-entrypoint.sh mysqld - כצפוי מה Dockerfile, אם תחשבו מעט), או את ה id המלא של ה container אני יכול להשתמש בפרמטר no-trunc-- שיציג את כל הנתונים ולא "יחתוך" אותם.
  2. פקודת docker stop היא השקולה ל kill. אני מספק את ה container id בכדי להצביע את מי יש לסגור.
  3. עכשיו אני רואה שבאמת ה container נסגר ואיננו רץ עוד. אני יכול לקרוא ל docker ps -a ולראות גם containers שכבר נסגרו.
    1. כברירת מחדל, Docker משאיר את ה mount (כלומר: ה mount הגבוה ביותר ב union mount, המוקצה ל container עבור כתיבה). פקודת docker stop עוצרת את ה container - אבל לא מוחקת את הנתונים שלו - וזה פרט חשוב. 
      1. זה אומר שאוכל לחזור ולהריץ את ה container הזה, מאותו state אחרון שהיה בעת הסגירה (נעשה זאת בהמשך הפוסט).
      2. זה אומר שתוך כדי עבודה, מצטברים לי הרבה "states" של containers סגורים חסרי שימוש - וצריך מדי פעם לנקות אותם.
    2. אפשר להשתמש ב docker ps -a -f status=exited על מנת להציג רק את הקונטיינרים שכבר לא רצים (אך השאירו שאריות, אחרת הם לא היו מופיעים בפקודה docker ps).
    3. בקיצור: הפקודה  (docker rm $(docker ps -a -q -f status=exited - תנקה את כל השאריות שנותרו. זהירות לא למחוק יותר מדי.



ניסיון שני


הפעם נשתמש בפרמטר d- על מנת להריץ את הקונטיינר ברקע, כמו שמתאים לתהליך שהוא "שרת" (שזמן הריצה שלו - מתמשך). הנה אנחנו הולכים ומתמקצעים:


  1. אני משתמש בפרמטר d- (קיצור של detached) על מנת להריץ את ה container ברקע. הפעם השתמשתי ב image id ולא ב name:tag - שתי הדרכים אפשריות.
    1. כפלט לפעולה קיבלנו את ה container id (זהו ה hash הארוך). זה הנוהג ברוב הפקודות - וזו התנהגות שימושית למדי עבור כתיבת shell scripts.
  2. איך אני יכול לראות מה קורה עם ה container? בעזרת פקודת ה docker logs (ברבים)
  3. אני מפעיל את הפקודה כמה שניות מאוחר יותר - ורואה שהלוג אכן מתקדם (חתכתי את הטקסט בתמונה, כי הוא כבר נעשה ארוך).

יופי! הדברים באמת הולכים נהדר... נראה לי...


התחברות לשרת:

נתחיל לעבוד עם השרת. מכיוון שמדובר ב MySQL ואני על מק, רק טבעי שאשתמש בכלי בשם SequelPro [ג].


אני לוחץ על כפתור ה connect.... ו:


זה לא בסדר. מה ACCESS DENIED?

אני בודק שהקונטיינר רץ - אכן רץ.
אני בודק הלוגים (פקודת docker logs) - ואין שום דבר. שום זכר לתקלה.
אני מנסה ללחוץ עוד כמה פעמים על כפתור ה "Connect", אולי המחשב התבלבל - אבל לא. התקלה חוזרת על עצמה בעקביות.

למה זה קורה לי?

ספציפית יש פה מקרה קצת מבלבל: אני מריץ מקומית שרת MySQL 5.7, על הפורט ה default ועם משתמש בשם root - אבל לא ססמה מגוחכת כמו "123". בעצם ניסיתי להתחבר לשרת הזה - ונדחיתי כי הססמה הלא נכונה.
אם לא היה לי את השרת MySQL המקומי מותקן, הייתי מקבל הודעה קצת יותר אינפורמטיבית בנוסח "Server not found".

אנחנו יודעים ש Docker לא מריץ באמת VM אלא תהליך מקומי עם מגבלות / אמצעי בידוד מוגברים.
מה באמת יקרה אם אני מריץ גם MySQL 5.7 וגם כ Container שרת MySQL 8 על אותה המכונה? לאיזה שרת אני באמת אתחבר?

מה אתם חושבים?


כשאנחנו מבולבלים, זה זמן טוב ללכת ולהבין טיפה תאוריה.

אתם בוודאי זוכרים ש Docker היא טכנולוגיה "לינוקסית" המתבססת על טכנולוגיות ליבה של לינוקס (LXC, namespaces, cgroups וכו' [ב]). Docker לא באמת יכולה לרוץ ישירות על המק שלי, כי MacOS הוא וריאציה של Unix, לא Linux ואין לו את יכולות הליבה הנדרשות.

בכדי לרוץ על המק שלי, חבילת ה Docker Desktop for Mac כוללת VM המריץ לינוקס ועליו רץ Docker:


בעבר (כאשר היה צורך ב Docker Machine) היה עלי לכוון ל ip address של ה VM על מנת להתחבר ל Containers שאני מריץ.

היום ב Docker Desktop for Mac יש מיפוי אוטומטי של ports של ה containers ל localhost של ה host OS. הבעיה היחידה שיש לי היא ש port מספר 3306 כבר תפוס ע"י MySQL 5.7, ולכן המיפוי האוטומטי לא עובד.

האם עשינו תרגיל מוזר בכדי להריץ את Docker על המק? משהו חריג ששונה מאיך שנרוץ ב production? - לא ממש.

מכיוון שמטרת הפוסט היא להכיר Docker, ולא רק להריץ MySQL 8 מקומית, שווה להתייחס שניה לארכיטקטורה של Docker:
צהוב = תהליך, לבן = נתונים

הארכיטקטורה של Docker מתבסס/ת על 3 hosts:
  1. ה Docker Client - ממנו שולטים על הנעשה.
  2. ה Docker Host - המאחסן מקומית את ה images (להלן "local repository" - לא שם רשמי), ומריץ את ה containers. מכונת לינוקס.
  3. ה Docker Registry - המאחסן ומשתף docker images בין docker hosts שונים.

במקרה שלנו:
  • Docker Client - ה host הוא המק שלי. משם אני מריץ את ה Docker CLI שהוא משמש כ client.
  • Docker Host - ה host הוא ה VM של לינוקס שרץ על המק. זה בעצם מיפוי די אמיתי לאיך שהדברים ירוצו גם בפרודקשיין.
  • ה Docker Registry - הוא Docker Hub ה"ציבורי". 
    • כפי שניתן לראות בתרשים, הוא אינו חלק מתסריט של docker run - ולכן לא רלוונטי כרגע.

לסכם:

  • כשנרוץ ב production יהיה עלינו לעשות publish ל ports של ה container - על מנת לאפשר גישה מבחוץ. אין מיפוי פורטים אוטומטי.
    • הזכרנו את זה בתיאור של פקודת ה EXPOSE ב Dockerfile. חזרו והיזכרו.
  • מכיוון שה port כבר תפוס ב localhost - עלי לעשות מיפוי לפורט שונה ולחשוף אותו (גם נעשה בפקודת publish).
בקיצור: בכל מקרה נצטרך לעשות port publishing. הגיע הזמן.


בואו נעשה את זה!



  1. אני מבצע publish, כאשר ה port הראשון שמופיע הוא זה של ה HostOS והשני הוא זה של ה container.
    1. שווה לשים לב שפקודת P- (אות גדולה) עושה publish לכל ה exposed ports (ולאותו מספר port על ה HostOS).
  2. אני יכול לראות את המיפוי מכל כתובת IP (סימון: 0.0.0.0) בפורט 53306 לפורט 3306 של הקונטיינר.
    1. MySQL 8 חושף (expose) גם את פורט 33060 עבור עבודה ב X-Protocol המאפשר API דומה ל MongoDB. אין לנו כוונה להשתמש בו, ולכן אנו לא מפרסמים (publish) אותו.


זהו. אחרי כ"כ הרבה עבודה - מגיע לנו להתחבר כבר לקונטיינר שלנו.
נשנה את הפורט ב SequelPro ל 55306, ונלחץ שוב על Connect על מנת להתחבר:


אז זהו. אפשר להכריז שהחיים אינם הוגנים! עבדנו קשה - ועדיין לא סיימנו.

יש לנו משוכה נוספת לעבור. לפחות הפעם השגיאה השתנתה - כלומר: התקדמנו.



אל ייאוש - עוד צעד אחד ומסיימים!


הבטחתי תסריט "עולם אמיתי" עם כמה תקלות - ואני מקווה שאני מקיים.
המטרה: ללמוד על הדרך עבודה עם Docker.

אני מקווה שלא איבדתי 94% מהקוראים עד הנקודה הזו 😄

אני מחפש את הודעת השגיאה האחרונה ומוצאה פוסט ב Stackoverflow שמסביר את הבעיה וכמה דרכי פתרון. בגדול שיטת ה Authentication ברירת-המחדל של MySQL השתנתה בגרסה 8 (מה שקובע בהתקנה חדשה), והקליינט שלי SequelPro - לא תומך בשיטה הזו.



הפתרון שאני בוחר בו, הוא להפעיל את ה MySQL console ולשנות את שיטת ה Authentication למשתמש שלי. פעולה דיי פשוטה - אם היה מדובר בתהליך מקומי.

כאשר אתם עובדים עם Docker, תגיעו במוקדם או במאוחר לרגע שבו אתם צריכים לעשות "SSH לקונטיינר" ולשנות משהו. זה בלתי נמנע.

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


  1. אני מוצא את ה container id של הקונטיינר שרץ.
  2. הפקודה docker exec מריצה פקודה נתונה בתוך ה container. במקרה הזה, אני רוצה להריץ bash ולעבוד כפי שאני רגיל - אבל ב console של ה container.
    1. הפרמטר t- (קיצור של tty שהוא קיצור של יוניקס ל Terminal; אל תלמדו מיוניקס איך לבחור קיצורים!!)  אומר שאנו רוצים לעבוד עם פרוטוקול של טרמינל, שמבוסס input/output טקסטואלי, אבל מממש עוד כמה פקודות i/o נוספות (ioctls, למשל הסיגנלים).
      1. למען הדיוק: ה console הוא מימוש של פרוטוקול ה terminal, ו shell הוא המפרשן של ה console לפקודות. פעמים רבות אנו משתמשים ב terminal/shell/console כשמות נרדפים לאותו הדבר - אבל זה לא מדויק.
    2. הפרמטר i- (בקיצור interactive) שומר את ה STDIN שלנו פתוח לאורך כל זמן הריצה.
    3. נקצר ונאמר שברגע שאנו רוצים לעבוד עם console (כמו bash), יש להשתמש בצמד הפרמטרים it- אחרת ייתכן וניתקל בהתנהגות בלתי צפויה.
  3. הנה נכנסתי ל console ואני יכול לעבוד. אני רוצה לקרוא לפקודה ll ולראות את רשימת הקבצים, אבל ברור לי שהיא לא תהיה זמינה בהפצה מינימלית של לינוקס - ולכן אני קורא ל ls -lah הבסיסית יותר (והשקולה). עובד.


עכשיו שאני ב console, אני יכול להחיל את הפתרון שמצאתי ב Stackoverflow:


exit ראשון יוצא מה MySQL console, ו exit שני יוצא מה bash - ומחזיר אותי ל Host OS console.


אני מנסה, ומצליח הפעם להתחבר ל MySQL 8 שרץ על הקונטיינר.

אני יכול להתחיל ולהתנסות בפיצ'רים של MySQL 8! איזה כיף!


הפעולה שביצעתי בכדי לאפשר את החיבור היא אמנם פשוטה, אבל עדיין נדרשת.

כפי שדיברנו, ה state של ה container נשמר גם אחרי שהוא נסגר.
אני יכול לסגור את ה MySQL8, ואז להתחיל אותו מחדש - כאשר ה"תיקון" כבר מיושם בקונטיינר: בעזרת docker ps -a אני אמצא containers שנסגרו, ובעזרת docker start - אני יכול להריץ container הספציפי מהנקודה שבה נעצר.




אחרית דבר


נניח שאני רוצה להריץ כמה containers של MySQL8, עם נתונים שונים וכו'. האם אצטרך כל פעם לבצע את "התיקון" בצורה ידנית?

לא כ"כ מתחשק לי. אני רוצה לשמור את השינוי שביצעתי.

זוכרים שהוספנו Layers ל Image ב Dockerfile בעזרת פקודות RUN/ADD/COPY?

אז יש עוד דרך לייצר Layer בצורה דינאמית יותר. אני יכול לקחת את ה layer/mount העליון ביותר, זה של הקונטיינר - וליצור ממנו Layer חדש (ומכאן: image חדש). לפקודה הזו קוראים docker commit - והנה אני משתמש בה על מנת לשמור את השינויים שעשיתי בהגדרות ה authentication של משתמש ה root ב container שלי....


מלבד החיסרון הברור של פקודת docker commit שאין תיעוד (כמו ב Dockerfile) של מה באמת נעשה בה, ואין יכולת לחזור ולשנות נקודתית חלק ממה שהיא עושה - זה עדיין כלי שימושי.

האמת שלמקרה שלנו, הדרך הנכונה ביותר היא לבנות dockerfile שמוסיף את השינוי הקטן. בסה"כ אנו מריצים (RUN) עוד פקודה - ואני משוכנע שאתם כבר יכולים לעשות זאת לבד.

בשל ההתמכרות הקטנה שלי בפוסט לרוח הטלנובלה הטורקית (ארוכה ומלאת אסונות) - אני דווקא רוצה להשתמש ב docker commit על מנת לעשות את השינוי הנ"ל.

אני יכול לעשות commit, ליצור image חדש, ובאמת לראות שיש layer חדש שמוסיף מעט גודל - ולהריץ אותו. אבל אבוי: כשאני מריץ את ה image החדש - יש לי עדיין בעיית authentication בחיבור.

לא שגיתי בצעדים. הכל נכון, לכאורה.

הבעיה (והפתרון) טמונים בעצם בשורה הזו ב Dockerfile שלנו:


למה?
מה השורה הזו בעצם עושה?

נראה... זה נושא לפוסט המשך (אם יהיו לי הכוחות. בלי נדר!)

שיהיה בהצלחה!


----

[א] ביחד עם Serverless - נושא שכתבתי עליו עוד לפני שהציף את הפיד מכל עבר

[ב] Docker עצמה היא טכנולוגיה שמשתנה תדיר - וזו אחת הביקורות נגדה. מי שעבר עם Docker ב 2015 ולמד את ה Internals, בוודאי יופתע שהרבה מאוד השתנה בכמה השנים שחלפו.

פקודות השתנו ונוספו, אני זוכר שרק לפני שנתיים - שנתיים וחצי עבדתי עם Docker על מק והיה Docker Machine שמריץ boot2docker. זה כבר לא נכון, וזה משנה את אופן החיבור ל container, וכמה פקודות שהכרתי - וכבר חסרות שימוש (לפחות בתצורה הזו).

מנגנונים וטכנולוגיות התחלפו: aufs הוחלפה ב overlayFS, אפילו מנגנון הליבה LXC (להלן: Linux Containers שהחל את כל המהומה) הוחלף במימוש בשם libcontainer, כעת הוא ככל הנראה רצים בכלל על containerd ו runc.

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

אני רוצה להאמין שהבגרות של Docker אכן מתרחשת, וניתן לצפות לפחות שינויים ופחות דרמטיים בשנתיים שלפנינו - מאשר בשנתיים שעברו.

[ג] בעת כתיבת הפוסט, הגרסה האחרונה של SequelPro אינה תומכת ב MySQL 8. כמה מביך!
אני משתמש ב nightly build כבר כמה חודשים - והוא יציב למדי.