← PROJECT
#ansible#zabbix#linux#automation#mysql

Automating Zabbix Deployments with Ansible

Manual Zabbix deployments accumulate drift: package versions diverge across distros, hardening steps disappear, agent configs fall out of sync. Two Ansible roles cover the whole stack: database, server, proxies, and agents, all idempotent.

The Problem

Manual Zabbix installs across a mixed RHEL and Ubuntu fleet accumulate drift fast. Package versions diverge between environments. Some hosts skip the MySQL anonymous user removal step. Agent configs end up with the wrong Server= IP or a missing ServerActive= line. Every new host is a one-off.

Three specific problems drove this:

  • No repeatable way to add a new agent to an existing deployment without SSH one-liners
  • Database hardening varies across hosts: root passwords stay at defaults on some, the test database stays on others
  • community.zabbix 4.x changed how Ansible API modules work in a non-obvious way that breaks the first deployment attempt

The goal: one playbook, idempotent, handles the full stack from bare host to registered agent.

The Architecture

Two roles, one dependency chain. ansible-zabbix delegates database setup to ansible-mysql when deploying a server with a MySQL backend. The roles stay separate because ansible-mysql handles any MySQL deployment, not only Zabbix, and switching to PostgreSQL means changing db_backend in one place.

graph LR
    subgraph "ansible-zabbix"
        S["zabbix_function: server"]
        P["zabbix_function: proxy"]
        A["zabbix_function: agent"]
    end

    AM["ansible-mysql"]
    S -->|"db_backend: mysql"| AM

    subgraph "Tag execution order"
        T1["server.install"] --> T2["proxy.install"] --> T3["agent.install"] --> T4["add.proxy"] --> T5["add.host"]
    end

A single site.yml with four inventory groups drives the whole deployment. Tags control which tasks run, so you can re-run individual stages without touching the rest of the stack.

The four inventory groups:

  • ZABBIX_SERVER — the Zabbix server host
  • ZABBIX_PROXIES — any proxy hosts (optional)
  • ZABBIX_AGENTS — monitored hosts
  • ZABBIX_API — a dedicated entry for API operations (more on this below)

The tag execution order matters: the server schema must exist before proxies can register, and proxies must register before agents that route through them.

Three Things Worth Knowing

The ZABBIX_API Inventory Group

community.zabbix 4.x changed how the zabbix_host and zabbix_proxy modules work. They now use Ansible’s httpapi persistent connection plugin instead of making direct HTTP calls. This requires a dedicated inventory host configured with ansible_connection=httpapi.

It is not a real server. It is an inventory entry that points at the Zabbix web interface IP:

[ZABBIX_API]
zabbix-api ansible_host=192.168.1.10

[ZABBIX_API:vars]
ansible_connection=httpapi
ansible_network_os=community.zabbix.zabbix
ansible_user=Admin
ansible_httpapi_port=80
ansible_httpapi_use_ssl=false
ansible_httpapi_validate_certs=false

Without this group, add.host and add.proxy fail with connection errors that mention network connectivity. The real cause is the missing httpapi host.

The ansible_password for this host must go in its own group_vars file. Putting it in hosts.ini breaks Jinja2 templating:

# inventory/group_vars/ZABBIX_API.yml
ansible_password: "{{ zabbix_admin_password | default('zabbix') }}"

MySQL 8.0 Compatibility

Two breaking changes surface when targeting MySQL 8.0.

Auth plugin. MySQL 8.0 defaults to caching_sha2_password. Some Zabbix versions and older clients require mysql_native_password. The role exposes mysql_auth_plugin to switch:

# group_vars/ZABBIX_SERVER.yml
mysql_auth_plugin: mysql_native_password

Binary log expiry. MySQL 8.0 deprecated expire_logs_days, replacing it with binlog_expire_logs_seconds. The role detects the MySQL version and writes the correct directive to my.cnf. No manual intervention needed.

If MySQL is on the target host, run the role with --tags mysql to re-apply the configuration. The role is idempotent and leaves existing databases and users in place.

Decouple API Registration from Installation

add.host and add.proxy are separate tags from agent.install and proxy.install.

The Zabbix server must complete its database schema import and start its web interface before any API call succeeds. On a fresh install that takes 30 to 60 seconds after the package starts running. Running registration in the same play as installation causes the API to return 500 errors while the schema import is still in progress.

The workflow: run the installs, confirm the Zabbix web UI loads, then run registration as a second step:

# Install agents
ansible-playbook -i inventory/hosts.ini site.yml --tags agent.install -l ZABBIX_AGENTS

# After the web UI responds — register hosts
ansible-playbook -i inventory/hosts.ini site.yml --tags add.host -l ZABBIX_AGENTS

This lets you add new hosts to an existing deployment without touching the server or any existing agents.

Tutorial: Deploy Zabbix Server and Agent from Scratch

Two hosts: one for the Zabbix server, one as a monitored agent. Both running RHEL 8/9, Ubuntu 24.04, or Debian 12. The server needs at least 2 GB RAM for MySQL and the Zabbix server process to run alongside each other.

Prerequisites

  • Ansible 2.14+ on the control node
  • Both hosts reachable via SSH with become access
  • Python 3 on both managed hosts

Step 1: Install the Roles and Collections

ansible-galaxy install git+https://github.com/Sifungurux/ansible-zabbix
ansible-galaxy install git+https://github.com/Sifungurux/ansible-mysql
ansible-galaxy collection install community.zabbix community.mysql

Step 2: Create the Inventory

# inventory/hosts.ini

[ZABBIX_SERVER]
zabbix01.example.com

[ZABBIX_PROXIES]
# proxy01.example.com

[ZABBIX_AGENTS]
web01.example.com

[ZABBIX_API]
zabbix-api ansible_host=zabbix01.example.com

[ZABBIX_API:vars]
ansible_connection=httpapi
ansible_network_os=community.zabbix.zabbix
ansible_user=Admin
ansible_httpapi_port=80
ansible_httpapi_use_ssl=false
ansible_httpapi_validate_certs=false

Step 3: Configure group_vars

# inventory/group_vars/all.yml
zabbix_version: "7.4"
zabbix_server: 192.168.1.10
zabbix_proxies: []
# inventory/group_vars/ZABBIX_SERVER.yml
zabbix_function: server
db_backend: mysql

db_pass: "CHANGE_ME_strong_root_password"
zabbix_db: zabbix
zabbix_user: zabbix
zabbix_pass: "CHANGE_ME_strong_db_password"
zabbix_admin_password: "CHANGE_ME_strong_admin_password"
# inventory/group_vars/ZABBIX_AGENTS.yml
zabbix_function: agent
# inventory/group_vars/ZABBIX_API.yml
ansible_password: "{{ zabbix_admin_password | default('zabbix') }}"

Replace every CHANGE_ME value before running. Use ansible-vault encrypt inventory/group_vars/ZABBIX_SERVER.yml to keep credentials out of version control.

Step 4: Write site.yml

---
- name: Deploy Zabbix server
  hosts: ZABBIX_SERVER
  become: true
  roles:
    - role: ansible-zabbix

- name: Deploy Zabbix agents
  hosts: ZABBIX_AGENTS
  become: true
  roles:
    - role: ansible-zabbix

Step 5: Deploy

Run in order. The server must respond on its web interface before the add.host step.

# 1. Install and configure the Zabbix server and its MySQL database
ansible-playbook -i inventory/hosts.ini site.yml --tags server.install -l ZABBIX_SERVER

# 2. Install a local agent on the server
#    The built-in Zabbix server self-monitoring host checks 127.0.0.1:10050
ansible-playbook -i inventory/hosts.ini site.yml --tags agent.install -l ZABBIX_SERVER

# 3. Install agents on monitored hosts
ansible-playbook -i inventory/hosts.ini site.yml --tags agent.install -l ZABBIX_AGENTS

# 4. Open the web UI at http://<server-ip>/zabbix and confirm it loads, then register hosts
ansible-playbook -i inventory/hosts.ini site.yml --tags add.host -l ZABBIX_AGENTS

Result

The Zabbix web UI shows web01.example.com in the Linux servers host group, linked to Template OS Linux, with the agent availability indicator green. The built-in Zabbix server host at 127.0.0.1 also shows green.

Adding another host later takes two commands: agent.install on the new host, then add.host once the agent is running.

What’s Next

Three items remain on the list:

  • PostgreSQL backend — the role has db_backend: pgsql but the PostgreSQL path needs end-to-end testing. The MySQL path works; the role wires up pgsql but no one has tested past a fresh install.
  • TLS between agents and servertls: true enables the config, but a full walkthrough with certificate generation is missing.
  • Molecule tests — the role runs against real VMs in CI but has no Molecule scenario. A converge and verify cycle across RHEL 9 and Ubuntu 24.04 would catch regressions before they reach main.

Need this set up for your team?

Automation Sprint →