הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

טיפ דוקר: מה עושים עם node_modules

06/07/2022

בפיתוח יישומי node.js בתוך דוקר אני בדרך כלל אוהב להריץ npm install בתוך הקונטיינר בתור פקודה ראשונה כשהוא עולה, לפני שמפעיל את היישום האמיתי שלי, וככה אני יודע שתמיד כל החבילות יהיו במקום. ייתרון נוסף של הגישה הזאת הוא שאפשר להשתמש ב image שכבר קיים בדוקרהאב ב docker-compose.yml וזה חוסך לנו את שלב בניית האימג'.

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

במצב כזה האינטואיציה הראשונה היא להריץ npm install מתוך ה Dockerfile, למשל משהו כזה:

FROM node:18
WORKDIR /app

COPY package*.json .
RUN npm install

CMD ["node", "myapp.js"]

ואז אני משתמש בקובץ docker-compose.yml כזה כדי למפות את התיקיה הנוכחית (סביבת הפיתוח) פנימה לקונטיינר:

version: "3.9"
services:
  app:
    build: .
    command: sleep infinity
    volumes:
      - .:/app

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

זה קורה בגלל שמיפוי ה Volume של תיקיית הפיתוח מסתיר את קבצי ה node_modules שהתקנתי בעת בניית האימג'.

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

ב node לצערי זה לא אפשרי ו node_modules ממש חייב להיות בתוך תיקיית קוד המקור שלכם, ולכן הפיתרון דורש קצת יותר יצירתיות: אנחנו מגדירים Volume בתוך ה Dockerfile שיכיל את המודולים המותקנים, ואז בהרצה ממפים את אותו volume על גבי המיפוי של תיקיית המקור - כך יוצא שה volume החדש של ה node_modules מסתיר את המיפוי של node_modules מתיקיית המקור, שהוא בעצמו הסתיר את תיקיית node_modules של האימג' (שעכשיו ריקה כי ההתקנה היתה על ה Volume).

קוד? ברור. זה ה Dockerfile - התוספת היחידה היא פקודת volume:

FROM node:18
WORKDIR /app

COPY package*.json .

VOLUME /app/node_modules
RUN npm install

CMD ["node", "myapp.js"]

כשמפעילים את האימג', פקודת run תעתיק את התוכן של node_modules שנמצא באימג' לתוך ה Volume החדש, והוא ימופה לתוך /app/node_modules במקום התיקיה מהמחשב המארח.

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

טיפ דוקר: סדר פעולות ב Dockerfile

14/03/2022

קובץ ה Dockerfile הבא מגדיר בניה של אפליקציית Rails שגם מכילה קוד צד לקוח, שבתורו מתקמפל עם node.js:

FROM ruby:2.7

WORKDIR /app
COPY . .

ENV RAILS_ENV=production
ENV NODE_ENV=production

RUN gem install bundler:2.2.5
RUN sh -c "apt-get update && apt-get install -y nodejs npm"

RUN bundle install

RUN sh -c "cd client; npm install"
RUN "./bin/rails assets:precompile"

CMD ["./bin/rails", "s"]

רואים מה הבעיה כאן?

המשך קריאה

טיפ דוקר: שימוש ב Multistage Builds

06/03/2022

בבניית Docker Image יהיו לנו מצבים שאנחנו צריכים אימג' בסיס מסוים כדי לבצע עבודת "הכנה", אבל אחרי שההכנה הסתיימה אפשר לקחת את התוצרים ולהגיש אותם מאימג' אחר. דוגמה פשוטה היא אפליקציית next.js שיוצרת אתר HTML סטטי. אני צריך את node.js וכל מה שקשור אליו בשביל לבנות את האתר הסטטי, אבל אחרי שבניתי אותו אני יכול להגיש את התוצאה מאימג' של nginx.

בדוקר יש מנגנון שנקרא multi stage builds שנועד לתת מענה למצבים כאלה ולאפשר בניה מאימג' אחד ואריזה באימג' אחר. בואו נראה איך זה עובד.

המשך קריאה

טיפ דוקר - ניקוי volumes שאינם בשימוש

01/03/2022

את הפקודה המופלאה הבאה גיליתי רק לא מזמן, והיא כבר הצליחה לפנות לי 30 ג'יגה בדיסק בלי להתאמץ. היא נקראת:

$ docker volume prune

בעבודה עם דוקר ו docker compose נוצרים כל הזמן Volumes, אבל אותם Volumes לא נמחקים בעבודה השוטפת (אלא אם כן מחקתם אותם בצורה יזומה). במיוחד מעצבנים ה Anonymous Volumes, כלומר Volumes עם שם אקראי שנוצרים מהוראת Volume ב Dockerfile של אימג'ים מסוימים, ואתם אפילו לא יודעים שיצרתם אותם.

הפקודה prune מוחקת במכה אחת את כל ה Volumes שאינם בשימוש, כלומר Volumes שאף קונטיינר לא משתמש בהם. הנה הפלט שהופיע אצלי באחת ההפעלות:

WARNING! This will remove all local volumes not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Volumes:
af3aae34cdad2d27ce238052ad76066f5007a994afd8e43e630ed1e065e1adc2
1bd5b35c4b045efcedab000946b4b0cc47a31c6ab9797aa7c15abe68a187dd24
215e9ed82a1248713a37856d91bef06322a0aeba29983bb3bda73c6d3474975f
81800e9125a43c536733d540776576c459857fa161aad9a022089ce9f29f4b39
b86e5619f32e50c1750d06463f93f1222b0a1cb654961b03eb05f693602036d5
21053b79f98090cef2ed0d2e370aab1c87902712554fe106fcdf7776ad49e432
36da000251fd03a84d7d7aad5d1387b88067993d4db37f93be101ee6b39a8fa0
7f46b0540d958befaaa9eba7f546f58ac345d43aca76d78c82fd6fa0f2310a9d
0503e2f1595109e47fe34520f50d0cae62bf61829b110329b1736dead696a36a
38487902f058dd998a018d694c6a0415df30f1764caccd06aaefed7bcdc7f4f9
9262a4d3d727b2ed8ec6d1eb53f9d12666492a31b7b156bdbf25c2cc4f9e72ba
85acc9837d945b8da346e0a3005a41f25f11bbe6dba3254acf0b050ea760f99b
f84ef10a22b6c39e4b500fff3dd2ab41a47d9c3c16521c72f51accf3ee9ae733
25b3e355acae3df348d705e90b932b2152e42c74a2e522352244b7a461a7854b
c887268872c39fff371c05f59989d35e0ed2662c784fde35c65a31622ef3cb90
3a69d7d9f5e96c566f39aedc8b3fc6c07d8bea3beebd47c3c624cc5e4dae593b
491272fb1d91144b92ad990edb92cf8d15a7ebfbf26d7b8d4bbc2c3a077c2166
b75c33d193925f9cbd059e863eb9e44e0612fd5614716eac7de4cbc5847dfbd8

Total reclaimed space: 2.519GB

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

וובינר בחמישי - עולים לקוברנטיס

27/02/2022

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

המשך קריאה

טיפ קוברנטיס: שימוש בקונטקסט כדי לעבוד עם כמה קלאסטרים

16/02/2022

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

הכלי kubectl משתמש בקובץ קונפיגורציה ברירת מחדל שנמצא בנתיב ~/.kube/config. בתוך אותו קובץ יש הגדרות כמו לדוגמה:

apiVersion: v1
clusters:
- cluster:
    server: https://cimaks-dns-bca70ca9.hcp.westeurope.azmk8s.io:443
  name: cimaks
contexts:
- context:
    cluster: cimaks
    user: clusterUser_Playground_cimaks
  name: cimaks
current-context: cimaks
kind: Config
preferences: {}

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

apiVersion: v1
clusters:
- cluster:
    server: https://146.148.56.200:443
  name: cloud_okteto_com
contexts:
- context:
    cluster: cloud_okteto_com
    extensions:
    - extension: null
      name: okteto
    namespace: ynonp
    user: 6b5b39bc-9c83-4d7b-a345-b03922c7e979
  name: cloud_okteto_com
current-context: cloud_okteto_com
kind: Config
preferences: {}

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

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

$ export KUBECONFIG=~/.kube/config:~/Downloads/okteto-kube.config

ועכשיו הפקודה:

$ kubectl config get-contexts

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

CURRENT   NAME               CLUSTER            AUTHINFO        NAMESPACE
*         cimaks             cimaks             
          cloud_okteto_com   cloud_okteto_com                   ynonp

מההדפסה אני רואה שפקודת ה kubectl הבאה שאכתוב תגיע לקלאסטר cimaks כי הוא מסומן בכוכבית. הפקודה use context בוחרת את הקונטקסט הפעיל ולכן אני יכול לשנות קלאסטר עם:

$ kubectl config use-context cloud_okteto_com

ועכשיו כל פקודת kubectl שאכתוב תפעל על הקלאסטר השני.

מי צריך לכתוב את ה Dockerfile?

12/02/2022

המעבר לדוקר יצר הפרדת כוחות בריאה בין מפתחים ל Devops. המפתחים אחראים על כתיבת הקוד וה Devops אחראים על העברה של הקוד לפרודקשן, הרצה ומעקב אחריו.

נקודות המפגש בין שתי הקבוצות היא ה Dockerfile.

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

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

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

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

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

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

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

מושגים בסיסיים בקוברנטיס

14/12/2021

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

המשך קריאה

מדריך: תמיכה במספר סביבות קוברנטס עם Kustomize

02/12/2021

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

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

המנגנון המובנה ב kubectl לניהול שינויים בין סביבות נקרא kustomize. בפוסט זה אציג דוגמה פשוטה לפרויקט שמשתמש ב kustomize כדי לייצר קבצי yaml שונים לסביבות השונות.

המשך קריאה

היום למדתי: ארכיטקטורות ו Docker Images

12/11/2021

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

  1. בבניית Docker Image אנחנו בונים אימג' ספציפי לארכיטקטורת המעבד ולמערכת ההפעלה עליה אנחנו מריצים את תהליך הבניה.

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

  3. הרבה אימג'ים בדוקר האב נבנו כמה פעמים, עבור מספר מערכות הפעלה וארכיטקטורות מעבד. בבניה כזאת מי שמפרסם את האימג' בעצם בונה ומפרסם כמה אימג'ים ובסוף מאחד את כולם לשם אחד ב Dockerhub. תראו למשל את האימג' של פייתון. התג 3.9.8 מכיל רשימה של 11 אימג'ים שונים, כל אחד לזוג אחר של ארכיטקטורת מעבד ומערכת הפעלה.

  4. כל פעם שאתם כותבים docker run או docker pul המנוע של דוקר מחפש אימג' שמתאים לארכיטקטורה ולמערכת הפעלה שהוא רץ בה כרגע. זה אומר שכשאני אכתוב docker run python על מערכת לינוקס ועל מערכת Windows, אני אקבל אימג' שונה בכל מערכת. אפשר לראות את זה אם תפעילו docker image ls בכל אחת משתי המכונות ותראו את ה Digest השונה.

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

  6. קלאסטרים וקוברנטס כן רצים על ארכיטקטורת מעבד ספציפית ושם יש לנו בחירה כשמקימים את הקלאסטר. ל AWS יש לדוגמה מעבד שנקרא Graviton2 המבוסס ARM ולפי טענתם יכול לחסוך בעלות הקלאסטר בהשוואה למעבד אינטל. אבל, אם האימג' שלי בנוי רק בארכיטקטורת amd64 אני לא יכול להשתמש בקלאסטר כזה. אמזון אפילו מסבירים איך לבנות אימג' לארכיטקטורת ARM למי שכן רוצה לחסוך.

  7. אפשר לבדוק מה הארכיטקטורה שה Docker Engine שלכם כרגע מריץ עם docker version. חפשו את המפתח OS/Arch.

  8. משתנה הסביבה DOCKER_DEFAULT_PLATFORM קובע לאיזה ארכיטקטורת יעד אנחנו בונים את האימג'ים כשמריצים docker build. בשביל לבנות למעבדי אינטל כשאני רץ על מכונת ARM אני קובע את הערך ל linux/amd64. בשביל לבנות סכימות יותר מתוחכמות יש מנגנון שנקרא buildx.

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