• בלוג
  • היום למדתי: איך לדבג תוכנית בקונטיינר אחר

היום למדתי: איך לדבג תוכנית בקונטיינר אחר

10/10/2022

דוקר בנוי כל כך טוב שקל לשכוח שבעצם כל התוכניות שרצות בתוך קונטיינרים רצות על המחשב שלי ולא על איזו מכונה וירטואלית. מהסיבה הזאת גם אין בעיה לדבג תוכנית בין קונטיינרים, כשה 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 ושם לחקור מה הבעיה.