ToCode
رفتن به کانال در Telegram
1 419
مشترکین
اطلاعاتی وجود ندارد24 ساعت
-17 روز
+230 روز
آرشیو پست ها
1 419
# לאן הולכים הקומיטים אחרי reset?
טריק מעניין של גיט שהצליח לבלבל אותי כשרק התחלתי לעבוד עם הכלי הוא האופן בו גיט מנהל את הקומיטים שלו. בקצרה בגיט כל המידע נשמר בתור גרף של קומיטים, כאשר כל קומיט "מצביע" על הקומיט שבא לפניו. חוץ מהקומיטים יש גם דבר שנקרא Branch-ים, שאפשר לחשוב עליהם כמו מדבקות או סימניות. תרצו תוכלו להדביק אותם על הקומיטים, וכשיימאס לכם תוכלו להזיז אותם לקומיטים אחרים.
חוץ מה Branch-ים וה Commit-ים יש עוד סימניה חשובה שנקראת HEAD. ה HEAD הוא מצביע ל Branch והוא מסמן לנו מה הקומיט "הנוכחי", כלומר מה הקומיט שעכשיו אנחנו עובדים עליו.
נראה את זה עם משחק על מאגר פשוט. יצרתי מאגר עם שלושה קומיטים שהלוג שלו נראה כך:
* 8131ef2 (HEAD -> main) third commit
* 3a40bc4 second commit
* e8006f8 first commit
בגרף יש שלושה קומיטים, בראנץ אחד בשם main וה HEAD שלנו הוא אותו main.
עכשיו לשאלה מהכותרת ולטריק שיכול בקלות להלחיץ חברים במסימות. נפעיל:
$ git reset --hard HEAD~
ואז נסתכל בלוג:
3a40bc4 (HEAD -> main) second commit
e8006f8 first commit
בום! לאן נעלם הקומיט השלישי?
התשובה הקצרה היא שהוא לא נעלם לשום מקום. בסך הכל הזזתי את main ואיתו את HEAD ולכן כשאני מפעיל עכשיו git log אני מתחיל את הספירה ממקום אחר - מהקומיט השני. לצערנו קומיטים לא שומרים מצביע לקומיט שבא אחריהם (הגרף מכוון הפוך, כלומר כל קומיט יודע רק מי בא לפניו) - וזה אומר שאני בנקודה בחיים בה הקומיט הנוכחי פשוט לא יודע לספר לי איך להגיע לקומיט העדכני ביותר.
אבל זה לא אומר שאותו קומיט נעלם או נמחק.
אם אתם זוכרים את מזהה הקומיט אתם יכולים בקלות לחזור אליו עם:
$ git reset --hard 8131ef2
אפילו אם אתם לא זוכרים את המזהה אתם יכולים בקלות לבדוק מה הוא היה עם:
$ git reflog
3a40bc4 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~
8131ef2 HEAD@{1}: reset: moving to 8131ef2
3a40bc4 (HEAD -> main) HEAD@{2}: reset: moving to main
3a40bc4 (HEAD -> main) HEAD@{3}: reset: moving to HEAD~
8131ef2 HEAD@{4}: commit: third commit
3a40bc4 (HEAD -> main) HEAD@{5}: commit: second commit
e8006f8 HEAD@{6}: commit (initial): first commit
ששומר יומן של כל הפעולות שבוצעו אצלכם במאגר. מעניין לשים לב שהיומן הוא לא הקומיטים. אפשר למחוק מהיומן רשומות של קומיטים שאי אפשר להגיע אליהם עם:
$ git reflog expire --expire-unreachable=now --all
וזה ימחק מהיומן כל התיחסות לקומיט 8131ef2, מה שיהפוך את הטריק להרבה יותר מפחיד כיוון שעכשיו יהיה הרבה יותר קשה להגיע לאותו הקומיט (אבל עדיין אפשרי, אם יודעים את המזהה או אם יש לכם מספיק סבלנות לחטט בתיקיית .git כדי למצוא את המזהה).
מחיקה מוחלטת של אותו קומיט אפשרית עם הפקודה:
$ git gc --prune=now
רק אחרי שמחקתם את הקומיט מה reflog כדי שבאמת אי אפשר יהיה להגיע אליו או לכל קומיט אחר שמצביע עליו. אבל מהלך כזה עלול לצאת מתחום ההומור הבריא ולגרום לחברים שלכם להיבהל באמת.1 419
# הערכות זמנים (טייק 712)
איפה רושמים את כל הפעמים שכתבתי ומחקתי את הקוד עד שהוא עבד? וכמה זמן עבודה זה לקח?
הערכת זמנים היא אישית ותלויה בנקודה בזמן, וזה מבלבל. לרוב הבעיות שאנחנו מתמודדים איתן בתור מתכנתים יש פיתרון פשוט, וכתיבה של אותו פיתרון פשוט לוקחת גג כמה שעות. עיקר הזמן שאנחנו מבלים על בעיה הוא לא בכתיבת אותו פיתרון פשוט אלא בהגעה אליו - וזה יכול לקחת גם שבועות או חודשים.
החלק המבלבל מגיע כשמסתכלים על הקוד עצמו וכלום לא משתקף שם. אם ישבתי שבועיים על בעיה, וכל יום ניסיתי לפתור אותה בדרך קצת שונה עד שאחרי שבועיים הגעתי לפיתרון טוב, אז מבחינת הקוד השבועיים האלה הלכו לפח. מתכנת אחר יותר חכם ממני, שהיה פותר את אותה בעיה כמו שצריך ביום הראשון היה מייצר את אותו קוד בדיוק שאני כתבתי, רק הרבה יותר מהר.
פיתרונות? אין ממש. וזאת לא בעיה רק שלנו. השבועיים האלה הם חלק מהיופי בלבנות דברים חדשים שעדיין לא בנית בעבר, ומי שיחפש "להעלים" אותם יגלה שהוא בסך הכל פותר את אותן בעיות שהוא כבר יודע לפתור שוב ושוב.
1 419
למרות קלות ההתקנה כשהסרביסים מכילים טיפוסים שונים, ממשחק קצר עם בראמבל לא הבנתי עד הסוף איך הם מטפלים בסרביסים שחולקים טיפוסים ודוגמאות שניסיתי לכתוב שהיו קצת יותר מסובכות ממה שהם הראו בתיעוד לא עבדו לי. לא בטוח אם הבעיה אצלי או בבראמבל, ויכול להיות שבהמשך כן אצליח להבין את מגננון שיתוף הטיפוסים שלהם.
בינתיים מה שאהבתי בפרויקט:
1. זיהוי מהיר של בעיות - כל פעם שהיתה לי טעות בראמבל הציג הודעה די אינפורמטיבית לקונסול
2. זיהוי מהיר ואוטומטי של כל הסרביסים ובניית סכימה מחוברת אונליין, כולל המשך מעקב אחרי הסרביסים ועדכון הסכימה אם משהו משתנה בסרביסים הפנימיים.
3. התקנה פשוטה דרך דוקר (בניתי אימג' בעצמי - האימג' שלהם לא עבד).
4. תמיכה מובנית באימות זהות דרך JWT
מה שלא אהבתי בפרויקט היה:
1. אין מספיק דוגמאות וגם התיעוד לא מקיף במיוחד.
2. ה Docker Image שהם מספקים לא עבד.
3. חלק מהאפשרויות בקונפיגורציה שכן הופיעו בתיעוד לא עבדו.
לא בטוח אם הדברים שלא עבדו היו בגלל שאני לא יודע להשתמש בפרויקט עדיין או בגלל שבאמת יש להם עדיין לא מעט באגים. בכל מקרה הפרויקט נראה צעיר ומבטיח. לא חושב שהיה לי אומץ להשתמש בו בפרודקשן במקום אפולו אבל בוודאות אחזור עוד כמה חודשים לבדוק מה מצבם.
זה הלינק לגיטהאב של בראמבל:
https://github.com/movio/bramble
וזה פרויקט הדוגמה שלי שמשתמש בו עם שני סרביסים הכתובים ב Node.JS:
https://github.com/ynonp/playing-with-bramble
1 419
# חיבור מספר שרתי GraphQL עם Bramble
לפי האתר שלהם, בראמבל הם Production Ready GraphQL Federation. מאחר ואני תמיד שמח לבדוק חלופות פשוטות יותר ל Apollo הלכתי לבנות פרויקט דוגמה קטן עם בראמבל. קוד הפרויקט כאן:
https://github.com/ynonp/playing-with-bramble
בפוסט אספר איך ברמבל עובד ומתי שווה לשקול אותו.
## מה זה אומר GraphQL Federation
נניח שיש לכם מספר סרביסים שכל אחד מהם עובד ב GraphQL, ואתם רוצים לחשוף רק ממשק אחד לאפליקציית ה Front End שדרכו האפליקציה תוכל למשוך מידע מכל הסרביסים. במצב כזה אתם תרצו לבנות סרביס אחד שנקרא GraphQL Gateway שישתמש בסכימה שתהיה סוג-של-חיבור של כל הסכימות מכל הסרביסים, וכל פעם שנבקש ממנו מידע הוא פשוט יפנה לסרביס המתאים לקבל ממנו את המידע.
שתי הדרכים המרכזיות לחבר סכימות נקראות Schema stitching ו Federation כשההבדלים המרכזיים נוגעים לאופן שבו אנחנו מתיחסים לטיפוסים המשותפים בין סרביסים, ולאיך ממזגים את הסכימות. אומנם ברמבל הם פיתרון מסוג Federation אבל בפרויקט הדוגמה שכתבתי לא הגעתי לחלקים המעניינים של ה Federation של מיזוג טיפוסים ולכן לפחות מבחינת מה שנראה בפוסט הם היו יכולים באותה מידע לקרוא לזה Schema Stiching. מה שחשוב להבין כאן זה ש Bramble יהיה סוג של Gateway אוטומטי שפשוט מקבל את הסרביסים ויודע להעביר בקשות ולחבר מידע משניהם. ומה שאהבתי בו זה שהסטאפ היה מאוד מאוד פשוט.
## הקמת שרת Bramble
כל מה שצריך בשביל להרים שרת Bramble הוא להריץ את הבינארי (כתוב ב go) ולהעביר לו קובץ קונפיגורציה שאומר איפה הסרביסים שהוא צריך לחבר. קובץ הקונפיגורציה שאני כתבתי נראה כך:
{
"services": [
"http://tasks-server:3000/graphql",
"http://fortune:3000/graphql"
],
"plugins": [
{
"name": "playground"
}
]
}
את הסכימות בראמבל מקבל מהסרביסים עצמם ולכן כל סרביס שצריך לשבת מאחורי בראמבל נדרש להוסיף ל Query שלו שאילתה שמחזירה טיפוס בשם Service.
בפרויקט הדוגמה יש לי שני סרביסים, אחד נקרא fortune שמחזיר את התוצאה של פקודת fortune והשני נקרא tasks-server שמחזיר רשימת משימות. קובץ הסכימה מתוך סרביס fortune שהוא הקטן יותר נראה כך:
type Query {
fortune: String!
service: Service!
}
type Service {
name: String! # unique name for the service
version: String! # any string
schema: String! # the full schema for the service
}
והחלק הרלוונטי בסכימה מסרביס המשימות נראה כך:
type Query {
tasks(done: Boolean): [Task]
task(id: String): Task
service: Service!
}
type Service {
name: String! # unique name for the service
version: String! # any string
schema: String! # the full schema for the service
}
גם ה Resolver עבור Service הוא די העתק-הדבק מהדוגמאות שלהם. הנה אחד שכתבתי לדוגמה בסרביס המשימות:
service(_obj, _args, _context, info) {
return {
name: 'Tasks',
version: '1.0',
schema: require('./schema.graphql'),
}
},
ובסך הכל זה כל מה שצריך בשביל שבראמבל יעבוד.
## איך בראמבל עובד
בעליית הפרויקט ברמאבל מתחבר לסרביסים שהעברנו לו בקובץ הקונפיגורציה, לוקח את הסכימות מהם, בונה אוטומטית סכימה משותפת ומאפשר להתחיל להריץ שאילתות. אם אתם מורידים את פרויקט הדוגמה ורוצים לנסות את זה הפעילו מהתיקיה הראשית שלו:
$ docker-compose up
ואחרי זה כנסו בדפדפן ל localhost:8082/playground.
אחרי זה תוכלו להריץ שאילתה כמו זו:
query a{
tasks {
text
}
}
ולקבל את רשימת המשימות או שאילתה כזו:
query b{
fortune
}
כדי לקבל fortune אקראי.
זה מדליק כי שתי השאילתות הולכות לאותו Gateway והוא כבר יודע לשלוח את השאילתה הראשונה לסרביס המשימות ואת השניה לסרביס ה fortune.
באותו Playground אפשר לראות גם את הסכימה המחוברת ובלוק Query שלה נראה כך:
type Query {
tasks(done: Boolean): [Task]
task(id: String): Task
fortune: String!
}
כלומר הוא חיבור של ה Query משני הסרביסים. את כל זה בראמבל הצליח לעשות אוטומטית.
## מה עדיין לא הצלחתי לעשות1 419
# אינדאסטרי סטנדרט
הכלי שנחשב Industry Standard הוא לא בהכרח יותר טוב, לא בהכרח יותר פשוט ולא בהכרח יותר יעיל מהמתחרים. היתרונות הם אחרים:
1. מישהו שמשתמש בו כנראה כבר נתקל ברוב הבעיות שאתם צפויים להיתקל בהן.
2. יש מנגנון מובנה להתמודד עם הארכיטקטורה המוזרה של המערכת שלכם.
3. יש תיעוד לא רע בכלל, ודי הרבה תשובות ב Stack Overflow וב Github Issues של האנשים שלא הצליחו למצוא את מה שמחפשים בתיעוד.
אבל ל Industry Standard יש גם חסרונות. בגלל שהוא מתאים לכל התעשיה, רוב הזמן הוא יהיה יותר מסובך ממה שאני צריך לפרויקט שלי (במיוחד בתחילת הדרך). יותר מסובך זה אומר:
1. עקומת לימוד יותר קשה, כי צריך לקרוא הרבה יותר תיעוד ולהבין איזה מהיכולות רלוונטיות למערכת שלי.
2. ביצועי ברירת מחדל פחות טובים. נכון שכמעט תמיד תהיה אפשרות לשיפור ביצועים וגם יהיו אינסוף מאמרים איך לשפר ביצועים, אבל ברירות המחדל חייבות לכלול פשרות שיתאימו לכולם.
כשמישהו מספר לי על Bundler יותר טוב מ Webpack, על Web Framework יותר טוב מ Express, על GraphQL Federation יותר טוב מ Apollo או על Orchestrator יותר טוב מקוברנטס אני לא מתווכח. כל עוד אני שומר לעצמי את האפשרות להחליף בעתיד, אני אפילו שמח לתת לכלי החדש צ'אנס.
1 419
# טיפ JavaScript: טיפול באוביקט או Promise מאותה פונקציה
הטריק הבא לא מסובך למימוש אבל יכול לחסוך לכם קצת if-ים כשאתם זוכרים להשתמש בו במקומות הנכונים. בואו נניח שיש לי פונקציה שצריכה להפוך את האות הראשונה בכל מילה לאותיות גדולות, משהו כזה:
function capitalize(item) {
return item.replace(/\b(\w)/g, c => c.toUpperCase());
}
ופתאום מגיע חבר שרוצה להשתמש בה במקום נוסף במערכת - אבל אין לו ביד מחרוזת טקסט אלא Promise למחרוזת. האינטואיציה הראשונה היא להתעצבן על החבר או לבקש ממנו להוסיף await בכל מקום, אבל אם נעצור לחשוב על זה לרגע נראה שיש פיתרון הרבה יותר פשוט. כל מה שצריך הוא לעדכן את הפונקציה שלנו שתדע להתמודד עם קלטים מכל מיני סוגים:
function capitalize(itemOrPromise) {
if (itemOrPromise.then) {
return itemOrPromise.then(capitalize);
}
// Now we know it's an item
return itemOrPromise.replace(/\b(\w)/g, c => c.toUpperCase());
}
אם היא קיבלה Promise (כלומר משהו שיש לו then) היא תתלבש על ה then ותישאר באותו הממשק כך שתחזיר גם Promise. אם היא קיבלה מחרוזת טקסט עדיין הכל טוב והיא תחזיר את המחרוזת המעודכנת.
ומי שבכלל רוצה להתחשב יכול להוסיף גם טיפול במערכים ועל הדרך במערכים של Promises:
function capitalize(itemOrPromise) {
if (Array.isArray(itemOrPromise)) {
return itemOrPromise.map(capitalize);
}
if (itemOrPromise.then) {
return itemOrPromise.then(capitalize);
}
// Now we know it's an item
return itemOrPromise.replace(/\b(\w)/g, c => c.toUpperCase());
}
אם קיבלנו מערך נריץ map כדי להפוך לאותיות גדולות את כל התאים שלו (ואם התאים הם Promises הכל עדיין יעבוד); אם קיבלנו Promise אז נחכה שהוא יסתיים, ואז הרקורסיה תטפל גם במקרה שקיבלנו Promise למערך וכשמגיעים בסוף למחרוזת טקסט אפשר לסיים ולהפוך את האות הראשונה בכל מילה לאות גדולה.
אגב JavaScript והכל, אז אין גם בעיה להוציא החוצה את כל הלוגיקה הרקורסיבית כדי שיהיה קל להשתמש בה על כמה פונקציות. זה נראה כך:
function handleAllThenThings(func) {
return function handler(itemOrPromise) {
if (Array.isArray(itemOrPromise)) {
return itemOrPromise.map(handler);
}
if (itemOrPromise.then) {
return itemOrPromise.then(handler);
}
// Now we know it's an item
return func(itemOrPromise);
};
}1 419
# מתי לכתוב קוד גרוע
הדעה הרווחת היא שאנחנו כותבים קוד גרוע כשאין זמן לכתוב קוד טוב או כשצריך לעבוד מול Legacy Code קיים או להתממשק לקוד חיצוני שאין לנו שליטה עליו. כמתכנתים אנחנו אוהבים לחשוב שאם רק היו נותנים לנו מספיק זמן, מספיק משאבים ומאפשרים לנו להשתמש בכל הכלים שאנחנו רוצים אז היינו מצליחים לכתוב קוד מעולה.
זה לעתים נדירות המקרה.
הפיתרונות הכי טובים שכתבתי לא היו אלה שכתבתי פעם ראשונה, לא משנה כמה זמן השקעתי באותה כתיבה ראשונה. המנגנונים הכי יציבים והכי גמישים שהצלחתי לבנות היו תוצאה של Refactoring ובדרך כלל יותר מאחד. קוד טוב הוא בסך הכל קוד גרוע שעבר מספיק שיכתובים כדי לנקות ממנו את הבאגים ולטפל כמו שצריך במקרי הקצה. קוד גנרי הוא בסך הכל קוד גרוע שעבר מספיק שכתובים כדי שבפעם הבאה שנצטרך להוסיף פיצ'ר הקוד כבר יהיה מוכן לשינוי.
זה לא סתם שכל Front End Framwork שהיתה בשכונה מספיק זמן עברה אינסוף סבבי Refactoring ובשלב מסוים גם כתיבה מחדש מאפס. באנגולר זה קרה במעבר מגירסה 1 ל-2, בריאקט במעבר מגירסה 15 ל 16 ובויו זה היה המעבר ל Vue3. אנחנו יכולים לקשקש עד מחר על Design Patterns וכתיבה נכונה, אבל בסוף כשמערכת פוגשת את העולם דברים משתנים.
קוד טוב הוא קוד חי, הוא קוד שאנחנו לא פוחדים לגעת בו, ושאנחנו משנים אותו כל הזמן. קוד טוב דורש מסגרת - של בדיקות, של ניהול תצורה; אבל יותר מהכל קוד טוב דורש קוד גרוע ממנו הוא יכול להתחיל ומתכנתים שלא מפחדים לשכתב עד שהקוד יהיה מספיק טוב. לפחות עד השכתוב הבא.
1 419
NAME READY STATUS RESTARTS AGE ingress-nginx-admission-create-2pz8l 0/1 Completed 0 68m ingress-nginx-admission-patch-kjrv2 0/1 Completed 0 68m ingress-nginx-controller-59b45fb494-nvkbg 1/1 Running 0 68mאחרי שאנחנו יודעים ש nginx שלנו באוויר אנחנו צריכים לכתוב לו קובץ הגדרות. בגלל קוברנטס זה לא יהיה קובץ הגדרות ספציפי של nginx אלא קובץ הגדרות של קוברנטס וקוברנטס יתרגם אותו להגדרות nginx (אולי כדי שיום אחד בעתיד נוכל להחליף את ה Gateway בכניסה לקלאסטר בצורה אוטומטית). בכל מקרה כתבו קובץ חדש בתיקיית k8s בשם
ingress.yaml עם התוכן הבא:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 3000
בעברית זה אומר שכל פעם שמישהו נכנס לקלאסטר שלנו לנתיב הראשי אנחנו פשוט נשלח אותו לסרביס web בפורט 3000. בעתיד כשיהיו לנו יותר סרביסים נוכל לבנות מערכת ניתוב יותר מתוחכמת, אבל זה כבר נושא לפוסט אחר.
הפעילו שוב:
$ kubectl apply -f k8s
ואז:
$ kubectl get ingress
ואם הכל עבד כמו שצריך תקבלו על המסך שורה שנראית בערך כך עם כתובת IP של הקלאסטר שלכם:
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress <none> * 192.168.49.2 80 46m
כניסה דרך הדפדפן או עם curl לכתובת ה IP שקיבלתם תחזיר את אוביקט ה JSON הריק:
$ curl 192.168.49.2
{"count":null}
ושליחת בקשת POST תעלה את המונה ב-1:
$ curl -X POST 192.168.49.2
{"count":1}
$ curl -X POST 192.168.49.2
{"count":2}
$ curl -X POST 192.168.49.2
{"count":3}
## מה הלאה
מה הלאה? יש לכם קלאסטר קוברנטס שמריץ אפליקציה דרך NGINX, שמתחבר ל Redis ושומר את המידע ב Persistent Volume. מה עוד אפשר לרצות!?
נו, לא יותר מדי.
השלב הבא יהיה לעבור על קבצי ה yaml בתוך תיקיית k8s, לקרוא אותם ולחפש בתיעוד מה כל שורה בהם אומרת. אחרי זה אפשר לנסות להוסיף עוד סרביסים ל docker-compose.yml ולראות איך kompose מתרגם אותם ולנסות להפעיל גם אותם. אחרי שתעשו את זה מספיק פעמים אולי התיעוד של קוברנטס כבר לא ירגיש כל כך משעמם ותוכלו אפילו לקרוא אותו מקצה לקצה.
נ.ב. שימו לב רק לשורת הגירסה ב docker-compose.yml שלכם. ברוב המקרים בכתיבת docker-compose.yml אנחנו מציינים גירסה כמה שיותר מתקדמת, אבל בשביל kompose צריך לוותר על החלק שאחרי הנקודה ולציין פשוט:
version: "3"
אם תשכחו את זה אז kompose פשוט ידפיס שגיאה למסך, וזה דווקא נחמד מצידו כי על רוב הדברים הוא לא טורח לדווח.1 419
אבל הריאליסטיים מבין הקוראים יודעים שזה לא הולך לעבוד כל כך בקלות. הפקודה
kubectl apply פשוט לוקחת קובץ הגדרות או תיקיה של קבצי הגדרות ומנסה "להתקין" אותם על הקלאסטר, כלומר לשנות את מצב הקלאסטר כדי שמה שכתוב בהגדרות זה גם מה שיהיה עליו. הבעיה שמה שכתוב בהגדרות ש kompose יצר עדיין לא מספיק מוכן.
את הנקודה הבעייתית הראשונה אנחנו יכולים לראות במבט חטוף על הקבצים שנוצרו: קומפוז יצר שני קבצים בשביל הסרביס web אבל רק קובץ אחד בשביל הסרביס redis. הקבצים שנוצרו ל web הם מסוג Deployment ו Service, ול redis נוצר רק קובץ Deployment.
אתם יכולים לראות מה ההבדל בין שני הבלוקים בקובץ docker-compose.yml המקורי שלנו?
web:
image: ynonp/kubedemo:1.0
ports:
- "3000:3000"
deploy:
restart_policy:
condition: any
redis:
image: "redis:alpine"
volumes:
- data:/data
אז נכון יש כמה הבדלים אבל ההבדל המרכזי שגרם לקומפוז להתנהג אחרת הוא ש web הגדיר מפתח ports ו redis לא הגדיר. מבחינת קומפוז אם אין לך ports אי אפשר יהיה להתחבר אליך (למרות שב docker-compose אין בעיה להתחבר לסרביס שלא הגדיר מיפוי פורטים). בכל מקרה ובשביל להיות נחמדים לקומפוז נעדכן את הקובץ לגירסה הזו:
version: "3"
services:
web:
image: ynonp/kubedemo:1.0
ports:
- "3000:3000"
deploy:
restart_policy:
condition: any
redis:
image: "redis:alpine"
ports:
- "6379:6379"
volumes:
- data:/data
volumes:
data:
ונריץ מחדש את קומפוז:
$ kompose convert -f docker-compose.yml -o k8s
INFO Kubernetes file "k8s/redis-service.yaml" created
INFO Kubernetes file "k8s/web-service.yaml" created
INFO Kubernetes file "k8s/redis-deployment.yaml" created
INFO Kubernetes file "k8s/data-persistentvolumeclaim.yaml" created
INFO Kubernetes file "k8s/web-deployment.yaml" created
ניסיון שני להתקין את הקבצים על הקלאסטר ובדיקה של התוצאה מביא לנו:
$ kubectl apply -f k8s
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
redis-784899f6c7-mtwzs 1/1 Running 0 5s
web-675ddc94f5-gcz8s 0/1 CrashLoopBackOff 1 5s
כמה שתריצו יותר פעמים את kubectl get pods תוכלו לראות שבסך הכל מונה ה Restarts יעלה אבל הסרביס שלנו לא יעבוד. בדיקה של הלוגים תראה לכם שסרביס web לא מוצא את הסרביס redis. המשך מחקר יספר לכם שהסיבה היא שלא הגדרנו hostname ב docker-compose.yml. במעבר מדוקר קומפוז לקוברנטס הכלי kompose לא יגדיר בשבילכם את ה hostname בצורה אוטומטית אם לא הגדרתם אותו בצורה מפורשת בקובץ.
העדכון הבא ל docker-compose.yml מביא אותנו לגירסה הזו:
version: "3"
services:
web:
hostname: web
image: ynonp/kubedemo:1.0
ports:
- "3000:3000"
deploy:
restart_policy:
condition: any
redis:
hostname: redis
image: "redis:alpine"
ports:
- "6379:6379"
volumes:
- data:/data
volumes:
data:
והפעל התקנה והפעלה מחדש והכל כבר עובד:
$ kompose convert -f docker-compose.yml -o k8s
$ kubectl apply -f k8s
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
redis-f7f66c7cb-zd9r9 1/1 Running 0 68s
web-6b56cc97d7-mclw2 1/1 Running 2 74s
עכשיו אני יכול להיכנס לשרת ה web ולבצע ממנו curl כדי לראות שהכל עובד:
$ kubectl exec -it web-6b56cc97d7-mclw2 -- /bin/bash
$ curl web:3000/
{"count":null}
אבל עדיין לא יכול להתחבר למערכת מבחוץ. בשביל זה צריך Ingress Controller.
## הוספת ingress
ב Kubernetes יש מנגנון אוטומטי שמפעיל שרת Nginx בכניסה לקלאסטר שלכם ומנהל את כל החיבורים פנימה. נכון, אפשר היה לחבר את הסרביס web שכתבתי ישירות לפורט 3000 של הקלאסטר אבל זה היה משעמם ולא מתאים למצב פרודקשן.
בשביל להגדיר את אותו Nginx אוטומטי על minikube אני מפעיל את הפקודה:
$ minikube addons enable ingress
ואם הכל עבד כמו שצריך אתם תוכלו להריץ:
$ kubectl get pods -n ingress-nginx
ולראות את הפלט:1 419
# העלאת סרביס מ docker-compose ל Kubernetes
דרך אחת ללמוד קוברנטס היא לקרוא את כל דפי התיעוד שלהם, אבל זה לוקח המון זמן ולא תמיד מבטיח שגם נבין את מה שנקרא. דרך יותר מעשית היא לקחת פרויקט פשוט ולהעלות אותו על קלאסטר. בפוסט זה אני אראה את הדרך השניה ומקווה שהיא תתן לכם נקודת התחלה טובה לעבודה עם k8s.
## תיאור הפרויקט
רציתי למצוא פרויקט פשוט אבל עדיין שיכיל היבטים מעניינים של Deployment ולכן בחרתי בשרת Node.JS שמשתמש בבסיס נתונים Redis. קובץ ה docker-compose.yml שאני מתחיל איתו נראה כך:
version: "3"
services:
web:
image: ynonp/kubedemo:1.0
ports:
- "3000:3000"
deploy:
restart_policy:
condition: any
redis:
image: "redis:alpine"
volumes:
- data:/data
volumes:
data:
האימג' kubedemo נוצר מה Dockerfile הבא:
FROM node:16
# Create app directory
WORKDIR /app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
RUN npm install
# If you are building your code for production
# RUN npm ci --only=production
# Bundle app source
COPY . .
EXPOSE 3000
CMD [ "node", "server.js" ]
ותוכן הקובץ server.js באותה תיקיה הוא:
const express = require('express')
const app = express()
const port = 3000
let
redis = require('redis'),
/* Values are hard-coded for this example, it's usually best to bring these in via file or environment variable for production */
client = redis.createClient({
port : 6379,
host : 'redis', // replace with your hostanme or IP address
});
app.get('/', (req, res) => {
client.get(req.hostname, (err, count) => {
if (err) return next(err);
res.send({ count });
});
});
app.post('/', (req, res, next) => {
client.incr(req.hostname, (err, count) => {
if (err) return next(err);
res.send({ count });
});
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
הקובץ מתחבר לרדיס ויודע לקבל שני סוגי בקשות: בקשת GET לנתיב הראשי מחזירה ערך מונה מסוים, ובקשת POST לנתיב הראשי מגדילה את המונה ב-1. מה שחשוב מבחינת ה Deployment זה שיש רק שרת אחת שמקשיב על פורט 3000 ומתחבר למכונת רדיס אחת. מספיק פשוט בשביל פרויקט קוברנטס ראשון.
## התקנה ויצירת קבצי yaml של Kubernetes
יש המון דרכים לקבל גישה לקלאסטר קוברנטס: אתר https://labs.play-with-k8s.com/ ייתן לכם קלאסטר בחינם ל-4 שעות. כל ספקי הענן הגדולים יתנו לכם קלאסטר למשחקים בתוכנית החינמית שלהם וספקי ענן קטנים יותר יתנו לכם קלאסטר בתשלום סמלי. אבל מכל האפשרויות אני מוצא שהכי נוחה היא התקנת minikube אצלכם על הלפטופ. זה פשוט עובד הכי מהר והכי קל למצוא בעיות.
כאן יש הוראות התקנה לכל מערכות ההפעלה:
https://minikube.sigs.k8s.io/docs/start/
אתם יודעים שהצלחתם אם אתם יכולים להפעיל:
$ kubectl config current-context
והמחשב מדפיס את מילת הקסם minikube.
הכלי השני שאשתמש בו בדוגמה נקרא kompose והוא יודע להפוך קובץ docker-compose.yml לקבצי הגדרות של kubernetes. מתקינים אותו מכאן:
https://kompose.io/.
אחרי התקנה מוצלחת אתם צריכים להיות מסוגלים להריץ משורת הפקודה:
$ kompose version
ולקבל משהו כמו 1.22.0.
אחרי שכל הכלים מותקנים נצא לדרך עם הפרויקט.
## המרת docker-compose.yml לקבצי k8s
אני יוצר תיקיה חדשה ליד קובץ docker-compose.yml וקורא לה k8s. אחר כך מפעיל:
$ kompose convert -f docker-compose.yml -o k8s
ומקבל את ההדפסה:
INFO Kubernetes file "k8s/web-service.yaml" created
INFO Kubernetes file "k8s/redis-deployment.yaml" created
INFO Kubernetes file "k8s/data-persistentvolumeclaim.yaml" created
INFO Kubernetes file "k8s/web-deployment.yaml" created
עכשיו האופטימיים מוזמנים להריץ את הפקודה הבאה כדי לנסות להפעיל את הסרביס על הקלאסטר שלנו:
$ kubectl apply -f k8s1 419
# פרופורציה
כל יום יש אינסוף פיצ'רים שדחוף לסיים ובגללם היום לא מצאתי זמן ל Refactoring.
כל שבוע יש אינסוף באגים שדחוף לפתור ובגללם היום לא מצאתי זמן לכתוב בדיקות אוטומטיות.
כל חודש יש אינסוף משימות דחופות שלא השאירו לי זמן ללמוד מיומנות חדשה.
וכל שנה יש אינסוף ישיבות מיותרות שלא השאירו לי זמן ליצור קשרים אמיתיים עם אנשים מחוץ לחברה.
ועדיין-
אם נצליח כל יום לארגן מחדש קצת מהקוד שלנו, לאורך זמן פיתוח פיצ'רים יהיה הרבה יותר קל.
אם נצליח כל שבוע לכתוב קצת בדיקות אוטומטיות, לאורך זמן יהיו פחות באגים.
אם נצליח כל חודש ללמוד אפילו חלק קטן ממיומנות חדשה, לאורך זמן נוכל להיות הרבה יותר יעילים בעבודה ובחיים.
ואם נצליח במהלך השנה ליצור קשרים משמעותיים עם אנשים מחוץ לחברה, לאורך זמן יהיו לנו הרבה יותר אנשים להחליף איתם רעיונות יצירתיים ויהיה לנו הרבה יותר קל לבנות מסלול קריירה שמתאים לנו.
שתהיה לכולנו שנה נפלאה, שנדע לקחת את הדברים הדחופים בפרופורציה ולמצוא זמן ל Refactoring, לבדיקות אוטומטיות, ללימוד מיומנויות חדשות וליצירת קשרים איכותיים מחוץ למעגל הקרוב של האנשים שאיתנו בחברה.
1 419
# טיפ דוקר: איך להוסיף קוד איתחול לאימג'
בעבודה עם דוקר אנחנו פוגשים אימג'ים טובים ב Dockerhub שהיו יכולים להיות מושלמים אם רק היה מקום להוסיף עוד כמה שורות קוד לפני שהקונטיינר עולה. אם זה פיצ'ר שגם אתם צריכים, תשמחו לשמוע שהוא די פשוט למימוש בעזרת שני הטריקים הבאים.
## יצירת אימג' שלנו שמתבסס על אימג' קיים
לפני שאוכל להגיע לטריקים נצטרך לבנות אימג' זהה לאימג' שאנחנו רוצים אבל עם מקום שלנו להריץ את הקוד. אני לוקח את האימג' של mongo בשביל הדוגמה אבל אפשר לבצע מהלך דומה על כל אימג' בדוקרהאב.
בגיטהאב אני מוצא את תיקיית המקור של האימג'. של מונגו נמצאת כאן:
https://github.com/docker-library/mongo
מזהה את ה Dockerfile שלה ומוצא בתוכו את השורות שמגדירות Entrypoint ו CMD. במונגו אלה השורות:
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 27017
CMD ["mongod"]
זה אומר שנקודת הכניסה של הקונטיינר היא הקובץ /usr/local/bin/docker-entrypoint.sh עם הפרמטר mongod.
נפתח תיקיה חדשה וניצור בתוכה Dockerfile עם התוכן הבא:
FROM mongo:5
COPY docker-entrypoint.sh /my/docker-entrypoint.sh
RUN chmod +x /my/docker-entrypoint.sh
ENTRYPOINT ["/my/docker-entrypoint.sh"]
CMD ["mongod"]
דרסתי את ה Entrypoint עם סקריפט שלי - שכל מה שהסקריפט שלי יעשה הוא להפעיל את ה entrypoint המקורי של מונגו. אני מעביר את mongod ל CMD בדיוק כמו שעשו באימג' המקורי, ומחרוזת זו תעבור בתור פרמטר לסקריפט שלי וממנו לנקודת הכניסה המקורית.
הסקריפט docker-entrypoint.sh שלי אגב הוא ממש פשוט:
#!/usr/bin/env bash
/usr/local/bin/docker-entrypoint.sh "$@"
שני הקבצים האלה מספיקים בשביל לבנות את האימג' ולנסות להריץ אותו ואם הכל עבד תקבלו קונטיינר של מונגו זהה לזה שהייתם מקבלים מהרצת האימג' המקורי.
## הרצת קוד לפני שהדבר שרצינו להפעיל עולה
החלק הקל הוא להפעיל קוד לפני שמונגו עולה. כל מה שצריך לעשות הוא להוסיף ל docker-entrypoint.sh שאני כתבתי את הקוד שרוצים לפני שמפעילים את docker-entrypoint.sh שלהם. לדוגמה התוכן הבא בקובץ יכתוב למסך שורת טקסט ורק אז יריץ את מונגו:
#!/usr/bin/env bash
echo "This code runs before mongo"
/usr/local/bin/docker-entrypoint.sh "$@"
## הרצת קוד אחרי שהדבר שרצינו להפעיל עולה
טריק קצת יותר מורכב הוא להפעיל את ההדפסה אחרי שמונגו עלה. המשחק כאן הוא שצריך להפעיל את מונגו ברקע, לחכות שהוא יעלה, להדפיס את מה שרוצים ואז "להצטרף" אליו כדי שהסקריפט docker-entrypoint.sh שלי לא יסתיים (סקריפט entrypoint שמשאיר רק תהליכי רקע מסתיים מיד וסוגר איתו את הקונטיינר).
מילת הקסם כדי להפעיל פקודת bash ברקע היא & ומילת הקסם שעוצרת את הסקריפט עד שתהליכי הרקע מסתיימים היא wait. בשביל להפעיל את ההדפסה אחרי שמונגו עלה אני אוסיף לקונטיינר את wait-for-it ואשתמש בו כדי לחכות שמונגו יעלה.
סך הכל נעדכן את ה Dockerfile לתוכן הבא:
FROM mongo:5
COPY docker-entrypoint.sh /my/docker-entrypoint.sh
RUN apt-get update && \
apt-get install -y wait-for-it && \
chmod +x /my/docker-entrypoint.sh
ENTRYPOINT ["/my/docker-entrypoint.sh"]
CMD ["mongod"]
ואת docker-entrypoint.sh לתוכן הבא:
#!/usr/bin/env bash
(/usr/local/bin/docker-entrypoint.sh "$@") &
wait-for-it localhost:27017
echo "This code runs after mongo is ready"
wait
הדוגמה בפוסט היתה על מונגו אבל היא עובדת על כל אימג' שתרצו. הנה השלבים בקצרה:
1. מזהים את ה Entrypoint וה CMD של האימג', רושמים אותם בצד. וגם את הפורט מ EXPOSE.
2. יוצרים אימג' שלנו שיש לו סקריפט entrypoint שמפעיל את ה entrypoint המקורי. לוקחים גם את ה CMD המקורי ואם רוצים להיות נחמדים גם את ה EXPOSE.
3. אם רוצים להריץ קוד לפני האימג' המקורי אפשר פשוט להוסיף את הקוד ב entrypoint החדש שיצרנו.
4. אם רוצים להריץ קוד אחרי האימג' המקורי נוסיף את wait-for-it ל Dockerfile, נריץ את האימג' המקורי ברקע, נשתמש ב wait-for-it כדי לחכות שהוא יהיה מוכן ואז נריץ את הקוד שלנו. בסוף נקרא ל wait כדי שהסקריפט entrypoint החדש שכתבנו לא יסתיים והקונטיינר ימשיך לעבוד.
اکنون در دسترس! پژوهش تلگرام ۲۰۲۵ — مهمترین بینشهای سال 
