-->

יום שלישי, 16 במאי 2017

כשאתה אומר "Event-Driven Architecture", למה אתה מתכוון?

אחד הכנסים האהובים עלי, שאני נוהג לעקוב אחרי התוכן שמוצג בו הוא כנס ;goto

ה Keynote ב Goto Chicago שהתקיים לפני מספר ימים היה של מרטין פאוולר, בו הוא ביצע סיווג של סגנונות של "Event-Driven Architecture" (או בקיצור: EDA).


יש לי באופן אישי בעיה עם ההגדרה "Event-Driven Architecture".
  • האם הארכיטקטורה עצמה מונעת על ידי אירועים?
  • האם באמת הפרט החשוב ביותר בארכיטקטורה הוא האירועים או כיצד הם מטופלים?

לומר ש "יש לנו ארכיטקטורה שהיא Event-Driven" זה כמעט כמו לומר "יש לנו ארכיטקטורה שהיא Database Driven" או "ארכיטקטורה שהיא Java Driven". כלומר:

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

בכלל, ככל שמערכת גדלה, ובמיוחד כאשר היא נעשית מבוזרת - יותר סביר שנשתמש באירועים איפשהו במערכת. האם זה הופך את המערכת שלנו ל Event-Driven?

ובכן... מדוע אנשים מציינים שיש להם "Event Driven Architecture"? כנראה כי:
א. זה נשמע "טוב". באזז מרשים.
ב. כי אותם אנשים מרוצים מהשימוש ב events - ונתונים לתכונה הזו של המערכת דגש מיוחד.

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

הייתי קורא למערכת כזו "Event Based", ומנסה להבין מה מניע את המערכת והארכיטקטורה שלה. האם מדובר ב Scalability? האם חלוקה של תהליך עיבוד ליחידות קטנות ופשוטות יותר?


אז למה באמת מתכוונים ב "Event-Driven Architecture"?


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

מתוך הספר Event-Driven Architecture: How SOA Enables the Real-Time Enterprise

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




בספרון של מארק ריצ'רדס בשם Software Architecture Patterns הוא מתאר שני סוגים עיקריים של EDA:
  • Mediator Topology - בה יש "מח" מרכזי שיודע איזה שלבים יש לכל event, אלו ניתן למקבל וכו'- והוא זה שמנהל אותם
  • Broker Topology - בה החוכמה היא "מבוזרת" וכל רכיב שמטפל ב event יודע מה היעד הבא של ה Event / או אילו events חדשים יש לשלוח.
הוא בוחן את נקודות החוזק והחולשה בכל גישה - וזה נחמד, אך הוא עדיין לא ממש מספק תמונה שמכסה את השימושיים העיקריים של events במערכות תוכנה.


קצת עזרה??


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

פאוולר מגדיר ארבעה צורות עיקריות לשימוש ב Events במערכת. אם נשתמש במונחים הללו, ולא במונח הכללי "Event-Driven Architecture" - הרי נוכל להבין טוב יותר אחד את השני.


Event Notification


זה המקרה הפשוט בו אני רוצה להעביר הודעה בין מודול א' למודול ב' בצורה אסינכרונית.
למשל: 
  1. אפליקציית הנהג מודיעה שהסיעה הסתיימה.
  2. המערכת לניהול נסיעות סוגרת את הנסיעה ושולחת הודעה שהנסיעה הסתיימה.
    הקריאה היא אסינכרונית בפועל: אין צורך להמתין לחיוב שהתבצע - אלא רק ל Queue שהוא אכן קיבל את ההודעה. אם מערכת החיוב פונה לחברות האשראי ייתכן שייקח לה מספר שניות לבצע את החיוב, בעוד Queue יקבל את ההודעה ב מילישניות בודדות.
  3. מערכת החיוב בודקת את ה Queue כל הזמן אם יש הודעת. כאשר היא שולפת הודעות - היא מטפלת בהן ב"זמנה החופשי".

המודל של Event Notification הוא פשוט ושימושי.
  • הוא מתאים כאשר מישהו רוצה לשלוח הודעה והוא לא מצפה לתשובה.
  • הוא מתאים כאשר הצד מרוחק יבצע את הפעולה בקצב שונה. למשל: בשל תקלה בחברת כרטיסי האשראי ייתכן שהחיוב יתבצע רק לאחר שעה (דוגמה קיצונית).
  • הוא מאפשר למערכת ניהול הנסיעות להיות בלתי תלויה במערכת החיוב: אם יום אחד רכיב אחר יטפל בחיוב / הטיפול יחולק בין כמה רכיבים - מערכת ניהול הנסיעות לא תשתנה כתוצאה מכך.
    • חוסר התלות הזו היא לא מושלמת: מה קורה כאשר מערכת החיוב זקוקה לנתון נוסף לצורך החיוב? (על כך בהמשך)
    • כלל טוב לצמצום התלות הוא לשגר אירוע "כללי" שמתאר את מה שקרה במערכת "נסיעה הסתיימה" ולא פקודה כמו "חייב נסיעה!". שימוש במינוחים של פקודה גורם לנו לקבל בצורה עמוקה יותר את התלות בין המודולים - ולהעצים אותה לאורך הזמן.
  • כאשר יש ריבוי של Events Notifications במערכת - קשה יותר לעקוב אחרי ה flow, במיוחד כאשר events מסוימים מתרחשים רק לפעמים ו/או במקביל.
    Mitigation אפשרי הוא מערכת לוגים מרכזית ופעפוע "request id" (ואולי גם hop counter) על גבי ה events. כל כתיבה ללוג תציין את ה request id - וכך יהיה אפשר לפלטר את כל מה שהתרחש במערכת במערכת הלוגים ולראות תמונה שלמה. בערך.

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


בגישת קיצון אחת, להלן הגישה העצלה - ניתן לשלוח ב event רק את id של הנסיעה שהסתיימה. מערכת החיוב תשלים את הנתונים החסרים ממערכת הנסיעות / מערכות אחרות.
בגישת קיצון שנייה, להלן הגישה הנלהבת (eager) - אנחנו מעדכנים את ה event לכלול את כל הנתונים שמערכת החיוב זקוקה להם.

בשני המקרים, יש לנו תלות מסוימת במערכת החיוב: שינוי במערכת החיוב עלול לדרוש שינוי במערכת ניהול הנסיעות - בגרסה אחת להוסיף שדה ב API של מערכת ניהול הנסיעות, בגרסה שניה - נתון על ה event.

איזו גישה עדיפה? - אין לי תשובה חד משמעית.

בד"כ אני מעדיף את הגישה הנלהבת, כי:
  • יותר קל לעקוב מה מתרחש - כי אפשר להסתכל על ה event ולהבין אלו נתונים נשלחו - ובמקום אחד.
  • הערכים שעברו על ה event הם קונסיסטנטים ל event - לא יקרה מצב שבחצי שנייה (או דקה) שלוקח למערכת החיוב לבצע קריאה - אחד הערכים השתנה.
  • יש פחות קריאות ברשת, ומעט פחות דברים שעשויים להשתבש.
כמובן שאם יש נתונים שצריכים להיות fresh, כדאי לערבב בין הגישות - ולקרוא אותם ברגע האמת בעזרת API.



Event-Carried State Transfer

גישה זו היא וריאציה של גישת ה Event Notification, אבל שינוי אחד בה - משנה בצורה משמעותית את כללי המשחק:

  1. אפליקציית הנהג מודיעה שהנסיעה הסתיימה.
  2. מערכת ניהול הנסיעות שולחת את כל הנתונים שיש לה על ה event.
    איך שולחים את כל הנתונים? בד"כ לוקחים את אובייקט המודל של ה ORM - ועושים לו serialization. 
  3. מערכת החיוב בודקת כל הזמן אחר הודעות. כאשר יש הודעה - היא קוראת ממנה רק את הנתונים שהיא זקוקה להם. היא עשויה לשמור עותק מקומי שלהם.
בעצם במקום להעביר הודעה, אנחנו מעבירים את ה state השלם של האובייקט בין המערכות השונות.

לגישה זו כמה יתרונות:
  • השגנו isolation גדול יותר (במימד אחד): שינוי במערכת החיוב לא ידרוש שינוי במערכת ניהול הנסיעות.
  • Availability - אם מערכת ניהול הנסיעות קרסה, ניתן להמשיך לבצע חיובים, כי למערכת החיוב יש את כל הנתונים שהיא צריכה.

לגישה יש גם כמה חסרונות:
  • שברנו את ה encapsulation: מערכת החיוב מכירה את מבנה הנתונים הפנימי של מערכת ניהול הנסיעות. מעכשיו יהיה קשה הרבה יותר לבצע שינויים במבנה הנתונים, ויש גם סכנה שהמתכנת של מערכת החיוב לא יבין את השדות נכון - ויפעל ע"פ נתונים מוטעים.
  • העברנו הרבה נתונים מיותרים ברשת - בד"כ זו בעיה משנית.
  • יצרנו עותקים שונים של הנתונים ברשת, מה שפוטנציאלית יוצר בעיה של Consistency בין הנתונים. נתונים שכן צריכים להיות up-to-date לצורך הפעולה - יהיו לא מעודכנים ויתכן שיהיה צורך בליישם מנגנון של eventual consistency.

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


מה קורה כאשר מידע נוסף נמצא על מערכת שלישית? למשל, את הפרט אם נסיעה התחילה בשדה תעופה (ואז יש לתת 30% הנחה ;-)) ניתן להסיק רק כאשר יש נתונים נוספים ממערכת האזורים?

גם כאן יש בחירה: אפשר לתת את המשימה למערכת ששולחת את ה event, או לזו שמקבלת אותו. כנראה הבחירה תעשה ע"פ איזו תלות בין המערכות - היא סבירה יותר.




Event Sourcing


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



אם מישהו רוצה לקרוא את האובייקט - הוא צריך להרכיב את שרשרת השינויים בכדי לקבל את האובייקט העדכני.

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

פה עשויה לעלות שאלה פילוסופית: אם אני מקבל את העדכונים כ delta, אבל אז בכל עדכון עושה merge עם האובייקט שאני מחזיק אצלי - האם זה עדיין Event Sourcing?

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



Command Query Responsibility Segregation (בקיצור: CQRS)

מספרים שאם הייתם מחפשים בגוגל "CQRS" לפני עשור, הוא היה שואל: "`?did you mean `Cars"
הרעיון הוא דיי ישן, ומקורו בשנות השמונים, אבל רק בשנים האחרונות הוא הפך למאוד-מוכר.

אני מניח שהרוב הגדול של הקוראים מכיר את השם, אבל לא בהכרח מכיר את הרעיון מאחוריו. לרוב האנשים CQRS מתקשר ל "high performance".

האמת שהרעיון של CQRS אינו קשור קשר ישיר ל events, אבל פעמים רבות - משתמשים בו כך.

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




מתי זה שימושי?

כאשר דפוס הקריאה ודפוס הכתיבה שונים זה מזה.

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

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

במקום זה, לאחר שכתבתי את ה tweet יש Background Processor שמעתיק את הטוויט שלי לפיד של כל העוקבים.
כלומר:
  • מודל "הכתיבה" הוא רשימה של טוויטים ע"פ מחבר.
  • מודל "הקריאה" הוא הפיד של כל משתמש בנפרד.
זה אומר שיש הרבה שכפול נתונים במערכת, ושטח האחסון הנדרש הוא אדיר. אם יש למישהו מיליון עוקבים - כל טוויט ישוכפל מיליון פעמים.
מצד שני, זה גם אומר שגם אם אני עוקב אחרי 1000 פרופילים ויותר - הפיד שלי ייטען ב (O(1.
במקרה של טוויטר סביר שמודל הכתיבה ומודל הקריאה הן בכלל מערכות שונות - כאשר כל הוספה של טוויט למודל הכתיבה - שולחת אירוע של עדכון (event notification) למודל הקריאה - שם נמצא ה Background processor.

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

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


שימוש אחר של CQRS הוא לא דווקא מבחינת ביצועים גבוהים, אלא פשטות של הקוד. אם המודלים סבוכים למדי - החלוקה ל"מודל כתיבה" ו"מודל קריאה" יקטינו את ה code base בחצי, בערך.

כאן שווה לציין ש Event Sourcing ו CQRS הולכים יד ביד זה עם זה:
מודל הכתיבה הוא ה State Log - אבל יש מודל קריאה שהוא המצב העדכני. זה יכול להיות בסיס נתונים או טבלה אחרת בה שומרים את המצב העדכני, וזה יכול להיות מודל שעובד מעל אותם נתונים - ורק מכיל את הקוד של "השטחת" העדכונים בזמן ה Query.




סיכום


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

הייתי מעורב בבניית מערכת שמבצעים בה רפליקציה של נתונים לשירותים אחרים, כמו בגישת ה Event-Carried State Transfer - בכדי להשיג High Availability. מצד שני, כמות הנתונים שמועתקת היא קטנה ומדודה מאוד, והנתונים הם ברמת הפשטה של ממשק ולא מבנה נתונים פנימי - כך שאין פגיעה בהכמסה של המערכת.

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

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


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


---

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

ההרצאה של פאוולר ב Goto; Chicago
פוסט של פאוולר בנושא

יום שלישי, 2 במאי 2017

על אימות זהות בווב, וקצת על OAuth 2.0 ו OpenID Connect

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

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

אנחנו ניגשים לאנשי השרת - וגם הם לא ממש בטוחים מה לעשות:
  • האם יש משהו לא טוב ב Basic Authentication? הרי "Basic is beautiful" (ע"פ פוקס).
  • אולי עדיף להוסיף כל מיני "טריקים שמקשים"? אולי לחפש ב StackOverflow?

הנה מדריך יפה שמצאתי בגוגל, "The Ultimate Guide Of Mobile Security" של חברת StormPath (לארונה נרכשה ע"י OKTA).
בטוח שיש בחברה הזו מומחי אבטחה גדולים ממני. אין ספק!
מצד שני... זה המדריך האולטימטיבי? האם הוא באמת משרת את צורכי האבטחה של הקורא, או בעיקר עושה On-Boarding מהיר ספציפית לפתרון של StormPath?

גם כשאתם ניגשים למדריך מוכן, כדאי שיהיה קצת מושג - ועל כן הפוסט הבא.
אדבר על אימות שרת-לשרת, מובייל ועל אפליקציה שרצה בדפדפן, על Basic Authentication אבל גם על OAuth 2.0 וקצת על OpenID Connect.


נתחיל מההתחלה: Basic Authentication (בקיצור: BA)


נתחיל במקרה הפשוט ביותר (מבחינת האבטחה): תקשורת שרת-לשרת.

שיטת אימות הזהות (Authentication) הבסיסית ביותר בווב נקראת "Basic Authentication".
נניח שיש לנו תסריט בו סוכן-נסיעות (להלן "שוקה") רוצה לתקשר בצורה מאבטחת עם חברת תעופה מסוימת (להלן "שחקים").

הנה אופן הפעולה:

1. המתכנת של שחקים מייצר מפתח-זיהוי ייחודי וקשה מאוד לניחוש, שיזהה את הלקוח הספציפי - שוקה:
jsa7arpZ8sPZ60YZyZwfD97gf5cHbEBj77VF6nF4
מחרוזת אקראית באורך 40 תווים נחשבת כיום למפתח מספיק חזק.

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

3. כאשר השרת של שוקה פונה לשרת של שחקים, על גבי HTTPS, הוא מוסיף על הבקשות את ה header הבא:
Authorization: Basic anNhN2FycFo4c1BaNjBZWnlad2ZEOTdnZjVjSGJFQmo3N1ZGNm5GNA==
הפרמטר הראשון אומר איזו סוג זיהוי (Authentication) מדובר. במקרה הזה: BA.
הפרמטר השני הוא ה credentials ("אישור ההרשאות"), במקרה הזה: מפתח הזיהוי מקודד ב Base64.

Base64 הוא קידוד של binary-to-text הנדרש על מנת להעביר את המידע על פרוטוקול HTTP בצורה תקינה. הפרוטוקול מצפה לטקסט, ותווים מסוימים יכולים להתפרש אחרת לחלוטין (למשל: שורה ריקה משמעה שהסתיימו ה headers ומתחיל ה body של בקשת ה HTTP).

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


חוזקות:
  • פרוטוקול פשוט שקל ליישום.

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


סה״כ, עבור תקשורת שרת-לשרת, בסביבה שאיננה מרובת איומים - ה BA הוא סביר.
נקודת מפתח היא הקושי ליירט תקשורת שרת לשרת (בהנחה שהשרת לא יושב במקום ציבורי ומשתמש ב Wifi / תא סלולארי)



גרסאת ה Web Client

כאשר התקשורת היא בין דפדפן לשרת, ה flow עובד מעט אחרת:
  • אין תקשורת מוקדמת עם המשתמש, ולא שלוחים לו מפתח-זיהוי.
  • המשתמש פונה ל URL הרצוי.
  • השרת שאינו מזהה את המשתמש, מחזיר HTTP Status 401, כלומר: Unauthorized - "אני לא יודע מי אתה". כמו כן הוא שולח header המסביר באיזו סוג authentication הוא תומך:
WWW-Authenticate: Basic
  • הדפדפן יקפיץ למשתמש חלון להקלדת שם-משתמש וסיסמה.
  • הדפדפן ייצור מחרוזת credentials כ ״שם משתמש:סיסמה״ - יקודד אותה ב Base64 ויעביר אותה על ה Authorization Header בקריאה הבאה:

Authorization: Basic <credentials>
  • השרת יפתח את הקידוד ויזהה אם יש לו משתמש עם סיסמה שכזו. אם כן - הוא יניח שהבקשה הגיעה מהמשתמש.
  • שם המשתמש והסיסמה ישארו ב cache של הדפדפן לזמן מוגדר (15 דקות?). בכל בקשה לשרת יישלח ה Authorization Header עם הסיסמה.


חוזקות:
  • פרוטוקול פשוט שקל ליישום.

חולשות:
  • שם המשתמש והסיסמה עוברים כ clear text על כל בקשה (Base64 שקול ל clear text). תקשורת בין endpoint (לצורך פוסט זה: ״תחנת קצה״ -כמו מחשבים ניידים או סמארטפונים) לשרת - הרבה יותר קל ליירט מאשר תקשורת שרת לשרת. למשל: התוקף ותחנת הקצה נמצאים על אותה רשת Wifi.
    • אם התקשורת נעשית על גבי HTTP (רחמנא ליצלן!) - אזי גניבת הסיסמה היא פעולת sniffing פשוטה.
  • שם המשתמש והסיסמה נשארים cached בדפדפן. תוקף יכול לעשות שימוש גם הוא ב credentials מבלי שאף אחד יידע (התקפת CSRF).

סה״כ, BA בדפדפן היא שיטה שנחשבת כחלשה למדי לזיהוי מאובטח של המשתמש, והיא איננה מומלצת לשימוש.



גרסאת המובייל

במובייל יש כמה אפשרויות:
  • אפשר לנסות ולהיצמד לגרסת השרת של ה BA: ולשלוח מפתח-זיהוי לכל משתמש - אך זה אפשרי רק עם התקנה ידנית של Certificate (תהליך סזיפי למשתמש) או מערכות ששותלות Agent במכשיר ועושות זאת - בעיקרון מערכות Enterprise שלא ישימות בסביבת Consumers.
  • אפשר לנסות ולהיצמד לגרסת הדפדפן של ה BA: ולשמור את שם המשתמש והסיסמה באחסון המוגן של המכשיר (shared preferences / key chain) - ואז לשלוח אותם כ credentials בכל בקשה.
    שליחת ה credentials, בכל בקשה, ובמיוחד מתוך endpoints היא חולשה משמעותית - ולכן לא ניתן להמליץ על הגישה הזו.


ל HTTP יש עוד שיטת Authentication שנקראת HTTP Digest Access Authentication, שמנסה להקשות על גילוי הסיסמה - אך בפועל כיום היא נחשבת שיטה פחות בטוחה מ BA.



הבו לנו OAuth 2.0!


רוב הסיכויים שאתם שמעתם על פרוטוקול 2.0 OAuth ל Authentication. יש לו שם של פרוטוקול מכובד "Enterprise Level", אולי הנפוץ ביותר לשימוש היום - ולכן אפשר לסמוך עליו!

המחשבה ש"אני משתמש ב OAuth 2.0 - ולכן אני מאובטח", היא אולי נעימה - אך לא ממש נכונה. ל OAuth 2.0 יש יתרונות - אך גם חסרונות.

ראשית כל OAuth 2.0 הוא פרוטוקול ל Delegated Access ("ייפוי כח") ולא ל Authentication ("אימות זהות). הפרוטוקול נבנה על מנת לאפשר "ייפוי כח" למישהו אחר לגשת למידע שבבעלותי אך נשמר על ידי צד שלישי. בתוך ה Protocol מוגדרת "מסגרת ל Authentication" שהיא כללית, ואיננה נחשבת כחזקה במיוחד.

החלק הזה "מפיל" אנשים, ולכן אני הולך לתת חיזוק לטענה:

מתוך האתר הרשמי של OAuth, קישור: https://oauth.net/articles/authentication 

והנה הגדרה פורמאלית מה OAuth כן עושה:

The OAuth 2.0 authorization framework enables a third-party application to obtain limited
access to an HTTP service, either on behalf of a resource owner by orchestrating an
approval interaction between the resource owner and the HTTP service, or by allowing the
third-party application to obtain access on its own behalf.

שימו לב לקשר החזק ל HTTP.

מכיוון Authentication ב OAuth היא רק Framework ולא פרוטוקול - אין הגדרה חד-משמעית ומדויקת כיצד ה Authentication אמורה להתבצע. זה אומר ש:
  • ה Framework משאיר נושאים מסוימים לבחירת המימוש: איך להשתמש ב scopes, איך לבצע endpoint discovery, רישום דינאמי של לקוחות, וכו'.
  • אין בהכרח תאימות בין מימוש א' למימוש ב'. אם גוגל ופייסבוק מאפשרים חיבור ב OAuth - לא בטוח שאותו מימוש (של Client) יוכל לעבוד מול שניהם.
  • יש מימושים מאובטחים יותר, ומימושים מאובטחים פחות. תו התקן "OAuth" מציין בעיקר את סדר ההתקשרות - אבל מבטיח אבטחה רק במידה מסוימת.
    • למשל: לא נאמר כיצד להעביר את מפתח ההזדהות. ב URL או כ Header? (עדיף Header כי פחות סביר שהמידע הרגיש הזה יצוץ אח"כ בלוגים)
    • כיצד להעביר את המידע בין השחקנים השונים? לכאורה עדיף להצפין את נתוני ה token, אך יש כאלו שמשאירים אותם גלויים כי נוח שה Client יכול לקרוא גם הוא נתונים על המשתמש....
    • הנה רשימת עשרת החולשות הנפוצות ביותר במימושים של OAuth 2.0.


כיצד OAuth 2.0 עובד (בקצרה)

OAuth בעצם מגדיר ארבע אופנים שונים (נקראים Grants) לקבל Token ולהשתמש בו.

ה Token הוא תחליף ל Password, מכיוון שיצירת password במערכות רבות היא סכנת אבטחה ממשית: אנשים נוטים לעשות שימוש חוזר ב passwords שלהם במערכות שונות, ומערכת אחת שנפרצת - מאפשרת גישה למערכות נוספות, לעתים חשובות יותר.

כדי להבין את OAuth כדאי להשקיע דקה ולהכיר את ה"שחקנים" (Roles) המדוברים:

Resource Owner 
זה בעצם המשתמש, וכך גם אקרא לו בפוסט.

Resource Server (נקרא גם בשם המבלבל "protected resource")
אקרא לו בפוסט גם "שירות B" - השרת שמאכסן את הנתונים של המשתמש, למשל פייסבוק.

Authentication Server 
השרת שמולו המשתמש מזדהה, כלומר שם מתבצע תהליך ה Authentication והוא מנפיק Token שמאמת את זהות המשתמש. ברוב המקרים זהו יהיה ה Resource Server עצמו (או שירות B) - אך התקן לא מחייב זאת.

Client
אקרא לו בפוסט גם בשם "שירות A" - השירות שאליו בעצם פונה המשתמש בבקשה לשירות.
שירות זה לקוח של ה Resource Server והוא מבקש ממנו גישה למשאבים השייכים למשתמש, להלן "ייפוי הכח".


Authorization code grant

זהו כנראה אופן השימוש הנפוץ ביותר ב OAuth. אתחיל בלתאר אותו בצורה "סיפורית":

עכשיו המשתמש רוצה להתחבר לשירות A (למשל: ל Comeet), שם מופיעה לו אופציה להתחבר בעזרת החשבון הקיים שלו בשירות B, למשל: "Connect with Linkedin".


המשתמש מקיש על הלינק של "חיבור בעזרת...", וקופצת חלונית חדשה בדפדפן (יש הנחה שמדובר בדפדפן) בה מתבצע תהליך ה Authentication מול ה Auth. Server, שהוא לרוב בעצם שירות B עצמו (למשל: לינק-אין).

שירות B מציג אלו פריטי מידע (resource) הוא התבקש לחשוף (לעתים זהו רק ה email, לעתים רשימת חברים ויותר) וכאשר המשתמש מאשר - הוא חוזר לאתר המקורי (שירות A) ומקבל אליו גישה.

התוצאה: המשתמש קיבל גישה לשירות A מבלי שהיה צריך להזין בו שם משתמש / סיסמה. לעתים חושבים שלא נוצר שם חשבון - אך לרוב הוא נוצר על בסיס מזהה ייחודי שסיפק שירות B.


עכשיו נחזור על ה flow בצורה יותר טכנית:

לפני שהכל מתחיל, ה Client (למשל "Site") נרשם אצל Resource Server (למשל: Facebook). כתוצאה מתהליך הרישום ה Client מקבל client_secret ("מפתח זיהוי") - אותו הוא שומר בצורה מאובטחת.


כעת המשתמש רוצה להתחבר לשירות A:
  • שירות A מבקש מהמשתמש להזדהות, ומאפשר לו לעשות את זה בעזרת שירות B.
  • המשתמש בוחר בשירות B, ומופנה ל Authentication Server המתאים.
    • שירות A סומך על ה Authentication Server לבצע אימות מאובטח מספיק, ולנפק access-token  מתאים.
    • בד"כ מוצג למשתמש אילו נתונים הוא הולך למסור, ולעתים הוא יכול להחליט לא למסור חלק מהנתונים. כל קבוצת נתונים מוגדרת כ scope. על ה redirect URL ה Client ציין אלו scopes הוא מבקש.
    • המשתמש מזדהה בעזרת שם משתמש וסיסמה (או כל אימות אחר) ומאשר את ה scopes.
  • ישנו Redirect חזרה ל Client עליו מופיעים פרטים של הבקשה המקורית ל Authentication - שבהם ישתמש ה Client על מנת לאמת שזו אכן תשובה לבקשה שנשלחה, authorization code.
  • ה Client עכשיו פונה ל Auth. Server בעצמו,
    •  ושולח לו:
      • את ה authorization code שקיבל
      • client_id - זיהוי של ה client (לא של המשתמש), אינו סודי.
      • client_secret - קוד סודי שרק ה client מכיר (אותו הוא קיבל ברישום). 
    • בתמורה הוא מקבל אובייקט JSON המכיל:
      • תאריך תפוגה - ה token טוב לזמן מוגבל (באיזור השעה, בד"כ).
      • access_token - איתו הוא יכול לגשת לנתונים.
      • refresh_token - המאפשר לבקש גישה נוספת, במידה וה access_token פג תוקף.
    • הערה: התקן של OAuth לא מציין מה מבנה ה token, אך מקובל להשתמש ב JWT (קיצור של: JSON Web Token), פורמט הכולל 3 חלקים: Claims, Header, וחתימה.

העיקרון מאחורי ה tokens נקרא TOFU (קיצור של Trust On First Use), והוא אומר שבמקום לשלוח את מפתח הזיהוי בכל בקשה - תוך כדי שאנחנו חשופים ל sniffing, נצמצם את החשיפה לתקשורת הראשונה. בפעולה הראשונה אנו סומכים (יותר) על הצד השני, תחת ההנחה שפחות סביר להיות חשופים ל sniffing ברגע ההתקשרות הראשונית. אם ה access token, למשל, נחשף ב sniffing - הפגיעה תהיה מוגבלת לזמן הנתון, ועדיין ה refresh token וה client_secret לא נחשפו והם יכולים להמשיך לנפק בצורה מאובטחת יחסית access_tokens אחרים.

  • בשלב האחרון ה Client ניגש לשירות B בעזרת ה access_token - ושולף ממנו את הנתונים שביקש.
    לכאורה בתרשים זו קריאה בודדת, אך בעצם יכולות להיות עוד קריאות לאורך זמן.
    • אם פג התוקף של ה access token, ה Client לא יורשה לקבל את הנתונים. הוא יצטרך לפנות עם ה refresh_token ל Auth. Server ולקבל access_token ו refresh_token חדשים.


ציינו שיש 4 סוגים שונים של Grant בתקן. הנה תרשים החלטה מקובל שעוזר להחליט באיזה Grant כדאי להשתמש (מבוסס על תרשים של Alex Bilbie):



בקצרה:

  • Authorization Code Grant - הוא ה flow שעברנו עליו. אם המשאב הוא זיהוי של המשתמש (למשל: email), אזי יישמנו סוג של מנגנון Authentication על גבי OAuth.
    • ה Flow מתבסס על המצאות דפדפן והיכולת לבצע Re-direct (כלומר: Multi-page app, לפחות לשלב ה login).
    • ה Client הוא צד-השרת של האפליקציה, בו ניתן לשמור את ה client_secret בצורה מאובטחת.
  • Client Credentials Grant - הוגדר עבור חלקים שונים של אותה האפליקציה (הגדרה: "ה Client עצמו הוא ה Resource Owner"), שיש בין החלקים אמון גבוה. האימות הוא שרת-לשרת, ולא דורש התערבות של המשתמש. זהו בעצם מן גרסה משופרת של Basic Authentication בין שרת לשרת שמיישם את עקרון ה TOFU. מיותר לציין שנעשה שימוש נרחב ב Flow זה גם בין אפליקציות זרות, וזה לא דבר רע בהכרח (יותר טוב מ Basic Auth).
    • Flow זה גם נקרא two-legged OAuth (כי יש בו רק שני שחקנים מעורבים), בניגוד לכל שאר ה flows הנקראים three-legged OAuth.
  • Implicit Grant - הוגדר עבור אפליקציות ווב או מובייל שבהן לא ניתן לסמוך ברמה גבוהה על ה client_secret. לאפליקציית mobile ניתן לעשות reverse engineering ולשלוף את הקוד, בווב - ...זה אפילו יותר קל.
    • יש כאן פשרה של אבטחה על מנת לאפשר כן סוג של Delegation סביר של הרשאות.
    • ב Flow הזה אין refresh Token - ולאחר שפג תוקפו של ה access_token יש לבצע Authentication מחדש.
    • לא מעבירים ל Client את ה Auth. Code (כי לא סומכים עליו - הוא לא מספק אבטחה מוגברת), אלא ישר שולחים לו את ה access-token.
  • Password Credentials Grant (נקרא גם Resource Owner Credentials Grant) - הוגדר עבור אפליקציות בעלות Trust גבוה. ב Flow הזה המשתמש מספק את שם המשתמש וההרשאות שלו לאפליקציה (שלא שומרת אותן!), והיא משתמשת בהן על מנת לקבל access_token בשמו. את ה access_token אפשר לשמור בצורה מאובטחת(!) באפליקציה. 
    • אם ה access_token נגנב אז אפשר לגשת איתו רק לנתונים מסוימים של משתמש יחיד - נזק מוגבל.
    • את ה access_token יש לחדש מדי פעם, על ידי זיהוי מחדש של המשתמש. באפליקציות מובייל לא מקובל לבקש מהמשתמש סיסמה מחדש יותר מפעם בזמן מה.... חודש, נניח?


באיזה Flow כדאי להשתמש באפליקציית מובייל?

ב Spec המקורי ההגדרה הייתה להשתמש ב Implicit Grant: מעט אמון, ומעט אבטחה.
מאז יש מעבר לשימוש ב Authorization Code Grant, כאשר משתמשים להרשאות ב Native Browser ולא WebView. למשל: SFSafariViewController ולא WKWebView.

האם אפליקציית מובייל יכולה לשמור Client Secret ב Source Code שלה בצורה אמינה? באנדרואיד, למשל, קל מאוד להוריד apk ולבצע לו Reverse Engineering. ההמלצה אם כן היא להחזיק את הקוד ויישום ה OAuth בקוד ++C (קרי שימוש ב NDK) ולא ב Java - שם ה reverse engineering הוא קשה משמעותית.

Stormpath (שנרכשו לאחרונה ע"י OKTA) בכלל לא דנים בשאלה - ושולחים את המתכנתים ליישם Password Grant. אני לא בטוח שלא מעורבים פה שיקולים עסקיים (לספק Flow שקל ללקוחות ליישם, ולא להטריד אותם ב"זוטות").

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


לסיכום:

בתקשורת שרת לשרת

חוזקות:
  • עדיין פשוט ליישום.
  • TOFU עדיף על העברה של המפתח בכל פעם.

חולשות:
  • לא תוכנן במקור לשימוש בין שרתים עם Trust מוגבל. 

סה"כ טוב מספיק עבור מגוון שימושים, ועדיף על פני Basic Authentication.



בתקשורת Web 

חוזקות:
  • עדיף על Basic Authentication, בכמה מובנים (בעיקר Authorization Code Grant).
  • הפך כמעט לקונצנזוס - יש מעט מאוד מתחרים, לכל אחד את הבעיות שלו.

חולשות:
  • לא תוכנן כפרוטוקול Authentication.
  • מימוש יחסית מורכב בצד-השרת (בצד-הלקוח המימוש טריוויאלי)
  • Room For Error: ה Specification שלו מבלבל, ומגוון האפשרויות - מובילים למימושים / יישומים רבים שאינם באמת מאובטחים דיים. מספק "תחושת אבטחה מוגזמת".


בתקשורת מובייל

חוזקות:
  • עדיף על Basic Authentication.

חולשות:
  • Mobile App נחשבת לעתים קרובות כ Trusted - למרות שזה שיקוף לא נכון של המציאות.
  • חלק ממנגנוני האבטחה של OAuth מסתמכים על המצאות דפדפן שאפשר לסמוך עליו: אין יכולת ל redirect. שימוש ב WebView פותח פתח ל Phishing / Clickjacking.
  • Implicit Grant טוב מ Basic Authentication רק במעט.



OpenID Connect 


מעט היסטוריה שתעזור להבין את השתלשלות האירועים:
  • 2006- יצא תקן לאימות זהות בשם OpenID 1.0.  הוא עובד בערך כך: "אני ליאור", שואלים צד שלישי שסומכים עליו: "האם זה ליאור?", התשובה היא "זה ליאור" - ואז מאשרים את זהותי.
  • 2007 - יצא OpenID 2.0 עם כמה שיפורים.
  • 2007 - יצא 1.0 OpenID Attribute Exchange (בקיצור OIDAE) - הרחבה של OpenID 2.0 המאפשרת גם למשוך פרטים נוספים על המשתמש "זה ליאור, הוא בן 40, ויש לו 3 ילדים".
  • 2010 - יצא OAuth 1.0
  • 2012 - יצא OAuth 2.0
  • 2014 - יצא פורטוקול OpenID Connect (בקיצור: OIDC) שמשלב בין OpenID 2.0 + OIDAE על גבי ה Framework של OAuth 2.0 בצירוף של JWT (וחברים). 

לא נכון להשוות בין OpenID ל OpenID Connect. זה כמו להשוות בין Ext2 ללינוקס (בהגזמה).
OIDC אינו תואם ל OpenID בגרסאותיו השונות - הוא רק שואב מהם רעיונות.



OIDC הוא פרוטוקול, והוא סוגר הרבה מהפינות הפתוחות של OAuth 2.0:
  • הוא מחייב כללים רבים שהם בגדר המלצה ב OAuth 2.0.
  • הוא מגדיר את מבנה ה tokens (על בסיס JWT) - שב OAuth 2.0 פתוחים לפרשנות.
  • הוא מגדיר scopes סטנדרטיים (email, phone, וכו') - המאפשרים התנהגות סטנדרטית מסביב למידע בסיסי של המשתמש. הפרוטוקול מגדיר endpoint חדש הנקרא userInfo Endpoint ממנו ניתן לשאוב מידע בסיסי על המשתמש.
  • הוא גם פותח בתקופה בה אפליקציות מובייל הן מאוד משמעותיות, ויש התייחסות רחבה יותר למובייל בהגדרת הפרוטוקול.
  • החל מ 2015 יש הסמכה של OIDC, ויש רשימה של מימושים שהם Certified, אפשר כבר לצפות ל Compatibility.
  • OIDC מגדיר סוג נוסף של token הנקרא ID Token המאפשר ל Client לדעת כמה פרטים על המשתמש ועל מצב ב Authentication שלו. עיוות נפוץ של מימושי OAuth 2.0 הוא לאפשר ל Client לקרוא את ה Access Token - מה שפוגע ב Segregation of duties.
  • הפרוטוקול מוסיף הגנה בפני התקפת replay: עשיתי sniffing לתקשורת ולא הבנתי מה נאמר - אבל אבקש מהשרת לעשות זאת עוד פעם (מה שלעתים יגרום נזק).

הנה ה Flow הבסיסי של OIDC, שהוא בעצם התאמה של ה Flow המקביל שתואר עבור OAuth 2.0:

ה Flow מעט פשוט יותר, לא מתבסס בהכרח על דפדפן, והיעד הוא להשיג מידע על המשתמש - ולא "משאב כללי".



האם OpenID Connect הוא הטוב מכל העולמות?

אם אתם מעוניינים ב Flow של authentication - אז הוא אופציה מבטיחה. קשה לי לחשוב על מקרה שבו עדיף לעשות Authentication על גבי OAuth 2.0 ולא על גבי OCID.

OIDC הוא פרוטוקול צעיר יחסית, שעדיין לא זוכה לאימוץ דומה לזה של OAuth 2.0. יש אולי עשר מימושים ויותר של OAuth 2.0 על כל מימוש OIDC.

בשנים הראשונות, מימושים של OIDC זכו לכמה "פאדיחות". במימוש של Facebook התגלה באג חמור, שזיכה את המגלה שלו ב Bug Bounty הגדול ביותר בהיסטוריה של פייסבוק. מימוש ה reference של ארגון ה OpenID עצמו סבל מכמה בעיות, וגם מוזילה נכוותה.

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

OpenID מסומן היום כמחליף הטבעי של SAML 2.0 (פרוטוקול אימות זהות / SSO) מסורבל ועם תיעוד לקוי - אבל שמילא את מטרתו במשך תקופה נכבדת.

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

בתחום המובייל (כלומר: native app) פרוטוקול ה OIDC עדיין לא נחשב טוב מספיק. שתי תקנים מרכזיים Proof Of Possession ו PKCE בפיתוח - ואמורים להעלות מדרגה בתחום.

OpenID Connect הוא לא מושלם. למשל: קיימת עדיין הדילמה של החזקת גישה למערכות רבות ע"י Identity Provider אחד. יש סכנה שפריצה למערכת הזו תאפשר גישה לתוכן רב - אך היא ככל הנראה פחותה מהסכנה שבפיזור ססמאות. מבין האופציות הקיימות, OpenID Connect היא האופציה העדיפה.


השוואה מהירה בין SAML 2.0 ל OCID


סיכום


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

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


----

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

ה OAuth 2.0 Threat Model המגדיר את הסיכונים השונים ביישום OAuth.
ה OAuth Grants מוסברים על ידי Alex Bilbie.
עוד קישור טוב על OAuth 2.0
"OAuth has ruined everything"
מדריך טוב ל OpenID Connect



יום שני, 3 באפריל 2017

כיצד בודקים מיקרו-שירותים? בעזרת יהלום הבדיקות, כמובן! (פוסט מאויר)

שאלו אותי פעם איך מבצעים בדיקות למיקרו-שירותים. "כמו כל קוד אחד" - עניתי בטבעיות. למה שמשהו השתנה?

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

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

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


לא גלידה, לא פירמידה - אלא יהלום


המודל הקלאסי של בדיקות אוטומציה הוא מודל "פירמידת הבדיקות":


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


המודל המועדף עלי הוא דווקא מודל היהלום:


לאחר שנים שהייתי חסיד של מודל הפירמידה, וריבוי בכתיבות Unit Tests - השתכנעתי שמודל היהלום הוא יעיל יותר.
ההבחנה של ההבדלים בין בדיקות Integration, Component, ו E2E - היא משנית. העיקר הוא:
  • יש לנו מעט בדיקות ידניות: על מנת לא לשחוק, לא לעשות עבודה רוטינית שוב-ושוב-ושוב - אלא להשתמש במוח האנושי שאין לו תחליף, לזהות דברים לא טובים במערכת. (ויש אנשים שעושים זאת טוב יותר מאחרים).
  • יש לנו כמות בינונית של בדיקות יחידה: 
    • בדיקות יחידה הן מעולות (!!) לבדיקת Pure Business Logic, קרי parsers, business rules, אלגוריתמים וכו' - כל מה שיש לו מחזור: קלט - הרבה עבודה לוגית - פלט. ניתן לזהות ביתר קלות אזורי קוד כאלה בעזרת צפיפות של משפטי if ו for (בשפות המתאימות).
    • בדיקות יחידה הן פחות יעילות לקוד אינטגרציה ("עשה א, ואז עשה ב, ואז עשה ג" - כמו שליפת נתונים מבסיס נתונים).
    • בדיקות יחידה הן דיי לא יעילות ל UI.
    • בקיצור: נשתמש בהן ב sweet spot שלהן בלבד: Pure business logic.
  • הדגש של המודל הוא על בדיקות Component (לעתים נקראות אינטגרציה, או API) - הבודקות התנהגות של כל שירות בפני עצמו. בבדיקה פשוטה אפשר לכסות הרבה מאוד קוד, בסביבה יחסית מציאותית, מה שמייצר מעין  sweet spot של עלות-תועלת: בין כמות ההשקעה בבדיקה - והערך שהיא מחזירה.
העקרונות של הפירמידה עדיין נשמרים ביהלום:
  • כמה שעולים למעלה הבדיקות הן: איטיות יותר להרצה, דורשות יותר תחזוקה --> יקרות יותר, ולכן ממעיטים בהן.
  • כמה שיורדים למטה הבדיקות הן ספציפיות יותר, רצות מהר יותר, ותלויות פחות בסביבה / אמינות יותר.


מה עם סוגי הבדיקות השונים? 


ישנה חוסר סטנדרטיזציה ברורה ובעייתית בשמות בהן משתמשים לתיאור בדיקות שונות: מהו API Test ומהו Integration Test? - רבים לא יסכימו על ההגדרה. אני אצמד למונחים שמקובלים ב Gett, לא כי הם "בהכרח הטובים ביותר", אלא כי אני רגיל אליהם כרגע.

להלן ההסברים של סוגי הבדיקות השונות, שישפכו גם אור על ההבדלים ביניהם.

לצורך הדיון, כך נראה שירות:


  • תקשורת בינו ובין שירותים אחרים מתבצעת על גבי HTTP (סינכרונית) או על גבי RabbitMQ (אסינכרונית)
  • ה flow המרכזי של השירות מופעל ע"י איזה Invoker מסתורי -  שעדיף לא לחשוב עליו לפרטים, כשכותבים את הבדיקות. הוא עלול לעשות כל מה שה API מאפשר. לרוב זה יהיה שירות אחר.

הנה כמה דוגמאות לבדיקות יחידה:



  • המחלקה A היא מחלקה "חברה" של מחלקה X - הבודקת אותה. זוהי בדיקת pure unit tests, האידאל של בדיקות יחידה.
  • המחלקה B היא מחלקה "חברה" של מחלקה Y - הבודקת אותה. מכיוון ש Y תלויה ב X, ואנו רוצים בדיקה "טהורה", קרי: נקודתית ומבודדת - אנו יוצרים Mock של X וכך מריצים את הבדיקה B על המחלקה Y בלבד. 
  • מחלקה C בודקת את המחלקה Z, אבל גם את החברה האחרת שלה - מחלקה X. לכן היא נקראת sociable unit tests. היא חברותית. 
  • כמה שבסיס הקוד שנבדק ע"י בדיקת יחידה הוא גדול יותר (יותר branching של ה flow - בעיקר), בדיקת היחידה היא יעילה פחות: יהיה קשה יותר לבדוק מקרי קצה, וכשלון של בדיקות יצביע בצורה פחות מדוייקת על מקור התקלה.
  • ככלל, אנו מעדיפים בדיקות pure unit tests על פני ב sociable unit tests - אבל זו הבחנה שלא תמיד מדייקת. למשל: אם עשינו refactoring למחלקה גדולה Z, והוצאנו ממנה קוד למחלקה חדשה X - אזי הבדיקה C הפכה ל sociable unit tests. למרות זאת, היא טובה בדיוק באותה המידה כפי שהייתה לפני ה Refactoring.
  • הערה: בתרשים נראה שאני בודק את כל המחלקות שיש לי, בפועל כנראה שאבדוק בעזרת בדיקות יחידה רק 10-30% מהמחלקות ב Service (תלוי כמובן בשירות)


המאסה העיקרית של האוטומציה מתבצעת ע"י בדיקות Component - הבודקות רכיב בודד במערכת.



  • במקרה של MSA, הרכיב הוא שירות, עם ה Database שלו - אך ללא תלויות בשירותים חיצוניים.
  • הבדיקות מתבצעות רק דרך ה APIs של השירות, אם כי פעמים רבות מייצרים נתונים ב Database לפני הבדיקה ישירות דרך ה Models של השירות עצמו (כך שקל לייצר נתונים עקביים, בפורמט המעודכן ביותר).
  • המטאפורה של השם Component היא כמו של רכיב אלקטרוני. המערכת עשויה להיות מחשב ויש לה ערך עסקי רק כשהיא שלמה, אבל כל רכיב (Component) נבדק ביסודיות בפני עצמו: זיכרון, דיסק, מעבד, וכו'.
    יצרני הזיכרון, למשל, בודקים את הרכיב שלהם בקנאות - בודקים שהוא עובד, גם בכל מצבי הקצה, כפי שמצופה ממנו. כאשר הממשקים ("contracts") ברורים, וכל רכיב בדוק כראוי - ניתן כבר להשיג רמת אמינות מרשימה של המערכת.

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

במונחים שלנו מדובר על End-To-End Tests, ובקיצור E2E Tests (מה שלעתים קרוי גם System Test):


  • אמנם אין בתרשים הרבה שירותים - אנא הניחו שמדובר במערכת מלאה (עשרות Services ויותר), עם תצורה קרובה ככל האפשר ל Production, עם האפליקציה / UI, ושירותים חיצונים בהם משתמשים (למשל: שירות לשליחת הודעות SMS).
  • המטרה: לפני שמשחררים שינוי משמעותי לפרודקשיין, נרצה להשקיע בבדיקה ולאתר תקלות לפני ההגעה לפרודקשיין, בסביבה אמיתית ומלאה ככל האפשר.

ואכן בדיקות E2E הן יקרות, וכדאי למעט בהן.
כאן מגיע התפקיד של בדיקות אינטגרציה: לאזן קצת בין עלות לתועלת:


  • בבדיקות אינטגרציה בודקים כמה שירותים כשהם עובדים ביחד. בדרך כלל זו קבוצה קבועה של שירותים הנמצאים בקשר עמוק זה עם זה, מה שניתן גם לכנות גם: "sub-system".
  • לפני הרצת בדיקת E2E - מריצים את בדיקת האינטגרציה המתאימה לשירות / תסריט (אם יש כזו). הקמת הסביבה היא מהירה וזולה יותר (נניח 2 עד 7 שירותים - במקום עשרות רבות של שירותים). הבדיקות רצות מהר יותר והן ממוקדות יותר (כלומר: סבירות גבוהה יותר לאתר תקלות).
  • עבור שינוי קטן או בינוני באחד השירותים, ניתן להסתפק בהרצה של ה sub-system הרלוונטי. עניין של בחירה וניהול סיכונים. 
  • ב Sub-System שבתרשים - שירות A הוא "המוביל" ותמיד דרכו ייעשו הבדיקות, גם כאשר השינוי שנבדק הוא בשירות אחר.
    • לא נשלח באמת SMSים, אלא נבצע Mock לשירות החיצוני.
    • ייתכן ואחד השירותים שלנו ב Sub-system הוא מורכב מדי להפעלה / קונפיגורציה - ולכן גם אותו נחליף ב Mock. כדאי להימנע מגישה זו במידת האפשר.

סיכום


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


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


-----


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

First Class Tests - מאמר דעה של הדוד בוב בנושא, הוא הרי חסיד של בדיקות יחידה קפדניות ו TDD.



יום שני, 27 במרץ 2017

מחפש ארכיטקט מערכת לגטט (Gett)

היי,

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

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


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

גטט היא חברה ישראלית, גם הפרודקט נמצא בישראל וגם רוב ההנהלה. זו עדיין חברה פרטית, שגייסה קצת יותר מ 620 מיליון דולר מאז הקמתה לפני 6 וחצי שנים (נראה לי שזה שיא ישראלי). היא כנראה אחת מחברות ה Consumer הגדולות בארץ: מה שיוצא ל production מקבל תגובה מיידית. לטוב, ולרע. אנו יושבים בת"א, ברחוב הברזל.

למרות שהחברה עדיין צעירה יחסית, יש עוד הרבה מה לבנות ולהקים. הרבה שאלות שעדיין לא נענו מעולם, ו/או מנגנונים שהחזיקו עד היום - אבל הגיע הזמן להגדיר אותם מחדש. יש עוד הרבה עבודת הקמה!

בגדול מה שעל הארכיטקט אצלנו לעשות הוא:
  • ארכיטקטורה 
    • להיות חלק מהמאמץ של צוות האכיטקטים לתחזק ארכיטקטורה טובה של המערכת: מודולריזציה טובה, flows מערכתיים טובים, טכנולוגיה מתאימה, ורמה מספיקה של תכונות אכות מסוג 3S, כלומר: Stability, Scalability, ו Security.
    • המערכת, כמובן, משתנה כל הזמן. הפיצ'רים משתנים - אך גם הצרכים משתנים.
    • איך עושים את זה?
      • ע"י Reviews (יושבים עם צוותים ולומדים מה קורה במערכת), מחקר (לפעמים צריך לחפור יותר ו/או ללמוד בצד), ותהליכים שונים נוספים שעוזרים להבין כיצד עדיף לבנות את המערכת.
      • התובנות הללו הופכות למשימות: 
        • חלקן גדולות, למשל: לשפר את מערכת זיהוי המתשמשים, ליישם תהליך של בדיקות יצביות ("chaos testing"), או להוביל מאמץ לשיפור Scalability בחלקים מסוימים של המערכת.
        • חלקן קטנות: לעבוד עם דבאופס על שינוי X, לעשות רביו לפיצ'ר Y, או להגדיר תהליך קטן Z.
      • את המשימות הללו יש לבצע. לגרום לדברים להתקדם, ולא רק להתקדם - אלא גם ברמה גבוהה.
      • חשוב גם לתקשר כל הזמן בצורה פרואקטיבית ל R&D את התמונה המלאה, את הארכיטקטורה - רק כך יוכלו להיצמד אליה ולהמשיך אותה (ולא לסתור אותה). זה אומר לכתוב מסמכים (קצרים ויעילים!), לעשות sessions, ולחשוב כל הזמן היכן יש פערים.
    • חשוב לציין שכיום, כארכיטקטים אנו מעורבים רק בפיצ'רים הקריטיים לכלל המערכת. שאר הפיצ'רים הם של הצוותים, ואנחנו עוזרים לייצר את הכללים כיצד משלבים את הפיצ'רים במערכת בצורה טובה. איך יתוכנן הפיצ'ר - זה עניין של הצוות, כל עוד הוא משתלב בצורה נכונה במערכת.
  • מקצועיות של R&D
    • קבוצת הפיתוח צמחה בחברה בקצב מהיר מאוד. כמעט גדלה פי 4 בשנתיים האחרונות, מאז אני הצטרפתי. 
    • הדרך היחידה לשרוד כזאת צמיחה היא לשמור על רמה גבוהה לכל האורך.
    • אנו עוזרים להגדיר את תהליכי הגיוס (בצד הטכני. לוודא שהמבחנים מספיקים והמראיינים יעילים), תהליכי ה On-Boarding, וכמובן שיפור מקצועי תמידי בתוך ה R&D. יש לכך היבטים רבים: בניית בסיס ידע, בניית קהילות טכנולוגיות בתוך החברה, תהליכי למידה (למשל: Code Review), ועוד... 
    • אנחנו רוצים להתחיל גם להיות פעילים בקהילת ה Startup הישראלית, ולשתף חלק מהידע החוצה / להביא יותר ידע מבחוץ פנימה.
  • שירותים טכניים כאלו או אחרים
    • ניתוחים טכניים כאלו או אחרים שנדרשים בחברה נעשים על ידנו. זה החלק הקטן של התפקיד.

דרישות:

כמקובל, הגדרנו דרישות שיעזרו לנו למקסמם את סיכויי ההתאמה של מועמד. אני רציני לגביהן ומנסה לבחון אותם ברצינות כחלק מהתהליך:
  • חמש שנות ניסיון בפיתוח של מערכות צד-שרת/ווב. זה לא המון - אבל זה חשוב!
  • לפחות שנה אחת בסביבת SaaS/Production משמעותית. חשוב מאוד "לחיות" פרודקשיין. 
  • ניסיון של שנתיים כארכיטקט בתפקיד דומה. אני זקוק למישהו שיכול להתחיל לתפקד תוך כמה חודשים. 
    • בזמנו קיבלתי הרבה קו"ח של מתכנתים מנוסים / מובילים טכניים - ודחיתי אותם. המעבר לתפקיד ארכיטקט (היבטים לדוגמה: hands-off - eyes on, השפעה ללא סמכות, חשיבה ברמת המערכת) הוא לא קל, ולצערי כרגע אין לי את הפריווילגיה לגדל מישהו לתפקיד - מוכשר ככל שיהיה. אני צריך מישהו שהוא כבר שם.
  • 10% Hands-On. ידיד הציע לי לא לכתוב את זה, כי אולי זה לא נשמע אטרקטיבי - אבל חשוב לי לשקף את המציאות:
    • אין כתיבת קוד כחלק מהתפקיד. אולי ייצא מדי פעם קצת ב POC, או איזה כלי, או לבצע איזה Pull request - אך זה חלק קטן למדי בפעילות.
    • אני מאמין בכל לבי שאדם שלא ילמד את שפות התכנות שאנחנו עובדים בהן (Go ורובי) ולא יוכל להתעמק בקריאת קוד בעת הצורך - לא יוכל לפרוח בתפקיד. הרבה פעמים התיאור מהמפתחים כיצד משהו עובד נשמע משהו אחד - ורק נבירה בקוד יכולה להסביר מה באמת מתרחש שם. הבנה עמוקה של שפת התכנות וסביבת הריצה הן לפעמים ההבדל בין עצה מעמיקה, לעצה לא-רלוונטית.
    • כלומר: נבירה בקוד היא כלי חשוב, ובהחלט חלק מהתפקיד. חשוב לי שהארכיטקטים בצוות ישארו ברמה הזו ולא יאבדו את היכולת הזו - ואנחנו משקיעים בזה.
  • מקצועיות טכנולוגיות גבוהה: רוחב אופקים (מכיר הרבה שיטות וטכנולוגיות), אך לא פחות נדרש עומק: יכולת לצלול לנושאים לעומק, גם נושאים סבוכים, ולהביא תובנות משמעותיות.
  • יכולת לעבוד בסביבה דינאמית ובשוק תחרותי מאוד. זה נשמע קלישאתי, אבל קצב ההתרחשויות בחברה הוא באמת גבוה מאוד. זו לא סביבה "קלה".
  • היכולת לפשט נושאים / רעיונות סבוכים לתיאור פשוט וממוקד - שקל לתקשר הלאה. יש לנו הרבה פרטים וידע במערכת - הדורשים זיקוק תמידי על מנת שהדמויות הרבות בפיתוח יוכלו לעקוב אחריהן.


זהו, פחות או יותר.

אם אתם מתאימים - אנא שלחו לי קורות חיים ל liorb[@]gett.com    (הפורמט נועד להקשות על ספאממרים הסורקים אחר כתובות אימייל)
אם אתם מכירים מישהו שעשוי להיות מתאים - אודה לכם אם תשתפו איתו את הפוסט!

נ.ב. - אם יש שאלות, אפשר לפרסם - כמובן.

תודה!



יום ראשון, 5 במרץ 2017

על העיקרון הפתוח-סגור (ארכיטקטורה)

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

העיקרון הפתוח-סגור אומר ש:

Software Entities (classes, modules, functions, etc.)
should be open for extension, but closed for modification.

העיקרון הפתוח-סגור (בקיצור: OCP, שזה ה Open-Closed Principle), הוא חלק ממערכת ה S.O.L.I.D - מערכת כללים לבחינת איכות של עיצוב (Design) תוכנה שהגדיר הדוד בוב (Robert C. Martin) בתחילת שנות ה 2000.

כמו עקרונות אחרים במערכת, ואולי יותר מכולם - ה OCP איננו ברור כ"כ במבט ראשון.
גם לא במבט שני.
  • מה המשמעות של "סגור" ו"פתוח"?
  • מה זה Extension? הורשה?     (תשובה: לא בדיוק)
  • מה זה modification? שינוי קוד? שינוי State?

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


לא עזרתם בהרבה! מקור: http://www.tomdalling.com


בגרסה היותר ידידותית, העיקרון אומר משהו כזה:
  • כתבו את הקוד כך, שרכיבי התוכנה (מחלקות, מודולים, פונקציות, וכו') לא ישתנו לאחר שסיימו לכתוב אותם - להלן: "Closed for modification".
  • תוספת של פונקציונליות - יש להוסיף ברכיבי תוכנה חדשים (עוד מחלקות, עוד מודולים, או עוד פונקציות) - להלן "Open for Extension".

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


---------

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


שאלות ראשונות


מדוע לא לשנות קוד שכבר נכתב?

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

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


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

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


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


מדוע "הרחבה" של קוד היא בסדר?




בשלב הראשון אנו כותבים את ה "Basic Functionality". הקוד עובד ומתייצב עם הזמן.
כאשר אנחנו נדרשים להרחיב את הפונקצינליות אנו מוסיפים את תוספות 1, 2, ואז 3.

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

כמו כן - הרחבה תכופה תאפשר לכל איזור קוד להיות קטן ופשוט מספיק בכדי להבין אותו: "Scalability" של כתיבת קוד.

חשוב מאוד לציין: הרחבה איננה בהכרח הורשה. אמנם השתמשתי בסמנטיקה שמקובלת לתיאור הורשה ("Generalization") - אבל אנו כבר יודעים שהורשה היא חרב פיפיות (ראו הרצאה). עדיף להשתמש בכלי אחר. הכלי המוצלח ביותר ל OCP הוא Interface: ממשק שמי שרוצה להרחיב את המערכת - צריך לממש אותו.



מדוע עדיף לנו שבאגים יופיעו באיזור החדש של הקוד, ולא באיזור הישן?

זה לא בדיוק מה שנאמר.
חלק גדול מהיתרון של יישום OCP הוא שהקוד שלנו מחולק ל"חתיכות" קטנות של קוד שלא מסתבכות יותר מדי. קטן => פשוט (או יותר נכון: גדול => מסובך).

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


כיצד ממשים OCP בפועל?


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


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

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

אני בונה את המנגנון כך: Broker שמקבל את הבקשה ואז מעביר את רשימת ההתאמות האפשריות לרכיב "Base Condition" - הרכיב ידרג את ההתאמות מהטובה ביותר, להכי פחות טובה.

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


בעתיד אוכל להרחיב את המנגנון להתחשב בעוד תנאים, תנאי 2, תנאי 3, וכו'. כל התנאים עובדים לפי אותו Interface* כך שבעזרת ריבוי צורות פשוט - אני יכול להרחיב את המודול בעוד תנאים, מבלי לשנות שורת קוד בודדות בקוד שכבר כתבתי.

מנגנון ה Broker קורא מקובץ קונפיגורציה - באיזה סדר להפעיל את התנאים.

ובאמת לאחר כחצי שנה המנגנון נראה כך:


בכל שלב הוספתי עוד תנאי למנגנון ה Matching (העדפות של נהג, העדפות של נוסע, וכו').
הקוד החדש נכתב ללא נגיעה בקוד שהיה קיים קודם לכן.

בפבואר הוספתי את Condition 2 - ועד אפריל הוא התייצב בצורה טובה.
במרץ הוספתי את Condition 3 - ועד מאי הוא התייצב בצורה טובה.
במאי הוספתי את Condition 4 - ועד יולי הוא התייצב בצורה טובה.

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

ביישום הספציפי הזה, ה Broker בנוי כך שהוא יכול להכניס את התנאים החדשים לפעולה בצורה הדרגתית:
כהוספתי את Condition 2 בתחילת פבואר - הוא פעל במוד בדיקה, על 100 נסיעות בלבד ביום.
באמצע פבואר כבר שיניתי את הקונפיגורציה שיעבוד על 3000 נסיעות ביום (בכדי לבדוק טוב יותר), ורק בסוף פבואר, לאחר שתוקנו עיקרי הבאגים - הפעלתי אותו על כלל המערכת.

ה Design הזה כמובן הוא לא "מושלם" ואני לא ממליץ להעתיק אותו כי הוא מופיע כדוגמה לעיקרון חשוב של תכנון מערכות. הוא מתאים לצרכים מאוד ספציפיים.
ככל שיהיו יותר תנאים של Matching שלא משתלבים ב Pattern הנ"ל - הוא ילך ויסתבך. אין לו תוכנית ל "חריגים לא מוכרים" (כי הם לא מוכרים, דאא) ויהיה צורך לבצע חשיבה מחדש ברגע שיגיעו כאלו.

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

איכות המבנה / העיצוב של ה Matching System (או כל מודול אחר) תלוי בעיצוב הראשוני, ומידה לא פחותה - ברמת ההחלטות וההתאמות שידרשו עם הזמן.


ספקות לגבי OCP


ה OCP הוגדר בצורה לא ברורה. את זה כבר הזכרנו. היו כמה מאמרים שהתפרסמו בביקורת הזו (למשל: Say "No" to the Open/Closed pattern של Marco Cecconi ו The Open-Closed Principle, in review של Jon Skeet)

במאמר מאוחר של הדוד בוב מ 2013 הוא סיפק את ההגדרה הבאה ל OCP:

What it means is that you should strive to get your code into a position such that, when behavior changes in expected ways, you don't have to make sweeping changes to all the modules of the system. Ideally, you will be able to add the new behavior by adding new code, and changing little or no old code.

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


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

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

כלומר: בהינתן פרקטיקות של בדיקות יחידה / מערכת - הסיכון שבשינוי קוד ישן הוא הרבה יותר קטן.
אני לא נמצא באפילה של שנות ה-90, שבה בעקבות שינוי של קוד אני עושה כמה בדיקות ו/או מעביר לאיש ה QA - ומקווה שאנחנו לא מפספסים איזו פינה מה-זה-חשובה.

שינוי/הוספה של קוד לא הייתה תמיד מסוכנת, אבל כאשר יש אוטומציה - היא מסוכנת הרבה פחות.

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

האם בהכרח זהו קוד שכדאי להימנע ממנו?
לא בטוח. אם במשך 5 שנים של אורך חיי במערכת יצטברו במתודה 3 או 4 צורות - לא נראה לי שזה יהיה קוד רע. בואו לא נהיה פאנטיים. האם חשוב להוסיף final על כל משתנה בג'אווה? אפשר - אבל ה impact המעשי הוא כנראה באמת קטן.

אם המערכת שלנו תתרחב ל 10 צורות, וכמו המתודה DrawAllShapes יש מתודות כמו EditAllShapes, ResizeAllShapes ועוד - אז ברור לי שבסיס הקוד שלי הולך וגדל ללא היכר. זה מקרה ברור בו כן היינו רוצים ליישם את ה OCP.
בסיס קוד גדול יותר => קוד שקשה יותר לשלוט בו => קוד שקשה יותר לשנות / יותר באגים.

ברור לחלוטין שלא ניתן, ולא כדאי, להגיע ל 100% יישום של OCP במערכת.
בניגוד להוספת "final" למשתנים בג'אווה, פרקטיקה שמוסיפה מעט ערך אבל גם העלות שלה שולית - OCP משמעה כתיבת עוד קוד ולקיחת "סיכונים" בעצם הגדרת ההפשטה (למשל: ה Condition בדוגמה למעלה).
כאשר דרישה חדשה משתלבת יפה בהפשטה שיצרנו - השגנו ייתרון, אך כל דרישה חדשה שלא תתאים להפשטה - תקשה ותסבך את המערכת.

OCP מתבטא ב Patterns כמו Plug-ins, State, Strategy, ועוד. אולי גם Composite ו Delegate.
זוהי רשימה קלאסית של Patterns שאנשים נטו ונוטים לעשות בהם שימוש מוגזם: השימוש עשוי להרגיש "טוב" (המערכת שלנו היא "כמו בספרים"), אך בפועל ייתכן ויצרנו מערכת יותר מסובכת ויותר יקרה לשינוי / תחזוקה.

OCP הוא במיטבו כאשר ממשים אותו במידה, אבל מהי המידה הנכונה?


מקור: https://www.slideshare.net/PaulBlundell2/open-closedprinciple-kata


קרייג לרמן, ו PV


ובכן, אם לא הכרתם עד עכשיו - אז שווה להכיר:
במקביל לדוד בוב שהגדיר את עקרונות ה S.O.L.I.D, עבד בחור קנדי בשם קרייג לרמן על מערכת חוקים מאוד מאוד דומה בשם GRASP. המערכת של לרמן הרבה יותר פשוטה להבנה! מה שהופך אותה לעדיפה בעיני.

מסיבות שאינני יודע למנות, SOLID הפכה למערכת המוכרת, ורוב האנשים מכירים דווקא אותה.

העיקרון המקביל של GRASP ל OCP נקרא Predicted Variations או בקיצור PV. והוא מוגדר כך:

Identify points of predicted variation and create a stable interface around them


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

Predicted מצוין כ "educated guesses" מכיוון שלא סביר שבאמת נוכל לחזות כיצד תתפתח המערכת לאורך חייה.
כלומר: עלינו לחשוב לאילו כיוונים סביר שהמערכת תתפתח, ולבנות את הממשק באיזור הזה כך שיהיה ניתן להוסיף קוד ע"י הוספה של יחידות קוד עצמאיות, ולא ע"י שילוב של תוספות קוד בקוד הקיים. בהמשך המאמר מוסבר:

We can prioritize our goals and strategies as follows:
1. We wish to save time and money, reduce the introduction of new defects, and reduce the pain and suffering inflicted on overworked developers.
2. To achieve this, we design to minimize the impact of change.
3. To minimize change impact, we design with the goal of low coupling.
4. To design for low coupling, we design for PVs.

low coupling הוא העיקרון שגורס שיש לחתור לכמה שפחות תלויות בין מודולים / חלקים שונים של קוד זה בזה.


אנסה לתמצת את הרעיונות במילים שלי:
  1. זהו במערכת מקומות שבהם אתם צופים תוספת של קוד, ונסו להכין ממשקים ידועים להרחבות הללו.
  2. בעצם מדובר ב"ניהול סיכונים": הסבירות שאכן המערכת תתרחב לשם (תזכורת = בני-אדם נוטים לבצע הערכת יתר בציר הזה)  X  כמה קוד וסיבוכיות אתם צופים שתתווסף.
  3. עבור שינויים קלים (שורות קוד בודדות) - הערך ביישום OCP הוא גם נמוך. נסו לכוון למקומות משמעותיים.
  4. קלעתם טוב - השגתם ערך; לא קלעתם טוב - השקעתם זמן מיותר. זה בעצם העניין כאן.
  5. אפשר לנקוט בגישה אגי'לית: כשאתם רואים שאכן המערכת מתפתחת לכיוון מסוים, בצעו Refactoring והוסיפו ממשק מתאים / PV. אם יש לכם בדיקות יחידה - אין סיבה לפחד מ Refactoring.


אני מניח שאם GRASP הייתה השיטה השלטת - הפוסט שלי היה מתקצר בחצי: המטאפורה של Predicated Variations הרבה יותר קולעת מה Open-Closed Principle (סליחה, הדוד בוב).

לא מצאתי את הציוץ של הבחור שהציע: "בואו נחליף את OCP ב PV - ונתחיל להשתמש ב SPLID".


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


----

מקורות רלוונטיים:


יום ראשון, 26 בפברואר 2017

על היוגי והקומיסר

דמיינו ישיבה: כמה ראשי-צוותים, כמה ארכיטקטים, כמה מובילים טכניים.

"כבר עשינו עבודת הכנה מקיפה לגבי המעבר ל MDA (קרי: Model-Driven Architecture):", פתח הארכיטקט הבכיר (להלן א׳, שמו שמור במערכת). "הגדרנו Meta-Model לכל המערכת שלנו, וביססנו אותו על Meta-Meta Model (או super model). יש לנו רשימה של נקודות שצריך עוד לסגור - אבל אנחנו כבר יכולים להתחיל. הצוות של יוסי החל ליישם Repository למודלים, ועוד חודשיים נוכל כבר לתת לכל שאר הצוותים בארגון להמיר את הקוד שלהם למבנה ה MDA."

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

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

"זו פרקטיקה בצמיחה, יש כבר 'body of knowledge' שהולך ונבנה בתחום, ויש גם כמה יישומים מוצלחים (במיוחד בעולם ה Databases)" - ענה א׳ בלי למצמץ, בלי לשדר אי-נוחות, בלי בושה.

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

"אה... אתה צודק מאוד." - נראה שלא׳ נפל סוף-סוף האסימון.
"אנחנו נכתוב רשימה של היתרונות של MDA ונראה אילו הכי משמעותיים עבורנו, נתמקד במה ששימושי עבורנו, כמובן. נקודה טובה!".


הוא אידיוט? מנותק מהמציאות? הוא בכלל דמות מרכזית בארגון מצליח?
האם אין גבול לטיפשות או הציניות??
אולי זו גחמה אגואיסטית שלו שהוא רוצה לכתוב ״MDA״ בקורות החיים?

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

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

מה קורה פה בעצם?

(בעקבות שאלה: לא, זה לא אירוע שקרה בגטט... אלא שנים רבות לפני כן)


מקור: https://vidyow.com/video/the-main-differences-between-t/vcbaDoo8guL

ציר טראמפ-אובמה


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

* אני לא מתכוון לעשות ניתוח פוליטי מעמיק או אפילו שטחי, אלא להתייחס ל״תפיסה המקובלת״ לגבי האישים. מקובלת - כפי שאני מכיר וחווה אותה בסביבתי.

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

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


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

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

על טראמפ קל לצחוק. אני אישית נהנה - אני מודה!
הוא מספק לי בידור לפחות פעמיים בשבוע. תיאור המעשים שלו בעיתונות (/ טוויטר) - גורם לי להרגיש נבון יותר!


הנה סיפור נוסף לדוגמה מעולם הטכנולוגיה. איזו חוסר הבנה!....:

מקור: הפוסט http://getindata.com/blog/post/lean-big-data-how-to-avoid-wasting-money-with-big-data-technologies-and-get-some-roi

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

יש משהו ציני וקוטבי בתיאור המקרה הזה (ובדומים לו):
  • הגופים שרחוקים מאיתנו, ואין לנו היכרות אינטימית איתם (יועצים, הנהלה, ״vendors״) - מוצגים באור שלילי חד משמעי (טיפשים, חסרי רגישות, חסרי מחויבות לצורכי הארגון).
  • התוצאה היא חד-משמעית (דלי של דולרים נשפך לפח)
  • מביני העניין (peers שאנחנו מעריכים את תבונתם) - פשוט אוחזים את הראש בידיים, שלא ייפול אני מניח.


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


מה הסיפור שלכם?


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

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


It's all about tradeoffs


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



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

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



סיכום


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

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

הייתי מנסה לנסח את נקודות המחשבה הבאות (ניסוח ראשוני):
  • אם אתם פוגשים בקומיסר/יוגי מרתיע, חשבו: האם יש לכם שותפות בכוונות / יעדים? אם כן - כנראה שהמצב לא כ"כ רע!
  • גם אם דרכו של הקומיסר / יוגי משונה לכם ("אידיוט!"☺) - נסו להבין מה עומד מאחוריה. כנראה שיש שם תובנה שקשה לאדם השני לתאר בצורה פשוטה. בכל זאת, יש סיכוי טוב שיש בה ערך. אם תיקחו את הרעיון ותפתחו אותו - אולי תמצאו משהו חשוב - שפספסתם!
  • חשבו על הצוות שלכם: 
    • האם הבחור ההוא שנראה קצת מטופש הוא Executor טוב, חסר מורא? אולי תוכלו להעריך אותו ולהיעזר בו כקומיסר.
    • האם הבחור השני, שמתקשה לקחת החלטות, ונתקע בפרטים הוא בעצם אדם נבון עם תובנות עמוקות? אולי תוכלו להעריך אותו ולהיעזר בו כיוגי.


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

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

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


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



יום שבת, 21 בינואר 2017

אז... אתם רוצים Continuous Deployment? (חלק ג')

בפוסט הקודם בסדרה דיברנו על Continuous Delivery (בקיצור CD), על ה Deployment Pipeline, ופרקטיקות נוספות.

בפוסט הזה אני רוצה להתמקד ולהשלים את הסקירה של Continuous Deployment (לצורך הפוסט: CDD), ולדבר על הטמעה של הפרקטיקות הללו - בארגון שעדיין לא נמצא שם.


תנו לי CDD - ועכשיו!


Continuous Deployment הוא הרעיון שהפער בין קוד המערכת לסביבת הפרדוקשיין יהיה מינימלי - דקות עד שעות ספורות לכל היותר.

שאלה בוודאי שאתם שואלים את עצמם, לאחר הבנת התהליכים של CD - היא מהו הפער ליישום CDD?

הנה תיאור ה Deployment pipeline שהשתמשנו בו בפוסט הקודם:



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


יש לכך כמה קשיים עיקריים:

המעבר מביטחון של 95% באוטומציה לביטחון של 100% באוטומציה - אינו קטן!

שמעתי הרצאה של קבוצה בגוגל שלאורך 18 חודשים (שנה וחצי) ניסתה לעבור מ CD ל CDD - ועדיין לא הצליחה. עדיין כ 1% מה commits היו בעייתיים למרות שעברו את הבדיקות, ותהליך השחרור אצלם נותר ידני. כלומר: מפתח מבצע החלטה בכדי להעביר commit לפרודקשיין.
כותרת ההרצאה הייתה אודות "Continuous Deployment" - למרות שלא עמדו בתנאי הרשמי הזה (בסדר מבחינתי!). קצת הופתעתי שהדבר הטריד אותם ונראה שהיה חשוב להצהיר שתוך כמה חודשים הם חושבים שכבר יוכלו לעשות שחרור אוטומטי לחלוטין.

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


עם הזמן מספר ה commit ילך ויגדל

אולי כי הארגון גדל, ואולי כי המפתחים באמת השתפרו בתהליך ה CI?
כמות ה commits בשעה תלך ותגדל: 1 בשעה, 2 בשעה, אח"כ 5 בשעה, וכו'.

כבר באזור שלושה-ארבעה ה commits בשעה - תהיה בעיה: תהליך ה deploy אורך כ 30 דקות.
מה עושים? נותנים לתור ה commits להצטבר? להיכנס לשעות העומס של המערכת או שעות הלילה? אולי לגלוש למחר?
אולי פשוט נאחד כמה commits ונעשה להם deploy ביחד?
אולי נשקיע מאמץ במיקבול טוב יותר של תהליכי ה deploy?

הבעיה הזו כנראה לא הולכת להיעלם, במיוחד אם אתם ארגון בצמיחה. גם אם תמקבלו את התהליך, את שלב ה deploy עצמו (blue/green או rolling) ואז צפייה בשינויים בסביבה בעקבות ה deploy - תהליך שאורך 10 דקות לפחות, כנראה ולא תוכלו ממש למקבל.

הפתרון המקובל הוא אימוץ של Microservices Architectures. הרבה repositories שונים של קוד, שעוברים deploy לשרתים שונים - מאפשרים מקביליות. כאשר יותר שורות קוד --> יותר שירותים (תנאי של Microservices Architecture) - המקבילות היא, תיאורטית, אינסופית.


Database migration

אם אתם עובדים עם RDBMS אזי המיגרציות:
  • יכולות לארוך זמן רב (שעה? שעתיים? - כאשר הטבלאות גדולות), ולא ניתנות למיקבול של ממש.
  • יכולות לגרום ל downtime קצר בכל מיגרציה (כאשר נניח משנים או מוחקים עמודה).
  • טעויות במיגרציה יכולות להיות הרסניות לפעילות המערכת.
הפתרונות המקובלים הוא לעבור לפתרונות בהם כמעט ולא צריך מיגרציה: Document Databases על פני Relational Databases, או Databases שבהם המיגרציה היא נוחה יותר (למשל PostgreSQL על פני MySQL).

כאשר יש מיקרו-שירותים, המיגרציה כבר לא כ"כ מפריעה: שירות אחד יכול להיות "תקוע"* על מיגרציה למשך 4 שעות - שאר המערכת ממשיכה להתעדכן כרגיל, וללא הפרעה.

* "תקוע" הכוונה שהשירות עובד - פשוט אי אפשר לבצע עוד deploys בזמן הזה.




אז למה באמת ארגונים בוחרים ב CDD?!


(אני קצת חוזר על הנאמר בפוסט הראשון)
הרעיון של Continuous Deployment קיבל תנופה גדולה מתנועת ה Lean Startup, שם צמצום מחזורי הלמידה והפידבק הם ערך מרכזי. הדרישה ל CDD, ע״פ המתודולוגיה, אמורה להגיע כיעד עסקי מה CEO - זה לא עניין ״פנימי״ של ה R&D...

במקום להיות ״נביאים״, או ״יודעי-כל״, תהליך ה CDD מאפשר לנו לנסות בשטח.
הפיצ'ר של אמזון של "אנשים שקנו את X קנו גם את Y", נדחה ע"י אנשי המוצר כ"מטופש" - ובארגון אחר היה נקבר לעד.

התרבות הארגונית של אמזון, שגובתה בעזרת יכולות CDD, אפשרה למפתח שמאוד מאמין במשהו לבצע A/B test (על אוכלוסיה קטנה) - ולנסות להוכיח את צדקתו. כל אחד כמעט - יכול להציע ולהתניע ניסוי.
הניסוי הנ״ל הראה גידול של כ 3% במכירות (נתון מרשים מאוד!) - מה שהסיר מייד כל התנגדות, והפך אותו בן רגע לפיצ׳ר רצוי ע"י הפרודקט.

אם כולם מאמינים במערכת ובנתונים שהיא מייצרת, אזי נתונים מהשטח הם ״מחסלי ויכוחים ארגוניים״.


CDD, אם כן, בשילוב A/B Tests ותרבות ארגונית מתאימה - מספקת Empowerment לעובדים, ויכולה להביא רעיונות חכמים שלהם - לידי יישום.


מקור: Ash Maurya

ישנן עדויות ליתרונות נוספים הנובעים מאימוץ CDD:
  • ע"פ עדות של אינסטגרם ששמעתי - המעבר ל CDD שיפר את מהירות הפיתוח בכך שצמצם הסתבכויות של מפתחים: פחות ספקולציות ("אולי הקוד לא היה מספיק מהיר וכדאי לייעל פה קצת?") ויותר חיבור למעשיות בשטח.
  • בגלל שעושים deploy לכל commit באופן נפרד - קל יותר לזהות commits בעייתיים, לתקן אותם, וכך להגביר את הלמידה של המהנדסים. היתרון ההדרגתי בלמידה - מצטבר ליתרון של מהנדסים טובים יותר.
  • שביעת הרצון של המפתחים גבוהה יותר, הן בשל שהם עושים משהו שרבים מנסים - אף רק חלק קטן מהתעשייה הגיע אליו בפועל, ואם בגלל שזו באמת סביבת עבודה מתגמלת יותר (רגשית) עבור המפתחים.
שימו לב: ככל הידוע לי לפחות, אין נתונים ממשיים שתומכים בטענות הללו. האם מדובר בדיסוננס קוגניטיבי בו מי שהשקיע מאמץ רב כ"כ בהגעה ל CDD בפועל - מנסה להצדיק את ההשקעה הזו בדיעבד? - אני לא יודע לומר.



אך איך מיישמים CD/CDD בארגון שעדיין לא שם?


לצורך הדיון, אתעלם לרגע האם מדובר ב CD או CDD - זה פחות מהותי.
כמו כן, כמובן שכל ארגון הוא עולם בפני עצמו. אני לא מנסה לתת מתכון אחיד לכל בעיה, אלא להציג כמה גישות חשובות ומהותיות.

נתחיל בגישות העיקריות, הנוגעות לארגונים שרק מתחילים בתהליך האימוץ:

גישה: AquaSlump

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

בעצם יש פה פרויקט שצריך להריץ:
  • שלב א' - משקיעים בתשתיות build - חשוב מאוד לוודא שמהכנסה של קוד עד להגעה לפרודקשיין הכל הוא 100% אוטומטי.
  • שלב ב' - בונים סביבת staging יציבה לאוטומציה. יש פה עניין של הקמת סביבות בצורה אוטומטית ומהירה - זהה לפרודקשיין, הבאת נתונים אמיתיים מפרודשיין, וכתיבת בדיקות End-to-End שיכסו אותנו בצורה טובה ויספקו אמינות שהקוד שאנחנו משחררים הלאה ב pipeline הוא באמת תקין.
  • שלב ג' - מביאים את כל חלקי המערכת לרף הבדיקות שנקבע: למשל 85% Code Coverage.
  • שלב ד' - מתחילים לאפשר לצוותים לשחרר קוד לפרודקשיין, אזור אחר אזור.

למי שלא הבין עדיין, ה AquaSlump היא מילה נרדפת ל WaterFall - יש עוד מאמינים גדולים בעקרונות ה Waterfall (בעיקר בארגונים גדולים), אך נראה שכולם הפסיקו להשתמש במונח ה"מביך" הזה.
אם תתקלו ב Waterfall ב CD (או בכלל) - סביר יותר שתתקלו בו עוטה שם אחר. אל תתנו לשם השונה לבלבל אתכם!

"משהו כמו CI/CD אי אפשר לעשות בלי תכנון מסודר ומרכזי" - הוא טיעון ששמעתי לא פעם...
בכל זאת, אם ייקח חצי שנה לפי התוכנית (ואולי שנה+ בפועל?) לראות Deploy מתרחש מקצה לקצה וללמוד מה עובד, מה לא עובד, ואיפה הכאבים הספציפיים של הארגון שלנו - אז זה פשוט לא אג'ילי ולא יעיל.

הסתייגות קלה: אם עשיתם פעולה בהצלחה כבר כמה פעמים, למשל: מעבר ל CI/CD - אז ייתכן ובאמת יעיל לקחת גישה יותר Waterfall-ית שמתמקדת ב Efficiency ולא בלמידה. אתם כבר יודעים כנראה לא מעט. אני מדבר ידע מוכח (ולא משהו כמו ContinuousIntegrationCertification)


CI/CD/CDD Maturity Model - מודל אידאלי ואוניברסילי ליישום השיטה. תורת הצורות/האידאות של אפלטון בהתגלמותה! מקור



גישה: Guerilla Transformation


חבר שעבד בסטארט-אפ ישראלי קטן סיפר לי שהם עשו בזמנו 5 וגם 10 deploys ביום. בלי בדיקות יחידה, בלי אוטומציה, בלי "מערכת CI". פשוט קימפלו - וזרקו לפרודקשיין. שהיו בעיות - תיקנו בזריזות.

ע"פ מודל ה Maturity למעלה, או רבים אחרים דומים - התנהלות שכזו לא נמצאת בכלל על הסקאלה!

בכל הפרמטרים החבר'ה האלה היו מתחת לשלב הבסיס הראשוני (קרי: 1-). בתכונה אחת, שלא מוגדרת ברוב המודלים הללו כיישות עצמאית: "כמות ה deploys ביום לפרודקשיין / ביחס למספר המפתחים" - הם היו בדרגה הגבוהה ביותר (קרי: 5).

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

אבל זה יותר CD/CDD ממה שארגונים מגיעים אליהם אחרי חודשים של עבודה קשה, בגישת ה AquaSlump.

ג'ז האמבל (המחבר של ספר ה CD המפורסם) מציע ששני המדדים העיקריים לאימוץ של CD יהיו:
  • כמה commits ל master ביום / ביחס למספר המפתחים.
  • כמה זמן אורך מרגע שיש רעיון - ועד שהוא מגיע לפרודקשיין (קרי: lead time). 

בגישת הגרילה מתמקדים במדדים הללו - עד הסוף.
קודם הולכים ומתחילים לעשות Deploys תכופים לפרודקשיין - ואז מתחילים לשפר את התהליך.

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

בשלב הבא, כשמעט מתבגרים - אלו כנראה הדברים שתרצו להתמקד בהם:
  • בדיקות אוטומטיות מהירות, עם כיסוי סביר.
  • תהליך של canary release.
  • בדיקות post-deployment ויכולת ל rollback מהירה.
  • יצירת אווירה שבה טוב ונוח למפתחים לעשות הרבה deploys ביום.

הסכנה העיקרית בגישה הזו - היא "להיתקע" באמצע הדרך עם מצב בינוני של CD, שהתרגלתם אליו אבל הוא עדיין לא כ"כ יעיל.
אני לא מחשיב את הגישה הזו כמסוכנת יותר לפרודקשיין, מגישת ה AquaSlump. גם בגישה ההיא אפשר לעשות נזקים רבים - ואז, כשהמנגנון גדול וסבוך - דווקא יותר קשה לשפר ולהתאים אותו למציאות שמתגלה.
היתרון של גישת הגרילה - היא שזו הדרך המהירה ובעלת הסיכויים הגדולים יותר להגיע לתוצאות משמעותיות כלשהן במהלך, נאמר, שנה.

חוויתי כמה וכמה מאמצים אדירים בגישת ה AquaSlump - שלא הגיעו לתוצאות משמעותיות (אולי החבר'ה שם עדיין מנסים...?!) וחוויתי פעם אחת את הגישה הרזה - שסיפקה תוצאות יפות בזמן קצר. דעתי בנושא - התגבשה.






כמה בעיות של CDD שאולי לא חשבתם עליהן בתוכנית ה AquaSlump שלכם...


"פקקים" ב Deployment Pipeline

במהלך השעה האחרונה נכנסו לי סדרה של ארבעה commits לשירות מסוים: A, B, C, ו D.
commit A - עבר deployment בהצלחה.
commit B - נכשל.
האם אפשר לקחת את commits C +D לפקודקשיין בצורה אוטומטית? חבל לבזבז זמן!
 איך יודעים האם הם תלויים ב commit B או לא?

הבעיה איננה בעיה לוגית כ"כ גדולה: בד"כ אפשר לבצע merge אוטומטי של commit C על גבי commit A וללא commit B - ואז להעביר את השינוי ב pipeline. מה שנקרא "commit selection" או "מי הבא בתור?". אבל זה עדיין תהליך, הדורש עבודה וליטוש.


זיהוי בעיות ב UI

בעוד בדיקות אוטומציה הן טובות מאוד לקוד לוגי - קשה הרבה יותר לבדוק UI בצורה אוטומטית ומלאה.

יש סיפור של גוגל איך מתכנת הוסיף לאחד המוצרים אנימציה של unicorn מרקד כאינדיקציה לכך שאיזה Feature Flag, של פיצ'ר שעליו הם עבדו - הוא מודלק.
תהליך ה deploy עבר בהצלחה. המתכנת נכנס לפרודקשיין עם החשבון שלו - וראה את ה unicorn מרקד והכל נראה בסדר!

הוא שכח לבדוק, מה קורה עם משתמש שאינו ברשימת ה beta users של הפיצ'ר. מסתבר שכל משתמשי המוצר (מיליונים!) ראו את ה unicorn המרקד הזה - גם אם הפיצ'ר לא הופעל עבורם.

כיצד מאתרים בעיות מסוג זה? אולי כפתור שזז למקום מוזר? או טקסט שהפך לקטן? - כשהשינויים קטנים, "יקר" לבצע בדיקה ידנית של ה UI על כל deploy (שזה בעצם: commit קטן).

במוצר הספציפי הוסיפו ל pipeline שכבה של בדיקות אוטומציה שעונות "באיזה מסכים היה שינוי בUI, לכלל המשתמשים, בעקבות ה commit". הבדיקות היו מבוססות בעיקר על צילום והשוואת screenshots של המוצר. אם היה שינוי שכזה (שמתגלה ב staging), המפתח צריך לאשר שהשינוי תקין, בכדי שהקוד יגיע לפרודקשיין.

שוב - אין פה rocket science, אך יש פה צורך ספציפי למוצר - שניתן להבין וללמוד אותו רק בעקבות "התנסות בשטח".


תלויות בין מיקרו-שירותים

נניח שיש לי פיצ'ר בשירות X שדורש API מתאים בשירות Y?
זה נשמע אתגר קטן: נגדיר את התלויות. סגור!

בפועל: מצב ה pipeline של כל שירות הוא דינאמי - ולכן קשה לתכנון. בכל רגע נתון יכולים להיות לפני ה commit שלי מספר commits שמחכים להרצה ב pipeline. כיצד אם מוודאים שהשינוי ב API (שירות Y) יהיה זמן בפרודקשיין, לפני שהקוד בשירות X "נוחת" בפרודקשיין בעצמו? אם יש שלושה commits בתור בשירות Y, אך אין תור כלל בשירות X?

שוב - אין פה מדע טילים, אך יש בעיה מציאותית שצריך לפתור, כאשר לא כל פתרון מתאים באותה מידה לכל מוצר / מערכת.


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


קצרים


רעיונות אחרונים בנושא שארצה להוסיף לגבי יישום של CD/CDD:
  • פגישת Slow Deployment Retrospective - לא משנה השם, אבל אם לא תדאגו לשפר כל הזמן את תהליך ה deploy שלכם - הוא הולך להיות אטי ומסורבל עם הזמן. במיוחד כאשר המוצר והחברה בשלבי גדילה.
  • ב Lean Startup (ממנו הגיע ה CDD) ה Definition of Done של פיצר הוא "הפיצ'ר ייצר את האימפקט המצופה בפרודקשיין". לא סוגרים "טיקט" עד שלא מגיעים לשם, כלומר: עושים עוד ועוד איטרציות על הפיצ'ר עד שהאיפקט מושג. לסירוגין: אפשר להחליט שהפיצ'ר פשוט לא שווה את ההשקעה המסתמנת.
    • ב CDD מן הראוי, לא לסגור טיקט של פיצ'ר לפחות עד שלא רואים אותו בפרודקשיין. שינוי קטן ב Jira - ושינוי קצת יותר גדול ב mindset.
  • תזכורת: כמו ב CD - בעיות פרודקשיין ובעיות ב Deployment pipeline מטפלות בדחיפות הגבוהה ביותר בארגון! הזנחה של הכלל הזה - היא דרך מצוינת להכשיל יישום CD/CDD.
  • Hello Prod (על משקל "Hello World") - כאשר מפתחים שירות חדש, דבר ראשון שלחו את ה template של שירות חדש (להלן "Hello Prod") לפרודקשיין - ועליו תתחילו לפתח. בפרודקשיין. אל תחכו שבוע או שבועיים עם קוד בצד לפני שראיתם פרודקשיין. מייד!
    • זה תרגיל מצויין כדי להתחיל לתרגל CDD, גם אם אתם ממש בתחילת התהליך.
  • שאיפה של מוצר CDD הוא שעם הזמן (וגדילת מספר העובדים), קצב השחרורים לפרודשיין /יחסית למספר המפתחים - רק יגדל!




סיכום


CD ו CDD מרגישים ממש קרובים - אבל הם לא.
אם ב CD שיגרנו חללית לחלל, מיד גילינו שיש לנו עוד עבודה לגרום לה להשתחרר מהכבידה של כדור הארץ (להלן CDD). לא מאמץ קטן!

אני רוצה להתוודות שכאשר התחלתי את הפוסט... עוד דיקלמתי לעצמי: "אם יש לכם מוצר SaaS - חבל לעצור ב CD. לכו על CDD!"
במהלך כתיבת הפוסט, נוכחתי בעצמי שוב - עד כמה זה לא-פשוט!

ולכן אתקן: אני ממליץ לכל מוצר SaaS להגיע ל 90% CDD. שיהיו הרבה deploys ביום / ביחד למספר המתכנתים. יותר deploys ביום ממפתחים - לפחות.
אם יש מוצר SaaS ואתם משחררים לפרודקשיין רק פעם בשבוע או שבועיים - זה מרגיש פספוס.

אם תבחרו בגישת הגרילה - זה גם לא יהיה כ"כ קשה. מפתיע כמה זה קל!

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


נ.ב. - יש!! סיימתי סדרת-פוסטים!