mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 22:46:39 +02:00
feat(infra): vollständige Ansible-Struktur Phase 1
- Alle Ansible-Rollen erstellt: common, disk_setup, docker, postgresql, pgbouncer, redis, nginx, zabbix_agent - ansible.cfg mit Pipeline-Optimierung - hosts.yml mit echten IPs (DBS01=.20, APS01=.21, WEB01=.22) - group_vars für alle Server (dbs, aps, web) - Zabbix-Server auf sentinel.xinion.de gesetzt - vault.yml.example als Vorlage für Secrets - site.yml nutzt import_playbook (DBS01→APS01→WEB01) - BRIEFING.md für alle 4 Repos angelegt (Platform, Apps, Infra, Shared) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0c8a23ddc4
commit
36196457ea
43 changed files with 1570 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -50,6 +50,9 @@ tmp/
|
|||
temp/
|
||||
*.tmp
|
||||
|
||||
# SSH Keys & Deploy Keys (NIEMALS committen!)
|
||||
.keys/
|
||||
|
||||
# Certificates (generierte Zertifikate, nicht die CA-Config)
|
||||
config/step-ca/secrets/
|
||||
config/step-ca/db/
|
||||
|
|
|
|||
79
repos/INSIGHT-Apps/BRIEFING.md
Normal file
79
repos/INSIGHT-Apps/BRIEFING.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# INSIGHT-Apps — Developer Briefing
|
||||
|
||||
> Dieses Dokument ist der Einstiegspunkt für jede neue Claude Code Session in diesem Repo.
|
||||
> Lies es vollständig bevor du Code schreibst.
|
||||
|
||||
## Was ist dieses Repo?
|
||||
|
||||
**INSIGHT-Apps** enthält alle fachlichen Module der INSIGHT-Plattform.
|
||||
Jedes Modul ist ein eigenständiges NestJS-Service + React-Frontend-Paket,
|
||||
das sich über den `InsightModule`-Contract in die Platform-Shell einbindet.
|
||||
|
||||
## Struktur
|
||||
|
||||
```
|
||||
packages/
|
||||
├── crm-service/ # NestJS: Company, Contact, Deal, Activity, Pipeline
|
||||
└── crm-frontend/ # React: CRM-Seiten → wird in INSIGHT-Platform Shell eingebunden
|
||||
# zukünftig:
|
||||
# ├── hr-service/
|
||||
# └── hr-frontend/
|
||||
```
|
||||
|
||||
## Module-Contract (PFLICHT lesen!)
|
||||
|
||||
Jedes Frontend-Paket **muss** einen `InsightModule`-Manifest exportieren.
|
||||
Siehe: `INSIGHT-Shared/contracts/module-interface.md`
|
||||
|
||||
```typescript
|
||||
// Beispiel crm-frontend/src/index.ts
|
||||
export const CrmModule: InsightModule = {
|
||||
id: 'crm',
|
||||
name: 'CRM',
|
||||
requiredRole: 'crm',
|
||||
routes: [...],
|
||||
navigation: [...],
|
||||
}
|
||||
```
|
||||
|
||||
## Abhängigkeiten zu anderen Repos
|
||||
|
||||
| Repo | Beziehung |
|
||||
|------|-----------|
|
||||
| `INSIGHT-Shared` | Lesen: module-interface.md, api-platform.md — Schreiben: api-crm.md nach Änderungen |
|
||||
| `INSIGHT-Platform` | Nutzt Core-Service APIs (Auth, User) — eigene Module registrieren sich in der Shell |
|
||||
| `INSIGHT-Infra` | Stellt DBS01 (PostgreSQL) und APS01 (Docker) bereit |
|
||||
|
||||
## Confluence Dokumentation
|
||||
|
||||
- **Phase 3 — CRM Modul:** https://xinion.atlassian.net/wiki/x/AoCUEw
|
||||
- **CRM-Datenmodell:** https://xinion.atlassian.net/wiki/x/AYCVEw
|
||||
- **Projekt-Übersicht:** https://xinion.atlassian.net/wiki/spaces/ProjektINS/overview
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend:** NestJS 10, TypeScript strict, Prisma ORM
|
||||
- **Frontend:** React 18, Vite, TanStack Query
|
||||
- **Datenbank:** PostgreSQL 16 (eigenes Schema, kein Multi-Tenancy)
|
||||
- **Auth:** Tokens kommen von INSIGHT-Platform Core-Service
|
||||
|
||||
## Sicherheitsregeln
|
||||
|
||||
- Jeder Endpoint mit `@Roles('crm')` oder spezifischerer Rolle absichern
|
||||
- Kein `any` in TypeScript
|
||||
- Daten-Ownership prüfen: User darf nur eigene Deals bearbeiten (außer CRM_ADMIN/MANAGER)
|
||||
- Soft-Delete (`deletedAt`) statt hartem Löschen
|
||||
|
||||
## Deploy Key (dieses Repo)
|
||||
|
||||
```
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAw2W+ChU6bjQdlTjHgK2FjMPXUD3/1vwNlU+aosmDqt deploy@INSIGHT-Apps
|
||||
```
|
||||
|
||||
Privater Key: `.keys/deploy_apps_ed25519`
|
||||
|
||||
## Aktueller Status
|
||||
|
||||
- **Phase:** 3 — CRM Modul
|
||||
- **Stand:** Konzeption (CRM-Datenmodell in Klärung)
|
||||
- **Nächster Schritt:** CRM-Datenmodell finalisieren → Prisma Schema → Company/Contact API
|
||||
82
repos/INSIGHT-Infra/BRIEFING.md
Normal file
82
repos/INSIGHT-Infra/BRIEFING.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# INSIGHT-Infra — Developer Briefing
|
||||
|
||||
> Dieses Dokument ist der Einstiegspunkt für jede neue Claude Code Session in diesem Repo.
|
||||
> Lies es vollständig bevor du Code schreibst.
|
||||
|
||||
## Was ist dieses Repo?
|
||||
|
||||
**INSIGHT-Infra** enthält alle Infrastruktur-Definitionen für die INSIGHT-Plattform:
|
||||
Ansible Playbooks zur Server-Provisionierung und Docker Compose Konfigurationen.
|
||||
|
||||
## Struktur
|
||||
|
||||
```
|
||||
ansible/
|
||||
├── inventory/
|
||||
│ ├── hosts.yml # DBS01, APS01, WEB01
|
||||
│ └── group_vars/
|
||||
│ ├── all.yml
|
||||
│ ├── insight_dbs.yml
|
||||
│ ├── insight_aps.yml
|
||||
│ └── insight_web.yml
|
||||
├── playbooks/
|
||||
│ ├── site.yml # Master (alle Server)
|
||||
│ ├── dbs01.yml
|
||||
│ ├── aps01.yml
|
||||
│ └── web01.yml
|
||||
└── roles/
|
||||
├── common/ # Hardening, NTP, SSH, unattended-upgrades
|
||||
├── docker/ # Docker CE + Compose Plugin
|
||||
├── postgresql/ # PostgreSQL 16 + PgBouncer
|
||||
├── redis/ # Redis 7
|
||||
├── nginx/ # Nginx + TLS
|
||||
└── zabbix_agent/ # Zabbix Agent 2
|
||||
```
|
||||
|
||||
## Server
|
||||
|
||||
| Server | Hostname | Rolle |
|
||||
|--------|----------|-------|
|
||||
| INSIGHT-DBS01 | insight-dbs01.xinion.lan | PostgreSQL 16 + PgBouncer + Redis 7 |
|
||||
| INSIGHT-APS01 | insight-aps01.xinion.lan | Docker: core-service, crm-service |
|
||||
| INSIGHT-WEB01 | insight-web01.xinion.lan | Nginx + React Build |
|
||||
|
||||
IPs und Details: https://xinion.atlassian.net/wiki/x/FYCVEw
|
||||
|
||||
## SSH Keys (Ansible → Server)
|
||||
|
||||
| Server | Key |
|
||||
|--------|-----|
|
||||
| DBS01 | `.keys/ansible_dbs01_ed25519` |
|
||||
| APS01 | `.keys/ansible_aps01_ed25519` |
|
||||
| WEB01 | `.keys/ansible_web01_ed25519` |
|
||||
|
||||
Öffentliche Keys sind via Cloud-Init bereits auf den Servern hinterlegt.
|
||||
|
||||
## Confluence Dokumentation
|
||||
|
||||
- **Phase 1 — Infrastruktur:** https://xinion.atlassian.net/wiki/x/PACVEw
|
||||
- **Hardware-Anforderungen:** https://xinion.atlassian.net/wiki/x/tYKSEw
|
||||
- **Cloud-Init Konfiguration:** https://xinion.atlassian.net/wiki/x/BgCREw
|
||||
- **SSH Keys & Zugangsverwaltung:** https://xinion.atlassian.net/wiki/x/UACVEw
|
||||
|
||||
## Wichtige Regeln
|
||||
|
||||
- Secrets ausschließlich in **Ansible Vault** (kein Klartext in Git)
|
||||
- Alle Tasks mit `become: true` wenn Root-Rechte nötig
|
||||
- Idempotenz: Playbooks müssen mehrfach ausführbar sein ohne Fehler
|
||||
- Nach jeder Infrastruktur-Änderung `dev-status/infra.md` in INSIGHT-Shared aktualisieren
|
||||
|
||||
## Deploy Key (dieses Repo)
|
||||
|
||||
```
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILKnKLskXQH6p9Wb+UuzPekLremPJCDxFzE1dchqkXkt deploy@insight-infra
|
||||
```
|
||||
|
||||
Privater Key: `.keys/deploy_infra_ed25519`
|
||||
|
||||
## Aktueller Status
|
||||
|
||||
- **Phase:** 1 — Infrastruktur
|
||||
- **Stand:** VMs angelegt (DBS01 VM 1020), SSH-Zugang funktioniert
|
||||
- **Nächster Schritt:** IPs eintragen → Ansible Inventory befüllen → Playbooks schreiben
|
||||
11
repos/INSIGHT-Infra/ansible/ansible.cfg
Normal file
11
repos/INSIGHT-Infra/ansible/ansible.cfg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[defaults]
|
||||
inventory = inventory/hosts.yml
|
||||
roles_path = roles
|
||||
host_key_checking = False
|
||||
retry_files_enabled = False
|
||||
stdout_callback = yaml
|
||||
callbacks_enabled = profile_tasks
|
||||
|
||||
[ssh_connection]
|
||||
pipelining = True
|
||||
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
|
||||
28
repos/INSIGHT-Infra/ansible/inventory/group_vars/all.yml
Normal file
28
repos/INSIGHT-Infra/ansible/inventory/group_vars/all.yml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
# Globale Variablen für alle INSIGHT-Server
|
||||
# Secrets gehören in ansible/vault.yml (Ansible Vault verschlüsselt)
|
||||
|
||||
# Zeitzone
|
||||
timezone: "Europe/Berlin"
|
||||
|
||||
# NTP Server (intern)
|
||||
ntp_servers:
|
||||
- 0.de.pool.ntp.org
|
||||
- 1.de.pool.ntp.org
|
||||
|
||||
# SSH Hardening
|
||||
ssh_port: 22
|
||||
ssh_permit_root_login: "no"
|
||||
ssh_password_authentication: "no"
|
||||
ssh_pubkey_authentication: "yes"
|
||||
|
||||
# Zabbix
|
||||
zabbix_server: "sentinel.xinion.de"
|
||||
zabbix_agent_version: "2"
|
||||
zabbix_hostname_prefix: "INSIGHT-"
|
||||
|
||||
# Docker
|
||||
docker_compose_version: "latest"
|
||||
|
||||
# Domain
|
||||
internal_domain: "xinion.lan"
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
# Variablen für INSIGHT-APS01
|
||||
|
||||
# Docker (core-service + crm-service)
|
||||
# App-Konfiguration kommt per .env via Vault / CI/CD
|
||||
|
||||
# Kein Data-Disk für APS01 geplant
|
||||
# Falls später Volume nötig, hier ergänzen
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
# Variablen für INSIGHT-DBS01
|
||||
|
||||
# PostgreSQL
|
||||
postgresql_version: "16"
|
||||
postgresql_data_dir: "/data/postgresql"
|
||||
postgresql_port: 5432
|
||||
postgresql_max_connections: 100 # PgBouncer übernimmt Pooling
|
||||
postgresql_shared_buffers: "512MB"
|
||||
postgresql_effective_cache_size: "2GB"
|
||||
postgresql_work_mem: "8MB"
|
||||
postgresql_maintenance_work_mem: "128MB"
|
||||
|
||||
# PostgreSQL Datenbanken (Secrets in vault.yml)
|
||||
postgresql_databases:
|
||||
- insight_core
|
||||
- insight_crm
|
||||
|
||||
# PgBouncer
|
||||
pgbouncer_port: 6432
|
||||
pgbouncer_pool_mode: "transaction"
|
||||
pgbouncer_default_pool_size: 20
|
||||
pgbouncer_min_pool_size: 5
|
||||
pgbouncer_reserve_pool_size: 5
|
||||
pgbouncer_max_client_conn: 200
|
||||
|
||||
# Redis
|
||||
redis_port: 6379
|
||||
redis_data_dir: "/data/redis"
|
||||
redis_maxmemory: "1gb"
|
||||
redis_maxmemory_policy: "allkeys-lru"
|
||||
|
||||
# Data Disk (separate Volume für Daten)
|
||||
# Wird von role/disk_setup gemountet
|
||||
data_disk_device: "/dev/sdb"
|
||||
data_disk_mountpoint: "/data"
|
||||
data_disk_filesystem: "ext4"
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
# Variablen für INSIGHT-WEB01
|
||||
|
||||
# Nginx
|
||||
nginx_webroot: "/var/www/insight"
|
||||
nginx_server_name: "insight.xinion.lan"
|
||||
aps01_ip: "172.20.10.21"
|
||||
|
||||
# Kein Data-Disk (WEB01 braucht keinen separaten Mount)
|
||||
35
repos/INSIGHT-Infra/ansible/inventory/hosts.yml
Normal file
35
repos/INSIGHT-Infra/ansible/inventory/hosts.yml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
# INSIGHT Ansible Inventory
|
||||
# IPs eintragen sobald Server provisioniert sind
|
||||
# Confluence: https://xinion.atlassian.net/wiki/x/FYCVEw
|
||||
|
||||
all:
|
||||
vars:
|
||||
ansible_user: ansible
|
||||
ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
|
||||
|
||||
children:
|
||||
insight_dbs:
|
||||
hosts:
|
||||
insight-dbs01:
|
||||
ansible_host: 172.20.10.20
|
||||
ansible_ssh_private_key_file: ~/.ssh/insight/ansible_dbs01_ed25519
|
||||
# Proxmox VM: 1020
|
||||
# Rolle: PostgreSQL 16 + PgBouncer + Redis 7
|
||||
# Data-Disk: /dev/sdb → /data (250 GB)
|
||||
|
||||
insight_aps:
|
||||
hosts:
|
||||
insight-aps01:
|
||||
ansible_host: 172.20.10.21
|
||||
ansible_ssh_private_key_file: ~/.ssh/insight/ansible_aps01_ed25519
|
||||
# Proxmox VM: 1021
|
||||
# Rolle: Docker (core-service, crm-service)
|
||||
|
||||
insight_web:
|
||||
hosts:
|
||||
insight-web01:
|
||||
ansible_host: 172.20.10.22
|
||||
ansible_ssh_private_key_file: ~/.ssh/insight/ansible_web01_ed25519
|
||||
# Proxmox VM: 1022
|
||||
# Rolle: Nginx + React Build
|
||||
12
repos/INSIGHT-Infra/ansible/playbooks/aps01.yml
Normal file
12
repos/INSIGHT-Infra/ansible/playbooks/aps01.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
# Playbook nur für APS01
|
||||
# Ausführung: ansible-playbook -i inventory/hosts.yml playbooks/aps01.yml
|
||||
|
||||
- name: "INSIGHT-APS01 Setup"
|
||||
hosts: insight_aps
|
||||
become: true
|
||||
roles:
|
||||
- role: common
|
||||
- role: disk_setup
|
||||
- role: docker
|
||||
- role: zabbix_agent
|
||||
14
repos/INSIGHT-Infra/ansible/playbooks/dbs01.yml
Normal file
14
repos/INSIGHT-Infra/ansible/playbooks/dbs01.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
# Playbook nur für DBS01 — schnelles Update ohne alle Server
|
||||
# Ausführung: ansible-playbook -i inventory/hosts.yml playbooks/dbs01.yml
|
||||
|
||||
- name: "INSIGHT-DBS01 Setup"
|
||||
hosts: insight_dbs
|
||||
become: true
|
||||
roles:
|
||||
- role: common
|
||||
- role: disk_setup
|
||||
- role: postgresql
|
||||
- role: pgbouncer
|
||||
- role: redis
|
||||
- role: zabbix_agent
|
||||
9
repos/INSIGHT-Infra/ansible/playbooks/site.yml
Normal file
9
repos/INSIGHT-Infra/ansible/playbooks/site.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
# INSIGHT Master Playbook — importiert alle Server-Playbooks
|
||||
# Reihenfolge: DBS01 (Datenbank) → APS01 (App) → WEB01 (Web)
|
||||
# Ausführung: ansible-playbook -i inventory/hosts.yml playbooks/site.yml --ask-vault-pass
|
||||
# Einzeln: ansible-playbook -i inventory/hosts.yml playbooks/dbs01.yml --ask-vault-pass
|
||||
|
||||
- import_playbook: dbs01.yml
|
||||
- import_playbook: aps01.yml
|
||||
- import_playbook: web01.yml
|
||||
13
repos/INSIGHT-Infra/ansible/playbooks/web01.yml
Normal file
13
repos/INSIGHT-Infra/ansible/playbooks/web01.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
# Playbook nur für WEB01
|
||||
# Ausführung: ansible-playbook -i inventory/hosts.yml playbooks/web01.yml
|
||||
|
||||
- name: "INSIGHT-WEB01 Setup"
|
||||
hosts: insight_web
|
||||
become: true
|
||||
roles:
|
||||
- role: common
|
||||
- role: disk_setup
|
||||
- role: docker
|
||||
- role: nginx
|
||||
- role: zabbix_agent
|
||||
87
repos/INSIGHT-Infra/ansible/roles/common/tasks/main.yml
Normal file
87
repos/INSIGHT-Infra/ansible/roles/common/tasks/main.yml
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
# Role: common
|
||||
# Basis-Hardening für alle INSIGHT-Server
|
||||
|
||||
- name: "System-Pakete aktualisieren"
|
||||
apt:
|
||||
update_cache: true
|
||||
upgrade: dist
|
||||
cache_valid_time: 3600
|
||||
|
||||
- name: "Basis-Pakete installieren"
|
||||
apt:
|
||||
name:
|
||||
- curl
|
||||
- wget
|
||||
- git
|
||||
- htop
|
||||
- vim
|
||||
- unzip
|
||||
- ca-certificates
|
||||
- gnupg
|
||||
- lsb-release
|
||||
- ufw
|
||||
- fail2ban
|
||||
- chrony
|
||||
- python3-pip
|
||||
state: present
|
||||
|
||||
- name: "Unattended-Upgrades installieren"
|
||||
apt:
|
||||
name: unattended-upgrades
|
||||
state: present
|
||||
|
||||
- name: "Unattended-Upgrades aktivieren"
|
||||
copy:
|
||||
dest: /etc/apt/apt.conf.d/20auto-upgrades
|
||||
content: |
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
APT::Periodic::Download-Upgradeable-Packages "1";
|
||||
APT::Periodic::AutocleanInterval "7";
|
||||
|
||||
- name: "Zeitzone setzen"
|
||||
timezone:
|
||||
name: "{{ timezone }}"
|
||||
|
||||
- name: "Chrony (NTP) konfigurieren"
|
||||
template:
|
||||
src: chrony.conf.j2
|
||||
dest: /etc/chrony/chrony.conf
|
||||
notify: restart chrony
|
||||
|
||||
- name: "SSH Hardening — sshd_config"
|
||||
template:
|
||||
src: sshd_config.j2
|
||||
dest: /etc/ssh/sshd_config
|
||||
validate: 'sshd -t -f %s'
|
||||
notify: restart sshd
|
||||
|
||||
- name: "UFW — Standard: alles ablehnen"
|
||||
ufw:
|
||||
state: enabled
|
||||
policy: deny
|
||||
direction: incoming
|
||||
|
||||
- name: "UFW — SSH erlauben"
|
||||
ufw:
|
||||
rule: allow
|
||||
port: "{{ ssh_port }}"
|
||||
proto: tcp
|
||||
|
||||
- name: "Fail2ban aktivieren"
|
||||
service:
|
||||
name: fail2ban
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
handlers:
|
||||
- name: restart chrony
|
||||
service:
|
||||
name: chrony
|
||||
state: restarted
|
||||
|
||||
- name: restart sshd
|
||||
service:
|
||||
name: sshd
|
||||
state: restarted
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# chrony.conf — Managed by Ansible (INSIGHT-Infra)
|
||||
{% for server in ntp_servers %}
|
||||
pool {{ server }} iburst
|
||||
{% endfor %}
|
||||
|
||||
driftfile /var/lib/chrony/drift
|
||||
makestep 1.0 3
|
||||
rtcsync
|
||||
logdir /var/log/chrony
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# sshd_config — Managed by Ansible (INSIGHT-Infra)
|
||||
Port {{ ssh_port | default(22) }}
|
||||
AddressFamily inet
|
||||
ListenAddress 0.0.0.0
|
||||
|
||||
# Authentication
|
||||
PermitRootLogin {{ ssh_permit_root_login | default('no') }}
|
||||
PasswordAuthentication {{ ssh_password_authentication | default('no') }}
|
||||
PubkeyAuthentication {{ ssh_pubkey_authentication | default('yes') }}
|
||||
AuthorizedKeysFile .ssh/authorized_keys
|
||||
ChallengeResponseAuthentication no
|
||||
UsePAM yes
|
||||
|
||||
# Security
|
||||
X11Forwarding no
|
||||
AllowTcpForwarding no
|
||||
PermitEmptyPasswords no
|
||||
MaxAuthTries 3
|
||||
LoginGraceTime 30
|
||||
|
||||
# Session
|
||||
ClientAliveInterval 300
|
||||
ClientAliveCountMax 2
|
||||
|
||||
# Subsystem
|
||||
Subsystem sftp /usr/lib/openssh/sftp-server
|
||||
63
repos/INSIGHT-Infra/ansible/roles/disk_setup/tasks/main.yml
Normal file
63
repos/INSIGHT-Infra/ansible/roles/disk_setup/tasks/main.yml
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
# Role: disk_setup
|
||||
# Erkennt und mountet zusätzliche Datendisks (z.B. /dev/sdb auf DBS01)
|
||||
# Wird nur ausgeführt wenn data_disk_device definiert und Disk vorhanden ist
|
||||
|
||||
- name: "Prüfen ob Data-Disk vorhanden ist"
|
||||
stat:
|
||||
path: "{{ data_disk_device }}"
|
||||
register: data_disk_stat
|
||||
when: data_disk_device is defined
|
||||
|
||||
- name: "Prüfen ob Data-Disk bereits formatiert ist"
|
||||
command: "blkid {{ data_disk_device }}"
|
||||
register: data_disk_blkid
|
||||
ignore_errors: true
|
||||
changed_when: false
|
||||
when:
|
||||
- data_disk_device is defined
|
||||
- data_disk_stat.stat.exists
|
||||
|
||||
- name: "Data-Disk formatieren (ext4) — nur wenn noch nicht formatiert"
|
||||
filesystem:
|
||||
fstype: "{{ data_disk_filesystem | default('ext4') }}"
|
||||
dev: "{{ data_disk_device }}"
|
||||
when:
|
||||
- data_disk_device is defined
|
||||
- data_disk_stat.stat.exists
|
||||
- data_disk_blkid.rc != 0
|
||||
|
||||
- name: "Mountpoint erstellen"
|
||||
file:
|
||||
path: "{{ data_disk_mountpoint }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
when:
|
||||
- data_disk_device is defined
|
||||
- data_disk_stat.stat.exists
|
||||
|
||||
- name: "Data-Disk in /etc/fstab eintragen und mounten"
|
||||
mount:
|
||||
path: "{{ data_disk_mountpoint }}"
|
||||
src: "{{ data_disk_device }}"
|
||||
fstype: "{{ data_disk_filesystem | default('ext4') }}"
|
||||
opts: defaults
|
||||
state: mounted
|
||||
when:
|
||||
- data_disk_device is defined
|
||||
- data_disk_stat.stat.exists
|
||||
|
||||
- name: "LVM prüfen und erweitern (falls LVM genutzt wird)"
|
||||
block:
|
||||
- name: "pvs prüfen"
|
||||
command: pvs
|
||||
register: pvs_output
|
||||
changed_when: false
|
||||
ignore_errors: true
|
||||
|
||||
- name: "LVM-Infos ausgeben"
|
||||
debug:
|
||||
msg: "{{ pvs_output.stdout_lines }}"
|
||||
when: pvs_output.rc == 0
|
||||
when: data_disk_device is not defined
|
||||
ignore_errors: true
|
||||
65
repos/INSIGHT-Infra/ansible/roles/docker/tasks/main.yml
Normal file
65
repos/INSIGHT-Infra/ansible/roles/docker/tasks/main.yml
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
# Role: docker
|
||||
# Installiert Docker CE + Compose Plugin
|
||||
|
||||
- name: "Docker GPG Key hinzufügen"
|
||||
apt_key:
|
||||
url: https://download.docker.com/linux/ubuntu/gpg
|
||||
state: present
|
||||
|
||||
- name: "Docker Repository hinzufügen"
|
||||
apt_repository:
|
||||
repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
|
||||
state: present
|
||||
|
||||
- name: "Docker CE installieren"
|
||||
apt:
|
||||
name:
|
||||
- docker-ce
|
||||
- docker-ce-cli
|
||||
- containerd.io
|
||||
- docker-buildx-plugin
|
||||
- docker-compose-plugin
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: "Docker Service aktivieren und starten"
|
||||
service:
|
||||
name: docker
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: "ansible User zur docker-Gruppe hinzufügen"
|
||||
user:
|
||||
name: ansible
|
||||
groups: docker
|
||||
append: true
|
||||
|
||||
- name: "Docker Daemon konfigurieren (log rotation, data-root)"
|
||||
copy:
|
||||
dest: /etc/docker/daemon.json
|
||||
content: |
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "100m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"live-restore": true
|
||||
}
|
||||
notify: restart docker
|
||||
|
||||
- name: "Docker Version prüfen"
|
||||
command: docker --version
|
||||
register: docker_version
|
||||
changed_when: false
|
||||
|
||||
- name: "Docker Version ausgeben"
|
||||
debug:
|
||||
msg: "{{ docker_version.stdout }}"
|
||||
|
||||
handlers:
|
||||
- name: restart docker
|
||||
service:
|
||||
name: docker
|
||||
state: restarted
|
||||
10
repos/INSIGHT-Infra/ansible/roles/nginx/handlers/main.yml
Normal file
10
repos/INSIGHT-Infra/ansible/roles/nginx/handlers/main.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
- name: reload nginx
|
||||
service:
|
||||
name: nginx
|
||||
state: reloaded
|
||||
|
||||
- name: restart nginx
|
||||
service:
|
||||
name: nginx
|
||||
state: restarted
|
||||
64
repos/INSIGHT-Infra/ansible/roles/nginx/tasks/main.yml
Normal file
64
repos/INSIGHT-Infra/ansible/roles/nginx/tasks/main.yml
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
# Role: nginx
|
||||
# Installiert und konfiguriert Nginx als Reverse Proxy auf WEB01
|
||||
# Stellt das React-Frontend bereit und proxied auf APS01
|
||||
|
||||
- name: "Nginx installieren"
|
||||
apt:
|
||||
name:
|
||||
- nginx
|
||||
- certbot
|
||||
- python3-certbot-nginx
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: "Nginx Default-Site deaktivieren"
|
||||
file:
|
||||
path: /etc/nginx/sites-enabled/default
|
||||
state: absent
|
||||
notify: reload nginx
|
||||
|
||||
- name: "Nginx Konfiguration (Haupt)"
|
||||
template:
|
||||
src: nginx.conf.j2
|
||||
dest: /etc/nginx/nginx.conf
|
||||
mode: '0644'
|
||||
validate: 'nginx -t -c %s'
|
||||
notify: reload nginx
|
||||
|
||||
- name: "INSIGHT Site Konfiguration"
|
||||
template:
|
||||
src: insight.conf.j2
|
||||
dest: /etc/nginx/sites-available/insight
|
||||
mode: '0644'
|
||||
notify: reload nginx
|
||||
|
||||
- name: "INSIGHT Site aktivieren"
|
||||
file:
|
||||
src: /etc/nginx/sites-available/insight
|
||||
dest: /etc/nginx/sites-enabled/insight
|
||||
state: link
|
||||
notify: reload nginx
|
||||
|
||||
- name: "Web-Root Verzeichnis anlegen"
|
||||
file:
|
||||
path: "{{ nginx_webroot | default('/var/www/insight') }}"
|
||||
state: directory
|
||||
owner: www-data
|
||||
group: www-data
|
||||
mode: '0755'
|
||||
|
||||
- name: "Nginx Service aktivieren und starten"
|
||||
service:
|
||||
name: nginx
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: "Nginx Version prüfen"
|
||||
command: nginx -v
|
||||
register: nginx_version
|
||||
changed_when: false
|
||||
|
||||
- name: "Nginx Version ausgeben"
|
||||
debug:
|
||||
msg: "{{ nginx_version.stderr }}"
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
# insight.conf — Managed by Ansible (INSIGHT-Infra)
|
||||
# Reverse Proxy: WEB01 → APS01
|
||||
|
||||
upstream insight_api {
|
||||
server {{ aps01_ip | default('172.20.10.21') }}:3000;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{ nginx_server_name | default('_') }};
|
||||
|
||||
# React SPA (Static Files)
|
||||
root {{ nginx_webroot | default('/var/www/insight') }};
|
||||
index index.html;
|
||||
|
||||
# API Proxy → APS01
|
||||
location /api/ {
|
||||
proxy_pass http://insight_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
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;
|
||||
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
}
|
||||
|
||||
# WebSocket (für NestJS Events / Pub-Sub)
|
||||
location /socket.io/ {
|
||||
proxy_pass http://insight_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# React SPA Fallback (client-side routing)
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Cache Static Assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
|
||||
# Health Check
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "OK\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# nginx.conf — Managed by Ansible (INSIGHT-Infra)
|
||||
user www-data;
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
# Basics
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
server_tokens off;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Logging
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent"';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
# Gzip
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml application/json
|
||||
application/javascript application/xml+rss
|
||||
application/atom+xml image/svg+xml;
|
||||
|
||||
# Security Headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
include /etc/nginx/sites-enabled/*;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
- name: restart pgbouncer
|
||||
service:
|
||||
name: pgbouncer
|
||||
state: restarted
|
||||
43
repos/INSIGHT-Infra/ansible/roles/pgbouncer/tasks/main.yml
Normal file
43
repos/INSIGHT-Infra/ansible/roles/pgbouncer/tasks/main.yml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
# Role: pgbouncer
|
||||
# Installiert und konfiguriert PgBouncer als Connection Pooler vor PostgreSQL
|
||||
|
||||
- name: "PgBouncer installieren"
|
||||
apt:
|
||||
name: pgbouncer
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: "PgBouncer konfigurieren (pgbouncer.ini)"
|
||||
template:
|
||||
src: pgbouncer.ini.j2
|
||||
dest: /etc/pgbouncer/pgbouncer.ini
|
||||
owner: postgres
|
||||
group: postgres
|
||||
mode: '0640'
|
||||
notify: restart pgbouncer
|
||||
|
||||
- name: "PgBouncer userlist.txt konfigurieren"
|
||||
template:
|
||||
src: userlist.txt.j2
|
||||
dest: /etc/pgbouncer/userlist.txt
|
||||
owner: postgres
|
||||
group: postgres
|
||||
mode: '0640'
|
||||
notify: restart pgbouncer
|
||||
no_log: true
|
||||
|
||||
- name: "PgBouncer Service aktivieren und starten"
|
||||
service:
|
||||
name: pgbouncer
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: "PgBouncer Status prüfen"
|
||||
command: pgbouncer --version
|
||||
register: pgbouncer_version
|
||||
changed_when: false
|
||||
|
||||
- name: "PgBouncer Version ausgeben"
|
||||
debug:
|
||||
msg: "{{ pgbouncer_version.stdout }}"
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# pgbouncer.ini — Managed by Ansible (INSIGHT-Infra)
|
||||
[databases]
|
||||
{% for db in postgresql_databases | default([]) %}
|
||||
{{ db }} = host=127.0.0.1 port=5432 dbname={{ db }}
|
||||
{% endfor %}
|
||||
|
||||
[pgbouncer]
|
||||
listen_addr = 127.0.0.1
|
||||
listen_port = {{ pgbouncer_port | default(5433) }}
|
||||
|
||||
auth_type = md5
|
||||
auth_file = /etc/pgbouncer/userlist.txt
|
||||
|
||||
pool_mode = {{ pgbouncer_pool_mode | default('transaction') }}
|
||||
max_client_conn = {{ pgbouncer_max_client_conn | default(200) }}
|
||||
default_pool_size = {{ pgbouncer_default_pool_size | default(20) }}
|
||||
min_pool_size = {{ pgbouncer_min_pool_size | default(5) }}
|
||||
reserve_pool_size = {{ pgbouncer_reserve_pool_size | default(5) }}
|
||||
reserve_pool_timeout = 5
|
||||
|
||||
# Logging
|
||||
log_connections = 0
|
||||
log_disconnections = 0
|
||||
log_pooler_errors = 1
|
||||
stats_period = 60
|
||||
|
||||
# Admin
|
||||
admin_users = pgbouncer_admin
|
||||
stats_users = pgbouncer_stats
|
||||
|
||||
# PID
|
||||
pidfile = /var/run/postgresql/pgbouncer.pid
|
||||
logfile = /var/log/postgresql/pgbouncer.log
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# userlist.txt — Managed by Ansible (INSIGHT-Infra)
|
||||
# Format: "username" "md5hash_or_plaintext_password"
|
||||
# Aus Vault: pgbouncer_users
|
||||
{% for user in pgbouncer_users | default([]) %}
|
||||
"{{ user.name }}" "{{ user.password }}"
|
||||
{% endfor %}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
- name: restart postgresql
|
||||
service:
|
||||
name: "postgresql@{{ postgresql_version }}-main"
|
||||
state: restarted
|
||||
|
||||
- name: reload postgresql
|
||||
service:
|
||||
name: "postgresql@{{ postgresql_version }}-main"
|
||||
state: reloaded
|
||||
121
repos/INSIGHT-Infra/ansible/roles/postgresql/tasks/main.yml
Normal file
121
repos/INSIGHT-Infra/ansible/roles/postgresql/tasks/main.yml
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
---
|
||||
# Role: postgresql
|
||||
# Installiert und konfiguriert PostgreSQL 16 auf DBS01
|
||||
|
||||
- name: "PostgreSQL GPG Key hinzufügen"
|
||||
apt_key:
|
||||
url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
|
||||
state: present
|
||||
|
||||
- name: "PostgreSQL Repository hinzufügen"
|
||||
apt_repository:
|
||||
repo: "deb http://apt.postgresql.org/pub/repos/apt {{ ansible_distribution_release }}-pgdg main"
|
||||
state: present
|
||||
filename: pgdg
|
||||
|
||||
- name: "PostgreSQL {{ postgresql_version }} installieren"
|
||||
apt:
|
||||
name:
|
||||
- "postgresql-{{ postgresql_version }}"
|
||||
- "postgresql-client-{{ postgresql_version }}"
|
||||
- "postgresql-contrib-{{ postgresql_version }}"
|
||||
- python3-psycopg2
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: "PostgreSQL Data-Verzeichnis anlegen"
|
||||
file:
|
||||
path: "{{ postgresql_data_dir }}"
|
||||
state: directory
|
||||
owner: postgres
|
||||
group: postgres
|
||||
mode: '0700'
|
||||
when: postgresql_data_dir != '/var/lib/postgresql'
|
||||
|
||||
- name: "PostgreSQL Service stoppen (vor Konfiguration)"
|
||||
service:
|
||||
name: "postgresql@{{ postgresql_version }}-main"
|
||||
state: stopped
|
||||
when: postgresql_data_dir != '/var/lib/postgresql'
|
||||
|
||||
- name: "Prüfen ob PostgreSQL Cluster bereits initialisiert"
|
||||
stat:
|
||||
path: "{{ postgresql_data_dir }}/PG_VERSION"
|
||||
register: pg_cluster_initialized
|
||||
|
||||
- name: "PostgreSQL Cluster in Data-Dir initialisieren"
|
||||
become_user: postgres
|
||||
command: >
|
||||
/usr/lib/postgresql/{{ postgresql_version }}/bin/initdb
|
||||
-D {{ postgresql_data_dir }}
|
||||
when:
|
||||
- postgresql_data_dir != '/var/lib/postgresql'
|
||||
- not pg_cluster_initialized.stat.exists
|
||||
|
||||
- name: "postgresql.conf konfigurieren"
|
||||
template:
|
||||
src: postgresql.conf.j2
|
||||
dest: "/etc/postgresql/{{ postgresql_version }}/main/postgresql.conf"
|
||||
owner: postgres
|
||||
group: postgres
|
||||
mode: '0644'
|
||||
notify: restart postgresql
|
||||
|
||||
- name: "pg_hba.conf konfigurieren"
|
||||
template:
|
||||
src: pg_hba.conf.j2
|
||||
dest: "/etc/postgresql/{{ postgresql_version }}/main/pg_hba.conf"
|
||||
owner: postgres
|
||||
group: postgres
|
||||
mode: '0640'
|
||||
notify: reload postgresql
|
||||
|
||||
- name: "PostgreSQL Service aktivieren und starten"
|
||||
service:
|
||||
name: "postgresql@{{ postgresql_version }}-main"
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: "PostgreSQL INSIGHT Datenbanken anlegen"
|
||||
become_user: postgres
|
||||
postgresql_db:
|
||||
name: "{{ item }}"
|
||||
encoding: UTF8
|
||||
lc_collate: de_DE.UTF-8
|
||||
lc_ctype: de_DE.UTF-8
|
||||
template: template0
|
||||
state: present
|
||||
loop: "{{ postgresql_databases }}"
|
||||
when: postgresql_databases is defined
|
||||
|
||||
- name: "PostgreSQL INSIGHT User anlegen"
|
||||
become_user: postgres
|
||||
postgresql_user:
|
||||
name: "{{ item.name }}"
|
||||
password: "{{ item.password }}"
|
||||
role_attr_flags: "{{ item.role_attr_flags | default('LOGIN') }}"
|
||||
state: present
|
||||
loop: "{{ postgresql_users }}"
|
||||
when: postgresql_users is defined
|
||||
no_log: true
|
||||
|
||||
- name: "PostgreSQL Berechtigungen setzen"
|
||||
become_user: postgres
|
||||
postgresql_privs:
|
||||
db: "{{ item.db }}"
|
||||
role: "{{ item.role }}"
|
||||
privs: "{{ item.privs }}"
|
||||
type: database
|
||||
state: present
|
||||
loop: "{{ postgresql_grants }}"
|
||||
when: postgresql_grants is defined
|
||||
|
||||
- name: "PostgreSQL Version prüfen"
|
||||
become_user: postgres
|
||||
command: psql --version
|
||||
register: pg_version
|
||||
changed_when: false
|
||||
|
||||
- name: "PostgreSQL Version ausgeben"
|
||||
debug:
|
||||
msg: "{{ pg_version.stdout }}"
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# pg_hba.conf — Managed by Ansible (INSIGHT-Infra)
|
||||
# TYPE DATABASE USER ADDRESS METHOD
|
||||
|
||||
# Local connections (Unix socket)
|
||||
local all postgres peer
|
||||
local all all md5
|
||||
|
||||
# IPv4 local
|
||||
host all all 127.0.0.1/32 md5
|
||||
|
||||
# PgBouncer (gleicher Host)
|
||||
host all pgbouncer 127.0.0.1/32 md5
|
||||
|
||||
# Replication (falls später benötigt)
|
||||
# host replication replicator 127.0.0.1/32 md5
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# postgresql.conf — Managed by Ansible (INSIGHT-Infra)
|
||||
# Für {{ inventory_hostname }}
|
||||
|
||||
# Connection
|
||||
listen_addresses = 'localhost'
|
||||
port = 5432
|
||||
max_connections = {{ postgresql_max_connections | default(100) }}
|
||||
|
||||
# Data Directory
|
||||
data_directory = '{{ postgresql_data_dir }}'
|
||||
|
||||
# Memory (anpassen je nach RAM — Standard: 25% shared_buffers)
|
||||
shared_buffers = {{ postgresql_shared_buffers | default('256MB') }}
|
||||
effective_cache_size = {{ postgresql_effective_cache_size | default('1GB') }}
|
||||
work_mem = {{ postgresql_work_mem | default('4MB') }}
|
||||
maintenance_work_mem = {{ postgresql_maintenance_work_mem | default('64MB') }}
|
||||
|
||||
# WAL
|
||||
wal_level = replica
|
||||
max_wal_size = {{ postgresql_max_wal_size | default('1GB') }}
|
||||
min_wal_size = {{ postgresql_min_wal_size | default('80MB') }}
|
||||
|
||||
# Logging
|
||||
log_destination = 'stderr'
|
||||
logging_collector = on
|
||||
log_directory = 'log'
|
||||
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
|
||||
log_rotation_age = 1d
|
||||
log_rotation_size = 100MB
|
||||
log_min_duration_statement = 1000
|
||||
log_connections = on
|
||||
log_disconnections = on
|
||||
log_line_prefix = '%m [%p] %q%u@%d '
|
||||
|
||||
# Locale
|
||||
lc_messages = 'de_DE.UTF-8'
|
||||
lc_monetary = 'de_DE.UTF-8'
|
||||
lc_numeric = 'de_DE.UTF-8'
|
||||
lc_time = 'de_DE.UTF-8'
|
||||
|
||||
# Timezone
|
||||
timezone = '{{ timezone | default("Europe/Berlin") }}'
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
- name: restart redis
|
||||
service:
|
||||
name: redis-server
|
||||
state: restarted
|
||||
55
repos/INSIGHT-Infra/ansible/roles/redis/tasks/main.yml
Normal file
55
repos/INSIGHT-Infra/ansible/roles/redis/tasks/main.yml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
# Role: redis
|
||||
# Installiert und konfiguriert Redis 7 auf DBS01
|
||||
|
||||
- name: "Redis GPG Key hinzufügen"
|
||||
apt_key:
|
||||
url: https://packages.redis.io/gpg
|
||||
state: present
|
||||
|
||||
- name: "Redis Repository hinzufügen"
|
||||
apt_repository:
|
||||
repo: "deb https://packages.redis.io/deb {{ ansible_distribution_release }} main"
|
||||
state: present
|
||||
filename: redis
|
||||
|
||||
- name: "Redis installieren"
|
||||
apt:
|
||||
name: redis
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: "Redis konfigurieren"
|
||||
template:
|
||||
src: redis.conf.j2
|
||||
dest: /etc/redis/redis.conf
|
||||
owner: redis
|
||||
group: redis
|
||||
mode: '0640'
|
||||
notify: restart redis
|
||||
|
||||
- name: "Redis Daten-Verzeichnis anlegen"
|
||||
file:
|
||||
path: "{{ redis_data_dir | default('/data/redis') }}"
|
||||
state: directory
|
||||
owner: redis
|
||||
group: redis
|
||||
mode: '0750'
|
||||
|
||||
- name: "Redis Service aktivieren und starten"
|
||||
service:
|
||||
name: redis-server
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: "Redis Konnektivität prüfen"
|
||||
command: redis-cli ping
|
||||
register: redis_ping
|
||||
changed_when: false
|
||||
retries: 3
|
||||
delay: 2
|
||||
until: redis_ping.stdout == "PONG"
|
||||
|
||||
- name: "Redis Status ausgeben"
|
||||
debug:
|
||||
msg: "Redis antwortet: {{ redis_ping.stdout }}"
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# redis.conf — Managed by Ansible (INSIGHT-Infra)
|
||||
# Für {{ inventory_hostname }}
|
||||
|
||||
# Binding — nur localhost (kein externer Zugriff)
|
||||
bind 127.0.0.1
|
||||
port {{ redis_port | default(6379) }}
|
||||
protected-mode yes
|
||||
|
||||
# Passwort (aus Vault)
|
||||
requirepass {{ redis_password }}
|
||||
|
||||
# Persistenz
|
||||
dir {{ redis_data_dir | default('/data/redis') }}
|
||||
save 900 1
|
||||
save 300 10
|
||||
save 60 10000
|
||||
appendonly yes
|
||||
appendfilename "appendonly.aof"
|
||||
appendfsync everysec
|
||||
|
||||
# Memory
|
||||
maxmemory {{ redis_maxmemory | default('512mb') }}
|
||||
maxmemory-policy {{ redis_maxmemory_policy | default('allkeys-lru') }}
|
||||
|
||||
# Logging
|
||||
loglevel notice
|
||||
logfile /var/log/redis/redis-server.log
|
||||
|
||||
# Datenbanken
|
||||
databases 16
|
||||
|
||||
# Timeouts
|
||||
timeout 300
|
||||
tcp-keepalive 300
|
||||
|
||||
# Slow Log
|
||||
slowlog-log-slower-than 10000
|
||||
slowlog-max-len 128
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
- name: restart zabbix-agent2
|
||||
service:
|
||||
name: zabbix-agent2
|
||||
state: restarted
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
# Role: zabbix_agent
|
||||
# Installiert Zabbix Agent 2 und verbindet ihn mit sentinel.xinion.de
|
||||
|
||||
- name: "Zabbix Repository hinzufügen"
|
||||
apt:
|
||||
deb: "https://repo.zabbix.com/zabbix/6.4/ubuntu/pool/main/z/zabbix-release/zabbix-release_6.4-1+ubuntu{{ ansible_distribution_version }}_all.deb"
|
||||
state: present
|
||||
register: zabbix_repo_result
|
||||
ignore_errors: true
|
||||
|
||||
- name: "Zabbix Repository (Fallback — via URL)"
|
||||
get_url:
|
||||
url: "https://repo.zabbix.com/zabbix/6.4/ubuntu/pool/main/z/zabbix-release/zabbix-release_6.4-1+ubuntu22.04_all.deb"
|
||||
dest: /tmp/zabbix-release.deb
|
||||
mode: '0644'
|
||||
when: zabbix_repo_result is failed
|
||||
|
||||
- name: "Zabbix Repository installieren (Fallback)"
|
||||
apt:
|
||||
deb: /tmp/zabbix-release.deb
|
||||
when: zabbix_repo_result is failed
|
||||
|
||||
- name: "Zabbix Agent 2 installieren"
|
||||
apt:
|
||||
name: zabbix-agent2
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: "Zabbix Agent 2 konfigurieren"
|
||||
template:
|
||||
src: zabbix_agent2.conf.j2
|
||||
dest: /etc/zabbix/zabbix_agent2.conf
|
||||
owner: root
|
||||
group: zabbix
|
||||
mode: '0640'
|
||||
notify: restart zabbix-agent2
|
||||
|
||||
- name: "Zabbix Agent 2 Service aktivieren und starten"
|
||||
service:
|
||||
name: zabbix-agent2
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: "UFW — Zabbix Agent Port erlauben (vom Zabbix Server)"
|
||||
ufw:
|
||||
rule: allow
|
||||
src: "{{ zabbix_server }}"
|
||||
port: "10050"
|
||||
proto: tcp
|
||||
|
||||
- name: "Zabbix Agent Status prüfen"
|
||||
command: zabbix_agent2 -V
|
||||
register: zabbix_version
|
||||
changed_when: false
|
||||
|
||||
- name: "Zabbix Agent Version ausgeben"
|
||||
debug:
|
||||
msg: "{{ zabbix_version.stdout_lines[0] }}"
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# zabbix_agent2.conf — Managed by Ansible (INSIGHT-Infra)
|
||||
# Für {{ inventory_hostname }}
|
||||
|
||||
# Server (passive checks)
|
||||
Server={{ zabbix_server }}
|
||||
ServerActive={{ zabbix_server }}
|
||||
|
||||
# Hostname (muss im Zabbix Frontend übereinstimmen)
|
||||
Hostname={{ zabbix_hostname_prefix | default('INSIGHT-') }}{{ inventory_hostname | upper }}
|
||||
|
||||
# Verbindung
|
||||
ListenPort=10050
|
||||
ListenIP=0.0.0.0
|
||||
|
||||
# Timeouts
|
||||
Timeout=30
|
||||
|
||||
# Logging
|
||||
LogFile=/var/log/zabbix/zabbix_agent2.log
|
||||
LogFileSize=10
|
||||
DebugLevel=3
|
||||
|
||||
# Plugins
|
||||
Plugins.SystemRun.LogRemoteCommands=1
|
||||
|
||||
# Include (für zusätzliche Checks)
|
||||
Include=/etc/zabbix/zabbix_agent2.d/*.conf
|
||||
36
repos/INSIGHT-Infra/ansible/vault.yml.example
Normal file
36
repos/INSIGHT-Infra/ansible/vault.yml.example
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
# vault.yml.example — VORLAGE für ansible/vault.yml
|
||||
# NIEMALS echte Passwörter committen!
|
||||
# vault.yml mit Ansible Vault verschlüsseln:
|
||||
# ansible-vault create ansible/vault.yml
|
||||
# ansible-vault edit ansible/vault.yml
|
||||
|
||||
# PostgreSQL Passwörter
|
||||
postgresql_users:
|
||||
- name: insight_app
|
||||
password: "CHANGE_ME_STRONG_PASSWORD"
|
||||
role_attr_flags: "LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE"
|
||||
- name: pgbouncer
|
||||
password: "CHANGE_ME_PGBOUNCER_PASSWORD"
|
||||
role_attr_flags: "LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE"
|
||||
|
||||
postgresql_grants:
|
||||
- db: insight_core
|
||||
role: insight_app
|
||||
privs: "ALL"
|
||||
- db: insight_crm
|
||||
role: insight_app
|
||||
privs: "ALL"
|
||||
|
||||
# PgBouncer Auth
|
||||
pgbouncer_users:
|
||||
- name: insight_app
|
||||
password: "CHANGE_ME_STRONG_PASSWORD"
|
||||
- name: pgbouncer
|
||||
password: "CHANGE_ME_PGBOUNCER_PASSWORD"
|
||||
|
||||
# Redis
|
||||
redis_password: "CHANGE_ME_REDIS_PASSWORD"
|
||||
|
||||
# Zabbix (falls PSK verwendet)
|
||||
# zabbix_psk_key: "CHANGE_ME_PSK"
|
||||
74
repos/INSIGHT-Platform/BRIEFING.md
Normal file
74
repos/INSIGHT-Platform/BRIEFING.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# INSIGHT-Platform — Developer Briefing
|
||||
|
||||
> Dieses Dokument ist der Einstiegspunkt für jede neue Claude Code Session in diesem Repo.
|
||||
> Lies es vollständig bevor du Code schreibst.
|
||||
|
||||
## Was ist dieses Repo?
|
||||
|
||||
Die **INSIGHT-Plattform** ist das Fundament aller INSIGHT-Dienste.
|
||||
Sie stellt Authentifizierung, User-Verwaltung, Berechtigungssystem und die React-Shell bereit,
|
||||
in die sich fachliche Apps (aus `INSIGHT-Apps`) einbinden.
|
||||
|
||||
## Struktur
|
||||
|
||||
```
|
||||
packages/
|
||||
├── core-service/ # NestJS: Auth, JWT, User, Settings, Expertenprofil, RBAC
|
||||
└── frontend/ # React 18 + Vite: Shell, Login, Navigation, Admin-Bereich
|
||||
```
|
||||
|
||||
## Abhängigkeiten zu anderen Repos
|
||||
|
||||
| Repo | Beziehung |
|
||||
|------|-----------|
|
||||
| `INSIGHT-Shared` | Lesen: module-interface.md, api-platform.md — Schreiben: api-platform.md nach Änderungen |
|
||||
| `INSIGHT-Apps` | Apps registrieren sich via InsightModule-Contract in der Shell |
|
||||
| `INSIGHT-Infra` | Stellt DBS01 (PostgreSQL + Redis) und APS01 (Docker) bereit |
|
||||
|
||||
## Confluence Dokumentation
|
||||
|
||||
- **Phase 2 — Plattform:** https://xinion.atlassian.net/wiki/x/oYKSEw
|
||||
- **Berechtigungskonzept:** https://xinion.atlassian.net/wiki/x/FQCVEw
|
||||
- **Projekt-Übersicht:** https://xinion.atlassian.net/wiki/spaces/ProjektINS/overview
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend:** NestJS 10, TypeScript strict, Prisma ORM
|
||||
- **Frontend:** React 18, Vite, TanStack Query, React Router
|
||||
- **Datenbank:** PostgreSQL 16 (auf DBS01, via PgBouncer)
|
||||
- **Cache:** Redis 7 (auf DBS01)
|
||||
- **Auth:** JWT RS256, Refresh Token (HttpOnly Cookie), TOTP 2FA
|
||||
|
||||
## Sicherheitsregeln (nicht verhandelbar)
|
||||
|
||||
- Kein `any` in TypeScript — strict mode immer
|
||||
- Kein `localStorage` für Tokens (Access: Memory, Refresh: HttpOnly Cookie)
|
||||
- Globaler `ValidationPipe` (whitelist + forbidNonWhitelisted)
|
||||
- `@Public()` explizit für öffentliche Routen
|
||||
- Rollen immer via `@Roles()` + `RolesGuard` auf jedem Endpoint
|
||||
- Kein raw SQL — ausschließlich Prisma
|
||||
|
||||
## RBAC-Modell
|
||||
|
||||
```
|
||||
User
|
||||
├── platformRole: PLATFORM_ADMIN | USER
|
||||
└── moduleRoles: { crm: ADMIN | MANAGER | VIEWER, ... }
|
||||
```
|
||||
|
||||
Platform Admin: User-Verwaltung, System-Einstellungen, Modul-Zuweisung
|
||||
Module Admin: Berechtigungen innerhalb seines Moduls
|
||||
|
||||
## Deploy Key (dieses Repo)
|
||||
|
||||
```
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK2deXkhf9Ut9728mvxwl+MyFtbjoPRXNbTR2XjmDJVs deploy@INSIGHT-Platform
|
||||
```
|
||||
|
||||
Privater Key: `.keys/deploy_platform_ed25519`
|
||||
|
||||
## Aktueller Status
|
||||
|
||||
- **Phase:** 2 — Plattform
|
||||
- **Stand:** Konzeption (Berechtigungskonzept in Klärung)
|
||||
- **Nächster Schritt:** Berechtigungskonzept finalisieren → Prisma Schema → Auth-Service
|
||||
59
repos/INSIGHT-Shared/BRIEFING.md
Normal file
59
repos/INSIGHT-Shared/BRIEFING.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# INSIGHT-Shared — Developer Briefing
|
||||
|
||||
> Dieses Dokument ist der Einstiegspunkt für jede neue Claude Code Session in diesem Repo.
|
||||
> Lies es vollständig bevor du Code schreibst.
|
||||
|
||||
## Was ist dieses Repo?
|
||||
|
||||
**INSIGHT-Shared** ist das zentrale Wissens- und Vertrags-Repository der INSIGHT-Plattform.
|
||||
**Kein ausführbarer Code** — ausschließlich Dokumentation, API-Verträge und Architektur-Entscheidungen.
|
||||
|
||||
Ziel: Jede Claude Code Session in jedem Repo kann dieses Repo lesen und weiß sofort:
|
||||
- Was andere Repos liefern
|
||||
- Welche Interfaces einzuhalten sind
|
||||
- Was der aktuelle Entwicklungsstand ist
|
||||
|
||||
## Struktur & Zuständigkeiten
|
||||
|
||||
```
|
||||
contracts/
|
||||
├── module-interface.md # InsightModule-Contract (Platform ↔ Apps) — PFLICHTLEKTÜRE für Apps
|
||||
├── api-platform.md # REST-API von INSIGHT-Platform Core-Service
|
||||
└── api-crm.md # REST-API von INSIGHT-Apps CRM-Service
|
||||
|
||||
architecture/
|
||||
├── decisions/ # ADRs — Architektur-Entscheidungen mit Begründung
|
||||
│ ├── 001-plugin-system.md
|
||||
│ ├── 002-rbac-model.md
|
||||
│ └── 003-repo-struktur.md
|
||||
└── overview.md # Gesamtarchitektur
|
||||
|
||||
types/
|
||||
└── shared-types.md # TypeScript Interfaces die alle Repos kennen müssen
|
||||
|
||||
dev-status/
|
||||
├── platform.md # Was ist fertig / läuft / blockiert in INSIGHT-Platform
|
||||
├── apps.md # Status INSIGHT-Apps
|
||||
└── infra.md # Status INSIGHT-Infra
|
||||
```
|
||||
|
||||
## Wer liest/schreibt was
|
||||
|
||||
| Repo | Liest | Schreibt |
|
||||
|------|-------|----------|
|
||||
| INSIGHT-Platform | module-interface.md, dev-status/* | api-platform.md, dev-status/platform.md |
|
||||
| INSIGHT-Apps | api-platform.md, module-interface.md | api-crm.md, dev-status/apps.md |
|
||||
| INSIGHT-Infra | dev-status/* | dev-status/infra.md |
|
||||
|
||||
## Confluence Dokumentation
|
||||
|
||||
- **Projekt-Übersicht:** https://xinion.atlassian.net/wiki/spaces/ProjektINS/overview
|
||||
- **Konzeptdokument:** https://xinion.atlassian.net/wiki/x/AQCVEw
|
||||
|
||||
## Deploy Key (dieses Repo)
|
||||
|
||||
```
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAZp7rLiyNmEm3439yjChUC6TQbP8O7xdfGQyr37MNjh deploy@INSIGHT-Shared
|
||||
```
|
||||
|
||||
Privater Key: `.keys/deploy_shared_ed25519`
|
||||
84
repos/INSIGHT-Shared/contracts/module-interface.md
Normal file
84
repos/INSIGHT-Shared/contracts/module-interface.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# InsightModule — Contract
|
||||
|
||||
> Version: 1.0 (Entwurf)
|
||||
> Dieser Contract definiert wie sich ein App-Modul in die INSIGHT-Platform-Shell einbindet.
|
||||
|
||||
## TypeScript Interface
|
||||
|
||||
```typescript
|
||||
export interface InsightRoute {
|
||||
path: string
|
||||
component: React.ComponentType
|
||||
exact?: boolean
|
||||
}
|
||||
|
||||
export interface InsightNavItem {
|
||||
label: string
|
||||
path: string
|
||||
icon?: string
|
||||
children?: InsightNavItem[]
|
||||
}
|
||||
|
||||
export interface InsightModule {
|
||||
/** Eindeutige ID des Moduls (z.B. 'crm', 'hr') */
|
||||
id: string
|
||||
/** Anzeigename in der Navigation */
|
||||
name: string
|
||||
/** Icon-Name (aus Icon-Bibliothek der Platform) */
|
||||
icon?: string
|
||||
/** Welche moduleRole wird benötigt? (aus JWT moduleRoles) */
|
||||
requiredRole: string
|
||||
/** React-Routen die registriert werden */
|
||||
routes: InsightRoute[]
|
||||
/** Navigationspunkte in der Sidebar */
|
||||
navigation: InsightNavItem[]
|
||||
/** Optionaler Dashboard-Widget für die Startseite */
|
||||
dashboardWidget?: React.ComponentType
|
||||
}
|
||||
```
|
||||
|
||||
## Implementierung (Beispiel CRM)
|
||||
|
||||
```typescript
|
||||
// INSIGHT-Apps/packages/crm-frontend/src/index.ts
|
||||
import { CrmDashboard } from './pages/CrmDashboard'
|
||||
import { DealsPage } from './pages/DealsPage'
|
||||
import { ContactsPage } from './pages/ContactsPage'
|
||||
|
||||
export const CrmModule: InsightModule = {
|
||||
id: 'crm',
|
||||
name: 'CRM',
|
||||
icon: 'briefcase',
|
||||
requiredRole: 'crm',
|
||||
routes: [
|
||||
{ path: '/crm', component: CrmDashboard, exact: true },
|
||||
{ path: '/crm/deals', component: DealsPage },
|
||||
{ path: '/crm/contacts', component: ContactsPage },
|
||||
],
|
||||
navigation: [
|
||||
{ label: 'Dashboard', path: '/crm', icon: 'home' },
|
||||
{ label: 'Deals', path: '/crm/deals', icon: 'dollar' },
|
||||
{ label: 'Kontakte', path: '/crm/contacts', icon: 'users' },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Registrierung in der Platform-Shell
|
||||
|
||||
```typescript
|
||||
// INSIGHT-Platform/packages/frontend/src/shell/module-registry.ts
|
||||
import { CrmModule } from '@insight/crm-frontend'
|
||||
|
||||
export const registeredModules: InsightModule[] = [
|
||||
CrmModule,
|
||||
// weitere Module hier eintragen
|
||||
]
|
||||
```
|
||||
|
||||
## Regeln
|
||||
|
||||
- `id` muss eindeutig und lowercase sein
|
||||
- `requiredRole` muss einer gültigen moduleRole entsprechen (definiert in INSIGHT-Platform)
|
||||
- Jede Route muss innerhalb des Modul-Pfads liegen (z.B. `/crm/*`)
|
||||
- Komponenten dürfen den Platform-Auth-Context nutzen (`useAuth()` Hook)
|
||||
- Kein direkter Datenbankzugriff aus Frontend-Komponenten
|
||||
24
repos/INSIGHT-Shared/dev-status/apps.md
Normal file
24
repos/INSIGHT-Shared/dev-status/apps.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Dev-Status: INSIGHT-Apps
|
||||
|
||||
_Zuletzt aktualisiert: 15.03.2026_
|
||||
|
||||
## Status: 🔴 Konzeption
|
||||
|
||||
## Fertig
|
||||
- (noch nichts — Neubeginn)
|
||||
|
||||
## In Arbeit
|
||||
- CRM-Datenmodell (Entitäten, Felder, Beziehungen)
|
||||
- CRM-Berechtigungskonzept
|
||||
|
||||
## Ausstehend
|
||||
- Prisma Schema CRM
|
||||
- Company / Contact / Deal / Activity API
|
||||
- CRM Frontend (Kanban, Listen, Detail)
|
||||
|
||||
## Blockiert durch
|
||||
- Phase 2 (Platform) muss abgeschlossen sein (Auth + RBAC)
|
||||
- CRM-Datenmodell muss finalisiert werden
|
||||
|
||||
## Offene Fragen an andere Teams
|
||||
- INSIGHT-Platform: Welchen Auth-Context stellt die Shell zur Verfügung? (`useAuth()` Hook API?)
|
||||
32
repos/INSIGHT-Shared/dev-status/infra.md
Normal file
32
repos/INSIGHT-Shared/dev-status/infra.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Dev-Status: INSIGHT-Infra
|
||||
|
||||
_Zuletzt aktualisiert: 15.03.2026_
|
||||
|
||||
## Status: 🟡 In Arbeit
|
||||
|
||||
## Fertig
|
||||
- VM INSIGHT-DBS01 (Proxmox, VM 1020) angelegt
|
||||
- SSH-Zugang via ansible-Key funktioniert
|
||||
- Alle SSH + Deploy Keys generiert
|
||||
- Confluence-Dokumentation vollständig
|
||||
|
||||
## In Arbeit
|
||||
- APS01 + WEB01 VMs anlegen
|
||||
- IP-Adressen vergeben
|
||||
|
||||
## Ausstehend
|
||||
- Ansible Inventory befüllen (IPs)
|
||||
- Ansible Role: common
|
||||
- Ansible Role: docker
|
||||
- Ansible Role: postgresql + pgbouncer
|
||||
- Ansible Role: redis
|
||||
- Ansible Role: nginx
|
||||
- Ansible Role: zabbix_agent
|
||||
- Forgejo Repos anlegen + Deploy Keys eintragen
|
||||
|
||||
## Blockiert durch
|
||||
- IP-Adressen ausstehend (APS01, WEB01)
|
||||
- Ansible-Server Zugang konfigurieren
|
||||
|
||||
## Offene Fragen
|
||||
- Ansible-Server: IP/Hostname, Version, SSH-Zugang?
|
||||
25
repos/INSIGHT-Shared/dev-status/platform.md
Normal file
25
repos/INSIGHT-Shared/dev-status/platform.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Dev-Status: INSIGHT-Platform
|
||||
|
||||
_Zuletzt aktualisiert: 15.03.2026_
|
||||
|
||||
## Status: 🔴 Konzeption
|
||||
|
||||
## Fertig
|
||||
- (noch nichts — Neubeginn)
|
||||
|
||||
## In Arbeit
|
||||
- Berechtigungskonzept (Platform- vs. Modul-Rollen)
|
||||
- Repo-Struktur definiert
|
||||
|
||||
## Ausstehend
|
||||
- Prisma Schema (User, Roles, Settings)
|
||||
- NestJS Core-Service (Auth, JWT, User)
|
||||
- React Shell (Login, Navigation, Module-Registry)
|
||||
- Admin-Bereich
|
||||
|
||||
## Blockiert durch
|
||||
- Berechtigungskonzept muss finalisiert werden
|
||||
- Phase 1 (Infra) muss abgeschlossen sein
|
||||
|
||||
## Offene Fragen an andere Teams
|
||||
- INSIGHT-Apps: Welche Felder braucht das User-Profil für CRM-Kontext?
|
||||
Loading…
Add table
Reference in a new issue