Guía del usuario de Open WebUI para modelos de pesos abiertos en GDC aislado

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:

  • StandardClusterRoleBinding al StandardClusterRole cluster-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.

  1. 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/secrets
    
  2. Crea 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_key
    
  3. Crea 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"
    
    EOF
    

    Reemplaza lo siguiente:

    • OPEN_WEBUI_ENABLE_OLLAMA_API: true si 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: true si 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/v1 o http://google-gemma-4-e2b-it.vllm-gemma.svc.cluster.local/v1. Déjalo vacío si no implementaste ningún backend de vLLM.
  4. 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"
    EOF
    

    Reemplaza lo siguiente:

    • IMPLEMENTATION: ollama o vllm, la implementación de referencia que completaste en esta estación de trabajo.
  5. 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.sh
    
  6. Carga el archivo de entorno:

    source ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.sh
    

    El 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

  1. 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}"
    
  2. Autentícate en el entorno de GDC:

    gdcloud auth login
    
  3. Recupera las credenciales del clúster:

    gdcloud clusters get-credentials "${GDC_STANDARD_CLUSTER_NAME}" \
    --project="${GDC_PROJECT}" \
    --standard \
    --zone="${GDC_ZONE}"
    
  4. Verifica que existan los backends de inferencia Service a 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

  1. 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)
    
  2. 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

  1. Crea el espacio de nombres:

    kubectl create namespace "${GDCS_WORKLOAD_NAMESPACE}"
    
  2. 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 -
    
  3. Crea el Secret con 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

  1. Crea el manifiesto para el PersistentVolumeClaim de 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
    EOF
    
  2. Aplica 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

  1. Crea el manifiesto para el ConfigMap, Deployment y Service de Open WebUI. Open WebUI se ejecuta en modo sin conexión (OFFLINE_MODE), mantiene este ConfigMap como 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
    EOF
    
  2. Aplica el manifiesto para el ConfigMap, el Deployment y el Service de Open WebUI:

    kubectl apply \
    --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-deployment.yaml" \
    --namespace="${GDCS_WORKLOAD_NAMESPACE}"
    
  3. Crea el manifiesto para el NetworkPolicy de 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
    EOF
    
  4. Aplica el manifiesto para la WebUI de Open NetworkPolicy:

    kubectl apply \
    --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-netpol.yaml"
    
  1. Espera a que el Pod esté 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'"
    
  2. Espera a que el Deployment esté 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'"
    
  3. 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

  1. 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}"
    
  2. 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 && echo
    

    El resultado es similar a lo siguiente:

    {"status":true}
    {"version":"0.11.3"}
    
  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}
    
  4. 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'
    
  5. 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

  1. Actualiza GDCS_OPEN_WEBUI_OLLAMA_BASE_URLS o GDCS_OPEN_WEBUI_OPENAI_API_BASE_URLS en env.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 el ConfigMap en 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

  1. Establece el nuevo GDCS_OPEN_WEBUI_IMAGE_TAG en el archivo de entorno, genera la fuente, propaga la imagen nueva y ejecuta el 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}"
    

Copia de seguridad

  1. Crea un VolumeSnapshot del volumen de datos (el standard VolumeSnapshotClass está 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

  1. Borra los recursos de Open WebUI:

    kubectl delete namespace "${GDCS_WORKLOAD_NAMESPACE}"
    
  2. Borra los archivos locales:

    rm -rf ${HOME}/gdcag-solutions/open-weight-models/open-webui
    

Materiales adicionales