Helm chart and values
Runs myriarchd (the myriarch server) with high availability: replicas: 2 (all replicas serve agents and the API; the
singleton jobs run on the leader elected through a PostgreSQL advisory lock), a Service (8080 HTTP/UI/REST/OpAMP, 9090
metrics), a PodDisruptionBudget, a NetworkPolicy, a ConfigMap with myriarchd.yaml, and secrets mounted as files.
Access from outside the cluster is optional: Gateway API HTTPRoute (recommended), Ingress, or an OpenShift Route.
Failover and shutdown timing are in High availability.
The chart installs with its defaults, never starts the dev mode and has a login without an enterprise identity provider (the bootstrap admin). Single sign-on with any OpenID Connect provider is a configuration change: docs/operations/authentication.md.
Available from v0.2.0. The values are validated against a strict schema,
values.schema.json:helm install,upgrade,templateandlintrefuse a key that the chart does not know, a value of the wrong type and a malformed value, before anything is rendered. The chart of v0.1.0 has no schema and ignores such a key without a message. See Values schema.
Minimal installation
Two things are needed: a Secret with the database DSN and the external URL.
kubectl create namespace myriarch
kubectl -n myriarch create secret generic myriarch-secrets \
--from-literal=database-dsn='postgres://myriarch:<password>@db.example.com:5432/myriarch?sslmode=verify-full'
# from a checkout of the repository, or, from v0.2.0, from the signed OCI chart (verify it first, see "Signed OCI chart" below):
helm install myriarch deploy/helm/myriarch -n myriarch --set config.server.publicUrl=https://fleet.example.com
helm install myriarch oci://ghcr.io/myriarch/charts/myriarch --version 0.2.1 -n myriarch --set config.server.publicUrl=https://fleet.example.com
The documented release name and namespace are both myriarch, so the Service, Deployment, ConfigMap and ServiceAccount
are all called myriarch.
Signed OCI chart
Available from v0.2.0. The chart of v0.1.0 is installed from a checkout of the repository.
Every release publishes the chart as oci://ghcr.io/myriarch/charts/myriarch; its version and its appVersion are the version of
the release. The release workflow writes the digest of the signed server image into image.digest before it packages the chart, so
the published chart references the image by digest, signs the chart keyless with the identity of the release workflow, and checks
before publication that helm template renders every image by that digest. In the repository image.digest stays empty (the image
is then appVersion’s tag). Verification commands: Verify a release;
installation: Install with Helm.
Access
Without Ingress, HTTPRoute or Route nothing is exposed. Use a port-forward (the NetworkPolicy does not affect it):
kubectl -n myriarch port-forward svc/myriarch 8080:8080 # http://localhost:8080
For access from outside the cluster choose one:
| Path | Values | Notes |
|---|---|---|
| Gateway API (recommended) | httpRoute.enabled, parentRefs, hostnames, tlsTerminatedAtGateway | the chart creates the HTTPRoute, not the Gateway |
| Ingress | ingress.enabled, className, annotations, tls | ingress-nginx is no longer maintained since March 2026; examples for maintained controllers are in examples/ingress-*.yaml |
| OpenShift Route | openshift.enabled=true | Route with edge TLS, see “Profiles” |
Ingress and HTTPRoute need config.server.trustedProxies (CIDR of the controller or Gateway data plane) and
networkPolicy.ingress.from (their namespace) and networkPolicy.ingress.allowSameNamespace=false.
First login
The default login is the bootstrap admin, a break-glass administrator that is configured declaratively
(bootstrapAdmin). The chart generates the Secret myriarch-bootstrap with a one-time token; the install notes print
the command to read it (the notes never print a secret):
kubectl -n myriarch get secret myriarch-bootstrap -o jsonpath='{.data.token}' | base64 -d
Open the UI and paste the token into the bootstrap admin form (it sits behind “Use the bootstrap admin” when single
sign-on is offered as well). The token works for one sign-in. For a new one
delete the Secret and run helm upgrade. GitOps tools that render offline (helm template, Argo CD) cannot see existing
Secrets and would generate a new token on every render: use bootstrapAdmin.existingSecret there.
For a durable break-glass account use a bcrypt password hash in your own Secret:
printf '%s' "$PASSWORD" | myriarchd auth hash-password > password-hash # 12 to 72 characters, bcrypt cost 12
kubectl -n myriarch create secret generic my-bootstrap --from-file=password-hash=password-hash
helm upgrade myriarch ... --set bootstrapAdmin.existingSecret=my-bootstrap \
--set bootstrapAdmin.tokenKey= --set bootstrapAdmin.passwordHashKey=password-hash
The bootstrap admin is meant for the initial setup: configure config.auth.oidc and set bootstrapAdmin.enabled=false.
With OIDC enabled the chart does not mount its credentials (and the server deactivates the account) unless
bootstrapAdmin.keepWithOidc=true. Every sign-in is audited as auth.bootstrap.login.
Restrict network access to the bootstrap login. The account has an account-wide backoff: after five failures in a
row the server delays every further attempt, doubling the delay up to 15 minutes, whoever sends it. Anyone who can reach
POST /api/v1/auth/bootstrap-login can therefore keep the break-glass login in these delays with wrong credentials, and
the legitimate administrator gets 429 as well. Allow this path only from the networks of your administrators (a rule
for the path at the ingress controller, Gateway or WAF, or block it at the edge and use kubectl port-forward when the
break-glass account is needed). The counter is held in memory per replica, so restarting the pods clears it:
kubectl -n myriarch rollout restart deployment/myriarch. Details and the audit entries to watch:
docs/operations/authentication.md.
Single sign-on with Dex (quickstart)
examples/quickstart-dex.yaml (values for this chart) and examples/dex/dex.yaml (values for the upstream Dex chart)
set up Dex as a demo identity provider with one static user:
helm upgrade --install dex dex/dex -n dex -f deploy/helm/myriarch/examples/dex/dex.yaml # see the header of the file
helm upgrade --install myriarch deploy/helm/myriarch -n myriarch -f deploy/helm/myriarch/examples/quickstart-dex.yaml
The files contain placeholders only (<bcrypt hash>, <client secret>); replace them and never commit real values. For
Keycloak, Okta, Entra ID, Google, authentik and Zitadel see
docs/operations/authentication.md.
Single sign-on switched on through the environment
The chart reads config.auth.oidc.enabled to decide whether a login path exists and whether the bootstrap admin is
active (its Secret is mounted only then). If your platform switches single sign-on on with environment variables instead
(MYRIARCH_AUTH_OIDC_ENABLED=true in extraEnv, or injected from a ConfigMap or a Secret), the chart cannot see it, and
with bootstrapAdmin.enabled=false it stops with bootstrapAdmin.enabled: no login path is configured. Declare the
environment with oidcFromEnvironment:
oidcFromEnvironment: true # the chart treats OIDC as enabled; the server reads the environment
bootstrapAdmin:
enabled: false
extraEnv:
- {name: MYRIARCH_AUTH_OIDC_ENABLED, value: "true"}
- {name: MYRIARCH_AUTH_OIDC_CLIENTID, value: myriarch}
oidcFromEnvironment only declares what the chart assumes: no login path error, no bootstrap Secret unless
bootstrapAdmin.keepWithOidc=true, and the config.auth.oidc.*File fields stay in the rendered configuration (set an
empty value to drop one, for example clientSecretFile when the client secret comes from the environment). The server
reads the environment at runtime and still refuses to start when no login path remains.
Declarative enrollment tokens
Tokens for agents that exist from the first start, without the UI (enrollmentTokens):
enrollmentTokens:
- name: gateway # DNS label, unique; the file is /etc/myriarchd/enrollment/gateway
labels: {site: hq} # copied to the agents at registration
expiresAt: "" # RFC 3339, empty = no expiry
# existingSecret: "" # empty = the chart generates <fullname>-enrollment-<name> (myre_ + 40 characters)
# key: token
The chart renders opamp.enrollmentTokens and mounts the Secrets as one projected volume. Agents in the cluster can
mount the same Secret. The server stores only the SHA-256 hash of each token. Generated values survive upgrades; the
GitOps caveat above applies.
Prerequisites
- External PostgreSQL (>= 14) with a database and a user. myriarchd runs migrations itself
(
database.migrateOnStart, protected by an advisory lock, so several replicas are safe). No PgBouncer in transaction pooling mode: LISTEN/NOTIFY and session advisory locks (leader election) need a direct connection or session pooling. - Secret (
existingSecret, defaultmyriarch-secrets) with at leastdatabase-dsn. Optional keys, depending on the configuration:oidc-client-secret(oroidc-client-certandoidc-client-key),download-token-secret,git-token,gitops-webhook-secret,github-token,mirror-token,mirror-password, notification URLs. It is mounted completely at/etc/myriarchd/secrets(mode 0440); the configuration points to the files through its*Filefields. Executor secrets (webhook/AWX tokens) are passed as environment variables throughsecretEnv. - Access with WebSocket support and long timeouts for the OpAMP connections (see “Access”). Session affinity is not needed.
- Kubernetes >= 1.30 (native
preStopsleep action; the distroless image has nosleep;Chart.yamlenforces this throughkubeVersion).
Profiles: standard Kubernetes and OpenShift
openshift.enabled selects the profile:
| Value | Meaning |
|---|---|
false (default) | standard profile: fixed user and group 65532 and fsGroup 65532 |
true | OpenShift profile |
auto | OpenShift profile when the cluster offers the API security.openshift.io/v1 |
Caveat for auto: helm template has no cluster access and silently renders the standard profile unless you pass
--api-versions security.openshift.io/v1. GitOps tools that template offline need the same flag, or use true/false.
The OpenShift profile
- drops
runAsUser,runAsGroupandfsGroupfrom the pod security context (restricted-v2 assigns them from the project range;runAsNonRootand the seccomp profile stay; norunAsUserappears anywhere in the Deployment), - creates a
Route(openshift.route.*): edge TLS, the host ofopenshift.route.hostor ofconfig.server.publicUrl, router timeoutshaproxy.router.openshift.io/timeoutandtimeout-tunnel(openshift.route.timeout, default3600sfor the long-lived OpAMP WebSockets),insecureEdgeTerminationPolicy, optionalexternalCertificateSecret, - adds the router namespaces (
openshift.routerNamespaceSelector) as source of port 8080 and uses the OpenShift DNS rule (openshift.dns: namespaceopenshift-dns, ports 5353 and 53).
The Route does not require trustedProxies, but without it all clients appear with the address of the router. Set it to
the pod CIDR of the router and networkPolicy.ingress.allowSameNamespace=false. examples/openshift.yaml is an overlay of
the production example. Every container has requests and limits for cpu, memory and ephemeral-storage, as LimitRanges on
OpenShift usually demand.
Network policy
networkPolicy.enabled (default) restricts the pods. A CNI with NetworkPolicy support is a prerequisite.
- Port 8080 (UI, REST, OpAMP) is reachable from: the own namespace (
networkPolicy.ingress.allowSameNamespace, defaulttrue; agents and collector gateways next to the pods), plusnetworkPolicy.ingress.from, plus the router namespaces in the OpenShift profile with a Route. An empty set means no rule for 8080 (denied);kubectl port-forwardstill works.allowSameNamespacemust befalsewhileconfig.server.trustedProxiesorconfig.opamp.clientCertHeaderis set (rendering fails otherwise): any pod of the namespace could forge the forwarded headers. - The Pod of
helm test(Helm test,tests.enabled, defaulttrue) gets a source for port 8080 of its own whenallowSameNamespaceisfalse: a pod selector with its labels. The chart adds it only while neitherconfig.server.trustedProxiesnorconfig.opamp.clientCertHeaderis set, because every pod that carries these labels could use it (Network access of the test). To keep the policy of v0.1.0, in which no pod of the namespace reaches port 8080 unlessnetworkPolicy.ingress.fromnames it, settests.enabled: false. - Port 9090 (
/metrics, unauthenticated) only fromnetworkPolicy.metrics.from; empty means nobody. - Egress (
networkPolicy.egress.enabled): DNS (egress.dns; the OpenShift profile usesopenshift.dns, UDP and TCP per port), and one rule for TCPegress.ports(default 5432 PostgreSQL, 443 HTTPS) plusegress.extraPortsplus the Git SSH port (22, or the port of anssh://host:portURL; forssh://and scp-likeuser@host:pathURLs whileconfig.gitops.enabled), duplicates removed. Destinations are all addresses exceptegress.exceptCidrs(cloud metadata service) and every pod of the cluster (PostgreSQL or an IdP such as Dex inside the cluster).egress.extraRulesare appended in NetworkPolicy format. In addition myriarchd checks every outbound connection againstconfig.outbound.allowedCidrsitself.
Storage, resources and secrets
emptyDir.sizeLimit(default 4Gi) holds the GitOps clone and the artifact cache of each replica;config.artifacts.maxCacheBytes(default 3 GiB) must stay below it.resources.limits.ephemeral-storage(default 5Gi) must stay aboveemptyDir.sizeLimitplus the 256Mitmpvolume plus the container logs, otherwise the kubelet evicts the pod. Withpersistence.enableda shared PVC (persistence.size, ReadWriteMany) is mounted instead.replicaCountdefaults to 2. The PodDisruptionBudget is created only whilereplicaCountis greater than 1 (with one replicaminAvailable: 1would block every node drain);podDisruptionBudget.maxUnavailablereplacesminAvailablewhen it is set.secretChecksum.enabled(default) adds the annotationchecksum/secretto the pods: the SHA-256 over the uid and resourceVersion of every Secret the pod mounts (existingSecret, the bootstrap Secret, the enrollment Secrets) as seen throughlookup, so a changed Secret rolls the pods onhelm upgrade(no digest of the secret data is stored in the Deployment). It is constant underhelm template. The first upgrade after an installation with generated Secrets restarts the pods once. An upgrade that creates a generated Secret (a new enrollment token entry, or a generated Secret you deleted to rotate it) restarts the pods too: the checksum then contains the release revision, so even a second rotation rolls the pods (an enrollment token is reconciled at startup). Changes toconfig:roll the pods throughchecksum/config.- Mounted files:
/etc/myriarchd/secrets/<key>(existingSecret),/etc/myriarchd/bootstrap/tokenand/etc/myriarchd/bootstrap/password-hash(SecretbootstrapAdmin.existingSecretor the generated<fullname>-bootstrap),/etc/myriarchd/enrollment/<name>(one projected volume). All with mode 0440, read-only. *Filefields of disabled features are not rendered:auth.oidc.*Filewhile OIDC is off (oidcFromEnvironment=truecounts as on), the gitops token, key, CA and webhook secret while gitops is off,releases.tokenFilewhile releases is off; empty*Filevalues are dropped.config.auth.downloadTokenSecretFileis optional (empty = a random key stored in the database and shared by all replicas).config.executors.allowedWebhookHostsempty means fail closed: every webhook and AWX target is refused (myriarchd still starts). List the permitted hosts (host,host:port,*.example.com,http://hostfor plain HTTP) and bind each executor secret to its hosts inconfig.executors.secretEnvHosts.
Operation
- Rolling update:
maxUnavailable: 0,preStop10 s,terminationGracePeriodSeconds: 60; agents reconnect with backoff through other replicas. Exception for the upgrade to migration00003: replicas of an older version can no longer store OIDC users afterwards; for this upgrade setreplicaCount=1and scale up again afterwards. - Upgrade and rollback: back up the PostgreSQL database before every upgrade. From v0.2.0 on the server checks the schema at
start (
config.database.migrateOnStartstaystrue): it refuses to start on a database that a newer version has migrated in a way it cannot work with (the error names the schema version and the compatibility levels), and it starts with a warning on a newer schema that is still compatible.helm rollbacktherefore works as long as the migrations since the revision were compatible; otherwise the old pods stop at start and you restore the backup. A rollback to v0.1.0 is not supported: it does not check the schema, so going back means restoring the backup. See Server behaviour and ADR-0036. - Security: non-root,
readOnlyRootFilesystem, all capabilities dropped, seccompRuntimeDefault; only/var/lib/myriarchd(GitOps clone, artifact cache) and/tmpare writable. The image creates/var/lib/myriarchd(0750, owned by 65532:65532) and the chart mounts the data volume there withfsGroup65532. The chart always startsmyriarchd serve; the dev mode and the dev login are not available (settingconfig.devorconfig.auth.devLoginfails the rendering). config.server.tls.terminatedUpstream(defaulttrue) confirms to myriarchd that TLS ends at the ingress, Gateway or router (otherwise myriarchd does not start in production).secretEnvnames must start withexecutors.secretEnvPrefix(MYRIARCH_EXEC_).config.auth.sessionTtlandsessionMaxAgelimit sessions (2 h idle, 12 h absolute).- TLS: if
config.server.publicUrluses https, the Ingress needsingress.tls(ingress.allowWithoutTls=trueonly when TLS terminates outside the ingress); an HTTPRoute needshttpRoute.tlsTerminatedAtGateway=true. The Route terminates TLS at the router and redirects plain HTTP requests (openshift.route.tls.insecureEdgeTerminationPolicy: Redirect);Allowserves plain HTTP at the router, and sows://, andNoneserves none. - Metrics: port
metrics(9090);serviceMonitor.enabled=truecreates a ServiceMonitor, andprometheusRule.enabled=truea PrometheusRule with the alerts of the server (Alerting rules). - Test:
helm test <release>checks/readyzand the OpAMP endpoint of the Service (Helm test). - Image:
image.registry(defaultghcr.io) andimage.repository(defaultmyriarch/myriarch) give the repository, andimage.tag(default:appVersion) or, immutable,image.digest(sha256:<64 hex>; when set, the image is referenced as<repository>@<digest>and the tag is ignored) the version. Private registry:imagePullSecrets. See Image. Available from v0.2.0.image.registryandglobal.imageRegistry: the chart of v0.1.0 ignores them without a message, and itsimage.repositoryholds the registry host as well (ghcr.io/myriarch/myriarch). - Proxy and internal CA for outbound connections:
extraEnv(HTTPS_PROXY,NO_PROXY) andextraVolumes/extraVolumeMountsfor a CA ConfigMap, together withconfig.outbound.caFileandconfig.outbound.allowedCidrs.
Image
Available from v0.2.0. The chart of v0.1.0 has no
image.registryand noglobal.imageRegistryand ignores them without a message; there the registry host is part ofimage.repository(defaultghcr.io/myriarch/myriarch). With that chart, a repository without a host is pulled from the default registry of your container runtime.
The image is <registry>/<repository>:<tag>, or <registry>/<repository>@<digest> with image.digest:
| Value | Default | Meaning |
|---|---|---|
image.registry | ghcr.io | Registry host with an optional port. It applies when the repository has no registry host of its own. Empty means the default registry of the container runtime. |
image.repository | myriarch/myriarch | Repository without the registry host. |
image.tag | empty | Empty means the appVersion of the chart. |
image.digest | empty | sha256: and 64 hexadecimal characters. It replaces the tag. |
global.imageRegistry | not set | Replaces image.registry, for example with the host of a mirror, also when a parent chart passes it. |
A repository that starts with a registry host is used unchanged, and neither image.registry nor global.imageRegistry
applies to it. The first path segment of the repository is a registry host when it contains a . or a : or is localhost.
This is the form of the values of v0.1.0 (image.repository: registry.example.com/myriarch/myriarch), which therefore render
the same image as before. A repository without a host now takes image.registry; if you pulled such an image through the
default registry of your container runtime, set image.registry to the empty string or put the host into the repository.
# the image of a mirror, pinned by digest
helm install myriarch ... --set image.registry=registry.example.com:5000 --set image.digest=sha256:<64 hex>
# the v0.1.0 form still works
helm install myriarch ... --set image.repository=registry.example.com/myriarch/myriarch
Values schema
Available from v0.2.0. The chart of v0.1.0 has no schema and ignores a key that it does not know.
values.schema.json is a JSON Schema (draft 2020-12) that Helm reads for helm install, upgrade,
template and lint. It is strict:
- A key that the chart does not read is an error, also inside
config. Before, Helm ignored such a key and the chart rendered without it, so a misspelled setting went unnoticed. - A value has the type that the chart needs: a number for a port, a string for a name, a list for a list, a boolean for a
switch. Strings that name something have the syntax of the receiving system: a Kubernetes name, a quantity such as
512Mi, a registry host, a digest, a duration in Go syntax (30s,1m30s; not30and not30d). The type of a switch matters for the TLS checks: a template counts the string"false"as true, soingress.allowWithoutTls: "false"used to switch the check of the Ingress off, and the schema refuses it. - The block
configmirrors the configuration file of the server. It is generated from the configuration structure of the server (internal/config), with the same keys, types and descriptions as the configuration reference, so it cannot drift from the server. - Secrets are not values. The schema has no key for a database DSN, an OIDC client secret, a webhook secret, the URL of a
notification channel or an enrollment token: use the matching
*Filesettings and the SecretexistingSecret. A declared enrollment token (enrollmentTokens[]) names a Secret or lets the chart generate one, and a Secret reference that has the form of an enrollment token (myre_...) is refused. The settings that the chart generates (config.auth.bootstrapAdmin,config.opamp.enrollmentTokens) and the development mode (config.dev,config.auth.devLogin) are not offered inconfig. - The few places that take a Kubernetes object as it is (
extraVolumes,extraVolumeMounts,podSecurityContext,securityContext,topologySpreadConstraints,affinity, the Gateway API references) are open objects; the API server validates them. - A parent chart that includes this chart as a dependency can use it:
globalis open (the chart reads onlyglobal.imageRegistry), and the keyenabled, which Helm reads forcondition: myriarch.enabled, is accepted and ignored by the chart. Every other key that the parent passes undermyriarch:is checked like a key of a standalone installation. nullremoves a value thatvalues.yamldefines, as in Helm. For a key thatvalues.yamldoes not define Helm keeps the null, and the schema accepts it only for the entries of the maps of the chart (annotations, labels, selectors), where the values files ofexamples/use it to switch off an entry of an earlier file, and forglobal.imageRegistry. It refuses it everywhere else, also for the entries of the maps inconfig(executors.secrets,executors.secretEnvHosts,executors.secretFileHosts): the chart would render the null into the ConfigMap, and the server refuses it when it starts.
An error names the path of the value, for example at '/image/tagg': additional properties 'tagg' not allowed (the wording
depends on the Helm version). The schema does not replace the checks of the templates, which relate several values
(an Ingress needs trustedProxies and a source in the network policy) and which also hold when the validation is switched
off with --skip-schema-validation; and it does not replace myriarchd config validate, which checks the meaning of the
configuration (Verification).
values.schema.json is generated by make docs-gen from values.schema.source.yaml, which holds
the hand-written part, and from internal/config; do not edit the JSON file. A new value of the chart goes into values.yaml,
the templates and values.schema.source.yaml; the tests fail when values.yaml or a template uses a value that the schema does
not list.
Values
Every value of the chart with its type, its default and its description. The table is generated from
values.schema.json and values.yaml (make docs-gen), so it cannot drift from them.
“unset” means that values.yaml has no entry and the chart applies its own default.
| Value | Type | Default | Description |
|---|---|---|---|
replicaCount | integer | 2 | Number of myriarchd replicas. All replicas serve agents and the API; the singleton jobs run on the leader. |
image.registry | string | "ghcr.io" | Registry host, with an optional port, of the image, for example ghcr.io. It applies when image.repository has no registry host of its own, and global.imageRegistry takes precedence over it. Empty means the default registry of the container runtime. |
image.repository | string | "myriarch/myriarch" | Repository of the image without a registry host, for example myriarch/myriarch. A repository that starts with a registry host (registry.example.com/myriarch/myriarch, the form of the values of v0.1.0) is used unchanged and neither image.registry nor global.imageRegistry applies. The first segment is a registry host when it contains a . or a : or is localhost, as for the container runtimes; otherwise it belongs to the repository, which is in lower case. |
image.tag | string | "" | Image tag. Empty means the appVersion of the chart. |
image.digest | string | "" | Image digest, sha256: followed by 64 hexadecimal characters. When set, the image is referenced as repository@digest and the tag is ignored. |
image.pullPolicy | enum | "IfNotPresent" | Image pull policy. |
imagePullSecrets | array | [] | Secrets for pulling the image from a private registry. |
nameOverride | string | "" | Overrides the chart name in resource names. |
fullnameOverride | string | "" | Overrides the complete resource name. |
global.imageRegistry | string or null | unset | Registry host, with an optional port, that replaces image.registry, for example the host of a mirror. A repository that has a registry host of its own is not affected. A parent chart may pass null for no mirror. |
enabled | boolean | unset | Read by the dependency condition of a parent chart (condition: myriarch.enabled), not by this chart, which ignores it. It has no default and no effect on a standalone installation. |
serviceAccount.create | boolean | true | Create the ServiceAccount. |
serviceAccount.name | string | "" | Name of the ServiceAccount. Empty means a name generated from the release name. |
serviceAccount.annotations | object | {} | Annotations of the ServiceAccount. |
config | object | The configuration file of the server (myriarchd.yaml); keys, types and descriptions are those of the configuration reference. The chart defaults are in values.yaml. | |
oidcFromEnvironment | boolean | false | Declares that single sign-on is switched on by environment variables (MYRIARCH_AUTH_OIDC_ENABLED in extraEnv, a ConfigMap or a Secret), which the chart cannot read. The chart then treats OIDC as enabled; the server reads the environment. |
bootstrapAdmin.enabled | boolean | true | Configure the bootstrap admin. |
bootstrapAdmin.username | string | "admin" | User name of the account, 1 to 64 characters from letters, digits, “.”, “_”, “@” and “-”. |
bootstrapAdmin.existingSecret | string | "" | Secret that holds the credentials. Empty means that the chart generates the Secret <fullname>-bootstrap with a random one-time token. |
bootstrapAdmin.tokenKey | string | "token" | Key of the one-time token in the Secret. Empty means no token, only a password hash, and needs existingSecret. |
bootstrapAdmin.passwordHashKey | string | "" | Key of a bcrypt password hash in existingSecret. Needs existingSecret. |
bootstrapAdmin.keepWithOidc | boolean | false | Keep the bootstrap admin active next to single sign-on (break-glass). |
enrollmentTokens | array | [] | Enrollment tokens for agents that exist from the first start. A token is never a value: each entry names a Secret that holds it, or the chart generates the Secret with a random token. |
existingSecret | string | "myriarch-secrets" | Name of the Secret with the files of the server (database-dsn and the optional keys). It is mounted at /etc/myriarchd/secrets. |
secretChecksum.enabled | boolean | true | Add the annotation checksum/secret to the pods. |
secretEnv | array | [] | Environment variables for the executors, each taken from one key of the Secret existingSecret. |
extraEnv | array | [] | Additional environment variables of the myriarchd container, in the format of a Kubernetes container. |
extraVolumes | array | [] | Additional volumes of the pod, in the format of a Kubernetes pod. |
extraVolumeMounts | array | [] | Additional volume mounts of the myriarchd container, in the format of a Kubernetes container. |
openshift.enabled | boolean or enum | false | true selects the OpenShift profile, false the standard profile, and auto the OpenShift profile when the cluster offers the API security.openshift.io/v1. helm template renders the standard profile for auto unless it gets –api-versions security.openshift.io/v1. |
openshift.route.enabled | boolean | true | Create the Route in the OpenShift profile. |
openshift.route.host | string | "" | Host of the Route. Empty means the host of config.server.publicUrl, without the port. |
openshift.route.annotations | object | {} | Further annotations of the Route. The router timeout annotations are always set unless this map sets them. |
openshift.route.timeout | string | "3600s" | Router timeout for the long-lived OpAMP WebSockets, a number with a unit (us, ms, s, m, h or d), for example 3600s. |
openshift.route.tls.insecureEdgeTerminationPolicy | enum | "Redirect" | What the router does with plain HTTP requests. |
openshift.route.tls.externalCertificateSecret | string | "" | Secret (kubernetes.io/tls, same namespace) with a certificate for the host of the Route. Empty means the default certificate of the router. |
openshift.routerNamespaceSelector.matchLabels | object | {"policy-group.network.openshift.io/ingress":""} | A map from strings to strings. An entry may be null: the values files of the examples switch off an entry of an earlier values file that way, and Helm keeps such a null for a key that values.yaml does not define. |
openshift.routerNamespaceSelector.matchExpressions | array | unset | |
openshift.dns.to | array | see values.yaml | Destinations in NetworkPolicy peer format. |
openshift.dns.ports | array | [5353,53] | Ports of the DNS service, used for UDP and TCP. |
service.type | enum | "ClusterIP" | Service type. |
service.port | integer | 8080 | Port of the HTTP service (UI, REST and OpAMP). |
service.metricsPort | integer | 9090 | Port of the metrics endpoint. |
service.annotations | object | {} | Annotations of the Service. |
networkPolicy.enabled | boolean | true | Render the NetworkPolicy. |
networkPolicy.ingress.from | array | [] | Sources in NetworkPolicy peer format, for example the namespace of the ingress controller or Gateway. Required while ingress or httpRoute is enabled. |
networkPolicy.ingress.allowSameNamespace | boolean | true | Allow every pod of the release namespace to reach port 8080. It must be false while config.server.trustedProxies or config.opamp.clientCertHeader is set. |
networkPolicy.metrics.from | array | [] | Sources in NetworkPolicy peer format. Empty means nobody. |
networkPolicy.egress.enabled | boolean | true | Restrict egress to DNS and the ports below. |
networkPolicy.egress.ports | array | [5432,443] | Allowed destination ports (TCP) for all destinations, by default 5432 for PostgreSQL and 443 for HTTPS. The Git SSH port is added automatically while config.gitops is enabled with an SSH URL. |
networkPolicy.egress.extraPorts | array | [] | Further ports (TCP), merged into the rule above. |
networkPolicy.egress.exceptCidrs | array | ["169.254.0.0/16"] | Destinations that the rule excludes, by default the cloud metadata service. |
networkPolicy.egress.dns.to | array | see values.yaml | Destinations in NetworkPolicy peer format. |
networkPolicy.egress.dns.ports | array | [53] | Ports of the DNS service, used for UDP and TCP. |
networkPolicy.egress.extraRules | array | [] | Further egress rules in NetworkPolicy format. |
ingress.enabled | boolean | false | Create an Ingress. |
ingress.className | string | "" | Ingress class. Empty means the default class of the cluster. |
ingress.annotations | object | {} | Annotations for the controller. OpAMP connections are long-lived WebSockets, so the controller needs long timeouts. |
ingress.hosts | array | see values.yaml | Host names and paths of the Ingress. |
ingress.tls | array | [] | TLS of the Ingress. Required while config.server.publicUrl is https, unless allowWithoutTls is set. |
ingress.allowWithoutTls | boolean | false | Allow an https publicUrl without ingress.tls, because TLS terminates outside the Ingress. |
ingress.allowWithoutTrustedProxies | boolean | false | Allow an empty config.server.trustedProxies, so that all clients appear with the address of the ingress controller. |
httpRoute.enabled | boolean | false | Create an HTTPRoute. |
httpRoute.parentRefs | array | [] | Gateways that the route is bound to, in the format of the Gateway API. Required while enabled. |
httpRoute.hostnames | array | [] | Host names of the route. They must match config.server.publicUrl. Required while enabled. |
httpRoute.annotations | object | {} | Annotations of the HTTPRoute. |
httpRoute.tlsTerminatedAtGateway | boolean | false | Confirms that the Gateway listener terminates TLS. Required with an https publicUrl, because the chart cannot check the Gateway. |
httpRoute.allowWithoutTrustedProxies | boolean | false | Allow an empty config.server.trustedProxies. |
httpRoute.timeouts.request | string | unset | A duration in the format of the Gateway API, for example 30s or 1h30m. |
httpRoute.timeouts.backendRequest | string | unset | A duration in the format of the Gateway API, for example 30s or 1h30m. |
resources.requests | object | {"cpu":"250m","ephemeral-storage":"512Mi","memory":"512Mi"} | Quantities by resource name, for example cpu, memory and ephemeral-storage. |
resources.limits | object | {"ephemeral-storage":"5Gi","memory":"1Gi"} | Quantities by resource name, for example cpu, memory and ephemeral-storage. |
resources.claims | array | unset | |
preStopSleepSeconds | integer | 10 | Pause before the shutdown, so that the endpoint disappears from the load balancers before the connections are closed. |
terminationGracePeriodSeconds | integer | 60 | Grace period of the pod on termination. |
probes.startup.path | string | "/healthz" | HTTP path of the probe. |
probes.startup.failureThreshold | integer | 30 | A number of seconds or attempts, at least 1. |
probes.startup.periodSeconds | integer | 2 | A number of seconds or attempts, at least 1. |
probes.liveness.path | string | "/healthz" | HTTP path of the probe. |
probes.liveness.periodSeconds | integer | 10 | A number of seconds or attempts, at least 1. |
probes.liveness.timeoutSeconds | integer | 3 | A number of seconds or attempts, at least 1. |
probes.liveness.failureThreshold | integer | 3 | A number of seconds or attempts, at least 1. |
probes.readiness.path | string | "/readyz" | HTTP path of the probe. |
probes.readiness.periodSeconds | integer | 5 | A number of seconds or attempts, at least 1. |
probes.readiness.timeoutSeconds | integer | 3 | A number of seconds or attempts, at least 1. |
probes.readiness.failureThreshold | integer | 3 | A number of seconds or attempts, at least 1. |
tests.enabled | boolean | true | Render the test Pod. The Pod exists only while “helm test” runs. “helm template | kubectl apply” creates it as an ordinary Pod; “helm template –skip-tests” leaves it out. |
tests.token.existingSecret | string | "" | Name of the Secret with the token of the test. Empty means that the Pod mounts no Secret. |
tests.token.key | string | "token" | Key of the token in the Secret. |
tests.podAnnotations | object | {} | Annotations of the test Pod, for example the opt-out from the sidecar injection of a service mesh. A sidecar that keeps running stops the Pod from completing. The chart sets the annotations of the hook itself, and a key that starts with helm.sh/hook is refused: it would change or, as a null, remove the hook, and the Pod would become an ordinary resource of the release. |
tests.resources.requests | object | {"cpu":"10m","ephemeral-storage":"16Mi","memory":"32Mi"} | Quantities by resource name, for example cpu, memory and ephemeral-storage. |
tests.resources.limits | object | {"ephemeral-storage":"64Mi","memory":"64Mi"} | Quantities by resource name, for example cpu, memory and ephemeral-storage. |
podDisruptionBudget.enabled | boolean | true | Create the PodDisruptionBudget. |
podDisruptionBudget.minAvailable | integer or string | 1 | Minimum number or percentage of available pods. Ignored while maxUnavailable is set. |
podDisruptionBudget.maxUnavailable | integer or string | unset | Maximum number or percentage of unavailable pods. It replaces minAvailable when set. |
podAnnotations | object | {} | Annotations of the pods. |
podLabels | object | {} | Additional labels of the pods. |
priorityClassName | string | "" | Priority class of the pods. |
podSecurityContext.runAsNonRoot | boolean | true | |
podSecurityContext.runAsUser | integer | 65532 | |
podSecurityContext.runAsGroup | integer | 65532 | |
podSecurityContext.fsGroup | integer | 65532 | |
podSecurityContext.seccompProfile.type | enum | "RuntimeDefault" | |
securityContext.allowPrivilegeEscalation | boolean | false | |
securityContext.readOnlyRootFilesystem | boolean | true | |
securityContext.privileged | boolean | unset | |
securityContext.runAsNonRoot | boolean | unset | |
securityContext.runAsUser | integer | unset | |
securityContext.runAsGroup | integer | unset | |
securityContext.capabilities.add | array | unset | |
securityContext.capabilities.drop | array | ["ALL"] | |
securityContext.seccompProfile.type | enum | unset | |
persistence.enabled | boolean | false | Mount a PersistentVolumeClaim instead of an emptyDir. |
persistence.existingClaim | string | "" | Name of an existing claim. Empty means that the chart creates one. |
persistence.storageClass | string | "" | Storage class of the created claim. Empty means the default class. |
persistence.accessModes | array | ["ReadWriteMany"] | Access modes of the created claim. |
persistence.size | number or string | "10Gi" | Size of the created claim. |
emptyDir.sizeLimit | number or string | "4Gi" | Size limit of the emptyDir volume. It must stay above config.artifacts.maxCacheBytes. |
topologySpreadConstraints | array | see values.yaml | Spreads the replicas, in the format of a Kubernetes pod. The chart adds the label selector of the release to every entry. |
nodeSelector | object | {} | Node selector of the pods. |
tolerations | array | [] | Tolerations of the pods. |
affinity.nodeAffinity | object | unset | |
affinity.podAffinity | object | unset | |
affinity.podAntiAffinity | object | unset | |
serviceMonitor.enabled | boolean | false | Create the ServiceMonitor. |
serviceMonitor.interval | string | "30s" | Scrape interval, for example 30s. |
serviceMonitor.labels | object | {} | Labels of the ServiceMonitor, for example the label that selects it in the Prometheus resource. |
prometheusRule.enabled | boolean | false | Create the PrometheusRule. |
prometheusRule.labels | object | {} | Labels of the PrometheusRule, for example the label that selects it in the Prometheus resource (ruleSelector). |
Misconfiguration messages
Available from v0.2.0. The schema, the first of the two layers. The chart of v0.1.0 has only the messages of the templates.
Two layers refuse a value, and both name it. The schema refuses what one value can get wrong (an unknown key, a wrong type,
a malformed string) before it renders anything; Helm reports the path of the value as a JSON pointer. The templates refuse
what depends on several values, and they repeat a few checks of the schema (the digest, the OpenShift mode, the names, the keys
that the chart generates or does not offer) for the case that the validation is off. A template message is an execution error
that starts with the value path, followed by what is wrong and how to fix it, for example
config.server.trustedProxies: must not be empty when ingress.enabled is true; .... The complete list of paths:
| Path | Cause |
|---|---|
image.digest | not of the form sha256:<64 hex> |
image.repository | empty, for example set to null |
config.server.trustedProxies | empty while ingress.enabled or httpRoute.enabled (unless allowWithoutTrustedProxies) |
ingress.tls | empty with an https publicUrl (unless allowWithoutTls) |
networkPolicy.ingress.from | empty while networkPolicy.enabled and ingress.enabled or httpRoute.enabled |
networkPolicy.ingress.allowSameNamespace | true while trustedProxies or opamp.clientCertHeader is set |
httpRoute.parentRefs | empty while httpRoute.enabled |
httpRoute.hostnames | empty while httpRoute.enabled |
httpRoute.tlsTerminatedAtGateway | not true with an https publicUrl |
openshift.enabled | not true, false or auto |
openshift.route.host | no host derivable from openshift.route.host or publicUrl |
config.auth.bootstrapAdmin | set in config; use the top-level bootstrapAdmin values |
config.opamp.enrollmentTokens | set in config; use the top-level enrollmentTokens values |
config.auth.devLogin | not available in this chart |
config.dev | not available in this chart |
config.database.dsn | set in config (the ConfigMap would hold the password); put the DSN into the Secret and use config.database.dsnFile |
config.auth.oidc.clientSecret | set in config; put it into the Secret and use config.auth.oidc.clientSecretFile |
config.gitops.webhookSecret | set in config; put it into the Secret and use config.gitops.webhookSecretFile |
config.notifications.channels[<i>].url | set in config; put the URL into the Secret and set urlFile |
bootstrapAdmin.enabled | no login path: bootstrapAdmin.enabled is false while neither config.auth.oidc.enabled nor oidcFromEnvironment is true |
bootstrapAdmin.tokenKey | existingSecret set without tokenKey and passwordHashKey |
bootstrapAdmin.passwordHashKey | set without existingSecret |
bootstrapAdmin.username | not 1 to 64 characters of letters, digits, ., _, @, - |
enrollmentTokens[<i>].name | not a DNS label, or duplicate |
tests.token.key | empty while tests.token.existingSecret is set |
tests.token.existingSecret | a Secret of the server itself: existingSecret or the bootstrap Secret (generated, or bootstrapAdmin.existingSecret) |
tests.podAnnotations | a key that starts with helm.sh/hook (the schema names the key; the template repeats the check) |
Helm test
Available from v0.2.0. The chart of v0.1.0 has no test.
helm test myriarch -n myriarch
runs one Pod (templates/tests/test-connection.yaml, the Helm hook test) with the image of the server and the command
myriarchd healthcheck against the Service of the release
(http://<fullname>.<namespace>.svc:<service.port>):
GET /readyzmust answer 200: the database answers, the schema fits and the event bus is connected.- A WebSocket upgrade at
config.opamp.pathmust be refused with 401. That shows that the route and the authentication are in place, and the Pod needs no credential for it.
The test goes through the Service, so it checks one ready replica, not every replica. The command sends no OpAMP message, so it
registers no agent and stores nothing (Server behaviour).
The server counts the second request as an unauthorized OpAMP connect (myriarch_opamp_connect_rejected_total{reason="unauthorized"}),
one for each run of the test; the alert MyriarchUnauthorizedConnects does not notice that. The Pod exists only while the test
runs: a successful Pod is deleted, a failed one stays for kubectl -n myriarch logs <fullname>-test-connection until the next run.
The output names the failing request, for example myriarchd: readyz failed: GET http://myriarch.myriarch.svc:8080/readyz answered 503, want 200.
The Pod follows the restricted Pod Security profile: non-root, no privilege escalation, all capabilities dropped, seccomp
RuntimeDefault, a read-only root file system and no service account token. The OpenShift profile (openshift.enabled) leaves the user, the
group and the fsGroup to the SCC, as for the server. It has no environment variable, mounts no Secret unless you opt in
(below), uses image.*, imagePullSecrets, nodeSelector and tolerations of the server, and takes its requests and limits from
tests.resources (default 10m CPU and 32Mi memory requested, 64Mi memory limit). tests.podAnnotations adds annotations, for
example the opt-out from the sidecar injection of a service mesh (sidecar.istio.io/inject: "false"): a sidecar that keeps running
stops the Pod from completing, and helm test then waits for it. The chart sets the annotations of the hook itself, so it refuses a
key that starts with helm.sh/hook (a changed or null value would turn the Pod into an ordinary resource of the release).
tests.enabled: false removes the Pod.
helm template | kubectl apply and tools that render hooks as ordinary resources create it as a normal Pod that runs once and
ends: use helm template --skip-tests or tests.enabled: false there.
helm upgrade --reuse-values from a release of v0.1.0 renders with the values of that release in place of the defaults of the chart,
and they have no tests and no prometheusRule: the chart then renders neither the Pod nor the PrometheusRule, as v0.1.0 did. Upgrade
with --reset-then-reuse-values or a values file to get the defaults (Install with Helm).
Complete handshake with a token (opt-in)
With a token the test also proves that the server accepts an enrollment token: the upgrade must be answered with 101, and the command closes the connection at once without a message. Create a token for the test only (in the UI under Settings, Enrollment tokens, with a label of its own and a short expiry; never the token of your agents), put it into a Secret and name the Secret:
kubectl -n myriarch create secret generic myriarch-test-token --from-file=token=./test-token
helm upgrade myriarch ... --set tests.token.existingSecret=myriarch-test-token # tests.token.key is token
The Pod mounts that one key, read-only, at /etc/myriarchd-test/token, and the command reads the token from there and nowhere
else: it is never an argument or an environment variable and it is never printed. The chart refuses the Secrets of the server
itself (existingSecret and the bootstrap Secret) as the Secret of the test, the schema refuses a name or key in the form of an
enrollment token (myre_...), and the command refuses a file that holds anything but one enrollment token, so a Secret named by
mistake is not sent to the server. Revoke the token when you no longer run the test.
Network access of the test
The NetworkPolicy selects the pods of the server, not the test Pod. For port 8080 of the server:
allowSameNamespace | trustedProxies or clientCertHeader | Source for the test Pod |
|---|---|---|
true (default) | not set | none needed: every pod of the namespace is a source |
false | not set | the chart adds one: the pods with the labels of the test Pod |
false (required) | set | none: the test cannot connect unless you list the test Pod yourself |
The last row is deliberate. Those settings are safe only if no pod of the namespace reaches port 8080 directly (any pod could
forge the forwarded client address or certificate), and every pod that carries the labels of the test Pod could use a rule for
it. To run helm test there, accept that and name the labels in networkPolicy.ingress.from (the chart prints this hint with
the install notes as well):
networkPolicy:
ingress:
allowSameNamespace: false
from:
- namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: <ingress-namespace>}}
- podSelector:
matchLabels:
app.kubernetes.io/component: test
app.kubernetes.io/instance: myriarch # the release name
app.kubernetes.io/name: myriarch-test # the name of the chart, or nameOverride, and -test
A cluster with a default-deny policy for the namespace (an egress rule for the test Pod, DNS) needs rules of its own for the test Pod as well.
tests.enabled: false removes the Pod and the rule: the NetworkPolicy is then the one of v0.1.0.
When the test fails
| Output of the Pod | Cause |
|---|---|
readyz failed: cannot reach ...: no answer within 10s, or connection refused | No ready replica, or the NetworkPolicy keeps the test Pod out (Network access of the test), or the Service name does not resolve in this cluster |
readyz failed: ... answered 503, want 200: the server is not ready | The server reports that its database, schema or event bus is not ready; its log names the cause |
opamp failed: the upgrade at /v1/opamp was answered with 404 | config.opamp.path differs from the path that the server serves, for example an environment override in extraEnv |
opamp failed: ... was refused with 401 although a token was given | The token of the test is revoked, expired or unknown, or the server also requires a client certificate |
--token-file: file ... must contain an enrollment token | The key of tests.token.key holds something else (the Pod then makes no request) |
Alerting rules
Available from v0.2.0.
prometheusRule.enabled=true creates a Prometheus Operator PrometheusRule (CRD monitoring.coreos.com/v1; the cluster needs
the CRD, otherwise helm install fails) with the alerts of the server. prometheusRule.labels adds labels, for example the one that
the ruleSelector of the Prometheus resource uses (release: kube-prometheus-stack). There is no dashboard.
| Group | Alert | Severity | Fires when |
|---|---|---|---|
| availability | MyriarchNoLeader | critical | no replica holds the leader lock for 3 minutes |
| availability | MyriarchMultipleLeaders | critical | more than one replica reports the leader lock for 3 minutes |
| availability | MyriarchReplicaNotReady | warning | fewer than replicaCount pods are available for 10 minutes (kube-state-metrics) |
| availability | MyriarchTargetDown | warning | the metrics of a replica are unreachable for 5 minutes (networkPolicy.metrics.from) |
| agents | MyriarchAgentConnectionsDropped | warning | the OpAMP connections fall by more than 20 % within 15 minutes |
| agents | MyriarchConnectRejected | warning | agents are rejected with 429 or 503 (connection limits) for 10 minutes |
| agents | MyriarchUnauthorizedConnects | info | more than one unauthorized OpAMP connect per second for 15 minutes |
| jobs | MyriarchReleaseTrackerErrors | warning | the release tracker fails twice within 3 hours |
| jobs | MyriarchGitOpsSyncFailing | warning | the GitOps repository is invalid or unreachable for 15 minutes |
| jobs | MyriarchGitOpsStale | warning | no successful GitOps sync for 30 minutes |
| jobs | MyriarchRolloutFailed | warning | a rollout moved to failed |
| jobs | MyriarchNotificationsFailing | info | a notification channel fails for 10 minutes |
| jobs | MyriarchHousekeepingFailing | info | housekeeping runs fail for 10 minutes |
The rules select the series of the server by job and namespace. The job label is the name of the Service, because the
ServiceMonitor of this chart (serviceMonitor.enabled=true) sets no jobLabel; with another scrape configuration use the
example for clusters without Helm, which has the same rules for the
release myriarch in the namespace myriarch, and adapt the selectors. ci/helm-conform.sh fails when the two differ. Details of
the metrics: Metrics.
Production example
A complete, neutral example (HA values with OIDC, gateway collector overlay, secrets script and an offline verification
script) lives in deploy/examples/production/; see its README. The overlays in deploy/helm/myriarch/examples/
(ingress-*.yaml, openshift.yaml, quickstart-dex.yaml, dex/dex.yaml, install-guide.yaml) are not part of the packaged
chart. install-guide.yaml is the values file of the guide Install with Helm: minimal
installation, GitOps over HTTPS, an HTTPRoute and one declared enrollment token.
Verification
helm lint deploy/helm/myriarch
helm template myriarch deploy/helm/myriarch -f my-values.yaml | kubectl apply --dry-run=server -f -
# validate the rendered configuration without starting myriarchd (files and secrets do not have to exist):
helm template myriarch deploy/helm/myriarch -f my-values.yaml | yq 'select(.kind == "ConfigMap").data["myriarchd.yaml"]' > myriarchd.yaml
myriarchd config validate --config myriarchd.yaml --skip-files
# render the OpenShift profile without a cluster:
helm template myriarch deploy/helm/myriarch --set openshift.enabled=true
helm template myriarch deploy/helm/myriarch --set openshift.enabled=auto --api-versions security.openshift.io/v1
# everything together for the production example (lint, render, config validate, kustomize, kubeconform):
deploy/examples/production/verify.sh
# CI: both profiles, all examples and the assertions of the chart contract:
ci/helm-conform.sh
# after the installation: /readyz and the OpAMP endpoint through the Service (see "Helm test")
helm test myriarch -n myriarch