attune: Complete Specification¶
Safe, in-place Kubernetes pod resource right-sizing operator. VPA done right, powered by In-Place Pod Resize (K8s 1.32+).
Table of Contents¶
- Vision & Goals
- Technology Decisions
- CRD Design
- Architecture
- Algorithm Design
- Resize Engine
- Safety System
- Metrics & Observability
- Testing Strategy
- CI/CD Pipeline
- Distribution
- Documentation
- Project Structure
- Roadmap
- Competitor Lessons
1. Vision & Goals¶
Problem¶
99.94% of Kubernetes clusters are over-provisioned. Average CPU utilization is 8%, memory 20% (CAST AI 2026). VPA, the tool designed to fix this, is universally feared: fewer than 1% of organizations run it in production (ScaleOps 2026). VPA historically evicts pods (newer modes can attempt in-place where supported), conflicts with HPA on the same metrics, and has caused cluster-wide outages.
In December 2025, In-Place Pod Resize graduated to GA in Kubernetes 1.35 (KEP-1287; alpha since 1.27, feature-gated through 1.32; beta and enabled by default in 1.33–1.34). CPU and memory can be changed on running pods without restarts. That unlocks a ground-up redesign of resource right-sizing.
Mission¶
Attune is the first production-grade right-sizing operator built exclusively for in-place resize. It exists to make VPA obsolete by delivering:
- Zero-downtime right-sizing: Resize pods in-place without restarts (CPU) or with minimal container-only restarts (memory)
- Safety-first design: Graduated rollout from observe to full-fleet, with automatic revert on OOMKill or throttle
- HPA coexistence: Adjusts base resource requests without breaking HPA percentage targets
- Production confidence: Composable recommendation algorithm with confidence-based widening for sparse data
Non-Goals¶
- Traffic shifting or canary deployments (use Argo Rollouts/Flagger)
- Node-level autoscaling (use Karpenter/Cluster Autoscaler)
- Cost visibility dashboards (use OpenCost/Kubecost)
- GPU or ephemeral-storage right-sizing (not supported by in-place resize API)
2. Technology Decisions¶
2.1 Language: Go 1.27.2¶
| Factor | Decision |
|---|---|
| Language | Go 1.27.2 |
| Module directive | go 1.27.2 |
| Rationale | 85%+ of production K8s operators use Go. Largest ecosystem, hiring pool, and controller-runtime support. Green Tea GC (1.26) provides lower latency. |
| What competitors use | right-sizer: Go 1.25, OptiPod: Go 1.24.6, VPA: Go |
| What model operators use | CloudNativePG: Go 1.26.3, Kyverno: Go 1.26.2 |
2.2 Framework: Kubebuilder v4 + controller-runtime v0.25.2¶
| Component | Version | Purpose |
|---|---|---|
| Kubebuilder | v4.14.0 | Project scaffolding, Makefile, CRD generation |
| controller-runtime | v0.25.2 | Controller lifecycle, reconciliation, caching, webhooks |
| client-go | v0.37.1 | K8s API access, /resize subresource calls |
| k8s.io/api | v0.37.1 | K8s type definitions |
| k8s.io/apimachinery | v0.37.1 | Resource quantities, conditions, meta types |
Why Kubebuilder over Operator SDK: For a new operator without OLM/OperatorHub requirements, Kubebuilder provides the cleanest scaffolding. Operator SDK adds OLM bundle generation on top of the same controller-runtime foundation. We can add Operator SDK later for OperatorHub distribution.
Why controller-runtime v0.25.2: PriorityQueue (default since v0.23.0) enables prioritizing
resize reconciliations for critical pods. Subresource Apply support enables clean SSA patches
to the /resize subresource. Generic Validator/Defaulter webhooks provide type-safe CRD
validation.
2.3 Prometheus Querying¶
| Component | Module | Version |
|---|---|---|
| Query client | github.com/prometheus/client_golang/api/prometheus/v1 |
v1.24.1 |
| Result types | github.com/prometheus/common/model |
transitive |
The official Prometheus Go client for querying (not exposing metrics). Returns typed results
(model.Vector, model.Matrix). Supports auth via custom http.RoundTripper.
2.4 Complete Dependency Table¶
go 1.27.2
# Core
sigs.k8s.io/controller-runtime v0.25.2
k8s.io/client-go v0.37.1
k8s.io/api v0.37.1
k8s.io/apimachinery v0.37.1
# Prometheus querying
github.com/prometheus/client_golang v1.24.1
# Testing
github.com/onsi/ginkgo/v2 latest
github.com/onsi/gomega latest
github.com/stretchr/testify latest
# Tools (CI/build, not Go module deps)
kubebuilder v4.14.0
golangci-lint v2.14.x
goreleaser v2.15.x
ko latest
cosign latest
trivy latest
chainsaw v0.2.15
ct (chart-testing) v3.14.x
crdoc v0.6.4
helm-docs latest
3. CRD Design¶
3.1 API Group and Version¶
Group: attune.io
Version: v1alpha1
3.2 AttunePolicy (Namespaced)¶
The primary CRD. Defines a right-sizing policy for a set of workloads.
apiVersion: attune.io/v1alpha1
kind: AttunePolicy
metadata:
name: api-services
namespace: production
spec:
# Which workloads to target
targetRef:
# Option A: specific workload
kind: Deployment # Deployment | StatefulSet | DaemonSet | CronJob | Job | ReplicaSet | Rollout
name: api-server # optional; omit to match by selector
# Option B: label selector (matches all matching workloads in namespace)
selector:
matchLabels:
tier: api
# Prometheus connection
metricsSource:
prometheus:
address: http://prometheus-server.monitoring:80
headers:
X-Scope-OrgID: tenant-a
queryParameters:
dedup: "true"
# Optional: auth and TLS settings. On AttunePolicy /
# AttuneNamespaceDefaults the Secret is in that namespace.
# Do not use bearerTokenSecret on cluster AttuneDefaults for a
# shared token (deprecated: name is still read in each policy
# namespace). Cluster-wide auth is the operator SA token or a
# Secret in the operator namespace (prometheusAuth).
# Datadog apiKeySecretRef on cluster AttuneDefaults has the same
# shape: use datadogAuth.existingSecret for a cluster-wide API key.
bearerTokenSecret:
name: prometheus-token
key: token
# Amazon Managed Prometheus replaces bearerTokenSecret with sigv4.
# Do not set both. The address is the workspace root. Attune appends
# /api/v1/query. region is required. roleArn is optional.
# sigv4:
# region: us-east-1
# roleArn: arn:aws:iam::123456789012:role/attune-amp
tls:
insecureSkipVerify: false
# How far back to look for usage patterns
historyWindow: 168h # default: 168h (7d), min: 1h, max: 720h
# Minimum Prometheus range-query samples before making recommendations
minimumDataPoints: 48 # default: 48 (~4h at the default queryStep: 5m)
queryStep: 5m # default: 5m, min: 10s, max: 1h
rateWindow: 5m # default: queryStep, min: 30s, max: historyWindow
# Per-resource configuration
cpu:
# Algorithm parameters
percentile: 95 # supported: 50, 90, 95, 99
overhead: "20" # default: 20 (20% headroom above percentile)
# Optional hard bounds
minAllowed: "50m"
maxAllowed: "4000m"
# Optional: control what is adjusted
controlledValues: RequestsAndLimits # RequestsOnly | RequestsAndLimits
# limitMultiplier stays off until set. Omitted keeps the live ratio.
# limitMultiplier: "2"
# Maximum change per reconciliation cycle
maxChangePercent: 50 # default: 50
# startupBoost: # optional CPU cold-start multiplier
# multiplier: "2.0"
# duration: 2m
# # excludeFromHistory: true drops CPU samples from
# # attune.io/startup-boost-at when that stamp is set and not
# # before creation, otherwise from CreationTimestamp, until that
# # start plus duration plus rateWindow. Omitted keeps today's
# # percentile. Deleted pods stay until historyWindow.
# # A recreated name keeps samples older than the new CreationTimestamp.
memory:
percentile: 99 # supported: 50, 90, 95, 99
overhead: "30" # default: 30 (30% headroom)
minAllowed: "64Mi"
maxAllowed: "8Gi"
controlledValues: RequestsAndLimits
# limitMultiplier stays off until set. Omitted keeps the live ratio.
# limitMultiplier: "1.5"
# Scaled memory limits are rounded up to a whole byte.
# Memory-specific safety
allowDecrease: false # default: false (OOM risk), set true only when confident
# Maximum change per reconciliation cycle
maxChangePercent: 30 # default: 30
# Optional per-container overrides. Omitted or empty keeps the shared
# cpu and memory engines. Not on AttuneDefaults or AttuneNamespaceDefaults.
# containerPolicies:
# - containerName: "*"
# cpu:
# maxAllowed: "300m"
# - containerName: sidecar
# cpu:
# maxAllowed: "200m"
# Rollout strategy
updateStrategy:
type: Recommend # Observe | Recommend | OneShot | Canary | Auto
# mode-specific config (for Canary and Auto):
canary:
percentage: 10 # % of pods to resize first
observationPeriod: 30m # example. Omit or 0s for 5m. Shortest accepted value is 1m.
# Cooldown between resizes of the same workload
cooldown: 1h # default: 1h, min: 1m; other apps are not locked
# Automatic revert on OOMKill, throttle, restarts, NotReady, or SLO breach
autoRevert: true # default: true
safetyObservationPeriod: 5m # observe pod post-resize (default: 5m, min: 1m)
# Opt-in: write recommendations into Deploy/STS pod templates so replacement
# pods start correctly sized (default off; avoid unmanaged GitOps thrash).
# templatePersistence:
# enabled: true
# when: AfterSuccessfulResize # or OnRecommendation
sloGuardrails: # optional: application-level SLO checks post-resize
- name: p99-latency
query: "histogram_quantile(0.99, rate(http_duration_seconds_bucket{namespace=\"{{ .Namespace }}\"}[5m]))"
threshold: "0.5"
comparison: above # revert if value > threshold
evaluationWindow: 5m # wait before checking (default: 5m, min: 1m)
# Priority/weight for conflict resolution
# When multiple policies match a workload, highest weight wins
weight: 100 # default: 100, range: 1-1000
status:
# Standard conditions
conditions:
- type: Ready
status: "True"
reason: Monitoring
message: "Watching 3 workloads, 12 pods"
lastTransitionTime: "2026-01-15T10:30:00Z"
observedGeneration: 2
- type: Resizing
status: "False"
reason: Idle
lastTransitionTime: "2026-01-15T10:30:00Z"
observedGeneration: 2
# Discovered workloads
workloads:
discovered: 3
withRecommendations: 3
resized: 2
pending: 1
# Recommendations summary
recommendations:
- workload: api-server
kind: Deployment
containers:
- name: api
current:
cpuRequest: "500m"
cpuLimit: "1000m"
memoryRequest: "512Mi"
memoryLimit: "1Gi"
recommended:
cpuRequest: "150m"
cpuLimit: "300m"
memoryRequest: "280Mi"
memoryLimit: "560Mi"
confidence: 0.92
dataPoints: 1680
lastUpdated: "2026-01-15T10:30:00Z"
# Savings estimate
savings:
cpuRequestReduction: "1050m" # total across all pods
memoryRequestReduction: "696Mi"
estimatedMonthlySavings: "$142.50" # if costModel is configured
# Resize history (last 50)
resizeHistory:
- timestamp: "2026-01-15T09:00:00Z"
workload: api-server
container: api
resource: cpu
from: "500m"
to: "150m"
method: InPlace
result: Success
- timestamp: "2026-01-15T09:05:00Z"
workload: worker
container: app
resource: cpu+memory
from: ""
to: ""
method: Eviction
result: Evicted
# Last GitOps notification PR (survives annotation wipe by Flux/Argo)
gitopsPR:
driftFingerprint: "sha256:..."
lastAttempt: "2026-01-15T09:00:00Z"
url: "https://github.com/org/repo/pull/41"
CRD Validation (Webhook)¶
Validation is implemented in the admission webhook (internal/webhook/validation.go),
not via CEL x-kubernetes-validations markers. The webhook enforces:
minAllowed <= maxAllowedfor both CPU and memory resource configs. On an AttunePolicy write, a min inherited from AttuneNamespaceDefaults or AttuneDefaults is included when the policy omits min and a max is already known on the policy or a container. A stored inverted pair is not rechecked until the next update. A stored min above a non-zero max is clamped to that max andboundsAppliedismaxcpu.maxAllowedmust not exceed 256 cores;memory.maxAllowedmust not exceed 16Ti (AttunePolicy and AttuneDefaults)- Canary config required when
updateStrategy.typeisCanary historyWindowbounded between 1h and 720h (30 days), including an unchanged stored value below 1hcooldown,safetyObservationPeriod, and an SLOevaluationWindowmust be at least 1m on create. An unchanged stored0sis accepted onAttunePolicy,AttuneDefaults, andAttuneNamespaceDefaults, including a finalizer clear. Changing a positive duration to0sis rejected. A stored cooldown of0sis not a wait: the controller uses 1h, including after defaults merge. A stored safety period of0sis unset (5m, or a positive canary period). A stored SLO window of0suses 5mburstSensitivitybounded between 0 and 10.0- A set
limitMultiplieron an AttunePolicy requirescontrolledValues: RequestsAndLimitson that same CPU or memory block. Omitted and empty modes are rejected.RequestsOnlywith a multiplier stays rejected by the earlier check. An empty multiplier stays unset, and"1"is not exempt. AttuneDefaults and AttuneNamespaceDefaults may still store a multiplier without the mode. A stored policy is not rewritten - All float fields (percentile, overhead, etc.) reject NaN and Inf
- Prometheus address SSRF protection (scheme, host, and IP validation)
containerPoliciesentries: duplicatecontainerName, a second*, and an emptycontainerNameare rejected.startupBoost,memoryFromCpuRatio,decreaseUsageMarginPercent,limitMultiplier,oomBump, andsurgeon a container entry are rejected because they stay policy-wide in v1.minAllowed <= maxAllowedstill applies to each containercpuandmemoryblock in the webhook. The CRD quantity rule stays onspec.cpuandspec.memory, because copying it onto each containerPolicies entry exceeds the API server CEL cost budget.
containerPolicies is a field-wise list on AttunePolicySpec only. A literal name beats * per field, and * beats the merged policy block. v1 honors percentile, overhead, minAllowed, maxAllowed, burstSensitivity, maxChangePercent, maxIncreasePercent, maxDecreasePercent, allowDecrease, and controlledValues. An omitted container maxAllowed inherits * and then the policy block, and is uncapped only when that effective value is nil. Known sidecars stay excluded by default. Normal init containers are not managed. Policy-wide startupBoost, memoryFromCpuRatio, decreaseUsageMarginPercent, limitMultiplier, oomBump, and surge still apply from the policy block to every non-excluded container, and a container max caps startup boost and a policy memory OOM bump. After the field-wise merge, minAllowed above maxAllowed is rejected, including a defaults min when the container omits min and a max is already known. A stored min above a non-zero max stays clamped to that max until the next update. Container maxAllowed uses the same 256-core and 16Ti ceilings. There is no new Prometheus metric.
At resize time, a Guaranteed memory limitMultiplier is not an admission check. The limit is the multiplier times the engine request, then the request is raised to that limit so the pod stays Guaranteed. The applied request can exceed memory.maxAllowed. maxAllowed still caps the engine request. That raise is not the in-hold oomBump clamp. A CPU multiplier that would leave Guaranteed is skipped before UpdateResize and is not evicted, including when resizeMethod is InPlaceOrRecreate. RequestsOnly does not apply the multiplier.
Printer Columns¶
// +kubebuilder:printcolumn:name="Type",type=string,JSONPath=`.spec.updateStrategy.type`
// +kubebuilder:printcolumn:name="Workloads",type=integer,JSONPath=`.status.workloads.discovered`
// +kubebuilder:printcolumn:name="Recs",type=integer,JSONPath=`.status.workloads.withRecommendations`
// +kubebuilder:printcolumn:name="Resized",type=integer,JSONPath=`.status.workloads.resized`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
// +kubebuilder:printcolumn:name="CPU Saved",type=string,JSONPath=`.status.savings.cpuRequestReduction`,priority=1
// +kubebuilder:printcolumn:name="Mem Saved",type=string,JSONPath=`.status.savings.memoryRequestReduction`,priority=1
$ kubectl get attunepolicies
NAME MODE WORKLOADS RECS RESIZED READY AGE
api-services Canary 3 3 2 True 7d
$ kubectl get attunepolicies -o wide
NAME MODE WORKLOADS RECS RESIZED READY AGE CPU SAVED MEM SAVED
api-services Canary 3 3 2 True 7d 1050m 696Mi
3.3 AttuneDefaults (Cluster-Scoped, Optional)¶
Global defaults to avoid repetition across many AttunePolicy resources.
containerPolicies is not a field of AttuneDefaultsSpec. AttuneNamespaceDefaults uses that same spec, so the list is not inherited.
3.4 AttuneNamespaceDefaults (Namespaced, Optional)¶
Namespace-scoped defaults reuse the same spec as AttuneDefaults but apply
only within one namespace. Precedence is per field: policy spec >
namespace defaults > cluster AttuneDefaults > built-in defaults.
Fields set on the namespace object win; fields left unset (including
costPricing.cpuPerCoreHour vs memoryPerGiBHour independently) inherit
from cluster AttuneDefaults, then built-ins.
apiVersion: attune.io/v1alpha1
kind: AttuneDefaults
metadata:
name: default
spec:
metricsSource:
prometheus:
address: http://prometheus-server.monitoring:80
historyWindow: 168h
minimumDataPoints: 48
cpu:
percentile: 95
overhead: "20"
controlledValues: RequestsAndLimits
memory:
percentile: 99
overhead: "30"
controlledValues: RequestsAndLimits
allowDecrease: false
updateStrategy:
type: Recommend
cooldown: 1h
autoRevert: true
3.5 Status Conditions¶
| Condition Type | Reasons | Description |
|---|---|---|
Ready |
Monitoring, InsufficientData, NoWorkloadsFound, MetricsUnavailable (alias PrometheusUnavailable), InvalidConfig, WorkloadDiscoveryFailed, WorkloadCRDMissing, ConflictCheckFailed, Paused, PrometheusSeriesCapped |
Overall health. PrometheusSeriesCapped keeps Ready True and means the query result was partial. WorkloadCRDMissing means a Rollout policy's CRD is not installed |
TemplatePersistence |
TemplateWorkloadRef, WorkloadRefUnread |
TemplateWorkloadRef is False when a Rollout spec.workloadRef was read. Attune does not patch that template. WorkloadRefUnread stays when that read failed. It is not replaced in the same reconcile |
Resizing |
InProgress, Idle, CooldownActive |
Active resize operation |
Degraded |
HighRevertRate |
Some resizes failing |
ScheduleBlocked |
OutsideWindow, InsideWindow |
Whether the current time is within the configured resize schedule window. An equal start and end never opens. |
ResizeBlocked |
NamespaceFrozen, HPAListUnavailable, VPAListUnavailable, PodsDeferred, PodsInfeasible, PodsDeferredAndInfeasible |
Namespace freeze, HPA or VPA list failure, or pods stuck Deferred or Infeasible |
SafetyObservation |
Observing, Evaluating, RestorePending, Incomplete |
Pods still carrying attune.io resize-tracking annotations |
Status conditions use meta.SetStatusCondition() from k8s.io/apimachinery/pkg/api/meta
(the Kyverno pattern) with observedGeneration on every condition.
4. Architecture¶
4.1 High-Level Components¶
┌──────────────────────────────────────────────────────────────────┐
│ attune │
│ │
│ ┌─────────────────────┐ ┌─────────────────────────┐ │
│ │ Policy Controller │ │ Metrics Collector │ │
│ │ ───────────────── │ │ ─────────────────── │ │
│ │ Reconciles │ │ Queries Prometheus │ │
│ │ AttunePolicy │◄──►│ Aggregates usage data │ │
│ │ CRs │ │ Builds time-of-day │ │
│ │ Discovers target │ │ profiles │ │
│ │ workloads │ │ Detects bursts │ │
│ └──────────┬──────────┘ └─────────────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ ┌─────────────────────────┐ │
│ │ Recommender Engine │ │ Resize Engine │ │
│ │ ───────────────── │ │ ─────────────────── │ │
│ │ Composable │ │ In-place via /resize │ │
│ │ estimator chain: │ │ subresource │ │
│ │ percentile -> │◄──►│ CPU first, then memory │ │
│ │ margin -> │ │ Poll for completion │ │
│ │ confidence -> │ │ Timeout cascade: │ │
│ │ bounds clamping │ │ Deferred/Infeasible │ │
│ └─────────────────────┘ └─────────────────────────┘ │
│ │
│ ┌─────────────────────┐ ┌─────────────────────────┐ │
│ │ Safety Monitor │ │ Status Reporter │ │
│ │ ───────────────── │ │ ─────────────────── │ │
│ │ Watches OOMKills │ │ Updates CRD status │ │
│ │ Detects CPU │ │ conditions │ │
│ │ throttle │◄──►│ Emits Prometheus │ │
│ │ Tracks restarts │ │ metrics │ │
│ │ Auto-reverts │ │ Sends notifications │ │
│ │ Blocks bad resizes │ │ Records history │ │
│ └─────────────────────┘ └─────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
4.2 Controller Reconciliation Loop¶
A single controller reconciles AttunePolicy resources. The reconcile function:
1. FETCH policy and resolve defaults: merge AttuneNamespaceDefaults (if present) over AttuneDefaults; unset namespace fields inherit from cluster, then built-ins
2. DISCOVER target workloads (by name or label selector)
3. For each workload:
a. CHECK for conflicting policies (highest weight wins)
b. QUERY Prometheus for historical usage data
c. VALIDATE data sufficiency (minimum data points)
d. COMPUTE recommendation via estimator chain
e. COMPARE recommendation to current resources
f. IF mode allows resize AND change exceeds threshold AND cooldown expired:
i. SELECT pods (all, canary %, or single)
ii. RESIZE pods via /resize subresource (CPU first, then memory)
iii. MONITOR resized pods for safety (OOM, throttle, restarts)
iv. REVERT if safety checks fail
g. UPDATE status (recommendations, savings, conditions, history)
4. REQUEUE after cooldown interval
4.3 Informer Configuration¶
| Resource | Cache | Purpose |
|---|---|---|
| AttunePolicy | Full | Primary reconciliation target |
| AttuneNamespaceDefaults | Full | Namespace defaults lookup |
| AttuneDefaults | Full | Cluster defaults lookup |
| Deployment | Metadata-only | Discover target workloads, read replicas |
| StatefulSet | Metadata-only | Discover target workloads |
| DaemonSet | Metadata-only | Discover target workloads |
| Pod | Full | Read current resources, status, conditions |
| HorizontalPodAutoscaler | Metadata-only | Detect HPA conflicts |
| Event | None (use watch) | Detect OOMKill events |
4.4 RBAC Requirements¶
# Pods: read + resize subresource
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/resize"]
verbs: ["update", "patch"]
# Workload controllers: read-only
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets", "daemonsets"]
verbs: ["get", "list", "watch"]
# Events: read (OOMKill detection) + create (operator events)
- apiGroups: ["events.k8s.io"]
resources: ["events"]
verbs: ["get", "list", "watch", "create", "patch"]
# HPA: read-only (conflict detection)
- apiGroups: ["autoscaling"]
resources: ["horizontalpodautoscalers"]
verbs: ["get", "list", "watch"]
# Own CRDs: full access
- apiGroups: ["attune.io"]
resources: ["attunepolicies", "attunepolicies/status"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: ["attune.io"]
resources: ["attunedefaults", "attunenamespacedefaults"]
verbs: ["get", "list", "watch"]
5. Algorithm Design¶
5.1 Composable Estimator Chain¶
Inspired by VPA's decorator pattern, but with critical improvements:
When cpu.startupBoost.excludeFromHistory is true, the Prometheus CPU
query is max by (pod, container) so each pod and container is one
series. The exclusion window starts at attune.io/startup-boost-at
when that stamp is set and not before creation, otherwise at
CreationTimestamp. CPU samples from that start until start plus duration
plus the rate window are dropped, then the remaining pod series are
reduced with podAggregation. A sample at the cutoff stays. Nil and
false keep today's percentile. Memory samples are unchanged. Deleted
pods stay until historyWindow. A recreated pod name keeps samples
older than the new CreationTimestamp. If that CPU matrix is larger
than the Prometheus series cap, Attune keeps one series per pod before
any pod gets a second series, and chooses the container with fewer
series already kept. A cap smaller than the container count leaves
some containers out.
Raw Prometheus Data
│
▼
┌──────────────────┐
│ Percentile │ Select P95 (CPU) or P99 (memory) from histogram
│ Estimator │ Using configurable percentile per policy
└──────┬───────────┘
│
▼
┌──────────────────┐
│ Overhead │ Add overhead percentage (default 20% CPU, 30% memory)
│ Estimator │ Ensures headroom above observed usage
└──────┬───────────┘
│
▼
┌──────────────────┐
│ Confidence │ Widen recommendation when data is sparse:
│ Multiplier │ result *= 1 + multiplier * (1 - confidence) ^ exponent
│ │ confidence = clamp(min(days, sqrt(points/24)) / 7, 0, 1)
│ │ Factor is 1.0 at confidence 1 and 2.0 at confidence 0
└──────┬───────────┘
│
▼
┌──────────────────┐
│ Bounds │ Clamp to user-defined min/max
│ Clamper │ Enforce QoS class preservation (requests <= limits)
└──────┬───────────┘
│
▼
┌──────────────────┐
│ Change │ Reject if change < threshold (prevent micro-adjustments)
│ Filter │ Reject if change > maxChangePercent (prevent shocks)
└──────┬───────────┘
│
▼
Final Recommendation
Each estimator is an interface:
type Estimator interface {
Estimate(usage UsageProfile, current resource.Quantity) resource.Quantity
}
This makes each stage independently testable and composable.
5.2 Prometheus Queries¶
# CPU usage (rate of CPU seconds consumed)
rate(container_cpu_usage_seconds_total{
namespace="$NAMESPACE",
pod=~"$POD_PREFIX.*",
container="$CONTAINER",
container!=""
}[$STEP])
# Memory usage (working set, excludes cache)
container_memory_working_set_bytes{
namespace="$NAMESPACE",
pod=~"$POD_PREFIX.*",
container="$CONTAINER",
container!=""
}
# CPU throttling (detect under-provisioning)
rate(container_cpu_cfs_throttled_periods_total{...}[$STEP])
/ rate(container_cpu_cfs_periods_total{...}[$STEP])
CloudWatch Container Insights uses the same workload, and matches
PodName rather than a PromQL pod selector. With prefer_full_pod_name
left false, that dimension is the Deployment name or the CronJob name.
The ReplicaSet hash and the Job stamp still match when the parent name
is absent. A sibling name does not match. prefer_full_pod_name: true
publishes the full pod name.
5.3 Time-of-Day Awareness¶
Instead of a single histogram over the entire history window, build 24 hourly profiles (optionally 168 for weekday/weekend distinction):
type UsageProfile struct {
// HourlyPercentiles[hour][percentile] = value
// hour: 0-23, percentile: p50, p90, p95, p99, max
HourlyPercentiles [24]PercentileSet
// Overall (used when insufficient hourly data)
OverallPercentiles PercentileSet
// Burst detection
BurstDetected bool
BurstMagnitude float64 // peak / p95 ratio
BurstDuration time.Duration
// Data quality
DataPoints int
TimeSpanDays float64
Confidence float64 // 0.0 - 1.0
}
The recommendation uses the maximum across all hourly profiles at the configured percentile, ensuring the recommendation covers the busiest hour of the day.
5.4 HPA Coexistence¶
When an HPA targets the same Deployment on CPU:
- Attune adjusts requests (the base resource allocation)
- HPA adjusts replica count based on utilization percentage of requests
- By right-sizing requests, HPA's percentage calculations become more accurate
To prevent conflicts:
- Detect HPA presence via informer
- If HPA targets CPU utilization, an omitted limitMultiplier keeps the
request-to-limit ratio. An explicit multiplier replaces that ratio and
stays off until set. When controlledValues is RequestsAndLimits, HPA
auto-tune caps the CPU utilization target using that multiplied limit,
which can sit above maxAllowed.
- If HPA targets custom metrics (not CPU/memory), no conflict exists
- Log a warning if both VPA and Attune target the same workload
Workload scale states (per workload, not a policy-wide condition):
- HPA ScaledToZero: matching HPA status.conditions[type=ScaledToZero]=True.
Classifier reads the condition string; it does not gate on cluster version.
- Manual zero: owner spec.replicas == 0 and no ScaledToZero. Never
status.replicas.
- Active: otherwise.
Idle (HPA ScaledToZero or manual zero) skips apply for that workload: no in-place resize, template persist, startup boost, or eviction. CREATE initial sizing does not look at owner replica count.
6. Resize Engine¶
6.1 Resize Flow¶
1. SELECT target pods based on update strategy mode:
- OneShot: one eligible needing pod per cycle
- Canary: canaryPercentage% of pods (round up to at least 1)
per app; CREATE sizing, startup boost, and HPA stay off until
that app's own watch promotes it (`status.canary.workloads`)
- Auto: canary first, then remaining after observation period
(observation `startTime` is the first successful in-place canary
resize, not the first skipped attempt; a revert clears the clock)
2. For each selected pod:
a. PRE-CHECK:
- Pod is Running and Ready
- Pod is not being deleted (DeletionTimestamp == nil)
- Pod is not owned by attune itself
- No active resize in progress (PodResizeInProgress condition)
- QoS class will be preserved after resize
- New values satisfy LimitRange constraints
b. RESIZE CPU (if needed):
- Patch via /resize subresource
- Poll status.containerStatuses[].resources until CPU matches
- Timeout: 60 seconds
- On failure: log, emit event, skip memory resize
c. RESIZE MEMORY (if needed):
- Patch via /resize subresource
- Poll status.containerStatuses[].resources until memory matches
- Timeout: 120 seconds (memory resize can be slower)
- On Infeasible: record, do not retry until spec changes
- On Deferred: record, retry on next reconciliation
d. POST-CHECK:
- Verify pod is still Running and Ready
- Start safety observation window
On Kubernetes 1.33 and earlier, a NotRequired memory limit decrease is
clamped to the current limit. Kubernetes 1.34 and newer allow that
decrease. Attune skips the platform clamp from 1.34 on. The usage floor
still raises a limit that would sit at or below recent usage plus
memory.decreaseUsageMarginPercent.
6.2 client-go Resize Pattern¶
func (r *ResizeEngine) ResizePod(ctx context.Context, pod *corev1.Pod,
container string, target corev1.ResourceRequirements) error {
updated := pod.DeepCopy()
for i := range updated.Spec.Containers {
if updated.Spec.Containers[i].Name == container {
updated.Spec.Containers[i].Resources = target
break
}
}
_, err := r.clientset.CoreV1().Pods(pod.Namespace).UpdateResize(
ctx, pod.Name, updated, metav1.UpdateOptions{},
)
return err
}
6.3 Resize Status Polling¶
func (r *ResizeEngine) WaitForResize(ctx context.Context, ns, podName,
container string, target corev1.ResourceRequirements, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
return wait.PollUntilContextCancel(ctx, 3*time.Second, true,
func(ctx context.Context) (done bool, err error) {
pod, err := r.clientset.CoreV1().Pods(ns).Get(ctx, podName, metav1.GetOptions{})
if err != nil {
return false, err
}
// Check for Infeasible (permanent failure)
for _, cond := range pod.Status.Conditions {
if string(cond.Type) == "PodResizePending" &&
cond.Status == corev1.ConditionTrue &&
cond.Reason == "Infeasible" {
return false, fmt.Errorf("resize infeasible: %s", cond.Message)
}
}
// Check if actual resources match target
for _, cs := range pod.Status.ContainerStatuses {
if cs.Name == container && cs.Resources != nil {
if quantitiesMatch(cs.Resources, target) {
return true, nil
}
}
}
return false, nil
})
}
6.4 Edge Cases¶
| Scenario | Handling |
|---|---|
| Pod deleted during resize | New pod uses workload template; with opt-in templatePersistence, template tracks recommended/applied sizes so replacements start correctly sized (default off) |
| Node has insufficient resources | Resize marked Deferred; retry on next reconciliation |
| QoS class would change | Pre-check rejects the resize |
| Guaranteed memory limitMultiplier | The request is raised to the multiplied limit and can exceed memory.maxAllowed. The pod stays Guaranteed. A CPU multiplier that would leave Guaranteed is skipped and is not evicted. RequestsOnly does not apply the multiplier |
| StatefulSet partition holds the old revision | Pods whose controller-revision-hash is not status.updateRevision stay skipped while currentRevision and updateRevision differ. A stale generation skips every pod. OnDelete pods are resized |
| LimitRange violation | API server rejects; log and skip |
| ResourceQuota exceeded | API server rejects; log and skip |
| Static CPU/Memory Manager | Infeasible for Guaranteed QoS pods; skip with warning |
| Multiple containers in pod | Resize each independently; any failure skips remaining |
| VPA also targeting workload | Detect via VPA informer; log conflict warning, defer to VPA |
7. Safety System¶
7.1 Graduated Rollout Modes¶
| Mode | Behavior | Risk Level |
|---|---|---|
Observe |
Collect metrics and track data-point progress; no recommendations surfaced | None |
Recommend |
Generate recommendations in status, no changes | None |
OneShot |
Resize one eligible needing pod per cycle | Low |
Canary |
Resize canary%, monitor, then remaining | Medium |
Auto |
Full automated canary-then-fleet | Medium-High |
7.2 Auto-Revert¶
When autoRevert: true (default), the Safety Monitor watches resized pods for:
- OOMKilled: Container terminated with reason OOMKilled within observation period
- CPU Throttle: CPU throttle ratio exceeds 50% post-resize. The threshold is fixed.
- Excessive Restarts: Container restart count increases by 2+ post-resize
- Pod Not Ready: Pod becomes NotReady within observation period
- SLO Guardrail Breach: Application-level PromQL query breached its threshold after
evaluationWindowelapsed (fails open on query errors). A window longer than the observation period keeps tracking until it elapses. The query then runs, including when that is after the observation period.
The observation period starts when the resize is submitted. It does not
finish while the kubelet still has the resize in progress or deferred
(PodResizePending with a reason other than Infeasible). If the period
elapses first, tracking stays, attune.io/resize-apply-pending is set
once, and the period starts again when the kubelet finishes. An infeasible
resize does not extend the period. An in-progress condition that has been
true for an hour does not extend the period and does not start a new one.
A resize that finishes during the original period does not gain a second
window.
On trigger:
1. Restore original resources via /resize subresource
2. Emit Kubernetes event on the Pod
3. Update AttunePolicy status with revert reason
4. Increment revert counter
5. Apply exponential backoff before retrying that workload (2x cooldown per revert)
7.3 Conflict Detection¶
Before any resize:
- Check for existing VPA targeting the same workload
- Check for existing HPA (adjust behavior, don't block)
- Check for other AttunePolicy with higher weight
- Check for attune.io/skip: "true" annotation on workload (opt-out)
- Check for attune.io/freeze: "true" on the policy namespace (skip apply
only: resize, eviction, startup boost, template persist, CREATE initial
sizing; recommendations still compute; pending safety observation still
reverts unsafe pods and restores AfterSuccessfulResize templates; fail
closed if the namespace cannot be read). Freeze is re-checked immediately
before apply so a flag set during PromQL still skips persist and resize.
- Check for active rollout on the parent Deployment (don't resize during rollouts)
7.4 Memory OOM bump (off until set)¶
Memory requests stay on the percentile path unless memory.oomBump is set. An empty oomBump: {} turns the feature on and fills ratio 1.2, minBump 100Mi, maxBumps 3, and hold 24h. CPU rejects the block. The step is max(ceil(origin * ratio^count), origin + minBump * count), then maxAllowed. Origin is the live memory request before the first bump of the streak, not the latest live request and not the pod template. When maxAllowed is omitted, maxBumps is the only cap. The count increments only after a successful resize. The streak is stored on attune.io/oom-bump.<container>. The name segment oom-bump.<container> must be at most 63 characters.
After hold expires, recommendations follow the normal percentile, allowDecrease, and template rules. Hold expiry does not clear the original request stored on the pod. A later OOM can step again from that same origin until maxBumps. During hold, a newer OOM whose next step from the frozen origin is not above the live request takes one step from that live request and keeps the origin. Auto, OneShot, and Canary record this oomAt and restart on the pod. The request stays clamped to maxAllowed. When maxAllowed is already at or below the live request, Auto, OneShot, and Canary consume the signal once as skipped and the count does not increase. Recommend and Observe do not write that stamp, so the same OOM still counts as skipped on every reconcile. Outside a hold, an OOM whose live request is already at or above maxAllowed does not raise the request. That signal is stored once and counted as clamped, including in Recommend and Observe. A newer finish time counts again. The hold stops a lower percentile from replacing the floor. It does not ignore a new OOM. During hold, the bump floor blocks a memory revert below the floor. A positive memory limit below the floor is raised only when that container's effective controlledValues is RequestsAndLimits. An empty containerPolicies list uses the policy memory block. A zero or missing limit is not created. oomBump stays on the policy block. An OOMKill verdict does not undo the bump. A non-OOM termination still reverts. A held OOM does not end observation. Throttle, NotReady, and SLO still run on that pass and still revert CPU, keeping memory at or above the floor. A Guaranteed pod with RequestsOnly is skipped. Attune does not evict to change QoS.
explanation.memory.finalAdjustment can include oomBump. The counter is attune_oom_bump_total with result applied, clamped, capped, or skipped. When maxAllowed drops below a held floor, the next recommendation publishes the cap and leaves the stored floor in place. Omitted maxAllowed does not add a cap. Default memory allowDecrease is false, so the highest in-hold pod request stays above the cap. That request is not the workload template. A replica still under the cap is raised only to the cap. The recommendation records the skip. A quiet reclamp counts clamped once per floor and cap per process. A new OOM step is clamped once in the step math and is not clamped again below maxAllowed.
7.5 Usage surge (off until set)¶
CPU and memory each stay on the long history window unless that resource's surge block is set. An empty surge: {} turns the feature on and fills trigger ratio 1.5, percentile 99, and window 30m. There is no surge: false. A policy that omits surge inherits an AttuneDefaults surge. To keep a workload off, omit surge on both.
The long statistic is the published percentile: the max of the overall percentile and the 24 hour-of-day percentiles, at the parent percentile. The short statistic is the overall percentile only, at surge.percentile, of finite samples inside surge.window. Attune does not take the max across hour-of-day buckets for the short statistic. It fires when the window actually shortened, the finite count is at least 3 and at least half of window / queryStep (or at least 3 when queryStep is 0), and either the long percentile is positive and short/long is at least triggerRatio, or the long percentile is 0 and the short percentile is positive. Otherwise the long profile and the parent percentile are used. minimumDataPoints gates the long window only. One sample at 1.5 times the long percentile does not fire.
When the short window is selected, confidence is copied from the long profile. Percentile and burst use the short profile. CPU and memory choose separately. memoryFromCpuRatio does not switch the memory sample set; derived memory follows the surged CPU request. explanation.<resource>.finalAdjustment can include surge on the resource that used the short window.
The webhook rejects a trigger ratio that is not finite, not greater than 1, or above 100 (100 is accepted). Empty trigger ratio is unset. Percentile must be 50, 90, 95, or 99. Window must be at least 5m and on a policy must not be longer than the effective history: metricsSource.historyWindow on the policy, then namespace defaults, then cluster defaults, then 168h. On AttuneDefaults or AttuneNamespaceDefaults, the limit is that object's own historyWindow, or 168h. A window equal to that limit is accepted. The surge check reads defaults only when the policy sets surge.window and omits historyWindow. The minimum-above-maximum check is a second read when the policy or a named container omits minAllowed and sets maxAllowed. Explicit 0s is invalid.
7.6 Memory HPA retune (annotation still required)¶
Auto-tune stays off unless the HPA annotation attune.io/auto-tune is "true". A successful in-place memory resize then retunes memory utilization targets with the same truncation as CPU. A Resource metric uses one pod's request sum: spec.containers plus init containers with restartPolicy Always, including unchanged containers and off-pod history rows. A one-shot init stays out of that sum, matching the Kubernetes Resource metric. CREATE initial sizing counts those same restartPolicy Always inits. A ContainerResource metric uses that container only, including a named init container. The limit cap matches that scope. Requests-only uses the live limit. No limit, or a limit at or below the new request, caps the percent at 100.
Pod Resource baselines are attune.io/original-target-memory and attune.io/original-memory-request. ContainerResource baselines share attune.io/original-container-memory, a JSON object keyed by container name. A second resize multiplies the stored original target by the stored request divided by the new request. Corrupt JSON is not rewritten. The percent for that cycle still uses this cycle's old and new request.
The first Resource retune also stores the containers in that sum as attune.io/original-cpu-request-containers or attune.io/original-memory-request-containers. The value is a sorted comma-separated list of DNS-1123 names. It includes spec.containers and init containers with restartPolicy: Always, including a container whose pre-resize request is zero. A one-shot init stays out. A later resize of a container already named on the list does not add that request to the stored quantity. A container that joins with a zero pre-resize request is added to the list without changing the quantity and without HPABaseRepaired. When the list is well formed, a later resize adds only a container that is in this cycle's pre-resize sum and missing from the list, at that container's pre-resize request. A listed container is not added again because the stored quantity equals the history-old sum. A listed name that is gone from the pod stays in the quantity. An empty value or a token that is not a DNS-1123 label is no list. A stored base with no list is replaced with this cycle's pre-resize pod sum when no counted history row grew, it is strictly below that sum, and it equals that sum with native sidecars removed or the history-old sum. That repair writes the list of the containers in the new sum. When a counted row grew and the stored quantity equals either of those sums, the quantity stays and the container list is recorded so a later shrink does not add those containers again. A stored value that matches neither sum stays. HPABaseRepaired is a Warning emitted after that HPA update succeeds, one event per rewritten annotation, and the message names the annotation. It is not emitted when the quantity stays. A failed update does not emit it. The repair is stored even when the published percent does not change. ContainerResource baselines are not repaired this way.
updateStrategy.hpaTargetBounds is optional. Nil or an empty object applies no user band and does not fill 50 or 90. CPU and memory bands are separate. When a side is set, Attune truncates, floors at 1, applies the limit cap, then max, then min, and publishes the limit cap again if min would exceed it. HPATargetClamped is a Normal event emitted after the HPA update succeeds, and only when the user band changes the post-limit percent. min and max are integers from 1 to 10000. max must be greater than or equal to min when both are set. AttuneDefaults may supply the block. There is no Helm value that turns the annotation or the band on.
7.7 Namespace authors¶
Cluster AttuneDefaults, operator flags, and Helm values are cluster trust. AttunePolicy and AttuneNamespaceDefaults are namespace trust. A namespace author does not receive operator Prometheus or Datadog credentials for an address they chose. Their VPA reference stays in their namespace unless cluster AttuneDefaults named it. Their sigv4.roleArn and cloudwatch.roleArn must match the operator allowlists, which are empty until set. A namespaced AssumeRole sends ExternalId attune:<namespace>. Their SLO guardrails run only on Prometheus, and when those queries would use operator credentials every vector selector is limited to the policy namespace. A scoped query that returns no samples does not revert and sets SLOGuardrailNoSamples. A rejected namespace metrics source still reverts an OOM or crash from a resize already applied, and it still expires a startup boost, without querying that source. A paused policy stays Paused. The breach event does not include the sample value. See Tenancy.
8. Metrics & Observability¶
8.1 Prometheus Metrics Exposed¶
All metrics use the attune_ prefix and are exposed on the operator's
metrics endpoint (default port 8080). The operator registers 26 metrics
across five categories:
| Category | Metrics | Examples |
|---|---|---|
| Recommendations | 3 gauges | attune_recommendation_cpu_cores, attune_recommendation_memory_bytes, attune_confidence |
| Resize operations | 4 counters + 1 histogram | attune_resize_total, attune_eviction_total, attune_resize_duration_seconds |
| Safety | 2 counters | attune_reverts_total, attune_throttle_deferred_total |
| Savings | 3 gauges | attune_savings_cpu_cores_total, attune_savings_memory_bytes_total, attune_savings_estimated_monthly_dollars |
| Data quality | 2 counters | attune_nan_inf_samples_total, attune_request_clamped_total |
| Operational guards | 4 counters + 1 gauge | attune_schedule_skipped_total, attune_budget_exhausted_total, attune_startup_boost_total, attune_burst_factor |
| Operator health | 2 counters + 2 histograms | attune_reconcile_errors_total, attune_reconcile_duration_seconds, attune_prometheus_query_duration_seconds |
| Webhooks | 1 counter + 1 histogram | attune_webhook_validation_total, attune_webhook_duration_seconds |
For the complete list with labels, descriptions, and query examples, see the Metrics Reference.
8.2 Kubernetes Events¶
| Event | Type | Reason | Message Example |
|---|---|---|---|
| Resize succeeded | Normal | Resized | "Resized cpu api-server/app: 500m -> 250m" |
| Resize failed | Warning | ResizeFailed | "Failed to resize pod api-server-abc12 container app: node has insufficient resources" |
| Resize skipped (QoS) | Warning | ResizeSkipped | "Resize blocked for pod X container Y: would change QoS class from Burstable to Guaranteed" |
| Resize skipped (envelope) | Warning | ResizeSkipped | "Resize blocked for pod X container Y: pod-level resource envelope would be exceeded" |
| Auto-revert triggered | Warning | Reverted | "Reverted resize on api-server/app: oomkill" |
8.3 Grafana Dashboard¶
Ship a pre-built Grafana dashboard JSON covering: - Savings overview (CPU/memory saved across cluster) - Per-namespace breakdown - Recommendation vs. actual usage over time - Resize success/failure rates - Revert rate and reasons - Confidence scores - Prometheus query latency
9. Testing Strategy¶
9.1 Test Pyramid¶
┌───────────┐
│ E2E │ Chainsaw: real cluster, full lifecycle
│ Tests │ 36 Chainsaw + 22 Go E2E scenarios
├───────────┤
│Integration│ envtest: real API server + etcd
│ Tests │ Controller reconciliation, CRD validation
├───────────┤
│ Unit │ Standard Go testing + testify
│ Tests │ Algorithm, estimators, resize logic
│ │ 1500+ test cases
└───────────┘
9.2 Unit Tests¶
Framework: Standard testing + github.com/stretchr/testify
What to unit test (table-driven tests): - Each estimator in the chain (percentile, margin, confidence, bounds, change filter) - UsageProfile construction from Prometheus data - Time-of-day profile aggregation - Burst detection algorithm - Confidence calculation - QoS class preservation check - HPA conflict detection logic - Resource quantity arithmetic (CPU millicore, memory byte conversions) - Resize patch construction - Status condition building
Coverage target: 80%+ on internal/ packages.
9.3 Integration Tests (envtest)¶
Framework: standard testing + github.com/stretchr/testify + controller-runtime/pkg/envtest
What to test:
- AttunePolicy CR creation, validation, defaulting
- AttuneNamespaceDefaults overrides set fields on cluster AttuneDefaults; unset namespace fields inherit from cluster, then built-ins
- AttuneDefaults merging with policy-level overrides
- Controller discovers workloads by name and by selector
- Controller handles workload updates (new pods, scale events)
- Controller resolves policy conflicts (highest weight wins)
- Status conditions are set correctly
- Status recommendations are populated
- CRD CEL validation rules reject invalid inputs
- Printer columns render correctly
- Finalizer cleanup on policy deletion
Test setup:
var _ = BeforeSuite(func() {
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "config", "crd", "bases"),
},
}
cfg, err := testEnv.Start()
Expect(err).NotTo(HaveOccurred())
// ... setup manager, controllers
})
Key pattern: Use a non-cached client for assertions to avoid stale reads:
// Bad: uses cached client, may see stale data
Expect(k8sClient.Get(ctx, key, &policy)).To(Succeed())
// Good: use a separate non-cached client for assertions
directClient, _ := client.New(cfg, client.Options{})
Eventually(func(g Gomega) {
g.Expect(directClient.Get(ctx, key, &policy)).To(Succeed())
g.Expect(policy.Status.Workloads.Discovered).To(Equal(3))
}).Should(Succeed())
9.4 E2E Tests (Chainsaw)¶
Framework: Kyverno Chainsaw v0.2.15
Test scenarios:
| # | Scenario | What It Validates |
|---|---|---|
| 1 | Install operator via Helm | Deployment runs, CRDs registered |
| 2 | Create AttunePolicy in Recommend mode | Recommendations appear in status |
| 3 | Create AttunePolicy in OneShot mode | Single pod resized, status updated |
| 4 | Canary rollout | canary% pods resized first |
| 5 | Auto-revert on OOMKill | Resize reverted after simulated OOM |
| 6 | HPA coexistence | No conflict, both operate correctly |
| 7 | Policy conflict resolution | Highest weight policy wins |
| 8 | Opt-out annotation | Workload with skip annotation is ignored |
| 9 | Insufficient data | Policy reports InsufficientData condition |
| 10 | Upgrade operator version | CRDs migrated, no downtime |
Test cluster: CI uses k3d, not Kind. The push/PR E2E job runs a single K3S version (v1.36.4-k3s1), and e2e-nightly.yaml runs Kubernetes v1.32–v1.36 as required cells plus v1.37 as experimental (k3s prerelease image). 1.32 stays so the alpha InPlacePodVerticalScaling feature-gate path keeps running. 1.38 is added when k3s or kindest/node publishes an image. Prometheus is installed in-cluster from the Helm chart and cert-manager is bootstrapped before the operator tests run.
9.5 Fuzz Tests¶
Framework: Go native fuzzing (go test -fuzz)
What to fuzz: - CRD validation functions (malformed resource quantities, empty strings, boundary values) - Prometheus query response parsing (malformed JSON, NaN values, empty vectors) - Estimator chain with extreme inputs (zero usage, max int64, negative values) - Resize patch construction with edge-case resource values
func FuzzEstimatorChain(f *testing.F) {
f.Add(float64(0.1), float64(1.0), 95, 1.2)
f.Fuzz(func(t *testing.T, usage, current float64, percentile int, margin float64) {
if percentile < 50 || percentile > 99 || margin < 1.0 || margin > 5.0 {
t.Skip()
}
// Ensure estimator never panics, always returns positive value
result := chain.Estimate(usage, current, percentile, margin)
if result.IsZero() || result.Cmp(resource.Quantity{}) < 0 {
t.Errorf("estimator returned non-positive: %v", result)
}
})
}
9.6 Benchmark Tests¶
Framework: Standard Go benchmarks (testing.B)
What to benchmark: - Prometheus response parsing (1K, 10K, 100K data points) - Percentile calculation on large datasets - Estimator chain execution - Resize patch construction - Status update serialization
func BenchmarkPercentileCalculation(b *testing.B) {
data := generateSamples(100000)
b.ResetTimer()
for b.Loop() {
calculatePercentile(data, 95)
}
}
9.7 Conformance Tests¶
Validate compatibility with Kubernetes API conventions:
- CRD structural schema validation passes kubectl apply --dry-run=server
- Status subresource works correctly
- Printer columns render
- Short names work (kubectl get ap)
- Scale subresource (if applicable)
10. CI/CD Pipeline¶
10.1 GitHub Actions Workflows¶
ci.yaml - Continuous Integration (PRs, merge_group, and dispatch)¶
Product tests do not run again on push to main. The branch ruleset
requires an up-to-date PR, so a squash of a green PR is the same tree.
push to main still runs cheap promote jobs (release-please, Docs
deploy, Scorecard). Curated GitHub Release notes live on
release-note-<semver> (or Actions vars), not on main. The Release
job applies them and deletes the notes branch. Version-bump
release-please* PRs skip Helm/Docs/YAML lint and CodeQL analyze
(required CodeQL (go) / CodeQL (actions) still report a stand-in).
Jobs:
changes:
- dorny/paths-filter classifies Go, Helm, YAML, and docs changes
- Downstream jobs skip irrelevant work on docs-only or YAML-only diffs
lint:
- golangci-lint v2.14.x (with .golangci.yml config)
- `go mod tidy` cleanliness check
- License boilerplate verification
- Documentation defaults / dashboard metrics / tool-version consistency checks
docs-check:
- mkdocs build via `make docs-build`
- Helm README freshness via `make helm-docs-check`
- Supported tool version reference checks
yaml-lint:
- yamllint for `config/` and Helm values/chart metadata
test-unit:
- gotestsum over `./api/... ./cmd/... ./internal/... ./pkg/...`
- race-enabled coverage run
- Upload JUnit results and Codecov coverage
- Fail if coverage < 80%
test-fuzz-bench:
- targeted Go fuzz runs for recommendation logic
- benchmark run for `./internal/...`
test-integration:
- setup-envtest for Kubernetes 1.35 assets
- gotestsum over `./test/integration/...` with `-tags=integration`
test-e2e:
- Create a k3d cluster for the current default K3S image
- Install cert-manager and Prometheus in-cluster
- Build and load the operator image
- Run Chainsaw and Go E2E suites
- Collect cluster debug info on failure
crd-freshness:
- Run `make manifests generate`
- Fail if CRDs, RBAC, Helm CRDs, or deepcopy output drift
helm-lint:
- helm lint and template validation for chart CI values
- helm-unittest
- Helm README freshness check
- Helm RBAC parity check
build:
- Build manager and kubectl plugin binaries
- Build the container image locally (no push)
e2e-nightly.yaml - Full nightly E2E matrix (scheduled + manual)¶
Jobs:
prepare-matrix:
- Expands the selected Kubernetes version input (`v1.32`–`v1.37`, or all)
- Selects the requested suite (`chainsaw`, `go-e2e`, or all)
test-e2e:
- Runs the full k3d/K3S E2E flow per selected version
- Uses isolated cluster names and kubeconfig paths per matrix entry
- Uploads per-version logs and debug artifacts
report:
- Fails when prepare-matrix, E2E, or fuzz fails, or when one of those
jobs is cancelled or skipped while another succeeded. A fully
cancelled run does not fail Nightly Results and does not change
the open failure issue. A scheduled 1.37 (experimental) failure
does not fail Nightly Results.
- On schedule, a red run opens or updates an assigned issue labeled
e2e-nightly-failure and ready. The assignee is the
NIGHTLY_FAILURE_ASSIGNEE repository variable, or SebTardif when
that variable is empty or is the org login. The same UTC day and
signature updates the issue. The signature names failed or
cancelled jobs. A skipped job is named only when no other job
failed. A later UTC day closes the open issue and opens a new one
whose title includes the consecutive-day count, counted from the
first failed UTC day. A green scheduled run closes the open
failure issue. workflow_dispatch does not open or close that issue.
release.yaml - Release (push to main and workflow_dispatch)¶
A v* tag push does not start this workflow. No workflow under
.github/workflows/ starts on push.tags. release-please runs on every
push to main. When a release pull request is merged, that same run
creates the GitHub Release and the tag, then the publish jobs run
because release_created is true. A tag created by Actions would not
start a separate workflow. Manual re-release is workflow_dispatch
with inputs.tag, or a dispatch whose ref is already a tag. Only the
release job uses the release environment. When inputs.tag is set,
the image rebuild, Helm publish, OperatorHub pull request, and Docker
Hub README are skipped.
Jobs:
release-please:
- Runs on every push to main, and on workflow_dispatch when inputs.tag is empty
- Opens or updates the release pull request
- On a merged release, creates the GitHub Release and tag and sets release_created
release:
- Runs when release_created is true, or on a manual re-release
- Waits on the release environment
- GoReleaser publishes binaries and release artifacts
- docker/build-push-action pushes multi-arch images to GHCR and Docker Hub when the Docker build is enabled
- cosign signs the GHCR image and docker.io/attuneio/attune
- anchore/sbom-action writes the SPDX SBOM
- Trivy scans the released image
- Attaches the install manifest, CRDs, and SBOM to the GitHub release
- A re-release (inputs.tag set) skips the Docker build, image signing, SBOM, Trivy scan, and manifest attach
helm-release:
- Runs after release succeeds, and only when inputs.tag is empty
- Packages and pushes the Helm chart to GHCR OCI
- Copies the chart to Docker Hub with oras
- Signs the GHCR chart and the Docker Hub chart with cosign
- Pushes Artifact Hub metadata
operatorhub-pr:
- Runs after release succeeds, and only when inputs.tag is empty
- Opens OperatorHub bundle pull requests
- The bundle step continues on error so a catalog miss does not fail the release
dockerhub-readme:
- Runs after release succeeds, and only when inputs.tag is empty
- Updates the Docker Hub description from docker/README.md
release-notify:
- Runs when release succeeded
- Opens an issue if the OperatorHub step or the Helm job failed
bench-baseline.yaml - Shared bench cache (push to main, path-filtered)¶
Writes the default-branch Actions cache that PR test-bench jobs restore.
PR jobs cannot update that cache scope.
security.yaml - Security Scanning (on pull request, merge queue, weekly schedule, and manual dispatch)¶
Jobs:
codeql:
- CodeQL for Go and GitHub Actions, queries security-and-quality
- A release-please pull request skips analysis and reports a stand-in
govulncheck:
- govulncheck ./...
- Skipped on a release-please pull request
trivy:
- Trivy filesystem scan with self-hosted Docker credential-store workaround
- Skipped on a release-please pull request
trivy-image:
- Build the operator image with `docker buildx build --load`, then `docker save`
- Trivy image scan of that tarball
- Skipped on a release-please pull request
gitleaks:
- Full-repo secret scan with `fetch-depth: 0`
- Also runs on a release-please pull request
dependency-review:
- dependency-review-action, fail-on-severity high
- Pull requests only
Notes:
- None of these jobs run on a push to main.
- CI also runs govulncheck, only when Detect Changes marks `go`
(or on a manual CI dispatch). That CI job is not this workflow.
docs.yaml - Documentation build and Pages deploy (docs pushes to main + manual)¶
Jobs:
build:
- mkdocs build via `make docs-build`
- Upload the built site as a Pages artifact
- Path filter: `docs/**`, `mkdocs.yml`, `Makefile`, and this workflow
deploy:
- Deploy that artifact to GitHub Pages
dependabot-auto-merge.yaml - Dependabot merge automation¶
Jobs:
auto-merge (pull_request_target, Dependabot only):
- Approves with GITHUB_TOKEN; enables squash auto-merge with the App token
- All semver types; CI is the safety gate
- Docker PRs: copy verify-go-version-sync.sh to /tmp, --write --root workspace
rebase-outdated (push to main):
- App-token git rebase + force-with-lease on Dependabot PRs with mergeable_state=behind
- Does not comment @dependabot rebase (Apps are rejected)
10.2 CI Configuration Files¶
.golangci.yml (key linters):
version: "2"
linters:
enable:
- importas # Enforce corev1, metav1 aliases
- forbidigo # Ban fmt.Printf, context.Background() in controllers
- ginkgolinter # Catch Ginkgo/Gomega anti-patterns
- errorlint # errors.Is/errors.As enforcement
- revive # Style
- staticcheck # Advanced analysis
- bodyclose # HTTP response body leak prevention
- nilerr # Nil error return detection
- govet # Vet checks
- unused # Dead code
- gosec # Security
settings:
importas:
alias:
- pkg: k8s.io/api/core/v1
alias: corev1
- pkg: k8s.io/apimachinery/pkg/apis/meta/v1
alias: metav1
- pkg: k8s.io/apimachinery/pkg/api/errors
alias: apierrors
forbidigo:
forbid:
- pattern: ^fmt\.Print
msg: "Use structured logging (slog or logr)"
- pattern: ^context\.Background
msg: "Use the context passed to Reconcile"
10.3 Branch Protection¶
main branch:
- Require PR reviews (1 reviewer)
- Require status checks: lint, test-unit, test-integration, crd-freshness, helm-lint, build
- Require up-to-date branches
- No force push
- No deletion
11. Distribution¶
11.1 Helm Chart¶
Primary installation method. Structure:
charts/attune/
├── Chart.yaml
├── values.yaml
├── values.schema.json
├── README.md # Auto-generated by helm-docs
├── templates/
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── serviceaccount.yaml
│ ├── clusterrole.yaml
│ ├── clusterrolebinding.yaml
│ ├── service.yaml # Webhook service
│ ├── certificate.yaml # Webhook TLS (cert-manager or self-signed)
│ └── tests/
│ └── test-connection.yaml
└── ci/
├── default-values.yaml
├── ha-values.yaml
└── minimal-values.yaml
Key values.yaml fields:
- replicaCount (default: 1, HA: 2 with leader election)
- image.repository, image.tag (empty tag renders appVersion, bare SemVer)
- resources (operator pod resources)
- metrics.enabled (expose /metrics)
- securityContext (non-root, read-only root filesystem, drop all capabilities)
11.2 OCI Registry¶
# Push Helm chart
helm push attune-0.1.0.tgz oci://ghcr.io/attune-io/charts
# Install from OCI
helm install attune oci://ghcr.io/attune-io/charts/attune --version 0.1.0
11.3 kubectl Plugin¶
Distributed via Krew:
kubectl krew install attune
kubectl attune doctor
kubectl attune status -n production
kubectl attune savings
kubectl attune recommendations -n production
11.4 Raw Manifests¶
For users who don't use Helm:
kubectl apply -f https://github.com/attune-io/attune/releases/latest/download/install.yaml
12. Documentation¶
12.1 Documentation Site¶
Framework: MkDocs + Material for MkDocs
Structure:
docs/
├── index.md # Overview, elevator pitch
├── getting-started/
│ ├── installation.md # Helm, raw manifests, prerequisites
│ ├── quickstart.md # 5-minute first policy
│ └── concepts.md # CRDs, modes, algorithm overview
├── guides/
│ ├── recommend-mode.md # Safe first step
│ ├── canary-rollout.md # Production right-sizing
│ ├── hpa-coexistence.md # Using with HPA
│ ├── gitops-integration.md # Flux, ArgoCD compatibility
│ ├── migrating-from-vpa.md # Step-by-step VPA replacement
│ └── troubleshooting.md # Common issues, debug steps
├── reference/
│ ├── api.md # Auto-generated CRD reference
│ ├── metrics.md # Prometheus metrics reference
│ ├── configuration.md # Helm values reference
│ └── cli.md # kubectl plugin reference
├── architecture/
│ ├── design.md # Architecture overview
│ ├── version-best.md # Version-best Kubernetes (1.32+)
│ ├── algorithm.md # Estimator chain details
│ ├── safety.md # Safety system design
│ └── resize-api.md # K8s In-Place Resize reference
└── contributing/
├── development.md # Local dev setup
├── testing.md # Running tests
└── releasing.md # Release process
12.2 README.md¶
Must include: - One-sentence description - Architecture diagram - 5-minute quickstart - Feature comparison table (vs VPA, Goldilocks) - CRD example - Link to docs site - Badges (CI, Go version, License, CNCF if applicable) - ADOPTERS.md link
12.3 ADOPTERS.md¶
Create from day one (even if empty). CloudNativePG's format:
# Adopters
If you are using attune in your organization, please add your
company to this list. It helps the project understand its user base
and prioritize features.
| Organization | Contact | Date | Description |
|-------------|---------|------|-------------|
13. Project Structure¶
attune/
├── .github/
│ ├── workflows/
│ │ ├── ci.yaml
│ │ ├── bench-baseline.yaml
│ │ ├── release.yaml
│ │ ├── security.yaml
│ │ └── docs.yaml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── dependabot.yml
├── api/
│ └── v1alpha1/
│ ├── groupversion_info.go
│ ├── attunepolicy_types.go
│ ├── attunepolicy_types_test.go
│ ├── attunedefaults_types.go
│ ├── conditions.go
│ ├── zz_generated.deepcopy.go
│ └── doc.go
├── cmd/
│ ├── manager/
│ │ └── main.go # Operator entry point
│ └── kubectl-attune/
│ └── main.go # kubectl plugin
├── internal/
│ ├── conflict/
│ │ ├── detector.go # VPA, HPA, policy conflict detection
│ │ └── detector_test.go
│ ├── controller/ # Reconciler (core business logic)
│ │ ├── attunepolicy_controller.go
│ │ ├── attunepolicy_controller_test.go
│ │ └── ... # helpers, resize, prometheus, surge, export, etc.
│ ├── metrics/
│ │ ├── collector.go # Prometheus/Datadog/CloudWatch query client
│ │ ├── collector_test.go
│ │ ├── profile.go # UsageProfile construction
│ │ └── profile_test.go
│ ├── operatormetrics/ # Operator-level Prometheus metrics (init-registered)
│ │ └── metrics.go
│ ├── recommendation/
│ │ ├── estimator.go # Estimator interface
│ │ ├── percentile.go # Percentile estimator
│ │ ├── margin.go # Safety margin estimator
│ │ ├── confidence.go # Confidence multiplier
│ │ ├── bounds.go # Bounds clamper
│ │ ├── chain.go # Composable chain
│ │ └── fuzz_test.go
│ ├── resize/
│ │ ├── engine.go # Pod resize via /resize subresource
│ │ └── engine_test.go
│ ├── safety/
│ │ ├── monitor.go # OOMKill, throttle, restart, NotReady, SLO guardrails, auto-revert
│ │ └── monitor_test.go
│ ├── throttle/ # Shared throttle checker interface
│ ├── transform/ # Informer cache transform functions
│ ├── validation/ # Shared validation (Prometheus SSRF checks)
│ └── webhook/
│ ├── defaulting.go # Defaulting webhook
│ ├── validation.go # Validation webhook
│ └── defaults_validation.go # AttuneDefaults validation
├── config/
│ ├── crd/
│ │ └── bases/ # Generated CRD manifests
│ ├── rbac/
│ │ ├── role.yaml
│ │ └── role_binding.yaml
│ ├── manager/
│ │ └── manager.yaml
│ ├── webhook/
│ └── samples/
│ ├── recommend-mode.yaml
│ ├── canary-mode.yaml
│ └── defaults.yaml
├── charts/
│ └── attune/
│ ├── Chart.yaml
│ ├── values.yaml
│ ├── values.schema.json
│ └── templates/
├── test/
│ ├── e2e/ # Chainsaw test cases
│ │ ├── install/
│ │ ├── recommend-mode/
│ │ ├── canary-rollout/
│ │ ├── auto-revert/
│ │ └── hpa-coexistence/
│ └── integration/ # envtest-based tests
├── docs/ # MkDocs site
├── hack/ # Development scripts
│ ├── setup-envtest.sh
│ └── update-codegen.sh
├── .golangci.yml
├── .goreleaser.yaml
├── .ko.yaml
├── Makefile
├── Dockerfile # Fallback (ko is primary)
├── go.mod
├── go.sum
├── LICENSE # Apache 2.0
├── README.md
├── ADOPTERS.md
├── CONTRIBUTING.md
├── CHANGELOG.md
└── SECURITY.md
14. Roadmap¶
Phase 1: Foundation (MVP)¶
- [x] Project scaffolding (Kubebuilder)
- [x] AttunePolicy CRD (v1alpha1)
- [x] Prometheus metrics collector
- [x] Percentile-based recommendation engine
- [x] Status reporting (recommendations, conditions)
- [x] Observe and Recommend modes only (no resize)
- [x] Helm chart
- [x] Unit tests (75%+ coverage)
- [x] envtest integration tests
- [x] CI pipeline (lint, test, build)
- [x] README with quickstart
Phase 2: Resize Engine¶
- [x] In-place resize via /resize subresource
- [x] OneShot mode
- [x] Canary mode with graduated rollout
- [x] Resize status polling and timeout handling
- [x] QoS preservation checks
- [x] LimitRange/ResourceQuota compatibility
- [x] E2E tests (Chainsaw)
- [x] Security scanning in CI
Phase 3: Safety & Intelligence¶
- [x] Safety monitor (OOMKill, throttle, restart, NotReady, SLO guardrails)
- [x] Auto-revert mechanism
- [x] Confidence-based recommendation widening
- [x] Time-of-day-aware algorithm
- [x] Burst detection
- [x] HPA coexistence logic
- [x] VPA conflict detection
- [x] Policy weight-based conflict resolution
Phase 4: Production Readiness¶
- [x] Auto mode (canary then fleet)
- [x] AttuneDefaults / AttuneNamespaceDefaults
- [x] Grafana dashboard
- [x] MkDocs documentation site
- [x] Cosign image signing
- [x] SBOM generation
- [x] Release automation (GoReleaser)
- [x] OCI Helm chart distribution
- [x] Fuzz tests
- [x] Benchmark tests
Phase 5: Ecosystem¶
- [x] kubectl plugin (via krew)
- [x] Datadog/CloudWatch metrics support
- [x] Memory decrease support (with gradual decrease)
- [x] Multi-cluster aggregated reporting (Phase A/B: federation docs, fleet Grafana dashboard, recording rules, fleet report ConfigMap + collect script; hub control plane deferred)
- [ ] CNCF Sandbox application
- [ ] KubeCon talk proposal
- [ ] ADOPTERS.md with real organizations
15. Competitor Lessons¶
Patterns Adopted¶
| Pattern | Source | How We Use It |
|---|---|---|
| Mandatory resource bounds | OptiPod | minAllowed/maxAllowed fields |
| Weight-based policy resolution | OptiPod | weight field for deterministic conflict resolution |
| Gradual memory decrease | OptiPod | memory.maxChangePercent + allowDecrease flag |
| Composable estimator chain | VPA | Decorator pattern: percentile -> overhead -> confidence -> bounds |
| Confidence-based widening | VPA | 1 + multiplier * (1 - confidence) ^ exponent |
| Two-phase resize (CPU then memory) | right-sizer | CPU first (safer), then memory, with proper polling |
| Conditions via meta.SetStatusCondition | Kyverno | Standard library helper, not hand-rolled |
| Print columns with priority | Kyverno | -o wide shows savings columns |
| Strict CI shell defaults | CloudNativePG | bash -Eeuo pipefail -x {0} in all workflows |
| ADOPTERS.md from day one | CloudNativePG | Social proof drives adoption |
| envtest + property-based testing | OptiPod | Fast feedback + invariant testing |
| Percentage overhead (not multiplier) | CAST AI, KRR, VPA | overhead: "20" = +20% headroom (ecosystem consensus) |
minAllowed/maxAllowed naming |
VPA | Direct match with VPA containerPolicies field names |
controlledValues field |
VPA | Direct match with VPA (RequestsOnly / RequestsAndLimits) |
| Hierarchical defaults CRD | PerfectScale | Cluster > namespace > policy precedence (3-tier) |
| Per-step change cap in ResourceConfig | StormForge | maxChangePercent per resource (StormForge uses maxPercentIncrease/maxPercentDecrease) |
| Preview/Apply progression | Datadog | Our Observe > Recommend > Canary > Auto mirrors Datadog's Preview > Apply |
| Unified vertical CRD (not VPA+HPA) | Datadog | Single AttunePolicy instead of separate VPA + HPA objects |
| Cron-style scheduling | Oblik | schedule.windows + daysOfWeek (Oblik uses cron + cronAddRandomMax) |
| Annotation-based opt-out | CAST AI, Oblik | attune.io/skip: "true" for workload exclusion |
| Namespace freeze kill-switch | Kilter | attune.io/freeze: "true" on the namespace skips apply (resize, persist, CREATE initial sizing); pending safety revert still runs |
Anti-Patterns Avoided¶
| Anti-Pattern | Source | Why We Avoid It |
|---|---|---|
| Bloated CRD (15+ config sections) | right-sizer | Focused CRD + separate defaults CRD |
| Emoji logging / fmt.Printf | right-sizer, OptiPod | Structured logging only (logr) |
| Hardcoded time.Sleep between operations | right-sizer | Proper polling via wait.PollUntilContextCancel |
| No CRD (annotation-only) | kube-reqsizer, Oblik | Full CRD with proper status (Oblik supports both but annotations are fragile at scale) |
| Manual memory string parsing | kube-reqsizer | Always use resource.Quantity |
| Status Phase as bare string | right-sizer | Typed constants with kubebuilder enum validation |
| ObservedGeneration via annotations | right-sizer | Proper status subresource field |
| All containers resized together | VPA | Per-container independent resize |
| HPA conflict undefined | VPA | Detect and handle HPA coexistence |
| SaaS-only with no self-hosted option | CAST AI, PerfectScale, Sedai, nOps | Fully self-contained operator, metrics stay in-cluster |
| Black-box ML recommender | StormForge, Sedai, ScaleOps | Transparent percentile + overhead + confidence chain; every step visible in explanation |
| Combined horizontal + vertical in one field | VPA (updateMode) |
Separate type (what to do) and resizeMethod (how to apply) for clarity |
| Platform API instead of CRD | Sedai, Densify | Kubernetes-native CRD; works with GitOps, kubectl, and standard tooling |
| Multiplier-based overhead (1.2x) | kube-reqsizer | Percentage-based overhead ("20" = +20%), matching ecosystem consensus |
Competitor Landscape (16 tools surveyed)¶
| Category | Tools | Key takeaway |
|---|---|---|
| OSS recommenders | VPA, Goldilocks, KRR, Kubecost/OpenCost | Good for visibility and one-time audits; no autonomous application (except VPA Auto, which evicts) |
| OSS appliers | Oblik, kube-reqsizer, Kedify, k8s-sustain, CruiseKube | In-place /resize or cron apply; k8s-sustain adds KEDA/Argo and a dashboard; none match Attune canary + SLO revert |
| Commercial full-stack | CAST AI, ScaleOps, StormForge, PerfectScale, Sedai, Densify | Pod + node optimization with ML; $10k-50k+/year; SaaS dependency (except ScaleOps self-hosted) |
| Observability-integrated | Datadog, nOps, Spot Ocean | Leverage existing monitoring; Datadog's DatadogPodAutoscaler CRD is well-designed |
| Attune | (this project) | Focused on in-place resize with safety; open-source; no SaaS; Kubernetes-native CRDs |