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.
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:
- Authentication and authorization for all components
- TLS encryption for all communication channels
- Proper network isolation and access controls
- 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.
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.
If you need to expose the Pulsar Proxy outside the cluster:
-
USE INTERNAL LOAD BALANCERS ONLY
- Set type to LoadBalancer only in secured environments with proper network controls
- Add cloud provider-specific annotations for internal load balancers:
- Kubernetes documentation about internal load balancers:
- See cloud provider documentation:
- AWS / EKS: AWS Load Balancer Controller / Service Annotations
- Azure / AKS: Use an internal load balancer with Azure Kubernetes Service (AKS)
- GCP / GKE: LoadBalancer service parameters
- Examples (verify correctness for your environment):
- AWS / EKS:
service.beta.kubernetes.io/aws-load-balancer-internal: "true" - Azure / AKS:
service.beta.kubernetes.io/azure-load-balancer-internal: "true" - GCP / GKE:
networking.gke.io/load-balancer-type: "Internal"
- AWS / EKS:
-
IMPLEMENT AUTHENTICATION AND AUTHORIZATION
- Configure all clients to authenticate properly
- Set up appropriate authorization policies
-
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.
-
NETWORK SECURITY
- Use private networks (VPCs)
- Configure firewalls, security groups, and IP restrictions
-
CLIENT IP ADDRESS BASED ACCESS RESTRICTIONS
- When using a LoadBalancer service type, restrict access to specific IP ranges by configuring
proxy.service.loadBalancerSourceRangesin 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
loadBalancerSourceRangesparameter
- 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
- When using a LoadBalancer service type, restrict access to specific IP ranges by configuring
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.
- 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
loadBalancerSourceRangesmay 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.
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.
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.
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
- self-signed
- Let's Encrypt
- TLS Encryption
- Proxy
- Broker
- Toolset
- Bookie
- ZooKeeper (requires the
AdditionalCertificateOutputFormats=truefeature 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)
- Automatically provisioned TLS certs, using Jetstack's cert-manager
- 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
In order to use this chart to deploy Apache Pulsar on Kubernetes, the followings are required.
- kubectl 1.25 or higher, compatible with your cluster (+/- 1 minor release from your cluster)
- Helm v3 (3.12.0 or higher)
- A Kubernetes cluster, version 1.25 or higher.
Before proceeding to deploying Pulsar, you need to prepare your environment.
helm and kubectl need to be installed on your computer.
To add this chart to your local Helm repository:
helm repo add apachepulsar https://pulsar.apache.org/charts
helm repo updateYou 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/
-
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 inexamples/.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
-
Install the chart:
helm install -n <namespace> --create-namespace <release-name> -f your-values.yaml apachepulsar/pulsar
-
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>
-
Access the Pulsar cluster
The default values will create a
ClusterIPfor 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.
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:
- Deploy a minimal single-node cluster
- Deploy ZooKeeper only as a configuration store
- Deploy a Pulsar cluster with an external configuration store
- Deploy a Pulsar cluster with local persistent volume
- Deploy a Pulsar cluster to Minikube
- Deploy a Pulsar cluster with no persistence
- Deploy a Pulsar cluster with TLS encryption (self-signed)
- Deploy a Pulsar cluster with TLS encryption (CA issuer)
- Deploy a Pulsar cluster with JWT authentication using symmetric key
- Deploy a Pulsar cluster with JWT authentication using asymmetric key
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.
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: RuntimeDefaultBoth 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.*.
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: OnRootMismatchfsGroup 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.
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: 1GisizeLimit 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.
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.
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: falseAdditionally, 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: falseThis is shown in some examples/values-disable-monitoring.yaml.
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.dekafproperty totruein the Helm releasevalues.yamlfile (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
- Open http://localhost:8090 in browser.
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.
Choose the metadata store when you first install the cluster. Disable ZooKeeper and enable Oxia in your
values.yaml:
components:
zookeeper: false
oxia: trueexamples/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.
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: falseThe 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: trueThis 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).
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
persistentVolumeClaimis aReadWriteOnceclaim on the cluster's defaultStorageClass— no extra configuration is required. -
Multiple broker replicas: the package directory must be on a
ReadWriteManyshared filesystem — a managed file service, not block storage (Persistent Disk / EBS / Azure Disk areReadWriteOnceand cannot be shared across replicas). Provision one with the matching cloud CSI driver, setpersistentVolumeClaim: {}so the chart does not create a claim, and pointclaimNameat the pre-created PVC:Cloud Shared file service to use CSI driver Reference GCP / GKE Filestore (managed NFS) filestore.csi.storage.gke.ioFilestore CSI AWS / EKS Amazon EFS (managed NFS) efs.csi.aws.comEFS CSI on EKS Azure / AKS Azure Files file.csi.azure.comAzure 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.
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
- The
apache/pulsarGitHub 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.
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/pulsarFor more detailed information, see our Upgrading guide.
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:
- Default Apache Pulsar version is now 5.0.0
- Pulsar Manager support has been removed
- X.509 certificate subject configuration has changed
Then check the other changes in behavior and the new configuration options.
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.14The 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.14There 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 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.
-
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: trueand 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
statsProviderClassinbookkeeper.configData, replaceorg.apache.pulsar.metrics.prometheus.bookkeeper.PrometheusMetricsProviderwithorg.apache.bookkeeper.stats.prometheus.PrometheusMetricsProvider. Otherwise, bookies fail to start. -
Configuration defaults: review your
configDataoverrides against the configuration default changes. Some settings have been removed, and explicitly set values keep their old behavior.
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.
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: falseAlso remove these keys if you set them, since they are no longer used:
- the whole
pulsar_manager:section images.pulsar_managerauth.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.
Dekaf is the supported web UI in this chart and the recommended replacement. Enable it with:
components:
dekaf: trueSee 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.
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: ""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 setscerts.internal_issuer.privateKey.rotationPolicy: Never, which keeps the CA key across renewals as with earlier cert-manager versions. Setcerts.internal_issuer.privateKey: nullto 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.httpand can be configured withautorecovery.probe. - ZooKeeper and broker upgrade cleanup Jobs: the pods of the
sts-cleanuppre-upgrade hook Jobs now get the chart's pod labels, and they use the component'snodeSelector,tolerations,priorityClassName,topologySpreadConstraints,affinity.nodeAffinityand the chart'simagePullSecrets, 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-initJob now uses the same ZooKeeper connection string as the other components, including the TLS port when ZooKeeper TLS is enabled.
- Pod and container security contexts: the global
podSecurityContextandcontainerSecurityContextvalues, and the per-component<component>.securityContextand<component>.containerSecurityContextoverrides, apply to every pod and container that the chart renders. SettingreadOnlyRootFilesystem: trueis also supported. See Pod and container security contexts andexamples/values-psa-restricted.yaml. - Node affinity: each component accepts
<component>.affinity.nodeAffinityto pin its pods to specific nodes. See Deploy Pulsar to Kubernetes. - Job pod annotations:
job.podAnnotationsadds annotations to the pods created by the chart's Jobs, for examplesidecar.istio.io/inject: "false". The existingjob.annotationsonly applies to the Job objects. - Certificate SAN mode:
tls.common.sanModeselects the subject alternative names (SANs) of the certificates that the chart creates.wildcard(the default) keeps the previous wildcard SANs.fqdnadds an explicit FQDN for each pod instead of the wildcard.noneadds only the service names, so you can add your own withtls.<component>.dnsNamesandtls.<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 thetls-combined.pemkey, so a publicly trusted certificate can be used for the proxy while the other components use certificates from a private CA. The## TLScomments invalues.yamldescribe the supported setups and the required Secret keys.
Note: Upgrading existing installs may cause a brief service disruption. The StatefulSet's
serviceNameis 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 StatefulSetserviceNamefor stable per-pod DNS.
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.
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-upgradehook lifecycle, which isn't always honored by GitOps tooling that renders the chart and applies the manifests directly. Verify that your tool runshelm.sh/hook: pre-upgradeJobs 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.
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.
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.
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.
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.
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: truePlease 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.
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/pulsarAfter, this you can proceed with helm upgrade.
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:
-
Install the Helm mapkubeapis plugin:
helm plugin install https://github.com/helm/helm-mapkubeapis
-
Run the
helm mapkubeapiscommand 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.
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.
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!
In example commands, k8s is namespace pulsar replace with your deployment namespace.
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:8429Now you can access the UI at http://localhost:8429 and http://localhost:8429/vmui (for similar UI as in Prometheus)
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:8429Now you can access the UI at http://localhost:8429
Active Targets UI
Scraping Configuration
See RELEASE.md