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)
This commit is contained in:
Clément Désiles
2026-09-05 17:24:08 +02:00
parent b94743567f
commit 12b81919c3
29 changed files with 1288 additions and 0 deletions
+220
View File
@@ -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/)
+33
View File
@@ -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 }}"
@@ -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" }
]
}
]
}
@@ -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" }
]
}
]
}
@@ -0,0 +1,9 @@
apiVersion: 1
datasources:
- name: prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://127.0.0.1:9090
isDefault: true
+12
View File
@@ -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
+5
View File
@@ -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: []
+242
View File
@@ -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
@@ -0,0 +1,11 @@
apiVersion: 1
providers:
- name: default
orgId: 1
folder: ''
type: file
disableDeletion: false
updateIntervalSeconds: 30
options:
path: {{ grafana_provisioning_path }}/dashboards
@@ -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";
}
}
+10
View File
@@ -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
+13
View File
@@ -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
+43
View File
@@ -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.
+6
View File
@@ -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"
+5
View File
@@ -0,0 +1,5 @@
---
- name: Restart node_exporter
ansible.builtin.systemd:
name: "{{ node_exporter_service }}"
state: restarted
+2
View File
@@ -0,0 +1,2 @@
---
dependencies: []
+27
View File
@@ -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
@@ -0,0 +1,2 @@
# Managed by Ansible - DO NOT EDIT MANUALLY
{{ node_exporter_args_var }}="--web.listen-address={{ node_exporter_listen_address }}"
+5
View File
@@ -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
+5
View File
@@ -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
+89
View File
@@ -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)
+49
View File
@@ -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
+10
View File
@@ -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
+78
View File
@@ -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
@@ -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;
}
}
@@ -0,0 +1,2 @@
# Managed by Ansible - DO NOT EDIT MANUALLY
{{ prometheus_args_var }}="--web.listen-address={{ prometheus_web_listen_address }}"
@@ -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 %}
+9
View File
@@ -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
+9
View File
@@ -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