diff --git a/.ansible-lint b/.ansible-lint
index 5f1bb03f5d7e6736afe240895bfc09473536d894..e2e7f6c9428e065d1af6ee23afa2cf5bf4c87f1f 100644
--- a/.ansible-lint
+++ b/.ansible-lint
@@ -1,9 +1,3 @@
-parseable: true
-quiet: true
+---
+
 use_default_rules: true
-skip_list:
-  - '204'  # line length is checked by yamllint
-  - '401'  # git checkout must contain explicit version
-  - '701'  # 7xx is about ansible galaxy guidelines
-  - '702'
-  - '703'
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6506c3143166f79ffce9901b0eb2c9c950e6d357..2cb3b27ae2520ef1511e9bfc5339f3e432f29ff8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,11 +1,12 @@
 ---
 
-image: registry.git.fsmpi.rwth-aachen.de/infra/ci-containers/fsmpi-ansible:bullseye
+image: alpine:3.17
 
 variables:
   GIT_SUBMODULE_STRATEGY: recursive
 
 before_script:
+  - apk --no-cache add ansible ansible-lint yamllint ripgrep black
   - export LANG=en_US.UTF-8
   - chmod o-w .
   - ansible --version
@@ -19,5 +20,11 @@ test:
   stage: test
   script:
     - yamllint .
-    - ansible-lint ./*/
+    - >-
+      ansible-lint
+      --format codeclimate
+      > codeclimate.json
     - "! rg --fixed-strings 'passwordstore' ./*/templates"
+  artifacts:
+    reports:
+      codequality: codeclimate.json
diff --git a/alertmanager/handlers/main.yml b/alertmanager/handlers/main.yml
index 94a8c21a636aa943a469d880eb671a209bc09a1a..75340c12f9ef77f71281d5381c19993fcb348493 100644
--- a/alertmanager/handlers/main.yml
+++ b/alertmanager/handlers/main.yml
@@ -1,11 +1,11 @@
 ---
 
 - name: Restart alertmanager
-  systemd:
+  ansible.builtin.systemd:
     name: prometheus-alertmanager.service
     state: restarted
 
 - name: Reload alertmanager
-  systemd:
+  ansible.builtin.systemd:
     name: prometheus-alertmanager.service
     state: reloaded
diff --git a/alertmanager/tasks/main.yml b/alertmanager/tasks/main.yml
index 71f61859f31c7ac77e740124360203f2b989899c..33c431cfd1a9bdfebf389797f4ada5049f580c15 100644
--- a/alertmanager/tasks/main.yml
+++ b/alertmanager/tasks/main.yml
@@ -1,23 +1,29 @@
 ---
 
 - name: Install alertmanager
-  apt:
+  ansible.builtin.apt:
     name: prometheus-alertmanager
     state: present
 
 - name: Configure alertmanager command arguments
-  template:
+  ansible.builtin.template:
     src: default.j2
     dest: /etc/default/prometheus-alertmanager
+    owner: root
+    group: root
+    mode: "0644"
   notify:
     - Restart alertmanager
   tags:
     - config
 
 - name: Configure alertmanager
-  template:
+  ansible.builtin.template:
     src: alertmanager.yml.j2
     dest: /etc/prometheus/alertmanager.yml
+    owner: root
+    group: root
+    mode: "0644"
   notify:
     - Reload alertmanager
   tags:
diff --git a/grafana/handlers/main.yml b/grafana/handlers/main.yml
index d4d98b6e35ea5faff5d2f1707afc141fc44eaf31..a4204926a3997a5ac87979cefd69c7d7be2e8dd1 100644
--- a/grafana/handlers/main.yml
+++ b/grafana/handlers/main.yml
@@ -1,10 +1,10 @@
 ---
 
 - name: Restart Grafana
-  systemd:
+  ansible.builtin.systemd:
     name: grafana-server.service
     state: restarted
 
 - name: Reload systemd
-  systemd:
+  ansible.builtin.systemd:
     daemon_reload: true
diff --git a/grafana/tasks/main.yml b/grafana/tasks/main.yml
index 4b961b97846e93d615a48e8c70b2b283cd5a6b15..49ddf77d9cbcd1be848b21219c73155d503e57b0 100644
--- a/grafana/tasks/main.yml
+++ b/grafana/tasks/main.yml
@@ -1,9 +1,13 @@
 ---
 
 - name: Install Grafana repository keys
-  apt_key:
+  ansible.builtin.get_url:
     url: https://packages.grafana.com/gpg.key
-    state: present
+    dest: /etc/apt/trusted.gpg.d/grafana.asc
+    force: true
+    owner: root
+    group: root
+    mode: "0644"
   tags:
     - packages
     - repo
@@ -11,7 +15,7 @@
     - config
 
 - name: Install Grafana repo
-  apt_repository:
+  ansible.builtin.apt_repository:
     repo: "deb https://packages.grafana.com/enterprise/deb stable main"
   tags:
     - packages
@@ -20,7 +24,7 @@
     - config
 
 - name: Install Grafana
-  apt:
+  ansible.builtin.apt:
     name:
       - grafana-enterprise
     state: present
@@ -29,7 +33,7 @@
     - grafana
 
 - name: Create systemd unit override directory
-  file:
+  ansible.builtin.file:
     path: /etc/systemd/system/grafana-server.service.d
     state: directory
     owner: root
@@ -40,7 +44,7 @@
     - config
 
 - name: Configure Grafana systemd service
-  copy:
+  ansible.builtin.copy:
     src: grafana-server-override.service
     dest: /etc/systemd/system/grafana-server.service.d/ansible-override.conf
     owner: root
@@ -54,7 +58,7 @@
     - config
 
 - name: Configure Grafana
-  template:
+  ansible.builtin.template:
     src: grafana.ini.j2
     dest: /etc/grafana/grafana.ini
     owner: root
@@ -67,7 +71,7 @@
     - grafana
 
 - name: Configure Grafana LDAP auth
-  template:
+  ansible.builtin.template:
     src: ldap.toml.j2
     dest: /etc/grafana/ldap.toml
     owner: root
@@ -81,7 +85,8 @@
     - config
     - grafana
 
-- import_tasks: postgres.yml
+- name: Configure Postgres for Grafana
+  ansible.builtin.import_tasks: postgres.yml
   when:
     - grafana_database is defined
     - grafana_database.type == "postgres"
@@ -90,10 +95,11 @@
     - grafana
     - postgres
 
-- meta: flush_handlers
+- name: Flush handlers
+  ansible.builtin.meta: flush_handlers
 
 - name: Enable and start Grafana
-  systemd:
+  ansible.builtin.systemd:
     name: grafana-server.service
     state: started
     enabled: true
diff --git a/grafana/tasks/postgres.yml b/grafana/tasks/postgres.yml
index f69793bb92f49d36d184073ab8ee5fc288d41a62..ee1e1725f193c9402386afb8f63a2c13895b4f52 100644
--- a/grafana/tasks/postgres.yml
+++ b/grafana/tasks/postgres.yml
@@ -1,21 +1,22 @@
 ---
 
-- become: true
+- name: Become postgres system user
+  become: true
   become_user: postgres
   block:
     - name: Create postgres user
-      postgresql_user:
+      community.postgresql.postgresql_user:
         name: grafana
         state: present
 
     - name: Create database
-      postgresql_db:
+      community.postgresql.postgresql_db:
         name: grafana
         owner: grafana
         state: present
 
     - name: Grant database privileges
-      postgresql_privs:
+      community.postgresql.postgresql_privs:
         database: grafana
         privs: ALL
         state: present
diff --git a/mysqld_exporter/handlers/main.yml b/mysqld_exporter/handlers/main.yml
index 0c1a1c1143c0504c6607e99777d853d23a9f9ddf..a3779728b30ae3ac9f084a26513096919c951bba 100644
--- a/mysqld_exporter/handlers/main.yml
+++ b/mysqld_exporter/handlers/main.yml
@@ -1,6 +1,6 @@
 ---
 
 - name: Restart mysqld_exporter
-  systemd:
+  ansible.builtin.systemd:
     name: prometheus-mysqld-exporter.service
     state: restarted
diff --git a/mysqld_exporter/tasks/main.yml b/mysqld_exporter/tasks/main.yml
index e84070c33c913484db2c8dfbf2fed349ea959ae4..a2a44fcd325f46850e5da6977dc2e0a6e813a850 100644
--- a/mysqld_exporter/tasks/main.yml
+++ b/mysqld_exporter/tasks/main.yml
@@ -1,7 +1,7 @@
 ---
 
 - name: Install mysqld_exporter
-  apt:
+  ansible.builtin.apt:
     name: prometheus-mysqld-exporter
     state: present
   when: ansible_distribution_major_version|int >= 10
@@ -10,7 +10,7 @@
     - prometheus-exporter
 
 - name: Install mysqld_exporter (stretch)
-  apt:
+  ansible.builtin.apt:
     name: prometheus-mysqld-exporter
     state: present
     default_release: stretch-backports
@@ -20,9 +20,12 @@
     - prometheus-exporter
 
 - name: Configure mysqld_exporter
-  template:
+  ansible.builtin.template:
     src: prometheus-mysqld-exporter.j2
     dest: /etc/default/prometheus-mysqld-exporter
+    owner: root
+    group: root
+    mode: "0644"
   notify:
     - Restart mysqld_exporter
   tags:
@@ -31,9 +34,12 @@
     - config
 
 - name: Configure Prometheus server to scrape us
-  template:
+  ansible.builtin.template:
     src: scrape.yml.j2
     dest: "/etc/prometheus/scrape/mysqld_{{ ansible_fqdn }}.yml"
+    owner: root
+    group: root
+    mode: "0644"
   delegate_to: "{{ prometheus_host }}"
   tags:
     - prometheus
diff --git a/node_exporter/handlers/main.yml b/node_exporter/handlers/main.yml
index f25d9e7a2440def84a001b76b2539cec5b5a95cf..61d109da76bdaa718f44395fc9307b51d571e860 100644
--- a/node_exporter/handlers/main.yml
+++ b/node_exporter/handlers/main.yml
@@ -1,6 +1,6 @@
 ---
 
 - name: Restart node_exporter
-  systemd:
+  ansible.builtin.systemd:
     name: prometheus-node-exporter.service
     state: restarted
diff --git a/node_exporter/tasks/main.yml b/node_exporter/tasks/main.yml
index 4f187771df09eedd6ea0d61e47c8090cd9606fa8..f43ef2227b76190f7129aadb6ce8f23a8debc8a7 100644
--- a/node_exporter/tasks/main.yml
+++ b/node_exporter/tasks/main.yml
@@ -1,7 +1,7 @@
 ---
 
 - name: Install node_exporter
-  apt:
+  ansible.builtin.apt:
     name: prometheus-node-exporter
     state: present
   when: ansible_distribution_major_version|int >= 10
@@ -10,7 +10,7 @@
     - prometheus-exporter
 
 - name: Install node_exporter (stretch)
-  apt:
+  ansible.builtin.apt:
     name: prometheus-node-exporter
     state: present
     default_release: stretch-backports
@@ -20,7 +20,7 @@
     - prometheus-exporter
 
 - name: Install additional node_exporter collectors
-  apt:
+  ansible.builtin.apt:
     name: prometheus-node-exporter-collectors
     state: present
   when: ansible_distribution_major_version|int >= 11
@@ -29,7 +29,7 @@
     - prometheus-exporter
 
 - name: Ensure smartmontools is present only on bare-metal hosts
-  apt:
+  ansible.builtin.apt:
     name: smartmontools
     state: >-
       {% if force_smartmontools_on_vm_guest or
@@ -41,9 +41,12 @@
       {%- endif %}
 
 - name: Configure node_exporter
-  template:
+  ansible.builtin.template:
     src: prometheus-node-exporter.j2
     dest: /etc/default/prometheus-node-exporter
+    owner: root
+    group: root
+    mode: "0644"
   notify:
     - Restart node_exporter
   tags:
@@ -52,14 +55,18 @@
     - config
 
 - name: Configure Prometheus server to scrape us
-  template:
+  ansible.builtin.template:
     src: scrape.yml.j2
     dest: "/etc/prometheus/scrape/node_{{ ansible_fqdn }}.yml"
+    owner: root
+    group: root
+    mode: "0644"
   delegate_to: "{{ prometheus_host }}"
   tags:
     - prometheus
     - prometheus-exporter
     - config
 
-- import_tasks: needrestart.yml
+- name: Configure needrestart integration
+  ansible.builtin.import_tasks: needrestart.yml
   when: node_exporter_needrestart
diff --git a/node_exporter/tasks/needrestart.yml b/node_exporter/tasks/needrestart.yml
index 8a3be68ea066426420cf15235e3aa4f9c5804db2..07a518ad6ff4d4a3ae9e495276b382925b9a151b 100644
--- a/node_exporter/tasks/needrestart.yml
+++ b/node_exporter/tasks/needrestart.yml
@@ -1,7 +1,7 @@
 ---
 
 - name: Install needrestart
-  apt:
+  ansible.builtin.apt:
     name: needrestart
     state: present
   tags:
@@ -9,7 +9,7 @@
     - prometheus-exporter
 
 - name: Install needrestart2prom
-  get_url:
+  ansible.builtin.get_url:
     url: >-
       https://git.fsmpi.rwth-aachen.de/api/v4/projects/233/packages/generic/needrestart2prom/{{
       needrestart2prom_version }}/needrestart2prom-{{ ansible_system|lower }}-{{
@@ -24,7 +24,7 @@
     - prometheus-exporter
 
 - name: Configure needrestart2prom cronjob PATH
-  cron:
+  ansible.builtin.cron:
     cron_file: needrestart2prom
     user: root
     env: true
@@ -36,7 +36,7 @@
     - prometheus-exporter
 
 - name: Configure needrestart2prom cronjob
-  cron:
+  ansible.builtin.cron:
     cron_file: needrestart2prom
     user: root
     name: needrestart2prom
diff --git a/prometheus/handlers/main.yml b/prometheus/handlers/main.yml
index 0be95fde5cf43a7cc63ad41056e53e48512d5446..208fecb0460de0f6074758f9cc328925d5b13e46 100644
--- a/prometheus/handlers/main.yml
+++ b/prometheus/handlers/main.yml
@@ -1,11 +1,11 @@
 ---
 
 - name: Restart prometheus
-  systemd:
+  ansible.builtin.systemd:
     name: prometheus.service
     state: restarted
 
 - name: Reload prometheus
-  systemd:
+  ansible.builtin.systemd:
     name: prometheus.service
     state: reloaded
diff --git a/prometheus/tasks/main.yml b/prometheus/tasks/main.yml
index ce4f8e2c3a41b0b832598f3e9973f243668bd6ee..d5a7f63f2297df5be8397c72ead57a5a28118d5f 100644
--- a/prometheus/tasks/main.yml
+++ b/prometheus/tasks/main.yml
@@ -1,7 +1,7 @@
 ---
 
 - name: Install prometheus
-  apt:
+  ansible.builtin.apt:
     name:
       - prometheus
     state: present
@@ -9,9 +9,12 @@
     - prometheus
 
 - name: Configure prometheus command arguments
-  template:
+  ansible.builtin.template:
     src: default-prometheus.j2
     dest: /etc/default/prometheus
+    owner: root
+    group: root
+    mode: "0644"
   notify:
     - Restart prometheus
   tags:
@@ -19,10 +22,13 @@
     - config
 
 - name: Configure prometheus
-  template:
+  ansible.builtin.template:
     src: prometheus.yml.j2
     dest: /etc/prometheus/prometheus.yml
     validate: "promtool check config %s"
+    owner: root
+    group: root
+    mode: "0644"
   notify:
     - Reload prometheus
   tags:
@@ -30,9 +36,12 @@
     - config
 
 - name: Create necessary directories
-  file:
+  ansible.builtin.file:
     path: "/etc/prometheus/{{ item }}"
     state: directory
+    owner: root
+    group: root
+    mode: "0755"
   with_items:
     - alertmanagers
     - rules
@@ -42,10 +51,13 @@
     - config
 
 - name: Configure rules
-  template:
+  ansible.builtin.template:
     src: "rules.yml.j2"
     dest: "/etc/prometheus/rules/ansible_rules.yml"
     validate: "promtool check rules %s"
+    owner: root
+    group: root
+    mode: "0644"
   notify:
     - Reload prometheus
   tags: