From 9d2b18fac5293e5d0cd33c45c6eb8e31fc167ec4 Mon Sep 17 00:00:00 2001 From: Nikita Navalikhin Date: Thu, 10 Apr 2025 11:17:44 +0100 Subject: [PATCH] Add Helm charts for Vespa multinode HA deployment --- examples/operations/helm/Chart.lock | 9 + examples/operations/helm/Chart.yaml | 11 ++ examples/operations/helm/README.md | 170 ++++++++++++++++++ .../operations/helm/charts/config/Chart.yaml | 3 + .../charts/config/templates/configmap.yml | 15 ++ .../charts/config/templates/configserver.yml | 84 +++++++++ .../helm/charts/config/templates/headless.yml | 12 ++ .../operations/helm/charts/config/values.yaml | 4 + .../helm/charts/services/Chart.yaml | 3 + .../helm/charts/services/templates/admin.yml | 50 ++++++ .../charts/services/templates/content.yml | 69 +++++++ .../services/templates/feed-container.yml | 34 ++++ .../services/templates/query-container.yml | 34 ++++ .../services/templates/service-feed.yml | 17 ++ .../services/templates/service-query.yml | 17 ++ .../helm/charts/services/values.yaml | 3 + examples/operations/helm/conf/hosts.xml | 33 ++++ .../operations/helm/conf/schemas/music.sd | 25 +++ examples/operations/helm/conf/services.xml | 55 ++++++ examples/operations/helm/hosts.xml | 33 ++++ examples/operations/helm/values.yaml | 4 + 21 files changed, 685 insertions(+) create mode 100644 examples/operations/helm/Chart.lock create mode 100644 examples/operations/helm/Chart.yaml create mode 100644 examples/operations/helm/README.md create mode 100644 examples/operations/helm/charts/config/Chart.yaml create mode 100644 examples/operations/helm/charts/config/templates/configmap.yml create mode 100644 examples/operations/helm/charts/config/templates/configserver.yml create mode 100644 examples/operations/helm/charts/config/templates/headless.yml create mode 100644 examples/operations/helm/charts/config/values.yaml create mode 100644 examples/operations/helm/charts/services/Chart.yaml create mode 100644 examples/operations/helm/charts/services/templates/admin.yml create mode 100644 examples/operations/helm/charts/services/templates/content.yml create mode 100644 examples/operations/helm/charts/services/templates/feed-container.yml create mode 100644 examples/operations/helm/charts/services/templates/query-container.yml create mode 100644 examples/operations/helm/charts/services/templates/service-feed.yml create mode 100644 examples/operations/helm/charts/services/templates/service-query.yml create mode 100644 examples/operations/helm/charts/services/values.yaml create mode 100644 examples/operations/helm/conf/hosts.xml create mode 100644 examples/operations/helm/conf/schemas/music.sd create mode 100644 examples/operations/helm/conf/services.xml create mode 100644 examples/operations/helm/hosts.xml create mode 100644 examples/operations/helm/values.yaml diff --git a/examples/operations/helm/Chart.lock b/examples/operations/helm/Chart.lock new file mode 100644 index 000000000..6a9cb9260 --- /dev/null +++ b/examples/operations/helm/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: config + repository: "" + version: 0.1.0 +- name: services + repository: "" + version: 0.1.0 +digest: sha256:7550b9a2ad831bd383e6bf22c51013efd466f8d287df676433766fd59e9aac29 +generated: "2025-04-10T10:54:29.929533+01:00" diff --git a/examples/operations/helm/Chart.yaml b/examples/operations/helm/Chart.yaml new file mode 100644 index 000000000..37aedca23 --- /dev/null +++ b/examples/operations/helm/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: vespa +version: 0.1.0 +dependencies: + - name: config + alias: config + version: 0.1.0 + - name: services + alias: services + version: 0.1.0 + condition: services.enabled diff --git a/examples/operations/helm/README.md b/examples/operations/helm/README.md new file mode 100644 index 000000000..ec79525a0 --- /dev/null +++ b/examples/operations/helm/README.md @@ -0,0 +1,170 @@ + + + + + + #Vespa + + +# Multinode-HA using Helm +This guide uses the multinode-HA configuration and principles and deploys a Vespa application using Kubernetes and Helm. + +## Overview +This deployment is designed for high availability and uses the Helm chart consisting of two primary modules: +1. `config` - contains and deploys **vespa-configserver**, which must be running successfully before starting other components. +2. `services` - starts **admin**, **content**, **feed**, and **query** clusters, depending on a successful `configserver` startup. + +A key mechanism ensures the correct start order of the modules: `initContainers` in `services` waits for the `configserver` to become ready by repeatedly checking its health. Only after the `configserver` successfully initializes, the `services` module will proceed to start. Here’s the command used in the `initContainers`: + +```bash +until curl -f http://vespa-configserver-0.vespa-internal.vespa.svc.cluster.local:19071/state/v1/health; do + echo "Waiting for Vespa ConfigServer to be ready in namespace $CONFIGSERVER_NAMESPACE..."; + sleep 5; +done +``` + +--- + +## Prerequisites +Make sure the following tools are installed and configured: +* [Helm](https://helm.sh/docs/intro/install/) +* A Kubernetes cluster - either local or hosted (e.g., Azure AKS, AWS EKS, etc.) + +--- + +## Installation +Clone the repository: +```bash +git clone --depth 1 https://github.com/vespa-engine/sample-apps.git +cd sample-apps/examples/operations/multinode-HA/helm +``` + +Prepare your `values.yaml` with the desired configuration. Here's an example: +```yaml +config: + serverReplicas: 3 +services: + content: + replicas: 2 + storage: 25Gi +``` + +Deploy Vespa using Helm: +```bash +helm dependency update helm +helm upgrade --install vespa . -n vespa --create-namespace -f values.yaml +``` + +This will create the namespace `vespa` and deploy all components of the application. + +--- + +## Deploy the application package + +``` +kubectl port-forward -n vespa pod/vespa-configserver-0 19071 +``` + +``` +(cd conf && zip -r - .) | \ + curl --header Content-Type:application/zip \ + --data-binary @- \ + http://localhost:19071/application/v2/tenant/default/prepareandactivate +``` + +--- + +## Module Details + +### Config Module +The `config` module contains the **vespa-configserver**, which is essential for Vespa's operation. This module deploys a StatefulSet with `serverReplicas` to ensure high availability. + +#### ConfigServer Health Check +The `configserver` health is verified by an HTTP curl to its `/state/v1/health` endpoint. The `services` module will not start until all `configserver` replicas are running and reachable. + +Here’s an example of the expected `configserver` health response: +```json +{ + "time" : 1678268549957, + "status" : { + "code" : "up" + }, + "metrics" : { + "snapshot" : { + "from" : 1.678268489718E9, + "to" : 1.678268549718E9 + } + } +} +``` + +### Services Module +The `services` module contains the following components: +- **Admin**: Handles Vespa cluster administration. +- **Feed**: Handles document feeding. +- **Query**: Handles document queries. +- **Content**: Stores indexed data. + +This module is configured to depend on the `config` module startup. The `initContainers` logic ensures that no pods in the `services` module are started until the `configserver` reaches a stable, healthy state. + +Below is the typical `initContainers` logic defined for `services`: +```bash +until curl -f http://vespa-configserver-0.vespa-internal.vespa.svc.cluster.local:19071/state/v1/health; do + echo "Waiting for Vespa ConfigServer to be ready in namespace $CONFIGSERVER_NAMESPACE..."; + sleep 5; +done +``` + +--- + +## Verification +Once the installation completes, you can test the Vespa application by: +1. Feeding a document: + ```bash + curl -X POST http://vespa-query-container-0.vespa.svc.cluster.local/document/v1/my-space/my-doc \ + -d '{"id": "id:my-space:my-doc::1", "fields": {"field1": "value1"}}' + ``` + +2. Querying documents: + ```bash + curl "http://vespa-query-container-0.vespa.svc.cluster.local/search/?query=my-query" + ``` + + or + + ``` + kubectl -n vespa port-forward svc/vespa-query 8080 + ``` + ``` + curl --data-urlencode 'yql=select * from sources * where true' \ + http://localhost:8080/search/ + ``` + +--- + +## Customization and Scaling +Values such as `config.serverReplicas`, `services.content.replicas`, and `services.content.storage` can be adjusted in `values.yaml` to match your requirements for scaling and resource configuration. For example: +```yaml +config: + serverReplicas: 5 +services: + content: + replicas: 4 + storage: 50Gi +``` + +Refer to the official [Vespa documentation](https://docs.vespa.ai/en/) for advanced deployment details and customization options. + +--- + +## Troubleshooting +Check Helm release status to confirm all components deployed successfully: +```bash +helm status vespa -n vespa +``` + +If pods are stuck, ensure that: +1. The `configserver` is running and reachable. +2. Kubernetes networking allows communication between the pods. + +For further troubleshooting details, refer to the [Vespa troubleshooting guide](https://docs.vespa.ai/en/operations.html). \ No newline at end of file diff --git a/examples/operations/helm/charts/config/Chart.yaml b/examples/operations/helm/charts/config/Chart.yaml new file mode 100644 index 000000000..37ec018c2 --- /dev/null +++ b/examples/operations/helm/charts/config/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: config +version: 0.1.0 \ No newline at end of file diff --git a/examples/operations/helm/charts/config/templates/configmap.yml b/examples/operations/helm/charts/config/templates/configmap.yml new file mode 100644 index 000000000..d0efa21af --- /dev/null +++ b/examples/operations/helm/charts/config/templates/configmap.yml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: vespa-config +data: + VESPA_CONFIGSERVERS: >- + {{- $domain := printf "%s.%s.svc.cluster.local" .Values.headlessName .Release.Namespace }} + {{- $replicas := int .Values.serverReplicas }} + {{- $serverList := list }} + {{- range $i := until $replicas }} + {{- $serverList = append $serverList (printf "vespa-configserver-%d.%s" $i $domain) }} + {{- end }} + {{ join "," $serverList }} + VESPA_CONFIGSERVER_JVMARGS: "{{ .Values.serverJvmArgs }}" + VESPA_CONFIGPROXY_JVMARGS: "{{ .Values.proxyJvmArgs }}" \ No newline at end of file diff --git a/examples/operations/helm/charts/config/templates/configserver.yml b/examples/operations/helm/charts/config/templates/configserver.yml new file mode 100644 index 000000000..890049890 --- /dev/null +++ b/examples/operations/helm/charts/config/templates/configserver.yml @@ -0,0 +1,84 @@ +# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: vespa-configserver +spec: + # Use three servers for proper ZooKeeper quorum: + replicas: {{ .Values.serverReplicas }} + selector: + matchLabels: + app: vespa-configserver + name: {{ .Values.headlessName }} + serviceName: {{ .Values.headlessName }} + template: + metadata: + labels: + app: vespa-configserver + name: {{ .Values.headlessName }} + spec: + initContainers: + - name: chown-var + securityContext: + runAsUser: 0 + image: busybox + command: ["sh", "-c", "chown -R 1000 /opt/vespa/var"] + volumeMounts: + - name: vespa-var + mountPath: /opt/vespa/var + - name: chown-logs + securityContext: + runAsUser: 0 + image: busybox + command: ["sh", "-c", "chown -R 1000 /opt/vespa/logs"] + volumeMounts: + - name: vespa-logs + mountPath: /opt/vespa/logs + containers: + - name: vespa-configserver + image: vespaengine/vespa + args: ["configserver,services"] + imagePullPolicy: Always + securityContext: + runAsUser: 1000 + volumeMounts: + - name: vespa-var + mountPath: /opt/vespa/var + - name: vespa-logs + mountPath: /opt/vespa/logs + - name: vespa-workspace + mountPath: /workspace + envFrom: + - configMapRef: + name: vespa-config + # Note that the below are minimum resources for demo use. + # Use 4G or more for real use cases + resources: + requests: + memory: "1.5G" + limits: + memory: "1.5G" + volumeClaimTemplates: + # The below are tiny volumes for demo purposes. + - metadata: + name: vespa-var + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 5Gi + - metadata: + name: vespa-logs + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 5Gi + - metadata: + name: vespa-workspace + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi diff --git a/examples/operations/helm/charts/config/templates/headless.yml b/examples/operations/helm/charts/config/templates/headless.yml new file mode 100644 index 000000000..c5c31abd0 --- /dev/null +++ b/examples/operations/helm/charts/config/templates/headless.yml @@ -0,0 +1,12 @@ +# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.headlessName }} + labels: + name: {{ .Values.headlessName }} +spec: + selector: + name: {{ .Values.headlessName }} + clusterIP: None diff --git a/examples/operations/helm/charts/config/values.yaml b/examples/operations/helm/charts/config/values.yaml new file mode 100644 index 000000000..4dc62351f --- /dev/null +++ b/examples/operations/helm/charts/config/values.yaml @@ -0,0 +1,4 @@ +headlessName: "vespa-internal" +serverReplicas: 3 +serverJvmArgs: "-Xms32M -Xmx128M" +proxyJvmArgs: "-Xms32M -Xmx32M" \ No newline at end of file diff --git a/examples/operations/helm/charts/services/Chart.yaml b/examples/operations/helm/charts/services/Chart.yaml new file mode 100644 index 000000000..0cc9693a8 --- /dev/null +++ b/examples/operations/helm/charts/services/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: services +version: 0.1.0 \ No newline at end of file diff --git a/examples/operations/helm/charts/services/templates/admin.yml b/examples/operations/helm/charts/services/templates/admin.yml new file mode 100644 index 000000000..cddc1378c --- /dev/null +++ b/examples/operations/helm/charts/services/templates/admin.yml @@ -0,0 +1,50 @@ +# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: vespa-admin +spec: + replicas: 1 + selector: + matchLabels: + app: vespa-admin + name: vespa-internal + serviceName: vespa-internal + template: + metadata: + labels: + app: vespa-admin + name: vespa-internal + spec: + initContainers: + - name: wait-for-configserver + image: curlimages/curl + env: + - name: CONFIGSERVER_NAMESPACE + value: {{ .Release.Namespace }} + - name: CONFIGSERVER_SERVICE + value: vespa-configserver-0.vespa-internal + command: + - /bin/sh + - -c + - | + until curl -f http://vespa-configserver-0.vespa-internal.vespa.svc.cluster.local:19071/state/v1/health; do + echo "Waiting for Vespa ConfigServer to be ready in namespace $CONFIGSERVER_NAMESPACE..."; + sleep 5; + done + containers: + - name: vespa-admin + image: vespaengine/vespa + args: ["services"] + imagePullPolicy: Always + envFrom: + - configMapRef: + name: vespa-config + securityContext: + runAsUser: 1000 + resources: + requests: + memory: "1G" + limits: + memory: "1G" diff --git a/examples/operations/helm/charts/services/templates/content.yml b/examples/operations/helm/charts/services/templates/content.yml new file mode 100644 index 000000000..9644b43b4 --- /dev/null +++ b/examples/operations/helm/charts/services/templates/content.yml @@ -0,0 +1,69 @@ +# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: vespa-content +spec: + replicas: {{ .Values.content.replicas }} + selector: + matchLabels: + app: vespa-content + name: vespa-internal + serviceName: vespa-internal + template: + metadata: + labels: + app: vespa-content + name: vespa-internal + spec: + initContainers: + - name: wait-for-configserver + image: curlimages/curl + env: + - name: CONFIGSERVER_NAMESPACE + value: {{ .Release.Namespace }} + - name: CONFIGSERVER_SERVICE + value: vespa-configserver-0.vespa-internal + command: + - /bin/sh + - -c + - | + until curl -f http://vespa-configserver-0.vespa-internal.vespa.svc.cluster.local:19071/state/v1/health; do + echo "Waiting for Vespa ConfigServer to be ready in namespace $CONFIGSERVER_NAMESPACE..."; + sleep 5; + done + - name: chown-var + securityContext: + runAsUser: 0 + image: busybox + command: [ "sh", "-c", "chown -R 1000 /opt/vespa/var" ] + volumeMounts: + - name: vespa-var + mountPath: /opt/vespa/var + containers: + - name: vespa-content + image: vespaengine/vespa + args: [ "services" ] + imagePullPolicy: Always + envFrom: + - configMapRef: + name: vespa-config + securityContext: + runAsUser: 1000 + volumeMounts: + - name: vespa-var + mountPath: /opt/vespa/var + resources: + requests: + memory: "1G" + limits: + memory: "1G" + volumeClaimTemplates: + - metadata: + name: vespa-var + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: {{ .Values.content.storage }} diff --git a/examples/operations/helm/charts/services/templates/feed-container.yml b/examples/operations/helm/charts/services/templates/feed-container.yml new file mode 100644 index 000000000..e8161ceef --- /dev/null +++ b/examples/operations/helm/charts/services/templates/feed-container.yml @@ -0,0 +1,34 @@ +# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: vespa-feed-container +spec: + replicas: 2 + selector: + matchLabels: + app: vespa-feed-container + name: vespa-internal + serviceName: vespa-internal + template: + metadata: + labels: + app: vespa-feed-container + name: vespa-internal + spec: + containers: + - name: vespa-feed-container + image: vespaengine/vespa + args: ["services"] + imagePullPolicy: Always + envFrom: + - configMapRef: + name: vespa-config + securityContext: + runAsUser: 1000 + resources: + requests: + memory: "1.5G" + limits: + memory: "1.5G" diff --git a/examples/operations/helm/charts/services/templates/query-container.yml b/examples/operations/helm/charts/services/templates/query-container.yml new file mode 100644 index 000000000..779a393b1 --- /dev/null +++ b/examples/operations/helm/charts/services/templates/query-container.yml @@ -0,0 +1,34 @@ +# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: vespa-query-container +spec: + replicas: 2 + selector: + matchLabels: + app: vespa-query-container + name: vespa-internal + serviceName: vespa-internal + template: + metadata: + labels: + app: vespa-query-container + name: vespa-internal + spec: + containers: + - name: vespa-query-container + image: vespaengine/vespa + args: ["services"] + imagePullPolicy: Always + envFrom: + - configMapRef: + name: vespa-config + securityContext: + runAsUser: 1000 + resources: + requests: + memory: "1.5G" + limits: + memory: "1.5G" diff --git a/examples/operations/helm/charts/services/templates/service-feed.yml b/examples/operations/helm/charts/services/templates/service-feed.yml new file mode 100644 index 000000000..9e93b9a04 --- /dev/null +++ b/examples/operations/helm/charts/services/templates/service-feed.yml @@ -0,0 +1,17 @@ +# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +apiVersion: v1 +kind: Service +metadata: + name: vespa-feed + labels: + app: vespa +spec: + # Set LoadBalancer for an endpoint reachable from the internet + #type: LoadBalancer + selector: + app: vespa-feed-container + ports: + - name: api + port: 8080 + targetPort: 8080 diff --git a/examples/operations/helm/charts/services/templates/service-query.yml b/examples/operations/helm/charts/services/templates/service-query.yml new file mode 100644 index 000000000..9b3f8ee17 --- /dev/null +++ b/examples/operations/helm/charts/services/templates/service-query.yml @@ -0,0 +1,17 @@ +# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +apiVersion: v1 +kind: Service +metadata: + name: vespa-query + labels: + app: vespa +spec: + # Set LoadBalancer for an endpoint reachable from the internet + #type: LoadBalancer + selector: + app: vespa-query-container + ports: + - name: api + port: 8080 + targetPort: 8080 diff --git a/examples/operations/helm/charts/services/values.yaml b/examples/operations/helm/charts/services/values.yaml new file mode 100644 index 000000000..036a885bb --- /dev/null +++ b/examples/operations/helm/charts/services/values.yaml @@ -0,0 +1,3 @@ +content: + replicas: 2 + storage: 10Gi \ No newline at end of file diff --git a/examples/operations/helm/conf/hosts.xml b/examples/operations/helm/conf/hosts.xml new file mode 100644 index 000000000..4fb1638eb --- /dev/null +++ b/examples/operations/helm/conf/hosts.xml @@ -0,0 +1,33 @@ + + + + node0 + + + node1 + + + node2 + + + node3 + + + node4 + + + node5 + + + node6 + + + node7 + + + node8 + + + node9 + + diff --git a/examples/operations/helm/conf/schemas/music.sd b/examples/operations/helm/conf/schemas/music.sd new file mode 100644 index 000000000..3274c43ec --- /dev/null +++ b/examples/operations/helm/conf/schemas/music.sd @@ -0,0 +1,25 @@ +# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +schema music { + + document music { + + field artist type string { + indexing: summary | index + } + + field album type string { + indexing: summary | index + } + + field year type int { + indexing: summary | attribute + } + + field category_scores type tensor(cat{}) { + indexing: summary | attribute + } + + } + +} diff --git a/examples/operations/helm/conf/services.xml b/examples/operations/helm/conf/services.xml new file mode 100644 index 000000000..ff355f9a8 --- /dev/null +++ b/examples/operations/helm/conf/services.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + diff --git a/examples/operations/helm/hosts.xml b/examples/operations/helm/hosts.xml new file mode 100644 index 000000000..6ac831381 --- /dev/null +++ b/examples/operations/helm/hosts.xml @@ -0,0 +1,33 @@ + + + + node0 + + + node1 + + + node2 + + + node3 + + + node4 + + + node5 + + + node6 + + + node7 + + + node8 + + + node9 + + diff --git a/examples/operations/helm/values.yaml b/examples/operations/helm/values.yaml new file mode 100644 index 000000000..55d55bbe2 --- /dev/null +++ b/examples/operations/helm/values.yaml @@ -0,0 +1,4 @@ +services: + enabled: true + content: + storage: 20Gi