הקריאה חוזרת לפני שהתוכנה נמצאת על המכונה.
עשר דלתות כניסה פורסות גרסה: deploy_spa, deploy_embed, deploy_pwa, deploy_mpa, deploy_api, deploy_cron, deploy_emitter, deploy_preview, deploy_user_group ו-deploy_mini_app. כל אחת מהן קובעת שדה אחד בדיוק, req.data.deploy_type, ומעבירה את הבקשה ל-deploy_app. המתודה deploy_app אינה מוצהרת ב-cpi_methods, ולכן אין קריאת פריסה גנרית שאפשר לבצע: הסוג נקבע לפי השם שנקרא, לא לפי ארגומנט שהועבר. הדבר הראשון שהפונקציה הזאת עושה אינו הקצאה. היא משכפלת אובייקט בסיס, קובעת app_type לפי סוג הפריסה, קובעת is_deployment ל-true, מצרפת את מסמך הגרסה כולו כ-app_release_doc ושומרת את התוצאה ב-xuda_master, הכול לפני שמתבקש ה-droplet השני. השם הציבורי מחושב ממזהה המסמך במקום להירשם: app_access_url הוא המזהה ואחריו .xuda.app, ובשום מקום במסלול הפריסה אין קריאת API ל-DNS. worker של Cloudflare מנתב כל מה שתחת xuda.app לאזור, והאזור הזה מבצע reverse proxy אל המכונה. מכאן העבודה מתפצלת לשניים. הקובץ config.json מצהיר על 12 שלבי job לפריסת link, וכל 12 מכסים את שלב היצירה. כשה-droplet עולה בפעם הראשונה ומבצע curl חזרה אל ה-webhook, אותו job נפתח מחדש מול רשימה אחרת וארוכה יותר: 24 שמות שנקראים מ-_INSTALL_STEPS, מ-connecting to server ועד activating the server. הגשר בין שני השלבים הוא מפתח memcached, install_job_ ואחריו מזהה האפליקציה, שנכתב בעת יצירת ה-droplet עם TTL של 3600 שניות. אם הוא פג עד האתחול הראשון, ההתקנה עדיין רצה, פשוט בלי סרגל התקדמות.

משותף מול ייעודי הוא מחרוזת חיבור, והוא זה שקובע את שלוש השורות האחרות.
ההבדל בין פרויקט ב-Studio לבין פריסה אינו הגדרה, אלא לאיזה CouchDB מחרוזת החיבור מפנה ב-common/xuda_node_common.mjs. פרויקט ב-Studio מפנה למסד הנתונים המשותף של פרויקטי האזור. פריסה מפנה למרכז הנתונים שלה עצמה, https על 6984 באופן ציבורי או http על 5984 דרך הכתובת הפרטית, עם סיסמת האדמין שנוצרה לאותו מרכז נתונים, ושם מסד הנתונים שלה מחושב ולא נבחר: db_ ואחריו מזהה מרכז הנתונים. את ארבע העמודות שלמטה כדאי לקרוא לאורך ולא לרוחב. מה שדלת הכניסה מצהירה עליו הוא מה שניתן לשלוט בו: deploy_spa מקבל app_datacenter או create_datacenter, deploy_mini_app אינו מקבל אף אחד מהם, ו-deploy_vps מקבל app_name, app_region ו-app_server_type בלי release_doc_id כלל.
פרויקט ב-Studio | אפליקציית mini | פריסת link | VPS | |
|---|---|---|---|---|
| היכן יושבים הנתונים | ה-CouchDB המשותף של פרויקטי האזור, נקבע לפי מחרוזת החיבור. | db_ ואחריו מזהה מרכז הנתונים, אותו כלל כמו בכל פריסה אחרת. | db_ ואחריו מזהה מרכז הנתונים, על ה-CouchDB של אותו מרכז נתונים. כל פריסה שמחוברת אליו קוראת וכותבת לאותו מסד נתונים אחד. | שום דבר מזה אינו רץ. deploy_vps כותב מסמך אפליקציה משלו עם app_type בערך vps ולעולם אינו קורא ל-deploy_app. |
| מכונות שהקריאה הזאת יוצרת | אין. זהו פרויקט, לא פריסה. | droplet פריסה אחד. deploy_mini_app אינו מצהיר על שדה מרכז נתונים כלל, ולכן אינו יכול ליצור כזה. | עד שניים: droplet מרכז הנתונים מתוך datacenter_image_id, ואחריו droplet הפריסה מתוך deployment_image_id. העברת data.app_datacenter מדלגת על הראשון. | VM אחד. אין שדה release_doc_id בקריאה, ולכן לא מצורפת אליו שום בנייה. |
| חומת האש שהמסלול יוצר | אין למה לחבר אותה. | אין. mini_app חולק case ב-switch עם vps ועם webserver שמסתיים ב-break בלי ליצור חומת אש. | כלל נכנס אחד: tcp 443 מ-0.0.0.0/0 ומ-::/0. בתעבורה יוצאת icmp, tcp ו-udp פתוחים. חומת האש של מרכז הנתונים פותחת את 5984 רק ל-droplets שמתויגים ב-datacenter_for_ ואחריו הפרויקט. | אין, וזה מכוון. במסלול ה-farm המתודה proxmox_apply_firewall חוזרת מוקדם עם vps: customer-managed firewall. |
| רצפת עלות חודשית | כלום בשורת התשתית. | 24 דולר, service_hosting_basic. הערך min_plan בדרגת free הוא שער המסלול, לא החשבון. | 97 דולר בפעם הראשונה: 24 עבור שרת הפריסה ועוד 73 עבור optimized_hosting_2, מעל מנוי Pro ב-49 דולר. user_group מוסיף 35, emitter 75, ו-pwa ו-preview 15. | כמה שה-slug עולה. deploy_vps אינו מצהיר על min_plan. |
שלבי job מוצהרים, וכולם שלב היצירה
שלבי התקנה שאותו job נפתח איתם מחדש באתחול הראשון
שרתים שפריסה ראשונה מקימה, מרכז נתונים ואז אפליקציה
הפריסה הראשונה החוקית הזולה ביותר לחודש, לפני המנוי
תיקייה אחת, שמתמלאת כולה מבחוץ.
ההתקנה היא ssh ו-scp שרצים מאזור הפלטפורמה מול root בכתובת המכונה. הערך copy_source_path מצביע על builds/ ואחריו הפרויקט ואחריו מזהה הבנייה, והקבצים נדחפים אל /var/xuda. אין image registry, אין העלאת artifact ואין משיכה שהמכונה יוזמת: זו מכונה שכותבים אליה. לכל שורה למטה יש שני פנים בכוונה, מה שההתקנה מעתיקה פנימה ומה שמגיש או קורא את זה אחר כך, כי בכמה מהן לא מדובר באותו דבר בכלל.
בנייה כותבת ארבעה דברים, ואף אחד מהם אינו ארכיון.
שום דבר בבנייה ב-Xuda אינו מייצר tarball שאפשר לקחת איתו למקום אחר. build_app מריץ את בודק התוכנית, מקדם מספר שלם אחד, כותב מסמכים למסד הנתונים של הפרויקט, ומניח תיקייה על כונן הפלטפורמה. את התיקייה הזאת ההתקנה קוראת אחר כך דרך scp: בנייה היא אזור היערכות על הפלטפורמה, לא artifact שנמסר למישהו. הבודק שלפניה הוא המלצה בלבד: ב-build_app ברירת המחדל של ignore_errors היא true, ולכן מספר השגיאות נוחת במסמך מידע הבנייה והגרסה ממשיכה בכל מקרה.
- הגרסה היא מספר שלם, לא hash ולא semver. הערך app_publish_info.app_lastBuild מקודם באחד בשורה 327 של build_module, והמספר הזה הוא ה-build_id שכל מסמך וכל נתיב במורד הזרם נושאים. שדה version שלצידו הוא מה שהקורא העביר, ברירת מחדל 0, ולכן אין לו משמעות שהפלטפורמה מקצה.
- הפרויקט נכתב החוצה כמסמכים: מסמך deployed_app אחד לכל מסמך ב-Studio בתוך מסד הנתונים של הפרויקט, כשכל אחד נושא build_id, ובנוסף מסמך גרסה אחד עם stat 3. המזהה של מסמך הגרסה הזה הוא מה ש-data.release_doc_id מציין בקריאת פריסה, ולכן פריסה מצביעה על שורה מוקפאת ולא על מה שהחדש ביותר.
- התיקייה היא builds/<project>/<build>/ על כונן הפלטפורמה: עותק של כונן ה-Studio בשם studio_assets, ובנוסף deployment/, common/*.mjs, runtime/xuda-sw.js, dist/runtime, dist/images/devices ושני פלטי Nuxt בנויים מראש. עקיפת המטמון קורית כאן ולא בזמן ההגשה, וזו פעולת שינוי שם: כל קובץ תחת dist/runtime/js נושא .<build_id> לפני הסיומת.
- מה שבנייה אינה עושה הוא לפרוס. build_app קובע update_required ל-true בכל פריסה חיה של הפרויקט ושולח התראת new_version_available, ושם עוצר. הבנייה עצמה אינה נוגעת בשום מכונה: כל פריסה מתעדכנת בקריאת update_app_version משלה. גם תמונת המצב אינה הרמטית, כי כמה מהעתקות ההתקנה נקראות scp <build folder> || scp <platform live copy>.
קריאה אחת, שתי מכונות, וארבע דרכים שבהן היא נעצרת.
עשר דלתות כניסה, פונקציה אחת. deploy_spa, deploy_pwa, deploy_api ושבע אחיותיהן קובעות כל אחת req.data.deploy_type על הבקשה ומעבירות ל-deploy_app, שאינו מוצהר ב-cpi_methods, ולכן אין קריאת פריסה גנרית שאפשר לבצע: הסוג נקבע לפי השם שנקרא. הריצה זהה בכל פעם: היא מסרבת על סמך מסמכים לפני שהיא מוציאה כסף, יוצרת לכל היותר שני droplets, ומעבירה את השני לשלב התקנה ש-job הפריסה כבר הפסיק לתאר.
ה-job שהופעל מצהיר על 12 שלבים. המכונה עונה עם 24 נוספים.
הקובץ config.json מצהיר על job_steps לכל מתודה, ולפריסת link הרשימה הזאת היא 12 שמות עם job_timeout של 1800000 ms. היא מכסה את שלב היצירה בלבד: שני שרתים, שתי חומות אש, שתי תוכניות גיבוי. ההתקנה היא שלב שני, מאוחר יותר, על המכונה, כשה-droplet עולה בפעם הראשונה ומבצע curl הביתה אל ה-webhook. באותו רגע אותו job נפתח מחדש מול רשימה אחרת, 24 השמות של _INSTALL_STEPS ב-deploy_module. שתי הרשימות מודפסות כאן במלואן.
שלוש פעולות, והקובץ שכל אחת מהן מגיעה ממנו.
פריסת גרסה אינה פקודה עם דגלים. היא שם של מתודה, מזהה מסמך, ו-job שמשנה צורה תוך כדי הצפייה בו.
- 01
בחירת דלת הכניסה, ציון הגרסה
אין קריאת פריסה גנרית. קוראים ל-deploy_spa, ל-deploy_pwa, ל-deploy_api או לאחד משבעת השמות האחרים, והבחירה הזאת היא ה-deploy_type: כל אחד מהם קובע req.data.deploy_type ומעביר ל-deploy_app, ש-cpi_methods אינו מצהיר עליו. השדה data.release_doc_id הוא חובה: זהו המזהה של מסמך הגרסה שהבנייה האחרונה כתבה עם stat 3. cpi/deploy_module/index.mjs שורות 418 עד 443.
- 02
מעקב אחרי ה-job שמחליף רשימות באמצע הריצה
ה-job מדווח על 12 השלבים של שלב היצירה ש-config.json מצהיר עליהם. כשה-droplet עולה בפעם הראשונה הוא מבצע curl אל ה-webhook, מזהה ה-job נקרא חזרה ממפתח ה-memcached בשם install_job_<app_id> שנכתב עם TTL של 3600 שניות, ואותו job נפתח מחדש מול רשימת ההתקנה בת 24 השמות. config.json cpi_methods.deploy_spa.job_steps וגם cpi/deploy_module/index.mjs שורות 4186 עד 4241. התקנה שנכשלה משאירה את האפליקציה ב-app_status_code 2 עד לקריאה ל-reinstall_<type>_resources.
- 03
קבלת המכונה המופעלת
ההתקנה כותבת את /var/xuda/ecosystem.config.js, ואז מריצה pm2 delete all, pm2 start ו-pm2 startup. spa, embed, pwa, mpa ו-mini_app מריצים webserver אחד ב-cluster mode עם instances max, בעוד cron, emitter ו-api מריצים כל אחד תהליך אחד ב-fork mode. השם הציבורי חושב ממזהה המסמך ומעולם לא נרשם: <_id>.xuda.app, מנותב על ידי worker של Cloudflare לאזור שמבצע reverse proxy אל המכונה. cpi/deploy_module/index.mjs 6795 עד 7040, cpi/router_module/index.mjs 1896 עד 1992.
שמונה דברים שמסלול הפריסה אינו עושה, החל בזה שכולם מניחים שקיים.
- אין rollback לפריסה על שרת. update_app_version מקבל שדה אחד, app_id, ומחלץ את build_id_current מתוך app_publish_info.app_lastBuild, שהוא תמיד הבנייה החדשה ביותר שקיימת. רק אתרים סטטיים יכולים לחזור אחורה, דרך set_site_live_version. בפריסה, חזרה אחורה פירושה בנייה קדימה מחדש.
- בנייה אינה פורסת. build_app קובע update_required ל-true בכל פריסה חיה של הפרויקט ושולח התראת new_version_available, ואז עוצר. הבנייה עצמה אינה דוחפת דבר לשום מכונה. כל פריסה מתעדכנת בקריאת update_app_version משלה, אחת בכל פעם, ולפרויקט עם שש פריסות חיות דרושות שש כאלה.
- תמונת הבנייה אינה הרמטית. כמה מהעתקות ההתקנה נקראות כ-scp מתיקיית הבנייה, ובמקרה של כישלון scp מהעותק החי של הפלטפורמה, כך שפריסה יכולה להיות מורכבת בחלקה ממה שאזור הפלטפורמה מחזיק בזמן ההתקנה, ולא כולה מהבנייה המוקפאת.
- הפריסה הראשונה החוקית הזולה ביותר עולה $97 לחודש של תשתית: service_hosting_basic ב-$24, שהם 1 vCPU, 2 GB ו-50 GB של דיסק, עבור שרת הפריסה, ובנוסף optimized_hosting_2 ב-$73 עבור מרכז הנתונים, מעל מנוי Pro ב-$49. גם min_plan בדרגת free אינו אחסון חינם. deploy_mini_app עדיין דורש slug מסוג service_hosting_, הזול ביותר $24, ואינו מצהיר על שדה מרכז נתונים כלל, כך שעם השדות המוצהרים שלו בלבד הוא עובד על פרויקט החשבון ומחזיר code -2 בכל מקום אחר.
- תשע מתודות מצהירות על data.create_datacenter.app_region ועל app_server_type אך לא על app_name, ש-deploy_app דורש כשהוא יוצר מרכז נתונים בתוך הקריאה. שליחת השדות המוצהרים בדיוק מחזירה code -5, app_name ריק. רק deploy_datacenter מצהיר על השדה הזה.
- deploy_load_balancer דורש farm, ובכל זאת _render_haproxy_config מאגד רק backends שאינם מוקצים דרך Proxmox ואינם בכתובת 10., 172.16-31. או 192.168. קבוצת backends שכולה על ה-farm מייצרת הערה אחת: no reachable backends yet.
- התקנה שנכשלה משאירה את האפליקציה ב-app_status_code 2. install_hosting_resources מחזיר code -1, ה-webhook מסמן את ה-job ככושל אך לעולם לא את מסמך האפליקציה, ושום דבר אינו מנסה שוב מעבר לארבעה ניסיונות ssh חולפים. התיקון הוא קריאת reinstall ידנית עבור סוג הפריסה הזה.
- התעודה בכל מכונה היא בחתימה עצמית: openssl req -x509 ל-99999 ימים, RSA 2048, כאשר ה-common name הוא מזהה האפליקציה תחת xuda.app. כל ssh וכל scp בפריסה רצים עם StrictHostKeyChecking=no ועם UserKnownHostsFile=/dev/null, כי כתובות ה-IP של האורחים ב-farm ממוחזרות ממאגר.
השאלות שהאשף אינו עונה עליהן
שישה דברים שנקראו מתוך deploy_module, build_module ו-config.json, ונענים מול הקוד שנשלח בפועל ולא מול הכוונה.
אותו מסלול, עם כפתורים.
למי שמעדיף לראות את לוח הבקרה על פני קריאת שמות השדות, כאן נבחרים סוג הפריסה, סוג השרת ושלבי ה-job.
פריסה היא מסמך, אחר כך droplet, ואחר כך scp.
deploy_app כותב את המסמך אל xuda_master לפני שהוא מבקש מכונה, מחשב את השם הציבורי ממזהה המסמך הזה, גוזר את שם מסד הנתונים ממרכז הנתונים, ואז דוחף את תיקיית הבנייה מאזור הפלטפורמה אל /var/xuda דרך ssh ומפעיל אותה תחת pm2. שום דבר במסלול הזה אינו registry, דחיפת image או משיכה מצד המכונה. איזה מבין עשרת סוגי הפריסה מתאים הוא שאלה אחרת מהשאלה איך כל אחד מהם עובד, והתשובה לה נמצאת בעמוד התרחישים. כשברור מה הצורה, אפשר להתחיל פריסה מלוח הבקרה.