- שחזור: אותו image פועל אותו דבר במחשב, ב-CI, ובענן.
- דיפלוי מהיר: Pull ל-image והרצה.
- אבטחה: אפשר לסרוק image לפגיעויות ולנעול גרסאות.
- יעילות: שכבות מאפשרות cache לבילדים מהירים.
ענן והפצה - Docker + Cloud Run
בפרודקשן, "זה עובד אצלי" לא מספיק. צריך אריזה קבועה, ניהול תלויות, קונפיגורציה, ושיטה עקבית לפריסה. כאן נכנסים Docker ו-Cloud Run.
המטרה: לקחת שירות FastAPI (למשל Notes או RAG API), לארוז אותו לקונטיינר שניתן להריץ זהה בכל מקום, ואז לפרוס אותו ל-Cloud Run עם ניהול סודות, סקיילינג, ותצפיות (logs/metrics).
1) מהו Docker באמת?
Docker הוא סט כלים שמאפשר לבנות Container Image - חבילה שמכילה את האפליקציה, התלויות שלה, והוראות איך להריץ אותה. זה לא VM: הקונטיינר משתף את הקרנל של מערכת ההפעלה, ולכן הוא קל ומהיר יותר.
Image
תבנית בלתי משתנה (immutable) שמכילה שכבות. בונים אותה עם Dockerfile ומעלים לרג'יסטרי.
Container
תהליך רץ שנוצר מ-image. אפשר להריץ הרבה קונטיינרים מאותו image.
למה זה חשוב בהנדסת מערכות?
2) עקרונות Dockerfile נכון (ל-Python + FastAPI)
Dockerfile הוא מתכון. המטרה: בילד מהיר, image קטן, והרצה יציבה. הדפוס הפופולרי הוא:
- להעתיק קודם קבצי תלויות (requirements/pyproject) כדי לנצל cache.
- להתקין תלויות ואז להעתיק את קוד האפליקציה.
- להריץ עם שרת ASGI כמו uvicorn או gunicorn+uvicorn worker.
# Dockerfile
FROM python:3.12-slim
# 1) יציבות והרצה צפויה
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# 2) תלויות קודם - כדי לקבל cache
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 3) קוד אחר כך
COPY . .
# 4) Cloud Run מצפה לשרת שמאזין ל-$PORT
ENV PORT=8080
EXPOSE 8080
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
.dockerignore שחוסך זמן וכסף
בפרויקט אמיתי לא רוצים לשלוח ל-Docker build קבצים מיותרים:
__pycache__/ *.pyc .venv/ .env .git/ .gitignore tests/ *.db *.log
הדגש: כל מה שלא צריך בזמן ריצה - לא נכנס ל-image.
3) פרמטרים תפעוליים: env, secrets, ו-config
קונטיינר טוב לא "מקשיח" פרטים סביבתיים. במקום זה משתמשים במשתני סביבה:
DATABASE_URL=postgresql+psycopg://user:pass@host/db OPENAI_API_KEY=... LOG_LEVEL=info
כלל פרודקשן: לא שמים סודות בתוך ה-image ולא בקוד. ב-Cloud Run משתמשים ב-Secret Manager וממפים secrets למשתני סביבה בזמן דיפלוי.
4) Cloud Run במודל מחשבתי
Cloud Run מריץ קונטיינרים בצורה מנוהלת. אתה מספק image, והוא דואג ל:
- סקיילינג אוטומטי (כולל scale-to-zero).
- HTTPS, דומיין, ונתיב routing.
- ניהול גרסאות ושחרורים.
- לוגים ל-Cloud Logging.
דרישה מרכזית: האפליקציה חייבת להאזין לפורט שניתן ב-PORT (בדרך כלל 8080). לכן ב-Dockerfile אנחנו מאזינים ל-0.0.0.0:8080.
5) בנייה מקומית והרצה
נבנה image ונריץ קונטיינר:
# build docker build -t notes-api:local . # run docker run --rm -p 8080:8080 \ -e LOG_LEVEL=info \ notes-api:local
עכשיו אפשר לגשת ל-http://localhost:8080/docs ולראות את ה-OpenAPI.
6) Health checks וניטור מינימלי
Cloud Run לא דורש endpoints מיוחדים, אבל פרודקשן כן. מוסיפים endpoint כמו:
@app.get("/health")
def health():
return {"status": "ok"}
בנוסף, מומלץ להדפיס לוגים כ-JSON כדי להקל על חיפוש:
{
"level": "info",
"event": "request_done",
"path": "/api/v1/notes",
"status": 200,
"latency_ms": 37
}
7) Cloud Run: build, push, deploy (תהליך טיפוסי)
יש שני דפוסים נפוצים. הראשון הוא להשתמש ב-Cloud Build לבניית image ישירות מה-repo, בלי Docker מקומי.
# 1) build + push (Artifact Registry) gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT/REPO/notes-api:1.0 . # 2) deploy gcloud run deploy notes-api \ --image REGION-docker.pkg.dev/PROJECT/REPO/notes-api:1.0 \ --region REGION \ --platform managed \ --allow-unauthenticated \ --set-env-vars LOG_LEVEL=info
הערות פרודקשן חשובות ב-Cloud Run
- Concurrency: כמה בקשות במקביל לכל instance. גבוה יותר חוסך כסף אבל דורש קוד יעיל.
- Timeout: ברירת מחדל לא תמיד מתאימה ל-LLM. מגדירים timeout מתאים.
- CPU allocation: אפשר CPU רק בזמן בקשה או תמיד. משפיע על ביצועים ומשימות רקע.
- Min instances: אם חשוב latency נמוך, משאירים 1 instance תמיד, אבל עולה כסף.
- Secrets: ממפים מ-Secret Manager ולא מעבירים בקוד.
8) Object Storage - למה תמונות, וידאו וקבצים כבדים הולכים ל-"S3" ולא ל-DB
במערכות מודרניות, קבצים כבדים (תמונות, וידאו, PDFים, ZIPים) לא נשמרים בתוך הקונטיינר ולא בתוך טבלאות DB כ-BLOBים (למעט מקרים חריגים). במקום זה משתמשים ב-Object Storage - שירות בסגנון S3 (ב-GCP זה Cloud Storage, ב-AWS זה S3).
למה לא לשמור בתוך הקונטיינר?
- Cloud Run הוא stateless: ה-instance יכול להיסגר בכל רגע, והדיסק המקומי לא מובטח להתמיד.
- סקיילינג: כשיש כמה instances, לכל אחד דיסק מקומי שונה - הנתונים לא משותפים.
- העתקה איטית: לשכפל קבצים בין instances זו מורכבות ותקלות.
למה לא לשמור ב-DB כ-BLOB?
- עלות וביצועים: DB רלציוני לא בנוי לזרימת קבצים גדולים בקצב גבוה.
- גיבויים: גיבוי DB נהיה כבד ויקר.
- CDN: Object storage משתלב בקלות עם CDN והורדות מהירות.
הדפוס הנכון: מעלים את הקובץ ל-Object Storage ומקבלים URL (או key). ב-DB שומרים רק מטא-דאטה: מי העלה, מתי, איזה סוג, גודל, וכתובת הקובץ.
# טבלת DB (Postgres/Mongo) - מטא-דאטה בלבד file_id | user_id | content_type | size_bytes | storage_key | created_at # Object storage (S3/Cloud Storage) storage_key -> actual binary object (image/video/pdf)
איך מעלים קובץ בצורה נכונה (פרקטי)
- Signed URL: השרת יוצר URL חתום להעלאה, והלקוח מעלה ישירות ל-storage בלי להעביר את הקובץ דרך השרת.
- Public vs private: לרוב שומרים פרטי ומגישים דרך Signed URLs להורדה.
- Lifecycle: מדיניות מחיקה/ארכוב לקבצים זמניים כדי לשלוט בעלויות.
9) DB בענן - Postgres/Mongo לטבלאות מידע
במקומי השתמשנו ב-SQLite כדי ללמוד. בענן בדרך כלל משתמשים ב-PostgreSQL (Cloud SQL או ספק אחר). עקרונות חשובים:
- לא מחזיקים יותר מדי connections. קובעים pool קטן ומבוקר.
- מגדירים DATABASE_URL כ-env var.
- מוסיפים migrations (Alembic) כחלק מתהליך שחרור.
מלכודת נפוצה: לעלות ל-Cloud Run עם SQLite ולצפות להתמדה. Cloud Run הוא stateless, והדיסק המקומי לא מיועד להיות DB. SQLite מצוין ללמידה, לא לשירות רשת סקיילבילי.
10) ארכיטקטורה של דיפלוי למערכת GenAI
במערכת GenAI, לרוב נפריד בין:
- Service API (FastAPI) - חשוף ללקוחות או לשער API.
- RAG Service - אינדקס ושליפה, לעתים נפרד מה-API הראשי.
- Worker - תהליכים אסינכרוניים (אינדוקס מסמכים, batch jobs).
למה להפריד worker מ-API?
כי אינדוקס מסמכים יכול לקחת דקות, וזה לא תואם request-response. לכן שולחים job לתור (למשל Pub/Sub) ו-worker עושה את העבודה. Cloud Run מתאים גם ל-workers, אבל צריך לבחור מודל הרצה מתאים.
תרגול
שאלות בדיקה (10)
- מה ההבדל בין image ל-container?
- למה Docker build נהיה מהיר יותר כשמעתיקים requirements לפני הקוד?
- למה אסור לשים סודות בתוך image?
- מה המשמעות של PORT ב-Cloud Run?
- למה חשוב להאזין ל-0.0.0.0 ולא ל-127.0.0.1?
- מה tradeoff של concurrency גבוה ב-Cloud Run?
- למה SQLite לא מתאים לפרודקשן ב-Cloud Run?
- מה תפקיד Secret Manager?
- למה צריך health endpoint גם אם Cloud Run לא דורש?
- מתי כדאי להפריד worker משירות API?
משימת בית (Dockerize + Deploy)
בחר אחד מהשירותים שבנית (Notes DB או RAG API) ובצע:
- הוסף Dockerfile ו-.dockerignore.
- וודא שהשירות מאזין ל-PORT=8080 וכולל /health.
- בנה והרץ מקומית עם docker run והדגם שה-OpenAPI ב-/docs עובד.
- פרוס ל-Cloud Run עם gcloud (או דרך Console) והדגם URL ציבורי עובד.
docker build -t my-api:local . docker run --rm -p 8080:8080 my-api:local gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT/REPO/my-api:1.0 . gcloud run deploy my-api --image REGION-docker.pkg.dev/PROJECT/REPO/my-api:1.0 \ --region REGION --allow-unauthenticated
בדיקת פתרון (Checklist)
- הקונטיינר עולה מקומית ו-GET /health מחזיר 200.
- הקונטיינר מאזין ל-8080, והפורט נפתח עם -p 8080:8080.
- אין סודות בתוך ה-repo או ה-image.
- ב-Cloud Run יש URL עובד, ו-/docs נגיש.
- לוגים מופיעים ב-Cloud Logging וכוללים trace id אם הוספת.
הפרק הבא בסדרה יהיה Webhooks + Automation (Make/N8N) כולל חתימות, retries, idempotency, ותירגול.