Zum Inhalt

🏛️ Konventionen

Jeder Block MUSS ein kind haben

Mögliche Werte für kind sind:

  • k8sapp für Apps die in Kubernetes installiert werden
  • k8cluster für Kubernetes Cluster, egal wie sie installiert werden
  • linuxapp für Apps die in Linux installiert werden
  • generic

Beispiel

# block.poly
name: my-block
kind: k8sapp
config: {}

Tip

Dynamische Blöcke, die via from: von Blöcken mit kind: k8sapp erben, exportieren kind: k8sappinstance. Damit wird sichtbar, dass diese Blöcke eine in Kubernetes installierte Variante des ursprünglichen Codes bereitstellen – also eine App-Instanz im Cluster.

App-Blöcke: SOLLTEN type und flavor haben

Beispiele für type:

  • db
  • kv
  • mq
  • s3
  • web

Type steht für die übergeordnete Kategorie in die der App-Block fällt: ist es eine Datenbank, ein Key-Value-Store, etc.

Beispiele für flavor:

  • mysql
  • postgresql
  • nats
  • redis
  • minio / ceph
  • rubyonrails
  • python
  • golang

Flavor steht für die Implementierung des Type - mysql ist eine Variante vom Typ db (Database).

App-Blöcke: 1 Block = 1 App = 1 Chart = 1 Namespace

Dient ein Block dazu, eine einzelne App (Anwendung, Software, …) auf ein Zielsystem zu installieren (z.B. ingress-nginx in Kubernetes), muss sich der Block auf die Installation dieser einzelnen App beschränken.

Hat ein solcher App Block weitere Dependencies (z.B. PostgreSQL), müssen die Dependencies in separaten Blöcken installiert werden.

Ein App-Block darf außerdem nur 1 Helm Chart nutzen. In manchen Fällen ist es okay, die im Helm Chart definierten Dependencies (z.B. Redis) zu aktivieren - in solchen Fällen bleibt das "1 Block - 1 App" Prinzip intakt.

Ein Block muss sich außerdem auf einen Namespace beschränken und als Teil seiner Installations-Routine die Möglichkeit bieten, den Namespace anzulegen.

Beispiel

# block.poly
name: my-block
kind: k8sapp
config:
  namespace: my-namespace
  create_namespace: true
  chart:
    name: my-chart
    oci: false
    auth:
      enabled: false
      username: ""
      password: ""
      registry: ""
    version: 0.0.1
    repo:
      url: oci://my.registry.com/helm
      name: "my-chart"

Info

Das oben dargestellte Config-Schema ist Voraussetzung, um cargo.ayedo.cloud/ayedo/k8s/library als Library für Helm-Charts zu verwenden.

Kubernetes-Workspaces müssen Metriken und Logs exportieren

Die Logs und Metriken müssen entweder im Cluster selbst verfügbar gemacht werden, oder an ein zentrales Aggragator-Cluster übermittelt werden.

Beispiel

# workspace.poly
  - name: vector
    from: cargo.ayedo.cloud/ayedo/k8s/vector
    kubeconfig:
      from: k8s
    config:
      dependencies:
        victorialogs: 
          connection:
            endpoint: https://logs.example.com/insert/elasticsearch
            username: user
            password: password

  - name: victoria-metrics-stack
    from: cargo.ayedo.cloud/ayedo/k8s/victoria-metrics-stack:0.6.10
    kubeconfig:
      from: k8s
    config:
      vm:
        additionalremotewrite:
          urls:
            - url: "https://$USER:$PASSWORD@metrics.example.com/api/v1/write"

App-Blöcke MÜSSEN Monitoring und Backups implementieren

Für Monitoring sind folgende Ressourcen bereitzustellen:

  • VMServiceScrape
  • Grafana Dashboard ConfigMap

Für Backups sind folgende Ressourcen bereitzustellen:

  • Velero BackupSchedule

Beispiel

# block.poly
name: my-block
kind: k8sapp
config:
  monitoring:
    enabled: false
    vmservicescrape:
      enabled: false
    dashboard:
      enabled: false
  backups:
    enabled: false
    schedule: "* * * * *"

App-Blöcke MÜSSEN Kubernetes Manifeste exportieren

Installiert ein Block Ressourcen (z.B. Helm Charts oder einzelne Manifeste) in Kubernetes, müssen die finalen Manifeste in der Block Artifacts Directory abgespeichert werden.

Info

Nutzt ein Block cargo.ayedo.cloud/ayedo/k8s/library zum Installieren von Anwendungen, speichert Polycrate die generierten Manifeste und finalen Values automatisch in der Block-Artifact-Directory.

App-Blöcke MÜSSEN Polycrate Labels vererben

Polycrate fügt jedem Block unter block.labels einige Plattform-Labels hinzu, die Polycrate helfen, aktive Ressourcen, z.B. Anwendungen in Kubernetes, ihren ursprünglichen Blöcken und Workspaces zuzuordnen.

Folgende Labels setzt Polycrate automatisch:

name: my-block
kind: k8sappinstance
labels:
    blocks.polycrate.io/kind: k8sappinstance
    blocks.polycrate.io/name: my-block
    k8sappinstances.polycrate.io/name: my-block
    organizations.polycrate.io/name: my-org
    workspaces.polycrate.io/name: my-workspace

Info

Die finalen Labels eines Blocks lassen sich mit polycrate block inspect $BLOCK_NAME einsehen.

Tip

dynamische Blöcke, die via from: von Blöcken mit kind: k8sapp ableiten, exportieren automatisch kind: k8sappinstance. Das kennzeichnet sie als laufende Instanz der App in einem Kubernetes-Cluster.

Diese Labels stehen in Ansible-basierten Blöcken unter block.labels zur Verfügung und müssen an Helm Charts und Kubernetes Manifeste vererbt werden.

Beispiel Helm Chart

# values.yaml.j2
commonLabels: {{ block.labels }}

# oder
additionalLabels: {{ block.labels }}

Warning

Nicht alle Helm-Charts unterstützen die Vererbung von Labels. Falls das der Fall ist, kann auf die Vererbung verzichtet werden.

Beispiel Manifest

# clusterissuer.yaml.j2
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: my-cluster-issuer
  labels: {{ block.labels }}
[...]

Blöcke MÜSSEN versioniert werden

Polycrate folgt einem Versionierungsschema basierend auf Conventional Commits. Conventional Commits ist eine Methodik Versionsnummern auf Basis von Commit-Messages, bzw. deren Typ (Type of Change), zu berechnen.

In Polycrate nutzen wir Git-Commits nur zur Persistierung finaler Stände des Workspaces in einem Git-Repository - einzelne Git-Commits haben aber keinen Einfluss auf die Versionierung eines Blockes.

Ein Block wird in der Regel nicht entwickelt wie reguläre Software. Die Entwicklung von Blöcken passiert immer im Kontext eines Workspaces - sobald ein Block-Autor der Meinung ist, seine Änderungen am Block sind komplett, erzeugt er eine neue Version, taggt diese mit einem Prefix passend zu der Art und des Umfangs seiner Änderungen am Block, und beschreibt die Anpassungen am Block. Am Ende pusht er die neue Version des Blocks mit polycrate block push … in eine OCI-kompatible Registry und macht ihn dadurch verfügbar für andere.

Wir nutzen die in Conventional Commits etablierten Typen und Mechanismen, um die Komplexität einer Block-Version zu beschreiben und damit den Nutzern des Blocks ein qualifiziertes Bild von Risiko und Komplexität eines Versionssprungs zu vermitteln.

Die folgende Tabelle beschreibt die empfohlene Verwendung von Prefixes einer Block-Version. Im nächsten Abschnitt wird erläutert, wo genau diese Form der Versionierung relevant ist.

Type of Change Beschreibung Initial Version New Version
chore keine wesentliche Änderung vorgenommen (z.B. nur Änderungen an der README oder Teilen des Blocks die keinen Einfluss auf das Gesamtverhalten des Blocks haben), abwärts-/aufwärts-kompatibel 0.0.1 0.0.2
fix ein Fehler wurde behoben oder eine unkritische Änderung vorgenommen, ggf. abwärtskompatibel (hängt von Datenbank-Migrationen u.ä. ab); aufwärts-kompatibel 0.0.2 0.0.3
feat ein neues Feature wurde eingebaut, ggf. abwärtskompatibel (hängt von Datenbank-Migrationen u.ä. ab); aufwärts-kompatibel 0.0.3 0.1.0
breaking eine nicht-abwärtskompatible oder kritische Änderung wurde vorgenommen; das ist vor allem dann der Fall, wenn ein Versionssprung eine Inkompatibilität und/oder manuellen Eingriff bedingt (Beispiel: manuelles Upgrade von Postgres 11 auf 14; oder: Config-Struktur hat sich geändert) 0.1.0 1.0.0

Info

Neue Blöcken starten per Konvention bei Version 0.0.1.

Warning

Achtung: Im Gegensatz zu den Defaults von Conventional Commits führt jede Änderung an Polycrate-Blöcken zu einem Versionssprung. Selbst chore erhöht die Patch-Version.

Blöcke MÜSSEN ein CHANGELOG führen

Jede Änderung eines Blockes muss eine neue Version erzeugen. Jede neue Version eines Blockes muss einen Eintrag im CHANGELOG erzeugen.

Traditionell wurde das Changelog in einer eigenen Sektion der README Datei des Blockes geführt.

# My Block

Installiert [My App](https://my-app.com/) in Kubernetes.

---

## Changelog

- `0.1.0` feat: My App Helm Chart von 1.12.2 auf 1.14.2 aktualisiert.
- `0.0.1` feat: initial-commit.

Seit April 2025 wird jedoch das Führen des Changelogs in der Datei CHANGELOG.poly nach folgendem Format empfohlen:

- version: 0.0.2
  type: fix
  message: fixed a typo
  description: "Hier ist Platz für längere Texte, inklusive Markdown"
  date: "2024-02-02"
  email: user@example.com
- version: 0.0.1
  type: feat
  message: initial commit
  description: ""
  date: "2024-02-02"
  email: user@example.com

1 Workspace = 1 Kubernetes Cluster

Pro Workspace sollte nur 1 Kubernetes Cluster verwaltet werden.

Prinzipiell ist es möglich, beliebig viele Cluster zu verwalten, der Übersicht und der Vermeidung von Fehlern halber wird aber empfohlen, die Anzahl an Kubernetes Clustern pro Workspace auf 1 zu limiteren.

Kubeconfig und Inventory

Das Workspace Inventory muss in Form eines yaml-formatierten Ansible-Inventory in der Datei inventory.yml in der Workspace Root liegen.

Die Workspace Kubeconfig muss in Form einer gültigen Kubeconfig in der Datei kubeconfig.yml in einer Block Artifact Directory liegen. Wird Kubernetes mit Hilfe eines Blocks installiert, muss der Block die Kubeconfig zur Steuerung des Clusters in seine Block Artifact Directory exportieren.

Per Konvention ist der Name des Blocks der die Kubeconfig bereitstellt k8s.

Beispiel

my-workspace/
├─ artifacts/
│  ├─ blocks/
│  │  ├─ my-block
│  │  ├─── kubeconfig.yml
├─ inventory.yml

Ein Workspace MUSS eine Organization haben

Die Organization wird von Polycrate für das automatische Labelling von Ressourcen, die durch Blöcke erzeugt wurden, genutzt.

Beispiel

# workspace.poly
name: my-workspace
organization: my-org
blocks: []

Jeder Block MUSS eine install und eine uninstall Action haben

Nuff said.

Beispiel

# block.poly
name: my-block
kind: k8sapp
config: {}

actions:
  - name: install
    playbook: install.yml
  - name: uninstall
    playbook: uninstall.yml

Block Config MUSS einem Schema folgen

Das Block Config Schema ist nicht bindend (d.h. die Polycrate CLI beendet die Ausführung nicht, wenn Werte nicht gesetzt sind), jedoch interagieren andere Teile des Ökosystems (z.B. Polycrate API) mit dem Schema, um aus einer Block-Definition abzuleiten, welche Funktion der Block hat, in welcher Plattform er läuft oder welche Endpunkte überwacht werden müssen.

kind: k8sapp

name: cr.ayedo.cloud/blocks/k8s/my-app
version: 0.0.1
kind: k8sapp
type: web # or: db|kv
flavor: python # or: postgresql|redis|golang|nodejs
icon_url: https://example.com/icon.svg
website_url: https://example.com/
display_name: My App
license: MIT
license_url: https://git.examlpe.com/license.md 
documentation_url: https://docs.examlpe.com
releases_url: https://examlpe.com/releases
git_repository_url: https://git.examlpe.com
descrition: "This app is a good app"
supports_ha: false
app_version: 1.2.3
labels:
  foo: bar
config: # TYPED
  namespace: my-app # every k8sapp should have this
  create_namespace: true # every k8sapp should have this
  connection:
    endpoint: my-app.com
    port: 80
    username: ""
    password: ""
    tls: 
      enabled: false
  chart: # every k8sapp should have this
    name: my-app
    version: 1.2.3
    oci: false # must be `true` if repo.uri starts with `oci://`
    auth:
      enabled: false
      username: ""
      password: ""
      registry: ""
    repo:
      url: https://my.repo.com/charts/
      name: my-app
  app: # UNTYPED
    signup:
      enabled: false # true = Signup für alle aktiviert // false = Signup für Domains aktiviert
      domains: []
      verify_user_email: false
      require_user_email: false
      allow_invitation: true
    admin:
      enabled: false
      token: extremely_long_admin_token_and_secure_too
  replica_count: 1 # every k8sapp should have this
  persistence: # every k8sapp should have this
    enabled: false
    size: 5Gi
    access_mode: ReadWriteOnce
    storage_class: "-"
  update_strategy: # every k8sapp should have this
    type: Recreate # or Rolling
  ingress: # every k8sapp should have this
    enabled: false
    class: nginx
    host: my-app.com
    extra_annotations: {}
    extra_labels: {}
    extra_hosts: [] # ggf. mit /path?
    tls:
      enabled: false
      letsencrypt:
        issuer: letsencrypt-production
  extra_manifests: [] # every k8sapp should have this; UNTYPED
  dependencies:
    postgres: # content will be replaced with the block from `from` if found; could be instanciated as a block on its own in the future
      enabled: true
      from: my-app-db
      connection:
        endpoint: postgresql
        port: 5432
        username: postgresql
        password: postgresql
        tls: 
          enabled: false
          accept_invalid_certificate: false
          accept_invalid_hostname: false
      app:
        database: postgresql
        admin:
          username: ""
          password: ""
      # url: postgresql://user:pass@postgresql:5432/database-name
  monitoring:
    enabled: false
    vmservicescrape:
      enabled: false
    dashboard:
      enabled: false
  backup:
    enabled: false
    schedule: ""
  extra_env:
    - name: ENV_1
      value: value1
  resources:
    limits:
      memory: 256Mi
      cpu: 200m
      ephemeral-storage: 512Mi
    requests:
      memory: 128Mi
      cpu: 100m
      ephemeral-storage: 256Mi

actions:
  - name: install
    playbook: install.yml
  - name: uninstall
    playbook: uninstall.yml

Dependency: Postgres

name: postgres-db
from: cr.ayedo.cloud/blocks/k8s/cloudnative-pg-cluster
version: 0.0.1
flavor: postgresql
type: database
kind: k8sapp
labels:
  foo: bar
config: # UNTYPED
  connection:
    endpoint: lol.com
    port: 25
    username: asdasd
    password: asdasd
    tls: 
      enabled: false
  namespace: cloudnative-pg # every k8sapp should have this
  create_namespace: false # every k8sapp should have this
  chart: # every k8sapp should have this
    name: cloudnative-pg
    version: 1.2.4
    oci: false
    auth:
      enabled: false
      username: ""
      password: ""
      registry: ""
    repo:
      url: https://cloudnative-pg.github.io/charts/
      name: cloudnative-pg
  app: # UNTYPED
    signup:
      enabled: false # true = Signup für alle aktiviert // false = Signup für Domains aktiviert
      domains: []
      verify_user_email: false
      require_user_email: false
      allow_invitation: true
    admin:
      enabled: false
      token: extremely_long_admin_token_and_secure_too
  replica_count: 1 # every k8sapp should have this
  persistence: # every k8sapp should have this
    enabled: false
    size: 5Gi
    access_mode: ReadWriteOnce
    storage_class: "-"
  strategy: # every k8sapp should have this
    type: Recreate
  ingress: # every k8sapp should have this
    enabled: false
    class: nginx
    host: vaultwarden.example.com
    extra_hosts: [] # ggf. mit /path?
    tls:
      enabled: false
      letsencrypt:
        issuer: letsencrypt-production
  extra_manifests: []
  dependencies: {}
  monitoring:
    enabled: false
    vmservicescrape:
      enabled: false
  backup:
    enabled: false
    schedule: ""

actions:
  - name: install
    playbook: install.yml
  - name: uninstall
    playbook: uninstall.yml