multi-componentledger.secrets.RABBITMQ_DEFAULT_PASS and ledger.secrets.RABBITMQ_CONSUMER_PASS (operator-provided), rabbitmq.authentication.erlangCookie.value while the bundled RabbitMQ is enabled (a stable value, e.g. openssl rand -hex 32; the chart refuses to render without it, since the broker cannot boot on an empty cookie), plus crm.secrets.LCRYPTO_HASH_SECRET_KEY and crm.secrets.LCRYPTO_ENCRYPT_SECRET_KEY (app crypto material). The standalone CRM defaults to crm.configmap.KMS_VENDOR: "none" (legacy mode on those keys, like the ledger); for envelope encryption set KMS_VENDOR: "hashicorp-vault" and your own KMS_VAULT_ADDR (the chart bundles no Vault and no longer defaults it to http://midaz-hc-vault:8200). With a 4.x ledger.image.tag and KMS_VENDOR=none, ledger.secrets.LCRYPTO_HASH_SECRET_KEY and ledger.secrets.LCRYPTO_ENCRYPT_SECRET_KEY are required too: the unified binary serves CRM in-process and initializes its cipher from them at boot. LCRYPTO_ENCRYPT_SECRET_KEY must be hex encoding an AES key of 16, 24, or 32 bytes (32, 48, or 64 hex characters); the chart rejects anything else at render time. A 4.x ledger also refuses to boot with ENV_NAME=production (the default) unless PLUGIN_AUTH_ENABLED=true, so the chart refuses that pair at render time: enable auth (global.auth.enabled or ledger.configmap.PLUGIN_AUTH_ENABLED) or set a non-production global.env.name. The database, replica, and cache passwords (DB_ONBOARDING_PASSWORD, DB_ONBOARDING_REPLICA_PASSWORD, MONGO_ONBOARDING_PASSWORD, DB_TRANSACTION_PASSWORD, DB_TRANSACTION_REPLICA_PASSWORD, MONGO_TRANSACTION_PASSWORD, REDIS_PASSWORD, crm.secrets.MONGO_PASSWORD) are single-sourced from the bundled datastores’ Secrets (see Dependency notes) and are only required when the matching backend is external.<release>-postgresql key password/replication-password, <release>-mongodb key mongodb-root-password), Valkey’s from the bundled Bitnami subchart Secret (<release>-valkey key valkey-password); all are injected into the ledger/crm workloads via secretKeyRef. RabbitMQ and optional OpenTelemetry are also bundled. The bundled RabbitMQ gives its only users, transaction and consumer, the ledger’s RABBITMQ_DEFAULT_PASS and RABBITMQ_CONSUMER_PASS (from ledger.secrets, or from ledger.existingSecretName, which the chart reads at render: that path needs helm install/helm upgrade against the cluster, and helm template or Argo CD refuse it). When a backend is external (<subchart>.enabled=false/.external=true), supply its password through the component secrets block or point the subchart at an auth.existingSecret.secrets (or useExistingSecret); let the chart own the database/cache passwords (or set <subchart>.auth.existingSecret/<subchart>.auth.password). Override image tags, ingress, resources, namespace, and persistence as needed.github.com/LerianStudio/helm; license is Elastic License 2.0.Release name: The single-source Secret names are resolved collapse-aware from the bundled subcharts (
common.names.dependency.fullname), so they follow any release name automatically — nomidazassumption. The bundled service hosts inconfigmap(e.g.midaz-postgresql-primary,midaz-mongodb,midaz-valkey-primary,midaz-rabbitmq), however, are static literals that assume the release is namedmidaz(helm install midaz ...). If you install under a different release name, override the*_HOSTentries underledger.configmap/crm.configmap(or the equivalentglobal.datastores.*host masks) accordingly. Two more names do not follow the release name: the component Services are alwaysmidaz-ledger,midaz-crmandmidaz-tracer(themidazprefix comes fromnameOverride, defaultmidaz), so aTRACER_BASE_URLpoints atmidaz-tracer:<port>whatever the release is called; and the tracer’s ownDB_HOSTdoes follow the release name (see the Tracer section). Mixed naming is therefore expected under a custom release name.
Uninstall keeps the data.
helm uninstallleaves the bundled MongoDB volume (<release>-mongodb), the PostgreSQL volumes (data-<release>-postgresql-*) and both password Secrets (<release>-postgresql,<release>-mongodb), so a reinstall under the same release name opens the same data with the same passwords. Deleting the data is a separate, manual step: delete those PVCs and Secrets. Deleting only the Secrets resets nothing: a reinstall generates new passwords that the kept data never learned.
Source code can be found here:
This helm chart installs Midaz, a high-performance ledger.
The default installation is similar to the one provided in the Midaz repo.
global.cloud)Point ledger/crm/tracer at a managed-cloud environment (AWS/GCP/Azure) instead of the bundled in-cluster PostgreSQL/MongoDB/Valkey/RabbitMQ, with one knob:
global:
cloud: "aws" # aws | gcp | azure — leave unset for the bundled dev topology
datastores:
postgresOnboarding: { host: "my-rds.example.com", user: "midaz" }
postgresTransaction: { host: "my-rds.example.com", user: "midaz" }
mongoOnboarding: { host: "my-docdb.example.com", user: "midaz" }
mongoTransaction: { host: "my-docdb.example.com", user: "midaz" }
mongoCrm: { host: "my-docdb.example.com", user: "midaz" }
redis: { host: "my-elasticache.example.com", port: "6379" }
broker: { host: "my-amazonmq.example.com" }
env:
name: "production"
observability:
enabled: true
auth:
host: "http://plugin-access-manager-auth:4000"
global.cloud sets the connection TOPOLOGY (TLS, AMQP scheme/ports, sslmode,
DocumentDB connection params) for the masks above. Per-module identity
(host/port/user — postgresOnboarding/postgresTransaction/mongoOnboarding/
mongoTransaction/mongoCrm/mongoFees) always comes from global.datastores
or a dedicated ledger.datastores/crm.datastores/tracer.datastores override
— a cloud preset can’t know your RDS host. TOPOLOGY-only fields that a cloud
preset actually sets (postgres.ssl, mongo.params, redis.tls,
broker.scheme/amqpPort/port) are shared across every module through the
generic postgres/mongo/redis/broker keys, since onboarding/transaction/
crm/fees share one physical Postgres/Mongo engine in the overwhelming majority
of real deployments even though their host/port/user can differ per module.
Precedence (highest to lowest): native <component>.configmap.<KEY> >
dedicated <component>.datastores (this component’s own instance) > shared
global.datastores (env-wide) > global.cloud topology preset (for the
TOPOLOGY-only fields above) > chart default.
REDIS_HOST combines global.datastores.redis.host and .port (or the
matching dedicated-tier fields) into the single host:port string the ledger
service expects (e.g. host: "my-elasticache.example.com" + port: "6380" →
REDIS_HOST=my-elasticache.example.com:6380); a native
ledger.configmap.REDIS_HOST override is expected to already carry its own
host:port and is used verbatim.
ENV_NAME is resolved once per component via global.env.name and reused as
the default for that component’s OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT, so
the two can’t drift out of sync; a native <component>.configmap.ENV_NAME (or
.OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT) still overrides independently.
The ledger’s dedicated migration Job (templates/ledger/migrations-job.yaml)
resolves its own DB_ONBOARDING_*/DB_TRANSACTION_* connection through the
SAME masks as the ledger ConfigMap, so an operator who repoints the database
via global.datastores/ledger.datastores migrates the exact host/db the app
then connects to.
Copy values-template.yaml as your starting point — it documents every
global.* mask with a working example. values.yaml is the full power-user
reference; values.schema.json validates it.
To install Midaz using Helm, run:
$ helm install midaz oci://registry-1.docker.io/lerianstudio/midaz-helm --version <version> -n midaz --create-namespace
Replace <version> with the desired chart version. This creates a namespace called midaz if it doesn’t already exist and deploys the Midaz Helm chart.
After installation, you can verify that the release was successful by listing the Helm releases in the midaz namespace:
$ helm list -n midaz
The Midaz Helm Chart supports different Ingress Controllers for exposing services. Ingress is configured per component, under ledger.ingress, crm.ingress and tracer.ingress; the chart has no top-level ingress: key, and one placed there is silently ignored (no Ingress is rendered). The examples below use ledger.ingress; the same fields apply to the other two components. Below are configurations for commonly used controllers.
Note: Before configuring Ingress, ensure that you have an Ingress Controller installed in your cluster. The Ingress Controller manages external access to services. Examples include NGINX, AWS ALB, and Traefik.
To use the NGINX Ingress Controller, configure the values.yaml as follows:
ledger:
ingress:
enabled: true
className: "nginx"
# The `annotations` field adds custom metadata to the Nginx resource.
# Annotations are key-value pairs that augment the behavior of the Nginx resource.
# See https://github.com/kubernetes/ingress-nginx/blob/main/docs/user-guide/nginx-configuration/annotations.md
annotations: {}
hosts:
- host: midaz.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: midaz-tls # Ensure this secret exists or is managed by cert-manager
hosts:
- midaz.example.com
For AWS ALB Ingress Controller, use the following configuration:
ledger:
ingress:
enabled: true
className: "alb"
annotations:
alb.ingress.kubernetes.io/scheme: internal # Use "internet-facing" for public ALB
alb.ingress.kubernetes.io/target-type: ip # Use "instance" if targeting EC2 instances
alb.ingress.kubernetes.io/group.name: "midaz" # Group ALB resources under this name
alb.ingress.kubernetes.io/healthcheck-path: "/health" # Health check path (the ledger serves /health and /readyz)
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]' # Listen on HTTP and HTTPS
hosts:
- host: midaz.example.com
paths:
- path: /
pathType: Prefix
tls: [] # TLS is managed by the ALB using ACM certificates
For Traefik, configure the values.yaml as follows:
ledger:
ingress:
enabled: true
className: "traefik"
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: "web, websecure" # Entrypoints defined in Traefik
traefik.ingress.kubernetes.io/router.tls: "true" # Enable TLS for this route
hosts:
- host: midaz.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: midaz-tls # Ensure this secret exists and contains the TLS certificate
hosts:
- midaz.example.com
global.scheduling.spread)Every Deployment of this chart (ledger, crm and tracer) renders
topologySpreadConstraints from the lerian-common spread preset. The default is
soft (ScheduleAnyway) across nodes, so replicas of the same
component are spread across nodes (e.g. spot nodes)
whenever possible, but a pod is never left Pending because of it.
Zone spreading is off by default (zone: ""). When scoring a soft spread, the
scheduler skips every node that lacks the topology.kubernetes.io/zone label, so on
clusters without zone labels (bare-metal, k3s) a zone constraint silently cancels the
node spread. Turn it on (zone: ScheduleAnyway) where every node carries zone labels
(EKS, GKE, AKS).
global:
scheduling:
spread:
enabled: true # master switch
hostname: ScheduleAnyway # kubernetes.io/hostname: ScheduleAnyway | DoNotSchedule | "" (off)
zone: "" # topology.kubernetes.io/zone: same values (off by default)
maxSkew: 1 # integer >= 1
minDomains: 0 # DoNotSchedule constraints only; 0 = off
nodeTaintsPolicy: "" # Honor | Ignore | "" (Kubernetes default Ignore)
ledger:
spread: { hostname: DoNotSchedule } # field-level override of the global preset
topologySpreadConstraints: [] # raw list; non-empty replaces the preset entirely
On EKS + Karpenter use a hard node spread:
global:
scheduling:
spread:
hostname: DoNotSchedule
zone: ScheduleAnyway
maxSkew: 1
minDomains: 2
nodeTaintsPolicy: Honor
minDomains: 2 is required with DoNotSchedule. Skew is measured only against nodes
that already exist, so on a single eligible node all replicas would otherwise share
it. With fewer than 2 nodes the extra replica stays Pending until Karpenter launches
another node. nodeTaintsPolicy: Honor stops tainted nodes (for example a node being
drained) from counting as domains.
Precedence: <component>.topologySpreadConstraints (non-empty, wins entirely) >
<component>.spread.<field> > global.scheduling.spread.<field> > off. Each
preset constraint selects only the component’s own pods
(labelSelector = the Deployment’s spec.selector.matchLabels) of the same
ReplicaSet (matchLabelKeys: [pod-template-hash]), so a rolling update is never
blocked by the old ReplicaSet’s pods, even with DoNotSchedule. Requires
Kubernetes >= 1.27 (matchLabelKeys). Use DoNotSchedule only when the cluster
can always provide enough distinct nodes/zones for the replica count (a hard
constraint leaves extra pods Pending otherwise). Jobs (migrations, bootstrap) keep their nodeSelector / affinity / tolerations and are not spread.
Upgrade note: the preset is on by default, so upgrading to the chart version
that introduces it adds topologySpreadConstraints to the pod template of every
Deployment and triggers one rolling restart of each. No values change is
required. To keep the previous pod template exactly, set
global.scheduling.spread.enabled: false.
Midaz deploys the following core services:
| Parameter | Description | Default Value |
|---|---|---|
ledger.enabled |
Enable or disable the ledger service. | true |
ledger.name |
Service name. | "ledger" |
ledger.replicaCount |
Number of replicas for the ledger service. | 2 |
ledger.terminationGracePeriodSeconds |
Seconds Kubernetes waits after SIGTERM before killing the pod; must cover the shutdown drain (HTTP, streaming events, producer flush). tracer.terminationGracePeriodSeconds works the same way (default 55). |
72 |
ledger.image.repository |
Repository for the ledger service container image. | "lerianstudio/midaz-ledger" |
ledger.image.pullPolicy |
Image pull policy. | "IfNotPresent" |
ledger.image.tag |
Image tag used for deployment. | See values.yaml |
ledger.imagePullSecrets |
Secrets for pulling images from a private registry. | [] |
ledger.nameOverride |
Overrides the default generated name by Helm. | "" |
ledger.fullnameOverride |
Overrides the full name generated by Helm. | "" |
ledger.podAnnotations |
Pod annotations for additional metadata. | {} |
ledger.podSecurityContext |
Security context applied at the pod level. | {} |
ledger.securityContext.* |
Security context for every ledger container: the application, its wait-for-dependencies init container and the migration Job. |
See values.yaml |
ledger.pdb.enabled |
Specifies whether PodDisruptionBudget is enabled. | true |
ledger.pdb.maxUnavailable |
Maximum number of unavailable pods; ignored when minAvailable is set. |
1 |
ledger.pdb.annotations |
Annotations for the PodDisruptionBudget. | {} |
ledger.deploymentUpdate.* |
Deployment update strategy. | See values.yaml |
ledger.service.type |
Kubernetes service type. | "ClusterIP" |
ledger.service.port |
Port for the HTTP API. | 3002 |
ledger.service.annotations |
Annotations for the service. | {} |
ledger.ingress.enabled |
Specifies whether Ingress is enabled. | false |
ledger.ingress.className |
Ingress class name. | "" |
ledger.ingress.annotations |
Additional ingress annotations. | {} |
ledger.ingress.hosts |
Configured hosts for Ingress and associated paths. | [] |
ledger.ingress.tls |
TLS configurations for Ingress. | [] |
ledger.resources.* |
CPU/Memory resource requests/limits. | See values.yaml |
ledger.autoscaling.enabled |
Specifies whether autoscaling is enabled. | true |
ledger.autoscaling.minReplicas |
Minimum number of replicas for autoscaling. | 2 |
ledger.autoscaling.maxReplicas |
Maximum number of replicas for autoscaling. | 9 |
ledger.autoscaling.targetCPUUtilizationPercentage |
Target CPU utilization percentage for autoscaling. | 80 |
ledger.autoscaling.targetMemoryUtilizationPercentage |
Target memory utilization percentage for autoscaling. | 80 |
ledger.spread |
Per-component override of global.scheduling.spread (fields: enabled, hostname, zone, maxSkew). |
{} |
ledger.topologySpreadConstraints |
Raw topologySpreadConstraints; non-empty replaces the spread preset. | [] |
ledger.nodeSelector |
Node selectors for pod scheduling. | {} |
ledger.tolerations |
Tolerations for pod scheduling. | [] |
ledger.affinity |
Affinity rules for pod scheduling. | {} |
ledger.configmap.* |
Environment variables for the service. | See values.yaml |
ledger.secrets.* |
Secrets for the service. | See values.yaml |
ledger.useExistingSecret |
Use an existing secret instead of creating a new one. | false |
ledger.existingSecretName |
The name of the existing secret to use. | "" |
ledger.extraEnvVars |
A list of extra environment variables. | [] |
ledger.serviceAccount.create |
Specifies whether the service account should be created. | true |
ledger.serviceAccount.annotations |
Annotations for the service account. | {} |
ledger.serviceAccount.name |
Service account name. If not defined, it will be generated automatically. | "" |
ledger.aws.rolesAnywhere.enabled |
Enable the AWS IAM Roles Anywhere sidecar on the ledger, for clusters without IRSA. The multi-tenant ledger reads its per-tenant Tracer M2M credential from Secrets Manager. On EKS keep it false and set ledger.serviceAccount.annotations["eks.amazonaws.com/role-arn"]; never enable both. When enabled, trustAnchorArn, profileArn and roleArn are required, the client certificate Secret (certificateSecretName, default <midaz-ledger.fullname>-iam-tls) must be provisioned outside the chart, and ledger.podSecurityContext is replaced with fsGroup: 65532. Set AWS_REGION in ledger.extraEnvVars. |
false |
If you want to use an existing Kubernetes Secret for the ledger service, you can create it manually with the following command:
kubectl create secret generic midaz-ledger \
--from-literal=DB_ONBOARDING_PASSWORD='<your-db-onboarding-password>' \
--from-literal=DB_ONBOARDING_REPLICA_PASSWORD='<your-db-onboarding-replica-password>' \
--from-literal=MONGO_ONBOARDING_PASSWORD='<your-mongo-onboarding-password>' \
--from-literal=DB_TRANSACTION_PASSWORD='<your-db-transaction-password>' \
--from-literal=DB_TRANSACTION_REPLICA_PASSWORD='<your-db-transaction-replica-password>' \
--from-literal=MONGO_TRANSACTION_PASSWORD='<your-mongo-transaction-password>' \
--from-literal=REDIS_PASSWORD='<your-redis-password>' \
--from-literal=RABBITMQ_DEFAULT_PASS='<your-rabbitmq-password>' \
--from-literal=RABBITMQ_CONSUMER_PASS='<your-rabbitmq-consumer-password>' \
-n midaz
Note: The ledger service uses module-specific database credentials (onboarding and transaction) since it combines both modules. The DB_*, MONGO_*, and REDIS_PASSWORD keys above are only read when the matching backend is external; for the bundled Bitnami subcharts those passwords are single-sourced from the subchart Secret via secretKeyRef, so the keys in this secret are ignored. Only RABBITMQ_DEFAULT_PASS, RABBITMQ_CONSUMER_PASS, and (for external backends) the infra passwords are required.
Then configure the ledger service to use this existing secret:
ledger:
enabled: true
useExistingSecret: true
existingSecretName: "midaz-ledger"
To enable the ledger service:
ledger:
enabled: true
The crm service provides APIs for managing holder data and their relationships with ledger accounts. Previously available as a separate chart (plugin-crm) deployed in the midaz-plugins namespace, the CRM is being migrated to become a core component of Midaz, now deployed in the midaz namespace.
For more details, refer to the official documentation: CRM Documentation
Migration Note: If you are currently using
plugin-crmin themidaz-pluginsnamespace, we recommend migrating to this new integrated CRM workload. See the Upgrade Guide for migration steps.
| Parameter | Description | Default Value |
|---|---|---|
crm.enabled |
Enable or disable the CRM service. | false |
crm.name |
Service name. | "crm" |
crm.replicaCount |
Number of replicas for the CRM service. | 1 |
crm.image.repository |
Repository for the CRM service container image. | "lerianstudio/midaz-crm" |
crm.image.pullPolicy |
Image pull policy. | "Always" |
crm.image.tag |
Image tag used for deployment. | See values.yaml |
crm.imagePullSecrets |
Secrets for pulling images from a private registry. | [] |
crm.nameOverride |
Overrides the default generated name by Helm. | "" |
crm.fullnameOverride |
Overrides the full name generated by Helm. | "" |
crm.podAnnotations |
Pod annotations for additional metadata. | {} |
crm.podSecurityContext |
Security context applied at the pod level. | {} |
crm.securityContext.* |
Defines security context settings for the container. | See values.yaml |
crm.pdb.enabled |
Specifies whether PodDisruptionBudget is enabled. | true |
crm.pdb.maxUnavailable |
Maximum number of unavailable pods; ignored when minAvailable is set. |
1 |
crm.pdb.annotations |
Annotations for the PodDisruptionBudget. | {} |
crm.deploymentUpdate.type |
Type of deployment strategy. | "RollingUpdate" |
crm.deploymentUpdate.maxSurge |
Maximum number of pods that can be created over the desired number of pods. | 1 |
crm.deploymentUpdate.maxUnavailable |
Maximum number of pods that can be unavailable during the update. | 0 |
crm.service.type |
Kubernetes service type. | "ClusterIP" |
crm.service.port |
Service port. | 4003 |
crm.ingress.enabled |
Specifies whether Ingress is enabled. | false |
crm.ingress.className |
Ingress class name. | "" |
crm.ingress.annotations |
Additional ingress annotations. | {} |
crm.ingress.hosts |
Configured hosts for Ingress and associated paths. | [] |
crm.ingress.tls |
TLS configurations for Ingress. | [] |
crm.resources.* |
CPU/Memory resource requests/limits. | See values.yaml |
crm.autoscaling.enabled |
Specifies whether autoscaling is enabled. | true |
crm.autoscaling.minReplicas |
Minimum number of replicas for autoscaling. | 1 |
crm.autoscaling.maxReplicas |
Maximum number of replicas for autoscaling. | 3 |
crm.autoscaling.targetCPUUtilizationPercentage |
Target CPU utilization percentage for autoscaling. | 80 |
crm.autoscaling.targetMemoryUtilizationPercentage |
Target memory utilization percentage for autoscaling. | 80 |
crm.spread |
Per-component override of global.scheduling.spread (fields: enabled, hostname, zone, maxSkew). |
{} |
crm.topologySpreadConstraints |
Raw topologySpreadConstraints; non-empty replaces the spread preset. | [] |
crm.nodeSelector |
Node selectors for pod scheduling. | {} |
crm.tolerations |
Tolerations for pod scheduling. | [] |
crm.affinity |
Affinity rules for pod scheduling. | {} |
crm.configmap.* |
Environment variables for the service. | See values.yaml |
crm.secrets.* |
Secrets for the service. | See values.yaml |
crm.useExistingSecret |
Use an existing secret instead of creating a new one. | false |
crm.existingSecretName |
The name of the existing secret to use. | "" |
crm.extraEnvVars |
A list of extra environment variables. | {} |
If you want to use an existing Kubernetes Secret for the CRM service, you can create it manually with the following command:
kubectl create secret generic midaz-crm \
--from-literal=LCRYPTO_HASH_SECRET_KEY='<your-hash-secret-key>' \
--from-literal=LCRYPTO_ENCRYPT_SECRET_KEY='<your-encrypt-secret-key>' \
--from-literal=MONGO_PASSWORD='<your-mongo-password>' \
-n midaz
Note: MONGO_PASSWORD is only read when MongoDB is external; for the bundled MongoDB it is single-sourced from the chart’s <release>-mongodb Secret via secretKeyRef, so the key in this secret is ignored.
Then configure the CRM service to use this existing secret:
crm:
enabled: true
useExistingSecret: true
existingSecretName: "midaz-crm"
To enable the CRM service:
crm:
enabled: true
configmap:
MONGO_HOST: "midaz-mongodb" # Use your MongoDB host
MONGO_NAME: "crm"
MONGO_USER: "midaz"
# MONGO_PASSWORD is single-sourced from the chart's bundled-MongoDB Secret
# (Secret `midaz-mongodb`, key `mongodb-root-password`) — only set it here when
# using an EXTERNAL MongoDB (mongodb.enabled=false / mongodb.external=true).
# secrets:
# MONGO_PASSWORD: "<your-mongo-password>"
The tracer service provides real-time transaction validation and fraud prevention. From midaz v4 it ships from the monorepo (components/tracer) and is published as lerianstudio/midaz-tracer; the standalone lerianstudio/tracer 1.x image predates this configuration contract. It is disabled by default (tracer.enabled=false) and existing releases render unchanged.
midaz role that owns onboarding and transaction. tracer.configmap.DB_HOST is empty by default and resolves to this release’s PostgreSQL primary Service (<release>-postgresql-primary, or <release>-postgresql in the standalone topology), so a release name other than midaz still connects; set it explicitly for an external PostgreSQL, where the render fails rather than guessing. Its database (tracer.configmap.DB_NAME, default tracer) is created by files/midaz/init.sql on a fresh internal cluster, and by the external bootstrap Job (global.externalPostgresDefinitions.enabled=true) otherwise; both paths use the configured name. On a pre-existing internal cluster, initdb scripts no longer run: create the configured database once by hand before enabling tracer (CREATE DATABASE tracer; with the default tracer.configmap.DB_NAME).tracer.migrations and ledger.migrations render the dedicated midaz-tracer-migrations / midaz-ledger-migrations runner Jobs, whose tag defaults to the matching application tag. The tracer Job renders when both tracer.enabled and tracer.migrations.enabled (default true) are true; the ledger Job is on for 4.x ledger tags only (ledger.migrations.enabled pins it either way). Setting either migrations.enabled to false means the schema must be applied out of band, because the v4 services no longer migrate themselves. Both Jobs are plain Sync-phase resources because the bundled PostgreSQL Secret they read is only created during Sync; while a Job renders, its Deployment carries argocd.argoproj.io/sync-wave: "1" (<component>.migrations.deploymentSyncWave) so Argo CD applies the service only after the Job reports Complete. Against a pre-provisioned database, set argocd.argoproj.io/hook: PreSync under <component>.migrations.annotations instead.Ledger to tracer transport. The chart renders ledger.configmap.TRACER_TRANSPORT as empty by default, which the ledger resolves to gRPC. It only matters when the integration is on: with TRACER_BASE_URL empty (the default) the ledger never reads it. What the ledger accepts depends on its application version (checked against components/ledger/internal/bootstrap/config.go at each tag):
| Ledger app version | TRACER_TRANSPORT unset or empty |
grpc |
rest |
|---|---|---|---|
| up to 4.1.x | gRPC | accepted | accepted (fallback) |
| 4.2.0 and later (this chart’s default image tag) | gRPC | accepted | refused at boot: TRACER_TRANSPORT=rest is no longer supported |
The 4.2+ ledger is gRPC-only, so the render fails when TRACER_BASE_URL is set and TRACER_TRANSPORT is rest (matched case-insensitively and trimmed, as the ledger does). Leave it empty or set grpc. The tracer at 4.2.0 or later always listens on gRPC (:4021 when TRACER_GRPC_PORT is unset), and the chart always publishes that port as grpc on the tracer container and Service: the numeric tail of tracer.configmap.TRACER_GRPC_PORT when set, else tracer.service.grpcPort (4021). The minimal combination for a bundled tracer at app tag 4.2.x:
tracer:
enabled: true
ledger:
configmap:
TRACER_BASE_URL: "midaz-tracer:4021" # gRPC Service port; the Service name is fixed, see the release-name note
TRACER_TRANSPORT: "" # empty resolves to gRPC; "grpc" is equivalent
With the integration on, PLUGIN_AUTH_ENABLED=true and MULTI_TENANT_ENABLED=false, the render also fails unless ledger.configmap.TRACER_M2M_CLIENT_ID and ledger.secrets.TRACER_M2M_CLIENT_SECRET are set, mirroring the ledger’s boot check. With ledger.useExistingSecret=true the chart cannot see the secret, so only the client id is checked and the ledger enforces the secret at boot. Multi-tenant reads the identity per tenant (M2M_SECRETS_BACKEND) and is not checked. Which seam identity and TLS mode to use (TRACER_API_KEY, the TRACER_M2M_* pair, TRACER_TLS_MODE) follows the tracer’s API_KEY_ENABLED / PLUGIN_AUTH_ENABLED settings; see the app’s components/tracer/README.md for the seam rules. A live ledger-to-tracer connection has not been verified with this chart.
tracer.configmap.GOMEMLIMIT must track tracer.resources.limits.memory (~90%). The image ships GOMEMLIMIT=1800MiB, sized for a 2Gi container, so without the override the Go heap outgrows a 512Mi cgroup and the pod is OOM-killed.helm template instead of crash-looping: API_KEY_ENABLED=true requires an API key and rejects wildcard CORS; MULTI_TENANT_ENABLED=true requires PLUGIN_AUTH_ENABLED=true, a service API key, and rejects API_KEY_ENABLED_ONLY_VALIDATION=true; tracer.useExistingSecret=true requires tracer.existingSecretName. Booleans are matched the way the runtime parses them (strconv.ParseBool), so TRUE, True, 1, t and T are treated as enabled here too.tracer:
enabled: true
configmap:
API_KEY_ENABLED: "true"
CORS_ALLOWED_ORIGINS: "https://app.example.com"
secrets:
API_KEY: "<your-api-key>"
# DB_PASSWORD is single-sourced from the chart's bundled-PostgreSQL Secret
# (Secret `midaz-postgresql`, key `password`) — only set it here when using an
# EXTERNAL PostgreSQL (postgresql.enabled=false / postgresql.external=true).
Images. Every image this chart references is expected to be pullable anonymously, so no
imagePullSecretsare needed for a default install; anonymous manifest requests for the ledger, ledger-migrations, tracer, tracer-migrations and CRM tags invalues.yamlreturned HTTP 200 on Docker Hub when this was last checked. If you mirror the images to a private registry, setimagePullSecretsandimage.repositoryon each component.
This chart deploys no observability stack. It creates no Grafana Deployment, Service or Ingress, and it bundles no collector.
The application workloads export OpenTelemetry traces and metrics to a
collector you run yourself. That export is configured with
otel-collector-lerian.enabled — see
OpenTelemetry Collector wiring for the
supported settings and for how to point the workloads at a collector that is
not node-local.
This chart includes the following dependencies for the default installation. All dependencies are enabled by default.
valkey.enabled to false in the values file.Note: If you have an existing Valkey or Redis instance, you can disable this dependency and configure Midaz Components to use your external instance, like this:
ledger:
configmap:
REDIS_HOST: { your-host }:{ your-host-port }
secrets:
REDIS_PASSWORD: { your-host-pass }
deferrable fees. An eviction or a lost write loses that state: a retried request can post twice, and an open fee debt is a receivable gone. The bundled Valkey therefore:
appendonly yes from the subchart configuration) on its own volume, the claim valkey-data-midaz-valkey-primary (8Gi, ReadWriteOnce; valkey.primary.persistence.size and valkey.primary.persistence.storageClass change it). The claim is kept on helm uninstall, like the PostgreSQL and MongoDB volumes. Under Argo CD it survives deleting the application, but a sync with pruning deletes it once it leaves the rendered manifests (Valkey or its persistence turned off, kind changed, a sync to an older chart). To keep it then too, add argocd.argoproj.io/sync-options: Prune=false to valkey.primary.persistence.annotations (default kind: Deployment; Helm merges maps, so the chart’s helm.sh/resource-policy: keep stays). A restart reloads the file.--appendfsync always: Valkey writes each change to disk before it answers, so a host crash loses no write the Ledger saw succeed. Under everysec, the Valkey default, a host crash loses the last second of writes, which can fork a balance from its operations, drop a fee debt or let a retry post twice. Every Ledger request that writes waits for the disk, so a slow volume shows in its latency.--maxmemory-policy noeviction. A full Valkey refuses new writes and the Ledger answers those requests with an error; no key is ever evicted.Recreate strategy, so an upgrade that changes the Valkey pod stops the old pod before the new one starts and two pods never write the same file. The Ledger answers errors until the new pod is ready. With valkey.primary.kind: StatefulSet, set valkey.primary.updateStrategy.type: RollingUpdate: a StatefulSet refuses Recreate.Sizing: in the Lua engine (the default), --maxmemory must be at least 14 KB × (approvals per second × X-TTL + open PENDING transactions) + 1 KB × balances used in the last 24 hours, plus room for the open fee-debt lists and the balance-sync schedule. The client sets X-TTL per request (300 seconds by default, at most 604800), so use the longest one your clients send. At the default X-TTL and with few balances, 640mb holds about 150 approvals per second; about 600,000 balances used in a day fill it alone. The 14 KB is a two-leg transaction with little metadata: more legs, fees and metadata hold more, so measure your own with MEMORY USAGE. When you raise --maxmemory, raise the pod memory too (valkey.primary.resourcesPreset or valkey.primary.resources), leaving room for the copy an AOF rewrite makes, and raise valkey.primary.persistence.size to about three times --maxmemory, which the append-only file can reach during a rewrite. Growing an existing claim needs a StorageClass with allowVolumeExpansion. extraFlags is a list: setting it replaces the chart default, so restate every flag you want to keep:
valkey:
primary:
extraFlags:
- "--maxmemory { your-size }"
- "--maxmemory-policy noeviction"
- "--appendfsync always"
- "--hz 100"
- "--maxclients 10000"
With valkey.primary.kind: StatefulSet (not the default), Kubernetes refuses a new persistence.size, because it changes the StatefulSet’s claim template. Set the new size (16Gi here) in your values, grow the claim itself, and delete the StatefulSet but not its pod or claim, so the next upgrade or Argo CD sync creates it again with the new template:
kubectl -n midaz patch pvc valkey-data-midaz-valkey-primary-0 \
-p '{"spec":{"resources":{"requests":{"storage":"16Gi"}}}}'
kubectl -n midaz delete statefulset midaz-valkey-primary --cascade=orphan
With an external Valkey or Redis instance, configure the same persistence, appendfsync always included, and maxmemory-policy noeviction on that instance. A volatile-* policy keeps fee debts, which carry no TTL, but still evicts idempotency keys.
postgresql.enabled to false in the values file.Note: If you have an existing PostgreSQL instance, you can disable this dependency and configure Midaz Components to use your external PostgreSQL, like this:
ledger:
configmap:
DB_ONBOARDING_HOST: { your-host }
DB_ONBOARDING_USER: { your-host-user }
DB_ONBOARDING_PORT: { your-host-port }
DB_ONBOARDING_REPLICA_HOST: { your-replication-host }
DB_ONBOARDING_REPLICA_USER: { your-replication-host-user }
DB_ONBOARDING_REPLICA_PORT: { your-replication-host-port }
DB_TRANSACTION_HOST: { your-host }
DB_TRANSACTION_USER: { your-host-user }
DB_TRANSACTION_PORT: { your-host-port }
DB_TRANSACTION_REPLICA_HOST: { your-replication-host }
DB_TRANSACTION_REPLICA_USER: { your-replication-host-user }
DB_TRANSACTION_REPLICA_PORT: { your-replication-host-port }
secrets:
DB_ONBOARDING_PASSWORD: { your-host-pass }
DB_ONBOARDING_REPLICA_PASSWORD: { your-replication-host-pass }
DB_TRANSACTION_PASSWORD: { your-host-pass }
DB_TRANSACTION_REPLICA_PASSWORD: { your-replication-host-pass }
When using an external PostgreSQL (i.e., postgresql.enabled: false), this chart provides a one-shot bootstrap Job that:
onboarding and transaction databases if they do not exist.midaz role/user if it does not exist and sets its password.public schema permissions so midaz can create tables.Is idempotent: if everything already exists, it prints and exits.
charts/midaz/templates/bootstrap-postgres.yamlmidaz-bootstrap-postgresConfigure in values.yaml:
postgresql:
enabled: false # disable bundled PostgreSQL to use an external one
global:
externalPostgresDefinitions:
enabled: true
connection:
host: "your-postgres-host"
port: "5432"
postgresAdminLogin:
# Option A: Use an existing Secret (recommended)
# Required keys: DB_USER_ADMIN, DB_ADMIN_PASSWORD
useExistingSecret:
name: "my-postgres-admin-secret"
# Option B: Inline credentials (not recommended in production)
# username: "postgres"
# password: "s3cret"
midazCredentials:
# Option A: Use an existing Secret (recommended)
# Required key: DB_PASSWORD_MIDAZ
useExistingSecret:
name: "my-midaz-credentials-secret"
# Option B: Inline password (not recommended in production)
# password: "midaz-password"
Notes:
mongodb.enabled to false in the values file.Note: If you have an existing MongoDB instance, you can disable this dependency and configure Midaz Components to use your external MongoDB, like this:
ledger:
configmap:
MONGO_ONBOARDING_HOST: { your-host }
MONGO_ONBOARDING_NAME: { your-host-name }
MONGO_ONBOARDING_USER: { your-host-user }
MONGO_ONBOARDING_PORT: { your-host-port }
MONGO_TRANSACTION_HOST: { your-host }
MONGO_TRANSACTION_NAME: { your-host-name }
MONGO_TRANSACTION_USER: { your-host-user }
MONGO_TRANSACTION_PORT: { your-host-port }
secrets:
MONGO_ONBOARDING_PASSWORD: { your-host-pass }
MONGO_TRANSACTION_PASSWORD: { your-host-pass }
How to disable: Set rabbitmq.enabled to false in the values file.
Important — no persistent volume: by default, the bundled RabbitMQ has no persistent volume. When its pod is recreated, it loses its queues and messages. For production, give it a volume with rabbitmq.storage.requestedSize (for example 8Gi, plus rabbitmq.storage.className if you need a specific StorageClass), or use an external RabbitMQ instance. Kubernetes refuses changes to a StatefulSet’s volumeClaimTemplates, so adding the volume to an existing release requires deleting the RabbitMQ StatefulSet first.
Important: When using an external RabbitMQ instance, it is essential to create the transaction and consumer users and load the RabbitMQ definitions from the load_definitions.json file. These definitions contain crucial configurations (the two users’ permissions, queues, exchanges, bindings) required for Midaz Components to function correctly. Without these definitions, Midaz Components will not operate as expected.
Automatically: Enable the bootstrap job in your values.yaml to automatically apply the RabbitMQ definitions to your external instance:
global:
externalRabbitmqDefinitions:
enabled: true
Manually:
You can also apply them with RabbitMQ’s HTTP API. First create the two users with your own passwords, the ones you give the ledger as RABBITMQ_DEFAULT_PASS and RABBITMQ_CONSUMER_PASS (JSON-escape a " or \ in them), then load the definitions, whose permissions need those users:
curl -u {admin-user}:{admin-pass} -X PUT -H "Content-Type: application/json" \
-d '{"password":"{transaction-pass}","tags":"management"}' http://{host}:{port}/api/users/transaction
curl -u {admin-user}:{admin-pass} -X PUT -H "Content-Type: application/json" \
-d '{"password":"{consumer-pass}","tags":"management"}' http://{host}:{port}/api/users/consumer
curl -u {admin-user}:{admin-pass} -X POST -H "Content-Type: application/json" \
-d @load_definitions.json http://{host}:{port}/api/definitions
The load_definitions.json file is located at:
charts/midaz/files/rabbitmq/load_definitions.json
To streamline external RabbitMQ setup, this chart provides a one-shot Job that:
charts/midaz/files/rabbitmq/load_definitions.json) via the HTTP API.transaction and consumer users with custom passwords.Waits for AMQP connectivity with a 300s timeout.
charts/midaz/templates/bootstrap-rabbitmq.yamlmidaz-bootstrap-rabbitmqConfigure in values.yaml:
rabbitmq:
enabled: false # disable bundled RabbitMQ to use an external one
global:
externalRabbitmqDefinitions:
enabled: true
connection:
protocol: "http" # http or https
host: "your-rabbitmq-host"
port: "15672" # HTTP management port
portAmqp: "5672" # AMQP port (for connectivity check)
rabbitmqAdminLogin:
# Option A: Use an existing Secret (recommended)
# Required keys: RABBITMQ_ADMIN_USER, RABBITMQ_ADMIN_PASS
useExistingSecret:
name: "my-rabbitmq-admin-secret"
# Option B: Inline credentials (not recommended in production)
# username: "admin"
# password: "s3cret"
appCredentials:
# Option A: Use an existing Secret (recommended)
# Required keys: RABBITMQ_DEFAULT_PASS, RABBITMQ_CONSUMER_PASS
useExistingSecret:
name: "my-rabbitmq-app-credentials"
# Option B: Inline passwords (not recommended in production)
# transactionPassword: "transaction-pass"
# consumerPassword: "consumer-pass"
Notes:
kubectl logs job/midaz-bootstrap-rabbitmq shows the reason.If your RabbitMQ server requires TLS/SSL, update the client environment variables to use secure protocols:
ledger:
configmap:
RABBITMQ_URI: "amqps" # was "amqp"
RABBITMQ_PROTOCOL: "https" # was "http"
Since 8.4.0 the otel-collector-lerian subchart is no longer installed as a dependency of this chart (the dependency was removed; see UPGRADE-8.4.md for the migration story). The otel-collector-lerian.enabled flag now only controls whether HOST_IP, POD_IP, OTEL_EXPORTER_OTLP_ENDPOINT=$(HOST_IP):4317 and OTEL_RESOURCE_ATTRIBUTES=k8s.pod.ip=$(POD_IP) are injected into the ledger and crm deployments.
By default enabled: true — Midaz expects a node-local OTel collector listening on 4317 (hostPort or hostNetwork). Install the otel-collector-lerian chart separately or point your own collector at the same port.
otel-collector-lerian:
enabled: true # injects OTEL env vars (default)
If you don’t want telemetry exported, or you want to send telemetry to an endpoint that isn’t a node-local collector on :4317, leave enabled: false and configure OTEL_EXPORTER_OTLP_ENDPOINT directly in the application configmap:
otel-collector-lerian:
enabled: false
ledger:
configmap:
OTEL_EXPORTER_OTLP_ENDPOINT: "https://your-collector.example.com:4317"
crm:
configmap:
OTEL_EXPORTER_OTLP_ENDPOINT: "https://your-collector.example.com:4317"
The schema declares only enabled under otel-collector-lerian: and does not reject other keys (additionalProperties is true), but the templates read only enabled: legacy keys such as external, extraEnvs, exporters or opentelemetry-collector render without error and have no effect.