איקס עיגול בClojure – חלק ד׳ עולים לפרודקשיין*

* פרודקשיין – כמו הרבה מונחים בשפת ההייטק מדובר בעצם בתרגום של המילה production למונח עברי – לשימוש במשפט בסלאק כמו: ״תדפלם לי בבקשה את הגרסה החדשה של הקוד לפרודקשיין״ שפירושה תתקין (deploy) את הגרסה החדשה של הקוד על המחשבים שמריצים את השרת האמיתי (השרת שעושה את העבודה – באתר אינטרנט לדוגמא מדובר בשרת שהמשתמשים מתחברים אליו).
מתוך מדריך ההייטקיסט לגלקסיה
אז כתבנו לוגיקה, שרת וממשק משתמש וכעת אנחנו רוצים לפתוח את המוצר שלנו לשימוש העולם, במקרה שלי הכוונה היא להעלות את המשחק לאתר אינטרנט פעיל, תחת מתן דגש לנקודות הבאות:
- פשטות – מערכת שאינה מסובכת להרמה, ניתן לעדכן אותה ולהוסיף שרתים חדשים בקלות.
- מחיר – לא לשלם הרבה כסף על אחזקת השרתים.
- תמיכה במספר גדול של משתמשים – מה שקרוי בלע״ז סקלאביליות, היכולת לשכפל את השרת על מנת לאפשר תמיכה בהרבה משתמשים.
בפוסט זה אסקור כמה אפשרויות שונות לבצע את זה ואדגים בהרחבה את האפשרות שאני בחרתי – לארח את האתר בשיטת serverless.
הפרויקט שלי מתחלק לשני חלקים שצריכים להיות זמינים למשתמש
- צד שרת שמורכב מקבצי jar – קובץ בינארי של המכונה הווירטואלית של java, Clojure רץ על java בסופו של דבר.
- צד משתמש שמורכב מקבצי html/css/js (שכן הקוד של הsvelte מתקמפל לקבצים אלו), ומתקשר עם צד השרת בקריאות REST.
וכעת נסקור 3 דרכים שונות איך לדפלם את הקוד לפרודקשיין (אני אוהב את המשפט הזה…) שרת רגיל, שימוש בקונטיינר, שיטת הserverless.
שרת רגיל
בשרת רגיל העבודה קצת דומה לאופן שבו אנחנו עובדים על שרת הפיתוח, בשלב הראשון אנחנו קונים מחשב (פיזי או בענן) ומתקינים עליו סביבת הרצה של השרת עליו אנחנו מוסיפים הקוד שלנו. במקרה שלנו נתקין Apache-Tomcat שמאפשר הרצה של Java Servlet אליו נעלה קובץ war שיכיל בתוכו את הקוד של השרת, וגם יארח את הקבצים של הקליינט. לשרת צריך לדאוג לכתובת IP קבוע אליה נפנה את רישום הDNS של האתר שלנו. (מומלץ להשתמש בכל מיני שיטות CI/CD על מנת להכניס אוטומציה של עדכון גרסה בשרת אך לא ניכנס לזה כאן)
היתרונות:
פשטות יחסית עם שליטה מלאה בברזלים של המחשב.
חסרונות:
כשהעומס גדל אזי צריך להוסיף load-balncer, לשכפל את השרתים ולהוסיף דאטה-בייס ראשי שנגיש לכל המכונות שכן כל בקשה של המשחק יכולה להגיע לכל אחת מהמכונות.
אני משלם על המכונה את אותו המחיר בין אם יש לי לקוח אחד שמשתמש או עשרת אלפים לקוחות.
הסבר מלא על הדרכים השונות להעלות שרת clojure-ring לפרודקשיין ניתן למצוא כאן.
קונטיינר (Docker)
שיטה מודרנית יותר להעלות שרתים היא ע״י שימוש בטכנולוגיה של קונטיינרים – שזו בעצם שכבת הפשטה להתקנה והרצת יישומים בתוך מכולות נפרדות שכל אחת מהם מתפקדת כמכונה וריטואלית אך ברמה של מערכת ההפעלה כך שהם רצות באופן יעיל. אחד ההיתרונות בשימוש בקונטיינר זה שהוא מאפשר לי ליצור סביבה אחידה ותואמת שהשרת שלי ירוץ אליה ללא קשר למערכת ההפעלה שמותקנת על השרת האמיתי – כך שבין עם אני רץ על שרת פיזי של לינוקס או ווינדוס הקוד שלי יתפקד אותו דבר בתוך הקונטיינר כי בשני המקרים אני ירוץ עם אותה ליבה של מערכת הפעלה וריטואלית.
לאחר שבניתי את הקונטיינר פעם אחת קל מאוד לשכפל אותו לסביבות נוספות ולהתאים את עצמי למחשבים פיזיים שונים ומערכות הפעלה שונות מבלי שאני צריך לפתור את כל הבעיות מחדש. את הקונטיינר עצמו צריך גם כן להריץ על מחשב פיזי או בענן כאשר לספקיות הענן הגדולות יש אפשרות להריץ את הקונטיינר מבלי להתעסק בכלל במחשב עליו הם רצות.
מדריך לבניית קונטיינר של Clojure ניתן למצוא כאן.
היתרונות:
גמישות, קל ליצור סביבת פיתוח שעובדת בדיוק כמו סביבת בפרודקשיין
חסרונות:
תשלום לפי כמות הקונטיינרים שבאוויר כך שכל בעיות העומס אני צריך לפתור כמו בשרת רגיל.
ללא-שרת (סערברלס בלע״ז)
שיטה זו שהפכה לנפוצה מאוד בשנים האחרונות מאפשרת לי לשלם אך ורק על זמן השימוש הממשי של המכונות שלי (כתלות במעבד שבחרתי וגודל הזיכרון שאני משתמש). כך שבעצם בכל פעם שאני מבצע קריאת API ל״שרת״ אזי קטע הקוד שלי רץ מבלי שאני צריך לדאוג בכלל למחשב מריץ אותו, זמן החישוב של המכונה נרשם ובסוף החודש אני מחויב אך ורק על הזמן שהשתמשתי.
מאחורי הקלעים החברת הענן מעלה קונטיינר (אם אין אחד למעלה או שיש אבל הוא כבר עמוס מדי) ומריצה עליו את הקוד. בדרך זו אני כמפתח מתעסק רק בקוד וחלק גדול מהעיסוק בכובע הdevOps נחסך ממני ואני בכלל לא צריך לדאוג לסקאלביליות של האתר שלי (חוץ מלדאוג לדאטהבייס משותף) שכן הוא יכול לשרת ללא בעיה 20 משתמשים או 10,000 (כל זמן שיש לי כסף לממן את זה).
באותו אופן במקום שיהיה שרת שמחזיר את דפי האינטרנט של צד הלקוח שבניתי, אני יכול לאחר את האתר שלי בשירות של אחסון אובייקטים בbucket s3 (או משהו דומה אצל ספק ענן אחר) ולקנפג את הbucket לארח את אתר האינטרנט שלי וכך לשלם רק על התעבורה והמקום שהמידע תופס – מבלי לדאוג לאיזה שרת פיזי מחזיק את הקבצים של צד הלקוח.
היתרונות:
סקאלביליות – קל לתמוך בהרבה משתמשים
תשלום לפי שימוש
חסרונות:
לוקח זמן עד שהקונטיינר עולה מאחורי הקלעים – יכול להגיע לשמונה שניות במקרה של Clojure.
בשונה מpython ומNode.js ומכיוון שחבילת הjar כוללת בתוכה הרבה סיפריות אין אפשרות לערוך את הקוד דרך הממשק של aws – ועל כל שינוי צריך להעלות גרסה חדשה מהמחשב לענן כך שלולאת המשוב על כל שינוי קטן יכולה לקחת 4-5 דקות.
קצת devOps
אחרי כל ההתלבטויות בחרתי לעבוד בשיטת ללא שרת מהסיבות הבאות:
- אני לא צופה הרבה משתמשים באפליקציה שבניתי – כך ששרת קטן היה ללא ספק מספיק, אך לא בא לי לשלם 3.5 דולר כל חודש כל זמן שהבלוג שלי באוויר.
- מבין כל הדרכים זו הדרך הכי מאתגרת ומעניינת – ובכלל כיף לקמפל Cloujre לjar ולהשתמש בו באופן מגניב.
צד לקוח
את האתר אינטרנט אירחתי בaws s3 בעזרת ההוראות מכאן (דילגתי על הקטע של הwww שכן בכל מקרה אני כבר מעלה את האתר בסאב-דומיין) ולאחר מכן הוספתי ברישום הDNS של האתר שלי הפנייה לs3.
את הבנייה של צד הלקוח והעלאה לשרת ביצעתי ע״י הסקריפט:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
exit_on_failure() { exit_code=${1} message=${2} if [ ${exit_code} -ne 0 ]; then echo "${message}, exit code:${exit_code}" exit ${exit_code} fi } npm run build exit_on_failure $? "Failure while building svelte project, did you run 'npm install'?" cp -r public tmp_public ver=$(echo $RANDOM | md5sum | head -c 20;) cd tmp_public/build mv bundle.css bundle.$ver.css mv bundle.js bundle.$ver.js mv bundle.js.map bundle.$ver.js.map cd .. sed -i -e "s/bundle/bundle.$ver/g" index.html cd .. s3_bucket=tic-tac-toe.route42.co.il echo Synching Build Folder: $s3_bucket... aws s3 sync tmp_public/ s3://$s3_bucket --delete --cache-control max-age=31536000,public --profile personal echo Adjusting cache... # setting index.html cache to 0 age so react js file will be update every time that we upload new version aws s3 cp s3://$s3_bucket/index.html s3://$s3_bucket/index.html --metadata-directive REPLACE --cache-control max-age=0,no-cache,no-store,must-revalidate --content-type text/html --profile personal rm -rf tmp_public echo DONE! |
כאשר מה שאני עושה בגדול זה לקמפל את החבילה לjs שמיועד לפרודקשיין, לאחר מכן אני משנה את השמות של קבצי הcss והjs המרכזיים כדי להימנע מבעיות של caching מעלה את הכל לs3 ומגדיר שלא יהיה בכלל cache לקובץ הindex.
חוץ מזה הוספתי לindex.html חבילה קטנה של אנליטקס RUM בשביל לעקוב אחריי שימוש:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width,initial-scale=1'> <title>Route42 Tic Tac Toe</title> <link rel='icon' type='image/png' href='/favicon.png'> <link rel='stylesheet' href='/global.css'> <link rel='stylesheet' href='/build/bundle.css'> <script defer src='/build/bundle.js'></script> <script> (function(n,i,v,r,s,c,x,z){x=window.AwsRumClient={q:[],n:n,i:i,v:v,r:r,c:c};window[n]=function(c,p){x.q.push({c:c,p:p});};z=document.createElement('script');z.async=true;z.src=s;document.head.insertBefore(z,document.head.getElementsByTagName('script')[0]);})( 'cwr', '1346b3f6-5bb7-480d-87df-6706f3adc022', '1.0.0', 'eu-west-1', 'https://client.rum.us-east-1.amazonaws.com/1.2.1/cwr.js', { sessionSampleRate: 1, guestRoleArn: "arn:aws:iam::944156395127:role/RUM-Monitor-eu-west-1-944156395127-5587485204561-Unauth", identityPoolId: "eu-west-1:b525562c-cf84-4759-b4aa-1180aaf495ca", endpoint: "https://dataplane.rum.eu-west-1.amazonaws.com", telemetries: ["performance","errors","http"], allowCookies: true, enableXRay: false } ); </script> </head> <body> </body> </html> |
צד שרת
בשביל לעבוד בlambda serverless הייתי צריך לבצע את השינויים הבאים בקוד של השרת:
להתחיל לעבוד עם בסיס נתונים – בקריאת למדא אי אפשר לסמוך על זיכרון הRAM שכן הוא מתנקה בכל קריאה ולכן הוספתי עבודה מול בסיס נתונים dynamoDB (שגם הוא serverless) כאשר השימוש שלי בו הוא מאוד פשוט, שמירה של אובייקט והוצאה של אובייקט. כך שהקוד נראה כך:
1 2 3 4 5 6 7 8 9 10 11 |
(ns lambda-serverless.dynamo-db (:require [taoensso.faraday :as far])) (def client-opts {:endpoint "http://dynamodb.eu-west-1.amazonaws.com"}) (defn update-game [game-id game-data] (far/put-item client-opts :tic-tac-toe {:id game-id :data game-data})) (defn get-game [game-id] (println "get-game game-id" game-id) (get (far/get-item client-opts :tic-tac-toe {:id game-id}) :data)) |
ובנוסף לכך היה צריך להרים טבלה בשם tic-tac-toe ולתת הרשאות קריאה וכתיבה לIAM שמריץ את הפונקציה בענן.
לשנות את שיטת המשחק של המחשב -מכיוון שאין לי שרת פעיל כל הזמן העדפתי לשחק את התור של המחשב רק כשמבקשים עדכון במקום לרוץ על כל המשחקים. השגתי את זה על ידי שמירה של הזמן בו המחשב ישחק את התור שלו – ובכל בקשת עדכון תתבצע בדיקה האם הזמן עבר ובמידה וכן אזי המחשב יבצע את התור שלו.
להפריד בין הקריאות לשרת לבין ההרצה של המשחק – זה משהו שבכל מקרה רציתי כבר לעשות ולמען האמת הוא שיפר את הקריאות של הקוד שלי. כך שקוד ניהול המשחק השתנה ל:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
(ns lambda-serverless.game-runner (:require [lambda-serverless.dynamo-db :as db] [lambda-serverless.gamelogic :as gamelogic])) (defn start_new_game [] (let [game_id (.toString (java.util.UUID/randomUUID))] (db/update-game game_id {:board (gamelogic/get_initial_board) :player (gamelogic/get_player_sign) :ttl (/ (+ (System/currentTimeMillis) (* 1000 60 30)) 1000.0) :next-cpu-turn (+ (System/currentTimeMillis) 250 (rand-int 1500))}) {:game_id game_id})) (defn get_game_update [request] (let [game_id (get request "game_id") game-data (db/get-game game_id) {:keys [board player next-cpu-turn]} game-data turn (gamelogic/get_current_player board) winner (gamelogic/get_winner board)] (if (and (nil? winner) (not= turn player) (< next-cpu-turn (System/currentTimeMillis))) (db/update-game game_id (assoc game-data :board (gamelogic/move board (gamelogic/get_current_player board) (gamelogic/get_random_move board))))) {:board board :player player :turn turn :winner winner})) (defn move [request] (let [game_id (get request "game_id") x (get request "x") y (get request "y") game-data (db/get-game game_id) {:keys [board player]} game-data] (if (not= (gamelogic/get_current_player board) player) {:status "ERROR" :error "NOT YOUR TURN"} (if (gamelogic/is_game_ended board) {:status "ERROR" :error "GAME ENDED"} (if (not (gamelogic/is_cord_empty board [x y])) {:status "ERROR" :error "SQUARE NOT EMPTY"} (do (db/update-game game_id (assoc (assoc game-data :board (gamelogic/move board player [x y])) :next-cpu-turn (+ (System/currentTimeMillis) 250 (rand-int 1500)))) {:status "OK"})))))) |
להוסיף נקודת כניסה (handler) שממנו פונקציית הלמדא תתחיל לרוץ – לצורך כך השתמשתי בספריה הזאת בשביל להתממש עם הלמדא, והוספתי את הקוד הבא:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
(ns lambda-serverless.core (:require [uswitch.lambada.core :refer [deflambdafn]] [clojure.data.json :as json] [taoensso.faraday :as far] [clojure.java.io :as io] [lambda-serverless.game-runner :as game-runner])) (defn handle-event [event] (println "Got the following event: " (pr-str event)) (let [body (json/read-str (get event "body")) action (get body "action")] (if (= action "start_new_game") (game-runner/start_new_game) (if (= action "move") (game-runner/move body) (if (= action "get_game_update") (game-runner/get_game_update body) {:status "ERROR NO SUCH ACTION"})) ))) ;; {:status "OK"}) (deflambdafn lambda-serverless.core.handler [in out ctx] (let [event (json/read (io/reader in)) res (handle-event event)] (with-open [w (io/writer out)] (json/write res w)))) |
כאשר בשביל ליצור לעלות ולעדכן את פונקציית הלמדא ביצעתי:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# Create new function lein uberjar aws lambda create-function --profile personal \ --function-name tic-tac-toe-clojure \ --runtime java8 \ --memory-size 256 \ --role arn:aws:iam::944156395127:role/basic_lambda_role \ --handler lambda-serverless.core.handler \ --zip-file fileb://target/lambda-serverless.jar \ --region eu-west-1 # Update function lein uberjar aws s3 cp target/lambda-serverless.jar s3://lambda-uploads-a1v.prd/lambda-serverless.jar --profile personal aws lambda update-function-code --profile personal \ --function-name tic-tac-toe-clojure \ --s3-bucket lambda-uploads-a1v.prd \ --s3-key lambda-serverless.jar \ --region eu-west-1 |
לאחר מכן הוספתי API gatway שמתחבר לפונקציית הלמדא כך שכל פנייה לכתובת שלו לגרום לריצה של הפונקציה. את הgateway הייתי צריך לפתוח לCORS בשביל שהדפדפן יוכל לשלוח אליו קריאות REST (בגלל שהם יושבים על כתובות שונות). זה היה קצת מעצבן שכן לא הצלחתי לעשות את זה כשהוספתי את הapi ישר מהדף של הלמדא – רק כשיצרתי rest-api וחיברתי את הPOST שלו ללמדא הצלחתי ליצור גם option שמחזיר את הheaders הנכונים של הCORS.
וזהו יש לנו משחק באוויר! תעזבו הכל ולכו ל tic-tac-toe.route42.co.il
זה היה הפרק האחרון בסדרת בונים איקס עיגול בClojure. השפה בהחלט השאירה לי חשק להמשיך ולהשתמש בה ולחשוב על דרכים להתחיל לעבוד איתה ביומיום. בהתחלה השפה נראית ומרגישה כמו ג׳בריש עם הרבה סוגריים שלו ברור היכן הם מתחילים והיכן הם נגמרים, והרגשתי תסכול כשלקח לי ארבעים דקות לכתוב פונקציה של שלוש שורות אך ברגע שמתחילים להיכנס הדברים נהיים הרבה יותר ברורים והגיוניים.
הבנייה של השפה על גביי המכונה של java הופכת אותה לשפה שקל לדפלם ולהשתמש בה בפרודקשיין בדרכים שונות מה שללא ספק מוסיף לזה נקודות רבות בעיניי.
כתיבת תגובה