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 כבר כמה חודשים - והוא יציב למדי.