Guida dell'utente di Open WebUI per i modelli con pesi aperti su GDC con air gap

Questo documento fornisce istruzioni passo passo per il deployment di Open WebUI, un'interfaccia di chat self-hosted basata su browser, in ambienti con air gap di Google Distributed Cloud (GDC). Open WebUI si connette ai backend di inferenza di cui è stato eseguito il deployment con le guide dell'utente di Ollama e vLLM tramite le relative API compatibili con Ollama e OpenAI, in modo che gli sviluppatori e gli utenti finali possano interagire con i modelli con pesi aperti senza scrivere chiamate API.

Architettura

Open WebUI viene eseguito come Deployment a replica singola nel proprio spazio dei nomi del cluster standard. Il suo stato (account utente, chat, impostazioni) viene archiviato su un ReadWriteOnce PersistentVolumeClaim. Un ConfigMap contiene le impostazioni di connessione per i backend di inferenza, che vengono raggiunti tramite i relativi nomi Service nel cluster, mentre un Secret contiene la chiave di firma della sessione. Un Service Kubernetes di tipo LoadBalancer espone l'interfaccia web e gli oggetti NetworkPolicy/ProjectNetworkPolicy proteggono l'accesso.

Prima di iniziare

Assicurati che sia stato eseguito il deployment di almeno un backend di inferenza con la guida dell'utente dei modelli Gemma con Ollama, la guida dell'utente dei modelli Gemma con vLLM o la guida dell'utente dei modelli Mistral con vLLM e di avere accesso alla directory ${HOME}/gdcag-solutions dell'implementazione di riferimento corrispondente sulla workstation.

Identity and Access Management

Assicurati che gli account, i ruoli e le autorizzazioni IAM necessari siano configurati correttamente.

Ruoli Utente GDC nell'organizzazione:

  • (Facoltativo) Amministratore cluster standard (visualizza i cluster dalla console)

Ruoli GDC User sul progetto:

  • Standard Cluster Admin (standard-cluster-admin)

Ruolo GDC User nel cluster standard:

  • StandardClusterRoleBinding al cluster-admin StandardClusterRole (creato da un amministratore di progetto IAM, consulta la sezione Identity and Access Management dell'implementazione di riferimento)

Workstation

Esegui tutti i comandi di questa guida da una workstation con gli strumenti e la connettività necessari per l'ambiente e internet, se non diversamente indicato in un passaggio.

  1. Crea la struttura di directory della guida dell'utente:

    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 chiave di firma della sessione:

    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 il file di configurazione dell'ambiente della guida dell'utente:

    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
    

    Sostituisci quanto segue:

    • OPEN_WEBUI_ENABLE_OLLAMA_API: true se hai eseguito il deployment di un backend Ollama, altrimenti false.
    • OPEN_WEBUI_OLLAMA_BASE_URLS: gli URL in-cluster dei servizi Ollama, separati da ;, ad esempio http://gemma4-e4b.ollama-gemma.svc.cluster.local. Lascia il campo vuoto se non hai eseguito il deployment di un backend Ollama.
    • OPEN_WEBUI_ENABLE_OPENAI_API: true se hai eseguito il deployment di un backend vLLM, altrimenti false.
    • OPEN_WEBUI_OPENAI_API_BASE_URLS: gli URL in-cluster dei servizi vLLM con il percorso /v1, separati da ;, ad esempio 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. Lascia il campo vuoto se non hai eseguito il deployment di un backend vLLM.
  4. Crea il file del caricatore dell'ambiente della guida dell'utente:

    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
    

    Sostituisci quanto segue:

    • IMPLEMENTATION: ollama o vllm, l'implementazione di riferimento che hai completato su questa workstation.
  5. Modifica e rivedi il file di ambiente con l'editor che preferisci:

    ${EDITOR:-vi} ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.d/open-webui.sh
    
  6. Esegui il sourcing del file di ambiente:

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

    L'output è simile al seguente:

    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
    

Cluster

  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. Esegui l'autenticazione nell'ambiente GDC:

    gdcloud auth login
    
  3. Recupera le credenziali del cluster:

    gdcloud clusters get-credentials "${GDC_STANDARD_CLUSTER_NAME}" \
    --project="${GDC_PROJECT}" \
    --standard \
    --zone="${GDC_ZONE}"
    
  4. Verifica che i backend di inferenza Service a cui viene fatto riferimento nel file dell'ambiente esistano:

    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
    

Implementazione

Migrazione degli artefatti

  1. Definisci l'elenco delle immagini container richieste per la soluzione:

    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. Esegui il seeding delle immagini container richieste in Artifact Registry con lo script dell'implementazione di riferimento:

    ${GDCS_IMPLEMENTATION_HOME}/seed_registry.sh
    

Spazio dei nomi

  1. Crea lo spazio dei nomi:

    kubectl create namespace "${GDCS_WORKLOAD_NAMESPACE}"
    
  2. Aggiungi 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 il Secret con la chiave di firma della sessione:

    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 -
    

Archiviazione

  1. Crea il manifest per il PersistentVolumeClaim per i dati di 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. Applica il manifest per PersistentVolumeClaim:

    kubectl apply \
    --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-rwo-pvc.yaml" \
    --namespace="${GDCS_WORKLOAD_NAMESPACE}"
    

Interfaccia web

  1. Crea il manifest per Open WebUI ConfigMap, Deployment e Service. Open WebUI viene eseguita in modalità offline (OFFLINE_MODE), mantiene questa ConfigMap come fonte di attendibilità (ENABLE_PERSISTENT_CONFIG=false) e disattiva le funzionalità che richiedono l'accesso 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. Applica il manifest per Open WebUI ConfigMap, Deployment e Service:

    kubectl apply \
    --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-deployment.yaml" \
    --namespace="${GDCS_WORKLOAD_NAMESPACE}"
    
  3. Crea il manifest per 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
    
  4. Applica il manifest per Open WebUI NetworkPolicy:

    kubectl apply \
    --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-netpol.yaml"
    
  1. Attendi che Pod sia in esecuzione:

    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. Attendi che Deployment sia pronto:

    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. (Facoltativo) Trasmetti in streaming i log del container per monitorare l'avanzamento dell'avvio:

    kubectl logs deployment/${GDCS_OPEN_WEBUI_NAME} \
    --follow \
    --namespace="${GDCS_WORKLOAD_NAMESPACE}"
    

Convalida

  1. Recupera l'indirizzo IP del bilanciatore del carico per 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. Controlla gli endpoint di integrità e versione utilizzando il bilanciatore del carico:

    curl --show-error --silent http://${OPEN_WEBUI_LB_IP}/health && echo
    
    curl --show-error --silent http://${OPEN_WEBUI_LB_IP}/api/version && echo
    

    L'output è simile al seguente:

    {"status":true}
    {"version":"0.11.3"}
    
  3. Controlla gli endpoint di integrità e versione utilizzando il port forwarding:

    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 che Open WebUI possa raggiungere i backend di inferenza:

    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. Apri http://${OPEN_WEBUI_LB_IP} in un browser e crea il primo account; il primo account creato diventa l'amministratore. Seleziona un modello nella visualizzazione chat e invia un prompt.

Operazioni

Aggiungi o modifica i backend di inferenza

  1. Aggiorna GDCS_OPEN_WEBUI_OLLAMA_BASE_URLS o GDCS_OPEN_WEBUI_OPENAI_API_BASE_URLS in env.d/open-webui.sh, esegui il sourcing del file di ambiente ed esegui di nuovo il passaggio del manifest nell'interfaccia web. Quindi applica il manifest e riavvia Open WebUI, che legge ConfigMap a ogni avvio perché la configurazione persistente è disabilitata:

    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}"
    

Esegui l'upgrade

  1. Imposta il nuovo GDCS_OPEN_WEBUI_IMAGE_TAG nel file dell'ambiente, esegui il sourcing, esegui il seeding della nuova immagine e l'implementazione di 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}"
    

Backup

  1. Crea un VolumeSnapshot del volume di dati (il standard VolumeSnapshotClass è disponibile sui cluster standard 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
    

Risoluzione dei problemi

Sintomo Causa probabile Azione
Nessun modello offerto nella visualizzazione della chat Indirizzo del backend Service errato, backend non pronto o backend NetworkPolicy non ammette lo spazio dei nomi Open WebUI Esegui il passaggio di verifica del backend in Convalida; controlla il backend Deployment e il relativo NetworkPolicy
Pod riavvii durante l'avvio Primo avvio lento su PersistentVolume Controlla kubectl describe pod; startupProbe consente 10 minuti
Caricamento di documenti (RAG) non riuscito La modalità offline blocca il download del modello di embedding predefinito Configura RAG_EMBEDDING_ENGINE=ollama con un modello di embedding fornito da Ollama o mantieni RAG disattivato
L'IP LoadBalancer rimane <pending> o non è raggiungibile Bilanciatore del carico non disponibile nell'ambiente o ProjectNetworkPolicy mancante Utilizza il passaggio di convalida del port forwarding; chiedi all'amministratore Project NetworkPolicy un ProjectNetworkPolicy
Sessione non valida dopo un riavvio di Pod WEBUI_SECRET_KEY cambiato Mantieni stabile Secret; ripristinalo da secrets/open_webui_secret_key

Esegui la pulizia

  1. Elimina le risorse Open WebUI:

    kubectl delete namespace "${GDCS_WORKLOAD_NAMESPACE}"
    
  2. Elimina i file locali:

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

Materiali aggiuntivi