Templates declarativos¶
Un DSL fluido (igual al Template de E2B v2) que compila a un Dockerfile y
un zip deterministas, compuestos sobre una imagen rayito-base/
rayito-base-caps ya publicada, y los sube con
create/update-microvm-image. Sin plano de control propio: todo ocurre
en el cliente (investigación §3, docs/research/2026-10-e2b-out-of-scope.md).
Desde 0.6.0.
Coste y activación
- Por defecto: apagado. Sin llamar a
Template.build()/build_in_background()(TypeScript:buildInBackground), el SDK no construye ningún cliente delambda-microvms/S3/Logs: el builder (Template(),copy(),pip_install(),to_dockerfile()...) es puro. - Activa:
Template.build(t, "mi-template", bucket="...")/Template.build(t, "mi-template", { bucket: "..." }). - Recursos y llamadas AWS:
lambda:GetMicrovmImageVersion(leer la imagen base y, al terminar, sondear el build),CreateMicrovmImage/UpdateMicrovmImage,GetMicrovmImage,ListMicrovmImageVersions;s3:GetObject(zip de la imagen base),HeadObject/PutObject(subir el artefacto nuevo, sólo si no existe ya por su hash);logs:DescribeLogStreams/GetLogEventssólo si el build no termina enSUCCESSFUL+ACTIVE(para explicar el fallo). VerAWS_API_NOTES.md§27. - Coste aproximado (consultado 2026-10-01): cada versión de imagen
nueva cuesta almacenamiento de snapshot, ≈ $0,04/semana por versión
(mínimo una semana), igual que publicar con
rayito image publish; el build en sí no se factura aparte (sin CodeBuild, opción A de la investigación). Cada zip de contexto queda en tu bucket bajorayito/templates/(almacenamiento S3 estándar): ponle una regla de ciclo de vida a ese prefijo; ningún build posterior lo vuelve a leer. - IAM:
RayitoTemplateBuilder(infra/templates.yaml) sobre quien llama aTemplate.build():UpdateMicrovmImage/GetMicrovmImage*/ListMicrovmImageVersionssobre las imágenes de la cuenta y la región, con unDenyexplícito que impide actualizar las imágenes base publicadas (ProtectedImageNamePrefix,rayito-basepor defecto);CreateMicrovmImagesobre*, porque AWS lo autoriza sobre*y no sobre el ARN de la imagen nueva, así que no se puede acotar por nombre (uncreatesobre un nombre que ya existe falla, de modo que tampoco sobrescribe una base);lambda:PassNetworkConnectorsobre los conectores gestionados por AWS, quecreate/updatepasan aunque no nombres ninguno (AWS_API_NOTES.mdQ114);iam:PassRolesobre el rol de build (el mismo que usarayito image publish);s3:PutObject/GetObjectenrayito/templates/del bucket de artefactos ys3:GetObjectsólo enrayito/*del bucket de la imagen base (por defecto, el mismo; nunca el bucket entero, que puede guardar también losHOMEpersistidos y las transferencias de los sandboxes); lectura del grupo de logs de la imagen. Deliberadamente separada de cualquier política de lanzar sandboxes. - Cómo apagarla: no llames a
Template.build()/buildInBackground(). Las versiones antiguas de una imagen ya construida se borran conrayito image prune --image-name <nombre>(no las borraTemplate;pruneconserva al menos una). El grupo de logs/rayito/<nombre>que crea el rol de build en el primer build tampoco se borra con la imagen (delete-microvm-imageno lo toca, y ni el SDK ni la CLI borran imágenes): al retirar una imagen, bórralo tú conaws logs delete-log-group --log-group-name /rayito/<nombre>(logs:DeleteLogGroup) o ponle una retención.
Publica antes todos los nombres protegidos que vayas a usar
Como CreateMicrovmImage no se puede acotar por nombre, quien tenga
RayitoTemplateBuilder puede crear una imagen con un nombre
protegido que todavía no exista en esa cuenta y región (por ejemplo una
variante de tamaño o -caps que no hayas publicado). Si luego un
servicio pide ese nombre (Sandbox.create(size=...) lo resuelve por
sufijo), correría el código del builder con el rol de ejecución de ese
servicio. Publica con rayito image publish, en cada región que uses,
todas las variantes que tus servicios puedan pedir.
Cuándo usarlo¶
- Migras un agente que ya usa
Templatede E2B: el DSL es el mismo (fromBaseImage/from_base_imageen vez defromDockerImage,copy/pipInstall/setEnvs/workdir/setUser/setStartCmd). - Quieres que tu CI construya una imagen nueva a partir de un
Dockerfiledeclarativo en vez de mantener el pipeline derayito image publisha mano. - Cuándo no: un único cambio puntual en una imagen ya publicada —
rayito image publishsigue siendo el camino directo.
Antes de empezar: la política IAM¶
Quien llama a Template.build() necesita RayitoTemplateBuilder, el
componente templates de rayito stack. Los dos
ARN obligatorios salen de tu pila de IAM (BuildRoleArn es una salida de
infra/iam.yaml):
rayito stack deploy templates \
--param ArtifactBucketArn=arn:aws:s3:::amzn-s3-demo-bucket \
--param BuildRoleArn=arn:aws:iam::123456789012:role/<rol-de-build>
rayito stack status templates # el ARN de la política, para vincularla
BaseImageBucketArn sólo hace falta si el zip de la imagen base está en
otro bucket; ProtectedImageNamePrefix (rayito-base por defecto) es el
prefijo de las imágenes que el builder nunca puede actualizar.
Divergencias con E2B¶
- Sin caché de capas entre builds. Rayito no tiene una (investigación
§3.4): cada
Template.build()reconstruye todo lo que cambió desde la imagen base. Una versión ya construida con exactamente el mismo artefacto y la misma configuración se reusa sin llamar acreate/update-microvm-image;skip_cache()/skipCache()equivale aforce=True/force: truey envía un build nuevo de todos modos. - Sólo ARM64, CPU según la memoria. La API nativa acepta
cpu_count=pero lo ignora; el shim de E2B avisa conRayitoCompatWarningsi se da (la CPU sale dememory_mb, Q87).memory_mb/memoryMbse valida contra los cinco tamaños soportados (RES-1); el shim de E2B redondea al tamaño siguiente conRayitoCompatWarning. - La imagen compuesta hereda la configuración de la base. Hooks, rol
de build, imagen gestionada y
additionalOsCapabilitiessalen de la versión derayito-base/rayito-base-capssobre la que compones: un template sobrerayito-base-capssigue teniendo capabilities. - Nombres sin
:tag. El nombre de un template es el de la imagen de Lambda MicroVMs: 1-64 caracteres de[A-Za-z0-9_-](TemplateException/TemplateErrorsi no). - Sólo
fromBaseImage()compone de verdad.from_image/fromImage,from_template/fromTemplate,from_dockerfile/fromDockerfileyfrom_gcp_registry/fromGcpRegistrylanzanUnimplementedError: inyectarraydy sus hooks en una imagen externa no tiene todavía un camino soportado (raydsólo existe ya instalado dentro de una imagenrayito-*publicada).apt_install/aptInstalltambién lanza:rayito-basees Amazon Linux 2023 (dnf, noapt). - Sin streaming en vivo de los pasos. El log de cada paso se relee al terminar el build (TPL-1/Q83: el grupo de logs recibe la salida completa de BuildKit de golpe, no en vivo), nunca durante.
- El etiquetado de E2B no tiene análogo.
alias_exists/assign_tags/remove_tags/get_tags(y sus equivalentes TS) lanzanUnimplementedError:create/update-microvm-imagesólo etiqueta la imagen entera, no una versión concreta. set_start_cmd/setStartCmdnecesita una imagen base conrayd0.6. ElStartSpecse hornea en/etc/rayito/template.json; unrayd0.6 o posterior lo lee al arrancar, lanzastart_cmdcomo proceso gestionado antes del/readydel build, así que el snapshot ya lo lleva en marcha (aparece encommands.list()), y responde 503 en/readyhasta queready_cmdsale con 0; si se agota su plazo,/readyfalla y el build termina conBuildException(reason="ready_server_error"). Una imagen base publicada con unraydanterior ignora el fichero.- El
start_cmdya corre cuando llega el/run. Como va en el snapshot, en cada sandbox lanzado desde la plantilla se descongela antes que el hook/runque instala el token de acceso.raydrechaza un/runque venga de un proceso del sandbox (cualquier uid ≥ 1000) sin consumir el/rundel arranque, así que unstart_cmdmalicioso o comprometido no puede instalar su propio token ni dejar el sandbox sin él; sólo uno que corra como root (user="root"conRAYITO_ALLOW_ROOT=1en la imagen) queda fuera de esa protección (SECURITY.mdT2).
Contexto de build¶
copy("app/", "/srv/app/") lee app/ relativo a context_dir=/
contextDir (por defecto, el directorio actual) y respeta su
.dockerignore. Una ruta que sale del contexto (../secreto, o un src
que es un enlace simbólico hacia fuera) da
BuildException(reason="context_path_outside"). Dentro de un directorio
copiado, los enlaces simbólicos (a fichero o a directorio) nunca se
siguen, igual que en Docker y en los dos SDKs: un config ->
~/.aws/credentials dentro de app/ no acaba en el artefacto que se sube a
S3 ni en la imagen, donde el código del sandbox lo podría leer. Si
necesitas ese contenido, cópialo como fichero real dentro del contexto.
Los ficheros van bajo __rayito_context/ dentro del zip: nunca sustituyen
el Dockerfile compuesto ni el binario de rayd de la imagen base.
.dockerignore¶
Los dos SDKs siguen la semántica de .dockerignore de Docker (con los
mismos vectores de prueba):
- Cada patrón queda anclado en la raíz del contexto:
README.mdno excluyedocs/README.md, y/build,buildybuild/son lo mismo. *y?no cruzan/:*.pycsólo excluye los de la raíz; para cualquier profundidad,**/*.pyc.**es cero o más directorios:**/.envexcluye el.envde la raíz y los de cualquier subdirectorio (el idioma que generadocker init).- Un patrón que casa con un directorio excluye todo lo que hay dentro, y
el último patrón que casa gana:
node_modulesseguido de!node_modules/keep.jsdeja pasarkeep.js.
Si el contexto va a empaquetar algo que suele llevar secretos (.env,
.env.*, .git/, .aws/, .ssh/, *.pem, *.key), el SDK avisa con
las rutas (nunca el contenido) antes de subir nada: en la imagen, cualquier
código del sandbox podría leerlo. Para que el aviso pare un build en CI:
import { Template } from "rayito";
process.on("warning", (warning) => {
if (warning.name === "RayitoContextWarning") {
console.error(warning.message);
process.exit(1);
}
});
const t = new Template().fromBaseImage().copy(".", "/app");
await Template.build(t, "mi-template", { bucket: "mi-bucket-de-artefactos" });
Cambio de comportamiento en 0.6.x
Antes, * cruzaba / (*.pyc excluía también sub/x.pyc) y **/X
no casaba en la raíz. Si tu .dockerignore contaba con lo primero,
añade **/ delante del patrón.
Errores¶
reason |
Qué pasó |
|---|---|
None + step/command/exit_code/log_tail |
un RUN del Dockerfile compuesto salió con error |
ready_client_error / ready_server_error |
el proceso detrás de /ready respondió 4xx/5xx durante el build (Q85) |
build_quota |
ya hay 10 builds en marcha en este proceso, o AWS rechazó el undécimo de la cuenta (Q83) |
aws_error |
AWS rechazó el build por otro motivo; el mensaje y la causa llevan sólo el código y el mensaje saneados (sin la cadena canónica de un error de firma) |
build_timeout |
el build no terminó en timeout; sigue en AWS y get_build_status() lo consulta |
context_path_missing / context_path_outside |
un copy() nombra algo que no existe, o fuera del contexto |
base_image_not_s3 / base_image_missing_artifact / base_image_missing_entrypoint |
la imagen base no es una imagen rayito-* publicada con rayito image publish |
Los mensajes nombran el template que pasaste, nunca un ARN ni el texto libre de AWS sin sanear.
Ejemplo rápido¶
from rayito import Sandbox, Template, wait_for_port
t = (
Template()
.from_base_image("rayito-base") # (1)!
.pip_install(["pandas==2.2.3"])
.copy("app/", "/srv/app/")
.set_envs({"MODE": "prod"})
.set_start_cmd("python3 -m http.server 8000", wait_for_port(8000))
)
info = Template.build(t, "mi-template", bucket="mi-bucket-de-artefactos") # (2)!
print(info.template_id) # arn:aws:lambda:...:microvm-image:mi-template
with Sandbox.create(info.template_id) as sbx:
sbx.commands.run("curl -s localhost:8000")
- Puro: no llama a AWS hasta
Template.build(). - Resuelve la última versión
ACTIVEderayito-base, compone el Dockerfile, sube el zip y espera a que el build termine.
import asyncio
from rayito import AsyncTemplate, Template, wait_for_port
async def main() -> None:
t = Template().from_base_image().set_start_cmd(
"python3 -m http.server 8000", wait_for_port(8000)
)
info = await AsyncTemplate.build(t, "mi-template", bucket="mi-bucket-de-artefactos")
print(info.template_id)
asyncio.run(main())
import { Sandbox, Template, waitForPort } from "rayito";
const t = new Template()
.fromBaseImage("rayito-base")
.pipInstall(["pandas==2.2.3"])
.copy("app/", "/srv/app/")
.setEnvs({ MODE: "prod" })
.setStartCmd("python3 -m http.server 8000", waitForPort(8000));
const info = await Template.build(t, "mi-template", { bucket: "mi-bucket-de-artefactos" });
console.log(info.templateId);
await using sbx = await Sandbox.create({ template: info.templateId });
from rayito.e2b import E2B, Template
client = E2B(region="us-east-1", bucket="mi-bucket-de-artefactos") # (1)!
t = Template().from_base_image().pip_install(["pandas==2.2.3"])
info = client.Template.build(t, alias="mi-template", memory_mb=2048, skip_cache=False)
bucket=es una extensión de Rayito; también vale en cada llamada.
En TypeScript, con la forma de opciones de E2B JS:
Opciones de Template.build()¶
| Python | TypeScript | Por defecto | Qué hace |
|---|---|---|---|
bucket |
bucket |
— (obligatorio) | bucket de artefactos donde se sube el zip, bajo rayito/templates/ |
memory_mb |
memoryMb |
2048 |
tamaño de la imagen, uno de 512/1024/2048/4096/8192 |
force |
force |
False |
construye aunque ya exista una versión con el mismo artefacto (skip_cache()) |
timeout (s) |
timeoutMs |
1800 s | espera máxima del build (sólo build()); al agotarla, BuildException(reason="build_timeout") |
base_image_version |
baseImageVersion |
la última ACTIVE |
versión de la imagen base sobre la que compones |
on_build_logs |
onBuildLogs |
— | recibe el log del build línea a línea, al terminar (sólo build()) |
context_dir |
contextDir |
el directorio actual | raíz de las rutas de copy() |
region, session |
region, credentials |
la sesión por defecto | dónde y con qué credenciales |
cpu_count, build_role_arn |
— | — | se aceptan y se ignoran: la CPU sale de memory_mb y el rol de build es el de la imagen base |
Template.exists(nombre) dice si la imagen ya existe, y
t.to_dockerfile()/t.to_json() (toDockerfile()/toJSON()) enseñan lo
que se va a construir sin llamar a AWS. Con la CLI, rayito template build
lee un fichero .py que define una variable template.
Un build que falla¶
from rayito import BuildException, Template
t = Template().from_base_image().pip_install(["no-existe-este-paquete"])
try:
Template.build(t, "mi-template-rota", bucket="mi-bucket-de-artefactos")
except BuildException as exc:
print(exc.step, exc.command, exc.exit_code) # 1 'RUN pip install ...' 1
Build en segundo plano y logs¶
Template.build() espera a que el build termine (como mucho timeout, 30
minutos por defecto). Para no bloquear, build_in_background()
(TypeScript: buildInBackground()) devuelve en cuanto AWS acepta el build
un BuildHandle, que get_build_status() consulta cuando quieras, también
desde otro proceso:
import time
from rayito import Template
t = Template().from_base_image().pip_install(["pandas==2.2.3"])
handle = Template.build_in_background(t, "mi-template", bucket="mi-bucket-de-artefactos")
status = Template.get_build_status(handle)
while status.state == "IN_PROGRESS":
time.sleep(15)
status = Template.get_build_status(handle)
if status.info is not None:
print(status.info.template_id) # SUCCESSFUL
else:
print(status.error_message) # FAILED
import { setTimeout as sleep } from "node:timers/promises";
import { Template } from "rayito";
const t = new Template().fromBaseImage().pipInstall(["pandas==2.2.3"]);
const handle = await Template.buildInBackground(t, "mi-template", {
bucket: "mi-bucket-de-artefactos",
});
let status = await Template.getBuildStatus(handle);
while (status.state === "IN_PROGRESS") {
await sleep(15_000);
status = await Template.getBuildStatus(handle);
}
console.log(status.info?.templateId ?? status.errorMessage);
El log de BuildKit llega al terminar el build, nunca en vivo (el grupo
de logs de la imagen lo recibe de golpe, Q83). Para recibirlo línea a línea
desde Template.build(), pasa on_build_logs (TypeScript:
onBuildLogs); sin él, el SDK no lee el grupo de logs salvo para explicar
un fallo:
En TypeScript, onBuildLogs y la explicación de un build fallido leen el
log con el peer opcional @aws-sdk/client-cloudwatch-logs
(pnpm add @aws-sdk/client-cloudwatch-logs); sin él instalado, el build
funciona igual, pero onBuildLogs no recibe ninguna línea y
BuildError llega sin step/command/logTail.
Del Template de E2B a Rayito¶
El shim (rayito.e2b / rayito/e2b) acepta la firma de build de E2B y la
traduce a la nativa; el DSL es la misma clase en los dos.
| E2B | Rayito nativo | Nota |
|---|---|---|
Template().from_image("python:3.12"), from_template, from_dockerfile, from_gcp_registry |
Template().from_base_image("rayito-base") |
sólo se compone sobre una imagen rayito-* publicada; esos cuatro orígenes lanzan UnimplementedError |
copy, run_cmd, pip_install, set_envs, set_user, skip_cache |
los mismos métodos | skip_cache() equivale a force=True |
set_workdir(path) |
workdir(path) |
— |
apt_install([...]) |
run_cmd("dnf install -y ...") |
rayito-base es Amazon Linux 2023; apt_install lanza UnimplementedError |
set_start_cmd(cmd, wait_for_port(8000)) |
igual, también wait_for_url/wait_for_process/wait_for_file |
necesita una imagen base con rayd 0.6 |
Template.build(t, alias="x", cpu_count=, memory_mb=, skip_cache=, on_build_logs=) |
Template.build(t, "x", bucket=, memory_mb=, force=, on_build_logs=) |
el shim pide bucket en la llamada o en E2B(bucket=...); memory_mb se redondea al tamaño siguiente y cpu_count se ignora, los dos con RayitoCompatWarning |
Template.build_in_background(...) + Template.get_build_status(...) |
igual, con un BuildHandle |
— |
Template.exists(name) |
igual | — |
alias_exists, assign_tags, remove_tags, get_tags |
— | lanzan UnimplementedError: Lambda MicroVMs etiqueta la imagen entera, no una versión |
TemplateException, BuildException |
las mismas clases | BuildException trae reason, step, command, exit_code y log_tail |
Referencia¶
rayito.Template/rayito.AsyncTemplateAWS_API_NOTES.md§27 (logs de build y contrato decodeArtifact)ARCHITECTURE.mdADR-022