CI באוריבי

image description

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

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

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

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

הכרות

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

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

המשמעות של שתי הנקודות הראשונות היא שעבור service או orchestration מסוימים, כל מה שמפתח צריך לעשות בכדי לפרושׂ קוד הוא ללחוץ על כפתור. המשפט השלישי מתאר יותר הלך רוח יותר מאשר פרט מימוש, והוא אחד מהעקרונות המנחים שלנו כצוות פיתוח. לפי עקרון זה, שהונחל לנו ע״י אבי (מנהל הפיתוח באוריבי), מפתח לא צריך להסס לפרושׂ קוד ל-Production ברגע בו הוא משוכנע שהעבודה עליו הושלמה מבחינת חווית המשתמש. במובן הזה, אנחנו מעדיפים שקוד ישבר ב-Production אם האלטרנטיבה היא שסט השינויים הזה יעלה אבק דיגיטלי בענף VCS נשכח, בהמתנה ל-Code Review או למיזוג ל-master. קורא חד עין (כמוך!) יעלה דאגה שייתכן ושבירת Production גרועה בהרבה מהמתנה לאישור ובחינה מדוקדקת של הקוד לפני פרישׂתו – וזוהי נקודה מצוינת למחשבה. בשביל זה יש Rollbacks, אבל נגיע לזה בהמשך.

טכנולוגיה

על מנת להריץ תהליך CI מוצלח עבור צוות קטן כמו שלנו (8 מהנדסים בעת כתיבת שורות אלו), בחרנו לשמור על פשטות ויותר חשוב – אחידות – בכל הליכי הפיתוח ופרישׂת הקוד. כמו שנהוג בחברות רבות, אנחנו עובדים ומפתחים תשתית של micro-services, שמשמעותה פירוק רכיבים במערכת שלנו ליחידות עבודה ואחריות תחומות ומוגדרות היטב.

את שירותי ה-Backend של המערכת אנחנו כותבים ב-Java מעל Spring Boot. בצד ה-client אנחנו משתמשים בשילוב של React ו-MobX. תשתית המחשב ומסדי הנתונים שלנו רצים מעל הענן של AWS, וכל הקוד שלנו נפרשׂ כקונטיינרים (Containers) של Docker. די פשוט, נפוץ וסטנדרטי. אם להיות כנה, בעוד זה תמיד מרגש וכיף לנסות כלים וטכנולוגיות חדשות, יש הרבה יתרונות לשמירה על Stack פשוט ובסיסי:

  • כל אחד מחברי הצוות יכול לעבוד על כל חלק במערכת. מבנה הקוד שלנו אחיד ומוכר בכל השירותים והטכנולוגיה זהה, אפילו אם טרם יצא לו לעבוד על הרכיב המסוים הזה.
  • תהליך הבנייה והפרישׂה (Build & Deploy) תמיד זהה, ללא קשר למהות והתפקיד של ה-service עצמו.
  • הגדרה של deployment עבור service חדש הנה תהליך פשוט יחסית ואין צורך להוסיף טלאים במערכת בכדי להתמודד עם מקרי קצה, הרפתקאות או תעלומות בלשיות.
  • בעיות ops-יות למיניהן הופכות למוכרות ויש סיכוי סביר ביותר שמישהו בצוות כבר נתקל והתמודד עמן בהצלחה בהקשר אחר לגמרי.

עבור ניהול ה-Continuous Integraion עצמו אנחנו משתמשים בשרת Jenkins CI – הוא חינמי, בעל יכולות רבות ומגוונות, מתוחזק ע״י קהילה פעילה וידידותית, ומאוד פשוט להתקנה. מערכת האוטומציה שלנו ממומשת ב-Ansible. אנחנו משתמשים בה לשורה של משימות, החל בהגדרה ותחזוקה של שרתים, פרישׂת קוד או סתם בכדי להריץ פקודות או איסוף מידע משרתים מרוחקים. Ansible הנו כלי שלהרגשתנו היה פשוט ללימוד ומקנה לנו מגוון רחב מאוד של יכולות שכבר באות מובנות בתוכו (למשל שליטה במשאבי AWS או ניהול כל מאפייני ה-Docker של השירותים שלנו).

התהליך עצמו

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

מאגר קוד חדש ב-GitHub – לרוב זהו פרויקט Java סטנדרטי. מלבד הקוד האפליקטיבי, הפרויקט מכיל גם קובץ הגדרה של Docker (מה שנקרא Dockerfile), המתאר את ״המתכון״ לבניית ה-image שממנו יורץ הקוד. בגדול הוא מעתיק לתוכה את ה-JAR ומריץ אותו.

ג׳וב ב-Jenkins שאחראי ל-Build – כאשר נדחף שינוי חדש ל-GitHub, מתקבלת ב-Jenkins קריאה שמפעילה את הג׳וב שתפקידו לבנות את ה-JAR של הפרויקט, לייצר עבורו את ה-image של Docker, ולדחוף את התוצר המוגמר ל-Docker registry1, שהנו שרת המכיל את כל ה-builds של כל service.

הגדרת deployment באמצעות Ansible – זהו קובץ JSON קטן המכיל metadata אודות פרישׂת ה-service החדש: על אילו שרתי EC2 הוא ירוץ, מגוון הגדרות ל-Docker עצמו (אילו פורטים ותיקיות לחשוף החוצה לשרת המארח, משתני סביבה פנימיים שיש להגדיר וכו׳). קובץ טיפוסי יכול להראות כך:

קובץ קונפיגורציה לדוגמא

service טיפוסי נפרש באמצעות ווריאציה של קובץ זה

הגדרת שרתי EC2 שיריצו את האפליקציה – השלב הבא הוא להרים מכונה אחת או יותר שעל גבה יפרשׂ הקוד. יש לנו עותק של מכונה גנרית (AMI) שכבר מוגדרת עם כל מה שצריך – בגדול Docker ועוד כמה כלי ניטור ונוחות. תהליך ההגדרה הוא אוטומטי ואפוי לתוך playbook של Ansible שמנהל את כל הקונפיגורציה של המכונה ומכין אותה עם כל מה שתזדקק לו ע״מ להצליח בעולם האמיתי (של אוריבי). נודה ונתוודה שיש לנו תכניות גדולות כיצד לפשט עוד יותר את החלק הזה בתהליך… יותר מאוחר.

ג׳וב ב-Jenkins שאחראי ל-Deployment – כל service זוכה לקבל ג׳וב שתפקידו לפרושׂ את הקוד. ג׳וב זה פשוט מריץ playbook של Ansible שעושה את הדברים הבאים על כל המכונות ב-deploy group הרלוונטי (קבוצת השרתים שמריצה את ה-service המסוים):

  1. מושך את ה-Docker image הרלוונטי מה-registry לתוך המכונה הרלוונטית. בד״כ הוא ימשוך את ה-image הכי עדכני אלא אם כן המשתמש פירט גרסה ספציפית של האפליקציה (אנחנו מתייגים כל Build לפי ה-commit של Git).
  2. מוריד את ה-instance הספציפי מה-Load Balancer על מנת שלא ישרת בקשות בזמן העדכון.
  3. מרים את ה-service עם כל הפרמטרים שהוגדרו ב-JSON של Ansible.
  4. מבצע בדיקת שפיות לוודא שה-service חי ובועט (למשל ע״י קריאה ל-endpoint ולוודא שחזר HTTP סטטוס של 200).
  5. רושם את ה-instance מחדש ל-Load Balancer ומחכה שידווח הצלחה.

אותו playbook רץ במקביל על כל המכונות הרלוונטיות ל-deploy. בהגדרה, הג׳וב ינסה קודם כל להרים בהצלחה את ה-service על מכונה אחת, ורק אם שלב זה צלח ימשיך הלאה ליתר המכונות. הליך זה מכונה Rolling Deployment, וניתן להשיג אותו די בקלות עם Ansible (ע״י שימוש בפרמטר בשם serial). בצורה הזו, גם אם ה-deploy למכונה הראשונה נכשל, יתר המכונות עדיין מריצות את הגרסה הקודמת של האפליקציה והמשתמשים של המוצר לא יושפעו.

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

ניטור והודעות

אפילו שכל ה-pipeline רץ בצורה אוטומטית, עדיין חשוב להשאר עם האצבע על הדופק ולדעת מה קורה עם האפליקציה. אנחנו מודדים הרבה פרמטרים בזמן אמת מכמעט כל נקודה בקוד (ע"י שליחת מטריקות לשרת Graphite), ומגדירים מעליהם גרפים והתראות. במידה ויש בעיה מיד נשלחת הודעה לערוץ המתאים ב-Slack אליו מחוברים כל אנשי הצוות, ולרוב היא נפתרת תוך דקות. אנחנו גם משתמשים ב-Slack בכדי לקבל התראות על כל Push שנעשה לקוד וכל ג'וב Build או Deploy שנעשה. ככה כולם מסונכרנים על תהליכי ה-CI, כולל אנשי הפרודקט שמקבלים עדכונים בזמן אמת על כל שינוי אפליקטיבי שישפיע על משתמשים.

לסיכום

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

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

טיפים אקראיים:

  • בחרו בכלי אוטומציה מההתחלה וכתבו playbook / recipe עבור כל מטלה הנדסית שאתם או מישהו אחר עשוי לחזור עליה בעתיד (למשל – התקנת תעודת SSL, הגדרת משתני סביבה בשרתים מרוחקים וכד׳). בעולם אידאלי לא תצטרכו לגשת למחשבים מרוחקים ב-SSH (גם אנחנו חוטאים בזה לפעמים, אבל להיכן היינו מגיעים בלי חלומות?…).
  • הקפידו לתייג כל Build של האפליקציה שלכם בגרסה משמעותית וייחודית על מנת שתוכלו בצורה פשוטה להבין איזה קוד רץ כרגע, מה הוא מכיל ואילו שינויים הוא הוסיף – אנחנו משתמשים בגרסת ה-commit של Git. יתרון נוסף הוא שאם דברים נשברים תוכלו בקלות יחסית לעשות Rollback ולחזור לגרסה שעבדה.
  • באותו משקל, תנו לשירותים שלכם דרך לדווח איזו גרסה הם מריצים. אצלנו לכל service יש endpoint שמחזיר את הגרסה שחיה כרגע.
  • השתמשו בספקי צד שלישי היכן שניתן, בתלות ביכולות הכלכליות של הארגון. אנחנו משתמשים ב-AWS ו-GitHub בשביל תשתיות ה-DevOps שלנו, Slack לתקשורת ארגונית ועוד כמה שירותים ברמה האפליקטיבית.
  • השתמשו ב-Rolling Deployment והקפידו שישנו יותר משרת אחת עבור כל service בכדי למנוע נקודות כשל בדידות (Service Redundancy). אסטרטגיה פשוטה זו מנעה לא מעט כשלים כש-deploy מסוים נשבר באמצע.
  • הבטיחו שה-stack הטכנולוגי שלכם אחיד ורזה – גישה זו תפשט בניית תהליכי CI ותחסוך כאבי ראש בהמשך של התמודדות עם מקרי קצה בשביל להביא קוד ל-Production.
  • הבטיחו שכל או רוב הצוות מעודכן על תהליכי ה-CI בחברה ואיך עובדות הקוביות השונות. זה ישתלם מאוד כשמשהו לא יעבוד כמצופה בזמן שאתם מחוץ למשרד ;).
1אנחנו מתחזקים Registry משלנו, למרות שישנם הרבה כאלו מבוססי ענן כמו Amazon ECS, docker.com, Google’s Container Engine ואחרים.