לקבל מושג ירוק על nginx

nginx הוא רכיב בסיסי ונפוץ בחלק נכבד ממערכות הווב כיום. לאלו שאין nginx, בד"כ יש Apache Httpd – כלי מקביל שנחשב קצת יותר מיושן.את nginx מתקינים ב-3 תצורות עיקריות:

  • שרת Web המגיש תוכן HTML/JS/CSS למשתמשים. שימוש נפוץ – אבל נראה שזה לא השימוש הנפוץ של קוראי הבלוג הזה.
  • Reverse Proxy – מותקן מאחורי ה Load Balancer (למשל: ELB) ולפני המערכת שלנו / המיקרו-שירות. זה כנראה השימוש הנפוץ בקרב קוראי הבלוג.
    • וריאציה של התצורה האחרונה היא API Gateway – מונח שנטען בבאזז מעולם המיקרו-שירותים. בוריאציה הזו ה nginx גם מבצע את ה Authentication.
  • Load Balancer – בתצורה הזו nginx משמש לרוב גם כ Reverse Proxy וגם כ Load Balancer.

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

בפלטפורמות שאינן בנויות ל concurrency (למשל: PHP, Ruby, או פייטון) nginx הוא רכיב קריטי לטפל ב traffic גבוה. ה nginx יכול "לספוג" מאות, אלפי, ועשרות אלפי concurrent connections ש backends מהסוגים הללו לא מתמודדים איתם יפה, ולהקל על ה backend במקרים בעייתיים כמו בעיית ה Slow Client.

ב Backends הבנויים למקביליות (כמו Java או Go) – הצורך ב nginx הוא פחות מובן-מאליו, ויש מקרים שבהם הוא לא באמת נדרש, אך אנו ממשיכים להתקין אותו כי "זה Best Practice" או מתוך הרגל.

בכל מקרה, נראה שכל הנושא של nginx נמצא בידע חסר. מי שניגש אליו הוא אנשי ה Operations ו/או מפתחים ובעיקר כאשר יש "בעיות". למשל: nginx החליט (בחוצפתו) לחתוך URLs ארוכים במיוחד של בקשות GET.

המפגש עם nginx עשוי לפעמים להתאפיין בסריקה של StackOverflow והדבקה של כל מיני Settings לקונפיגורציה עד אשר נראה שהבעיה חדלה מלהציק.

בפוסט הזה אני רוצה לספק הצצה מהירה, hands-on ברובה, ל nginx כך שהמפגשים הבאים שלנו איתו יהיו מעמיקים, יעילים, ונעימים יותר.

ירוק כבר יש לנו. עכשיו חסר רק מושג.

להכיר את nginx

כיצד כדאי לגשת ולהכיר את nginx (מבוטא כ "engine X")?
אולי מדריך התקנה והרצה? אולי התעמקות במבנה הארכיטקטורה? אולי השוואה ל Apache httpd (שגם אותו – רובנו לא ממש מכירים)?

נראה לי שהדרך היעילה ביותר היא פשוט לגשת לקונפיגורציה. הקונפיגורציה היא הממשק העיקרי של מפתחים עם nginx – והיא משקפת בצורה טובה את אופן העבודה של nginx.

לקחתי כדוגמה את הגדרות ברירת-המחדל (או לפחות אלו שמגיעות עם ה Docker Image הרשמי). נראה לי שזו התחלה טובה. בחלק הבא אנו נעבור פריט פריט על הקונפיגורציה. מתוך הפריטים – נלמד כיצד nginx עובד ומקונפג. כלומר: זהו טקסט לקריאה – ולא טקסט לרפרנס.

קובץ ההגדרות הראשי של nginx נקרא nginx.conf ולרוב נמצא בתיקיה etc/nginx/.

אתם בוודאי שואלים את עצמכם "איזה פורמט זה?". ובכן, זהו לא פורמט "סטנדרטי" – זהו פורמט ספציפי שבו משתמשים ב nginx – המושפע מ C-Syntax. בואו נתחיל:

  1. השורה הראשונה היא דוגמה טיפוסית למה שנקרא בקונפיגורציה directive (= פקודה/הוראה). לאחר ה directive יש רווח ואז מספר משתנה של פרמטרים. את ה directive מסיים הסימן ; – שהוא חשוב.
    1. הדיירקטיב של user מגדיר באיזה משתמש (מערכת) התהליכים (OS processes) של nginx ירוצו. לא משהו שנשנה, בד"כ.
  2. כמה Worker Processes להפעיל.
    בניגוד ל Apache Httpd המייצר worker process לכל connection, ב nginx ה worker processes עובדים בסוג של Event Loop ומטפלים כ"א במאות ואלפי connections. הרי, nginx נכתב בכדי להתמודד עם האתגר של C10K – טיפול של יותר מ 10,000 connections בו זמנית מתוך שרת אחד.

    1. כלל האצבע המומלץ הוא להגדיר מספר workers כמספר ה CPU cores הזמינים לנו. את זה ניתן להשיג בעזרת הערך auto. אני לא יודע לומר למה קבעו אותו פה ל 1.
    2. אם הפעולות חסומות ב I/O (למשל: nginx משמש בעיקר ב proxy) – אזי כלל האצבע אומר לקבוע את הערך ל 1.5-2 ממספר ה cores הזמינים למכונה.
  3.  מה הרמה המינימלית של Log level, שאותה נרצה לשמור לתוך לוג ייעודי לבעיות? אפשר להגדיר כמה קבצים כאלו, ברמות (levels) שונות.
    שימו לב ששמות ה log levels ב nginx הוא קצת לא-סטנדרטי.
    בכדי לקבל logs ברמת debug יש להשתמש ב executable של nginx שקומפל עם פרמטר של with-debug–. זהו שיקול של אופטימיזציית ביצועים (לקמפל בלי משמע לדלג על הרבה בדיקות של רמת ה log level).
  4. שם קובץ שבו יישמר מספר התהליך (ברמת מערכת ההפעלה) שאותו קיבל ה master process של nginx.
  5. כאן אנו נתקלים בכמה דברים חדשים:
    1. יש לנו תחביר מעט שונה: תחביר של context בקונפיגורציה.
      1. context מגדיר scope, וכל ה directives שהוגדו בתוכו זמינים רק לו, או ל contexts שנמצאים בתוכו.
      2. יש בקונפיגורציה קונספט של הורשה, כך ש context מקבל את כל ה directives של ה context מעליו – אך לא ליהפך. directive שהוגדר בתוך context ידרוס את ההגדרות של אותו directive שהוגדרו ב context חיצוני יותר (יש גם יוצאי-דופן). החלק הזה שימושי בעיקר ב contexts של server ו location – שלא הגענו אליהם עדיין. אבל שם מוגדרת רוב הקונפיגורציה הספציפית למערכת שלנו.
      3. הרמה הגבוהה ביותר נקראת ה main context, והיא לא מפורשת – אך מתנהגת כ context לכל דבר. סעיפים 1-4 בעצם הוגדרו בתוך ה main context.
    2. ספציפית ה event context מנהל את מה שקשור לניהול connections. היה כנראה יותר נכון לקרוא לו connections.
      1. אנו מגדירים כאן שכל worker process יוכל לפתוח עד 1024 connections. בקשת ה connection ה connection ה 1025 יצטרך להמתין עד ש connection קיים ייסגר – על מנת שיטופל.
      2. אם nginx משמש להגיש קבצים סטטיים – אזי נוכל להגיש קבצים ל 1024 connections. צריך לזכור שדפדפנים עדיין פותחים 2-3 simultaneous connections ל host אם הם צריכים כמה קבצים. אם nginx משמש כ proxy אז המספר הרלוונטי הוא חצי – כי חצי מה connections יפתחו ל application server שמאחורי ה nginx – מה שנקרא במינוח של nginx ה upstream.
      3. הארכיטקטורה של nginx מאפשרת ל nginx לצרוך רק כמה MB של זכרון לכל 1000 connections פתוחים. התוכן שמועבר ב connections (ה buffers אם יש הרבה מידע, ויש פער בקצב ההעברה של ה client וה applications server) הוא גורם שעלול להגדיל את צריכת הזיכרון בצורה מורגשת.
      4. אם צריכת הזיכרון קטנה כ"כ, למה לא להגדיר ערך של 10,000 ב directive הזה?
        אליה וקוץ בה: ללינוקס יש מגבלה 1024 file descriptors ל process, ויש לשנות את המגבלה הזו – בכדי שנוכל להפעיל באמת יותר מ 1000 connections ל worker. ב distros מסוימים של לינוקס יש מגבלה קשיחה ל 4000 open files descriptors ל process.
      5. מסקנת ביניים חשובה: אנו יכולים להגדיר כל מיני דברים בקונפיגורציה של nginx, אבל לא פעם ההגדרות הללו הן לא מה שיקרה בגלל מגבלות / התנהגויות של פרוטוקול ה HTTP, של מערכת ההפעלה, או של ה Application Server שלנו. העבודה ב nginx היא הרבה פעמים עבודה ב infrastructure רחב יותר מסביב ל nginx.
  6. כאן אנו נתקלים ב directive מיוחד של הקונפיגורציה של nginx: ה include.
    1. בעצם מה שקורה הוא שכל תוכן הקובץ המתואר "מוכנס" (inlined) בקונפיגורציה במקום שורת ה include. ה include מאפשר ניהול נוח יותר של קונפיגורציה בקבצים קטנים וממוקדים יותר.
    2. במקרה הזה מדובר על קובץ די משעמם הכולל את ה types context ובו שורה ארוכה של מיפויים בין MIME-types לסיומות של שמות קבצים.
    3. ה directive הבא מתאר fallback: איזה MIME-type להצהיר אם לא מוגדר לנו MIME-type לסיומת של קובץ. גם משעמם.
    4. השימוש בקובץ שהוא included יכולה לגרום לבלבול בהתחלה: בקובץ לא מוגדר ה context בו אנו פועלים ואנו יכולים בטעות להגדיר directives לא רלוונטיים. nginx עשוי לזרוק warnings בעליה (שלא נראה אותם) או פשוט להתעלם – ואז לא נבין מדוע הוא לא מתנהג כפי שציפינו. אולי צריך לשנות עוד קונפיגורציה?
      ארחיב על העניין בזה בהמשך.
  7. כאן אנחנו מגדירים פורמט מסוים ל access log ושומרים את הפורמט בשם "main".
    ה Access log של nginx הוא כלי שימושי למדי, אשר קיומו הוא לעתים אחת הסיבות מדוע אנו מציבים nginx כ reverse-proxy לפני שרת האפליקציה שלנו. הוא שומר שורה בקובץ הלוג לכל בקשה שעברה דרך שרת ה nginx – מה שיכול לסייע לניתוח ה traffic שאנו מקבלים.

    1. לעתים, כאשר ה nginx מטפל בכמות גדולה של תעבורה (נניח: אלפי בקשות בשנייה) – יש היגיון לסגור את ה access log כהתנהגות ברירת מחדל, או לכתוב סלקטיבית רק חלק מהרשומות.
  8. אנו כותבים access log במיקום מסוים, וע"פ פורמט ה main שהגדרנו שורה קודם.
    1. את הפורמט של ה error_log לא ניתן לשנות.
  9. sendfile היא קריאת מערכת של לינוקס שמעתיקה נתונים בין 2 file descriptions כפעולת kernel, בלי להעתיק נתונים ל user spaces. כאשר nginx משמש להגיש קבצים סטטיים (קובץ ל http connection) – היא יכולה לייעל מאוד את עבודתו.
    1. מצד שני, sendfile לא עובדת עבור file descriptors של UNIX sockets, וסתם מציבה מעט overhead מיותר. שרתי רובי (Unicorn למשל) מתקשרים מול nginx על גבי Unix socket ולא קריאות Http (מה שחוסך ב latency לתרגם את המידע לפורמט HTTP, ובחזרה). עבורם – לא נרצה להשתמש ב sendfiile.
  10. tcp_nopush היא אופטימיזציה של לינוקס / FreeBSD שאומרת ל TCP למלא packets לפני שהוא שולח אותם. למשל: לשלוח את ה headers של תשובת ה HTTP באותן tcp packets עם ה body. זו אופטימיזציה ל throughput. מהצד השני קיימת התנהגות הנקראת tcp_nodelay שהיא אופטימיזציה לצמצום ה latency. "כשיש משהו – תשלח".
    1. אם nginx מגיש הרבה תוכן, ה tcp_nopush עשויה להיות אופטימיזציה יעילה למדי: שימוש יעיל יותר ב bandwidth של הרשת, כי ip+tcp headers הם overhead על כל pakcet,  וגם שימוש יותר יעיל בזיכרון של nginx (בעקבות ניצולת גבוהה של ה buffers).
    2. במקרה שלנו האופציה הזו כבויה. לא רוצים להפעיל אותה בתצורת ברירת-המחדל, אבל רוצים להזכיר לנו על קיומה.
    3. tcp_nopush היא קונפיגורציה שתפעל רק עם sendfile מופעלת. התלות הזו מוזכרת בתיעוד הרשמי – אבל היא מסוג הדברים שאפשר לפספס ולבזבז שעות בכדי להבין מה חסר. כמו כן תיאור הפקודה מסתמך על הבנה עמוקה של לינוקס / http. התיאור בתיעוד הרשמי הוא דיי לקוני למי שלא מכיר את המנגנון:

      1. נ.ב. – נאמר לי שאם נראה לי ש nginx מתועד בצורה לקונית, ומאפשר למשתמשים שלו מרחב הגון של טעויות – עלי לנסות לקנפג Apache Httpd. לא זכיתי.
  11. הנה סופסוף קונפיגורציה שמאוד קל להבין: אנו רוצים לעשות שימוש חוזר ב tcp connections שנוצרו (three-way-handshake, או שבע ב https, וכד') על מנת לשרת בקשות HTTP מאותם מקורות. אני מניח שאנחנו זוכרים ש keep_alive ארוך הוא מצוין לביצועים של ה clients, אבל מפחית את ה utilization של ה connections בצד השרת – וזה מחיר בזיכרון / מקביליות שהשרת משלם עליו.
    1. בכדי לאפשר keepalive connections גם מול שרת האפליקציה, עלינו להגדיר את ערך ב keepalive directive שב upstream context. פרמטר זה אומר כמה connections אנו מרשים לשמור "בחיים" במסגרת keepalive.
    2. עוד דוגמה לקונפיגורציה תלויה, ולא בהכרח צפויה היא ה directive הבא בתוך context ה location או ה server:
      ;"" proxy_set_header Connection
      המשמעות כאן היא שאנו דורסים את ה header בשם Connection. בכדי למנוע מצב בו ה Client שלח ל nginx את ה header עם ערך close (הוא מבקש מאיתנו לסגור לאחר הבקשה את ה keep_alive connection), ואנו נעביר את ה header הלאה ל Application Server – והוא יבין ש nginx ביקש ממנו לסגור את ה connection. קומדיה של טעויות.
    3. כאשר אנו קובעים keepalive_timeout מומלץ גם להגדיר את proxy_http_version (גרסת ה http שאנו עובדים עם שרת האפליקציה שאנו עושים לו proxy) ל 1.1. אם ה nginx והשרת החליטו לעבוד ב http 1.0 (ברירת המחדל מצד nginx) – אז לא יהיה שימוש ב keepalive.
  12. אני מניח שאתם מכירים את דחיסת ה gzip: לא הדחיסה הכי אגרסיבית, אבל יעילה מבחינת ההשקעה של ה CPU לצמצום ה bandwidth ובעלת תמיכה רחבה בקרב דפדפנים ורכיבי-רשת שונים. לכאורה בחירה ב gzip היא no_brainer, אך דבר שעלול לגרום למשהו לא לעבוד – לא צריך להיות ב default ולכן אני מניח שהיא נמצאת ב comment.
  13. זוהי שורה חשובה מאוד: בעצם כאן אנו עושים include לכל קבצי הקונפיגורציה בתיקיה בשם conf.d. בתיקיה הזו נהוג להחזיק קובץ לכל Virtual Host ועוד קובץ בשם default.conf המכיל הגדרות ברירת-מחדל של ה server context. ה server context חייב להיות מוגדר בתוך ה http context (אחרת הוא לא תקף).
    1. Virtual hosting – הוא הרעיון שעל שרת פיסי אחד אנחנו מארחים כמה שרתים "לוגים", למשל: אפליקציות שונות. כאשר nginx (או בזמנו: Apache httpd) אירח כמה אתרי-אינטרנט לוגיים – זה היה ממש virtual hosting. בעזרת patterns על הבקשה (למשל: רושמים כל אתר ב DNS כ path מעט שונה) – ה nginx יודע לאיזה שרת לוגי להפנות את הבקשה.
      1. התצורות המקובלות קצת השתנו, בעוד הטרמינולוגיה נשארה. גם כאשר ה nginx משרת כ reverse proxy אך הוא מתנהג מעט שונה לכמה urls שונים – אנו עדיין קוראים לזה virtual hosting.
        ה context בשם server מגדיר "שרת וירטואלי" שבו אנו מטפלים, כלומר: port ותחילית של URL, בעוד קיימת רמה נוספת בשם location המתארת urls ספציפיים יותר בתוך השרת.
      2. בתוך ה location אנו יכולים לאכוף / לעדכן headers מסוימים, לנהל רמה מסוימת של אבטחה (למשל: Authentication או סינון בקשות העונות ל patterns מסוימים), לפצל traffic עבור A/B Testing ועוד. בפוסט הזה לא אכסה אף אחד מהנושאים הללו.
    2. ההפרדה לקבצים היא מודולריזציה חשובה של הקונפיגורציה של nginx, בעיקר כאשר הקונפיגורציה גדלה.
  14. אם אתם עדיין מסוגלים לקלוט עוד מידע – אז משהו בוודאי נראה לכם משונה בשורה הזו. היא הופיעה כבר קודם לכן!
    1. זה נכון. שורה זו איננה חלק מהקונפיגורציה ברירת-המחדל, אך רציתי להשתמש בה להזכיר עקרון חשוב: ה nginx הוא (לרוב) רכיב קריטי ב infrastructure, ולא כ"כ קשה לעשות בו טעויות בהגדרות.
    2. המלצה ראשונה היא לערוך את הקונפיגורציה של nginx ב Editor או IDE שמכיר את מבנה הקובץ ויודע להתריע בפני שגיאות אפשריות. יש plugins כאלו ל IntelliJ ול VS Code, ובוודאי לעוד עורכים.
    3. כאשר אנו טוענים קונפיגורציה שגויה (= עם טעות) ל nginx, ייתכנו אחת מ-3 תגובות:
      1. nginx יעלה כשורה, ויום אחד (ע"פ חוק מרפי – השיא יגיע ביום שישי בערב) – תתגלה בעיית production.
      2. nginx יעלה כשורה, אך יזרוק warnings בעליה – אבל מי בודק אותם בכלל לפני שיש בעיה כללית? הבעיה עוד תגיע…
      3. nginx יסרב לעלות – ואז יש לנו פאדיחת deployment קטנה.
    4. אף אחת מהאופציות היא לא נהדרת.
      1. על מנת לצמצם את הבעיות, ניתן להפעיל בשלב מוקדם ב deployment את הפקודה nginx -t. הפקודה הזו תבצע בדיקות על הקונפיגורציה ותזרוק error אם נמצאה בעיה. וריאציה אחרת: static analysis בזמן ה build.
        למשל: ההגדרה הכפולה שביצעתי למעלה לא נתפסה ע"י ה IDE (בעיות רבות אחרות – נתפסות), אך היא נתפסת כ warning ע"י nginx -t.
      2. דפוס שימוש מקובל הוא לבצע בדיקת קונפיגורציה לפני reload, ולהכשיל את ה reload על כל
        בעיה אפשרית: nginx -t && nginx -s reload. נראה שזו התנהגות ברירת-המחדל ב AWS Beanstalk, למשל.
      3. חברות עתירות משאבי כ"א עשויות להשקיע אף יותר בבדיקות התקינות של קונפיגורציות ה nginx שלהן. כפי שאמרנו, בעיות קונפיגורציה רבות נובעות מתוך פרוטוקולי התקשורת או מערכת ההפעלה – לא משהו ש nginx יידע בהכרח לבדוק עבורנו.
תצורה טיפוסית של התקנת nginx כ reverse proxy בענן.
האם nginx באמת נדרש?!

אז למה בעצם צריך nginx עבור ג'אווה (JVM) או Go?

לכאורה כאשר יש לנו פלטפורמה המסוגלת בקלות לטפל במספיק connections במקביל, וכאשר יש לנו Load Balancer – אנחנו "מסודרים". מדוע רבים עדיין מתקינים ומתפעלים nginx בין ה LB ל Application Server/Process?
האם זה רק כוחו של הרגל?

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

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

  • Access Log – רישום כל הקריאות שנעשו לשירות, עם overhead מינימלי.
  • תפקיד קלאסי של Reverse Proxy – "להחביא" כתובות פנימיות של שרתים, משיקולי אבטחה.
  • Easy SSL termination – כאשר יש תקשורת https.
  • Caching והגשה סופר-יעילה של static resources.
  • GZIP
  • אפשרות קלה להוסיף headers מסוימים על הבקשות המתקבלות.
  • אפשרות לשכתב URLs או להפנות URLs ל ports או URLs פנימיים אחרים.
    • אופציה פופולרית לדוגמה: הפניית (redirect) קריאות http ל https.

היכולות האלו שימושיות בעיקר עבור שרתים שהם Customer Facing, כלומר – אלו שאליהם פונים ישירות המשתמשים (דפדפנים / אפליקציות מובייל).
אם אנחנו עובדים עם AWS אזי Cloudfront מספק פתרון עדיף להגשת static resources, וה ELB (בעיקר הווריאציה שנקראת ALB) מספקת יכולות של Reverse Proxy, SSL Termination ועוד. למשל: לאחרונה הוסיפו את היכולת להפנות קריאות http ל https בסימון של checkbox. שרתי ג'אווה ו Go מספקים Access Log יעיל גם כן.

אם אתם עובדים על גבי תשתית ענן – ליתרונות הנ"ל יש תחליפים מובנים.
ככל שהזמן עובר, ספקי הענן "נוגסים" בצורך ב nginx כ reverse proxy ומצמצמים אותו.

אבטחה

סט יכולות חשוב נוסף ש nginx ממלא הוא יכולות אבטחה:

  • אימות זהות (Authentication) – מה שהופך אותו מ "סתם Reverse Proxy" ל "API Gateway מ-ט-ו-ר-ף"
    • המונח API Gateway הוא באזז רשמי לשנים 2017-2018. אולי גם יגלוש ל 2019.
    • ל nginx יש מגוון יכולות Authenticatoin ואפילו SSO, רבות מבוססות modules (כלומר: plugins).
    • רבים ממשמשים פתרונות אימות לבד, או בעזרת צד-שלישי כמו OKTA או Auth0. גם שירותי הענן השונים נוגסים בנישה הזו של nginx.
  • הגבלת מספר ה connections ע"פ לקוח (נניח: טווח כתובות IP)
  • אפשור / חיוב הצפנה קרי TLS/SSL – הרבה יותר קל ליישם על גבי AWS ELB.
  • היכולת לחסום תעבורה מאזורים גאוגרפים שונים. למשל: אני פועל בארה"ב וארצה לחסוך תעבורה מסין (שיותר סביר שהיא לא-לגיטימית)
    • על בסיס module
    • יכולת בסיסית ומקובלת היום של WAF
  • חשוב לציין שעצם כך שהשרת הראשון שה Traffic רואה הוא שרת פשוט (לא מסובך, פחות באגים סבירים) המאמת את השימוש בפרוטוקולי הרשת השונים (קרי IP/TCP/HTTP, וכו') – זה כבר יתרון אבטחה חשוב שעשוי למנוע לנו בעיות.
    • היתרון הזה נכון גם ל ELB.

כפי שאתם רואים, גם כאן מוצרי צד-שלישי ויכולות ענן מכרסמות ב"טריטוריה" של nginx.

מצד האבטחה עולה שאלה נוספת: האם כדאי לנהל nginx לכל שירות – או אחד לכל המערכת?

תפקיד חשוב ש nginx יכול למלא הוא לאכוף כללי אבטחה על כלל המערכת. למשל: מדיניות אבטחה המחייבת headers של HSTS ו X-XSS-Protection ומצד שני – הסרה של כמה headers פנימיים מה requests (למשל session token). קל יותר, ונכון יותר לאכוף כללים כאלו פעם אחת ברמת ה nginx מאשר בקוד מספר רב של פעמים. ההתנהגות מ nginx ברמת ה HTTP היא מובנת וצפויה יותר מאשר התנהגויות של frameworks ומנועים שונים שאנו עובדים איתם.

  • nginx לכל שירות יכול להופיע כהתקנה אחת לכל cluster של השירות (s1 ו s2 בתרשים למטה) או nginx על כל server node (בתרשים למטה – s3. התצורה הזו יותר מקובלת).
  • nginx יכול להיות גם מותקן כ cluster יחיד על כלל המערכת. ה cluster של ה nginx חשוב מאוד עבור high-availability..

למרות ש cluster יחיד של nginx לכלל המערכת נשמע הגיוני מבחינת ריכוז השליטה על האבטחה, בעידן הענן קל יותר לנהל nginx על כל מכונה (או pod, אם אנחנו עובדים עם קוברנטיס). את אחידות כללי האבטחה בין כל מופעי ה nginx, עלינו לנהל בעזרת configuration management. נניח: קובץ אחיד conf. שיהיה Included בכל מופעי ה nginx במערכת.

גמישויות נוספות

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

לעתים מדובר גם בצרכים שהם לא מידיים כמו רצון להגדיר throttling, סתם לדבר תעבורה לשירות מסוים, ביצוע a/b testing ועוד. אלו דברים שהכלי שנקרא nginx יכול לספק בצורה טובה ומהירה – למי שיודע להשתמש בו.

אם אתם עובדים בענן, על פלטפורמה ללא מגבלות concurrency, מעולם לא שיניתם קונפיגורציה של nginx ואתם גם לא ממש יודעים איך לעשות זאת – אולי באמת nginx הוא רכיב מיותר שמוסיף סיכונים לתקלות – ללא ערך המצדיק את הסיכון הנוסף הזה.

סיכום

ראינו פרטי קונפיגורציה של nginx בכדי לספק תחושה והתמצאות בסיסית בכלי, וקצת דנו בשיקולי ארכיטקטורה ומקומו ב System Landscape.

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

להלן כמה לינקים שימושיים שאספתי בזמן כתיבת הפוסט, שעשויים לשרת אתכם הלאה:

שיהיה בהצלחה!

Infrastructure as a Service: Terraform

אם אני מעוניין לתת דוגמה לפער בין התיעוד הרשמי ל\"מידע מהשטח\" בעולם של AWS – אני נותן את הדוגמה של Terraform.
בתיעוד של אמזון, הכלי המדובר להגדרת תצורה רחבות היקף הוא Cloud-Formation (של אמזון) – אבל בפועל, אני לא מכיר אף אחד שעובד עם Cloud Formation לאורך זמן: כולם עוברים ל Terraform.
אם תשאלו מישהו שעובד כבר עם AWS – הוא כנראה יפנה אתכם ישר לשם.

Terraform הוא כלי שמנסה לספק כמה מטרות:

  • לספק תמונה אחת לכל תשתית המערכת שלנו.
  • לתמוך בתצורות production מודרניות (ענן, וכו\')
  • לספק אופן צפוי ובטוח לעדכן את תצורת התשתית שלנו.
  • לספק Workflow אחיד לעבודה מול ספקי-תשתיות שונים.
    • משום מה, אנשים נוטים להניח ש Terraform מאפשר להגדיר תצורה לספק ענן אחד (נניח גוגל) ואז להפעיל אותה על ענן אחר (נניח Azure). זה בהחלט לא המצב! זה לכל היותר מיתוס!

נפתח בדוגמה

מה ש Terraform (מעתה אשתמש בקיצור: טרה) מאפשר – הוא להגדיר תצורה רצויה בצורה פשוטה – ו\"לגרום לה לקרות\".

את הגדרת התצורה עושים בקבצים עם סיומת  tf. כמו זה:

\"היי!\" – אתם עשויים לומר, גם בעזרת  aws-cli , או סתם סקריפט פייטון שמשתמש ב AWS SDK – אני יכול להרים instance בצורה פשוטה למדי! אז מה ההבדל?

יש כמה הבדלים:

  • בעזרת טרה, אני משתמש באותה שפה ואותו תהליך אני יכול להגדיר תצורה על תשתיות שונות: AWS, גוגל, Dyn, Cloudflare – ועוד. זה קצת יותר פשוט מלהתחיל לעבוד עם כלים שונים ו SDKs שונים.
  • טרה מוסיף \"חוכמה\" של חישוב המסלול הנכון להגיע למצב הרצוי: טרה בודק מה המצב הקיים, ומחשב אלו שינויים יש לבצע בכדי להגיע לתצורה הרצויה. לעשות את זה לבד – זו הרבה מאוד עבודה!

נמשיך:
לאחר שיצרתי את קובץ הגדרת התצורה שלי, אני מקליד את הפקודה

$ terraform plan
ומקבל את הפלט הבא:

מה קרה כאן?
עדיין לא קרה שומדבר. המצב שלי באמזון עדיין לא השתנה.

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

הפלט מופיע בתצורה של diff:

  • + – על משאב חדש שנוצר.
  • – על משאב שמבוטל.
  • ~ – על משאב שעובר שינויי תצורה.
  • -/+ – על משאב שמבוטל, ומוחלף במשאב אחר.

ניתן גם:

  • להעשיר את הפלט בצורה מותאמת אישית, בעזרת פקודות output (או פונקציות כמו count) בהגדרת התצורה. למשל: אני רוצה לדעת איזה IP ה ELB שלי הולך לקבל, או כמה instances מסוג מסוים יהיו על מכונות spot.
  • להשתמש בפקודה terraform graph על מנת לקבל גרף המתאר את סדר הפעולות המתוכנן. טרה עובד עם פורמט גרפים סטנדרטי, ואת הפלט של הפקודה ניתן לטעון ל webgraphviz.com – על מנת לצפות בגרף בצורה ויזואלית.
    • הפלט של פקודת plan מסודר ע\"פ סדר הא\"ב על מנת לספק יכולת סריקה מהירה של התוכן.
  • לשמור את תוצאת התכנון, בעזרת הפרמטר out=path-, כך שיובטח שבעת הפעולה תרוץ התוכנית שבחנתי.

כאשר אני מקליד:

$ terraform apply

עברה כחצי דקה ואני מקבל את ההודעה הבאה:

אני יכול גם להסתכל ולראות את ה instance שנוצר ב console

או פשוט לקרוא ל \"aws ec2 describe-instances\" ולראות אותו ב console.

לאחר ביצוע קבוצת ה apply, טרה יצר לנו שני קבצים: קובץ tfstate. וקובץ tfstate.backup.

הקבצים הללו (יש להם תוכן שונה זה מזה) מתארים את ה last-known-state של התצורה המרוחקת (כלומר: מה שקורה בפועל), והם ישמשו אותנו לפעולות הבאות.

במקרה שלנו הקבצים נוצרו רק לאחר פעולת ה apply (כי נוצר מצב שלא ניתן לשחזר בעזרת ה API של AWS בלבד), אבל גם בהרצת פקודת plan ייתכן והקבצים הללו ייווצרו / יתעדכנו.

את הקבצים הללו אני אוסיף ל gitignore. כך שלא יגיעו ל source control. מדוע – אסביר בהמשך.

נמשיך:

נעשה שינוי קטן בכדי לראות את ניהול הדלתאות של טרה:

ביצעתי שני שינויים. קראתי ל terraform plan וקיבלתי:

האמת שהייתי מצפה בשינוי instance_type שזו תהיה פעולת +/- – אבל כנראה בגלל שספציפית באמזון השוני בין t2.micro  ל t2.nano הוא רק ב CPU capping – אני מניח שה instance לא מוחלף פיסית. אם הייתי משנה AMI, למשל – זו הייתה פעולת +/-.

לאחר terraform apply אקבל:

הפעולה לקחה קצת יותר זמן ממה שציפיתי – אבל הזמן הזה קשור לאמזון ולא לטרה.

כמובן שלאחר ה apply אני מעדכן את הקוד בגיט: אני מאוד רוצה snapshot נכון של התצורה שהפעלתי בכל רגע: אנחנו מדברים הרי על infrastructure as code. אולי שווה אפילו לעשות מקרו שלאחר apply מוצלח דואג להכניס לעדכן commit – שלא אשכח…

והנה המצב ב Aws Console:

ניסוי אחרון, נעשה שינוי אפילו פחות מהותי ב instance:

נריץ plan:

נריץ apply:

הפעם השינוי היה מהיר מאוד. זה רק שינוי של metadata ופה בכל מקרה לא ישתנה לי ה instance ID.

נכניס את הקוד לגיט. נבדוק את ה console עכשיו:

כדי שלא נשלם הרבה $$, בואו נסגור את ה landscape שיצרנו:

פקדנו terraform destroy, אימתנו שזה לא בטעות – yes, וזהו. תוך כמה שניות כל המכונות (כלומר: instance אחד בגודל nano) – למטה.

אם אני מעוניין בניתוח של מה המשמעות של destroy אני יכול לפקוד: terraform plan -destroy – ולקבל הדמייה / הערכה.
destroy כמובן לא ייגע במשאבים שלא הוגדרו ע\"י טרה.

Great Success!

מה קרה כאן, בעצם?

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

הקובץ הוא בפורמט HCL (קיצור של HashiCorp Configuration Language) ומתאר בצורה דקלרטיבית קונפיגורציה של Infrastructure. הוא מבוסס על פורמט \"רזה\" של JSON בשם UCL בה משתמשים בקונפיגורציה של nginx.
אפשר לכתוב את הקבצים בצורה ה JSON-ית ה\"כבדה\" (מרכאות מסביב לכל אלמנט, וסוגריים מסולסלים מסביב לכל Key-Value pair) – או בצורה ה\"רזה\". אנחנו, כמובן, ניצמד לצורה ה\"הרזה\".

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

  • טרה תומך בעשרות \"ספקי תשתיות\", כולל Cloudflare, New Relic, MailGun, ועוד. 
  • בתוך provider מסוג aws\' עלי לספק את ה region. בחרתי region זול (בכל זאת, לבלוג אין הרבה תקציבים…)
  • צורת העבודה, והשפה (HCL) הם אחידים עבור כל ספק תשתיות – אבל הערכים שזמינים לכל ספק הם שונים לחלוטין. ל Dyn אין בכלל regions, אבל נדרשים לתאר את שם הלקוח (שלכם). בענן של גוגל יש לתאר region אבל גם project.

    2. בכל ספק תשתיות, ניתן להגדיר כמות וסוגים שונים של resources. לכל סוג resource – הגדרות משלו.
    הפורמט הוא:

    resource  \"_\" \"\" {
     
    }
    כאשר: 
    • provider צריך לתאום ל provider שהגדרנו בתחילת הקובץ
    • ה type הוא ערך מתוך מרחב ערכים שמוגדל לכל provider. למשל ל dyn יש resource בשם \"dyn_record\"
    • ה resource_name הוא שם שיזהה בצורה ייחודית את המשאב הזה בתוך ההגדרות של טרה.
    • ה config הוא רשימה של שדות, שמרחב הערכים מוגדר ע\"פ הצמד provider ו type.

    3. AMI, כפי שאתם בוודאי מכירים הוא Amazon Machine Image. מאיפה בוחרים AMI, אם לא שלכם? אני בוחר בעזרת ה AMI Locator של אובונטו (רק AMIs של אובונטו). שווה להכיר ש:

    • ל regions שונים יש AMI ID שונים: ה AMI של אובונטו בצפון וריגיניה וזה של אורגון אולי יהיה זהים ברמת הביטים – אבל יהיה להם ID אחר.
    • במכונה מסוג t2.micro או t2.nano אני יכול להשתמש רק ב AMI שעובד עם hvm (וירטואליזציה) ו ebs (אחסון). סתם שווה להכיר.
    • אפשר לקבל במהירות עוד מידע על AMI בעזרת הפקודה הבאה:
      aws ec2 describe-images –image-ids ami-a60c23b0

    בגלל שה id של ה image לא אומר לי כלום – אני מוסיף הערה. Infra as Code משמעו גם להתייחס לקונפיגורציה שלכם ברצינות שמתייחסים לקוד Production שהולך ומזדקן עם הזמן.

    4. בתוך ה config של resource type מסוים יכולים להיות אובייקטים מקוננים, כמו אובייקט ה tags.
    tags מסוימים, כמו Name – הם משמעותיים מאוד ב AWS.

    ב HCL מגדירים:

    • ארגומנט (כמו אלו של ה resource) כקלט לקונפיגורציה. משהו שאנחנו יודעים ומציבים בקונפיגורציה.
    • Attribute (\"תכונה\") כפלט של הקונפיגורציה בעת ההרצה – פקודת apply. משהו שלא ידענו לספק בעצמנו אבל מתאר את התצורה. למשל: כאשר אני יוצר instance ב AWS אני לא יודע איזה כתובת IP הוא יקבל – אבל ייתכן והכתובת הזו צריכה לשמש אותי לקונפיגורציה בהמשך, למשל: בכדי ליצור רשומת DNS.
      • ה attributes הם חלק חשוב מה state – שנגדיר אותו מייד.
    כאשר אנו מתכננים plan, טרה מאחורי הקלעים יוצר גרף (מסוג DAG) של תוכנית הפעולה להגעה למצב הרצוי. הקונפיגורציה עצמה לא מתארת את סדר הפעולות (אלא אם נוסיף אילוץ – depends_on) – טרה מחשב אותו בעצמו.
    טרה ידע מתי הוא יכול למקבל פעולות – והוא ינסה לעשות זאת ככל האפשר, אבל יימנע ממקבול מתי שהוא מסוכן. למשל: כאשר אני מחליף instance ממכונה קטנה לגדולה – טרה לא יודע מה המשמעות של כמה השניות שהמכונות הללו יחיו במקביל. בגלל שזה עלול להיות מזיק – הוא יימנע מכך ויעשה את סדר הפעולות סדרתי.
    קונספט חשוב נוסף הוא ה State
    בטרה מדברים על:
    • Last known state – המורכב מהארגומנטים השונים, משתני הסביבה בעת ההרצה, וכו\' – כל מה שטרה עשוי להשתמש בו בעתיד. הוא נשמר בקבצי terraform.tfstate ו terraform.tfstate.backup.
    • Actual state – המצב בפועל של התשתית שלנו. זהו מצב לא ידוע כי ייתכן ויש שינויים שלא נעשו ע\"י טרה, או אפילו לא ע\"י העותק הנוכחי של טרה.
    לפני שטרה מבצע תוכנית או apply הוא פונה ל APIs ולומד מה שהוא יכול על ה actual state. אנחנו בהחלט רוצים להימנע מטעויות.

    הוא גם ייעזר ב local state על מנת לבצע:

    • מיפוי נכון, למשל ה resource x הוא בעצם instance עם id כזה וכזה ב EC2
    • לשפר את מהירות הפעולה: אם instance id קיים – אני יכול להניח שכמה attributes שלו לא השתנו.

    ולכן ה state הוא חשוב לפעולה תקינה. אם אתם עובדים לבדכם, שמירת קבצי ה state בדיסק הלוקלי הוא אולי רעיון טוב, אבל כשמגיעים לעבוד בקבוצה – יש מחשבה לשתף את קבצי ה state ב source control. יש בזה כמה בעיות:

    • מישהו, מתישהו, הולך להפעיל את טרה עם קבצי state שאינם up-to-date (למשל: שכח לעשות git pull לפני). אתם יודעים, כל טעות בפרודקשיין עשויה להיות אסון. לא טוב!
    • כל קבצי ה state נשמרים כ plain text וכוללים את כל משתני הסביבה. יש סיכוי טוב שסודות (ססמאות, keys, וכו\') יכנסו לתוכן הקבצים. אתם לא מכניסים ססמאות לתוך ה source control שלכם – אני מקווה!
    בקיצור: ההמלצה הגורפת היא להכניס את הקבצים הללו ל gitignore. ולא לעדכן אותם לתוך ה source control.
    אז מה עושים בקבוצה?

    לטרה יש מנגנון שנקרא remote state (אולי היה אפשר לקרוא לו shared state) ששומר את ה state באחסון מרוכז (consul, S3, ועוד) – ומשתמש בהם משם. כמובן שאת שטח האכסון (למשל: S3) – חשוב להגדיר כמוצפן.

    Remote State דורש קצת עבודת הגדרות – אבל הוא בהחלט הדרך הנכונה לעבוד!

    דוגמה ל folder structure של פרויקט טרה. מקור

    נקודת מפתח שאולי לא הזכרתי מספיק בטרה, היא באמת הפילוסופיה של Infrastructure as Code. אפילו שזה קוד דקלרטיבי – ישמש אתכם מאוד לעבוד בפרקטיקות טובות של קוד.

    למשל: קבצים גדולים? חס וחלילה – קשה להתמצא ולקרוא את הקוד.
    טרה מעודדת לפרק את הקוד לקבצים קטנים יותר: כל הקבצים בסיומת tf. ו/או tf.json. בתוך אותה התיקיה – יעברו merge לפני שיורצו.

    יש כאלו שמפרקים את הפרויקט לקבצים כמו: variables, output ו main – חלוקה בהחלט לא scalable מבחינת ניהול קוד.
    תצורה עדיפה היא חלוקה ע\"פ תוכן: dns, instances, launch_configurations, security_groups, וכו\'.

    כמו כל פרויקט, וקצת מעבר – ההמלצה היא לפרק את הפרויקט לתתי פרויקטים (כמו בתרשים למעלה).
    החלוקה לתתי פרויקטים היא ה bulkhead שלכם! אם משהו משתבש – אתם תרצו שהוא ישתבש ב scope מצומצם ומוגדר היטב.

    כלים נוספים בשפת ה HCL הם:

    מודולים

    אם יש לכם 100 שירותים בפרודקשיין, אני מקווה בשבילכם שאתם לא מנהלים 100 עותקים מאוד דומים של קוד!
    אתם רוצים להגדיר מודול של service_instance, של service_elb, של service_db וכו\' – ולשכפל מעט עד כמה שאפשר קוד.

    Interpolation (התייחסות למשאבים אחרים)

    בעזרת תחביר ה interpolation אנחנו יכולים לקצר ולייעל את הקוד שלנו (וגם להשיג דברים – שאחרת פשוט לא יכולנו). התחביר נקרא לעתים גם \"dirty money\" והוא נראה כך:

    אני מגדיר בתוך רשומת ה DNS את כתובת ה IP של ה ec2 instance בשם example. ערך שאי אפשר לדעת לפני הריצה.
    ה interpolation הוא, כמובן, כלי עיקרי לטרה לקבוע את סדר הפעולות ההרצה.

    משתנים

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

    סיכום

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

    לאורך הפוסט ניסיתי להדגיש גם את האלמנטים של Infrastructure as Code.

    שיהיה בהצלחה!

    —-

    לינקים רלוונטיים

    מדריך לכתיבת מודולים בטרה: https://linuxacademy.com/howtoguides/posts/show/topic/12369-how-to-introduction-to-terraform-modules

    Terraform Comprehensive Training – מצגת שנראה שמקיפה הרבה נקודות:
    https://www.slideshare.net/brikis98/comprehensive-terraform-training

    על DevOps וה DevOps toolkit

    יש כמה הבדלים מהותיים בין פיתוח תוכנה של המתכנת הבודד (\"לבד בבית\") לקבוצה גדולה יותר של אנשים שמפתחת את הפרויקט ביחד. המעבר לקבוצת פיתוח מכתיב צרכים של תיאום וסנכרון, ובסיס הקוד הגדול והמורכב יותר (שהוא פועל יוצא של יותר ידיים עובדות) – דורש תהליכים של \"ניהול תצורה\", Deployment, וניהול הקוד ב Production.   בכדי להתמודד עם האתגרים הללו, קבוצות הפיתוח השתמשו בשני כלי-עוצמה ניהוליים: התמחויות ואחריות, וכך נוצרו התפקידים הבאים:

    • מפתחים אחראים לכתוב קוד
    • איש ה UX אחראי לעצב את חווית השימוש
    • ה DBA מתמחה ב Database Systems
    • ה QA בודק את המערכת
    • איש ה Operations אחראי להתקין את התוכנה ולנטר אותה
    • וכו\'…
    מקור: http://nealford.com/abstracts.html#cd

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

    • איש ה UX רוצה להשיג את חווית השימוש הטובה ביותר – והמפתח רוצה לשחרר פיצ\'רים מהר.
    • איש ה QA רוצה שלמות במוצר – והמפתח רוצה לשחרר פיצ\'רים מהר.
    • איש ה Operations רוצה יציבות – והמפתח רוצה לשחרר פיצ\'רים מהר.

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

    שינוי #1: הגבולות בין אנשי ה QA והמפתחים החלו להיטשטש

    בעקבות צירוף של אנשי ה QA \"לצוותי סקראם\" והגדרת מטרות משותפות ברמה צוותית – ניגוד-האינטרסים המובנה פחת: ה QA שותפים לזמני ה Delivery והמתפתחים אחראים יותר לאיכות.
    מפתחים החלו להיות שותפים פעילים יותר בתהליכי האיכות (בדיקות יחידה, בדיקות אינטגרציה וכו\') דבר שהן השפיע על הגישה שלהם לנושא וגם יצר שותפות-גורל גדולה יותר בין ה QA למפתחים.

    שינוי #2: המעבר לענן ואוטומציה של התפעול

    המעבר לענן הסיר מאנשי ה Operations בארגון כמה עיסוקים הרחוקים מאוד מהמפתחים: טיפול בחומרה ורשתות תקשורת – עולם שלם בפני עצמו. את החומרה והרשת מנהלים בצורה אוטומטית בעזרת תוכנה (להלן Virtualization ו Software Defined Networks) וכך התקרב העולם של אנשי ה Operations לעולם המתכנתים – עולם של כתיבת סקריפטים ~= קוד.    

    בד בבד, התצורות הלוגיות של המערכת (להלן מיקרו-שירותים, ומערכות מבוזרות אחרות) הפכו למורכבות הרבה יותר – לא עוד Cluster בודד של שרתים עם בסיס נתונים שיש לתחזק.    

    היכולת של ה Operations להתמודד עם אתגרים אלו כקבוצה חיצונית לפיתוח – הפכה מורכבת יותר ויותר. חילופי ההאשמות והמתח בין הקבוצות רק הלך וגבר (להלן blaming culture).

    שינוי שאנו נמצאים במהלכו כיום נקרא The DevOps Movement – ההכרה שהדרך הטובה ביותר להתמודד עם סביבות המחשוב המורכבות שנוצרו – הוא בשיתוף פעולה אמיתי בין Operations ל Developers.

    מקור: ThoughtWorks Radar – מארס 2012

     

    כמובן שארגונים רבים לא מכירים בעומק הרעיונות שמאחורי תנועה זו, ופשוט משנים את ה title של אנשי ה Operations ל \"DevOps\" – שם פופולארי יותר.

    מקור: ThoughtWorks Radar – נוב\' 2015

     

    עבודת ה Dev-Ops בארגון

    בשלב הזה אני לסקור את העבודה שיש בפני ה Operations וה Dev (וגם קצת IT וגם קצת QA) בארגון. אני יכול להתחיל בהצהרות ברמה גבוהה \"לדאוג ליציבות המערכת\". הצהרות אלו הן נכונות – הן אם לא תמיד כ\"כ מוחשיות. מה כן מוחשי? כלים ותהליכים. אספתי את סוגי הכלים והתהליכים הקיימים בעולם Dev-Ops לכדי רשימה, ואני הולך לפרט לגבי כל קטגוריה בנפרד. אני אציג תמונה (/ \"סיפור\") מציאותית – אך כמובן לא יחידה: ניתן לתאר את התהליכים הללו בצורות שונות, ולאנשים שונים וארגונים שונים יש תפיסות שונות לגבי גבולות הגזרה בין התהליכים / הכלים / ותחומי-האחריות השונים. אני לוקח כמה הנחות בסט הכלים שאראה:

    • אני מניח שמדובר ב Deployment בענן, ולא ב On-Premises (קרי: אין ניהול של חומרה ורשת פיסיים) – ואני מתמקד ב AWS (עננים אחרים הם דיי דומים בהיבט זה).
    • אני מניח שמדובר באפליקציית ווב.
    • אני מניח ש Docker (או פתרון אחר של Linux Containers) אינו מעורב – אחרת התמונה תשנה במידת-מה.
    אם יצאתם זה עתה מהאוניברסיטה, או אי בודד – פוסט זה יכול לספק תמונה לא רעה על תהליכי פיתוח בקבוצות-תוכנה בתעשיה.
    אם אתם כבר בתעשיה – אני מקווה שתמונה זו תוכל להשלים \"חורים\" שאולי חסרים לכם.

           

    המורכבות של Stack מודרני, מול Stack בן עשור. מקור: https://goo.gl/9yCLtp

    לרבים מההיבטים שאתאר (למשל: Logging, Version Control, וכו\') יש היבטים ברורים מאוד של פיתוח: קביעת conventions לעבודה, למשל –  בד בבד עם היבטים של תפעול (התקנה ותחזוק של השרתים, גיבויים וכו\').

    להזכיר: השאלה החשובה היא לא \"מי צריך לתפעל מה\" אלא: כיצד ביחד, יכולים Operations ופיתוח – להגיע לתוצאות טובות יותר. תזכורת אחרונה: אני בעצם מנסה לתאר את הצרכים דרך הכלים – אך חשוב לזכור את הפער. כלי הוא לא צורך.  

    כלים \"פנימיים\"

    אלו הכלים שלרוב משמשים את הפיתוח. מישהו צריך לנהל ולתפעל אותם. ה\"מישהו\" הזה, הוא הרבה פעמים צוות ה IT) Operations). בסטארט-אפים קטנים – הרבה פעמים האלמנטים הללו מנוהלים ע\"י המתכנתים עצמם.

    דוגמאות לכמה כלים בכל קטגוריה



    Issue Tracking

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

    יש כאן ניגוד אינטרסים קטן: מי שמביט על כלל הפרויקט (מנהל פרויקטים, מנהל פיתוח) מעוניין לאסוף כמה שיותר פרטים על כל משימה – עבור יכולות הניתוח, בעוד מי שעובד עם המערכת (מתכנתים) מעדיף להתעסק עם הכלי כמה שפחות – כי בסוף העיסוק ב Issue Tracking לא מייצר Value ללקוחות.
    Jira (ויש מתכנתים האומרים: ״Jifa״) הוא כנראה הכלי הנפוץ ביותר, שמכסה גם היבטים של ניהול פרויקטים ובאגים – אך יש גם כלים אחרים.

    Version Control

    בסטארט-אפים מקובל בעיקר לעבוד עם Github.com – ולקבל משם שירותים מנוהלים של ניהול קוד.
    ארגונים גדולים, יותר מקובל לעבוד עם Git ישירות או עם GitHub Enterprise (או החלופות שלו) – תוכנה שיש להתקין ולנהל לבד, עם היכולת לאבטח בצורה יותר טובה.

    מלבד ניהול השרת של ה Version Control, יש עניין של ניהול התוכנה עצמה: ניהול ה Repositories, וההרשאות (האם אתם בודקים כל תקופה שעובדים שעזבו הוסרו מהמערכת?), הכשרה לעובדים (לא כולם מכירים את Git), ופתרון בעיות (\"אופס… עשיתי force– למשהו שגוי….\").

    Build

    את הקוד צריך לקמפל (במקרה של Java, Go, #C, וכו\') – או שלא (במקרה של רובי, Python, או ג\'אווהסקריפט).
    לרוב יהיו לנו בדיקות (יחידה, אינטגרציה, ביצועים, אבטחה) אוטומטיות – שירוצו כחלק מתהליך ה Build.
    תוצרי הקוד (תמונות, קבצי JavaScript, קבצים בינאריים) לעתים קרובות עוברים עיבוד נוסף: דחיסה / אופטימיזציה – לתמונות, minification / obfuscation ו unification לקבצי ג\'אווהסקריפט, או bundling לקבצים בינאריים (למשל: אריזת קבצי jar. כקובץ war. או ear. – בעולם הג\'אווה).

    את תהליך ה Build לרוב מריצים על כל submit / push של קוד ל Version Control – על מנת לאתר תקלות בשלב מוקדם ככל האפשר. באופן זה העלות לתקן את הבעיות – פחותה.

    בטעות, יש כאלו שקוראים לתהליך ה Build בשם \"Continuous Integration\", ולשרת ה Build – כ \"CI Server\". זה שימוש שגוי בטרמינולוגיה: Continuous Integration הוא תהליך בו מגבירים את תדירות האינטגרציה בין המפתחים השונים. למשל: כל מפתח מבצע merge של הקוד שלו חזרה ל master / trunk – כל יום!

    ו Git Flow? – הוא כבר לא כ\"כ באופנה. זו בערך הגישה ההפוכה לגישה של Continuous Integration.
    בעולם ה Build יש כלים ברמות שונות: Jenkins או TravisCI הם Build Servers שמנהלים את ה Build Pipeline.
    כלים כמו Maven, SBT, Ant, או Gradle (כולם מעולם הג\'אווה) – הם כלים להגדרת המיקרו של ה build (וניהול תלויות). דומים יותר ל Make או Rake.
    בעולם הג\'אווהסקריפט משימות מסוימות מבוצעות עם כלים כמו Grunt, Gulp, או npm – ואת משימת האריזה עם כלים כמו webpack או Browserify.

    Artifact Management (בקיצור AM)

    תוכנת AM היא כזו שעוזרת לנהל תוצרי תהליך ה-build (למשל: קבצים בינאריים, Gem Files, או Images של VM) בצורה: מסודרת, מאובטחת, Highly Available, ו Durable (כלומר: יש גיבויים).

    תהליך ה Build, מרכזי או כזה שנעשה על המחשב של המפתח, ישלוף חלק מהתוצרים ב AM – במקום לייצר אותם מחדש.

    AM יכולים לנהל תוצרים פנימיים שנוצרו ע\"י הארגון, או Caching לתוצרים חיצוניים כמו ספריות Open Source (בכדי לקצר זמני הורדה שלהם ברשת). הרבה פעמים יהיו בארגון כמה כלי AMs שונים (למשל: תוצרי ג\'אווה או רובי, תוצרי AWS, ותוצרי Linux).

    Development Environment Management 

    אין מה לעשות: בכדי לפתח בצורה יעילה – אנו זקוקים לסביבות שאפשר לנסות עליהן הכל, ואפשר \"לחרב\" אותן מבלי לחשוש.
    הרבה פעמים אנו רוצים בסביבות הללו נתונים (data) מציאותיים, קרי – עותק של חלק מהנתונים מ production, כאשר הם מצומצמים ומנוקים (sanitized) ממידע רגיש (למשל: כתובות אימייל של משתמשים אמיתיים). ההכנה של הסביבות הללו – היא עבודה לא קטנה.

    היבט אחר של \"סביבת הפיתוח\" הוא המחשב האישי של המתכנת (או QA וכו\'). כמה זמן לוקח להתקין מחשב חדש ומפורמט – עד שיש סביבת עבודה מלאה? לעתים זה יכול אפילו כמה ימים מלאים של עבודה.
    כנראה שהייתם רוצים שיהיה דף וויקי (במינימום), תמיד מעודכן, שמסביר את סדרת התהליכים הידניים שיש לעבור על להתקנה מלאה של הסביבה, ועדיף – שיהיה כלי אוטומטי שמבצע את כל תהליך ההתקנה בלחיצת כפתור (ובזמן סביר).

    Collaboration

    Collaboration טוב נוצר קודם כל ע\"י אנשים – ולא כלים. בכל זאת – הכלים יכולים לעזור.
    מישהו צריך להתקין ולנהל את כלי ה Collaboration הללו. את התוכן, באופן טבעי – בדרך כלל ינהלו המתכנתים.

    כלל קטן: אם אנשי ה Operations מתחזקים את כלי ה Collaboration אך לא משתמשים בהם – אין לכם תרבות של DevOps בארגון. שקיפות ושיתוף בין פיתוח ו Operations – זה הבסיס.

    כלי \"Production\"

    את הכלים הללו לרוב מפעיל צוות שנקרא בארגונים גדולים בשם ״Web Operations״. זו נחשבת התמחות שונה מצווות ה IT Operations.

    בחברות קטנות, צוות ה ״ Operations״ עושה גם וגם, אך אם אתם מגייסים אנשים מארגונים גדולים – חשוב להבין את ההבדל.

    כשעבדתי ב SAP למשל, צוות ה IT Operations ישב איתנו ונקרא \"DevOps\". הם ניהלו את כל הכלים הפנימיים, הדריכו ותמכו במפתחים וממש עזרו להם. הצוות הזה הכיר Git, Jira, ו Maven – יותר טוב מכולם.
    צוות ה Web Operations נקרא \"Hosting\", ישב בגרמניה והיה מאוד מרוחק מאיתנו – פיסית ומנטלית. אם השתקפה בעיה ב Monitoring – מישהו היה מתקשר, אבל מעבר לזה הקשר ביננו היה חלש, ולא היה הרבה מה לעשות בנידון. זה היה תת-ארגון אחר. לכלי ה Monitoring למשל, לא הייתה לנו גישה. אפילו לא לצפייה. ממש לא DevOps Culture…

    דוגמאות לכמה כלים בכל קטגוריה

     Web/HTTP

    בקטגוריה זו יש לרוב יהיה שימוש ב 2 או 3 מתוך תתי הקטגוריות הבאות:

    1. HTTP Server – עבור הגשת Static Content ו/או ניהול התעבורה עבור Technology Stack שאינו multi-threaded מטבעו. כלומר: PHP, Ruby, Python. הכלים הנפוצים בקטגוריה זו היא Nginx ו Apache httpd.
    2. Application Server – סביבת ריצה לאפליקציות ווב. בעולם הרובי Applications Servers נפוצים הם Puma, Unicorn, ו Raptor. בעולם הג\'אווה הם יכולים להיות Tomcat, Jetty, או Glassfish.
    3. Content Delivery Network (בקיצר CDN) – שהיא יעילה אף יותר מ HTTP Server בהגשת Static Content, אך יש לעתים סיבות להשתמש בשניהם.
    כלים אלו משפיעים במידה ניכרת על ה Traffic / התנהגות המערכת. כנראה שתרצו מישהו בארגון שמבין בהם, יידע לקנפג אותם, לנטר אותם, ולאתר תקלות בצורה מהירה – ולא תמיד אלו יהיו המפתחים.

    Configuration Management

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

    Packaging

    ה Packaging מטפל באריזת ה VM Image שממנו ניצור instances של מכונות בענן.
    אם אתם עובדים בענן כלשהו (אמזון, Azure, או אפילו VMWare) – סביבת הענן תאפשר לכם לייצר Image ממכונה.

    כלים כמו boxfuse או packer מאפשרים לכם לייצר מאותה המכונה Images מסוגים שונים (למשל: אחד ל Azure ואחד ל AWS במקביל), וגם ליצור Images קטנים ויעילים יותר (ע\"י זיהוי התלויות הנדרשות ובניית \"סביבת מערכת הפעלה\" מספיקה לצרכים הספציפיים).
    Image מוקטן שכזה מתאים ל Immutable Deployments (להלן הפוסט על כלי ניהול תצורה), ומגיע על חשבון החופש להתקין בקלות עוד כלים על המכונה במהלך הדרך.

    למרות שאני מציג את ה Packaging כקטגוריה בלתי-תלויה בכלי ניהול תצורה – לרוב משלבים בתהליך ה Packaging כלי ניהול תצורה, שילוב שלרוב הוא מובנה בכלי ה Packaging.

    Deployment

    \"מה הבעיה לעשות Deployment?!\" – היא השאלה הראשונה כאשר עוסקים בקטגוריה זו.
    \"אני פשוט הולך ל UI של אמזון, בוחר image, ומאשר – תוך 2 וחצי דקות השרת למעלה\".

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

    • כיצד להתקין שרתים חדשים מבלי לפגוע ב Traffic הקיים? לא Downtime ולא שיהוק.
    • אם אנו עושים זאת בהדרגה, כיצד מתמודדים עם מצב הביניים בו יש כמה שרתים בגרסה n, וכמה שרתים בגרסה n-1 החיים זה לצד זה?
    • מכיוון שמדובר ב cluster, כיצד עושים זאת עם כל הגדרות ה Load Balances, רשת, אבטחה – ומבלי להתבלבל. ה UI הרגיל של אמזון כבר לא כ\"כ טוב לבעיה הזו.
    • מה עושים במצב של כשל? מתי מזהים אותו וכיצד – ואיך עושים Rollbacl? (לא נחכה לתיקון קוד עם בעיה חמורה בפרודקשיין, ולא משנה עד כמה המתכנתים שלנו זריזים).

    בגדול, יש שתי \"אסטרטגיות: עיקריות לביצוע Deployment:

    • Rolling Deployment
      • התהליך (נניח שהשרת שלנו רץ על m nodes):
        • ניקח node אחד ואז:
          • ננתק אותו מה LB 
          • נוודא שכל ה requests שבתהליך סיימו להיות מטופלים
          • נתקין את הגרסה החדשה
          • נחבר אותו בחזרה ל LB וננטר אותו לזמן קצר לראות שהכל בסדר.
        • נחזור על כל התהליך הזה עוד כ m-1 פעמים.
      • ל Rolling Deployments יש ייתרון של חסכון במשאבים (צריך עוד שרת אחד, אפילו אם יש לנו 100 nodes), אבל הוא יכול להיות תהליך ארוך – ו rollback הוא קשה וארוך גם כן.
    • Blue/Green Deployment – 
      • התהליך (נניח שהשרת שלנו רץ על m nodes – \"ה cluster הכחול\"):
        • ניצור cluster חדש (\"ירוק\") זהה בחומרה ל cluster המקורי – ונתקין עליו את הגרסה החדשה.
        • ננתב חלק קטן מה traffic ל cluster החדש וננטר לזמן קצר. הכל בסדר? נחליף ברגע אחד את ה cluster הירוק להיות ה cluster הפעיל.
          • ניתוב ה Traffic יכול להיעשות באסטרטגיה של DNS cutover, באמזון – זמינה גם אסטרטגיה בשם swap auto-scaling groups, ויש גם אסטרטגיות אחרות.
        • ה cluster הכחול ימשיך לחיות עוד זמן מה (החיוב באמזון למכונות הוא ע\"פ שעה עגולה של שימוש). אם נגלה בעיות בגרסה החדשה נוכל לחזור ל cluster הכחול – ברגע.
      • Blue/Green Deployment דורש יותר משאבים – אך הוא בטוח יותר, ומאפשר Rollback מהיר.
    האם אתם רוצים לבצע בשטח את אסטרטגיית ה Deployment בצורה ידנית? קרוב לוודאי שלא, שתעדיפו לעשות את התהליך בעזרת כלי שיצמצם משמעותית את טעויות האנוש ויקצר את התהליך.

    לרוב ספקי הענן יש כלים משלהם לפעולות Deployment (ובכלל ניהול ה Deployment Pipeline) – אך הכלים החיצוניים עדיין נחשבים טובים יותר.

    הערה: Jenkins הוא לא כלי מוצלח לניהול Deployment. נכון: גרסה 2.0 שלו הציגה קונספט של \"Pipeline as code\", אך קונספט זה ממשיך לעקוב אחר המודל הקיים והסבוך של pipelines של Jenkins והגמישות בו מבוססת על Plugins.
    Jenkins הוא סבבה כנראה ל build pipeline, כך בכל הקשור ל deployment – מומלץ להיצמד לכלים ייעודיים.

    כדאי להכיר: אלו סט הכלים של HashiCorp – ארגון מוערך מאוד שהוציא כמה כלים מאוד מעניינים (דגש על Vagrant ו Consul) בתחום. חשוב גם להיזהר מהנטייה האישית \"לאסוף סריות\": לא כל כלי של HashiCorp הוא בהכרח מתאים לכם. בוודאי שלא!

    RDBMS Migration
    אם אתם זוכרים – בבסיסי נתונים רלציוניים (וגם כמה אחרים), ישנה סכמה מוגדרת מראש של הנתונים. סביר שחלק מהעדכונים של התוכנה יידרשו שינוי סכמה בבסיס הנתונים.

    להזכיר: בזמן ה Deployments (אם רוצים zero downtime) יהיו שלבים בהם ירוץ קוד חדש וישן זה לצד זה – ושניהם מול בסיס נתונים אחד (זה לא ריאלי בד\"כ לרפלק את בסיס הנתונים לצורך deployment. יותר מדי סיכונים והשקעה).
    ניתן לבחור ב-2 גישות: 
    • קוד תואם לאחר שיתמוך ב-2 הסכמות (השקעה גדולה). את המיגרציה ניתן לעשות לאחר עדכון הקוד (סיכון).
    • סכמה תואמת לאחור – כלומר רק תוספות ולא מחיקות או שינויים (הגישה הנפוצה). את המיגרציה יש לעשות לפני עדכון הקוד, בשלב מוקדם של ה deployment.
    עבור migrations יש כלים רבים. חלקים מגיעים כחלק מה Framework (כמו ב !Ruby On Rails, Play, או Django). ישנם כלים כמו Flyway או Liquibase שאינם תלויים בשפת תכנות או Framework ספציפי.

    ה Migration עצמו הוא סכנה ליציבות המערכת: תקלה בו יכולה להיות בעיה קשה.

    סיכון נוסף הוא שינוי של טבלאות גדולות במיוחד (עשרות או מאות מיליוני רשומות?) או פעילות במיוחד (הרבה locking). פעולות migration שכזו עשויה להתארך לאורך שעות – ולפגוע לאורך כל הזמן הזה בביצועי המערכת.

    כלים כמו LHM או Table_migrator מצמצמים סיכונים ע\"י העתקת הטבלה הצידה, ביצוע ה migration, ואז החלפתה (rename) – קונספט דומה ל Blue/Green Deployment – רק שצריך לנהל delta של השינויים שלא נכנסו לטבלאת העותק בזמן ה migration – ולעדכן אותם מאוחר יותר.

    Monitoring

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

    כלי ה Monitoring הם אלמנט חשוב לתפעול, ולרוב יהיה לנו Monitoring בכמה רמות שונות (מערכת, אפליקציה, Proactive Monitoring, וכו\'.) – ייתכן וכל רמה בכלי שונה.

    כתבתי כבר בעבר פוסט על New Relic – הכלי האהוב עלי בתחום: על Performance Monitoring ו New Relic.
    כתבתי גם פוסט בשם Monitoring: מבוא ל Graphite ושימוש ב Time-Series – המתאר כלים ל custom monitoring (בהיבט של time series).

    תת-קטגוריה אחת ששווה להזכיר היא נושא ה Alerting – שליחת אימייל / SMS / או טלפון (עם הודעה קולית) לאנשים הרלוונטיים (ע\"פ תורניות), ועל סמך הצטברות מדדים כאלו או אחרים ממערכות ה Monitoring. כלי כזה לדוגמה הוא PagerDuty.

    תת-קטגוריה קרובה אחרת היא הנושא של Event Management: בהינתן מדדים כאלו ואחרים – הפעל response שנקבע מראש (ונבדק). למשל: restart ל service, הריגת טרנזקציה מסוימת ב DB, הגדלת מספר השרתים, וכו\'.

    Logging

    לכתוב מהקוד ללוג – זו לרוב פעולה פשוטה למדי: בוחרים ספריית Logging, מסכימים על כמה כללים – וכותבים.

    מה קורה עם הלוגים אח\"כ?
    מה קורה בענן ו/או ובארכיטקטורה של מיקרו-שירותים? אם לא נאסוף את הלוגים הללו למקום מרכזי – הם יעלמו לנו.
    אם לא נאנדקס אותם – לא נוכל לחפש בהם בצורה יעילה.
    אם לא נמחק אותם (מהמכונות, ומהאינדקסים של ה repository המרכזי) – ייגרם נזק לשרתים או ליכולת שלנו לחפש בהם ביעילות.

    הכלים הנפוצים לניהול לוגים הם ה ELK Stack (קיצור של ElasticSearch, LogStash, ו Kibana) או לחלופין Splunk או SumoLogic (שלא כוללים את איסוף הלוגים – LogStash או מוצרים חלופיים).

    יש גם תת קטגוריה של Error Tracking – שהם כלים ייחודיים לניתוח תקלות משמעותיות במערכת (מול לוגים – שכוללים את כל המידע וגם info, traces, וכו\'). כלים מוכרים בקטגוריה זו הם Airbrake, Sentry, ו Raygun.

    יכולות ייחודיות לכלים אלו הם סנכרון עם עדכוני קונפיגורציה / תוכנה – וניתוח ה impacts שלהם מבחינת צפיפות ה errors, חיתוך ע\"פ id של משתשמש (שאנחנו סיפקנו) בכדי לנסות ולאתר מדוע שימוש מסוים גורר בעיות – וכו\'.



    מקור: Sounil Yu

     

    Security

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

    • מתחילים עם Firewall – שבדרך כלל ספק הענן יספק.
    • אם המערכת שלכם עובדת על גבי HTTP, קרוב לוודאי Web Application Firewall (בקיצור WAF) – כמו Cloudflare, אינקפסולה, או BigIP (של F5).
    • דרך כלים ל Authentication ו/או Federated Identity Management (בקיצור FIM) כמו PingIdentity, OKTA, או Auth0.
    • כלי Intrusion Detection System (בקיצור: IDS) – כמו Snort, או Trend Micro\'s Deep Security.
    • כלים ל audit בגישה ל Production כמו ObserveIt או CyberArk.
    • כלי SIEM – כלי לניהול לוגים בהיבט האבטחה, כלים כמו ArcSight, QRadar, או Splunk (שהוא גם \"חזק\" בעולם ה Security).
    • כלים ל Security Static Code Analysis המחברים לתהליך ה build כגון Checkmarx או Fortify.
    • ועוד…

    בארגונים יותר גדולים, כל הנושא של כלי ה Security מופקד ביחיד צוות Operations מיוחד, שמתמחה בנושא.
    בארגונים קטנים יותר – זה היה צוות ה Web Operations או אפילו המפתחים.

    לכאורה, ניתן לכתוב פוסט מקביל לזה – רק בהיבט האבטחה.  

    סיכום

    עוד פוסט שחששתי שלא יהיה לי מה לכתוב בו – אך התארך מעבר הצפוי 🙂
    נושא ה Operations וה Production הוא עולם בלתי נגמר.

    תנועת ה DevOps שמשפיעה על כלל התעשיה (שלב א\': שינוי ה Title של אנשי ה Operations – אך יש גם את השלבים הבאים) – משנה את העולם.

    עוד כוח שמשנה את התמונה הוא טכנולוגיית ה Linux Containers (ובחזיתה: Docker), גישה החותרת ל Fully Immutable Deployment – גישה שעשויה לשנות כמה מהתהליכים ויחסי-הכוחות בעולם זה.
    תוצר לוואי של גישת ה Containers היא החתירה ליצירת כלי Infrastructure Management חדשים (כמו Mesos, Tectonic, או Atlas), שלא מבוססים על ספקי הענן  – מה שעלול לגרום לעוד כמה תזוזות.

    שיהיה בהצלחה!

    לינקים רלוונטיים

    DevOps Awesome Links – כמה קישורים מעניינים!

    DevOps From the ground up Deep Dive into Blue/Green Deployments on AWS – על אסטרטגיות Deployment.

    ההבדל בין Packer ל Vagrant – מאמר + דברים של מיטשל האשימוטו (Hashi)  

    Cybersecurity Market Review – סוקר הרבה כלי Security במגוון קטגוריות

    על כלי ניהול תצורה / Provisioning – מבראשית ועד ימי ה Docker

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

    ישנם מגוון כלים לניהול תצורה (Configuration Management בקיצור CM), המאפשרים לנהל מספר גדול של שרתים (מאות, אולי אלפים) בצורה אוטומטית ומסודרת. כלים אלו לרוב מספקים את היכולות הבאות:

    • Change Management – ניהול הגדרות ה"מצב הרצוי" בתצורת השרתים, בצורה מסודרת וניתנת לשליטה. למשל: שינוי הגדרות של nginx בשרתים מסוג x או הוספת תיקיה חדשה במערכת הקבצים של שרתים מסוג y.
      • חשוב שיהיה קל להבין ולהגדיר מהם "שרתים מסוג x". לעתים אגב, אלו יכולים להיות שרתים עם מערכות הפעלה שונות, ושמריצים תוכנות שונות.
    • Provisioning – זהו בעצם התהליך של העברת שרת ממצב נתון כלשהו – למצב הרצוי.
      • זה יכול להיות התקנה של שרת מאפס, או בדיקת המצב בשרת קיים – וביצוע השינויים הנדרשים על מנת להגיע למצב הרצוי.
      • לרוב שינוי ההגדרה יכלול ביצוע שינוי בשרתים שכבר "באוויר", וגם הכללת השינוי החדש בשרתים הבאים שיקומו.
      • כאשר מבצעים שינויים, ישנן תקלות (פעולות רצויות שנכשלו) – תקלות שיש לנהל.
    • Orchestration – תיאום הפעולות בין כמה שרתים. למשל:
      • ביצוע שינוי מסוים באותו הזמן על מספר שרתים. אולי כדאי קודם לבצע בדיקה שמאשרת (כמיטב היכולת) שאכן כל השרתים מסוגלים לעבור את השינוי.
      • אם אחוז גבוה מהשרתים לא מצליח לעבור שינוי – אולי כדאי לעצור את השינוי המתגלגל, ולהתחיל פעולה של rollback (חזרה למצב הקודם)?
      • יש שינויים שצריכים להיעשות בצורה מדורגת כלשהי: קודם ביצוע שינוי בשרתי x ורק אח"כ אפשר לבצע את השינוי בשרתי y.

    הכל כמובן – בצורה אוטומטית.

    בפוסט הבא אני הולך לעבור בקצרה על כלי ה CM הנפוצים בשוק, ולסקור את המעבר שהם עשו ב 20 השנים האחרונות.

    "דור ראשון"

    CFEngine

    את הכלי הזה יכירו בעיקר הוותיקים שבינינו. הוא התחיל לפעול ב93' (כלומר: שנת 1993- למי שלא הבין) והיווה מהפכה תפיסתית בעולם ניהול-השרתים. הוא התחיל כפרויקט קוד פתוח, אך ב 2008 נוספה לו גרסה "מסחרית" עם תמיכה – ומאז הוא משתמש בעיקר תצורות מסובכות של ארגונים גדולים.

    CFEngine ממומש בשפת C, והוא ידוע במהירות הגבוהה שלו. מצד שני – הוא דורש ידע מלא בתכנות בשפת C על מנת לתפעל אותו, מה שמגביל מאוד את קהל היעד.

    כלי זה, כמו הכלים הבאים אחריו עובדים ע"פ עקרון דומה:

    • שרת מרכזי מחזיק את התצורה הרצויה לסוגי השרתים השונים (ע"פ ארגון של קבוצות או כללים).
    • על כל שרת מותקן Agent שמתשאל מדי פעם את השרת המרכזי, וברגע שהוא מבין שיש לבצע שינוי תצורה (קרי: עדכון תוכנה, שינוי הגדרות, וכו') – הוא מבצע אותו.
      • כששרת מופעל בפעם הראשונה (לפעמים הוא כולל מערכת הפעלה ו Agent בלבד) – הוא יבצע את כל שינויי התצורה לפני שיתחבר ל Load Balancer (קרי: יהיה זמין לשאר ה Landscape).
    איך CFEngine עובד. מקור: CFEngine.com

    "דור שני"

    Puppet

    הכלי הבא שהופיע והשאיר חותם הוא Puppet, עם המטפורה המפורסמת של ה "Puppet Master" – השרת המרכזי שלו שהוא כמו "מפעיל בובות". גם Puppet החל בשנת 2005 כ Open Source ורק מאוחר יותר הופיעה גרסה מסחרית (Enterprise).
    החידוש המשמעותי של Puppet היה שהוא לא דרש ידע מעמיק בתכנות, אלא כתיבת DSL (כלומר: Domain Specific Language) המבוססת על שפת רובי – דבר שצוות ה Operations של הארגון היה מסוגל לתפעל בצורה מלאה.

    קוד Puppet לדוגמה

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

    השפה של Puppet (או כלי CM אחרים) היא DSL שמותאמת לצרכים המאוד ספציפיים של ניהול תצורה, ועוזרת לשרת את קריאות הקוד (הקוד הרבה יותר נוח לקריאה מ bash) ולצמצם טעויות אפשריות. עוד ייתרון הוא היכולת להגדיר תצורה שתפעל על מערכות הפעלה שונות (דיסטרוס שונים של לינוקס, או אפילו לינוקס ו"חלונות").

    השפה של Puppet מספקת שורה של פרמיטיבים (File, User, Package, Service, וכו') שניתן להרכיב למודולים (הניתנים לשימוש חוזר) או פשוט להוריד מוכנים מ PuppetForge[א]. היכולת לשתף מודולים ברמת הקהילה מסייעת לקיצור זמני-פיתוח והגברת אמינות ניהול התצורה – כל עוד מדובר במודולים איכותיים. יש לציין שלא כולם כאלו, וחשוב מאוד לברור את מקור המודולים בהם אתם משתמשים.

    בזכות קלות השימוש – Puppet לקח מ CFEngine את הבכורה.

    Chef

    לא הרבה מאוחר יותר, בשנת 2008, הופיע כלי חשוב נוסף בשם Chef. גם הוא, אגב, כלל סדרת מטפורות בלתי-נשכחות ("Knife", "Kitchen", "Cookbook", ו "Recipe", וכו'). Chef שיפר כמה מהדברים שהציקו ב Puppet וזכה לפופולריות גדולה בעצמו. גם הוא – מבוסס על שפת רובי.
    בשלב הזה Chef ו Puppet החלו בתחרות "ראש בראש", כאשר הם משלימים כל הזמן את הפיצ'רים החדשים אחד של השני. במהלך התחרות הזו נוספו לכל אחד מהם יכולות וכלים, מה שגרם להם בעצם להפוך מכלים פשוטים יחסית – לכלים מורכבים לשימוש, הדורשים מהמשתמשים למידה משמעותית.

    בגדול, Chef נחשב יותר "ידידותי למפתח" (כזה שרגיל לכתוב בדיקות יחידה, ו/או לעבוד בכלי פיתוח)  בעוד Puppet מעט יותר ידידותי לאיש ה Operations (שאינו מתכנת).
    יש אומרים ש Puppet נוח יותר לניהול תחנות עבודה של משתמשים, וש Chef – נוח יותר לניהול חוות שרתים (אולי בגלל ה output המפורט שלו שמסייע לאתר הבעיה – כאשר יש תקלות). בסופו של דבר, שני הכלים קרובים זה לזה ביכולות, ובבגרות. ל Puppet יש קהילה גדולה יותר, וב Chef מורכב יותר להתמקצע.

    לא לזכותם של הכלים הללו ניתן לומר שהם כיום מורכבים למדי לשימוש, עבור רוב המשימות הנפוצות, וה Learning Curve שלהם הוא תלול ממה שארגונים רבים היו רוצים.

    עוד נקודה מעניינת היא ששלושת הכלים שהזכרתי עד רגע זה (Chef, Puppet, ו CFEngine) תוכננו ונבנו בתקופה בה התפיסה הייתה לעדכן שרתים קיימים, ולאורך זמן.

    אני אסביר: כאשר יש לכם שרתים פיסיים ("ברזלים"), התקנה מלאה של שרת עשויה לקחת עשרות דקות – ואפילו מספר שעות. לבצע את ההליך הזה אפילו פעם אחת בחודש – זו בעיה רצינית.
    כלים כמו הכלים הנ"ל התמקצעו בנושא של עדכון הדרגתי של שרתים, מה שגם נקרא Convergence, קרי: א. בדוק באיזה מצב השרת, ב. עדכן את תצורת השרת, רק במה שצריך – לתצורה הרצויה (deltas). עדכונים כאלו יכלו להיות כל כמה ימים.

    גישת ה Convergence אפשרה לבצע עדכונים תכופים בתצורת השרתים, מבלי לגזול זמן רב / לשלם ב downtime.
    המחיר של גישה זו היא שניהול deltas לאורך זמן הוא מסוכן ולא יציב – ודברים יכולים (ואכן) נוטים להשתבש. למשל: מפתח התחבר ב SSH לשרת ושינה הרשאה של איזו תיקיה. עדכון נכשל והשאיר אחריו מצב בלתי רצוי שאינו מתוקן ע"י הגדרות התצורה של הכלי, וכו'.
    הפתרון היה להחזיר שרת ל "baseline" – ברגע שדברים מסתבכים. לשלם את המחיר ההתקנה מחדש – בכדי לחזור למצב של סדר ושליטה.
    הבעיה: בזמן שחלף עד שהבנו שהשרת הוא בתצורה בעייתית – התצורה הבעייתית יוצרת "רעש", ועוד רעש בפרודקשיין!

    התפיסה השלטת היום היא תפיסה של Immutable Servers (או "כמעט Immutable Servers"), כאשר כל שינוי תצורה (או: כמעט כל שינוי תצורה) – משמעו ליצור Image חדש, נקי ומסודר של שרת, ולעדכן את המכונות הרצויות.
    היכולת הזו מתאפשרת בעקבות טכנולוגיות הוירטואליזציה והענן שהפכו לנפוצות כ"כ, ומתקדמות צעד אחד נוסף קדימה בעקבות טכנולוגיה כמו Containers.

    כלומר: כל פעם שאנו רוצים לבצע שינוי בשרתים (נניח להוסיף לשרת agent של logstash) – ניצור Image חדש ונקי, ונריץ את כלי שלבי ה provisioning מאפס עד לקבלת שרת בתצורה הרצויה (כולל השלב החדש של התקנת ה logstash agent). כעת נייצר מהשרת הזה Image ואת ה Image הזה – נתקין על המכונות הפיסיות בפרודקשיין.

    גישת ה Immutable Server נחשבת אמינה ופשוטה יותר לשליטה – ולכן עדיפה. החיסרון: שינויים קטנים עדיין דורשים בנייה של Image – תהליך שיכול לארוך זמן רב (עשרות דקות עד שעות).
    אם כל השינוי הנדרש בשרת הוא העתקה של קובץ בודד, שינוי הרשאות על תיקיה, או שינוי הגדרה – רבים מאיתנו יעדיפו שיהיה על השרת גם כלי CM בעזרתו יוכלו לבצע את השינוי בזריזות. הזריזות הזו חשובה מאוד כאשר יש להגיב לתקלות בפרודקשיין. במקביל, ניתן לבצע את השינוי בצורה מסודרת על ה Image ו"ליישר קו" ב deployment הבא.
    גישה זו, היא גישת ה "כמעט Immutable Servers" – המשלבת בעצם בין שתי הגישות.

    שינוי הגישה מניהול תצורה בעזרת deltas שמתמשכים למשך חודשים, לצורה של "Immutable Servers" או "כמעט Immutable Servers" (לא ידוע לי על מונח מוסכם) פתח הזדמנות לכלים פשוטים משמעותית, שלא צריכים להתמודד עם המורכבות הרבה של ניהול deltas בתצורת השרת – להצטרף לזירה.

    אפשר לציין ש Chef ו Puppet הם עדיין הכלים הנפוצים. הם שולטים בזירת ה CM כבר זמן רב, והשפיעו עליה ועל עולם התוכנה רבות.

    מקור: google trends

    "דור שלישי"



    Ansible

    בשנת 2012, הופיעו כלי CM חדש בשם Ansible ששינה הרבה מצורת העבודה של Chef ו Puppet:

    • ניהול העדכונים: במקום שיהיה שרת מרכזי (או כמה לצרכי HA ו Scale) אליו כל ה Agent פונים ב pull, הגישה היא לבצע push: אתם פונים לשרתים השונים ואומרים להם לבצע את השינוי.
      • יתרונות: הסרת שרת הקונפיגורציה מה landscape. הבדיקות של תצורות חדשות נעשות ב cycles קצרים יותר, ב push העדכונים מגיעים לשרתים מהר יותר. קל לשימוש בסביבת פיתוח, בה ניתן לפתוח laptop ולעדכן שרת – בלי להתקין ולתחזק "שרת מרכזי".
      • חסרונות: כאשר מדובר ב scale גדול (מאות ואלפי שרתים) – עדכון השרתים ב push ייקח יותר זמן. אם בדיוק עלה שרת חדש לפני שיש image מעודכן – הוא עשוי לפספס את העדכון.
    • שפת התכנות: במקום DSL ומיומנויות של semi-תכנות, ההגדרה של התצורה נעשית בעזרת קובץ yaml פשוט.
      • יתרונות: הרבה פחות "קוד" לכתוב. זמן למידה קצר בהרבה של הכלי. אין צורך ברקע בתכנות בכלל על מנת ליצור הגדרות תצורה.
      • במקום שפת תכנות שיוצרת הפשטה מעל כל סוג של מערכת הפעלה, שכבת ההפשטה היא רזה. יש מודול לפקודות apt (נניח אובונטו) ומודול לפקודות yum (נניח Red Hat). ההפשטה הרזה לא מאפשרת "לכתוב סקריפט אחד שמתאים לכל מערכת הפעלה", אך מפשטת מאוד את הלמידה (כי אתם יודעים כיצד apt או yum עובדים, ומקלה להבין מה קורה בפועל.
      • חסרונות: פחות גמישות: בעיות CM מורכבות יידרשו כתיבת סקריפט מיוחד (בעיקר: שימוש ב Jinja2 – שפת templates בה ניתן לכתוב מעט קוד פייטון או להפעיל קוד bash).
      • הערה: Ansible הושפעה מפייטון (השפה בה היא כתובה) בגישת ה "Batteries included". המודולים הטובים שנכתבו / אושרו ע"י Ansible הם חלק מהכלי, ולא צריך להוריד אותם מ repository מרכזי כמו Chef או Puppet.
    • אין Agnets: אין צורך להתקין Agent (להלן Chef-Client או Puppet-Agent): על גבי השרתים. Ansible משתמש ב SSH סטנדרטי (ליתר דיוק: OpenSSH) בכדי לגשת לשרתים ולבצע בהם שינויים. עבור Windows – משתמשים ב PowerShell remoting.
      • יתרונות: אין צורך בהתקנה, עדכון, וניטור של Agents על גבי השרתים. רק צריך שרת עם SSH ו Python 2.5.
      • חסרונות: SSH הוא אטי יותר (פחות מתאים לעדכון אלפי שרתים),
    Salt (מבית SaltSlack)
    מעט לפני Ansible הופיעה ספרייה בשם Salt.

    בכמה מובנים, Slat דומה למדי ל Ansible: הגדרות התצורה מוגדרות ב Yaml ולא בשפת תכנות, והוא פשוט בסדר גודל מ Chef או Puppet.
    Salt ו Ansible שניהם כתובים בפייטון (Chef וגם Puppet כתובים בשפת רובי).

    בכל זאת, הגישה של Salt להעברת העדכונים לשרתים – היא דיי דומה לזו של Chef ו Puppet.

    המוטיבציה המוצהרת ליצירת Slat הייתה עדכון תצורה מהיר של קנה מידה גדול של שרתים. נראה שיצירת סביבת עבודה פשוטה יותר מ Chef/Puppet הייתה מהלך טבעי, אך משני – בתכנון של Salt.

    • השרת (הקרוי Slat Master) מבצע עדכונים ב push לתור של ZeroMQ. על השרתים מותקנים Minions (סוג של agents) שמאזינים לעדכונים על ה Queue ומבצעים מייד את העדכון. זו גישה משולבת בין ה Push של Ansible וה Pull של Chef/Puppet, שמבטיחה בד"כ זמני תגובה מהירים יותר מ Chef/Puppet (כי יש Push), ו scalability גבוה – היכולת לעדכן אלפי שרתים במקביל.
    • ניתן להגדיר ה Salt Master בהיררכיה כך שיש Centralized Master אחד שמגדיר מה לעשות, אך כמה Masters תחתיו שמפיצים את הבשורה לשרתים שונים. גישה זו יכולה לספק Scale גבוה אף יותר (נאמר: עשרות אלפי שרתים?)
    בשנת 2014 Salt הציג Queue ייעודי משלו בשם RAET שהוא Scalable ואמין יותר מ ZeroMQ – לצורך הספציפי.
    מקור: Google Trends
    עידן ה Docker – "דור רביעי?"
    כל כלי ה Configuration Management שותפים למורכבים מסוימת, מורכבות שנובעת מהקושי לבצע provisioning למערכת על "שרת אמיתי בפרודקשיין". מה לעשות: אתם זקוקים לכלי מוניטורינג, אבטחה, לוגים, הגדרות, וכו' – תצורת השרת היא מורכבת.

    Docker הוא כלי שמנבאים שיחולל מהפיכה לא רק בסביבת ה Production – אלא גם בעולם הגדרות התצורה.
    מכיוון ש Docker "מפרק" את סביבת השרת להרבה Containers קטנים, הוא גם הופך את בעיית ניהול התצורה לסט בעיות קטנות יותר: תצורת ה Application Server ב Container A, תצורת ה Nginx ב Container B, ותצורת ה Monitoring Deamon ב Container C. על אחד מהן עשויה להיות בלתי תלויה, ופשוטה הרבה יותר מתצורה כוללת.

    ה dockerfile, אם כן, הוא בערך כמו קובץ ה Yaml של Ansible או Salt, המאתר כיצד להתקין תהליך ואת התלויות שלו. טעינה מחדש של container היא כ"כ מהירה (עשרות שניות) – שניתן (אולי?) להסתפק בגישה טהורה של "Immutable Servers".

    מצד שני, לנהל אלפי containers (הנחה: על כל שרת יש 5-10 containers) – זו משימה קשה בהחלט! זו בעצם בעיית ה Orchestration ש Docker איננו מכסה. למשימה זו, נבנים כיום כלים ייעודיים, שהמוכרים ביניהם הם:

    • Kubernetes – כלי מבית גוגל, המשתמש בניסיון הקיים של גוגל בעבודה עם Linux Containers וניסיון שהצטבר עם כלי פנימי של גוגל בשם borg – האחראי על ה provisioning של השרתים הרבים מאוד ב Data Centers של גוגל. בעצם, גוגל כבר עושה היום משהו דומה.
      הניסיון הזה מתבטא למשל, בכל ש Kubernetes פתרה בעיות של Docker עצמו שהיו קיימות בגרסאות מוקדמות.
      Kubernetes מציע פתרון שלם יחסית, עם etcd ל container discovery, עם flannel לניהול הרשת בין ה containers (עניין מסובך ומתיש), ועוד.
      לסביבה העשירה הזו יש מחיר: יש לכלי CLI משלו, API משלו, וקבצי YAML משלו להגדרות התצורה – וזו סביבה שיש ללמוד להשתמש בה, בנוסף ל Docker.
    • Docker Swarm – הוא כלי אשר נוקט בגישה כמעט הפוכה: הוא תומך ב API הקיים של Docker, ובסביבת ה CLI – מה שאומר שקל מאוד להשתמש ב Docker Swarm עם ה toolchain הקיים של Docker (למשל: Docker Machine (לשעבר Krane) או Compose, וכו').
      Docker Swarm מתוחזק ע"י קהילת ה Docker, והוא חלק מ"פתרון כולל" שנבנה בתוך קהילה זו לניהול תצורה.
      האם זו בהחלט הגישה המועדפת? קשה לומר. התאימות ל Docker API מגבילה את הכלי למגבלות של Docker עצמו ומקשה עליו לבצע חלק מהדברים שניתן יהיה לעשות ב Kubernetes.
    זה לא כ"כ משנה איזה עוד כלים קיימים או ייווצרו בקהילה של Docker.
    אם וכאשר Docker יהפוך לסביבת ה Deployment המקובלת ב Production – הוא עתיד לבצע Disruption גם בעולם ה CM ולשנות אותו משמעותית ממה שאנו מכירים אותו היום.

    סיכום

    אם תשאלו את מי שעובד עם כלי CM כלשהו, מהו הכלי הטוב ביותר – יש סיכוי סביר שהוא יצהיר על הכלי שבו הוא כבר עובד. מדוע? כי לכלים הללו יש learning curve לא פשוט של התמחות. אם כבר מתרגלים לכלי ומתמחים בו – קשה ליזום החלפה שתדרוש למידה והסתגלות משמעותית מחדש. כפי שלא מחליפים מערכת תוכנה במכה אחת – כך גם בד"כ לא מחליפים סביבת CM במכה אחת.

    הכלי הנכון ביותר לשימוש – משתנה עם הזמן, מכיוון שבחברות הצרכים משתנים: פעם יש רק לינוקס (נניח RedHat) ואז יש גם OpenSuse ו Windows. פעם ה deploy הוא תהליך חודשי – ולאחר מכן כבר מדברים על Continuous Deployment. פעם זה על שרתים של הארגון, ומעט אחרי זה – בענן. פעם על VMs וקצת אחרי זה על Containers.
    הצרכים הארגוניים משתנים ככל כנראה מהר יותר ממה שניתן להסתגל לכלים השונים, ומכאן ניתן להסיק שאחוז גבוה מהארגונים משתמש בכל רגע נתון בכלי CM שאינו אופטימלי לצרכים שלו.

    ישנה תקווה אמיתית שכלים חדשים ופשוטים יותר (Ansible או Swarm, למשל) יאפשרו לארגונים לבצע שינויים תכופים יותר בצורת העבודה – ולהתאים בצורה טובה יותר את מערכת ה CM לצרכים המשתנים של הארגון.

    שיהיה בהצלחה!

    —–
    קישורים רלוונטיים
    Chef vs. Ansible vs. Puppet vs. Salt בהקשר של OpenStack – אך עדיין מעניין ורלוונטי.

    http://martinfowler.com/bliki/ImmutableServer.html – פוסט של מרטין פאוולר על Immutable Servers.

    http://techblog.netflix.com/2013/03/ami-creation-with-aminator.html – פוסט מהבלוג של Netflix שמתאר את תהליך ה Provisioning שלהם

    על Convergence מול Immutable Deployment

    —–
    [א] רמז לכך ש puppet נבנה בימים בהם הכלי המקובל לניהול קוד פתוח היה SourceForge? היום בוודאי היו קוראים לו PuppetHub…

    על Circuit Breakers ויציבות של מערכות מבוזרות

    דמיינו 2 חנויות מכולת כמעט זהות: \"המכולת של שמואל\" ו\"המכולת של הלל\".

    בשתי חנויות המכולת יש מוצרים איכותיים שאתם אוהבים, מוזיקה טובה מתנגנת, השירות מצוין, הן פתוחות 24/7, והמחירים – הוגנים. אתם אוהבים את המכולות הללו, ואתם אוהבים לקנות בהן.

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

    במכולת של הלל – המצב הפוך. הם מוכנים לכך שלא יהיו מוצרים מסוימים זמינים. הם קוראים למצב הזה partial service או degraded service (הם קצת גיקים עם המונחים שלהם). המכולת פתוחה 24/7 למעט מקרים נדירים בהם הם סוגרים אותה (כאשר הלל צריך לצאת לאיזה סידור ואין לו מחליף), אבל כאשר אני הולך לקנות משהו – ייתכן ואחזור ללא קוטג\' הביתה. לפעמים לא אכפת לי, אבל לפעמים זה מבאס. אם זה ממש חשוב לי אפילו אקפוץ למכולת אחרת להביא את הקוטג\'. אבל בגלל שאני אוהב את החוויה במכולת – אני עדיין אחזור אליה בקנייה הבאה.
    במקום לדאוג ל\"אפס חוסרים במלאי\", החבר\'ה של הלל עסוקים בכך שחוסר של מוצר מסוים במלאי – לא יגרום למכולת להיסגר מעצמה. זה מאוד מוזר אבל הם מספרים שאם לא יעשו שום דבר, מחסור בטונה פתאום יגרום למכולת להסגר מעצמה. גיקים – אמרנו כבר?

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

    כאשר אנו בונים מערכת תוכנה, והמערכת גדלה ונהיית מורכבת – מתרבים בה הכשלים (הגוררים \"מכולת סגורה\").
    גישה אחת אומרת להשקיע את מירב הזמן במניעת כשלים בכלל – כדי שהמערכת לא תפסיק לספק שירות.
    גישה שנייה אומרת להשקיע את מירב הזמן בהתמודדות עם כשלים חלקיים: למפות חלקים לא-קריטיים במערכת ולהתאים את הקוד כך שיוכל להמשיך לעבוד גם בלעדיהם.

    בצורה בלתי מובנת, האינטואיציה של רובנו היא ללכת בגישה הראשונה.
    הסבר אחד הוא שגישה זו ישימה בעת שהמערכת עוד קטנה – וקשה לנו להתרגל לרעיון שכללי המשחק השתנו עם הזמן.
    הסבר שני הוא שאנו \"מהנדסים באופי\" שחושבים מנקודת מבט של \"המוצר שאו בנינו ויש לנו אחריות עליו\" וקצת פחות מנקודת המבט של חווית השירות שמתקבלת בסוף. אם היינו צרכנים של המוצר – קרוב לוודאי שהיינו מעדיפים את חווית השימוש השנייה (בד\"כ. ישנם כמובן גם מקרים בהם הראשונה עדיפה, למשל: בניתוח לב פתוח, לא היינו רוצים להתחיל ניתוח אם חסר משהו… 🙂 ).

    Circuit Breakers

    המנגנון הבסיסי המאפשר את מודל \"המכולת של הלל\" הוא ה Circuit Breaker.
    לצורך הדיון אניח שמדובר במערכת בארכיטקטורת Micro-Services (בקיצור: MSA) והשירותים השונים הם הרזולוציה לבידוד בפני תקלות. ניתן לעשות אותו הדבר במערכת \"מונוליטית\", אם כי המשמעת העצמית הנדרשת היא לעתים מעט גבוהה יותר.

    ה Circuit Breaker הוא מן Proxy לשירות מרוחק – אשר רק דרכו עושים את הקריאות. השירות המרוחק יכול להיות:
    א. זמין
    ב. לא זמין
    ג. זמין – אך כושל (יש errors)
    ד. זמין אבל אטי

    אם ה Circuit Breaker מזהה שהשירות המרוחק אינו עובד כמו שצריך – הוא \"יוצר נתק\" (מטאפורה: פותח את המעגל) ולא מאפשר לקריאות נוספות לעבור. הפונקציות השונות מקבלות אינדיקציה על המצב הזה ופועלות כמיטב יכולתן לספק שירות חלקי – שעדיף על חוסר שירות.

    במשך הזמן, ה Circuit Breaker נותן לקריאות בודדות לעבור ומנסה לזהות אם המערכת המרוחקת חזרה לספק שירות תקין. אם כן – הוא \"סוגר את המעגל\" ומחזיר את המצב לקדמותו.

    מקורות: \"דפוס העיצוב\" של ה Circuit Breaker הוצג לראשונה ב 2007 ע\"י מייקל נייגארד, בספר המצויין (!!): !Release It.
    ב 2014 מרטין פאולר כתב פוסט בבלוג על דפוס העיצוב (הוא לא חידש שם, רק הסביר יותר פשוט), ומאז זהו ה reference המקובל.

    המימוש הפנימי של ה Circuit Breaker הוא לרוב כמכונת מצבים פשוטה:

    מקור: orgsync/stoplight (מימוש ברובי של Circuit Breaker)

    • המצב ההתחלתי הוא \"מעגל סגור\" (ירוק).
    • אם יש מספר מסוים של תקלות (מוצג בתרשים כ \"fail\") – עוברים למצב \"אדום\".
      • בד\"כ לא מדובר בתקלה יחידה אלא threshold של סדרת תקלות. למשל: רצף של 5 exceptions, כאשר ה threshold הוא של 5.
    • במצב \"אדום\" – כל ניסיון קריאה לשירות המרוחק ייענה בערך החזרה מסוים (או Exception) מצדו של ה Circuit Breaker שאומר \"תסתדרו בלי!\" (כלומר: בלי השירות).
    • לאחר זמן מה (נניח: 120 שניות) של מצב אדום, ה circuit breaker יעבור למצב צהוב – ניסיון להחזיר שירות.
      • הוא יאפשר למספר נתון של קריאות לעבור לשירות המרוחק כדי לבדוק את התגובה. למשל: 10 קריאות. לכל שאר הקריאות הוא עדיין יחזיר את התשובה \"תסתדרו בלי!\".
      • אם ב 10 הקריאות הללו, הוא מזהה treshhold מסוים בעייתי, למשל: רצף של 3 exceptions מצד השירות המרוחק (לרוב ה threshold של המצב הצהוב הוא יותר מחמיר מזה של המצב הירוק) – הוא חוזר למצב אדום.
      • אחרת – הוא מחזיר את המערכת למצב ירוק.
    כמו שאתם מבינים – יש המון וריאציות אפשריות של התנהגות של Circuit Breakers:
    • אפשר לשים thresholds שונים ומשונים. למשל, משהו שלא הזכרנו: שניסיונות חזרה למצב הצהוב יקרו בתדירות משתנה: יש גישה של המתנה של 2 דקות ואז כל 30 שניות (כאשר השירות המרוחק הוא חשוב) ויש גישה של להאריך את זמני הניסיון, למשל: פעם ראשונה 2 דקות, אח\"כ 5 דקות, וכל פעם נוספת – 10 דקות (כאשר השירות המרוחק פחות חשוב ודווקא שגיאה ממנו היא לא נעימה).
    • כאשר circuit breaker מופעל – כנראה שתרצו שסוג של alert יעלה ל monitoring, הרי מדובר בהחלטה לתת שירות פחות טוב. מתי ואיך להעלות את ה alert – עניין לבחירה.
    • לעתים יש אפשרות של override ידני – אפשרות לקבע מצב ירוק/אדום/צהוב של ה circuit breaker בהתערבות ידנית.
    • אולי הכי חשוב: כיצד ה circuit breaker מאתר שגיאה של השירות המרוחק?
      • האם ע\"י ניטור ה responses של ההודעות שחזרו מהשירות המרוחק (למשל: HTTP 5xx)?
      • האם ע\"י ביצוע בדיקה יזומה (proactive) לשרת המרוחק (למשל: שליחת pinging או בדיקת health-check)?
      • אם מדובר על אטיות, ה circuit breaker יכול למדוד את מהירות החזרה של קריאות מהשרת המרוחק. אפשר להגיב לממוצע של קריאות אטיות, או לעתים להסתכל על אחוזון מסוים. למשל: אם 5% מהקריאות אטיות מ 10 שניות, אנו רוצים לנתק – מכיוון שזה אומר שירות גרוע למשתמש הקצה. האם מנתקים רק את הקריאות האטיות או את כולן?!
      • וכו\'
    תוכלו למצוא מספר מימושים שונים של Circuit Breakers, בכל שפת תכנות כמעט (לא ראיתי באסמבלי ;-)), אבל לא נדיר המקרה בהם תרצו לממש גם וריאציה משלכם – במידה ויש לכם מקרה חשוב שלא מטופל ע\"י המימושים הזמינים.

    לא נדיר גם מצב בו אתם משתמשים בכמה circuit breakers שונים בתוך המערכת. תלוי כמה גדולה ומורכבת היא.
    ההתעסקות עם circuit breakers גם היא יכולה להיות משמעותית, וכדאי לשים לב ש:

    • אתם לא נסחפים לאזור ה over-optimization שאיננו משתלם מבחינת ההשקעה.
    • אתם יוצרים מערכת של circuit breaker שהיא מורכבת מדי לניטור ושליטה בזמן אירוע אמת ב Production (כאשר אתם לא יכולים לענות על שאלות כמו: \"מדוע x התנתק\"? או \"אילו ניתוקים היו בזמן נתון\").

    אלו דוגמאות מהעולם האמיתי של ל Partial Service ניתן לתת? הנה כמה שאני נתקלתי בהן:

    • לוגים, לוגים, לוגים! בעם הייתה לנו מערכת עם שירות ירוד כמעט יומיים (!!) עד שהבנו שהיא נופלת כל הזמן כי הדיסק מלא ופעולות כתיבה ללוג נכשלות. אם כתיבה ללוג נכשלת – עדיף לא לכתוב לוגים, מאשר לגרום ל IO exceptions שמשבשים תהליכים שלמים במערכת. 
      • בווריאציה אחרת מערכת מרוחקת לדיווח של בעיות (סוג של alerts) הגיבה ב latency של 4 שניות, וגרמה לשיבושים רבים בשירות שדיווח לה על בעיות זניחות, יחסית.
    • שירות ש\"מצייר\" מסלול נסיעה על המפה של החשבונית (בעולם המוניות). אם הוא לא זמין / מגיב היטב – שלח חשבוניות בלי ציור של המסלול, מה הבעיה?
    • שירות שמבצע סליקה של תשלומים. עדיף לשמור את סכום העסקה ולנסות לחייב כמה דקות מאוחר יותר (תחת סיכון של חיובים, עד סכום מסוים, ללא כיסוי) – מאשר לדחות על הסף את כל העסקאות, אפילו בפרק זמן קצר יחסית (כמה דקות).
    • וכו\' וכו\'

    Throttling

    עוד וריאציה דומה של Circuit Breaker היא מנגנון throttling (\"להחזיק אצבע על הקשית כך שלא יהיה זרם חזק מדי\").

    למשל (מקרה אמיתי שנתקלתי בו): ידוע שיש שרת Backend מרוחק שאין לו יכולות scale. המערכת שלנו – עם scale משתנה. אם נפנה לשרת המרוחק בקצב גבוה מדי, הוא עלול להתרסק (לא רק \"להגיב לאט\") – מה שיפגע בחזרה, ובצורה מורגשת בשירות שלנו.

    ניתן לייצר circuit breaker ופשוט לספק שירות חלקי בזמן שהוא \"שוכב מעולף על הרצפה\" או שאפשר להגביל, מתוך אינטרס הדדי, את קצב הקריאות אליו – לכזה שלא יקשה עליו יותר מדי (ולהוסיף circuit breaker, במידה והדברים לא מתנהגים כפי שציפינו).

    המנגנון הוא דיי פשוט (אתאר את המימוש הספציפי) – עוקבים אחר מספר הבקשות הפעילות לשרת המרוחק בעזרת distributed, non-blocking lock (קל יותר באנגלית) על בסיס רדיס: לפני קריאה לשרת המרוחק מנסים \"לקבל\" Lock – אם מצליחים – מבצעים את הקריאה. אם לא מצליחים (כי כבר יש 10 קריאות פתוחות) – מחזירים הודעת \"תסתדרו בלי!\" לפונקציה שביקשה את השירות, ונותנים לה לספק שירות חלקי.

    תלויות בין שירותים הן כמו אבני דומינו: שירות אחד שתקוע (או אטי מאוד) יכול לתקוע עוד ועוד שירותים בשרשרת עד להפסקת שירות של כלל המערכת.

    TCP timeouts

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

    למרות ש TCP הוא \"reliable protocol\", גם לו יש תקלות: בעיקר התנתקויות. כאשר קצב הקריאות בין שירותים הולך גדל – אנו נחווה תקלות אלו יותר ויותר.

    התנהגות נפוצה היא לספק אותו ה timeout ליצירת ה connection וביצוע הקריאה עצמה (פעולת \"קריאה\"). אבל:

    • פעולת ה connection היא פעולה פשוטה ומהירה – היא אורכת, באופן טיפוסי, שבריר של שנייה (בניכוי network latency).
    • פעולת הקריאה לרוב גורמת לשירות השני לעבוד: לקרוא מידע מבסיס הנתונים, לקרוא לשירותים אחרים, לבצע עבודת CPU משמעותית וכו. זמן מקובל לקריאה שכזו הוא כ 100ms וגם לא נדיר להיתקל במצב של 1000ms ויותר (שוב: בניכוי network latency).

    לכן, אם נגדיר timeouts באופן הבא:

    • עבור פעולת ה connection של ה TCP – נגדיר timeout בסך ה: tolerable latency
    • עבור פעולת ה read של ה TCP – נגדיר timeout בסך: tolerable latency + tolerable server time

    נוכל לצמצם בצורה מורגשת זמני המתנה מיותרים.

    כמו כן, אם יש לנו timeout על connection – כדאי לשקול בחיוב פעולת retry מיידית.
    ניתן להראות ש timeout קצר ליצירת connection + ביצוע retry יחיד – היא מדיניות שברוב המקרים תהיה עדיפה בהרבה על יצירת connection עם timeout ארוך.

    הסבר (דרוש רקע תאורטי בעבודה של TCP): אם ההודעה על לחיצת היד השלישית של TCP אובדת – אין מנגנון (מלבד timeout) לדעת זו. בעיה זו במדעי-המחשב ידועה כבעיית הגנרלים, ומשמעותה שלא ניתן לייצר מנגנון אמין ל handshake. הפתרון של TCP הוא לבצע 3 קריאות handshake בתקווה שזו נקודת איזון טובה בין עלות (מספר הקריאות) / סיכון לכישלון.

    אחד הנתונים הידועים ב AWS הוא ש latency בין AZs יכול להיות עד 10ms. אם מסתכלים על הנתונים עצמם רואים ש 10ms הוא לא ממוצע, אלא אירוע נדיר יחסית: בבדיקות שערך Matthew Barlocker (חברת Lucid Software) – הוא ראה שבאחוזון ה 99.85% מקבלים latency בין AZ של 3ms בלבד:

    הערה: אמזון מתעדפת נמוך (de-prioritize) קריאות ICMP (פרוטוקול השליטה של TCP/IP, הכולל גם את פקודת ה ping) – ולכן לא כדאי להסתמך על Ping להערכת ה latency ב AWS.

    מסקנה אפשרית אחת היא שסביר לקבוע TCP Timeout של 3ms כאשר ליצירת connection באותו ה AWS region.
    על פעולות HTTP GET ניתן לשקול (תלוי במקרה) מדיניות דומה של מתן timeouts קצרים יחסית (יש לקחת בחשבון את זמן השרת + network latency) – עם אפשרות ל retry.

    \"Be Resilient\" vs. \"Fail Fast\"

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

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

    מדיניות טובה אחרת אומרת: Fail Fast – אם המערכת מזהה שגיאה או סבירות גבוהה לשגיאה, עליה \"לשבור את הכלים באותו הרגע\" – וכך יהיה קל יותר לאתר את התקלה. אם התקלה נגררת ונגררת – ה\"שבר\" צפוי להיות בשלב מאוחר יותר במערכת, בשלב שיהיה קשה מאוד להבין ממנו מה מקור הבעיה האמיתי (root cause).

    כיצד 2 גישות חכמות אלו משתלבות זו עם זו בעולם של מערכות מבוזרות, השאופות ליציבות גבוהה?

    למרות הסתירה הבסיסית, ניתן לשלב את שתי הגישות:

    • אם מזהים כשל שנראה שניתן \"לספוג\" – אפשר לבצע Alert Fast, בשאיפה שה Alert מספיק ברור ומשמעותי כדי שיהיה ניתן לנטר אותו ולטפל בו בזמן סביר.
    • אם מזהים כשל שנראה שלא ניתן \"לספוג\" אותו – זהו באמת מצב שכדאי לעשות Fail Fast.
    פעמים רבות, מה שמתחיל כ\"כשל חלקי\" שניתן לספוג אותו – מתגלגל בהמשך לכשל משמעותי יותר שלא ניתן עוד לספוג (\"כמות הופכת לאיכות\"  – של הכשל). אם אתם בוחרים בכיוון של Alerts – כדאי שאלו יהיו Alerts שתוכלו, ואכן תתייחסו אליהם – בזמן סביר.

    סיכום

    בעוד שמערכת יחידה (\"מונוליטית\") מתעסקים באופטימיזציה של ה up-time (מדד ברור למדי) – במערכות מבוזרות, המציאות יכולה להיות מורכבת / גמישה ברבה.

    אם המערכת המבוזרת מתוכננת נכון – היא יכולה להימצא במצבים שונים של כשלים חלקיים לאורך הזמן.
    לא עוד מצב \"למעלה\" מול מצב \"למטה\", אלא יותר בנוסח \"כואב לי קצת הראש, אבל אני סה\'\'כ בסדר\" 🙂

    היכולת של מערכת להימצא במצבים שונים של כשלים-חלקיים מוסיפה מורכבות למערכת – אך מאפשרת להגיע לזמינות (availability) גבוהה יותר כאשר מדובר במערכת מורכבת.
    העצה של מרטין פאוולר ל MonolithFirst היא טובה בהיבט הזה: מערכת מבוזרת היא בהחלט מורכבת יותר – ומבחינת זמינות  משתלם לעבור למודל מבוזר רק כאשר כלל המערכת הגיע לסף מסוים של מורכבות.
    זמינות גבוהה של מערכת מושגת לא רק על ידי תכנון נבון, אלא גם ע\"י אימונים ושיפור תמידי.
    Chaos Monkey, למשל, הוא כלי Open Source ש\"יפיל\" לכם בצורה יזומה אך אקראית שירותים (או שרתים) במערכת – כדי שתוכלו לבדוק את ההתמודדות שלכם, ולשפר את צורות התגובה שלכם לכישלון שכזה.
    בשלב ראשון ניתן להפעיל אותו בסביבת בדיקות – בה נפילה היא למידה ללא נזק, ובהדרגה ניתן להתקדם לסביבות יותר ויותר מציאויתיות ומחייבות. אם אתם מסוגלים להפעיל אותו ב production, בשעות העומס שלכם, ולשרוד עם שירות סביר – אז אתם בליגה העולמית!
    שיהיה בהצלחה!