Every team eventually inherits autoscaling configuration it didn't write. The author left, the Terraform was half-applied, someone tuned a threshold in the console during an incident two years ago, and now the scaling behavior of production is defined by an unknowable merge of code, clicks, and defaults. Everyone is afraid to touch it, which is exactly how it stays wrong.
The technical trap that makes inherited autoscaling worse than other inherited config is the silent overwrite: autoscaling policies are one of the few places where two writers routinely fight without either noticing.
The silent overwrite, per platform
AWS ASGs and ECS services. Scaling policies live as their own API objects (ASG scaling policies; Application Auto Scaling targets and policies for ECS). If Terraform or CloudFormation defines them, the next apply reverts every console tweak made since, silently, including the incident-era threshold change that was load-bearing. The reverse also happens: policies created by hand in the console are invisible to the code, survive until someone "cleans up drift", and vanish. Either way, the system's real behavior changes with no deploy, no PR, and no alert.
Azure VMSS. Autoscale is a separate autoscaleSettings resource attached to the scale set, with profiles and rules inside it. Portal edits modify that resource in place; the next ARM/Bicep/Terraform deployment that also defines it replaces the whole object, profiles and all. Because it's one resource, you don't lose a rule; you lose the entire tuned profile set at once.
GCP MIGs. The autoscaler is likewise its own object attached to the managed instance group. gcloud edits and Terraform definitions overwrite each other whole, and a deleted-then-recreated MIG quietly comes back with whatever the code says, not what the console said.
The pattern is identical everywhere: the autoscaling policy is a separate object with multiple possible writers and last-writer-wins semantics. Nothing merges. Nothing warns.
The adoption runbook
1. Inventory every policy that exists right now. Not what the code says; what the API says:
aws autoscaling describe-policies --query 'ScalingPolicies[].[AutoScalingGroupName,PolicyName,PolicyType]' --output table
aws application-autoscaling describe-scaling-policies --service-namespace ecs
az monitor autoscale list -o table
gcloud compute instance-groups managed list --format="table(name,zone,autoscaler)"
2. Freeze before you fix. The worst adoption mistake is tuning while two writers still exist. First decide the single source of truth (almost always the IaC), then export the live state into it verbatim, warts included, so the first apply after adoption is a no-op. Import, don't rewrite: terraform import (or the ARM/gcloud equivalent) on the live policy objects, then diff until plan shows zero changes. Now the incident-era tweak from two years ago is in code with a commit message, instead of being a landmine.
3. Audit the now-visible config. With one honest copy, the standard smells are quick to check, and inherited policies reliably have several:
- Cooldowns under 120 seconds: the group reacts to its own noise, scaling up and down in oscillation, which costs money on the way up and availability on the way down.
- Targets above 90%: scaling triggers so late the new capacity arrives after the damage; effectively a post-incident notification system.
- Targets below 30%: perpetual over-provisioning wearing an autoscaling costume.
- min equal to max: not autoscaling at all; a fixed fleet with extra steps, worth making explicit.
- No policy on a group that clearly expected one: the scaling that everyone assumes exists and doesn't.
- Step policies stacked with target tracking on the same metric: two controllers steering one wheel.
4. Enforce one writer, then alert on the other. After adoption, out-of-band changes are the enemy. CloudTrail (PutScalingPolicy, PutAutoScalingPolicy), Azure activity logs, and GCP audit logs all record policy writes; a simple alert on policy-write events that didn't come from the deploy pipeline's principal turns the next silent overwrite into a loud one. Incident-time tuning stays possible; it just arrives with a follow-up task to codify or revert, instead of becoming un-owned state.
5. Only now, tune. Changes go one variable at a time, through the pipeline, watched across a full traffic cycle. Inherited systems punish batch tuning: if you change the target, the cooldown, and the max in one apply, and behavior degrades, you've learned nothing except fear.
The ownership rule that prevents the next inheritance
Autoscaling config decays into archaeology because it has no owner, and it has no owner because it sits between platform and application teams. The fix is boring and organizational: every scaling policy lives in code, next to the service it scales, owned by the team that gets paged for that service, reviewed like any other change. The console is for reading. The one exception (break-glass tuning during an incident) is legitimate exactly once per incident, and the audit alert from step 4 is what guarantees the exception gets folded back instead of becoming the next decade's mystery threshold.
FAQ
Why did my autoscaling behavior change with no deployment?
Almost always a second writer: an IaC apply reverted a console-made change (or someone's console edit overwrote what the code had set). Scaling policies are standalone objects with last-writer-wins semantics on every major platform, so two writers produce silent flip-flops. Check CloudTrail or your platform's activity log for policy-write events and their principals.
How do I take over autoscaling configuration safely?
Inventory the live policies from the API, pick one source of truth, and import the live state into it verbatim until a plan/diff shows zero changes. Only after the no-op point do you start tuning, one variable at a time. Adopting by rewriting (instead of importing) is how load-bearing incident-era tweaks get destroyed.
What are reasonable autoscaling targets and cooldowns?
Most services live between 40% and 70% target utilization: high enough to be efficient, low enough that scaling completes before saturation. Cooldowns (or stabilization windows) shorter than about two minutes usually cause oscillation because they're faster than the metric and provisioning loop they react to. Above 90% or below 30%, the policy is either a late alarm or standing over-provisioning.
Is min equal to max ever legitimate?
Occasionally, deliberately: a fixed fleet that wants instance-replacement semantics without elasticity (or a temporary pin during an incident). The problem is when it's accidental or forgotten, because everyone downstream assumes elasticity exists. If a group is intentionally pinned, name it in the code with a comment and an expiry, so it reads as a decision rather than drift.
Top comments (0)