From 12b81919c3bcb710b506de79ba51a43e18a60390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20D=C3=A9siles?= <1536672+cdesiles@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:24:08 +0200 Subject: [PATCH] feat(monitoring): add grafana, prometheus, node_exporter roles - grafana: postgres backend, provisioning (prometheus datasource + node/NUT dashboards) - prometheus: native service, scrape config via host_vars, localhost bind - node_exporter: host metrics exporter (localhost:9100) --- roles/grafana/README.md | 220 ++++++++++++++++ roles/grafana/defaults/main.yml | 33 +++ .../files/node-exporter-dashboard.json | 147 +++++++++++ roles/grafana/files/nut-status-dashboard.json | 102 ++++++++ .../grafana/files/prometheus-datasource.yaml | 9 + roles/grafana/handlers/main.yml | 12 + roles/grafana/meta/main.yml | 5 + roles/grafana/tasks/main.yml | 242 ++++++++++++++++++ .../templates/dashboards-provider.yaml.j2 | 11 + roles/grafana/templates/nginx-vhost.conf.j2 | 53 ++++ roles/grafana/vars/archlinux.yml | 10 + roles/grafana/vars/debian.yml | 13 + roles/node_exporter/README.md | 43 ++++ roles/node_exporter/defaults/main.yml | 6 + roles/node_exporter/handlers/main.yml | 5 + roles/node_exporter/meta/main.yml | 2 + roles/node_exporter/tasks/main.yml | 27 ++ roles/node_exporter/templates/env.conf.j2 | 2 + roles/node_exporter/vars/archlinux.yml | 5 + roles/node_exporter/vars/debian.yml | 5 + roles/prometheus/README.md | 89 +++++++ roles/prometheus/defaults/main.yml | 49 ++++ roles/prometheus/handlers/main.yml | 10 + roles/prometheus/tasks/main.yml | 78 ++++++ .../prometheus/templates/nginx-vhost.conf.j2 | 30 +++ .../templates/prometheus-env.conf.j2 | 2 + roles/prometheus/templates/prometheus.yml.j2 | 60 +++++ roles/prometheus/vars/archlinux.yml | 9 + roles/prometheus/vars/debian.yml | 9 + 29 files changed, 1288 insertions(+) create mode 100644 roles/grafana/README.md create mode 100644 roles/grafana/defaults/main.yml create mode 100644 roles/grafana/files/node-exporter-dashboard.json create mode 100644 roles/grafana/files/nut-status-dashboard.json create mode 100644 roles/grafana/files/prometheus-datasource.yaml create mode 100644 roles/grafana/handlers/main.yml create mode 100644 roles/grafana/meta/main.yml create mode 100644 roles/grafana/tasks/main.yml create mode 100644 roles/grafana/templates/dashboards-provider.yaml.j2 create mode 100644 roles/grafana/templates/nginx-vhost.conf.j2 create mode 100644 roles/grafana/vars/archlinux.yml create mode 100644 roles/grafana/vars/debian.yml create mode 100644 roles/node_exporter/README.md create mode 100644 roles/node_exporter/defaults/main.yml create mode 100644 roles/node_exporter/handlers/main.yml create mode 100644 roles/node_exporter/meta/main.yml create mode 100644 roles/node_exporter/tasks/main.yml create mode 100644 roles/node_exporter/templates/env.conf.j2 create mode 100644 roles/node_exporter/vars/archlinux.yml create mode 100644 roles/node_exporter/vars/debian.yml create mode 100644 roles/prometheus/README.md create mode 100644 roles/prometheus/defaults/main.yml create mode 100644 roles/prometheus/handlers/main.yml create mode 100644 roles/prometheus/tasks/main.yml create mode 100644 roles/prometheus/templates/nginx-vhost.conf.j2 create mode 100644 roles/prometheus/templates/prometheus-env.conf.j2 create mode 100644 roles/prometheus/templates/prometheus.yml.j2 create mode 100644 roles/prometheus/vars/archlinux.yml create mode 100644 roles/prometheus/vars/debian.yml diff --git a/roles/grafana/README.md b/roles/grafana/README.md new file mode 100644 index 0000000..e2e9793 --- /dev/null +++ b/roles/grafana/README.md @@ -0,0 +1,220 @@ +# Grafana Role + +Installs and configures Grafana as a system service with PostgreSQL backend. + +## Description + +This role: + +- Installs Grafana from official repositories (Arch Linux or Debian/Ubuntu) +- Creates a dedicated PostgreSQL database and user +- Configures Grafana to use PostgreSQL +- Optionally configures Nginx reverse proxy +- Follows the shared services pattern + +## Dependencies + +- `postgres` role (for database backend) + +## Variables + +### Required Variables (Override with Ansible Vault!) + +```yaml +grafana_db_password: changeme +grafana_admin_password: changeme +grafana_secret_key: changeme +``` + +### Server Configuration + +```yaml +grafana_http_port: 3000 +grafana_domain: localhost +grafana_root_url: "%(protocol)s://%(domain)s:%(http_port)s/" +``` + +### Database Configuration + +```yaml +grafana_db_type: postgres +grafana_db_name: grafana +grafana_db_user: grafana_user +grafana_db_host: localhost +grafana_db_port: 5432 +grafana_db_ssl_mode: disable +``` + +### Admin User + +```yaml +grafana_admin_user: admin +``` + +### Analytics + +```yaml +grafana_reporting_enabled: false +grafana_check_for_updates: false +``` + +### Nginx Reverse Proxy + +```yaml +grafana_nginx_enabled: false +grafana_nginx_hostname: grafana.local +``` + +## Example Playbook + +```yaml +--- +- hosts: monitoring + roles: + - role: grafana + vars: + grafana_domain: monitoring.example.com + grafana_nginx_enabled: true + grafana_nginx_hostname: grafana.example.com + grafana_admin_password: !vault | + $ANSIBLE_VAULT;1.1;AES256 + ... + grafana_db_password: !vault | + $ANSIBLE_VAULT;1.1;AES256 + ... + grafana_secret_key: !vault | + $ANSIBLE_VAULT;1.1;AES256 + ... +``` + +## Architecture + +### Database Isolation + +Grafana gets its own PostgreSQL database with a dedicated user that has minimal privileges: + +```sql +CREATE DATABASE grafana; +CREATE USER grafana_user WITH PASSWORD 'xxx'; +GRANT ALL PRIVILEGES ON DATABASE grafana TO grafana_user; +ALTER USER grafana_user NOSUPERUSER NOCREATEDB NOCREATEROLE; +``` + +### Service Layout + +``` +System Services: +├── PostgreSQL (port 5432, localhost only) +│ └── grafana database → grafana_user +└── Grafana (port 3000, localhost only) + └── Nginx (port 80) → reverse proxy (optional) +``` + +## Post-Installation + +### Access Grafana + +**Direct access:** + +``` +http://localhost:3000 +``` + +**With Nginx reverse proxy:** + +``` +http://grafana.local +``` + +### Default Credentials + +- Username: `admin` (or value of `grafana_admin_user`) +- Password: Value of `grafana_admin_password` + +**Important:** Change the admin password immediately after first login! + +### Verify Installation + +```bash +# Check service status +systemctl status grafana-server # Debian/Ubuntu +systemctl status grafana # Arch Linux + +# Check database connection +sudo -u postgres psql -c "\l" | grep grafana +sudo -u postgres psql -c "\du" | grep grafana + +# Check configuration +sudo cat /etc/grafana/grafana.ini | grep -A 5 "\[database\]" + +# Check logs +journalctl -u grafana-server -f # Debian/Ubuntu +journalctl -u grafana -f # Arch Linux +``` + +## Security Considerations + +1. **Passwords:** Always use Ansible Vault for sensitive variables +2. **Database:** Grafana user has minimal privileges (no superuser, can't create databases/roles) +3. **Network:** Grafana binds to localhost by default, use Nginx for external access +4. **Secret Key:** Used for signing cookies and other security features - keep it secret! + +## Nginx Integration + +When `grafana_nginx_enabled: true`, the role: + +- Deploys an Nginx vhost configuration to `/etc/nginx/conf.d/grafana.conf` +- Configures reverse proxy with WebSocket support for live updates +- Reloads Nginx gracefully + +When disabled, the vhost configuration is removed. + +## OS Support + +- **Arch Linux:** Installs from community repository +- **Debian/Ubuntu:** Installs from official Grafana APT repository + +## Troubleshooting + +### Grafana won't start + +```bash +# Check logs +journalctl -u grafana-server -n 50 # Debian/Ubuntu +journalctl -u grafana -n 50 # Arch Linux + +# Check configuration syntax +sudo grafana-cli admin validate-config +``` + +### Can't connect to PostgreSQL + +```bash +# Verify PostgreSQL is running +systemctl status postgresql + +# Test database connection +psql -h localhost -U grafana_user -d grafana + +# Check PostgreSQL logs +journalctl -u postgresql -n 50 +``` + +### 502 Bad Gateway with Nginx + +```bash +# Verify Grafana is running +systemctl status grafana-server + +# Check if Grafana is listening +ss -tlnp | grep 3000 + +# Test direct access +curl http://localhost:3000 +``` + +## References + +- [Grafana Documentation](https://grafana.com/docs/grafana/latest/) +- [Grafana Configuration](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/) +- [PostgreSQL Data Source](https://grafana.com/docs/grafana/latest/datasources/postgres/) diff --git a/roles/grafana/defaults/main.yml b/roles/grafana/defaults/main.yml new file mode 100644 index 0000000..db2570b --- /dev/null +++ b/roles/grafana/defaults/main.yml @@ -0,0 +1,33 @@ +--- +# Grafana version +grafana_version: "stable" + +# Network configuration +grafana_http_port: 3200 +grafana_domain: grafana.nas.local +grafana_root_url: "https://{{ grafana_domain }}" + +# PostgreSQL Database configuration +# The user and database will be created by this role. +# REQUIRED: The password must be set in your inventory (e.g., in inventory/host_vars/your_host.yml). +grafana_postgres_type: postgres +grafana_postgres_host: 127.0.0.1 +grafana_postgres_port: 5432 +grafana_postgres_db_name: grafana +grafana_postgres_user: grafana +# grafana_postgres_password: "" # Intentionally undefined - role will fail if not set +grafana_postgres_ssl_mode: "prefer" # 'disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full' + +# Security +# REQUIRED: These values must be set in your inventory. +grafana_admin_user: admin +# grafana_admin_password: "" # Intentionally undefined - role will fail if not set +# grafana_secret_key: "" # Intentionally undefined - role will fail if not set + +# Analytics +grafana_reporting_enabled: false +grafana_check_for_updates: false + +# Nginx reverse proxy +grafana_nginx_enabled: true +grafana_nginx_hostname: "{{ grafana_domain }}" diff --git a/roles/grafana/files/node-exporter-dashboard.json b/roles/grafana/files/node-exporter-dashboard.json new file mode 100644 index 0000000..368a173 --- /dev/null +++ b/roles/grafana/files/node-exporter-dashboard.json @@ -0,0 +1,147 @@ +{ + "title": "Node Exporter - Host Overview", + "uid": "node-exporter-overview", + "tags": ["node-exporter", "linux", "system"], + "schemaVersion": 39, + "version": 1, + "refresh": "1m", + "time": { "from": "now-6h", "to": "now" }, + "timezone": "browser", + "editable": true, + "graphTooltip": 1, + "templating": { + "list": [ + { + "name": "instance", + "label": "Host", + "type": "query", + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "query": { "query": "label_values(node_uname_info, instance)", "refId": "A" }, + "refresh": 2, + "multi": false, + "includeAll": false, + "sort": 1, + "current": { "selected": false, "text": "", "value": "" } + } + ] + }, + "panels": [ + { + "id": 1, + "type": "stat", + "title": "Uptime", + "gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "dtdurations", "color": { "mode": "thresholds" } }, "overrides": [] }, + "options": { "reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" }, "colorMode": "none", "graphMode": "none", "textMode": "auto", "justifyMode": "auto", "orientation": "auto" }, + "targets": [ { "refId": "A", "expr": "node_time_seconds{instance=\"$instance\"} - node_boot_time_seconds{instance=\"$instance\"}" } ] + }, + { + "id": 2, + "type": "stat", + "title": "CPU Cores", + "gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "short", "color": { "mode": "thresholds" } }, "overrides": [] }, + "options": { "reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" }, "colorMode": "none", "graphMode": "none", "textMode": "auto", "justifyMode": "auto", "orientation": "auto" }, + "targets": [ { "refId": "A", "expr": "count(count by (cpu) (node_cpu_seconds_total{mode=\"idle\", instance=\"$instance\"}))" } ] + }, + { + "id": 3, + "type": "stat", + "title": "Load 1m", + "gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "short", "color": { "mode": "thresholds" } }, "overrides": [] }, + "options": { "reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" }, "colorMode": "none", "graphMode": "none", "textMode": "auto", "justifyMode": "auto", "orientation": "auto" }, + "targets": [ { "refId": "A", "expr": "node_load1{instance=\"$instance\"}" } ] + }, + { + "id": 4, + "type": "stat", + "title": "Memory Used", + "gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "percent", "min": 0, "max": 100, "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "orange", "value": 75 }, { "color": "red", "value": 90 } ] } }, "overrides": [] }, + "options": { "reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" }, "colorMode": "background", "graphMode": "none", "textMode": "auto", "justifyMode": "auto", "orientation": "auto" }, + "targets": [ { "refId": "A", "expr": "100 * (1 - (node_memory_MemAvailable_bytes{instance=\"$instance\"} / node_memory_MemTotal_bytes{instance=\"$instance\"}))" } ] + }, + { + "id": 5, + "type": "timeseries", + "title": "CPU Usage", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "percent", "min": 0, "max": 100, "color": { "mode": "palette-classic" }, "custom": { "fillOpacity": 20, "lineWidth": 1, "drawStyle": "line", "showPoints": "never" } }, "overrides": [] }, + "targets": [ { "refId": "A", "expr": "100 - (avg by (instance) (rate(node_cpu_seconds_total{mode=\"idle\", instance=\"$instance\"}[$__rate_interval])) * 100)", "legendFormat": "CPU used" } ] + }, + { + "id": 6, + "type": "timeseries", + "title": "System Load", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "short", "color": { "mode": "palette-classic" }, "custom": { "fillOpacity": 10, "lineWidth": 1, "drawStyle": "line", "showPoints": "never" } }, "overrides": [] }, + "targets": [ + { "refId": "A", "expr": "node_load1{instance=\"$instance\"}", "legendFormat": "load1" }, + { "refId": "B", "expr": "node_load5{instance=\"$instance\"}", "legendFormat": "load5" }, + { "refId": "C", "expr": "node_load15{instance=\"$instance\"}", "legendFormat": "load15" } + ] + }, + { + "id": 7, + "type": "timeseries", + "title": "Memory Usage %", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 12 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "percent", "min": 0, "max": 100, "color": { "mode": "palette-classic" }, "custom": { "fillOpacity": 20, "lineWidth": 1, "drawStyle": "line", "showPoints": "never" } }, "overrides": [] }, + "targets": [ { "refId": "A", "expr": "100 * (1 - (node_memory_MemAvailable_bytes{instance=\"$instance\"} / node_memory_MemTotal_bytes{instance=\"$instance\"}))", "legendFormat": "Used %" } ] + }, + { + "id": 8, + "type": "timeseries", + "title": "Memory", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 12 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "bytes", "color": { "mode": "palette-classic" }, "custom": { "fillOpacity": 20, "lineWidth": 1, "drawStyle": "line", "showPoints": "never" } }, "overrides": [] }, + "targets": [ + { "refId": "A", "expr": "node_memory_MemTotal_bytes{instance=\"$instance\"} - node_memory_MemAvailable_bytes{instance=\"$instance\"}", "legendFormat": "Used" }, + { "refId": "B", "expr": "node_memory_Cached_bytes{instance=\"$instance\"} + node_memory_Buffers_bytes{instance=\"$instance\"}", "legendFormat": "Cached+Buffers" }, + { "refId": "C", "expr": "node_memory_MemAvailable_bytes{instance=\"$instance\"}", "legendFormat": "Available" } + ] + }, + { + "id": 9, + "type": "timeseries", + "title": "Filesystem Usage %", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 20 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "percent", "min": 0, "max": 100, "color": { "mode": "palette-classic" }, "custom": { "fillOpacity": 15, "lineWidth": 1, "drawStyle": "line", "showPoints": "never" } }, "overrides": [] }, + "targets": [ { "refId": "A", "expr": "100 * (1 - (node_filesystem_avail_bytes{instance=\"$instance\", fstype!~\"tmpfs|overlay|squashfs|ramfs\"} / node_filesystem_size_bytes{instance=\"$instance\", fstype!~\"tmpfs|overlay|squashfs|ramfs\"}))", "legendFormat": "{{mountpoint}}" } ] + }, + { + "id": 10, + "type": "timeseries", + "title": "Disk I/O", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 20 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "Bps", "color": { "mode": "palette-classic" }, "custom": { "fillOpacity": 10, "lineWidth": 1, "drawStyle": "line", "showPoints": "never" } }, "overrides": [] }, + "targets": [ + { "refId": "A", "expr": "rate(node_disk_read_bytes_total{instance=\"$instance\", device!~\"ram.*|loop.*\"}[$__rate_interval])", "legendFormat": "{{device}} read" }, + { "refId": "B", "expr": "-rate(node_disk_written_bytes_total{instance=\"$instance\", device!~\"ram.*|loop.*\"}[$__rate_interval])", "legendFormat": "{{device}} write" } + ] + }, + { + "id": 11, + "type": "timeseries", + "title": "Network Traffic", + "gridPos": { "h": 8, "w": 24, "x": 0, "y": 28 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "Bps", "color": { "mode": "palette-classic" }, "custom": { "fillOpacity": 10, "lineWidth": 1, "drawStyle": "line", "showPoints": "never" } }, "overrides": [] }, + "targets": [ + { "refId": "A", "expr": "rate(node_network_receive_bytes_total{instance=\"$instance\", device!~\"lo|veth.*|cni.*|br-.*|docker.*\"}[$__rate_interval])", "legendFormat": "{{device}} in" }, + { "refId": "B", "expr": "-rate(node_network_transmit_bytes_total{instance=\"$instance\", device!~\"lo|veth.*|cni.*|br-.*|docker.*\"}[$__rate_interval])", "legendFormat": "{{device}} out" } + ] + } + ] +} diff --git a/roles/grafana/files/nut-status-dashboard.json b/roles/grafana/files/nut-status-dashboard.json new file mode 100644 index 0000000..2ad200f --- /dev/null +++ b/roles/grafana/files/nut-status-dashboard.json @@ -0,0 +1,102 @@ +{ + "title": "UPS - NUT Status", + "uid": "nut-status", + "tags": ["nut", "ups", "power"], + "schemaVersion": 39, + "version": 1, + "refresh": "30s", + "time": { "from": "now-24h", "to": "now" }, + "timezone": "browser", + "editable": true, + "graphTooltip": 1, + "templating": { + "list": [ + { + "name": "instance", + "label": "UPS", + "type": "query", + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "query": { "query": "label_values(network_ups_tools_device_info, instance)", "refId": "A" }, + "refresh": 2, + "multi": false, + "includeAll": false, + "sort": 1, + "current": { "selected": false, "text": "", "value": "" } + } + ] + }, + "panels": [ + { + "id": 1, + "type": "stat", + "title": "Battery Charge", + "gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "percent", "min": 0, "max": 100, "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "orange", "value": 25 }, { "color": "green", "value": 50 } ] } }, "overrides": [] }, + "options": { "reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" }, "colorMode": "background", "graphMode": "none", "textMode": "auto", "justifyMode": "auto", "orientation": "auto" }, + "targets": [ { "refId": "A", "expr": "network_ups_tools_battery_charge{instance=\"$instance\"}" } ] + }, + { + "id": 2, + "type": "stat", + "title": "UPS Load", + "gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "percent", "min": 0, "max": 100, "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "orange", "value": 70 }, { "color": "red", "value": 85 } ] } }, "overrides": [] }, + "options": { "reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" }, "colorMode": "background", "graphMode": "none", "textMode": "auto", "justifyMode": "auto", "orientation": "auto" }, + "targets": [ { "refId": "A", "expr": "network_ups_tools_ups_load{instance=\"$instance\"}" } ] + }, + { + "id": 3, + "type": "stat", + "title": "On Battery", + "gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "none", "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "mappings": [ { "type": "value", "options": { "0": { "text": "No", "color": "green" }, "1": { "text": "YES", "color": "red" } } } ] }, "overrides": [] }, + "options": { "reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" }, "colorMode": "background", "graphMode": "none", "textMode": "auto", "justifyMode": "auto", "orientation": "auto" }, + "targets": [ { "refId": "A", "expr": "network_ups_tools_ups_status{instance=\"$instance\", flag=\"OB\"}" } ] + }, + { + "id": 4, + "type": "stat", + "title": "Low Battery", + "gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "none", "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "mappings": [ { "type": "value", "options": { "0": { "text": "No", "color": "green" }, "1": { "text": "YES", "color": "red" } } } ] }, "overrides": [] }, + "options": { "reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" }, "colorMode": "background", "graphMode": "none", "textMode": "auto", "justifyMode": "auto", "orientation": "auto" }, + "targets": [ { "refId": "A", "expr": "network_ups_tools_ups_status{instance=\"$instance\", flag=\"LB\"}" } ] + }, + { + "id": 5, + "type": "timeseries", + "title": "Battery Charge", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "percent", "min": 0, "max": 100, "color": { "mode": "palette-classic" }, "custom": { "fillOpacity": 20, "lineWidth": 1, "drawStyle": "line", "showPoints": "never" } }, "overrides": [] }, + "targets": [ { "refId": "A", "expr": "network_ups_tools_battery_charge{instance=\"$instance\"}", "legendFormat": "charge" } ] + }, + { + "id": 6, + "type": "timeseries", + "title": "UPS Load", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "percent", "min": 0, "max": 100, "color": { "mode": "palette-classic" }, "custom": { "fillOpacity": 20, "lineWidth": 1, "drawStyle": "line", "showPoints": "never" } }, "overrides": [] }, + "targets": [ { "refId": "A", "expr": "network_ups_tools_ups_load{instance=\"$instance\"}", "legendFormat": "load" } ] + }, + { + "id": 7, + "type": "timeseries", + "title": "Status Flags", + "gridPos": { "h": 8, "w": 24, "x": 0, "y": 12 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "none", "min": 0, "max": 1, "color": { "mode": "palette-classic" }, "custom": { "fillOpacity": 0, "lineWidth": 1, "drawStyle": "line", "showPoints": "never", "staircase": true } }, "overrides": [] }, + "targets": [ + { "refId": "A", "expr": "network_ups_tools_ups_status{instance=\"$instance\", flag=\"OL\"}", "legendFormat": "On line" }, + { "refId": "B", "expr": "network_ups_tools_ups_status{instance=\"$instance\", flag=\"OB\"}", "legendFormat": "On battery" }, + { "refId": "C", "expr": "network_ups_tools_ups_status{instance=\"$instance\", flag=\"LB\"}", "legendFormat": "Low battery" }, + { "refId": "D", "expr": "network_ups_tools_ups_status{instance=\"$instance\", flag=\"CHRG\"}", "legendFormat": "Charging" } + ] + } + ] +} diff --git a/roles/grafana/files/prometheus-datasource.yaml b/roles/grafana/files/prometheus-datasource.yaml new file mode 100644 index 0000000..d774500 --- /dev/null +++ b/roles/grafana/files/prometheus-datasource.yaml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: + - name: prometheus + uid: prometheus + type: prometheus + access: proxy + url: http://127.0.0.1:9090 + isDefault: true diff --git a/roles/grafana/handlers/main.yml b/roles/grafana/handlers/main.yml new file mode 100644 index 0000000..f2dcc8a --- /dev/null +++ b/roles/grafana/handlers/main.yml @@ -0,0 +1,12 @@ +--- +# Grafana handlers + +- name: Restart grafana + ansible.builtin.systemd: + name: "{{ grafana_service }}" + state: restarted + +- name: Reload nginx + ansible.builtin.systemd: + name: nginx + state: reloaded diff --git a/roles/grafana/meta/main.yml b/roles/grafana/meta/main.yml new file mode 100644 index 0000000..c5d965a --- /dev/null +++ b/roles/grafana/meta/main.yml @@ -0,0 +1,5 @@ +--- +# No meta dependencies: shared services (postgres) are deployed once via +# playbook role ordering (tagged 'infra'), not re-run on every deploy of +# this role. See playbooks/*.yml. +dependencies: [] diff --git a/roles/grafana/tasks/main.yml b/roles/grafana/tasks/main.yml new file mode 100644 index 0000000..6dc35f0 --- /dev/null +++ b/roles/grafana/tasks/main.yml @@ -0,0 +1,242 @@ +--- +# Grafana installation and configuration + +- name: Validate required passwords are set + ansible.builtin.assert: + that: + - grafana_postgres_password is defined + - grafana_postgres_password | length >= 12 + - grafana_admin_password is defined + - grafana_admin_password | length >= 12 + - grafana_secret_key is defined + - grafana_secret_key | length >= 16 + fail_msg: | + grafana_postgres_password, grafana_admin_password, and grafana_secret_key are required. + See roles/grafana/defaults/main.yml for configuration instructions. + success_msg: "Password validation passed" + +- name: Load OS-specific variables + ansible.builtin.include_vars: "{{ item }}" + with_first_found: + - "{{ ansible_facts['os_family'] }}.yml" + - "debian.yml" + +# Debian/Ubuntu installation +- name: Ensure apt keyrings directory exists (Debian/Ubuntu) + ansible.builtin.file: + path: /etc/apt/keyrings + state: directory + mode: "0755" + when: ansible_facts['os_family'] == 'Debian' + +- name: Install Grafana APT key (Debian/Ubuntu) + ansible.builtin.get_url: + url: "{{ grafana_apt_key_url }}" + dest: /etc/apt/keyrings/grafana.asc + mode: "0644" + when: ansible_facts['os_family'] == 'Debian' + +- name: Add Grafana APT repository (Debian/Ubuntu) + ansible.builtin.deb822_repository: + name: grafana + types: [deb] + uris: "{{ grafana_apt_repo_uri }}" + suites: "{{ grafana_apt_repo_suite }}" + components: [main] + signed_by: /etc/apt/keyrings/grafana.asc + state: present + when: ansible_facts['os_family'] == 'Debian' + +- name: Install Grafana package (Debian/Ubuntu) + ansible.builtin.apt: + name: "{{ grafana_package }}={{ grafana_version }}" + state: present + update_cache: true + when: ansible_facts['os_family'] == 'Debian' + +# Arch Linux installation +- name: Install Grafana package (Arch Linux) + community.general.pacman: + name: "{{ grafana_package }}" + state: present + when: ansible_facts['os_family'] == 'Archlinux' + +# Database setup +- name: Create PostgreSQL database + community.postgresql.postgresql_db: + name: "{{ grafana_postgres_db_name }}" + state: present + become: true + become_user: "{{ postgres_admin_user }}" + +- name: Create PostgreSQL user + community.postgresql.postgresql_user: + name: "{{ grafana_postgres_user }}" + password: "{{ grafana_postgres_password }}" + state: present + become: true + become_user: "{{ postgres_admin_user }}" + +- name: Grant database privileges + community.postgresql.postgresql_privs: + login_db: "{{ grafana_postgres_db_name }}" + roles: "{{ grafana_postgres_user }}" + type: database + privs: ALL + state: present + become: true + become_user: "{{ postgres_admin_user }}" + +- name: Grant schema privileges + community.postgresql.postgresql_privs: + login_db: "{{ grafana_postgres_db_name }}" + roles: "{{ grafana_postgres_user }}" + type: schema + objs: public + privs: CREATE,USAGE + state: present + become: true + become_user: "{{ postgres_admin_user }}" + +- name: Ensure minimal privileges + community.postgresql.postgresql_user: + name: "{{ grafana_postgres_user }}" + role_attr_flags: NOSUPERUSER,NOCREATEDB,NOCREATEROLE + state: present + become: true + become_user: "{{ postgres_admin_user }}" + +# Grafana configuration +- name: Configure Grafana + community.general.ini_file: + path: "{{ grafana_config_path }}" + section: "{{ item.section }}" + option: "{{ item.option }}" + value: "{{ item.value }}" + mode: "0640" + owner: root + group: "{{ grafana_group }}" + loop: + - section: "server" + option: "http_port" + value: "{{ grafana_http_port }}" + - section: "server" + option: "domain" + value: "{{ grafana_domain }}" + - section: "server" + option: "root_url" + value: "{{ grafana_root_url }}" + - section: "database" + option: "type" + value: "{{ grafana_postgres_type }}" + - section: "database" + option: "host" + value: "{{ grafana_postgres_host }}:{{ grafana_postgres_port }}" + - section: "database" + option: "name" + value: "{{ grafana_postgres_db_name }}" + - section: "database" + option: "user" + value: "{{ grafana_postgres_user }}" + - section: "database" + option: "password" + value: "{{ grafana_postgres_password }}" + - section: "database" + option: "ssl_mode" + value: "{{ grafana_postgres_ssl_mode }}" + - section: "security" + option: "admin_user" + value: "{{ grafana_admin_user }}" + - section: "security" + option: "admin_password" + value: "{{ grafana_admin_password }}" + - section: "security" + option: "secret_key" + value: "{{ grafana_secret_key }}" + - section: "analytics" + option: "reporting_enabled" + value: "{{ grafana_reporting_enabled }}" + - section: "analytics" + option: "check_for_updates" + value: "{{ grafana_check_for_updates }}" + notify: Restart grafana + +# Provisioning (datasources + dashboards as code) +- name: Ensure Grafana provisioning directories exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ grafana_user }}" + group: "{{ grafana_group }}" + mode: "0755" + loop: + - "{{ grafana_provisioning_path }}/datasources" + - "{{ grafana_provisioning_path }}/dashboards" + +- name: Deploy Prometheus datasource provisioning + ansible.builtin.copy: + src: prometheus-datasource.yaml + dest: "{{ grafana_provisioning_path }}/datasources/prometheus.yaml" + owner: "{{ grafana_user }}" + group: "{{ grafana_group }}" + mode: "0644" + notify: Restart grafana + +- name: Deploy dashboard provider provisioning + ansible.builtin.template: + src: dashboards-provider.yaml.j2 + dest: "{{ grafana_provisioning_path }}/dashboards/dashboards.yaml" + owner: "{{ grafana_user }}" + group: "{{ grafana_group }}" + mode: "0644" + notify: Restart grafana + +- name: Deploy node_exporter dashboard + ansible.builtin.copy: + src: node-exporter-dashboard.json + dest: "{{ grafana_provisioning_path }}/dashboards/node-exporter.json" + owner: "{{ grafana_user }}" + group: "{{ grafana_group }}" + mode: "0644" + notify: Restart grafana + +- name: Deploy NUT dashboard + ansible.builtin.copy: + src: nut-status-dashboard.json + dest: "{{ grafana_provisioning_path }}/dashboards/nut-status.json" + owner: "{{ grafana_user }}" + group: "{{ grafana_group }}" + mode: "0644" + notify: Restart grafana + +# Service management +- name: Enable and start Grafana service + ansible.builtin.systemd: + name: "{{ grafana_service }}" + enabled: true + state: started + daemon_reload: true + +# Nginx integration +- name: Provision TLS certificate for Grafana + ansible.builtin.include_tasks: "{{ role_path }}/../nginx/tasks/certbot.yml" + vars: + certbot_hostname: "{{ grafana_nginx_hostname }}" + when: grafana_nginx_enabled + +- name: Deploy nginx vhost + ansible.builtin.template: + src: nginx-vhost.conf.j2 + dest: "{{ nginx_conf_dir | default('/etc/nginx/conf.d') }}/grafana.conf" + owner: root + group: root + mode: "0644" + when: grafana_nginx_enabled + notify: Reload nginx + +- name: Remove nginx vhost when disabled + ansible.builtin.file: + path: "{{ nginx_conf_dir | default('/etc/nginx/conf.d') }}/grafana.conf" + state: absent + when: not grafana_nginx_enabled + notify: Reload nginx diff --git a/roles/grafana/templates/dashboards-provider.yaml.j2 b/roles/grafana/templates/dashboards-provider.yaml.j2 new file mode 100644 index 0000000..821394c --- /dev/null +++ b/roles/grafana/templates/dashboards-provider.yaml.j2 @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: default + orgId: 1 + folder: '' + type: file + disableDeletion: false + updateIntervalSeconds: 30 + options: + path: {{ grafana_provisioning_path }}/dashboards diff --git a/roles/grafana/templates/nginx-vhost.conf.j2 b/roles/grafana/templates/nginx-vhost.conf.j2 new file mode 100644 index 0000000..78dcbf2 --- /dev/null +++ b/roles/grafana/templates/nginx-vhost.conf.j2 @@ -0,0 +1,53 @@ +# Grafana vhost with Let's Encrypt (Certbot) +# Managed by Ansible - DO NOT EDIT MANUALLY + +server { + listen 80; + listen [::]:80; + server_name {{ grafana_nginx_hostname }}; + + # Certbot webroot for ACME challenges + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect to HTTPS + location / { + return 301 https://$server_name$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ grafana_nginx_hostname }}; + + # Let's Encrypt certificates (managed by Certbot) + ssl_certificate /etc/letsencrypt/live/{{ grafana_nginx_hostname }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ grafana_nginx_hostname }}/privkey.pem; + + # SSL configuration + ssl_protocols {{ nginx_ssl_protocols | default('TLSv1.3') }}; + ssl_prefer_server_ciphers on; + +{% if nginx_log_backend | default('journald') == 'journald' %} + access_log syslog:server=unix:/dev/log,nohostname,tag=nginx_grafana; + error_log syslog:server=unix:/dev/log,nohostname,tag=nginx_grafana; +{% else %} + access_log /var/log/nginx/{{ grafana_nginx_hostname }}_access.log main; + error_log /var/log/nginx/{{ grafana_nginx_hostname }}_error.log; +{% endif %} + + location / { + proxy_pass http://127.0.0.1:{{ grafana_http_port }}; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support for live updates + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} diff --git a/roles/grafana/vars/archlinux.yml b/roles/grafana/vars/archlinux.yml new file mode 100644 index 0000000..f5c4f22 --- /dev/null +++ b/roles/grafana/vars/archlinux.yml @@ -0,0 +1,10 @@ +--- +# Arch Linux specific variables for Grafana + +grafana_package: grafana +grafana_service: grafana +grafana_user: grafana +grafana_group: grafana +grafana_config_path: /etc/grafana.ini +grafana_data_path: /var/lib/grafana +grafana_provisioning_path: /var/lib/grafana/conf/provisioning diff --git a/roles/grafana/vars/debian.yml b/roles/grafana/vars/debian.yml new file mode 100644 index 0000000..1e4dae0 --- /dev/null +++ b/roles/grafana/vars/debian.yml @@ -0,0 +1,13 @@ +--- +# Debian/Ubuntu specific variables for Grafana + +grafana_package: grafana +grafana_service: grafana-server +grafana_user: grafana +grafana_group: grafana +grafana_config_path: /etc/grafana/grafana.ini +grafana_data_path: /var/lib/grafana +grafana_provisioning_path: /etc/grafana/provisioning +grafana_apt_key_url: https://apt.grafana.com/gpg.key +grafana_apt_repo_uri: https://apt.grafana.com +grafana_apt_repo_suite: stable diff --git a/roles/node_exporter/README.md b/roles/node_exporter/README.md new file mode 100644 index 0000000..89254c8 --- /dev/null +++ b/roles/node_exporter/README.md @@ -0,0 +1,43 @@ +# node_exporter — Prometheus exporter for host metrics + +Exposes machine metrics (CPU, memory, disk, filesystem, network) for Prometheus. + +## Supported distributions + +- Arch Linux (package `prometheus-node-exporter`, extra repo) +- Debian/Ubuntu (package `prometheus-node-exporter`) + +## Configuration + +See [defaults/main.yml](defaults/main.yml). + +Optional: + +```yaml +node_exporter_listen_address: "127.0.0.1:9100" +``` + +Keep localhost-only — Prometheus scrapes it locally on the same host. To add +collectors (e.g. `--collector.systemd`, `--collector.textfile.directory`), extend +`node_exporter_listen_address` or override the args in host_vars. + +## Pairing with Prometheus + +```yaml +prometheus_scrape_configs: + - job_name: 'node' + static_configs: + - targets: ['localhost:9100'] +``` + +## Operations + +```bash +systemctl status prometheus-node-exporter +curl -s http://127.0.0.1:9100/metrics | head +journalctl -u prometheus-node-exporter -f +``` + +## Dependencies + +None. Standalone exporter. diff --git a/roles/node_exporter/defaults/main.yml b/roles/node_exporter/defaults/main.yml new file mode 100644 index 0000000..8be3366 --- /dev/null +++ b/roles/node_exporter/defaults/main.yml @@ -0,0 +1,6 @@ +--- +# Prometheus node_exporter configuration + +# Address the exporter listens on. Keep localhost-only — Prometheus scrapes it +# locally on the same host; no need to expose on the network. +node_exporter_listen_address: "127.0.0.1:9100" diff --git a/roles/node_exporter/handlers/main.yml b/roles/node_exporter/handlers/main.yml new file mode 100644 index 0000000..5f2a22c --- /dev/null +++ b/roles/node_exporter/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Restart node_exporter + ansible.builtin.systemd: + name: "{{ node_exporter_service }}" + state: restarted diff --git a/roles/node_exporter/meta/main.yml b/roles/node_exporter/meta/main.yml new file mode 100644 index 0000000..23d65c7 --- /dev/null +++ b/roles/node_exporter/meta/main.yml @@ -0,0 +1,2 @@ +--- +dependencies: [] diff --git a/roles/node_exporter/tasks/main.yml b/roles/node_exporter/tasks/main.yml new file mode 100644 index 0000000..490be02 --- /dev/null +++ b/roles/node_exporter/tasks/main.yml @@ -0,0 +1,27 @@ +--- +- name: Load OS-specific variables + ansible.builtin.include_vars: "{{ item }}" + with_first_found: + - "{{ ansible_facts['os_family'] }}.yml" + - "debian.yml" + +- name: Install node_exporter + ansible.builtin.package: + name: "{{ node_exporter_package }}" + state: present + +- name: Deploy node_exporter configuration + ansible.builtin.template: + src: env.conf.j2 + dest: "{{ node_exporter_args_file }}" + owner: root + group: root + mode: "0644" + notify: Restart node_exporter + +- name: Enable and start node_exporter + ansible.builtin.systemd: + name: "{{ node_exporter_service }}" + enabled: true + state: started + daemon_reload: true diff --git a/roles/node_exporter/templates/env.conf.j2 b/roles/node_exporter/templates/env.conf.j2 new file mode 100644 index 0000000..4302c15 --- /dev/null +++ b/roles/node_exporter/templates/env.conf.j2 @@ -0,0 +1,2 @@ +# Managed by Ansible - DO NOT EDIT MANUALLY +{{ node_exporter_args_var }}="--web.listen-address={{ node_exporter_listen_address }}" diff --git a/roles/node_exporter/vars/archlinux.yml b/roles/node_exporter/vars/archlinux.yml new file mode 100644 index 0000000..24cbef9 --- /dev/null +++ b/roles/node_exporter/vars/archlinux.yml @@ -0,0 +1,5 @@ +--- +node_exporter_package: prometheus-node-exporter +node_exporter_service: prometheus-node-exporter +node_exporter_args_file: /etc/conf.d/prometheus-node-exporter +node_exporter_args_var: NODE_EXPORTER_ARGS diff --git a/roles/node_exporter/vars/debian.yml b/roles/node_exporter/vars/debian.yml new file mode 100644 index 0000000..aed1c06 --- /dev/null +++ b/roles/node_exporter/vars/debian.yml @@ -0,0 +1,5 @@ +--- +node_exporter_package: prometheus-node-exporter +node_exporter_service: prometheus-node-exporter +node_exporter_args_file: /etc/default/prometheus-node-exporter +node_exporter_args_var: ARGS diff --git a/roles/prometheus/README.md b/roles/prometheus/README.md new file mode 100644 index 0000000..1bca79a --- /dev/null +++ b/roles/prometheus/README.md @@ -0,0 +1,89 @@ +# Prometheus Role + +Installs and configures Prometheus monitoring system as a native system service. + +## Supported Distributions + +- Arch Linux +- Debian/Ubuntu + +## Features + +- Native system installation (no containers) +- Configurable scrape targets and intervals +- Optional Nginx reverse proxy with SSL +- Retention policy configuration +- Alert manager integration support +- Rule files support + +## Configuration + +See [defaults/main.yml](defaults/main.yml) for all available variables. + +### Basic Configuration + +```yaml +prometheus_scrape_interval: 15s +prometheus_retention_time: 15d +prometheus_web_listen_address: "127.0.0.1:9090" +``` + +### Scrape Targets + +```yaml +prometheus_scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + - job_name: 'node_exporter' + static_configs: + - targets: ['localhost:9100'] +``` + +### Nginx Reverse Proxy + +```yaml +prometheus_nginx_enabled: true +prometheus_nginx_server_name: prometheus.example.com +prometheus_nginx_ssl_certificate: /path/to/cert.pem +prometheus_nginx_ssl_certificate_key: /path/to/key.pem +``` + +## Usage + +```yaml +- hosts: monitoring + roles: + - prometheus +``` + +With custom configuration: + +```yaml +- hosts: monitoring + vars: + prometheus_retention_time: 30d + prometheus_scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + - job_name: 'node_exporter' + static_configs: + - targets: ['localhost:9100'] + roles: + - prometheus +``` + +## Architecture + +- **Installation**: Native package from distribution repositories +- **Service**: System systemd service +- **Configuration**: `/etc/prometheus/prometheus.yml` +- **Data**: `/var/lib/prometheus` +- **Listen**: `127.0.0.1:9090` (localhost only by default) + +## Security + +- Binds to localhost by default +- Optional SSL-enabled reverse proxy via Nginx +- Data directory restricted to prometheus user (mode 0750) diff --git a/roles/prometheus/defaults/main.yml b/roles/prometheus/defaults/main.yml new file mode 100644 index 0000000..4d21b08 --- /dev/null +++ b/roles/prometheus/defaults/main.yml @@ -0,0 +1,49 @@ +--- +# Prometheus configuration +# See: https://prometheus.io/docs/prometheus/latest/configuration/configuration/ + +# Scrape interval +prometheus_scrape_interval: 15s +prometheus_evaluation_interval: 15s + +# Retention settings +prometheus_retention_time: 15d +prometheus_retention_size: 0 + +# Web interface settings +prometheus_web_listen_address: "127.0.0.1:9090" +prometheus_web_external_url: "" + +# Storage settings (uses prometheus_data_dir from vars/) +prometheus_storage_tsdb_path: "{{ prometheus_data_dir }}" + +# Scrape targets configuration +# Example: +# prometheus_scrape_configs: +# - job_name: 'prometheus' +# static_configs: +# - targets: ['localhost:9090'] +# - job_name: 'node_exporter' +# static_configs: +# - targets: ['localhost:9100'] +prometheus_scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + +# Alertmanager configuration (optional) +# prometheus_alertmanager_configs: +# - static_configs: +# - targets: ['localhost:9093'] +prometheus_alertmanager_configs: [] + +# Rule files (optional) +# prometheus_rule_files: +# - '/etc/prometheus/rules/*.yml' +prometheus_rule_files: [] + +# Nginx reverse proxy (optional) +prometheus_nginx_enabled: false +prometheus_nginx_server_name: prometheus.example.com +prometheus_nginx_ssl_certificate: /etc/letsencrypt/live/prometheus.example.com/fullchain.pem +prometheus_nginx_ssl_certificate_key: /etc/letsencrypt/live/prometheus.example.com/privkey.pem diff --git a/roles/prometheus/handlers/main.yml b/roles/prometheus/handlers/main.yml new file mode 100644 index 0000000..f38d5fe --- /dev/null +++ b/roles/prometheus/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: Restart Prometheus + ansible.builtin.systemd: + name: "{{ prometheus_service }}" + state: restarted + +- name: Reload Nginx + ansible.builtin.systemd: + name: nginx + state: reloaded diff --git a/roles/prometheus/tasks/main.yml b/roles/prometheus/tasks/main.yml new file mode 100644 index 0000000..d8c1ee9 --- /dev/null +++ b/roles/prometheus/tasks/main.yml @@ -0,0 +1,78 @@ +--- +- name: Load OS-specific variables + ansible.builtin.include_vars: "{{ item }}" + with_first_found: + - "{{ ansible_facts['os_family'] }}.yml" + - "debian.yml" + +- name: Install Prometheus + ansible.builtin.package: + name: "{{ prometheus_package }}" + state: present + +- name: Create Prometheus configuration directory + ansible.builtin.file: + path: "{{ prometheus_config_dir }}" + state: directory + owner: "{{ prometheus_user }}" + group: "{{ prometheus_group }}" + mode: "0755" + +- name: Create Prometheus data directory + ansible.builtin.file: + path: "{{ prometheus_data_dir }}" + state: directory + owner: "{{ prometheus_user }}" + group: "{{ prometheus_group }}" + mode: "0750" + +- name: Create Prometheus rules directory + ansible.builtin.file: + path: "{{ prometheus_config_dir }}/rules" + state: directory + owner: "{{ prometheus_user }}" + group: "{{ prometheus_group }}" + mode: "0755" + when: prometheus_rule_files | length > 0 + +- name: Deploy Prometheus configuration + ansible.builtin.template: + src: prometheus.yml.j2 + dest: "{{ prometheus_config_dir }}/prometheus.yml" + owner: "{{ prometheus_user }}" + group: "{{ prometheus_group }}" + mode: "0644" + validate: "promtool check config %s" + notify: Restart Prometheus + +- name: Deploy Prometheus environment file + ansible.builtin.template: + src: prometheus-env.conf.j2 + dest: "{{ prometheus_args_file }}" + owner: root + group: root + mode: "0644" + notify: Restart Prometheus + +- name: Enable and start Prometheus service + ansible.builtin.systemd: + name: "{{ prometheus_service }}" + enabled: true + state: started + +- name: Deploy Nginx vhost configuration + ansible.builtin.template: + src: nginx-vhost.conf.j2 + dest: "{{ nginx_conf_dir }}/prometheus.conf" + owner: root + group: root + mode: "0644" + when: prometheus_nginx_enabled + notify: Reload Nginx + +- name: Remove Nginx vhost configuration + ansible.builtin.file: + path: "{{ nginx_conf_dir }}/prometheus.conf" + state: absent + when: not prometheus_nginx_enabled + notify: Reload Nginx diff --git a/roles/prometheus/templates/nginx-vhost.conf.j2 b/roles/prometheus/templates/nginx-vhost.conf.j2 new file mode 100644 index 0000000..ada95f2 --- /dev/null +++ b/roles/prometheus/templates/nginx-vhost.conf.j2 @@ -0,0 +1,30 @@ +server { + listen 80; + listen [::]:80; + server_name {{ prometheus_nginx_server_name }}; + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name {{ prometheus_nginx_server_name }}; + + ssl_certificate {{ prometheus_nginx_ssl_certificate }}; + ssl_certificate_key {{ prometheus_nginx_ssl_certificate_key }}; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + access_log /var/log/nginx/prometheus_access.log; + error_log /var/log/nginx/prometheus_error.log; + + location / { + proxy_pass http://{{ prometheus_web_listen_address }}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/roles/prometheus/templates/prometheus-env.conf.j2 b/roles/prometheus/templates/prometheus-env.conf.j2 new file mode 100644 index 0000000..0d29579 --- /dev/null +++ b/roles/prometheus/templates/prometheus-env.conf.j2 @@ -0,0 +1,2 @@ +# Managed by Ansible - DO NOT EDIT MANUALLY +{{ prometheus_args_var }}="--web.listen-address={{ prometheus_web_listen_address }}" diff --git a/roles/prometheus/templates/prometheus.yml.j2 b/roles/prometheus/templates/prometheus.yml.j2 new file mode 100644 index 0000000..2dc346d --- /dev/null +++ b/roles/prometheus/templates/prometheus.yml.j2 @@ -0,0 +1,60 @@ +# Prometheus configuration file +# Managed by Ansible - DO NOT EDIT MANUALLY + +global: + scrape_interval: {{ prometheus_scrape_interval }} + evaluation_interval: {{ prometheus_evaluation_interval }} +{% if prometheus_web_external_url %} + external_labels: + monitor: '{{ prometheus_web_external_url }}' +{% endif %} + +{% if prometheus_alertmanager_configs | length > 0 %} +alerting: + alertmanagers: +{% for alertmanager in prometheus_alertmanager_configs %} + - {{ alertmanager | to_nice_yaml(indent=2) | indent(6) }} +{% endfor %} +{% endif %} + +{% if prometheus_rule_files | length > 0 %} +rule_files: +{% for rule_file in prometheus_rule_files %} + - {{ rule_file }} +{% endfor %} +{% endif %} + +scrape_configs: +{% for scrape_config in prometheus_scrape_configs %} + - job_name: '{{ scrape_config.job_name }}' +{% if scrape_config.scrape_interval is defined %} + scrape_interval: {{ scrape_config.scrape_interval }} +{% endif %} +{% if scrape_config.scrape_timeout is defined %} + scrape_timeout: {{ scrape_config.scrape_timeout }} +{% endif %} +{% if scrape_config.metrics_path is defined %} + metrics_path: {{ scrape_config.metrics_path }} +{% endif %} +{% if scrape_config.static_configs is defined %} + static_configs: +{% for static_config in scrape_config.static_configs %} + - targets: +{% for target in static_config.targets %} + - {{ target }} +{% endfor %} +{% if static_config.labels is defined %} + labels: +{% for label_key, label_value in static_config.labels.items() %} + {{ label_key }}: {{ label_value }} +{% endfor %} +{% endif %} +{% endfor %} +{% endif %} +{% if scrape_config.relabel_configs is defined %} + relabel_configs: +{% for relabel_config in scrape_config.relabel_configs %} + - {{ relabel_config | to_nice_yaml(indent=2) | indent(8) }} +{% endfor %} +{% endif %} +{% endfor %} diff --git a/roles/prometheus/vars/archlinux.yml b/roles/prometheus/vars/archlinux.yml new file mode 100644 index 0000000..cb33124 --- /dev/null +++ b/roles/prometheus/vars/archlinux.yml @@ -0,0 +1,9 @@ +--- +prometheus_package: prometheus +prometheus_user: prometheus +prometheus_group: prometheus +prometheus_service: prometheus +prometheus_config_dir: /etc/prometheus +prometheus_data_dir: /var/lib/prometheus +prometheus_args_file: /etc/conf.d/prometheus +prometheus_args_var: PROMETHEUS_ARGS diff --git a/roles/prometheus/vars/debian.yml b/roles/prometheus/vars/debian.yml new file mode 100644 index 0000000..a2fe3b2 --- /dev/null +++ b/roles/prometheus/vars/debian.yml @@ -0,0 +1,9 @@ +--- +prometheus_package: prometheus +prometheus_user: prometheus +prometheus_group: prometheus +prometheus_service: prometheus +prometheus_config_dir: /etc/prometheus +prometheus_data_dir: /var/lib/prometheus +prometheus_args_file: /etc/default/prometheus +prometheus_args_var: ARGS