[{"data":1,"prerenderedAt":1742},["ShallowReactive",2],{"post:\u002F2026\u002F05\u002F06\u002Fgoogle-cloud-choosing-between-gke-and-cloud-run-for-an-internal-tool\u002F":3},{"post":4,"newer":1693,"older":1701,"related":1712,"series":1740},{"id":5,"title":6,"body":7,"canonical":1676,"categories":1677,"date":1679,"description":1680,"extension":1681,"featured":1682,"hero":1683,"image":1683,"meta":1684,"navigation":170,"path":1685,"readingTime":188,"seo":1686,"series":1683,"seriesOrder":1683,"sites":1687,"source":1683,"stem":1688,"tags":1689,"updated":1683,"url":1691,"__hash__":1692},"blog\u002Fblog\u002F2026\u002F05\u002F06\u002Fgoogle-cloud-choosing-between-gke-and-cloud-run-for-an-internal-tool.md","Google Cloud: Choosing Between GKE and Cloud Run for an Internal Tool",{"type":8,"value":9,"toc":1667},"minimark",[10,14,19,22,39,47,50,54,57,92,95,99,121,877,880,891,895,902,1451,1470,1474,1477,1481,1578,1585,1589,1663],[11,12,13],"p",{},"Every time an internal tool needs a real home on Google Cloud (an approval workflow, a reporting dashboard, a small API that a handful of other services call), the same question comes up: does this need Kubernetes, or is that reaching for a bigger hammer than the nail requires? Having run both under similar internal tools, my answer has stopped being \"it depends\" in the vague sense and become a short, concrete checklist. Some of the reasons I used to give for GKE have also stopped being true, so this is the updated version, with the actual deploy commands for each side.",[15,16,18],"h2",{"id":17},"start-from-the-traffic-shape-not-the-technology","Start from the traffic shape, not the technology",[11,20,21],{},"Internal tools tend to have one of two traffic shapes: near-constant light load from a fixed set of employees during business hours, or bursty, occasional load triggered by a batch job, a webhook, or a monthly report run.",[11,23,24,25,29,30,33,34,38],{},"Cloud Run's default is ",[26,27,28],"strong",{},"request-based billing",". In Google's words, instances \"are only charged when they process requests, when they start, and when they shut down,\" and with no minimum instances the service scales to zero between requests. The cost of idle time is a cold start on the next call. If that cold start bothers people, set a minimum instance count. Under request-based billing, idle minimum instances are billed at a lower rate than active ones. The alternative, ",[26,31,32],{},"instance-based billing"," (",[35,36,37],"code",{},"--no-cpu-throttling","), charges for the whole instance lifecycle and is meant for services that do background work outside requests.",[11,40,41,42,46],{},"GKE Autopilot is closer than it used to be, but it doesn't reach zero. General-purpose Autopilot pods use pod-based billing: you're charged per second for the CPU, memory, and ephemeral storage the running pods ",[43,44,45],"em",{},"request",", with no node pool to pay for. But a Deployment keeps at least one replica running around the clock. The standard HorizontalPodAutoscaler doesn't scale to zero without a feature gate. On top of that, every GKE cluster, Autopilot included, pays a flat cluster management fee of $0.10 per cluster per hour. The GKE free tier gives each billing account $74.40 a month in credits, which is equivalent to one free Autopilot or zonal Standard cluster. So the first internal cluster's fee is effectively covered, and the second one isn't.",[11,48,49],{},"If the tool sits idle sixteen hours a day, Cloud Run's model wins by construction. If it's busy all day, scale-to-zero never triggers and the gap narrows to whatever your pod requests cost versus Cloud Run's per-instance rates.",[15,51,53],{"id":52},"count-the-moving-parts-the-tool-actually-has","Count the moving parts the tool actually has",[11,55,56],{},"A single container serving HTTP, backed by Cloud SQL or Firestore, is Cloud Run's design center: deploy a container image, get a URL, done. I used to draw the line at the first sidecar, but that's outdated. Cloud Run supports multi-container instances with up to 10 containers per instance, one of them the ingress container, so a proxy, a log shipper, or an auth helper next to the app no longer forces a move. The same goes for two other old objections:",[58,59,60,71],"ul",{},[61,62,63,66,67,70],"li",{},[26,64,65],{},"Static outbound IP for a partner allowlist."," Direct VPC egress with ",[35,68,69],{},"--vpc-egress=all-traffic",", plus Cloud NAT on that VPC, gives a Cloud Run service a fixed egress address. Google warns about slower instance startup with Cloud NAT on Direct VPC (cold starts of 30 seconds or more), which matters if you scale to zero.",[61,72,73,76,77,80,81,80,84,87,88,91],{},[26,74,75],{},"Keeping it off the internet."," Ingress settings (",[35,78,79],{},"all",", ",[35,82,83],{},"internal",[35,85,86],{},"internal-and-cloud-load-balancing",") restrict who can reach the service at the network level. For an employee-facing tool, IAP now attaches directly to a Cloud Run service with ",[35,89,90],{},"--iap",", with no load balancer required.",[11,93,94],{},"What still pushes me toward GKE is a tool that's really several cooperating services with their own lifecycles: an API, a queue consumer, a scheduler, and a cache that address each other over cluster DNS, share config through ConfigMaps, and get deployed together from one Helm chart or Kustomize overlay. The rule of thumb I use now: if I can draw the architecture as one box (sidecars included), it's Cloud Run. The moment I'm drawing a second box that needs to know about the first at the infrastructure level rather than just calling an HTTPS endpoint, GKE starts looking more honest than stitching Cloud Run services together.",[15,96,98],{"id":97},"what-the-cloud-run-version-actually-looks-like","What the Cloud Run version actually looks like",[11,100,101,102,80,105,108,109,112,113,116,117,120],{},"This is the whole deployment for a typical internal dashboard: a dedicated runtime service account, no public invoker, IAP in front for employee sign-in, private egress into the VPC for a database on a private IP, and one warm instance during the day. It needs ",[35,103,104],{},"roles\u002Frun.admin",[35,106,107],{},"roles\u002Fiam.serviceAccountAdmin",", and ",[35,110,111],{},"roles\u002Fiam.serviceAccountUser"," on the project, plus ",[35,114,115],{},"roles\u002Fiap.admin"," for the access grant and ",[35,118,119],{},"roles\u002Fartifactregistry.reader"," on the image's repository.",[122,123,128],"pre",{"className":124,"code":125,"language":126,"meta":127,"style":127},"language-bash shiki shiki-themes github-dark","#!\u002Fusr\u002Fbin\u002Fenv bash\n# deploy-internal-tool.sh\n# Deploys an internal tool to Cloud Run behind IAP with Direct VPC egress.\nset -euo pipefail\n\nPROJECT_ID=\"my-tools-project\"\nREGION=\"us-central1\"\nSERVICE=\"approvals\"\nIMAGE=\"us-central1-docker.pkg.dev\u002F${PROJECT_ID}\u002Ftools\u002Fapprovals:1.4.2\"\nNETWORK=\"tools-vpc\"\nSUBNET=\"tools-run-subnet\"          # Direct VPC needs a \u002F26 or larger\nACCESS_GROUP=\"group:approvals-users@example.com\"\n\nPROJECT_NUMBER=$(gcloud projects describe \"$PROJECT_ID\" --format='value(projectNumber)')\nRUNTIME_SA=\"${SERVICE}-run@${PROJECT_ID}.iam.gserviceaccount.com\"\n\ngcloud services enable run.googleapis.com iap.googleapis.com --project=\"$PROJECT_ID\"\n\n# 1. A runtime identity of its own, never the default compute service account\nif ! gcloud iam service-accounts describe \"$RUNTIME_SA\" --project=\"$PROJECT_ID\" >\u002Fdev\u002Fnull 2>&1; then\n  gcloud iam service-accounts create \"${SERVICE}-run\" \\\n    --project=\"$PROJECT_ID\" --display-name=\"${SERVICE} Cloud Run runtime\"\nfi\n\n# 2. Deploy: no unauthenticated invokers, IAP on, private egress, request-based billing\ngcloud run deploy \"$SERVICE\" \\\n  --project=\"$PROJECT_ID\" \\\n  --region=\"$REGION\" \\\n  --image=\"$IMAGE\" \\\n  --service-account=\"$RUNTIME_SA\" \\\n  --no-allow-unauthenticated \\\n  --iap \\\n  --network=\"$NETWORK\" \\\n  --subnet=\"$SUBNET\" \\\n  --vpc-egress=private-ranges-only \\\n  --cpu-throttling \\\n  --max-instances=5\n\n# 3. Service-level minimum instances (no new revision needed to change it)\ngcloud run services update \"$SERVICE\" --project=\"$PROJECT_ID\" --region=\"$REGION\" --min=1\n\n# 4. Let the IAP service agent invoke the service\ngcloud run services add-iam-policy-binding \"$SERVICE\" \\\n  --project=\"$PROJECT_ID\" --region=\"$REGION\" \\\n  --member=\"serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-iap.iam.gserviceaccount.com\" \\\n  --role=roles\u002Frun.invoker\n\n# 5. Decide who gets through IAP\ngcloud iap web add-iam-policy-binding \\\n  --project=\"$PROJECT_ID\" --region=\"$REGION\" \\\n  --resource-type=cloud-run --service=\"$SERVICE\" \\\n  --member=\"$ACCESS_GROUP\" \\\n  --role=roles\u002Fiap.httpsResourceAccessor\n\ngcloud run services describe \"$SERVICE\" --project=\"$PROJECT_ID\" --region=\"$REGION\" --format='value(status.url)'\n","bash","",[35,129,130,139,145,151,165,172,186,197,208,224,235,249,260,265,304,325,330,357,362,368,418,442,464,470,475,481,501,515,530,545,559,567,575,590,605,613,621,627,632,638,676,681,687,707,728,744,750,755,761,776,797,814,828,834,839],{"__ignoreMap":127},[131,132,135],"span",{"class":133,"line":134},"line",1,[131,136,138],{"class":137},"sAwPA","#!\u002Fusr\u002Fbin\u002Fenv bash\n",[131,140,142],{"class":133,"line":141},2,[131,143,144],{"class":137},"# deploy-internal-tool.sh\n",[131,146,148],{"class":133,"line":147},3,[131,149,150],{"class":137},"# Deploys an internal tool to Cloud Run behind IAP with Direct VPC egress.\n",[131,152,154,158,161],{"class":133,"line":153},4,[131,155,157],{"class":156},"sDLfK","set",[131,159,160],{"class":156}," -euo",[131,162,164],{"class":163},"sU2Wk"," pipefail\n",[131,166,168],{"class":133,"line":167},5,[131,169,171],{"emptyLinePlaceholder":170},true,"\n",[131,173,175,179,183],{"class":133,"line":174},6,[131,176,178],{"class":177},"s95oV","PROJECT_ID",[131,180,182],{"class":181},"snl16","=",[131,184,185],{"class":163},"\"my-tools-project\"\n",[131,187,189,192,194],{"class":133,"line":188},7,[131,190,191],{"class":177},"REGION",[131,193,182],{"class":181},[131,195,196],{"class":163},"\"us-central1\"\n",[131,198,200,203,205],{"class":133,"line":199},8,[131,201,202],{"class":177},"SERVICE",[131,204,182],{"class":181},[131,206,207],{"class":163},"\"approvals\"\n",[131,209,211,214,216,219,221],{"class":133,"line":210},9,[131,212,213],{"class":177},"IMAGE",[131,215,182],{"class":181},[131,217,218],{"class":163},"\"us-central1-docker.pkg.dev\u002F${",[131,220,178],{"class":177},[131,222,223],{"class":163},"}\u002Ftools\u002Fapprovals:1.4.2\"\n",[131,225,227,230,232],{"class":133,"line":226},10,[131,228,229],{"class":177},"NETWORK",[131,231,182],{"class":181},[131,233,234],{"class":163},"\"tools-vpc\"\n",[131,236,238,241,243,246],{"class":133,"line":237},11,[131,239,240],{"class":177},"SUBNET",[131,242,182],{"class":181},[131,244,245],{"class":163},"\"tools-run-subnet\"",[131,247,248],{"class":137},"          # Direct VPC needs a \u002F26 or larger\n",[131,250,252,255,257],{"class":133,"line":251},12,[131,253,254],{"class":177},"ACCESS_GROUP",[131,256,182],{"class":181},[131,258,259],{"class":163},"\"group:approvals-users@example.com\"\n",[131,261,263],{"class":133,"line":262},13,[131,264,171],{"emptyLinePlaceholder":170},[131,266,268,271,273,276,280,283,286,289,292,295,298,301],{"class":133,"line":267},14,[131,269,270],{"class":177},"PROJECT_NUMBER",[131,272,182],{"class":181},[131,274,275],{"class":177},"$(",[131,277,279],{"class":278},"svObZ","gcloud",[131,281,282],{"class":163}," projects",[131,284,285],{"class":163}," describe",[131,287,288],{"class":163}," \"",[131,290,291],{"class":177},"$PROJECT_ID",[131,293,294],{"class":163},"\"",[131,296,297],{"class":156}," --format=",[131,299,300],{"class":163},"'value(projectNumber)'",[131,302,303],{"class":177},")\n",[131,305,307,310,312,315,317,320,322],{"class":133,"line":306},15,[131,308,309],{"class":177},"RUNTIME_SA",[131,311,182],{"class":181},[131,313,314],{"class":163},"\"${",[131,316,202],{"class":177},[131,318,319],{"class":163},"}-run@${",[131,321,178],{"class":177},[131,323,324],{"class":163},"}.iam.gserviceaccount.com\"\n",[131,326,328],{"class":133,"line":327},16,[131,329,171],{"emptyLinePlaceholder":170},[131,331,333,335,338,341,344,347,350,352,354],{"class":133,"line":332},17,[131,334,279],{"class":278},[131,336,337],{"class":163}," services",[131,339,340],{"class":163}," enable",[131,342,343],{"class":163}," run.googleapis.com",[131,345,346],{"class":163}," iap.googleapis.com",[131,348,349],{"class":156}," --project=",[131,351,294],{"class":163},[131,353,291],{"class":177},[131,355,356],{"class":163},"\"\n",[131,358,360],{"class":133,"line":359},18,[131,361,171],{"emptyLinePlaceholder":170},[131,363,365],{"class":133,"line":364},19,[131,366,367],{"class":137},"# 1. A runtime identity of its own, never the default compute service account\n",[131,369,371,374,377,380,383,386,388,390,393,395,397,399,401,403,406,409,412,415],{"class":133,"line":370},20,[131,372,373],{"class":181},"if",[131,375,376],{"class":181}," !",[131,378,379],{"class":278}," gcloud",[131,381,382],{"class":163}," iam",[131,384,385],{"class":163}," service-accounts",[131,387,285],{"class":163},[131,389,288],{"class":163},[131,391,392],{"class":177},"$RUNTIME_SA",[131,394,294],{"class":163},[131,396,349],{"class":156},[131,398,294],{"class":163},[131,400,291],{"class":177},[131,402,294],{"class":163},[131,404,405],{"class":181}," >",[131,407,408],{"class":163},"\u002Fdev\u002Fnull",[131,410,411],{"class":181}," 2>&1",[131,413,414],{"class":177},"; ",[131,416,417],{"class":181},"then\n",[131,419,421,424,426,428,431,434,436,439],{"class":133,"line":420},21,[131,422,423],{"class":278},"  gcloud",[131,425,382],{"class":163},[131,427,385],{"class":163},[131,429,430],{"class":163}," create",[131,432,433],{"class":163}," \"${",[131,435,202],{"class":177},[131,437,438],{"class":163},"}-run\"",[131,440,441],{"class":156}," \\\n",[131,443,445,448,450,452,454,457,459,461],{"class":133,"line":444},22,[131,446,447],{"class":156},"    --project=",[131,449,294],{"class":163},[131,451,291],{"class":177},[131,453,294],{"class":163},[131,455,456],{"class":156}," --display-name=",[131,458,314],{"class":163},[131,460,202],{"class":177},[131,462,463],{"class":163},"} Cloud Run runtime\"\n",[131,465,467],{"class":133,"line":466},23,[131,468,469],{"class":181},"fi\n",[131,471,473],{"class":133,"line":472},24,[131,474,171],{"emptyLinePlaceholder":170},[131,476,478],{"class":133,"line":477},25,[131,479,480],{"class":137},"# 2. Deploy: no unauthenticated invokers, IAP on, private egress, request-based billing\n",[131,482,484,486,489,492,494,497,499],{"class":133,"line":483},26,[131,485,279],{"class":278},[131,487,488],{"class":163}," run",[131,490,491],{"class":163}," deploy",[131,493,288],{"class":163},[131,495,496],{"class":177},"$SERVICE",[131,498,294],{"class":163},[131,500,441],{"class":156},[131,502,504,507,509,511,513],{"class":133,"line":503},27,[131,505,506],{"class":156},"  --project=",[131,508,294],{"class":163},[131,510,291],{"class":177},[131,512,294],{"class":163},[131,514,441],{"class":156},[131,516,518,521,523,526,528],{"class":133,"line":517},28,[131,519,520],{"class":156},"  --region=",[131,522,294],{"class":163},[131,524,525],{"class":177},"$REGION",[131,527,294],{"class":163},[131,529,441],{"class":156},[131,531,533,536,538,541,543],{"class":133,"line":532},29,[131,534,535],{"class":156},"  --image=",[131,537,294],{"class":163},[131,539,540],{"class":177},"$IMAGE",[131,542,294],{"class":163},[131,544,441],{"class":156},[131,546,548,551,553,555,557],{"class":133,"line":547},30,[131,549,550],{"class":156},"  --service-account=",[131,552,294],{"class":163},[131,554,392],{"class":177},[131,556,294],{"class":163},[131,558,441],{"class":156},[131,560,562,565],{"class":133,"line":561},31,[131,563,564],{"class":156},"  --no-allow-unauthenticated",[131,566,441],{"class":156},[131,568,570,573],{"class":133,"line":569},32,[131,571,572],{"class":156},"  --iap",[131,574,441],{"class":156},[131,576,578,581,583,586,588],{"class":133,"line":577},33,[131,579,580],{"class":156},"  --network=",[131,582,294],{"class":163},[131,584,585],{"class":177},"$NETWORK",[131,587,294],{"class":163},[131,589,441],{"class":156},[131,591,593,596,598,601,603],{"class":133,"line":592},34,[131,594,595],{"class":156},"  --subnet=",[131,597,294],{"class":163},[131,599,600],{"class":177},"$SUBNET",[131,602,294],{"class":163},[131,604,441],{"class":156},[131,606,608,611],{"class":133,"line":607},35,[131,609,610],{"class":156},"  --vpc-egress=private-ranges-only",[131,612,441],{"class":156},[131,614,616,619],{"class":133,"line":615},36,[131,617,618],{"class":156},"  --cpu-throttling",[131,620,441],{"class":156},[131,622,624],{"class":133,"line":623},37,[131,625,626],{"class":156},"  --max-instances=5\n",[131,628,630],{"class":133,"line":629},38,[131,631,171],{"emptyLinePlaceholder":170},[131,633,635],{"class":133,"line":634},39,[131,636,637],{"class":137},"# 3. Service-level minimum instances (no new revision needed to change it)\n",[131,639,641,643,645,647,650,652,654,656,658,660,662,664,667,669,671,673],{"class":133,"line":640},40,[131,642,279],{"class":278},[131,644,488],{"class":163},[131,646,337],{"class":163},[131,648,649],{"class":163}," update",[131,651,288],{"class":163},[131,653,496],{"class":177},[131,655,294],{"class":163},[131,657,349],{"class":156},[131,659,294],{"class":163},[131,661,291],{"class":177},[131,663,294],{"class":163},[131,665,666],{"class":156}," --region=",[131,668,294],{"class":163},[131,670,525],{"class":177},[131,672,294],{"class":163},[131,674,675],{"class":156}," --min=1\n",[131,677,679],{"class":133,"line":678},41,[131,680,171],{"emptyLinePlaceholder":170},[131,682,684],{"class":133,"line":683},42,[131,685,686],{"class":137},"# 4. Let the IAP service agent invoke the service\n",[131,688,690,692,694,696,699,701,703,705],{"class":133,"line":689},43,[131,691,279],{"class":278},[131,693,488],{"class":163},[131,695,337],{"class":163},[131,697,698],{"class":163}," add-iam-policy-binding",[131,700,288],{"class":163},[131,702,496],{"class":177},[131,704,294],{"class":163},[131,706,441],{"class":156},[131,708,710,712,714,716,718,720,722,724,726],{"class":133,"line":709},44,[131,711,506],{"class":156},[131,713,294],{"class":163},[131,715,291],{"class":177},[131,717,294],{"class":163},[131,719,666],{"class":156},[131,721,294],{"class":163},[131,723,525],{"class":177},[131,725,294],{"class":163},[131,727,441],{"class":156},[131,729,731,734,737,739,742],{"class":133,"line":730},45,[131,732,733],{"class":156},"  --member=",[131,735,736],{"class":163},"\"serviceAccount:service-${",[131,738,270],{"class":177},[131,740,741],{"class":163},"}@gcp-sa-iap.iam.gserviceaccount.com\"",[131,743,441],{"class":156},[131,745,747],{"class":133,"line":746},46,[131,748,749],{"class":156},"  --role=roles\u002Frun.invoker\n",[131,751,753],{"class":133,"line":752},47,[131,754,171],{"emptyLinePlaceholder":170},[131,756,758],{"class":133,"line":757},48,[131,759,760],{"class":137},"# 5. Decide who gets through IAP\n",[131,762,764,766,769,772,774],{"class":133,"line":763},49,[131,765,279],{"class":278},[131,767,768],{"class":163}," iap",[131,770,771],{"class":163}," web",[131,773,698],{"class":163},[131,775,441],{"class":156},[131,777,779,781,783,785,787,789,791,793,795],{"class":133,"line":778},50,[131,780,506],{"class":156},[131,782,294],{"class":163},[131,784,291],{"class":177},[131,786,294],{"class":163},[131,788,666],{"class":156},[131,790,294],{"class":163},[131,792,525],{"class":177},[131,794,294],{"class":163},[131,796,441],{"class":156},[131,798,800,803,806,808,810,812],{"class":133,"line":799},51,[131,801,802],{"class":156},"  --resource-type=cloud-run",[131,804,805],{"class":156}," --service=",[131,807,294],{"class":163},[131,809,496],{"class":177},[131,811,294],{"class":163},[131,813,441],{"class":156},[131,815,817,819,821,824,826],{"class":133,"line":816},52,[131,818,733],{"class":156},[131,820,294],{"class":163},[131,822,823],{"class":177},"$ACCESS_GROUP",[131,825,294],{"class":163},[131,827,441],{"class":156},[131,829,831],{"class":133,"line":830},53,[131,832,833],{"class":156},"  --role=roles\u002Fiap.httpsResourceAccessor\n",[131,835,837],{"class":133,"line":836},54,[131,838,171],{"emptyLinePlaceholder":170},[131,840,842,844,846,848,850,852,854,856,858,860,862,864,866,868,870,872,874],{"class":133,"line":841},55,[131,843,279],{"class":278},[131,845,488],{"class":163},[131,847,337],{"class":163},[131,849,285],{"class":163},[131,851,288],{"class":163},[131,853,496],{"class":177},[131,855,294],{"class":163},[131,857,349],{"class":156},[131,859,294],{"class":163},[131,861,291],{"class":177},[131,863,294],{"class":163},[131,865,666],{"class":156},[131,867,294],{"class":163},[131,869,525],{"class":177},[131,871,294],{"class":163},[131,873,297],{"class":156},[131,875,876],{"class":163},"'value(status.url)'\n",[11,878,879],{},"Two caveats from the IAP docs are worth knowing before you pick this pattern. You can't configure IAP on both a load balancer and the Cloud Run service, so choose one. And because Cloud Run enforces IAP before the IAM invoker check, service-to-service callers such as Pub\u002FSub push subscriptions can fail against an IAP-protected service. Give machine callers a separate service without IAP, or route them another way.",[11,881,882,883,886,887,890],{},"Because service-level minimum instances change without a new deployment, dropping to ",[35,884,885],{},"--min=0"," overnight and back to ",[35,888,889],{},"--min=1"," in the morning is a small scheduled job, if the cold start really matters.",[15,892,894],{"id":893},"what-the-gke-autopilot-version-looks-like","What the GKE Autopilot version looks like",[11,896,897,898,901],{},"For comparison, here's the minimum for the same tool on Autopilot, assuming the cluster already exists (",[35,899,900],{},"gcloud container clusters create-auto tools --region=us-central1 --project=my-tools-project","). Autopilot bills on the requests, so set them deliberately. It raises values below its minimums and fills in defaults for pods that don't set them.",[122,903,907],{"className":904,"code":905,"language":906,"meta":127,"style":127},"language-yaml shiki shiki-themes github-dark","# approvals.yaml\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: approvals\n  namespace: tools\n---\napiVersion: apps\u002Fv1\nkind: Deployment\nmetadata:\n  name: approvals\n  namespace: tools\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: approvals\n  template:\n    metadata:\n      labels:\n        app: approvals\n    spec:\n      serviceAccountName: approvals\n      containers:\n      - name: app\n        image: us-central1-docker.pkg.dev\u002Fmy-tools-project\u002Ftools\u002Fapprovals:1.4.2\n        ports:\n        - containerPort: 8080\n        resources:\n          requests:\n            cpu: 250m\n            memory: 512Mi\n        readinessProbe:\n          httpGet:\n            path: \u002Fhealthz\n            port: 8080\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: approvals\n  namespace: tools\nspec:\n  selector:\n    app: approvals\n  ports:\n  - port: 8080\n    targetPort: 8080\n---\napiVersion: networking.gke.io\u002Fv1\nkind: GCPBackendPolicy\nmetadata:\n  name: approvals-iap\n  namespace: tools\nspec:\n  default:\n    iap:\n      enabled: true\n      clientID: IAP_OAUTH_CLIENT_ID\n      oauth2ClientSecret:\n        name: approvals-iap-secret\n  targetRef:\n    group: \"\"\n    kind: Service\n    name: approvals\n","yaml",[35,908,909,914,926,936,944,954,964,969,978,987,993,1001,1009,1016,1026,1033,1040,1049,1056,1063,1070,1079,1086,1095,1102,1115,1125,1132,1145,1152,1159,1169,1179,1186,1193,1203,1212,1216,1224,1233,1239,1247,1255,1261,1267,1276,1283,1295,1304,1308,1317,1326,1332,1341,1349,1355,1363,1371,1382,1393,1401,1412,1420,1431,1441],{"__ignoreMap":127},[131,910,911],{"class":133,"line":134},[131,912,913],{"class":137},"# approvals.yaml\n",[131,915,916,920,923],{"class":133,"line":141},[131,917,919],{"class":918},"s4JwU","apiVersion",[131,921,922],{"class":177},": ",[131,924,925],{"class":163},"v1\n",[131,927,928,931,933],{"class":133,"line":147},[131,929,930],{"class":918},"kind",[131,932,922],{"class":177},[131,934,935],{"class":163},"ServiceAccount\n",[131,937,938,941],{"class":133,"line":153},[131,939,940],{"class":918},"metadata",[131,942,943],{"class":177},":\n",[131,945,946,949,951],{"class":133,"line":167},[131,947,948],{"class":918},"  name",[131,950,922],{"class":177},[131,952,953],{"class":163},"approvals\n",[131,955,956,959,961],{"class":133,"line":174},[131,957,958],{"class":918},"  namespace",[131,960,922],{"class":177},[131,962,963],{"class":163},"tools\n",[131,965,966],{"class":133,"line":188},[131,967,968],{"class":278},"---\n",[131,970,971,973,975],{"class":133,"line":199},[131,972,919],{"class":918},[131,974,922],{"class":177},[131,976,977],{"class":163},"apps\u002Fv1\n",[131,979,980,982,984],{"class":133,"line":210},[131,981,930],{"class":918},[131,983,922],{"class":177},[131,985,986],{"class":163},"Deployment\n",[131,988,989,991],{"class":133,"line":226},[131,990,940],{"class":918},[131,992,943],{"class":177},[131,994,995,997,999],{"class":133,"line":237},[131,996,948],{"class":918},[131,998,922],{"class":177},[131,1000,953],{"class":163},[131,1002,1003,1005,1007],{"class":133,"line":251},[131,1004,958],{"class":918},[131,1006,922],{"class":177},[131,1008,963],{"class":163},[131,1010,1011,1014],{"class":133,"line":262},[131,1012,1013],{"class":918},"spec",[131,1015,943],{"class":177},[131,1017,1018,1021,1023],{"class":133,"line":267},[131,1019,1020],{"class":918},"  replicas",[131,1022,922],{"class":177},[131,1024,1025],{"class":156},"1\n",[131,1027,1028,1031],{"class":133,"line":306},[131,1029,1030],{"class":918},"  selector",[131,1032,943],{"class":177},[131,1034,1035,1038],{"class":133,"line":327},[131,1036,1037],{"class":918},"    matchLabels",[131,1039,943],{"class":177},[131,1041,1042,1045,1047],{"class":133,"line":332},[131,1043,1044],{"class":918},"      app",[131,1046,922],{"class":177},[131,1048,953],{"class":163},[131,1050,1051,1054],{"class":133,"line":359},[131,1052,1053],{"class":918},"  template",[131,1055,943],{"class":177},[131,1057,1058,1061],{"class":133,"line":364},[131,1059,1060],{"class":918},"    metadata",[131,1062,943],{"class":177},[131,1064,1065,1068],{"class":133,"line":370},[131,1066,1067],{"class":918},"      labels",[131,1069,943],{"class":177},[131,1071,1072,1075,1077],{"class":133,"line":420},[131,1073,1074],{"class":918},"        app",[131,1076,922],{"class":177},[131,1078,953],{"class":163},[131,1080,1081,1084],{"class":133,"line":444},[131,1082,1083],{"class":918},"    spec",[131,1085,943],{"class":177},[131,1087,1088,1091,1093],{"class":133,"line":466},[131,1089,1090],{"class":918},"      serviceAccountName",[131,1092,922],{"class":177},[131,1094,953],{"class":163},[131,1096,1097,1100],{"class":133,"line":472},[131,1098,1099],{"class":918},"      containers",[131,1101,943],{"class":177},[131,1103,1104,1107,1110,1112],{"class":133,"line":477},[131,1105,1106],{"class":177},"      - ",[131,1108,1109],{"class":918},"name",[131,1111,922],{"class":177},[131,1113,1114],{"class":163},"app\n",[131,1116,1117,1120,1122],{"class":133,"line":483},[131,1118,1119],{"class":918},"        image",[131,1121,922],{"class":177},[131,1123,1124],{"class":163},"us-central1-docker.pkg.dev\u002Fmy-tools-project\u002Ftools\u002Fapprovals:1.4.2\n",[131,1126,1127,1130],{"class":133,"line":503},[131,1128,1129],{"class":918},"        ports",[131,1131,943],{"class":177},[131,1133,1134,1137,1140,1142],{"class":133,"line":517},[131,1135,1136],{"class":177},"        - ",[131,1138,1139],{"class":918},"containerPort",[131,1141,922],{"class":177},[131,1143,1144],{"class":156},"8080\n",[131,1146,1147,1150],{"class":133,"line":532},[131,1148,1149],{"class":918},"        resources",[131,1151,943],{"class":177},[131,1153,1154,1157],{"class":133,"line":547},[131,1155,1156],{"class":918},"          requests",[131,1158,943],{"class":177},[131,1160,1161,1164,1166],{"class":133,"line":561},[131,1162,1163],{"class":918},"            cpu",[131,1165,922],{"class":177},[131,1167,1168],{"class":163},"250m\n",[131,1170,1171,1174,1176],{"class":133,"line":569},[131,1172,1173],{"class":918},"            memory",[131,1175,922],{"class":177},[131,1177,1178],{"class":163},"512Mi\n",[131,1180,1181,1184],{"class":133,"line":577},[131,1182,1183],{"class":918},"        readinessProbe",[131,1185,943],{"class":177},[131,1187,1188,1191],{"class":133,"line":592},[131,1189,1190],{"class":918},"          httpGet",[131,1192,943],{"class":177},[131,1194,1195,1198,1200],{"class":133,"line":607},[131,1196,1197],{"class":918},"            path",[131,1199,922],{"class":177},[131,1201,1202],{"class":163},"\u002Fhealthz\n",[131,1204,1205,1208,1210],{"class":133,"line":615},[131,1206,1207],{"class":918},"            port",[131,1209,922],{"class":177},[131,1211,1144],{"class":156},[131,1213,1214],{"class":133,"line":623},[131,1215,968],{"class":278},[131,1217,1218,1220,1222],{"class":133,"line":629},[131,1219,919],{"class":918},[131,1221,922],{"class":177},[131,1223,925],{"class":163},[131,1225,1226,1228,1230],{"class":133,"line":634},[131,1227,930],{"class":918},[131,1229,922],{"class":177},[131,1231,1232],{"class":163},"Service\n",[131,1234,1235,1237],{"class":133,"line":640},[131,1236,940],{"class":918},[131,1238,943],{"class":177},[131,1240,1241,1243,1245],{"class":133,"line":678},[131,1242,948],{"class":918},[131,1244,922],{"class":177},[131,1246,953],{"class":163},[131,1248,1249,1251,1253],{"class":133,"line":683},[131,1250,958],{"class":918},[131,1252,922],{"class":177},[131,1254,963],{"class":163},[131,1256,1257,1259],{"class":133,"line":689},[131,1258,1013],{"class":918},[131,1260,943],{"class":177},[131,1262,1263,1265],{"class":133,"line":709},[131,1264,1030],{"class":918},[131,1266,943],{"class":177},[131,1268,1269,1272,1274],{"class":133,"line":730},[131,1270,1271],{"class":918},"    app",[131,1273,922],{"class":177},[131,1275,953],{"class":163},[131,1277,1278,1281],{"class":133,"line":746},[131,1279,1280],{"class":918},"  ports",[131,1282,943],{"class":177},[131,1284,1285,1288,1291,1293],{"class":133,"line":752},[131,1286,1287],{"class":177},"  - ",[131,1289,1290],{"class":918},"port",[131,1292,922],{"class":177},[131,1294,1144],{"class":156},[131,1296,1297,1300,1302],{"class":133,"line":757},[131,1298,1299],{"class":918},"    targetPort",[131,1301,922],{"class":177},[131,1303,1144],{"class":156},[131,1305,1306],{"class":133,"line":763},[131,1307,968],{"class":278},[131,1309,1310,1312,1314],{"class":133,"line":778},[131,1311,919],{"class":918},[131,1313,922],{"class":177},[131,1315,1316],{"class":163},"networking.gke.io\u002Fv1\n",[131,1318,1319,1321,1323],{"class":133,"line":799},[131,1320,930],{"class":918},[131,1322,922],{"class":177},[131,1324,1325],{"class":163},"GCPBackendPolicy\n",[131,1327,1328,1330],{"class":133,"line":816},[131,1329,940],{"class":918},[131,1331,943],{"class":177},[131,1333,1334,1336,1338],{"class":133,"line":830},[131,1335,948],{"class":918},[131,1337,922],{"class":177},[131,1339,1340],{"class":163},"approvals-iap\n",[131,1342,1343,1345,1347],{"class":133,"line":836},[131,1344,958],{"class":918},[131,1346,922],{"class":177},[131,1348,963],{"class":163},[131,1350,1351,1353],{"class":133,"line":841},[131,1352,1013],{"class":918},[131,1354,943],{"class":177},[131,1356,1358,1361],{"class":133,"line":1357},56,[131,1359,1360],{"class":918},"  default",[131,1362,943],{"class":177},[131,1364,1366,1369],{"class":133,"line":1365},57,[131,1367,1368],{"class":918},"    iap",[131,1370,943],{"class":177},[131,1372,1374,1377,1379],{"class":133,"line":1373},58,[131,1375,1376],{"class":918},"      enabled",[131,1378,922],{"class":177},[131,1380,1381],{"class":156},"true\n",[131,1383,1385,1388,1390],{"class":133,"line":1384},59,[131,1386,1387],{"class":918},"      clientID",[131,1389,922],{"class":177},[131,1391,1392],{"class":163},"IAP_OAUTH_CLIENT_ID\n",[131,1394,1396,1399],{"class":133,"line":1395},60,[131,1397,1398],{"class":918},"      oauth2ClientSecret",[131,1400,943],{"class":177},[131,1402,1404,1407,1409],{"class":133,"line":1403},61,[131,1405,1406],{"class":918},"        name",[131,1408,922],{"class":177},[131,1410,1411],{"class":163},"approvals-iap-secret\n",[131,1413,1415,1418],{"class":133,"line":1414},62,[131,1416,1417],{"class":918},"  targetRef",[131,1419,943],{"class":177},[131,1421,1423,1426,1428],{"class":133,"line":1422},63,[131,1424,1425],{"class":918},"    group",[131,1427,922],{"class":177},[131,1429,1430],{"class":163},"\"\"\n",[131,1432,1434,1437,1439],{"class":133,"line":1433},64,[131,1435,1436],{"class":918},"    kind",[131,1438,922],{"class":177},[131,1440,1232],{"class":163},[131,1442,1444,1447,1449],{"class":133,"line":1443},65,[131,1445,1446],{"class":918},"    name",[131,1448,922],{"class":177},[131,1450,953],{"class":163},[11,1452,1453,1454,1457,1458,1461,1462,1465,1466,1469],{},"That's still not the whole thing. IAP on GKE rides on a load balancer, so you also need a Gateway (",[35,1455,1456],{},"gatewayClassName: gke-l7-global-external-managed",", which Autopilot always has enabled) with an HTTPS listener and certificate, an HTTPRoute pointing at the Service, a ",[35,1459,1460],{},"HealthCheckPolicy"," that sends the load balancer's health check to ",[35,1463,1464],{},"\u002Fhealthz"," (unlike Ingress, Gateway doesn't read the readiness probe and checks ",[35,1467,1468],{},"\u002F"," by default), an OAuth client ID and a Kubernetes Secret for it, and IAM grants for the pod's Kubernetes service account (Workload Identity Federation for GKE) for any Google API calls. Every one of those is a thing someone has to understand when it breaks. Compare that with steps 4 and 5 of the Cloud Run script.",[15,1471,1473],{"id":1472},"weigh-the-teams-kubernetes-fluency-honestly","Weigh the team's Kubernetes fluency honestly",[11,1475,1476],{},"A GKE cluster is not free to operate even when Google handles the control plane and, with Autopilot, the nodes. Someone owns namespace and RBAC design, someone reviews the Helm chart or Kustomize overlay when it needs a change, and someone gets paged when a pod's readiness probe starts flapping. For a team that already runs GKE for the product's primary workloads, adding one more small internal tool to the existing cluster costs little extra: no new cluster fee, and the operational muscle already exists. For a team whose primary stack doesn't touch Kubernetes, standing up a cluster (or borrowing a namespace in someone else's) purely to host one internal dashboard buys a maintenance burden that outlives the tool's original author.",[15,1478,1480],{"id":1479},"the-decision-in-practice","The decision in practice",[1482,1483,1484,1500],"table",{},[1485,1486,1487],"thead",{},[1488,1489,1490,1494,1497],"tr",{},[1491,1492,1493],"th",{},"Question",[1491,1495,1496],{},"Leans Cloud Run",[1491,1498,1499],{},"Leans GKE Autopilot",[1501,1502,1503,1515,1526,1543,1554,1565],"tbody",{},[1488,1504,1505,1509,1512],{},[1506,1507,1508],"td",{},"Traffic",[1506,1510,1511],{},"Bursty, idle most of the day",[1506,1513,1514],{},"Busy all day; scale-to-zero never triggers",[1488,1516,1517,1520,1523],{},[1506,1518,1519],{},"Shape",[1506,1521,1522],{},"One service, sidecars at most",[1506,1524,1525],{},"Several services deployed and versioned together",[1488,1527,1528,1531,1537],{},[1506,1529,1530],{},"Employee sign-in",[1506,1532,1533,1534,1536],{},"IAP with ",[35,1535,90],{},", no load balancer",[1506,1538,1539,1540],{},"IAP through Gateway and ",[35,1541,1542],{},"GCPBackendPolicy",[1488,1544,1545,1548,1551],{},[1506,1546,1547],{},"Egress",[1506,1549,1550],{},"Direct VPC egress, Cloud NAT for a fixed IP",[1506,1552,1553],{},"Cluster networking you already run",[1488,1555,1556,1559,1562],{},[1506,1557,1558],{},"Team",[1506,1560,1561],{},"No one runs Kubernetes day to day",[1506,1563,1564],{},"Existing cluster and on-call rotation",[1488,1566,1567,1570,1575],{},[1506,1568,1569],{},"Cost floor",[1506,1571,1572,1573],{},"Zero with ",[35,1574,885],{},[1506,1576,1577],{},"Cluster fee (first cluster covered by free tier) plus 24\u002F7 pod requests",[11,1579,1580,1581,1584],{},"For a tool with one deployable unit, event-driven or bursty traffic, and no team already running Kubernetes day to day, Cloud Run wins on cost, deploy simplicity, and blast radius: a bad deploy is one ",[35,1582,1583],{},"gcloud run services update-traffic"," away from the previous revision. For a tool that's genuinely multi-service, needs to share infrastructure with workloads already on GKE, or has sustained enough traffic that scale-to-zero never kicks in, GKE's marginal cost is lower than it looks and the architectural flexibility is worth having. Most internal tools, in my experience, are closer to the first case than teams initially assume. The instinct to reach for Kubernetes because \"that's what we use for everything else\" is worth questioning every time a new tool is this small.",[15,1586,1588],{"id":1587},"references","References",[58,1590,1591,1600,1607,1614,1621,1628,1635,1642,1649,1656],{},[61,1592,1593],{},[1594,1595,1599],"a",{"href":1596,"rel":1597},"https:\u002F\u002Fdocs.cloud.google.com\u002Frun\u002Fdocs\u002Fconfiguring\u002Fbilling-settings",[1598],"nofollow","Cloud Run billing settings (request-based vs instance-based)",[61,1601,1602],{},[1594,1603,1606],{"href":1604,"rel":1605},"https:\u002F\u002Fdocs.cloud.google.com\u002Frun\u002Fdocs\u002Fconfiguring\u002Fmin-instances",[1598],"Cloud Run minimum instances",[61,1608,1609],{},[1594,1610,1613],{"href":1611,"rel":1612},"https:\u002F\u002Fdocs.cloud.google.com\u002Frun\u002Fdocs\u002Fdeploying#sidecars",[1598],"Deploying multi-container (sidecar) services to Cloud Run",[61,1615,1616],{},[1594,1617,1620],{"href":1618,"rel":1619},"https:\u002F\u002Fdocs.cloud.google.com\u002Frun\u002Fdocs\u002Fconfiguring\u002Fvpc-direct-vpc",[1598],"Direct VPC egress for Cloud Run",[61,1622,1623],{},[1594,1624,1627],{"href":1625,"rel":1626},"https:\u002F\u002Fdocs.cloud.google.com\u002Frun\u002Fdocs\u002Fsecuring\u002Fingress",[1598],"Cloud Run ingress settings",[61,1629,1630],{},[1594,1631,1634],{"href":1632,"rel":1633},"https:\u002F\u002Fdocs.cloud.google.com\u002Frun\u002Fdocs\u002Fsecuring\u002Fidentity-aware-proxy-cloud-run",[1598],"Configure IAP for Cloud Run",[61,1636,1637],{},[1594,1638,1641],{"href":1639,"rel":1640},"https:\u002F\u002Fcloud.google.com\u002Fkubernetes-engine\u002Fpricing",[1598],"GKE pricing (cluster management fee, free tier, Autopilot billing)",[61,1643,1644],{},[1594,1645,1648],{"href":1646,"rel":1647},"https:\u002F\u002Fdocs.cloud.google.com\u002Fkubernetes-engine\u002Fdocs\u002Fconcepts\u002Fautopilot-overview",[1598],"GKE Autopilot overview",[61,1650,1651],{},[1594,1652,1655],{"href":1653,"rel":1654},"https:\u002F\u002Fdocs.cloud.google.com\u002Fkubernetes-engine\u002Fdocs\u002Fhow-to\u002Fconfigure-gateway-resources",[1598],"Configure Gateway resources (GCPBackendPolicy and IAP)",[61,1657,1658],{},[1594,1659,1662],{"href":1660,"rel":1661},"https:\u002F\u002Fkubernetes.io\u002Fdocs\u002Ftasks\u002Frun-application\u002Fhorizontal-pod-autoscale\u002F",[1598],"Kubernetes Horizontal Pod Autoscaling",[1664,1665,1666],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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 .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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}",{"title":127,"searchDepth":141,"depth":141,"links":1668},[1669,1670,1671,1672,1673,1674,1675],{"id":17,"depth":141,"text":18},{"id":52,"depth":141,"text":53},{"id":97,"depth":141,"text":98},{"id":893,"depth":141,"text":894},{"id":1472,"depth":141,"text":1473},{"id":1479,"depth":141,"text":1480},{"id":1587,"depth":141,"text":1588},"techcolumnist",[1678],"engineering","2026-05-06T14:00:00Z","How to pick GKE Autopilot or Cloud Run for an internal tool: billing models, sidecars, IAP, VPC egress, and team fluency, with deploy scripts for both.","md",false,null,{},"\u002Fblog\u002F2026\u002F05\u002F06\u002Fgoogle-cloud-choosing-between-gke-and-cloud-run-for-an-internal-tool",{"title":6,"description":1680},[1676],"blog\u002F2026\u002F05\u002F06\u002Fgoogle-cloud-choosing-between-gke-and-cloud-run-for-an-internal-tool",[279,1690],"kubernetes","\u002F2026\u002F05\u002F06\u002Fgoogle-cloud-choosing-between-gke-and-cloud-run-for-an-internal-tool\u002F","qU57h-Tv4qknvzHtbSI7KRmgF3iwZak8bmPb6d-hi94",{"title":1694,"description":1695,"date":1696,"url":1697,"categories":1698,"tags":1699,"image":1683,"readingTime":188,"canonical":1676,"sites":1700,"series":1683,"seriesOrder":1683},"Google Cloud: Organization Policy Constraints That Actually Matter","Which Google Cloud organization policy constraints prevent real incidents, with policy YAML, the dry-run workflow, tag-based exemptions, and an audit script.","2026-05-13T14:00:00Z","\u002F2026\u002F05\u002F13\u002Fgoogle-cloud-organization-policy-constraints-that-actually-matter\u002F",[1678],[279],[1676],{"title":1702,"description":1703,"date":1704,"url":1705,"categories":1706,"tags":1707,"image":1683,"readingTime":199,"canonical":1676,"sites":1711,"series":1683,"seriesOrder":1683},"Azure: What We Got Wrong Moving File Servers to Azure Files","Latency, AD DS Kerberos, and cloud tiering mistakes from moving file servers to Azure Files, with the robocopy migration script and az CLI setup we use now.","2026-04-22T14:00:00Z","\u002F2026\u002F04\u002F22\u002Fazure-what-we-got-wrong-moving-file-servers-to-azure-files\u002F",[1678],[1708,1709,1710],"azure","active-directory","dfs",[1676],[1713,1724,1732],{"title":1714,"description":1715,"date":1716,"url":1717,"categories":1718,"tags":1720,"image":1683,"readingTime":167,"canonical":1676,"sites":1723,"series":1683,"seriesOrder":1683},"Hyper-V to Talos: Rethinking the Virtualization Stack","Why moving workloads off Hyper-V VMs onto a Talos Kubernetes cluster changed the whole stack, with an inventory script and a VM-to-manifest example.","2026-09-16T14:00:00Z","\u002F2026\u002F09\u002F16\u002Fhyper-v-to-talos-rethinking-the-virtualization-stack\u002F",[1678,1719],"strategy",[1721,1722,1690],"hyper-v","talos",[1676],{"title":1725,"description":1726,"date":1727,"url":1728,"categories":1729,"tags":1730,"image":1683,"readingTime":174,"canonical":1676,"sites":1731,"series":1683,"seriesOrder":1683},"Google Cloud: IAM Least Privilege in Practice, Not Just in Theory","Running least privilege in a real Google Cloud org: finding basic roles, working the IAM recommender, custom roles, temporary access, and service account risk.","2026-09-09T14:00:00Z","\u002F2026\u002F09\u002F09\u002Fgoogle-cloud-iam-least-privilege-in-practice-not-just-in-theory\u002F",[1678],[279],[1676],{"title":1733,"description":1734,"date":1735,"url":1736,"categories":1737,"tags":1738,"image":1683,"readingTime":188,"canonical":1676,"sites":1739,"series":1683,"seriesOrder":1683},"Talos Linux: Upgrading a Cluster Without a Maintenance Window","Rolling Talos and Kubernetes upgrades through a cluster one node at a time: preflight checks, etcd snapshots, Image Factory installers, and scripts.","2026-08-26T14:00:00Z","\u002F2026\u002F08\u002F26\u002Ftalos-linux-upgrading-a-cluster-without-a-maintenance-window\u002F",[1678],[1722,1690],[1676],{"doc":1683,"posts":1741},[],1790052513915]