Image - Docker - דוקר - הרצאה 2

פורסם: 1 בינואר 2020

תקציר

צלילה ל-Image: Dockerfile (FROM, ADD/COPY, RUN, EXPOSE), בנייה עם docker image build והעלאה ל-Hub; חמישה דגשים — קריאות ודוקומנטציה, .dockerignore, Stateless, גודל מינימלי, Official images; Multi-stage build לייצור ארטיפקטים בלי להשאיר את סביבות ה-build באימג' הסופי; שכבות, Image Manifest, שימוש חוזר בשכבות, Read-only מול Writable בקונטיינר; והמשך בפרק הבא על ניהול קונטיינרים.

האזנה ישירה

תמלול הפרק (לחצו לפתיחה)

צלילה ל-Image ואיך בונים אותו נכון

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

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

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

והקובץ הזה הוא מכיל זוגות של Key-Value Pairs, מכיל זוגות של פקודה והתיאור שלה. איזה סוג פקודות יש שם? אז יכולות להיות שם פקודות - קודם כל הפקודה הראשונה היא פקודת FROM. כלומר אנחנו אומרים מה ה-Base של האימג' שלנו, מאיפה אנחנו יוצאים. כלומר, אם אנחנו רוצים עכשיו לכתוב שירוץ מעל אובונטו, אז אנחנו נחליט שה-FROM שלנו יהיה מאובונטו. וזאת השכבה הבסיסית ומשם אנחנו נתקדם.

יכולות להיות לנו פקודות של להוסיף דברים כמו ADD או COPY, להעביר תיקיות מסוימות, או RUN להריץ פקודות מסוימות. כל מיני פקודות שייצגו את הקובץ הישן, קובץ הטקסט הישן ההוא שתיאר איך להרים את האפליקציה.

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

הדוקר פייל זה קובץ שאנחנו בדרך כלל נשים אותו בתיקיית Root, בתיקייה העליונה של האפליקציה שלנו. וכמו שאמרנו, הוא יורכב מזוגות של Key-Value Pairs, כאשר ה-Key יהיה פקודה. בדרך כלל מקובל שהיא תהיה ב-Capital letters, באותיות גדולות.

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

חשוב לזכור שאם אנחנו משתמשים ב-FROM של מערכת הפעלה, אז אנחנו לא באמת מקבלים את כל מערכת ההפעלה המלאה. דיברנו על זה בהרצאה הקודמת - אימג' בסוף מריץ קונטיינר מעל מערכת ההפעלה של ה-Host. זאת אומרת שאם אנחנו עובדים עם ווינדוס, אז בסוף האימג' והקונטיינר שירוץ הם ירוצו על מערכת הפעלה של ווינדוס, גם אם רשמנו לינוקס ב-FROM של הדוקר פייל. אז חשוב להבין את זה. מה שכן אנחנו נקבל זה את ה-Look and feel שלה, אנחנו נקבל את ה-File system שלה, את מערכת הקבצים שלה, כל מיני טולים וכלים שבאים יחד עם מערכת ההפעלה.

אחרי שנכתוב את פקודת ה-FROM בדוקר פייל, אנחנו יכולים להוסיף פקודות של ADD, RUN, COPY - כל מיני פקודות שמתארות שלבים ביצירת האפליקציה. אולי לעשות Build של Maven, אולי להעתיק כמה ספריות ממקום אחד לאחר, לעשות Zip/Unzip, לעשות כל מיני פעולות. אנחנו יכולים גם להוסיף לייבל (Label) של Maintainer כדי לציין מי המפתח שכרגע כתב את הדוקר פייל. לי זה נראה מיותר, בעיקר כשעובדים עם גיט, אז אפשר באמת לראות מי כתב כל שורה בקובץ, אז אין צורך באמת לשים לייבל של מי כתב את הקובץ, אבל זו אפשרות שקיימת.

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

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

וכשהדוקר פייל מוכן, אנחנו נוכל להריץ פקודה פשוטה: docker image build. ניתן לאימג' הזה את השם שאנחנו רוצים לתת ויוצר לנו Image. עכשיו שיצרנו את הדוקר פייל, הרצנו אותו ויש לנו Image, אנחנו נוכל להעלות אותו ל-Docker Hub או לכל רפוזיטורי אחר ולחשוף אותו לעולם. מי שירצה יוכל לעשות Pull, להוריד אותו אליו ולהריץ, ויש לו קונטיינר מוכן של האימג' שנוצר בעקבות הדוקר פייל שכתבנו.

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

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

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

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

גודל מינימלי: אנחנו תמיד נרצה שהאימג' שלנו יהיה בגודל מינימלי. ואנחנו יכולים להגיד "מה זה משנה אם זה עכשיו 20 מגה-בייט או 100 מגה-בייט?", אבל זה משנה בסוף כשאנחנו משתמשים בהרבה אימג'ים או יוצאים מאימג'ים אחרים. האימג' שלנו יכול להיות Base Image לאימג' אחר. יכול להיות שאנחנו עם הזמן נוסיף עוד דברים, וחשוב לא לקחת איתנו דברים שהם לא הכרחיים. בהמשך אנחנו נלמד על נושא שנקרא Multi-stage build וזה יהיה מאוד רלוונטי לגביו. אז חשוב לזכור שהדגש הרביעי הוא לשמור על האימג' עם גודל מינימלי.

Official Images: דיברנו עליו בהרצאה הקודמת - חלק מהאימג'ים שנמצאים ב-Docker Hub מוגדרים כ-Official וחלק מוגדרים כ-Non-official. חלק הם באמת של חברות גדולות ורציניות שעברו בדיקות, וחלק הם של אנשים פרטיים שיצרו אימג'ים. אז דגש מאוד חשוב זה תמיד להעדיף, כשאפשר, להשתמש באימג'ים שהם רשמיים, שהם Official.

הנושא הבא שנדבר עליו הוא הנושא של Multi-stage build, בנייה של האימג' בכמה שלבים. אז מה זה אומר ולמה אנחנו אומרים מולטי? אז מה זה Single-stage build? ה-Single-stage build זה למעשה השימוש בדוקר פייל כמו שתיארתי אותו בהתחלה - אנחנו מייצרים FROM ואז אנחנו מפעילים כל מיני פקודות, יוצרים כל מיני דברים ובסוף נוצרת לנו אפליקציה. אבל יכול להיות לנו מצב שבו אנחנו נרצה לבנות את האימג' שלנו בכמה שלבים. אנחנו נרצה לחלק את זה לכמה שלבים.

ולמה אני מתכוון? בוא נחשוב על מצב שבו אנחנו רוצים להרים את אפליקציה A. זאת האפליקציה שבסוף בסוף אנחנו רוצים שתייצג את אפליקציה A, זאת האפליקציה שלנו. עכשיו, נניח שכדי לייצר את אפליקציה A אנחנו צריכים ארטיפקטים (Artifacts), תוצרים של אפליקציות אחרות, למשל אפליקציה B ו-C. נניח שכדי לייצר את אפליקציה A אנחנו צריכים איזה Jar או War או Zip או איזה קובץ שהוא תוצאה של איזה Build מסוים או תהליך מסוים על אפליקציה B ו-C.

אם אנחנו לא משתמשים ב-Multi-stage build, מה שאנחנו צריכים לעשות זה למעשה כחלק מסדרת הפקודות שאנחנו מתארים בדוקר פייל שלנו, אנחנו צריכים ליצור את אפליקציה B, לעשות את כל הפעולות, ממש את כל הפעולות שמצריכות הרמה של אפליקציה B שעובדת, וליצור ממנה את הקובץ Jar (לצורך העניין קובץ Zip שיהיה יותר גנרי) - קובץ Zip שהוא התוצר של אפליקציה B, ונקרא לו B.zip. ואותו דבר עם אפליקציה C, אנחנו נרים את כל האפליקציה כדי לייצר את C.zip. ועכשיו שיש לנו את B.zip ו-C.zip אנחנו יכולים להתפנות ללבנות את האפליקציה A שלנו, שהיא באמת האימג' שאנחנו בונים, וכחלק מהבנייה של האימג' הזה אנחנו ניקח את התוצרים B.zip ו-C.zip. ויש לנו עכשיו את האפליקציה A ואנחנו יכולים להעלות אותה לדוקר, כולם יכולים להשתמש בה והכל טוב.

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

ה-Multi-stage build מאפשר לנו בתוך הדוקר פייל לכתוב סטייג' (Stage) מסוים, שלב מסוים, שבו מייצרים עכשיו את B. אנחנו יכולים לעשות FROM ולכלול את שכבת הבסיס של B, ליצור את שכבת הבסיס של B, לייצר ממנה את התוצר B.zip וזהו, סיימנו את הסטייג' - אנחנו לא באמת... זה לא הגיע לתוצר הסופי. יצרנו סטייג' מסוים שנקרא "create B" ואנחנו ממש נותנים לו שם. משתמשים בפקודה AS שמוכרת מ-SQL למי שמכיר - אנחנו יכולים ב-SQL לעשות SELECT ... FROM ... AS ... ולתת לאיזה טבלה מסוימת שם מסוים. באותה צורה כאן אנחנו יכולים לקרוא לשלב הזה AS create B. ובאותו אופן לעשות גם AS create C.

יש לנו עכשיו שני שלבים. אז אנחנו מריצים בתוך הדוקר פייל את השלב הראשון שמייצר לנו את B.zip, אחר כך את השלב שמייצר את C.zip, ובסוף את השלב האחרון שהוא באמת רלוונטי ומעניין אותנו - השלב שמייצר את האפליקציה A. ושם אנחנו פשוט ניקח את הקבצים B.zip ו-C.zip ונתעלם מכל הדאטה של האפליקציות האלה. ומה שזה מאפשר לנו לעשות זה למעשה על ידי Multi-stage build לייצר שלב של B, שלב של C ובסוף שלב של A שרק לוקח את התוצרים. וככה נוכל לייצר Image שהוא רזה ומכיל רק את מה שנחוץ לנו.

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

עכשיו שיש לנו כבר את האימג' מוכן, הוא מורכב משכבות. השכבות האלה מיוצגות על ידי קובץ JSON שנקרא Image Manifest. ב-Image Manifest אפשר להסתכל ולראות את השכבות שהאימג' מורכב מהן, וחשוב להבין שהשכבות האלה לא מכירות אחת את השנייה, הן לא יודעות שהן באמת יוצרות ביחד אימג' מסוים. וכשאנחנו באמת מורידים, עובדים מ-Docker Hub או מכל מקום אחר, כשאנחנו באמת עושים Pull, אז מי שעובד עם דוקר יכול לראות שאנחנו לא רואים איזה Progress bar יחיד כזה שסופר אחוזים מאפס ל-100 עבור איזה קובץ אחד, אלא אנחנו ממש יכולים לראות את השכבות.

אם למשל עשיתי פה docker pull mongo, הרצתי פקודה עבור MongoDB, אפשר לראות פה 13 שכבות שיורדות עם 13 Progress bars עבורן. זאת אומרת אני מבקש מה-Docker Hub את האימג' של מונגו ומה שהוא עושה זה למעשה מתחיל לבדוק - הוא יודע מה מערכת ההפעלה שלי, ה-Docker Engine יודע להגיד מה מערכת ההפעלה שלי והוא מבקש מה-Docker Hub את האימג' שמתאים למערכת ההפעלה שלי. ואז ה-Docker Hub מתחיל לשלוח שכבה אחרי שכבה.

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

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

אז דיברנו היום על מה זה אימג', איך מייצרים אותו בעזרת Dockerfile, עברנו על דגשים לכתיבה נכונה ועל הקסם של Multi-stage build שמאפשר לנו ליצור אימג'ים רזים. וגם הבנו קצת איך זה עובד מאחורי הקלעים עם שכבות ו-Manifest.

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