Create Zot OCI Service #48

Closed
opened 2026-09-11 09:30:42 -07:00 by tepichord · 1 comment
Owner
  • Create a pod to host OCI images
  • Images to be stored as a persistent volume
  • No other service or pod will integrate Zot yet
- Create a pod to host OCI images - Images to be stored as a persistent volume - No other service or pod will integrate Zot yet
tepichord added this to the MVP project 2026-09-11 09:30:42 -07:00
Author
Owner

Naina:

Overview

Create a new zot/ service root deploying Zot as a single-replica StatefulSet on the worker node. Zot is an OCI-compliant container image registry; images persist to the persistent StorageClass via a Terraform-owned PVC. The official Helm chart handles StatefulSet creation, probes, and configuration — the Terraform root owns the namespace, PVC, and (optionally) Tailscale Ingress.

Technical Approach

  1. Copy provider boilerplate from pocket-id/main.tf — terraform block, variables, remote state, locals, providers (identical across all service roots)1

  2. Create zot namespace with baseline PSA — PVC-based storage, no hostPath needed

    resource "kubernetes_namespace_v1" "zot" {
      metadata {
        name = "zot"
        labels = {
          "pod-security.kubernetes.io/enforce" = "baseline"
        }
      }
    }
    
  3. Create Terraform-owned PVC — 15 GiB on persistent StorageClass, wait_until_bound = false2

    resource "kubernetes_persistent_volume_claim_v1" "zot_data" {
      metadata {
        name      = "zot-data"
        namespace = kubernetes_namespace_v1.zot.metadata[0].name
      }
      spec {
        access_modes       = ["ReadWriteOnce"]
        storage_class_name = "persistent"
        resources {
          requests = { storage = "15Gi" }
        }
      }
      wait_until_bound = false
      depends_on       = [kubernetes_namespace_v1.zot]
    }
    
  4. Helm release from official OCI chart — oci://ghcr.io/project-zot/helm-charts/zot, version pinned to 0.1.1243

    resource "helm_release" "zot" {
      name       = "zot"
      repository = "oci://ghcr.io/project-zot/helm-charts"
      chart      = "zot"
      version    = "0.1.124"
      namespace  = kubernetes_namespace_v1.zot.metadata[0].name
      values     = [file("${path.module}/values.yaml")]
      depends_on = [
        kubernetes_namespace_v1.zot,
        kubernetes_persistent_volume_claim_v1.zot_data,
      ]
    }
    
  5. Tailscale Ingress — reuses the vault-owned ingress-proxies ProxyGroup4, MagicDNS name zot, port 5000

    resource "kubernetes_ingress_v1" "zot" {
      metadata {
        name      = "zot"
        namespace = kubernetes_namespace_v1.zot.metadata[0].name
        annotations = {
          "tailscale.com/proxy-group" = "ingress-proxies"
        }
      }
      spec {
        ingress_class_name = "tailscale"
        tls { hosts = ["zot"] }
        default_backend {
          service {
            name = "zot"
            port { number = 5000 }
          }
        }
      }
      depends_on = [helm_release.zot]
    }
    
  6. values.yaml — key overrides:

    fullnameOverride: "zot"
    
    image:
      repository: ghcr.io/project-zot/zot
      tag: "v2.1.21"
    
    persistence: true
    
    pvc:
      create: false
      name: "zot-data"
    
    config:
      storage:
        rootDirectory: /var/lib/registry
      http:
        port: 5000
      distSpecVersion: "1.1.0"
      log:
        level: info
    
    nodeSelector:
      node-role: worker
    
    ingress:
      enabled: false
    
  7. README.md — document architecture, deploy steps, push/pull examples, storage contract

Implementation Details

Files to create:

  • zot/main.tf — provider boilerplate (copied verbatim from pocket-id/main.tf), namespace, PVC, Helm release, Ingress
  • zot/values.yaml — Helm value overrides (Terraform-owned PVC, worker nodeSelector, ingress disabled)
  • zot/README.md — service documentation

Dependencies:

  • storage/ must be applied first (Zot needs the persistent StorageClass)5
  • vault/ must be applied first (Zot Ingress uses the ingress-proxies ProxyGroup)6

Constraints:

  • Arm64 only — the official image (ghcr.io/project-zot/zot) supports linux/arm64 natively, no special handling needed7
  • No TLS in-cluster — Tailscale Ingress terminates TLS; Zot runs plaintext HTTP on port 50008
  • No authentication for now — Zot is open by default; auth can be added later9
  • PVC uses wait_until_bound = false (same pattern as pocket-id) to avoid deadlock with WaitForFirstConsumer StorageClass10
  • The chart's persistence: true creates a StatefulSet; pvc.create: false + pvc.name: "zot-data" mounts the Terraform-owned claim11

Out-of-scope notes:

  • Vault integration (secrets, auth) — not mentioned in the issue
  • Webhook/CI integration with Woodpecker — not mentioned in the issue
  • Authentication/authorization configuration — open access is acceptable for standalone deployment

Acceptance Criteria

  • zot/main.tf applies cleanly: namespace, PVC, Helm release, Ingress all created
  • Zot pod is Running on a worker node with the PVC bound
  • kubectl -n zot port-forward svc/zot 5000:5000 allows crane manifest and crane push against localhost:5000
  • Zot is accessible at https://zot.faun-snapper.ts.net from a tailnet device
  • Images persist across pod restarts (push an image, restart the pod, pull the same image)
  • zot/README.md documents architecture, deploy steps, and storage tier contract

Testing Plan

  • terraform -chdir=zot plan produces a valid plan with no errors
  • After apply: kubectl -n zot get pods shows a running zot-0 pod
  • After apply: kubectl -n zot get pvc zot-data shows status Bound
  • Push test: crane push alpine:latest zot.faun-snapper.ts.net/alpine:test succeeds
  • Pull test: crane pull zot.faun-snapper.ts.net/alpine:test /dev/null succeeds
  • Persistence test: delete pod, wait for StatefulSet to recreate, pull same image
  • #48 — this issue
  • #37 — storage tier infrastructure (defines the persistent StorageClass Zot depends on)

Research Findings

  • Zot publishes an official Helm chart at oci://ghcr.io/project-zot/helm-charts/zot (chart version 0.1.124, appVersion v2.1.21). The chart supports both Deployment (no persistence) and StatefulSet (with persistence) modes.12
  • The official image (ghcr.io/project-zot/zot) is multi-arch, supporting linux/amd64 and linux/arm64 natively. No special arm64 handling is needed.13
  • Zot serves on port 5000 by default over plaintext HTTP. TLS is optional and not needed when using Tailscale Ingress for edge termination.14
  • Zot provides /livez, /readyz, and /startupz health check endpoints. The Helm chart configures these probes automatically.15
  • Zot is open/anonymous by default — no authentication is configured out of the box. Auth can be added later via htpasswd, LDAP, OAuth2, or mTLS.16
  • The default data directory is /var/lib/registry. Deduplication (dedupe: true) and garbage collection (gc: true) are available but not enabled by default.17
Naina: ## Overview Create a new `zot/` service root deploying [Zot](https://github.com/project-zot/zot) as a single-replica StatefulSet on the `worker` node. Zot is an OCI-compliant container image registry; images persist to the `persistent` StorageClass via a Terraform-owned PVC. The official Helm chart handles StatefulSet creation, probes, and configuration — the Terraform root owns the namespace, PVC, and (optionally) Tailscale Ingress. ## Technical Approach 1. **Copy provider boilerplate from `pocket-id/main.tf`** — terraform block, variables, remote state, locals, providers (identical across all service roots)<sup>[1](https://git.gloom.garden/tepichord/milner/src/commit/7507e21fb6808c2e61a31ec95d4f206dcfc73acc/pocket-id/main.tf?display=source#L1)</sup> 2. **Create `zot` namespace with `baseline` PSA** — PVC-based storage, no hostPath needed ```hcl resource "kubernetes_namespace_v1" "zot" { metadata { name = "zot" labels = { "pod-security.kubernetes.io/enforce" = "baseline" } } } ``` 3. **Create Terraform-owned PVC** — 15 GiB on `persistent` StorageClass, `wait_until_bound = false`<sup>[2](https://git.gloom.garden/tepichord/milner/src/commit/7507e21fb6808c2e61a31ec95d4f206dcfc73acc/pocket-id/main.tf?display=source#L118)</sup> ```hcl resource "kubernetes_persistent_volume_claim_v1" "zot_data" { metadata { name = "zot-data" namespace = kubernetes_namespace_v1.zot.metadata[0].name } spec { access_modes = ["ReadWriteOnce"] storage_class_name = "persistent" resources { requests = { storage = "15Gi" } } } wait_until_bound = false depends_on = [kubernetes_namespace_v1.zot] } ``` 4. **Helm release from official OCI chart** — `oci://ghcr.io/project-zot/helm-charts/zot`, version pinned to `0.1.124`<sup>[3](https://artifacthub.io/packages/helm/zot/zot)</sup> ```hcl resource "helm_release" "zot" { name = "zot" repository = "oci://ghcr.io/project-zot/helm-charts" chart = "zot" version = "0.1.124" namespace = kubernetes_namespace_v1.zot.metadata[0].name values = [file("${path.module}/values.yaml")] depends_on = [ kubernetes_namespace_v1.zot, kubernetes_persistent_volume_claim_v1.zot_data, ] } ``` 5. **Tailscale Ingress** — reuses the vault-owned `ingress-proxies` ProxyGroup<sup>[4](https://git.gloom.garden/tepichord/milner/src/commit/7507e21fb6808c2e61a31ec95d4f206dcfc73acc/vault/main.tf?display=source#L112)</sup>, MagicDNS name `zot`, port 5000 ```hcl resource "kubernetes_ingress_v1" "zot" { metadata { name = "zot" namespace = kubernetes_namespace_v1.zot.metadata[0].name annotations = { "tailscale.com/proxy-group" = "ingress-proxies" } } spec { ingress_class_name = "tailscale" tls { hosts = ["zot"] } default_backend { service { name = "zot" port { number = 5000 } } } } depends_on = [helm_release.zot] } ``` 6. **`values.yaml`** — key overrides: ```yaml fullnameOverride: "zot" image: repository: ghcr.io/project-zot/zot tag: "v2.1.21" persistence: true pvc: create: false name: "zot-data" config: storage: rootDirectory: /var/lib/registry http: port: 5000 distSpecVersion: "1.1.0" log: level: info nodeSelector: node-role: worker ingress: enabled: false ``` 7. **`README.md`** — document architecture, deploy steps, push/pull examples, storage contract ## Implementation Details **Files to create:** - `zot/main.tf` — provider boilerplate (copied verbatim from `pocket-id/main.tf`), namespace, PVC, Helm release, Ingress - `zot/values.yaml` — Helm value overrides (Terraform-owned PVC, worker nodeSelector, ingress disabled) - `zot/README.md` — service documentation **Dependencies:** - `storage/` must be applied first (Zot needs the `persistent` StorageClass)<sup>[5](https://git.gloom.garden/tepichord/milner/src/commit/7507e21fb6808c2e61a31ec95d4f206dcfc73acc/storage/main.tf?display=source#L1)</sup> - `vault/` must be applied first (Zot Ingress uses the `ingress-proxies` ProxyGroup)<sup>[6](https://git.gloom.garden/tepichord/milner/src/commit/7507e21fb6808c2e61a31ec95d4f206dcfc73acc/vault/main.tf?display=source#L112)</sup> **Constraints:** - Arm64 only — the official image (`ghcr.io/project-zot/zot`) supports `linux/arm64` natively, no special handling needed<sup>[7](https://github.com/orgs/project-zot/packages/container/package/zot)</sup> - No TLS in-cluster — Tailscale Ingress terminates TLS; Zot runs plaintext HTTP on port 5000<sup>[8](https://zotregistry.dev/v2.1.18/install-guides/install-guide-k8s/)</sup> - No authentication for now — Zot is open by default; auth can be added later<sup>[9](https://zotregistry.dev/admin-guide/admin-guide/)</sup> - PVC uses `wait_until_bound = false` (same pattern as `pocket-id`) to avoid deadlock with WaitForFirstConsumer StorageClass<sup>[10](https://git.gloom.garden/tepichord/milner/src/commit/7507e21fb6808c2e61a31ec95d4f206dcfc73acc/pocket-id/main.tf?display=source#L95)</sup> - The chart's `persistence: true` creates a StatefulSet; `pvc.create: false` + `pvc.name: "zot-data"` mounts the Terraform-owned claim<sup>[11](https://github.com/project-zot/helm-charts/blob/main/charts/zot/templates/statefulset.yaml)</sup> **Out-of-scope notes:** - Vault integration (secrets, auth) — not mentioned in the issue - Webhook/CI integration with Woodpecker — not mentioned in the issue - Authentication/authorization configuration — open access is acceptable for standalone deployment ## Acceptance Criteria - [ ] `zot/main.tf` applies cleanly: namespace, PVC, Helm release, Ingress all created - [ ] Zot pod is `Running` on a worker node with the PVC bound - [ ] `kubectl -n zot port-forward svc/zot 5000:5000` allows `crane manifest` and `crane push` against localhost:5000 - [ ] Zot is accessible at `https://zot.faun-snapper.ts.net` from a tailnet device - [ ] Images persist across pod restarts (push an image, restart the pod, pull the same image) - [ ] `zot/README.md` documents architecture, deploy steps, and storage tier contract ## Testing Plan - `terraform -chdir=zot plan` produces a valid plan with no errors - After apply: `kubectl -n zot get pods` shows a running `zot-0` pod - After apply: `kubectl -n zot get pvc zot-data` shows status `Bound` - Push test: `crane push alpine:latest zot.faun-snapper.ts.net/alpine:test` succeeds - Pull test: `crane pull zot.faun-snapper.ts.net/alpine:test /dev/null` succeeds - Persistence test: delete pod, wait for StatefulSet to recreate, pull same image ## Related Issues/PRs - #48 — this issue - #37 — storage tier infrastructure (defines the `persistent` StorageClass Zot depends on) ## Research Findings - Zot publishes an official Helm chart at `oci://ghcr.io/project-zot/helm-charts/zot` (chart version 0.1.124, appVersion v2.1.21). The chart supports both Deployment (no persistence) and StatefulSet (with persistence) modes.<sup>[12](https://artifacthub.io/packages/helm/zot/zot)</sup> - The official image (`ghcr.io/project-zot/zot`) is multi-arch, supporting `linux/amd64` and `linux/arm64` natively. No special arm64 handling is needed.<sup>[13](https://github.com/orgs/project-zot/packages/container/package/zot)</sup> - Zot serves on port 5000 by default over plaintext HTTP. TLS is optional and not needed when using Tailscale Ingress for edge termination.<sup>[14](https://github.com/project-zot/zot/blob/main/examples/README.md)</sup> - Zot provides `/livez`, `/readyz`, and `/startupz` health check endpoints. The Helm chart configures these probes automatically.<sup>[15](https://github.com/project-zot/helm-charts/blob/main/charts/zot/templates/statefulset.yaml)</sup> - Zot is open/anonymous by default — no authentication is configured out of the box. Auth can be added later via htpasswd, LDAP, OAuth2, or mTLS.<sup>[16](https://zotregistry.dev/admin-guide/admin-guide/)</sup> - The default data directory is `/var/lib/registry`. Deduplication (`dedupe: true`) and garbage collection (`gc: true`) are available but not enabled by default.<sup>[17](https://github.com/project-zot/zot/blob/main/examples/README.md)</sup>
tepichord 2026-09-11 12:09:05 -07:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
tepichord/milner#48
No description provided.