Cloud, DevOps e IoT en español

Parte 8 de 22 de la serie Domótica con ESP32 y AWS desde cero

API serverless para controlar un ESP32 con AWS Lambda

5 de octubre de 2026 · Steven Carvajal · Tutoriales

Código de este artículo en GitHub →

La web app puede hablar con AWS IoT Core directamente (parte 10), pero hay clientes que no pueden: un Atajo de Siri, un script en otro servidor, un botón de otra plataforma. Solo saben hacer una petición HTTP. Para ellos, lo más simple es una API HTTPS que por dentro cambie el Device Shadow.

En esta parte construimos esa API con una función Lambda, vemos las opciones para exponerla y protegerla, y la probamos con curl. En la parte 9 la llamamos desde Siri.

La idea

Cliente HTTP  --POST /orden-->  Lambda  --UpdateThingShadow-->  AWS IoT Core  --delta-->  ESP32

La Lambda hace tres cosas: comprueba quién llama, valida la orden y actualiza el estado deseado del dispositivo. El ESP32 no cambia nada: recibe el delta como siempre (parte 5).

Cómo exponer la Lambda

Lambda Function URLAPI Gateway (HTTP API)
ConfiguraciónUna opción en la funciónRutas, integraciones y etapas
Costo extraNinguno, solo la LambdaPor millón de peticiones (bajo)
Autenticación incluidaIAM o ningunaIAM, JWT (Cognito) o authorizer propio
Límite de peticionesConcurrencia de la LambdaThrottling configurable por ruta
Dominio propioNo directamenteSí

Function URL es ideal para una API pequeña con un solo endpoint. API Gateway conviene cuando quieres varias rutas, un dominio propio o validar tokens de Cognito sin escribir código.

Cómo autenticar

OpciónCómo funcionaPara quién
IAM (SigV4)El cliente firma cada petición con credenciales de AWSOtros servicios de AWS, scripts con la CLI
JWT de CognitoEl cliente envía el token del login; API Gateway lo verificaTu propia app web o móvil
Token propioEl cliente envía un secreto en una cabecera; la Lambda lo comparaClientes simples como un Atajo de Siri

Un Atajo de iOS no puede firmar con SigV4 ni renovar un token de Cognito con facilidad, así que para la voz lo práctico es un token propio. Reglas si eliges esta opción:

El código de la Lambda

Node.js 22 con el SDK v3 de AWS. Lee el token esperado de Parameter Store una vez por contenedor:

import { IoTDataPlaneClient, UpdateThingShadowCommand } from "@aws-sdk/client-iot-data-plane";
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";
import { timingSafeEqual } from "node:crypto";

const iot = new IoTDataPlaneClient({});
const ssm = new SSMClient({});

const THING = process.env.THING_NAME;                  // el dispositivo que controla esta API
const SALIDAS = new Set(["sala", "cocina"]);           // lista blanca de claves del Shadow
let tokenEsperado;                                     // caché por contenedor

async function obtenerToken() {
  if (!tokenEsperado) {
    const r = await ssm.send(new GetParameterCommand({ Name: process.env.TOKEN_PARAM, WithDecryption: true }));
    tokenEsperado = Buffer.from(r.Parameter.Value);
  }
  return tokenEsperado;
}

const respuesta = (statusCode, cuerpo) => ({
  statusCode,
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(cuerpo),
});

export const handler = async (event) => {
  // 1. Quién llama
  const recibido = Buffer.from(event.headers?.["x-api-token"] ?? "");
  const esperado = await obtenerToken();
  if (recibido.length !== esperado.length || !timingSafeEqual(recibido, esperado)) {
    return respuesta(401, { error: "No autorizado" });
  }

  // 2. Qué pide
  let orden;
  try {
    orden = JSON.parse(event.body ?? "{}");
  } catch {
    return respuesta(400, { error: "JSON inválido" });
  }
  const { salida, valor } = orden;
  if (!SALIDAS.has(salida) || !["on", "off"].includes(valor)) {
    return respuesta(400, { error: "Orden no válida" });
  }

  // 3. Cambiar el estado deseado
  const payload = JSON.stringify({ state: { desired: { [salida]: valor } } });
  await iot.send(new UpdateThingShadowCommand({ thingName: THING, payload: Buffer.from(payload) }));

  return respuesta(200, { ok: true, salida, valor });
};

Fíjate en la lista blanca de salidas: la Lambda nunca escribe en el Shadow una clave que no conoce, aunque el cliente la envíe.

Permisos mínimos de la Lambda

El rol de ejecución solo necesita dos permisos, sobre recursos concretos:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iot:UpdateThingShadow",
      "Resource": "arn:aws:iot:REGION:CUENTA:thing/mi-esp32"
    },
    {
      "Effect": "Allow",
      "Action": "ssm:GetParameter",
      "Resource": "arn:aws:ssm:REGION:CUENTA:parameter/demo/api-token"
    }
  ]
}

Más los permisos básicos de registros en CloudWatch (la política administrada AWSLambdaBasicExecutionRole). Si el parámetro está cifrado con una clave KMS propia, añade kms:Decrypt sobre esa clave.

Crear la Function URL

En la consola: Lambda → tu función → Configuration → Function URL → Create. Con Auth type: NONE la URL es pública y la autenticación la hace tu código; con AWS_IAM, AWS exige firma SigV4.

Con NONE, cualquiera que conozca la URL puede invocar la función. Tu código rechaza las peticiones sin token, pero cada intento cuenta como una invocación. Para limitar el abuso:

Probar con curl

# Sin token: 401
curl -i -X POST "$URL" -d '{"salida":"sala","valor":"on"}'

# Orden válida: 200
curl -i -X POST "$URL" -H "x-api-token: $TOKEN" \
  -H "Content-Type: application/json" -d '{"salida":"sala","valor":"on"}'

# Salida desconocida: 400
curl -i -X POST "$URL" -H "x-api-token: $TOKEN" -d '{"salida":"garaje","valor":"on"}'

Terminal con tres llamadas curl a la API: sin token responde 401, con una orden válida responde 200 y con una salida desconocida responde 400

Respuestas esperadas. La URL y el token son de ejemplo.

Con la orden válida, el ESP32 recibe el delta y enciende la salida. Puedes comprobarlo en el Shadow desde la consola de AWS IoT.

Errores comunes

Preguntas frecuentes

¿Por qué no publicar directamente por MQTT desde la Lambda?

Puedes, con PublishCommand del mismo SDK. Actualizar el Shadow tiene la ventaja de que la orden queda guardada si el dispositivo está desconectado.

¿Una API key de API Gateway sirve como autenticación?

No es su propósito: las API keys de API Gateway son para planes de uso y límites, no para identificar usuarios. Úsalas junto con otra forma de autenticación.

¿Cuánto cuesta?

Con unas decenas de órdenes al día, la Lambda entra en la capa gratuita y la actualización del Shadow cuesta fracciones de centavo. Lo vemos en la parte 22.

Sigue leyendo