✕ סגור 
צור קשר
תודה על ההתעניינות .

Thank you! Your submission has been received!

Oops! Something went wrong while submitting the form

כיצד להיות CONTAINER RUNTIME – חלק א'

דוד יונגמן
|
קלה
|
May 29, 2018
alt="facebook"alt="linkedin"
להרשמה לניוזלטר

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

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

איך מייצרים קונטיינר

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

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

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

What do you mean ephemeral?
‍What do you mean ephemeral?

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

The filesystem

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

הרעיון של להגדיר לתהליך תיקייה חילופית בתור ה-Filesystem root הוא לא בדיוק חדש. היישום הנפוץ של הרעיון, chroot, הוצג כ-system call ביוניקס ב-1979. אבל למרות שאולי נראה ש- chroot מבצע את מה שאנחנו רוצים, בפועל האופן שבו chroot מיישם את שינוי תיקיית ה-root עבור היישום לא מתאים להפרדה של קונטיינרים. chroot תוכנן לשימוש כחלק מפעולות תחזוקת מערכת, לא כפונקציה בידוד מאובטחת. אמנם יש בלינוקס מסורת של הגדרת chroot עבור daemons כקונפיגורציה להרצת שירותים בצורה "מאובטחת", אבל יש לתצורה הזו משמעות רק כשמדובר בתהליכים שרצים כמשתמשים מוגבלים ולא בתור root, שיכול באמצעים שונים לחזור חזרה למערכת הקבצים הכללית. אפילו בלי לרוץ בתור root, מי שמכיר קצת את הנושא של כליאת תהליכים ב-chroot jail ואת הפריצות הנפוצות של הקשחות כאלו מבין לבד שזה לא באמת זה (מצלצל למישהו ה-jailbrake באייפון?).

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

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

אבל רגע, אנחנו לא יכולים לעשות umount  למערכת הקבצים שלנו, אנחנו צריכים אותה ב-mount כדי שלא לשבש את הפעילות של שאר המערכת, אז איך pivot_root יכול לעזור לנו?  

Enter Namespace

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

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

ה-namespace הראשון שהתווסף לקרנל של לינוקס הוא ה-mount namespace, וההפרדה שהוא מאפשר ליצור היא של ה-mount list. mount namespace מאפשר ליצור רשימת mount עצמאית, כך שמערכת הקבצים שרואה תהליך שרץ ב-namespace מסוים יכולה להיות שונה לחלוטין מזו שרואה תהליך שרץ מחוץ לו, או ב-Namespace אחר. לאחר יצירה של mount namespace אפשר להשתמש ב-pivot_root להגדרת מיקום חדש ל-/ ולעשות umount למערכת הקבצים המקורית, עם כל ה-mounts שעליה, מבלי לגרום לשינוי כלשהו למערכת הקבצים מחוץ ל-namespace. כך מערכת ההפעלה ממשיכה לעבוד כרגיל ואילו התהליך שב-namespace רואה את התיקייה שאליה נעשה ה-pivot_root בתור / ורשימת ה-Mounts שהוא רואה ריקה. מנקודת מבטו, זו מערכת הקבצים הקיימת בכללותה.

כאשר תהליך בלינוקס יוצר תהליך נוסף (child process) באמצעות fork, ה-namespace משותף לשני התהליכים – תהליך האב וה-child process. על מנת שה-namespace לא יהיה משותף (כלומר, לא יהיה shared) ביצירת התהליך מעבירים לקרנל syscall בשם unshare. ל-unshare syscall יש פקודת מעטפת ברמת משתמש, שנקראת גם היא unshare.

Do try this at home

נדגים את הרצת הפקודה ואת תכונות ה-mountspace על ידי הרצה של shell בתהליך חדש ב-mount namespace עצמאי:

http://bit.ly/ns_mount

הסבר:

הפקודה הראשונה הכניסה אותנו בתור root ל-shell חדש שרץ ב-Mount namespace.

בפקודה השנייה איתרנו את הדיסק שמוגדר לנו כ-/.

בפקודות בשורות 4-5 יצרנו תיקיה בשם /media/tempns ועשינו mount של אותו דיסק לתיקייה שיצרנו (בנוסף ל-mount ל-/).

בשורה 6 בדקנו את תוכן התיקייה, ואמנם התוכן זהה לתוכן של /.

בשורה 8 חיפשנו את הדיסק ב-mount list, ואנחנו רואים שהוא מופיע פעמיים, פעם ב-/ ופעם ב-/media/tempns.

לאחר מכן פתחנו טרמינל נוסף ווידאנו שהשינוי שעשינו ב-namespace לא השפיע על המערכת מחוץ ל-namespace, ואכן התיקייה מחוץ ל-namespace ריקה וה-Mount השני לא מופיע ב-mount list.

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

מייסד-שותף בחברת Wizards

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

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

איך מייצרים קונטיינר

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

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

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

What do you mean ephemeral?
‍What do you mean ephemeral?

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

The filesystem

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

הרעיון של להגדיר לתהליך תיקייה חילופית בתור ה-Filesystem root הוא לא בדיוק חדש. היישום הנפוץ של הרעיון, chroot, הוצג כ-system call ביוניקס ב-1979. אבל למרות שאולי נראה ש- chroot מבצע את מה שאנחנו רוצים, בפועל האופן שבו chroot מיישם את שינוי תיקיית ה-root עבור היישום לא מתאים להפרדה של קונטיינרים. chroot תוכנן לשימוש כחלק מפעולות תחזוקת מערכת, לא כפונקציה בידוד מאובטחת. אמנם יש בלינוקס מסורת של הגדרת chroot עבור daemons כקונפיגורציה להרצת שירותים בצורה "מאובטחת", אבל יש לתצורה הזו משמעות רק כשמדובר בתהליכים שרצים כמשתמשים מוגבלים ולא בתור root, שיכול באמצעים שונים לחזור חזרה למערכת הקבצים הכללית. אפילו בלי לרוץ בתור root, מי שמכיר קצת את הנושא של כליאת תהליכים ב-chroot jail ואת הפריצות הנפוצות של הקשחות כאלו מבין לבד שזה לא באמת זה (מצלצל למישהו ה-jailbrake באייפון?).

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

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

אבל רגע, אנחנו לא יכולים לעשות umount  למערכת הקבצים שלנו, אנחנו צריכים אותה ב-mount כדי שלא לשבש את הפעילות של שאר המערכת, אז איך pivot_root יכול לעזור לנו?  

Enter Namespace

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

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

ה-namespace הראשון שהתווסף לקרנל של לינוקס הוא ה-mount namespace, וההפרדה שהוא מאפשר ליצור היא של ה-mount list. mount namespace מאפשר ליצור רשימת mount עצמאית, כך שמערכת הקבצים שרואה תהליך שרץ ב-namespace מסוים יכולה להיות שונה לחלוטין מזו שרואה תהליך שרץ מחוץ לו, או ב-Namespace אחר. לאחר יצירה של mount namespace אפשר להשתמש ב-pivot_root להגדרת מיקום חדש ל-/ ולעשות umount למערכת הקבצים המקורית, עם כל ה-mounts שעליה, מבלי לגרום לשינוי כלשהו למערכת הקבצים מחוץ ל-namespace. כך מערכת ההפעלה ממשיכה לעבוד כרגיל ואילו התהליך שב-namespace רואה את התיקייה שאליה נעשה ה-pivot_root בתור / ורשימת ה-Mounts שהוא רואה ריקה. מנקודת מבטו, זו מערכת הקבצים הקיימת בכללותה.

כאשר תהליך בלינוקס יוצר תהליך נוסף (child process) באמצעות fork, ה-namespace משותף לשני התהליכים – תהליך האב וה-child process. על מנת שה-namespace לא יהיה משותף (כלומר, לא יהיה shared) ביצירת התהליך מעבירים לקרנל syscall בשם unshare. ל-unshare syscall יש פקודת מעטפת ברמת משתמש, שנקראת גם היא unshare.

Do try this at home

נדגים את הרצת הפקודה ואת תכונות ה-mountspace על ידי הרצה של shell בתהליך חדש ב-mount namespace עצמאי:

http://bit.ly/ns_mount

הסבר:

הפקודה הראשונה הכניסה אותנו בתור root ל-shell חדש שרץ ב-Mount namespace.

בפקודה השנייה איתרנו את הדיסק שמוגדר לנו כ-/.

בפקודות בשורות 4-5 יצרנו תיקיה בשם /media/tempns ועשינו mount של אותו דיסק לתיקייה שיצרנו (בנוסף ל-mount ל-/).

בשורה 6 בדקנו את תוכן התיקייה, ואמנם התוכן זהה לתוכן של /.

בשורה 8 חיפשנו את הדיסק ב-mount list, ואנחנו רואים שהוא מופיע פעמיים, פעם ב-/ ופעם ב-/media/tempns.

לאחר מכן פתחנו טרמינל נוסף ווידאנו שהשינוי שעשינו ב-namespace לא השפיע על המערכת מחוץ ל-namespace, ואכן התיקייה מחוץ ל-namespace ריקה וה-Mount השני לא מופיע ב-mount list.

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

מייסד-שותף בחברת Wizards

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
דוד יונגמן
בואו נעבוד ביחד
צרו קשר