Dominio propio (experimental)¶
Desde 0.7.0, experimental (Novedades).
Qué hace¶
Sin esta función, un puerto del sandbox sólo es alcanzable mandando las
cabeceras x-aws-proxy-auth/x-aws-proxy-port que devuelve get_host():
un navegador, un webhook o un <iframe> no pueden. Con dominio propio, cada
puerto tiene una URL HTTPS normal bajo tu dominio:
que se abre desde un navegador sin cabecera del proxy. Por debajo hay una
distribución CloudFront en tu cuenta con una CloudFront Function que, en
cada petición, busca la ruta del hostname en un KeyValueStore, comprueba el
token de tráfico y reenvía al sandbox poniendo ella misma las cabeceras del
proxy de AWS.
Experimental: no verificado de punta a punta en AWS real
CustomDomain (desplegar y borrar la pila, y register/unregister/
refresh de rutas) está implementada en Python y TypeScript, con tests
unitarios, cfn-lint sobre la plantilla y tests de la Function de
enrutado. Se publica como experimental para que puedas probarla: la API
puede cambiar en una minor.
Verificado en AWS real (AWS_API_NOTES.md):
- La CloudFront Function compila y enruta en el runtime real
cloudfront-js-2.0(TestFunction, Q121): token correcto → sigue; sin token o con token erróneo → 403; ruta caducada o inexistente → 404. CustomDomain.deploy()crea elKeyValueStorey la Function de la plantilla tal cual (Q140, tras ajustar losCommental límite de 128 caracteres de CloudFront), y el rollback ydestroy()no dejan nada.
Sin verificar en AWS real: crear la distribución y servir tráfico
por ella (HTTP/1.1, WebSocket, latencia de propagación del
KeyValueStore, renovación del JWE). La cuenta de pruebas del proyecto
deniega cloudfront:CreateDistribution con una SCP de la organización
(Q141), así que nadie ha abierto todavía una URL de dominio propio
desplegada con esta plantilla. El e2e del repositorio
(tests/e2e/test_m15_custom_domain.py, custom-domain.e2e.test.ts)
automatiza esa prueba para una cuenta que sí lo permita.
Tampoco existe todavía que Sandbox.create(domain=...)/get_host()
devuelvan esa URL solos: domain= sigue lanzando UnimplementedError
(ADR-024 de ARCHITECTURE.md). Hoy registras la ruta tú mismo y
route.host es exactamente el hostname de arriba (ver el ejemplo).
Si lo pruebas, cuéntanos cómo te fue en un
issue de GitHub
con [custom-domain] en el título: SDK y versión, región, qué paso falló
(deploy, register, la petición HTTP), el código de estado y el
motivo de los eventos de la pila si la hubo. Nunca pegues el ARN de tu
certificado, tu dominio real, tokens ni JWE: usa example.com y
123456789012.
Coste y activación
- Estado: experimental. Las cifras de coste son de lista y no se han medido con una distribución real (ver el aviso de arriba).
- Por defecto: apagado. Sin instanciar
CustomDomainel SDK no construye ningún clientecloudformationnicloudfront-keyvaluestore. - Activa:
CustomDomain(public_domain="sbx.example.com").deploy(certificate_arn=...)orayito domain deploy. - Recursos y llamadas AWS: una distribución CloudFront, su
CloudFront Function de enrutado y un KeyValueStore
(
infra/custom-domain.yaml). En uso,register()/unregister()/refresh()llaman aDescribeKeyValueStore/PutKey/DeleteKey. - Coste aproximado (us-east-1,
precios de CloudFront;
cifras de lista de CloudFront Functions/KeyValueStore desde su
lanzamiento, por reconfirmar en la aceptación AWS): $0 en reposo;
~$0,085/GB + $0,0075/10 000 peticiones HTTPS de salida; la CloudFront
Function, ~$0,10 por 1 000 000 de invocaciones (una por petición); el
KeyValueStore, ~$0,50 por 1 000 000 de lecturas (las de la Function) y
~$5 por 1 000 000 de llamadas de gestión (
PutKey/DeleteKey). - IAM (credenciales de quien llama al SDK):
cloudformation:*Stack*paradeploy/status/destroy, y los permisos de CloudFront para crear y borrar la distribución, la Function y el KeyValueStore (la pila corre con esas mismas credenciales);cloudfront-keyvaluestore:DescribeKeyValueStore/PutKey/DeleteKeysobre el KVS de la pila. - Cómo apagarla:
destroy()orayito domain destroyborra la distribución (tarda ~15 min en deshabilitarse primero), la Function y el KeyValueStore — ninguna ruta sobrevive, son efímeras. Borra también elCNAMEque creaste en tu DNS.
Cuándo usarlo¶
- Necesitas que un puerto del sandbox responda en una URL HTTPS normal, sin
que el cliente tenga que añadir cabeceras: un navegador, un webhook, un
<iframe>. - Cuándo no: un cliente programático que ya sabe mandar
x-aws-proxy-auth/x-aws-proxy-port—get_host()ya lo resuelve sin desplegar nada; para desarrollo local,rayito sandbox proxy.
Instalación¶
Registrar rutas firma con SigV4A (lo exige el plano de datos de
cloudfront-keyvaluestore), que no viene en la instalación por defecto:
Sin el extra, register()/refresh()/unregister() lanzan
CustomDomainException con esta misma orden. deploy()/status()/
destroy() no lo necesitan.
Activarlo¶
Necesitas tres cosas:
- Un dominio bajo el que vivirán las URLs, p. ej.
sbx.example.com. - Un certificado ACM en
us-east-1(requisito de CloudFront, sin importar en qué región corran tus sandboxes) que cubra*.sbx.example.com. - Un registro DNS: tras desplegar, un
CNAME(o alias, en Route 53) de*.sbx.example.comalDistributionDomainNameque imprime el despliegue (dxxxxxxxxxxxx.cloudfront.net).
import { CustomDomain } from "rayito";
const domain = new CustomDomain({ publicDomain: "sbx.example.com" }); // no llama a AWS
const status = await domain.deploy({
certificateArn: "arn:aws:acm:us-east-1:<cuenta>:certificate/<id>",
});
console.log(status.outputs.DistributionDomainName); // destino del CNAME
Sin el SDK, la misma plantilla con CloudFormation:
curl -fsSLO https://raw.githubusercontent.com/alejandro-cedeno-10/rayito/main/infra/custom-domain.yaml
aws cloudformation deploy --stack-name rayito-custom-domain \
--template-file custom-domain.yaml \
--parameter-overrides PublicDomain=sbx.example.com CertificateArn=arn:aws:acm:us-east-1:<cuenta>:certificate/<id>
Exponer un puerto¶
import secrets
from rayito import CustomDomain, Sandbox
domain = CustomDomain(public_domain="sbx.example.com")
domain.status() # lee el KvsArn de la pila ya desplegada
with Sandbox.create("rayito-base", allowed_ports=[8000]) as sbx:
jwe = sbx.get_host(8000).headers["x-aws-proxy-auth"] # (1)!
traffic_token = secrets.token_urlsafe(32) # (2)!
route = domain.register(
sbx.sandbox_id, 8000, endpoint=sbx.endpoint, jwe=jwe,
traffic_token=traffic_token, ttl_seconds=2400,
)
print(f"https://{route.host}") # (3)!
# ... antes de que caduque, con un JWE recién acuñado (4):
fresh_jwe = sbx.get_host(8000).headers["x-aws-proxy-auth"]
domain.refresh(route, jwe=fresh_jwe, ttl_seconds=2400)
domain.unregister(sbx.sandbox_id, 8000)
- El JWE que ya emite
get_host(); es el mismo que valida el proxy de AWS Lambda MicroVMs. La Function lo añade por ti en cada petición. - Quien abra la URL lo manda en la cabecera
e2b-traffic-access-tokeno en la cookierayito_tt(lo práctico en un navegador: tu aplicación la fija para.sbx.example.com). Sin token,register()rechaza la llamada — una ruta sólo es pública conpublic=Trueexplícito, nunca por omisión. https://8000-<sandbox_id>.sbx.example.com.refresh()necesita un JWE nuevo:get_host()lo vuelve a acuñar bajo demanda. Volver a pasar el original sólo alarga el TTL de la ruta, no el token que comprueba el proxy de AWS.
import asyncio
import secrets
from rayito import AsyncCustomDomain, AsyncSandbox
async def main() -> None:
domain = AsyncCustomDomain(public_domain="sbx.example.com")
await domain.status()
async with await AsyncSandbox.create("rayito-base", allowed_ports=[8000]) as sbx:
jwe = (await sbx.get_host(8000)).headers["x-aws-proxy-auth"]
route = await domain.register(
sbx.sandbox_id, 8000, endpoint=sbx.endpoint, jwe=jwe,
traffic_token=secrets.token_urlsafe(32), ttl_seconds=2400,
)
print(f"https://{route.host}")
await domain.unregister(sbx.sandbox_id, 8000)
asyncio.run(main())
import { randomBytes } from "node:crypto";
import { CustomDomain, Sandbox } from "rayito";
const domain = new CustomDomain({ publicDomain: "sbx.example.com" });
await domain.status(); // lee el KvsArn de la pila ya desplegada
const sbx = await Sandbox.create({ template: "rayito-base", allowedPorts: [8000] });
try {
const jwe = (await sbx.getHost(8000)).headers["x-aws-proxy-auth"] ?? "";
const trafficToken = randomBytes(32).toString("base64url");
const route = await domain.register(sbx.sandboxId, 8000, {
endpoint: sbx.endpoint,
jwe,
trafficToken,
ttlSeconds: 2400,
});
console.log(`https://${route.host}`);
await domain.unregister(sbx.sandboxId, 8000);
} finally {
await sbx.kill();
}
Quitarlo¶
Después borra el CNAME de tu DNS. El certificado ACM es tuyo y no se toca.
Nombres explícitos en vez del comodín¶
Por defecto la distribución lleva el alias *.<tu dominio>. CloudFront no
admite el mismo alias en dos distribuciones, así que si otra distribución
ya tiene ese comodín, el despliegue falla. Puedes pasar una lista de
hostnames exactos (cada uno <etiqueta>.<tu dominio>, normalmente
host_for(alias, puerto)): ante un solape CloudFront elige el nombre más
específico, así que conviven con el comodín ajeno. Sólo las rutas cuyos
hostnames estén en la lista llegan a esta distribución; el coste no cambia.
Una excepción: si tu DNS ya tiene un registro comodín que apunta a la otra
distribución, CloudFront rechaza el nombre más específico hasta que crees
para él un registro propio que apunte a esta.
Probar sin tocar el DNS¶
Antes de crear el CNAME puedes comprobar la distribución conectando
directamente a su *.cloudfront.net y mandando tu hostname como SNI y como
Host (CloudFront elige la distribución por el Host):
curl --connect-to 8000-<sandbox_id>.sbx.example.com:443:dxxxxxxxxxxxx.cloudfront.net:443 \
-H "e2b-traffic-access-token: <token>" https://8000-<sandbox_id>.sbx.example.com/
Así funciona también el e2e del repositorio
(tests/e2e/test_m15_custom_domain.py, custom-domain.e2e.test.ts): toma
el dominio y el certificado sólo de RAYITO_E2E_DOMAIN (un dominio cuyo
comodín cubre el certificado) y RAYITO_E2E_CERT_ARN, despliega una pila
propia por corrida (rayito-cd-e2e-<id>) con nombres explícitos aleatorios
para no chocar con ningún alias existente, no crea ningún registro DNS y lo
borra todo al terminar. Sin esas variables se salta.
Cómo funciona¶
- La distribución tiene un único origen "placeholder" en la plantilla — nunca se contacta de verdad.
- Cada petición pasa primero por la CloudFront Function
(
infra/functions/custom_domain_router.js), que borra cualquier cabecerax-aws-proxy-*que el viewer haya mandado, lee la ruta delKeyValueStorepor la primera etiqueta del hostname ({puerto}-{alias}), comprueba el token de tráfico si la ruta no es pública, y llama acf.updateRequestOrigin()con elendpointreal del sandbox y las cabeceras del proxy que hacen falta. - Una ruta sin entrada en el
KeyValueStore, o cuyottl_secondsya pasó, recibe 404 directo de la Function: nunca llega al origen placeholder, y desde fuera no se distingue de una ruta que nunca existió.
Limitaciones conocidas¶
- Sin
Sandbox.create(domain=)niget_host()cableados, hay que registrar la ruta a mano (como en el ejemplo). - El token de tráfico sólo se guarda como su hash sha256: si lo pierdes, genera uno nuevo y vuelve a registrar la ruta.
- Una ruta no se borra sola cuando el sandbox muere: llama a
unregister()tú mismo. Sin eso, la ruta se acota sola por dos lados independientes:ttl_secondsderegister()/refresh()(que la propia Function comprueba — 404) y la caducidad del JWE en sí, del lado del proxy de AWS (401/403), lo primero que llegue. - No hay refresher automático (DOM-14): llama a
refresh()antes de que caduque el JWE si la ruta debe vivir más. - Los puertos
8080y9000(puerto de hooks y reservado) no se pueden exponer, igual que en el resto del SDK. - Si tu cuenta está en una organización de AWS, una SCP puede denegar
cloudfront:CreateDistributionaunque tu rol sea administrador. Entoncesdeploy()falla conStackException(la pila termina enROLLBACK_COMPLETE; el motivo,AccessDenied ... explicit deny in a service control policy, sale en los eventos de la pila,aws cloudformation describe-stack-events). No queda nada creado: el rollback borra la Function y el KeyValueStore, ydestroy()quita la pila fallida. Pide al administrador de la organización que permita esa acción (Q141 deAWS_API_NOTES.md).