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 URL | API Gateway (HTTP API) | |
|---|---|---|
| Configuración | Una opción en la función | Rutas, integraciones y etapas |
| Costo extra | Ninguno, solo la Lambda | Por millón de peticiones (bajo) |
| Autenticación incluida | IAM o ninguna | IAM, JWT (Cognito) o authorizer propio |
| Límite de peticiones | Concurrencia de la Lambda | Throttling configurable por ruta |
| Dominio propio | No directamente | Sí |
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ón | Cómo funciona | Para quién |
|---|---|---|
| IAM (SigV4) | El cliente firma cada petición con credenciales de AWS | Otros servicios de AWS, scripts con la CLI |
| JWT de Cognito | El cliente envía el token del login; API Gateway lo verifica | Tu propia app web o móvil |
| Token propio | El cliente envía un secreto en una cabecera; la Lambda lo compara | Clientes 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:
- Genera el token al azar y con longitud suficiente (por ejemplo, 32 bytes en hexadecimal).
- Guárdalo fuera del código: en AWS Secrets Manager o en Parameter Store como
SecureString. Si guardas varios, guarda un hash, no el token. - Compáralo en tiempo constante para no filtrar información por el tiempo de respuesta.
- Permite revocarlo sin desplegar código nuevo.
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:
- configura una concurrencia reservada baja en la función, como tope;
- o pon la API detrás de API Gateway con throttling.
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"}'

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
AccessDeniedExceptional actualizar el Shadow. Al rol le faltaiot:UpdateThingShadowsobre ese Thing, o el ARN tiene otra región.event.bodyllega en Base64. Pasa con contenido binario: revisaevent.isBase64Encodedy decodifica.- CORS en el navegador. Si la API se llama desde una web, configura CORS en la Function URL o en API Gateway. Los Atajos y
curlno lo necesitan. - Tiempo de arranque en frío. La primera invocación tarda algo más; para una orden de voz no se nota.
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.