מסמך זה מספק הוראות מפורטות לפריסת Open WebUI, ממשק צ'אט באירוח עצמי שמבוסס על דפדפן, בסביבות עם air gap של Google Distributed Cloud (GDC). Open WebUI מתחבר לקצוות העורפיים של ההסקה שנפרסו באמצעות המדריכים למשתמשים של Ollama ו-vLLM, דרך ממשקי ה-API שלהם שתואמים ל-Ollama ול-OpenAI. כך מפתחים ומשתמשי קצה יכולים לקיים אינטראקציה עם המודלים של המשקלים הפתוחים בלי לכתוב קריאות ל-API.
ארכיטקטורה
Open WebUI פועל כ-Deployment עם רפליקה אחת במרחב השמות שלו באשכול הרגיל. המצב שלו (חשבונות משתמשים, צ'אטים, הגדרות) מאוחסן ב-ReadWriteOnce PersistentVolumeClaim. ConfigMap אחד מכיל את הגדרות החיבור לקצוות העורפיים של ההסקה, שאליהם מגיעים דרך השמות שלהם בתוך האשכול Service, ו-Secret אחד מכיל את מפתח החתימה של הסשן. Service של Kubernetes מסוג LoadBalancer חושף את ממשק האינטרנט, ואובייקטים של NetworkPolicy / ProjectNetworkPolicy מאבטחים את הגישה אליו.
לפני שמתחילים
מוודאים שפרסתם לפחות קצה עורפי אחד להסקת מסקנות באמצעות המדריך למשתמש במודלים של Gemma עם Ollama, המדריך למשתמש במודלים של Gemma עם vLLM או המדריך למשתמש במודלים של Mistral עם vLLM, ושיש לכם גישה לספרייה ${HOME}/gdcag-solutions של הטמעת העזר המתאימה בתחנת העבודה.
ניהול זהויות והרשאות גישה
מוודאים שהחשבונות, התפקידים וההרשאות הנדרשים ב-IAM מוגדרים כמו שצריך.
תפקידי משתמש GDC בארגון:
- אופציונלי: אדמין רגיל של אשכול (הצגת אשכולות מהמסוף)
תפקידי GDC User בפרויקט:
- אדמין של אשכול רגיל (
standard-cluster-admin)
התפקיד GDC User באשכול הרגיל:
-
StandardClusterRoleBindingאלStandardClusterRolecluster-admin(נוצר על ידי אדמין IAM של פרויקט, ראו את הקטע ניהול זהויות והרשאות גישה בהטמעה לדוגמה)
תחנת עבודה
אלא אם צוין אחרת, צריך להריץ את כל הפקודות במדריך הזה מתחנת עבודה עם הכלים הנדרשים וקישוריות לסביבה ולאינטרנט.
יוצרים את מבנה הספרייה של המדריך למשתמש:
mkdir -p ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.d mkdir -p ${HOME}/gdcag-solutions/open-weight-models/open-webui/secrets chmod u=rwx,go= ${HOME}/gdcag-solutions/open-weight-models/open-webui/secretsיוצרים את מפתח החתימה של הסשן:
openssl rand -hex 32 > ${HOME}/gdcag-solutions/open-weight-models/open-webui/secrets/open_webui_secret_key chmod u=rw,go= ${HOME}/gdcag-solutions/open-weight-models/open-webui/secrets/open_webui_secret_keyיוצרים את קובץ התצורה של סביבת המדריך למשתמש:
cat << 'EOF' > ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.d/open-webui.sh && echo "Successfully created." || echo "Failed to create!" # Open WebUI export GDCS_OPEN_WEBUI_IMAGE_TAG="v0.11.3" export GDCS_OPEN_WEBUI_NAME="open-webui" export GDCS_OPEN_WEBUI_PVC_SIZE="20Gi" export GDCS_OPEN_WEBUI_SECRET_KEY="$(cat ${GDCS_USER_GUIDE_HOME}/secrets/open_webui_secret_key)" # Inference backends (in-cluster Service addresses, several backends separated by ';'). # The user guides name the Service after the model: Ollama uses GDCS_OLLAMA_MODEL_KUBERNETES # (gemma4:e4b -> gemma4-e4b) and vLLM uses GDCS_HF_MODEL_KUBERNETES (such as vllm-gemma or vllm-mistral) # (mistralai/Mistral-7B-Instruct-v0.2 -> mistralai-mistral-7b-instruct-v0-2); kubectl get service # in the workload namespace (ollama-gemma, vllm-gemma or vllm-mistral) lists them. export GDCS_OPEN_WEBUI_ENABLE_OLLAMA_API="OPEN_WEBUI_ENABLE_OLLAMA_API" export GDCS_OPEN_WEBUI_OLLAMA_BASE_URLS="OPEN_WEBUI_OLLAMA_BASE_URLS" export GDCS_OPEN_WEBUI_ENABLE_OPENAI_API="OPEN_WEBUI_ENABLE_OPENAI_API" export GDCS_OPEN_WEBUI_OPENAI_API_BASE_URLS="OPEN_WEBUI_OPENAI_API_BASE_URLS" export GDCS_OPEN_WEBUI_OPENAI_API_KEYS="none" # vLLM does not check API keys unless started with --api-key # Workload export GDCS_WORKLOAD_NAMESPACE="open-webui" # CPU resources export GDCS_WORKLOAD_CPU_LIMIT="2" export GDCS_WORKLOAD_CPU_REQUEST="1" # Memory resources export GDCS_WORKLOAD_MEMORY_LIMIT="4Gi" export GDCS_WORKLOAD_MEMORY_REQUEST="2Gi" EOFמחליפים את מה שכתוב בשדות הבאים:
-
OPEN_WEBUI_ENABLE_OLLAMA_API:trueאם פרסתם קצה עורפי של Ollama, אחרתfalse. -
OPEN_WEBUI_OLLAMA_BASE_URLS: כתובות ה-URL של שירותי Ollama בתוך האשכול, מופרדות באמצעות;, לדוגמהhttp://gemma4-e4b.ollama-gemma.svc.cluster.local. אם לא פרסתם קצה עורפי של Ollama, השאירו את השדה ריק. -
OPEN_WEBUI_ENABLE_OPENAI_API: הערךtrueאם פרסתם קצה עורפי של vLLM, אחרת הערךfalse. -
OPEN_WEBUI_OPENAI_API_BASE_URLS: כתובות ה-URL בתוך האשכול של שירותי vLLM עם הנתיב/v1, מופרדות באמצעות;, לדוגמהhttp://mistralai-mistral-7b-instruct-v0-2.vllm-mistral.svc.cluster.local/v1אוhttp://google-gemma-4-e2b-it.vllm-gemma.svc.cluster.local/v1. אם לא פרסתם קצה עורפי של vLLM, השאירו את השדה ריק.
-
יוצרים את קובץ טעינת הסביבה של המדריך למשתמש:
cat << 'EOF' > ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.sh && echo "Successfully created." || echo "Failed to create!" source "${HOME}/gdcag-solutions/open-weight-models/IMPLEMENTATION/env.sh" export GDCS_USER_GUIDE_HOME="${HOME}/gdcag-solutions/open-weight-models/open-webui" echo "GDCS_USER_GUIDE_HOME=${GDCS_USER_GUIDE_HOME}" # Sourced in dependency order source "${GDCS_USER_GUIDE_HOME}/env.d/open-webui.sh" EOFמחליפים את מה שכתוב בשדות הבאים:
-
IMPLEMENTATION: ollamaאוvllm, ההטמעה לדוגמה שהשלמתם בתחנת העבודה הזו.
-
עורכים ובודקים את קובץ הסביבה בעורך המועדף:
${EDITOR:-vi} ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.d/open-webui.shמפעילים את קובץ הסביבה:
source ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.shהפלט אמור להיראות כך:
GDCS_ROOT_HOME=HOME_DIRECTORY_PATH/gdcag-solutions GDCS_IMPLEMENTATION_HOME=HOME_DIRECTORY_PATH/gdcag-solutions/open-weight-models/IMPLEMENTATION GDCS_USER_GUIDE_HOME=HOME_DIRECTORY_PATH/gdcag-solutions/open-weight-models/open-webui
אשכול
הגדרת
gdcloud:gdcloud config set core/account "default-user" gdcloud config set core/organization_console_url "${GDC_ZONAL_CONSOLE_URL}" gdcloud config set core/project "${GDC_PROJECT}" gdcloud config set core/zone "${GDC_ZONE}"מאמתים את סביבת GDC:
gdcloud auth loginאחזור פרטי הכניסה של האשכול:
gdcloud clusters get-credentials "${GDC_STANDARD_CLUSTER_NAME}" \ --project="${GDC_PROJECT}" \ --standard \ --zone="${GDC_ZONE}"מוודאים שקיימים ה-backend של ההסקה
Serviceשמופיעים בקובץ הסביבה:for url in $(echo "${GDCS_OPEN_WEBUI_OLLAMA_BASE_URLS};${GDCS_OPEN_WEBUI_OPENAI_API_BASE_URLS}" | tr ';' ' '); do host=$(echo "${url}" | sed -E 's#https?://([^/:]+).*#\1#') kubectl get service "${host%%.*}" --namespace="$(echo "${host}" | cut -d. -f2)" --output=wide done
הטמעה
העברת פריטים
מגדירים את רשימת קובצי האימג' של הקונטיינרים שנדרשים לפתרון:
declare -a GDCS_REGISTRY_IMAGES=( "ghcr.io/open-webui/open-webui:${GDCS_OPEN_WEBUI_IMAGE_TAG}" ) export SERIALIZED_IMAGES=$(declare -p GDCS_REGISTRY_IMAGES)מזינים את קובצי האימג' הנדרשים של הקונטיינר ב-Artifact Registry באמצעות הסקריפט של הטמעת העזר:
${GDCS_IMPLEMENTATION_HOME}/seed_registry.sh
מרחב שמות
יוצרים את מרחב השמות:
kubectl create namespace "${GDCS_WORKLOAD_NAMESPACE}"מוסיפים את
imagePullSecret:kubectl create secret docker-registry "${GDCS_HARBOR_K8S_PULL_SECRET}" \ --dry-run=client \ --from-file=.dockerconfigjson=${GDCS_HARBOR_K8S_DOCKER_CONFIG}/config.json \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" \ --output=yaml | kubectl apply -f -יוצרים את
Secretבאמצעות מפתח החתימה של הסשן:kubectl create secret generic "${GDCS_OPEN_WEBUI_NAME}" \ --dry-run=client \ --from-literal=WEBUI_SECRET_KEY="${GDCS_OPEN_WEBUI_SECRET_KEY}" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" \ --output=yaml | kubectl apply -f -
אחסון
יוצרים את המניפסט של
PersistentVolumeClaimלנתוני Open WebUI:cat <<EOF > "${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-rwo-pvc.yaml" && echo "Successfully created." || echo "Failed to create!" apiVersion: v1 kind: PersistentVolumeClaim metadata: name: ${GDCS_OPEN_WEBUI_NAME}-rwo spec: accessModes: - ReadWriteOnce resources: requests: storage: ${GDCS_OPEN_WEBUI_PVC_SIZE} storageClassName: standard-rwo volumeMode: Filesystem EOFמחילים את המניפסט של
PersistentVolumeClaim:kubectl apply \ --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-rwo-pvc.yaml" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}"
ממשק אינטרנט
יוצרים את המניפסט ל-Open WebUI
ConfigMap,Deploymentו-Service. Open WebUI פועל במצב אופליין (OFFLINE_MODE), שומר עלConfigMapכמקור המידע המהימן (ENABLE_PERSISTENT_CONFIG=false) ומשבית את התכונות שדורשות גישה לאינטרנט:cat <<EOF > "${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-deployment.yaml" && echo "Successfully created." || echo "Failed to create!" apiVersion: v1 kind: ConfigMap metadata: name: ${GDCS_OPEN_WEBUI_NAME} data: DATA_DIR: "/app/backend/data" ENABLE_COMMUNITY_SHARING: "false" ENABLE_IMAGE_GENERATION: "false" ENABLE_OLLAMA_API: "${GDCS_OPEN_WEBUI_ENABLE_OLLAMA_API}" ENABLE_OPENAI_API: "${GDCS_OPEN_WEBUI_ENABLE_OPENAI_API}" ENABLE_PERSISTENT_CONFIG: "false" ENABLE_SIGNUP: "true" ENABLE_VERSION_UPDATE_CHECK: "false" ENABLE_WEB_SEARCH: "false" OFFLINE_MODE: "true" OLLAMA_BASE_URLS: "${GDCS_OPEN_WEBUI_OLLAMA_BASE_URLS}" OPENAI_API_BASE_URLS: "${GDCS_OPEN_WEBUI_OPENAI_API_BASE_URLS}" OPENAI_API_KEYS: "${GDCS_OPEN_WEBUI_OPENAI_API_KEYS}" PORT: "8080" --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: ${GDCS_OPEN_WEBUI_NAME} name: ${GDCS_OPEN_WEBUI_NAME} spec: replicas: 1 selector: matchLabels: app: ${GDCS_OPEN_WEBUI_NAME} strategy: type: Recreate template: metadata: labels: app: ${GDCS_OPEN_WEBUI_NAME} spec: containers: - envFrom: - configMapRef: name: ${GDCS_OPEN_WEBUI_NAME} - secretRef: name: ${GDCS_OPEN_WEBUI_NAME} image: ${GDCS_HARBOR_PROJECT_URI}/open-webui/open-webui:${GDCS_OPEN_WEBUI_IMAGE_TAG} imagePullPolicy: IfNotPresent livenessProbe: failureThreshold: 3 httpGet: path: /health port: 8080 periodSeconds: 30 timeoutSeconds: 10 name: open-webui ports: - containerPort: 8080 name: http readinessProbe: failureThreshold: 3 httpGet: path: /health port: 8080 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 5 resources: limits: cpu: "${GDCS_WORKLOAD_CPU_LIMIT}" memory: "${GDCS_WORKLOAD_MEMORY_LIMIT}" requests: cpu: "${GDCS_WORKLOAD_CPU_REQUEST}" memory: "${GDCS_WORKLOAD_MEMORY_REQUEST}" securityContext: allowPrivilegeEscalation: false startupProbe: failureThreshold: 60 httpGet: path: /health port: 8080 periodSeconds: 10 timeoutSeconds: 5 volumeMounts: - mountPath: /app/backend/data name: data imagePullSecrets: - name: ${GDCS_HARBOR_K8S_PULL_SECRET} volumes: - name: data persistentVolumeClaim: claimName: ${GDCS_OPEN_WEBUI_NAME}-rwo --- apiVersion: v1 kind: Service metadata: name: ${GDCS_OPEN_WEBUI_NAME} spec: ports: - name: http port: 80 protocol: TCP targetPort: 8080 selector: app: ${GDCS_OPEN_WEBUI_NAME} type: LoadBalancer EOFמחילים את המניפסט על
ConfigMap,Deploymentו-Serviceשל Open WebUI:kubectl apply \ --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-deployment.yaml" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}"יוצרים את המניפסט של Open WebUI
NetworkPolicy:cat <<EOF > "${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-netpol.yaml" && echo "Successfully created." || echo "Failed to create!" apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-${GDCS_OPEN_WEBUI_NAME}-ingress namespace: ${GDCS_WORKLOAD_NAMESPACE} spec: ingress: - from: - ipBlock: cidr: 0.0.0.0/0 - namespaceSelector: {} ports: - port: 8080 protocol: TCP podSelector: {} policyTypes: - Ingress EOFמחילים את המניפסט של Open WebUI
NetworkPolicy:kubectl apply \ --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-netpol.yaml"
מחכים שהסטטוס של
Podיהיה Running:watch --color --interval 5 --no-title \ "kubectl get pods --selector=app=${GDCS_OPEN_WEBUI_NAME} \ --namespace=${GDCS_WORKLOAD_NAMESPACE} | GREP_COLORS='mt=01;92' grep -E --color=always -e '^' -e 'Running'"מחכים שהסטטוס של
Deploymentיהיה "מוכן":watch --color --interval 5 --no-title \ "kubectl get deployment/${GDCS_OPEN_WEBUI_NAME} \ --namespace=${GDCS_WORKLOAD_NAMESPACE} | GREP_COLORS='mt=01;92' grep -E --color=always -e '^' -e '1/1 1 1'"אופציונלי: כדי לעקוב אחרי התקדמות ההפעלה, אפשר להעביר בסטרימינג את יומני הקונטיינר:
kubectl logs deployment/${GDCS_OPEN_WEBUI_NAME} \ --follow \ --namespace="${GDCS_WORKLOAD_NAMESPACE}"
אימות
מאחזרים את כתובת ה-IP של LoadBalancer בשביל Open WebUI:
export OPEN_WEBUI_LB_IP=$(kubectl get service/${GDCS_OPEN_WEBUI_NAME} --namespace="${GDCS_WORKLOAD_NAMESPACE}" --output=jsonpath='{.status.loadBalancer.ingress[*].ip}') echo "OPEN_WEBUI_LB_IP=${OPEN_WEBUI_LB_IP}"בודקים את נקודות הקצה של התקינות והגרסה באמצעות מאזן העומסים:
curl --show-error --silent http://${OPEN_WEBUI_LB_IP}/health && echo curl --show-error --silent http://${OPEN_WEBUI_LB_IP}/api/version && echoהפלט אמור להיראות כך:
{"status":true} {"version":"0.11.3"}בודקים את נקודות הקצה של התקינות והגרסה באמצעות העברה ליציאה אחרת:
kubectl port-forward service/${GDCS_OPEN_WEBUI_NAME} \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" 8080:80 & PF_PID=$! sleep 2 curl --show-error --silent http://127.0.0.1:8080/health && echo curl --show-error --silent http://127.0.0.1:8080/api/version && echo kill ${PF_PID}מוודאים ש-Open WebUI יכול להגיע לקצוות העורפיים של ההסקה:
kubectl exec deployment/${GDCS_OPEN_WEBUI_NAME} \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" \ -- sh -c 'for url in $(echo "${OLLAMA_BASE_URLS};${OPENAI_API_BASE_URLS}" | tr ";" " "); do case "${url}" in *"/v1") echo "${url}/models"; curl --max-time 10 --show-error --silent "${url}/models" | head -c 300; echo;; *) echo "${url}/api/tags"; curl --max-time 10 --show-error --silent "${url}/api/tags" | head -c 300; echo;; esac; done'פותחים את
http://${OPEN_WEBUI_LB_IP}בדפדפן ויוצרים את החשבון הראשון. החשבון הראשון שנוצר הופך לאדמין. בוחרים מודל בתצוגת הצ'אט ושולחים הנחיה.
תפעול
הוספה או שינוי של קצוות עורפיים להסקת מסקנות
מעדכנים את
GDCS_OPEN_WEBUI_OLLAMA_BASE_URLSאו אתGDCS_OPEN_WEBUI_OPENAI_API_BASE_URLSב-env.d/open-webui.sh, מפעילים את קובץ הסביבה ומריצים שוב את שלב המניפסט בממשק האינטרנט. אחר כך מפעילים את המניפסט ומפעילים מחדש את Open WebUI, שקורא אתConfigMapבכל הפעלה כי ההגדרה הקבועה מושבתת:kubectl apply \ --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-deployment.yaml" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" kubectl rollout restart deployment/${GDCS_OPEN_WEBUI_NAME} \ --namespace="${GDCS_WORKLOAD_NAMESPACE}"
שדרוג
מגדירים את
GDCS_OPEN_WEBUI_IMAGE_TAGהחדש בקובץ הסביבה, מגדירים אותו כמקור, מזינים את התמונה החדשה ומגלגלים אתDeployment:source ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.sh declare -a GDCS_REGISTRY_IMAGES=( "ghcr.io/open-webui/open-webui:${GDCS_OPEN_WEBUI_IMAGE_TAG}" ) export SERIALIZED_IMAGES=$(declare -p GDCS_REGISTRY_IMAGES) ${GDCS_IMPLEMENTATION_HOME}/seed_registry.sh kubectl set image deployment/${GDCS_OPEN_WEBUI_NAME} \ open-webui="${GDCS_HARBOR_PROJECT_URI}/open-webui/open-webui:${GDCS_OPEN_WEBUI_IMAGE_TAG}" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}"
גיבוי
יוצרים
VolumeSnapshotשל נפח הנתונים (האפשרותstandardVolumeSnapshotClassזמינה באשכולות רגילים של GDC):cat <<EOF | kubectl apply --namespace="${GDCS_WORKLOAD_NAMESPACE}" -f - apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot metadata: name: ${GDCS_OPEN_WEBUI_NAME}-$(date +%Y%m%d-%H%M) spec: source: persistentVolumeClaimName: ${GDCS_OPEN_WEBUI_NAME}-rwo volumeSnapshotClassName: standard EOF
פתרון בעיות
| תיאור הבעיה | סיבה אפשרית | פעולה |
|---|---|---|
| אין מודל שמוצע בתצוגת הצ'אט | הכתובת של הקצה העורפי Service שגויה, הקצה העורפי לא מוכן או שהקצה העורפי NetworkPolicy לא מאשר את מרחב השמות של Open WebUI |
מריצים את שלב האימות של הקצה העורפי ב-Validation, בודקים את הקצה העורפי Deployment ואת ה-NetworkPolicy שלו |
Pod מופעל מחדש במהלך ההפעלה |
הפעלה ראשונה איטית ב-PersistentVolume |
צריך לבדוק את kubectl describe pod; ב-startupProbe יש 10 דקות |
| העלאת מסמכים (RAG) נכשלת | מצב אופליין חוסם את ההורדה של מודל ההטמעה שמוגדר כברירת מחדל | צריך להגדיר את RAG_EMBEDDING_ENGINE=ollama עם מודל הטמעה שמופעל על ידי Ollama, או להשאיר את RAG מושבת |
כתובת ה-IP של LoadBalancer נשארת <pending> או שאי אפשר להגיע אליה |
מאזן העומסים לא זמין בסביבה, או שחסר ProjectNetworkPolicy |
משתמשים בשלב האימות של העברה ליציאה אחרת. מבקשים מהאדמין של Project NetworkPolicy את ProjectNetworkPolicy |
הסשן לא תקין אחרי הפעלה מחדש של Pod |
בוצע שינוי של WEBUI_SECRET_KEY |
שמירה על היציבות של Secret; שחזור מ-secrets/open_webui_secret_key |
הסרת המשאבים
מוחקים את המשאבים של Open WebUI:
kubectl delete namespace "${GDCS_WORKLOAD_NAMESPACE}"מוחקים את הקבצים המקומיים:
rm -rf ${HOME}/gdcag-solutions/open-weight-models/open-webui
חומרים נוספים
- מסמכי התיעוד של Open WebUI
- הפניית משתנה סביבה של Open WebUI
- תאימות של Ollama ל-OpenAI
- שרת תואם ל-OpenAI ב-vLLM