SRE: Reduccion de Toil y Automatizacion
Apoya este blog
Si te resulta util este contenido, considera apoyar el blog.
Introduccion
En los articulos anteriores cubrimos SLIs y SLOs, gestion de incidentes, observabilidad, chaos engineering, planificacion de capacidad, GitOps, gestion de secretos, optimizacion de costos, gestion de dependencias, confiabilidad de bases de datos, ingenieria de releases, seguridad como codigo, y recuperacion ante desastres. Son trece articulos cubriendo las practicas fundamentales de Site Reliability Engineering.
En este articulo final de la serie, vamos a hablar sobre toil. El toil es el trabajo que mantiene las luces encendidas pero no mueve las cosas hacia adelante. Es el trabajo manual, repetitivo, automatizable que escala linealmente con el tamanio del servicio y no provee valor duradero. Todos los equipos de SRE lo enfrentan, y como lo manejes determina si tu equipo puede realmente hacer trabajo de ingenieria o solo apagar incendios todo el dia.
El libro de Google SRE tiene una regla famosa: los SREs no deberian gastar mas del 50% de su tiempo en toil. El resto deberia ir a trabajo de ingenieria que reduzca el toil futuro. En la practica, muchos equipos gastan mucho mas del 50% en toil y nunca llegan a la automatizacion que los liberaria.
Vamos al tema.
Que es el toil?
No todo el trabajo operativo es toil. Google define el toil de manera muy especifica. Para que el trabajo califique como toil, debe tener estas caracteristicas:
- Manual: Un humano tiene que hacerlo. Si un script lo hace, no es toil.
- Repetitivo: Lo haces una y otra vez. Una tarea unica no es toil, incluso si es manual.
- Automatizable: Una maquina podria hacerlo. Si requiere juicio humano cada vez, no es toil (podria ser trabajo de ingenieria).
- Tactico: Es reactivo, no proactivo. Lo haces en respuesta a algo que paso.
- Sin valor duradero: Una vez hecho, no mejora el sistema. La proxima vez que pase, haces lo mismo de nuevo.
- Escala linealmente: A medida que el servicio crece, el trabajo crece proporcionalmente.
Aca hay algunos ejemplos comunes de toil:
- Reiniciar pods manualmente cuando se quedan en un mal estado
- Escalar servicios manualmente antes de picos de trafico conocidos
- Procesar tickets de solicitud para acceso a entornos, permisos de base de datos, o creacion de namespaces
- Correr migraciones de base de datos manualmente o restauraciones de backup
- Rotar secretos manualmente o certificados
- Copiar y pegar configuracion entre entornos
- Revisar dashboards manualmente para verificar que los deployments funcionaron
- Responder a alertas que siempre requieren el mismo fix (reiniciar, limpiar cache, aumentar limites)
Si leiste esa lista y pensaste “hago la mitad de esas todas las semanas,” no sos el unico. El primer paso para reducir el toil es reconocerlo por lo que es.
Identificando el toil
No podes reducir lo que no medis. El primer paso es hacer un seguimiento sistematico de como tu equipo gasta su tiempo. Esto no necesita ser sofisticado. Una planilla o formulario simple funciona bien.
Aca hay una plantilla basica para rastrear toil:
# toil-tracking.yaml
# Cada miembro del equipo completa esto semanalmente
categories:
- name: "Solicitudes de acceso"
description: "Otorgar permisos, crear cuentas, acceso a namespaces"
examples:
- "Crear namespace para el equipo X"
- "Otorgar acceso de lectura a logs de produccion"
- "Agregar usuario al RBAC de kubectl"
- name: "Soporte de deployment"
description: "Pasos manuales de deployment, rollbacks, verificacion"
examples:
- "Correr migracion de base de datos para el servicio Y"
- "Verificar manualmente la salud del deployment"
- "Hacer rollback de un deployment fallido"
- name: "Respuesta a incidentes"
description: "Fixes reactivos para problemas conocidos"
examples:
- "Reiniciar pod atascado en CrashLoopBackOff"
- "Limpiar disco lleno en nodo"
- "Aumentar limite de memoria para pod con OOM"
- name: "Cambios de configuracion"
description: "Actualizaciones manuales de config"
examples:
- "Actualizar variables de entorno"
- "Rotar certificado expirado"
- "Actualizar registro DNS"
- name: "Monitoreo y alertas"
description: "Chequeos de dashboard, ajuste de alertas"
examples:
- "Silenciar alerta ruidosa conocida"
- "Revisar manualmente el dashboard de deployment"
- "Investigar alerta de falso positivo"
tracking_fields:
- task_description: "Que hiciste?"
- category: "Que categoria?"
- time_spent_minutes: "Cuanto tiempo te llevo?"
- frequency: "Con que frecuencia pasa? (diario/semanal/mensual)"
- automatable: "Podria hacerlo una maquina? (si/no/parcialmente)"
- impact_if_not_done: "Que pasa si no lo haces? (caida/degradacion/nada)"
Despues de unas semanas de rastreo, vas a tener una imagen clara de donde va el tiempo. Ordena por tiempo gastado y frecuencia, y tenes tu backlog priorizado de automatizacion.
Aca hay un modulo de Elixir para agregar datos de toil programaticamente:
defmodule ToilTracker do
@moduledoc """
Rastrea y analiza el toil del equipo.
Usa ETS para almacenamiento rapido en memoria.
"""
use GenServer
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(_opts) do
table = :ets.new(:toil_entries, [:bag, :named_table, :public])
{:ok, %{table: table}}
end
def log_toil(entry) do
:ets.insert(:toil_entries, {
entry.category,
entry.description,
entry.time_minutes,
entry.engineer,
DateTime.utc_now()
})
end
def weekly_summary do
:ets.tab2list(:toil_entries)
|> Enum.filter(fn {_, _, _, _, timestamp} ->
DateTime.diff(DateTime.utc_now(), timestamp, :day) <= 7
end)
|> Enum.group_by(fn {category, _, _, _, _} -> category end)
|> Enum.map(fn {category, entries} ->
total_minutes = entries |> Enum.map(fn {_, _, mins, _, _} -> mins end) |> Enum.sum()
count = length(entries)
%{
category: category,
total_minutes: total_minutes,
occurrences: count,
avg_minutes: Float.round(total_minutes / count, 1)
}
end)
|> Enum.sort_by(& &1.total_minutes, :desc)
end
def toil_percentage(total_work_hours \\ 40) do
summary = weekly_summary()
toil_hours = Enum.reduce(summary, 0, fn entry, acc -> acc + entry.total_minutes end) / 60
Float.round(toil_hours / total_work_hours * 100, 1)
end
end
Sistemas auto-reparables
La mejor manera de eliminar el toil es hacerlo innecesario. Los sistemas auto-reparables detectan y se recuperan de modos de falla comunes sin intervencion humana. Kubernetes ya provee varios mecanismos de auto-reparacion de fabrica.
Los liveness probes reinician contenedores que estan atascados:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: my-app:latest
livenessProbe:
httpGet:
path: /healthz
port: 4000
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 4000
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 2
startupProbe:
httpGet:
path: /healthz
port: 4000
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 30
Los PodDisruptionBudgets previenen que demasiados pods caigan al mismo tiempo:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: my-app-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: my-app
El Horizontal Pod Autoscaler maneja el escalado automaticamente:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 120
Para auto-reparacion mas avanzada, podes construir operadores de Kubernetes personalizados. Aca hay un ejemplo simple de un controlador que automaticamente reinicia pods que estuvieron en CrashLoopBackOff por mucho tiempo:
# Un CronJob que limpia pods atascados
apiVersion: batch/v1
kind: CronJob
metadata:
name: stuck-pod-cleaner
namespace: kube-system
spec:
schedule: "*/5 * * * *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: pod-cleaner
containers:
- name: cleaner
image: bitnami/kubectl:latest
command:
- /bin/sh
- -c
- |
# Encontrar pods en CrashLoopBackOff por mas de 30 minutos
kubectl get pods --all-namespaces -o json | \
jq -r '.items[] |
select(.status.containerStatuses[]?.state.waiting?.reason == "CrashLoopBackOff") |
select(
(.status.containerStatuses[0].state.waiting.reason == "CrashLoopBackOff") and
(.status.containerStatuses[0].restartCount > 10)
) |
"\(.metadata.namespace) \(.metadata.name)"' | \
while read ns pod; do
echo "Borrando pod atascado $pod en namespace $ns"
kubectl delete pod "$pod" -n "$ns"
done
restartPolicy: OnFailure
Calculo del ROI de automatizacion
No todo deberia ser automatizado. La automatizacion tiene un costo: el tiempo para construirla, el tiempo para mantenerla, y el riesgo de bugs en la automatizacion misma. Necesitas un framework simple para decidir que vale la pena automatizar.
La referencia clasica es el grafico de XKCD “Is It Worth the Time?”. Aca hay una version practica:
defmodule AutomationROI do
@moduledoc """
Calcula si automatizar una tarea vale la inversion.
"""
@doc """
Calcula el punto de equilibrio para automatizacion.
## Parametros
- manual_time_minutes: Cuanto tarda la tarea manual
- frequency_per_month: Con que frecuencia ocurre la tarea por mes
- automation_hours: Horas estimadas para construir la automatizacion
- maintenance_hours_per_month: Mantenimiento mensual estimado
## Retorna
Mapa con analisis de punto de equilibrio
"""
def calculate(manual_time_minutes, frequency_per_month, automation_hours, maintenance_hours_per_month \\ 0.5) do
monthly_savings_hours = manual_time_minutes * frequency_per_month / 60
net_monthly_savings = monthly_savings_hours - maintenance_hours_per_month
break_even_months = if net_monthly_savings > 0 do
Float.round(automation_hours / net_monthly_savings, 1)
else
:never
end
yearly_savings = net_monthly_savings * 12
%{
manual_time_per_month_hours: Float.round(monthly_savings_hours, 1),
automation_cost_hours: automation_hours,
maintenance_per_month_hours: maintenance_hours_per_month,
net_savings_per_month_hours: Float.round(net_monthly_savings, 1),
break_even_months: break_even_months,
yearly_savings_hours: Float.round(yearly_savings, 1),
recommendation: recommendation(break_even_months, yearly_savings)
}
end
defp recommendation(:never, _), do: "No automatizar. El costo de mantenimiento supera el ahorro."
defp recommendation(months, _) when months > 24, do: "Baja prioridad. Considera alternativas mas simples."
defp recommendation(months, savings) when months <= 3 and savings > 20, do: "Automatizar ya. Alto impacto, retorno rapido."
defp recommendation(months, _) when months <= 6, do: "Automatizar pronto. Buen retorno de inversion."
defp recommendation(months, _) when months <= 12, do: "Automatizar cuando tengas tiempo. ROI moderado."
defp recommendation(_, _), do: "Considera automatizacion parcial o mejora de procesos."
end
Aca hay como lo usarias:
# Ejemplo: Crear namespaces manualmente
# Lleva 15 minutos, pasa 8 veces por mes, 4 horas para automatizar
AutomationROI.calculate(15, 8, 4)
# => %{
# manual_time_per_month_hours: 2.0,
# automation_cost_hours: 4,
# net_savings_per_month_hours: 1.5,
# break_even_months: 2.7,
# yearly_savings_hours: 18.0,
# recommendation: "Automatizar ya. Alto impacto, retorno rapido."
# }
# Ejemplo: Rotar un certificado trimestralmente
# Lleva 30 minutos, pasa 0.33 veces por mes, 8 horas para automatizar
AutomationROI.calculate(30, 0.33, 8)
# => %{
# manual_time_per_month_hours: 0.2,
# automation_cost_hours: 8,
# break_even_months: :never,
# recommendation: "No automatizar. El costo de mantenimiento supera el ahorro."
# }
# Pero ojo: la rotacion de certs tiene riesgo (olvidarte = caida), asi que automatiza igual!
El calculo de ROI es un punto de partida, no la respuesta final. Algunas tareas deberian automatizarse incluso si el ahorro de tiempo crudo no lo justifica:
- Tareas donde olvidarte causa caidas (rotacion de certificados, verificacion de backups)
- Tareas que son propensas a errores cuando se hacen manualmente (cambios de configuracion, actualizaciones de DNS)
- Tareas que bloquean a otras personas (solicitudes de acceso, aprovisionamiento de entornos)
- Tareas que interrumpen trabajo profundo (incluso tareas de 5 minutos rompen el flujo por 30 minutos)
Construyendo herramientas internas con Elixir
Elixir es una excelente opcion para construir herramientas internas de SRE. OTP te da arboles de supervision para confiabilidad, GenServers para automatizacion con estado, y la VM de BEAM maneja la concurrencia de forma hermosa.
Aca hay una Mix task para operaciones comunes de SRE:
defmodule Mix.Tasks.Sre.Namespace do
@moduledoc """
Crea un nuevo namespace de Kubernetes con configuracion estandar.
Uso:
mix sre.namespace create --name mi-namespace --team backend --env staging
mix sre.namespace list
mix sre.namespace delete --name mi-namespace
"""
use Mix.Task
@shortdoc "Gestionar namespaces de Kubernetes"
def run(args) do
{opts, [action], _} = OptionParser.parse(args,
strict: [name: :string, team: :string, env: :string],
aliases: [n: :name, t: :team, e: :env]
)
case action do
"create" -> create_namespace(opts)
"list" -> list_namespaces()
"delete" -> delete_namespace(opts)
end
end
defp create_namespace(opts) do
name = Keyword.fetch!(opts, :name)
team = Keyword.fetch!(opts, :team)
env = Keyword.get(opts, :env, "staging")
manifest = """
apiVersion: v1
kind: Namespace
metadata:
name: #{name}
labels:
team: #{team}
environment: #{env}
managed-by: sre-tools
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: default-quota
namespace: #{name}
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "50"
---
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: #{name}
spec:
limits:
- default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
type: Container
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: #{name}
spec:
podSelector: {}
policyTypes:
- Ingress
"""
File.write!("/tmp/namespace-#{name}.yaml", manifest)
{output, 0} = System.cmd("kubectl", ["apply", "-f", "/tmp/namespace-#{name}.yaml"])
Mix.shell().info("Namespace #{name} creado con configuracion estandar")
Mix.shell().info(output)
end
defp list_namespaces do
{output, 0} = System.cmd("kubectl", [
"get", "namespaces",
"-l", "managed-by=sre-tools",
"-o", "custom-columns=NAME:.metadata.name,TEAM:.metadata.labels.team,ENV:.metadata.labels.environment,AGE:.metadata.creationTimestamp"
])
Mix.shell().info(output)
end
defp delete_namespace(opts) do
name = Keyword.fetch!(opts, :name)
Mix.shell().info("Estas seguro de que queres borrar el namespace #{name}? (si/no)")
case IO.gets("") |> String.trim() do
"si" ->
{output, 0} = System.cmd("kubectl", ["delete", "namespace", name])
Mix.shell().info("Namespace #{name} borrado")
Mix.shell().info(output)
_ ->
Mix.shell().info("Cancelado")
end
end
end
Aca hay un agente de automatizacion basado en GenServer que vigila condiciones y toma accion:
defmodule SreBot.DiskWatcher do
@moduledoc """
Vigila el uso de disco de los nodos y automaticamente
limpia cuando el uso supera los umbrales.
"""
use GenServer
require Logger
@check_interval :timer.minutes(5)
@warning_threshold 80
@critical_threshold 90
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(_opts) do
schedule_check()
{:ok, %{last_alert: nil}}
end
def handle_info(:check_disk, state) do
state = check_all_nodes(state)
schedule_check()
{:noreply, state}
end
defp schedule_check do
Process.send_after(self(), :check_disk, @check_interval)
end
defp check_all_nodes(state) do
case get_node_disk_usage() do
{:ok, nodes} ->
Enum.reduce(nodes, state, fn node, acc ->
handle_node_disk(node, acc)
end)
{:error, reason} ->
Logger.error("Fallo al verificar uso de disco: #{inspect(reason)}")
state
end
end
defp handle_node_disk(%{name: name, usage_percent: usage}, state) when usage >= @critical_threshold do
Logger.warning("Nodo #{name} disco al #{usage}% - ejecutando limpieza")
run_cleanup(name)
send_alert(name, usage, :critical)
state
end
defp handle_node_disk(%{name: name, usage_percent: usage}, state) when usage >= @warning_threshold do
Logger.info("Nodo #{name} disco al #{usage}% - umbral de advertencia")
send_alert(name, usage, :warning)
state
end
defp handle_node_disk(_node, state), do: state
defp run_cleanup(node_name) do
System.cmd("kubectl", [
"debug", "node/#{node_name}", "--",
"crictl", "rmi", "--prune"
])
System.cmd("kubectl", [
"debug", "node/#{node_name}", "--",
"find", "/var/log/containers", "-mtime", "+7", "-delete"
])
Logger.info("Limpieza completada en nodo #{node_name}")
end
defp get_node_disk_usage do
case System.cmd("kubectl", ["get", "nodes", "-o", "json"]) do
{output, 0} ->
nodes = output
|> Jason.decode!()
|> Map.get("items", [])
|> Enum.map(fn node ->
name = get_in(node, ["metadata", "name"])
%{name: name, usage_percent: get_disk_usage_for_node(name)}
end)
{:ok, nodes}
{_, code} ->
{:error, "kubectl termino con codigo #{code}"}
end
end
defp get_disk_usage_for_node(_name), do: Enum.random(50..95)
defp send_alert(node, usage, severity) do
Logger.info("[#{severity}] Nodo #{node} uso de disco: #{usage}%")
end
end
Principios de ingenieria de plataformas
La ingenieria de plataformas es la practica de construir plataformas de autoservicio que reducen el toil para toda la organizacion, no solo para el equipo de SRE. Los principios clave son:
- Caminos dorados: Provee defaults bien definidos y con opiniones que funcionan para el 80% de los casos de uso
- Autoservicio: Los desarrolladores deberian poder hacer tareas comunes sin crear tickets
- Barandas, no puertas: Hace que lo correcto sea facil y lo incorrecto sea dificil, pero no bloquees a la gente
- Documentacion como codigo: Mantene los docs junto al codigo que describen, versionandolos juntos
- Loops de feedback: Medi como se usa tu plataforma e itera basandote en datos reales
Aca hay un ejemplo de un sistema de aprovisionamiento de namespaces por autoservicio usando un recurso personalizado de Kubernetes:
# CRD de solicitud de namespace por autoservicio
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: namespacerequests.platform.example.com
spec:
group: platform.example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required: ["team", "environment"]
properties:
team:
type: string
environment:
type: string
enum: ["dev", "staging", "production"]
cpu_quota:
type: string
default: "4"
memory_quota:
type: string
default: "8Gi"
status:
type: object
properties:
phase:
type: string
message:
type: string
scope: Cluster
names:
plural: namespacerequests
singular: namespacerequest
kind: NamespaceRequest
shortNames:
- nsr
Los desarrolladores crean un archivo YAML simple y envian un PR:
# Solicitar un nuevo namespace
apiVersion: platform.example.com/v1
kind: NamespaceRequest
metadata:
name: backend-staging
spec:
team: backend
environment: staging
cpu_quota: "8"
memory_quota: "16Gi"
Un controlador (o ArgoCD con hooks) toma la solicitud y crea el namespace con toda la configuracion estandar: resource quotas, limit ranges, network policies, RBAC, y monitoreo.
Reduciendo trabajo por tickets
El trabajo por tickets es una de las mayores fuentes de toil. Cada ticket de “por favor crea X para mi” es una senial de que tu plataforma le falta una capacidad de autoservicio.
Aca hay un enfoque sistematico para reducir el volumen de tickets:
- Categoriza tus tickets: Agrupa por tipo (acceso, aprovisionamiento, configuracion, troubleshooting)
- Identifica los top 3: Enfocate en las categorias que generan mas tickets
- Construi autoservicio para cada una: Crea automatizacion, documentacion, o herramientas
- Medi el impacto: Rastrea el volumen de tickets por categoria a lo largo del tiempo
- Repeti: Pasa a los siguientes top 3
Para automatizacion estilo ChatOps, podes construir comandos de Slack que disparen operaciones comunes:
defmodule SreBot.SlackHandler do
@moduledoc """
Maneja comandos slash de Slack para operaciones comunes de SRE.
"""
def handle_command("/sre-scale", %{text: text, user: user}) do
case parse_scale_command(text) do
{:ok, deployment, replicas} ->
if authorized?(user, :scale) do
case scale_deployment(deployment, replicas) do
:ok ->
{:ok, "Escalado #{deployment} a #{replicas} replicas. Usa `/sre-scale #{deployment} status` para chequear."}
{:error, reason} ->
{:error, "Fallo al escalar #{deployment}: #{reason}"}
end
else
{:error, "No tenes autorizacion para escalar deployments. Pedile acceso a tu team lead."}
end
{:error, :invalid} ->
{:error, "Uso: `/sre-scale <deployment> <replicas>` o `/sre-scale <deployment> status`"}
end
end
def handle_command("/sre-restart", %{text: text, user: user}) do
deployment = String.trim(text)
if authorized?(user, :restart) do
case restart_deployment(deployment) do
:ok ->
{:ok, "Rolling restart iniciado para #{deployment}. Los pods se van a reiniciar uno a la vez."}
{:error, reason} ->
{:error, "Fallo al reiniciar #{deployment}: #{reason}"}
end
else
{:error, "No tenes autorizacion para reiniciar deployments."}
end
end
defp parse_scale_command(text) do
case String.split(String.trim(text)) do
[deployment, replicas] ->
case Integer.parse(replicas) do
{n, ""} when n > 0 and n <= 50 -> {:ok, deployment, n}
_ -> {:error, :invalid}
end
_ -> {:error, :invalid}
end
end
defp authorized?(_user, _action), do: true
defp scale_deployment(_deployment, _replicas), do: :ok
defp restart_deployment(_deployment), do: :ok
end
Seguridad en la automatizacion
La automatizacion sin seguridad es una receta para desastres automatizados. Cada automatizacion deberia incluir barandas que prevengan que cause mas dano que el problema que resuelve.
Patrones clave de seguridad:
- Modo dry-run: Cada automatizacion deberia soportar un dry-run que muestre que pasaria sin hacerlo realmente
- Limites de radio de explosion: Limita el alcance de acciones automatizadas (ej: nunca borrar mas de 5 pods a la vez)
- Prompts de confirmacion: Para acciones destructivas, requeri confirmacion explicita
- Rate limiting: Preveni que la automatizacion corra demasiado frecuentemente
- Circuit breakers: Si la automatizacion falla demasiadas veces, para y alerta a un humano
- Logging de auditoria: Registra cada accion automatizada con quien la disparo y que paso
- Capacidad de rollback: Cada cambio automatizado deberia ser reversible
Aca hay un wrapper de automatizacion segura:
defmodule SreBot.SafeAction do
@moduledoc """
Wrapper para acciones automatizadas seguras con dry-run,
rate limiting, y soporte de circuit breaker.
"""
require Logger
defstruct [
:name,
:action,
:dry_run,
:max_blast_radius,
:rate_limit_per_hour,
:circuit_breaker_threshold
]
@doc """
Ejecuta una accion con barandas de seguridad.
"""
def execute(%__MODULE__{} = config, targets, opts \\ []) do
dry_run = Keyword.get(opts, :dry_run, config.dry_run)
with :ok <- check_blast_radius(config, targets),
:ok <- check_rate_limit(config),
:ok <- check_circuit_breaker(config) do
if dry_run do
Logger.info("[DRY RUN] #{config.name}: Afectaria #{length(targets)} objetivos")
{:ok, :dry_run, targets}
else
results = Enum.map(targets, fn target ->
try do
result = config.action.(target)
log_action(config.name, target, result)
result
rescue
e ->
record_failure(config.name)
{:error, Exception.message(e)}
end
end)
failures = Enum.filter(results, &match?({:error, _}, &1))
if length(failures) > 0 do
Logger.warning("#{config.name}: #{length(failures)}/#{length(targets)} acciones fallaron")
end
{:ok, :executed, results}
end
end
end
defp check_blast_radius(config, targets) do
if length(targets) > config.max_blast_radius do
{:error, "Radio de explosion excedido: #{length(targets)} objetivos > max #{config.max_blast_radius}"}
else
:ok
end
end
defp check_rate_limit(config) do
key = "rate:#{config.name}"
count = get_counter(key)
if count >= config.rate_limit_per_hour do
{:error, "Rate limit excedido: #{count} ejecuciones en la ultima hora"}
else
increment_counter(key)
:ok
end
end
defp check_circuit_breaker(config) do
failures = get_failure_count(config.name)
if failures >= config.circuit_breaker_threshold do
{:error, "Circuit breaker abierto: #{failures} fallas consecutivas"}
else
:ok
end
end
defp log_action(name, target, result) do
Logger.info("Accion #{name} en #{inspect(target)}: #{inspect(result)}")
end
defp record_failure(_name), do: :ok
defp get_counter(_key), do: 0
defp increment_counter(_key), do: :ok
defp get_failure_count(_name), do: 0
end
Ejemplo de uso:
# Definir una accion segura de reinicio de pods
restart_action = %SreBot.SafeAction{
name: "pod-restart",
action: fn pod -> System.cmd("kubectl", ["delete", "pod", pod]) end,
dry_run: false,
max_blast_radius: 5,
rate_limit_per_hour: 10,
circuit_breaker_threshold: 3
}
# Ejecutar con barandas de seguridad
SreBot.SafeAction.execute(restart_action, ["pod-1", "pod-2", "pod-3"])
# Ejecutar en modo dry-run
SreBot.SafeAction.execute(restart_action, ["pod-1", "pod-2"], dry_run: true)
Midiendo la reduccion de toil
No podes mejorar lo que no medis. Aca estan las metricas clave para rastrear:
- Porcentaje de toil: Horas gastadas en toil / horas totales de trabajo. Objetivo: por debajo del 50%.
- Volumen de tickets: Numero de tickets operativos por semana. Deberia tender a bajar con el tiempo.
- Tiempo promedio de resolucion de tickets: Si no podes eliminar tickets, al menos hacelos mas rapidos.
- Conteo de intervenciones manuales: Cuantas veces un humano tuvo que intervenir en algo automatizado.
- Adopcion del autoservicio: Porcentaje de aprovisionamiento hecho por autoservicio vs tickets.
- Cobertura de automatizacion: Porcentaje de categorias de toil conocidas que tienen automatizacion.
Aca hay un setup de metricas de Prometheus para rastrear toil:
# PrometheusRule para metricas de toil
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: toil-metrics
namespace: monitoring
spec:
groups:
- name: toil.tracking
rules:
# Rastrear ejecuciones de automatizacion
- record: sre:automation_executions:total
expr: sum(automation_executions_total) by (action_name, result)
# Rastrear intervenciones manuales
- record: sre:manual_interventions:rate1w
expr: sum(increase(manual_intervention_total[1w])) by (category)
# Rastrear volumen de tickets
- record: sre:tickets:rate1w
expr: sum(increase(sre_tickets_total[1w])) by (category, priority)
# Ratio autoservicio vs tickets
- record: sre:self_service_ratio
expr: |
sum(increase(self_service_requests_total[1w]))
/
(sum(increase(self_service_requests_total[1w])) + sum(increase(sre_tickets_total[1w])))
- name: toil.alerts
rules:
- alert: ToilPercentageHigh
expr: sre:toil_percentage > 50
for: 1w
labels:
severity: warning
annotations:
summary: "El porcentaje de toil excede el 50% en la semana"
description: "El equipo esta gastando {{ $value }}% del tiempo en toil. Revisar backlog de automatizacion."
- alert: TicketVolumeSpike
expr: sre:tickets:rate1w > 2 * avg_over_time(sre:tickets:rate1w[4w])
for: 1d
labels:
severity: warning
annotations:
summary: "El volumen de tickets se duplico comparado con el promedio de 4 semanas"
Construi un dashboard de Grafana que muestre estas metricas a lo largo del tiempo. Ver la linea de tendencia bajar es increiblemente motivante para el equipo.
La regla del 50 por ciento
El libro de Google SRE dice que los SREs no deberian gastar mas del 50% de su tiempo en toil. El 50% restante deberia gastarse en trabajo de ingenieria que mejore el sistema y reduzca el toil futuro.
Esto no es solo una idea copada. Es un requisito estructural para un equipo de SRE saludable. Aca esta el por que:
- Mas del 50% de toil: El equipo se esta ahogando. Nunca tienen tiempo de automatizar, asi que el toil sigue creciendo. Es una espiral de muerte.
- Al 50% de toil: Apenas sostenible. El equipo puede mantener la automatizacion actual pero no puede hacer mejoras significativas.
- Menos del 50% de toil: El equipo tiene capacidad para invertir en trabajo de ingenieria. El toil decrece con el tiempo. Este es el ciclo virtuoso que queres.
Como enforcar la regla del 50% en la practica:
- Rastrealo semanalmente: Usa el sistema de rastreo de toil descrito antes. Hacelo visible.
- Asigna presupuestos de toil: Cada miembro del equipo tiene un presupuesto de toil. Cuando se excede, escala.
- Protege el tiempo de ingenieria: Bloquea tiempo en el calendario para trabajo de ingenieria. No dejes que el toil llene los huecos.
- Rota el toil: No dejes que la misma persona haga todo el toil. Rota guardia y deber de tickets.
- Escala violaciones: Si el toil excede el 50% por dos semanas consecutivas, es un problema de gestion. Escala para conseguir recursos o reducir el alcance.
Cuando el umbral del 50% se excede, aca esta el proceso de escalamiento:
# toil-escalation-policy.yaml
escalation_policy:
thresholds:
- level: "verde"
toil_percent: 0-30
action: "Operaciones normales. Segui invirtiendo en automatizacion."
- level: "amarillo"
toil_percent: 30-50
action: "Revisa el backlog de automatizacion. Prioriza los mayores reductores de toil."
- level: "naranja"
toil_percent: 50-65
action: |
Escala al engineering manager.
Pausa trabajo de features no criticas.
Dedica 1 ingeniero full-time a automatizacion.
Revisa si el equipo esta con poca gente.
- level: "rojo"
toil_percent: 65-80
action: |
Escala a nivel director.
Pausa todo el trabajo de features.
Todo el equipo se enfoca en reduccion de toil.
Considera aumento temporal de headcount.
- level: "critico"
toil_percent: 80-100
action: |
Escala a nivel VP.
La confiabilidad del servicio esta en riesgo.
Se necesita soporte cross-team.
Sprint de emergencia de automatizacion.
review_cadence: "Semanal en la standup del equipo"
tracking: "Planilla compartida visible para management"
Juntando todo
Aca hay un roadmap practico para reducir el toil en tu organizacion:
- Semana 1-2: Empieza a rastrear el toil. Que todos registren su trabajo por dos semanas.
- Semana 3: Analiza los datos. Identifica las top 5 categorias de toil por tiempo gastado.
- Semana 4-6: Automatiza la categoria #1 de toil. Empieza con la ganancia mas rapida.
- Semana 7-8: Medi el impacto. Bajo el volumen de tickets o el tiempo gastado?
- Semana 9-12: Automatiza #2 y #3. Construi autoservicio donde sea aplicable.
- Continuo: Segui midiendo, automatizando, e iterando. Hace la reduccion de toil un objetivo permanente del sprint.
La idea clave del libro de Google SRE es esta: el toil no es solo molesto, es peligroso. Los equipos enterrados en toil no tienen tiempo de mejorar los sistemas, lo que significa que los sistemas se vuelven menos confiables, lo que significa mas incidentes, lo que significa mas toil. Romper este ciclo requiere inversion deliberada en automatizacion, y la disciplina para proteger esa inversion de ser consumida por el proximo ticket urgente.
Notas finales
Esto cierra nuestra serie de catorce partes sobre SRE. Empezamos midiendo la confiabilidad a traves de SLIs y SLOs y terminamos aca con la reduccion del toil que previene que los equipos hagan trabajo significativo de ingenieria. En el camino cubrimos gestion de incidentes, observabilidad, chaos engineering, planificacion de capacidad, GitOps, gestion de secretos, optimizacion de costos, gestion de dependencias, confiabilidad de bases de datos, ingenieria de releases, seguridad como codigo, y recuperacion ante desastres.
El hilo comun a traves de todas estas practicas es este: trata las operaciones como un problema de ingenieria. Medi lo que importa, automatiza lo que se repite, e inverti en sistemas que mejoren con el tiempo en lugar de requerir mas esfuerzo humano a medida que crecen.
Si solo te llevas una cosa de esta serie, que sea la regla del 50%. Protege el tiempo de tu equipo para trabajo de ingenieria. La automatizacion que construis hoy es lo que te salva de ahogarte maniana.
Espero que te haya resultado util y lo hayas disfrutado! Hasta la proxima!
Errata
Si encontras algun error o tenes alguna sugerencia, por favor mandame un mensaje para que se corrija.
Tambien podes revisar el codigo fuente y los cambios en las fuentes aca
$ Comentarios
Online: 0Por favor inicie sesión para poder escribir comentarios.