Actor audit on cluster lifecycle + auto-stop config¶
Motivation¶
The Azure Activity Log shows that AKS stop/start was performed by the dashboard's managed identity but not WHO inside the dashboard triggered it. To answer "who stopped this cluster" / "who turned auto-stop off" without grepping unstructured console logs, we now emit a structured App Insights customEvent at each action site, with the actor explicitly recorded.
User-facing change¶
- Every
POST /api/aks/{start,stop,scale,delete}emits acluster_lifecyclecustomEvent:{action, actor="user", actor_oid, cluster, resource_group, task_id, …}. PUT /api/aks/autostopemits anautostop_configcustomEvent with the new state AND the previous state (enabled/idle_minutes), so you can answer "who flipped it off" or "who shortened the idle window" directly.- The system auto-stop path (
auto_stop_aksCelery task) emits acluster_lifecycleevent withactor="system:auto-stop"and the evaluator reason (e.g.idle:120m) — it carries noactor_oid, so manual vs automatic stops cannot be confused.
Design¶
- Reuses the existing
record_feature_eventhelper (App Insights customEvents viamicrosoft.custom_event.name). No new sink, no new env var, no new dependency. Telemetry-off deployments still see the records in stdout logs. - The route layer is the only place that knows the authenticated caller, so the event is emitted there (not inside the Celery task) — the task name carries the action and the event carries the actor.
- The system path emits its own event after the
stop_aks.runcall succeeds, so a system stop is recorded exactly once and a failed stop does not falsely attribute it. actoris a closed vocabulary today:"user"or"system:auto-stop". The literalsystem:prefix can never collide with a caller object_id (GUID).
KQL — who stopped it / who configured auto-stop¶
customEvents
| where name == "cluster_lifecycle"
| where customDimensions.action == "stop"
| project timestamp, actor=tostring(customDimensions.actor),
actor_oid=tostring(customDimensions.actor_oid),
cluster=tostring(customDimensions.cluster),
reason=tostring(customDimensions.reason)
customEvents
| where name == "autostop_config"
| project timestamp, actor_oid=tostring(customDimensions.actor_oid),
cluster=tostring(customDimensions.cluster),
enabled=tobool(customDimensions.enabled),
prev_enabled=tobool(customDimensions.prev_enabled),
idle_minutes=toint(customDimensions.idle_minutes),
prev_idle_minutes=toint(customDimensions.prev_idle_minutes)
API / IaC diff summary¶
api/routes/aks/lifecycle.py: emitcluster_lifecycleon start/stop/scale/delete.api/routes/aks/autostop.py: emitautostop_configon PUT, with prev/new diff.api/tasks/azure/idle_autostop.py: emitcluster_lifecyclewithactor=system:auto-stopafter the system stop succeeds.- No frontend / IaC change. Telemetry sink is the same
api.eventslogger that the rest of the codebase uses.
Validation evidence¶
uv run pytest -q api/tests/test_actor_audit.py— 5 passed (start/stop/scale user actor, autostop PUT diff, system actor without actor_oid).uv run ruff check api— all checks passed.uv run pytest -q api/tests— 4677 passed, 3 skipped, 0 failed.