Files
homelab/k8s/infra/minio/minio-tenant.yaml
T
rock f3015522e3 fix(storage): pin minio tenant to a published image release (#53)
## Why

`minio/minio:RELEASE.2025-07-23T15-54-02Z` no longer exists. The Docker Hub repository
`minio/minio` returns **404 for every tag** (checked: the pinned tag and `latest`), and
MinIO has moved its images behind registry authentication (`quay.io/minio/minio` → 401,
`registry.min.io/minio/minio` → auth required, `dl.min.io` binaries → 410 Gone).

The tenant therefore sat in `ImagePullBackOff` — 44,229 attempts over 6d23h — and the chain
from there is:

```
minio tenant pod not ready (1/2)
  -> headless Service minio-cluster-hl has no endpoints
  -> CoreDNS: lookup minio-cluster-hl.storage.svc.cluster.local -> no such host
  -> Vault S3 storage init fails at startup
  -> vault-0 CrashLoopBackOff, 1943 restarts, ~7 days
```

## Control checks (so this is not a network story)

| check | result |
| --- | --- |
| `alpine:3.20` pulled on talos-cp-1 | Succeeded — docker.io works |
| `quay.io/prometheus/node-exporter:v1.8.2` on the same node | Succeeded — public quay works |
| `minio/minio` (docker.io) | HTTP 404, repo removed |
| old tag `RELEASE.2024-06-13T20-48-48Z` with `IfNotPresent` | not cached on the node |

## Change

Pin `spec.image` to `coollabsio/minio:RELEASE.2025-10-15T17-29-55Z` — a public mirror, and a
newer upstream release than the previous pin. Verified by pulling it on talos-cp-1 and
running it: the image carries `/usr/bin/docker-entrypoint.sh` (the layout the operator
expects, which runs `minio server … --console-address :9090`) and reports
`minio version RELEASE.2025-10-15T17-29-55Z`.

## Caveat

This is a community mirror, not MinIO's official image, because the official ones now
require credentials. If MinIO registry credentials are available, add an imagePullSecret
and switch back to `quay.io/minio/minio`. Supply-chain decision, flagged deliberately.

## After merge (ArgoCD, not kubectl)

The storage app tracks `k8s/infra/minio`, so the sync rolls the tenant; then:

```
kubectl get pods -n storage          # minio-cluster-az-a-0 -> 2/2
kubectl get pods -n iam | grep vault  # vault-0 -> Running (storage init succeeds)
```

Residual risk, unrelated to this fix: the tenant's 4 x 25Gi volumes are Longhorn-backed, and
Longhorn currently fails to create engines for new volume attaches cluster-wide. If the
restarted pod cannot attach, that is a second, separate fault.Reviewed-on: #53

Co-authored-by: rock <[email protected]>
2026-10-06 01:48:57 +00:00

133 lines
5.2 KiB
YAML

apiVersion: minio.min.io/v2
kind: Tenant
metadata:
name: minio-cluster
namespace: storage
labels:
app: minio
annotations:
# Let the operator own bucket/user provisioning declaratively.
prometheus.io/path: /minio/v2/metrics/cluster
prometheus.io/port: "9000"
prometheus.io/scrape: "true"
spec:
# Was minio/minio:RELEASE.2025-07-23T15-54-02Z — that Docker Hub repository no longer
# exists (404 for every tag), so the tenant sat in ImagePullBackOff and Vault's S3
# backend could not resolve minio-cluster-hl. Official images now need registry auth
# (quay.io/minio/minio returns 401), so this pins a public mirror of the same upstream
# release; swap it for the official image once MinIO registry credentials exist.
image: coollabsio/minio:RELEASE.2025-10-15T17-29-55Z
# Disable operator auto-TLS: MinIO serves plain HTTP internally on 9000.
# External TLS is terminated at nginx ingress (wildcard riotpiao-com-tls cert).
# Without this, MinIO auto-generates self-signed certs and serves HTTPS-only
# on 9000, which breaks plain-HTTP internal clients like Vault's S3 backend
# (they hang waiting for a TLS handshake that never completes on an HTTP request).
requestAutoCert: false
# Root credentials. v5 pods read `configuration` — a Secret whose `config.env`
# key holds shell `export MINIO_ROOT_USER=...` lines. Created out-of-band
# (SOPS), see minio-secrets.enc.yaml. NOTE: the operator health-monitor logs a
# cosmetic "empty tenant credentials" warning (it greps for legacy
# access_key/secret_key keys) — MinIO itself authenticates fine; ignore it.
configuration:
name: minio-creds
# ── Single pool on the sole storage/scheduling node (talos-cp-1, az-a) ──────
# Per the 3-CP topology only talos-cp-1 is schedulable and holds Longhorn, so
# MinIO is a single-server tenant. 4 volumes give erasure-coded durability
# (MinIO's minimum for parity) on that one node.
pools:
- name: az-a
servers: 1
volumesPerServer: 4
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: [az-a]
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
volumeClaimTemplate:
metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 25Gi
# ── Declarative buckets (operator creates on first boot) ────────────────────
buckets:
- name: loki-chunks
- name: loki-ruler
- name: loki-admin
- name: vault
- name: paperless
# Metrics are exposed at /minio/v2/metrics; scrape via a hand-rolled
# ServiceMonitor in the monitoring stack rather than operator auto-wiring
# (prometheusOperator:true makes the operator hunt for Prometheus in ns
# 'default' and fail the reconcile).
# Public hostnames the tenant serves (S3 + console via the cluster ingress).
# Must match actual ingress backends: the "minio" Ingress (host
# minio.riotpiao.com) routes to minio-cluster-console:9090, and "minio-api"
# (host minio-api.riotpiao.com) routes to minio:9000. Previously these were
# swapped, so MinIO's own console-domain validation rejected the OIDC
# callback arriving on minio.riotpiao.com's Host header.
features:
domains:
minio:
- https://minio-api.riotpiao.com
console: https://minio.riotpiao.com
# ── OIDC via Authentik (server-side env, valid in v2 schema) ────────────────
# Use in-cluster URL for config fetch (pod→authentik); browser redirects use
# public URLs embedded in the OIDC metadata response (issuer stays public).
env:
- name: MINIO_IDENTITY_OPENID_CONFIG_URL
# Must use external URL — well-known response contains external issuer/jwks_uri.
# MinIO validates issuer in JWT matches well-known issuer. Internal URL = mismatch.
# Hairpins through ingress-nginx but stays in-cluster.
value: "https://authentik.riotpiao.com/application/o/minio/.well-known/openid-configuration"
- name: MINIO_IDENTITY_OPENID_CLIENT_ID
value: "minio"
- name: MINIO_IDENTITY_OPENID_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: minio-oidc
key: MINIO_IDENTITY_OPENID_CLIENT_SECRET
- name: MINIO_IDENTITY_OPENID_CLAIM_NAME
value: "groups"
- name: MINIO_IDENTITY_OPENID_CLAIM_PREFIX
value: ""
- name: MINIO_IDENTITY_OPENID_REDIRECT_URI
value: "https://minio.riotpiao.com/oauth_callback"
- name: MINIO_IDENTITY_OPENID_DISPLAY_NAME
value: "Authentik"
- name: MINIO_IDENTITY_OPENID_SCOPES
value: "openid,profile,email,groups"
- name: MINIO_BROWSER_LOGIN_ANIMATION
value: "off"
- name: MINIO_BROWSER_REDIRECT_URL
value: "https://minio.riotpiao.com"