This is the multi-page printable view of this section. Click here to print.
AgileTV CDN Manager (esb3027)
- 1: Getting Started
- 2: System Requirements Guide
- 3: Networking Guide
- 4: Architecture Guide
- 5: Installation Guide
- 5.1: Quickstart
- 5.2: Inventory
- 5.3: Site Settings
- 5.4: TLS Certificates
- 5.5: MaxMind GeoIP Databases
- 5.6: Application Settings
- 5.7: Running deploy
- 5.8: Changing the Cluster
- 5.9: Air-Gapped Installation
- 5.10: Upgrade Guide
- 5.11: Installation Troubleshooting
- 5.12: Next Steps
- 6: Configuration Guide
- 7: Performance Tuning Guide
- 8: Operations Guide
- 9: Metrics & Monitoring Guide
- 10: API Guide
- 10.1: Authentication API
- 10.2: Health API
- 10.3: Selection Input API
- 10.4: Data Store API
- 10.5: Subnets API
- 10.6: Routing API
- 10.7: Discovery API
- 10.8: Metrics API
- 10.9: Configuration API
- 10.10: Operator UI API
- 10.11: OpenAPI Specification
- 11: Troubleshooting Guide
- 12: Command Line Reference
- 12.1: deploy
- 12.2: acd-admin
- 12.3: acd-support-dump
- 13: Glossary
1 - Getting Started
Overview
The AgileTV CDN Manager (product code ESB3027) is a cloud-native control plane for managing CDN deployments. It provides centralized orchestration for authentication, configuration, routing, and metrics collection across CDN infrastructure.
Before You Start:
- Deployment type: Lab (single-node) or Production (multi-node)? See Installation Guide
- Hardware: Nodes meeting specifications for your deployment type
- OS: RHEL 9.4 or later, or a compatible clone (Oracle Linux, AlmaLinux, Rocky Linux)
- Software: Installation ISO from AgileTV customer portal
- Network: A default route on every node and, optional but recommended, a backplane network for node-to-node traffic, separate from the public network. See the Networking Guide
Deployment Models
| Deployment Model | Description | Typical Use Case |
|---|---|---|
| Self-Hosted | K3s Kubernetes cluster on customer premises | Production deployments |
| Lab/Single-Node | Minimal single-node installation | Acceptance testing, demonstrations, development |
Functionality remains consistent across deployment models.
Prerequisites
- Installation ISO: Obtain
esb3027-cdn-manager-X.Y.Z.isofrom AgileTV customer portal - OS: RHEL 9.4 or later, or a compatible clone (Oracle Linux, AlmaLinux, Rocky Linux)
- Kubernetes familiarity: Basic understanding of pods and deployments helps with troubleshooting
For detailed hardware, network, and operating system requirements, see the System Requirements Guide.
Installation
Ready to install? The Quickstart takes three freshly installed machines to a working cluster, step by step, with the deploy command on the installation ISO. The Installation Guide covers the details and variations, including single-node labs.
Accessing the System
Following successful deployment, the following interfaces are available:
| Service | URL Path | Authentication |
|---|---|---|
| MIB Frontend | /gui | Zitadel SSO |
| API Gateway | /api | Bearer token |
| Zitadel Console | /ui/console | See Glossary |
| Grafana | /grafana | See Glossary |
All services are accessed via https://<cluster-host><path>.
Important:
<cluster-host>must be the main manager hostname, the first entry ofmanager_hostnamesin the site settings. Pages may load under another name, but signing in only works using the configured hostname. To reach the system by IP address, configure that IP address at installation time.
Note: Without a TLS certificate of your own, a temporary self-signed certificate is served, and browsers show a warning. For production deployments, install a certificate from a trusted certificate authority before exposing the system to users. See TLS Certificates.
Initial user configuration is performed through Zitadel. See Next Steps for securing the administrator account and adding users. For detailed guidance on managing users, roles, and permissions in the Zitadel Console, see Zitadel’s User Management Documentation.
Documentation Navigation
The following guides provide detailed information for specific operational tasks:
| Guide | Description |
|---|---|
| System Requirements | Hardware, operating system, and network specifications |
| Architecture | Detailed system architecture and scaling guidance |
| Installation | Step-by-step installation and upgrade procedures |
| Configuration | System configuration and customization |
| Performance Tuning | Optimization tips for improved performance |
| API Guide | REST API reference and integration examples |
| Operations | Day-to-day operational procedures |
| Metrics & Monitoring | Monitoring dashboards and alerting configuration |
| Troubleshooting | Common issues and resolution procedures |
| Glossary | Definitions of technical terms |
| Release Notes | Version-specific changes and known issues |
2 - System Requirements Guide
Overview
This document specifies the hardware, operating system, and networking requirements for deploying the AgileTV CDN Manager (ESB3027). Requirements vary based on deployment type and node role within the cluster.
Cluster Sizing
Production Deployments
Production deployments require a minimum of three nodes to achieve high availability. The cluster architecture employs distinct node roles:
| Role | Description |
|---|---|
| Server Node (Control Plane Only) | Runs control plane components (etcd, Kubernetes API server) only; does not host application workloads; requires separate Agent nodes |
| Server Node (Combined) | Runs control plane components and hosts application workloads; default configuration |
| Agent Node | Executes application workloads only; does not participate in cluster quorum |
Server nodes can be deployed in either Control Plane Only or Combined role configurations. The choice depends on your deployment requirements:
- Control Plane Only: Dedicated control plane nodes with lower resource requirements; requires separate Agent nodes for workloads
- Combined: Server nodes run both control plane and workloads; minimum 3 nodes required for HA
Why Use Control Plane Only Nodes?
Dedicated Control Plane Only nodes provide several benefits for larger deployments:
- Resource Isolation: Control plane components (etcd, API server, scheduler) run on dedicated hardware without competing with application workloads for CPU and memory
- Stability: Application workload spikes or misbehaving pods cannot impact control plane performance
- Security: Smaller attack surface on control plane nodes; fewer containers and services running
- Predictable Performance: Control plane responsiveness remains consistent regardless of application load
- Flexible Sizing: Control Plane Only nodes can use lower-specification hardware (2 cores, 4 GiB) since they don’t run application workloads
For most small to medium deployments, Combined role servers are simpler and more cost-effective. Control Plane Only nodes are recommended for larger deployments with significant workload requirements or where control plane stability is critical.
High Availability Considerations
Production deployments require 3 nodes running control plane (etcd) and 3 nodes capable of running workloads. These can be the same nodes (Combined role) or separate nodes (CP-Only + Agent).
Node Role Combinations:
| Configuration | Control Plane Nodes | Workload Nodes | Total Nodes |
|---|---|---|---|
| All Combined | 3 Combined servers | 3 Combined servers | 3 |
| Separated | 3 CP-Only servers | 3 Agent nodes | 6 |
| Hybrid | 2 CP-Only + 1 Combined | 1 Combined + 2 Agent | 5 |
Any combination works as long as you have 3 control plane nodes and 3 workload-capable nodes.
Note: Regardless of the deployment configuration, a minimum of 3 nodes capable of running workloads is required for production deployments. This ensures both high availability and sufficient capacity for application pods.
For detailed fault tolerance information and data replication strategies, see the Architecture Guide.
Hardware Requirements
Single-Node Lab Deployment
Lab deployments are intended for acceptance testing, demonstrations, and development only. These configurations are not suitable for production workloads.
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 8 cores | 12 cores |
| Memory | 16 GiB | 24 GiB |
| Disk* | 128 GiB | 128 GiB |
Production Cluster - Server Node (Control Plane Only)
Server nodes dedicated to control plane functions have modest resource requirements:
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 2 cores | 4 cores |
| Memory | 4 GiB | 8 GiB |
| Disk* | 64 GiB | 128 GiB |
These nodes run only control plane components and require separate Agent nodes to run application workloads.
Production Cluster - Server Node (Control Plane + Workloads)
Combined role nodes require resources for both control plane and application workloads:
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 4 cores | 16 cores |
| Memory | 8 GiB | 32 GiB |
| Disk* | 100 GiB | 500 GiB |
Production Cluster - Agent Node
Agent nodes execute application workloads and require the following resources:
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 4 cores | 16 cores |
| Memory | 8 GiB | 32 GiB |
| Disk* | 100 GiB | 500 GiB |
Swap
Swap may stay on: deploy leaves it as it is, and the cluster runs with or without it. Size memory so that swap is not used in normal operation. Swap in use is a sign of memory pressure.
Storage Notes
* Disk Space: All disk space values must be available in the
/var/lib/longhornpartition. It is recommended that/var/lib/longhornbe a separate partition on a fast SSD for optimal performance, though SSD is not strictly required.Longhorn Capacity: Longhorn storage requires an additional 30% capacity headroom for internal operations and scaling. If less than 30% of the total partition capacity is available, Longhorn may mark volumes as “full” and prevent further writes. Plan disk capacity accordingly.
Storage Performance
For optimal performance, the following storage characteristics are recommended:
- Disk Type: SSD or NVMe storage for Longhorn volumes
- Filesystem: XFS or ext4 with default mount options
- Partition Layout: Dedicated
/var/lib/longhornpartition for persistent storage
Virtual machines and bare-metal hardware are both supported. Nested virtualization (running multiple nodes under a single hypervisor) may impact performance and is not recommended for production deployments.
Operating System Requirements
Supported Operating Systems
The CDN Manager supports Red Hat Enterprise Linux and compatible distributions:
| Operating System | Status |
|---|---|
| Red Hat Enterprise Linux 9.4 or later 9.x | Supported |
| Red Hat Enterprise Linux 10 | Untested |
| Red Hat Enterprise Linux 8 | Not supported |
Compatible Clones
The following RHEL-compatible distributions are supported at release 9.4 or later:
- Oracle Linux 9
- AlmaLinux 9
- Rocky Linux 9
A minimal installation is enough. Keep firewalld and NetworkManager installed and running: deploy configures the firewall and network through them.
Air-Gapped Deployments
Important:
deployinstalls a few packages from the operating system’s repositories on every node. For air-gapped deployments (no internet access), make the OS installation media available todnfon all nodes first. See Air-Gapped Installation.
Oracle Linux UEK Kernel
On Oracle Linux 9 running Unbreakable Enterprise Kernel 8 (UEK 8), install the netfilter kernel modules that K3s and Longhorn need on every node before running the installer:
dnf install kernel-uek-modules-extra-netfilter-$(uname -r)
The package comes from Oracle’s UEK 8 repository, ol9_UEKR8. If dnf cannot find it, check that the repository is enabled, and enable it if not:
dnf repolist
dnf config-manager --enable ol9_UEKR8
Without internet access, that repository is not available. Make the package available to dnf from a local repository first, as for the other packages in Air-Gapped Installation, and check that dnf finds it:
dnf -q repoquery kernel-uek-modules-extra-netfilter-$(uname -r)
SELinux
SELinux is supported in any mode, “Enforcing” included. The installation process installs the SELinux policy for the cluster automatically.
Networking Requirements
Network Interface
Every cluster node needs an IPv4 default route on its public interface, the interface that clients reach it on. The route does not need to lead to the internet: on an isolated network, use your site’s router or any gateway address on that subnet. deploy uses this route to find each machine’s public interface and address, and stops with [ACD-NO-DEFAULT-ROUTE] without one.
A second interface on a backplane network, separate from the public network, is optional but recommended for production. All node-to-node traffic then uses the backplane, and every node must be reachable on it. Other hosts may share it. See Configuring Segregated Networks.
Port Requirements
The cluster requires the following network connectivity:
| Category | Ports | Purpose |
|---|---|---|
| Inter-Node | 2379-2380, 6443, 8472/UDP, 10250, 5001, 9100 | etcd, API server, Flannel VXLAN, Kubelet, Spegel, node-exporter |
| External Access | 80, 443, 8086 | HTTP and HTTPS ingress, metrics from the CDN Directors |
| CDN Directors | 9092, 30000-30003 (one more per broker beyond three) | Kafka |
By default, deploy configures the host firewall for the inter-node and external ports itself. The host firewall does not filter the Kafka ports: restrict them to your CDN Directors on your network’s own firewall. See Kafka Ports. The same applies to the two ingress NodePorts, chosen at installation: see Ingress NodePorts.
Important: Complete port requirements, network ranges, and firewall configuration procedures are provided in the Networking Guide. Do not expose VictoriaMetrics (8428, 8429), Grafana (3000), or PostgreSQL (5432) directly—access these services only through the secure HTTPS ingress (port 443).
Resource Planning
Calculating Cluster Capacity
When planning cluster capacity, consider the following factors:
- Base Overhead: Kubernetes system components consume approximately 1-2 cores and 2-4 GiB memory per node
- Application Workloads: Refer to individual component resource requirements in the Architecture Guide
- Headroom: Maintain 20-30% resource headroom for workload spikes and automatic scaling
Scaling Considerations
The CDN Manager supports horizontal scaling for most components. The Horizontal Pod Autoscaler (HPA) can automatically adjust replica counts based on resource utilization. Detailed scaling guidance is available in the Architecture Guide.
Example Production Deployment
A minimal production deployment with 3 server nodes (combined role) and 2 agent nodes would require:
| Node Type | Count | CPU Total | Memory Total | Disk Total |
|---|---|---|---|---|
| Server (Combined) | 3 | 12 cores | 24 GiB | 300 GiB |
| Agent | 2 | 8 cores | 16 GiB | 200 GiB |
| Total | 5 | 20 cores | 40 GiB | 500 GiB |
This configuration provides:
- High availability (survives loss of 1 server node)
- Capacity for application workloads across all nodes
- Headroom for horizontal scaling
Next Steps
After verifying system requirements:
- Review the Installation Guide for deployment procedures
- Consult the Networking Guide for firewall configuration
- Examine the Architecture Guide for component resource requirements
3 - Networking Guide
Overview
deploy configures each machine’s network for the cluster: its firewall, its routing rules for cluster traffic, and the kernel network settings. The site setting backplane_network chooses the layout. A second setting, manage_firewall, chooses whether deploy manages the firewall or you do.
| Layout | Guide | Recommended for |
|---|---|---|
| Segregated: a backplane network for traffic between the machines, separate from the public network for clients | Configuring Segregated Networks | Production. Cluster traffic is kept off the public network. |
| Shared: one network for everything | Shared Interface Setup | Machines with a single network interface. |
A backplane is optional, but we recommend one wherever the machines can have a second network. The backplane is the network the cluster machines use to reach each other (east-west traffic). Every machine must be reachable on it, and it should be a separate network from the public one. Setting it to the public network’s range works, but then every host on the public network is trusted for all traffic. It does not have to be reserved for the cluster: other hosts may also be on it, but with the firewall managed they can reach every port on the machines, including the cluster datastore (etcd, 2379-2380/tcp), the Kubernetes API (6443/tcp) and the kubelet (10250/tcp).
Interfaces and Addresses
Every machine needs an IPv4 default route on its public interface, the interface that clients reach it on. The route does not need to lead to the internet: on an isolated network, use your site’s router or any gateway address on that subnet. deploy uses this route to find each machine’s public interface and address, and stops with [ACD-NO-DEFAULT-ROUTE] without one.
From that route, deploy works out each machine’s two addresses:
- The public interface is the one holding the default route. Its address is the machine’s public address, which clients reach.
- The cluster address is the address other machines reach it on: its address inside
backplane_networkwhen that is set, and otherwise its public address.
Interfaces that are neither the public interface nor the backplane are left as they are.
The cluster also uses two internal address ranges, which must not be in use anywhere on your network:
| Network | Default | Setting |
|---|---|---|
| Pods | 10.42.0.0/16 | cluster_pod_cidr |
| Services | 10.43.0.0/16 | cluster_svc_cidr |
Firewall
With manage_firewall: true, the default, deploy starts and enables firewalld on every machine and keeps two things as its own:
- The
acd-clusterzone, with target ACCEPT, which trusts all traffic from the cluster: the backplane interface (or, without a backplane, the other machines’ addresses), and the pod and service networks. - The
acd-publicpolicy, from thepubliczone to the machine, which opens the ports clients need:Port Purpose 80/tcp HTTP ingress 443/tcp HTTPS ingress: the GUI, the API, Zitadel and Grafana 8086/tcp Metrics pushed by the CDN Directors (Vector)
It also places each machine’s public interface in the public zone, on its NetworkManager connection. Before the first run, make sure the public zone allows everything else you need on that interface, SSH included. To check, run firewall-cmd --zone=public --list-all on each machine: ssh should be listed under services, or port 22 under ports.
deploy removes anything in acd-cluster and acd-public that it did not put there. Add your own rules to other zones, such as public: deploy never changes their rules.
Ports Between the Machines
Within the cluster, the machines trust each other for all traffic. When you manage the firewall yourself, the ports the cluster needs between machines are:
| Port | Between | Purpose |
|---|---|---|
| 6443/tcp | all machines | Kubernetes API, also used by the image registry mirror on every machine |
| 2379-2380/tcp | servers | Cluster datastore (etcd) |
| 8472/udp | all machines | Pod network (Flannel VXLAN) |
| 10250/tcp | all machines | Kubelet |
| 5001/tcp | all machines | Image registry mirror (Spegel) |
| 9100/tcp | all machines | Host metrics (node-exporter) |
The pod and service networks must also be in a zone with target ACCEPT.
Kafka Ports
The CDN Directors reach Kafka on these ports, on every machine:
| Port | Purpose |
|---|---|
| 9092/tcp | Kafka bootstrap |
| 30000/tcp | Kafka bootstrap, external |
| 30001/tcp and up | One port per Kafka broker, numbered consecutively: 30001 for the first broker, 30002 for the second, and so on |
With the default three brokers, the broker ports are 30001-30003, so the full range is 30000-30003. With more brokers, the range grows by one port per broker: six brokers use 30000-30006. In the rest of this documentation, “30000-30003” means this range for the default three brokers. Widen it if your cluster runs more.
deploy neither opens nor checks these ports, and the host firewall does not filter them: as on any host running containers, traffic that Kubernetes forwards to its services passes before the firewall’s zone rules apply. These ports are reachable from every network that can reach the machines.
Important: Restrict 9092/tcp and the whole 30000 range (30000-30003 with three brokers) to your CDN Directors on your network’s own firewall. The same applies to any other port a Kubernetes service publishes on the machines.
Ingress NodePorts
The ingress service (Traefik) also publishes two NodePorts on every machine, which forward to its HTTP and HTTPS ports, 80 and 443. Kubernetes picks them at installation from 30000-32767, so they differ from one cluster to another, and the host firewall does not filter them, as for the Kafka ports. Find them on a server:
kubectl get service traefik -n kube-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
traefik LoadBalancer 10.43.12.34 192.0.2.11,192.0.2.12,192.0.2.13 80:31873/TCP,443:30306/TCP 3d
Here they are 31873 (HTTP) and 30306 (HTTPS). Clients need only 80 and 443: block the two NodePorts on your network’s own firewall.
Managing the Firewall Yourself
Set manage_firewall: false to configure firewalld yourself. deploy then only checks that the ports between the machines and the ports clients need are open in the zone their traffic arrives in, and that the pod and service networks are in a zone with target ACCEPT. It stops before changing anything, and shows the firewall-cmd commands to run, if they are not. Only firewalld is checked: if it is not running, the check is skipped, and your own firewall must allow the same traffic.
To hand the firewall of a running cluster over to yourself, on each machine:
Move the pod and service networks to a zone with target ACCEPT, such as firewalld’s own
trusted. For the default ranges:for network in 10.42.0.0/16 10.43.0.0/16; do firewall-cmd --permanent --zone=acd-cluster --remove-source="$network" firewall-cmd --permanent --zone=trusted --add-source="$network" firewall-cmd --zone=trusted --change-source="$network" doneOpen the ports in the zone their traffic arrives in, with
firewall-cmd --zone=<zone> --add-port=<port>, once with--permanentand once without.On a machine with a backplane, put the backplane interface in the zone you want:
nmcli connection modify <its connection> connection.zone <zone>.Set
manage_firewall: falseingroup_vars/all.yamland rundeploy.
deploy then empties the acd-cluster zone and the acd-public policy, and moves any interface still in acd-cluster to firewalld’s default zone, with a warning.
Routing
With manage_routes: true, the default, deploy adds lookup main routing rules at priority 50, so that cluster traffic always uses the main routing table, even on machines that use policy routing. On a machine with a backplane, ip rule then shows:
0: from all lookup local
50: from 10.42.0.0/16 lookup main proto static
50: from all to 10.42.0.0/16 lookup main proto static
50: from 10.43.0.0/16 lookup main proto static
50: from all to 10.43.0.0/16 lookup main proto static
50: from all to 10.10.0.0/24 lookup main proto static
32766: from all lookup main
32767: from all lookup default
Keep your own rules at other priorities. See manage_routes for how these rules interact with your own.
Managing Routing Yourself
If your own policy routing rules must sit at priority 50 or below, set manage_routes: false and add the cluster’s rules yourself. Add them before you change the setting on a running cluster: deploy then removes every priority-50 rule from the connections it manages.
Every machine needs five lookup main rules:
| Rule | Default value |
|---|---|
from <cluster_pod_cidr> | from 10.42.0.0/16 |
to <cluster_pod_cidr> | to 10.42.0.0/16 |
from <cluster_svc_cidr> | from 10.43.0.0/16 |
to <cluster_svc_cidr> | to 10.43.0.0/16 |
to <cluster network> | backplane_network if set, such as to 10.10.0.0/24. Without a backplane, the subnet of the machine’s public interface, such as to 192.0.2.0/24. |
Give them a priority other than 50, and a lower number than any of your own rules that send traffic to another table, such as your from <address> lookup <table> source rules. Store them on the NetworkManager connection of the interface the machines reach each other on: the backplane interface, or the public interface without a backplane. For example, at priority 40 on a backplane connection named eth1 (table 254 is main):
nmcli connection modify eth1 +ipv4.routing-rules "priority 40 from 10.42.0.0/16 table 254"
nmcli connection modify eth1 +ipv4.routing-rules "priority 40 to 10.42.0.0/16 table 254"
nmcli connection modify eth1 +ipv4.routing-rules "priority 40 from 10.43.0.0/16 table 254"
nmcli connection modify eth1 +ipv4.routing-rules "priority 40 to 10.43.0.0/16 table 254"
nmcli connection modify eth1 +ipv4.routing-rules "priority 40 to 10.10.0.0/24 table 254"
Stored rules take effect when the connection is next activated. To activate it now, run nmcli connection up eth1 in a maintenance window: it re-applies the whole connection, and briefly interrupts traffic on that interface. Then check the rules with ip rule show.
Whichever way the rules are managed, deploy checks before changing anything that cluster traffic resolves through the main routing table, and stops with [ACD-ROUTING-CONFLICT], naming the rule in the way, if it does not.
Changes Are Checked
After changing a machine’s network, deploy compares the live state with what it planned: the interface zones, the firewall, the routing rules and the kernel settings. It stops before installing anything on the cluster if they differ. This check cannot be skipped. See the [ACD-VERIFY-...] codes in Installation Troubleshooting.
Boot-Time Network Tools
deploy stores its interface zones and routing rules on the machines’ NetworkManager connections. Tools that rewrite those connections at boot, such as cloud-init or a virtualization platform’s contextualization, can undo them after a reboot: an interface back in the wrong zone, or the priority-50 rules gone. Nothing checks for this between deploy runs.
Disable such regeneration of the network connections on the cluster machines. Where you cannot, run deploy again after a reboot: it puts the settings back, prints Host network changed on <machine>: ... naming what it changed, and carries on.
3.1 - Shared Interface Setup
Overview
In a shared layout, each machine has a single network: the cluster’s own traffic and client traffic both use the interface holding the default route. This is the simplest layout, for machines with one network interface.
Security note: Cluster traffic shares the public network, so it is not isolated from clients. For production, a segregated backplane is recommended.
Configuration
Leave backplane_network empty in /etc/acd/deploy/group_vars/all.yaml. This is the default:
backplane_network: ""
Each machine’s cluster address is then its public address. The machines may be on one subnet or on routed subnets, as long as each can reach the others’ addresses.
What deploy Sets Up
On each machine, with manage_firewall: true:
- The public interface is placed in firewalld’s
publiczone, with the ports clients need opened through theacd-publicpolicy. - The other machines’ addresses, and the pod and service networks, are trusted in the
acd-clusterzone, which accepts all traffic. Hosts other than the cluster machines get only the ports open inpublic. - Routing rules keep traffic to the pod and service networks, and to the machine’s own subnet, on the main routing table. If your machines are on different subnets and use policy routing, make sure traffic to the other machines’ subnets also uses the main table.
Verification
After deploy, on any machine:
firewall-cmd --get-active-zones
firewall-cmd --zone=acd-cluster --list-sources
The acd-cluster zone lists the pod and service networks (10.42.0.0/16 and 10.43.0.0/16 by default) and the other cluster machines’ addresses, and the public interface is in the public zone.
For the ports the cluster uses, and for managing the firewall yourself, see the Networking Guide.
3.2 - Configuring Segregated Networks
Overview
In a segregated layout, each machine has two networks:
- A public network, holding the default route, which clients and CDN Directors use to reach the manager.
- A backplane network, separate from the public network, which carries all traffic between the machines (east-west traffic): the Kubernetes API, the cluster datastore, the pod network and storage replication.
This keeps cluster traffic off the public network, and lets the firewall trust the backplane completely while the public interface opens only the ports clients need. A backplane is optional, but it is the recommended layout for production.
The backplane does not have to be reserved for the cluster machines. Other hosts may be on it too, such as your CDN Directors: it is simply the network the cluster machines use to reach each other.
flowchart LR
clients[Users and CDN Directors] -->|public network| eth0
subgraph machine[Each cluster machine]
eth0[eth0: public zone]
eth1[eth1: acd-cluster zone]
end
eth1 <-->|backplane network| peers[Other cluster machines]Requirements
- The backplane is a separate network from the public network.
- Every machine has exactly one interface with an address in the backplane network, and every machine is reachable from the others on it.
- The default route is on the public interface, not the backplane.
- Only hosts you trust are on the backplane. With
manage_firewall: true, every host on the backplane can reach every port on every cluster machine, including the cluster datastore (etcd, 2379-2380/tcp), the Kubernetes API (6443/tcp) and the kubelet (10250/tcp), so connect only hosts you trust to it. - Both interfaces are managed by NetworkManager, with an active connection.
Configuration
Give the backplane’s address range in /etc/acd/deploy/group_vars/all.yaml, before the first deploy:
backplane_network: "10.10.0.0/24"
Nothing else is needed: no per-machine settings and no firewall commands. deploy finds each machine’s interface in that range and sets up the rest.
Important:
backplane_networkis locked at the first installation. A cluster cannot be moved on to or off a backplane later without reinstalling it.
What deploy Sets Up
On each machine, with manage_firewall: true:
- Kubernetes uses the backplane: each machine’s cluster address is its backplane address, the pod network (Flannel) runs over the backplane interface, and the public address is advertised as the machine’s external address.
- The backplane interface is placed in the
acd-clusterzone, which accepts all traffic, together with the pod and service networks. - The public interface is placed in firewalld’s
publiczone, with the ports clients need opened through theacd-publicpolicy. - Routing rules keep traffic to the pod, service and backplane networks on the main routing table.
Other interfaces are left as they are.
Verification
After deploy, on any machine:
firewall-cmd --get-active-zones
Expected output:
acd-cluster
interfaces: eth1
sources: 10.42.0.0/16 10.43.0.0/16
public
interfaces: eth0
Check the cluster addresses Kubernetes uses:
grep -E 'node-ip|external|flannel' /etc/rancher/k3s/config.yaml.d/20-acd-node.yaml
Expected output, on manager-1:
node-ip: "10.10.0.11"
node-external-ip: "192.0.2.11"
flannel-iface: "eth1"
And on a server:
kubectl get nodes -o wide
INTERNAL-IP shows each machine’s backplane address, and EXTERNAL-IP its public address.
Problems
| Code | Cause | Fix |
|---|---|---|
[ACD-BACKPLANE-NO-MATCH] | No interface on a machine has an address in backplane_network. | Fix backplane_network, or the machine’s addresses. Every machine needs an address in it. |
[ACD-BACKPLANE-AMBIGUOUS] | More than one interface on a machine matches backplane_network. | Narrow backplane_network, or set node_ip to the right address on that machine’s line in the inventory. |
[ACD-BACKPLANE-SPLIT] | The machines’ cluster addresses are not all in one network. | Put every machine’s backplane address inside backplane_network. |
[ACD-JOIN-UNREACHABLE] | A joining machine cannot reach the server over the backplane. | Check the backplane’s switching and addressing, and that reverse path filtering does not drop the replies. |
For every code, see Installation Troubleshooting.
4 - Architecture Guide
Overview
The AgileTV CDN Manager (ESB3027) is a cloud-native Kubernetes application designed for managing CDN operations. This guide provides a detailed description of the system architecture, component interactions, and scaling considerations.
High-Level Architecture
The CDN Manager follows a microservices architecture deployed on Kubernetes. The system is organized into logical layers:
graph LR
Clients[API Clients] --> Ingress[Ingress Controller]
Ingress --> Manager[Core Manager]
Ingress --> Frontend[MIB Frontend]
Ingress --> Grafana[Grafana]
Manager --> Redis[(Redis)]
Manager --> Kafka[(Kafka)]
Manager --> PostgreSQL[(PostgreSQL)]
Manager --> Zitadel[Zitadel IAM]
Manager --> Confd[Configuration Service]
Grafana --> VM[(VictoriaMetrics)]
Confd -.-> Gateway[NGinx Gateway]
Gateway --> Director[CDN Director]Component Architecture
Ingress Layer
The ingress layer manages all incoming traffic to the cluster:
| Component | Role |
|---|---|
| Ingress Controller | Primary ingress for all cluster traffic; routes requests to internal services based on path |
| NGinx Gateway | Reverse proxy for routing traffic to external CDN Directors; used by MIB Frontend to communicate with remote Confd instances on CDN Director nodes |
Traffic flow:
- API clients and Operator UI connect via the Ingress Controller at
/apiand/guipaths respectively - Grafana dashboards are accessed via the Ingress Controller at
/grafana - Zitadel authentication console is accessed via the Ingress Controller at
/ui/console
Application Services
The application layer contains the core CDN Manager services:
| Component | Role | Scaling |
|---|---|---|
| Core Manager | Main REST API server (v1/v2 endpoints); handles authentication, configuration, routing, and discovery | Horizontally scalable via HPA |
| MIB Frontend | Web-based configuration GUI for operators | Horizontally scalable via HPA |
| Confd | Configuration service for routing configuration; synchronizes with Core Manager application | Single instance |
| Grafana | Monitoring and visualization dashboards | Single instance |
| Selection Input Worker | Consumes selection input events from Kafka and updates configuration | Single instance |
| Metrics Aggregator | Collects and aggregates metrics from CDN components | Single instance |
| Telegraf | System-level metrics collection from cluster nodes | DaemonSet (one per node) |
| Alertmanager | Alert routing and notification management | Single instance |
Data Layer
The data layer provides persistent and ephemeral storage:
| Component | Role | Scaling |
|---|---|---|
| Redis | In-memory caching, session storage, and ephemeral state | Single instance |
| Kafka | Event streaming for selection input and subnet replication; provides durable message queue | Controller cluster (odd count) |
| PostgreSQL | Persistent configuration and state storage | 3-node cluster with HA |
| VictoriaMetrics (Analytics) | Real-time and short-term metrics for operational dashboards | Single instance |
| VictoriaMetrics (Billing) | Long-term metrics retention (1+ years) for billing and license compliance | Single instance |
External Integrations
| Component | Role |
|---|---|
| Zitadel IAM | Identity and access management; provides OAuth2/OIDC authentication |
| ESB3024 CDN Director | Edge routing infrastructure; receives configuration from Confd |
Detailed Component Descriptions
Core Manager
The Core Manager is the central application server that exposes the REST API. It is implemented in Rust using the Actix-web framework.
Key Responsibilities:
- Authentication and session management via Zitadel
- Configuration document storage and retrieval
- Selection input CRUD operations
- Routing rule evaluation and GeoIP lookups
- Host and namespace discovery
- Operator UI helper endpoints
API Endpoints:
/api/v1/auth/*- Authentication (login, token, logout)/api/v1/configuration- Configuration management/api/v1/selection_input/*- Selection input operations/api/v2/selection_input/*- Enhanced selection input with list operations/api/v1/routing/*- Routing evaluation and validation/api/v1/discovery/*- Host and namespace discovery/api/v1/metrics- System metrics/api/v1/health/*- Liveness and readiness probes/api/v1/operator_ui/*- Operator helper endpoints
Runtime Modes: The Core Manager supports multiple runtime modes, each deployed as a separate container:
http-server- Primary HTTP API server (default)metrics-aggregator- Background worker for metrics collectionselection-input- Background worker for Kafka selection input consumption
MIB Frontend
The MIB Frontend provides a web-based GUI for configuration management.
Key Features:
- Intuitive web interface for CDN configuration
- Real-time configuration validation
- Integration with Zitadel for SSO authentication
Confd (Configuration Service)
Confd provides routing configuration services and synchronizes with the Core Manager application.
Key Responsibilities:
- Hosts the service configuration for routing decisions
- Provides API and CLI for configuration management
- Synchronizes routing configuration with Core Manager
- Maintains configuration state in PostgreSQL
Selection Input Worker
The Selection Input Worker processes selection input events from the Kafka stream.
Key Responsibilities:
- Consumes messages from the
selection_inputKafka topic - Validates and transforms input data
- Updates configuration in the data store
- Maintains message ordering within partitions
Scaling Limitation: The Selection Input Worker cannot be scaled beyond a single consumer per Kafka partition, as message ordering must be preserved.
Metrics Aggregator
The Metrics Aggregator collects and processes metrics from CDN components.
Key Responsibilities:
- Polls metrics from CDN Director instances
- Aggregates usage statistics
- Writes data to VictoriaMetrics (Analytics) for dashboards
- Writes long-term data to VictoriaMetrics (Billing) for compliance
Telegraf
Telegraf is deployed as a DaemonSet to collect host-level metrics.
Key Responsibilities:
- CPU, memory, disk, and network metrics from each node
- Container-level resource usage
- Kubernetes cluster metrics
- Forwards metrics to VictoriaMetrics
Grafana
Grafana provides visualization and dashboard capabilities.
Features:
- Pre-built dashboards for CDN monitoring
- Custom dashboard support
- VictoriaMetrics as data source
- Alerting integration with Alertmanager
Access: https://<host>/grafana
Alertmanager
Alertmanager handles alert routing and notifications.
Key Responsibilities:
- Receives alerts from Grafana and other sources
- Deduplicates and groups alerts
- Routes to notification channels (email, webhook, etc.)
- Manages alert silencing and inhibition
Data Storage
Redis
Redis provides in-memory storage for:
- User sessions and authentication tokens
- Ephemeral configuration cache
- Real-time state synchronization
Deployment: Single instance, with no replicas. Redis is unavailable while its pod restarts or moves to another node.
Kafka
Kafka provides durable event streaming for:
- Selection input events
- Metrics data streams
- Inter-service communication
Deployment: Controller cluster with 3 replicas for production, 1 replica for lab deployments
Node Affinity: Kafka replicas must be scheduled on separate nodes to ensure high availability. The Helm chart configures topology spread constraints to enforce this distribution.
Topics:
selection_input- Selection input events. One replica, by design: see Data Replication.subnets- Subnet changes. Three replicas.
Note: For lab/single-node deployments, the Kafka replica count must be set to 1 in the Helm values. Production deployments require 3 replicas for fault tolerance.
PostgreSQL
PostgreSQL provides persistent storage for:
- Configuration documents
- User and permission data
- System state
- The subnet map
The cluster holds four databases: manager (the manager’s own data, including the subnet map), app (Grafana), zitadel and confsej.
Deployment: 3-node cluster managed by Cloudnative PG (CNPG) operator
High Availability: The CNPG operator manages automatic failover and ensures high availability:
- One primary node handles read/write operations
- Two replica nodes provide redundancy and can be promoted to primary on failure
- Automatic failover occurs within seconds of primary node failure
- Synchronous replication ensures data consistency
Note: The PostgreSQL cluster is deployed and managed automatically by the CNPG operator. Manual intervention is typically not required for normal operations.
VictoriaMetrics
Two VictoriaMetrics instances serve different purposes:
VictoriaMetrics (Analytics):
- Real-time and short-term metrics storage
- Supports Grafana dashboards
- Retention: Configurable (typically 30-90 days)
VictoriaMetrics (Billing):
- Long-term metrics retention
- Billing and license compliance data
- Retention: Minimum 1 year
Authentication and Authorization
Zitadel Integration
Zitadel provides identity and access management:
Authentication Flow:
- User accesses MIB Frontend or API
- Redirected to Zitadel for authentication
- Zitadel validates credentials and issues session token
- Session token exchanged for access token
- Access token included in API requests (Bearer authentication)
Default Credentials: See the Glossary for default login credentials.
Access Paths:
- Zitadel Console:
/ui/console - API authentication:
/api/v1/auth/*
External Hostname
Zitadel validates the URL that clients connect to against its configured external hostname. deploy sets it from the first entry of manager_hostnames in the site settings, which must be the hostname clients use to reach the system.
Network Architecture
Traffic Flow
graph TB
External[External Clients] --> Ingress[Ingress Controller]
External --> Redis[(Redis)]
External --> Kafka[(Kafka)]
External --> Telegraf[Telegraf]
Ingress --> Manager[Core Manager]
Ingress --> Frontend[MIB Frontend]
Ingress --> Grafana[Grafana]
Ingress --> Zitadel[Zitadel]Note: Certain services (Redis, Kafka, Telegraf) can be accessed directly by external clients without traversing the ingress controller. This is typically used for metrics collection, event streaming, and direct data access scenarios.
Internal Communication
All internal services communicate over the Kubernetes overlay network (Flannel VXLAN). Services discover each other via Kubernetes DNS.
External Communication
- CDN Directors: Accessed via NGinx Gateway for simplified routing.
- MaxMind GeoIP: Local database files (no external calls)
Scaling
Horizontal Pod Autoscaler (HPA)
The following components support automatic horizontal scaling via HPA:
| Component | Minimum | Maximum | Scale Metrics |
|---|---|---|---|
| Core Manager | 3 | 8 | CPU (50%), Memory (80%) |
| NGinx Gateway | 2 | 4 | CPU (75%), Memory (80%) |
| MIB Frontend | 2 | 4 | CPU (75%), Memory (90%) |
Note: HPA is enabled by default in the Helm chart. The default configuration is tuned for production deployments. Adjust min/max values based on expected load and available cluster capacity.
Manual Scaling
Components can also be scaled manually by setting replica counts in the Helm values:
cdn-manager:
manager:
replicaCount: 3
mibFrontend:
replicaCount: 2
Important: When manually setting replica counts, you must disable the Horizontal Pod Autoscaler (HPA) for the corresponding component. If HPA remains enabled, it will override manual replica settings. To disable HPA, set
autoscaling.hpa.enabled: falsefor the component in your Helm values.
Components That Do Not Scale
The following components do not support horizontal scaling:
| Component | Reason |
|---|---|
| Confd | Single instance required for configuration consistency |
| PostgreSQL | Cloudnative PG cluster; scaled by adding replicas via operator configuration |
| Kafka | Scaled by adding controllers, not via replica count |
| VictoriaMetrics | Stateful; single instance per role |
| Redis | Single instance |
| Grafana | Single instance sufficient for dashboard access |
| Alertmanager | Single instance for alert routing |
| Selection Input Worker | Kafka message ordering requires single consumer |
| Metrics Aggregator | Single instance for consistent metrics aggregation |
Node Scaling
Additional Agent nodes can be added to the cluster at any time to increase workload capacity. Kubernetes automatically schedules pods to nodes with available resources.
Cluster Balancing
The CDN Manager deployment includes the Kubernetes Descheduler to maintain balanced resource utilization across cluster nodes:
- Automatic Rebalancing: The descheduler periodically analyzes pod distribution and evicts pods from overutilized nodes
- Node Balance: Helps prevent resource hotspots by redistributing workloads across available nodes
- Integration with HPA: Works in conjunction with Horizontal Pod Autoscaler to optimize both pod count and placement
The descheduler runs as a background process and does not require manual intervention under normal operating conditions.
Resource Configuration
For detailed resource preset configurations and planning guidance, see the Configuration Guide.
High Availability
Server Node Redundancy
Production deployments require a minimum of 3 Server nodes:
- Survives loss of 1 server node
- Maintains quorum for etcd and Kafka
For enhanced availability, use 5 Server nodes:
- Survives loss of 2 server nodes
- Recommended for critical production environments
For large-scale deployments, 7 or more Server nodes can be used:
- Survives loss of 3+ server nodes
- Suitable for high-capacity production environments
Pod Distribution
Kubernetes automatically distributes pods across nodes to maximize availability:
- Pods with the same deployment are scheduled on different nodes when possible
- Pod Disruption Budgets (PDB) ensure minimum availability during maintenance
Data Replication
| Component | Replication Strategy |
|---|---|
| Redis | Single instance |
| Kafka | Three replicas per topic, except selection_input, which has one |
| PostgreSQL | 3-node cluster via Cloudnative PG; covers every database, including the subnet map in manager |
| VictoriaMetrics | Single instance |
| Longhorn | Three replicas per volume, except the Kafka data volumes, which have one |
Longhorn Storage: Each volume keeps three replicas on different nodes, so it survives the loss of a node. The three Kafka data volumes keep one replica each, because Kafka copies its topics across its brokers itself. Longhorn tries to keep one replica on the node where the volume’s pod runs, but a pod can run on a node with no replica of its volume.
selection_input has one replica by design. The topic is high-volume and short-lived, so it keeps a single replica, on one broker’s single-replica volume. The cost: selection input is unavailable while that broker restarts or moves to another node, and its messages are lost if that broker’s volume is lost. Every other topic survives the loss of one broker. See Safety before you restart Kafka or drain a node.
Next Steps
After understanding the architecture:
- Installation Guide - Deploy the CDN Manager
- Configuration Guide - Configure components for your environment
- Operations Guide - Day-to-day operational procedures
- Performance Tuning Guide - Optimize system performance
- Metrics & Monitoring - Set up monitoring and alerting
5 - Installation Guide
Overview
The CDN Manager is installed with deploy, a command on the installation ISO. You describe your machines and your site in a few files on the first machine, and deploy does the rest over SSH: it configures each machine’s firewall and network, builds the Kubernetes (k3s) cluster, installs the CDN Manager, and checks that it is healthy.
deploy is the only supported way to install the CDN Manager. Running the ISO’s install, join-server and join-agent scripts by hand is no longer supported.
New here? Start with the Quickstart. It walks you through a complete installation on three machines, from minimal operating system installs to signing in to the GUI. The other pages cover the details and the variations.
Upgrading from 1.6 or earlier? Version 1.8 cannot be installed over an earlier release. Install it on a new cluster. See the Upgrade Guide.
How It Works
flowchart LR
A[Mount the ISO<br>on the first server] --> B[deploy init]
B --> C[Edit /etc/acd/deploy]
C --> D[deploy]
D --> E[Sign in to the GUI]- Mount the ISO on the machine that will be the first server.
- Run
deploy initonce. It installs Ansible and creates the configuration folder/etc/acd/deploy. - Edit the configuration: the machines and their roles, your site settings, and optionally a TLS certificate, MaxMind databases and application settings.
- Run
deploy. It checks everything first, takes a backup, then installs or updates the cluster to match your configuration.
To change anything later, edit the configuration and run deploy again. Every run is safe to repeat.
Guides
| Guide | What it covers |
|---|---|
| Quickstart | A complete installation, start to finish |
| Inventory | Listing your machines and their roles, and per-machine options |
| Site Settings | Every setting in group_vars/all.yaml |
| TLS Certificates | Serving HTTPS with your own certificate |
| MaxMind GeoIP Databases | Enabling geographic routing |
| Application Settings | Presets and values.override.yaml |
| Running deploy | What a run does, checking and restoring backups, and what to send support |
| Changing the Cluster | Adding machines and changing a machine’s role |
| Air-Gapped Installation | Installing without internet access |
| Upgrade Guide | Moving to a new release |
| Installation Troubleshooting | What each failure code means and what to do |
| Next Steps | Securing the administrator account and adding users |
Prerequisites
- Machines meeting the System Requirements: three servers for production, optionally with agents for extra capacity. A single server is enough for a lab.
- Operating system: a minimal install of Red Hat Enterprise Linux 9.4 or later, or a compatible rebuild, with firewalld and NetworkManager running.
- Network: a default route on every machine and, optional but recommended, a backplane network, separate from the public network, that all the machines are reachable on. See the Networking Guide.
- Access: passwordless root SSH from the first server to every machine, itself included.
- Software: the installation ISO from the AgileTV customer portal, and
dnfaccess to the operating system’s repositories on every machine. - Recommended: a TLS certificate from a trusted certificate authority, and MaxMind GeoIP databases from your own MaxMind subscription.
Getting Help
- Installation Troubleshooting lists every failure code
deployreports. - If the final health check fails,
deploywrites/etc/acd/deploy/support-bundle-acd.tar.gz. Send it to AgileTV support. - The Release Notes list known issues for each version.
5.1 - Quickstart
Overview
This guide takes three freshly installed Enterprise Linux 9 machines and turns them into a production CDN Manager cluster, ending with you signed in to the web GUI. Follow it from top to bottom. Each step shows the commands to run and the output to expect, and links to a page with the details and the alternatives.
The whole installation is driven by one command, deploy, which ships on the installation ISO. You run it on the first machine, and it configures every machine over SSH: the host firewall and network, the Kubernetes cluster, and the CDN Manager itself. You do not run anything by hand on the other machines.
Estimated time: about 30 minutes, of which deploy itself takes 15 to 20.
The example cluster
The commands below use this example. Replace the names and addresses with your own as you go.
| Machine | Role | Public address (eth0) | Backplane address (eth1) |
|---|---|---|---|
manager-1 | server | 192.0.2.11 | 10.10.0.11 |
manager-2 | server | 192.0.2.12 | 10.10.0.12 |
manager-3 | server | 192.0.2.13 | 10.10.0.13 |
- Manager hostname:
manager.example.com, the name users and CDN Directors use to reach the manager. Its DNS record points at the public address of one or more of the machines. - Backplane network:
10.10.0.0/24, the network the machines use to talk to each other. A backplane is optional, but recommended. When you use one, all node-to-node (east-west) traffic stays on it, and every machine must be reachable on it. It should be a separate network from the public one. It does not have to be reserved for the cluster, so other hosts may also be on it. - CDN Director:
director-1at 192.0.2.201. It runs on its own machine, outside this cluster.
Single network? A dedicated backplane is recommended, but not required. If each machine has only one network interface, follow the guide as written and leave
backplane_networkempty in Step 6. See the Networking Guide for both layouts.
Before You Begin
Make sure you have:
- Three machines that meet the System Requirements, each with a minimal install of Red Hat Enterprise Linux 9.4 or later, or a compatible rebuild such as Oracle Linux, AlmaLinux or Rocky Linux. Keep the defaults: firewalld and NetworkManager running, and any SELinux mode.
- Final hostnames on every machine, each one unique. A machine cannot be renamed after it joins the cluster.
- A default route on every machine, on the public interface. It does not need to lead to the internet: on an isolated network, use your site’s router or any gateway address on that subnet.
- Root access to every machine, by password or key, to set up SSH in Step 1.
- Package repositories: every machine must be able to install packages with
dnffrom the operating system’s repositories.deployinstalls a few packages, such asansible-coreon the first machine and the storage tools on all of them. Without internet access, see Air-Gapped Installation. - A DNS name for the manager, such as
manager.example.com. - The installation ISO,
esb3027-cdn-manager-1.8.0.iso, from the AgileTV customer portal. - A TLS certificate for the manager hostname, issued by a certificate authority your users trust, with its private key. You can start without one; see Step 7.
- MaxMind GeoIP databases from your own MaxMind subscription: City, ASN and Anonymous IP. You can start without them; see Step 8.
Run every command in this guide as root on manager-1, unless a step says otherwise.
Step 1: Set Up SSH Keys
deploy connects from manager-1 to every machine as root, manager-1 itself included, and needs to log in without a password prompt.
Create a key on manager-1:
ssh-keygen -t ed25519 -N '' -C "acd-deploy@$(hostname)" -f /root/.ssh/id_ed25519
Expected output:
Generating public/private ed25519 key pair.
Your identification has been saved in /root/.ssh/id_ed25519
Your public key has been saved in /root/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:igFCBNWbW74iWVSPZdMrGhtpKXiPQedvUZEGPmzppsI acd-deploy@manager-1
...
Copy it to all three machines. Enter each machine’s root password when prompted:
for ip in 192.0.2.11 192.0.2.12 192.0.2.13; do
ssh-copy-id -o StrictHostKeyChecking=accept-new -i /root/.ssh/id_ed25519.pub root@$ip
done
Expected output, once per machine:
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/root/.ssh/id_ed25519.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
root@192.0.2.11's password:
Number of key(s) added: 1
Check that each login now works without a prompt:
for ip in 192.0.2.11 192.0.2.12 192.0.2.13; do
ssh -o BatchMode=yes root@$ip hostname
done
Expected output:
manager-1
manager-2
manager-3
If a machine asks for a password or prints Permission denied, fix that before going on. Otherwise deploy stops with UNREACHABLE.
Step 2: Check the Machines
Confirm that each machine runs a supported release, has its addresses and default route, and runs firewalld and NetworkManager:
for ip in 192.0.2.11 192.0.2.12 192.0.2.13; do
echo "===== $ip"
ssh root@$ip 'cat /etc/redhat-release; ip -br -4 addr; ip route show default; systemctl is-active firewalld NetworkManager'
done
Expected output, for each machine:
===== 192.0.2.11
Red Hat Enterprise Linux release 9.4 (Plow)
lo UNKNOWN 127.0.0.1/8
eth0 UP 192.0.2.11/24
eth1 UP 10.10.0.11/24
default via 192.0.2.1 dev eth0 proto static
active
active
Check for:
- A release of 9.4 or later.
- The default route on the public interface (
eth0). - The backplane address on another interface (
eth1), inside the backplane network. activetwice: firewalld, then NetworkManager.
deploy checks all of this again before it changes anything, along with processor cores, memory and kernel modules, and stops with a message naming the problem.
Step 3: Mount the Installation ISO
Copy the ISO to manager-1, for example with scp from your workstation:
scp esb3027-cdn-manager-1.8.0.iso root@192.0.2.11:/root/
Then, on manager-1, mount it at /mnt/esb3027:
mkdir -p /mnt/esb3027
mount -o loop,ro /root/esb3027-cdn-manager-1.8.0.iso /mnt/esb3027
ls -1 /mnt/esb3027
Expected output:
ansible
BUILD_ARCH
charts
common.sh
deploy
install
join-agent
join-server
k3s_installer.sh
load-maxmind-databases
openapi.yaml
Packages
rootfs.tar
values-lab.yaml
values-production.yaml
values.yaml
VERSION.txt
The ISO is needed only on manager-1. deploy copies what the other machines need.
Important: Keep the ISO mounted at
/mnt/esb3027whenever you rundeploy, now and later. The deployment’s configuration reads its application settings from the ISO, anddeploystops if the ISO is not mounted. The mount does not survive a reboot, so mount it again before the next run.
Step 4: Initialize the Deployment
Initialize manager-1 with the production preset:
/mnt/esb3027/deploy init prod
This installs ansible-core from the operating system’s repositories, installs the Ansible collections bundled on the ISO, and creates the configuration folder /etc/acd/deploy. Expected output (the package list is shortened here):
###############################################################
ESB3027 AgileTV CDN Manager -- Deployer
Version: 1.8.0
###############################################################
Installing ansible-core from the OS repositories ...
Dependencies resolved.
================================================================================
Package Arch Version Repository Size
================================================================================
Installing:
ansible-core x86_64 1:2.14.18-3.el9 appstream 3.9 M
...
Complete!
Installing bundled Ansible collections ...
Initialization complete, using the production preset. Next:
1. Edit /etc/acd/deploy/inventory.ini - your node addresses and roles.
2. Edit /etc/acd/deploy/group_vars/all.yaml - at minimum, manager_hostnames.
3. (Optional) drop a TLS certificate + key into /etc/acd/deploy/certs/
and the three MaxMind .mmdb files into /etc/acd/deploy/maxmind/.
4. (Optional) change application settings in /etc/acd/deploy/values.override.yaml.
5. Run: /mnt/esb3027/deploy
See /etc/acd/deploy/README.md for the full input contract.
WARNING: deploy connects over SSH from this node to every node in
inventory.ini, this node included, as root unless ansible_user says
otherwise. Make sure SSH keys are set up so each of those logins works
without a password prompt before running deploy.
The next four steps follow that list. Everything you configure lives in /etc/acd/deploy:
/etc/acd/deploy/
├── inventory.ini # Step 5: your machines and their roles
├── group_vars/all.yaml # Step 6: your site settings
├── certs/ # Step 7: your TLS certificate and key
├── maxmind/ # Step 8: your MaxMind databases
├── values.override.yaml # optional application settings, empty to start
├── preset.yaml # link to the production preset on the ISO; do not edit
└── README.md # the full reference for this folder
certs/ and maxmind/ each start with a README.txt: short instructions for that folder, which deploy ignores. Leave it there or delete it.
For other options, such as the lab preset, see the deploy reference.
Step 5: List Your Machines
Open /etc/acd/deploy/inventory.ini in an editor. It is a commented example with three sections. Edit the machine lines so that your three machines are listed under [server], with the address deploy reaches each one on over SSH, and the other two sections are empty:
[server]
manager-1 ansible_host=192.0.2.11
manager-2 ansible_host=192.0.2.12
manager-3 ansible_host=192.0.2.13
[agent]
[director]
[server]: the first server listed must be the machine you rundeployon, heremanager-1. The number of servers must be odd: one for a lab, three for production.[agent]: lists any additional machines beyond the three servers. It may be left empty, as here.[director]: leave it empty. CDN Directors run on their own machines and are listed in the next step.
To add agent machines later, or to set per-machine options, see Inventory.
Step 6: Enter Your Site Settings
Open /etc/acd/deploy/group_vars/all.yaml. Every setting in it is explained in its comments and already holds a working default. For this cluster, change three lines:
manager_hostnames: [manager.example.com]
external_directors: [{name: director-1, address: 192.0.2.201}]
backplane_network: "10.10.0.0/24"
| Setting | What to enter |
|---|---|
manager_hostnames | Every hostname users and CDN Directors use to reach the manager. The first one is the main hostname: it must match your TLS certificate, and it is the only name you can sign in under. |
external_directors | Each CDN Director, with a name of your choosing and its IPv4 address (not its hostname). Leave the list empty, [], if you have none yet. |
backplane_network | The address range of the backplane network, which should be separate from the public network. Leave it empty, "", if you have no backplane. |
To review your changes, print the settings without their comments:
grep -Ev '^\s*(#|$)' /etc/acd/deploy/group_vars/all.yaml
Expected output:
---
manager_hostnames: [manager.example.com]
manage_firewall: true
manage_routes: true
external_directors: [{name: director-1, address: 192.0.2.201}]
backplane_network: "10.10.0.0/24"
cluster_pod_cidr: 10.42.0.0/16
cluster_svc_cidr: 10.43.0.0/16
firewall_cluster_zone: acd-cluster
firewall_public_policy: acd-public
tls_cert_file: ""
tls_key_file: ""
maxmind_city_db: ""
maxmind_asn_db: ""
maxmind_anon_db: ""
backup_dir: /var/lib/acd/backups
Important:
backplane_network,cluster_pod_cidr,cluster_svc_cidr,firewall_cluster_zoneandfirewall_public_policyare locked at the first installation. Changing one later means reinstalling the cluster, which starts empty: the PostgreSQL databases, Kafka’s messages and everything else stored in the cluster are lost unless you have backed them up. Check them now.
With manage_firewall: true, the default, deploy configures firewalld on every machine for you. The backplane interface is trusted for all traffic, and the public interface stays in firewalld’s public zone with only the ports the manager needs opened. Before you run deploy, make sure the public zone allows everything else you need on the public interface, SSH included: firewall-cmd --zone=public --list-all on each machine should list ssh under services.
For every setting, see Site Settings.
Step 7: Add Your TLS Certificate
Copy your certificate and its private key into /etc/acd/deploy/certs/:
cp manager-fullchain.crt manager.key /etc/acd/deploy/certs/
chmod 600 /etc/acd/deploy/certs/manager.key
- Both files must be PEM (text beginning with
-----BEGIN), in two separate files. The file names do not matter. - Use the full chain: your certificate followed by any intermediate certificates.
- The key must not have a passphrase.
- The certificate must cover the first entry of
manager_hostnames, directly or by a wildcard.
Check what the certificate covers:
openssl x509 -in /etc/acd/deploy/certs/manager-fullchain.crt -noout -subject -issuer -dates -ext subjectAltName
Expected output:
subject=CN = manager.example.com
issuer=CN = Example Issuing CA
notBefore=Oct 14 18:57:23 2026 GMT
notAfter=Jan 16 18:57:23 2027 GMT
X509v3 Subject Alternative Name:
DNS:manager.example.com
deploy checks the certificate and key before it changes anything.
No certificate yet? Add nothing to the folder: its
README.txtcan stay.deploythen serves a temporary self-signed certificate, which browsers warn about. Add the real one later and rundeployagain to replace it. For production, use a certificate from a certificate authority your users already trust. See TLS Certificates.
Step 8: Add Your MaxMind Databases
The manager uses MaxMind GeoIP databases for geographic routing. Copy the three .mmdb files from your MaxMind subscription into /etc/acd/deploy/maxmind/:
cp GeoIP2-City.mmdb GeoLite2-ASN.mmdb GeoIP2-Anonymous-IP.mmdb /etc/acd/deploy/maxmind/
ls -l /etc/acd/deploy/maxmind/
Expected output:
total 126236
-rw-r--r--. 1 root root 4382 Oct 14 18:57 GeoIP2-Anonymous-IP.mmdb
-rw-r--r--. 1 root root 121642973 Oct 14 18:57 GeoIP2-City.mmdb
-rw-r--r--. 1 root root 7609502 Oct 14 18:57 GeoLite2-ASN.mmdb
-rw-r--r--. 1 root root 400 Oct 14 18:57 README.txt
Files are matched by name: one containing City, one ASN and one Anon. Provide all three or none. With none, geographic routing is off until you add them and run deploy again. See MaxMind GeoIP Databases.
Step 9: Run the Deployment
Start the installation:
/mnt/esb3027/deploy
deploy works through fixed stages, and stops at the first problem with a message naming it:
- Gather inputs: reads every machine and works out the settings. Changes nothing.
- Preflight: checks the machines and your settings. Changes nothing.
- Backup: archives each machine’s configuration before any change.
- Host network: configures the firewall zones, routing rules and kernel network settings on each machine, then checks the result.
- Cluster: installs Kubernetes (k3s) on
manager-1, then joins the other servers one at a time. - Platform: installs the CDN Manager, loads the MaxMind databases, and runs a final health check.
The output is long. Shortened, it looks like this:
###############################################################
ESB3027 AgileTV CDN Manager -- Deployer
Version: 1.8.0
###############################################################
Running the operator playbook (log: /etc/acd/deploy/deploy.log) ...
[WARNING]: Collection ansible.posix does not support Ansible version 2.14.18
PLAY [Gather inputs - host probes, host network plans and k3s] *****************
TASK [Check that the node can be reached] **************************************
ok: [manager-1]
ok: [manager-3]
ok: [manager-2]
...
PLAY [Join or upgrade the other servers, one at a time] ************************
...
TASK [cluster : Join or upgrade this node with join-server] ********************
changed: [manager-2]
FAILED - RETRYING: [manager-2]: Wait for this server to become Ready (20 retries left).
FAILED - RETRYING: [manager-2]: Wait for this server to become Ready (19 retries left).
...
TASK [cluster : Wait for this server to become Ready] **************************
ok: [manager-2]
...
TASK [acceptance : Report the go verdict] **************************************
ok: [manager-1] => {
"msg": "Deployment gate PASSED for cdn-platform. GeoIP is serving."
}
...
PLAY RECAP *********************************************************************
manager-1 : ok=185 changed=28 unreachable=0 failed=0 skipped=106 rescued=0 ignored=0
manager-2 : ok=116 changed=16 unreachable=0 failed=0 skipped=48 rescued=0 ignored=0
manager-3 : ok=116 changed=16 unreachable=0 failed=0 skipped=48 rescued=0 ignored=0
The installation succeeded when you see Deployment gate PASSED and the recap shows unreachable=0 and failed=0 for every machine. Your other counts may differ.
Some lines look alarming but are expected:
FAILED - RETRYING: ... Wait for this server to become Ready:deploypolls each joining server until it is ready. Ten or more of these per server is normal.[WARNING]: Collection ansible.posix does not support Ansible version ...: harmless.[WARNING]: Could not match supplied host pattern, ignoring: acd_host_network_verify: harmless.
The whole run is also appended to /etc/acd/deploy/deploy.log.
If deploy stops, its message starts with a code in square brackets, such as [ACD-TLS-HOSTNAME]. Look the code up in Installation Troubleshooting, fix the cause, and run deploy again. Running it again is safe: it picks up where the cluster is and re-applies your settings.
Step 10: Verify the Cluster
Check that all three machines are Ready:
kubectl get nodes -o wide
Expected output (the columns right of EXTERNAL-IP are trimmed here):
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP
manager-1 Ready control-plane,etcd 12m v1.35.0+k3s1 10.10.0.11 192.0.2.11
manager-2 Ready control-plane,etcd 8m47s v1.35.0+k3s1 10.10.0.12 192.0.2.12
manager-3 Ready control-plane,etcd 6m3s v1.35.0+k3s1 10.10.0.13 192.0.2.13
INTERNAL-IP is each machine’s backplane address, and EXTERNAL-IP its public address.
Check every component of the CDN Manager:
acd-admin status
Every admin target should show ok under HEALTH, and every node yes under READY. acd-admin status exits 0 either way, so read the output. See acd-admin status for what each column means.
Check that the manager answers on its hostname:
curl https://manager.example.com/api/v1/health/ready; echo
Expected output:
{"status":"Ok"}
Note: If your certificate was issued by a private certificate authority that this machine does not trust, add
--cacert <your CA certificate>to thecurlcommands. If you started without a certificate, use-kinstead.
If you listed a CDN Director in external_directors, check that the manager reaches it. The trailing slash is required:
curl -s -o /dev/null -w '%{http_code}\n' https://manager.example.com/confd/director-1/config/__active/
Expected output:
200
200 shows that the manager reached the Director and read its configuration. A 5xx status comes from the manager’s gateway when it cannot reach the Director: check the Director’s address in external_directors, that the Director is running, and that the network between them allows the connection. Connecting the Director to the manager is covered in the CDN Director documentation.
Important: Your CDN Directors reach Kafka on ports 9092/tcp and 30000-30003/tcp on every machine (one more port per broker if you run more than the default three). The host firewall does not filter these ports, so they are open to any network that can reach the machines. Restrict them to your CDN Directors on your network’s own firewall. See the Networking Guide.
Step 11: Back Up the Zitadel Master Key
Zitadel, the manager’s identity service, encrypts its stored credentials with a master key. If the key is lost, it cannot be recovered. Print it:
kubectl get secret zitadel-masterkey --namespace default -o jsonpath="{.data.masterkey}" | base64 --decode; echo
Store the output somewhere safe, away from the cluster.
Step 12: Sign In to the GUI
In a browser, open https://manager.example.com/gui/:

Sign in with the default administrator account:
- Username:
admin@zitadel.manager.example.com, that is,admin@zitadel.followed by your main manager hostname. - Password:
Password1!
Note: Use the manager hostname you configured, not a machine’s IP address or another name: signing in only works under the main manager hostname. The GUI also needs the full login name above; the email address or
adminalone is rejected.
After signing in, the GUI home page appears:

Your CDN Manager cluster is installed.
What’s Next
- Secure the administrator account. The default credentials are published. Create your own administrator, then disable the default account: see Next Steps.
- Add capacity with agent machines: see Inventory.
- Change application settings, such as retention or disk sizes: see Application Settings.
- Change your configuration later: edit the files in
/etc/acd/deployand rundeployagain. That is the only way to change what is installed. See Running deploy.
5.2 - Inventory
Overview
The inventory, /etc/acd/deploy/inventory.ini, lists every machine in the cluster and its role. deploy init creates it as a commented example. Edit it before the first deploy, and again whenever you add a machine.
Each machine is one line under the heading for its role:
[server]
manager-1 ansible_host=192.0.2.11
manager-2 ansible_host=192.0.2.12
manager-3 ansible_host=192.0.2.13
[agent]
manager-4 ansible_host=192.0.2.14
[director]
- The first word is the machine’s name. Use its hostname.
ansible_hostis the addressdeployreaches the machine on over SSH. It does not decide which network the cluster uses: see Site Settings for that.- Everything after a
#is a comment, and indentation does not matter.
Important: The inventory also has a
[director]section. Leave it empty. Installing the CDN Director in Kubernetes is not supported in this release. CDN Directors run on their own machines and are listed inexternal_directorsin Site Settings.
Roles
Servers
Servers run the Kubernetes control plane and the cluster datastore, and by default also run the CDN Manager’s workloads.
- The first server listed is the one you run
deployon. It holds the ISO and the configuration folder. - The number of servers must be odd, so the control plane can always reach a majority. Use three for production: the cluster then survives losing any one machine. One server is enough for a lab, with no redundancy.
deploystops with[ACD-INVENTORY-EVEN-SERVERS]on an even number, and with[ACD-INVENTORY-NO-SERVER]if there is none.
Agents
Agents run workloads only: they add capacity, but take no part in the control plane. List any machines beyond the servers under [agent], or leave the section empty.
To add an agent to a running cluster, add its line under [agent], set up passwordless SSH to it from the first server, and run deploy again. See Changing the Cluster.
A machine cannot be listed under both [server] and [agent] ([ACD-INVENTORY-SERVER-AND-AGENT]). Changing a machine’s role on a running cluster takes a few extra steps: see Changing a Machine’s Role.
How Many of Each
- Three servers are enough for most clusters. The cluster then survives losing any one server. You are unlikely to need more than five: five servers survive losing two, and more add little except overhead.
- Add servers to improve redundancy, two at a time, since the count must stay odd.
- Add agents to expand capacity. Agents take on workloads without adding to the control plane, so they are the way to grow a cluster.
Per-Machine Options
Each option is added to the end of a machine’s line, separated by a space. None is needed for a normal installation.
node_ip and node_external_ip
The machine’s cluster address (the one other machines reach it on) and its public address (the one clients use). Both are detected automatically:
- The public address is the address of the interface holding the default route.
- The cluster address is the machine’s address inside
backplane_networkif that is set, and otherwise its public address.
Set them only to correct one machine where the wrong address is chosen:
manager-2 ansible_host=192.0.2.12 node_ip=10.10.0.12
Every machine’s cluster address must be on one network that all of them share. deploy stops with [ACD-BACKPLANE-SPLIT] before changing anything if they are not.
Important: These addresses are locked at the first installation. Changing one later means reinstalling the cluster. See Locked at First Install.
node_labels
Your own Kubernetes labels for the machine, as a name-and-value mapping in single quotes:
manager-1 ansible_host=192.0.2.11 node_labels='{"rack": "a1"}'
For example, zone labels for Topology Aware Hints are described in the Performance Tuning Guide.
node_taints
Keeps ordinary workloads off the machine, for a server dedicated to the control plane. The platform’s own cluster services tolerate the taint below and keep running there. The value is a list in single quotes:
manager-1 ansible_host=192.0.2.11 node_taints='["CriticalAddonsOnly=true:NoSchedule"]'
Use it only when there are enough other machines to run the workloads, for example three tainted servers and at least three agents. Never taint every machine.
ansible_user
The account deploy logs in as on this machine. The default is root. Another account needs passwordless sudo:
manager-4 ansible_host=192.0.2.14 ansible_user=operator
To change the account for every machine at once, add it under an [all:vars] heading at the end of the file:
[all:vars]
ansible_user=operator
A setting on a machine’s own line still wins for that machine.
SSH Access
deploy logs in to every machine in the inventory from the first server, the first server included, without a password prompt. For each machine, on the first server:
ssh-copy-id root@<machine address>
ssh -o BatchMode=yes root@<machine address> true
The second command must succeed without a prompt. Otherwise deploy stops with UNREACHABLE for that machine.
5.3 - Site Settings
Overview
Your site settings live in /etc/acd/deploy/group_vars/all.yaml. They are the only deployment settings you set: everything that depends on them, such as the ingress hostnames, the TLS secret, Zitadel’s external domain and the list of CDN Directors, is worked out from them, so related values cannot fall out of step.
Every setting appears once in the file, with comments, and already holds a working default. Change a setting by editing its value in place, then run deploy.
A list can be written on one line or one entry per line. Both mean the same:
manager_hostnames: [manager.example.com, cdn.example.com]
manager_hostnames:
- manager.example.com
- cdn.example.com
Settings
manager_hostnames
Required. Every hostname that users and CDN Directors use to reach the manager.
manager_hostnames: [manager.example.com]
- The first entry is the main hostname. Your TLS certificate must cover it, and it is the only name users can sign in under: Zitadel rejects sign-ins made under any other name, including a machine’s IP address.
- Each name needs a DNS record pointing at the public address of one or more of the machines.
- Leave the list empty,
[], only for a quick lab try-out. A temporary hostname and a self-signed certificate are then used.
external_directors
The CDN Directors that use this manager. They run on their own machines, outside the cluster: deploy installs nothing on them, and the manager routes to them.
external_directors:
- {name: director-1, address: 192.0.2.201}
- {name: director-2, address: 192.0.2.202}
nameis your own name for the Director. The manager publishes its configuration under it, for example athttps://<manager hostname>/confd/director-1/config/__active/.addressmust be the Director’s IPv4 address, not a hostname.deploystops with[ACD-DIRECTOR-ADDRESS]otherwise.- Leave the list empty,
[], if you have none yet, then add your Directors later and rundeployagain. Until you do, the preset’s placeholder Directors stay in the configuration:director-1anddirector-2at 192.0.2.1 and 192.0.2.2 with the production preset,router-1at 192.0.2.1 with the lab preset. Those addresses are reserved for documentation and reach nothing.
The Directors reach Kafka on 9092/tcp and 30000-30003/tcp on every machine, with one more port per broker beyond the default three. The host firewall does not filter these ports: restrict them on your network’s own firewall. See Kafka Ports.
Important: Each CDN Director must also be able to reach the manager’s ingress (HTTPS on 443/tcp) under one of the names in
manager_hostnames. The ingress answers on every address of every cluster machine, public and backplane alike. So Directors on the backplane network can reach the manager there, instead of over the public network. In that case:
- Give the Directors a hostname that resolves, for them, to the machines’ backplane addresses, and add it to
manager_hostnames, after the main hostname.- Make sure your TLS certificate covers that name too, as a Subject Alternative Name (SAN). Otherwise the Directors’ certificate checks fail. See TLS Certificates.
manage_firewall
Whether deploy configures firewalld for you. The default is true.
true:deploystarts and enables firewalld (it must be installed) and configures it on every machine. The machines trust each other for all traffic, the public interface is placed in firewalld’spubliczone, and the ports clients need are opened. Before the first run, make sure thepubliczone allows everything else you need on the public interface, SSH included:firewall-cmd --zone=public --list-allshould listsshunderservices. See Firewall for exactly what is set up.false: you configure the firewall yourself.deployonly checks that the required ports are open and the pod and service networks are trusted, and stops before changing anything if not. Only firewalld is checked: if it is not running, the check is skipped, and your own firewall must allow the same traffic.
To switch a running cluster from true to false, follow Managing the Firewall Yourself first.
manage_routes
Whether deploy keeps cluster traffic on the main routing table. The default is true; leave it on.
With true, deploy adds lookup main routing rules at priority 50 for the pod network, the service network, and the network the machines reach each other on. This matters on machines that use policy routing, where traffic from an address is sent through its own routing table: without these rules, cluster traffic can leave through the wrong interface. On machines without policy routing, the rules are harmless.
deployowns priority 50 on the NetworkManager profiles of the backplane and public interfaces, and replaces any rule there at that priority. Keep your own rules at other priorities.- Your own rules that cluster traffic can match, such as
from <address> lookup <table>, must have a priority number of 51 or more.deploystops with[ACD-ROUTING-CONFLICT], naming the rule, otherwise. - If your rules must stay at 50 or below, set
manage_routes: falseand add your ownlookup mainrules for those three networks, at a priority other than 50 and ahead of your source rules. See Managing Routing Yourself for the rules and the commands.
Either way, deploy checks that cluster traffic uses the main routing table before it changes anything.
Locked at First Install
The settings below are marked as a separate section in the file. Set them before the first deploy and never change them afterwards: the cluster is built around them. Changing one later means uninstalling k3s from every machine and installing again. A reinstall starts with an empty cluster: the PostgreSQL databases, Kafka’s messages and everything else stored in the cluster are lost unless you have backed them up.
The same applies to each machine’s node_ip and node_external_ip in the inventory, and to the machines’ hostnames. On a running cluster, deploy compares these with what the cluster uses, and stops before changing anything if they differ.
backplane_network
Optional, but recommended. The address range of the backplane network, the network the cluster machines use to talk to each other, such as "10.10.0.0/24". Leave it empty, "", if you have no backplane: the machines then talk to each other over the public network.
When it is set, deploy finds each machine’s interface in that range and configures the cluster to use it: all node-to-node (east-west) traffic runs over it, including Kubernetes, its pod network and the cluster datastore.
- The backplane should be a separate network from the public network, the one holding the default route. Setting it to the public network’s range works, but then every host on the public network is trusted for all traffic.
- Every machine needs exactly one interface in the range, and must be reachable from the others on it.
deploystops with[ACD-BACKPLANE-NO-MATCH]or[ACD-BACKPLANE-AMBIGUOUS]otherwise. - The backplane does not have to be reserved for the cluster: other hosts, such as your CDN Directors, may also be on it. With
manage_firewall: true, every host on the backplane can reach every port on every cluster machine, including the cluster datastore (etcd, 2379-2380/tcp), the Kubernetes API (6443/tcp) and the kubelet (10250/tcp), so connect only hosts you trust to it.
See Configuring Segregated Networks.
cluster_pod_cidr and cluster_svc_cidr
The address ranges Kubernetes gives its pods and its services. The defaults are 10.42.0.0/16 and 10.43.0.0/16.
Change them only if your network already uses addresses in these ranges. The two must not overlap each other, or any address or route on any machine. deploy stops with [ACD-CIDR-OVERLAP] or [ACD-CIDR-IN-USE] before changing anything if they do.
firewall_cluster_zone and firewall_public_policy
The names of the firewalld zone and policy that deploy keeps as its own. The defaults are acd-cluster and acd-public. The zone trusts the cluster, and the policy opens the ports reached from outside.
deploy removes anything in them that it did not put there, so pick names nothing else on the machines uses. Never use one of firewalld’s own names, such as public or trusted. The zone name can be up to 17 characters and the policy name up to 18, using letters, digits, - and _. The defaults suit most sites.
File Locations
Most sites leave these empty: the files are picked up from the configuration folder.
| Setting | Default | Set it to use a file kept elsewhere |
|---|---|---|
tls_cert_file, tls_key_file | "", from certs/ | Your certificate and key. See TLS Certificates. |
maxmind_city_db, maxmind_asn_db, maxmind_anon_db | "", from maxmind/ | Your three MaxMind databases. See MaxMind GeoIP Databases. |
backup_dir | /var/lib/acd/backups | Where each run’s backup is written on the first server. See Backups. |
5.4 - TLS Certificates
Overview
The manager serves all its interfaces (the GUI, the API, Zitadel and Grafana) over HTTPS on port 443, under the hostnames in manager_hostnames. Give it a certificate by putting the certificate and its key in /etc/acd/deploy/certs/ and running deploy.
We recommend a certificate from a certificate authority (CA) your users and CDN Directors already trust, such as your organization’s own CA or a public one. Without a certificate, deploy serves a temporary self-signed one, which every browser and client warns about.
Requirements
- PEM format: text files beginning with
-----BEGIN. Convert DER or PKCS#12 files to PEM first. - Two files: the certificate in one, the private key in the other. The file names do not matter:
deployidentifies them by their contents. - The full chain: your certificate followed by any intermediate certificates, often supplied as
fullchain. - No passphrase on the private key.
- The right names: the certificate must cover the first entry of
manager_hostnames, directly or by a wildcard such as*.example.com. Cover the other entries too, as Subject Alternative Names (SANs), or clients using them fail their certificate check. This includes any name your CDN Directors use to reach the manager, such as a name for the backplane addresses. - Exactly one certificate and one key in the folder.
Adding or Replacing the Certificate
Copy the files into the folder, and keep the key readable only by root:
cp manager-fullchain.crt manager.key /etc/acd/deploy/certs/ chmod 600 /etc/acd/deploy/certs/manager.keyWhen replacing a certificate, remove the old files first, so the folder holds only one pair.
Check the names and dates:
openssl x509 -in /etc/acd/deploy/certs/manager-fullchain.crt -noout -subject -issuer -dates -ext subjectAltNameExpected output:
subject=CN = manager.example.com issuer=CN = Example Issuing CA notBefore=Oct 14 18:57:23 2026 GMT notAfter=Jan 16 18:57:23 2027 GMT X509v3 Subject Alternative Name: DNS:manager.example.comRun
deploy:/mnt/esb3027/deploy
deploy checks the pair before it changes anything: that both are readable, that they match, that the certificate has not expired, and that it covers the main hostname. It then stores them in the cluster as the ingress certificate for every manager hostname.
The certificate and key are not included in the backups deploy takes. Keep your own copies.
Checking the Served Certificate
From any machine that can reach the manager:
openssl s_client -connect manager.example.com:443 -servername manager.example.com </dev/null 2>/dev/null | grep -E 'subject=|issuer=|Verify return code'
Expected output:
subject=CN = manager.example.com
issuer=CN = Example Issuing CA
Verify return code: 0 (ok)
A verify return code other than 0 means this machine does not trust the issuing CA. With a private CA, add -CAfile <your CA certificate> to the command, and install the CA certificate on every client that connects to the manager, CDN Directors included.
A Certificate for a Test Cluster
For a lab or test cluster without a certificate, you can create a private CA and issue a certificate from it. Clients then trust the manager once they trust the CA. Do not use this for production.
mkdir -p /root/test-ca && cd /root/test-ca
# A private CA, valid for ten years
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
-keyout test-ca.key -out test-ca.crt -subj "/CN=Test CA"
# A key and certificate request for the manager
openssl req -newkey rsa:2048 -sha256 -nodes \
-keyout manager.key -out manager.csr -subj "/CN=manager.example.com"
# Sign it, naming every manager hostname
cat > manager.ext <<'EOF'
basicConstraints=CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=DNS:manager.example.com
EOF
openssl x509 -req -in manager.csr -CA test-ca.crt -CAkey test-ca.key -CAcreateserial \
-days 825 -sha256 -extfile manager.ext -out manager.crt
# Install the full chain and the key
cat manager.crt test-ca.crt > /etc/acd/deploy/certs/manager-fullchain.crt
install -m 600 manager.key /etc/acd/deploy/certs/manager.key
Then run deploy. Pass --cacert /root/test-ca/test-ca.crt to curl, and import test-ca.crt into your browser, to connect without warnings.
Problems
| Code | Cause | Fix |
|---|---|---|
[ACD-TLS-NOT-PEM] | No file in certs/ is a readable PEM certificate or key. | Convert the files to PEM. |
[ACD-TLS-COMBINED-FILE] | One file holds both the certificate and the key. | Split them into two files. |
[ACD-TLS-TOO-MANY] | More than one certificate or key in certs/. | Leave exactly one of each. |
[ACD-TLS-UNPAIRED] | A certificate without a key, or a key without a certificate. | Provide both, or neither for a self-signed certificate. |
[ACD-TLS-KEY-UNREADABLE] | The key is not PEM, or has a passphrase. | Remove the passphrase: openssl pkey -in <key> -out <key>.plain. |
[ACD-TLS-KEY-MISMATCH] | The certificate and key are not a pair. | Use the key the certificate was requested with. |
[ACD-TLS-CERT-EXPIRED] | The certificate has expired. | Replace it with a current one. |
[ACD-TLS-HOSTNAME] | The certificate does not cover the first entry of manager_hostnames. | Reissue it for that name, or for a wildcard covering it. |
5.5 - MaxMind GeoIP Databases
Overview
The manager looks up the location, network and anonymity of client addresses in MaxMind GeoIP databases, for geographic routing and the GeoIP API. The databases are not shipped with the CDN Manager: obtain them from your own MaxMind subscription.
Three databases are needed, as .mmdb files:
| Database | Example file name | Name must contain |
|---|---|---|
| City | GeoIP2-City.mmdb | City |
| ASN | GeoLite2-ASN.mmdb or GeoIP2-ASN.mmdb | ASN |
| Anonymous IP | GeoIP2-Anonymous-IP.mmdb | Anon |
Provide all three or none. Without them, the CDN Manager installs and runs, but geographic routing is off.
Adding or Updating the Databases
Copy the three files into
/etc/acd/deploy/maxmind/, replacing any older copies:cp GeoIP2-City.mmdb GeoLite2-ASN.mmdb GeoIP2-Anonymous-IP.mmdb /etc/acd/deploy/maxmind/ ls -l /etc/acd/deploy/maxmind/Expected output:
total 126236 -rw-r--r--. 1 root root 4382 Oct 14 18:57 GeoIP2-Anonymous-IP.mmdb -rw-r--r--. 1 root root 121642973 Oct 14 18:57 GeoIP2-City.mmdb -rw-r--r--. 1 root root 7609502 Oct 14 18:57 GeoLite2-ASN.mmdb -rw-r--r--. 1 root root 400 Oct 14 18:57 README.txtRun
deploy:/mnt/esb3027/deploy
deploy loads the databases into the cluster after installing the CDN Manager, and loads them again only when they have changed. MaxMind publishes updates regularly: repeat these steps to apply them.
The databases are not included in the backups deploy takes. Keep your own copies.
Checking the Databases
When the databases are loaded, the final health check of deploy reports:
"msg": "Deployment gate PASSED for cdn-platform. GeoIP is serving."
To look up an address yourself:
curl 'https://manager.example.com/api/v1/routing/geoip?ip=8.8.8.8'; echo
Expected output (shortened):
{"city":{"continent":{"code":"NA",...},"country":{"iso_code":"US",...},...},"asn":{"autonomous_system_number":15169,"autonomous_system_organization":"GOOGLE"},"anon":{}}
See the Routing API for the full response.
Keeping the Files Elsewhere
To use files kept in another folder, set their paths in Site Settings instead of copying them:
maxmind_city_db: "/opt/maxmind/GeoIP2-City.mmdb"
maxmind_asn_db: "/opt/maxmind/GeoLite2-ASN.mmdb"
maxmind_anon_db: "/opt/maxmind/GeoIP2-Anonymous-IP.mmdb"
Problems
| Code | Cause | Fix |
|---|---|---|
[ACD-MAXMIND-INCOMPLETE] | maxmind/ holds some, but not all three, databases. | Provide all three, or none. |
[ACD-MAXMIND-AMBIGUOUS] | More than one file matches the same database. | Leave one file per database. |
5.6 - Application Settings
Overview
The CDN Manager is a Helm release, cdn-platform, configured by Helm values. deploy builds those values from three layers, each overriding the one before it:
| Layer | File | Who writes it |
|---|---|---|
| 1. Preset | preset.yaml, a link to a file on the ISO | AgileTV |
| 2. Your overrides | values.override.yaml | You |
| 3. Derived settings | worked out from group_vars/all.yaml and the certs/ folder | deploy |
deploy merges them into /etc/acd/deploy/values.yaml on every run and installs the release with that one file.
Most sites need nothing beyond the preset and their site settings.
Presets
The preset is the complete starting configuration, chosen once with deploy init and remembered for every later run:
| Preset | Command | ISO file | Use it for |
|---|---|---|---|
| Production | deploy init prod (the default) | values-production.yaml | Production clusters of three or more machines. Replicated services are spread across machines, with production resource sizing. |
| Lab | deploy init lab | values-lab.yaml | A single-machine lab, demonstration or acceptance test. Single replicas and small resource requests. |
The preset is read from the ISO through the preset.yaml link, never copied. A new release’s ISO therefore brings its own defaults with it. This is also why the ISO must be mounted whenever you run deploy.
Overriding Settings
Write only the settings you want to change in /etc/acd/deploy/values.override.yaml, keeping the same nesting as in the preset. It starts empty. For example, to make the manager log in more detail while you investigate a problem:
cdn-manager:
manager:
logLevel: debug
Then run deploy. To undo it, remove the setting and run deploy again.
Warning: Set volume sizes before the first
deploy. Helm cannot change the size of a persistent volume claim that already exists, so changing asizeorpersistencesetting invalues.override.yamlon an installed cluster does not resize the volume, and can make the upgrade fail. This applies to every component with storage, such as Kafka, VictoriaMetrics, PostgreSQL and Redis. Raising a retention setting also uses more disk, so check the volume has room first.
To find a setting, look it up in the preset on the ISO (/mnt/esb3027/values-production.yaml or /mnt/esb3027/values-lab.yaml), in the full reference /mnt/esb3027/values.yaml, or in the Configuration Guide.
Note: Some settings are derived and override anything in
values.override.yaml: every manager hostname, Zitadel’s external domain, the TLS secret, and the list of CDN Directors wheneverexternal_directorsis not empty. Set those throughmanager_hostnames,external_directorsand thecerts/folder instead.
The Generated values.yaml
/etc/acd/deploy/values.yaml holds the exact values the cluster received. Read it to see the result of the three layers.
Do not edit it: deploy rewrites it on every run, and stops with [ACD-VALUES-EDITED] if it was changed by hand. Move the change into values.override.yaml or group_vars/all.yaml, then delete values.yaml and run deploy again.
5.7 - Running deploy
Overview
deploy is on the installation ISO, at /mnt/esb3027/deploy. Run it as root on the first server in the inventory, with the ISO mounted at /mnt/esb3027. It reaches every other machine over SSH. For its commands, options, files and exit status, see the deploy reference.
To change what is installed, edit the files in /etc/acd/deploy and run deploy. Do not change the release with helm directly: the next deploy replaces it with what the configuration says.
What a Run Does
/mnt/esb3027/deploy
The first run installs; every later run re-applies your settings and changes only what differs. Running it again is safe, including after a failed run. deploy checks each machine after changing its network, and on a running cluster changes them one at a time, but a wrong network setting can still cut a machine off, and the run then stops with [ACD-NODE-UNREACHABLE].
Each run goes through fixed stages:
| Stage | What it does | Changes anything? |
|---|---|---|
| Gather inputs | Reads every machine and works out the settings | No |
| Preflight | Checks the machines, the settings and, on a running cluster, the locked settings | No |
| Backup | Archives each machine’s configuration on the first server | Writes the backup only |
| Media | Copies what the other machines need from the ISO | Yes |
| Values | Writes values.yaml | Yes |
| Host network | Configures firewall zones, routing rules and kernel network settings, then verifies them | Yes |
| Cluster | Installs or upgrades k3s: the first server, then the other servers one at a time, then the agents | Yes |
| Platform | Installs or upgrades the cdn-platform release, loads the MaxMind databases, and runs the health check | Yes |
deploy backup runs only the first and third stages. --skip-tags preflight skips the second; use it only when you are sure a check is wrong for your site.
When deploy stops on a problem it recognizes, its message starts with a code such as [ACD-TLS-HOSTNAME]. See Installation Troubleshooting.
Backups
Important: These backups hold only the deployment’s configuration, each machine’s configuration and an etcd snapshot of the Kubernetes state. They do not include the data stored in the cluster: not the PostgreSQL databases, not Kafka’s messages, not the contents of any persistent volume, and not the metrics. Restoring one of these backups puts a machine’s configuration back; it cannot recover lost data. Back up the data separately: see Backup Procedures.
Every run of deploy takes a backup first, and deploy backup takes one without changing anything. Each backup is a folder under /var/lib/acd/backups on the first server: see the deploy reference for its files. deploy never deletes a backup: remove old folders yourself. Keep your own copies of the TLS certificate and key and the MaxMind databases, which the backup leaves out.
Important: Treat backups as secret material. A backup of a running cluster holds an etcd snapshot of the whole cluster, which contains every Kubernetes Secret: database passwords, the Zitadel master key and TLS keys among them. Every machine’s archive, servers’ and agents’, also holds the cluster token, and the servers’ hold the cluster-admin kubeconfig,
/etc/rancher/k3s/k3s.yaml. Keep backups, and any copies you make, readable only by root, and delete the ones you no longer need.
The etcd snapshot is in two places, and deploy prunes neither:
the backup folder under
/var/lib/acd/backupson the first server, inside one server’s<machine>.tar.gz. Remove an old backup folder with:rm -r /var/lib/acd/backups/<folder>/var/lib/rancher/k3s/server/db/snapshotson that server, namedacd-backup-<UTC time>-<machine>-<number>. List and delete old ones on that server with:k3s etcd-snapshot list k3s etcd-snapshot delete <name>
k3s also takes its own etcd snapshots, in the same folder on every server, named etcd-snapshot-<machine>-<time>: one every 12 hours, keeping the latest five, which k3s prunes itself. They hold every Kubernetes Secret too, so treat them, and any copy of them, as secret material.
The backup folder on the first server also holds a copy of /etc/acd/deploy, except the certs/ and maxmind/ folders and deploy.log, which it leaves out. Keep a copy of the latest backup folder off the first server, so that losing it does not lose the configuration deploy needs. Protect the copy as the warning above describes.
Before relying on a backup, check it:
cd /var/lib/acd/backups/<folder>
sha256sum -c SHA256SUMS
Restoring a Machine’s Files
Copy the machine’s archive and checksum file to it, then, as root:
tar --selinux --xattrs --acls -C / -xpzf <machine>.tar.gz
sha256sum -c <machine>.files.sha256
Keep the --selinux --xattrs --acls options: plain tar restores some k3s files with the wrong SELinux label. Then reboot the machine, or restart NetworkManager and k3s.
Rolling back the cluster itself also needs the etcd snapshot, restored on one server with the k3s cluster-reset procedure: stop k3s on every server, restore that server’s archive, run k3s server --cluster-reset --cluster-reset-restore-path=<snapshot path> on it, start k3s there, and rejoin the other servers. See the k3s etcd snapshot documentation for the full procedure.
What to Send Support
When the final health check fails, deploy writes /etc/acd/deploy/support-bundle-acd.tar.gz: send it to AgileTV support. When a run stops before the health check, send deploy.log, decision-record.json and the probe/ folder from /etc/acd/deploy instead.
5.8 - Changing the Cluster
Overview
The machines in the cluster are whatever inventory.ini lists. To change them, edit the inventory and run deploy. This page covers the changes that need more than that.
Before you start, check that the cluster is healthy: every node yes under READY and every admin target ok under HEALTH in acd-admin status.
Adding an Agent
Install the new machine as for the others: see Before You Begin. If you use a backplane, give it an address in
backplane_network.From the first server, set up SSH to it:
ssh-copy-id root@192.0.2.14 ssh -o BatchMode=yes root@192.0.2.14 hostnameAdd it under
[agent]in/etc/acd/deploy/inventory.ini:[agent] manager-4 ansible_host=192.0.2.14Run
deploy:/mnt/esb3027/deploy
deploy configures the new machine’s network, joins it to the cluster, and updates every other machine’s firewall to trust it.
Adding Servers
The number of servers must stay odd, so add servers two at a time, for example to go from one to three. Prepare both machines as for an agent, list both under [server], and run deploy once. deploy joins them one at a time.
Changing a Machine’s Role
k3s cannot turn a server into an agent, or an agent into a server, in place: the machine leaves the cluster and joins again in its new role. Because the number of servers must stay odd, a role change always moves two machines: two agents promoted to go from one server to three, two servers demoted to go from three to one, or a server and an agent swapped.
Take the first machine out of the cluster, whether it is a server or an agent. From any server, drain and remove it:
acd-admin drain <machine> kubectl delete node <machine>The drain moves the machine’s pods elsewhere. It can take several minutes while storage moves, and reports what it is waiting for. If it does not finish, see When a Drain Does Not Finish. Removing the node needs
kubectl:acd-adminhas no command for it. Read Safety first.Then, on the machine itself, uninstall k3s: run
/usr/local/bin/k3s-uninstall.shif it was a server, or/usr/local/bin/k3s-agent-uninstall.shif it was an agent.Only when the first machine is out, do the same for the second. Taking two out at once can leave too few machines for the workloads.
List both machines under their new roles in
inventory.ini, and rundeploy.
With a single server, promoting two agents leaves the server alone to run every workload while both agents are out. Expect pods to stay Pending until the machines rejoin. The deploy that brings them back allows for this, and waits up to 10 minutes for the pods to start.
If a machine is listed under a role other than the one it runs, deploy stops with [ACD-INVENTORY-ROLE-CHANGED] before changing anything.
When a Drain Does Not Finish
acd-admin drain waits for every pod it moves, and has no timeout of its own. Its session ends after 12 hours; running drain again carries on where it stopped. Storage does not hold it up for long: each volume has replicas on other machines, and for the Kafka data volumes, which have a single replica, the storage layer (Longhorn) copies the replica to another machine before it lets the drain go on. While that happens, the drain names the pod whose eviction a disruption budget refuses for now, and keeps retrying.
If it still waits, read what it names:
- A pod that is terminating but not gone, with the finalizer holding it. On a machine that is not Ready, such a pod stays until the machine returns. Once the drain waits only on such pods, press Ctrl-C: the machine stays drained.
- A pod whose eviction is refused for a long time. Check
acd-admin statusfor an admin target that is notok, andkubectl -n longhorn-system get volumes.longhorn.iofor a volume that is nothealthy. A disruption budget refuses to evict a pod while its other pods are not Ready. Fix that, and the drain carries on.
See acd-admin drain.
Things You Cannot Change
The cluster is built around a few settings that cannot change once it is installed: backplane_network, cluster_pod_cidr, cluster_svc_cidr, the firewall zone and policy names, each machine’s node_ip and node_external_ip, and the machines’ hostnames. See Locked at First Install.
5.9 - Air-Gapped Installation
Overview
The installation ISO carries everything the CDN Manager needs, container images included, so an installation needs no internet access. What deploy does need is the operating system’s own packages: it installs a few with dnf on every machine: ansible-core on the first server, and on all of them rsync, tar, zstd, bash-completion, the storage tools (iscsi-initiator-utils and nfs-utils), and container-selinux, which the cluster’s SELinux policy needs.
Without internet access, give every machine a local package repository from the operating system’s installation media, then follow the Quickstart as written.
Prerequisites
| Media | Purpose |
|---|---|
esb3027-cdn-manager-1.8.0.iso | The CDN Manager, on the first server only |
| The installation ISO of your operating system, for the same release as the machines | Operating system packages, on every machine |
Set Up a Local Repository on Every Machine
On each machine, mount the operating system ISO and add its two repositories. This example is for Red Hat Enterprise Linux 9; on a rebuild, use its ISO and the signing key file its own documentation names.
mkdir -p /mnt/os
mount -o loop,ro /root/rhel-9.4-x86_64-dvd.iso /mnt/os
cat > /etc/yum.repos.d/os-media.repo <<'REPO'
[os-media-baseos]
name=OS media - BaseOS
baseurl=file:///mnt/os/BaseOS
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
[os-media-appstream]
name=OS media - AppStream
baseurl=file:///mnt/os/AppStream
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
REPO
Disable any repository the machine cannot reach, or dnf fails while trying to read it:
dnf repolist
dnf config-manager --set-disabled <repository id>
Then check that dnf can find the packages:
dnf -q repoquery --latest-limit 1 ansible-core rsync tar zstd bash-completion iscsi-initiator-utils nfs-utils container-selinux
Each package should be listed once, whether or not it is already installed.
Important: Keep the local repository available on every machine whenever you run
deploy, not only for the first installation. Every run installs or checks packages withdnfon each machine, and stops if it cannot reach them. Remount the operating system ISO after a reboot, as you do the CDN Manager ISO.
Install
Follow the Quickstart. Copy the MaxMind databases, your TLS certificate and its key, and both ISOs to the machines by whatever means your site allows.
If the first server cannot install ansible-core, deploy init stops with:
Error: could not install ansible-core. Ensure your OS repositories
or mounted install media are available to dnf (air-gap SOP).
Check the repository on that machine and run deploy init again.
5.10 - Upgrade Guide
Upgrading to 1.8 from an Earlier Release
Version 1.8 cannot be installed over 1.6 or any earlier release. The platform’s Helm charts were rearranged, and the cluster is now built and managed by deploy, so an earlier cluster cannot be upgraded in place.
To move to 1.8:
- Install 1.8 on a new cluster, on new or reinstalled machines, with the Quickstart.
- Set up its users and configuration. Contact AgileTV support to plan moving your existing configuration.
- Point your CDN Directors at the new manager, then retire the old cluster.
Upgrading from 1.8 to a Later Release
From 1.8 on, an upgrade is a deploy run with the new release’s ISO: deploy upgrades k3s on each machine, one server at a time, then upgrades the cdn-platform release.
Read the Release Notes for the new release, for any step it needs before or after the upgrade.
Check that the cluster is healthy:
acd-admin statusReplace the ISO at
/mnt/esb3027with the new one, on the first server:umount /mnt/esb3027 mount -o loop,ro /root/esb3027-cdn-manager-<new version>.iso /mnt/esb3027The preset link in
/etc/acd/deploypoints into/mnt/esb3027, so mount the new ISO at the same path.Run
deploy initfrom the new ISO. It is safe to run again: it replaces the installed Ansible collections with the new ISO’s versions, installsansible-coreonly if it is missing, keeps your preset, and leaves the files you have edited alone./mnt/esb3027/deploy initRun
deploy:/mnt/esb3027/deploy
deploy takes a backup of the configuration before it changes anything, including an etcd snapshot of the cluster. That backup does not include the data stored in the cluster, such as the PostgreSQL databases: back that up first, as described in Backup Procedures. See Backups.
5.11 - Installation Troubleshooting
Overview
deploy checks the machines and your settings before it changes anything, and checks its own work as it goes. When it stops on a problem it recognizes, the message starts with a code in square brackets, such as [ACD-TLS-HOSTNAME], and names the machine, file or setting involved. The code stays the same when the wording changes, so search for it on this page, or quote it to support.
After fixing the cause, run deploy again. Re-running is safe: it picks up from the state the cluster is in. “Nothing was changed” in the table below means the run stopped before its first change.
Expected Messages
These appear in a successful run, and need no action:
| Message | Why |
|---|---|
FAILED - RETRYING: [<machine>]: Wait for this server to become Ready (N retries left). | deploy polls each joining machine until it is ready. Ten or more per machine is normal. |
[WARNING]: Collection ansible.posix does not support Ansible version ... | The bundled collection works with the ansible-core release of Enterprise Linux 9. |
[WARNING]: Could not match supplied host pattern, ignoring: acd_host_network_verify | No machine’s network needed checking at that point. |
Problems Without a Code
| Symptom | Cause | Fix |
|---|---|---|
UNREACHABLE for a machine | deploy cannot log in to it over SSH without a password. | Set up the key: see SSH Access. |
preset.yaml points at ..., which does not exist | The installation ISO is not mounted at the path deploy init used, normally /mnt/esb3027. | Mount it at that path, or run deploy init again from where it is mounted now. |
ansible-core is not installed | deploy init has not been run on this machine, or ansible-core was removed. | Run /mnt/esb3027/deploy init. It keeps the preset an existing configuration already uses; a new configuration gets prod unless you add lab. |
deploy init cannot install ansible-core | dnf cannot reach the operating system’s repositories. | Fix the repositories. Without internet access, see Air-Gapped Installation. |
| Signing in fails, though the pages load | The browser uses a name other than the main manager hostname, such as an IP address. | Browse to the first entry of manager_hostnames. Sign in to the GUI as <user>@zitadel.<manager hostname>. |
Failure Codes
Each code below gives the cause, then what to do.
[ACD-ANSIBLE-VERSION]
Cause: The installed ansible-core is older than 2.14.
What to do: Run dnf update ansible-core.
[ACD-HOST-PROBE-IN-DEPLOY]
Cause: A machine has a host_probe setting. It is an input for tests and for replaying a run, and deploy reads every machine itself. Nothing was changed.
What to do: Remove the host_probe setting from inventory.ini and host_vars.
[ACD-HOST-PROBE-SCHEMA]
Cause: A machine’s host_probe setting, used by tests and replays, has a format version this deploy cannot read. Nothing was changed.
What to do: Replace it with that machine’s probe/<machine>.json from a run of this release, or remove the host_probe setting.
[ACD-NO-DEFAULT-ROUTE]
Cause: A machine has no IPv4 default route, so its address cannot be chosen.
What to do: Give the machine a default route.
[ACD-BACKPLANE-AMBIGUOUS]
Cause: backplane_network matches more than one interface on a machine.
What to do: Narrow backplane_network, or set node_ip to the address on the backplane interface, on that machine’s line in inventory.ini.
[ACD-BACKPLANE-NO-MATCH]
Cause: backplane_network matches no interface on a machine.
What to do: Fix backplane_network, or set node_ip to the address on the backplane interface, on that machine’s line in inventory.ini. Every machine needs an address in backplane_network.
[ACD-NODE-ADDRESS-NOT-FOUND]
Cause: No interface on a machine holds the address chosen for it.
What to do: Check node_ip for that machine, or backplane_network.
[ACD-DEFAULT-ADDRESS-NOT-FOUND]
Cause: No interface on a machine holds its default route’s source address.
What to do: Give the default route a source address that one of the machine’s interfaces holds.
[ACD-NM-NOT-RUNNING]
Cause: NetworkManager is not running on a machine, so the deployment cannot set its interfaces’ zones or routing rules.
What to do: Start it with systemctl enable --now NetworkManager and run deploy again.
[ACD-NM-CONNECTION-MISSING]
Cause: An interface the deployment configures has no active NetworkManager connection.
What to do: Let NetworkManager manage it (nmcli device set <interface> managed yes), give it a connection (nmcli device connect <interface>, or nmcli connection add) and run deploy again.
[ACD-PARTIAL-RUN]
Cause: The run was limited to some machines, so it could not plan every machine’s network. What each machine trusts depends on all of them.
What to do: Run deploy without --limit.
[ACD-DIRECTOR-ADDRESS]
Cause: An entry in external_directors has no address, or one that is not an IPv4 address.
What to do: Give each CDN Director’s IPv4 address, such as 192.0.2.201.
[ACD-MAXMIND-AMBIGUOUS]
Cause: More than one file in maxmind/ matches the same MaxMind database.
What to do: Leave one file per database in maxmind/.
[ACD-MAXMIND-INCOMPLETE]
Cause: maxmind/ holds some, but not all three, MaxMind databases.
What to do: Provide all three databases, or none.
[ACD-TLS-COMBINED-FILE]
Cause: One file in certs/ holds both the certificate and the private key.
What to do: Split them into two PEM files in certs/.
[ACD-TLS-TOO-MANY]
Cause: certs/ holds more than one certificate or more than one key.
What to do: Leave exactly one certificate and one key.
[ACD-TLS-UNPAIRED]
Cause: certs/ holds a certificate without a key, or a key without a certificate.
What to do: Provide both, or neither for a temporary self-signed certificate.
[ACD-TLS-NOT-PEM]
Cause: certs/ holds files, but none is a readable PEM certificate or key.
What to do: Convert DER or PKCS#12 files to PEM.
[ACD-TLS-CERT-UNREADABLE]
Cause: The certificate file cannot be read as a PEM certificate.
What to do: Provide a PEM certificate.
[ACD-TLS-KEY-UNREADABLE]
Cause: The private key cannot be read: it is not PEM, or it has a passphrase.
What to do: Remove the passphrase with openssl pkey -in <key> -out <key>.plain.
[ACD-TLS-KEY-MISMATCH]
Cause: The certificate and the private key are not a pair.
What to do: Provide the key the certificate was requested with.
[ACD-TLS-CERT-EXPIRED]
Cause: The certificate has expired.
What to do: Replace it with a current certificate.
[ACD-TLS-HOSTNAME]
Cause: The certificate does not cover the first entry of manager_hostnames.
What to do: Reissue it for that name, or for a wildcard covering it.
[ACD-INVENTORY-NO-SERVER]
Cause: The inventory lists no server machine.
What to do: List at least one; the first server is the control machine.
[ACD-INVENTORY-STRAY-DIRECTOR]
Cause: A machine is listed under [director].
What to do: Leave the [director] section empty. List CDN Directors in external_directors in Site Settings.
[ACD-INVENTORY-SERVER-AND-AGENT]
Cause: A machine is listed under both server and agent.
What to do: List it under only one.
[ACD-INVENTORY-EVEN-SERVERS]
Cause: The inventory lists an even number of servers.
What to do: List an odd number of servers; move the rest under agent.
[ACD-INVENTORY-DUPLICATE-HOSTNAME]
Cause: Two machines share a hostname, ignoring case.
What to do: Give each machine a unique hostname.
[ACD-INVENTORY-ROLE-CHANGED]
Cause: A machine runs k3s as a server but is listed as an agent, or the other way round.
What to do: List it under its current role, or follow Changing a Machine’s Role.
[ACD-NODE-OS]
Cause: A machine does not run Enterprise Linux 9.4 or a later 9 release.
What to do: Update or reinstall it, or remove it from the inventory.
[ACD-NODE-KERNEL-MODULES]
Cause: A machine lacks kernel modules k3s and its storage need.
What to do: Install the kernel modules package for the running kernel.
[ACD-NODE-RESOURCES]
Cause: A machine has fewer processor cores or less memory than its role needs.
What to do: Add cores or memory, or make it an agent.
[ACD-VALUES-EDITED]
Cause: values.yaml was edited by hand.
What to do: Move the changes to values.override.yaml or group_vars/all.yaml, then delete values.yaml.
[ACD-CLUSTER-API-DOWN]
Cause: k3s is installed, but no server’s k3s API answers.
What to do: Get k3s answering on one server. If you are sure the check is wrong, run deploy --skip-tags preflight.
[ACD-CLUSTER-NODE-RENAMED]
Cause: A machine’s hostname changed after it joined the cluster.
What to do: Set the hostname back to the name in the message.
[ACD-CLUSTER-NODE-ADDRESS]
Cause: A machine’s cluster address differs from the one the settings choose.
What to do: Set the address settings back to what the cluster runs with; see Locked at First Install.
[ACD-CLUSTER-POD-CIDR]
Cause: A machine’s pod range lies outside cluster_pod_cidr.
What to do: Set cluster_pod_cidr back to what the cluster runs with.
[ACD-CLUSTER-SERVICE-CIDR]
Cause: The cluster’s service range differs from cluster_svc_cidr.
What to do: Set cluster_svc_cidr back to what the cluster runs with.
[ACD-HEALTH-NODE-NOT-READY]
Cause: A cluster machine is not Ready.
What to do: Check systemctl status k3s or k3s-agent on it, or remove it from the cluster.
[ACD-HEALTH-LIST-FAILED]
Cause: The databases or volumes could not be listed.
What to do: Check the cluster API answers with k3s kubectl get nodes.
[ACD-HEALTH-DATABASE]
Cause: A PostgreSQL database cluster is not healthy: it is still being built, is switching its primary, or more than one of its instances is not ready. One instance being rebuilt is accepted, and so is one instance per machine the run is bringing back after a role change.
What to do: Inspect it with the kubectl describe command in the message.
[ACD-HEALTH-VOLUME-FAULTED]
Cause: A storage volume has no replica with usable data, and not every replica is on a machine the run joins back.
What to do: Recover it from the Longhorn UI or a backup.
[ACD-CIDR-OVERLAP]
Cause: cluster_pod_cidr and cluster_svc_cidr overlap.
What to do: Choose two separate ranges.
[ACD-BACKPLANE-SPLIT]
Cause: The machines’ cluster addresses are not all on one network, so they could not reach each other. Either a machine’s address is outside backplane_network, or, without one, a machine using a second interface for cluster traffic has an address on a different network from the others. Nothing was changed.
What to do: The message names the machine and the network. Put every machine’s cluster address inside backplane_network (correct node_ip in inventory.ini if needed), or set backplane_network to the network the machines actually share.
[ACD-CIDR-IN-USE]
Cause: A machine already uses an address or route in cluster_pod_cidr or cluster_svc_cidr.
What to do: Choose ranges no machine uses.
[ACD-ROUTING-CONFLICT]
Cause: A routing rule sends cluster traffic away from the main routing table.
What to do: Follow the message: give the conflicting rules a priority number above 50, or with manage_routes: false, set it to true or add your own lookup main rules ahead of them.
[ACD-FIREWALL-SOURCE-CONFLICT]
Cause: Another firewalld zone on a machine already holds an address range that overlaps one deploy needs to trust: the pod or service network, backplane_network, or another machine’s address. firewalld allows a range in only one zone. Nothing was changed.
What to do: Run the commands the message shows: they remove the overlapping range from the other zone. If the zone it names is deploy’s own zone under an earlier name, set firewall_cluster_zone back instead: it is locked at first install. Or set manage_firewall: false.
[ACD-FIREWALL-NAME-INVALID]
Cause: firewall_cluster_zone or firewall_public_policy is the name of one of firewalld’s own zones or policies (such as public, trusted or allow-host-ipv6), is empty or not a string, is longer than firewalld allows (17 characters for the zone, 18 for the policy), has characters other than letters, digits, - and _, or both are the same. Nothing was changed.
What to do: Choose other names in group_vars/all.yaml.
[ACD-FIREWALL-NAME-IN-USE]
Cause: A machine already has a firewalld zone or policy named like firewall_cluster_zone or firewall_public_policy, and it holds rules deploy did not create. deploy would delete those rules, so it stops instead. Nothing was changed.
What to do: Choose names in group_vars/all.yaml that nothing on the machines uses, or empty that zone or policy yourself.
[ACD-FIREWALL-HANDOVER-STOPPED]
Cause: With manage_firewall: false, deploy needs to empty its own zone and policy, but firewalld is stopped. Rules saved in a stopped firewalld would come back when it starts. If this appears after the network changes, firewalld stopped during the run, and nothing was installed on the cluster.
What to do: Start firewalld (systemctl enable --now firewalld) and run deploy again. Or run the commands the message shows, then run deploy again.
[ACD-FIREWALL-CLOSED]
Cause: With manage_firewall: false, a machine’s firewall blocks something the cluster needs: a required port is closed, or the pod or service network is not in a zone that accepts all traffic. Nothing was changed.
What to do: Run the firewall-cmd commands the message shows. See Managing the Firewall Yourself. Or set manage_firewall: true.
[ACD-BACKUP-FOLDER-CREATE]
Cause: The folder for this run’s backup could not be created.
What to do: Check that the backup folder’s parent exists and is writable, or set backup_dir in group_vars/all.yaml.
[ACD-BACKUP-FOLDER-EXISTS]
Cause: Another run started in the same second and owns the backup folder.
What to do: Run deploy again; nothing was changed.
[ACD-BACKUP-SNAPSHOT]
Cause: The etcd snapshot for the backup was not found, or more than one was.
What to do: Check the snapshot folder and the k3s log (journalctl -u k3s) on the machine the message names, then run deploy again.
[ACD-NM-PENDING-EDITS]
Cause: A network connection deploy needs to change has edits saved in NetworkManager that are not active yet. Applying deploy’s change would activate them too. Nothing was changed on that machine.
What to do: Check the pending edits with nmcli connection show <uuid>. Either undo them with nmcli connection modify, or activate them in a maintenance window with nmcli connection up <uuid>. Then run deploy again.
[ACD-CLUSTER-ZONE-MISSING]
Cause: A machine already running k3s has no acd-cluster zone or no acd-public policy (or the names you set). Creating them needs a firewalld reload, which would remove the rules k3s depends on, so deploy will not do it on a running machine. Nothing was changed.
What to do: If you changed firewall_cluster_zone or firewall_public_policy, set them back: they are locked at first install. Otherwise, in a maintenance window, one machine at a time, create them (firewall-cmd --permanent --new-zone=acd-cluster, --zone=acd-cluster --set-target=ACCEPT, --new-policy=acd-public, --policy=acd-public --add-ingress-zone=public --add-egress-zone=HOST), run firewall-cmd --reload, and restart k3s. Then run deploy again.
[ACD-NODE-UNREACHABLE]
Cause: A machine stopped answering over SSH while its host network changed, one machine at a time on a running cluster, so the machines after it were left as they were. The change itself may have cut it off.
What to do: Check that machine’s network from its console, then run deploy again.
[ACD-VERIFY-ZONE]
Cause: After changing a machine’s network, an interface is not in the firewalld zone deploy planned for it. Nothing was installed on the cluster.
What to do: Run deploy again. If the message comes back, send it to support.
[ACD-VERIFY-FIREWALL]
Cause: After changing a machine’s network, its firewall still blocks something the cluster needs, or another zone holds an address range deploy needs to trust. Nothing was installed on the cluster.
What to do: Run the firewall-cmd commands the message shows, then run deploy again. If the message comes back, send it to support.
[ACD-VERIFY-LEFTOVER]
Cause: After changing a machine’s network, deploy’s own zone or policy holds a rule it did not create, or is missing one it planned. Nothing was installed on the cluster.
What to do: Run deploy again, which removes the extra rule. Keep your own rules in other zones. If the message names a zone target or priority, run firewall-cmd --reload in a maintenance window and restart k3s (or k3s-agent) on that machine, then run deploy again. If it comes back otherwise, send it to support.
[ACD-VERIFY-ROUTING]
Cause: After changing a machine’s network, its routing rules are not what deploy planned: a cluster rule is missing, a removed rule is still active, a rule was added by hand with ip rule add, or cluster traffic uses a routing table other than main. Nothing was installed on the cluster.
What to do: Run the ip rule del commands the message shows, check with ip rule show, then run deploy again. If the message comes back, send it to support.
[ACD-VERIFY-SYSCTL]
Cause: After the network changes, a kernel network setting on a machine has another value than the deployment set.
What to do: Find what sets it (a file under /etc/sysctl.d, or a tuned profile), then run deploy again. No k3s step ran.
[ACD-JOIN-UNREACHABLE]
Cause: A joining machine cannot reach the server it joins through.
What to do: Check the firewall on both machines, that their node_ip addresses are on one network, and that reverse path filtering (net.ipv4.conf.all.rp_filter) does not drop the replies; see the message.
[ACD-GATE-DATABASE-UNHEALTHY]
Cause: After the install, a PostgreSQL database cluster did not become healthy in time. This message is followed by [ACD-GATE-FAILED], which writes the support bundle.
What to do: Wait until kubectl get clusters.postgresql.cnpg.io -A shows Cluster in healthy state, then run deploy again.
[ACD-GATE-UNHEALTHY-PODS]
Cause: After the install, pods in a critical namespace (default, kube-system or longhorn-system) were still not running after 10 minutes. The message lists each such pod with its namespace. It is followed by [ACD-GATE-FAILED], which writes the support bundle.
What to do: On a server, find the cause, fix it, then run deploy again. How depends on the pod’s namespace:
default: the platform’s own pods, whichacd-admincovers. Most pods belong to an admin target, whose podsacd-admin status <target>lists with their last termination and recent warnings. Find the pod’s target, then readacd-admin logs <target>. For a pod that belongs to no target, such as the Kafka operator’s, use thekubectlcommands in the next item.kube-systemandlonghorn-system: the cluster’s own components and the storage layer, whichacd-admindoes not cover. Use the commands in the message:kubectl describe pod -n <namespace> <pod>andkubectl logs -n <namespace> <pod>.
[ACD-GATE-RELEASE-STATUS]
Cause: After the install, the Helm release is not deployed. This message is followed by [ACD-GATE-FAILED], which writes the support bundle.
What to do: Find the cause with the helm status command in the message and acd-admin status, fix it, then run deploy again.
[ACD-GATE-FAILED]
Cause: The final health check failed. It follows the message that names the failing check.
What to do: Read that earlier message, and send /etc/acd/deploy/support-bundle-acd.tar.gz to support.
Getting Help
When the final health check fails ([ACD-GATE-FAILED]), deploy writes /etc/acd/deploy/support-bundle-acd.tar.gz, with the run’s log, its decision record and what it read from each machine. Send it to AgileTV support.
When a run stops earlier, send support these files from /etc/acd/deploy instead:
deploy.logdecision-record.json- the
probe/folder
5.12 - Next Steps
Overview
With the cluster installed, finish these tasks before you let users on to the system. They are done in the web interfaces, not with deploy.
Every interface is reached under the main manager hostname, the first entry of manager_hostnames. Signing in under any other name, a machine’s IP address included, fails.
| Interface | Address |
|---|---|
| GUI | https://<manager hostname>/gui/ |
| Zitadel Console (users and roles) | https://<manager hostname>/ui/console |
| Grafana (dashboards) | https://<manager hostname>/grafana/ |
| API | https://<manager hostname>/api/v1/ |
Step 1: Sign In to the Zitadel Console
Zitadel is the manager’s identity service: every user account and role lives there.
- Open
https://<manager hostname>/ui/console. - Sign in with the default administrator: login name
admin@zitadel.<manager hostname>, passwordPassword1!. The console also accepts the account’s email address,admin@agiletv.dev. - If you are asked to set up multi-factor authentication (MFA), skip it. MFA is not supported in this release, and setting it up can lock you out of the account.

Step 2: Replace the Default Administrator
The default credentials are published, so replace them before the system is reachable by anyone else.
- Create your own administrator account, as in Step 4, and give it the administrator roles. See Zitadel’s administrator documentation for the roles.
- Sign out, and sign in with the new account to check that it works.
- Disable or delete the default
adminaccount.
Step 3: Configure SMTP (Recommended)
Zitadel sends email to verify addresses and reset passwords. Without an SMTP server, those features do not work.
- In the Zitadel Console, open Settings > Default Settings.
- Enter your mail server’s host, port (typically 587 or 465), user name and password, and the sender address, such as
noreply@example.com. - Save.
Step 4: Add Users
In the Zitadel Console, open Users > Add User.
Enter a username, first and last name, and email address.
Known issue: The username must match the local part of the email address, the part before the
@. Forfoo@example.com, the username must befoo. Otherwise Zitadel can block sign-in with the full email address.Without SMTP, tick Email Verified and set an initial password. Otherwise the account is not enabled.
Select Create User, then give the user their username, temporary password, and the console address. They are asked to change the password at first sign-in.
Assign the user’s roles. For Grafana’s roles, see Grafana Authentication.
Users sign in to the GUI with their login name, <username>@zitadel.<manager hostname>, not their email address. For more on managing users, see Zitadel’s user management documentation.
Step 5: Open Grafana
Open https://<manager hostname>/grafana/ and select Sign in with Zitadel. The dashboards are installed with it. See Metrics and Monitoring.
Step 6: Connect Your CDN Directors
Each CDN Director in external_directors is configured through the manager at https://<manager hostname>/confd/<name>/. Each one is known to Grafana as <name>.external, for example director-1.external. Connecting a Director to the manager is described in the CDN Director documentation.
Make sure your network’s firewall lets the Directors reach Kafka on every machine, and nothing else does: see Kafka Ports.
Step 7: Record Your Installation
Keep a record, off the cluster, of:
- The Zitadel master key, from Step 11 of the Quickstart.
- Your TLS certificate and key, and its expiry date.
- Your MaxMind databases.
- A copy of
/etc/acd/deploy. - The machines, their addresses, and the manager hostnames.
Further Reading
- Configuration Guide: every application setting.
- Operations Guide: day-to-day procedures.
- Performance Tuning Guide: topology labels, resources and database tuning.
- API Guide: the REST API.
6 - Configuration Guide
Overview
The CDN Manager is the Helm release cdn-platform, configured by Helm values. The release is installed and updated only by deploy, which builds the values from three layers:
- The preset on the installation ISO:
values-production.yamlorvalues-lab.yaml, chosen withdeploy init. - Your overrides in
/etc/acd/deploy/values.override.yaml: only the settings you want to change. - Derived settings worked out from your site settings in
/etc/acd/deploy/group_vars/all.yamland thecerts/folder. These always win.
deploy merges them into /etc/acd/deploy/values.yaml, which is what the cluster receives. See Application Settings.
This guide is the reference for the Helm values.
Applying Configuration
To change a value:
Write the setting in
/etc/acd/deploy/values.override.yaml, with the same nesting as below. For example:cdn-manager: manager: logLevel: debugRun
deploy:/mnt/esb3027/deploy
Do not run helm install, helm upgrade or helm rollback yourself: the next deploy replaces the release with what the configuration says. To undo a change, revert it in values.override.yaml and run deploy again.
helm list -A shows six more releases, which the cluster itself runs on. They come from the installation ISO and k3s, not from your values, so leave them alone too:
| Release | What it is |
|---|---|
acd-cluster | The PostgreSQL database cluster |
cloudnative-pg | The operator that runs the PostgreSQL cluster |
longhorn | Persistent storage |
descheduler | Moves pods to even out the load on the nodes |
traefik, traefik-crd | The ingress controller, part of k3s |
Set through site settings, not values: the manager hostnames (
global.hosts.managerand Zitadel’sExternalDomain), the CDN Directors (global.hosts.routers, whenexternal_directorsis not empty) and the TLS certificate secret are derived bydeploy, and override anything invalues.override.yaml. Set them withmanager_hostnames,external_directorsand thecerts/folder.
Reference Files
The installation ISO provides the presets and a complete reference at /mnt/esb3027/:
| File | Purpose |
|---|---|
values-production.yaml | The production preset, for clusters of three or more machines |
values-lab.yaml | The lab preset, for a single machine |
values.yaml | Every configurable option with its default and inline comments |
Hardware requirements: For per-node hardware specifications, refer to the System Requirements Guide. The System Requirements Guide is the authoritative source — the hardware comments in the header of
values-production.yamlmay not reflect the current requirements.
Configuration Reference
Global Settings
The global section contains cluster-wide settings. The most critical configuration is global.hosts.
global:
hosts:
manager:
- host: manager.local
routers:
- name: default
address: 127.0.0.1
edns_proxy: []
geoip: []
| Key | Type | Description |
|---|---|---|
global.hosts.manager | Array | External IP addresses or DNS hostnames for all Manager cluster nodes |
global.hosts.routers | Array | ESB3024 CDN Director instances |
global.hosts.edns_proxy | Array | EDNS Proxy addresses (currently unused) |
global.hosts.geoip | Array | GeoIP Proxy addresses for Frontend GUI |
Important: deploy sets global.hosts.manager, Zitadel’s ExternalDomain and global.hosts.routers from manager_hostnames and external_directors in Site Settings. Do not set them in values.override.yaml.
Manager Configuration
Core Manager API server settings:
| Key | Type | Default | Description |
|---|---|---|---|
cdn-manager.manager.image.registry | String | ghcr.io | Container image registry |
cdn-manager.manager.image.repository | String | edgeware/acd-manager | Container image repository |
cdn-manager.manager.image.tag | String | Image tag override (uses latest if empty) | |
cdn-manager.manager.logLevel | String | info | Log level (trace, debug, info, warn, error) |
cdn-manager.manager.replicaCount | Number | 1 | Number of replicas (HPA manages this when enabled) |
cdn-manager.manager.containerPorts.http | Number | 80 | HTTP container port |
cdn-manager.manager.maxmindDbVolume | String | Name of PVC containing MaxMind GeoIP databases |
Manager Resources
The chart supports both resource presets and explicit resource specifications:
| Key | Type | Default | Description |
|---|---|---|---|
cdn-manager.manager.resourcesPreset | String | `` (empty) | Resource preset (see Resource Presets table). Ignored if cdn-manager.manager.resources is set. |
cdn-manager.manager.resources.requests.cpu | String | 300m | CPU request |
cdn-manager.manager.resources.requests.memory | String | 512Mi | Memory request |
cdn-manager.manager.resources.limits.cpu | String | 1 | CPU limit |
cdn-manager.manager.resources.limits.memory | String | 4Gi | Memory limit |
Note: For production workloads, explicitly set cdn-manager.manager.resources rather than using presets.
Manager Datastore
cdn-manager:
manager:
datastore:
type: redis
namespace: "cdn_manager_ds"
default_ttl: ""
compression: zstd
| Key | Type | Default | Description |
|---|---|---|---|
cdn-manager.manager.datastore.type | String | redis | Datastore backend type |
cdn-manager.manager.datastore.namespace | String | cdn_manager_ds | Redis namespace for manager data |
cdn-manager.manager.datastore.default_ttl | String | `` (empty) | Default TTL for entries |
cdn-manager.manager.datastore.compression | String | zstd | Compression algorithm (none, zstd, etc.) |
Manager Database
The manager keeps its own data, including the subnet map, in a manager database in the PostgreSQL cluster. The manager creates the database if it is absent.
cdn-manager:
manager:
database:
name: manager
host: acd-cluster-postgresql-rw
port: 5432
user: app
provision:
enabled: true
| Key | Type | Default | Description |
|---|---|---|---|
cdn-manager.manager.database.name | String | manager | Database name |
cdn-manager.manager.database.host | String | acd-cluster-postgresql-rw | PostgreSQL read-write service |
cdn-manager.manager.database.port | Number | 5432 | PostgreSQL port |
cdn-manager.manager.database.user | String | app | Role the manager connects as |
cdn-manager.manager.database.provision.enabled | Boolean | true | Create the database when it is absent. Set to false when the manager uses an external database that you create yourself |
Manager Discovery
cdn-manager:
manager:
discovery: []
# Example:
# - namespace: "other"
# hosts:
# - other-host1
# - other-host2
# pattern: "other-.*"
| Key | Type | Description |
|---|---|---|
cdn-manager.manager.discovery | Array | Array of discovery host configurations. Each entry can specify hosts (list of hostnames), pattern (regex pattern), or both |
Manager Tuning
cdn-manager:
manager:
tuning:
enable_cache_control: true
cache_control_max_age: "5m"
cache_control_miss_max_age: ""
subnet:
mutation_lock:
wait: ""
postgres:
retry_min: ""
retry_max: ""
acquire_timeout: ""
pool_size: ""
| Key | Type | Default | Description |
|---|---|---|---|
cdn-manager.manager.tuning.enable_cache_control | Boolean | true | Enable cache control headers in responses |
cdn-manager.manager.tuning.cache_control_max_age | String | 5m | Maximum age for cache control headers |
cdn-manager.manager.tuning.cache_control_miss_max_age | String | `` (empty) | Maximum age for cache control headers on cache misses |
cdn-manager.manager.tuning.subnet.mutation_lock.wait | String | `` (empty) | How long a subnet change waits for another to finish before reporting 503. Empty uses the default of 2s |
cdn-manager.manager.tuning.postgres.retry_min | String | `` (empty) | First pause before retrying a failed connection to the manager’s database. Each pause doubles, up to retry_max. Empty uses the default of 100ms |
cdn-manager.manager.tuning.postgres.retry_max | String | `` (empty) | Longest pause between those retries. Empty uses the default of 5s |
cdn-manager.manager.tuning.postgres.acquire_timeout | String | `` (empty) | How long a request waits for a database connection before reporting 503. Empty uses the default of 2s |
cdn-manager.manager.tuning.postgres.pool_size | String | `` (empty) | Most database connections each manager pod opens. Raise it if requests report 503 while waiting for a database connection. Empty uses the default of 5 |
Manager Container Arguments
cdn-manager:
manager:
args:
- --config-file=/etc/manager/config.toml
- http-server
Gateway Configuration
NGinx Gateway settings:
| Key | Type | Default | Description |
|---|---|---|---|
cdn-manager.gateway.replicaCount | Number | 1 | Number of gateway replicas |
cdn-manager.gateway.resources.requests.cpu | String | 100m | CPU request |
cdn-manager.gateway.resources.requests.memory | String | 128Mi | Memory request |
cdn-manager.gateway.resources.limits.cpu | String | 150m | CPU limit |
cdn-manager.gateway.resources.limits.memory | String | 192Mi | Memory limit |
cdn-manager.gateway.service.type | String | ClusterIP | Service type |
MIB Frontend Configuration
Web-based configuration GUI settings:
| Key | Type | Default | Description |
|---|---|---|---|
cdn-manager.mibFrontend.enabled | Boolean | true | Enable the frontend GUI |
cdn-manager.mibFrontend.resourcesPreset | String | nano | Resource preset |
cdn-manager.mibFrontend.autoscaling.hpa.enabled | Boolean | true | Enable HPA |
cdn-manager.mibFrontend.autoscaling.hpa.minReplicas | Number | 2 | Minimum replicas |
cdn-manager.mibFrontend.autoscaling.hpa.maxReplicas | Number | 4 | Maximum replicas |
Confd Configuration
Confd settings for configuration management:
| Key | Type | Default | Description |
|---|---|---|---|
confd.enabled | Boolean | true | Enable Confd |
confd.service.ports.internal | Number | 15000 | Internal service port |
VictoriaMetrics Configuration
Time-series database for metrics:
| Key | Type | Default | Description |
|---|---|---|---|
acd-metrics.enabled | Boolean | true | Enable metrics components |
acd-metrics.victoria-metrics-single.enabled | Boolean | true | Enable VictoriaMetrics |
acd-metrics.grafana.enabled | Boolean | true | Enable Grafana |
acd-metrics.telegraf.enabled | Boolean | true | Enable Telegraf |
acd-metrics.prometheus.enabled | Boolean | true | Enable Prometheus metrics |
Ingress Configuration
Traffic exposure settings:
| Key | Type | Default | Description |
|---|---|---|---|
cdn-manager.ingress.enabled | Boolean | true | Enable ingress record generation |
cdn-manager.ingress.pathType | String | Prefix | Ingress path type |
cdn-manager.ingress.hostname | String | `` (empty) | Primary hostname (defaults to manager.local via global.hosts) |
cdn-manager.ingress.path | String | /api | Default path for ingress |
cdn-manager.ingress.tls | Boolean | false | Enable TLS configuration |
cdn-manager.ingress.selfSigned | Boolean | false | Generate self-signed certificate via Helm |
cdn-manager.ingress.secrets | Array | Custom TLS certificate secrets |
Ingress Extra Paths
The chart includes default extra paths for Confd and GeoIP:
cdn-manager:
ingress:
extraPaths:
- path: /confd
pathType: Prefix
backend:
service:
name: cdn-platform-cdn-manager-gateway
port:
name: http
- path: /geoip
pathType: Prefix
backend:
service:
name: cdn-platform-cdn-manager-gateway
port:
name: http
TLS Certificate Secrets
Note: With
deploy, put the certificate and key in/etc/acd/deploy/certs/instead:deploycreates the secret and wires it to every ingress. See TLS Certificates. The settings below are for reference.
For production TLS certificates:
cdn-manager:
ingress:
secrets:
- name: manager.local-tls
key: |-
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
certificate: |-
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
tls: true
Resource Configuration
Resource Presets
Predefined resource configurations for common deployment sizes:
| Preset | Request CPU | Request Memory | Limit CPU | Limit Memory | Ephemeral Storage Limit |
|---|---|---|---|---|---|
nano | 100m | 128Mi | 150m | 192Mi | 2Gi |
micro | 250m | 256Mi | 375m | 384Mi | 2Gi |
small | 500m | 512Mi | 750m | 768Mi | 2Gi |
medium | 500m | 1024Mi | 750m | 1536Mi | 2Gi |
large | 1000m | 2048Mi | 1500m | 3072Mi | 2Gi |
xlarge | 1000m | 3072Mi | 3000m | 6144Mi | 2Gi |
2xlarge | 1000m | 3072Mi | 6000m | 12288Mi | 2Gi |
Note: Limits are calculated as requests plus 50% (except for xlarge/2xlarge and ephemeral-storage).
Custom Resources
Override preset with custom values:
cdn-manager:
manager:
resources:
requests:
cpu: "300m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
Note:
- CPU values use millicores (1000m = 1 core)
- Memory values use binary SI units (1024Mi = 1GiB)
- Requests represent minimum guaranteed resources
- Limits represent maximum consumable resources
Capacity Planning
When sizing resources:
- Requests determine scheduling (node must have available capacity)
- Limits prevent resource starvation
- Maintain 20-30% cluster headroom for scaling
- Total capacity = sum of all requests × replica count + headroom
Security Contexts
Pod Security Context
cdn-manager:
manager:
podSecurityContext:
enabled: true
fsGroup: 1001
fsGroupChangePolicy: Always
sysctls: []
supplementalGroups: []
Container Security Context
cdn-manager:
manager:
containerSecurityContext:
enabled: true
runAsUser: 1001
runAsGroup: 1001
runAsNonRoot: true
readOnlyRootFilesystem: true
privileged: false
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: "RuntimeDefault"
Health Probes
Probe Types
| Probe | Purpose | Failure Action |
|---|---|---|
startupProbe | Initial startup verification | Container restart |
readinessProbe | Traffic readiness check | Remove from load balancer |
livenessProbe | Health monitoring | Container restart |
Default Probe Configuration
Liveness Probe
cdn-manager:
manager:
livenessProbe:
enabled: true
initialDelaySeconds: 5
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 5
successThreshold: 1
httpGet:
path: /api/v1/health/alive
port: http
Readiness Probe
cdn-manager:
manager:
readinessProbe:
enabled: true
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 7
failureThreshold: 3
successThreshold: 1
httpGet:
path: /api/v1/health/ready
port: http
Startup Probe
cdn-manager:
manager:
startupProbe:
enabled: true
initialDelaySeconds: 0
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 10
successThreshold: 1
httpGet:
path: /api/v1/health/alive
port: http
Autoscaling Configuration
Horizontal Pod Autoscaler (HPA)
cdn-manager:
manager:
autoscaling:
hpa:
enabled: true
minReplicas: 3
maxReplicas: 8
targetCPU: 50
targetMemory: 80
| Key | Type | Default | Description |
|---|---|---|---|
cdn-manager.manager.autoscaling.hpa.enabled | Boolean | true | Enable HPA |
cdn-manager.manager.autoscaling.hpa.minReplicas | Number | 3 | Minimum number of replicas |
cdn-manager.manager.autoscaling.hpa.maxReplicas | Number | 8 | Maximum number of replicas |
cdn-manager.manager.autoscaling.hpa.targetCPU | Number | 50 | Target CPU utilization percentage |
cdn-manager.manager.autoscaling.hpa.targetMemory | Number | 80 | Target Memory utilization percentage |
Network Policy
cdn-manager:
networkPolicy:
enabled: true
allowExternal: true
allowExternalEgress: true
addExternalClientAccess: true
| Key | Type | Default | Description |
|---|---|---|---|
cdn-manager.networkPolicy.enabled | Boolean | true | Enable NetworkPolicy |
cdn-manager.networkPolicy.allowExternal | Boolean | true | Allow connections from any source (don’t require pod label) |
cdn-manager.networkPolicy.allowExternalEgress | Boolean | true | Allow pod to access any range of port and destinations |
cdn-manager.networkPolicy.addExternalClientAccess | Boolean | true | Allow access from pods with client label set to “true” |
Pod Affinity and Anti-Affinity
cdn-manager:
manager:
podAffinityPreset: ""
podAntiAffinityPreset: soft
nodeAffinityPreset:
type: ""
key: ""
values: []
affinity: {}
| Key | Type | Default | Description |
|---|---|---|---|
cdn-manager.manager.podAffinityPreset | String | `` (empty) | Pod affinity preset (soft or hard). Ignored if affinity is set |
cdn-manager.manager.podAntiAffinityPreset | String | soft | Pod anti-affinity preset (soft or hard). Ignored if affinity is set |
cdn-manager.manager.nodeAffinityPreset.type | String | `` (empty) | Node affinity preset type (soft or hard) |
cdn-manager.manager.affinity | Object | {} | Custom affinity rules (overrides presets) |
Service Configuration
cdn-manager:
service:
type: ClusterIP
ports:
http: 80
annotations:
service.kubernetes.io/topology-mode: Auto
externalTrafficPolicy: Cluster
sessionAffinity: None
| Key | Type | Default | Description |
|---|---|---|---|
service.type | String | ClusterIP | Service type |
service.ports.http | Number | 80 | HTTP service port |
service.annotations | Object | service.kubernetes.io/topology-mode: Auto | Service annotations |
service.externalTrafficPolicy | String | Cluster | External traffic policy |
Persistence Configuration
Warning: Set volume sizes before the first
deploy. Helm cannot change the size of a persistent volume claim that already exists, so changing asizeorpersistencesetting invalues.override.yamlon an installed cluster does not resize the volume, and can make the upgrade fail. This applies to every component with storage, such as Kafka, VictoriaMetrics, PostgreSQL and Redis. Raising a retention setting also uses more disk, so check the volume has room first.
cdn-manager:
persistence:
enabled: false
mountPath: /agiletv/manager/data
storageClass: ""
accessModes:
- ReadWriteOnce
size: 8Gi
| Key | Type | Default | Description |
|---|---|---|---|
persistence.enabled | Boolean | false | Enable persistence using PVC |
persistence.mountPath | String | /agiletv/manager/data | Mount path |
persistence.storageClass | String | `` (empty) | Storage class (uses cluster default if empty) |
persistence.size | String | 8Gi | Size of data volume |
RBAC and Service Account
cdn-manager:
rbac:
create: false
rules: []
serviceAccount:
create: true
name: ""
automountServiceAccountToken: true
annotations: {}
Metrics
cdn-manager:
metrics:
enabled: false
serviceMonitor:
enabled: false
namespace: ""
annotations: {}
labels: {}
interval: ""
scrapeTimeout: ""
| Key | Type | Default | Description |
|---|---|---|---|
metrics.enabled | Boolean | false | Enable Prometheus metrics export |
metrics.serviceMonitor.enabled | Boolean | false | Create Prometheus Operator ServiceMonitor |
Next Steps
After configuration:
- Installation Guide - Deploy with your configuration
- Operations Guide - Day-to-day management
- Performance Tuning Guide - Optimize system performance
- Architecture Guide - Understand component relationships
7 - Performance Tuning Guide
Overview
This guide provides performance tuning recommendations for the AgileTV CDN Manager (ESB3027). While the default configuration is suitable for most deployments, certain environments may benefit from additional optimizations.
Network Topology Optimization
Topology Aware Hints
The CDN Manager uses Kubernetes Topology Aware Hints to prefer routing pods in the same zone as the source of network traffic. This reduces cross-zone latency and improves overall system responsiveness.
How It Works
When nodes are labeled with topology zones, Kubernetes automatically routes traffic to pods in the same zone when possible. This is particularly beneficial for:
- Low-latency requirements: Keeps traffic local to reduce round-trip time
- Cost optimization: Reduces cross-zone data transfer costs in cloud environments
- Load distribution: Prevents hotspots by distributing load across zones
Configuring Availability Zones
Each node must have zone and region labels applied for Topology Aware Hints to function. Set them with node_labels on each machine’s line in /etc/acd/deploy/inventory.ini, then run deploy, which applies them:
[server]
manager-1 ansible_host=192.0.2.11 node_labels='{"topology.kubernetes.io/zone": "us-east-1a", "topology.kubernetes.io/region": "us-east-1"}'
Adjust the zone and region values to match your deployment geography. Labels set this way are reapplied on every deploy, so they are kept with the rest of your configuration. See Inventory.
Verify Topology Configuration
Verify labels are applied:
kubectl get nodes --show-labels | grep topology.kubernetes.io
Verify EndpointSlices are being generated with hints:
kubectl get endpointslices
Requirements for Topology Aware Hints
For Topology Aware Hints to activate:
- Minimum Nodes: At least one node must be labeled with each zone referenced by endpoints
- Symmetry: The control plane checks for sufficient CPU capacity across zones to balance traffic
- Zone Coverage: All zones with endpoints should have at least one ready node
Integration with Pod Anti-Affinity
Topology labels complement the pod anti-affinity rules already configured in the Helm chart:
- Pod Anti-Affinity: Handles pod-to-node placement to ensure high availability
- Topology Aware Hints: Handles service-to-pod traffic routing to keep requests within the same zone
Together, these features optimize both placement and routing for improved performance.
Fallback Behavior
If zone labels are not configured, the system falls back to random load-balancing across all available pods. This is functionally correct but may result in:
- Increased cross-zone traffic
- Higher latency for some requests
- Less predictable performance characteristics
Kernel Network Tuning (sysctl)
deploy tunes the kernel’s network settings on every machine for you, for high connection rates and large numbers of concurrent connections. There is nothing to configure: it applies these values on every run, whatever the firewall settings:
| Parameter | Value | Purpose |
|---|---|---|
net.core.somaxconn | 1024 | Maximum socket listen backlog. Increases pending connection queue size. |
net.core.netdev_max_backlog | 2048 | Maximum packets queued at network device level. Helps handle burst traffic. |
net.ipv4.tcp_max_syn_backlog | 2048 | Maximum SYN requests queued. Improves handling of connection floods. |
net.ipv4.ip_local_port_range | 10240 65535 | Range of local ports for outbound connections. Expands available ephemeral ports. |
net.ipv4.tcp_tw_reuse | 1 | Allows reusing TIME_WAIT sockets. Reduces port exhaustion under high load. |
net.core.rmem_max | 8388608 | Maximum receive socket buffer size (8MB). Improves high-bandwidth transfers. |
net.core.wmem_max | 8388608 | Maximum send socket buffer size (8MB). Improves high-bandwidth transfers. |
Connection tracking (net.netfilter.nf_conntrack_*) is not in the list: k3s sets those values itself every time it starts, so values set elsewhere do not last.
Important: Do not set these parameters yourself, in
/etc/sysctl.d/, a tuned profile or anywhere else. After changing a machine’s network,deploychecks that each one holds the value above, and stops with[ACD-VERIFY-SYSCTL]before installing anything if another value is in place. If an earlier installation left a file such as/etc/sysctl.d/99-cdn-manager.conf, remove these parameters from it.
To check the values on a machine:
sysctl net.core.somaxconn net.core.netdev_max_backlog net.ipv4.tcp_max_syn_backlog \
net.ipv4.ip_local_port_range net.ipv4.tcp_tw_reuse net.core.rmem_max net.core.wmem_max
Monitoring Impact
After applying these settings, monitor:
- Connection establishment rates
- TIME_WAIT socket count:
netstat -n | grep TIME_WAIT | wc -l - Connection tracking table usage:
cat /proc/sys/net/netfilter/nf_conntrack_count - Network buffer utilization via Grafana dashboards
Resource Configuration
Horizontal Pod Autoscaler (HPA)
The default HPA configuration is tuned for production workloads. For environments with variable load, consider adjusting the scale metrics:
| Component | Default Scale Metrics | Tuning Consideration |
|---|---|---|
| Core Manager | CPU 50%, Memory 80% | Lower CPU threshold for faster scale-out |
| NGinx Gateway | CPU 75%, Memory 80% | Increase for cost optimization |
| MIB Frontend | CPU 75%, Memory 90% | Adjust based on operator concurrency |
For detailed HPA configuration, see the Architecture Guide.
Resource Requests and Limits
Ensure resource requests and limits are appropriately sized for your workload. Under-provisioned resources can cause:
- Pod evictions during high load
- Increased latency due to CPU throttling
- Slow scaling responses
Refer to the Configuration Guide for preset configurations and planning guidance.
Database Optimization
PostgreSQL
The PostgreSQL cluster is managed by the Cloudnative PG operator. For improved performance:
- Connection Pooling: The application uses connection pooling by default
- Replica Usage: Read queries can be offloaded to replicas for read-heavy workloads
- Backup Scheduling: Schedule backups during low-traffic periods to minimize I/O impact
Redis
Redis provides in-memory caching for sessions and ephemeral state:
- Memory Allocation: Ensure sufficient memory for cache hit rates
- Persistence: RDB snapshots are enabled; adjust frequency based on durability needs
Kafka
Kafka handles event streaming for selection input and metrics:
- Partition Count: Default partitions are sized for typical workloads
- Replication Factor: Production deployments use 3 replicas for fault tolerance
- Consumer Groups: The Selection Input Worker is limited to one consumer per partition
Monitoring Performance
Key Metrics to Watch
Monitor the following metrics for performance insights:
- API Response Time: Track via Grafana dashboards
- Pod CPU/Memory Usage: Identify resource bottlenecks
- Kafka Lag: Monitor consumer lag for selection input processing
- Database Connections: Watch for connection pool exhaustion
Grafana Dashboards
Pre-built dashboards are available at https://<manager-host>/grafana:
- System Health: Overall cluster and application health
- CDN Metrics: Routing and usage statistics
- Resource Utilization: CPU, memory, and network usage per component
Troubleshooting Performance Issues
High Latency
- Check pod distribution across nodes:
kubectl get pods -o wide - Verify topology labels are applied:
kubectl get nodes --show-labels - Review network latency between nodes
- Check for resource contention:
kubectl top pods
Slow Scaling
- Verify HPA is enabled:
kubectl get hpa - Check cluster capacity for scheduling new pods
- Review HPA metrics:
kubectl describe hpa cdn-platform-cdn-manager
Database Performance
- Check PostgreSQL cluster status:
kubectl get pods -l app=postgresql - Review slow query logs (if enabled)
- Monitor connection pool usage
Next Steps
After reviewing performance tuning:
- Architecture Guide - Understand component interactions
- Configuration Guide - Detailed configuration options
- Metrics & Monitoring Guide - Comprehensive monitoring setup
- Troubleshooting Guide - Resolve performance issues
8 - Operations Guide
Overview
This guide covers day-to-day operational procedures for managing the AgileTV CDN Manager (ESB3027). Topics include routine maintenance, backup procedures, log management, and common operational tasks.
Prerequisites
Before performing operations, ensure you have:
kubectlaccess to the cluster (available as root on every server)- Access to the first server, where
deployand its configuration in/etc/acd/deploylive - Appropriate RBAC permissions for administrative tasks
Cluster Access
There are two supported methods for accessing the Kubernetes cluster:
- SSH to a Server Node (Recommended for operations staff) - SSH into any Server node and run
kubectlcommands directly - Remote kubectl - Install
kubectlon your local machine and configure it to connect to the cluster remotely
Method 1: SSH to Server Node (Recommended)
The kubectl command-line tool is pre-configured on all Server nodes and can be used directly without additional setup:
# SSH to any Server node
ssh root@<server-ip>
# Run kubectl commands directly
kubectl get nodes
kubectl get pods
This method is recommended for day-to-day operations as it requires no local configuration and provides direct access to the cluster.
For routine status checks, logs, restarts and node maintenance, acd-admin on a server does the same by admin target name, without kubectl.
Method 2: Remote kubectl from Local Machine
To use kubectl from your local workstation or laptop:
Step 1: Install kubectl
Download and install kubectl for your operating system:
- Official Documentation: Install kubectl
- macOS (Homebrew):
brew install kubectl - Linux: Download from the official Kubernetes release page
- Windows: Download from the official Kubernetes release page
Step 2: Copy kubeconfig from Server Node
# Copy kubeconfig from any Server node
scp root@<server-ip>:/etc/rancher/k3s/k3s.yaml ~/.kube/config
Step 3: Update kubeconfig
Edit the kubeconfig file to point to the correct server address:
# Replace localhost with the actual server IP
# macOS/Linux:
sed -i '' 's/127.0.0.1/<server-ip>/g' ~/.kube/config # macOS
sed -i 's/127.0.0.1/<server-ip>/g' ~/.kube/config # Linux
# Or manually edit ~/.kube/config and change:
# server: https://127.0.0.1:6443
# to:
# server: https://<server-ip>:6443
Step 4: Verify connectivity
kubectl get nodes
Managing Multiple Clusters
If you manage multiple Kubernetes clusters from the same machine, you can maintain multiple kubeconfig files:
# Set KUBECONFIG environment variable to include multiple config files
export KUBECONFIG=~/.kube/config-prod:~/.kube/config-lab
# View all contexts
kubectl config get-contexts
# Switch between clusters
kubectl config use-context <context-name>
# View current context
kubectl config current-context
For more information, see the official Kubernetes documentation: Organizing Cluster Access
Helm Commands
Helm releases are managed cluster-wide. Besides cdn-platform, the cluster runs six releases of its own: see Configuration.
# List all releases
helm list -A
# View release history
helm history cdn-platform
# Get deployed values
helm get values cdn-platform -o yaml
# Get deployed manifest
helm get manifest cdn-platform
These commands are read-only. Change the release only through deploy: see Changing Application Settings.
Backup Procedures
PostgreSQL Backup
PostgreSQL is managed by the Cloudnative PG operator, which provides continuous backup capabilities.
A backup of the cluster covers all of its databases: manager (the manager’s own data, including the subnet map), app (Grafana), zitadel and confsej. The subnet map is not in Redis, so back up PostgreSQL to keep it.
# Check backup status
kubectl get backup
# Create manual backup
kubectl apply -f - <<EOF
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
name: manual-backup-$(date +%Y%m%d-%H%M%S)
spec:
cluster:
name: acd-cluster-postgresql
EOF
# List available backups
kubectl get backup -o wide
# Restore from backup (requires downtime)
# See Upgrade Guide for restore procedures
Longhorn Volume Backups
Longhorn provides snapshot and backup capabilities for persistent volumes:
# List all volumes
kubectl get volumes -n longhorn-system
# Create snapshot via Longhorn UI
# Port-forward to Longhorn UI (do not expose via ingress)
kubectl port-forward -n longhorn-system svc/longhorn-frontend 8080:80
# Access: http://localhost:8080
# WARNING: Longhorn UI grants access to sensitive storage information
# and should never be exposed through the ingress controller
Accessing Internal Services
For debugging and troubleshooting, you may need direct access to internal services.
PostgreSQL
PostgreSQL is managed by the Cloudnative PG operator. Connection details are stored in the acd-cluster-postgresql-app Secret:
# View connection details
kubectl describe secret acd-cluster-postgresql-app
# Extract individual fields
PG_HOST=$(kubectl get secret acd-cluster-postgresql-app -o jsonpath='{.data.host}' | base64 -d)
PG_USER=$(kubectl get secret acd-cluster-postgresql-app -o jsonpath='{.data.username}' | base64 -d)
PG_PASS=$(kubectl get secret acd-cluster-postgresql-app -o jsonpath='{.data.password}' | base64 -d)
PG_DB=$(kubectl get secret acd-cluster-postgresql-app -o jsonpath='{.data.dbname}' | base64 -d)
# Connect via psql
kubectl exec -it acd-cluster-postgresql-1 -- psql -U $PG_USER -d $PG_DB
Secret fields: The CNPG operator populates the following fields: username, password, host, port, dbname, uri, jdbc-uri, fqdn-uri, fqdn-jdbc-uri, pgpass.
Redis
Redis runs on port 6379 with no authentication:
# Connect via redis-cli
kubectl exec -it cdn-platform-redis-master-0 -- redis-cli
# Or connect from another pod
kubectl run redis-test --rm -it --image=redis -- redis-cli -h cdn-platform-redis-master
Kafka
Kafka’s bootstrap listens on the default port, 9092, on every machine. See Kafka Ports. To list the topics from a host with the Kafka command-line tools:
kafka-topics.sh --bootstrap-server <node-ip>:9092 --list
The selection_input topic is pre-configured for selection input events, with a single replica by design: see Data Replication.
Kubernetes Port Forwarding
For accessing internal Kubernetes services that are not exposed via ingress or services, use kubectl port-forward to create a secure tunnel from your local machine to the service.
Basic Port Forwarding
# Forward local port to a service
kubectl port-forward -n <namespace> svc/<service-name> <local-port>:<service-port>
# Example: Forward local port 8080 to Grafana (port 3000)
kubectl port-forward -n default svc/grafana 8080:3000
Note: “Local” refers to the machine where you run kubectl. This can be:
- A Server node in the cluster (common for administrative tasks)
- A remote machine with
kubectlconfigured to access the cluster
Accessing the Forwarded Service
Once the port-forward is established, access the service at http://localhost:<local-port> from the machine where you ran kubectl port-forward.
If running on a Server node: kubectl port-forward listens only on the node’s own loopback address, 127.0.0.1, so the node’s IP address does not reach it from your workstation. Carry the port over SSH instead, from your workstation:
# From your workstation: forward <local-port> to the same port on the Server node
ssh -L <local-port>:127.0.0.1:<local-port> root@<server-node>
Keep that SSH session open, and open http://localhost:<local-port> on your workstation.
For simplicity, consider running port-forward from your local machine (if kubectl is configured for remote cluster access) rather than from a Server node.
Background Port Forwarding
To run port-forward in the background:
kubectl port-forward -n <namespace> svc/<service-name> <local-port>:<service-port> &
Security Considerations
Port forwarding is recommended for:
- Administrative interfaces (e.g., Longhorn UI) that should not be publicly exposed
- Debugging and troubleshooting internal services
- Temporary access to services without modifying ingress configuration
The port-forward tunnel remains active only while the kubectl port-forward command is running. Press Ctrl+C to terminate the tunnel.
Example: The Longhorn storage UI is intentionally not exposed via ingress due to security risks. Access it via port-forward:
kubectl port-forward -n longhorn-system svc/longhorn-frontend 8080:80Then navigate to
http://localhost:8080in your browser.
Longhorn Storage
Longhorn is a distributed block storage system for Kubernetes that provides persistent volumes for stateful applications such as PostgreSQL and Kafka.
Architecture
Longhorn deploys controller and replica engines on each node, forming a distributed storage system. When a volume is created, Longhorn replicates data across multiple nodes to ensure durability even in the event of node failures.
Storage Protocols:
- iSCSI: Used for standard Read-Write-Once (RWO) volumes
- NFS: Used for Read-Write-Many (RWX) volumes that can be mounted by multiple pods simultaneously
Configuration
Each volume keeps three replicas on different nodes, the Longhorn default. The three Kafka data volumes are the exception, with one replica each, because Kafka copies its topics across its brokers itself. Longhorn tries to keep one replica on the node where the volume’s pod runs, but a pod can run on a node with no replica of its volume.
When a node is drained, its replicas stop. Volumes with three replicas stay degraded while the node is away; on a cluster with a spare node, Longhorn rebuilds the missing replica there after about 10 minutes. After a short drain, the replicas catch up within about a minute of the node’s return. Before the drain of a node that holds a Kafka volume’s only replica finishes, Longhorn copies that replica to another node.
Capacity Planning
Longhorn storage requires an additional 30% capacity headroom for internal operations and scaling. If less than 30% of the total partition capacity is available, Longhorn may mark volumes as “full” and prevent further writes.
For detailed storage requirements and disk partitioning guidance, see the System Requirements Guide.
Configuration Backup
Every run of deploy backs up each machine’s configuration, and the configuration folder /etc/acd/deploy, before it changes anything. It does not back up the data stored in the cluster: use the PostgreSQL procedure above for the databases. To take a backup without changing anything:
/mnt/esb3027/deploy backup
The backup leaves out your TLS certificate and key and the MaxMind databases: keep your own copies. A backup of a running cluster holds an etcd snapshot with every Kubernetes Secret, and the cluster token: protect and prune backups as secret material. See Backups.
Backup Schedule Recommendations
| Component | Frequency | Retention |
|---|---|---|
| PostgreSQL | Daily | 30 days |
| Longhorn Snapshots | Before changes | 7 days |
| Configuration | Before each change | Indefinite |
Updating MaxMind GeoIP Databases
The MaxMind GeoIP databases (GeoIP2-City, GeoLite2-ASN, GeoIP2-Anonymous-IP) are used for GeoIP-based routing and validation features. Update them periodically to keep IP geolocation accurate.
To update them, copy the new .mmdb files into /etc/acd/deploy/maxmind/, replacing the old ones, and run deploy. It loads the databases only when they have changed. See MaxMind GeoIP Databases.
Update Frequency Recommendations
| Database | Recommended Update Frequency |
|---|---|
| GeoIP2-City | Weekly or monthly |
| GeoLite2-ASN | Monthly |
| GeoIP2-Anonymous-IP | Weekly or monthly |
MaxMind releases database updates on a regular schedule. Subscribe to MaxMind notifications to stay informed of new releases.
Log Management
acd-admin logs <target> shows the logs of every pod of a component at once: see acd-admin logs.
Application Logs
# View manager logs
kubectl logs -l app.kubernetes.io/component=manager
# Follow logs in real-time
kubectl logs -l app.kubernetes.io/component=manager -f
# View logs from specific pod
kubectl logs <pod-name>
# View previous instance logs (after crash)
kubectl logs <pod-name> -p
# View logs with timestamps
kubectl logs <pod-name> --timestamps
# View logs from all containers in pod
kubectl logs <pod-name> --all-containers
Component-Specific Logs
# Zitadel logs
kubectl logs -l app.kubernetes.io/name=zitadel
# Gateway logs
kubectl logs -l app.kubernetes.io/component=gateway
# Confd logs
kubectl logs -l app.kubernetes.io/component=confd
# MIB Frontend logs
kubectl logs -l app.kubernetes.io/component=mib-frontend
# PostgreSQL logs
kubectl logs -l app.kubernetes.io/name=postgresql
# Kafka logs
kubectl logs -l app.kubernetes.io/name=kafka
# Redis logs
kubectl logs -l app.kubernetes.io/name=redis
Log Aggregation
Logs are collected by Telegraf and sent to VictoriaMetrics:
# Access Grafana for log visualization
# https://<manager-host>/grafana
# Query logs via Grafana Explore
# Select VictoriaMetrics datasource and use log queries
Log Rotation
Container logs are automatically rotated by Kubernetes:
- Default max size: 10MB per container
- Default max files: 5 rotated files
- Total per pod: ~50MB maximum
Scaling Operations
Manual Scaling
Note: If HPA (Horizontal Pod Autoscaler) is enabled for a deployment, manual scaling changes will be overridden by the HPA. To manually scale, you must first disable the HPA.
# Check if HPA is enabled
kubectl get hpa
# Disable HPA before manual scaling
kubectl patch hpa cdn-platform-cdn-manager -p '{"spec": {"minReplicas": null, "maxReplicas": null}}'
# Or delete the HPA entirely
kubectl delete hpa cdn-platform-cdn-manager
# Scale manager replicas
kubectl scale deployment cdn-platform-cdn-manager --replicas=3
# Scale gateway replicas
kubectl scale deployment cdn-platform-cdn-manager-gateway --replicas=2
# Scale MIB frontend replicas
kubectl scale deployment cdn-platform-cdn-manager-mib-frontend --replicas=2
HPA Configuration
# View HPA status
kubectl get hpa
# Describe HPA details
kubectl describe hpa cdn-platform-cdn-manager
# Edit HPA configuration
kubectl edit hpa cdn-platform-cdn-manager
Configuration Updates
Changing Application Settings
Application settings are changed through deploy, never with helm directly: the next deploy would undo the change.
# Write only the settings you want to change
vi /etc/acd/deploy/values.override.yaml
# Apply them
/mnt/esb3027/deploy
# Check every component
acd-admin status
See Application Settings.
Rolling Back Changes
To undo a change, revert it in values.override.yaml or group_vars/all.yaml and run deploy again. To see the release’s revisions:
helm history cdn-platform
Certificate Management
Checking Certificate Expiration
# Check TLS secret expiration
kubectl get secret <tls-secret> -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -dates
# Check via Grafana dashboard
# Certificate expiration metrics are available in Grafana
Renewing Certificates
Replace the certificate and key in /etc/acd/deploy/certs/ and run deploy. See TLS Certificates.
Health Checks
acd-admin status shows the health of every component and node in one view: see acd-admin status.
Component Health
# Check all pods
kubectl get pods
# Check specific component
kubectl get pods -l app.kubernetes.io/component=manager
# Check persistent volumes
kubectl get pvc
# Check cluster status
kubectl get nodes
# Check ingress
kubectl get ingress
API Health Endpoints
# Liveness check
curl -k https://<manager-host>/api/v1/health/alive
# Readiness check
curl -k https://<manager-host>/api/v1/health/ready
Database Health
# PostgreSQL cluster status
kubectl get clusters -n default
# Check PostgreSQL pods
kubectl get pods -l app.kubernetes.io/name=postgresql
# Kafka cluster status
kubectl get pods -l app.kubernetes.io/name=kafka
# Redis status
kubectl get pods -l app.kubernetes.io/name=redis
Maintenance Windows
Planned Maintenance
Before performing maintenance:
- Notify users of potential service impact
- Verify backups are current
- Document the maintenance procedure
- Prepare rollback plan
Node Maintenance
# Take the node out of service: cordon it and evict its pods
acd-admin drain <node-name>
# Perform maintenance
# Return the node to service
acd-admin uncordon <node-name>
Before you drain or restart anything, read Safety: drain one server at a time, check which broker holds selection_input, and wait for every admin target ok and every storage volume healthy before the next drain. Cordoning or draining the node of the PostgreSQL primary switches the primary to another instance.
A drained node keeps running its DaemonSet pods (host metrics, ingress and storage) and any acd-admin session. Everything else moves to other nodes. Longhorn’s node components keep running, but its storage replicas on the node stop: see Longhorn Storage for what that means for volumes.
acd-admin drain refuses a node that runs a pod that no controller recreates until you add --force. Ctrl-C stops only the waiting, and pods still waiting to move are not retried until drain runs again, which carries on. The node stays drained through reboots and deploy runs until acd-admin uncordon; kubectl uncordon returns the node to service but leaves its drained label. See acd-admin drain and Drained Nodes.
Uncordoning a node does not move pods back to it: it takes only new pods. After a round of drains, the last node returned can sit almost empty while the others carry its load. The descheduler gradually moves pods without persistent storage from busy nodes to underused ones. To spread a target’s pods sooner, restart it with acd-admin restart <target>, starting with the targets that have the most pods.
Cluster Upgrades
See the Upgrade Guide for cluster upgrade procedures.
Troubleshooting Quick Reference
Common Commands
# Describe problematic pod
kubectl describe pod <pod-name>
# View pod events
kubectl get events --sort-by='.lastTimestamp'
# Check resource usage
kubectl top pods
kubectl top nodes
# Exec into container
kubectl exec -it <pod-name> -- /bin/sh
# Check network policies
kubectl get networkpolicies
# Check service endpoints
kubectl get endpoints
Restarting Components
acd-admin restart <target> restarts an admin target the safe way for its kind and waits for it, including kafka and postgresql through their operators: see acd-admin restart.
Important: Never restart Kafka or PostgreSQL with the
kubectlcommands below. Their operators, Strimzi and CloudNativePG, own their pods: akubectlrestart or pod deletion bypasses the broker-by-broker roll and the PostgreSQL switchover. Useacd-admin restart kafkaandacd-admin restart postgresql.
For other workloads:
# Restart deployment
kubectl rollout restart deployment/<deployment-name>
# Restart statefulset
kubectl rollout restart statefulset/<statefulset-name>
# Delete pod (auto-recreated)
kubectl delete pod <pod-name>
Security Operations
Rotating Service Account Tokens
# Delete service account secret (auto-regenerated)
kubectl delete secret <service-account-token-secret>
# Tokens are automatically regenerated
Updating RBAC Permissions
# View current roles
kubectl get roles
kubectl get clusterroles
# View role bindings
kubectl get rolebindings
kubectl get clusterrolebindings
# Edit role
kubectl edit role <role-name>
Disaster Recovery
Pod Recovery
Pods are automatically recreated if they fail:
# Check pod status
kubectl get pods
# If pod is stuck in Terminating
kubectl delete pod <pod-name> --force --grace-period=0
# If pod is stuck in Pending, check resources
kubectl describe pod <pod-name>
kubectl get events --sort-by='.lastTimestamp'
Node Failure Recovery
When a node fails:
- Automatic: Pods are rescheduled on healthy nodes (after timeout)
- Manual: Force delete stuck pods
# Force delete pods on failed node
kubectl delete pod --all --force --grace-period=0 \
--field-selector spec.nodeName=<failed-node>
Data Recovery
For data recovery scenarios, refer to:
- PostgreSQL: Cloudnative PG backup/restore procedures
- Longhorn: Volume snapshot restoration
- Kafka: Topics other than
selection_inputsurvive the loss of one broker.selection_inputhas a single replica: see Data Replication
Routine Maintenance Checklist
Daily
- Review Grafana dashboards for anomalies
- Check alert notifications
- Verify backup completion
Weekly
- Review pod restart counts
- Check certificate expiration dates
- Review log storage usage
- Verify HPA is functioning correctly
Monthly
- Test backup restoration procedure
- Review and rotate credentials if needed
- Update documentation if configuration changed
- Review resource utilization trends
Next Steps
After mastering operations:
- Troubleshooting Guide - Deep dive into problem resolution
- Performance Tuning Guide - Optimize system performance
- Metrics & Monitoring Guide - Comprehensive monitoring setup
- API Guide - REST API reference and automation
9 - Metrics & Monitoring Guide
Overview
The CDN Manager includes a comprehensive monitoring stack based on VictoriaMetrics for time-series data storage, Telegraf for metrics collection, and Grafana for visualization. This guide describes the monitoring architecture and how to access and use the monitoring capabilities.
Quick Links
| Guide | Description |
|---|---|
| Grafana Dashboards | Using and customising the built-in and advanced Grafana dashboards |
| Grafana Authentication & Roles | Configuring Grafana authentication, roles, and permissions |
| Alerts & Alarms | Configuring and managing alerts and alarms |
Architecture
Components
| Component | Purpose |
|---|---|
| Telegraf | Metrics collector running on each node, gathering system and application metrics |
| VictoriaMetrics Agent | Metrics scraper and forwarder; scrapes Prometheus endpoints and forwards to VictoriaMetrics |
| VictoriaMetrics (Short-term) | Time-series database for operational dashboards (30-90 day retention) |
| VictoriaMetrics (Long-term) | Time-series database for billing and compliance (1+ year retention) |
| Grafana | Visualization and dashboard platform; deployed as two replicas for high availability |
| Alertmanager | Alert routing and notification management |
Metrics Flow
The following diagram illustrates how metrics flow through the monitoring stack:
flowchart TB
subgraph External["External Sources"]
Streamers[Streamers/External Clients]
end
subgraph Cluster["Kubernetes Cluster"]
Telegraf[Telegraf DaemonSet]
subgraph Applications["Application Components"]
Director[CDN Director]
Kafka[Kafka]
Redis[Redis]
Manager[ACD Manager]
Alertmanager[Alertmanager]
end
VMAgent[VictoriaMetrics Agent]
subgraph Storage["Storage"]
VMShort[VictoriaMetrics<br/>Short-term]
VMLong[VictoriaMetrics<br/>Long-term]
end
Grafana[Grafana<br/>2 replicas, HA]
PostgreSQL[(PostgreSQL)]
Zitadel[Zitadel]
end
Streamers -->|Push metrics| Telegraf
Telegraf -->|remote_write| VMShort
Telegraf -->|remote_write| VMLong
Director -->|Scrape| VMAgent
Kafka -->|Scrape| VMAgent
Redis -->|Scrape| VMAgent
Manager -->|Scrape| VMAgent
Alertmanager -->|Scrape| VMAgent
VMAgent -->|remote_write| VMShort
VMAgent -->|remote_write| VMLong
VMShort -->|Query| Grafana
VMLong -->|Query| Grafana
Grafana <-->|Shared state| PostgreSQL
Grafana -->|OAuth2 / OIDC| ZitadelMetrics Flow Summary:
External metrics ingestion:
- External clients (streamers) push metrics to Telegraf
- Telegraf forwards metrics via
remote_writeto both VictoriaMetrics instances
Internal metrics scraping:
- VictoriaMetrics Agent scrapes Prometheus endpoints from:
- CDN Director instances
- Kafka cluster
- Redis
- ACD Manager components
- Alertmanager
- VMAgent forwards scraped metrics via
remote_writeto both VictoriaMetrics instances
- VictoriaMetrics Agent scrapes Prometheus endpoints from:
Data visualization:
- Grafana queries both VictoriaMetrics databases depending on the dashboard requirements
- Operational dashboards use short-term storage
- Billing and compliance dashboards use long-term storage
Metrics Collection
Application Metrics
Applications expose metrics on Prometheus-compatible endpoints. VictoriaMetrics Agent (VMAgent) scrapes these endpoints and forwards metrics to VictoriaMetrics via remote_write.
System Metrics
Telegraf collects system-level metrics including:
- CPU usage
- Memory utilization
- Disk I/O
- Network statistics
- Process metrics
Kubernetes Metrics
Cluster metrics are collected including:
- Pod resource usage
- Node status
- Deployment status
- Persistent volume usage
Metrics Retention
VictoriaMetrics is configured with default retention policies. For custom retention settings, modify the VictoriaMetrics configuration in your values.yaml:
acd-metrics:
victoria-metrics-single:
retentionPeriod: "3" # Retention period in months
Troubleshooting
Metrics Not Appearing
If metrics are not appearing in Grafana:
Check Telegraf pods:
kubectl get pods -l app.kubernetes.io/component=telegrafCheck Telegraf logs:
kubectl logs -l app.kubernetes.io/component=telegrafVerify VictoriaMetrics is running:
kubectl get pods -l app.kubernetes.io/component=victoria-metricsCheck application metrics endpoints:
kubectl exec <pod-name> -- curl localhost:8080/metrics
For dashboard and authentication issues, see the Grafana Dashboards and Grafana Authentication & Roles guides.
Next Steps
After setting up monitoring:
- Grafana Authentication & Roles - Configure SSO and permissions before accessing Grafana
- Grafana Dashboards - Explore and customise dashboards
- Alerts & Alarms - Set up alerting and notifications
- Operations Guide - Day-to-day operational procedures
- Troubleshooting Guide - Resolve monitoring issues
- API Guide - Access metrics via API
9.1 - Grafana Authentication & Roles
Overview
Grafana authentication is delegated entirely to Zitadel via OAuth2/OIDC. Local username/password login is not available to end users. When a user logs into Grafana, they are redirected to Zitadel to authenticate, and their Grafana role is automatically determined by the Zitadel project roles assigned to their account.
The OIDC integration between Grafana and Zitadel is configured automatically at install time — no manual Zitadel application registration is required.
How It Works
During installation, an init container runs before Grafana starts and:
- Authenticates with Zitadel using a machine-account service key.
- Registers a
GrafanaOIDC application in the Zitadel project (or re-uses an existing one if already registered). - Writes the resulting
client_idandclient_secretinto a Kubernetes Secret, which Grafana picks up on startup.
This means the Grafana OIDC application in Zitadel is managed automatically and does not need to be created or modified manually.
Role Mapping
Grafana roles are mapped from Zitadel project roles using the following rule:
| Zitadel Project Role | Grafana Role |
|---|---|
grafana_admin | Admin — full access, can manage users, datasources, and dashboards |
| (any other role, or no role) | Viewer — read-only access to dashboards |
Note: There is no Grafana
Editorrole mapped by default. All authenticated users who are not explicitly grantedgrafana_adminreceive Viewer access. If you need an Editor tier, see Customising the Role Mapping.
The mapping is enforced on every login. If a user’s Zitadel role changes, the change takes effect the next time they log into Grafana.
Prerequisites
- Zitadel is configured and accessible at
https://<manager-host> - At least one Zitadel user account exists (see Next Steps — Add Users)
- Grafana is accessed via the correct DNS hostname (see Accessing Grafana)
Accessing Grafana
Grafana is accessible at:
https://<manager-host>/grafana
Important: Grafana must be accessed using the main manager hostname, the first entry of
manager_hostnamesin the site settings. Accessing Grafana via an IP address or an alternative hostname will cause OAuth2 redirect URI mismatches and CORS errors, preventing login from completing successfully.
To log in:
- Navigate to
https://<manager-host>/grafana - Click “Login with Zitadel”
- Authenticate with your Zitadel account credentials
Granting Admin Access
By default, all Zitadel users who log into Grafana receive Viewer access. To grant a user Admin access, assign them the grafana_admin project role in Zitadel.
Step 1: Ensure the grafana_admin Role Exists
- Log into the Zitadel Console at
https://<manager-host>/ui/console - Navigate to Projects and open the ZITADEL project
- Click the Roles tab
- Check whether a role named
grafana_adminalready exists - If it does not exist, click New Role and create it:
- Key:
grafana_admin - Display Name: Grafana Admin (or any label you prefer)
- Click Save
- Key:
Step 2: Assign the Role to a User
- In the Zitadel Console, navigate to Users and open the user you want to grant admin access to
- Click the Authorizations tab
- Click New Authorization
- Select the ZITADEL project
- Select the
grafana_adminrole - Click Save
The user will have Grafana Admin access the next time they log in.
Revoking Admin Access
To demote a user back to Viewer, remove the grafana_admin authorization from their account:
- In the Zitadel Console, open the user’s Authorizations tab
- Find the
grafana_adminauthorization on the ZITADEL project - Click the delete icon to remove it
The change takes effect on their next Grafana login.
Customising the Role Mapping
The role mapping expression is configured in values.yaml under grafana."grafana.ini".auth.generic_oauth.role_attribute_path. It uses JMESPath syntax evaluated against the OIDC token’s role claims.
The default expression is:
grafana:
"grafana.ini":
auth.generic_oauth:
role_attribute_path: >-
contains(keys("urn:zitadel:iam:org:project:roles"), 'grafana_admin') && 'Admin' || 'Viewer'
Example: Adding an Editor Tier
To map a grafana_editor Zitadel role to Grafana’s Editor role, create the grafana_editor role in Zitadel (following the same steps as above) and extend the expression:
grafana:
"grafana.ini":
auth.generic_oauth:
role_attribute_path: >-
contains(keys("urn:zitadel:iam:org:project:roles"), 'grafana_admin') && 'Admin'
|| contains(keys("urn:zitadel:iam:org:project:roles"), 'grafana_editor') && 'Editor'
|| 'Viewer'
Apply the change using the standard upgrade procedure in the Configuration Guide.
Blocking Unauthenticated Access
By default, role_attribute_strict is set to false, which means any authenticated Zitadel user can log into Grafana as a Viewer even if they have no explicit Grafana role assigned. To restrict Grafana access to only users who have been explicitly granted a role, set this to true:
grafana:
"grafana.ini":
auth.generic_oauth:
role_attribute_strict: true
With role_attribute_strict: true, users who do not match any role in the role_attribute_path expression will be denied access entirely.
Managing Users in Grafana
User accounts in Grafana are created automatically on first login via Zitadel. There is no need to pre-create users in the Grafana UI.
To view and manage users who have logged in:
- Log into Grafana as an Admin
- Navigate to Administration > Users and access > Users
From here you can see each user’s current role, last login time, and authentication provider. Role changes should always be made via Zitadel (as described above) rather than directly in Grafana, as they will be overwritten on the user’s next login.
Break-Glass Admin Access
A local Grafana admin account is available as a break-glass fallback for situations where Zitadel is unavailable. This account is not accessible via the standard login page (which only shows the Zitadel SSO button).
To use the local admin account, navigate directly to:
https://<manager-host>/grafana/login
The default credentials are listed in the Glossary. Change the default password immediately after installation.
Security recommendation: The break-glass account should be used only for emergency access. Do not use it for routine administration.
Troubleshooting
OAuth2 Redirect URI Mismatch / CORS Errors
Grafana is registered in Zitadel with the redirect URI https://<manager-host>/grafana/login/generic_oauth, derived from the first entry of manager_hostnames. Accessing Grafana via a different hostname or IP address will not match this URI and will cause the login to fail.
Resolution: Always access Grafana via the configured hostname. If the hostname has changed, run deploy again to re-register the application with the updated URI.
User Receives Viewer Instead of Admin
The grafana_admin role is not included in the user’s Zitadel token.
Resolution:
- Confirm the
grafana_adminrole exists on the ZITADEL project in the Zitadel Console - Confirm the role is assigned to the user under their Authorizations tab
- Ask the user to log out of Grafana and log back in — role changes are applied on the next login, not the current session
Login Fails with “Role not found” or Access Denied
role_attribute_strict may be set to true and the user has no matching Zitadel role.
Resolution: Either assign the user an appropriate Zitadel project role, or set role_attribute_strict: false in values.yaml to allow all authenticated users Viewer access.
Admin Role Assigned in Zitadel but User Still Gets Viewer
The grafana_admin role is correctly assigned to the user in Zitadel, but Grafana still grants them Viewer access. This indicates that role claims are not being included in the Zitadel userinfo response.
Grafana determines roles by calling the Zitadel userinfo endpoint (/oidc/v1/userinfo) and evaluating the urn:zitadel:iam:org:project:roles claim. Zitadel only includes this claim when the Grafana OIDC application has Access Token Role Assertions enabled. If the claim is absent, the role_attribute_path expression always falls through to 'Viewer'.
To verify and fix:
- Log into the Zitadel Console at
https://<manager-host>/ui/console - Navigate to Projects > ZITADEL > Applications > Grafana
- Open the Token Settings tab
- Ensure Access Token Role Assertions is enabled
- Save the change
The fix takes effect on the user’s next login — no Grafana or Helm changes are required.
Grafana OIDC App Not Registered in Zitadel
If the init container failed during installation, the Grafana OIDC application may not have been created in Zitadel.
Resolution: Check the init container logs for errors:
kubectl logs -l app.kubernetes.io/component=grafana --previous -c zitadel-oauth-setup
Common causes are Zitadel not being ready when the init container ran, or a machine-key permission issue. Running deploy again re-triggers the init container and attempts registration again.
Next Steps
- Grafana Dashboards - Using and customising dashboards
- Alerts & Alarms - Configure alerting and notifications
- Metrics & Monitoring Overview - Return to the monitoring overview
9.2 - Grafana Dashboards
Overview
Grafana is the primary visualization platform for the CDN Manager monitoring stack. It provides pre-built dashboards for cluster health, application performance, and billing analytics, and is accessible via the manager ingress.
Prerequisites
- Grafana is deployed and running (verify with
kubectl get pods -l app.kubernetes.io/component=grafana) - A Zitadel user account is available for login
- Grafana is accessed via the correct DNS hostname (see Grafana Authentication & Roles)
Accessing Grafana
Grafana is accessible via the manager ingress:
URL: https://<manager-host>/grafana
To log in:
- Navigate to
https://<manager-host>/grafana - Click the “Login with Zitadel” button
- Authenticate with your Zitadel account credentials
Important: Grafana must be accessed using the main manager hostname, the first entry of
manager_hostnamesin the site settings. Accessing Grafana via an IP address or an alternative hostname will cause OAuth2 redirect URI mismatches and CORS errors, preventing login from completing successfully.
For details on authentication and role configuration, see Grafana Authentication & Roles.
Standard Dashboards
Accessing Dashboards
After logging into Grafana:
- Navigate to Dashboards in the left menu
- Browse the folder structure to find the dashboard you need
- Click on a dashboard to open it
Dashboards are organised into the following folders:
- Alerting — alert state history and alerting system health
- Billing — redirect counts for billing analytics
- CDN Manager — ACD Manager API performance
- Hardware — host-level CPU, memory, disk, and network telemetry
- Infrastructure — Kubernetes cluster, Kafka, Longhorn, and Redis health
- Streaming — CDN routing, streamer performance, and QoE
- Internal Debugging — low-level CDN Director diagnostics
Alerting
Active Alarms
A live view of all currently firing alerts. Shows the alert name, severity, affected host, and description. Use this as the first stop when investigating an active incident.
Alert Statistics
Historical view of alert firing activity over time. Shows which alert groups and individual rules have been firing, with timelines and trend charts. Useful for identifying recurring or flapping alerts.
vmalert
Operational health dashboard for the vmalert component itself. Covers evaluation rate, evaluation errors, alerting and recording rule counts, remote write throughput, and resource usage. Use this to verify the alerting pipeline is functioning correctly.
Billing
Billing Dashboard
Tracks redirect volumes for billing and usage analytics. Shows initial managed and unmanaged redirects, segment redirects, and endpoint redirects — both as totals and as ratios over time. Data is sourced from long-term VictoriaMetrics storage to support historical reporting.
CDN Manager
CDN Manager API
Health and performance dashboard for the ACD Manager REST API. Covers:
- Overview: API health status, active pod count, total request volume, 5xx error rate, and average latency
- Traffic: Request rate by pod, distribution across API endpoints
- Errors: 5xx errors per endpoint, response code breakdown per endpoint, error rate by pod
- Latency: P99 and average latency by endpoint, overall API response latency
- Resources & Auth: Route validation API activity
Hardware
HW Metrics
Condensed host hardware overview covering CPU usage and load averages, memory utilisation, network interface throughput, swap usage, and root filesystem disk space. Suitable for day-to-day health checks across all cluster nodes.
An expanded HW Metrics (Advanced) dashboard is available as part of the Advanced Dashboards licence.
Infrastructure
k3s Cluster Infrastructure
Kubernetes cluster health overview using node-exporter and kube-state-metrics. Covers:
- Cluster Overview: Node count, running pod count, OOMKilled containers, and overall cluster health status
- Compute: CPU usage, memory usage, and load average per node
- Network: Inbound and outbound bytes per node
- Disk: Read/write throughput and I/O pressure per node
- Longhorn PVC Disk Usage: Usage percentage per persistent volume
- Workload Health: Pod restart counts and OOMKill occurrences
Kafka
Kafka broker health using JMX exporter metrics. Covers:
- Cluster Health: Active controller, broker state, topic and partition counts, offline and under-replicated partitions, active and fenced broker counts, metadata log lag
- Throughput: Bytes in/out and messages in by topic, replication bytes in/out
- Internals: Request handler idle percentage, network processor idle percentage
Longhorn Storage
Persistent storage health for the Longhorn distributed block storage layer. Covers:
- Overview: Total, healthy, degraded, and faulted volume counts; nodes down
- Capacity: Total cluster capacity, used, and available storage
- Volume Detail: Usage percentage per volume, actual size per volume, volume robustness state, volumes approaching capacity (>85%)
- Node & Disk: Disk usage percentage and available bytes per node, node condition checks
Redis
Redis instance health using redis-exporter metrics. Covers:
- Instance Health: Status, uptime, connected and blocked clients, slow log length, rejected connections
- Memory: Usage and fragmentation ratio
- Throughput & Keyspace: Commands processed, network I/O, keyspace hit rate, keys per database
- Evictions & Persistence: Evictions, expirations, RDB unsaved changes
- CPU & Connections: CPU usage and connection metrics
- Command Analysis: Per-command breakdown
Streaming
Extended Monitoring
The primary operational dashboard for CDN routing activity. This is the home dashboard displayed on Grafana login. Covers:
- Latency Statistics: CDN Director latency and CDN latency over time
- Redirects: Total redirect volume, status code breakdown, managed vs unmanaged ratio
- Content Popularity: Top 10 requested content and top 10 most rapidly increasing popularity scores
- CDN Selection: Redirect distribution across CDN endpoints, current and historical ratios
- CDN Failovers and Retries: Failover events and retry rates by CDN
- Host Selection: Endpoint request distribution
- Session Statistics: Active session counts and session type breakdown
- Client Responses: Client-facing HTTP status code distribution
- Incoming Requests: Raw request volume
- HTTPS Certificate Statistics: Certificate validity and expiry indicators
- Warnings & Errors: Application-level warnings and errors over time
- LUA Statistics: Lua exception counts and execution time
- Configuration Change History: Timeline of routing configuration changes
Router Monitoring
External-facing view of CDN Director routing activity. Shows the number of initial routing decisions made, HTTP status code distribution, incoming HTTP/HTTPS request volumes, and selection input metrics. Useful for a high-level view of traffic hitting the directors.
QoE Monitoring
Quality of Experience scoring dashboard. Shows average QoE scores per host, per session group, per CDN, and per agent, as well as the initial CDN selection rate. Use this to identify CDN providers or content hosts that are delivering a degraded experience.
Streamer Statistics
Condensed view of streamer node performance, covering network ingress/egress throughput, TCP and HTTP connection counts, active session counts, HTTP request rates and response codes, response times (ingress and egress), and storage/memory/CPU. Suitable for routine streamer health monitoring.
An expanded Streamer Statistics (Advanced) dashboard is available as part of the Advanced Dashboards licence.
Internal Debugging
These dashboards expose low-level CDN Director internals and are primarily intended for advanced diagnostics and support investigations.
Debugging Information
Lua runtime statistics from the CDN Director: exception counts, active Lua context count, time spent in Lua execution, and CDN Director latency. Use when investigating unexpected CDN Director behaviour or Lua errors.
ACD: Incoming Internet Connections
SSL-level connection statistics at the CDN Director: SSL warnings and errors, valid and invalid HTTP and HTTPS request counts from external clients. Use when investigating TLS handshake failures or unexpected rejection rates.
Performance Metrics
CDN Director process-level resource usage: CDN Director CPU utilisation, CDN Director memory usage, and Lua memory consumption. Useful for identifying resource pressure on the CDN Director process itself rather than the host.
Prometheus: ACD
ACD application metrics exposed via Prometheus: active and total session counts, session type breakdown, managed and unmanaged redirect counts, QoE corrections, manifest parse failures, initial endpoint request counts, HTTP request rates, and logged warnings and errors.
CDN Failures
CDN-level failure tracking: response code distribution from CDN backends, CDN-level failover events, host-level failovers, and host retry counts. Use when investigating CDN reliability issues or failover behaviour.
ACD: CDN Latencies Detail
Detailed CDN latency analysis with configurable percentile plots and a full latency histogram. Use when investigating tail latency issues on specific CDN backends.
ACD: Router Latencies
CDN Director routing latency distributions for both 2xx (successful) and 3xx (redirect) responses, visualised as heatmap buckets over time. Use alongside CDN Latencies Detail to separate CDN Director processing time from CDN response time.
Prometheus/ACD: SubRunners
Internal async processing queue depth and throughput metrics for the CDN Director subrunner system: client connection counts, low/medium/high priority queue depths (current and max), send/receive data block usage, wakeup counts, overload events, and autopause activations. Use when investigating CDN Director throughput bottlenecks or queue backpressure.
Advanced Dashboards
Advanced dashboards are a paid add-on that unlocks more detailed variants of two standard dashboards, providing deeper visibility for performance investigation and capacity planning.
Licensing: Advanced dashboards require a separate licence key. To obtain a key for your deployment, contact your AgileTV account representative.
Enabling Advanced Dashboards
Once you have your licence key, add the following to your values.yaml:
dashboards:
advanced:
licenceKey: "<your-licence-key>"
Then apply the change by following the upgrade procedure in the Configuration Guide. The advanced dashboards will become available in Grafana automatically once the upgrade completes.
HW Metrics (Advanced)
Expanded hardware telemetry that supplements the standard HW Metrics dashboard with additional depth and additional sections: kernel metrics, TCP and UDP network stack statistics, per-interface error counters, disk IOPS, and metrics collection velocity. Use this when investigating performance anomalies surfaced by the standard dashboard or by alerts.
Streamer Statistics (Advanced)
Full streamer telemetry that supplements the standard Streamer Statistics dashboard. Includes all standard metrics plus:
- OTT JCQ: Server group and node request rates and ratios, circuit breaker state (closed/open backends), global backend stats, pending requests per backend, and pop-out per backend
- Account Records: Session counts, total traffic in/out, HTTP request rates (ingress/egress), cache hit ratio, backend request rates, and response times
- Detailed network error and drop counters from
/proc/net/dev
CDN Director Metrics
CDN Director DNS Names in Grafana
CDN Director instances are identified in Grafana by their DNS name, which is derived from the name field in global.hosts.routers:
global:
hosts:
routers:
- name: my-router-1
address: 192.0.2.1
The DNS name used in Grafana dashboards will be: my-router-1.external
This naming convention is automatically applied for all configured directors.
Customising Dashboards
Permissions: Creating and importing dashboards requires Grafana Admin access. See Grafana Authentication & Roles for details on granting admin rights.
The pre-provisioned dashboards are read-only and managed by the Helm chart — changes made to them in the Grafana UI will not persist across upgrades. To create persistent custom dashboards:
- In Grafana, navigate to Dashboards > New > New Dashboard
- Add panels using the VictoriaMetrics or Prometheus datasource
- Save the dashboard to a folder of your choice
Custom dashboards saved this way are stored in the Grafana PostgreSQL database and are unaffected by Helm upgrades.
Note: Do not save custom dashboards into the provisioned folders (Alerting, Billing, CDN Manager, etc.). Grafana marks these folders as provisioned and may behave unexpectedly if user dashboards are mixed in.
Customising a Pre-provisioned Dashboard
If you want a modified version of one of the built-in dashboards as a starting point:
- Open the dashboard you want to customise
- Click the Share icon (top toolbar) > Export > Save to file to download the dashboard JSON
- In Grafana, navigate to Dashboards > New > Import
- Upload the downloaded JSON file
- On the import screen, give the dashboard a new name to distinguish it from the original, and choose a destination folder outside the provisioned set
- Click Import
You now have an independently editable copy. The original provisioned dashboard remains unchanged and will continue to be updated by future Helm upgrades. Your copy is stored in PostgreSQL and persists across upgrades independently.
Troubleshooting
Dashboard Loading Issues
If dashboards fail to load:
Check Grafana pods:
kubectl get pods -l app.kubernetes.io/component=grafanaReview Grafana logs:
kubectl logs -l app.kubernetes.io/component=grafanaVerify datasource configuration in Grafana UI
For login and authentication issues, see Grafana Authentication & Roles.
Next Steps
- Alerts & Alarms - Set up alerting and notifications
- Operations Guide - Day-to-day operational procedures
- Metrics & Monitoring Overview - Return to the monitoring overview
9.3 - Alerts & Alarms
Overview
The CDN Manager ships a set of pre-configured alerting rules evaluated by vmalert against VictoriaMetrics. When a rule fires, the alert is routed to Alertmanager, which handles deduplication, grouping, silencing, and delivery to configured notification channels.
This page documents every built-in alert rule, what it means, its severity, and the recommended operator action.
Alert Severity Levels
| Severity | Meaning |
|---|---|
| critical | Immediate action required. The condition poses a risk to data integrity, service availability, or active traffic. |
| warning | Investigate soon. The condition is not immediately harmful but will degrade into a critical state if left unattended. |
Alert Groups
Alerts are organised into the following groups, each evaluated on a 15-second interval.
- infra-disk — Disk space and I/O
- infra-compute — CPU and memory
- infra-network — Network errors and traffic anomalies
- longhorn — Persistent storage health
infra-disk
Monitors disk space utilisation and I/O latency on cluster nodes.
StorageFillingUp
| Property | Value |
|---|---|
| Severity | warning |
| Condition | Root filesystem usage exceeds 85% |
| Must persist for | 2 minutes |
What it means: A node’s root filesystem is running low on space. If left unchecked this will progress to a full disk, which can cause pod evictions, write failures, and potential data loss.
Recommended actions:
- Identify the node from the
hostlabel in the alert. - Log into the node and check disk usage:
df -h / du -sh /var/log/* | sort -rh | head -20 - Clear old log files, unused container images, or temporary files:
# On the node journalctl --vacuum-size=500M crictl rmi --prune - If disk usage is due to application data growth, consider expanding the volume or adjusting retention settings. See Metrics Retention.
HighDiskLatency
| Property | Value |
|---|---|
| Severity | warning |
| Condition | Average disk write latency exceeds 100 ms |
| Must persist for | 2 minutes |
What it means: Disk write operations are taking longer than 100 ms on average. High disk latency can degrade database performance (PostgreSQL, VictoriaMetrics) and cause timeouts in write-heavy components.
Recommended actions:
- Identify the affected disk from the
namelabel in the alert. - Check for I/O-intensive processes on the node:
iostat -x 2 5 iotop -o - Check for Longhorn replica rebuilds or rebalancing activity, which can saturate disk I/O.
- If the issue persists on a production node, review whether the storage hardware meets the System Requirements.
infra-compute
Monitors CPU and memory utilisation on cluster nodes.
CpuSaturation
| Property | Value |
|---|---|
| Severity | warning |
| Condition | Total CPU usage exceeds 90% |
| Must persist for | 5 minutes |
What it means: A node is running at near-full CPU capacity. Sustained CPU saturation causes request latency increases across all workloads on that node and may result in pod throttling.
Recommended actions:
- Identify the saturated node from the
hostlabel in the alert. - Check which pods are consuming CPU:
kubectl top pods --sort-by=cpu -A - Check for runaway processes on the node:
top -b -n 1 | head -20 - If saturation is caused by a legitimate workload spike (e.g. CDN traffic burst), consider scaling the deployment or redistributing load across nodes.
MemoryCriticallyLow
| Property | Value |
|---|---|
| Severity | critical |
| Condition | Available RAM falls below 10% |
| Must persist for | 2 minutes |
What it means: The node has very little free memory remaining. The Linux OOM killer may begin terminating processes, which can cause abrupt pod restarts, data corruption in in-memory caches, and service unavailability.
Recommended actions:
- Identify the affected node from the
hostlabel in the alert. - Immediately check for memory-leaking or oversized pods:
kubectl top pods --sort-by=memory -A - Identify and restart any pods showing abnormal memory consumption:
kubectl rollout restart deployment/<name> - Check kernel OOM kill log for any processes already killed:
dmesg | grep -i "oom\|killed" - Review memory resource limits and requests for affected deployments and adjust if necessary.
SwapUsageDetected
| Property | Value |
|---|---|
| Severity | warning |
| Condition | Swap usage exceeds 5% |
| Must persist for | 1 minute |
What it means: The node is swapping memory to disk. Swap usage in a Kubernetes cluster is a strong indicator of memory pressure. It degrades performance significantly and may mask an underlying memory shortage that could escalate to a MemoryCriticallyLow event.
Recommended actions:
- Treat this as an early warning for the same conditions as
MemoryCriticallyLow. - Identify memory-intensive pods and investigate whether resource limits are configured appropriately.
- Swap should ideally never be active on a production Kubernetes node. If it persists, escalate to a memory capacity review.
infra-network
Monitors network interface errors and traffic anomalies on cluster nodes.
NetworkInterfaceErrors
| Property | Value |
|---|---|
| Severity | critical |
| Condition | Any non-zero rate of inbound or outbound packet errors on a network interface |
| Must persist for | 1 minute |
What it means: A network interface is dropping or corrupting packets. Even a low error rate can cause TCP retransmissions, increased latency, and connection failures — directly impacting CDN Director communication and external traffic delivery.
Recommended actions:
- Identify the affected host and interface from the
hostandinterfacelabels in the alert. - Check interface error counters on the node:
ip -s link show <interface> ethtool -S <interface> | grep -i error - Check for duplex/speed mismatches between the node NIC and the upstream switch:
ethtool <interface> | grep -E "Speed|Duplex" - Escalate to network/hardware team if errors are persistent and cannot be attributed to a software configuration issue.
SuddenNetworkEgressDrop
| Property | Value |
|---|---|
| Severity | critical |
| Condition | Egress throughput drops to less than 50% of the 2-minute baseline, when baseline traffic is above 1 Mbit/s |
| Must persist for | 1 minute |
What it means: A significant, sudden reduction in outbound traffic has been detected. This typically indicates a upstream network failure, link fault, or a routing issue. A CDN node that stops transmitting traffic is effectively out of service.
Recommended actions:
- Identify the affected node and interface from the alert labels.
- Verify the node’s network connectivity:
ping <gateway-ip> traceroute <upstream-endpoint> - Check for interface errors or link-down events:
ip link show dmesg | grep -i "link\|eth\|nic" - Verify that upstream routing and firewall rules have not changed.
- If the node is healthy and traffic has legitimately dropped (e.g. a CDN traffic shift), the alert can be silenced if the traffic reduction is expected and understood.
SuddenNetworkIngressSpike
| Property | Value |
|---|---|
| Severity | warning |
| Condition | Ingress throughput exceeds twice the 5-minute baseline |
| Must persist for | 1 minute |
What it means: A sudden surge of inbound traffic has been detected. This may indicate a legitimate traffic event (e.g. a large stream audience spike), a DDoS attempt, or a misconfigured client sending unexpected volume.
Recommended actions:
- Identify the affected node and interface from the alert labels.
- Review active connections and top talkers:
ss -s netstat -an | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head - Correlate with CDN Director metrics in Grafana to determine whether the spike is legitimate CDN traffic.
- If the spike is unexpected and sustained, consider rate-limiting or blocking the source at the network edge.
longhorn
Monitors the health of Longhorn distributed block storage, which backs persistent volumes for PostgreSQL, VictoriaMetrics, and other stateful components.
Note: Longhorn alert rules are always present in the alert configuration, but will not fire in environments where Longhorn is not installed (e.g. cloud deployments using external storage).
LonghornVolumeDegraded
| Property | Value |
|---|---|
| Severity | warning |
| Condition | A Longhorn volume’s robustness state is Degraded |
| Must persist for | 2 minutes |
What it means: A Longhorn volume has fewer healthy replicas than its configured replication factor. Data is not at immediate risk, but the volume has reduced redundancy. A single additional node or disk failure could result in data loss or volume unavailability.
Recommended actions:
- Identify the affected volume from the
volumelabel in the alert. - Open the Longhorn UI and inspect the volume’s replica status.
- Check whether a replica is in the process of rebuilding (this is normal after a node restart). Rebuilding may take several minutes depending on volume size.
- If a replica has failed and is not rebuilding, attempt to evict and re-schedule it via the Longhorn UI.
- Investigate the health of the node that hosted the failed replica:
kubectl get nodes kubectl describe node <node-name>
LonghornVolumeFaulted
| Property | Value |
|---|---|
| Severity | critical |
| Condition | A Longhorn volume’s robustness state is Faulted |
| Must persist for | 1 minute |
What it means: A Longhorn volume has lost all healthy replicas and is no longer accessible. Any workload that depends on this volume (e.g. PostgreSQL, VictoriaMetrics) will be unable to write and may crash. Data may be at risk.
Recommended actions:
- Identify the affected volume from the
volumelabel. - Immediately check which pods are using the volume:
kubectl get pods -A -o wide | grep -i <volume-name> - Open the Longhorn UI. Check whether any replicas are still present and whether they can be recovered.
- Do not delete faulted replicas without first attempting recovery — they may contain the only copy of the data.
- Contact AgileTV support if the volume cannot be recovered, providing Longhorn UI screenshots and node logs.
LonghornNodeDown
| Property | Value |
|---|---|
| Severity | critical |
| Condition | A Longhorn node reports a non-ready state |
| Must persist for | 2 minutes |
What it means: A storage node is unreachable or unhealthy from Longhorn’s perspective. All volumes with replicas on this node are at reduced redundancy. If more than one node goes down simultaneously, faulted volumes and data loss become a risk.
Recommended actions:
- Identify the affected node from the
nodelabel in the alert. - Check the node’s status in Kubernetes:
kubectl get nodes kubectl describe node <node-name> - Attempt to SSH to the node and check system health:
ssh root@<node-ip> systemctl status k3s - If the node has crashed and cannot be recovered quickly, consider evicting its Longhorn replicas to allow rebuilding on healthy nodes — but only if the remaining healthy nodes have sufficient capacity.
LonghornDiskSpaceLow
| Property | Value |
|---|---|
| Severity | warning |
| Condition | Available Longhorn disk space on a node falls below 15% |
| Must persist for | 2 minutes |
What it means: A node’s Longhorn-managed disk is running low on storage. When Longhorn disk space is exhausted, it cannot schedule new replicas or accommodate volume growth, which can lead to LonghornVolumeDegraded or LonghornVolumeFaulted conditions.
Recommended actions:
- Identify the affected node and disk from the
nodeanddisklabels in the alert. - Open the Longhorn UI and check which volumes have replicas on this disk.
- Check for snapshots or backups that can be cleaned up to reclaim space.
- If space cannot be reclaimed, consider adding a disk to the node or expanding the underlying block device.
- Review Metrics Retention settings — reducing VictoriaMetrics retention is often the fastest way to reclaim Longhorn disk space in a monitoring-heavy deployment.
Adding Custom Alert Rules
Additional alert rules can be defined by extending the victoria_metrics_alert.server.config.alerts.groups list in your values.yaml. Rules follow the Prometheus alerting rule format.
Example: Adding a Custom Alert
The following example adds an alert group that fires when a Kafka consumer lag exceeds a threshold:
victoria_metrics_alert:
server:
config:
alerts:
groups:
# ... existing groups are preserved alongside your additions ...
- name: kafka
interval: 15s
rules:
- alert: KafkaConsumerLagHigh
expr: kafka_consumer_group_lag > 10000
for: 5m
labels:
severity: warning
annotations:
summary: "High consumer lag on {{ $labels.topic }}"
description: "Consumer group {{ $labels.group }} is {{ $value }} messages behind on topic {{ $labels.topic }}."
Apply the change using the standard upgrade procedure in the Configuration Guide.
Rule Fields Reference
| Field | Required | Description |
|---|---|---|
alert | Yes | Alert name. Must be unique within the group. |
expr | Yes | PromQL expression. The alert fires when this evaluates to a non-zero/non-empty result. |
for | No | How long the condition must hold before the alert fires. Omitting this fires immediately. |
labels.severity | Recommended | Set to critical or warning to match the built-in routing rules. |
annotations.summary | Recommended | Short human-readable description. Supports Go template labels (e.g. {{ $labels.host }}). |
annotations.description | Recommended | Detailed description with context for the on-call operator. |
Tip: Use the Alertmanager UI (
https://<manager-host>/alertmanager) to verify that fired alerts are being received and routed correctly after adding new rules.
Configuring Alert Routes
By default, all alerts are routed to the built-in null receiver, which silently discards them. To receive alerts, configure one or more receivers and update the routing rules — all within the alertmanager.config section of your values.yaml.
Route Structure
The top-level route defines the default behaviour. Child routes under routes match alerts by label and direct them to specific receivers:
alertmanager:
config:
route:
receiver: 'null' # Default: discard unmatched alerts
group_by: ['alertname']
group_wait: 10s # Wait before sending first notification for a new group
group_interval: 10s # Wait before sending updated notifications for a group
repeat_interval: 1h # Re-notify if an alert is still firing after this period
routes:
- matchers:
- severity="critical"
receiver: 'slack'
- matchers:
- severity="warning"
receiver: 'email-warning'
Routes are evaluated top-to-bottom. The first matching route wins unless continue: true is set on the route.
Notification Channels
Email requires an SMTP server to be configured globally. Both a critical and warning receiver can be defined independently.
alertmanager:
config:
global:
smtp_smarthost: 'smtp.example.com:587'
smtp_from: 'alertmanager@example.com'
smtp_require_tls: true
route:
routes:
- matchers:
- severity="critical"
receiver: 'email-critical'
- matchers:
- severity="warning"
receiver: 'email-warning'
receivers:
- name: 'null'
- name: 'email-critical'
email_configs:
- to: 'oncall@example.com'
send_resolved: true
- name: 'email-warning'
email_configs:
- to: 'alerts@example.com'
send_resolved: true
Slack
Requires an incoming webhook URL created in your Slack workspace.
alertmanager:
config:
route:
routes:
- matchers:
- severity="critical"
receiver: 'slack'
receivers:
- name: 'null'
- name: 'slack'
slack_configs:
- api_url: 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL'
channel: '#alerts'
send_resolved: true
title: '[{{ .Status | toUpper }}] {{ .CommonLabels.alertname }}'
text: |
*Severity:* {{ .CommonLabels.severity }}
*Host:* {{ .CommonLabels.host }}
{{ range .Alerts }}{{ .Annotations.description }}{{ end }}
Telegram
Requires a Telegram bot token and a channel/group chat ID. Create a bot via @BotFather and add it to your alert channel before configuring.
alertmanager:
config:
route:
routes:
- matchers:
- severity="critical"
receiver: 'telegram'
receivers:
- name: 'null'
- name: 'telegram'
telegram_configs:
- bot_token: 'your-bot-token'
chat_id: -1234567890
parse_mode: 'Markdown'
send_resolved: true
message: |
*Alert:* {{ .CommonLabels.alertname }}
*Severity:* {{ .CommonLabels.severity }}
*Host:* {{ .CommonLabels.host }}
{{ range .Alerts }}
{{ .Annotations.description }}
{{ end }}
Finding your chat ID: Add your bot to the channel or group, send a message, then call
https://api.telegram.org/bot<token>/getUpdatesand read thechat.idfrom the response. Note that group and channel chat IDs are negative numbers.
Combining Multiple Receivers
Routes and receivers can be combined to send different alert severities to different channels simultaneously. For example, critical alerts to PagerDuty and Slack, warnings to email only:
alertmanager:
config:
route:
receiver: 'null'
routes:
- matchers:
- severity="critical"
receiver: 'slack'
continue: true # Continue matching so the next route also fires
- matchers:
- severity="critical"
receiver: 'email-critical'
- matchers:
- severity="warning"
receiver: 'email-warning'
receivers:
- name: 'null'
- name: 'slack'
slack_configs:
- api_url: 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL'
channel: '#critical-alerts'
send_resolved: true
- name: 'email-critical'
email_configs:
- to: 'oncall@example.com'
send_resolved: true
- name: 'email-warning'
email_configs:
- to: 'alerts@example.com'
send_resolved: true
Apply any receiver or routing changes using the standard upgrade procedure in the Configuration Guide.
Silencing Alerts
Silences suppress alert notifications for a defined time window without disabling the underlying alert rule. They are useful during planned maintenance, known incidents, or when investigating a non-urgent condition.
Silences are managed via the Alertmanager UI, accessible at:
https://<manager-host>/alertmanager
Creating a Silence
- Navigate to the Alertmanager UI and click Silences in the top navigation.
- Click Create Silence.
- Set the Start and End times for the silence window.
- Add one or more matchers to scope which alerts are suppressed. For example:
alertname = StorageFillingUp— silence a specific alertseverity = warning— silence all warningshost = node-01— silence all alerts from a specific host
- Add a Comment describing the reason for the silence (e.g.
Planned disk expansion on node-01). - Click Create. The silence takes effect immediately.
Expiring a Silence
Silences expire automatically at the configured end time. To remove a silence early, navigate to Silences in the Alertmanager UI, locate the silence, and click Expire.
Next Steps
- Operations Guide - Day-to-day operational procedures
- Troubleshooting Guide - Resolve underlying issues surfaced by alerts
- Metrics & Monitoring Overview - Return to the monitoring overview
10 - API Guide
Overview
The CDN Manager exposes versioned HTTP APIs under /api (v1 and v2), using JSON payloads by default. When sending request bodies, set Content-Type: application/json. Server errors typically respond with { "message": "..." } where available, or an empty body with the relevant status code.
Authentication uses a two-step flow:
- Create a session
- Exchange that session for an access token with
grant_type=session
Use the access token in Authorization: Bearer <token> when calling bearer-protected routes. CORS preflight (OPTIONS) is supported and wildcard origins are accepted by default.
Durations such as TTLs use humantime strings (for example, 60s, 5m, 1h).
Base URL
All API endpoints are relative to:
https://<manager-host>/api
API Reference Guides
The API documentation is organized by functional area:
| Guide | Description |
|---|---|
| Authentication API | Login, token exchange, logout, and session management |
| Health API | Liveness and readiness probes |
| Selection Input API | Key-value and list storage with search capabilities |
| Data Store API | Generic JSON key/value storage |
| Subnets API | CIDR-to-value mappings for routing decisions |
| Routing API | GeoIP lookups and IP validation |
| Discovery API | Host and namespace discovery |
| Metrics API | Metrics submission and aggregation |
| Configuration API | Configuration document management |
| Operator UI API | Blocked tokens, user agents, and referrers |
| OpenAPI Specification | Complete OpenAPI 3.0 specification |
Authentication Flow
All authenticated API calls follow the same authentication flow. For detailed instructions, see the Authentication API Guide.
Quick Start:
# Step 1: Login to get session
curl -s -X POST "https://cdn-manager/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "Password1!"
}' | tee /tmp/session.json
SESSION_ID=$(jq -r '.session_id' /tmp/session.json)
SESSION_TOKEN=$(jq -r '.session_token' /tmp/session.json)
# Step 2: Exchange session for access token
curl -s -X POST "https://cdn-manager/api/v1/auth/token" \
-H "Content-Type: application/json" \
-d "$(jq -nc --arg sid "$SESSION_ID" --arg st "$SESSION_TOKEN" \
'{session_id:$sid,session_token:$st,grant_type:"session",scope:"openid"}')" \
| tee /tmp/token.json
ACCESS_TOKEN=$(jq -r '.access_token' /tmp/token.json)
# Step 3: Call a protected endpoint
curl -s "https://cdn-manager/api/v1/metrics" \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
Error Responses
The API uses standard HTTP response codes to indicate the success or failure of an API request.
Most errors return an empty response body with the relevant HTTP status code (e.g., 404 Not Found or 409 Conflict).
In some cases, the server may return a JSON body containing a user-facing error message:
{
"message": "Human-readable error message"
}
Next Steps
After learning the API:
- Operations Guide - Day-to-day operational procedures
- Troubleshooting Guide - Resolve API issues
- Configuration Guide - Full configuration reference
10.1 - Authentication API
Overview
The Authentication API provides endpoints for user authentication, session management, and token exchange. All authenticated API calls require a valid access token obtained through the authentication flow.
Base URL
https://<manager-host>/api/v1/auth
Endpoints
POST /api/v1/auth/login
Create a session from email/password credentials.
Request:
POST /api/v1/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "Password1!"
}
Success Response (200):
{
"session_id": "session-1",
"session_token": "token-1",
"verified_at": "2024-01-01T00:00:00Z",
"expires_at": "2024-01-01T01:00:00Z"
}
Errors:
401- Authentication failure (invalid credentials)500- Backend/state errors
POST /api/v1/auth/token
Exchange a session for an access token (required for bearer auth).
Request:
POST /api/v1/auth/token
Content-Type: application/json
{
"session_id": "session-1",
"session_token": "token-1",
"grant_type": "session",
"scope": "openid profile"
}
Success Response (200):
{
"access_token": "<token>",
"scope": "openid profile",
"expires_in": 3600,
"token_type": "bearer"
}
Token Scopes
The scope parameter in the token exchange request is a space-separated string of permissions requested for the access token.
Scope Resolution When a token is requested, the backend system filters the requested scopes against the user’s actual permissions. The resulting access token will only contain the subset of requested scopes that the user is authorized to possess.
Naming and Design
Scope names are defined by the applications that consume the tokens, not by the central IAM system. To prevent collisions between different applications or modules, it is highly recommended that application developers use URN-style prefixes for scope names (e.g., urn:acd:manager:config:read).
Errors:
401- Authentication failure (invalid session)500- Backend/state errors
POST /api/v1/auth/logout
Revoke a session. Note: This does not revoke issued access tokens; they remain valid until expiration.
Request:
POST /api/v1/auth/logout
Content-Type: application/json
{
"session_id": "session-1",
"session_token": "token-1"
}
Success Response (200):
{
"status": "Ok"
}
Errors:
400- Invalid session parameters500- Backend/state errors
Complete Authentication Flow Example
# Step 1: Login to get session
curl -s -X POST "https://cdn-manager/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "Password1!"
}' | tee /tmp/session.json
SESSION_ID=$(jq -r '.session_id' /tmp/session.json)
SESSION_TOKEN=$(jq -r '.session_token' /tmp/session.json)
# Step 2: Exchange session for access token
curl -s -X POST "https://cdn-manager/api/v1/auth/token" \
-H "Content-Type: application/json" \
-d "$(jq -nc --arg sid "$SESSION_ID" --arg st "$SESSION_TOKEN" \
'{session_id:$sid,session_token:$st,grant_type:"session",scope:"openid"}')" \
| tee /tmp/token.json
ACCESS_TOKEN=$(jq -r '.access_token' /tmp/token.json)
# Step 3: Call a protected endpoint
curl -s "https://cdn-manager/api/v1/metrics" \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
Using the Access Token
Once you have obtained an access token, include it in the Authorization header of all API requests:
Authorization: Bearer <access_token>
Example:
curl -s "https://cdn-manager/api/v1/configuration" \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
Token Expiration
Access tokens expire after the duration specified in expires_in (typically 3600 seconds / 1 hour). When a token expires, you must re-authenticate to obtain a new token.
Next Steps
- Health API - Liveness and readiness probes
- Selection Input API - Key-value and list storage
- OpenAPI Specification - Complete API specification
10.2 - Health API
Overview
The Health API provides endpoints for Kubernetes health probes and service health checking.
Base URL
https://<manager-host>/api/v1/health
Endpoints
GET /api/v1/health/alive
Liveness probe that indicates whether the service is running. Always returns 200 OK.
Request:
GET /api/v1/health/alive
Response (200):
{
"status": "Ok"
}
Use Case: Kubernetes liveness probe to determine if the pod should be restarted.
GET /api/v1/health/ready
Readiness probe that checks service readiness including downstream dependencies.
Request:
GET /api/v1/health/ready
Success Response (200):
{
"status": "Ok"
}
Failure Response (503):
{
"status": "Fail"
}
Use Case: Kubernetes readiness probe to determine if the pod should receive traffic. Returns 503 if any downstream dependencies (database, Kafka, Redis) are unavailable.
Kubernetes Configuration
Example Kubernetes probe configuration:
livenessProbe:
httpGet:
path: /api/v1/health/alive
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/v1/health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
Next Steps
- Authentication API - User authentication
- Selection Input API - Key-value and list storage
- OpenAPI Specification - Complete API specification
10.3 - Selection Input API
Overview
The Selection Input API provides JSON key/value storage with search capabilities. It supports two API versions (v1 and v2) with different operation models.
Base URL
https://<manager-host>/api/v1/selection_input
https://<manager-host>/api/v2/selection_input
Version Comparison
| Feature | v1 /api/v1/selection_input | v2 /api/v2/selection_input |
|---|---|---|
| Primary operation | Merge/UPSERT (POST) | Insert/Replace (PUT) |
| List append | N/A | POST to push to list |
| Search syntax | Wildcard prefix (foo* implicit) | Full wildcard (foo* explicit) |
| Query params | search, sort, limit, ttl | search, ttl, correlation_id |
| Sort support | Yes (asc/desc) | No |
| Limit support | Yes | No |
| Use case | Simple key-value with optional search | List-like operations, full wildcard |
When to Use Each Version
| Scenario | Recommended Version |
|---|---|
| Simple key-value storage | v1 |
| List/queue operations (append to array) | v2 POST |
| Full wildcard pattern matching | v2 |
| Need to sort or paginate results | v1 |
v1 Endpoints
GET /api/v1/selection_input/{path}
Fetch stored JSON. If value is an object, optional search/limit/sort applies to its keys.
Query Parameters:
search- Wildcard prefix search (adds*implicitly)sort- Sort order (ascordesc)limit- Maximum results (must be > 0)
Success Response (200):
{
"foo": 1,
"foobar": 2
}
Errors:
404- Path does not exist400- Invalid search/sort/limit parameters500- Backend failure
Example:
curl -s "https://cdn-manager/api/v1/selection_input/config?search=foo&limit=2"
POST /api/v1/selection_input/{path}
Upsert (merge) JSON at path. Nested objects are merged recursively.
Query Parameters:
ttl- Expiry time as humantime string (e.g.,10m,1h)
Request:
{
"feature_flag": true,
"ratio": 0.5
}
Success: 201 Created echoing the payload
Errors:
500/503- Backend failure
Example:
curl -s -X POST "https://cdn-manager/api/v1/selection_input/config?ttl=10m" \
-H "Content-Type: application/json" \
-d '{
"feature_flag": true,
"ratio": 0.5
}'
DELETE /api/v1/selection_input/{path}
Delete stored value.
Success: 204 No Content
Errors: 503 - Backend failure
v2 Endpoints
GET /api/v2/selection_input/{path}
Fetch stored JSON with optional wildcard filtering.
Query Parameters:
search- Full wildcard pattern (e.g.,foo*,*bar*)correlation_id- Accepted but currently ignored (logging only)
Success Response (200):
{
"foo": 1,
"foobar": 2
}
Errors:
400- Invalid search pattern404- Path does not exist500- Backend failure
Example:
curl -s "https://cdn-manager/api/v2/selection_input/config?search=foo*"
PUT /api/v2/selection_input/{path}
Insert/replace value. Old value is discarded.
Query Parameters:
ttl- Expiry time as humantime string
Request:
{
"items": ["a", "b", "c"]
}
Success: 200 OK
Example:
curl -s -X PUT "https://cdn-manager/api/v2/selection_input/catalog" \
-H "Content-Type: application/json" \
-d '{
"items": ["a", "b", "c"]
}'
POST /api/v2/selection_input/{path}
Push a value to the back of a list-like entry (append to array).
Query Parameters:
ttl- Expiry time as humantime string
Request (any JSON value):
{
"item": 42
}
Or a simple string:
"ready-for-publish"
Success: 200 OK
Example:
curl -s -X POST "https://cdn-manager/api/v2/selection_input/queue" \
-H "Content-Type: application/json" \
-d '"ready-for-publish"'
DELETE /api/v2/selection_input/{path}
Delete stored value.
Success: 204 No Content
Next Steps
- Data Store API - Generic key/value storage
- Subnets API - CIDR-to-value mappings
- OpenAPI Specification - Complete API specification
10.4 - Data Store API
Overview
The Data Store API provides generic JSON key/value storage for short-lived or simple structured data.
Base URL
https://<manager-host>/api/v1/datastore
Endpoints
GET /api/v1/datastore
List all known keys.
Query Parameters:
show_hidden- Boolean (defaultfalse). Whentrue, includes internal keys starting with_.
Success Response (200):
["user:123", "config:settings", "session:abc"]
Hidden Keys: Keys starting with _ are reserved for internal use. Writing to hidden keys via the datastore API returns 400 Bad Request.
GET /api/v1/datastore/{key}
Retrieve the JSON value for a specific key.
Success Response (200): The stored JSON value
Errors:
404- Key does not exist500- Backend failure
Example:
curl -s "https://cdn-manager/api/v1/datastore/user:123"
POST /api/v1/datastore/{key}
Create a new JSON value at the specified key. Fails if the key already exists.
Query Parameters:
ttl- Expiry time as humantime string (e.g.,60s,1h)
Request:
{
"id": 123,
"name": "alice"
}
Success: 201 Created
Errors:
409 Conflict- Key already exists500- Backend failure
Example:
curl -s -X POST "https://cdn-manager/api/v1/datastore/user:123?ttl=1h" \
-H "Content-Type: application/json" \
-d '{"id":123,"name":"alice"}'
PUT /api/v1/datastore/{key}
Update or replace the JSON value at an existing key.
Query Parameters:
ttl- Expiry time as humantime string
Success: 200 OK
Errors:
404- Key does not exist500- Backend failure
Example:
curl -s -X PUT "https://cdn-manager/api/v1/datastore/user:123" \
-H "Content-Type: application/json" \
-d '{"id":123,"name":"alice-updated"}'
DELETE /api/v1/datastore/{key}
Delete the value at the specified key. Idempotent operation.
Success: 204 No Content
Errors: 500 - Backend failure
Example:
curl -s -X DELETE "https://cdn-manager/api/v1/datastore/user:123"
Next Steps
- Subnets API - CIDR-to-value mappings
- Routing API - GeoIP lookups
- OpenAPI Specification - Complete API specification
10.5 - Subnets API
Overview
The Subnets API manages CIDR-to-value mappings used for routing decisions. This allows classification of IP ranges for routing purposes.
Base URL
https://<manager-host>/api/v1/subnets
Endpoints
PUT /api/v1/subnets
Create or update subnet mappings.
Request:
{
"192.168.1.0/24": "office",
"10.0.0.0/8": "internal",
"203.0.113.0/24": "external"
}
Success: 200 OK
Errors:
400- Invalid CIDR format500- Backend failure
Example:
curl -s -X PUT "https://cdn-manager/api/v1/subnets" \
-H "Content-Type: application/json" \
-d '{
"192.168.1.0/24": "office",
"10.0.0.0/8": "internal"
}'
GET /api/v1/subnets
List all subnet mappings.
Success Response (200): JSON object of CIDR-to-value mappings
Example:
curl -s "https://cdn-manager/api/v1/subnets" | jq '.'
DELETE /api/v1/subnets
Delete all subnet mappings.
Success: 204 No Content
GET /api/v1/subnets/byKey/{subnet}
Retrieve subnet mappings whose CIDR begins with the given prefix.
Example:
curl -s "https://cdn-manager/api/v1/subnets/byKey/192.168" | jq '.'
GET /api/v1/subnets/byValue/{value}
Retrieve subnet mappings with the given classification value.
Example:
curl -s "https://cdn-manager/api/v1/subnets/byValue/office" | jq '.'
DELETE /api/v1/subnets/byKey/{subnet}
Delete subnet mappings whose CIDR begins with the given prefix.
DELETE /api/v1/subnets/byValue/{value}
Delete subnet mappings with the given classification value.
Next Steps
- Routing API - GeoIP lookups and IP validation
- Discovery API - Host and namespace discovery
- OpenAPI Specification - Complete API specification
10.6 - Routing API
Overview
The Routing API provides GeoIP information lookup and IP address validation for routing decisions.
Base URL
https://<manager-host>/api/v1/routing
Endpoints
GET /api/v1/routing/geoip
Look up GeoIP information for an IP address.
Query Parameters:
ip- IP address to look up
Success Response (200):
{
"city": {
"name": "Washington"
},
"asn": 64512
}
Errors:
400- Invalid IP format500- Backend failure
Caching: Cache-Control: public, max-age=86400 (24 hours)
Example:
curl -s "https://cdn-manager/api/v1/routing/geoip?ip=149.101.100.0"
GET /api/v1/routing/validate
Validate if an IP address is allowed (not blocked).
Query Parameters:
ip- IP address to validate
Success Response (200): Empty body (IP is allowed)
Forbidden Response (403):
Access Denied
Errors:
400- Invalid IP format500- Backend failure
Caching: Cache-Control headers included (default: max-age=300, configurable via [tuning] section)
Example:
curl -i "https://cdn-manager/api/v1/routing/validate?ip=149.101.100.0"
Use Cases
GeoIP-Based Routing
Use the /geoip endpoint to determine the geographic location and ASN of an IP address for routing decisions:
# Get location data for routing
IP_INFO=$(curl -s "https://cdn-manager/api/v1/routing/geoip?ip=203.0.113.50")
CITY=$(echo "$IP_INFO" | jq -r '.city.name')
ASN=$(echo "$IP_INFO" | jq -r '.asn')
echo "Routing based on city: $CITY, ASN: $ASN"
IP Validation
Use the /validate endpoint to check if an IP is allowed before processing requests:
# Check if IP is allowed
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
"https://cdn-manager/api/v1/routing/validate?ip=203.0.113.50")
if [ "$RESPONSE" = "200" ]; then
echo "IP is allowed"
elif [ "$RESPONSE" = "403" ]; then
echo "IP is blocked"
fi
Next Steps
- Discovery API - Host and namespace discovery
- Metrics API - Metrics submission and aggregation
- OpenAPI Specification - Complete API specification
10.7 - Discovery API
Overview
The Discovery API provides information about discovered hosts and namespaces. Discovery is configured via the Helm chart values.yaml file. Each entry defines a namespace with a list of hostnames.
Base URL
https://<manager-host>/api/v1/discovery
Endpoints
GET /api/v1/discovery/hosts
Return discovered hosts grouped by namespace.
Success Response (200):
{
"directors": [
{ "name": "director-1.example.com" }
],
"edge-servers": [
{ "name": "cdn1.example.com" },
{ "name": "cdn2.example.com" }
]
}
Example:
curl -s "https://cdn-manager/api/v1/discovery/hosts"
GET /api/v1/discovery/namespaces
Return discovery namespaces with their corresponding Confd URIs.
Success Response (200):
[
{
"namespace": "edge-servers",
"confd_uri": "/api/v1/confd/edge-servers"
},
{
"namespace": "directors",
"confd_uri": "/api/v1/confd/directors"
}
]
Example:
curl -s "https://cdn-manager/api/v1/discovery/namespaces"
Configuration
Discovery is configured via the Helm chart values.yaml file under cdn-manager.manager.discovery:
cdn-manager:
manager:
discovery:
- namespace: "directors"
hosts:
- director-1.example.com
- director-2.example.com
- namespace: "edge-servers"
hosts:
- cdn1.example.com
- cdn2.example.com
Each entry defines a namespace with a list of hostnames. Optionally, a pattern field can be specified for regex-based host matching.
Next Steps
- Metrics API - Metrics submission and aggregation
- Configuration API - Configuration document management
- OpenAPI Specification - Complete API specification
10.8 - Metrics API
Overview
The Metrics API allows submission and retrieval of metrics data from CDN components.
Base URL
https://<manager-host>/api/v1/metrics
Endpoints
POST /api/v1/metrics
Submit metrics data.
Request:
{
"example.com": {
"metric1": 100,
"metric2": 200
}
}
Success: 200 OK
Errors: 500 - Validation/backend errors
Example:
curl -s -X POST "https://cdn-manager/api/v1/metrics" \
-H "Content-Type: application/json" \
-d '{
"example.com": {
"metric1": 100,
"metric2": 200
}
}'
GET /api/v1/metrics
Return aggregated metrics per host.
Response: JSON object with aggregated metrics per host
Note: Metrics are stored per host for up to 5 minutes. Hosts that stop reporting disappear from aggregation after that window. When no metrics are being reported, returns empty object {}.
Example:
curl -s "https://cdn-manager/api/v1/metrics"
Metrics Retention
- Metrics are stored for up to 5 minutes in the aggregation layer
- For long-term metrics storage, data is forwarded to VictoriaMetrics
- Query historical metrics via Grafana dashboards at
/grafana
Next Steps
- Configuration API - Configuration document management
- Operator UI API - Blocked tokens, user agents, and referrers
- OpenAPI Specification - Complete API specification
10.9 - Configuration API
Overview
The Configuration API provides endpoints for managing the system configuration document. ETag is supported; send If-None-Match for conditional GET (may return 304).
Operational Note: This API is intended for internal verification only. Behavior is undefined in multi-replica clusters because pods do not coordinate config writes.
Base URL
https://<manager-host>/api/v1/configuration
Endpoints
GET /api/v1/configuration
Retrieve the configuration document.
Success: 200 OK with configuration JSON
Conditional GET: Returns 304 Not Modified if If-None-Match header matches current ETag
Example:
# Get ETag from response headers
etag=$(curl -s -D- "https://cdn-manager/api/v1/configuration" | awk '/ETag/{print $2}')
# Conditional GET - returns 304 if config unchanged
curl -s -H "If-None-Match: $etag" "https://cdn-manager/api/v1/configuration" -o /tmp/cfg.json -w "%{http_code}\n"
PUT /api/v1/configuration
Replace the configuration document.
Request:
{
"feature_flag": false,
"ratio": 0.25
}
Success: 200 OK
Errors:
400- Invalid configuration format500- Backend failure
DELETE /api/v1/configuration
Delete the configuration document.
Success: 200 OK
ETag Usage
The configuration API supports ETags for optimistic concurrency control:
# 1. Get current config and ETag
response=$(curl -s -D headers.txt "https://cdn-manager/api/v1/configuration")
etag=$(grep -i ETag headers.txt | cut -d' ' -f2 | tr -d '\r')
# 2. Modify the config as needed
modified_config=$(echo "$response" | jq '.feature_flag = true')
# 3. Update with ETag to prevent overwriting concurrent changes
curl -s -X PUT "https://cdn-manager/api/v1/configuration" \
-H "Content-Type: application/json" \
-H "If-Match: $etag" \
-d "$modified_config"
Next Steps
- Operator UI API - Blocked tokens, user agents, and referrers
- OpenAPI Specification - Complete API specification
10.10 - Operator UI API
Overview
The Operator UI API provides read-only helpers exposing curated selection input content for the operator interface.
Query Parameters: search, sort, limit (same as selection input v1)
Note: Stored keys for user agents/referrers are URL-safe base64; responses decode them to human-readable values.
Base URL
https://<manager-host>/api/v1/operator_ui
Endpoints
Blocked Household Tokens
GET /api/v1/operator_ui/modules/blocked_tokens
List all blocked household tokens.
Success Response (200):
[
{
"household_token": "house-001_token-abc",
"expire_time": 1625247600
}
]
GET /api/v1/operator_ui/modules/blocked_tokens/{token}
Get details for a specific blocked token.
Success Response (200):
{
"household_token": "house-001_token-abc",
"expire_time": 1625247600
}
Blocked User Agents
GET /api/v1/operator_ui/modules/blocked_user_agents
List all blocked user agents.
Success Response (200):
[
{
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
},
{
"user_agent": "curl/7.68.0"
}
]
GET /api/v1/operator_ui/modules/blocked_user_agents/{encoded}
Get details for a specific blocked user agent. The path variable is URL-safe base64 encoded.
Example:
# Encode the user agent
ENC=$(python3 -c "import base64; print(base64.urlsafe_b64encode(b'curl/7.68.0').decode().rstrip('='))")
# Get details
curl -s "https://cdn-manager/api/v1/operator_ui/modules/blocked_user_agents/$ENC"
Blocked Referrers
GET /api/v1/operator_ui/modules/blocked_referrers
List all blocked referrers.
Success Response (200):
[
{
"referrer": "https://spam-example.com"
}
]
GET /api/v1/operator_ui/modules/blocked_referrers/{encoded}
Get details for a specific blocked referrer. The path variable is URL-safe base64 encoded.
Example:
# Encode the referrer
ENC=$(python3 -c "import base64; print(base64.urlsafe_b64encode(b'spam-example.com').decode().rstrip('='))")
# Get details
curl -s "https://cdn-manager/api/v1/operator_ui/modules/blocked_referrers/$ENC"
URL-Safe Base64 Encoding
The Operator UI API uses URL-safe base64 encoding for path parameters. To encode values:
Python:
import base64
# Encode
encoded = base64.urlsafe_b64encode(b'value').decode().rstrip('=')
# Decode
decoded = base64.urlsafe_b64decode(encoded + '=' * (-len(encoded) % 4)).decode()
Bash (with openssl):
# Encode
echo -n "value" | openssl base64 -urlsafe | tr -d '='
# Decode
echo "encoded" | openssl base64 -urlsafe -d
Next Steps
- OpenAPI Specification - Complete API specification
10.11 - OpenAPI Specification
Overview
The CDN Manager API is documented using the OpenAPI 3.0 specification. This appendix provides the complete specification for reference and for generating API clients.
OpenAPI Specification (YAML)
openapi: 3.0.3
info:
title: AgileTV CDN Manager API
version: "1.0"
servers:
- url: https://<manager-host>/api
description: CDN Manager API server
paths:
/v1/auth/login:
post:
summary: Login and create session
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: Session created
content:
application/json:
schema:
$ref: '#/components/schemas/LoginResponse'
'401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
'500': { description: Internal error, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
/v1/auth/token:
post:
summary: Exchange session for access token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TokenRequest'
responses:
'200':
description: Access token
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
'401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
'500': { description: Internal error, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
/v1/auth/logout:
post:
summary: Revoke session
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LogoutRequest'
responses:
'200': { description: Revoked, content: { application/json: { schema: { $ref: '#/components/schemas/LogoutResponse' } } } }
'401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
'500': { description: Internal error, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
/v1/selection_input{tail}:
get:
summary: Read selection input
parameters:
- $ref: '#/components/parameters/Tail'
- $ref: '#/components/parameters/Search'
- $ref: '#/components/parameters/Sort'
- $ref: '#/components/parameters/Limit'
responses:
'200': { description: JSON value }
'400': { description: Bad request, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
'404': { description: Not found }
'500': { description: Backend failure }
post:
summary: Merge selection input
parameters:
- $ref: '#/components/parameters/Tail'
- $ref: '#/components/parameters/Ttl'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AnyJson'
responses:
'201': { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/AnyJson' } } } }
'500': { description: Backend failure }
'503': { description: Service unavailable }
delete:
summary: Delete selection input
parameters:
- $ref: '#/components/parameters/Tail'
responses:
'204': { description: Deleted }
'503': { description: Service unavailable }
/v2/selection_input{tail}:
get:
summary: Read selection input v2
parameters:
- $ref: '#/components/parameters/TailV2'
- $ref: '#/components/parameters/Search'
responses:
'200': { description: JSON value }
'400': { description: Invalid search pattern }
'404': { description: Not found }
'500': { description: Backend failure }
put:
summary: Replace selection input v2
parameters:
- $ref: '#/components/parameters/TailV2'
- $ref: '#/components/parameters/Ttl'
- $ref: '#/components/parameters/CorrelationId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AnyJson'
responses:
'200': { description: Updated }
'500': { description: Backend failure }
post:
summary: Push to selection input v2
parameters:
- $ref: '#/components/parameters/TailV2'
- $ref: '#/components/parameters/Ttl'
- $ref: '#/components/parameters/CorrelationId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AnyJson'
responses:
'200': { description: Pushed }
'500': { description: Backend failure }
delete:
summary: Delete selection input v2
parameters:
- $ref: '#/components/parameters/TailV2'
responses:
'204': { description: Deleted }
'500': { description: Backend failure }
/v1/configuration:
get:
summary: Read configuration
responses:
'200': { description: Configuration, content: { application/json: { schema: { $ref: '#/components/schemas/AnyJson' } } }, headers: { ETag: { schema: { type: string } } } }
'304': { description: Not modified }
'500': { description: Backend failure }
put:
summary: Replace configuration
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AnyJson'
responses:
'200': { description: Replaced }
'500': { description: Backend failure }
delete:
summary: Delete configuration
responses:
'200': { description: Deleted }
'500': { description: Backend failure }
/v1/routing/geoip:
get:
summary: GeoIP lookup
parameters:
- name: ip
in: query
required: true
schema: { type: string }
responses:
'200': { description: GeoIP data, content: { application/json: { schema: { $ref: '#/components/schemas/GeoIpResponse' } } } }
'400': { description: Invalid IP }
'500': { description: Backend failure }
/v1/routing/validate:
get:
summary: Validate routing
parameters:
- name: ip
in: query
required: true
schema: { type: string }
responses:
'200': { description: Allowed }
'403': { description: Access Denied }
'400': { description: Invalid IP }
'500': { description: Backend failure }
/v1/metrics:
post:
summary: Ingest metrics
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MetricsIngress'
responses:
'200': { description: Stored }
'500': { description: Validation/back-end error }
get:
summary: Aggregate metrics
responses:
'200': { description: Aggregated metrics, content: { application/json: { schema: { $ref: '#/components/schemas/AnyJson' } } } }
'500': { description: Backend failure }
/v1/discovery/hosts:
get:
summary: List discovered hosts by namespace
responses:
'200':
description: Discovered hosts keyed by namespace
content:
application/json:
schema:
type: object
additionalProperties:
type: array
items:
$ref: '#/components/schemas/DiscoveryHost'
'500': { description: Backend failure }
/v1/discovery/namespaces:
get:
summary: List discovery namespaces with Confd URIs
responses:
'200':
description: Namespaces with Confd links
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/DiscoveryNamespace'
'500': { description: Backend failure }
/v1/datastore:
get:
summary: List datastore keys
responses:
'200': { description: Keys list, content: { application/json: { schema: { type: array, items: { type: string } } } } }
'500': { description: Backend failure }
/v1/datastore/{key}:
get:
summary: Get a JSON value by key
parameters:
- name: key
in: path
required: true
schema: { type: string }
responses:
'200': { description: JSON value, content: { application/json: { schema: { $ref: '#/components/schemas/AnyJson' } } } }
'404': { description: Not found }
'500': { description: Backend failure }
post:
summary: Create a JSON value at key
parameters:
- name: key
in: path
required: true
schema: { type: string }
- $ref: '#/components/parameters/Ttl'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AnyJson'
responses:
'201': { description: Created }
'409': { description: Conflict (already exists) }
'500': { description: Backend failure }
put:
summary: Update/replace a JSON value at key
parameters:
- name: key
in: path
required: true
schema: { type: string }
- $ref: '#/components/parameters/Ttl'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AnyJson'
responses:
'200': { description: Updated }
'404': { description: Not found }
'500': { description: Backend failure }
delete:
summary: Delete a datastore key
parameters:
- name: key
in: path
required: true
schema: { type: string }
responses:
'204': { description: Deleted }
'500': { description: Backend failure }
/v1/subnets:
get:
summary: List all subnet mappings
responses:
'200': { description: Subnet mappings, content: { application/json: { schema: { type: object, additionalProperties: { type: string } } } } }
'500': { description: Backend failure }
put:
summary: Create or update subnet mappings
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties:
type: string
description: Map of CIDR strings to classification values
responses:
'200': { description: Created }
'400': { description: Invalid CIDR format }
'500': { description: Backend failure }
delete:
summary: Delete all subnet mappings
responses:
'204': { description: Deleted }
'500': { description: Backend failure }
/v1/subnets/byKey/{subnet}:
get:
summary: Get subnet mappings by CIDR prefix
parameters:
- name: subnet
in: path
required: true
schema: { type: string }
responses:
'200': { description: Subnet mappings, content: { application/json: { schema: { type: object, additionalProperties: { type: string } } } } }
'500': { description: Backend failure }
delete:
summary: Delete subnet mappings by CIDR prefix
parameters:
- name: subnet
in: path
required: true
schema: { type: string }
responses:
'204': { description: Deleted }
'500': { description: Backend failure }
/v1/subnets/byValue/{value}:
get:
summary: Get subnet mappings by value
parameters:
- name: value
in: path
required: true
schema: { type: string }
responses:
'200': { description: Subnet mappings, content: { application/json: { schema: { type: object, additionalProperties: { type: string } } } } }
'500': { description: Backend failure }
delete:
summary: Delete subnet mappings by value
parameters:
- name: value
in: path
required: true
schema: { type: string }
responses:
'204': { description: Deleted }
'500': { description: Backend failure }
/v1/operator_ui/modules/blocked_tokens:
get:
summary: List blocked tokens
parameters:
- $ref: '#/components/parameters/Search'
- $ref: '#/components/parameters/Sort'
- $ref: '#/components/parameters/Limit'
responses:
'200': { description: Blocked tokens, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/BlockedToken' } } } } }
'400': { description: Parse error, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
/v1/operator_ui/modules/blocked_tokens/{token}:
get:
summary: Get blocked token
parameters:
- name: token
in: path
required: true
schema: { type: string }
responses:
'200': { description: Blocked token, content: { application/json: { schema: { $ref: '#/components/schemas/BlockedToken' } } } }
'404': { description: Not found }
'400': { description: Parse error, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
/v1/operator_ui/modules/blocked_user_agents:
get:
summary: List blocked user agents
parameters:
- $ref: '#/components/parameters/Search'
- $ref: '#/components/parameters/Sort'
- $ref: '#/components/parameters/Limit'
responses:
'200': { description: Blocked user agents, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/BlockedUserAgent' } } } } }
'400': { description: Parse error, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
/v1/operator_ui/modules/blocked_user_agents/{encoded}:
get:
summary: Get blocked user agent
parameters:
- name: encoded
in: path
required: true
schema: { type: string }
responses:
'200': { description: Blocked user agent, content: { application/json: { schema: { $ref: '#/components/schemas/BlockedUserAgent' } } } }
'400': { description: Parse error, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
/v1/operator_ui/modules/blocked_referrers:
get:
summary: List blocked referrers
parameters:
- $ref: '#/components/parameters/Search'
- $ref: '#/components/parameters/Sort'
- $ref: '#/components/parameters/Limit'
responses:
'200': { description: Blocked referrers, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/BlockedReferrer' } } } } }
'400': { description: Parse error, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
/v1/operator_ui/modules/blocked_referrers/{encoded}:
get:
summary: Get blocked referrer
parameters:
- name: encoded
in: path
required: true
schema: { type: string }
responses:
'200': { description: Blocked referrer, content: { application/json: { schema: { $ref: '#/components/schemas/BlockedReferrer' } } } }
'400': { description: Parse error, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
/v1/health/alive:
get:
summary: Liveness check
responses:
'200': { description: Alive, content: { application/json: { schema: { $ref: '#/components/schemas/HealthStatus' } } } }
/v1/health/ready:
get:
summary: Readiness check
responses:
'200': { description: Ready, content: { application/json: { schema: { $ref: '#/components/schemas/HealthStatus' } } } }
'503': { description: Unready, content: { application/json: { schema: { $ref: '#/components/schemas/HealthStatus' } } } }
components:
parameters:
Tail:
name: tail
in: path
required: true
schema: { type: string }
TailV2:
name: tail
in: path
required: true
schema: { type: string }
Search:
name: search
in: query
required: false
schema: { type: string }
Sort:
name: sort
in: query
required: false
schema: { type: string, enum: [asc, desc] }
Limit:
name: limit
in: query
required: false
schema: { type: integer, minimum: 1 }
Ttl:
name: ttl
in: query
required: false
schema: { type: string, description: Humantime duration }
CorrelationId:
name: correlation_id
in: query
required: false
schema: { type: string }
schemas:
LoginRequest:
type: object
required: [email, password]
properties:
email: { type: string, format: email }
password: { type: string, format: password }
LoginResponse:
type: object
properties:
session_id: { type: string }
session_token: { type: string }
verified_at: { type: string, format: date-time }
expires_at: { type: string, format: date-time }
LogoutRequest:
type: object
required: [session_id]
properties:
session_id: { type: string }
session_token: { type: string }
LogoutResponse:
type: object
properties:
status: { $ref: '#/components/schemas/StatusValue' }
TokenRequest:
type: object
required: [session_id, session_token, grant_type]
properties:
session_id: { type: string }
session_token: { type: string }
scope: { type: string }
grant_type: { type: string, enum: [session] }
TokenResponse:
type: object
required: [access_token, scope, expires_in, token_type]
properties:
access_token: { type: string }
scope: { type: string }
expires_in: { type: integer, format: int64 }
token_type: { type: string, enum: [bearer] }
ErrorResponse:
type: object
properties:
message: { type: string }
AnyJson:
description: Arbitrary JSON value
MetricsIngress:
type: object
additionalProperties:
type: object
additionalProperties: { type: number }
GeoIpResponse:
type: object
properties:
city:
type: object
properties:
name: { type: string }
asn: { type: integer }
is_anonymous: { type: boolean }
BlockedToken:
type: object
properties:
household_token: { type: string }
expire_time: { type: integer, format: int64 }
BlockedUserAgent:
type: object
properties:
user_agent: { type: string }
BlockedReferrer:
type: object
properties:
referrer: { type: string }
DiscoveryHost:
type: object
properties:
name: { type: string }
DiscoveryNamespace:
type: object
properties:
namespace: { type: string }
confd_uri: { type: string }
HealthStatus:
type: object
properties:
status: { $ref: '#/components/schemas/StatusValue' }
StatusValue:
type: string
enum: [Ok, Fail]
Using the OpenAPI Specification
Generating API Clients
The OpenAPI specification can be used to generate client libraries in multiple languages:
Using openapi-generator:
# Generate Python client
openapi-generator generate -i openapi.yaml -g python -o ./python-client
# Generate TypeScript client
openapi-generator generate -i openapi.yaml -g typescript-axios -o ./typescript-client
# Generate Go client
openapi-generator generate -i openapi.yaml -g go -o ./go-client
Using swagger-codegen:
swagger-codegen generate -i openapi.yaml -l python -o ./python-client
Validating the Specification
To validate the OpenAPI specification:
# Using swagger-cli
swagger-cli validate openapi.yaml
# Using spectral
spectral lint openapi.yaml
Next Steps
- Authentication API - Detailed authentication flow
- API Guide Index - Browse all API documentation
- Operations Guide - Day-to-day operational procedures
11 - Troubleshooting Guide
Overview
This guide provides troubleshooting procedures for common issues encountered when operating the AgileTV CDN Manager (ESB3027). Use the diagnostic commands and resolution steps to identify and resolve problems.
Diagnostic Tools
On a server, acd-admin covers the most common checks and fixes by admin target name: acd-admin status, acd-admin logs <target> and acd-admin restart <target>.
Cluster Status
# Check node status
kubectl get nodes
# Check all pods
kubectl get pods -A
# Check events sorted by time
kubectl get events --sort-by='.lastTimestamp'
# Check resource usage
kubectl top nodes
kubectl top pods
Component Status
# Check deployments
kubectl get deployments
# Check statefulsets
kubectl get statefulsets
# Check persistent volumes
kubectl get pvc
kubectl get pv
# Check services
kubectl get services
# Check ingress
kubectl get ingress
Common Issues
Pods Stuck in Pending State
Symptoms: Pods remain in Pending state indefinitely.
Causes:
- Insufficient cluster resources (CPU/memory)
- No nodes match scheduling constraints
- PersistentVolume not available
Diagnosis:
# Describe the pending pod
kubectl describe pod <pod-name>
# Check events for scheduling failures
kubectl get events --field-selector reason=FailedScheduling
# Check node capacity
kubectl describe nodes | grep -A 5 "Allocated resources"
# Check available PVs
kubectl get pv
Resolution:
# Free up resources by scaling down non-critical workloads
kubectl scale deployment <deployment> --replicas=0
# Or add additional nodes to the cluster
# If PV is stuck, delete and recreate
kubectl delete pvc <pvc-name>
kubectl delete pod <pod-name>
Pods Stuck in ContainerCreating
Symptoms: Pods remain in ContainerCreating state.
Causes:
- Image pull failures
- Volume mount issues
- Network configuration problems
Diagnosis:
kubectl describe pod <pod-name>
# Check for image pull errors
kubectl get events | grep -i "failed to pull"
# Check volume mount status
kubectl get events | grep -i "mount"
Resolution:
# For image pull issues, verify image exists and credentials
kubectl get secret <pull-secret-name> -o yaml
# For volume issues, check Longhorn volume status
kubectl get volumes -n longhorn-system
# Delete stuck pod to trigger recreation
kubectl delete pod <pod-name> --force --grace-period=0
Persistent Volume Mount Failures
Symptoms: Pod fails to start with error “AttachVolume.Attach failed for volume… is not ready for workloads” or similar volume attachment errors.
Causes:
- Longhorn volume created but unable to be successfully mounted
- Network connectivity issues between nodes (Longhorn requires iSCSI and NFS traffic)
- Longhorn service unhealthy
- Incorrect storage class configuration
Diagnosis:
# Describe the failing pod to see the error
kubectl describe pod <pod-name>
# Check Longhorn volumes status
kubectl get volumes -n longhorn-system
# Check Longhorn UI for detailed volume status
kubectl port-forward -n longhorn-system svc/longhorn-frontend 8080:80
# Access: http://localhost:8080
Resolution:
# Longhorn traffic runs over the pod network, which deploy trusts between nodes.
# Check that each node's firewall still matches what deploy set up: run deploy
# again, and it stops with an [ACD-VERIFY-...] code if a node's network differs.
# Check Longhorn is healthy
kubectl get pods -n longhorn-system
# If volume is stuck, delete PVC and pod to trigger recreation
kubectl delete pvc <pvc-name>
kubectl delete pod <pod-name>
Pods in CrashLoopBackOff
Symptoms: Pods repeatedly crash and restart.
Causes:
- Application configuration errors
- Missing dependencies (database not ready)
- Resource limits too low
- Liveness probe failures
Diagnosis:
# View current logs
kubectl logs <pod-name>
# View previous instance logs
kubectl logs <pod-name> -p
# Describe pod for restart reasons
kubectl describe pod <pod-name>
# Check if dependencies are healthy
kubectl get pods | grep -E "(postgres|kafka|redis)"
Resolution:
# For dependency issues, wait for dependencies to be ready
kubectl wait --for=condition=Ready pod/<dependency-pod> --timeout=300s
# For resource issues, increase limits
kubectl edit deployment <deployment-name>
# For configuration issues, check ConfigMaps and Secrets
kubectl get configmap <configmap-name> -o yaml
kubectl get secret <secret-name> -o yaml
# Restart the deployment
kubectl rollout restart deployment/<deployment-name>
Pods in Terminating State
Symptoms: Pods stuck in Terminating state indefinitely.
Causes:
- Volume detachment issues
- Node communication problems
- Finalizer blocking deletion
Diagnosis:
kubectl describe pod <pod-name>
# Check if node is reachable
kubectl get nodes
# Check finalizers
kubectl get pod <pod-name> -o jsonpath='{.metadata.finalizers}'
Resolution:
# Force delete the pod
kubectl delete pod <pod-name> --force --grace-period=0
# If node is unreachable, drain and remove from cluster
acd-admin drain <node-name>
# Add --force only if drain refuses and you accept losing the pods it names.
# Pods on an unreachable node never finish terminating, so once drain is
# waiting only on those, press Ctrl-C. The node stays drained.
kubectl delete node <node-name>
acd-admin drain names each pod it is waiting on, with when its deletion started and the finalizer holding it, if any.
Leftover acd-admin Pods
Symptoms: acd-admin-* pods remain in the acd-system namespace after an SSH session was lost.
Cause: Each acd-admin command runs in its own pod, which is removed when the command ends, Ctrl-C included. A session lost so abruptly that the command cannot clean up leaves its pod behind, holding its resources. Its command may still be running, such as a drain or the wait of a restart. A drain never evicts these pods. A leftover pod ends on its own at its session deadline, which for a drain, shell or exec is 12 hours and for logs -f 24 hours: see acd-admin.
Diagnosis:
# The age tells a leftover from a command someone is running now
kubectl get pods -n acd-system
Resolution:
kubectl delete pod -n acd-system <pod-name>
Deleting the pod stops its command. A drain stopped this way leaves the node as Ctrl-C does: drained, with the pods already moved where they went. A restart already started carries on in the cluster.
Service Unreachable
Symptoms: Service endpoints not accessible.
Causes:
- No ready pods backing the service
- Network policy blocking traffic
- Service port mismatch
Diagnosis:
# Check service endpoints
kubectl get endpoints <service-name>
# Check if pods are ready
kubectl get pods -l app=<label>
# Check network policies
kubectl get networkpolicies
# Test connectivity from within cluster
kubectl run test --rm -it --image=busybox -- wget -O- <service-name>:<port>
Resolution:
# Ensure pods are ready and matching service selector
kubectl get pods --show-labels
# Check service selector matches pod labels
kubectl get service <service-name> -o jsonpath='{.spec.selector}'
# Temporarily disable network policy for testing
kubectl edit networkpolicy <policy-name>
Ingress Not Working
Symptoms: External access via ingress fails.
Causes:
- Traefik ingress controller not running
- Ingress configuration errors
- TLS certificate issues
- DNS resolution problems
Diagnosis:
# Check Traefik pods
kubectl get pods -n kube-system -l app.kubernetes.io/name=traefik
# Check ingress resources
kubectl get ingress
# Describe ingress for errors
kubectl describe ingress <ingress-name>
# Check Traefik logs
kubectl logs -n kube-system -l app.kubernetes.io/name=traefik
# Test DNS resolution
nslookup <hostname>
Resolution:
# Restart Traefik, which runs as a DaemonSet: one pod per node
kubectl rollout restart daemonset -n kube-system traefik
# Fix ingress configuration
kubectl edit ingress <ingress-name>
# Renew or recreate TLS secret
kubectl create secret tls <secret-name> --cert=tls.crt --key=tls.key \
--dry-run=client -o yaml | kubectl apply -f -
# Verify hostname matches certificate
openssl x509 -in tls.crt -noout -subject -issuer
Database Connection Failures
Symptoms: Application cannot connect to PostgreSQL.
Causes:
- PostgreSQL cluster not ready
- Connection pool exhausted
- Network connectivity issues
- Authentication failures
Diagnosis:
# Check PostgreSQL cluster status
kubectl get clusters
# Check PostgreSQL pods
kubectl get pods -l app.kubernetes.io/name=postgresql
# Check PostgreSQL logs
kubectl logs -l app.kubernetes.io/name=postgresql
# Test connectivity
kubectl exec -it <app-pod> -- psql -h <postgres-service> -U <user> -d <database>
Resolution:
# Wait for PostgreSQL to be ready
kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=postgresql --timeout=300s
# Check connection string in application config
kubectl get secret <secret-name> -o jsonpath='{.data}' | base64 -d
# Restart application pods
kubectl rollout restart deployment/<deployment-name>
Kafka Connection Issues
Symptoms: Application cannot connect to Kafka.
Causes:
- Kafka controllers not ready
- Topic not created
- Network connectivity issues
Diagnosis:
# Check Kafka pods
kubectl get pods -l app.kubernetes.io/name=kafka
# Check Kafka logs
kubectl logs -l app.kubernetes.io/name=kafka
# List topics
kubectl exec -it <kafka-pod> -- kafka-topics.sh --bootstrap-server localhost:9092 --list
Resolution:
# Wait for Kafka controllers to be ready
kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=kafka --timeout=300s
# Create missing topic
kubectl exec -it <kafka-pod> -- kafka-topics.sh --bootstrap-server localhost:9092 \
--create --topic <topic-name> --partitions 3 --replication-factor 3
# Restart application to reconnect
kubectl rollout restart deployment/<deployment-name>
Redis Connection Issues
Symptoms: Application cannot connect to Redis.
Diagnosis:
# Check Redis pods
kubectl get pods -l app.kubernetes.io/name=redis
# Check Redis logs
kubectl logs -l app.kubernetes.io/name=redis
# Test connectivity
kubectl exec -it <redis-pod> -- redis-cli ping
Resolution:
# Wait for Redis to be ready
kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=redis --timeout=300s
# Restart application
kubectl rollout restart deployment/<deployment-name>
High Memory Usage
Symptoms: Pods approaching or hitting memory limits.
Diagnosis:
# Check memory usage
kubectl top pods
# Check OOMKilled pods
kubectl get pods --field-selector=status.phase=Failed
# Check for memory leaks in logs
kubectl logs <pod-name> | grep -i "memory\|oom"
Resolution:
# Temporarily increase memory limit
kubectl edit deployment <deployment-name>
# Or scale horizontally if HPA is enabled
kubectl scale deployment <deployment-name> --replicas=<n>
# Long-term: set the resources in /etc/acd/deploy/values.override.yaml and run deploy
High CPU Usage
Symptoms: Pods consistently using high CPU.
Diagnosis:
# Check CPU usage
kubectl top pods
# Check for runaway processes
kubectl top pods --sort-by=cpu
Resolution:
# Scale horizontally if HPA is enabled
kubectl scale deployment <deployment-name> --replicas=<n>
# Or increase CPU limits
kubectl edit deployment <deployment-name>
Persistent Volume Issues
Symptoms: PVC not binding or volume errors.
Diagnosis:
# Check PVC status
kubectl get pvc
# Check PV status
kubectl get pv
# Check Longhorn volumes
kubectl get volumes -n longhorn-system
# Check Longhorn UI for details
kubectl port-forward -n longhorn-system svc/longhorn-frontend 8080:80
Resolution:
# For stuck PVC, delete and recreate
kubectl delete pvc <pvc-name>
kubectl delete pod <pod-name>
# For Longhorn issues, check Longhorn UI
# Access via http://localhost:8080
# Recreate Longhorn volume if necessary
Zitadel Authentication Failures
Symptoms: Users cannot authenticate via Zitadel.
Causes:
- CORS configuration mismatch
- External domain misconfigured
- Zitadel pods not healthy
Diagnosis:
# Check Zitadel pods
kubectl get pods -l app.kubernetes.io/name=zitadel
# Check Zitadel logs
kubectl logs -l app.kubernetes.io/name=zitadel
# Verify external domain configuration
grep -A 5 ExternalDomain /etc/acd/deploy/values.yaml
Resolution:
Browse to the main manager hostname, the first entry of manager_hostnames in /etc/acd/deploy/group_vars/all.yaml. deploy sets Zitadel’s external domain from it. Sign-ins under any other name, including a machine’s IP address, fail.
# Restart Zitadel
kubectl rollout restart deployment -l app.kubernetes.io/name=zitadel
Certificate Errors
Symptoms: TLS/SSL errors in browser or API calls.
Diagnosis:
# Check certificate expiration
kubectl get secret <tls-secret> -o jsonpath='{.data.tls\.crt}' | base64 -d | \
openssl x509 -noout -dates
# Check certificate subject
kubectl get secret <tls-secret> -o jsonpath='{.data.tls\.crt}' | base64 -d | \
openssl x509 -noout -subject -issuer
Resolution:
Replace the certificate and key in /etc/acd/deploy/certs/ and run deploy. See TLS Certificates.
/mnt/esb3027/deploy
Log Collection
Support Dumps (Recommended)
The AgileTV CDN Manager ships with acd-support-dump, a single command that captures a point-in-time diagnostic snapshot of the whole cluster into one compressed, redacted tarball suitable for attaching to a support ticket. It is preinstalled on every control-plane node and works in air-gapped environments with no additional setup or downloads.
Running a support dump
Run the command from a control-plane (server) node:
acd-support-dump
The tool writes a tarball named support-dump-<hostname>-<timestamp>.tar.gz to the current directory and prints the path when it finishes. Collection usually takes a minute or two.
To choose the output location, pass a path as the first argument:
acd-support-dump /tmp/support-dump.tar.gz
See the acd-support-dump reference for its options and exit status.
What the dump contains
A support dump captures the management-plane state needed to diagnose the majority of field issues without direct access to the cluster:
- Pod logs from the Manager workloads, covering up to the last 72 hours.
- Cluster resources and events, including
OOMKilled, restart reasons, and eviction events. - Node system logs (
journald) — kernel messages such as the out-of-memory killer, plus k3s, containerd, and systemd unit output. - Database health for PostgreSQL and Redis — connectivity, version, sizes, and replication/keyspace statistics. This is health information only; application data is never collected.
- Longhorn volume and replica health.
- Metric history for the last 72 hours (node CPU, memory, and disk; per-container memory; container restarts; and volume usage) from the monitoring stack.
- Automated analyzers that flag common failure modes — out-of-memory kills, disk exhaustion, and unhealthy or crash-looping pods — as a quick triage summary.
Kubernetes Secret values are excluded, and the tool’s built-in redactors run over the collected data before the tarball is written.
Note: A support dump captures Manager (management-plane) state only; it does not collect data-plane state. It also captures database health, not database contents — for data, use the backup procedures in the Operations Guide.
Sharing a support dump
Attach the generated .tar.gz to your support ticket. Because it contains log output and cluster configuration, treat it as potentially sensitive and share it only through approved support channels.
Manual Log Collection
If acd-support-dump is unavailable — for example, on an older installation — collect the essential logs by hand:
# Capture timestamp once to ensure consistency
TS=$(date +%Y%m%d-%H%M%S)
# Create log collection directory
mkdir -p ~/cdn-logs-$TS
cd ~/cdn-logs-$TS
# Collect pod logs
for pod in $(kubectl get pods -o name); do
kubectl logs $pod > ${pod#pod/}.log 2>&1
kubectl logs $pod -p > ${pod#pod/}.previous.log 2>&1 || true
done
# Collect cluster events
kubectl get events --sort-by='.lastTimestamp' > events.log
# Collect pod descriptions
for pod in $(kubectl get pods -o name); do
kubectl describe $pod > ${pod#pod/}.describe.txt
done
# Compress for transfer
tar czf cdn-logs-$TS.tar.gz *.log *.txt
Emergency Procedures
Complete Cluster Recovery
If the cluster is completely down:
Assess node status:
kubectl get nodesRestart k3s on every machine. Start it on all the servers at once, without waiting for one to finish before the next, then on the agents. The unit is
k3son servers andk3s-agenton agents:# On every server, at the same time systemctl restart --no-block k3s # Then on every agent systemctl restart --no-block k3s-agentThe cluster datastore needs a majority of the servers running, so a server’s k3s does not finish starting until enough other servers have started too. With three servers, the Kubernetes API returns once two are up. Restart servers one at a time only during maintenance, while the rest of the cluster is running: see Safety.
Restore from backup if necessary: see Backups.
The first server holds /etc/acd/deploy and the backups. Keep a copy of the latest backup folder off it, so that you can recover the configuration if the first server is lost. The backup folder holds every Kubernetes Secret: protect the copy as Backups describes.
Data Recovery
For data recovery scenarios:
- PostgreSQL: Use Cloudnative PG backup/restore
- Longhorn: Restore from volume snapshots
- Kafka: Topics other than
selection_inputsurvive the loss of one broker.selection_inputhas a single replica: see Data Replication
Getting Help
If issues persist:
- Generate a support dump with
acd-support-dump(see Log Collection), or collect logs manually if the tool is unavailable - Check release notes for known issues
- Contact support with the support dump and a description of the issue
Next Steps
After resolving issues:
- Operations Guide - Preventive maintenance procedures
- Configuration Guide - Verify configuration is correct
- Architecture Guide - Understand component dependencies
12 - Command Line Reference
Overview
These pages are the reference for each command line tool shipped with the CDN Manager: what it accepts, what it reads and writes, and how it exits. For step-by-step use, follow the links under each page’s See Also.
| Command | Where it runs | Purpose |
|---|---|---|
deploy | The first server, from the installation ISO | Installs, upgrades and reconfigures the cluster, and takes backups |
acd-admin | Any server, as root | Restarts, logs, shells, status and node maintenance by admin target name |
acd-support-dump | Any server, as root | Writes a redacted diagnostic tarball for AgileTV support |
12.1 - deploy
Synopsis
deploy init [prod|lab]
deploy [run] [-i <inventory>] [<ansible-playbook option>...]
deploy backup [-i <inventory>] [<ansible-playbook option>...]
deploy -h | --help
Description
deploy is on the installation ISO, at /mnt/esb3027/deploy; it is not on the PATH. Run it as root on the first server in the inventory, with the ISO mounted at /mnt/esb3027. It reaches every machine in the inventory, the first server included, over SSH.
deploy reads its configuration from /etc/acd/deploy, applies it to every machine, and installs or upgrades the cdn-platform Helm release. The first run installs; every later run re-applies the configuration and changes only what differs. Running it again is safe, including after a failed run. It is the only supported way to change what is installed.
When deploy stops on a problem it recognizes, its message starts with a code such as [ACD-TLS-HOSTNAME]. Each code is described in Installation Troubleshooting.
With -h or --help, deploy prints its usage and exits.
Commands
init
deploy init [prod|lab]
Prepares the first server. Run it once before the first deploy, and again after mounting a new ISO elsewhere. It:
- Installs
ansible-corefrom the operating system’s repositories withdnf, unless it is already installed. - Installs the Ansible collections bundled on the ISO, every time, replacing any version already installed.
- Creates
/etc/acd/deployand every file in it that is missing: see Files. It never overwrites a file that exists. - Points
preset.yamlat the chosen preset on the ISO.
| Argument | Effect |
|---|---|
prod | Uses the production preset. A new configuration gets prod when no preset is given. |
lab | Uses the lab preset. |
| (none) | On an existing configuration, keeps the current preset and re-points preset.yaml at the mounted ISO. |
deploy init takes no other arguments.
run
deploy [run] [-i <inventory>] [<ansible-playbook option>...]
Runs the deployment. run is the default and can be left out. Every run takes a backup before it changes anything, even with the preflight checks skipped, and stops if the backup fails. The stages of a run are described in Running deploy.
backup
deploy backup [-i <inventory>] [<ansible-playbook option>...]
Takes a backup and changes nothing. It runs only the gather-inputs and backup stages, so it also skips the preflight checks. See Files for what a backup holds.
Options
| Option | Commands | Effect |
|---|---|---|
-i <inventory> | run, backup | Uses this inventory file instead of /etc/acd/deploy/inventory.ini. The settings still come from /etc/acd/deploy/group_vars/all.yaml, which must exist. |
-h, --help | all | Prints the usage and exits with status 0. |
--skip-tags preflight | run | Skips the preflight checks. The run still works out every setting and takes the backup. The check of each machine’s network after it is changed cannot be skipped. |
-v, -vv, -vvv | run, backup | More detailed output, for support. |
Every other option is passed to ansible-playbook. These are rejected, because every run must start with gathering inputs and the backup:
| Option | Rejected with |
|---|---|
--start-at-task, or any prefix of it from --sta | every command |
--skip-tags naming always | every command |
-t, --tags, or any prefix of --tags from --ta | deploy backup |
--limit | run: the run stops with [ACD-PARTIAL-RUN], because what each machine’s firewall trusts depends on all of them |
Environment
| Variable | Effect |
|---|---|
ACD_CONFIG_DIR | The configuration folder. Default: /etc/acd/deploy. Every path on this page under /etc/acd/deploy moves with it. |
Files
Configuration
In /etc/acd/deploy, created by deploy init and edited by you:
| File | Contents |
|---|---|
inventory.ini | The machines and their roles. See Inventory. |
group_vars/all.yaml | The site settings. See Site Settings. |
values.override.yaml | Your application settings. See Application Settings. |
preset.yaml | A link to the chosen preset on the ISO. Change it with deploy init prod or deploy init lab. |
certs/ | The TLS certificate and key. See TLS Certificates. |
maxmind/ | The MaxMind databases. See GeoIP Databases. |
README.md | A copy of the configuration reference shipped on the ISO. |
Written by a Run
In /etc/acd/deploy:
| File | Contents |
|---|---|
deploy.log | Every run’s output, newest last, each under a header with its start time in UTC |
values.yaml | The Helm values the cluster received. Generated: do not edit. See Application Settings. |
decision-record.json | What the run worked out about the cluster and each machine |
probe/<machine>.json | Everything the run read from each machine |
support-bundle-acd.tar.gz | Written only when the final health check fails |
Backups
Each run, and each deploy backup, writes a folder on the first server, readable only by root: <backup_dir>/<UTC time>-<release>/, where <release> is the version on the installation ISO, such as /var/lib/acd/backups/20261019T201232Z-1.8.0/. backup_dir is a site setting and defaults to /var/lib/acd/backups. deploy never deletes a backup.
| File | Contents |
|---|---|
<machine>.tar.gz | That machine’s files as they were before the run: k3s configuration, unit files and token, network connection profiles, firewall, sysctl and kernel module settings, SELinux configuration, /etc/hosts and routing table names. The first server’s archive also holds /etc/acd/deploy, except certs/, maxmind/ and deploy.log. On a running cluster, one server’s archive also holds an etcd snapshot of the cluster. |
<machine>.files.sha256 | A checksum for every file in that archive |
SHA256SUMS | A checksum for each archive |
manifest.yaml | The release, the machines, the run time and the etcd snapshot’s path |
A backup holds configuration only, never the data stored in the cluster. See Running deploy.
Exit Status
| Status | Meaning |
|---|---|
0 | The run, backup or initialization finished, or -h printed the usage |
1 | deploy refused to start: an unknown command or bad arguments, a rejected option, no configuration in /etc/acd/deploy (run deploy init), ansible-core missing, a preset.yaml that points at an ISO no longer mounted there, or a failed deploy init |
| other | The exit status of ansible-playbook: the run started and stopped on a failure. The last message names the cause, usually with an [ACD-…] code. |
Examples
Prepare a new lab installation:
/mnt/esb3027/deploy init lab
Install, or apply a changed configuration:
/mnt/esb3027/deploy
Apply the configuration without the preflight checks:
/mnt/esb3027/deploy --skip-tags preflight
Take a backup and change nothing:
/mnt/esb3027/deploy backup
See Also
- Running deploy: the stages of a run, checking and restoring backups, and what to send support
- Installation Troubleshooting: every
[ACD-…]code acd-admin: day-to-day operations oncedeployhas installed the cluster
12.2 - acd-admin
Synopsis
acd-admin list
acd-admin status [<target>]
acd-admin volumes [<name> [--avg]]
acd-admin logs <target> [-f] [-n <lines>] [-p] [-c <container>] [--since <time>] [--until <time>]
acd-admin restart <target> [--timeout <duration> | --no-wait | --node <node>]
acd-admin shell <target> [--pod <pod>] [--node <node>] [-l <selector>] [-c <container>]
acd-admin exec <target> [--pod <pod>] [--node <node>] [-l <selector>] [-c <container>] -- <command> [<argument>...]
acd-admin exec <target> --all [-l <selector>] [-c <container>] -- <command> [<argument>...]
acd-admin cordon <node>
acd-admin uncordon <node>
acd-admin drain <node> [--force]
acd-admin help [<command>]
acd-admin [<command>] -h | --help
acd-admin -V | --version
Description
acd-admin manages the running cluster by admin target: a short name such as manager, kafka or postgresql. It works out which Kubernetes objects, labels and namespaces a target means, and handles each kind the safe way for it. acd-admin list prints the valid names. A name that is not a target is an error that lists them.
It is installed on every machine at /usr/local/bin/acd-admin, and on every server /usr/bin/acd-admin links to it, so sudo acd-admin works without changing the sudo configuration. Run it on a server, as root or with sudo, after deploy has installed the cluster. Anywhere else it stops at once with:
Error: run acd-admin on a server node as root, or set KUBECONFIG.
Before deploy has installed the cdn-platform release, it stops with:
Error: the cdn-platform chart is not installed; run deploy first.
Each command runs in a short-lived pod in the acd-system namespace, started from the release that is installed, so the tool always matches it. The pod is removed when the command ends, Ctrl-C included, and when the session is hung up or terminated. A pod left behind by a dropped session can be removed: see Leftover acd-admin Pods.
Each session also has a deadline, so that one whose terminal is gone still ends. At the deadline the session stops, whether or not anyone is still attached:
| Command | Deadline |
|---|---|
logs -f | 24 hours. To carry on, run it again, with --since to pick up where it stopped. |
drain | 12 hours. The node stays drained; run drain again to carry on where it stopped. |
shell, exec | 12 hours |
restart, waiting | Its --timeout plus 5 minutes, when the --timeout is written as whole numbers with units, such as 90s or '1h 30m'. A --timeout in any other form gets 12 hours, which is also the most any --timeout gets. Without --timeout, 20 minutes. |
restart --no-wait, restart --node | 10 minutes |
any other command, logs without -f included | 10 minutes |
acd-admin never changes a chart setting. A later deploy leaves its changes alone, and does not undo a drain.
restart, cordon, uncordon, drain, and opening a shell or exec session each record a Kubernetes Event on the affected object. The Event’s message holds the command line, quoted as a shell would need, the user and the server’s hostname:
acd-admin exec manager -- sh -c 'exit 7' run by alice on manager-1
The user is the one who logged in to the server, which sudo and su keep: alice when she logged in as herself and ran sudo acd-admin or sudo -i, and root only after a root login. This is attribution, not proof: it is only as reliable as the server’s logins. acd-admin status <target> shows the Events of restart, which are on the target’s workloads, and of shell and exec, which are on the pod the session ran in. A shell or exec Event is shown only while its pod exists. Once the pod is replaced, by a restart for example, read it with kubectl:
kubectl get events -n default --field-selector reason=Exec
kubectl get events -n default --field-selector reason=Shell
The Events of cordon, uncordon and drain are on the node, in the default namespace:
kubectl get events -n default --field-selector involvedObject.kind=Node
The command line includes every argument, exec’s command among them, and anyone who can read Events in the namespace can see it until the Event expires, after about an hour by default. Read or save an Event you need before then. Never pass a password or other secret as an argument: pass it on standard input (not with --all, which gives the command no standard input).
Bash completion for commands, options, targets and nodes is installed on every machine.
Commands
list
acd-admin list
Prints every admin target and its kind, including targets with no pods.
TARGET KIND
status
acd-admin status [<target>]
With no target, prints a table of admin targets, then a table of nodes:
TARGET READY RESTARTS/1H HEALTH CHANGED
alertmanager 1/1 0 ok 103m ago
confd 1/1 0 ok 2h ago
confsej 2/2 0 ok 104m ago
director 0/0 0 ok -
gateway 2/2 0 ok 104m ago
geoip-loader 1/1 0 ok 104m ago
grafana 2/2 0 ok 104m ago
kafka 3/3 0 ok 103m ago
kube-state-metrics 1/1 0 ok 104m ago
manager 3/3 0 ok 57m ago
metrics-aggregator 1/1 0 ok 104m ago
mib-frontend 2/2 0 ok 104m ago
node-exporter 4/4 0 ok 2h ago
postgresql 3/3 0 ok 55m ago
redis 1/1 0 ok 2h ago
selection-input 1/1 0 ok 2h ago
vector 1/1 0 ok 2h ago
victoria-metrics 1/1 0 ok 103m ago
victoria-metrics-longterm 1/1 0 ok 103m ago
vmagent 1/1 0 ok 103m ago
vmalert 1/1 0 ok 118m ago
zitadel 6/6 0 ok 104m ago
NODE READY ROLES SCHEDULING DIRECTOR
manager-1 yes control-plane,etcd schedulable no
manager-2 yes control-plane,etcd schedulable no
manager-3 yes control-plane,etcd schedulable no
manager-4 yes - schedulable no
The admin target table:
| Column | Contents |
|---|---|
TARGET | The admin target |
READY | Ready pods out of desired pods, such as 2/3 |
RESTARTS/1H | Containers whose last restart was in the past hour. A container that restarted several times counts once, so this is a lower bound. |
HEALTH | The first that applies: down (pods are desired but none is Ready), rolling (a new version is rolling out; never shown for kafka or postgresql), degraded (some desired pods are not Ready), ok |
CHANGED | How long ago a pod of the target last became Ready or stopped being Ready, such as 12s ago; - for a target with no pods |
The node table:
| Column | Contents |
|---|---|
NODE | The node |
READY | yes when the node reports Ready, otherwise no |
ROLES | The node’s roles, such as control-plane,etcd; - for an agent |
SCHEDULING | schedulable, cordoned, or drained for a drained node. The column ignores taints, so a server with the CriticalAddonsOnly taint shows schedulable, although cordon and drain do not count it as schedulable. |
DIRECTOR | yes when the node is labeled to run the CDN Director, otherwise no |
status exits 0 whatever the health it shows. To check health, read HEALTH: every admin target should show ok.
With a target, prints one line per pod (POD, NODE, READY, RESTARTS, AGE, LAST TERMINATION). For kafka it adds the Strimzi conditions, and for postgresql the CloudNativePG phase and the current primary. Then it lists the Volumes the target’s pods mount, if any. It ends with the target’s recent Warning events and the audit Events of acd-admin itself (WHEN, KIND, OBJECT, REASON, MESSAGE).
The Volume table of acd-admin status redis looks like this:
VOLUME SIZE USED HEALTH REPLICAS
maxmind 1Gi unknown healthy 1/1
redis-data 8Gi unknown degraded 1/2
The output is for reading; there is no machine-readable format.
volumes
acd-admin volumes [<name> [--avg]]
A Volume is the persistent storage that a pod of an admin target mounts, named by its claim (PersistentVolumeClaim). volumes lists every claim in the platform’s namespace. It only reads and changes nothing.
With no name, it prints one row per Volume:
VOLUME TARGET SIZE USED HEALTH REPLICAS
maxmind alertmanager,confd +3 more 1Gi unknown healthy 3/3
data-kafka-0 kafka 20Gi ━━╸─────── 25% healthy 1/1
redis-data redis 8Gi ━━━━━━━━╸─ 88% rebuilding 1/3
scratch - 2Gi unknown - -
| Column | Contents |
|---|---|
VOLUME | The claim name |
TARGET | The admin targets whose pods mount the Volume; - when none does |
SIZE | The claim’s size |
USED | How full the Volume’s filesystem is; unknown when no running pod mounts it |
HEALTH | Longhorn’s view of the Volume, in the words below; - for a Volume that Longhorn does not back |
REPLICAS | Replicas in sync out of the replicas the Volume is configured to have, such as 2/3; - for a Volume that Longhorn does not back |
| Health | Meaning |
|---|---|
healthy | Every configured replica is in sync. |
rebuilding | Fewer replicas are in sync than configured, and one is catching up. It becomes healthy when the rebuild ends. |
degraded | Fewer replicas are in sync than configured, and none is catching up. |
faulted | No replica is healthy. |
detached | The Volume is not attached to any node, such as when its workload is scaled down. This is not a fault. |
unknown | Longhorn cannot assess the Volume, or its records could not be read. |
On a narrow terminal, the less important columns are left out, but VOLUME and HEALTH always show. Piped or redirected output always has every column.
One Volume
With a Volume name, volumes prints that Volume in detail. The detail adds the Longhorn volume name (as Longhorn’s events and support dumps name it), the access mode, the pods that mount the Volume and their nodes, and the node and state of each replica.
Volume: data
Target: kafka
Longhorn: pvc-data
Size: 20Gi
Used: ━━╸─────── 25% 5.0 GiB of 20.0 GiB
Health: healthy
Mode: ReadWriteOnce
Locality: local (pod and a running replica on node-1)
IO (last 1s): read 1.50 MiB/s 120 IOPS 0.80 ms, write 4.00 KiB/s 6 IOPS 1.05 ms
POD NODE
kafka-0 node-1
REPLICA NODE STATE
node-1 running
Locality says whether a running replica is on the node where the Volume’s pod runs: local or remote. It is - when there is no single pod node, as for a detached or ReadWriteMany Volume.
For a Volume with one replica, Locality decides all of its IO: when it is remote, every read and write crosses the network to another node. For a Volume with several replicas, every write goes to every replica, so write latency includes the network even when Locality is local. A remote multi-replica Volume is not, by itself, the cause of slow writes.
IO (last 1s) is the Volume’s read and write load over the last second: throughput, IOPS and latency.
| Option | Effect |
|---|---|
--avg | Shows the average over the last 5 minutes, IO (5m avg), from the platform’s VictoriaMetrics instead. |
Only --avg uses VictoriaMetrics. When VictoriaMetrics does not answer, volumes --avg prints the rest of the detail, then exits 1 with an error.
Partial Data
When part of the storage information cannot be read, such as a node’s kubelet that does not answer, only the affected cells show unknown, a warning goes to standard error, and volumes still exits 0. Only failing to list the claims is an error. For example:
warning: reading the kubelet on node node-2 failed; fill of its Volumes is unknown: HTTP 503
logs
acd-admin logs <target> [-f] [-n <lines>] [-p] [-c <container>] [--since <time>] [--until <time>]
Shows the logs of every pod and container of the target, init containers excluded. Each line starts with its pod, and its container when the pod has more than one: <pod>[/<container>] <line>. A bounded window is merged in time order across pods.
| Option | Effect |
|---|---|
-f, --follow | Keeps printing new lines, including from pods created meanwhile, such as during a restart. |
-n, --lines <lines> | Shows only the last <lines> lines of each container. Default: all. Not with --until. |
-p, --previous | Shows the previous instance of each container, such as one that crashed. |
-c, --container <container> | Shows only this container. |
--since <time> | Shows lines from this time on. |
--until <time> | Shows lines up to this time. Not with --follow. |
<time> is a duration before now, such as 2m, '1h 30m' or '10m ago', or a timestamp such as '2026-10-16 12:00:00' or 2026-10-16T12:00:00Z. Quote a value that contains a space, as shown: unquoted, --since 1h 30m is refused with unexpected argument '30m'. Timestamps are always UTC, never the server’s local time, unlike journalctl. Only the logs the cluster still holds are available: those of the current and the previous instance of each container.
restart
acd-admin restart <target> [--timeout <duration> | --no-wait | --node <node>]
Restarts every pod of the target, the safe way for its kind: a rolling restart for ordinary workloads, a broker-by-broker rolling update through Strimzi for kafka, and for postgresql a CloudNativePG restart of the replicas first, then a switchover of the primary.
By default it waits, showing progress such as 2/3 pods updated, until the restart is complete. Ctrl-C stops the waiting only: the restart carries on, and acd-admin says so. A target with nothing running is left alone, and acd-admin says so.
| Option | Effect |
|---|---|
--timeout <duration> | How long to wait, such as 90s or 10m, before exiting non-zero and reporting what is stuck. The restart carries on, and nothing is rolled back. Default: 5m for a Deployment, 10m for a StatefulSet, DaemonSet or postgresql, and 15m for kafka. A target with several kinds of workload gets the longest. |
--no-wait | Starts the restart and returns at once. Not with --timeout. |
--node <node> | Restarts only the target’s pods on this node, by deleting every one of them for its controller to recreate. The scheduler places the replacements, which may start on another node. Returns once the deletions are requested, without waiting for the pods to go. Refused for kafka and postgresql, whose operators restart their own pods. Not with --timeout or --no-wait. |
A workload that a restart would not roll is refused before anything changes. The message says why, and what to do where there is an alternative. For example, a workload with the OnDelete update strategy is restarted one node at a time with --node.
Most admin targets are one workload. zitadel is two, the Deployments cdn-platform-zitadel and cdn-platform-zitadel-login, so restart zitadel --node <node> deletes the pods of both on that node.
shell
acd-admin shell <target> [--pod <pod>] [--node <node>] [-l <selector>] [-c <container>]
Opens an interactive session in one Ready pod of the target:
| Target | Session |
|---|---|
postgresql | psql on the current primary |
kafka | A shell on a broker, with the Kafka command line tools on the PATH and the bootstrap address set |
| any other | /bin/sh |
For example, to list the Kafka topics, run this in acd-admin shell kafka:
kafka-topics.sh --bootstrap-server "$BOOTSTRAP_SERVER" --list
Caution:
shell postgresqlopenspsqlas the database superuser. Every statement runs with full rights over every database of the manager, and nothing asks for confirmation. Prefer a replica (-l cnpg.io/instanceRole=replica) for reading, and run changes on the primary only when you mean to.
| Option | Effect |
|---|---|
--pod <pod> | Uses this pod. |
--node <node> | Uses the target’s pod on this node. |
-l, --selector <selector> | Uses a pod of the target whose labels match <selector>. |
-c, --container <container> | Uses this container instead of the pod’s first. |
Without --pod, --node or --selector, it uses the first Ready pod. For postgresql it uses the primary, and refuses if the primary is not Ready, even when replicas are. With only --selector on postgresql, it uses the primary if the selector matches it, and otherwise the first Ready pod that matches. To use a replica, pass -l cnpg.io/instanceRole=replica. If no Ready pod matches, it refuses and lists the pods.
The options combine, and each only narrows the target’s own pods: a selector never reaches a pod outside the target. <selector> takes the label selector syntax of kubectl -l, such as key=value, key!=value, key in (a,b) or !key. The cluster checks it: one it cannot parse is refused with the cluster’s error before anything runs in a pod.
exec
acd-admin exec <target> [--pod <pod>] [--node <node>] [-l <selector>] [-c <container>] -- <command> [<argument>...]
acd-admin exec <target> --all [-l <selector>] [-c <container>] -- <command> [<argument>...]
Runs a command, exactly as given after --, in one pod of the target, chosen as for shell. It gets a terminal only when both standard input and standard output are terminals, so output can be redirected or piped.
On kafka the command runs in the same environment as shell kafka: the Kafka command line tools on the PATH and the bootstrap address in BOOTSTRAP_SERVER. No shell parses the command in the pod, though. Each argument arrives as your own shell on the server left it, and that shell has already replaced $BOOTSTRAP_SERVER with its own value, which is empty. To use the pod’s variables, run the command through sh -c, in single quotes:
acd-admin exec kafka -- sh -c 'kafka-topics.sh --bootstrap-server "$BOOTSTRAP_SERVER" --list'
A command that cannot start, such as one not found in the container, prints the reason on standard error. On kafka the pod’s shell reports it, and exits 127.
| Option | Effect |
|---|---|
--pod <pod> | Runs in this pod. |
--node <node> | Runs in the target’s pod on this node. |
-l, --selector <selector> | Runs in a pod of the target whose labels match <selector>, as for shell. With --all, runs in every Ready pod it matches. |
--all | Runs in every Ready pod of the target, one after another, with no terminal and no standard input. Each line starts with its pod. Pods that are not Ready are skipped and named before the run starts. Ends with a summary of the pods where the command failed and the pods skipped, and exits non-zero only if the command failed in a pod. Not with --pod or --node. |
-c, --container <container> | Runs in this container instead of the pod’s first. |
The command and its arguments are recorded in the audit Event on each pod, where anyone who can read Events in the namespace can see them. Pass a password or other secret on standard input (not with --all), never as an argument.
cordon
acd-admin cordon <node>
Stops new pods from being scheduled on the node. Pods already there keep running.
cordon refuses, changing nothing, when no other node would stay schedulable, because no pod that is moved, restarted or recreated could then start anywhere. A node counts as unschedulable when it is cordoned or carries a NoSchedule or NoExecute taint, such as CriticalAddonsOnly on dedicated servers. Cordoning a node that is already unschedulable (cordoned or tainted) is never refused. drain refuses the same way, even with --force, but without that exemption: it refuses a node that is already unschedulable too.
Error: cordon manager-3
Caused by:
cordoning manager-3 would leave no schedulable node, so no pod moved, restarted or recreated could start anywhere; make another node schedulable first, or run `k3s kubectl cordon manager-3` to cordon it anyway
kubectl adds a line such as pod acd-system/acd-admin-... terminated (Error) after this and some other errors. It names the session pod ending, not a second failure.
A refused drain says:
draining manager-3 would leave no schedulable node, so no pod moved, restarted or recreated could start anywhere; make another node schedulable first, or run `k3s kubectl drain --ignore-daemonsets --delete-emptydir-data manager-3` to drain it anyway
To cordon the last schedulable node anyway, run k3s kubectl cordon <node>, and to drain it anyway, k3s kubectl drain --ignore-daemonsets --delete-emptydir-data <node>. acd-admin keeps working with every node cordoned, so acd-admin uncordon can still return a node to service. On a cluster with dedicated servers, which carry the CriticalAddonsOnly taint, the acd-admin session cannot start once every agent is cordoned: return a node to service with k3s kubectl uncordon <node> instead.
uncordon
acd-admin uncordon <node>
Returns a cordoned or drained node to service: it clears both the unschedulable mark and the drained label.
drain
acd-admin drain <node> [--force]
Takes the node out of service for maintenance. It cordons the node, marks it as drained, and moves its workloads to other nodes through the eviction API, respecting their disruption budgets. Metrics and ingress forwarding keep running on the node, so it can still be observed. Longhorn’s node components keep running too, but its storage replicas on the node stop. acd-admin session pods are never moved either, whether the drain’s own, another operator’s or one left behind: a session on the node runs until it ends.
While it waits, it reports what it is waiting for. A disruption budget can refuse an eviction for now, such as for a storage volume’s last healthy replica. The drain names that pod and keeps retrying. It also names any pod that is terminating but not yet gone, with when its deletion started and the finalizer holding it, if any. On a node that is not Ready, such a pod can stay until the node comes back.
The drain has no timeout of its own, but its session ends after 12 hours: see the deadlines under Description. Ctrl-C, or the end of the session, stops the waiting, not the drain: the node stays drained, and pods already moved stay where they went. Pods still waiting to move are not retried until drain runs again, which carries on where it stopped; uncordon returns the node to service instead.
Before it changes anything, drain refuses, and exits non-zero, when:
- no other node would stay schedulable, as for
cordon.--forcedoes not override this. - the node runs a pod that no controller recreates, such as one started by hand, because evicting it deletes it for good. The message names each such pod as
<namespace>/<name>. A pod that has finished does not count, and neither does anacd-adminsession pod. - the node runs every Ready CDN Director pod, because draining it would stop CDN routing.
| Option | Effect |
|---|---|
--force | Drains despite a pod that no controller recreates, or the last CDN Director pods. Pods that no controller recreates are gone for good, and CDN routing stops until a CDN Director pod is Ready again. |
Drained Nodes
A drained node carries two marks, which drain sets:
- the node label
acd.agiletv.com/drained=true, whichstatusshows asdrainedunderSCHEDULING, and which keeps the CDN Director off the node; - the unschedulable mark that
cordonalso sets (spec.unschedulable, shown bykubectlas thenode.kubernetes.io/unschedulabletaint).
A drained node stays drained through reboots and later deploy runs, until acd-admin uncordon clears both marks.
kubectl uncordon clears only the unschedulable mark. The node then takes new pods again and is back in service, but it keeps the drained label: status still shows it as drained, and the CDN Director stays off it. Run acd-admin uncordon on it to clear the label.
Safety
Read this before you drain, cordon or restart anything on a production cluster.
One server at a time. The servers hold the cluster datastore (etcd), which needs a majority of them. With three servers, two down at once stops the Kubernetes API: nothing can be scheduled, moved or changed until one returns. Drain, reboot or restart one server, and return it to service, before you start on the next.
Wait for the cluster to recover before the next drain. After you return a drained node to service, wait until
acd-admin statusshows every admin targetok, andacd-admin volumesshows no Volumedegraded,rebuildingorfaulted. A drain stops the drained node’s storage replicas, and Volumes staydegradedorrebuildinguntil they recover: see Longhorn Storage. Plainacd-admin statusdoes not show this.Kafka tolerates one broker down, because every topic but one keeps three replicas. The exception is
selection_input, which has a single replica: selection input pauses while the broker that holds it restarts or moves, and is lost with that broker’s volume. To see which broker holds it, run this inacd-admin shell kafka:kafka-topics.sh --bootstrap-server "$BOOTSTRAP_SERVER" --describe --topic selection_inputThe
LeadernumberNis the podcdn-platform-kafka-N, andacd-admin status kafkashows its node.Cordoning or draining the PostgreSQL primary’s node switches the primary. CloudNativePG moves the primary to an instance on another node, which briefly interrupts database writes.
acd-admin status postgresqlshows the current primary.Some admin targets run a single pod. In the production preset these are
alertmanager,confd,geoip-loader(with MaxMind databases),kube-state-metrics,metrics-aggregator,redis,selection-input,vector,victoria-metrics,victoria-metrics-longterm,vmagentandvmalert. Restarting one, or draining its node, stops it until its new pod is Ready.restart --nodeskips disruption budgets. It deletes the target’s pods on the node rather than evicting them, so nothing holds it back, even when they are the target’s only Ready pods.
Options
Every command accepts:
| Option | Effect |
|---|---|
-h | Prints a summary of the help for acd-admin, or for the command, and exits. |
--help | Prints the full help, with details the summary leaves out, such as the HEALTH rules of status, and exits. |
acd-admin help prints the same as acd-admin --help, and acd-admin help <command> the same as acd-admin <command> --help.
acd-admin alone also accepts:
| Option | Effect |
|---|---|
-V, --version | Prints the version and exits. |
Environment
| Variable | Effect |
|---|---|
KUBECONFIG | The cluster credentials to use. Default: /etc/rancher/k3s/k3s.yaml. |
Exit Status
| Status | Meaning |
|---|---|
0 | The command succeeded. status and volumes exit 0 whatever the health they show, and volumes also when some of its sources could not be read. |
1 | The command failed, including a restart that timed out, an exec --all that failed in any pod, a refused cordon or drain, and a volumes --avg that VictoriaMetrics did not answer |
2 | Invalid command line, such as an unknown option or two options that cannot be combined |
129 | The session was hung up, such as by a dropped SSH connection |
130 | Interrupted with Ctrl-C |
143 | Terminated, such as by kill |
An interrupted, hung-up or terminated restart carries on in the cluster, and such a drain leaves the node drained.
shell, and exec in one pod, exit with the status of the command in the pod. Any of these statuses from them may come from that command.
Examples
See which targets exist and how they are doing:
acd-admin list
acd-admin status
acd-admin status postgresql
Check the cluster’s storage, then look into one Volume and its 5-minute IO average:
acd-admin volumes
acd-admin volumes data-kafka-0
acd-admin volumes data-kafka-0 --avg
Follow the manager’s logs from ten minutes ago:
acd-admin logs manager -f --since '10m ago'
See why a container crashed:
acd-admin logs manager -p
Restart Kafka, allowing up to 20 minutes:
acd-admin restart kafka --timeout 20m
Restart only the manager’s pods on one node:
acd-admin restart manager --node manager-3
Open psql on the PostgreSQL primary:
acd-admin shell postgresql
List the Kafka topics:
acd-admin exec kafka -- sh -c 'kafka-topics.sh --bootstrap-server "$BOOTSTRAP_SERVER" --list'
Run a command in every Redis pod:
acd-admin exec redis --all -- redis-cli ping
Open psql on a PostgreSQL replica rather than the primary:
acd-admin shell postgresql -l cnpg.io/instanceRole=replica
Take a node out of service and return it:
acd-admin drain manager-3
acd-admin uncordon manager-3
See Also
- Operations Guide: day-to-day procedures
- Troubleshooting Guide
deploy: installing, upgrading and reconfiguring the cluster
12.3 - acd-support-dump
Synopsis
acd-support-dump [<output path>]
Description
acd-support-dump captures a point-in-time diagnostic snapshot of the cluster into one compressed tarball, for attaching to a support ticket. It is installed on every machine at /usr/local/bin/acd-support-dump, and on every server /usr/bin/acd-support-dump links to it, so sudo acd-support-dump works without changing the sudo configuration. Run it on a server, as root or with sudo. It needs no network access beyond the cluster, so it works air-gapped.
The tarball holds pod logs, cluster resources and events, node system logs, database and Longhorn health, metric history and automated triage findings. Secret values are never collected, and the collected data is redacted before the tarball is written. It does not collect database contents. See Support Dumps for the full list.
The tarball is readable by its owner only. Progress goes to standard error. The only line written to standard output is the path of the tarball written, so a script can capture it.
Arguments
| Argument | Effect |
|---|---|
<output path> | Where to write the tarball. .tar.gz is added to a path that does not end in it. A path that starts with - is taken for an option and refused: write it as ./-name. Default: support-dump-<hostname>-<UTC time>.tar.gz in the current folder, where <UTC time> looks like 20261016T120000Z. |
Environment
| Variable | Effect |
|---|---|
KUBECONFIG | The cluster credentials to use. Default: /etc/rancher/k3s/k3s.yaml. |
Files
| File | Contents |
|---|---|
/usr/share/acd/acd-support-dump.yaml | What to collect and how to redact it. Shipped with the release: do not edit. |
Exit Status
| Status | Meaning |
|---|---|
0 | The tarball was written |
2 | Invalid command line: more than one argument, or an option. acd-support-dump takes no options. |
| other non-zero | The collection failed, or the collection engine or its specification is missing |
Examples
Write the tarball to the current folder:
acd-support-dump
Write it to a chosen path:
acd-support-dump /tmp/support-dump.tar.gz
See Also
- Log Collection: when to collect, and how to share the tarball
13 - Glossary
Overview
This glossary defines key terms and acronyms used throughout the AgileTV CDN Manager (ESB3027) documentation.
A
ACD (Agile Content Delivery)
The overall CDN solution comprising the Manager (ESB3027) and CDN Director (ESB3024) components.
Agent Node
A Kubernetes node that runs workloads but does not participate in the control plane. Agent nodes provide additional capacity for running application pods.
API Gateway
See NGinx Gateway.
ASN (Autonomous System Number)
A unique identifier for a network on the internet. Used in GeoIP-based routing decisions.
B
Backplane
The network the cluster machines use to reach each other (east-west traffic), set with backplane_network. Optional but recommended, and ideally separate from the public network that clients and CDN Directors use. Other trusted hosts, such as CDN Directors, may also be on it. See Configuring Segregated Networks.
C
CDN Director
The Edge Server Business (ESB3024) component that handles actual content routing and delivery. Multiple CDN Directors can be managed by a single CDN Manager.
Cloudnative PG (CNPG)
A Kubernetes operator that manages PostgreSQL clusters. Provides high availability, automatic failover, and backup capabilities for the Manager’s database layer.
Confd
Configuration daemon that synchronizes configuration from the Manager to CDN Directors. Runs as a sidecar or separate deployment.
CORS (Cross-Origin Resource Sharing)
A security mechanism that allows web applications to make requests to a different domain. Zitadel enforces CORS policies requiring the external domain to match the configured hostname.
CrashLoopBackOff
A Kubernetes pod state indicating the container is repeatedly crashing and being restarted. Typically indicates a configuration or dependency issue.
D
Datastore
The internal key-value storage system used by the Manager for short-lived or simple structured data. Backed by Redis.
Descheduler
A Kubernetes component that periodically analyzes pod distribution and evicts pods from overutilized nodes to optimize cluster balance.
Director
See CDN Director.
E
EDB (EnterpriseDB)
A company that provides PostgreSQL-related software and services. The Cloudnative PG operator was originally developed by EDB.
Ephemeral Storage
Temporary storage available to pods. Used for temporary files and caches. Not persistent across pod restarts.
ESB (Edge Server Business)
The product family designation for CDN components. ESB3027 is the Manager, ESB3024 is the CDN Director.
etcd
A distributed key-value store used by Kubernetes for cluster state management. Runs on Server nodes as part of the control plane.
F
FailedScheduling
A Kubernetes event indicating a pod could not be scheduled due to insufficient resources or scheduling constraints.
Flannel
A network overlay solution for Kubernetes. Provides VXLAN-based networking for pod-to-pod communication.
Frontend GUI
See MIB Frontend.
G
GeoIP
Geographic IP lookup service using MaxMind databases. Used for location-based routing decisions.
Grafana
A visualization and dashboard platform for time-series data. Used to display metrics collected by Telegraf and stored in VictoriaMetrics.
H
Helm Chart
A package of pre-configured Kubernetes resources. The CDN Manager is deployed via a Helm chart that handles all component installation.
HPA (Horizontal Pod Autoscaler)
A Kubernetes feature that automatically scales the number of pods based on CPU/memory utilization or custom metrics.
HTTP Server
The main API server component of the Manager, built with Actix Web (Rust framework).
I
Ingress
A Kubernetes resource that exposes HTTP/HTTPS routes from outside the cluster to services within. The CDN Manager uses Traefik as the ingress controller.
Ingress Controller
A component that implements ingress rules. The CDN Manager uses Traefik for primary ingress and NGinx for external CDN Director communication.
K
Kafka
A distributed event streaming platform used by the Manager for asynchronous communication and event processing.
K3s
A lightweight Kubernetes distribution optimized for edge and production deployments. Used as the underlying cluster technology.
Kubernetes (K8s)
An open-source container orchestration platform. The CDN Manager runs on a K3s-based Kubernetes cluster.
L
Login Name
The identifier a user signs in with. Zitadel composes it from the user’s username and the organization’s primary domain, which is derived from the manager’s configured external hostname — for example, admin@zitadel.manager.example.com. A login name is distinct from the user’s email address.
Longhorn
A distributed block storage system for Kubernetes. Provides persistent volumes for stateful components like PostgreSQL and Kafka.
Liveness Probe
A Kubernetes health check that determines if a container is running properly. Failed liveness probes trigger container restart.
M
Manager
The central management component (ESB3027) for configuring and monitoring CDN Directors.
MaxMind
A provider of IP intelligence databases including GeoIP City, GeoLite2 ASN, and Anonymous IP databases used by the Manager.
MIB Frontend
The web-based configuration GUI for CDN operators. Provides a user interface for managing streams, CDN Directors, and other configuration.
Multi-Factor Authentication (MFA)
An authentication method requiring multiple forms of verification. Note: MFA is not currently supported in the CDN Manager and should be skipped during setup.
N
Name-based Virtual Hosting
A technique where multiple hostnames are served from the same IP address. Zitadel uses this to match the URL a client connects to against its configured external hostname.
Namespace
A Kubernetes abstraction for organizing cluster resources. The CDN Manager uses namespaces to group related components.
NGinx Gateway
An NGinx-based gateway that handles external communication with CDN Directors.
Node Token
A secret token used to authenticate new nodes joining a K3s cluster. Located at /var/lib/rancher/k3s/server/node-token on Server nodes.
O
Operator
A method of packaging, deploying, and managing a Kubernetes application. Cloudnative PG is an operator for PostgreSQL.
OOMKilled
A Kubernetes pod state indicating the container was terminated due to exceeding memory limits.
P
PDB (Pod Disruption Budget)
A Kubernetes feature that ensures a minimum number of pods remain available during voluntary disruptions like maintenance.
PersistentVolume (PV)
A piece of storage in the Kubernetes cluster. Created dynamically by Longhorn for stateful components.
PersistentVolumeClaim (PVC)
A request for storage by a pod. Bound to a PersistentVolume.
Pod
The smallest deployable unit in Kubernetes. Contains one or more containers.
PostgreSQL
An open-source relational database. Used by the Manager for persistent data storage, managed by Cloudnative PG.
Probe
A Kubernetes health check mechanism. Types include liveness, readiness, and startup probes.
Prometheus
An open-source monitoring and alerting toolkit. Telegraf exports metrics in Prometheus format.
R
RBAC (Role-Based Access Control)
A method of regulating access to resources based on user roles. Used by Kubernetes for authorization.
Readiness Probe
A Kubernetes health check that determines if a container is ready to receive traffic. Failed readiness probes remove the pod from service load balancing.
Redis
An in-memory data structure store used for caching and as the datastore backend for the Manager.
Replica
A copy of a pod. Multiple replicas provide high availability and load distribution.
Resource Preset
Predefined resource configurations (nano, micro, small, medium, large, xlarge, 2xlarge) for common deployment sizes.
Rolling Update
A deployment strategy that updates pods one at a time to maintain availability during upgrades.
S
Selection Input
A key-value storage mechanism used for configuration data that can be queried with wildcard patterns. Available in v1 and v2 APIs with different semantics.
Server Node
A Kubernetes node that participates in the control plane (etcd, API server). Can also run workloads unless tainted.
Service
A Kubernetes abstraction that defines a logical set of pods and a policy for accessing them. Provides stable networking endpoints.
ServiceAccount
A Kubernetes identity for processes running in pods. Used for authentication between Kubernetes components.
StatefulSet
A Kubernetes workload API object for managing stateful applications. Used for PostgreSQL and Kafka deployments.
Startup Probe
A Kubernetes health check that determines if a container application has started. Disables liveness and readiness checks until it succeeds.
Stream
A content stream configuration defining source and routing parameters.
T
Telegraf
An agent for collecting, processing, aggregating, and writing metrics. Runs on each node to gather system and application metrics.
TLS (Transport Layer Security)
A cryptographic protocol for secure communication. The CDN Manager uses TLS for all external HTTPS connections.
Topology Aware Hints
A Kubernetes feature that prefers routing traffic to pods in the same zone as the source. Reduces latency by keeping traffic local.
Traefik
A modern HTTP reverse proxy and ingress controller. Used as the primary ingress controller for the CDN Manager.
TTL (Time To Live)
The duration after which data expires. Used in the datastore and selection input APIs.
V
Values.yaml
The Helm chart configuration file. Contains all configurable parameters for the CDN Manager deployment.
VictoriaMetrics
A time-series database used for storing metrics data. Provides long-term storage and querying capabilities.
VXLAN
Virtual Extensible LAN. A network virtualization technology used by Flannel for pod networking.
Z
Zitadel
An identity and access management (IAM) platform used for authentication and authorization in the CDN Manager. Provides OAuth2/OIDC capabilities.
Default Credentials
The following table lists all default credentials used by the CDN Manager. Change these defaults before deploying to production.
| Service | Username | Password | Notes |
|---|---|---|---|
| Zitadel Console and GUI | admin@zitadel.<manager-host> | Password1! | The login name of the default administrator; <manager-host> is the first entry of manager_hostnames. The Zitadel Console, at /ui/console, also accepts the account’s email address, admin@agiletv.dev; the GUI does not. |
Security Warning: Use the default administrator account only to create a new administrator account with proper roles. After verifying the new account works, disable or delete the default admin account before exposing the system to users. For details on required roles and administrator permissions, see Zitadel’s Administrator Documentation. See the Next Steps Guide for initial configuration procedures.
Common Abbreviations
| Abbreviation | Meaning |
|---|---|
| API | Application Programming Interface |
| ASN | Autonomous System Number |
| CORS | Cross-Origin Resource Sharing |
| CPU | Central Processing Unit |
| DNS | Domain Name System |
| EDB | EnterpriseDB |
| ESB | Edge Server Business |
| GUI | Graphical User Interface |
| HA | High Availability |
| Helm | Helm Package Manager |
| HPA | Horizontal Pod Autoscaler |
| HTTP | Hypertext Transfer Protocol |
| HTTPS | HTTP Secure |
| IAM | Identity and Access Management |
| IP | Internet Protocol |
| JSON | JavaScript Object Notation |
| K8s | Kubernetes |
| MFA | Multi-Factor Authentication |
| MIB | Management Information Base |
| NIC | Network Interface Card |
| OAuth | Open Authorization |
| OIDC | OpenID Connect |
| PVC | PersistentVolumeClaim |
| PV | PersistentVolume |
| RBAC | Role-Based Access Control |
| SSL | Secure Sockets Layer |
| TCP | Transmission Control Protocol |
| TLS | Transport Layer Security |
| TTL | Time To Live |
| UDP | User Datagram Protocol |
| UI | User Interface |
| VPA | Vertical Pod Autoscaler |
| VXLAN | Virtual Extensible LAN |
Next Steps
After reviewing terminology:
- Architecture Guide - Understand component relationships
- Configuration Guide - Full configuration reference
- Operations Guide - Day-to-day operational procedures