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


2019-01-05

דרכים להתמודד עם חוב טכני (Technical Debt)

אני מניח שכולכם מכירים את המונח "חוב טכני" (Technical Debt), מונח שקבע Ward Cunningham בכדי לעזור ולחשוב על פשרות טכניות במערכת - שיש להן השפעות ארוכות טווח.

משמעות "חוב טכני" היא שפיתחנו פיצ'ר מסוים, וידענו שהפתרון הנכון ארכיטקטונית הוא פתרון X.
משיקולי זמנים (time-to-market, בעיקר), בחרנו ליישם פתרון נחות-ארכיטקטונית שנקרא לו x`.

את הפער בין הפתרון הרצוי ארכיטקטונית X לפתרון המצוי x` מתארים כחוב שיש להחזיר "למערכת". כמו חוב לבנק - הוא צובר "ריבית" בדמות פיתוח יקר יותר של הפיצ'רים הבאים, שייקח יותר זמן לפתח - בגלל קיצור הדרך שלקחנו.

למשל: בפיצר מסוים, במקום ליצור טבלה חדשה בבסיס הנתונים המתארת בצורה מדויקת את אופי האובייקט o, החלטנו "להלביש" את קיום האובייקט o על טבלה קיימת. שחררו את הפיצ'ר מהר יותר - אבל יצרנו עיוות שעכשיו בפיצ'רים חדשים נצטרך לעשות עבודה נוספת / לפתור חוסרי-התאמה חדשים בתוך המערכת, כי לא עשינו את ה"פתרון הנכון".

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

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







קצת מעבר להגדרה היבשה


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

  • לא תמיד אנחנו צודקים בהערכה שארכיטקטורה X עדיפה על ארכיטקטורה x`. 
    • לעתים אנו מבצעים הערכות-יתר לחשיבות של אופן מימוש מסוים.
    • לפעמים הנסיבות משתנות באופן בלתי-צפוי: דרישה או צורך באובייקט מרכזי במערכת מתבטל או משתנה, ופתאום x` הופכת לארכיטקטורה עדיפה על X. זה לא קורה כל יום, אבל זה קורה. 
  • גם כאשר ארכיטקטורה X עדיפה - לא תמיד היתרון שלה הוא משמעותי או מוצדק.
    • יכול להיות ש"לתקן" את החוב הטכני יעלה יותר מכל מחיר שנשלם אי פעם בחיי המערכת, בשל העיוותים שיצר. דוגמה קלאסית: שמות של עמודת בבסיס הנתונים: השם הלא-נכון מציק, אבל שינוי עשוי להיות יקר מאוד לביצוע.
  • הצהרה על חוב טכני הוא לעתים "קלף מיקוח" בניסיון להשפיע על אחרים בכדי להסכים דרך פעולה שאני רוצה לקדם. מתוך רצון להשפיע - אני עלול להגזים (ויותר) בתיאור החוב הטכני.
  • "חוב טכני" הוא טיעון של אנשי-טכנולוגיה, לא אנשי ביזנס 
    • אחד מסוגי החובות הטכניים הנפוצים והמזיקים ביותר הוא הנדסת-יתר (over-engineering). הרבה יותר קשה להתקדם ולפתח פ'צרים עם 6 שכבות הפשטה - אם נדרשות רק 2.
      • במקרים של הנדסת-יתר לרוב לא נשמע את הקול שאומר "זהו חוב טכני. אנחנו חייבים להחזיר אותו (להסיר רמות הפשטה) בכדי להתקדם יותר מהר", אבל הרבה פעמים - זה בדיוק המצב.
    • וריאציה אחרת היא סיבוך המערכת ויצירת חוב-טכני, דווקא תחת ה ticket של "החזרת חוב טכני".
      לא תמיד מתן אשראי למי שמוכן "להחזיר חוב טכני" - היא פעולה נכונה. עולם התוכנה הוא אכן מורכב: על מנת לפעול נכון חשוב לצלול ולהבין את הדברים.
  • למרות ערימת ההסתייגויות למעלה, חוב טכני עמוק הוא דבר הרסני למוצר ולחברה
    • חובות טכניים עמוקים, בליבת המוצר והמודל שלו - עשויים לעשות את ההבדל בין מוצר מצליח לכושל.


חוב טכני - אבוי! מקור: וויקיפדיה


חובות טכניים, הם לא אחידים, ולא כדאי להתייחס אליהם ככאלו. אם ניזכר לרגע בכמה חובות טכניים מפורסמים:
  • באג 2000 (Y2K) - אבותינו חסכו בשטח האחסון ולכן מידלו שנה כ-2 ספרות ("95") ולא כ-4 ספרות ("1995"). מה קורה כאשר מגיעה שנת 2000? מיון? בדיקה איזה תאריך קדם לשני? - בלאגן.
    החוב הטכני קיבל פרופיל תקשורתי עולמי - והיה סיכון ברור ומידי. למרות נבואות-זעם על עולם לא-מתפקד, הוא תוקן בזמן (ובמחיר עצום), ועברנו לשנות ה-2000 בשלום.
  • Referer של HTTP - בתקן ה HTTP הוגדר header חשוב בשם Referrer. בתקן נפלה שגיאת כתיב (נכתב: "Referer") אך עד שגילו את הטעות כבר היו עשרות מפתחים ואולי מאות מפתחים כבר מימשו את שגיאת הכתיב. מלבד בדיחה על בקשה שהוגשה למילון אוקסופרד לתקן את הטעות במילון (שם יותר קל לתקן) - לא נעשה ניסיון אמיתי לתקן את שגיאת הכתיב. זו דוגמה טובה לחוב טכני שהוא טעות - אך לא משתלם לתקן. ככל שהזמן עובר - זה הופך למשתלם אפילו פחות.
    • דוגמה נוספת למשהו שאולי היה רצוי לתקן, אך בלתי-אפשרי בעליל הוא שיטת המדידה האימפריאלית הנהוגה בארה"ב. מדוע צריך לעבוד עם המרות לא נוחות כמו "מייל הוא 5280 רגל"? האם השיטה המטרית היא לא נוחה יותר? - כנראה שכן, אבל כבר מאוחר מדי.
  • להתחיל מערכת חדשה ללא בדיקות-יחידה / CI-CD אמיתי / תשתית לוגים סבירה - היא דוגמה לחוב טכני הרסני. אלו דברים שאם לא מתחילים איתם, הקושי להוסיף אותם מאוחר יותר הוא חסר תקדים. הייתי שותף להשקעה של שנות-אדם רבות בניסיון להחיל בדיקות יחידה, CI אמיתי, או תשתיות לוגים במספר מערכות שונות ש"גררו כמה שנים בלי". על אף מאמצים הרואיים, מגובי-הנהלה גבוהה - לא זכיתי לחזות במקרה שהייתי מגדיר כמוצלח.
    • רק להסיר ספק: קיום כל אחד מהשלושה הנ"ל הוא כלי productivity חשוב מאוד לארגון פיתוח / למערכת.


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



איך (לא) מחזירים חוב טכני?


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

זה לא מעניין מבחינתי.

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

הניסיון "להחזיר את החוב עד תומו" - הוא נאיבי וחסר-בסיס.

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

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


את החובות הטכניים המשמעותיים - לא יגלו לכם כלי בדיקה אוטומטיים (כגון Static Analysis tools): הם יגדלו את הקטנים עד הפצפונים - והמון מהם, אך גם לא את כולם.

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

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

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

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

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

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

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




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


איך (כן) מחזירים חוב טכני?


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

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

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


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

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

מציאת המנגנון הארגוני לטיפול בבעיה:

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



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



מציאת המנגנון הארגוני לצמצום חובות טכניים


תפיסה נפוצה (נתקלתי בה כבר מספר פעמים) היא שיש להקצות זמן נתון (הכלל המקובל: כ 20% בזמן העבודה) לצמצום חובות טכניים.


גישת הצוות:

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


הגישה הגלובאלית:

גם כאשר הזמן המוקצה לצמצום חובות טכניים הוא "גלובאלי" (משותף לכל ארגון הפיתוח) - חשוב מאוד כיצד הוא מתוקשר ומנוהל. 

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


יתירות בפיתוח:

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

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



לפעול במהירות ובנחישות:

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


צוות לצמצום חובות טכניים:

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


צמצום חוב טכני - כאירוע:

מודל אחר שיכול לעבוד הוא לרכז מאמץ של יחידה גדולה, או כל הפיתוח - על מנת לצמצם במרוכז חובות טכניים.
  • וריאציה אחת ששמעתי עליה, היא חברה שמקדישה 3 ימים כל תקופה מסוימת - על מנת לשפר ולהעשיר את המערכת בבדיקות אוטומטיות (בדיקות יחידה, בדיקות אינטגרציה, וכו' - בדיקות שמפתחים כותבים). ע"פ הסיפור - זה עבד דיי טוב.
  • וריאציה שניסינו לאחרונה בחברה הנוכחית שאני עובד בה (Next-Insurance) היא להפעיל את כל הפיתוח למשך שבועיים - על מנת להתמודד עם רשימת בעיות שהוכנה ותועדפה מראש.
    • הזמן הארוך יחסית - מאפשר לפתור בעיות שהן לא רק פצפונות / נקודתיות.
    • האמירה שיש השקעה משמעותית באיכות הטכנית של המערכת - משדרת מסר חשוב גם למתכנתים, וגם לשאר הארגון
    • אני אישית, מאוד נהנה מתקופות כאלו.

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



סיכום


שוב אמרתי לעצמי, שאבחר נושא קצרצר לפוסט, ואכתוב משהו באורך 4 tweets לכל היותר. לא הצלחתי - היה לי יותר מה לומר משחשבתי.

כמה דגשים שחשוב לי שלא תצאו מקריאת הפוסט בלעדיהם:
  • נקודת האופטימום הארגונית היא בהחלט לא "אפס חוב טכני", או לפחות לא בתפיסה המקובלת. "מקסימום הנדסה" - היא לא מקום טוב לביזנס או למערכת חיה להיות בו. חשוב לקחת כמה סיכונים, ולהתקדם בקצב טוב.
  • "חוב טכני" הוא מושג מאוד סובייקטיבי. בהגדרה מסוימת - רעיון החוב הטכני עשוי להתרגם לאיסוף עבודה חסרת חשיבות.
    • חשוב חשוב חשוב להתמקד בטיפול בחוב טכני שיש לו השפעה חיובית מורגשת (Impact).
    • לעתים יש חוב טכני (Design, שמות של משתנים) שעצם קיומו מציק לנו כמהנדסים, אבל אין לו חשיבות לפעילות המערכת. 
      • הייתי משקיע מעט עבודה (low hanging fruits) עבור ההרגשה הטובה, בנוסח "החלונות השבורים" (לשמר אווירה של אכפתיות במערכת)
      • הייתי מתאמץ לקבל גם אלמנטים לא-אלגנטיים במערכת, ואפילו צורמים - כל עוד העלות / תועלת לפתור אותם היא לא סבירה. למשל: עניין ה referer ב HTTP.
        • מערכות מתחדשות מטבען. אם תתעדו ("רשימה") ותשתפו ("שיקוף") בבעיה - יש סיכוי טוב שבשכתוב הבא המצב יהיה טוב יותר.
        • הכי מעצבן זה לראות קוד ששוכתב ושימר חובות טכניים מהותיים - מחוסר הבנה.
  • חוב טכני לא צריך להיפתר לחלוטין. הורדה של בעיה מרמת Critical לרמת Medium - עשויה להיות התקדמות חשובה ומספיקה. 
    • בפעם הבאה שנרצה לשפר משהו, כנראה שנבחר בעיה קריטית אחרת - על פני העלמה של בעיה בעלת חומרה בינונית.
  • למרות ש"חוב טכני" הוא עניין בד"כ עניין טכני וטכנולוגי - מציאת המנגנון לצמצום חובות טכניים הוא בעיקר ארגוני. עבדו עם ההנהלה ומי שיכול לקדם דברים בארגון - לא רק עם מקצועני התוכנה.

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