Control del temps d’activitat d’ instàncies EC2 amb script
Hi ha moltes formes d’estalviar costos a AWS, i algunes són més sofisticades que d’altres: reservar instàncies, usar Savings Plans, revisar perfils d’ús. Però quan la despesa ve de màquines que es fan servir només en horari laboral, no hi ha res més directe que apagar-les fora d’aquest horari. No és elegant, però funciona i evita gastar hores en debats. La idea és senzilla: aturar instàncies EC2 a la nit i arrencar-les de nou al matí. El problema és que a la pràctica no és tan trivial com sembla. Entre etiquetes mal definides, dependències entre instàncies i petites trampes d’AWS, aquest tipus d’automatització pot acabar sent més molest que útil si no es pensa bé des del començament.
Decisions prèvies
Abans d’escriure una sola línia de codi convé prendre diverses decisions que després estalvien molt de temps:
- Quines instàncies s’han de controlar i quines no.
No tot s’ha d’apagar. Hi ha bases de dades que no vols aturar cada dia, ni encara que siguin de desenvolupament, perquè el temps d’arrencada no sempre és immediat i pot deixar els equips sense servei. Una etiqueta clara com AutoSchedule=true és suficient per discriminar. No convé barrejar múltiples criteris; una sola etiqueta booleana simplifica la lògica. - Horari en una sola zona.
AWS sempre treballa a UTC, però la gent de desenvolupament parla en hora local. El més estable és definir els horaris en UTC i documentar que “les nostres instàncies s’aturen a les 22:00 UTC”. Intentar adaptar l’script a zones locals sol acabar en inconsistències, sobretot quan hi ha equips distribuïts. - Apagar vs acabar.
Pot sonar obvi, però no és el mateix aturar que destruir. L’script ha d’aturar instàncies, no acabar-les. Hi ha qui comet l’error de fer servir terminate-instances en comptes de stop-instances en copiar/enganxar codi. - Ordre d’ arrencada.
Algunes aplicacions depenen de que primer estigui disponible la base de dades, després el backend i, finalment, el frontend. Si tot arrenca a l’atzar, el resultat són errors que algú acaba corregint manualment. Definir una etiqueta StartOrder i ordenar-les per aquest valor en arrencar pot ser suficient. - Evitar dependències fràgils.
L’script no hauria de dependre de noms d’instàncies o IDs fixos. Sempre és millor basar-se en etiquetes, que sobreviuen al canvi d’IDs. Una instància reemplaçada en un autoscaling group, per exemple, perdria l’automatització si depenem de l’ID.
Eines disponibles i per què quedar-se amb Lambda + boto3
Existeixen diverses formes de programar aquests apagats/arrencades:
- EventBridge (abans CloudWatch Events) + Lambda: És l’opció més comuna i pràctica. Permet llançar funcions Lambda en horaris fixos i controlar tot des de codi.
- Instance Scheduler (solució oficial d’AWS): AWS té una solució empaquetada a CloudFormation anomenada Instance Scheduler. És potent, però massa rígida si només vols cobrir un cas simple. Acaba introduint més complexitat que beneficis.
- Scripts des de fora (Jenkins, cron en on-premises, etc.): Funcionen, però depenen de la disponibilitat d’aquesta infraestructura. Si el cron no s’ executa perquè el servidor de control es cau, les instàncies no s’ aturen.
El més equilibrat és EventBridge + Lambda. No requereix servidors externs, es manté amb poques línies de codi i escala bé.
Script base en Python amb boto3
Aquest és un punt de partida que funciona. L’important no és la quantitat de línies, sinó com s’estructura:
import boto3
import os
ec2 = boto3.client("ec2", region_name=os.environ["AWS_REGION"])
def lambda_handler(event, context):
action = event.get("action")
if action not in ["start", "stop"]:
raise ValueError("Acción no válida: debe ser start o stop")
# Buscar instancias con la etiqueta AutoSchedule=true
filters = [
{"Name": "tag:AutoSchedule", "Values": ["true"]},
{"Name": "instance-state-name", "Values": ["running" if action=="stop" else "stopped"]}
]
instances = ec2.describe_instances(Filters=filters)
ids = []
for reservation in instances["Reservations"]:
for instance in reservation["Instances"]:
ids.append(instance["InstanceId"])
if ids:
if action == "stop":
ec2.stop_instances(InstanceIds=ids)
else:
ec2.start_instances(InstanceIds=ids)
L’esdeveniment que dispara Lambda ha de passar el paràmetre “action”: “stop” o “action”: “start”. Això es resol creant dues regles a EventBridge: una a l’hora d’apagat i una altra a la d’encesa.
Errors habituals i com evitar-los
1. Deixar instàncies en estat “pending” o “stopping”
De vegades l’script s’executa dues vegades seguides. Per exemple, si es configuren dues regles amb minuts diferents. El filtre instance-state-name en el codi evita arrencar una cosa que ja està arrencant, o aturar una cosa que ja està aturant-se.
2. Oblidar el region_name
Lambda s’executa en una regió específica, però si en el codi no es fixa region_name, pot buscar per defecte en una altra. Això porta a situacions estranyes: la funció s’executa bé, però no troba cap instància perquè està mirant a la regió equivocada.
3. Permisos incomplets en IAM
El rol de Lambda ha de tenir almenys aquestes accions sobre EC2:
- ec2:DescribeInstances
- ec2:IniciInstàncies
- ec2:StopInstances
Res més. Donar permisos més amplis és innecessari i augmenta la superfície de risc.
4. Dependències de xarxa innecessàries
Lambda no necessita estar en una VPC per fer això. Si es fica en una VPC, llavors cal configurar subxarxes, NAT, etc. i l’ script pot quedar-se penjat. Millor deixar-lo fora de la VPC tret que hi hagi un motiu clar.
5. El problema dels volums EBS
Aturar instàncies redueix el cost de còmput, però no elimina el cost d’ emmagatzematge. De vegades es dona per fet que en apagar la instància el cost desapareix, i després apareix la sorpresa a la factura. Convé explicar-ho als equips que faran servir aquest sistema.
Control d’ ordre amb etiquetes
Per als casos en què algunes màquines depenen d’ altres, es pot afegir un camp StartOrder:
# Extreure instàncies i ordenar-les per StartOrder
instances_data = []
for reservation in instances["Reservations"]:
for instance in reservation["Instances"]:
order = 100 # valor por defecto
for tag in instance.get("Tags", []):
if tag["Key"] == "StartOrder":
order = int(tag["Value"])
instances_data.append((order, instance["InstanceId"]))
# Ordenar abans d' arrencar
instances_data.sort(key=lambda x: x[0])
ids = [x[1] for x in instances_data]
D’ aquesta manera les instàncies amb StartOrder = 1 s’ inicien primer. Tot i que EC2 no garanteix que una instància estigui llesta quan una altra la necessita, almenys es redueix el risc que tot arrenqui en ordre incorrecte.
Alternatives a Lambda quan l’ horari no és fix
Hi ha equips que treballen en torns canviants. En aquest cas, tenir un apagat automàtic pot molestar més que ajudar. Algunes solucions que es poden fer servir en aquest cas:
- Apagat automàtic amb retard configurable: L’script atura les instàncies a les 22:00 UTC, llevat que algú hagi posat l’etiqueta KeepAliveUntil=2025-08-19T01:00:00Z. Això permet estendre temporalment l’ horari sense modificar les regles.
- Bot de Slack/Teams: Un petit bot que permet escriure /stop dev-env o /start dev-env. Útil quan els horaris no són previsibles.
No tots els casos encaixen amb l’enfocament de Lambda + EventBridge. Convé avaluar-ho abans d’ implantar-lo.
Validació i proves
Un detall que sol passar-se per alt és com provar el sistema sense molestar ningú. Algunes idees pràctiques:
- Crear un grup d’instàncies “dummy” amb etiquetes correctes, però que no afectin producció.
- Programar l’apagat/arrencada en horaris propers (exemple: apagar a les 10:00 i encendre a les 10:05) per validar el comportament en minuts.
- Revisar els logs de CloudWatch després de cada execució per confirmar que s’han trobat instàncies i s’han llançat les accions.
Una vegada que tot està provat, es canvien les regles als horaris reals.
Què fer quan alguna cosa falla
Tot i que el sistema és simple, les fallades ocorren. Algunes situacions típiques:
- La instància no arrenca: Pot estar associada a un host dedicat, o tenir problemes amb la llicència. Revisar system status checks en EC2.
- Es va apagar una instància crítica per error d’etiquetes: Per això és útil mantenir una llista d’exclusió dura en el codi (excluded_instance_ids), encara que sigui un últim recurs.
- L’horari canvia per requeriments de negoci: En comptes de modificar el codi cada vegada, el més pràctic és fer servir variables d’entorn a la Lambda i actualitzar només aquestes.
Estendre el sistema a entorns multi-regió
Un problema comú és assumir que totes les instàncies rellevants estan en una sola regió. En empreses que han crescut orgànicament, hi sol haver instàncies repartides entre diverses regions sense un inventari clar. Si la Lambda només controla la regió on es desplega, l’estalvi serà parcial.
El més senzill és recórrer totes les regions de forma dinàmica. boto3 permet llistar-les:
import boto3
def get_all_regions():
ec2 = boto3.client("ec2")
regions = ec2.describe_regions(AllRegions=False)
return [r["RegionName"] for r in regions["Regions"]]
Després s’ itera sobre elles:
for region in get_all_regions():
ec2 = boto3.client("ec2", region_name=region)
# executar el mateix filtre d' instàncies aquí
Cal tenir en compte els límits de temps de Lambda (màx. 15 minuts). Si el nombre d’instàncies és gran, convé llançar la funció en paral·lel per regió fent servir Step Functions o EventBridge amb múltiples targets. En cas contrari, una regió lenta pot retardar la resta.
Casos especials: RDS, ASG, ECS
El patró d’apagar de nit, encendre de matí no es limita a EC2. A la pràctica, altres serveis també consumeixen recursos fora d’horari i requereixen un enfocament similar.
RDS
Les bases de dades RDS tenen l’opció de “stop/start” manual, però amb limitacions:
- No poden romandre apagades més de 7 dies seguits; després s’arrenquen automàticament.
- Les instàncies multi-AZ o de producció crítica no són candidates per a aquest esquema.
El mateix script pot ampliar-se per aturar RDS, amb boto3:
rds = boto3.client("rds", region_name=region)
instances = rds.describe_db_instances()
for db in instances["DBInstances"]:
tags = rds.list_tags_for_resource(ResourceName=db["DBInstanceArn"])["TagList"]
if {"Key": "AutoSchedule", "Value": "true"} in tags:
if action == "stop" and db["DBInstanceStatus"] == "available":
rds.stop_db_instance(DBInstanceIdentifier=db["DBInstanceIdentifier"])
elif action == "start" and db["DBInstanceStatus"] == "stopped":
rds.start_db_instance(DBInstanceIdentifier=db["DBInstanceIdentifier"])
Grups d’escala automàtica (ASG)
En un ASG no té sentit aturar instàncies individuals, perquè el mateix grup les reemplaçaria. La forma correcta és modificar la mida del grup:
asg = boto3.client("autoscaling", region_name=region)
asg.update_auto_scaling_group(
AutoScalingGroupName="mi-grupo",
MinSize=0 if action=="stop" else 1,
DesiredCapacity=0 if action=="stop" else 1
)
En entorns de desenvolupament, deixar el grup amb DesiredCapacity=0 a la nit redueix els costos sense perdre la configuració.
ECS
Si s’ usen serveis ECS amb instàncies EC2, l’ ideal és reduir el nombre de tasques o d’ instàncies del cluster fora d’ horari. Si es fa servir Fargate, l’avantatge és menor perquè ja es factura per ús, tot i que pot interessar aturar serveis que generen tasques programades.
Gestió d’ excepcions
En qualsevol sistema automàtic, sempre apareixen casos que no encaixen en el patró general. Alguns enfocaments pràctics:
- Llista blanca d’instàncies crítiques: encara que tinguin etiquetes mal posades, mai s’han d’apagar. Es codifica explícitament en una variable d’ entorn o arxiu de configuració.
- Etiqueta DoNotStop =true: evita dependre només d’AutoSchedule. És una segona capa de protecció.
- Programacions diferents: alguns equips volen que les seves instàncies s’ apaguin a les 21: 00 i altres a les 23: 00. En aquest cas, convé fer servir una etiqueta Schedule=dev-hours i a Lambda mapejar dev-hours → “22:00–07:00 UTC”. Així es mantenen diversos horaris sense duplicar funcions.
Auditoria i traçabilitat
Un dels problemes d’apagar màquines automàticament és que els usuaris pensin que “algú ha espatllat el seu entorn”. Per reduir friccions, convé deixar un registre clar del que fa el sistema.
- Logs a CloudWatch: cada execució ha de mostrar quines instàncies es van apagar/van encendre, amb IDs i noms.
- Etiquetes dinàmiques: afegir una etiqueta LastStoppedBy=AutoSchedule amb la data/hora. Així, si algú revisa la consola d’EC2, veurà immediatament què va ocórrer.
Exemple de codi per a etiquetes:
ec2.create_tags(
Resources=ids,
Tags=[{"Key": "LastStoppedBy", "Value": f"AutoSchedule-{action}-{datetime.utcnow().isoformat()}"}]
)
Cost real del sistema
Convé tenir clar quant costa mantenir aquest mecanisme:
- Lambda: pràcticament zero, llevat que gestioni milers d’instàncies per execució.
- EventBridge: el cost per regla programada és menyspreable.
- CloudWatch Logs: aquí sí que pot créixer la despesa si es deixa aconseguir tot sense retenció. Ajustar la retenció a 7 o 14 dies sol ser suficient.
L’estalvi davant de deixar instàncies enceses és molt superior, fins i tot en entorns petits.
Manteniment a llarg termini
Una automatització d’aquest tipus sembla per “posar i oblidar”, però no és cert. Amb el temps sorgeixen petits problemes que convé anticipar:
- Instàncies noves sense etiquetes. Els equips creen noves màquines i obliden l’etiqueta AutoSchedule. Resultat: es queden enceses tota la nit. Solució: activar un Config Rule o un Service Control Policy que obligui a etiquetar les instàncies en la creació.
- Canvi d’ horari laboral.
Passar de 9–18 a 8–17 obliga a modificar les regles. Si l’ horari està codificat a Lambda, es desplega de nou el codi. Millor guardar-lo en variables d’entorn i canviar-lo amb un terraform apply o des de consola. - Entorns de prova que creixen.
Al principi el sistema controla 5–10 instàncies. Un any després, n’hi ha 200 distribuïdes en diverses regions. El que abans era una Lambda senzilla pot necessitar Step Functions o un sistema més robust. Convé pensar en escalabilitat des del començament.
Aturar vs hibernar
AWS ofereix l’opció d'”hibernar” instàncies EC2, en la qual es conserva l’estat de la RAM. Sobre el paper sembla atractiu, perquè en arrencar tot segueix exactament igual que abans. A la pràctica:
- No totes les instàncies suporten hibernació.
- Només funciona si el volum arrel és EBS i està xifrat.
- El temps d’arrencada no sempre és gaire menor que amb una arrencada normal.
- Continua costant més en emmagatzematge, perquè guarda el contingut de RAM en EBS.
Per a la majoria d’ entorns de desenvolupament, aturar és suficient. Hibernar només val la pena si algú insisteix a mantenir processos en memòria.
Què no fer
Hi ha diverses pràctiques que semblen estalviar temps al principi, però després es converteixen en un problema:
- Apagar tot sense excepció: pot estalviar més a l’inici, però tard o d’hora algú es queda bloquejat perquè la seva màquina crítica es va apagar enmig d’un procés.
- Basar-se en noms en comptes d’etiquetes: els noms canvien, les etiquetes no.
- Afegir lògica complexa a Lambda: és temptador afegir excepcions, llistes negres, horaris per equip… tot en un sol script. El resultat és un monstre difícil de mantenir. Millor separar la lògica en diverses funcions petites o parametritzar-la amb etiquetes.
- Donar permisos excessius: de vegades s’ assigna al rol de Lambda AmazonEC2FullAccess per mandra. És obrir una porta enorme que després costa de justificar en auditories.
Variació amb Terraform o CloudFormation
Qui gestiona infra amb IaC prefereix no haver de desplegar la Lambda a mà. Un exemple simple amb Terraform per a una regla que executa la Lambda d’apagat:
resource "aws_cloudwatch_event_rule" "stop_ec2" {
name = "stop-ec2-night"
schedule_expression = "cron(0 22 * * ? *)" # 22:00 UTC
}
resource "aws_cloudwatch_event_target" "stop_lambda_target" {
rule = aws_cloudwatch_event_rule.stop_ec2.name
target_id = "StopEC2Lambda"
arn = aws_lambda_function.ec2_scheduler.arn
input = jsonencode({ action = "stop" })
}
resource "aws_lambda_permission" "allow_eventbridge" {
statement_id = "AllowExecutionFromEventBridge"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.ec2_scheduler.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.stop_ec2.arn
}
Així es garanteix que el sistema sempre es desplega igual en tots els entorns.
Impacte cultural en els equips
Encara que sembli un detall secundari, apagar instàncies automàticament té un impacte en la forma de treballar. Els equips deixen d’assumir que “la meva màquina estarà sempre encesa” i s’acostumen a arrencar-la només quan la necessiten. Aquest canvi redueix costos, però també educa els usuaris a ser més conscients dels recursos que consumeixen. En alguns casos, fins i tot porta a replantejar l’ús d’EC2 i migrar a serveis gestionats o serverless, que s’adapten millor a càrregues intermitents.
Llista de verificació final abans del desplegament
- Definir clarament quines instàncies participen i etiquetar-les.
- Provar en una regió i amb poques instàncies abans d’estendre’l.
- Verificar permisos mínims en el rol de Lambda.
- Configurar retenció de logs a CloudWatch.
- Documentar els horaris en UTC i compartir-los amb tots els equips.
- Tenir un mecanisme d’exclusió (etiqueta o llista blanca).
Conclusió
Automatitzar l’apagada i arrencada d’instàncies EC2 en horari laboral no és complicat, però la diferència entre un script improvisat i un sistema estable està en els petits detalls: com es gestionen les etiquetes, què passa amb les dependències, com es comuniquen els canvis d’estat i quines excepcions es permeten. Quan es fa amb cura, l’ estalvi és immediat i l’ operació se simplifica. I, més enllà de la factura, obliga els equips a mantenir cert ordre, cosa que a llarg termini és gairebé tan valuosa com els diners estalviats.

