From 2f3eebd422eae68e9ab5e10ae36defddc4a40bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20D=C3=A9siles?= <1536672+cdesiles@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:01:00 +0200 Subject: [PATCH] feat: add metabase role --- roles/metabase/README.md | 20 +++ roles/metabase/defaults/main.yml | 16 +++ roles/metabase/handlers/main.yml | 20 +++ roles/metabase/meta/main.yml | 4 + roles/metabase/tasks/main.yml | 130 +++++++++++++++++++ roles/metabase/templates/metabase.service.j2 | 15 +++ roles/metabase/templates/metabase.yaml.j2 | 42 ++++++ roles/metabase/templates/nginx-vhost.conf.j2 | 49 +++++++ 8 files changed, 296 insertions(+) create mode 100644 roles/metabase/README.md create mode 100644 roles/metabase/defaults/main.yml create mode 100644 roles/metabase/handlers/main.yml create mode 100644 roles/metabase/meta/main.yml create mode 100644 roles/metabase/tasks/main.yml create mode 100644 roles/metabase/templates/metabase.service.j2 create mode 100644 roles/metabase/templates/metabase.yaml.j2 create mode 100644 roles/metabase/templates/nginx-vhost.conf.j2 diff --git a/roles/metabase/README.md b/roles/metabase/README.md new file mode 100644 index 0000000..f5409e7 --- /dev/null +++ b/roles/metabase/README.md @@ -0,0 +1,20 @@ +# Metabase + +Business intelligence and analytics. Defaults: [`defaults/main.yml`](defaults/main.yml). + +## Requirements + +- `podman` role +- `postgres` role +- `nginx` role (optional, for public access) + +## Usage + +Set in inventory: + +```yaml +metabase_postgres_password: "strongpassword" +metabase_postgres_host: "{{ podman_gw_gateway }}" +metabase_nginx_enabled: true +metabase_nginx_hostname: metabase.example.com +``` diff --git a/roles/metabase/defaults/main.yml b/roles/metabase/defaults/main.yml new file mode 100644 index 0000000..8f6699e --- /dev/null +++ b/roles/metabase/defaults/main.yml @@ -0,0 +1,16 @@ +--- +metabase_version: latest +metabase_image: metabase/metabase + +metabase_port: 3000 + +metabase_postgres_db_name: metabase +metabase_postgres_user: metabase +# metabase_postgres_password: "" # Intentionally undefined - role will fail if not set +# metabase_postgres_host: "" # Must be set in inventory (e.g. "{{ podman_gw_gateway }}") +metabase_postgres_port: 5432 + +metabase_timezone: UTC + +metabase_nginx_enabled: false +metabase_nginx_hostname: metabase.nas.local diff --git a/roles/metabase/handlers/main.yml b/roles/metabase/handlers/main.yml new file mode 100644 index 0000000..637d314 --- /dev/null +++ b/roles/metabase/handlers/main.yml @@ -0,0 +1,20 @@ +--- +- name: Reload systemd user + ansible.builtin.systemd: + daemon_reload: true + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Restart Metabase + ansible.builtin.systemd: + name: metabase.service + state: restarted + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Reload nginx + ansible.builtin.systemd: + name: nginx + state: reloaded diff --git a/roles/metabase/meta/main.yml b/roles/metabase/meta/main.yml new file mode 100644 index 0000000..0eff038 --- /dev/null +++ b/roles/metabase/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: podman + - role: postgres diff --git a/roles/metabase/tasks/main.yml b/roles/metabase/tasks/main.yml new file mode 100644 index 0000000..3532945 --- /dev/null +++ b/roles/metabase/tasks/main.yml @@ -0,0 +1,130 @@ +--- +- name: Validate required passwords are set + ansible.builtin.assert: + that: + - metabase_postgres_password is defined + - metabase_postgres_password | length >= 12 + fail_msg: | + metabase_postgres_password is required (min 12 chars). + See roles/metabase/defaults/main.yml for configuration instructions. + success_msg: "Password validation passed" + +- name: Create PostgreSQL database for Metabase + community.postgresql.postgresql_db: + name: "{{ metabase_postgres_db_name }}" + owner: "{{ metabase_postgres_user }}" + state: present + become: false + become_user: "{{ postgres_admin_user | default('postgres') }}" + +- name: Create PostgreSQL user for Metabase + community.postgresql.postgresql_user: + name: "{{ metabase_postgres_user }}" + password: "{{ metabase_postgres_password }}" + state: present + become: false + become_user: "{{ postgres_admin_user | default('postgres') }}" + +- name: Grant all privileges on database to Metabase user + community.postgresql.postgresql_privs: + login_db: "{{ metabase_postgres_db_name }}" + roles: "{{ metabase_postgres_user }}" + type: database + privs: ALL + state: present + become: false + become_user: "{{ postgres_admin_user | default('postgres') }}" + +- name: Ensure Metabase user has no superuser privileges + community.postgresql.postgresql_user: + name: "{{ metabase_postgres_user }}" + role_attr_flags: NOSUPERUSER,NOCREATEDB,NOCREATEROLE + state: present + become: false + become_user: "{{ postgres_admin_user | default('postgres') }}" + +- name: Create Metabase project directory + ansible.builtin.file: + path: "{{ podman_projects_dir }}/metabase" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + +- name: Pull Metabase container image + ansible.builtin.command: "podman pull {{ metabase_image }}:{{ metabase_version }}" + register: pull_result + changed_when: pull_result.stdout is search('Writing manifest') + become: false + become_user: "{{ ansible_user }}" + +- name: Deploy Kubernetes YAML for Metabase + ansible.builtin.template: + src: metabase.yaml.j2 + dest: "{{ podman_projects_dir }}/metabase/metabase.yaml" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0644" + notify: Restart Metabase + +- name: Get home directory for {{ ansible_user }} + ansible.builtin.getent: + database: passwd + key: "{{ ansible_user }}" + +- name: Set user home directory fact + ansible.builtin.set_fact: + user_home_dir: "{{ ansible_facts['getent_passwd'][ansible_user][4] }}" + +- name: Create systemd user directory for Metabase + ansible.builtin.file: + path: "{{ user_home_dir }}/.config/systemd/user" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + +- name: Deploy systemd service for Metabase (user scope) + ansible.builtin.template: + src: metabase.service.j2 + dest: "{{ user_home_dir }}/.config/systemd/user/metabase.service" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0644" + notify: Reload systemd user + +- name: Enable lingering for user {{ ansible_user }} + ansible.builtin.command: "loginctl enable-linger {{ ansible_user }}" + when: ansible_user != 'root' + +- name: Enable and start Metabase service (user scope) + ansible.builtin.systemd: + name: metabase.service + enabled: true + state: started + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Provision TLS certificate for Metabase + ansible.builtin.include_tasks: "{{ role_path }}/../nginx/tasks/certbot.yml" + vars: + certbot_hostname: "{{ metabase_nginx_hostname }}" + when: metabase_nginx_enabled + +- name: Deploy nginx vhost configuration for Metabase + ansible.builtin.template: + src: nginx-vhost.conf.j2 + dest: "{{ nginx_conf_dir | default('/etc/nginx/conf.d') }}/metabase.conf" + owner: root + group: root + mode: "0644" + when: metabase_nginx_enabled + notify: Reload nginx + +- name: Remove nginx vhost configuration for Metabase + ansible.builtin.file: + path: "{{ nginx_conf_dir | default('/etc/nginx/conf.d') }}/metabase.conf" + state: absent + when: not metabase_nginx_enabled + notify: Reload nginx diff --git a/roles/metabase/templates/metabase.service.j2 b/roles/metabase/templates/metabase.service.j2 new file mode 100644 index 0000000..44468e2 --- /dev/null +++ b/roles/metabase/templates/metabase.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=Metabase BI Server + +[Service] +Type=notify +NotifyAccess=all +WorkingDirectory={{ podman_projects_dir }}/metabase +ExecStart=/usr/bin/podman kube play --replace --service-container=true --network=pasta:--map-host-loopback={{ podman_gw_gateway }} metabase.yaml +ExecStop=/usr/bin/podman kube down metabase.yaml +Restart=on-failure +RestartSec=10 +TimeoutStartSec=180 + +[Install] +WantedBy=default.target diff --git a/roles/metabase/templates/metabase.yaml.j2 b/roles/metabase/templates/metabase.yaml.j2 new file mode 100644 index 0000000..d1e5293 --- /dev/null +++ b/roles/metabase/templates/metabase.yaml.j2 @@ -0,0 +1,42 @@ +apiVersion: v1 +kind: Pod +metadata: + name: metabase +spec: + containers: + - name: server + image: {{ metabase_image }}:{{ metabase_version }} + ports: + - containerPort: 3000 + hostPort: {{ metabase_port }} + env: + - name: MB_DB_TYPE + value: postgres + - name: MB_DB_DBNAME + value: "{{ metabase_postgres_db_name }}" + - name: MB_DB_PORT + value: "{{ metabase_postgres_port }}" + - name: MB_DB_USER + value: "{{ metabase_postgres_user }}" + - name: MB_DB_PASS + value: "{{ metabase_postgres_password }}" + - name: MB_DB_HOST + value: "{{ metabase_postgres_host }}" + - name: JAVA_TIMEZONE + value: "{{ metabase_timezone }}" + volumeMounts: + - name: localtime + mountPath: /etc/localtime + readOnly: true + livenessProbe: + httpGet: + path: /api/health + port: 3000 + initialDelaySeconds: 90 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + restartPolicy: Never + volumes: + - name: localtime + hostPath: { path: /etc/localtime, type: File } diff --git a/roles/metabase/templates/nginx-vhost.conf.j2 b/roles/metabase/templates/nginx-vhost.conf.j2 new file mode 100644 index 0000000..0dfb04e --- /dev/null +++ b/roles/metabase/templates/nginx-vhost.conf.j2 @@ -0,0 +1,49 @@ +# Metabase vhost +# Managed by Ansible - DO NOT EDIT MANUALLY + +server { + listen 80; + listen [::]:80; + server_name {{ metabase_nginx_hostname }}; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + location / { + return 301 https://$server_name$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name {{ metabase_nginx_hostname }}; + + ssl_certificate /etc/letsencrypt/live/{{ metabase_nginx_hostname }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ metabase_nginx_hostname }}/privkey.pem; + 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_metabase; + error_log syslog:server=unix:/dev/log,nohostname,tag=nginx_metabase; +{% else %} + access_log /var/log/nginx/{{ metabase_nginx_hostname }}_access.log main; + error_log /var/log/nginx/{{ metabase_nginx_hostname }}_error.log; +{% endif %} + + location / { + proxy_pass http://127.0.0.1:{{ metabase_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; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_connect_timeout 60s; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + } +}