From aa78e238aa05b1cbeb70cd6ba57336880b229771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20D=C3=A9siles?= <1536672+cdesiles@users.noreply.github.com> Date: Sun, 6 Sep 2026 09:37:36 +0200 Subject: [PATCH] feat(monitoring): add nginx, postgres, valkey prometheus exporters - nginx: nginx-prometheus-exporter gated by nginx_exporter_enabled, deploys loopback-only stub_status (8090), exporter on 9113 - postgres: postgres_exporter gated by postgres_exporter_enabled, creates postgres_exporter role with pg_monitor, exporter on 9187 - valkey: redis_exporter gated by valkey_exporter_enabled, adds read-only ACL user to users.acl, exporter on 9121 Each exporter follows the nut pattern (tasks/exporter.yml included from main.yml, OS vars, 0600 secret env files, password asserts). --- roles/nginx/README.md | 20 +++++++ roles/nginx/defaults/main.yml | 7 +++ roles/nginx/handlers/main.yml | 5 ++ roles/nginx/tasks/exporter.yml | 34 ++++++++++++ roles/nginx/tasks/main.yml | 4 ++ roles/nginx/templates/exporter-env.conf.j2 | 2 + roles/nginx/templates/stub-status.conf.j2 | 12 +++++ roles/nginx/vars/archlinux.yml | 6 +++ roles/nginx/vars/debian.yml | 6 +++ roles/postgres/README.md | 21 ++++++++ roles/postgres/defaults/main.yml | 10 ++++ roles/postgres/handlers/main.yml | 5 ++ roles/postgres/tasks/exporter.yml | 54 +++++++++++++++++++ roles/postgres/tasks/main.yml | 4 ++ roles/postgres/templates/exporter-env.conf.j2 | 3 ++ roles/postgres/vars/archlinux.yml | 6 +++ roles/postgres/vars/debian.yml | 6 +++ roles/valkey/README.md | 20 +++++++ roles/valkey/defaults/main.yml | 14 +++++ roles/valkey/handlers/main.yml | 5 ++ roles/valkey/tasks/exporter.yml | 36 +++++++++++++ roles/valkey/tasks/main.yml | 4 ++ roles/valkey/templates/exporter-env.conf.j2 | 5 ++ roles/valkey/templates/users.acl.j2 | 3 ++ roles/valkey/vars/archlinux.yml | 6 +++ roles/valkey/vars/debian.yml | 6 +++ 26 files changed, 304 insertions(+) create mode 100644 roles/nginx/tasks/exporter.yml create mode 100644 roles/nginx/templates/exporter-env.conf.j2 create mode 100644 roles/nginx/templates/stub-status.conf.j2 create mode 100644 roles/postgres/tasks/exporter.yml create mode 100644 roles/postgres/templates/exporter-env.conf.j2 create mode 100644 roles/valkey/tasks/exporter.yml create mode 100644 roles/valkey/templates/exporter-env.conf.j2 diff --git a/roles/nginx/README.md b/roles/nginx/README.md index 96f6a36..05dd541 100644 --- a/roles/nginx/README.md +++ b/roles/nginx/README.md @@ -28,6 +28,26 @@ hostname and breaking TLS verification on other vhosts. Disable with `nginx_default_server_enabled: false`. +## Prometheus exporter + +Optionally exposes nginx metrics (active connections, requests/sec) for +Prometheus via `nginx-prometheus-exporter`, which scrapes a loopback-only +`stub_status` endpoint this role deploys. Enable it: + +```yaml +nginx_exporter_enabled: true +``` + +The exporter binds to `127.0.0.1:9113`; the `stub_status` block listens on +`127.0.0.1:8090` and denies non-localhost clients. Pair it with a scrape job: + +```yaml +prometheus_scrape_configs: + - job_name: 'nginx' + static_configs: + - targets: ['127.0.0.1:9113'] +``` + ## Service Integration Pattern Each service role should deploy its own vhost config: diff --git a/roles/nginx/defaults/main.yml b/roles/nginx/defaults/main.yml index d9c63be..c00eff9 100644 --- a/roles/nginx/defaults/main.yml +++ b/roles/nginx/defaults/main.yml @@ -44,3 +44,10 @@ nginx_forwarder: {} # Let's Encrypt / Certbot configuration # acme_email: "" # Required for Let's Encrypt - intentionally undefined # Set this variable to enable Certbot installation and certificate management + +# Prometheus exporter (optional, via nginx-prometheus-exporter) +# Scrapes a loopback-only stub_status endpoint this role deploys. +nginx_exporter_enabled: false +nginx_exporter_listen_address: "127.0.0.1:9113" +nginx_exporter_stub_status_listen: "127.0.0.1:8090" +nginx_exporter_scrape_uri: "http://127.0.0.1:8090/stub_status" diff --git a/roles/nginx/handlers/main.yml b/roles/nginx/handlers/main.yml index 98d2229..bae5519 100644 --- a/roles/nginx/handlers/main.yml +++ b/roles/nginx/handlers/main.yml @@ -8,3 +8,8 @@ ansible.builtin.systemd: name: nginx state: restarted + +- name: Restart nginx exporter + ansible.builtin.systemd: + name: "{{ nginx_exporter_service }}" + state: restarted diff --git a/roles/nginx/tasks/exporter.yml b/roles/nginx/tasks/exporter.yml new file mode 100644 index 0000000..a684cbe --- /dev/null +++ b/roles/nginx/tasks/exporter.yml @@ -0,0 +1,34 @@ +--- +# Prometheus nginx exporter (optional, gated by nginx_exporter_enabled). +# Installs nginx-prometheus-exporter and exposes a loopback-only stub_status +# endpoint that the exporter scrapes. + +- name: Install nginx-prometheus-exporter + ansible.builtin.package: + name: "{{ nginx_exporter_package }}" + state: present + +- name: Deploy nginx stub_status endpoint + ansible.builtin.template: + src: stub-status.conf.j2 + dest: "{{ nginx_conf_dir }}/99-stub-status.conf" + owner: root + group: root + mode: "0644" + notify: Reload nginx + +- name: Deploy exporter environment file + ansible.builtin.template: + src: exporter-env.conf.j2 + dest: "{{ nginx_exporter_args_file }}" + owner: root + group: root + mode: "0644" + notify: Restart nginx exporter + +- name: Enable and start nginx exporter + ansible.builtin.systemd: + name: "{{ nginx_exporter_service }}" + enabled: true + state: started + daemon_reload: true diff --git a/roles/nginx/tasks/main.yml b/roles/nginx/tasks/main.yml index 0fb0253..b727b48 100644 --- a/roles/nginx/tasks/main.yml +++ b/roles/nginx/tasks/main.yml @@ -207,3 +207,7 @@ name: nginx enabled: true state: started + +- name: Deploy Prometheus nginx exporter + ansible.builtin.include_tasks: exporter.yml + when: nginx_exporter_enabled | bool diff --git a/roles/nginx/templates/exporter-env.conf.j2 b/roles/nginx/templates/exporter-env.conf.j2 new file mode 100644 index 0000000..e62b0db --- /dev/null +++ b/roles/nginx/templates/exporter-env.conf.j2 @@ -0,0 +1,2 @@ +# Managed by Ansible - DO NOT EDIT MANUALLY +{{ nginx_exporter_args_var }}="-nginx.scrape-uri={{ nginx_exporter_scrape_uri }} -web.listen-address={{ nginx_exporter_listen_address }}" diff --git a/roles/nginx/templates/stub-status.conf.j2 b/roles/nginx/templates/stub-status.conf.j2 new file mode 100644 index 0000000..319a4c1 --- /dev/null +++ b/roles/nginx/templates/stub-status.conf.j2 @@ -0,0 +1,12 @@ +# Managed by Ansible - DO NOT EDIT MANUALLY +# stub_status endpoint for nginx-prometheus-exporter (loopback only) +server { + listen {{ nginx_exporter_stub_status_listen }}; + server_name localhost; + + location /stub_status { + stub_status; + allow 127.0.0.1; + deny all; + } +} diff --git a/roles/nginx/vars/archlinux.yml b/roles/nginx/vars/archlinux.yml index bf42eab..71d4df8 100644 --- a/roles/nginx/vars/archlinux.yml +++ b/roles/nginx/vars/archlinux.yml @@ -1,3 +1,9 @@ --- nginx_user: http certbot_timer: certbot-renew.timer + +# Prometheus exporter +nginx_exporter_package: nginx-prometheus-exporter +nginx_exporter_service: nginx-prometheus-exporter +nginx_exporter_args_file: /etc/conf.d/nginx-prometheus-exporter +nginx_exporter_args_var: NGINX_EXPORTER_ARGS diff --git a/roles/nginx/vars/debian.yml b/roles/nginx/vars/debian.yml index d7c0f01..7e58eb0 100644 --- a/roles/nginx/vars/debian.yml +++ b/roles/nginx/vars/debian.yml @@ -1,3 +1,9 @@ --- nginx_user: www-data certbot_timer: certbot.timer + +# Prometheus exporter +nginx_exporter_package: prometheus-nginx-exporter +nginx_exporter_service: prometheus-nginx-exporter +nginx_exporter_args_file: /etc/default/prometheus-nginx-exporter +nginx_exporter_args_var: ARGS diff --git a/roles/postgres/README.md b/roles/postgres/README.md index 0e9319c..f82d242 100644 --- a/roles/postgres/README.md +++ b/roles/postgres/README.md @@ -85,6 +85,27 @@ sudo -u postgres psql -c "SHOW shared_buffers;" sudo -u postgres psql -c "SHOW effective_cache_size;" ``` +## Prometheus exporter + +Optionally exposes PostgreSQL metrics (connections, cache hit ratio, deadlocks) +via `postgres_exporter`. Enable it: + +```yaml +postgres_exporter_enabled: true +postgres_exporter_password: "" # keep URL-safe +``` + +The role creates a `postgres_exporter` monitoring role with the built-in +`pg_monitor` role (no superuser). The exporter binds to `127.0.0.1:9187`; +pair it with a scrape job: + +```yaml +prometheus_scrape_configs: + - job_name: 'postgres' + static_configs: + - targets: ['127.0.0.1:9187'] +``` + ## References - [PostgreSQL Documentation](https://www.postgresql.org/docs/current/) diff --git a/roles/postgres/defaults/main.yml b/roles/postgres/defaults/main.yml index 447d8a7..6520b0d 100644 --- a/roles/postgres/defaults/main.yml +++ b/roles/postgres/defaults/main.yml @@ -49,3 +49,13 @@ postgres_log_truncate_on_rotation: "off" # Overwrite old log files with same nam postgres_logrotate_rotate: 14 # Keep 14 days of logs postgres_logrotate_frequency: daily # daily|weekly|monthly postgres_logrotate_compress: true # Compress rotated logs + +# Prometheus exporter (optional, via postgres_exporter) +# Creates its own minimal monitoring role (pg_monitor, no superuser). +postgres_exporter_enabled: false +postgres_exporter_listen_address: "127.0.0.1:9187" +postgres_exporter_user: postgres_exporter +# postgres_exporter_password: "" # Intentionally undefined - required when enabled +postgres_exporter_pg_host: "127.0.0.1" +postgres_exporter_pg_port: "{{ postgres_port }}" +postgres_exporter_pg_dbname: postgres diff --git a/roles/postgres/handlers/main.yml b/roles/postgres/handlers/main.yml index b548554..e459e1d 100644 --- a/roles/postgres/handlers/main.yml +++ b/roles/postgres/handlers/main.yml @@ -8,3 +8,8 @@ ansible.builtin.systemd: name: "{{ postgres_service_name }}" state: reloaded + +- name: Restart postgres exporter + ansible.builtin.systemd: + name: "{{ postgres_exporter_service }}" + state: restarted diff --git a/roles/postgres/tasks/exporter.yml b/roles/postgres/tasks/exporter.yml new file mode 100644 index 0000000..2c6b0b6 --- /dev/null +++ b/roles/postgres/tasks/exporter.yml @@ -0,0 +1,54 @@ +--- +# Prometheus postgres exporter (optional, gated by postgres_exporter_enabled). +# Creates a minimal monitoring role (pg_monitor) and runs postgres_exporter +# against the local instance. + +- name: Validate exporter password is set + ansible.builtin.assert: + that: + - postgres_exporter_password is defined + - postgres_exporter_password | length >= 12 + fail_msg: | + postgres_exporter_password is required (min 12 chars) when + postgres_exporter_enabled is true. + See roles/postgres/defaults/main.yml for configuration instructions. + success_msg: "Exporter password validation passed" + +- name: Install postgres_exporter + ansible.builtin.package: + name: "{{ postgres_exporter_package }}" + state: present + +- name: Create postgres_exporter monitoring role + community.postgresql.postgresql_user: + name: "{{ postgres_exporter_user }}" + password: "{{ postgres_exporter_password }}" + role_attr_flags: NOSUPERUSER,NOCREATEDB,NOCREATEROLE + state: present + become: false + become_user: "{{ postgres_admin_user }}" + +- name: Grant pg_monitor to postgres_exporter + community.postgresql.postgresql_membership: + group: pg_monitor + target_roles: + - "{{ postgres_exporter_user }}" + state: present + become: false + become_user: "{{ postgres_admin_user }}" + +- name: Deploy exporter environment file + ansible.builtin.template: + src: exporter-env.conf.j2 + dest: "{{ postgres_exporter_args_file }}" + owner: root + group: root + mode: "0600" + notify: Restart postgres exporter + +- name: Enable and start postgres exporter + ansible.builtin.systemd: + name: "{{ postgres_exporter_service }}" + enabled: true + state: started + daemon_reload: true diff --git a/roles/postgres/tasks/main.yml b/roles/postgres/tasks/main.yml index bb5c917..01271f4 100644 --- a/roles/postgres/tasks/main.yml +++ b/roles/postgres/tasks/main.yml @@ -99,3 +99,7 @@ state: present become: false become_user: "{{ postgres_admin_user }}" + +- name: Deploy Prometheus postgres exporter + ansible.builtin.include_tasks: exporter.yml + when: postgres_exporter_enabled | bool diff --git a/roles/postgres/templates/exporter-env.conf.j2 b/roles/postgres/templates/exporter-env.conf.j2 new file mode 100644 index 0000000..0cfb098 --- /dev/null +++ b/roles/postgres/templates/exporter-env.conf.j2 @@ -0,0 +1,3 @@ +# Managed by Ansible - DO NOT EDIT MANUALLY +{{ postgres_exporter_args_var }}="--web.listen-address={{ postgres_exporter_listen_address }}" +DATA_SOURCE_NAME="postgresql://{{ postgres_exporter_user }}:{{ postgres_exporter_password }}@{{ postgres_exporter_pg_host }}:{{ postgres_exporter_pg_port }}/{{ postgres_exporter_pg_dbname }}?sslmode=disable" diff --git a/roles/postgres/vars/archlinux.yml b/roles/postgres/vars/archlinux.yml index fb811ae..fdca66b 100644 --- a/roles/postgres/vars/archlinux.yml +++ b/roles/postgres/vars/archlinux.yml @@ -11,3 +11,9 @@ postgres_service_name: postgresql postgres_config_path: "{{ postgres_data_dir }}/postgresql.conf" postgres_config_dir: "{{ postgres_data_dir }}/conf.d" postgres_hba_path: "{{ postgres_data_dir }}/pg_hba.conf" + +# Prometheus exporter +postgres_exporter_package: prometheus-postgres-exporter +postgres_exporter_service: prometheus-postgres-exporter +postgres_exporter_args_file: /etc/conf.d/prometheus-postgres-exporter +postgres_exporter_args_var: POSTGRES_EXPORTER_ARGS diff --git a/roles/postgres/vars/debian.yml b/roles/postgres/vars/debian.yml index fd71fd0..bb344a9 100644 --- a/roles/postgres/vars/debian.yml +++ b/roles/postgres/vars/debian.yml @@ -8,3 +8,9 @@ postgres_service_name: postgresql postgres_config_path: /etc/postgresql/current/main/postgresql.conf postgres_config_dir: /etc/postgresql/current/main/conf.d postgres_hba_path: /etc/postgresql/current/main/pg_hba.conf + +# Prometheus exporter +postgres_exporter_package: prometheus-postgres-exporter +postgres_exporter_service: prometheus-postgres-exporter +postgres_exporter_args_file: /etc/default/prometheus-postgres-exporter +postgres_exporter_args_var: ARGS diff --git a/roles/valkey/README.md b/roles/valkey/README.md index a1fedf0..4325963 100644 --- a/roles/valkey/README.md +++ b/roles/valkey/README.md @@ -326,6 +326,26 @@ INFO memory INFO stats ``` +### Prometheus exporter + +Enable the exporter and set a password to create a dedicated read-only +`valkey_exporter` ACL user: + +```yaml +valkey_exporter_enabled: true +valkey_exporter_password: "" +``` + +The role appends the user to `users.acl` and runs `redis_exporter` on +`127.0.0.1:9121`. Pair it with a scrape job: + +```yaml +prometheus_scrape_configs: + - job_name: 'valkey' + static_configs: + - targets: ['127.0.0.1:9121'] +``` + Check connected clients: ```bash diff --git a/roles/valkey/defaults/main.yml b/roles/valkey/defaults/main.yml index 16fefa4..afba624 100644 --- a/roles/valkey/defaults/main.yml +++ b/roles/valkey/defaults/main.yml @@ -35,6 +35,20 @@ valkey_acl_users: [] # keypattern: "*" # Keys this user can access # commands: "+@all -@dangerous" # Allowed commands +# Prometheus exporter (optional, via redis_exporter). Creates a dedicated +# read-only ACL user for monitoring. +valkey_exporter_enabled: false +valkey_exporter_listen_address: "127.0.0.1:9121" +valkey_exporter_redis_addr: "redis://127.0.0.1:6379" +valkey_exporter_user: valkey_exporter +# valkey_exporter_password: "" # Intentionally undefined - required when enabled +# Read-only command set recommended by redis_exporter for monitoring. +valkey_exporter_commands: >- + -@all +@connection +memory -readonly +strlen +config|get +xinfo +pfcount + -quit +zcard +type +xlen -readwrite -command +client -wait +scard +llen + +hlen +arcount +get +eval +slowlog +cluster|info +cluster|slots +cluster|nodes + -hello -echo +info +latency +scan -reset -auth -asking + # Valkey log level (debug, verbose, notice, warning) valkey_loglevel: notice diff --git a/roles/valkey/handlers/main.yml b/roles/valkey/handlers/main.yml index 1b55aaa..66ca973 100644 --- a/roles/valkey/handlers/main.yml +++ b/roles/valkey/handlers/main.yml @@ -13,6 +13,11 @@ name: "{{ valkey_service_name }}" state: reloaded +- name: Restart valkey exporter + ansible.builtin.systemd: + name: "{{ valkey_exporter_service }}" + state: restarted + - name: Update GRUB ansible.builtin.shell: | if command -v update-grub &> /dev/null; then diff --git a/roles/valkey/tasks/exporter.yml b/roles/valkey/tasks/exporter.yml new file mode 100644 index 0000000..ce3dd84 --- /dev/null +++ b/roles/valkey/tasks/exporter.yml @@ -0,0 +1,36 @@ +--- +# Prometheus valkey exporter (optional, gated by valkey_exporter_enabled). +# Authenticates as the read-only valkey_exporter ACL user created by the +# users.acl template. + +- name: Validate exporter password is set + ansible.builtin.assert: + that: + - valkey_exporter_password is defined + - valkey_exporter_password | length >= 12 + fail_msg: | + valkey_exporter_password is required (min 12 chars) when + valkey_exporter_enabled is true. + See roles/valkey/defaults/main.yml for configuration instructions. + success_msg: "Exporter password validation passed" + +- name: Install redis_exporter + ansible.builtin.package: + name: "{{ valkey_exporter_package }}" + state: present + +- name: Deploy exporter environment file + ansible.builtin.template: + src: exporter-env.conf.j2 + dest: "{{ valkey_exporter_args_file }}" + owner: root + group: root + mode: "0600" + notify: Restart valkey exporter + +- name: Enable and start redis_exporter + ansible.builtin.systemd: + name: "{{ valkey_exporter_service }}" + enabled: true + state: started + daemon_reload: true diff --git a/roles/valkey/tasks/main.yml b/roles/valkey/tasks/main.yml index faae244..7512274 100644 --- a/roles/valkey/tasks/main.yml +++ b/roles/valkey/tasks/main.yml @@ -60,3 +60,7 @@ delay: 2 register: ufw_result until: ufw_result is succeeded + +- name: Deploy Prometheus valkey exporter + ansible.builtin.include_tasks: exporter.yml + when: valkey_exporter_enabled | bool diff --git a/roles/valkey/templates/exporter-env.conf.j2 b/roles/valkey/templates/exporter-env.conf.j2 new file mode 100644 index 0000000..6c8a312 --- /dev/null +++ b/roles/valkey/templates/exporter-env.conf.j2 @@ -0,0 +1,5 @@ +# Managed by Ansible - DO NOT EDIT MANUALLY +{{ valkey_exporter_args_var }}="--web.listen-address={{ valkey_exporter_listen_address }}" +REDIS_ADDR="{{ valkey_exporter_redis_addr }}" +REDIS_USER="{{ valkey_exporter_user }}" +REDIS_PASSWORD="{{ valkey_exporter_password }}" diff --git a/roles/valkey/templates/users.acl.j2 b/roles/valkey/templates/users.acl.j2 index dd435e9..f0cedd1 100644 --- a/roles/valkey/templates/users.acl.j2 +++ b/roles/valkey/templates/users.acl.j2 @@ -2,3 +2,6 @@ user default on >{{ valkey_admin_password }} ~* &* +@all {% for acl_user in valkey_acl_users %} user {{ acl_user.username }} on >{{ acl_user.password }} {% for pattern in acl_user.keypattern.split() %}~{{ pattern }} {% endfor %}{{ acl_user.commands | default('+@all -@dangerous') }} {% endfor %} +{% if valkey_exporter_enabled | bool %} +user {{ valkey_exporter_user }} on >{{ valkey_exporter_password }} ~* {{ valkey_exporter_commands }} +{% endif %} diff --git a/roles/valkey/vars/archlinux.yml b/roles/valkey/vars/archlinux.yml index f667b51..0793e1c 100644 --- a/roles/valkey/vars/archlinux.yml +++ b/roles/valkey/vars/archlinux.yml @@ -6,3 +6,9 @@ valkey_config_file: /etc/valkey/valkey.conf valkey_user: valkey valkey_group: valkey valkey_dir: /var/lib/valkey + +# Prometheus exporter +valkey_exporter_package: prometheus-redis-exporter +valkey_exporter_service: prometheus-redis-exporter +valkey_exporter_args_file: /etc/conf.d/prometheus-redis-exporter +valkey_exporter_args_var: REDIS_EXPORTER_ARGS diff --git a/roles/valkey/vars/debian.yml b/roles/valkey/vars/debian.yml index 368d79a..b3c9206 100644 --- a/roles/valkey/vars/debian.yml +++ b/roles/valkey/vars/debian.yml @@ -6,3 +6,9 @@ valkey_config_file: /etc/valkey/valkey.conf valkey_user: valkey valkey_group: valkey valkey_dir: /var/lib/valkey + +# Prometheus exporter +valkey_exporter_package: prometheus-redis-exporter +valkey_exporter_service: prometheus-redis-exporter +valkey_exporter_args_file: /etc/default/prometheus-redis-exporter +valkey_exporter_args_var: ARGS