היום למדתי: איך לדבג תוכנית בקונטיינר אחר
דוקר בנוי כל כך טוב שקל לשכוח שבעצם כל התוכניות שרצות בתוך קונטיינרים רצות על המחשב שלי ולא על איזו מכונה וירטואלית. מהסיבה הזאת גם אין בעיה לדבג תוכנית בין קונטיינרים, כשה gdb רץ בקונטיינר אחד והתוכנית בקונטיינר אחר. ככה נעשה את החיבור:
1. הפעלת קונטיינר עם תוכנית פשוטה
כתבתי תוכנית c מאוד פשוטה שבתוך לולאה אינסופית מדפיסה כל פעם הודעה למסך ואז לוקחת שניה לישון. זה הקוד:
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv) {
int c = 0;
while(1) {
sleep(1);
printf("Keep going..., %d seconds passed\n", c);
}
}
יש שם משתנה שרציתי להעלות אותו ב-1 בכל איטרציה, אבל שכחתי לכתוב את השורה כדי שיהיה באג בתוכנית. את התוכנית בניתי ב gcc עם סמלי דיבג ויצרתי ממנה אימג' נטול הפצה עם ה Dockerfile הזה:
FROM gcc:4.9 as build
COPY . /usr/src/myapp
WORKDIR /usr/src/myapp
RUN gcc -g -o myapp buggy.c
CMD ["./myapp"]
FROM gcr.io/distroless/nodejs:14
WORKDIR /app
COPY --from=build --chown=nonroot:nonroot /usr/src/myapp/myapp .
USER nonroot
ENTRYPOINT ["./myapp"]
פירסמתי אותו כבר בדוקר האב אז אתם יכולים גם להריץ קונטיינר מהאימג' הזה עם:
docker run -it ynonp/buggy-demo
2. חיבור gdb לקונטיינר רץ
אני מפעיל קונטיינר מהאימג' כמו שסיפרתי לכם עם:
docker run --name myapp -it ynonp/buggy-demo
ועכשיו רוצה לחבר אליו gdb כדי לדבג אותו. המפתח הוא הפקודה הבאה להפעיל קונטיינר דיבאג:
docker run --cap-add=sys_admin --cap-add=sys_ptrace --security-opt seccomp=unconfined --pid container:myapp -it ubuntu
קונטיינר הדיבאג שמופעל רואה את אותם תהליכים שיש בקונטיינר myapp, אבל יש לו אימג' שונה, הוא רץ מאימג' של ubuntu. בגלל זה אני יכול להתקין עליו כלים כמו gdb. בתוך קונטיינר הדיבאג אני מריץ:
apt-get update
apt-get install gdb
עכשיו צריך למצוא את קובץ התוכנית myapp, ואת הקוד המקורי buggy.c כי gdb צריך את שניהם בשביל לעבוד טוב. לגבי קובץ המקור בגלל שאני יצרתי אותו אני יכול פשוט להעתיק את הקוד לקונטיינר הדיבאג ולשמור אותו בשם /tmp/buggy.c
. לגבי הקובץ myapp
הוא מסתתר במערכת הקבצים של הקונטיינר myapp. בשביל להגיע אליו אני משתמש בתיקיה /proc
ומפעיל מתוך קונטיינר הדיבאג:
cp /proc/1/root/app/myapp /tmp
הנתיב /proc/1/root
הוא לינק סימבולי לשורש עץ מערכת הקבצים של תהליך מספר 1. בתוך הקונטיינר תהליך מספר 1 הוא האפליקציה שהקונטיינר הריץ, ובגלל שקונטיינר הדיבאג חולק את מספרי התהליכים עם הקונטיינר myapp אז התהליך יהיה myapp כלומר התהליך אותו אני רוצה לדבג. סך הכל בקונטיינר הדיבאג יש לי עכשיו בתיקיית /tmp
גם את הקובץ myapp
שהוא קובץ התוכנית שהרצתי וגם את קובץ המקור שלו buggy.c
.
עם שני אלה אפשר להפעיל את gdb ולהתחבר לתהליך מספר 1 שכרגע רץ:
gdb ./myapp 1
אחרי זה אני נוחת בתוך gdb, יכול ללחוץ l
כדי לראות את הקוד, next
כדי להריץ את השורה הבאה ו print
כדי להדפיס תוכן של משתנים.
3. נ.ב. למה לא להיכנס לקונטיינר הקיים?
בנקודה הזאת רבים מכם אולי חושבים שהיה אפשר פשוט להיכנס לקונטיינר myapp ולהריץ עליו gdb, משהו כזה:
docker exec -it myapp /bin/bash
התשובה שלפחות בדוגמה כאן זה פשוט לא עובד, והיא לא היחידה. הקונטיינר myapp הוא קונטיינר "ללא הפצה", זה אומר שבאימג' שהוא מריץ אין shell, אין מנהל חבילות, אין את gdb ואין דרך קלה להתקין אחד. הרבה פעמים כשאנחנו בונים אימג'ים לפרודקשן אנחנו מנקים אותם מכמה שיותר דברים גם כדי לא להשאיר מקום לחורי אבטחה וגם בשביל שהכל פשוט יעבוד מהר יותר. כשקונטיינר היעד כמעט ריק, חיבור קונטיינר נוסף ודיבאג דרכו הוא הדרך הקלה ביותר להגיע לנתונים ולבדוק מה שבור.
גם בעבודה עם כלים אחרים (לא C), אם קונטיינר היעד ריק ואפילו לא כולל Shell, תמיד אפשר להשתמש באותו טריק ולחבר קונטיינר דיבאג, ולהגיע למערכת הקבצים של קונטיינר היעד דרך תיקיית /proc
ושם לחקור מה הבעיה.