OKE released Kubernetes 1.37.0 as a preview on 8 October 2026. The release note carries a line that is easy to read past: from 1.37, a Service of type: LoadBalancer with no shape annotation is provisioned as a flexible load balancer with a minimum bandwidth of 100 Mbps and a maximum bandwidth of 100 Mbps.
The number is not new. Before 1.37, the default for an unannotated Service was a fixed-shape load balancer at 100Mbps. What changed is the word. A team that sees “flexible” in the console will assume the load balancer scales, and with the minimum equal to the maximum it has nowhere to go. The same release enforces a TLS rule that earlier versions only warned about, and it arrives while two support windows are closing. All three need handling before the first cluster moves.
What 1.37 changes, and what it leaves alone
| Before 1.37 | From 1.37 | |
|---|---|---|
| LoadBalancer Service with no shape annotation | Fixed-shape load balancer, 100Mbps | Flexible load balancer, minimum 100 Mbps, maximum 100 Mbps |
| Existing load balancers | Unchanged | Not converted or resized |
| Explicit shape annotations | Applied | Still take precedence |
| TLS secret in another namespace | Warning | Rejected before the secret is read. The Service is not reconciled until the reference is fixed |
| Release status | 1.36.4, 1.35.2 and 1.34.10 supported | 1.37.0 is a preview, OC1 realm only. Oracle recommends waiting for 1.37.1 for production |
Two details make the default matter more than it looks. A load balancer shape is the maximum total bandwidth, ingress plus egress, so a 100 Mbps shape is a 100 Mbps budget for both directions. And “existing load balancers are not converted” protects the load balancers that exist today, not the Services that create them. Delete and recreate a Service, or rebuild a cluster for a new environment, and every unannotated Service gets the new default on the way back up.
Two clocks inside a month
1.37 arrives on top of two deadlines. With 1.36.4 released on 1 October, OKE stops supporting 1.36.1 on 3 November 2026. Separately, 1.34 stays supported only until 30 days after OKE announces 1.37.1, and 1.37.1 has no date yet. A fleet that runs a mix of 1.34 and 1.36.1 therefore has one fixed date and one floating one, and the floating one will be set by a release note nobody on the team controls.

The upgrade rules limit the plan. Control plane nodes move one minor version at a time. Worker nodes can skip versions, and the skew policy lets the control plane run up to three minor versions ahead of them: with the control plane on 1.37.0, workers may sit on 1.37.0, 1.36.4, 1.35.2 or 1.34.10. A control plane upgrade cannot be reversed. Since 14 August, managed node pools can be rolled back to an earlier available version, so the node layer is the only place a mistake can be undone.
Step one: find what 1.37 will touch
Before anything moves, we list two kinds of Service on every cluster: LoadBalancer Services of the load balancer type with no shape annotation, and Services whose TLS annotations name a secret in a different namespace. Network load balancers are skipped, because they have no bandwidth shape.
# bash: Services that 1.37 provisions or reconciles differently
kubectl get svc -A -o json | jq -r '
.items[]
| select(.spec.type == "LoadBalancer")
| .metadata as $m
| ($m.annotations // {}) as $a
| select(($a["oci.oraclecloud.com/load-balancer-type"] // "lb") == "lb")
| ($a["service.beta.kubernetes.io/oci-load-balancer-shape"] // "") as $shape
| [ $a["service.beta.kubernetes.io/oci-load-balancer-tls-secret"],
$a["service.beta.kubernetes.io/oci-load-balancer-tls-backendset-secret"] ]
| map(select(. != null and contains("/") and (split("/")[0] != $m.namespace)))
| . as $xns
| select($shape == "" or ($xns | length) > 0)
| [ $m.namespace + "/" + $m.name,
(if $shape == "" then "NO-SHAPE" else $shape end),
(if ($xns | length) > 0 then "CROSS-NS-TLS " + ($xns | join(",")) else "-" end) ]
| @tsv'
# payments/checkout NO-SHAPE CROSS-NS-TLS cert-store/wildcard-tls
# tools/grafana NO-SHAPE -
Run it against every cluster, not only the one going first. The NO-SHAPE rows are the Services that pick up the new default the next time they are created. The CROSS-NS-TLS rows are the ones that stop reconciling as soon as the cluster is on 1.37.
Step two: set the shape once, in the library chart
The obvious fix is to add three annotations to each chart. It works for a quarter. Then a new service is scaffolded from an old one, a value is copied without being read, and the fleet ends up with as many shapes as it has charts. Setting the shape is also a trade-off, which is why it deserves one owner.
- Raise the maximum, keep the minimum low.
service.beta.kubernetes.io/oci-load-balancer-shape-flex-maxlets traffic scale toward the ceiling, but only the minimum is always available. A spike gets whatever the load balancer has scaled to by then. - Raise the minimum.
service.beta.kubernetes.io/oci-load-balancer-shape-flex-minmakes the bandwidth available from the first second. Oracle bills flexible load balancers per minute at the minimum or at actual usage, whichever is higher, so a high minimum is paid for around the clock, on every load balancer that inherits it.
When every chart sets its own load balancer shape, nobody owns the shape. Put it in one template and give that template an owner.
So we put the decision in the Helm library chart that every service chart already depends on, as a small set of named tiers. A chart picks a tier. An unknown tier fails the render instead of falling through to the platform default.
# templates/_service.tpl in the library chart
# Tier values are examples. Size them from your own traffic and price list.
{{- define "lib.ociLbAnnotations" -}}
{{- $tiers := dict
"edge" (dict "min" "100" "max" "1000")
"internal" (dict "min" "10" "max" "100") -}}
{{- $tier := .Values.service.lbTier | default "internal" -}}
{{- $t := get $tiers $tier -}}
{{- if not $t }}{{ fail (printf "unknown service.lbTier %q" $tier) }}{{ end -}}
service.beta.kubernetes.io/oci-load-balancer-shape: "flexible"
service.beta.kubernetes.io/oci-load-balancer-shape-flex-min: {{ $t.min | quote }}
service.beta.kubernetes.io/oci-load-balancer-shape-flex-max: {{ $t.max | quote }}
{{- end -}}
# templates/service.yaml in a service chart
metadata:
{{- if eq .Values.service.type "LoadBalancer" }}
annotations:
{{- include "lib.ociLbAnnotations" . | nindent 4 }}
{{- end }}
Bandwidth values carry no unit for flexible shapes: "100", not "100Mbps". Both ends must sit between 10 and 8,000 Mbps. The library chart only covers charts that use it, so CI also renders every chart and fails on any LoadBalancer Service that reaches the cluster without a shape:
# bash: CI gate, after helm template
unshaped=$(helm template "$RELEASE" "$CHART" -f "$VALUES" | yq ea '
[ select(.kind == "Service" and .spec.type == "LoadBalancer"
and .metadata.annotations["service.beta.kubernetes.io/oci-load-balancer-shape"] == null)
| .metadata.name ] | join(" ")')
if [ -n "$unshaped" ]; then
echo "LoadBalancer Service without an OCI shape: $unshaped" >&2
exit 1
fi
Step three: move TLS secrets before the control plane
Both TLS annotations, service.beta.kubernetes.io/oci-load-balancer-tls-secret and service.beta.kubernetes.io/oci-load-balancer-tls-backendset-secret, accept either <secret-name> or <service-namespace>/<secret-name>. Oracle’s instruction is to fix them before the cluster goes to 1.37: copy each secret into the Service’s namespace and update the annotation.
A copied secret does not rotate. If cert-manager issues the certificate, we issue a Certificate in the Service’s namespace instead of copying the result, and the annotation change goes through Git, because a GitOps controller will revert an annotation edited in place.
Step four: control plane, then node pools, in OpenTofu
We keep the two versions as separate inputs so they ship as two changes: the control plane first, and the node pools after the cluster has run on the new version for a while. With node cycling enabled, changing the pool’s version replaces nodes one at a time, surge first.
# opentofu: two inputs, two merges
resource "oci_containerengine_cluster" "this" {
# ...
kubernetes_version = var.control_plane_version # e.g. "v1.36.4" now
}
resource "oci_containerengine_node_pool" "workers" {
cluster_id = oci_containerengine_cluster.this.id
kubernetes_version = var.node_pool_version
# ...
node_pool_cycling_details {
is_node_cycling_enabled = true
maximum_surge = "1"
maximum_unavailable = "0"
}
node_eviction_node_pool_settings {
eviction_grace_duration = "PT60M"
is_force_action_after_grace_duration = false
}
}
If you use the terraform-oci-oke module, take v5.5.2 (13 October) before the upgrade. It renders a one-hour eviction grace as PT1H, the form the OCI API returns. Earlier versions emitted PT60M, and every plan showed drift on a value nobody had changed.
The one-hour cap
eviction_grace_duration has a maximum of PT60M. If a PodDisruptionBudget blocks eviction for longer than that, the node pool has to choose, and you choose for it in advance with is_force_action_after_grace_duration. Set it to true and the node is replaced after the hour, so the budget was advisory. Set it to false and that node is not replaced, so the upgrade stalls while the 3 November date keeps running.
We keep it false and treat a stall as a finding. A budget that blocks eviction for an hour is usually a single-replica workload with minAvailable: 1, and the fix is a second replica, not a forced delete. The audit to run before the node pools move is a list of every PDB whose status.disruptionsAllowed is zero.
What the practice sees
An OKE estate from our discovery work shows the pattern: Helm charts deployed to one cluster per region, and no single place that says what a Service should look like. Launching a new environment there means rebuilding clusters, and every rebuild creates load balancers again. On that kind of estate the 1.37 default reaches production through the rebuilds, not through the Services anyone is editing. The baseline we bring to OKE is the one we run on EKS, coded in OpenTofu and Terragrunt, and the library chart is where defaults like this get decided once.
Honest trade-offs
- A network load balancer avoids the question, and costs you layer 7.
oci.oraclecloud.com/load-balancer-type: "nlb"has no bandwidth shape, but TLS termination at the load balancer and HTTP routing go with it. That work moves into an ingress controller you now operate. - Tiers trade precision for consistency. Two or three tiers will over-provision some Services. A per-Service value is more exact, and it brings back the drift that tiers remove.
- Do not validate on the preview in production. 1.37.0 is OC1 only and not intended for production workloads. Use it on a staging cluster to run the audit and the TLS fixes, and wait for 1.37.1 to move production.
- A one-hour grace is not always enough. Long-running batch pods will not finish inside it. Those pools are better replaced out of place: create a new pool on the new version, drain the old one on your schedule, then delete it.
Facing this on your stack? Naviteq’s senior platform team brings the same Kubernetes baseline to OKE that it runs on EKS, for SaaS, FinTech, and Enterprise teams across the US, EU, and Israel. Let’s talk.
Naviteq: DevOps, FinOps & AI-driven cloud automation, delivered at scale.