📖 Die Geschichte nimmt ihren Lauf
Ein Muster, das wir bei Nine häufig beobachten, ist, dass viele unserer Kund*innen Webanwendungen betreiben – etwa Content-Management-Systeme, Webshops, Dashboards oder eigene APIs. Solche Anwendungen setzen typischerweise auf eine bewährte Kombination aus Infrastrukturdiensten: eine Datenbank zur Datenspeicherung, eine Cache-Schicht für schnelle Performance und einen Server, auf dem die Anwendungslogik läuft.
Unser Szenario spiegelt genau diesen Praxisfall wider. Wir zeigen hier nicht einfach nur das Aufsetzen einer VM oder stellen eine API zur Schau (na gut, vielleicht ein bisschen 😉), sondern stellen typische Komponenten bereit, die in echten Produktionsumgebungen vorkommen:
- Eine PostgreSQL-Datenbank für Nutzerdaten, Bestellungen und Produktinformationen
- Einen Redis Key-Value-Store für schnelles Caching und geringere Ladezeiten
- Eine CloudVM, auf der Anwendungen wie Webserver oder Backend-APIs laufen
Ziel ist es, zu zeigen, wie Sie diese Dienste einfach und praxisnah mit unserer API und unseren Tools bereitstellen und verwalten können – und zwar so, dass das Ganze Ihre Arbeitslast und Herausforderungen widerspiegelt.
Als Head of Engineering bei Nine durfte ich miterleben, wie unsere Plattform kontinuierlich weiterentwickelt wurde, um Entwickler*innen leistungsstarke Werkzeuge zur Infrastrukturautomatisierung zu bieten. Eines der wichtigsten Tools dabei ist unsere öffentliche API, mit der Sie Ihre Infrastruktur programmatisch bereitstellen und steuern können.
Dieser Blogpost dient nicht als reines Tutorial: Er erzählt die Geschichte, wie aus einem leeren Blatt eine funktionsfähige, produktionsähnliche Umgebung entsteht. Wir erstellen und validieren dabei folgende Infrastrukturkomponenten:
- Eine PostgreSQL-Datenbank für persistente Datenspeicherung
- Einen Redis Key-Value-Store für schnelles Caching
- Eine CloudVM, die beliebige Anwendungen hosten kann
Jeder Schritt wird nicht nur erklärt, sondern auch hinsichtlich der Entscheidungen und Befehle kontextuell eingeordnet. Zielgruppe sind Entwickler*innen oder DevOps-Engineers, die noch nicht mit unserer API, Ansible oder unserem CLI-Tool `nctl` vertraut sind.
🤖 Warum Ansible?
Bevor wir ins Technische eintauchen, kurz zu unserem zentralen Werkzeug, das alles zusammenhält: Ansible.
Ansible ist ein beliebtes Automatisierungstool für Konfigurationsmanagement, Applikations-Deployment und allgemeine Infrastruktur-Orchestrierung. Wieso ist es so beliebt?
- Agentenlos: Im Gegensatz zu anderen Tools ist keine Installation auf den Zielsystemen notwendig, denn Ansible nutzt SSH.
- Lesbare YAML-Syntax: Das macht es einfach zu lernen und gemeinsam nutzbar.
- Dynamische Abläufe: Es ermöglicht Bedingungen, Variablen, Schleifen sowie direkte Shell-Befehle und API-Aufrufe.
Ansible passt hervorragend zu Szenarien wie dem unseren, in dem nicht nur Infrastruktur erstellt, sondern auch anschliessend konfiguriert und validiert werden soll.
Vielleicht kennen Sie bereits OpenTofu oder Terraform – diese Tools überzeugen bei der deklarativen Verwaltung von Infrastrukturzuständen. Da wir eine öffentlich zugängliche API anbieten, ist sie vollständig kompatibel mit Terraform, Beispielkonfigurationen dazu finden Sie hier. Ansible hingegen ist prozedural und ereignisgetrieben, ideal für unseren Schritt-für-Schritt-Ansatz und konditionale Logik.
🛠️ Das Toolkit einrichten (macOS)
Unser Abenteuer beginnt auf einem MacBook. Wir installieren die notwendigen Tools mit Homebrew, dem de facto Standard-Paketmanager für macOS.
Linux-Nutzer können der Ansible-Installationsanleitung folgen und `nctl` manuell über unsere GitHub-Releases installieren.
Für Windows empfehlen wir die Verwendung von WSL2 und dann das Befolgen der Linux-Anweisungen von oben. So können Sie dieselben Unix-ähnlichen Tools und Befehle auf Windows nutzen.
Homebrew installieren
Falls Sie Homebrew noch nicht nutzen, installieren Sie es wie folgt:
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
Mit Homebrew können Sie dann auch diverse andere Tools einfach installieren.
Ansible installieren
```bash
brew install ansible
```
Ansible ist unser Hauptwerkzeug, um die Bereitstellung, Konfiguration und Validierung der Infrastrukturressourcen zu automatisieren.
Unser CLI-Tool `nctl` installieren
```bash
brew tap ninech/nine
brew install nctl
```
`nctl` ist unser Befehlszeilen-Client und vereinfacht das Handling unserer API erheblich, wie beispielsweise das Erstellen einer VM oder Datenbank, das Abrufen von Secrets oder die Zugriffskontrollenverwaltung. Es wird immer wieder zum Einsatz kommen.
Authentifizierung mit unserer API
Um mit unserer API zu interagieren, benötigen Sie einen Inhaber-Token. Dieser Token gewährt Ihnen autorisierten Zugriff und ist sowohl für Ansible als auch für ‘nctl’ notwendig.
Sie können entweder im Cockpit einen Service-Account erstellen und dort den Token einsehen:
👉 https://cockpit.nine.ch/de/customer/api\_service\_accounts/
Oder Sie erstellen den Token via CLI.
Loggen Sie sich dazu mit Ihrem Account ein:
```bash
nctl auth login
```
Dann erstellen wir einen apiserviceaccount mit dem Namen `cicd`:
```bash
nctl create apiserviceaccount cicd
```
Schliesslich rufen wir den Token für den apiserviceaccount mit dem Namen `cicd` ab:
```bash
nctl get apiserviceaccounts cicd --print-token
```
Sobald Sie Ihren Token haben, exportieren Sie ihn als Umgebungsvariable:
```bash
export NINE_API_TOKEN="<your-token>"
```
Vergessen Sie nicht, Ihren öffentlichen SSH-Key ebenfalls zu exportieren. Den brauchen wir, um später auf die VM zuzugreifen:
```bash
export SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)"
```
> 🛡️ Behandeln Sie Ihren API-Token wie ein Passwort – nie direkt in Dateien speichern.
📊 Projektstruktur
Um den Überblick zu behalten, legen wir ein Projektverzeichnis an und definieren unsere wiederverwendbare Konfiguration.
```bash
mkdir nine-infra && cd nine-infra
```
Gruppenvariablen
Ansible ermöglicht es uns, mittels group vars Variablen an einem Ort zu gruppieren. Erstellen Sie einen Ordner mit:
```bash
mkdir group_vars
```
So könnte `group_vars/all.yml` aussehen:
```yaml
# group_vars/all.yml
postgres_name: example-postgres
postgres_version: 15
redis_name: example-redis
vm_name: example-vm
location: nine-cz41
machine_type: nine-small-1
os: ubuntu24.04
```
Das macht unsere Playbooks besser lesbar und erleichtert das Wiederverwenden oder Updaten von Einstellungen. Je nach Bedarf können Sie die Werte hier anpassen – wie zum Beispiel die Betriebssystemversion, den Maschinentyp oder die Serviceversionen. Um herauszufinden, welche Werte unterstützt werden, können Sie die Dokumentation verwenden, die wir für unsere API bieten:
- CloudVM Maschinentypen und Locations
- Von PostgreSQL unterstützte Versionen und Typen
- KeyValueStore-Spezifikationen
🔺 Schritt 1: PostgreSQL bereitstellen
Um eine verwaltete PostgreSQL-Instanz bereitzustellen, verwenden wir `nctl` mit einem Ansible-Playbook. So wird sichergestellt, dass die Datenbank mit unserer gewünschten Konfiguration konsistent erstellt wird.
Erstellen Sie das folgende File und speichern Sie es als `provision_postgres.yml`:
```yaml
---
# provision_postgres.yml
- name: Provision PostgreSQL
hosts: localhost
gather_facts: false
vars_files:
- group_vars/all.yml
tasks:
- name: Check if PostgreSQL already exists
ansible.builtin.shell: "nctl get postgres {{ postgres_name }}"
changed_when: false
failed_when: false
register: pg_exists
- name: Create PostgreSQL
ansible.builtin.command: >
nctl create postgres {{ postgres_name }}
--postgres-version {{ postgres_version }}
--machine-type nine-db-xs
--location {{ location }}
--allowed-cidrs 0.0.0.0/0
--wait
changed_when: "pg_result.rc == 0"
ignore_errors: true
when: "pg_exists.rc != 0"
register: pg_result
- name: Get PostgreSQL connection string
ansible.builtin.command: "nctl get postgres {{ postgres_name }} --print-connection-string"
register: pg_conn_str
changed_when: "pg_conn_str.rc != 0"
- name: Set Postgres connection string fact
ansible.builtin.set_fact:
postgres_conn_string: "{{ pg_conn_str.stdout }}/postgres"
- name: Debug Postgres connection string
ansible.builtin.debug:
var: postgres_conn_string
```
Führen Sie das Ganze mithilfe von folgendem Befehl aus:
```bash
ansible-playbook provision_postgres.yml
```
Achtung: Dieser Schritt kann einige Zeit in Anspruch nehmen.
Zuerst kommt unsere Datenbank: Eine PostgreSQL-Instanz bietet uns einen dauerhaften, zuverlässigen Datenspeicher. Es ist eine übliche Backend-Wahl für moderne Apps.
Wir verwenden `nctl`, um die Instanz zu erstellen und rufen dann gleich den Verbindungs-String ab, damit wir ihn später verwenden können.
✅ Validierung:
Nutzen Sie den Verbindungs-String, um zu bestätigen, dass die Instanz erreichbar ist:
```bash
psql <postgres_connection_string>
```
Wenn die Verbindung gelingt, können Sie zum nächsten Schritt übergehen. Denken Sie daran, dass die Erstellung einer DB einige Zeit dauern kann, da wir stets eine dedizierte Datenbank bereitstellen. Bald werden wir auch Shared DBs anbieten, die diesen Schritt beschleunigen werden. Bitte beachten Sie auch, dass wir – der Einfachheit halber und für diesen Blogbeitrag – für die IP 0.0.0.0/0 den Zugriff auf alle unsere Dienste erlauben. Ich empfehle Ihnen aber dringend, Ihre Infrastruktur nicht auf diese Weise aufzusetzen.
🔺 Schritt 2: Redis bereitstellen
Redis verkörpert unseren KeyValue Store. Genau wie PostgreSQL, nutzen wir ein Ansible-Playbook, um die Bereitstellung zu automatisieren.
Erstellen Sie das folgende File als `provision_redis.yml`:
```yaml
---
# provision_redis.yml
- name: Provision Redis
hosts: localhost
gather_facts: false vars_files:
- group_vars/all.yml tasks:
- name: Check if Redis already exists
ansible.builtin.shell: "nctl get keyvaluestore {{ redis_name }}"
changed_when: false
failed_when: false
register: redis_exists - name: Create Redis
ansible.builtin.command: >
nctl create keyvaluestore {{ redis_name }}
--memory-size 1Gi
--location {{ location }}
--allowed-cidrs 0.0.0.0/0
--wait
changed_when: "redis_result.rc == 0"
ignore_errors: true
when: "redis_exists.rc != 0"
register: redis_result - name: Get Redis connection info
ansible.builtin.command: nctl get keyvaluestore {{ redis_name }} -o yaml
register: redis_info
changed_when: "redis_info.rc != 0" - name: Parse Redis connection info
ansible.builtin.set_fact:
redis_host: "{{ redis_info.stdout | from_yaml | json_query('status.atProvider.connection.address') }}"
redis_password: "{{ redis_info.stdout | from_yaml | json_query('status.atProvider.connection.password') }}" - name: Debug Redis details
ansible.builtin.debug:
msg: "Redis host: {{ redis_host }}, password: {{ redis_password }}"
```
Führen Sie das Ganze mit folgendem Befehl aus:
```bash
ansible-playbook provision_redis.yml
```
Achtung: Dieser Schritt kann einige Zeit dauern.
Als Nächstes richten wir einen Redis KeyValue Store ein. Redis wird häufig für Caching, Sitzungsspeicher, Ratenbegrenzung oder Pub/Sub verwendet – es ist schnell und speicherbasiert.
Wir verwenden abermals `nctl`, um den Store zu erstellen, und extrahieren dann die Adresse und das Passwort aus dem YAML-Output.
✅ Validierung:
Versuchen Sie, sich mittels CLI mit Redis zu verbinden:
```bash
redis-cli -h <redis_host> -a <redis_password>
```
Sollte die Antwort ‘PONG’ sein, geht’s mit dem nächsten Abschnitt weiter.
🔺 Schritt 3: CloudVM provisionieren
Da nun die Datenbankdienste eingerichtet sind, können wir eine virtuelle Maschine einrichten, die als Rechen-Node fungiert. Diese VM kann später als Host für Applikationen oder Skripte konfiguriert werden.
Speichern Sie das folgende Playbook als `provision_vm.yml`:
```yaml
---
# provision_vm.yml
- name: Provision Cloud VM
hosts: localhost
gather_facts: false vars_files:
- group_vars/all.yml tasks:
- name: Check if VM already exists
ansible.builtin.shell: "nctl get cloudvirtualmachine {{ vm_name }}"
changed_when: false
failed_when: false
register: vm_exists - name: Create VM using nctl (wait until ready)
ansible.builtin.command: >
nctl create cloudvirtualmachine {{ vm_name }}
--hostname {{ vm_name }}
--location {{ location }}
--machine-type {{ machine_type }}
--os {{ os }}
--public-keys "{{ lookup('env', 'SSH_PUBLIC_KEY') }}"
--wait
changed_when: "vm_create_output.rc == 0"
ignore_errors: true
when: "vm_exists.rc != 0"
register: vm_create_output - name: Show VM creation output
ansible.builtin.debug:
var: vm_create_output.stdout_lines
when: "vm_exists.rc != 0 and vm_create_output.rc == 0"
```
Dann führen Sie es wie folgt aus:
```bash
ansible-playbook provision_vm.yml
```
Achtung: Dieser Schritt kann einige Zeit in Anspruch nehmen.
Nun richten wir die virtuelle Maschine ein. Dies wird unsere Rechen-Layer sein, auf der Ihre Anwendung – oder etwas anderes – läuft.
Wir verwenden erneut `nctl`: dieses Mal, um eine VM mit dem von Ihnen gewünschten Betriebssystem-Image und SSH-Schlüssel einzurichten.
✅ Validierung:
Via SSH einloggen:
```bash
ssh root@<vm_ip>
```
Dieser Befehl bestätigt, dass der Server online, erreichbar und bereit für die Konfiguration ist.
🧩 Das Puzzle zusammenfügen
🔚 Fazit
Wir haben nun die Basis für ein vollständig cloud-natives Deployment geschaffen:
- Eine sichere, verwaltete PostgreSQL-Instanz
- Ein schneller Redis-Service, bereit fürs Caching oder Queues
- Eine komplett bereitgestellte und erreichbare virtuelle Maschine
All das automatisiert über Ansible und unsere API – ohne manuelles Rumgeklicke und Ratespiele.
Jetzt kannst du deine Anwendung konfigurieren, Code deployen und deine Architektur skalieren – alles sauber im Code dokumentiert und einfach reproduzierbar.