En este documento, se proporcionan instrucciones paso a paso para implementar Open WebUI, una interfaz de chat basada en el navegador y alojada de forma autónoma, en entornos aislados de Google Distributed Cloud (GDC). Open WebUI se conecta a los backends de inferencia implementados con las guías del usuario de Ollama y vLLM a través de sus APIs compatibles con Ollama y OpenAI, para que los desarrolladores y los usuarios finales puedan interactuar con los modelos de pesos abiertos sin escribir llamadas a la API.
Arquitectura
Open WebUI se ejecuta como un Deployment de réplica única en su propio espacio de nombres del
clúster estándar. Su estado (cuentas de usuario, chats y configuración) se almacena en un ReadWriteOnce PersistentVolumeClaim. Un ConfigMap contiene la configuración de conexión
para los backends de inferencia, a los que se accede a través de sus nombres de Service
en el clúster, y un Secret contiene la clave de firma de la sesión. Un Service de Kubernetes de tipo LoadBalancer expone la interfaz web, y los objetos NetworkPolicy/ProjectNetworkPolicy protegen el acceso a ella.
Antes de comenzar
Asegúrate de que se haya implementado al menos un backend de inferencia con la
guía del usuario de modelos de Gemma con Ollama, la
guía del usuario de modelos de Gemma con vLLM o la guía del usuario de modelos de Mistral con vLLM, y de que
tengas acceso al directorio ${HOME}/gdcag-solutions de la implementación de referencia correspondiente
en la estación de trabajo.
Identity and Access Management
Asegúrate de que las cuentas, los roles y los permisos de IAM necesarios estén configurados de forma correcta.
Roles de usuario de GDC en la organización:
- Opcional: Administrador de clústeres estándar (visualiza clústeres desde la consola)
Roles de usuario de GDC en el proyecto:
- Administrador de clústeres estándar (
standard-cluster-admin)
Rol de usuario de GDC en el clúster estándar:
StandardClusterRoleBindingalStandardClusterRolecluster-admin(creado por un administrador de IAM del proyecto, consulta la sección Identity and Access Management de la implementación de referencia)
Estación de trabajo
Ejecuta todos los comandos de esta guía desde una estación de trabajo con las herramientas y la conectividad necesarias para el entorno y la Internet, a menos que un paso indique lo contrario.
Crea la estructura de directorios de la guía del usuario:
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/secretsCrea la clave de firma de la sesión:
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_keyCrea el archivo de configuración del entorno de la guía del usuario:
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" EOFReemplaza lo siguiente:
OPEN_WEBUI_ENABLE_OLLAMA_API:truesi implementaste un backend de Ollama; de lo contrario,false.OPEN_WEBUI_OLLAMA_BASE_URLS: Son las URLs en el clúster de los servicios de Ollama, separadas por;, por ejemplo,http://gemma4-e4b.ollama-gemma.svc.cluster.local. Déjalo vacío si no implementaste ningún backend de Ollama.OPEN_WEBUI_ENABLE_OPENAI_API:truesi implementaste un backend de vLLM; de lo contrario,false.OPEN_WEBUI_OPENAI_API_BASE_URLS: Son las URLs en el clúster de los Services de vLLM con la ruta de acceso/v1, separadas por;, por ejemplo,http://mistralai-mistral-7b-instruct-v0-2.vllm-mistral.svc.cluster.local/v1ohttp://google-gemma-4-e2b-it.vllm-gemma.svc.cluster.local/v1. Déjalo vacío si no implementaste ningún backend de vLLM.
Crea el archivo de carga del entorno de la guía del usuario:
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" EOFReemplaza lo siguiente:
IMPLEMENTATION:ollamaovllm, la implementación de referencia que completaste en esta estación de trabajo.
Edita y revisa el archivo de entorno con tu editor preferido:
${EDITOR:-vi} ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.d/open-webui.shCarga el archivo de entorno:
source ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.shEl resultado es similar a lo siguiente:
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
Clúster
Configura
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}"Autentícate en el entorno de GDC:
gdcloud auth loginRecupera las credenciales del clúster:
gdcloud clusters get-credentials "${GDC_STANDARD_CLUSTER_NAME}" \ --project="${GDC_PROJECT}" \ --standard \ --zone="${GDC_ZONE}"Verifica que existan los backends de inferencia
Servicea los que se hace referencia en el archivo de entorno: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
Implementación
Migración de artefactos
Define la lista de imágenes de contenedor necesarias para la solución:
declare -a GDCS_REGISTRY_IMAGES=( "ghcr.io/open-webui/open-webui:${GDCS_OPEN_WEBUI_IMAGE_TAG}" ) export SERIALIZED_IMAGES=$(declare -p GDCS_REGISTRY_IMAGES)Propaga las imágenes de contenedor necesarias en Artifact Registry con la secuencia de comandos de la implementación de referencia:
${GDCS_IMPLEMENTATION_HOME}/seed_registry.sh
Espacio de nombres
Crea el espacio de nombres:
kubectl create namespace "${GDCS_WORKLOAD_NAMESPACE}"Agrega el
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 -Crea el
Secretcon la clave de firma de la sesión: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 -
Almacenamiento
Crea el manifiesto para el
PersistentVolumeClaimde los datos de 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 EOFAplica el manifiesto para el
PersistentVolumeClaim:kubectl apply \ --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-rwo-pvc.yaml" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}"
Interfaz web
Crea el manifiesto para el
ConfigMap,DeploymentyServicede Open WebUI. Open WebUI se ejecuta en modo sin conexión (OFFLINE_MODE), mantiene esteConfigMapcomo la fuente de información (ENABLE_PERSISTENT_CONFIG=false) y inhabilita las funciones que necesitan acceso a Internet: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 EOFAplica el manifiesto para el
ConfigMap, elDeploymenty elServicede Open WebUI:kubectl apply \ --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-deployment.yaml" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}"Crea el manifiesto para el
NetworkPolicyde Open WebUI: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 EOFAplica el manifiesto para la WebUI de Open
NetworkPolicy:kubectl apply \ --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-netpol.yaml"
Espera a que el
Podesté en ejecución: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'"Espera a que el
Deploymentesté listo: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'"Opcional: Transmite los registros del contenedor para supervisar el progreso del inicio:
kubectl logs deployment/${GDCS_OPEN_WEBUI_NAME} \ --follow \ --namespace="${GDCS_WORKLOAD_NAMESPACE}"
Validación
Recupera la dirección IP del LoadBalancer para 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}"Verifica los extremos de estado y versión con el balanceador de cargas:
curl --show-error --silent http://${OPEN_WEBUI_LB_IP}/health && echo curl --show-error --silent http://${OPEN_WEBUI_LB_IP}/api/version && echoEl resultado es similar a lo siguiente:
{"status":true} {"version":"0.11.3"}Verifica los extremos de estado y versión con la redirección de puertos:
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}Verifica que Open WebUI pueda acceder a los backends de inferencia:
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'Abre
http://${OPEN_WEBUI_LB_IP}en un navegador y crea la primera cuenta. La primera cuenta que se cree se convertirá en la de administrador. Selecciona un modelo en la vista de chat y envía una instrucción.
Operaciones
Agrega o cambia backends de inferencia
Actualiza
GDCS_OPEN_WEBUI_OLLAMA_BASE_URLSoGDCS_OPEN_WEBUI_OPENAI_API_BASE_URLSenenv.d/open-webui.sh, genera el archivo de entorno y vuelve a ejecutar el paso del manifiesto en la interfaz web. Luego, aplica el manifiesto y reinicia Open WebUI, que lee elConfigMapen cada inicio porque la configuración persistente está inhabilitada: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}"
Actualizar
Establece el nuevo
GDCS_OPEN_WEBUI_IMAGE_TAGen el archivo de entorno, genera la fuente, propaga la imagen nueva y ejecuta elDeployment: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}"
Copia de seguridad
Crea un
VolumeSnapshotdel volumen de datos (elstandardVolumeSnapshotClassestá disponible en los clústeres estándar de 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
Soluciona problemas
| Síntoma | Causa probable | Acción |
|---|---|---|
| No se ofrece ningún modelo en la vista de chat | La dirección del backend Service es incorrecta, el backend no está listo o el backend NetworkPolicy no admite el espacio de nombres de Open WebUI |
Ejecuta el paso de verificación del backend en Validación; verifica el backend Deployment y su NetworkPolicy |
Pod reinicios durante el inicio |
Inicio lento por primera vez en el PersistentVolume |
Revisa kubectl describe pod; el startupProbe permite 10 minutos |
| Falla la carga de documentos (RAG) | El modo sin conexión bloquea la descarga del modelo de embeddings predeterminado | Configura RAG_EMBEDDING_ENGINE=ollama con un modelo de embeddings entregado por Ollama o mantén la RAG inhabilitada |
La IP del LoadBalancer permanece en <pending> o no se puede acceder a ella |
El balanceador de cargas no está disponible en el entorno o falta ProjectNetworkPolicy |
Usa el paso de validación de redirección de puertos y pídele al administrador de NetworkPolicy del proyecto un ProjectNetworkPolicy |
La sesión no es válida después de un reinicio de Pod |
Se modificó WEBUI_SECRET_KEY |
Mantén estable el Secret y restablécelo desde secrets/open_webui_secret_key |
Realiza una limpieza
Borra los recursos de Open WebUI:
kubectl delete namespace "${GDCS_WORKLOAD_NAMESPACE}"Borra los archivos locales:
rm -rf ${HOME}/gdcag-solutions/open-weight-models/open-webui
Materiales adicionales
- Documentación de Open WebUI
- Referencia de la variable de entorno de Open WebUI
- Compatibilidad de Ollama con OpenAI
- Servidor de vLLM compatible con OpenAI