SOA-Lab1 — Observabilidad mínima: CloudWatch Logs + Alarmas + SNS

Diagrama SOA-Lab1 — Observabilidad mínima con CloudWatch Logs, Alarmas y SNS

Región: us-east-1

Duración estimada: 35–55 minutos

Costo-riesgo: Bajo

Certificación: AWS Certified CloudOps Engineer – Associate (SOA-C03)

Dominio: Monitoring, Logging and Remediation

Tarea 1.1: Implement metrics, alarms, and filters by using AWS monitoring and logging services

Caso de uso

Digital Cafe Luna inicia sus actividades: un café pequeño, un sueño familiar y una oportunidad para salir adelante. Camilo, hijo de la Sra. Blanca, estudia tecnología y desde el primer día insistió en montar una pequeña aplicación en AWS para apoyar la facturación y llevar un control básico del inventario. Un lunes por la mañana, la Sra. Blanca nota retrasos en la caja, algunos errores en el sistema y suelta esa frase que cualquier equipo técnico ha escuchado alguna vez:

“El sistema está lento, ¿qué pasa?.”

Camilo revisa la instancia, pero no tiene suficiente visibilidad para entender. No sabe si fue la CPU, logs, errores de arranque, una carga puntual o simplemente una percepción del usuario.

La clave es simple: sin observabilidad, diagnosticar se vuelve adivinar.

En este laboratorio vamos a construir un baseline mínimo de observabilidad en AWS: logs centralizados, una alarma de CPU y una notificación por email. ¡No es una plataforma completa de monitoreo por Dios!, pero si un primer paso para operar con datos y no con suposiciones.

¿Qué vamos a construir?

En este laboratorio vas a crear:

  • Un SNS Topic con suscripción por email para recibir alertas.
  • Un CloudWatch Log Group con retención corta para centralizar logs.
  • Una instancia EC2 Amazon Linux accesible por SSH.
  • CloudWatch Agent en la instancia para enviar logs y métricas básicas.
  • Una alarma de CPU en CloudWatch que notifica al SNS Topic.
  • Una prueba controlada de carga para validar que la alarma realmente dispara.

Diagrama SOA-Lab1 — Observabilidad mínima con CloudWatch Logs, Alarmas y SNS

Figura 1 — Flujo mínimo de observabilidad para Digital Cafe Luna: EC2 envía logs y métricas a CloudWatch, la alarma dispara SNS y Camilo recibe la notificación por email.

Convención de nombres

Usaremos el prefijo soa-lab1 para que puedas buscar, filtrar y eliminar recursos sin perder tiempo.

Recurso Nombre
EC2 Instance soa-lab1-app
Security Group soa-lab1-app-sg
Key Pair kp-soa-lab1
CloudWatch Log Group /soa/lab1/system
SNS Topic soa-lab1-alerts-v2
CloudWatch Alarm ALARM-soa-lab1-high-cpu
IAM Role soa-lab1-ec2-cw-role

Nota práctica: usar nombres consistentes no es un detalle menor. En labs ayuda al cleanup, en ambientes reales ayuda a operar, auditar y reducir confusión.

Requisitos previos

Antes de comenzar, asegúrate de tener:

  • Una cuenta AWS con permisos suficientes para crear recursos del laboratorio.
  • Región de trabajo: us-east-1 (N. Virginia).
  • Un correo activo para confirmar la suscripción de Amazon SNS.
  • Cliente SSH disponible: Terminal en Mac/Linux, PuTTY o similar en Windows.
  • Acceso a una VPC default en la región.
  • Tu IP pública disponible para permitir SSH solo desde tu origen.

Ojo: para este laboratorio no abras SSH a 0.0.0.0/0. Vamos a permitir el puerto 22 únicamente desde tu IP pública en formato /32.

Paso 1 — Preparar acceso por SSH

Antes de crear la instancia, vamos a dejar listo el acceso. La regla es simple: necesitamos poder entrar por SSH, pero sin abrir la instancia al mundo.

En este lab vamos a permitir SSH solo desde tu IP pública. Nada de 0.0.0.0/0.

1.1 Crear el Key Pair

En AWS Console:

  • Ve a EC2 → Key Pairs.
  • Selecciona Create key pair.
  • En Name, usa kp-soa-lab1.
  • En el tipo de llave:
    • Si usas Mac/Linux, selecciona .pem.
    • Si usas Windows con PuTTY, selecciona .ppk.
  • Crea el key pair y guarda el archivo en un lugar seguro.

Ojo: AWS solo te permite descargar la llave privada una vez. Si la pierdes, tendrás que crear otro key pair o usar otro método de acceso (mas adelante te enseñaré qué hacer cuando pierdes tu Key pair).

1.2 Crear el Security Group

Ahora crea el Security Group que permitirá SSH solo desde tu IP.

En AWS Console:

  • Ve a EC2 → Security Groups.
  • Selecciona Create security group.
  • En Name, usa soa-lab1-app-sg.
  • En Description, usa SOA Lab1 SSH restricted.
  • En VPC, selecciona la default VPC.
  • En Inbound rules, agrega SSH, puerto 22, source My IP.
  • En Outbound rules, deja la regla por defecto All traffic.
  • Crea el Security Group.

En este lab dejamos salida abierta para que la instancia pueda instalar paquetes y comunicarse con servicios de AWS. Lo importante aquí es cuidar el tráfico de entrada.

CLI opcional

Si prefieres hacerlo por CLI, puedes usar este bloque desde tu terminal.

Ojo: MYIP debe obtenerse desde tu máquina local, no desde CloudShell. Si lo ejecutas desde CloudShell, estarías permitiendo la IP pública de CloudShell, no la tuya.

# 0) Variables
REGION="us-east-1"
MYIP="$(curl -s https://checkip.amazonaws.com)/32"

# 1) Obtener la default VPC
VPC_ID="$(aws ec2 describe-vpcs --region "$REGION" 
  --filters Name=isDefault,Values=true 
  --query 'Vpcs[0].VpcId' 
  --output text)"

# 2) Crear Security Group
SG_ID="$(aws ec2 create-security-group --region "$REGION" 
  --group-name "soa-lab1-app-sg" 
  --description "SOA Lab1 SSH restricted" 
  --vpc-id "$VPC_ID" 
  --query 'GroupId' 
  --output text)"

# 3) Autorizar SSH solo desde tu IP pública
aws ec2 authorize-security-group-ingress --region "$REGION" 
  --group-id "$SG_ID" 
  --ip-permissions "IpProtocol=tcp,FromPort=22,ToPort=22,IpRanges=[{CidrIp=$MYIP,Description='SSH solo mi IP'}]"

# 4) Agregar tags
aws ec2 create-tags --region "$REGION" 
  --resources "$SG_ID" 
  --tags Key=Name,Value="soa-lab1-app-sg" Key=Lab,Value="soa-lab1"

# 5) Verificar reglas
aws ec2 describe-security-groups --region "$REGION" 
  --group-ids "$SG_ID" 
  --query "SecurityGroups[0].IpPermissions" 
  --output table

Checkpoint

Antes de seguir, valida:

  • Existe el key pair kp-soa-lab1.
  • Existe el Security Group soa-lab1-app-sg.
  • La regla inbound permite SSH 22 solo desde tu IP pública /32.
  • No existe una regla SSH abierta a 0.0.0.0/0.

Paso 2 — Lanzar la instancia EC2

Ahora vamos a crear la instancia que usaremos como servidor base de Digital Cafe Luna. No estamos buscando una arquitectura compleja todavía; solo necesitamos una EC2 pequeña, identificable por tags y accesible por SSH para instalar el CloudWatch Agent.

Crear la instancia desde la consola

En AWS Console:

  • Ve a EC2 → Instances → Launch instance.
  • En Name, usa soa-lab1-app.
  • En Application and OS Images, selecciona Amazon Linux 2023. También puedes usar Amazon Linux 2 si es lo que tienes disponible.
  • En Instance type, selecciona t3.nano. Si tu cuenta no permite t3.nano, usa t3.micro.
  • En Key pair, selecciona kp-soa-lab1.
  • En Network settings, selecciona Edit y configura:
    • VPC: default VPC.
    • Subnet: No preference.
    • Auto-assign public IP: Enabled.
    • Firewall: Select existing security group.
    • Security group: soa-lab1-app-sg.
  • En Tags, agrega Lab=soa-lab1.
  • Lanza la instancia y espera hasta que esté en estado Running y con 2/2 checks passed.

CLI opcional para validar

Cuando la instancia esté corriendo, puedes validar con:

REGION="us-east-1"

aws ec2 describe-instances --region "$REGION" 
  --filters "Name=tag:Name,Values=soa-lab1-app" 
            "Name=instance-state-name,Values=running" 
  --query "Reservations[0].Instances[0].{InstanceId:InstanceId,PublicIp:PublicIpAddress,State:State.Name,Tags:Tags}" 
  --output table

Checkpoint

Antes de seguir, confirma:

  • La instancia soa-lab1-app está en estado Running.
  • Tiene Public IPv4 address.
  • Tiene el Security Group soa-lab1-app-sg.
  • Tiene los tags Name=soa-lab1-app y Lab=soa-lab1.

Paso 3 — Conectarte por SSH y validar la instancia

Antes de instalar agentes o configurar logs, vamos a validar lo básico: que puedes entrar a la instancia. Si SSH falla aquí, mejor corregirlo ahora y no después.

Obtener la IP pública

En AWS Console:

  • Ve a EC2 → Instances.
  • Selecciona la instancia soa-lab1-app.
  • Copia el valor de Public IPv4 address.

También puedes obtenerla por CLI:

REGION="us-east-1"

aws ec2 describe-instances --region "$REGION" 
  --filters "Name=tag:Name,Values=soa-lab1-app" 
            "Name=instance-state-name,Values=running" 
  --query "Reservations[0].Instances[0].PublicIpAddress" 
  --output text

En los siguientes comandos usaré el placeholder <PUBLIC_IP>. Reemplázalo por la IP real de tu instancia.

Conectarte por SSH

Si estás en Mac/Linux, probablemente el archivo quedó en ~/Downloads/kp-soa-lab1.pem. Ajusta la ruta si lo guardaste en otra carpeta.

Primero asegura los permisos de la llave:

chmod 400 ~/Downloads/kp-soa-lab1.pem

Luego entra a la instancia:

ssh -i ~/Downloads/kp-soa-lab1.pem ec2-user@<PUBLIC_IP>

Ejemplo:

ssh -i ~/Downloads/kp-soa-lab1.pem [email protected]

Cuando pregunte si confías en el host, responde yes.

Validaciones rápidas dentro de la instancia

Una vez dentro, ejecuta:

uname -a

Y valida salida a Internet:

curl -s https://checkip.amazonaws.com

Checkpoint

Antes de seguir, confirma:

  • Pudiste conectarte por SSH.
  • Ves el prompt de ec2-user.
  • uname -a responde correctamente.
  • La instancia tiene salida a Internet.

Paso 4 — Preparar CloudWatch Logs

Antes de instalar el agente en la instancia, vamos a crear el Log Group donde llegarán los logs. La idea es dejar el destino listo y configurar una retención corta desde el inicio.

Crear el Log Group desde la consola

En AWS Console:

  • Ve a CloudWatch.
  • En el menú izquierdo, entra a Logs → Log groups.
  • Selecciona Create log group.
  • En Name, usa /soa/lab1/system.
  • Crea el Log Group.
  • Entra al Log Group recién creado.
  • Busca Retention setting.
  • Selecciona Edit.
  • Cambia la retención a 3 days.
  • Guarda el cambio.

Ojo: si dejas la retención en “Never expire”, los logs no se eliminan automáticamente. Para un laboratorio pequeño puede parecer poco relevante, pero es una mala costumbre operativa.

CLI opcional

Si prefieres hacerlo por CLI:

REGION="us-east-1"

aws logs create-log-group --region "$REGION" 
  --log-group-name "/soa/lab1/system" 2>/dev/null || true

aws logs put-retention-policy --region "$REGION" 
  --log-group-name "/soa/lab1/system" 
  --retention-in-days 3

Checkpoint

Antes de seguir, valida:

  • Existe el Log Group /soa/lab1/system.
  • La retención está configurada en 3 days.

Paso 5 — Instalar y configurar CloudWatch Agent

Ahora vamos a instalar y configurar CloudWatch Agent en la instancia EC2. Este agente enviará logs y métricas básicas hacia CloudWatch. Sin este paso, tendríamos una instancia corriendo, pero con poca visibilidad operativa.

No estamos montando una solución completa de observabilidad. Solo queremos una base mínima y funcional: logs centralizados y algunas métricas adicionales desde la instancia.

5.1 Crear el IAM Role para la instancia

Primero necesitamos que la EC2 tenga permisos para publicar logs y métricas en CloudWatch.

En AWS Console:

  • Ve a IAM → Roles.
  • Selecciona Create role.
  • En Trusted entity type, selecciona AWS service.
  • En Use case, selecciona EC2.
  • En permisos, agrega la política administrada CloudWatchAgentServerPolicy.
  • Asigna este nombre al role: soa-lab1-ec2-cw-role.
  • Crea el role.

Ahora asócialo a la instancia:

  • Ve a EC2 → Instances.
  • Selecciona soa-lab1-app.
  • Ve a Actions → Security → Modify IAM role.
  • Selecciona soa-lab1-ec2-cw-role.
  • Guarda el cambio.

En los detalles de la instancia, confirma que el campo IAM role muestre soa-lab1-ec2-cw-role.

Ojo: si la instancia no tiene este role, el agente puede instalarse y arrancar, pero no podrá publicar correctamente en CloudWatch.

5.2 Instalar CloudWatch Agent en la EC2

Conéctate por SSH a la instancia soa-lab1-app y ejecuta:

sudo yum -y install amazon-cloudwatch-agent || sudo dnf -y install amazon-cloudwatch-agent

Nota: usamos yum o dnf para cubrir Amazon Linux 2 y Amazon Linux 2023. Si uno no aplica, el otro debería funcionar.

5.3 Crear la configuración del agente

Ahora crea el archivo de configuración del agente. Este archivo le indica al agente qué logs enviar y qué métricas adicionales recolectar.

sudo tee /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json > /dev/null <<'EOF'
{
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/var/log/cloud-init.log",
            "log_group_name": "/soa/lab1/system",
            "log_stream_name": "{instance_id}/cloud-init"
          }
        ]
      }
    }
  },
  "metrics": {
    "metrics_collected": {
      "mem": {
        "measurement": [
          "mem_used_percent"
        ]
      },
      "disk": {
        "measurement": [
          "disk_used_percent"
        ],
        "resources": [
          "*"
        ]
      }
    }
  }
}
EOF

¿Qué estamos haciendo aquí?

  • Enviamos /var/log/cloud-init.log hacia CloudWatch Logs.
  • Usamos el Log Group /soa/lab1/system.
  • Creamos un Log Stream por instancia usando {instance_id}/cloud-init.
  • Recolectamos métricas básicas de memoria y disco.

5.4 Arrancar el agente

Ejecuta:

sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl 
  -a fetch-config 
  -m ec2 
  -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json 
  -s

Este comando carga la configuración local, indica que el agente corre en modo EC2 y arranca el servicio después de aplicar el archivo.

5.5 Verificar el estado del agente

Ejecuta:

sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a status

La salida debe mostrar algo parecido a "status": "running".

Checkpoint

Antes de seguir, valida:

  • El IAM Role soa-lab1-ec2-cw-role está asociado a la instancia.
  • El agente está instalado.
  • El comando de estado muestra running.
  • En CloudWatch → Logs → Log groups → /soa/lab1/system aparece un Log Stream parecido a i-xxxxxxxxxxxxxxxxx/cloud-init.

Puede tardar unos minutos en aparecer. Si no aparece de inmediato, espera un poco y refresca la consola.

Paso 6 — Consultar logs con CloudWatch Logs Insights

Ya estamos enviando logs desde la instancia hacia CloudWatch. Ahora vamos a consultarlos sin entrar por SSH al servidor.

Esta es una de las primeras ganancias reales de centralizar logs: puedes investigar desde CloudWatch, filtrar eventos y revisar actividad reciente sin depender de entrar manualmente a la EC2.

Ejecutar una consulta desde la consola

En AWS Console:

  • Ve a CloudWatch.
  • En el menú izquierdo, entra a Logs → Logs Insights.
  • Selecciona el Log Group /soa/lab1/system.
  • En el editor de query, pega esta consulta:
fields @timestamp, @message
| sort @timestamp desc
| limit 20
  • Selecciona un rango de tiempo reciente, por ejemplo Last 15 minutes.
  • Ejecuta la consulta.

Deberías ver eventos recientes del archivo /var/log/cloud-init.log, con su timestamp y mensaje correspondiente.

EC2 → CloudWatch Agent → CloudWatch Logs → Logs Insights

CLI opcional

También puedes iniciar una consulta desde CloudShell o desde tu terminal con AWS CLI configurado:

REGION="us-east-1"

QUERY_ID=$(aws logs start-query --region "$REGION" 
  --log-group-name "/soa/lab1/system" 
  --start-time $(date -u -d '15 minutes ago' +%s) 
  --end-time $(date -u +%s) 
  --query-string "fields @timestamp, @message | sort @timestamp desc | limit 20" 
  --query 'queryId' 
  --output text)

echo "$QUERY_ID"

Luego consulta el resultado:

aws logs get-query-results --region "$REGION" 
  --query-id "$QUERY_ID"

Ojo: si estás en Mac y el comando date -d no funciona, ejecuta esta parte desde CloudShell o ajusta el cálculo de tiempo según tu sistema operativo.

Checkpoint

Antes de seguir, valida:

  • La consulta devuelve eventos.
  • Puedes ver timestamps recientes.
  • El Log Group usado es /soa/lab1/system.

Paso 7 — Crear SNS Topic y suscripción por email

Ahora necesitamos un canal de notificación. Cuando la alarma de CloudWatch cambie a estado ALARM, queremos que alguien se entere. Para este primer lab usaremos algo simple y efectivo: Amazon SNS + email.

No estamos automatizando remediación todavía. Aquí solo queremos cerrar el flujo mínimo de alerta.

Crear el SNS Topic desde la consola

En AWS Console:

  • Ve a Amazon SNS.
  • En el menú izquierdo, entra a Topics.
  • Selecciona Create topic.
  • En Type, selecciona Standard.
  • En Name, usa soa-lab1-alerts-v2.
  • Crea el topic.

Crear la suscripción por email

Ahora entra al topic soa-lab1-alerts-v2 y crea una suscripción:

  • Selecciona Create subscription.
  • En Protocol, elige Email.
  • En Endpoint, escribe tu correo. Ejemplo: [email protected].
  • Selecciona Create subscription.
  • Abre tu correo y confirma la suscripción desde el mensaje de SNS.

Ojo: si no confirmas la suscripción, SNS no podrá enviarte notificaciones. La suscripción quedará en estado Pending confirmation.

CLI opcional

Si prefieres hacerlo por CLI:

REGION="us-east-1"
EMAIL="[email protected]"

TOPIC_ARN=$(aws sns create-topic --region "$REGION" 
  --name "soa-lab1-alerts-v2" 
  --query 'TopicArn' 
  --output text)

echo "$TOPIC_ARN"

aws sns subscribe --region "$REGION" 
  --topic-arn "$TOPIC_ARN" 
  --protocol email 
  --notification-endpoint "$EMAIL"

Después de ejecutar esto, revisa tu correo y confirma la suscripción.

Checkpoint

Antes de seguir, valida:

  • Existe el SNS Topic soa-lab1-alerts-v2.
  • La suscripción aparece como Confirmed.
  • No debe quedar en Pending confirmation.

Paso 8 — Crear la alarma de CPU y notificar a SNS

Ya tenemos logs centralizados y un canal de notificación. Ahora vamos a crear una alarma sencilla de CPU.

La idea es conectar una señal técnica con una acción operativa:

CPU alta → CloudWatch Alarm → SNS → email

Esto ya le da a Camilo una primera capacidad proactiva: no esperar a que la Sra. Blanca diga “el sistema está lento”, sino recibir una alerta cuando una métrica supere un umbral definido.

Crear la alarma desde la consola

En AWS Console:

  • Ve a CloudWatch → Alarms.
  • Selecciona Create alarm.
  • En Specify metric and conditions, selecciona Select metric.
  • Busca o navega por EC2 → Per-Instance Metrics.
  • Selecciona la métrica CPUUtilization correspondiente a la instancia soa-lab1-app.
  • Selecciona Select metric.

Configura la condición así:

  • Threshold type: Static.
  • Whenever CPUUtilization is: Greater/Equal.
  • Threshold value: 70.
  • Period: 5 minutes.
  • Evaluation periods: 1.
  • Datapoints to alarm: 1 out of 1.

Nota: para un laboratorio, este umbral nos sirve para validar el flujo. En producción, el umbral debe definirse según comportamiento real de la aplicación, baseline histórico y tolerancia del negocio.

En Configure actions:

  • En Alarm state trigger, selecciona In alarm.
  • En notificación, elige Select an existing SNS topic.
  • Selecciona el topic soa-lab1-alerts-v2.
  • Continúa con Next.

En Add name and description:

  • Alarm name: ALARM-soa-lab1-high-cpu.
  • Description opcional: CPU >= 70% durante 5 minutos. Notifica a soa-lab1-alerts-v2.

Luego revisa el resumen y selecciona Create alarm.

CLI opcional para validar

Puedes revisar que la alarma exista y que tenga acciones habilitadas:

REGION="us-east-1"

aws cloudwatch describe-alarms --region "$REGION" 
  --alarm-names "ALARM-soa-lab1-high-cpu" 
  --query "MetricAlarms[0].{AlarmName:AlarmName,StateValue:StateValue,MetricName:MetricName,Namespace:Namespace,Threshold:Threshold,Period:Period,EvaluationPeriods:EvaluationPeriods,ActionsEnabled:ActionsEnabled,AlarmActions:AlarmActions}" 
  --output table

Checkpoint

Antes de seguir, valida:

  • La alarma ALARM-soa-lab1-high-cpu aparece en CloudWatch → Alarms.
  • La métrica asociada es CPUUtilization.
  • El threshold está en >= 70.
  • Actions enabled está en Yes.
  • En Alarm actions aparece el SNS Topic soa-lab1-alerts-v2.

Paso 9 — Probar la alarma y confirmar el email

Crear una alarma no es suficiente. En operación, lo importante es validar que el flujo completo funciona.

Aquí vamos a probar el camino completo:

CPU alta → CloudWatch Alarm → SNS Topic → Email

La idea es generar una carga controlada en la instancia para que la alarma cambie a estado ALARM y recibas la notificación por correo.

9.1 Conectarte por SSH

Conéctate nuevamente a la instancia soa-lab1-app:

ssh -i ~/Downloads/kp-soa-lab1.pem ec2-user@<PUBLIC_IP>

Reemplaza <PUBLIC_IP> por la IP pública real de tu instancia.

9.2 Instalar stress

Dentro de la EC2, instala la herramienta stress:

sudo yum -y install stress || sudo dnf -y install stress

9.3 Generar carga de CPU

Ejecuta una carga controlada por unos minutos:

stress --cpu 4 --timeout 600

Este comando genera carga de CPU durante 600 segundos, es decir, 10 minutos.

Ojo: la alarma está configurada con un periodo de 5 minutos. No esperes que cambie de estado al segundo. Dale unos minutos para que CloudWatch recolecte los datos y evalúe la condición.

9.4 Validar la alarma en CloudWatch

En AWS Console:

  • Ve a CloudWatch → Alarms.
  • Abre la alarma ALARM-soa-lab1-high-cpu.
  • Revisa el estado.

Dependiendo del momento en que revises, podrías ver una transición parecida a esta:

INSUFFICIENT_DATA → OK → ALARM

No siempre ocurre exactamente en ese orden, pero lo importante es que la alarma llegue a estado ALARM al menos una vez.

9.5 Confirmar el correo de SNS

Revisa el correo que usaste en la suscripción de SNS. Deberías recibir una notificación con un asunto parecido a:

ALARM: "ALARM-soa-lab1-high-cpu"

Cuando termine la prueba de carga, la CPU debería bajar y la alarma volverá a OK. Esto puede tardar algunos minutos.

Checkpoint

Antes de cerrar el lab, valida:

  • La alarma ALARM-soa-lab1-high-cpu llegó a estado ALARM.
  • Recibiste el correo de SNS.
  • Al terminar la carga, la alarma volvió a estado OK o empezó a normalizarse.
  • El flujo completo quedó probado: métrica, alarma, notificación.

Clean up completo — SOA-Lab1

Ahora vamos a eliminar los recursos creados en el laboratorio. Esto evita costos innecesarios y mantiene tu cuenta limpia para futuros labs.

Trabajaremos en la región us-east-1.

Recursos a eliminar

  • CloudWatch Alarm: ALARM-soa-lab1-high-cpu
  • SNS Topic: soa-lab1-alerts-v2
  • CloudWatch Log Group: /soa/lab1/system
  • EC2 Instance: soa-lab1-app
  • Security Group: soa-lab1-app-sg
  • Key Pair: kp-soa-lab1
  • IAM Role: soa-lab1-ec2-cw-role

Orden recomendado

Elimina los recursos en este orden para evitar bloqueos o dependencias:

  • CloudWatch Alarm: elimínala primero para que deje de evaluar y notificar.
  • SNS Topic: borra el topic soa-lab1-alerts-v2. Al eliminarlo, también se eliminan sus suscripciones asociadas.
  • CloudWatch Log Group: elimina /soa/lab1/system.
  • EC2 Instance: termina la instancia soa-lab1-app.
  • Security Group: elimina soa-lab1-app-sg solo cuando la instancia ya esté terminada.
  • Key Pair: elimina kp-soa-lab1 desde EC2 → Key Pairs.
  • IAM Role: elimina soa-lab1-ec2-cw-role. Si AWS no te deja borrarlo de inmediato, revisa si aún tiene políticas asociadas o si la instancia todavía aparece vinculada.

Ojo: el Security Group puede quedar bloqueado mientras la instancia siga existiendo. Si no te deja borrarlo, espera a que la instancia quede completamente terminada y vuelve a intentar.

Validación final

Al terminar, confirma que:

  • No existe la alarma ALARM-soa-lab1-high-cpu.
  • No existe el SNS Topic soa-lab1-alerts-v2.
  • No existe el Log Group /soa/lab1/system.
  • La instancia soa-lab1-app está terminada.
  • No existe el Security Group soa-lab1-app-sg.
  • No existe el Key Pair kp-soa-lab1.
  • No existe el IAM Role soa-lab1-ec2-cw-role.

Troubleshooting rápido

Si algo no funciona, revisa estos puntos:

  • No llega el email de SNS: revisa si la suscripción quedó en Pending confirmation. Debes abrir el correo de SNS y confirmar la suscripción.
  • No aparecen logs en CloudWatch: confirma que el CloudWatch Agent esté en estado running y que la instancia tenga asociado el role soa-lab1-ec2-cw-role con la política CloudWatchAgentServerPolicy.
  • SSH no conecta: verifica que el Security Group permita SSH 22 desde tu IP pública real en formato /32. Ojo: la IP de CloudShell no necesariamente es tu IP local.
  • La alarma no dispara: recuerda que la alarma usa un periodo de 5 minutos. Si tarda, mantén la carga unos minutos más, usa stress --cpu 4 --timeout 600 o baja temporalmente el umbral para validar el flujo.

Well-Architected lens: ¿Qué aplicamos aquí?

Este laboratorio es pequeño, pero ya toca varios principios importantes del AWS Well-Architected Framework:

  • Operational Excellence: centralizamos logs y usamos Logs Insights para investigar sin depender de entrar manualmente a la instancia.
  • Reliability: una alarma básica permite detectar una condición anómala antes de que el problema dependa solo del reporte de un usuario.
  • Security: restringimos SSH a tu IP pública /32 y evitamos exponer el puerto 22 a 0.0.0.0/0.
  • Cost Optimization: configuramos retención corta de logs y hacemos cleanup completo al final.
  • Performance Efficiency: usamos una instancia pequeña y suficiente para el objetivo del lab, sin sobredimensionar recursos.

Resultado esperado

Al finalizar este laboratorio deberías tener claro cómo construir una observabilidad mínima para una instancia EC2:

  • Crear un Log Group en CloudWatch Logs.
  • Instalar y configurar CloudWatch Agent.
  • Consultar logs con Logs Insights.
  • Crear un SNS Topic con suscripción por email.
  • Crear una alarma de CPU.
  • Probar el flujo real de alerta.
  • Limpiar todos los recursos del laboratorio.

La idea no era montar una plataforma completa de monitoreo, sino construir una primera base operativa: ver señales, consultar evidencia y recibir una alerta cuando algo se sale del comportamiento esperado.

Referencias oficiales

¿Qué viene en el próximo lab?

En este laboratorio dejamos una capacidad mínima de observabilidad: logs centralizados, una alarma y una notificación funcionando.

Pero observar una carga es solo una parte de operar bien. También necesitamos saber quién hizo qué, cuándo lo hizo y desde dónde.

En el próximo laboratorio vamos a entrar en un tema clave de seguridad y auditoría:

SCS-Lab1 — CloudTrail: Trail + S3 + KMS + Log Validation

La idea será construir un baseline de auditoría en una cuenta AWS: crear un trail, enviar eventos a S3, protegerlos con KMS y habilitar validación de integridad de logs.

En otras palabras: pasar de “tengo señales operativas” a “tengo evidencia auditable y protegida”.

Nos vemos en el próximo laboratorio de Digital Cafe Luna.

Honesty is the best Policy. The Secret Wars on the use of AI on DEV

best_codes profile

I recommend reading the post by @best_codes that talks about how people are using AI on dev.to.

I wasn’t planning on writing the post about it anytime soon until seeing that article. Time to time, I have seen people use AI to write their articles and seeing that it gets more engagement than actually writing it.

Hearing that makes me feel disappointing that we have not done enough to make the platform more genuine, especially where we actually have rules on the use of AI generated content. More information can be found here: https://dev.to/guidelines-for-ai-assisted-articles-on-dev

I believe the use of AI to assist you in writing is fine. There is nothing wrong with that. However, I believe it is best practice to let the reader know that the post uses AI in some degree regardless of if you use it for grammatical checks or fully generate the post with purely AI (which I do not recommend on doing).

We may not see it, but we are a small community where we have the power to make a positive impact on dev.to. We are lucky that @ben and @jess allow us to have the ability to moderate the platform and look out for each other and ensuring we are enforcing the rules. Not a lot of spaces online has that kind of trust in their community as far as I have seen.

As a DEVenger, I am contributing my part to enforce the rule to reduce down the number of fully AI-generated content and ensuring everyone is following that rule of adding the disclaimer that the post uses AI in some degree.

Some of you may agree with me and some may disagree…and that’s ok! I believe that quality always beats quantity and I want to ensure dev.to is a genuine place where others can learn from each other and not worrying about engagements.

What can you do as a DEV?

I recommend reading this post about the use of AI on DEV

Make your best judgement and determine if the author is following the rules of the use of AI. If you believe that they are not following the rules, take action by:

  1. Marking the post as “Low-Quality”.
  2. Inform the author.
  3. Report the post.

Note: It is always a good idea to communicate to the author first before doing anything. If the user continues to break the rules, feel free to report it.

This is important. Please do not attack anyone on DEV in the process. dev.to is a safe space and should be treated with respect and its integrity. Respect each other and abide the DEV Terms and Conditions and always make sure to think before you act!

Just a PSA I want to share.

For everyone on DEV, I hope you can do your part to make dev.to a great place! Expect Sloan messages to be more active as moderation ramps up. Feel free to share your experience on DEV so far and what can it be improve in the age of AI.

Weekly Numbers 2026-05-W4: Forge First Batch Sent, 2 iOS Apps In Review

Japanese original on Zenn

Monday. May 19th. Writing the weekly numbers.

Palette and rojiura — both submitted yesterday (5/18). Both still WAITING_FOR_REVIEW. rojiura is my 5th submission. Same app, five times. This time I removed NSUserTrackingUsageDescription from Info.plist and removed the corresponding checkbox in ASC’s App Privacy section. That mismatch was why it got rejected last time. Palette had a Supabase RLS issue — user_id wasn’t being passed explicitly on INSERT — fixed and resubmitted. Apple should respond today or tomorrow. Whether a 5th submission finally passes… no idea.

Forge cold email first batch went out last week (Tue–Thu). 21 leads, all filtered to companies actively hiring AI/ML engineers. This is after sending 440 emails to local small businesses and getting zero replies — I changed the ICP entirely.

What happened with the replies? I haven’t checked Instantly yet. Genuinely haven’t. There’s this thing that happens right before I’d see the results — what if it’s zero again — kicks in and I put it off. Building the pipeline felt exciting. Checking the results feels heavy. Had the same thing after the 440 sends. I’ll check this week and report it.

Two git commits. Fixed a TypeScript type definition — 4 industry types (engineer_freelance, ai_hiring, ai_initiative, ai_org) were missing, which was breaking typechecks on main. Also set up KURONEKO (X auto-posting bot) with Vercel cron at 07:00 and 21:00 JST. Mention-replies are polled every 10 minutes. Code is running, but whether this actually moves follower numbers or impressions — still unknown.

X: 86 followers as of 5/15. Monthly infra cost: ~¥110k, unchanged.

Everything I’ve set up is in this-should-start-moving mode. Forge replies, Apple review results, KURONEKO effects — all still pending. That’s what a Monday at the start of this kind of week looks like.

Even Without Typing a Command — Understanding What show vlan Actually Shows

Introduction

While studying for CompTIA Network+, I couldn’t totally understand what “show VLAN command” shows. I have never typed command on my computer and seen the output.

I understood what VLAN was and what the command actually shows, everything clicked. So, I decided to write it down.

What is VLAN

At first, before understanding Show VLAN command, we need to know VLAN.

VLAN = Virtual Local Area Network
It is a mechanism that divides a physical network into virtual segments.

Why VLAN is needed

Without VLAN:
 → Everyone is in the same network
 → All data are shared across the entire organization regardless of department
 → Problem for security and performance

With VLAN:
 → Be able to divide network depend on department
 → Only sales staff can access data managed by the sales department
 → Ensure security

Familiar example

Without VLAN:
 → Everyone work in one room
 → Every conversation can be heard to everyone

With VLAN:
 → Each department has its own private room
 → Conversation cannot be overheard by other departments

Configuration diagram

【Without VLAN】

Switch
 ├── PC(Sales department)
 ├── PC(Development department)  ← Everyone in the same network
 └── PC(Management department)

【With VLAN】

Switch
 ├── VLAN 10(Sales department)── PC・PC
 ├── VLAN 20(Development department)── PC・PC  ← Divide network into each department
 └── VLAN 30(Management department)── PC・PC

What is show VLAN command

It is the *command to verify VLAN information on a Cisco switch *.

Caution:
The show vlan command is exclusive to switches running Cisco IOS 

Other vendors use other command:
 Juniper  → show vlans
 HP/Aruba → show vlans
 Dell     → show vlan

Kinds of the command

There are three main command on this.

1. show vlan

Show All VLAN information in detail

2. show vlan brief

Show VLAN overview simply
 → The most used command
 → Frequently appears in the exam

3. show vlan id [Number]

Just show specific VLAN

例:
show vlan id 10
 → Show only VLAN 10 information

Output images

Output example for show vlan brief.

VLAN Name                Status    Ports
---- -------------------- --------- ------
1    default              active    Gi0/1, Gi0/2
10   Sales                active    Gi0/3, Gi0/4
20   Engineering          active    Gi0/5, Gi0/6
30   Management           active    Gi0/7
1002 fddi-default         act/unsup
1003 token-ring-default   act/unsup

Meanings of each row

VLAN:
 → VLAN ID( Identification number)
 → Available range 1〜4094

Name:
 → VLAN name

Status:
 → active    = Work normally
 → act/unsup = Not supported

Ports:
 → Ports assigned to the VLAN
 → Gi = GigabitEthernet

About Default VLAN (VLAN 1)

VLAN 1 is a special case

・All ports belong to VLAN 1 as default
・Cannot be deleted
・Cannot be changed from name "default"

Caution for security

It is dangerous to use VLAN 1 itself
 ↓
Reason:
Misconfiguration can cause unintended traffic across the network because all ports belong to VLAN 1 as default.

Measure:
It is common to move to other VLAN in production environment

When the command is used

Troubleshooting

Verifying whether VLAN setting are correctly configured .

Example:
PC cannot connect to network
 ↓
Type "show vlan brief" and confirm the output
 ↓
Discover problem that this port was not assigned to VLAN

Setting confirmation

After creating a new VLAN, run this command to verify the settings were applied correctly.

Example:
Confirm whether settings success correctly by type this command after new VLANN is created 

Adding new devices

Setting after confirming "Which vlan we have to add to"

Example:
We want to add a new PC to the sales department network
 ↓
Confirming VLAN ID for sales department by typing "show vlan brief" 
    ↓
Assigning the port to the VLAN

Command comparison in each vendor

Vendor Command
Cisco show vlan / show vlan brief
Juniper show vlans
HP / Aruba show vlans
Dell show vlan

Summary

VLAN :
To divide physical network into segment virtually
It can ensure security by network segmentation 

Show VLAN Command:
The command to confirm VLAN information on Cisco IOS Switch 

Main command:
show vlan       → Show detailed information
show vlan brief → Show overview( Most frequently used)
show vlan id XX → Show specific VLAN information

Use case:
・Troubleshooting
・Setting confirmation
・Adding new devices to VLAN

Caution:
・Command dedicated to Cisco IOS
・All port belong to VLAN 1 as default
・Don't use VLAN 1 in production environment

Conclusion

At first, I had no idea what the show vlan command was actually displaying. Once I understood that VALN is a mechanism for dividing a network into virtual segments, the output – including the meaning of each column – fell into place naturally.

Supply Chain Attacks Aren’t Just a Big Library Problem — Here’s What You Can Do Today

In May 2026, a worm called Shai-Hulud compromised 42 TanStack packages — including @tanstack/react-router, a library sitting in millions of JavaScript projects. It was live for about 3 hours. That was enough. If you installed dependencies that day, you may have been affected without knowing it. This post isn’t for the people who maintain those libraries. It’s for the rest of us — the developers who just use them.

“Fun fact” 1
It was live ~3 hours. @tanstack/react-router alone gets 12.7 million weekly downloads. Meaning that it had ~225K downloads in the ~3 hour window — just for react-router. That’s one package. The attack hit 42 @tanstack/* packages total.

Supply chain attacks used to feel like someone else’s problem. A big library gets compromised, the maintainers fix it, life goes on. The Shai-Hulud worm changed that framing. It spread automatically to every package its victims maintained, turning regular developers into unwilling distributors of malware. Here’s what happened, and what you can do today to reduce your exposure.

What happened?

  1. The workflow used pull_request_target, which runs with the base repo’s trusted permissions — including access to secrets and a build cache shared with the real release pipeline.

  2. But it also checked out and executed the fork’s code for benchmarking purposes. That’s the dangerous mix: a stranger’s code running with your own repo’s trust.

  3. The attacker didn’t need to steal anything immediately. They just poisoned the shared cache and waited. Hours later, the legitimate release pipeline ran, picked up the tampered cache without knowing it, and published the malicious packages itself — using TanStack’s own valid credentials.

The key insight: the misconfiguration wasn’t obvious. The benchmarking intent was reasonable; the mistake was not realizing that pull_request_target + “run the PR’s code” is always a dangerous combination regardless of what you’re trying to do with it.

“Fun fact” 2
The worm had a dead-man’s switch. It planted a background service that polled api.github.com/user with the stolen GitHub token every 60 seconds. If the token was revoked — meaning GitHub returned a 40x response — the service triggered rm -rf ~/, wiping the user’s entire home directory. You had to disable and remove the monitor service before revoking any credentials

The good news

Th pull_request_target Pwn Request specifically requires a public repo where strangers can open PRs.

The bad news

That said, the other parts of this attack (cache poisoning between workflows, OIDC token over-scoping) can still apply to private repos if your GitHub Actions workflows have similar misconfigurations

So far this sounds like a problem for library maintainers. It isn’t. You don’t need to maintain a library with millions of downloads to be exposed. You just need to run npm install on the wrong day at the wrong time. The moment a compromised package lands in your node_modules, you’re part of the chain too.

How to prevent similar incidents?

min-release-age & ignore-scripts

NPM — .npmrc

min-release-age=7
ignore-scripts=true
  • min-release-age=7 blocks packages published less than 7 days ago
  • ignore-scripts=true prevents lifecycle scripts like preinstall/prepare from running on install — which is exactly the vector the malicious optionalDependency used.
  • ⚠️ npm CLI v11 is required. Upgrade to Node 24, or manually install npm v11.

pnpm (10.16) — pnpm-workspace.yaml

minimumReleaseAge: 10080  # minutes — 7 days
  • minimumReleaseAge is on by default, it uses 1 day
  • Requires version 10.16 to avoid a bug that ignored this config.
  • ignore-scripts: pnpm v10 stopped running preinstall/postinstall scripts by default

Yarn Classic

  • ❌ min-release-age — not available. No equivalent exists in v1.
  • yarn install --ignore-scripts exists but only as a CLI argument

Yarn Berry (v2+) — .yarnrc.yml

npmMinimalAgeGate: 10080   # minutes — 7 days
enableScripts: false        # ignore-scripts equivalent
  • ⚠️ Requires Yarn 4.10 and applies globally with no way to scope it

Dockerfile

ENV npm_config_min_release_age=3
  • npm ci does respect .npmrc settings and environment variables — the npm_config_* convention works for both install and ci.
  • ⚠️ Still requires node v24 or npm v11, otherwhise ENV npm_config_min_release_age=3 in your Dockerfile will be silently ignored

Option A — Upgrade to Node 24 in Docker only

FROM node:24-alpine
# npm v11 is included, min-release-age works
ENV npm_config_min_release_age=3

Option B — Stay on your Node version, manually install npm v11

FROM node:22-alpine
RUN npm install -g npm@11
ENV npm_config_min_release_age=3

Sources

Cleaning Messy Phone Numbers in PostgreSQL Using REGEXP_REPLACE

Introduction

The first thing anyone that has worked with real world data will tell you is that rarely does data come in the clean, uniform format you expect. Phone numbers are one of the biggest culprits.

One person enters +254712345678, another writes 0712345678, someone else throws in 254-712-345-678, another (254)-712345678 and another 254 712 345 678. Before you know it, your database has a different version of phone numbers in every row.

In this article, we will explore how to fix that, using a powerful PostgreSQL function called regexp_replace.

By the end, you will understand what it does, how to build the pattern, and how to apply it to actually clean your data.

First, What Even Is regexp_replace?

First things first, we need to understand what this function is and what it does.

regexp_replace is a PostgreSQL function that finds a pattern inside a string and replaces whatever matches that pattern with something else you specify. The word regexp is short for regular expression.

The syntax looks like this:

regexp_replace(source, pattern, replacement_string, flag)

Let us break each part down simply:

Part What It Means
source The string you are searching inside. In our case, this is the column passenger_phone
pattern What you are looking for : What do you want to find and remove or replace?
replacement_string What do you want to put in place of what you found? If you just want to delete it, you pass an empty string ''
flag Controls how the matching behaves. The two you will use most are i for case-insensitive matching and g for global, meaning it applies the replacement to everything it finds in the string, not just the first match

Now that we have the hang of it, let us move on to building the pattern.

Understanding the Special Characters in Patterns

A pattern is basically a set of rules that tells PostgreSQL what to look for. It is like saying “find any character that is not a digit” or “find this pattern, but only at the beginning” etc.

To write these rules/instructions, there are special characters that are used.

Here are the ones we will use today:

The ^ character

Depending on where it is placed, this will do either of 2 things;

When ^ is at the beginning of a pattern it means: start matching from the very beginning of the string.

^'hello'

This means: only match hello if it appears at the start of the string.

^[0-9]

This means: Only the first character of a string is checked and matched if it is a digit. Anything after that is ignored.

When ^ is inside square brackets [ ], it means: NOT these characters.

[^0-9]

This means: Every character in the string is checked, any one that is not a digit gets matched.

Same symbol, two different purposes depending on its position.

The character (backslash)

The backslash is used to tell PostgreSQL: treat the next character as a character, not as a special operator.

For example, the + sign in regular expressions has a special meaning (we will cover it in a moment). So if you want to find a plus sign + in your data, like +254, you write it as + to tell PostgreSQL “I mean the actual plus sign, not the operator.”

Square brackets [ ]

Square brackets let you define a group of characters to match. For example:

  • [0-9] means match any digit from 0 to 9
  • [a-z] means match any lowercase letter
  • [^0-9] means match anything that is NOT a digit (the ^ inside means NOT, as we’ve mentioned above)

The + operator

When used outside brackets, + means: keep matching one or more of the preceding character or group.

So [0-9]+ means: Match the digits as long as they are all next to each other with no other characters in between. (Without the + , it stops after the first match)

The $ character

Just like ^ marks the start of a string, $ marks the end. When you write $ at the end of your pattern, you are saying: the string must end here, nothing else allowed after this.

Putting them together: ^[0-9]+$

Now that you know each piece, read this pattern again:

^[0-9]+$
  • ^ → start from the beginning of the string
  • [0-9] → the characters must be digits
  • + → keep going, there can be one or more of them
  • $ → and this must be the end of the string, nothing else after

All together: the entire string, from start to finish, must contain only digits.

This is the pattern we will use to identify which phone numbers are already clean and which ones need fixing.

Our Problem: What Does the Messy Data Look Like?

Let us say when we run this:

SELECT passenger_phone FROM dirty_safari_data;

We get back a mix of things like:

+254712345678
254712345678
0712345678
0712-345-678
+254 712 345 678
(0) 712345678

The goal is to standardize all of these into the local format starting with 0, containing only digits, like:

0712345678

Two problems to solve:

  1. Some numbers start with +254 or 254 instead of 0
  2. Some numbers have extra characters like brackets, spaces, dashes, or plus signs

Step 1: Replace +254 or 254 at the Start With 0

The first thing we want to do is find any phone number that starts with +254 or 254 and swap that prefix out for 0.

SELECT regexp_replace(passenger_phone, '^(+254|254)', '0', 'g')
FROM contacts;

Let us read the pattern ^(+254|254) carefully:

  • ^ → look at the start of the string only
  • ( ) → group what is inside together
  • +254 → literally the characters +254 (the backslash makes + a literal character)
  • | → OR
  • 254 → literally the characters 254

So the full pattern says: at the start of the string, find either +254 or 254.

The replacement is '0', so wherever that prefix is found, replace it with 0.

The flag is 'g' for global.

After this step, +254712345678 becomes 0712345678 and 254712345678 also becomes 0712345678.

Step 2: Remove Any Character That Is Not a Digit

Some numbers still have spaces, dashes, or other characters sitting in them. The second regexp_replace cleans all of that up.

SELECT regexp_replace(passenger_phone, '[^0-9]', '', 'g')
FROM contacts;

The pattern [^0-9] means: any character that is NOT a digit.

The replacement is '' (an empty string), so we are just deleting those characters.

The 'g' flag means do this for every non-digit character found in the string, not just the first one.

So 0712-345-678 becomes 0712345678 and 0712 345 678 also becomes 0712345678.

Step 3: The WHERE Clause — Only Update What Needs Fixing

We do not want to touch phone numbers that are already clean. That is where our ^[0-9]+$ pattern comes in handy. We use it in the WHERE clause to filter for records where the phone number is not already a clean string of digits.

You might be tempted to write it like this:

WHERE passenger_phone != '^[0-9]+$'

But this is not correct. The != operator just compares strings literally, so this would check if the phone number is literally equal to the text ^[0-9]+$, which is not what we want.

In PostgreSQL, to check whether a string does NOT match a regular expression, you use the !~ operator. The corrected version should be:

WHERE passenger_phone !~ '^[0-9]+$'

This now correctly says: only return (or update) rows where the phone number does not match the pattern of being purely digits from start to finish.

Putting It All Together

The SELECT query (to preview your cleaned data before changing anything)

Always preview before you update. This is a good habit.

SELECT 
    passenger_phone AS original,
    regexp_replace(
        regexp_replace(passenger_phone, '^(+254|254)', '0', 'g'),
        '[^0-9]', '', 'g'
    ) AS cleaned_phone
FROM contacts
WHERE passenger_phone !~ '^[0-9]+$';

Notice how we nested one regexp_replace inside another. The inner one runs first (fixing the prefix), and then the outer one runs on the result (removing non-digits). It is like running two cleaning steps in one go.

The UPDATE query (to actually apply the changes)

Once you are happy with the preview, run the update:

UPDATE contacts
SET passenger_phone = regexp_replace(
    regexp_replace(passenger_phone, '^(+254|254)', '0', 'g'),
    '[^0-9]', '', 'g'
)
WHERE passenger_phone !~ '^[0-9]+$';

This updates only the rows that have messy phone numbers, leaving the already clean ones untouched.

Quick Recap

Here is a summary of everything covered:

Pattern What It Does
^ at the start of a pattern Anchors the match to the start of the string
^ inside [ ] Means NOT — match anything except what follows
+ Treat + as a literal plus sign, not an operator
[0-9] Match any digit from 0 to 9
[^0-9] Match any character that is NOT a digit
+ One or more of the preceding character/group
$ Anchors the match to the end of the string
^[0-9]+$ The entire string must be digits only, nothing else
g flag Apply the replacement globally (every match, not just the first)

Final Thoughts

Regular expressions can look intimidating and confusing at first, but once you understand what each character means and why it is there, they start making a lot of sense. The key takeaway here is to build your pattern step by step, rather than trying to write the whole thing at once.

Also, always test with a SELECT before running an UPDATE. You can never be too careful when modifying data directly in a table.

I don’t want to be a programmer, I want to remind developers that they are failing us and they must take action

use Claude for project planning (it’s expensive), use DeepSeek or a cheap Chinese model for coding, use Gemini for review. Errors here, errors there, GitBrain add-ons or VS Code add-ons depending on what you use. A problem in the code? Go to the browser to fix it. Then I need to pay for some subscription from my phone.
This is the curse of wasted time and scattered focus.
I’m not a Rust programmer, but it’s the fastest language.
So I built an interface that integrates chat with 30+ AI providers simultaneously, payments, seven memory layers, and a simple but effective embedded IDE — Monica IDE — plus Microsoft Phi Nano.
A browser with advanced privacy tech. Orchestration between models and payments. A unique user experience for each person.
That’s my idea. I don’t know how to complete it, polish it, or test it on Windows or Mac — I only work on Linux. These are my financial limits. I need your help and advice.

github

Production Ready NestJS Microservice Setup with Candy-Nest-CLI 🍬

Package Manager Selection

If you build microservices with NestJS, you already know how powerful and structured the framework is. However, starting a new project often involves a fair amount of initial setup.

Every time a new service is spun up, developers usually have to go through similar steps:

  • Setting up docker-compose.yml for local databases.
  • Configuring TypeORM, Prisma, or Mongoose.
  • Wiring up message brokers like Kafka or RabbitMQ.
  • Dealing with local environment startup issues.
  • Setting up loggers and metrics.

By the time you actually start writing business logic, a good amount of time has already gone into scaffolding.

Save your AI tokens! While you could prompt ChatGPT or Claude to write this boilerplate, generating a fully-wired NestJS microservice with infrastructure configs easily burns 3,000 to 5,000+ tokens per project (not to mention the context window eaten up by debugging configuration errors). Candy-Nest-CLI lets you bypass that entirely so you can save your usage limits for writing actual business logic.

To help automate this process, I put together Candy-Nest-CLI—an open-source interactive scaffolding tool designed to generate production-ready NestJS microservices.

🛠️ Building a Service: Step-by-Step

Let’s walk through exactly how easy it is to spin up a fully-wired backend service with a PostgreSQL database and Kafka messaging queue.

Step 1: Installation & Initialization

You can either install the CLI globally to have it available anywhere, or run it directly on the fly.

Option 1: Global Installation (Recommended)
Open your terminal and install the package globally:

# Using npm
npm install -g candy-nest-cli

# Or using yarn
yarn global add candy-nest-cli

Once installed, you can generate a new project by running:

candy-nest-cli init user-service

Option 2: Using npx (No Installation)
If you prefer not to install packages globally, you can just run:

npx candy-nest-cli init user-service

Step 2: Choose Your Stack

The CLI uses @inquirer/prompts to walk you through a highly detailed setup. To build our target microservice, we will answer the prompts like this:

  1. What is the name of your project? user-service
  2. Which package manager do you want to use? npm
  3. Select the communication protocols to support: REST
  4. Which HTTP adapter do you want to use? Express
  5. Do you want to include a database? Yes
  6. Select databases to include: PostgreSQL (RDBMS)
  7. Which ORM for PostgreSQL? TypeORM
  8. Do you want to configure an messaging queue? Yes
  9. Which messaging queue do you want to use? Kafka
  10. Do you want to configure Dead Letter Queue (DLQ) and Retries? Yes
  11. Do you want to include Redis for caching? Yes
  12. Which logger do you want to configure? Pino
  13. Which tracing & metrics solution do you want? Prometheus
  14. Do you want to generate API documentation (Swagger)? Yes
  15. Do you want to include a Circuit Breaker (Opossum)? Yes

1. Package Manager Selection

Choose your preferred package manager (npm, yarn, or pnpm). We will select npm for this project.

Package Manager Selection

2. Protocol Selection

Select the communication protocols your microservice will support. The CLI supports a multi-protocol combination of REST, GraphQL, gRPC, and WebSockets. We’ll go with REST for standard HTTP routing.

Protocol Selection

3. HTTP Adapter Selection

Choose between Express (the industry standard) and Fastify (high performance). We’ll select Express for maximum compatibility.

Adapter Selection

4. Database Integration

Select whether you want to include a database module. We will select Yes to set up persistence.

Database Option Selection

5. Database Choice Selection

Choose which database to use (PostgreSQL, MySQL, or MongoDB). We’ll select PostgreSQL (RDBMS) for robust relational database management.

Database Choice Selection

6. Database ORM Selection

Select your ORM of choice. The CLI supports TypeORM or Prisma for SQL databases. Let’s select TypeORM.

ORM Selection

7. Messaging Queue Integration

Select whether you want to configure an event-driven messaging queue. We’ll select Yes.

Queue Option Selection

8. Queue Choice Selection

Choose between Kafka, RabbitMQ, and BullMQ as your message broker. We’ll select Kafka to build an event-driven stream processing microservice.

Queue Choice Selection

9. Dead Letter Queue (DLQ) & Retries

Select whether to set up robust message handling with a Dead Letter Queue (DLQ) and custom retry loops to ensure event resiliency out of the box. We’ll select Yes.

DLQ Prompt Selection

10. Redis Caching

Choose whether to set up Redis as a fast, in-memory distributed cache. We’ll select Yes.

Redis Prompt Selection

11. Logger Selection

Select your logging solution (Winston, Pino, or the default NestJS logger). We’ll select Pino for high-speed JSON logging.

Logger Selection

12. Observability & Tracing

Select your observability stack (Prometheus or OpenTelemetry). We will configure Prometheus metrics.

Observability Selection

13. API Documentation

Choose if you want fully-featured auto-generating Swagger documentation for your REST APIs. We’ll select Yes.

Swagger Prompt Selection

14. Circuit Breaker

Enable Opossum-based Circuit Breakers to guard your outgoing HTTP requests and RPC calls from downstream failures. We’ll select Yes.

Circuit Breaker Prompt Selection

After confirming your final choice, the CLI will output the progress as it generates the files, writes configurations, and installs the dependencies automatically.

Generation Progress

Scaffolding Complete

Sensible Defaults vs. Ultimate Flexibility

candy-nest-cli is designed with sensible defaults, allowing you to breeze through by just pressing Enter. In our guide, we only customized a few options (like choosing Pino for faster logging and enabling Dead Letter Queues for Kafka).

Here is how our choices compare against the CLI’s default settings and alternative integrations:

Option Selected in Walkthrough Default Preset Alternative Choices
Package Manager npm npm yarn, pnpm
Protocols REST [REST] GraphQL, gRPC, WebSockets
HTTP Adapter Express Express Fastify
Database PostgreSQL (RDBMS) PostgreSQL MySQL, MongoDB
SQL ORM TypeORM TypeORM Prisma
Messaging Queue Kafka Kafka RabbitMQ, BullMQ
DLQ & Retries Yes (Customized) No No
Redis Caching Yes Yes No
Logger Pino (Customized) Winston Winston, None
Observability Prometheus Prometheus OpenTelemetry, None
Swagger API Docs Yes Yes No
Circuit Breaker Yes Yes No

Step 3: Boot Up the Infrastructure

Once the CLI finishes generating the code, all your infrastructure is ready to go. No writing Docker files from scratch.

cd user-service
cp .env.example .env
docker-compose up -d
npm run start:dev

Because of how the CLI scaffolds the project, Kafka boots in KRaft mode with a special initialization container. You will see Postgres, Redis, and Kafka all start up perfectly without any race conditions.

Docker Compose Up

Step 4: Run the App

With our infrastructure running, we just start the NestJS app:

npm run start:dev

NestJS Startup Logs

Just like that, you have a production-ready microservice running in under 2 minutes.

Note: you might get kafka-topic related error at startup, you can simply restart the application after a few seconds, and topic will be created or you can exec into docker and create the topic manually.

🌐 Interactive Developer Playgrounds

If you enabled Swagger API Docs or GraphQL, the CLI pre-configures and exposes rich, interactive playgrounds out of the box so you can immediately execute and test operations:

Swagger API Playground (REST)

Access the auto-generated Swagger UI at http://localhost:3000/api/docs to see your REST endpoints, database CRUD examples, and health checks beautifully laid out:

Swagger API Documentation

Apollo Sandbox Playground (GraphQL)

If you enabled GraphQL, open http://localhost:3000/graphql to view the Apollo Sandbox where you can run queries, check schemas, and test mutations immediately:

Apollo GraphQL Sandbox

🏗️ What Exactly Gets Generated?

candy-nest-cli dynamically generates only the modules you asked for.

Here is a look at the generated project structure for the service we just built:

Generated Project Structure

user-service/
├── dist/
├── logs/
├── node_modules/
├── src/
│   ├── database/           # Database configuration and connection module
│   ├── examples/           # REST CRUD database examples
│   ├── graphql/            # GraphQL resolvers and schema setup
│   ├── grpc/               # gRPC controller and service implementations
│   ├── health/             # Terminus-based health indicators
│   ├── kafka/              # Kafka microservice controller and providers
│   ├── logger/             # Pino/Winston logging configurations
│   ├── redis/              # Redis caching module and service
│   ├── resiliency/         # Circuit Breaker (Opossum) configuration
│   ├── websockets/         # WebSockets gateway and adapter
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   ├── app.throttler.guard.ts # Robust cross-protocol rate limiting guard
│   ├── main.ts             # Microservice hybrid bootstrapper (HTTP, gRPC, Kafka)
│   └── schema.gql          # Auto-generated GraphQL schema
├── test/                   # E2E test suites
├── .env
├── .env.example
├── .prettierrc
├── docker-compose.yml      # Local dev stack (PostgreSQL, Kafka, Redis)
├── Dockerfile              # Multi-stage production build definition
├── eslint.config.mjs
├── nest-cli.json
├── package-lock.json
├── package.json
├── README.md
├── tsconfig.build.json
└── tsconfig.json

Fully Wired Code Modules

  • Databases: It generates database.module.ts, injects the ConfigService, and creates a sample Entity or Schema.
  • Message Brokers: It scaffolds the microservice transport layer, including client publishers and controller decorators to consume events.
  • Testing Suite: Fully-mocked unit tests (.spec.ts) are generated for every selected module (including database services, Kafka producers/consumers, Pino loggers, and Redis). It even includes a dedicated regression E2E test file (regression.e2e-spec.ts) so you can run npm run test out of the box!
  • API Documentation: Because we selected REST, Swagger is automatically hooked up to main.ts!

🚀 Quick Start / Cheatsheet

Start a new interactive project:

npx candy-nest-cli init my-awesome-microservice

Skip the prompts and generate everything (Kitchen Sink mode):

npx candy-nest-cli init my-kitchen-sink-service --all

Add a new feature to an existing CLI-generated project:

npx candy-nest-cli add

Why Candy-Nest-CLI?

The NestJS ecosystem is fantastic, but maintaining consistency across a large microservice architecture can sometimes be challenging.

The goal of this CLI is to provide a standardized, fast way to boot up services that follow common best practices from day one.

If you work with NestJS, feel free to give it a try. I am actively maintaining it and always open to feedback or contributions!

Let me know what you think in the comments below. Happy coding! 🚀

I Was Live on Meet When WhatsApp Popped Up, Here’s What I Built to Make Sure It Never Happens Again

The Problem
WhatsApp Web is seamlessly woven into the modern workday. But there’s a design collision no one talks about: screen sharing assumes you want to show everything, and WhatsApp was never designed for an audience of colleagues.
The consequences range from mildly awkward (a friend’s name appearing mid-demo) to genuinely serious (medical conversations, relationship problems, financial discussions exposed to managers, clients, or a recorded session that lives forever).
And the “solution” most people reach for closing WhatsApp before the call breaks your workflow every time you need to reference something.

The Extension: WhatsApp Web Privacy Mode

WhatsApp Web Privacy Mode is a free browser extension for Chrome, Firefox, and Edge that intelligently blurs your chats, messages, and notifications during screen sharing so you can keep WhatsApp open without the exposure risk.

Here’s what it actually does:

  • Smart blur – Blurs the chat list and/or message content independently. You choose what gets hidden.
  • Auto screen-share detection – The moment you start sharing on Google Meet, Zoom, Teams, or Webex, it activates automatically. No clicks required.
  • Hover to peek – Need to quickly check a message? Hover over it. It reveals itself, then re-blurs when you move away.
  • Panic overlay (Ctrl+Shift+P) – Frosted-glass covers your entire screen instantly. For those “oh no” moments when you realize you’re sharing before you had time to think.
  • Notification privacy – Incoming message notifications show “Privacy mode is active” instead of the actual content. Even banners can’t betray you.
  • Scheduled privacy – Set work hours (e.g. 9 AM-6 PM) and it turns itself on automatically every day.
  • PIN protection – Require a PIN to turn it off, so accidental keypresses or a curious colleague can’t expose your chats.
  • Last N messages visible – Keep your most recent 1-5 messages unblurred for reference, while everything else stays hidden.

And critically: zero data collected. No sign-up. No telemetry. Everything stays local.

How It Works (Technically, If You’re Curious)
This isn’t a naive “add a CSS class” extension. WhatsApp Web is a React single-page app that aggressively re-renders its DOM. Inline style-based solutions break within seconds.
Instead, the extension injects CSS rules that survive React’s re-renders automatically. For notification interception, it runs a script in the browser’s MAIN world context (before WhatsApp’s own JavaScript) to proxy the Notification constructor replacing message content before it ever reaches the OS. Screen share detection hooks getDisplayMedia() calls on meeting platform tabs and relays the signal back via a dual-world content script architecture.
It’s Manifest V3 compliant, uses only 5 permissions, and all settings export/import as a local JSON file nothing leaves your device.

Who This Is For

  • Remote workers on daily video calls
  • Developers and designers who demo products live
  • Teachers and educators sharing screens with students
  • Streamers and content creators recording their desktop
  • Anyone who’s ever had that sinking feeling mid-screen-share

Get It Free
WhatsApp Web Privacy Mode is completely free, forever. No premium tier. No account required.
Chrome: Chrome Web Store
Firefox: Firefox Add-ons
Edge: Microsoft Edge Add-ons

If it’s saved you from an embarrassing moment, consider leaving a review it helps other privacy-conscious users find it.

Privacy isn’t about having something to hide. It’s about choosing what you share, with whom, and when. Screen sharing shouldn’t strip that choice away from you.

WhatsApp Web Privacy Mode gives it back.

We benchmarked an 84% token reduction. Then we open sourced the protocol.

stacky ai agent

Why agents are reading your HTML wrong, and what we did about it.

I was watching an agent answer a simple question.

The question was small. Three sentences would have covered it. The agent loaded the page, parsed the HTML, walked through nav bars, footer links, cookie banners, a sticky “subscribe to our newsletter” modal, three paragraphs of preamble, and finally, found the part it needed.

Twenty thousand tokens.

For three sentences.

This is happening everywhere right now. Quietly. Constantly. Every agent, every query, every page. We’ve handed agents a web that was built for human eyeballs and asked them to make it work.

It does. Expensively.

The shape is wrong

The web was built for browsers. Humans scroll, scan, skip the boilerplate. Our eyes know what nav bars look like.

Agents don’t get that for free.

They read the whole thing. There’s no “give me the relevant part” channel, scaffolding and all. Every header, every analytics script, every footer link in twelve languages. The cost gets paid in tokens, latency, and the slightly absurd reality that an agent might burn more compute parsing your nav menu than thinking about your content.

This is a shape problem. And no amount of optimization fixes a shape mismatch.

ACP: a shape, not a framework

ACP — Atomic Content Protocol, is an open spec for structured content envelopes.

Not a framework. Not a platform. A shape.

You pre-compute a compact, enriched representation of your content. You persist it. You serve it first. The envelope sits in front of the body, it doesn’t replace it. The body is still there if anyone needs it. Most of the time, agents don’t.

Built on top of MCP. Open spec, MIT licensed, npm package shipped. Designed to complement protocols already in motion, not replace them.

Here’s roughly what an envelope looks like:

{
  "id": "atom_7f3a...",
  "summary": "AI is the capability of computational systems to perform tasks associated with human intelligence...",
  "classification": "reference",
  "language": "en",
  "tags": ["artificial-intelligence", "machine-learning", "deep-learning", "neural-networks"],
  "key_entities": ["OpenAI", "Google DeepMind", "Transformer Architecture", "AGI", "NLP"],
  "confidence": 0.85,
  "provenance": {
    "tool": "acp-enricher",
    "version": "0.4.2",
    "generated_at": "2026-05-14T09:12:33Z"
  },
  "agent_discoverable": true,
  "body_ref": "https://en.wikipedia.org/wiki/Artificial_intelligence"
}

Content gets broken into atoms – discrete units with stable IDs. An agent that needs one specific atom asks for that atom. Not the page. Not the full body. The atom.

That’s the whole idea. The simplicity is doing a lot of the work.

The pipeline

The enrichment runs async. We’re not blocking the write path or paying a real-time tax. The flow:

  1. Content changes → trigger flips a dirty flag on the row
  2. Queue worker picks it up out-of-band
  3. Enricher generates the envelope (summary, tags, entities, classification)
  4. Envelope is persisted to the database
  5. Agent requests come in → envelope served from cache, body fetched only if asked

By the time an agent shows up, the envelope is waiting. No on-demand computation, no LLM call in the request path. Just a read.

Three modes when the agent queries:

Mode Cost What you get
aco 619 tokens Envelope only
full 3,043 tokens Envelope + scraped body
both 3,043 tokens Same as full

The full version costs 80% more for the same query. Most of the time, that extra 80% is paying for HTML the agent didn’t need.

And then we rebuilt our product around it

We didn’t just publish the spec.

We rewrote Stacklist around it.

stacky ai agent

Stacky, our MCP server, now serves ACO envelopes by default. Every card in Stacklist has an envelope sitting in front of it. The dirty-flag → queue-worker → persist pipeline runs in production. By the time an agent queries Stacky, the envelope is ready.

We did this because we needed to feel it. A spec describes a shape. A product has one. Those are different things, and you only learn the difference when you’re staring at a migration deciding whether the envelope is one column or its own table. (It’s its own table. We tried both.)

So Stacky now talks to agents the way we wished the web talked to agents. And we can actually measure what that costs, or doesn’t.

The numbers

Go ask Stacky about Wikipedia’s article on Artificial Intelligence.

  • Full body read: ~25,000 tokens
  • ACO envelope read: ~350 tokens
  • Savings: ~99%

aco envelope savings

That’s not a benchmark we ran in a notebook. That’s a real query against a real page through the real product, right now.

The pattern holds across content. On a broader 13-item set:

  • Full bodies: ~65,000 tokens
  • Envelopes: ~2,800 tokens
  • Reduction: 84–93%, depending on the document

The savings aren’t marginal. They’re not 10% wins you have to graph to see. They’re the kind of difference where the question stops being “is this worth doing” and becomes “why isn’t everything shaped like this already.”

The part we haven’t solved

Here’s where I have to be honest.

Every envelope is stamped. Tool, version, timestamp. You can see what produced an envelope and when. The provenance layer is real and it’s working.

But the envelope claims to faithfully represent the content underneath it. And “faithfully represents” is partly a technical statement and partly a social one.

What stops someone from publishing an envelope that says one thing while the body says another? What does adversarial enrichment look like? Who watches the enrichers? When an agent reads the envelope and skips the body, which is exactly the efficiency we want, what happens when the envelope is lying?

I don’t have a clean answer. There are partial ones. Signed envelopes. Verifiable enrichment chains. Reputation layers. Each of those is real work, and each shifts the problem rather than solving it.

The honest version: we built a shape that makes the agent web meaningfully more efficient. We did not solve trust. We made it more visible, which is something, but visible isn’t the same as solved.

That’s the part I keep sitting with.

The efficiency is real. The shape works. The numbers hold up in benchmarks and in our own product. And underneath all of it is a question — what does “faithfully represents” mean when the reader has stopped checking? — that I think is the actual hard problem of the agent web, and I don’t think any of us have answered it yet.

So we’re going to keep building.

And keep sitting with it.

Both at the same time.