Bring-your-own-App (BYOA)

Bring-your-own-App (BYOA)

Der Polycrate-Block ayedo/k8s/byoa paketiert eine 15-Factor-App für Kubernetes über block.config und Jinja — ohne Helm, ohne Image-Build, ohne eingebettete Datenbank.

Produktseite: Bring-your-own-App. Releases: Polycrate Hub. Methodik: 15-Factor Apps.

BYOA ist die Release- und Run-Schicht für ein bereits gebautes Container-Image. Die Codebase bleibt im VCS der Anwendung. Backing Services bleiben Geschwister-Blöcke (CloudNativePG, Valkey, RustFS). Metriken laufen über VictoriaMetrics, Logs über VictoriaLogs — nicht über Prometheus oder Loki.

Wann BYOA, wann ohMyHelm?

BYOA ohMyHelm
Paketierung Polycrate-Block, Jinja, block.config Helm Chart, values.yaml
Ausrollen polycrate run <instanz> install helm upgrade --install / Argo CD
Zielgruppe Workspaces, die Apps wie Infrastruktur als Block betreiben Teams mit bestehendem Helm/GitOps-Pfad
Image-Build außerhalb (CI) außerhalb (CI)
Backing Services Geschwister-Blöcke, per Env angehängt als URL in values.yaml

Beide Wege implementieren denselben 15-Factor-Vertrag. Ein Image, das in BYOA läuft, kann derselbe Build sein, den ohMyHelm deployt.

Schnellstart

Block-Instanz in der workspace.poly:

blocks:
  - name: acme-shop
    from: cargo.ayedo.cloud/ayedo/k8s/byoa
    config:
      namespace: acme-shop
      image:
        repository: registry.acme.corp/acme/shop
        tag: "1.2.3"
      env:
        - name: LOG_LEVEL
          value: info
      env_from:
        - secretRef:
            name: acme-shop-db-app
      workloads:
        web:
          kind: Deployment
          replicas: 2
          ports:
            - name: http
              containerPort: 8080
          probes:
            liveness:
              httpGet:
                path: /healthz
                port: http
            readiness:
              httpGet:
                path: /readyz
                port: http
        worker:
          kind: Deployment
          replicas: 3
          command: [./worker]
        migrate:
          kind: Job
          hook: pre
          command: [./migrate]
polycrate run acme-shop render
polycrate run acme-shop install
polycrate run acme-shop status

namespace ist Pflicht. Entweder image.repository auf Block-Ebene oder pro Workload. Ohne image.tag oder image.digest lehnt install implizites :latest ab.

Kopierbare Instanzen liegen in der examples.poly des Blocks (polycrate block inspect bzw. Hub).

Actions

Action Verhalten
render Manifeste nach Block-Artefakten schreiben, kein Cluster-Write
install Validieren, dann Namespace → RBAC → NetworkPolicies → ConfigMaps/Secrets → Extra-Manifeste → Pre-Jobs löschen und neu anlegen → auf Complete warten → Workloads → Services → HPA/PDB → Ingress oder Gateway → CronJobs → VMPodScrapes → Post-Jobs (überspringen, wenn schon Complete)
status Ressourcen mit app.kubernetes.io/instance={{ block.name }}
uninstall Dieselben Ressourcen löschen. PVCs bleiben, außer persistence.retain ist false. Namespace bleibt, außer delete_namespace: true

Server-Side Apply mit field_manager: polycrate und force_conflicts: true.

Maps statt Listen

In workspace.poly mergen Maps sauber (workloads.web.replicas). Listen ersetzen sich.

Maps: workloads, configmaps, secrets, network_policies, rbac.roles, rbac.role_bindings, rbac.cluster_roles, rbac.cluster_role_bindings, init_containers, sidecars.

Listen: env, ports, volume_mounts, volumes, tolerations, image_pull_secrets, extra_manifests.

Naming

  • Primärer Service-Workload (erster mit ports, sonst erstes Deployment) = {block.name}
  • Alle anderen = {block.name}-{key}
  • Override: workloads.<key>.name

Labels: app.kubernetes.io/{name,instance,component,part-of,managed-by} plus block.labels. Uninstall selektiert app.kubernetes.io/instance.

Workloads

kind Objekt 15-Factor-Prozesstyp
Deployment (Default) Deployment web, worker (Faktor 6 + 8)
StatefulSet StatefulSet, optional PVC-Templates nur wenn stabile Identität nötig ist
DaemonSet DaemonSet node-lokal, nicht die Standard-App
Job Job Admin / Migration (Faktor 12)
CronJob CronJob geplanter Admin (Faktor 8 + 12)

Pro Workload unter anderem: image (Override), command / args, ports, env / env_from (hängen an die Top-Level-Listen an), probes, resources, security_context, volume_mounts, volumes, init_containers, sidecars, hpa, pdb, ingress, gateway, service, node_selector, tolerations, affinity, termination_grace_period_seconds (Default 30).

Pre-Jobs (Faktor 12)

migrate:
  kind: Job
  hook: pre          # pre | post | none
  command: [./migrate]

hook: pre läuft bei jedem install: bestehende Pre-Jobs werden foreground gelöscht, neu angelegt, auf Complete gewartet, danach erst Workloads. Migrationen müssen idempotent sein (CREATE TABLE IF NOT EXISTS, nicht „einmalig INSERT ohne Guard“).

hook: post läuft nach den Workloads und wird übersprungen, wenn der Job bereits Complete ist.

CronJobs

nightly:
  kind: CronJob
  schedule: "0 3 * * *"
  concurrency_policy: Forbid
  command: [./report]

Konfiguration und Secrets (Faktor 3)

configmaps:
  app:
    data:
      FEATURE_CHECKOUT: "true"

secrets:
  app:
    string_data:
      SESSION_KEY: "{{ workspace.secrets.acme_shop.session_key }}"

env_from:
  - configMapRef:
      name: acme-shop-app
  - secretRef:
      name: acme-shop-app

Echte Credentials gehören nach secrets.poly, nicht in die workspace.poly.

Deployment-, StatefulSet- und DaemonSet-Pods bekommen Annotations (nicht Labels — SHA256 ist 64 Zeichen):

  • checksum/configmap-<key> und checksum/configmaps
  • checksum/secret-<key> und checksum/secrets

Ändert sich die Payload, rollt der nächste install die Pods.

Exposure (Faktor 7 + 15)

Ingress und Gateway sind default aus und gegenseitig exklusiv. Beide gleichzeitig aktiv → install bricht ab.

Ingress

workloads:
  web:
    ports:
      - name: http
        containerPort: 8080
    ingress:
      enabled: true
      class: nginx
      host: shop.acme.example
      path: /
      tls:
        enabled: true
        issuer: letsencrypt-production

Gateway API (Envoy)

workloads:
  web:
    ports:
      - name: http
        containerPort: 8080
    gateway:
      enabled: true
      create_gateway: true
      gateway_class_name: eg-shared
      host: shop.acme.example
      tls:
        issuer: letsencrypt-production

Erzeugt Certificate, Gateway, HTTPRoute sowie Envoy Client-/BackendTrafficPolicy. Anbindung an ein vorhandenes Shared-Gateway: create_gateway: false plus parent_gateway_name / parent_gateway_namespace / parent_gateway_section_name.

TLS terminiert am Edge. Die App bindet weiter einen unprivilegierten Container-Port.

Integration in die Polycrate API

Der Polycrate Operator beobachtet Ingress immer und HTTPRoute, sobald die Gateway-API-CRDs im Cluster liegen (CLI Spec 300). Der BYOA-Block schreibt denselben Kundenvertrag auf beide Ressourcen:

Annotation Default Wirkung
polycrate_endpoint_monitor "true" Operator legt pro Host eine Endpoint-CR an und sync't sie in die API (HTTP-Checks, TLS, Downtime). Im Discovery-Modus annotation (Standard) ohne dieses Flag kein Endpoint.
polycrate_k8sapp_name {block.name} bzw. config.k8sapp_name Operator legt eine K8sApp-CR an (kind=external, discovery-source=ingress bzw. httproute) oder associert eine vorhandene Secret-/Block-App gleichen Namens.
polycrate_k8sapp_byoa "true" API setzt K8sApp.byoa=true (CatalogueApp/Billing). Nur auf ingress-/httproute-sourced CRs; bestehende Block-Apps werden nicht umgeflaggt.
polycrate_endpoint_path Readiness-httpGet.path, sonst Liveness, sonst / (Gateway: sonst gateway.path) Health-Check-Pfad des Endpoints

Gleicher Hostname im selben Namespace: HTTPRoute gewinnt den Endpoint-CR; Ingress holt ihn nicht zurück. Ingress und Gateway bleiben im Block gegenseitig exklusiv.

Ergebnis: die App erscheint in der Polycrate API als K8sApp mit zugeordneten Endpoints. Claimed inkl. BYOA erzeugt Downtimes; nur polycrate_endpoint_monitor ohne polycrate_k8sapp_name bleibt unclaimed und erzeugt keine App-Downtime. Details: Endpoint-Monitoring.

workloads:
  web:
    ingress:
      enabled: true
      host: shop.acme.example
      # optional overrides:
      # endpoint_monitor: false
      # k8sapp_name: acme-shop-frontend
      # endpoint_path: /readyz
    # oder Gateway statt Ingress (gegenseitig exklusiv):
    # gateway:
    #   enabled: true
    #   host: shop.acme.example
    #   endpoint_path: /readyz

ingress.annotations und gateway.annotations werden danach gemerged und können einzelne Keys überschreiben. Gateway-Overrides: gateway.endpoint_monitor, gateway.k8sapp_name, gateway.endpoint_path.

Skalierung und Disruption (Faktor 8 + 9)

workloads:
  web:
    replicas: 2
    hpa:
      enabled: true
      min_replicas: 2
      max_replicas: 8
      cpu_utilization: 70
    pdb:
      enabled: true
      max_unavailable: 1

HPA und PDB gelten für Deployment und StatefulSet.

RBAC und NetworkPolicy (Faktor 15)

ServiceAccount default an (service_account.create, Name {block.name}).

rbac:
  roles:
    read-config:
      rules:
        - apiGroups: [""]
          resources: [configmaps]
          verbs: [get, list]
  role_bindings:
    read-config:
      role: acme-shop-read-config

cluster_roles / cluster_role_bindings analog. Subjects default = Block-ServiceAccount.

network_policies:
  default:
    policy_types: [Ingress, Egress]
    ingress:
      - from:
          - namespaceSelector:
              matchLabels:
                kubernetes.io/metadata.name: envoy-gateway-system
        ports:
          - protocol: TCP
            port: 8080

Default-podSelector: app.kubernetes.io/instance={{ block.name }}.

Telemetrie (Faktor 14) und Logs (Faktor 11)

metrics:
  enabled: true
  vmpodscrape:
    enabled: true
    path: /metrics
    port: http

Erzeugt ein VMPodScrape (operator.victoriametrics.com/v1beta1) pro Deployment/StatefulSet/DaemonSet. Override pro Workload: workloads.web.metrics.path / port.

Die Anwendung schreibt Logs nach stdout/stderr. Die Plattform sammelt sie in VictoriaLogs. Kein Loki-Sidecar, kein Prometheus-ServiceMonitor in diesem Block.

Backing Services (Faktor 4)

BYOA enthält keine Datenbank. Geschwister-Blöcke anbinden:

- name: acme-shop-db
  from: cargo.ayedo.cloud/ayedo/k8s/cloudnative-pg

- name: acme-shop
  from: cargo.ayedo.cloud/ayedo/k8s/byoa
  config:
    env:
      - name: DATABASE_URL
        value: postgres://acme-shop-db-rw.acme-shop.svc:5432/shop
    env_from:
      - secretRef:
          name: acme-shop-db-app

Staging vs. Production ist eine Config-Änderung (Hosts, Secret-Namen, Replicas) — nicht ein zweites Template (Faktor 10).

Security-Defaults

security_context:
  runAsNonRoot: true
  allowPrivilegeEscalation: false
  capabilities:
    drop: [ALL]

pod_security_context:
  seccompProfile:
    type: RuntimeDefault

Images, die als Root starten (z. B. traefik/whoami), brauchen einen Workload-Override (runAsUser: 65534). Produktionsimages sollen USER im Dockerfile setzen.

Private Registry:

image_credentials:
  enabled: true
  registry: cargo.ayedo.cloud
  username: "{{ workspace.secrets.cargo.username }}"
  password: "{{ workspace.secrets.cargo.password }}"

Was BYOA nicht tut

  • Images bauen oder pushen (Faktor 5: Build liegt in der CI)
  • PostgreSQL, Valkey oder Object Storage einbetten
  • Secret-Werte auto-generieren
  • :latest implizit setzen
  • Ingress oder Gateway default aktivieren
  • ein Git-Repository ausliefern (Distribution über den Hub)

15-Factor-Zuordnung

Die 15-Factor App erweitert die klassischen 12 Faktoren um API First, Telemetry und Authentication & Authorization. Detailseite in dieser Sektion: 15-Factor Apps.

Faktor Anwendung BYOA
1 Codebase Ein Repo, viele Deploys Eine Block-Definition, viele Workspace-Instanzen. Kein Git-URL am Block.
2 Dependencies Im Image (go.mod, …) image.tag / digest pinnen. image_credentials für private Registries.
3 Config Nichts Umfeld-spezifisches im Image env, env_from, ConfigMaps, Secrets, Checksum-Rollouts
4 Backing Services URLs, austauschbar Geschwister-Blöcke, keine embedded DB
5 Build, Release, Run CI baut das Image BYOA = Release (Image + Config) + Run (install)
6 Processes Zustandslos Default Deployment. State in CNPG/Valkey/RustFS
7 Port Binding Lauschen im Prozess ports → Container-Port + Service
8 Concurrency Nach Prozesstyp skalieren Map-Keys web / worker / nightly, HPA
9 Disposability Schnell start/stop Probes, Grace Period, PDB, Pre-Jobs disposable
10 Dev/Prod Parity Dasselbe Image Maps in der Workspace-Config überschreiben
11 Logs stdout/stderr VictoriaLogs auf der Plattform
12 Admin Processes One-off in derselben Env kind: Job, hook: pre / post, CronJob
13 API First OpenAPI/gRPC in der App HTTP/gRPC-Port + Edge-Route; CRDs via extra_manifests
14 Telemetry /metrics, Traces, Health VMPodScrape → VictoriaMetrics
15 AuthN/Z OIDC/OAuth2/JWT in der App TLS am Edge, SA+RBAC, NetworkPolicy, Non-Root

Häufige Fehler: Config im Image, PVC als „Datenbank“, :latest, nicht-idempotente Pre-Jobs, Ingress und Gateway gleichzeitig, Prometheus/Loki statt VictoriaMetrics/VictoriaLogs.

Weiterführend