Controller Helm chart examples
Use these controller configuration variations: TLS setup, database migration, bootstrap admin creation, and cluster service exposure, to adapt the baseline installation in Deploy controllers to your environment. Start with the baseline workflow and apply only the variations you need while preparing your values file. The examples assume PostgreSQL and KMS (key management system) access already exist.
Create the controller Secret
The chart reads sensitive settings from the Kubernetes Secret specified by secretRefs.secretName. You can change the Secret name and its key names in your values file:
| Default key | Used for |
|---|---|
database-url | env://BOUNDARY_PG_URL in database.url |
license | env://BOUNDARY_LICENSE in controller.license |
admin-username | Bootstrap administrator username |
admin-password | Bootstrap administrator password |
Add migration-url when controller.config references env://BOUNDARY_PG_MIGRATION_URL. Configure key names with secretRefs.keys.*.
The environment references used by the default controller HCL are fixed. Changing the Secret name or key names does not change env://BOUNDARY_PG_URL, env://BOUNDARY_LICENSE, or env://BOUNDARY_PG_MIGRATION_URL inside controller.config.
You can create and manage the Secret using any of the following methods:
- Create it manually with Kubernetes tools by using the command below.
- Synchronize it from Vault with the Vault Secrets Operator.
- Synchronize it from an external secret store with the External Secrets Operator.
Set secretRefs.validateExisting=true to make Helm look up the Secret during rendering. This targeted validation checks only keys required by the rendered HCL and enabled bootstrap settings. It does not validate the Secret by default, and it does not validate credentials or connectivity.
For a first installation, create the Secret with the default key names before you install the chart:
$ kubectl create secret generic boundary-controller-secrets \
--namespace boundary \
--from-literal=database-url='postgres://<user>:<password>@<host>:5432/<database>?sslmode=require' \
--from-literal=license='<boundary-enterprise-license>' \
--from-literal=admin-username='<admin-username>' \
--from-literal=admin-password='<admin-password>'
The database account must be allowed to initialize the Boundary schema when you leave database.init.enabled=true.
Create the TLS Secret
Create a Kubernetes Secret when either tls.api.disabled=false or tls.ops.disabled=false. The Secret must contain tls.crt and tls.key, and its name must match tls.secretName, which defaults to boundary-controller-tls. You can use the Kubernetes TLS Secret type described in the Kubernetes TLS Secrets documentation.
The following command creates the default Secret from an existing certificate and private key:
$ kubectl create secret tls boundary-controller-tls \
--namespace boundary \
--cert=tls.crt \
--key=tls.key
The chart mounts the TLS Secret while either listener uses TLS. It omits the mount only when TLS is disabled for both listeners. The chart does not validate certificate contents, trust, expiration, or SANs.
The same Secret supports the TLS variations below.
Enable database initialization
database.init.enabled defaults to true. Keep the default for the first installation against an empty PostgreSQL database. Set it to false when another workflow initializes the schema or the database is already initialized.
Enable bootstrap admin creation
bootstrapAdmin.enabled defaults to true and creates the initial global password auth method, user, account, and role. Set it to false if another workflow manages the initial administrator resources.
Controller with TLS enabled
Enable TLS when you want encrypted API and ops traffic from the controller listeners. Provide a Kubernetes TLS Secret. The chart derives HTTPS probes from tls.ops.disabled=false.
Example values:
tls:
api:
disabled: false
ops:
disabled: false
secretName: boundary-controller-tls
mountPath: /etc/boundary/tls
Matching HCL: set tls_disable = false and add tls_cert_file and tls_key_file on the API and ops listeners:
listener "tcp" {
address = "0.0.0.0:9200"
purpose = "api"
tls_disable = "{{ .Values.tls.api.disabled }}"
tls_cert_file = "{{ .Values.tls.mountPath }}/tls.crt"
tls_key_file = "{{ .Values.tls.mountPath }}/tls.key"
}
listener "tcp" {
address = "0.0.0.0:9203"
purpose = "ops"
tls_disable = "{{ .Values.tls.ops.disabled }}"
tls_cert_file = "{{ .Values.tls.mountPath }}/tls.crt"
tls_key_file = "{{ .Values.tls.mountPath }}/tls.key"
}
Controller with TLS disabled
Disable TLS when the controller listeners are protected by an upstream proxy or load balancer that terminates TLS, or in non-production environments where encryption is not required.
Example values:
tls:
api:
disabled: true
ops:
disabled: true
Matching HCL: set tls_disable = true on each listener that had TLS enabled and remove the tls_cert_file and tls_key_file settings:
listener "tcp" {
address = "0.0.0.0:9200"
purpose = "api"
tls_disable = true
}
listener "tcp" {
address = "0.0.0.0:9203"
purpose = "ops"
tls_disable = true
}
The chart always renders the probes. Their default scheme derives only from tls.ops.disabled, so this configuration uses HTTP. The chart omits the TLS Secret mount only when both listener values are true.
Internal-only cluster service
If all workers run inside the cluster or connect through private networking, keep the cluster listener internal and set public_cluster_addr to a private DNS name or internal load balancer address reachable by workers.
Example values:
controller:
service:
api:
type: LoadBalancer
cluster:
type: ClusterIP
ops:
type: ClusterIP
Set the advertised cluster address in the existing controller stanza:
controller {
public_cluster_addr = "boundary-controller-cluster.boundary.svc.cluster.local:9201"
}
Externally accessible cluster service
When workers run outside the cluster, the default ClusterIP cluster service is not reachable. Workers in another namespace can use the Service's fully qualified cluster DNS name when network policy permits it. Configure the cluster service as a LoadBalancer only when workers need to connect from outside the cluster.
Example values:
controller:
service:
cluster:
type: LoadBalancer
port: 9201
targetPort: 9201
Update the advertised address in the existing controller stanza after the load balancer is provisioned:
controller {
public_cluster_addr = "<LOAD_BALANCER_ADDRESS>:9201"
}
Watch the cluster Service until the external address is assigned:
$ kubectl get svc boundary-controller-cluster --namespace boundary --watch
Example output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
boundary-controller-cluster LoadBalancer 10.0.0.123 <pending> 9201:31234/TCP 10s
boundary-controller-cluster LoadBalancer 10.0.0.123 203.0.113.10 9201:31234/TCP 45s
Update public_cluster_addr in controller.config with the assigned address, then upgrade:
$ helm upgrade boundary-controller hashicorp/boundary-controller \
--namespace boundary \
--values controller-values.yaml \
--rollback-on-failure \
--wait
Use this pattern when workers require an endpoint reachable from outside the Kubernetes cluster. Restrict access to trusted worker networks.
Controller with a pre-configured DNS address
If you already control the DNS name for the controller's cluster listener, set public_cluster_addr to that address before installation:
controller {
public_cluster_addr = "boundary-controller.example.com:9201"
}
After the cluster Service receives an external address, point the DNS record to that address. This avoids changing the controller HCL after installation, but you remain responsible for ensuring that DNS resolves to the reachable cluster listener endpoint.
Configure migration URL
If you use a different PostgreSQL connection string for schema migrations, store it in the controller Secret and reference it from controller.config.
Matching HCL:
controller {
name = "boundary-controller"
public_cluster_addr = "boundary-controller-cluster.boundary.svc.cluster.local:9201"
license = "env://BOUNDARY_LICENSE"
database {
url = "env://BOUNDARY_PG_URL"
migration_url = "env://BOUNDARY_PG_MIGRATION_URL"
}
}
Store the value under the Secret key configured by secretRefs.keys.migrationUrl, which defaults to migration-url. The chart injects it as BOUNDARY_PG_MIGRATION_URL when the rendered HCL references the environment variable shown above.
Migrate the database during an upgrade
When upgrading to a Boundary version that requires a schema change, stop the controller pods first, then run the migration Job during the Helm upgrade.
Scale controllers to zero:
$ helm upgrade boundary-controller hashicorp/boundary-controller \
--namespace boundary \
--values controller-values.yaml \
--set controller.replicas=0 \
--rollback-on-failure \
--wait
Back up PostgreSQL after the controllers stop. Then run the migration as a one-time command-line override instead of adding it to the values file:
$ helm upgrade boundary-controller hashicorp/boundary-controller \
--namespace boundary \
--values controller-values.yaml \
--set controller.replicas=0 \
--set database.migrate.enabled=true \
--rollback-on-failure \
--wait
Run a repair only when the Boundary migration output directs you to a specific version:
$ helm upgrade boundary-controller hashicorp/boundary-controller \
--namespace boundary \
--values controller-values.yaml \
--set controller.replicas=0 \
--set database.migrate.enabled=true \
--set database.repair.version=<version_id> \
--rollback-on-failure \
--wait
After the migration completes, reset all migration flags and restore the controller replica count:
$ helm upgrade boundary-controller hashicorp/boundary-controller \
--namespace boundary \
--values controller-values.yaml \
--set database.migrate.enabled=false \
--set database.repair.version="" \
--set controller.replicas=2 \
--rollback-on-failure \
--wait
Change service ports
If you change listener ports, update both the HCL and chart values.
Example values:
controller:
service:
api:
port: 9210
targetPort: 9210
Matching HCL:
listener "tcp" {
address = "0.0.0.0:9210"
purpose = "api"
tls_disable = false
tls_cert_file = "/etc/boundary/tls/tls.crt"
tls_key_file = "/etc/boundary/tls/tls.key"
}
The chart does not synchronize these settings automatically. Apply the same pattern for the cluster and ops listeners if those ports change.
Troubleshooting
Migration Job fails
If the migration Job started by database.migrate.enabled=true fails, the controller pods remain scaled to zero until the migration succeeds. Refer to the PostgreSQL backup you took before running migrations or repairs to restore the database, then correct the underlying issue (for example, an invalid database.repair.version) before setting database.migrate.enabled=true again and re-running helm upgrade.
More information
To view supported Helm values so that you can configure or update controllers, refer to Controller values.