This article covers configuring the self-hosted Helm chart for a highly available (HA) deployment on AWS: no single point of failure in the pieces that keep Hudu itself reachable, and a safe path through upgrades. It assumes you're already familiar with a standard install — see "Deploying Hudu with the Helm chart" if you haven't installed the chart before.
What "highly available" means here
HA, for this chart, means the web tier and the database can survive a node loss, an AZ loss, or a rolling upgrade without an outage. That's the boundary the rest of this article is built around, and it's worth being explicit about what's inside it and what isn't:
- Web is stateless and scales horizontally — this is the piece the chart is designed to make redundant.
- The database is where your data actually lives, so it's the other piece that has to stay up (or fail over quickly) for Hudu to be up.
- Redis backs Sidekiq's job queues and ActionCable. It matters for Hudu to function well, but it isn't a system of record — see "Redis" below.
- The worker is not part of this boundary. As of this chart version it runs as a single replica, full stop — there's no HA story for it today. A worker outage delays background jobs (emails, imports, scheduled reports) until it recovers, but it does not take down the app: as long as web and the database are healthy, users can keep using Hudu. See "Worker/scheduler" below for why this is a hard chart constraint, not a tuning knob.
Reference architecture
┌─────────────────────┐
│ ALB / Ingress │
│ (multi-AZ, TLS term) │
└──────────┬───────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ web pod │ │ web pod │ │ web pod │ AZ-a / AZ-b / AZ-c
│ (replica) │ │ (replica) │ │ (replica) │ (replicaCount or HPA)
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└──────────────────┼──────────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ RDS for │ │ ElastiCache │ │ S3 │
│ PostgreSQL │ │ (Multi-AZ) │ │ (bucket) │
│ (Multi-AZ) │ └─────────────┘ └─────────────┘
└──────────────┘
┌───────────────┐
│ worker pod │ single replica — owns Sidekiq's
│ (singleton) │ cron/scheduler, not part of the
└───────────────┘ web/DB uptime boundary above
Everything above the dotted line between "web" and the datastores is what this chart manages directly. RDS, ElastiCache, and S3 are AWS-managed services the chart points at (externalDatabase.*, externalRedis.*, storage.type: s3) — it doesn't provision or operate them.
Web tier
Scale web.replicaCount manually, or turn on web.autoscaling.enabled to let a HorizontalPodAutoscaler do it based on CPU (and optionally memory) utilization:
web:
replicaCount: 3
# or:
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10A few things that matter once you're running more than one web pod:
-
storage.typemust bes3if you turn on autoscaling — the chart refuses to renderweb.autoscaling.enabled: truewithstorage.type: pvc, because there's no way to confirm at install time that your StorageClass actually supportsReadWriteManyacross nodes, and a silent failure there (uploads invisible across pods) is worse than a loud one at install. Use real S3, not the bundled MinIO (storage.s3.useLocalS3: true) — see "Object storage" below. -
A
PodDisruptionBudgetrenders automatically onceweb.replicaCount > 1or autoscaling is on (web.podDisruptionBudget, defaultminAvailable: 1) — this is what keeps a node drain or cluster upgrade from evicting every web pod at once. -
Spread replicas across AZs. The chart doesn't do this for you by default — set
web.affinitywith apodAntiAffinityrule (there's a commented example invalues.yaml) so the scheduler prefers placing web pods on different nodes/zones. -
If you're changing
web.autoscaling.enabledon a live release, read the README's "Autoscaling" section first — flipping it in either direction causes a one-time pod-count collapse if you don't setminReplicas/replicaCountto match your current pod count before the upgrade that flips it.
Worker/scheduler
worker.replicaCount must stay at 1 — the chart's template hard-fails the render if you set it to anything else:
worker.replicaCount must be 1 — the worker process owns Sidekiq's cron/scheduler; running more than one replica duplicates scheduled jobs.
There's also no PodDisruptionBudget on the worker — with only one replica to protect, a minAvailable: 1 budget would just block every voluntary node drain outright, for no HA benefit.
In practice: treat the worker as a component you can't make redundant yet, not as something you've misconfigured. If it goes down, background jobs queue up in Redis and process once it's back — that's a delay, not data loss, and it doesn't affect whether Hudu itself is reachable. A worker pod that (re)starts while Redis is unreachable is a separate, worse case — see the Redis section's callout below.
Database
Disable the bundled Postgres and point at a managed, highly-available instance instead:
postgresql: enabled: false externalDatabase: host: my-hudu-db.cluster-xxxxxxxxxx.us-east-1.rds.amazonaws.com port: 5432 username: hudu database: hudu_production existingSecret: hudu-db-credentials existingSecretPasswordKey: password
On AWS, that's Amazon RDS for PostgreSQL in Multi-AZ mode, or Aurora PostgreSQL if you want faster failover and read replicas. Either gives you the automatic primary/replica failover the bundled single-replica StatefulSet can't — the chart doesn't implement Postgres streaming replication or a connection router itself, so this is deliberately left to your managed database rather than something the chart takes on.
Prefer existingSecret over a plaintext externalDatabase.password — point it at a Secret populated from AWS Secrets Manager (e.g. via the External Secrets Operator) rather than committing the RDS password anywhere. The chart never reads the secret value itself at render time, only the Secret name/key, so this works the same way regardless of how the Secret gets populated.
Redis
Same pattern — disable the bundled Redis, point at a managed instance:
redis: enabled: false externalRedis: host: my-hudu-cache.xxxxxx.ng.0001.use1.cache.amazonaws.com port: 6379 existingSecret: hudu-redis-credentials existingSecretUrlKey: REDIS_URL
On AWS, that's Amazon ElastiCache — either the Redis OSS or Valkey engine, in a Multi-AZ replication group with automatic failover enabled. The chart has no Sentinel/cluster-mode integration of its own; it expects externalRedis.host to already be a single, reliable endpoint, which is exactly what an ElastiCache Multi-AZ primary endpoint gives you.
Redis doesn't need a backup plan. It holds Sidekiq's queues and ActionCable state — not customer data. If it's lost, whatever background jobs were in flight get dropped and need to be retriggered; nothing a client created is lost, and there's nothing to restore.
Multi-AZ failover minimizes a Redis outage — it doesn't eliminate the app's exposure to one. As of this app version, nothing that talks to Redis directly has a rescue or fallback around it: a Sidekiq enqueue call made from a web request (uploading a photo, refreshing a website check, starting a Hudini AI chat) raises straight out to a 500 if Redis is unreachable at that moment, rather than degrading gracefully or falling back to the database. /healthcheck also only checks Postgres, so pods stay in rotation and keep taking traffic through a Redis blip. Worse, a worker pod that (re)starts during that window will crash-loop — a startup hook enqueues a job unconditionally, and an unreachable Redis there kills the process outright rather than skipping and continuing. ElastiCache Multi-AZ still meaningfully shortens how long this window is (automatic failover vs. manual recovery), but plan for brief, real user-facing errors during it rather than assuming HA Redis makes this transparent.
Object storage
Use real S3, not the bundled evaluation-only MinIO:
storage:
type: s3
s3:
bucket: my-hudu-uploads
region: us-east-1If you're on EKS, leave storage.s3.accessKeyId/secretAccessKey/existingSecret all blank and annotate the ServiceAccount with an IRSA role instead:
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/hudu-s3-accessThe chart detects this and lets the AWS SDK's default credential chain resolve the role via the EKS Pod Identity webhook — no long-lived access keys in your values file. Turn on S3 versioning on the bucket if you want protection against accidental overwrite/delete; that's an S3 setting, not a chart one.
Migrations & upgrade safety
Migrations run in a dedicated Job (post-install,pre-upgrade Helm hook), not at container boot — this is what lets multiple web replicas start together without racing each other on db:prepare. On helm upgrade, the hook runs and completes before the new web/worker pods roll out, retrying up to migrate.backoffLimit times if the database isn't reachable yet.
What this means for a low-downtime upgrade: the chart's ordering guarantees the schema is migrated before new code starts, but it does not give you an automatic expand/contract pattern — the previous app version's web pods are still serving traffic against the database while the hook runs (a few seconds to a couple of minutes, depending on the migration). Write migrations that stay backward-compatible with the currently-running app version — add columns instead of renaming them in place, deploy code that tolerates both old and new schema, that kind of thing — the same discipline you'd want on any live database. If a migration fails outright, the hook fails and blocks the upgrade before any new pods roll, so a bad migration can't take down currently-running pods.
Ingress / load balancing / TLS
Nothing about HA changes the Ingress/TLS setup covered in "Deploying Hudu with the Helm chart" — the AWS Load Balancer Controller (ingress.className: alb) or your ingress controller of choice already handles routing across whichever web replicas are healthy, and across AZs, on its own. Multi-AZ here is a property of the load balancer and your EKS node groups, not something this chart configures.
Backups & disaster recovery
The database is the thing worth planning a real recovery process around — it's where the data is:
- RDS/Aurora: enable automated backups and set a retention window that matches your recovery requirements; both support point-in-time recovery out of the box. This replaces the "back up the bundled Postgres PVC" advice in the general setup article — you're no longer running the bundled StatefulSet at all.
- S3: versioning (see above) covers accidental overwrite/delete; the bucket itself doesn't need a separate backup process for HA purposes.
- Redis: no backup needed — see "Redis" above.
-
The generated-secrets Secret (
<release>-hudu-generated): still applies exactly as in the general setup article — back it up once, it's not reproducible from anything else.
Monitoring & health
Most of "Debugging" in the general setup article still applies unchanged. The HA-specific things worth alerting on:
-
HPA showing
<unknown>for its target metrics — meansweb.resources.requestsgot cleared; it renders fine but never scales. -
PodDisruptionBudget blocking a drain — expected behavior if a drain would take you below
minAvailable, not a bug; make sure you have enough replicas that a single node drain doesn't stall on it. -
The migrate Job failing (
kubectl logs job/<release>-hudu-migrate) — blocks the whole upgrade; check this first if ahelm upgradehangs. - RDS/ElastiCache failover events — watch these through CloudWatch alarms on your instances, not through the chart/cluster; a failover is usually a short connection blip the app retries through, but it's worth knowing when one happens.
Full example
Combining everything above into one values file:
config:
domain: hudu.example.com
postgresql:
enabled: false
externalDatabase:
host: my-hudu-db.cluster-xxxxxxxxxx.us-east-1.rds.amazonaws.com
port: 5432
username: hudu
database: hudu_production
existingSecret: hudu-db-credentials
existingSecretPasswordKey: password
redis:
enabled: false
externalRedis:
host: my-hudu-cache.xxxxxx.ng.0001.use1.cache.amazonaws.com
port: 6379
existingSecret: hudu-redis-credentials
existingSecretUrlKey: REDIS_URL
storage:
type: s3
s3:
bucket: my-hudu-uploads
region: us-east-1
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/hudu-s3-access
web:
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
ingress:
host: hudu.example.com
className: alb
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
tls:
enabled: truehelm upgrade --install my-hudu oci://public.ecr.aws/q4n0g0t7/hudu-helm-chart \ -f values-ha.yaml \ --set secrets.secretKeyBase=$(openssl rand -hex 64) \ --set secrets.passwordKey=$(openssl rand -hex 16) \ --set secrets.twoFactorKey=$(openssl rand -hex 16)
Related reading
- Deploying Hudu with Helm — basic install, TLS, day-to-day operations, common failure modes.