Collector Helm chart and values

Runs an OpenTelemetry Collector that the Myriarch server manages over OpAMP, as a StatefulSet (a gateway with a stable identity per replica) or a DaemonSet (one collector per node). Every pod is its own OpAMP agent: remote configuration with rollback, health, effective configuration, component list and restart work as on a host.

The chart runs the unchanged upstream images. Nothing is built from them:

ContainerImageWhat it does
init install-updaterghcr.io/myriarch/myriarch-updatermyriarch-updater install-self /shared copies its own static binary to a shared volume
init seed-collectorotel/opentelemetry-collector-contrib/shared/myriarch-updater copy /otelcol-contrib /shared/ copies the collector binary (the image has no shell and no cp)
collectorotel/opentelemetry-collector-opampsupervisorthe OpAMP Supervisor starts the collector from the shared volume

The pod runs as user and group 10001 by default (overridable; the OpenShift profile sets no ID), adds no capability, has a read-only root file system and runs under the Pod Security level restricted (StatefulSet). The documentation for operators is in Install the collector with Helm; the design of the optional container mode is in docs/design/container-mode.md.

Available from v0.2.0. The values are validated against a strict schema, values.schema.json: helm install, upgrade, template and lint refuse a key that the chart does not know, a value of the wrong type and a malformed value, before anything is rendered.

Minimal installation

Two things are needed: a Secret with the enrollment token and the wss:// address of the server.

kubectl create namespace observability
kubectl -n observability create secret generic myriarch-enrollment --from-literal=token='myre_...'
# from a checkout of the repository, or, from v0.2.0, from the signed OCI chart (verify it first, see the operations guide):
helm install otel-gateway deploy/helm/myriarch-collector -n observability \
  --set server.endpoint=wss://fleet.example.com/v1/opamp --set site.id=k8s-de-prod --set site.name="Regional gateway DE"
helm install otel-gateway oci://ghcr.io/myriarch/charts/myriarch-collector --version 0.2.1 -n observability \
  --set server.endpoint=wss://fleet.example.com/v1/opamp

A node agent:

helm install otel-agent deploy/helm/myriarch-collector -n observability --set mode=daemonset \
  --set server.endpoint=wss://fleet.example.com/v1/opamp --set-json 'tolerations=[{"operator":"Exists"}]'

The chart renders without the namespace; it uses the namespace of the release.

Modes

mode: statefulset (default)mode: daemonset
Workloadone pod per replica (replicaCount)one pod per node
Identity (instance UID)a PVC per replica (persistence.size)an emptyDir (a restarted pod is a new agent), or a hostPath with persistence.hostPath.enabled
Servicesa headless Service and, with service.enabled, one for ports (OTLP 4317 and 4318)none
site.type defaultk8s-gatewayk8s-node
Node logsnonodeLogs.enabled: read-only hostPath
Container modecontainerMode.enablednot available (the volume of the binaries needs a claim per replica)

The configuration of the collector is not part of the chart: it comes from the Myriarch server (a fleet that matches site.type). The chart ships one local file (healthCheck.enabled, default on) with the health_check extension on 0.0.0.0:13133, the port of the readiness probe. The supervisor merges it in every state except the startup fallback (whose file carries the same endpoint), before the configuration from the server, so the pod is Ready without an assignment. With an assignment the server wins on conflicts: maps are merged with the server’s values last, and service.extensions is concatenated, so a fleet cannot remove health_check, but it can move it. A fleet configuration that sets extensions.health_check.endpoint to localhost or another port makes the pod not Ready; keep it on the pod IP (0.0.0.0:13133). The fallback configuration is separate and applies only while the server is unreachable at startup. A fleet configuration must not put a second health_check instance (health_check/<name>) or another extension on port 13133: the collector then fails to start, otelcol validate does not catch it, and automatic_config_rollback reverts the configuration. With healthCheck.enabled: false the pod is Ready only if the fleet configuration has health_check on the pod IP, or while the startup fallback runs.

Pod Security

  • User and group 10001, as overridable defaults. The supervisor image runs under a named user, so the kubelet cannot check runAsNonRoot: true without a numeric ID. The standard profile therefore sets runAsUser, runAsGroup and fsGroup to 10001 on the pod, so that the init containers and the main container share one identity that can read and write /shared and the storage volume. Override a value, or set it to null to leave the field out. The OpenShift profile renders none of the three; the SCC assigns them from the range of the project.
  • Every container: allowPrivilegeEscalation: false, capabilities.drop: [ALL], readOnlyRootFilesystem: true, runAsNonRoot: true; the pod has seccompProfile: RuntimeDefault. No Kubernetes API token is mounted (serviceAccount.automountToken: false).
  • Writable paths: the storage volume, an emptyDir for /tmp and, in container mode, the binaries volume. The shared volume is writable for the init containers and read-only for the main container.
  • The only hostPath volumes exist in the DaemonSet and only when you ask for them: nodeLogs.enabled (read-only, for a filelog receiver) and persistence.hostPath.enabled (the identity of a node agent). The Pod Security levels baseline and restricted refuse a hostPath volume: label the namespace privileged, or use an exception of your admission policy. The chart has no root init container: prepare the directory of persistence.hostPath on the nodes, writable for the pod user (10001 by default); the pod log files must be readable for that user. containerd creates them as 0640 root:root and CRI-O as 0600, so neither “others” nor the user’s group can read them: with containerd set podSecurityContext.supplementalGroups: [0] (a group ID, not a user ID; the OpenShift profile removes supplementalGroups, so grant the group through the SCC there); with CRI-O that is not enough.
  • NetworkPolicy (networkPolicy.enabled): the ports of the receivers are reachable from the release namespace and from the namespaces that carry the label myriarch.dev/otlp-client=true (and the sources in networkPolicy.ingress.from); the health ports only from the release namespace. Egress stays open, because the server, the backends and DNS differ per environment.
  • RBAC: the chart creates no Role. A collector that needs the Kubernetes API (the k8sattributes processor) needs a ServiceAccount with a read-only Role that you create; set serviceAccount.create=false, serviceAccount.name and serviceAccount.automountToken=true.

OpenShift

openshift.enabled=true (or auto) selects the OpenShift profile: no runAsUser, runAsGroup or fsGroup, so the SCC restricted-v2 assigns them from the range of the project. The StatefulSet runs under restricted-v2 without further grants. The DaemonSet with hostPath volumes needs an SCC that allows them for its ServiceAccount (and the files of the node logs must be readable under the SELinux context of the pod); the chart does not create a binding.

Container mode

containerMode.enabled=true makes myriarch-updater PID 1 of the main container. It starts the supervisor as a child, swaps the collector binary on a second volume (volumeClaimTemplate binaries) when the server asks for a new version, and rolls back on a failed health check. The image then no longer says which collector runs. Probes use the updater on port 13135 (/healthz, /readyz), and terminationGracePeriodSeconds is at least 360 seconds. The verification of the downloaded collector needs the Sigstore trusted root in a ConfigMap (containerMode.trustedRoot.existingConfigMap, created from myriarch-updater fetch-trusted-root); switching the verification off is an explicit containerMode.update.requireSignature=false.

Values

The values are validated against values.schema.json. The table is generated by make docs-gen from the schema and values.yaml; edit values.schema.source.yaml, never the schema or the table. Defaults and long explanations are in values.yaml.

ValueTypeDefaultDescription
modeenum"statefulset"statefulset - a gateway with a stable identity per replica (a PVC). daemonset - one collector per node.
replicaCountinteger1Number of replicas of the StatefulSet. Every replica is its own OpAMP agent. The DaemonSet ignores it.
images.updater.registrystring"ghcr.io"Registry host, with an optional port, of the image, for example ghcr.io. It applies when repository has no registry host of its own, and global.imageRegistry takes precedence over it. Empty means the default registry of the container runtime.
images.updater.repositorystring"myriarch/myriarch-updater"Repository of the image without a registry host, for example myriarch/myriarch-updater. A repository that starts with a registry host is used unchanged and neither registry nor global.imageRegistry applies.
images.updater.tagstring""Image tag. Empty means the appVersion of the chart.
images.updater.digeststring""Image digest, sha256: followed by 64 hexadecimal characters. When set, the image is referenced as repository@digest and the tag is ignored.
images.updater.pullPolicyenum"IfNotPresent"Image pull policy.
images.collector.registrystring"docker.io"Registry host, with an optional port, of the image, for example ghcr.io. It applies when repository has no registry host of its own, and global.imageRegistry takes precedence over it. Empty means the default registry of the container runtime.
images.collector.repositorystring"otel/opentelemetry-collector-contrib"Repository of the image without a registry host, for example myriarch/myriarch-updater. A repository that starts with a registry host is used unchanged and neither registry nor global.imageRegistry applies.
images.collector.tagstring"0.162.0"Image tag. Empty means the appVersion of the chart.
images.collector.digeststringsee values.yamlImage digest, sha256: followed by 64 hexadecimal characters. When set, the image is referenced as repository@digest and the tag is ignored.
images.collector.pullPolicyenum"IfNotPresent"Image pull policy.
images.supervisor.registrystring"docker.io"Registry host, with an optional port, of the image, for example ghcr.io. It applies when repository has no registry host of its own, and global.imageRegistry takes precedence over it. Empty means the default registry of the container runtime.
images.supervisor.repositorystring"otel/opentelemetry-collector-opampsupervisor"Repository of the image without a registry host, for example myriarch/myriarch-updater. A repository that starts with a registry host is used unchanged and neither registry nor global.imageRegistry applies.
images.supervisor.tagstring"0.162.0"Image tag. Empty means the appVersion of the chart.
images.supervisor.digeststringsee values.yamlImage digest, sha256: followed by 64 hexadecimal characters. When set, the image is referenced as repository@digest and the tag is ignored.
images.supervisor.pullPolicyenum"IfNotPresent"Image pull policy.
imagePullSecretsarray[]Secrets for pulling the images from a private registry.
nameOverridestring""Overrides the chart name in resource names.
fullnameOverridestring""Overrides the complete resource name.
global.imageRegistrystring or nullunsetRegistry host, with an optional port, that replaces images.*.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.
enabledbooleanunsetRead by the dependency condition of a parent chart, not by this chart, which ignores it. It has no default and no effect on a standalone installation.
serviceAccount.createbooleantrueCreate the ServiceAccount.
serviceAccount.namestring""Name of the ServiceAccount. Empty means a name generated from the release name.
serviceAccount.annotationsobject{}Annotations of the ServiceAccount.
serviceAccount.automountTokenbooleanfalseMount the API token into the pod. Off by default: the pod runs configuration from Git. Turn it on only with a dedicated ServiceAccount whose Role is read-only, for a collector that needs the Kubernetes API.
server.endpointstring"wss://fleet.example.com/v1/opamp"OpAMP endpoint of the Myriarch server. wss:// is the default; ws:// is refused unless allowInsecureTransport is true.
server.allowInsecureTransportbooleanfalseExplicit opt-in to an unencrypted ws:// endpoint. The enrollment token then travels in clear text.
server.tls.existingSecretstring""Name of an existing Secret with the CA bundle. Empty means none. Not together with existingConfigMap.
server.tls.existingConfigMapstring""Name of an existing ConfigMap with the CA bundle. Empty means none. Not together with existingSecret.
server.tls.keystring"ca.pem"Key of the PEM bundle in the Secret or ConfigMap.
enrollment.existingSecretstring"myriarch-enrollment"Name of the Secret in the release namespace.
enrollment.keystring"token"Key of the token in the Secret.
site.typestring""site.type. Empty means k8s-gateway for the StatefulSet and k8s-node for the DaemonSet.
site.idstring""site.id.
site.namestring""site.name.
site.countrystring""site.country.
site.regionstring""site.region.
site.timezonestring""site.timezone.
site.environmentstring""deployment.environment.name.
site.clusterNamestring""k8s.cluster.name.
extraEnvarray[]Additional environment variables of the main container, in the format of a Kubernetes container. The collector inherits the environment of the supervisor.
extraEnvFromarray[]Additional environment sources (Secrets, ConfigMaps) of the main container, in the format of a Kubernetes container.
healthCheck.enabledbooleantrueMount the local health configuration. Off means the pod is Ready only if the fleet configuration has health_check on the pod IP.
portsarraysee values.yamlThe ports of the receivers of the collector configuration: declared on the container, offered by the Service of the StatefulSet and allowed by the NetworkPolicy. The health ports are always declared.
service.enabledbooleantrueCreate the Service.
service.typeenum"ClusterIP"Service type.
service.annotationsobject{}Annotations of the Service.
persistence.sizenumber or string"1Gi"StatefulSet - size of the PVC “storage” of every replica.
persistence.storageClassstring""StatefulSet - storage class of the PVC. Empty means the default class.
persistence.accessModesarray["ReadWriteOnce"]StatefulSet - access modes of the PVC.
persistence.hostPath.enabledbooleanfalseUse a hostPath volume for the storage directory.
persistence.hostPath.pathstring""Absolute directory on the node. Empty means /var/lib/myriarch/<fullname>.
persistence.hostPath.typeenum"DirectoryOrCreate"Type of the hostPath volume.
nodeLogs.enabledbooleanfalseMount the logs of the node read-only.
nodeLogs.hostPathstring"/var/log/pods"Absolute directory on the node.
nodeLogs.mountPathstring"/var/log/pods"Absolute mount path in the container.
containerMode.enabledbooleanfalseRun the updater as PID 1 of the main container.
containerMode.binaries.sizenumber or string"3Gi"Size of the PVC “binaries”.
containerMode.binaries.storageClassstring""Storage class of the PVC. Empty means the default class.
containerMode.seedPolicyenum"newest"newest - the higher version wins, a tie keeps the volume. image - the binary of the image is always activated (emergency switch).
containerMode.keepVersionsinteger2Versions of the collector kept on the volume, at least 2.
containerMode.update.requireSignaturebooleantrueVerify the Sigstore bundle of a downloaded collector. Needs trustedRoot.existingConfigMap.
containerMode.update.healthTimeoutstring"120s"Time to wait for the health check after a swap.
containerMode.trustedRoot.existingConfigMapstring""Name of an existing ConfigMap with the trusted root. Empty means none.
containerMode.trustedRoot.keystring"trusted_root.json"Key of the trusted root in the ConfigMap.
containerMode.stopTimeoutstring"30s"Time from SIGTERM to SIGKILL for the supervisor.
resources.requestsobject{"cpu":"100m","memory":"512Mi"}Quantities by resource name, for example cpu, memory and ephemeral-storage.
resources.limitsobject{"memory":"1Gi"}Quantities by resource name, for example cpu, memory and ephemeral-storage.
initResources.requestsobject{"cpu":"10m","memory":"32Mi"}Quantities by resource name, for example cpu, memory and ephemeral-storage.
initResources.limitsobject{"memory":"1Gi"}Quantities by resource name, for example cpu, memory and ephemeral-storage.
terminationGracePeriodSecondsinteger45Grace period of the pod on termination, in seconds. In container mode the pod gets at least 360.
podSecurityContext.runAsNonRootbooleantrue
podSecurityContext.runAsUserinteger10001
podSecurityContext.runAsGroupinteger10001
podSecurityContext.fsGroupinteger10001
podSecurityContext.fsGroupChangePolicyenum"OnRootMismatch"
podSecurityContext.seccompProfile.typeenum"RuntimeDefault"
securityContext.allowPrivilegeEscalationbooleanfalse
securityContext.readOnlyRootFilesystembooleantrue
securityContext.privilegedbooleanunset
securityContext.runAsNonRootbooleantrue
securityContext.runAsUserintegerunset
securityContext.runAsGroupintegerunset
securityContext.capabilities.addarrayunset
securityContext.capabilities.droparray["ALL"]
securityContext.seccompProfile.typeenumunset
tmpSizeLimitnumber or string"256Mi"Size limit of the emptyDir for /tmp.
sharedSizeLimitnumber or string"1Gi"Size limit of the emptyDir “shared” that holds the updater and the seeded collector binary.
tolerationsarray[]Tolerations of the pods.
nodeSelectorobject{}Node selector of the pods.
affinity.nodeAffinityobjectunset
affinity.podAffinityobjectunset
affinity.podAntiAffinityobjectunset
topologySpreadConstraintsarray[]Spreads the replicas, in the format of a Kubernetes pod. The chart adds the label selector of the release to every entry.
priorityClassNamestring""Priority class of the pods.
podAnnotationsobject{}Annotations of the pods.
podLabelsobject{}Additional labels of the pods.
updateStrategy.maxUnavailableinteger or string"10%"Maximum number or percentage of unavailable pods during a rolling update.
extraVolumesarray[]Additional volumes of the pod, in the format of a Kubernetes pod.
extraVolumeMountsarray[]Additional volume mounts of the main container, in the format of a Kubernetes container.
openshift.enabledenumfalsetrue, false or auto.
networkPolicy.enabledbooleantrueCreate the NetworkPolicy.
networkPolicy.ingress.allowSameNamespacebooleantrueAllow every pod of the release namespace to reach the ports.
networkPolicy.ingress.fromarraysee values.yamlFurther sources of the ports in NetworkPolicy “from” format.

Checks of the chart

ci/lint-helm.sh lints the chart (default values, both modes, container mode, the OpenShift profile) and packages it; ci/helm-conform.sh renders it for several Kubernetes versions, validates every manifest with kubeconform and asserts the contract above, including the guards that make rendering fail (ws:// without server.endpoint opt-in, container mode on a DaemonSet, a missing trusted root).