Skip to content

Repository files navigation

Apache Pulsar Helm Chart Ask DeepWiki

This project provides Helm Charts for installing Apache Pulsar on Kubernetes.

Read Deploying Pulsar on Kubernetes for more details.

⚠️ This helm chart is updated outside of the regular Pulsar release cycle and might lag behind a bit. It only supports basic Kubernetes features now. Currently, it can be used as no more than a template and starting point for a Kubernetes deployment. In many cases, it would require some customizations.

Important Security Advisory for Helm Chart Usage

Notice of Default Configuration

This Helm chart's default configuration DOES NOT meet production security requirements. Users MUST review and customize security settings for their specific environment.

IMPORTANT: This Helm chart provides a starting point for Pulsar deployments but requires significant security customization before use in production environments. We strongly recommend implementing:

  1. Authentication and authorization for all components
  2. TLS encryption for all communication channels
  3. Proper network isolation and access controls
  4. Regular security updates and vulnerability assessments

As an open source project, we welcome contributions to improve security features. Please consider submitting pull requests to address security gaps or enhance existing security implementations.

Pulsar Proxy Security Considerations

As per the Pulsar Proxy documentation, it is explicitly stated that the Pulsar proxy is not designed for exposure to the public internet. The design assumes that deployments will be protected by network perimeter security measures. It is crucial to understand that relying solely on the default configuration can expose your deployment to significant security vulnerabilities.

External Access Recommendations

If you need to expose the Pulsar Proxy outside the cluster:

  1. USE INTERNAL LOAD BALANCERS ONLY

  2. IMPLEMENT AUTHENTICATION AND AUTHORIZATION

    • Configure all clients to authenticate properly
    • Set up appropriate authorization policies
  3. USE TLS FOR ALL CONNECTIONS

    • Enable TLS for client-to-proxy connections
    • Enable TLS for proxy-to-broker connections
    • Enable TLS for all internal cluster communications
    • Note: TLS alone is NOT sufficient as a security solution. Even with TLS enabled, clusters exposed to untrusted networks remain vulnerable to denial-of-service attacks, authentication bypass attempts, and protocol-level exploits.
  4. NETWORK SECURITY

    • Use private networks (VPCs)
    • Configure firewalls, security groups, and IP restrictions
  5. CLIENT IP ADDRESS BASED ACCESS RESTRICTIONS

    • When using a LoadBalancer service type, restrict access to specific IP ranges by configuring proxy.service.loadBalancerSourceRanges in your values.yaml:
      proxy:
        service:
          loadBalancerSourceRanges:
            - 10.0.0.0/8     # Private network range
            - 172.16.0.0/12  # Private network range
            - 192.168.0.0/16 # Private network range
    • This feature:
      • Provides an additional defense layer by filtering traffic at the load balancer level
      • Only allows connections from specified CIDR blocks
      • Works only with LoadBalancer service type and when your cloud provider supports the loadBalancerSourceRanges parameter
    • Important: This should be implemented alongside other security measures (internal load balancer, authentication, TLS, network policies) as part of a defense-in-depth strategy, not as a standalone security solution

Alternative for External Access

As an alternative method for external access, Pulsar has support for SNI proxy routing. SNI Proxy routing is supported with proxy servers such as Apache Traffic Server, HAProxy and Nginx.

Note: This option isn't currently implemented in the Apache Pulsar Helm chart.

IMPORTANT: Pulsar binary protocol cannot be exposed outside of the Kubernetes cluster using Kubernetes Ingress. Kubernetes Ingress works for the Admin REST API and topic lookups, but clients would be connecting to the advertised listener addresses returned by the brokers and it would only work when clients can connect directly to brokers. This is not a supported secure option for exposing Pulsar to untrusted networks.

General Recommendations

  • Network Perimeter Security: It is imperative to implement robust network perimeter security to safeguard your deployment. The absence of such security measures can lead to unauthorized access and potential data breaches.
  • Restricted Access: For environments where security is less critical, such as certain development or testing scenarios, the use of loadBalancerSourceRanges may be employed to restrict access to specified IP addresses or ranges. This, however, should not be considered a substitute for comprehensive security measures in production environments.

User Responsibility

The user assumes full responsibility for the security and integrity of their deployment. This includes, but is not limited to, the proper configuration of security features and adherence to best practices for securing network access. The providers of this Helm chart disclaim all warranties, whether express or implied, including any warranties of merchantability, fitness for a particular purpose, and non-infringement of third-party rights.

No Security Guarantees

The providers of this Helm chart make no guarantees regarding the security of the chart under any circumstances. It is the user's responsibility to ensure that their deployment is secure and complies with all relevant security standards and regulations.

By using this Helm chart, the user acknowledges the risks associated with its default configuration and the necessity for proper security customization. The user further agrees that the providers of the Helm chart shall not be liable for any security breaches or incidents resulting from the use of the chart.

Features

This Helm Chart includes all the components of Apache Pulsar for a complete experience.

  • Pulsar core components:
    • ZooKeeper
    • Bookies
    • Brokers
    • Functions
    • Proxies
  • Management & monitoring components:
    • Dekaf UI (the supported web UI; Pulsar Manager support was removed in 4.8.0)
    • Optional PodMonitors for each component (enabled by default)
    • victoria-metrics-k8s-stack (as of 4.0.0)

It includes support for:

  • Security
    • Automatically provisioned TLS certs, using Jetstack's cert-manager
    • TLS Encryption
      • Proxy
      • Broker
      • Toolset
      • Bookie
      • ZooKeeper (requires the AdditionalCertificateOutputFormats=true feature gate to be enabled in the cert-manager deployment when using cert-manager versions below 1.15.0)
    • Authentication
      • JWT
      • OpenID
      • Mutual TLS
      • Kerberos
    • Authorization
    • Non-root broker, bookkeeper, proxy, and zookeeper containers (version 2.10.0 and above)
  • Storage
    • Non-persistence storage
    • Persistence Volume
    • Local Persistent Volumes
    • Tiered Storage
  • Functions
    • Kubernetes Runtime
    • Process Runtime
    • Thread Runtime
  • Operations
    • Independent Image Versions for all components, enabling controlled upgrades

Requirements

In order to use this chart to deploy Apache Pulsar on Kubernetes, the followings are required.

  1. kubectl 1.25 or higher, compatible with your cluster (+/- 1 minor release from your cluster)
  2. Helm v3 (3.12.0 or higher)
  3. A Kubernetes cluster, version 1.25 or higher.

Environment setup

Before proceeding to deploying Pulsar, you need to prepare your environment.

Tools

helm and kubectl need to be installed on your computer.

Add to local Helm repository

To add this chart to your local Helm repository:

helm repo add apachepulsar https://pulsar.apache.org/charts
helm repo update

Kubernetes cluster preparation

You need a Kubernetes cluster whose version is 1.25 or higher in order to use this chart, due to the usage of certain Kubernetes features.

We provide some instructions to guide you through the preparation: http://pulsar.apache.org/docs/helm-prepare/

Deploy Pulsar to Kubernetes

  1. Configure your values file. The best way to know which values are available is to read the values.yaml (or run helm show values apachepulsar/pulsar). A best practice is to start with an empty values file and only set the keys that differ from the default configuration. Ready-made example value files for common scenarios (single-node, TLS, JWT, Oxia, and more) are in examples/.

    Anti-affinity rules for Zookeeper and Bookie components require at least one node per replica. For Kubernetes clusters with less than 3 nodes, you must disable this feature by adding this to your initial values.yaml file:

    affinity:
      anti_affinity: false

    To pin a component to specific nodes (for example a dedicated GKE node pool), set that component's affinity.nodeAffinity. It is passed through to the pod spec verbatim, so any native node affinity expression works, and it composes with the chart's own pod anti-affinity rather than replacing them:

    broker:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: cloud.google.com/gke-nodepool
                    operator: In
                    values:
                      - pulsar-pool
  2. Install the chart:

    helm install -n <namespace> --create-namespace <release-name> -f your-values.yaml apachepulsar/pulsar
  3. Observe the deployment progress

    Watching events to view progress of deployment:

    kubectl get -n <namespace> events -o wide --watch

    Watching state of deployed Kubernetes objects, updated every 2 seconds:

    watch kubectl get -n <namespace> all

    Waiting until Pulsar Proxy is available:

    kubectl wait --timeout=600s --for=condition=ready pod -n <namespace> -l component=proxy

    Watching state with k9s (https://k9scli.io/topics/install/):

    k9s -n <namespace>
  4. Access the Pulsar cluster

    The default values will create a ClusterIP for the proxy you can use to interact with the cluster. To find the IP address of proxy use:

    kubectl get service -n <k8s-namespace>

For more information, please follow our detailed quick start guide.

Customize the deployment

We provide a detailed guideline for you to customize the Helm Chart for a production-ready deployment.

You can also check out the example values files for different deployments. See examples/README.md for the full annotated list. A few common ones:

These example files are small, focused overrides meant to be combined: pass -f multiple times (later files win), or use the merge-values.sh helper to merge several into a single file. See examples/README.md for the full list and usage details.

Pod and container security contexts

Two global values apply a security context to every pod and every container the chart itself renders, including its initContainers and the init/cleanup Jobs. Containers you supply through <component>.initContainers, oxia.coordinator.extraContainers or dekaf.deployment.extraContainers are passed through verbatim and are not merged with these settings -- set a securityContext on those yourself:

podSecurityContext:
  runAsNonRoot: true
  runAsUser: 10000
  runAsGroup: 10000
  fsGroup: 10000
  fsGroupChangePolicy: OnRootMismatch
  supplementalGroups: [10000]
containerSecurityContext:
  allowPrivilegeEscalation: false
  capabilities:
    drop: ["ALL"]
  seccompProfile:
    type: RuntimeDefault

Both are empty by default, so the chart's rendered output is unchanged unless you set them. Use them when a cluster policy engine (Pod Security Admission, OPA Gatekeeper, Kyverno) requires settings the chart does not set on its own.

The containerSecurityContext block above is what the Kubernetes restricted Pod Security Standard actually enforces, together with runAsNonRoot. See examples/values-psa-restricted.yaml.

Each global value is merged with the matching per-component override, and the per-component value wins on a per-key basis:

Global Per-component
podSecurityContext <component>.securityContext
containerSecurityContext <component>.containerSecurityContext

The per-component keys exist for zookeeper, bookkeeper, broker, autorecovery, proxy, toolset, function_worker, standalone, oxia.server, oxia.coordinator, pulsar_metadata, dekaf.deployment and auth.authentication.jwt.generateSecrets. Component-owned Jobs follow the component they belong to: the bookkeeper cluster-initialize Job uses bookkeeper.*, the zookeeper and broker sts-cleanup upgrade hooks use zookeeper.* and broker.*, and the pulsar-cluster-initialize Job uses pulsar_metadata.*.

Overriding fsGroup, if a policy constrains group IDs

zookeeper, bookkeeper, broker and oxia.server ship securityContext.fsGroup: 0 so that mounted volumes are group-owned by GID 0, the group the pulsar user (UID 10000) belongs to. That is deliberate, and it is what lets the images run under an arbitrary assigned UID — the model OpenShift uses. fsGroup: 0 is not a privilege: group 0 inside a container is an ordinary group, and root power comes from UID 0 and capabilities, neither of which this grants.

Most policies do not require you to change it. In particular the Kubernetes restricted Pod Security Standard places no constraint on fsGroup, fsGroupChangePolicy or supplementalGroups, and the PSS policy sets that Gatekeeper and Kyverno ship mirror PSA, so fsGroup: 0 is admitted there as-is.

Override it only when a policy actually constrains group IDs to a numeric range — for example Gatekeeper's PodSecurityPolicy-derived K8sPSPAllowedUsers with fsGroup: {rule: MustRunAs, ranges: [{min: 1, ...}]}, or an OpenShift SCC. Because a per-component value takes precedence over the global one, setting a global fsGroup alone does not change these four components — override their own securityContext as well:

podSecurityContext:
  runAsNonRoot: true
  runAsUser: 10000
  runAsGroup: 10000
  fsGroup: 10000
  fsGroupChangePolicy: OnRootMismatch
  supplementalGroups: [10000]
bookkeeper:
  securityContext:
    fsGroup: 10000
    fsGroupChangePolicy: OnRootMismatch

fsGroup is applied as a supplementary group, so runAsGroup does not need to match it -- the process keeps its own primary GID and still gets access to the volume.

On a cluster with existing volumes, changing fsGroup triggers a recursive ownership change on first mount; fsGroupChangePolicy: OnRootMismatch keeps that to the first pass, but it can still take a long time on large bookie ledger volumes. Test on a copy before changing a production cluster.

readOnlyRootFilesystem

The Pulsar images do not run on a read-only root filesystem unaided. They rewrite their configuration under /pulsar/conf on startup (bin/apply-config-from-env.py), write logs under /pulsar/logs, and the JVM and the functions worker use /tmp.

Setting containerSecurityContext.readOnlyRootFilesystem: true is nevertheless enough. For each entry in emptyDirVolumes the chart then mounts an emptyDir at that path on every container and initContainer of the component, and for entries marked seedFromImage: true it prepends a copy-pulsar-conf initContainer that copies the image's contents into the volume first — an emptyDir starts empty, and apply-config-from-env.py edits files that must already exist.

The chart-wide default describes the Pulsar images:

emptyDirVolumes:
  - path: /pulsar/conf
    seedFromImage: true
  - path: /pulsar/logs
    sizeLimit: 1Gi
  - path: /tmp
    sizeLimit: 1Gi

sizeLimit matters: without it an emptyDir is unbounded and a busy /pulsar/logs can fill a node's ephemeral storage and get pods evicted.

This is driven by the effective (merged) container securityContext, so a single global readOnlyRootFilesystem: true covers the release, and a component that overrides it back to false also loses the volumes.

Per-component paths

Every component takes its own <component>.emptyDirVolumes, which replaces the list it would otherwise inherit rather than adding to it — Helm merges maps per key but replaces lists wholesale. Use [] for "this component needs none".

The three paths above describe the Pulsar images, so they are not applied to components that run something else. Those carry their own default and never inherit the chart-wide list:

Component Default Why
oxia.server, oxia.coordinator /tmp the oxia binary keeps its own state under its data directory, but /tmp is provided so that a Go dependency or a future version falling back to os.TempDir() cannot take the metadata store down
dekaf.deployment /tmp the JVM writes scratch files there
everything else the three above Pulsar images

Setting <component>.emptyDirVolumes still overrides either default.

To hand a path back to yourself, drop its entry and declare it through <component>.extraVolumes / <component>.extraVolumeMounts. Declaring just the mount is enough: an entry whose path the component already mounts via extraVolumeMounts is skipped, because a duplicate mountPath is rejected by the API server. Bear in mind that whatever you mount over /pulsar/conf has to be populated, since nothing seeds it for you.

Rejected at render time rather than at apply time: a relative path, an unknown entry key, two paths that reduce to the same volume name (/pulsar/logs and /pulsar-logs both give pulsar-logs), and a path long enough to exceed the 63-character limit on a name.

Note that logs written to an emptyDir do not survive pod replacement. If you rely on reading /pulsar/logs from inside a pod, ship them off-node or keep readOnlyRootFilesystem disabled.

Disabling victoria-metrics-k8s-stack components

In order to disable the victoria-metrics-k8s-stack, you can add the following to your values.yaml. Victoria Metrics components can also be disabled and enabled individually if you only need specific monitoring features.

# disable VictoriaMetrics and related components
victoria-metrics-k8s-stack:
  enabled: false
  victoria-metrics-operator:
    enabled: false
  vmsingle:
    enabled: false
  vmagent:
    enabled: false
  kube-state-metrics:
    enabled: false
  prometheus-node-exporter:
    enabled: false
  grafana:
    enabled: false

Additionally, you'll need to set each component's podMonitor property to false.

# disable pod monitors
autorecovery:
  podMonitor:
    enabled: false
bookkeeper:
  podMonitor:
    enabled: false
oxia:
  server:
    podMonitor:
      enabled: false
  coordinator:
    podMonitor:
      enabled: false
broker:
  podMonitor:
    enabled: false
proxy:
  podMonitor:
    enabled: false
zookeeper:
  podMonitor:
    enabled: false

This is shown in some examples/values-disable-monitoring.yaml.

Dekaf UI

Dekaf is an open-source UI for Apache Pulsar and the supported web UI in this chart. It is the recommended alternative for users migrating away from Pulsar Manager, whose support was removed in chart version 4.8.0.

⚠️ At this moment Dekaf doesn't have built-in authentication. In order to prevent unwanted access, it relies on authentication on the Pulsar broker side. If your Pulsar instance stores sensitive data, make sure that:

  • You have configured authentication on the Pulsar side
  • Dekaf isn't accessible from the Internet
  • Only authorized persons have access to you Kubernetes namespace

Improvements in this area are planned to be implemented later.

To enable the Dekaf component:

  • Set the components.dekaf property to true in the Helm release values.yaml file (several example values files already enable it).
  • Run the following command to make Dekaf service accessible on your local machine.
kubectl port-forward svc/$(kubectl get svc -l component=dekaf -o jsonpath='{.items[0].metadata.name}') 8090:8090

Choosing the metadata store: use Oxia for new clusters

Pulsar stores its metadata in a metadata store. This chart can deploy either Apache ZooKeeper or Oxia as the metadata store.

For new production clusters, Oxia is the recommended metadata store. Use Oxia to get the full feature set of the scalable topics introduced in Pulsar 5.0. ZooKeeper remains supported.

The chart currently defaults to ZooKeeper (components.zookeeper: true, components.oxia: false). The default will change to Oxia in a future chart version.

Deploying a new cluster with Oxia

Choose the metadata store when you first install the cluster. Disable ZooKeeper and enable Oxia in your values.yaml:

components:
  zookeeper: false
  oxia: true

examples/values-oxia.yaml contains a ready-made example. The chart deploys an Oxia coordinator and an Oxia server StatefulSet with 3 replicas, and configures the brokers and bookies to use Oxia. Size Oxia for your workload with the oxia values, such as oxia.server.replicas, oxia.server.cpuLimit, oxia.server.memoryLimit, oxia.server.dbCacheSizeMb, oxia.server.storageSize, oxia.initialShardCount and oxia.replicationFactor.

If you run Pulsar Functions on Oxia, you must also enable FileSystemPackagesStorage. See Pulsar Functions package storage.

Existing ZooKeeper-based installations

Don't switch an existing release from ZooKeeper to Oxia by changing components.zookeeper and components.oxia. The chart doesn't migrate metadata, so the brokers and bookies would start with an empty metadata store, and the cluster would lose all its topics, subscriptions and ledger metadata.

Pulsar supports migrating an existing cluster from ZooKeeper to Oxia with a special procedure. See Migrate metadata store from ZooKeeper to Oxia. This Helm chart doesn't support that migration yet.

To keep an existing installation on ZooKeeper when a future chart version changes the default to Oxia, set the components explicitly in your values.yaml now:

components:
  zookeeper: true
  oxia: false

Pulsar Functions package storage (required for Oxia)

The Pulsar Packages Management Service — which stores uploaded function packages (pulsar-admin functions create --jar ...) — runs on the broker. Its default storage provider, BookKeeperPackagesStorage, relies on DistributedLog metadata in ZooKeeper, so it does not work when Oxia is used as the metadata store (components.oxia: true).

To run Pulsar Functions on Oxia you must enable FileSystemPackagesStorage on the broker. The Packages Management Service is configured in two levels: broker.packageManagement.enabled turns the service on, and broker.packageManagement.fileSystemStorage.enabled selects the FileSystem provider:

components:
  oxia: true
  functions: true
broker:
  packageManagement:
    enabled: true
    fileSystemStorage:
      enabled: true

This configures the broker with enablePackagesManagement=true and packagesManagementStorageProvider=FileSystemPackagesStorageProvider, and mounts a shared PersistentVolumeClaim on every broker pod as the package storage directory. If components.functions is enabled without ZooKeeper (using Oxia) but FileSystemPackagesStorage is not enabled, the chart fails the Helm install with an explanatory error (the default BookKeeper provider would not work without ZooKeeper).

Choosing a volume

FileSystemPackagesStorage is a directory on disk, so the volume backing it determines how many broker replicas can use it (all keys below are under broker.packageManagement.fileSystemStorage):

  • Single broker / single-node dev clusters (e.g. minikube): the default persistentVolumeClaim is a ReadWriteOnce claim on the cluster's default StorageClass — no extra configuration is required.

  • Multiple broker replicas: the package directory must be on a ReadWriteMany shared filesystem — a managed file service, not block storage (Persistent Disk / EBS / Azure Disk are ReadWriteOnce and cannot be shared across replicas). Provision one with the matching cloud CSI driver, set persistentVolumeClaim: {} so the chart does not create a claim, and point claimName at the pre-created PVC:

    Cloud Shared file service to use CSI driver Reference
    GCP / GKE Filestore (managed NFS) filestore.csi.storage.gke.io Filestore CSI
    AWS / EKS Amazon EFS (managed NFS) efs.csi.aws.com EFS CSI on EKS
    Azure / AKS Azure Files file.csi.azure.com Azure Files on AKS

Volume permissions. Pulsar container images run as uid 10000, gid 0 by default, so package files are written by that user/group. The chart sets broker.securityContext.fsGroup: 0 (fsGroupChangePolicy: OnRootMismatch), which tells Kubernetes to set the volume's group to 0 and make it group-writable — enough for the broker to read/write the package directory, and this works on most volume types (block-storage CSI drivers, hostPath). Some shared filesystems — notably the NFS/SMB-backed ReadWriteMany volumes above (EFS, Filestore, Azure Files) — ignore fsGroup. If package writes then fail with permission errors, grant uid 10000 / gid 0 read-write-execute on the share itself: make the directory group-0-owned and group-writable (e.g. chown :0 <dir> && chmod 2770 <dir> — rwxrwx--- plus the setgid bit so new entries inherit gid 0), or set it via the CSI driver's mount options (for Azure Files SMB, for example, mountOptions: [uid=10000, gid=0, file_mode=0770, dir_mode=0770]).

broker.packageManagement.fileSystemStorage can also create the StorageClass, PersistentVolume, and PersistentVolumeClaim directly from raw YAML — only apiVersion/kind are fixed by the chart, and a value of {} creates nothing. See the broker.packageManagement section in values.yaml and the examples/values-functions-fs-storage.yaml example.

Grafana Dashboards

The Apache Pulsar Helm Chart uses the victoria-metrics-k8s-stack Helm Chart to deploy Grafana.

There are several ways to configure Grafana dashboards. The default values.yaml comes with examples of Pulsar dashboards which get downloaded from the Apache-2.0 licensed lhotari/pulsar-grafana-dashboards OSS project by URL.

Dashboards can be configured in values.yaml or by adding ConfigMap items with the label grafana_dashboard: "1". In values.yaml, it's possible to include dashboards by URL or by grafana.com dashboard id (gnetId and revision). Please see the Grafana Helm chart documentation for importing dashboards.

You can connect to Grafana by forwarding port 3000

kubectl port-forward $(kubectl get pods -l app.kubernetes.io/name=grafana -o jsonpath='{.items[0].metadata.name}') 3000:3000

And then opening the browser to http://localhost:3000 . The default user is admin.

You can find out the password with this command

kubectl get secret -l app.kubernetes.io/name=grafana -o=jsonpath="{.items[0].data.admin-password}" | base64 --decode

Pulsar Grafana Dashboards

  • The apache/pulsar GitHub repo contains some Grafana dashboards here.
  • StreamNative provides Grafana Dashboards for Apache Pulsar in this GitHub repository.
  • DataStax provides Grafana Dashboards for Apache Pulsar in this GitHub repository.

Note: if you have third party dashboards that you would like included in this list, please open a pull request.

Upgrading

Once your Pulsar Chart is installed, configuration changes and chart updates should be done using helm upgrade.

helm repo add apachepulsar https://pulsar.apache.org/charts
helm repo update
# If you are using the provided victoria-metrics-k8s-stack for monitoring, this installs or upgrades the required CRDs
./scripts/victoria-metrics-k8s-stack/upgrade_vm_operator_crds.sh
# get the existing values.yaml used for the most recent deployment
helm get values -n <namespace> <pulsar-release-name> > values.yaml
# upgrade the deployment
helm upgrade -n <namespace> -f values.yaml <pulsar-release-name> apachepulsar/pulsar

For more detailed information, see our Upgrading guide.

Upgrading to Helm chart version 4.8.0

Helm chart 4.8.0 deploys Apache Pulsar 5.0.0 by default, using the apachepulsar/pulsar image, since there is no apachepulsar/pulsar-all image for Pulsar 5.0.0. It also removes Pulsar Manager support. Before upgrading, review these changes, which can require changes to your values.yaml:

  1. Default Apache Pulsar version is now 5.0.0
  2. Pulsar Manager support has been removed
  3. X.509 certificate subject configuration has changed

Then check the other changes in behavior and the new configuration options.

Default Apache Pulsar version is now 5.0.0

The chart now deploys Apache Pulsar 5.0.0 by default, using the apachepulsar/pulsar:5.0.0 image. There is no apachepulsar/pulsar-all image for Pulsar 5.0.0 (see apachepulsar/pulsar-all is no longer used). Before upgrading, read the Upgrading to Pulsar 5.0.x guide. It recommends first upgrading to the latest Pulsar 4.0.x or 4.2.x release and running it as a stable baseline that you can roll back to.

To keep running Pulsar 4.x with this chart version, pin the image tag in your values.yaml:

defaultPulsarImageTag: 4.0.14

The chart now uses the apachepulsar/pulsar image by default for Pulsar 4.x too. Unlike apachepulsar/pulsar-all, the Pulsar 4.x apachepulsar/pulsar image doesn't include the tiered-storage offloaders or the Pulsar IO connectors. If you need them with Pulsar 4.x, also set the repository back:

defaultPulsarImageRepository: apachepulsar/pulsar-all
defaultPulsarImageTag: 4.0.14

apachepulsar/pulsar-all is no longer used

There is no apachepulsar/pulsar-all image for Pulsar 5.0.0, so defaultPulsarImageRepository now defaults to apachepulsar/pulsar. If your values.yaml sets defaultPulsarImageRepository, images.<component>.repository or pulsar_metadata.image.repository to apachepulsar/pulsar-all, change it to apachepulsar/pulsar or remove the key.

The apachepulsar/pulsar 5.0.0 image includes the tiered-storage offloader for AWS S3 (and S3-compatible storage), Google Cloud Storage, Azure Blob Storage and Aliyun OSS, so broker.storageOffload keeps working. The filesystem offloader and the Pulsar IO connector NARs are no longer bundled. If you use them, build a custom image that adds the required NAR files.

PULSAR_GC defaults have been removed

Pulsar 5.0.0 runs on Java 25 with ZGC, and the Pulsar launcher selects the garbage collector options for the Java version in the image. The chart no longer sets PULSAR_GC in the configData of any component. -XX:+AlwaysPreTouch, -XX:+UseTransparentHugePages and -XX:+ExitOnOutOfMemoryError were moved to the default PULSAR_MEM values.

If your values.yaml sets PULSAR_GC for any component, remove it, and also remove garbage collector selection options from PULSAR_MEM and PULSAR_EXTRA_OPTS. If you override PULSAR_MEM, add the flags listed above to your value if you want to keep them.

-XX:+UseTransparentHugePages only lets the JVM request Transparent Huge Pages (THP). Pulsar benefits from them only when the Linux kernel on the Kubernetes nodes that run the Pulsar pods is configured in a specific way, and pod settings can't change the node's kernel settings. For example, ZGC keeps its heap in shared memory, so the node's /sys/kernel/mm/transparent_hugepage/shmem_enabled must allow huge pages (advise). With never, which is the Linux kernel default, the heap doesn't use huge pages even though the flag is set. Configure the nodes as described in Configure Linux hosts and Kubernetes nodes, and make the settings part of the node image or provisioning so that replaced and autoscaled nodes get them too.

Other Pulsar 5.0 changes to review

  • Metadata store: Oxia is recommended for new production clusters. Existing installations stay on ZooKeeper. See Choosing the metadata store.

  • Package management rollback: if you set broker.packageManagement.enabled: true and may need to roll back to Pulsar 4.x, keep the package metadata in a format that Pulsar 4.x can read. Set these values before the first Pulsar 5.0 broker starts:

    broker:
      configData:
        PULSAR_PREFIX_packagesManagementJsonSerializationEnabled: "false"
        PULSAR_PREFIX_packagesManagementAllowLegacyJavaSerialization: "true"
  • TLS hostname verification is now enabled by default for outbound TLS connections from brokers, proxies and Functions workers. The certificates issued by the chart include the service hostnames. If you provide your own certificates, check that they include matching subject alternative names.

  • BookKeeper metrics provider: if you set statsProviderClass in bookkeeper.configData, replace org.apache.pulsar.metrics.prometheus.bookkeeper.PrometheusMetricsProvider with org.apache.bookkeeper.stats.prometheus.PrometheusMetricsProvider. Otherwise, bookies fail to start.

  • Configuration defaults: review your configData overrides against the configuration default changes. Some settings have been removed, and explicitly set values keep their old behavior.

Pulsar Manager support has been removed

Pulsar Manager support has been removed from this Helm chart. The upstream Apache Pulsar Manager project has been poorly maintained for a long time, so all of its chart resources (StatefulSet, Services, ConfigMap, Ingress, admin Secret and the pulsar-manager-init Job) have been removed, together with the pulsar_manager values section, the images.pulsar_manager entry and the auth.superUsers.manager role.

What you must change before upgrading

The chart fails fast when the component is still enabled, so a helm upgrade with a leftover components.pulsar_manager: true aborts with:

ERROR: Pulsar Manager support has been removed from the Apache Pulsar Helm chart. ...

To allow the upgrade to proceed, remove the components.pulsar_manager key from your values.yaml or set it to false:

components:
  # remove this key entirely, or set it to false
  pulsar_manager: false

Also remove these keys if you set them, since they are no longer used:

  • the whole pulsar_manager: section
  • images.pulsar_manager
  • auth.superUsers.manager

The upgrade removes the Pulsar Manager workloads and its data PersistentVolumeClaim. If you want to keep the Pulsar Manager database contents, back up the PVC before upgrading.

Migrating to Dekaf

Dekaf is the supported web UI in this chart and the recommended replacement. Enable it with:

components:
  dekaf: true

See the Dekaf UI section for details, including the security caveats. Pulsar's own pulsar-admin CLI, available in the toolset component, also remains available for administration tasks.

X.509 certificate subject configuration has changed

The subject of the certificates that the chart creates is now configured with tls.common.subject, which supports all the X.509 subject fields. tls.common.organization is no longer supported, and the upgrade fails if your values.yaml still sets it. Move the value under tls.common.subject.organizations:

# before
tls:
  common:
    organization:
      - pulsar
# after
tls:
  common:
    subject:
      organizations:
        - pulsar
      # countries: []
      # organizationalUnits: []
      # localities: []
      # provinces: []
      # streetAddresses: []
      # postalCodes: []
      # serialNumber: ""

Other changes in behavior

These changes don't require changes to your values.yaml, but they change what the chart deploys:

  • Internal CA private key is kept across renewals: since cert-manager 1.18, cert-manager generates a new private key for a certificate on every renewal by default. For the self-signed CA created by the internal issuer (certs.internal_issuer), that invalidates all the certificates it issued until each of them is reissued. The chart now sets certs.internal_issuer.privateKey.rotationPolicy: Never, which keeps the CA key across renewals as with earlier cert-manager versions. Set certs.internal_issuer.privateKey: null to restore the previous rendering.
  • PodMonitor and HPA resources for disabled components: the PodMonitor (or VMPodScrape) for the proxy, broker, bookkeeper and autorecovery components, and the proxy and broker HorizontalPodAutoscalers, are no longer rendered when the component is disabled with components.<component>: false.
  • Autorecovery probes: the autorecovery StatefulSet now has liveness and readiness probes. They check the Prometheus metrics endpoint on autorecovery.ports.http and can be configured with autorecovery.probe.
  • ZooKeeper and broker upgrade cleanup Jobs: the pods of the sts-cleanup pre-upgrade hook Jobs now get the chart's pod labels, and they use the component's nodeSelector, tolerations, priorityClassName, topologySpreadConstraints, affinity.nodeAffinity and the chart's imagePullSecrets, so they can be scheduled on clusters that reserve nodes for Pulsar.
  • Oxia has been upgraded to 0.16.10.
  • BookKeeper cluster initialization with ZooKeeper TLS: the bookie-init Job now uses the same ZooKeeper connection string as the other components, including the TLS port when ZooKeeper TLS is enabled.

New configuration options

  • Pod and container security contexts: the global podSecurityContext and containerSecurityContext values, and the per-component <component>.securityContext and <component>.containerSecurityContext overrides, apply to every pod and container that the chart renders. Setting readOnlyRootFilesystem: true is also supported. See Pod and container security contexts and examples/values-psa-restricted.yaml.
  • Node affinity: each component accepts <component>.affinity.nodeAffinity to pin its pods to specific nodes. See Deploy Pulsar to Kubernetes.
  • Job pod annotations: job.podAnnotations adds annotations to the pods created by the chart's Jobs, for example sidecar.istio.io/inject: "false". The existing job.annotations only applies to the Job objects.
  • Certificate SAN mode: tls.common.sanMode selects the subject alternative names (SANs) of the certificates that the chart creates. wildcard (the default) keeps the previous wildcard SANs. fqdn adds an explicit FQDN for each pod instead of the wildcard. none adds only the service names, so you can add your own with tls.<component>.dnsNames and tls.<component>.ipAddresses. Restart the component pods after changing it so that they pick up the reissued certificates.
  • Externally managed certificates: every component now supports tls.<component>.createCert: false, not only the proxy and the function worker, so you can provide all the certificate Secrets yourself. The proxy Secret no longer needs the tls-combined.pem key, so a publicly trusted certificate can be used for the proxy while the other components use certificates from a private CA. The ## TLS comments in values.yaml describe the supported setups and the required Secret keys.

Upgrading to Helm chart version 4.6.0

ZooKeeper and Broker Services split into ClusterIP + headless

Note: Upgrading existing installs may cause a brief service disruption. The StatefulSet's serviceName is immutable, so the ZooKeeper and Broker StatefulSets are re-created during the upgrade (see Upgrading: pre-upgrade cleanup Job below).

PRs #649 and #650 replace the single Service that fronted each of the ZooKeeper and Broker StatefulSets with two:

  • a regular ClusterIP Service (<release>-zookeeper, <release>-broker) — used by clients; only routes to ready pods. The default for the main Broker service has changed from headless to ClusterIP.
  • a headless Service (*-headless, clusterIP: None, publishNotReadyAddresses: true) — used as the StatefulSet serviceName for stable per-pod DNS.

Why

ZooKeeper. The previous Service had publishNotReadyAddresses: true, so brokers and bookies could be routed to ZK pods that were still starting or unhealthy. Splitting into a ready-only ClusterIP Service for clients and a headless Service for per-pod DNS fixes that.

Brokers (issue #437). A broker registers itself in ZooKeeper using its per-pod DNS name; other brokers and clients then resolve that name to reach it. The previous headless Service did not set publishNotReadyAddresses, so the per-pod name only became resolvable after the pod's readiness probe passed (plus DNS-cache TTL). Meanwhile the load manager could already have assigned namespace bundles to the new broker, causing a brief disruption on those topics. The new headless Service sets publishNotReadyAddresses: true, so the per-pod name resolves immediately. Two further benefits:

  • Client lookups now go through a regular ClusterIP Service that returns a single IP. The previous headless Service returned one A record per broker, which can exceed the 512-byte UDP DNS limit in larger clusters. Some DNS clients cannot handle this due to lack of TCP fallback for DNS (for example Alpine <3.18).
  • StatefulSets require a headless Service for pod identity, so the headless Service can only be paired with — not replaced by — a ClusterIP Service.

Upgrading: pre-upgrade cleanup Job

Because serviceName is immutable, an in-place upgrade from a pre-4.6.0 chart would fail. The chart ships a pre-upgrade Job per component that uses kubectl (image images.kubectl, default alpine/k8s) to delete the old StatefulSet with --cascade=orphan. Pods (and ZooKeeper on-disk data) are preserved and keep running until the new StatefulSet rolls them, but a brief disruption around the cutover is possible. The Job reads the existing chart label and only acts when the prior version is < 4.6.0; disable with zookeeper.statefulsetUpgrade.enabled=false or broker.statefulsetUpgrade.enabled=false to manage the migration manually.

GitOps users (ArgoCD, Flux, Pulumi, etc.): the cleanup relies on Helm's pre-upgrade hook lifecycle, which isn't always honored by GitOps tooling that renders the chart and applies the manifests directly. Verify that your tool runs helm.sh/hook: pre-upgrade Jobs before the rest of the release — or disable the hook flags above and handle the StatefulSet deletion (with --cascade=orphan) as part of your migration — before upgrading to 4.6.0.

TLS

The hostnames of the broker and ZooKeeper pods have changed, and certificates now include the new *-headless DNS names as SANs. After cert-manager reissues them, do a rolling restart of ZooKeeper and brokers so the running pods pick up matching certificates.

In-chart JWT secret generation

PR #672 removes the need to run prepare_helm_release.sh — or any out-of-band script — to seed JWT secrets before installing.

Opt in with auth.authentication.jwt.generateSecrets.enabled: true. A pre-install/pre-upgrade Job mints the signing key (symmetric or RSA) and one token per auth.superUsers entry, storing them as the same <release>-token-* secrets the rest of the chart already consumes. The Job is idempotent — skipped if the signing key secret exists, and existing token secrets are never overwritten — and supports annotations on generated secrets for tooling like reflector. Default is false, so existing installs are unaffected.

A fully-authenticated cluster can now be deployed with a single helm install.

Standalone deployment mode

PR #674 adds a top-level standalone toggle that deploys a single Pulsar standalone instance instead of separate ZooKeeper, BookKeeper, Broker, etc. workloads.

The goal is to use the same Helm chart for minimal development and test deployments on Kubernetes — local Kind/k3d/minikube, ephemeral CI, developer sandboxes — without a separate chart or installer. Existing values, image overrides, and tooling carry over.

Upgrading to Helm chart version 4.2.0

TLS configuration for ZooKeeper has changed

The TLS configuration for ZooKeeper has been changed to fix certificate and private key expiration issues. This change impacts configurations that have tls.enabled and tls.zookeeper.enabled set in values.yaml. The revised solution requires the AdditionalCertificateOutputFormats=true feature gate to be enabled in the cert-manager deployment when using cert-manager versions below 1.15.0. If you installed cert-manager using ./scripts/cert-manager/install-cert-manager.sh, you can re-run the updated script to set the feature gate. The script currently installs or upgrades cert-manager LTS version 1.12.17, where the feature gate must be explicitly enabled.

Upgrading to Helm chart version 4.1.0

This version introduces OpenID authentication. Setting auth.authentication.provider is no longer supported, you need to enable the provider with auth.authentication.<provider>.enabled.

In the case of using JWT authentication, you need to set auth.authentication.jwt.enabled to true in your values.yaml.

auth:
  authentication:
    enabled: true
    jwt:
      # Enable JWT authentication
      enabled: true

Upgrading from Helm Chart versions before 4.0.0 to 4.0.0 version and above

Pulsar Proxy service's default type has been changed from LoadBalancer to ClusterIP

Please check the section "External Access Recommendations" for guidance and also check the security advisory section. You will need to configure keys under proxy.service in your values.yaml to preserve existing functionality since the default has been changed.

kube-prometheus-stack replaced with victoria-metrics-k8s-stack

The kube-prometheus-stack was replaced with victoria-metrics-k8s-stack in Pulsar Helm chart version 4.0.0. The trigger for the change was incompatibilities discovered in testing with most recent kube-prometheus-stack and Prometheus 3.2.1 which failed to scrape Pulsar metrics in certain cases without providing proper error messages or debug information at debug level logging.

Victoria Metrics is Apache 2.0 Licensed OSS and it's a fully compatible drop-in replacement for Prometheus which is fast and efficient.

Before upgrading to Pulsar Helm Chart version 4.0.0, it is recommended to disable kube-prometheus-stack in the original Helm chart version that is used:

# get the existing values.yaml used for the most recent deployment
helm get values -n <namespace> <pulsar-release-name> > values.yaml
# disable kube-prometheus-stack in the currently used version before upgrading to Pulsar Helm chart 4.0.0
helm upgrade -n <namespace> -f values.yaml --version <your-current-chart-version> --set kube-prometheus-stack.enabled=false  <pulsar-release-name> apachepulsar/pulsar

After, this you can proceed with helm upgrade.

Upgrading to Apache Pulsar 2.10.0 and above (or Helm Chart version 3.0.0 and above)

The 2.10.0+ Apache Pulsar docker image is a non-root container, by default. That complicates an upgrade to 2.10.0 because the existing files are owned by the root user but are not writable by the root group. In order to leverage this new security feature, the Bookkeeper and Zookeeper StatefulSet securityContexts are configurable in the values.yaml. They default to:

  securityContext:
    fsGroup: 0
    fsGroupChangePolicy: "OnRootMismatch"

This configuration is ideal for regular Kubernetes clusters where the UID is stable across restarts. If the process UID is subject to change (like it is in OpenShift), you'll need to set fsGroupChangePolicy: "Always".

The official docker image assumes that it is run as a member of the root group.

If you upgrade to the latest version of the helm chart before upgrading to Pulsar 2.10.0, then when you perform your first upgrade to version >= 2.10.0, you will need to set fsGroupChangePolicy: "Always" on the first upgrade and then set it back to fsGroupChangePolicy: "OnRootMismatch" on subsequent upgrades. This is because the root file won't mismatch permissions, but the RocksDB lock file will. If you have direct access to the persistent volumes, you can alternatively run chgrp -R g+w /pulsar/data before upgrading.

Here is a sample error you can expect if the RocksDB lock file is not correctly owned by the root group:

2022-05-14T03:45:06,903+0000  ERROR org.apache.bookkeeper.server.Main - Failed to build bookie server
java.io.IOException: Error open RocksDB database
    at org.apache.bookkeeper.bookie.storage.ldb.KeyValueStorageRocksDB.<init>(KeyValueStorageRocksDB.java:199) ~[org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
    at org.apache.bookkeeper.bookie.storage.ldb.KeyValueStorageRocksDB.<init>(KeyValueStorageRocksDB.java:88) ~[org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
    at org.apache.bookkeeper.bookie.storage.ldb.KeyValueStorageRocksDB.lambda$static$0(KeyValueStorageRocksDB.java:62) ~[org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
    at org.apache.bookkeeper.bookie.storage.ldb.LedgerMetadataIndex.<init>(LedgerMetadataIndex.java:68) ~[org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
    at org.apache.bookkeeper.bookie.storage.ldb.SingleDirectoryDbLedgerStorage.<init>(SingleDirectoryDbLedgerStorage.java:169) ~[org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
    at org.apache.bookkeeper.bookie.storage.ldb.DbLedgerStorage.newSingleDirectoryDbLedgerStorage(DbLedgerStorage.java:150) ~[org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
    at org.apache.bookkeeper.bookie.storage.ldb.DbLedgerStorage.initialize(DbLedgerStorage.java:129) ~[org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
    at org.apache.bookkeeper.bookie.Bookie.<init>(Bookie.java:818) ~[org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
    at org.apache.bookkeeper.proto.BookieServer.newBookie(BookieServer.java:152) ~[org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
    at org.apache.bookkeeper.proto.BookieServer.<init>(BookieServer.java:120) ~[org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
    at org.apache.bookkeeper.server.service.BookieService.<init>(BookieService.java:52) ~[org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
    at org.apache.bookkeeper.server.Main.buildBookieServer(Main.java:304) ~[org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
    at org.apache.bookkeeper.server.Main.doMain(Main.java:226) [org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
    at org.apache.bookkeeper.server.Main.main(Main.java:208) [org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
Caused by: org.rocksdb.RocksDBException: while open a file for lock: /pulsar/data/bookkeeper/ledgers/current/ledgers/LOCK: Permission denied
    at org.rocksdb.RocksDB.open(Native Method) ~[org.rocksdb-rocksdbjni-6.10.2.jar:?]
    at org.rocksdb.RocksDB.open(RocksDB.java:239) ~[org.rocksdb-rocksdbjni-6.10.2.jar:?]
    at org.apache.bookkeeper.bookie.storage.ldb.KeyValueStorageRocksDB.<init>(KeyValueStorageRocksDB.java:196) ~[org.apache.bookkeeper-bookkeeper-server-4.14.4.jar:4.14.4]
    ... 13 more

Recovering from helm upgrade error "unable to build kubernetes objects from current release manifest"

Example of the error message:

Error: UPGRADE FAILED: unable to build kubernetes objects from current release manifest:
[resource mapping not found for name: "pulsar-bookie" namespace: "pulsar" from "":
no matches for kind "PodDisruptionBudget" in version "policy/v1beta1" ensure CRDs are installed first,
resource mapping not found for name: "pulsar-broker" namespace: "pulsar" from "":
no matches for kind "PodDisruptionBudget" in version "policy/v1beta1" ensure CRDs are installed first,
resource mapping not found for name: "pulsar-zookeeper" namespace: "pulsar" from "":
no matches for kind "PodDisruptionBudget" in version "policy/v1beta1" ensure CRDs are installed first]

Helm documentation explains issues with managing releases deployed using outdated APIs when the Kubernetes cluster has been upgraded to a version where these APIs are removed. This happens regardless of whether the chart in the upgrade includes supported API versions. In this case, you can use the following workaround:

  1. Install the Helm mapkubeapis plugin:

    helm plugin install https://github.com/helm/helm-mapkubeapis
  2. Run the helm mapkubeapis command with the appropriate namespace and release name. In this example, we use the namespace "pulsar" and release name "pulsar":

    helm mapkubeapis --namespace pulsar pulsar

This workaround addresses the issue by updating in-place Helm release metadata that contains deprecated or removed Kubernetes APIs to a new instance with supported Kubernetes APIs and should allow for a successful Helm upgrade.

Uninstall

To uninstall the Pulsar Chart, run the following command:

helm uninstall <pulsar-release-name>

For the purposes of continuity, these charts have some Kubernetes objects that are not removed when performing helm uninstall. These items we require you to conciously remove them, as they affect re-deployment should you choose to.

  • PVCs for stateful data, which you must consciously remove
    • ZooKeeper: This is your metadata.
    • BookKeeper: This is your data.
    • Prometheus: This is your metrics data, which can be safely removed.
  • Secrets, if generated by our prepare release script. They contain secret keys, tokens, etc. You can use cleanup release script to remove these secrets and tokens as needed.

Troubleshooting

We've done our best to make these charts as seamless as possible, occasionally troubles do surface outside of our control. We've collected tips and tricks for troubleshooting common issues. Please examine these first before raising an issue, and feel free to add to them by raising a Pull Request!

VictoriaMetrics Troubleshooting

In example commands, k8s is namespace pulsar replace with your deployment namespace.

VictoriaMetrics Web UI

Connecting to vmsingle pod for web UI.

kubectl port-forward -n pulsar $(kubectl get pods -n pulsar -l app.kubernetes.io/name=vmsingle -o jsonpath='{.items[0].metadata.name}') 8429:8429

Now you can access the UI at http://localhost:8429 and http://localhost:8429/vmui (for similar UI as in Prometheus)

VictoriaMetrics Scraping debugging UI - Active Targets

Connection to vmagent pod for debugging targets.

kubectl port-forward -n pulsar $(kubectl get pods -n pulsar -l app.kubernetes.io/name=vmagent -o jsonpath='{.items[0].metadata.name}') 8429:8429

Now you can access the UI at http://localhost:8429

Active Targets UI

Scraping Configuration

Release Process

See RELEASE.md

Releases

Packages

Used by

Contributors

Languages