[{"data":1,"prerenderedAt":1049},["ShallowReactive",2],{"home-posts":3,"release-latest-biebie-kube":1001},[4,449],{"id":5,"title":6,"author":7,"body":8,"description":434,"extension":435,"image":436,"meta":437,"navigation":438,"path":439,"publishedAt":440,"relatedProducts":441,"seo":443,"stem":444,"tags":445,"updatedAt":436,"__hash__":448},"blog\u002Fblog\u002Fkubernetes-watches-versus-polling.md","Watches vs Polling: Why Kubernetes Tools Feel Slow","Biebie",{"type":9,"value":10,"toc":426},"minimark",[11,15,18,23,26,37,40,43,46,50,53,56,62,69,72,78,81,264,267,270,273,279,283,286,292,302,305,311,321,335,339,347,350,353,356,362,365,369,372,379,390,396,400,403,406,409,422],[12,13,14],"p",{},"There is a particular feeling you get from a slow Kubernetes interface. You delete a pod, the row stays. You hit refresh, it is still there. You hit refresh again and now there are two pods, one terminating and one starting, and you are not sure which of those states is current and which is the interface catching up.",[12,16,17],{},"That feeling almost always has the same cause: the tool is polling. Understanding what it should be doing instead explains a lot about why some Kubernetes tools feel immediate and others feel like they are describing the recent past.",[19,20,22],"h2",{"id":21},"what-polling-actually-costs","What polling actually costs",[12,24,25],{},"A polling client asks the same question on a timer:",[27,28,33],"pre",{"className":29,"code":31,"language":32},[30],"language-text","GET \u002Fapi\u002Fv1\u002Fnamespaces\u002Fdefault\u002Fpods\nGET \u002Fapi\u002Fv1\u002Fnamespaces\u002Fdefault\u002Fpods\nGET \u002Fapi\u002Fv1\u002Fnamespaces\u002Fdefault\u002Fpods\n","text",[34,35,31],"code",{"__ignoreMap":36},"",[12,38,39],{},"Each request makes the API server read every matching object from etcd, serialise all of them, and send the complete set over the wire. The client then throws away the previous copy and renders the new one.",[12,41,42],{},"The cost scales with the number of objects and the number of clients, not with how much actually changed. A namespace with 400 pods that have not moved in an hour still transfers 400 pods every poll. Ten engineers with the dashboard open is ten times that. This is why cluster administrators have opinions about dashboards.",[12,44,45],{},"The latency is also structurally bad. With a five-second interval, the average change is displayed 2.5 seconds after it happened, and the worst case is five. Shortening the interval improves the latency and makes the load problem worse in exact proportion. There is no interval that is both fast and cheap, because the mechanism is wrong.",[19,47,49],{"id":48},"the-list-and-watch-protocol","The list-and-watch protocol",[12,51,52],{},"Kubernetes provides a better primitive, and it is the same one every controller in the system uses.",[12,54,55],{},"The client starts with one list:",[27,57,60],{"className":58,"code":59,"language":32},[30],"GET \u002Fapi\u002Fv1\u002Fnamespaces\u002Fdefault\u002Fpods\n",[34,61,59],{"__ignoreMap":36},[12,63,64,65,68],{},"The response includes a ",[34,66,67],{},"resourceVersion"," on the list itself. That value is a position in the cluster's change stream — a bookmark saying \"this is the state of the world as of here\".",[12,70,71],{},"The client then opens a watch starting from that position:",[27,73,76],{"className":74,"code":75,"language":32},[30],"GET \u002Fapi\u002Fv1\u002Fnamespaces\u002Fdefault\u002Fpods?watch=true&resourceVersion=41827\n",[34,77,75],{"__ignoreMap":36},[12,79,80],{},"This request does not return. It stays open and the API server writes an event into it every time a matching object changes:",[27,82,86],{"className":83,"code":84,"language":85,"meta":36,"style":36},"language-json shiki shiki-themes github-dark","{\"type\":\"MODIFIED\",\"object\":{\"kind\":\"Pod\",\"metadata\":{\"name\":\"api-7d9f\",\"resourceVersion\":\"41831\"},...}}\n{\"type\":\"DELETED\",\"object\":{\"kind\":\"Pod\",\"metadata\":{\"name\":\"api-6c2a\",\"resourceVersion\":\"41832\"},...}}\n{\"type\":\"ADDED\",\"object\":{\"kind\":\"Pod\",\"metadata\":{\"name\":\"api-9f1b\",\"resourceVersion\":\"41833\"},...}}\n","json",[34,87,88,160,212],{"__ignoreMap":36},[89,90,93,97,101,104,108,111,114,117,120,122,125,127,130,132,135,137,140,142,145,147,150,153,157],"span",{"class":91,"line":92},"line",1,[89,94,96],{"class":95},"s95oV","{",[89,98,100],{"class":99},"sDLfK","\"type\"",[89,102,103],{"class":95},":",[89,105,107],{"class":106},"sU2Wk","\"MODIFIED\"",[89,109,110],{"class":95},",",[89,112,113],{"class":99},"\"object\"",[89,115,116],{"class":95},":{",[89,118,119],{"class":99},"\"kind\"",[89,121,103],{"class":95},[89,123,124],{"class":106},"\"Pod\"",[89,126,110],{"class":95},[89,128,129],{"class":99},"\"metadata\"",[89,131,116],{"class":95},[89,133,134],{"class":99},"\"name\"",[89,136,103],{"class":95},[89,138,139],{"class":106},"\"api-7d9f\"",[89,141,110],{"class":95},[89,143,144],{"class":99},"\"resourceVersion\"",[89,146,103],{"class":95},[89,148,149],{"class":106},"\"41831\"",[89,151,152],{"class":95},"},",[89,154,156],{"class":155},"s6RL2","...",[89,158,159],{"class":95},"}}\n",[89,161,163,165,167,169,172,174,176,178,180,182,184,186,188,190,192,194,197,199,201,203,206,208,210],{"class":91,"line":162},2,[89,164,96],{"class":95},[89,166,100],{"class":99},[89,168,103],{"class":95},[89,170,171],{"class":106},"\"DELETED\"",[89,173,110],{"class":95},[89,175,113],{"class":99},[89,177,116],{"class":95},[89,179,119],{"class":99},[89,181,103],{"class":95},[89,183,124],{"class":106},[89,185,110],{"class":95},[89,187,129],{"class":99},[89,189,116],{"class":95},[89,191,134],{"class":99},[89,193,103],{"class":95},[89,195,196],{"class":106},"\"api-6c2a\"",[89,198,110],{"class":95},[89,200,144],{"class":99},[89,202,103],{"class":95},[89,204,205],{"class":106},"\"41832\"",[89,207,152],{"class":95},[89,209,156],{"class":155},[89,211,159],{"class":95},[89,213,215,217,219,221,224,226,228,230,232,234,236,238,240,242,244,246,249,251,253,255,258,260,262],{"class":91,"line":214},3,[89,216,96],{"class":95},[89,218,100],{"class":99},[89,220,103],{"class":95},[89,222,223],{"class":106},"\"ADDED\"",[89,225,110],{"class":95},[89,227,113],{"class":99},[89,229,116],{"class":95},[89,231,119],{"class":99},[89,233,103],{"class":95},[89,235,124],{"class":106},[89,237,110],{"class":95},[89,239,129],{"class":99},[89,241,116],{"class":95},[89,243,134],{"class":99},[89,245,103],{"class":95},[89,247,248],{"class":106},"\"api-9f1b\"",[89,250,110],{"class":95},[89,252,144],{"class":99},[89,254,103],{"class":95},[89,256,257],{"class":106},"\"41833\"",[89,259,152],{"class":95},[89,261,156],{"class":155},[89,263,159],{"class":95},[12,265,266],{},"Three properties follow from this, and they are the entire difference.",[12,268,269],{},"Traffic is proportional to change. A quiet namespace costs nothing after the initial list, no matter how many objects it contains or how long you leave the window open.",[12,271,272],{},"Latency is proportional to nothing. The event is written when the change is committed. There is no interval to wait out.",[12,274,275,276,278],{},"Nothing is missed. Because every event carries a ",[34,277,67],{}," and the stream is ordered, the client always knows exactly where it is.",[19,280,282],{"id":281},"why-watches-expire-and-what-to-do-about-it","Why watches expire, and what to do about it",[12,284,285],{},"A watch is not permanent. The API server will close it, and there are two distinct reasons that need different handling.",[12,287,288,289,291],{},"The mundane one is that the server closes idle or long-lived connections deliberately, to spread load when API server instances come and go. The client reconnects from the last ",[34,290,67],{}," it saw and continues without a gap.",[12,293,294,295,298,299,301],{},"The interesting one is ",[34,296,297],{},"410 Gone",". The API server keeps a limited window of change history — etcd compacts old revisions, and the watch cache holds a bounded number of recent events. If your client is offline long enough that its ",[34,300,67],{}," falls out of that window, the server can no longer tell you what you missed. It cannot send you the events, because they are gone.",[12,303,304],{},"The only correct response is to list again from scratch and resynchronise:",[27,306,309],{"className":307,"code":308,"language":32},[30],"410 Gone  ->  GET \u002Fapi\u002Fv1\u002F...\u002Fpods  ->  watch from the new resourceVersion\n",[34,310,308],{"__ignoreMap":36},[12,312,313,314,317,318,320],{},"A client that handles ",[34,315,316],{},"410"," by simply reconnecting from the same stale version gets ",[34,319,316],{}," again, forever, and quietly stops updating while continuing to look like it is working. This is a real and common bug, and it produces exactly the symptom of a view that was correct when you opened it and has been drifting ever since.",[12,322,323,324,327,328,330,331,334],{},"There is also a ",[34,325,326],{},"BOOKMARK"," event type, which exists specifically for this problem. The server periodically sends an event with no object payload, just a current ",[34,329,67],{},", so that a client watching a quiet resource keeps its position fresh instead of falling behind the compaction window while nothing happens. A client that requests bookmarks with ",[34,332,333],{},"allowWatchBookmarks=true"," is much harder to strand.",[19,336,338],{"id":337},"what-an-informer-adds","What an informer adds",[12,340,341,342,346],{},"In practice, clients do not implement list-and-watch by hand. The Kubernetes client libraries provide an ",[343,344,345],"strong",{},"informer",", which wraps the protocol and adds a local cache.",[12,348,349],{},"The informer holds every object of its resource type in memory, updated by the watch stream. That cache is what your interface reads from. It has three consequences worth naming.",[12,351,352],{},"Reads become free. Filtering, sorting and searching happen against local memory. Typing in a filter box does not generate API traffic, so it can be genuinely instant rather than debounced-and-hopeful.",[12,354,355],{},"The full object is available. A watch event carries the entire object, not a diff, so the cache always has complete objects and any view can be rendered without going back to the API server for detail.",[12,357,358,359,361],{},"Reconnection is handled once. Expiry, ",[34,360,316],{},", resync and backoff live in the informer rather than being reimplemented, differently and incompletely, in each part of the application.",[12,363,364],{},"The cost is memory, and it is proportional to the number of objects being watched. This is a real constraint at scale, which is why watching every resource kind in every namespace at once is not a sensible default. Watching what is currently on screen is.",[19,366,368],{"id":367},"where-this-shows-up-in-the-interface","Where this shows up in the interface",[12,370,371],{},"The architecture is visible from the outside once you know what to look for.",[12,373,374,375,378],{},"A watch-backed view has no refresh button, because a refresh button would do nothing that is not already happening. When you delete a pod, the row disappears when the deletion is committed. A pod that enters ",[34,376,377],{},"CrashLoopBackOff"," at 14:02 changes colour at 14:02, not on the next tick.",[12,380,381,382,385,386,389],{},"A watch-backed view can also show transient states at all. Pods pass through ",[34,383,384],{},"Pending"," and ",[34,387,388],{},"ContainerCreating"," in a few seconds. A five-second poll will frequently step straight over them, so failures during startup appear as an object that was fine and is now broken, with the intermediate state that would have explained why never displayed.",[12,391,392,393,395],{},"And a watch-backed view stays honest when you leave it open. The reason ",[34,394,316],{}," handling matters is that the failure it causes is invisible: nothing errors, the view simply stops changing. If you have ever left a dashboard open over lunch and come back to a cluster state that turned out to be an hour old, you have probably seen it.",[19,397,399],{"id":398},"why-this-is-harder-in-a-multi-cluster-tool","Why this is harder in a multi-cluster tool",[12,401,402],{},"Everything above describes one client watching one cluster. A tool that shows several clusters at once has to do all of it per cluster, in parallel, and the failure modes stop being independent.",[12,404,405],{},"Each cluster needs its own informers and its own reconnection state. A cluster that becomes unreachable — a VPN dropping, most often — must not stall the views for the others. Credentials expire at different times, because they came from different identity providers. And a cluster you switch away from should have its watches released rather than left running, or the memory cost grows with every cluster you have visited rather than every cluster you are using.",[12,407,408],{},"None of that is conceptually difficult, but it is a meaningful amount of state to get right, and it is the reason that many otherwise good single-cluster tools feel fragile the moment you point them at a dozen clusters belonging to a dozen organisations.",[12,410,411,416,417,421],{},[412,413,415],"a",{"href":414},"\u002Fbiebie-kube","Biebie Kube"," is built on watch-backed resource views across every cluster you have configured, which is what makes its tables update in place rather than on a timer. If you want the operational patterns that go with it, the article on ",[412,418,420],{"href":419},"\u002Fblog\u002Fmanaging-kubernetes-clusters-for-multiple-customers","managing Kubernetes clusters for multiple customers"," covers kubeconfig layout, context naming and production safety.",[423,424,425],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s6RL2, html code.shiki .s6RL2{--shiki-default:#FDAEB7;--shiki-default-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":36,"searchDepth":214,"depth":214,"links":427},[428,429,430,431,432,433],{"id":21,"depth":162,"text":22},{"id":48,"depth":162,"text":49},{"id":281,"depth":162,"text":282},{"id":337,"depth":162,"text":338},{"id":367,"depth":162,"text":368},{"id":398,"depth":162,"text":399},"How the Kubernetes list-and-watch protocol works, what informers actually cache, and why a tool that polls the API server will always feel a step behind.","md",null,{},true,"\u002Fblog\u002Fkubernetes-watches-versus-polling","2026-08-11",[442],"biebie-kube",{"title":6,"description":434},"blog\u002Fkubernetes-watches-versus-polling",[446,447],"Kubernetes","Architecture","lzqyHUkWGNvVpL4Xm6IGGBJc9fMoYkGVqckZa8NAL7Y",{"id":450,"title":451,"author":7,"body":452,"description":991,"extension":435,"image":436,"meta":992,"navigation":438,"path":419,"publishedAt":993,"relatedProducts":994,"seo":996,"stem":997,"tags":998,"updatedAt":436,"__hash__":1000},"blog\u002Fblog\u002Fmanaging-kubernetes-clusters-for-multiple-customers.md","How to Manage Kubernetes Clusters for Multiple Customers",{"type":9,"value":453,"toc":980},[454,457,460,463,467,470,524,527,538,541,547,552,595,598,602,609,612,641,648,652,662,682,688,692,699,702,825,828,832,835,841,849,853,859,862,872,876,879,885,932,946,950,953,956,962,966,969,972,977],[12,455,456],{},"Most Kubernetes advice assumes your clusters belong to you. You have a dev cluster, a staging cluster and a production cluster, they share a naming scheme, and the worst thing that can happen is you run a command against staging when you meant dev.",[12,458,459],{},"Consultants, managed service providers and contractors work under a different set of constraints. The clusters belong to different organisations, the naming schemes were decided by people who have never met each other, credentials arrive by whatever channel that customer prefers, and a mistake is not an internal embarrassment but a conversation with someone else's incident process.",[12,461,462],{},"This article covers the patterns that hold up under those constraints.",[19,464,466],{"id":465},"do-not-merge-every-kubeconfig-into-one-file","Do not merge every kubeconfig into one file",[12,468,469],{},"The instinct when you collect kubeconfigs is to merge them:",[27,471,475],{"className":472,"code":473,"language":474,"meta":36,"style":36},"language-bash shiki shiki-themes github-dark","KUBECONFIG=~\u002F.kube\u002Fconfig:~\u002FDownloads\u002Fcustomer-a.yaml \\\n  kubectl config view --flatten > ~\u002F.kube\u002Fmerged\nmv ~\u002F.kube\u002Fmerged ~\u002F.kube\u002Fconfig\n","bash",[34,476,477,493,513],{"__ignoreMap":36},[89,478,479,482,486,489],{"class":91,"line":92},[89,480,481],{"class":95},"KUBECONFIG",[89,483,485],{"class":484},"snl16","=",[89,487,488],{"class":106},"~\u002F.kube\u002Fconfig:~\u002FDownloads\u002Fcustomer-a.yaml",[89,490,492],{"class":491},"svObZ"," \\\n",[89,494,495,498,501,504,507,510],{"class":91,"line":162},[89,496,497],{"class":106},"  kubectl",[89,499,500],{"class":106}," config",[89,502,503],{"class":106}," view",[89,505,506],{"class":99}," --flatten",[89,508,509],{"class":484}," >",[89,511,512],{"class":106}," ~\u002F.kube\u002Fmerged\n",[89,514,515,518,521],{"class":91,"line":214},[89,516,517],{"class":491},"mv",[89,519,520],{"class":106}," ~\u002F.kube\u002Fmerged",[89,522,523],{"class":106}," ~\u002F.kube\u002Fconfig\n",[12,525,526],{},"This works, and it is a trap.",[12,528,529,530,533,534,537],{},"A merged file has one namespace for context names, so two customers who both named their cluster ",[34,531,532],{},"production"," collide, and the loser gets silently renamed or overwritten depending on how you merged. It has one ",[34,535,536],{},"current-context",", so the notion of \"where am I\" is global across every customer you support. And it grows without bound: when an engagement ends, removing that customer's entries from a 2,000-line merged file is error-prone enough that most people simply do not, which means expired credentials for organisations you no longer work with sit in the same file as live ones.",[12,539,540],{},"Keep one file per customer instead:",[27,542,545],{"className":543,"code":544,"language":32},[30],"~\u002F.kube\u002F\n  config                 # your own clusters, if any\n  customers\u002F\n    acme.yaml\n    globex.yaml\n    initech.yaml\n",[34,546,544],{"__ignoreMap":36},[12,548,549,551],{},[34,550,481],{}," accepts a list, so you can still merge at the point of use when you actually need cross-cluster visibility:",[27,553,555],{"className":472,"code":554,"language":474,"meta":36,"style":36},"export KUBECONFIG=~\u002F.kube\u002Fconfig:$(find ~\u002F.kube\u002Fcustomers -name '*.yaml' | paste -sd:)\n",[34,556,557],{"__ignoreMap":36},[89,558,559,562,565,568,571,574,577,580,583,586,589,592],{"class":91,"line":92},[89,560,561],{"class":484},"export",[89,563,564],{"class":95}," KUBECONFIG",[89,566,567],{"class":484},"=~",[89,569,570],{"class":95},"\u002F.kube\u002Fconfig:$(",[89,572,573],{"class":491},"find",[89,575,576],{"class":106}," ~\u002F.kube\u002Fcustomers",[89,578,579],{"class":99}," -name",[89,581,582],{"class":106}," '*.yaml'",[89,584,585],{"class":484}," |",[89,587,588],{"class":491}," paste",[89,590,591],{"class":99}," -sd:",[89,593,594],{"class":95},")\n",[12,596,597],{},"The important part is that the merge is a runtime view, not a stored artefact. Deleting a customer is deleting a file.",[19,599,601],{"id":600},"make-context-names-carry-the-information-you-need","Make context names carry the information you need",[12,603,604,605,608],{},"The default context name from a cloud provider tells you almost nothing useful in a multi-customer setting. EKS gives you the cluster ARN. GKE gives you ",[34,606,607],{},"gke_project_region_cluster",". Neither says whose it is or how much care it deserves.",[12,610,611],{},"Rename contexts to a scheme you control, with the customer first and the environment explicit:",[27,613,615],{"className":472,"code":614,"language":474,"meta":36,"style":36},"kubectl config rename-context \\\n  arn:aws:eks:ap-southeast-1:123456789012:cluster\u002Fprod-main \\\n  acme-prod\n",[34,616,617,629,636],{"__ignoreMap":36},[89,618,619,622,624,627],{"class":91,"line":92},[89,620,621],{"class":491},"kubectl",[89,623,500],{"class":106},[89,625,626],{"class":106}," rename-context",[89,628,492],{"class":99},[89,630,631,634],{"class":91,"line":162},[89,632,633],{"class":106},"  arn:aws:eks:ap-southeast-1:123456789012:cluster\u002Fprod-main",[89,635,492],{"class":99},[89,637,638],{"class":91,"line":214},[89,639,640],{"class":106},"  acme-prod\n",[12,642,643,644,647],{},"A scheme like ",[34,645,646],{},"customer-environment"," sorts sensibly, tab-completes usefully, and puts the two facts you most need — whose cluster, and how dangerous — in the first thing you read. If you use a prompt that shows the current context, this is also the string that will be sitting in front of you when you type a delete command.",[19,649,651],{"id":650},"set-a-default-namespace-per-context","Set a default namespace per context",[12,653,654,655,658,659,103],{},"A context can carry a namespace, and setting it removes a whole class of mistake where a command lands in ",[34,656,657],{},"default"," because you forgot ",[34,660,661],{},"-n",[27,663,665],{"className":472,"code":664,"language":474,"meta":36,"style":36},"kubectl config set-context acme-prod --namespace=acme-platform\n",[34,666,667],{"__ignoreMap":36},[89,668,669,671,673,676,679],{"class":91,"line":92},[89,670,621],{"class":491},[89,672,500],{"class":106},[89,674,675],{"class":106}," set-context",[89,677,678],{"class":106}," acme-prod",[89,680,681],{"class":99}," --namespace=acme-platform\n",[12,683,684,685,687],{},"This matters more across customers than within one organisation, because the namespace conventions differ. One customer puts everything in namespaces named after teams, another mirrors their Git repository structure, a third uses ",[34,686,657],{}," for genuinely everything. Encoding each customer's convention into their context means you stop carrying it in your head.",[19,689,691],{"id":690},"separate-credentials-from-cluster-definitions","Separate credentials from cluster definitions",[12,693,694,695,698],{},"Kubeconfig files can embed credentials directly, and for customer clusters they frequently do — a service account token pasted into the ",[34,696,697],{},"users"," section, or a client certificate encoded inline. That means the file itself is a secret, which makes it awkward to back up, sync between machines or share with a colleague taking over the engagement.",[12,700,701],{},"Where the customer's setup allows it, use an exec credential plugin instead, so the file describes how to obtain a credential rather than containing one:",[27,703,707],{"className":704,"code":705,"language":706,"meta":36,"style":36},"language-yaml shiki shiki-themes github-dark","users:\n  - name: acme-prod\n    user:\n      exec:\n        apiVersion: client.authentication.k8s.io\u002Fv1\n        command: aws\n        args:\n          - eks\n          - get-token\n          - --cluster-name\n          - prod-main\n          - --region\n          - ap-southeast-1\n","yaml",[34,708,709,717,731,738,746,757,768,776,785,793,801,809,817],{"__ignoreMap":36},[89,710,711,714],{"class":91,"line":92},[89,712,697],{"class":713},"s4JwU",[89,715,716],{"class":95},":\n",[89,718,719,722,725,728],{"class":91,"line":162},[89,720,721],{"class":95},"  - ",[89,723,724],{"class":713},"name",[89,726,727],{"class":95},": ",[89,729,730],{"class":106},"acme-prod\n",[89,732,733,736],{"class":91,"line":214},[89,734,735],{"class":713},"    user",[89,737,716],{"class":95},[89,739,741,744],{"class":91,"line":740},4,[89,742,743],{"class":713},"      exec",[89,745,716],{"class":95},[89,747,749,752,754],{"class":91,"line":748},5,[89,750,751],{"class":713},"        apiVersion",[89,753,727],{"class":95},[89,755,756],{"class":106},"client.authentication.k8s.io\u002Fv1\n",[89,758,760,763,765],{"class":91,"line":759},6,[89,761,762],{"class":713},"        command",[89,764,727],{"class":95},[89,766,767],{"class":106},"aws\n",[89,769,771,774],{"class":91,"line":770},7,[89,772,773],{"class":713},"        args",[89,775,716],{"class":95},[89,777,779,782],{"class":91,"line":778},8,[89,780,781],{"class":95},"          - ",[89,783,784],{"class":106},"eks\n",[89,786,788,790],{"class":91,"line":787},9,[89,789,781],{"class":95},[89,791,792],{"class":106},"get-token\n",[89,794,796,798],{"class":91,"line":795},10,[89,797,781],{"class":95},[89,799,800],{"class":106},"--cluster-name\n",[89,802,804,806],{"class":91,"line":803},11,[89,805,781],{"class":95},[89,807,808],{"class":106},"prod-main\n",[89,810,812,814],{"class":91,"line":811},12,[89,813,781],{"class":95},[89,815,816],{"class":106},"--region\n",[89,818,820,822],{"class":91,"line":819},13,[89,821,781],{"class":95},[89,823,824],{"class":106},"ap-southeast-1\n",[12,826,827],{},"The kubeconfig becomes a description of how to reach the cluster, and the actual secret stays wherever your cloud CLI or identity provider keeps it. That also means the token is short-lived rather than a long-lived string sitting in a file.",[19,829,831],{"id":830},"treat-the-network-path-as-part-of-the-cluster-definition","Treat the network path as part of the cluster definition",[12,833,834],{},"Customer clusters are usually not reachable from the open internet. Getting to them means a VPN, and each customer has chosen a different one. In practice, \"connect to Acme's cluster\" is a two-part operation: bring up Acme's VPN with the right identity, then use Acme's kubeconfig.",[12,836,837,838,840],{},"The failure mode is subtle. If the VPN is down, your ",[34,839,621],{}," command does not fail with \"you are not connected to the right network\" — it fails with a connection timeout, which looks identical to a cluster that is genuinely unreachable. Time spent debugging an API server that is fine is time wasted.",[12,842,843,844,848],{},"Two things help. First, record which VPN profile and which login identity belong to which cluster, in the same place you record the cluster, so the association is written down rather than remembered. That is the job ",[412,845,847],{"href":846},"\u002Fbiebie-access","Biebie Access"," exists to do. Second, prefer tooling that distinguishes a network-level failure from a TLS failure from an authentication failure, so the error tells you which layer to look at.",[19,850,852],{"id":851},"mark-production-and-make-it-behave-differently","Mark production and make it behave differently",[12,854,855,856,858],{},"Every customer has clusters you can experiment with and clusters you cannot. The difference is invisible in ",[34,857,621],{}," — the same command runs the same way regardless of what is on the other end.",[12,860,861],{},"At minimum, make production visible in your shell. A prompt segment that shows the current context, coloured by environment, is a small change with a large effect, because the information arrives without you asking for it.",[12,863,864,865,867,868,871],{},"Better still, make destructive actions on production require something you cannot do by muscle memory. ",[412,866,415],{"href":414}," lets you mark a cluster as production and then requires the resource name to be typed out in full before a destructive action runs. Typing ",[34,869,870],{},"payments-api"," deliberately is a different cognitive act from clicking a button you have clicked a hundred times on staging.",[19,873,875],{"id":874},"prefer-optimistic-concurrency-when-editing-live-objects","Prefer optimistic concurrency when editing live objects",[12,877,878],{},"Editing a resource directly in a customer cluster is sometimes unavoidable — an incident, a misconfiguration to correct, a value to check. The risk is not that you make a mistake in the YAML. It is that someone else, or a controller, changed the object between when you read it and when you wrote it, and your write discards their change without either of you noticing.",[12,880,881,882,884],{},"Kubernetes has a mechanism for this. Every object carries a ",[34,883,67],{},", and an update that includes the version you read will be rejected with a conflict if the object has moved on:",[27,886,888],{"className":472,"code":887,"language":474,"meta":36,"style":36},"kubectl get deployment payments-api -o yaml > payments.yaml\n# edit payments.yaml, leaving metadata.resourceVersion intact\nkubectl replace -f payments.yaml\n",[34,889,890,914,920],{"__ignoreMap":36},[89,891,892,894,897,900,903,906,909,911],{"class":91,"line":92},[89,893,621],{"class":491},[89,895,896],{"class":106}," get",[89,898,899],{"class":106}," deployment",[89,901,902],{"class":106}," payments-api",[89,904,905],{"class":99}," -o",[89,907,908],{"class":106}," yaml",[89,910,509],{"class":484},[89,912,913],{"class":106}," payments.yaml\n",[89,915,916],{"class":91,"line":162},[89,917,919],{"class":918},"sAwPA","# edit payments.yaml, leaving metadata.resourceVersion intact\n",[89,921,922,924,927,930],{"class":91,"line":214},[89,923,621],{"class":491},[89,925,926],{"class":106}," replace",[89,928,929],{"class":99}," -f",[89,931,913],{"class":106},[12,933,934,935,938,939,942,943,945],{},"If the object changed in between, you get ",[34,936,937],{},"Operation cannot be fulfilled ... the object has been modified"," rather than a silent overwrite. Note that ",[34,940,941],{},"kubectl apply"," does not behave this way by default — it merges, and a merge can absorb someone else's change without a conflict. Any tool that offers a YAML editor should be sending the ",[34,944,67],{}," you loaded; if it does not, it is capable of losing changes you never saw.",[19,947,949],{"id":948},"when-the-engagement-ends-archive-rather-than-delete","When the engagement ends, archive rather than delete",[12,951,952],{},"Finished engagements produce a specific kind of clutter: credentials that no longer work, contexts you will never select again, and the small ongoing risk of selecting one by accident.",[12,954,955],{},"Deleting them is the obvious move, and it is usually wrong. Six months later someone asks what version of an operator that customer was running, or the engagement restarts, and the configuration you deleted has to be requested again from people who may have moved on.",[12,957,958,959,961],{},"Move it out of the active set instead. A separate directory that is not in your ",[34,960,481],{}," path achieves this with plain files. The point is that inactive access should require a deliberate act to become active again, without the information being destroyed.",[19,963,965],{"id":964},"what-this-adds-up-to","What this adds up to",[12,967,968],{},"None of this is exotic. One file per customer, context names that say whose cluster and how dangerous, namespaces encoded in the context, credentials obtained rather than stored, the network path recorded next to the cluster, production visibly marked, edits that fail loudly on conflict, and finished work archived rather than deleted.",[12,970,971],{},"The reason to be deliberate about it is that multi-customer work removes the safety net that single-organisation work has. There is no shared convention to fall back on, no colleague who would have caught it, and no internal blast radius. The structure has to come from you.",[12,973,974,976],{},[412,975,415],{"href":414}," was built around these patterns — clusters grouped by customer, kubeconfigs read where they already are, production clusters marked and guarded, and edits that carry their resource version. It is free for macOS and Windows.",[423,978,979],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":36,"searchDepth":214,"depth":214,"links":981},[982,983,984,985,986,987,988,989,990],{"id":465,"depth":162,"text":466},{"id":600,"depth":162,"text":601},{"id":650,"depth":162,"text":651},{"id":690,"depth":162,"text":691},{"id":830,"depth":162,"text":831},{"id":851,"depth":162,"text":852},{"id":874,"depth":162,"text":875},{"id":948,"depth":162,"text":949},{"id":964,"depth":162,"text":965},"Practical patterns for kubeconfig layout, context naming and production safety when the clusters you work with belong to different organisations.",{},"2026-07-14",[442,995],"biebie-access",{"title":451,"description":991},"blog\u002Fmanaging-kubernetes-clusters-for-multiple-customers",[446,999],"Operations","cWQPF0Z85rjJVXUCuPvv6PIdZpWteiYrPw_rGZaV8FY",{"ok":438,"data":1002},{"tag":1003,"name":1003,"publishedAt":1004,"url":1005,"prerelease":1006,"body":1007,"assets":1008},"v0.2.10","2026-09-02T14:43:45Z","https:\u002F\u002Fgithub.com\u002Fncmink\u002Fbiebie-kube\u002Freleases\u002Ftag\u002Fv0.2.10",false,"**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fncmink\u002Fbiebie-kube\u002Fcompare\u002Fv0.2.9...v0.2.10",[1009,1015,1021,1028,1034,1039,1043],{"id":1010,"name":1011,"downloadUrl":1012,"size":1013,"contentType":1014,"downloadCount":162},541310243,"biebie-kube-v0.2.10-darwin-universal.zip","https:\u002F\u002Fgithub.com\u002Fncmink\u002Fbiebie-kube\u002Freleases\u002Fdownload\u002Fv0.2.10\u002Fbiebie-kube-v0.2.10-darwin-universal.zip",34440274,"application\u002Fzip",{"id":1016,"name":1017,"downloadUrl":1018,"size":1019,"contentType":1020,"downloadCount":162},541310244,"biebie-kube-v0.2.10-darwin-universal.zip.sig","https:\u002F\u002Fgithub.com\u002Fncmink\u002Fbiebie-kube\u002Freleases\u002Fdownload\u002Fv0.2.10\u002Fbiebie-kube-v0.2.10-darwin-universal.zip.sig",89,"application\u002Fpgp-signature",{"id":1022,"name":1023,"downloadUrl":1024,"size":1025,"contentType":1026,"downloadCount":1027},541310251,"biebie-kube-v0.2.10-macos-universal.dmg","https:\u002F\u002Fgithub.com\u002Fncmink\u002Fbiebie-kube\u002Freleases\u002Fdownload\u002Fv0.2.10\u002Fbiebie-kube-v0.2.10-macos-universal.dmg",35543470,"application\u002Fx-apple-diskimage",0,{"id":1029,"name":1030,"downloadUrl":1031,"size":1032,"contentType":1033,"downloadCount":1027},541310248,"biebie-kube-v0.2.10-windows-amd64-installer.exe","https:\u002F\u002Fgithub.com\u002Fncmink\u002Fbiebie-kube\u002Freleases\u002Fdownload\u002Fv0.2.10\u002Fbiebie-kube-v0.2.10-windows-amd64-installer.exe",18048949,"application\u002Fx-msdos-program",{"id":1035,"name":1036,"downloadUrl":1037,"size":1038,"contentType":1014,"downloadCount":1027},541310247,"biebie-kube-v0.2.10-windows-amd64.zip","https:\u002F\u002Fgithub.com\u002Fncmink\u002Fbiebie-kube\u002Freleases\u002Fdownload\u002Fv0.2.10\u002Fbiebie-kube-v0.2.10-windows-amd64.zip",16602643,{"id":1040,"name":1041,"downloadUrl":1042,"size":1019,"contentType":1020,"downloadCount":1027},541310246,"biebie-kube-v0.2.10-windows-amd64.zip.sig","https:\u002F\u002Fgithub.com\u002Fncmink\u002Fbiebie-kube\u002Freleases\u002Fdownload\u002Fv0.2.10\u002Fbiebie-kube-v0.2.10-windows-amd64.zip.sig",{"id":1044,"name":1045,"downloadUrl":1046,"size":1047,"contentType":1048,"downloadCount":162},541310245,"SHA256SUMS","https:\u002F\u002Fgithub.com\u002Fncmink\u002Fbiebie-kube\u002Freleases\u002Fdownload\u002Fv0.2.10\u002FSHA256SUMS",650,"application\u002Foctet-stream",1788424373592]