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

Thank you! Your submission has been received!

Oops! Something went wrong while submitting the form

קפיצה למים העמוקים עם Docker - חלק א'

ליאור בר-און
|
קלה
|
Apr 30, 2019
alt="facebook"alt="linkedin"להרשמה לניוזלטר

הייתי צריך לשכנע את עצמי לכתוב מאמר שכזה.


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


מה אני הולך לעשות אחרת?


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


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


• Container הוא ״כמו lightweight VM״ (הגדרה די נכונה, אך ממש לא מדויקת). הקונטיינר צורך הרבה פחות משאבים ומציב תקורת פעולה נמוכה מ-VM - אבל גם מספק רמת הפרדה נמוכה יותר = פחות אבטחה ופחות הגנה על המערכת מהתרסקויות.
• כל עוד המחשב מריץ רק תהליכים שאתם סומכים עליהם + אתם ערוכים לספוק קריסה של מכונה בודדת (לא יקרה המון, יקרה מעט יותר) - אז הרצת קונטיינרים היא דבר הגיוני.
• מכיוון שה-Container הוא לא ״מכונה״ אלא תהליך שרץ תחת הגבלות / בצורה חצי-מבודדת, הוא עדין עשוי להיות מושפע מתהליכים אחרים / Containers אחרים הרצים על אותה המכונה ו/או הגדרות משותפות של מערכת ההפעלה.
o חבל להאריך במילים: 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 אכן תוצג מספר גרסה.


בכדי להתחיל את התסריט שדיברנו עליו, אלך ל-DockerHub ואחפש אחר ״MySQL״.


DockerHub הוא רפוזיטורי הציבורי הגדול לשיתוף של Docker Images.


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


ישנם שלושה סימונים ששווים התייחסות קצרה:


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

3. תווית ה-"Docker Certified" (שאיננה נגזרת מתווית ה-Verified Publisher) אומרת ש:
• ה-Image המדובר מבוסס על Official Image.
• ה-Image עבר כלי של Docker לבדיקת כמה היבטים של אבטחה ופעולה בסיסית.
• למרות שזה לא טוב כמו Official Image שעובר review ידני, זה עדין סימן טוב שכדאי להתייחס אליו בחיוב - במיוחד עם השינויים מה-Official Image מובנים לכם.


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



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


ה-Dockerfile


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


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


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



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


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



1. דרך נוספת להוסיף layer ל-Image הוא פקודת ADD, או הגרסה הפשוטה והשימושית יותר שלה: פקודת COPY - המוסיפה קבצים ל-Image מתוך פעולות העתקה.
2. לכל קובץ Dockerfile יש פקודת CMD יחידה, שהיא הפקודה-ברירת המחדל שתרוץ בעת הרצת ה-container.
3. אם הפקודה הזו מורכבת - משתמשים ב-shellscript שהועתק לתוך ה-Image.
4. [נושא מתקדם] כאשר מריצים את ה-container - יש אפשרות לשלוח כפרמטר פעולה אחרת שתרוץ, במקום הפעולה שצוינה CMD.
5. אם זה לא מצב טיפוסי ל-Image (ניתן להפעיל פעולות גם על container לאחר שרץ) - אזי משתמשים בפקודת ENTRYPOINT לציין פקודת בסיס, כאשר ה-command (אם הוגדר ע"י CMD או קלט חיצוני) - ישורשר אליה כפרמטר/פרמטרים.
6. רק פקודת ה-ENTRYPOINT האחרונה בקובץ - תופסת.
7. לפקודות CMD ו-ENTRYPOINT יש שתי צורות כתיבה עיקריות:
i. shell form - שורת טקסט רגילה - תפעיל את ה-shell בכדי לפענח אותו
b. מאבדים את היכולת של הקונטיינר לקבל סיגנלים מהמערכת שמארחת אותו
c. נחשב פורמט פחות אמין מבחינת אבטחה, כי אפשר לעשות כל מיני תרגילי shell תוקפניים
d. פחות אמין מבחינת ביצוע, כי סיכוי טוב שכל מיני פקודות שאנו מנסים להשתמש בהן (למשל tr או xargs) - פשוט לא זמינות בהפצה המינימלית של הלינוקס שאנו משתמשים בה.
i. execution form - קלט כמערך JSON: פקודה ואז פרמטרים - כמו שתי הדוגמאות בקובץ הנ"ל, כאשר המערך כולל רק איבר יחיד, ולכן אינו כולל פרמטרים.
e. זהו הפורמט המומלץ והנפוץ לשימוש.
f. אפשר בפרמטרים להתייחס למשתני סביבה של לינוקס. אין בעיה.
g. חסרון נפוץ: לא ניתן לשרשר פקודות בעזרת && (פונקציה של ה-shell).
8. פקודת EXPOSE מציינת לאילו ports ה-container יאזין. זה עדין לא מספיק על מנת לקבל תקשורת, כי בנוסף יש להפעיל (לרוב: בעת הרצת הקונטיינר) פקודה בשם publish (בעזרת הארגומנט p-) שתחליט מהיכן אנו יכולים לקבל את התקשורת.
9. בגדול אפשר לתאר 3 מצבים:
a. ללא expose - הקונטיינר לא יוכל לקבל תקשורת. עדין יש לכך שימושים.
b. עם expose, אך ללא publish - הקונטיינר יוכל לקבל תקשורת רק מ-containers אחרים.
c. עם expose ועם publish - הקונטיינר יוכל לקבל תקשורת מטווח כתובות ה-ip שהוגדר.
10. הפורטים אליהם מתייחסים הם מסוג 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.
3. אפשר לזהות Image ע"י name:tag - אבל לא תמיד הזיהוי יהיה "יציב" (למשל: mysql:latest נדרס ע"י עותק חדש יותר של latest)
4. דרך זיהוי יותר יציבה היא בעזרת ה-Image ID החלקי (תחילית של 12 סימנים ראשונים ב-hash) או ה-Image ID המלא (GUID באורך 256 ביט).
5. כבר הזכרנו שה-Tag הוא בעצם מספר גרסה. שימו לב ש `latest` הוא ערך ברירת המחדל של tags ב-Docker - ואין שום מחויבות שזו באמת הגרסה האחרונה שזמינה.
6. עבור Image מתוחזק-היטב כמו MySQL - זו כנראה באמת הגרסה האחרונה (בעת ההורדה) - אם כי אין מנגנונים של Docker שעוזרים לתחזק זאת (זו עבודה "ידנית" של מי שמנהל את ה-Images).
7. מסיבה זו - ההמלצה המקובלת היא להימנע משימוש ב-tag בשם latest ו/או להימנע מאי-ציון tag ל-Images שאנחנו בונים - אלא אם אתם מנהלים בדייקנות ש latest תמיד תהיה הגרסה האחרונה. ייתכן ובעתיד ה-ecosystem של docker יספק פתרון אמין לניהול גרסאות - אבל בינתיים זה בידכם.

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



מכיוון שלא ציינתי tag, אני מקבל את ה-latest - במקרה שלנו: MySQL 8.
1. אם נשווה את ה-layers, קל מאוד למצוא את ההתאמה ל-Dockerfile של MySQL8 שראינו למעלה - הקדישו דקה ונסו!
2. כפי שאמרנו, כל פעולת RUN / ADD / COPY מתרגמת ל-Layer פיסי חדש.
3. כל פעולה אחרת יוצרת מה-שנקרא temporary intermediate Image (שלב טכני בזמן היצירה) - וההשפעה "תמוזג" לתוך ה-Layer הפיסי הבא.
4. אני יכול לזהות intermediate layers ע"פ כך שיש להם גודל של 0B.
a. Docker מפעיל shell בבניית ה-Image על כל פעולת RUN, וכל פעולה אחרת (כולל COPY/ADD - המבוצעות ע"י Docker עצמו) - מסומנות כ (nop)# - קיצור של no operation.
5. אם בעבר בעמודת ה-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 הרצים.
2. אני יכול לראות את ה-container id (גם הוא hash באורך 256 בתים, כאן אנו רואים רק את התחילית)
3. אני רואה כמה זמן ה-container חי, וכמה זמן הוא רץ (לא תמיד זה אותו הדבר).
4. לכל container שלא נתתי לו שם, Docker בוחר שם מחיבור אקראי של שתי מילים, לפעמים זה יוצא מצחיק. אם הייתי מריץ הרבה containers מאותו סוג על אותה המכונה - כנראה היה לי חשוב לתת שם בעצמי - אבל בינתיים ה-container id מספיק לי.
5. אם אני רוצה לראות את הפקודה המלאה שהורצה (במקרה זה: docker-entrypoint.sh mysqld - כצפוי מה-Dockerfile, אם תחשבו מעט), או את ה-id המלא של ה-container אני יכול להשתמש בפרמטר no-trunc-- שיציג את כל הנתונים ולא "יחתוך" אותם.
6. פקודת docker stop היא השקולה ל-kill. אני מספק את ה-container id בכדי להצביע את מי יש לסגור.
7. עכשיו אני רואה שבאמת ה-container נסגר ואיננו רץ עוד. אני יכול לקרוא ל-docker ps -a ולראות גם containers שכבר נסגרו.
8. כברירת מחדל, Docker משאיר את ה-mount (כלומר: ה-mount הגבוה ביותר ב-union mount, המוקצה ל-container עבור כתיבה). פקודת docker stop עוצרת את ה-container - אבל לא מוחקת את הנתונים שלו - וזה פרט חשוב.
a. זה אומר שאוכל לחזור ולהריץ את ה-container הזה, מאותו state אחרון שהיה בעת הסגירה (נעשה זאת בהמשך הפוסט).
b. זה אומר שתוך כדי עבודה, מצטברים לי הרבה "states" של containers סגורים חסרי שימוש - וצריך מדי פעם לנקות אותם.
9. אפשר להשתמש ב-docker ps -a -f status=exited על מנת להציג רק את הקונטיינרים שכבר לא רצים (אך השאירו שאריות, אחרת הם לא היו מופיעים בפקודה docker ps).
10. בקיצור: הפקודה  (docker rm $(docker ps -a -q -f status=exited - תנקה את כל השאריות שנותרו. זהירות לא למחוק יותר מדי.


במאמר הבא נגיע לניסיון השני, ומשם נגיע לתוצאה המיוחלת!

------

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


מאת: ליאור בר-און, ארכיטקט ראשי בחברת Gett וכותב בבלוג http://www.softwarearchiblog.com

רוצים להתעדכן בתכנים נוספים בנושאי ענן וטכנולוגיות מתקדמות? הירשמו עכשיו לניוזלטר שלנו ותמיד תישארו בעניינים > להרשמה

הייתי צריך לשכנע את עצמי לכתוב מאמר שכזה.


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


מה אני הולך לעשות אחרת?


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


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


• Container הוא ״כמו lightweight VM״ (הגדרה די נכונה, אך ממש לא מדויקת). הקונטיינר צורך הרבה פחות משאבים ומציב תקורת פעולה נמוכה מ-VM - אבל גם מספק רמת הפרדה נמוכה יותר = פחות אבטחה ופחות הגנה על המערכת מהתרסקויות.
• כל עוד המחשב מריץ רק תהליכים שאתם סומכים עליהם + אתם ערוכים לספוק קריסה של מכונה בודדת (לא יקרה המון, יקרה מעט יותר) - אז הרצת קונטיינרים היא דבר הגיוני.
• מכיוון שה-Container הוא לא ״מכונה״ אלא תהליך שרץ תחת הגבלות / בצורה חצי-מבודדת, הוא עדין עשוי להיות מושפע מתהליכים אחרים / Containers אחרים הרצים על אותה המכונה ו/או הגדרות משותפות של מערכת ההפעלה.
o חבל להאריך במילים: 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 אכן תוצג מספר גרסה.


בכדי להתחיל את התסריט שדיברנו עליו, אלך ל-DockerHub ואחפש אחר ״MySQL״.


DockerHub הוא רפוזיטורי הציבורי הגדול לשיתוף של Docker Images.


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


ישנם שלושה סימונים ששווים התייחסות קצרה:


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

3. תווית ה-"Docker Certified" (שאיננה נגזרת מתווית ה-Verified Publisher) אומרת ש:
• ה-Image המדובר מבוסס על Official Image.
• ה-Image עבר כלי של Docker לבדיקת כמה היבטים של אבטחה ופעולה בסיסית.
• למרות שזה לא טוב כמו Official Image שעובר review ידני, זה עדין סימן טוב שכדאי להתייחס אליו בחיוב - במיוחד עם השינויים מה-Official Image מובנים לכם.


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



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


ה-Dockerfile


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


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


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



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


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



1. דרך נוספת להוסיף layer ל-Image הוא פקודת ADD, או הגרסה הפשוטה והשימושית יותר שלה: פקודת COPY - המוסיפה קבצים ל-Image מתוך פעולות העתקה.
2. לכל קובץ Dockerfile יש פקודת CMD יחידה, שהיא הפקודה-ברירת המחדל שתרוץ בעת הרצת ה-container.
3. אם הפקודה הזו מורכבת - משתמשים ב-shellscript שהועתק לתוך ה-Image.
4. [נושא מתקדם] כאשר מריצים את ה-container - יש אפשרות לשלוח כפרמטר פעולה אחרת שתרוץ, במקום הפעולה שצוינה CMD.
5. אם זה לא מצב טיפוסי ל-Image (ניתן להפעיל פעולות גם על container לאחר שרץ) - אזי משתמשים בפקודת ENTRYPOINT לציין פקודת בסיס, כאשר ה-command (אם הוגדר ע"י CMD או קלט חיצוני) - ישורשר אליה כפרמטר/פרמטרים.
6. רק פקודת ה-ENTRYPOINT האחרונה בקובץ - תופסת.
7. לפקודות CMD ו-ENTRYPOINT יש שתי צורות כתיבה עיקריות:
i. shell form - שורת טקסט רגילה - תפעיל את ה-shell בכדי לפענח אותו
b. מאבדים את היכולת של הקונטיינר לקבל סיגנלים מהמערכת שמארחת אותו
c. נחשב פורמט פחות אמין מבחינת אבטחה, כי אפשר לעשות כל מיני תרגילי shell תוקפניים
d. פחות אמין מבחינת ביצוע, כי סיכוי טוב שכל מיני פקודות שאנו מנסים להשתמש בהן (למשל tr או xargs) - פשוט לא זמינות בהפצה המינימלית של הלינוקס שאנו משתמשים בה.
i. execution form - קלט כמערך JSON: פקודה ואז פרמטרים - כמו שתי הדוגמאות בקובץ הנ"ל, כאשר המערך כולל רק איבר יחיד, ולכן אינו כולל פרמטרים.
e. זהו הפורמט המומלץ והנפוץ לשימוש.
f. אפשר בפרמטרים להתייחס למשתני סביבה של לינוקס. אין בעיה.
g. חסרון נפוץ: לא ניתן לשרשר פקודות בעזרת && (פונקציה של ה-shell).
8. פקודת EXPOSE מציינת לאילו ports ה-container יאזין. זה עדין לא מספיק על מנת לקבל תקשורת, כי בנוסף יש להפעיל (לרוב: בעת הרצת הקונטיינר) פקודה בשם publish (בעזרת הארגומנט p-) שתחליט מהיכן אנו יכולים לקבל את התקשורת.
9. בגדול אפשר לתאר 3 מצבים:
a. ללא expose - הקונטיינר לא יוכל לקבל תקשורת. עדין יש לכך שימושים.
b. עם expose, אך ללא publish - הקונטיינר יוכל לקבל תקשורת רק מ-containers אחרים.
c. עם expose ועם publish - הקונטיינר יוכל לקבל תקשורת מטווח כתובות ה-ip שהוגדר.
10. הפורטים אליהם מתייחסים הם מסוג 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.
3. אפשר לזהות Image ע"י name:tag - אבל לא תמיד הזיהוי יהיה "יציב" (למשל: mysql:latest נדרס ע"י עותק חדש יותר של latest)
4. דרך זיהוי יותר יציבה היא בעזרת ה-Image ID החלקי (תחילית של 12 סימנים ראשונים ב-hash) או ה-Image ID המלא (GUID באורך 256 ביט).
5. כבר הזכרנו שה-Tag הוא בעצם מספר גרסה. שימו לב ש `latest` הוא ערך ברירת המחדל של tags ב-Docker - ואין שום מחויבות שזו באמת הגרסה האחרונה שזמינה.
6. עבור Image מתוחזק-היטב כמו MySQL - זו כנראה באמת הגרסה האחרונה (בעת ההורדה) - אם כי אין מנגנונים של Docker שעוזרים לתחזק זאת (זו עבודה "ידנית" של מי שמנהל את ה-Images).
7. מסיבה זו - ההמלצה המקובלת היא להימנע משימוש ב-tag בשם latest ו/או להימנע מאי-ציון tag ל-Images שאנחנו בונים - אלא אם אתם מנהלים בדייקנות ש latest תמיד תהיה הגרסה האחרונה. ייתכן ובעתיד ה-ecosystem של docker יספק פתרון אמין לניהול גרסאות - אבל בינתיים זה בידכם.

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



מכיוון שלא ציינתי tag, אני מקבל את ה-latest - במקרה שלנו: MySQL 8.
1. אם נשווה את ה-layers, קל מאוד למצוא את ההתאמה ל-Dockerfile של MySQL8 שראינו למעלה - הקדישו דקה ונסו!
2. כפי שאמרנו, כל פעולת RUN / ADD / COPY מתרגמת ל-Layer פיסי חדש.
3. כל פעולה אחרת יוצרת מה-שנקרא temporary intermediate Image (שלב טכני בזמן היצירה) - וההשפעה "תמוזג" לתוך ה-Layer הפיסי הבא.
4. אני יכול לזהות intermediate layers ע"פ כך שיש להם גודל של 0B.
a. Docker מפעיל shell בבניית ה-Image על כל פעולת RUN, וכל פעולה אחרת (כולל COPY/ADD - המבוצעות ע"י Docker עצמו) - מסומנות כ (nop)# - קיצור של no operation.
5. אם בעבר בעמודת ה-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 הרצים.
2. אני יכול לראות את ה-container id (גם הוא hash באורך 256 בתים, כאן אנו רואים רק את התחילית)
3. אני רואה כמה זמן ה-container חי, וכמה זמן הוא רץ (לא תמיד זה אותו הדבר).
4. לכל container שלא נתתי לו שם, Docker בוחר שם מחיבור אקראי של שתי מילים, לפעמים זה יוצא מצחיק. אם הייתי מריץ הרבה containers מאותו סוג על אותה המכונה - כנראה היה לי חשוב לתת שם בעצמי - אבל בינתיים ה-container id מספיק לי.
5. אם אני רוצה לראות את הפקודה המלאה שהורצה (במקרה זה: docker-entrypoint.sh mysqld - כצפוי מה-Dockerfile, אם תחשבו מעט), או את ה-id המלא של ה-container אני יכול להשתמש בפרמטר no-trunc-- שיציג את כל הנתונים ולא "יחתוך" אותם.
6. פקודת docker stop היא השקולה ל-kill. אני מספק את ה-container id בכדי להצביע את מי יש לסגור.
7. עכשיו אני רואה שבאמת ה-container נסגר ואיננו רץ עוד. אני יכול לקרוא ל-docker ps -a ולראות גם containers שכבר נסגרו.
8. כברירת מחדל, Docker משאיר את ה-mount (כלומר: ה-mount הגבוה ביותר ב-union mount, המוקצה ל-container עבור כתיבה). פקודת docker stop עוצרת את ה-container - אבל לא מוחקת את הנתונים שלו - וזה פרט חשוב.
a. זה אומר שאוכל לחזור ולהריץ את ה-container הזה, מאותו state אחרון שהיה בעת הסגירה (נעשה זאת בהמשך הפוסט).
b. זה אומר שתוך כדי עבודה, מצטברים לי הרבה "states" של containers סגורים חסרי שימוש - וצריך מדי פעם לנקות אותם.
9. אפשר להשתמש ב-docker ps -a -f status=exited על מנת להציג רק את הקונטיינרים שכבר לא רצים (אך השאירו שאריות, אחרת הם לא היו מופיעים בפקודה docker ps).
10. בקיצור: הפקודה  (docker rm $(docker ps -a -q -f status=exited - תנקה את כל השאריות שנותרו. זהירות לא למחוק יותר מדי.


במאמר הבא נגיע לניסיון השני, ומשם נגיע לתוצאה המיוחלת!

------

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


מאת: ליאור בר-און, ארכיטקט ראשי בחברת Gett וכותב בבלוג http://www.softwarearchiblog.com

רוצים להתעדכן בתכנים נוספים בנושאי ענן וטכנולוגיות מתקדמות? הירשמו עכשיו לניוזלטר שלנו ותמיד תישארו בעניינים > להרשמה

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