Skip to main content

Architecture

Pagifier is a configuration-driven deployment platform for Kubernetes. Developers write exactly one file — pagifier.yaml — and interact with exactly one surface — the REST API. Everything else (images, manifests, ingress, scaling, security) is generated by the platform.

System overview

Components

ComponentBinaryResponsibility
API servercmd/pagifier-apiAuth, upload, config resolution, Release creation, history, rollback, logs, events, console
Operatorcmd/pagifier-operatorRelease build orchestration; rendering and reconciling workload resources; progressive delivery
Buildercmd/pagifier-builderBuild-job entrypoint: verify artifact, run build commands, generate Dockerfile, drive BuildKit, sign and attest

The two CRDs

Application (applications.pagifier.io) is long-lived: one per project per environment. It carries almost nothing — tenant, environment, and a pointer (spec.activeRelease) to the Release that should be live.

Release (releases.pagifier.io) is immutable: one per upload. It snapshots the artifact reference (bucket/key/SHA256) and the fully resolved configuration after all template merging.

Because the Application only points at a Release, the delivery strategies fall out naturally:

  • Rollback — point activeRelease at an older Release.
  • Canary — a parallel track serves staged traffic weights, gated on metrics.
  • Blue/green — two color tracks; the Service selector flip is the cutover.
  • Preview — an ephemeral Application per branch, TTL-bound.

Configuration resolution

Merging happens once, at upload time, in the API server. The result is embedded in the Release. The operator never re-reads templates for an existing Release, so publishing a new template version never changes already-deployed applications — it applies to the next upload.

Precedence, lowest to highest:

platform/defaults.yaml (S3)
runtime-images/<runtime>.yaml (S3)
templates/<name>/<version>.yaml (S3)
defaults/<environment>.yaml (S3)
project pagifier.yaml (user)

Merge semantics: objects merge key-by-key recursively; scalars and arrays replace wholesale; an explicit null deletes the key. Unknown fields are rejected everywhere — a typo is an error, never a silent no-op.

Build pipeline

The operator renders a Kubernetes Job per Release:

  1. An init container copies the static pagifier-builder binary, buildctl, and cosign into a shared volume.
  2. The build container — the runtime's toolchain image (e.g. node:22-alpine), selected by platform config, never by inspection — downloads app.zip, verifies its SHA256 against the value recorded at upload, extracts it (rejecting traversal and symlinks), runs the configured build commands verbatim, and assembles a build context containing only the artifact directory.
  3. BuildKit (a rootless sidecar, or the shared pool with its persistent layer cache) builds and pushes the image. No privileged containers, no Docker socket.
  4. When signing is configured, the image is signed and a SLSA provenance attestation is attached — hard failures both.

Images are content-addressed: the tag derives from a fingerprint of the artifact digest plus everything that reaches the image (build commands, env, base images). Identical inputs skip the build entirely and reuse the existing image, so revert commits deploy in seconds. Server configuration (nginx.conf) is not baked into the image; it mounts from a ConfigMap, so a headers or cache change redeploys without rebuilding.

Multi-tenancy and security

  • Namespace per tenant per environment (pgf-<tenant>-<env>), labeled for Pod Security Admission restricted.
  • API tokens map to tenants; all reads and writes are tenant-scoped.
  • Rendered pods: non-root, read-only rootfs, no capabilities, no service-account token, seccomp RuntimeDefault.
  • Default-deny NetworkPolicy admitting only the ingress controller namespace and same-namespace peers — selecting the application label so it covers canary and blue/green tracks too.
  • Artifact SHA256 recorded at upload and re-verified in the build job; optional per-tenant cosign signature enforcement.
  • Every state-changing API call lands in the audit table.

State ownership

StateOwnerWhy
Desired state (what should run)Kubernetes CRsDeclarative, watchable, RBAC-scoped
Deployment history & auditPostgreSQLSurvives CR garbage collection; queryable
ArtifactsS3Durable, versioned by key, lifecycle-manageable
Platform behavior (templates, profiles, gates)S3Changeable without redeploying Pagifier

Observability

  • Prometheus metrics from the API (request counts/latency) and the operator (controller-runtime reconcile metrics).
  • OpenTelemetry traces spanning upload → operator → build pod, propagated through a Release annotation.
  • Structured JSON logs everywhere; Kubernetes Events on every transition; build logs captured onto failed Releases.