Volúmenes EFS (experimental)¶
Desde 0.7.0 (Novedades).
Un sistema de ficheros compartido (NFS) en tu cuenta, montado dentro del
guest por rayd: el análogo de Volume de E2B. A diferencia de
persist= (una copia S3 restaurada/guardada en create/pause), un
volumen EFS se comparte en vivo entre varios sandboxes mientras están
vivos.
Experimental, y sólo con la imagen opcional que trae amazon-efs-utils
Sandbox.create(volumes=...) monta de verdad, pero sólo sobre una imagen
que traiga amazon-efs-utils y corra con additionalOsCapabilities
ALL: la imagen opcional rayito-base-caps-efs
(Imagen con amazon-efs-utils). Las
imágenes por defecto no cambian y en ellas Health.features.efs_volumes
es false: create(volumes=...) termina el sandbox y lanza
UnimplementedError. El estudio completo está en
docs/research/2026-10-efs-persistence.md.
¿Ya tienes una VPC?
Volúmenes EFS en tu VPC explica cómo comprobarla
sin crear nada (EfsVolumes.check, rayito doctor --efs-vpc-id),
desplegar sólo recursos nuevos dentro de ella y borrarlos todos.
Coste y activación
- Por defecto: apagado. Sin
VolumeStore(...)nivolumes=, Rayito no construye ningún clienteefsni hace ninguna llamada a AWS. - Activa:
VolumeStore(file_system_id=...)para el CRUD de volúmenes (access points);Sandbox.create(volumes={...})para montarlos, conexecution_role_arn=, un solo conector enegress=y la imagen opcionalrayito-base-caps-efs. - Recursos y llamadas AWS:
VolumeStore.create=CreateAccessPoint;get/list=DescribeAccessPoints;destroy=DeleteAccessPoint(AWS_API_NOTES.md §22).create(volumes=...)hace además unaDescribeMountTargetspor sistema de ficheros, antes de lanzar, si unEfsVolumeno traemount_target_ip. El sistema de ficheros, sus mount targets, el grupo de seguridad NFS y el conector de egress dedicado los crearayito stack deploy efs-volumes(infra/efs-volumes.yaml), por separado —VolumeStorenunca los crea. - Coste aproximado (us-east-1, consultado 2026-09-11, precios de
EFS): $0,30/GB-mes el primer
mes (Standard), $0,016/GB-mes tras 30 días (IA, con la política de
ciclo de vida de la plantilla); $0,03/GB leído y $0,06/GB escrito con
Elastic Throughput. Los access points no tienen cargo propio listado.
Un sistema de ficheros vacío cuesta $0. La imagen con
amazon-efs-utilsocupa ≈ 198 MB más de código instalado (AWS_API_NOTES.md§16 Q122); el tráfico hacia un mount target de otra AZ lo factura EC2 como transferencia entre AZs. - IAM:
elasticfilesystem:CreateAccessPoint/DescribeAccessPoints/ DeleteAccessPoint(yDescribeMountTargetspara montar sinmount_target_ip) sobre el sistema de ficheros (credenciales de quien llama al SDK). Dentro del guest, el execution role necesitaelasticfilesystem:ClientMount(+ClientWritesi el volumen admite escritura) condicionado alAccessPointArn, nuncaClientRootAccess; un volumen de sólo lectura va enread_only_access_point_arns, que le deniegaClientWrite(ver Sólo lectura de verdad). - Cómo apagarla: no instancies
VolumeStoreni pasesvolumes=. Para dejar de pagar:VolumeStore.destroy(nombre)borra el access point (los datos del directorio no se borran: ver Diferencias con E2B);rayito stack destroy efs-volumesborra el conector, los mount targets, los grupos de seguridad y el IAM, pero siempre conserva el sistema de ficheros y sus datos (DeletionPolicy: Retain, sin parámetro que lo cambie: redesplegar la pila nunca puede borrar ni sustituir los datos). Para borrarlos de verdad:EfsVolumes(...).destroy(delete_file_system=True), oEfsVolumes(...).delete_file_system("fs-…")después de unrayito stack destroy(ver Volúmenes EFS en tu VPC).
Cuándo usarlo¶
- Varios sandboxes necesitan leer y escribir el mismo directorio a la
vez, o un sandbox necesita ver en vivo lo que otro escribió — algo que
persist=(checkpoint/restore) no da: dos sandboxes conpersist=no comparten nada en vivo, y lo escrito entre checkpoints se pierde si la VM muere. - Cuándo no: un
HOMEpor sandbox que no necesita compartirse en vivo (ahípersist=basta, es más barato y ya funciona); un repositorio que sólo necesitas una vez (clónalo concommands.run("git clone ...")).
VolumeStore: CRUD de volúmenes¶
El CRUD trabaja sobre un sistema de ficheros EFS que ya exista
(desplegado con EfsVolumes.deploy/rayito stack deploy efs-volumes, ver
Volúmenes EFS en tu VPC, o uno propio). Cada
volumen es un access point con RootDirectory.Path =
/rayito-volumes/<nombre> y usuario POSIX 1000:1000 forzado.
from rayito import VolumeStore
store = VolumeStore(file_system_id="fs-0123abcd", region="us-east-1") # (1)!
vol = store.create("datos-agente-7") # (2)!
store.get("datos-agente-7")
store.list()
store.destroy("datos-agente-7") # (3)!
- Construirlo no llama a AWS: el cliente boto3
efsse crea en el primer método. CreateAccessPoint, idempotente por nombre (ClientTokenes un hash defile_system_id+nombre).DeleteAccessPoint; el directorio en sí no se borra.
list y get tardan unos segundos en ponerse al día
DescribeAccessPoints es eventualmente consistente (medido: hasta
~11 s en listar un volumen nuevo y ~8 s en dejar de listar uno
borrado). create siempre devuelve el volumen recién creado y, si el
nombre ya existía, espera hasta 30 s a que aparezca en el listado;
destroy de un volumen que el listado aún muestra pero ya no existe
devuelve False. Un get/list justo después de create o destroy
puede ver el estado anterior.
Sandbox.create(volumes=): montar volúmenes¶
from rayito import Sandbox, VolumeStore
store = VolumeStore(file_system_id="fs-0123abcd", region="us-east-1")
datos = store.create("datos-agente-7")
with Sandbox.create(
"rayito-base-caps-efs",
execution_role_arn="arn:aws:iam::123456789012:role/rayito-execution",
egress=["arn:aws:lambda:us-east-1:123456789012:network-connector:rayito-efs"],
volumes={"/mnt/datos": datos},
) as sbx:
sbx.commands.run("echo hola > /mnt/datos/saludo.txt")
print(sbx.volumes["/mnt/datos"].state) # "mounted"
import { Sandbox, VolumeStore } from "rayito";
const store = new VolumeStore({ fileSystemId: "fs-0123abcd", region: "us-east-1" });
const datos = await store.create("datos-agente-7");
const sbx = await Sandbox.create({
template: "rayito-base-caps-efs",
executionRoleArn: "arn:aws:iam::123456789012:role/rayito-execution",
egress: ["arn:aws:lambda:us-east-1:123456789012:network-connector:rayito-efs"],
volumes: { "/mnt/datos": datos },
});
await sbx.commands.run("echo hola > /mnt/datos/saludo.txt");
console.log((await sbx.volumes()).get("/mnt/datos")?.state); // "mounted"
await sbx.kill();
Lo que hace create(), en orden:
- Antes de lanzar, sin red: valida la petición (de 1 a 4 volúmenes,
rutas bajo
/mnt/o/home/user/sin solaparse, valoresEfsVolume, variante caps si el nombre de la imagen la revela, exactamente un conector propio enegress=yexecution_role_arn=). Cualquier fallo esInvalidArgumentException/InvalidArgumentErrory no se lanza nada. - Antes de lanzar, con EFS: si un volumen no trae
mount_target_ip, unaDescribeMountTargetspor sistema de ficheros (credenciales de quien llama; dos volúmenes del mismo sistema de ficheros comparten la respuesta) y elige el primer mount targetavailableporAvailabilityZoneId. Sin ninguno disponible,VolumeException/VolumeErrorantes de lanzar. - Tras la readiness: manda la sección
efs_volumesen el mismoConfigureSandboxquemounts=,gateways=,telemetry=yevents=.raydmonta los volúmenes de uno en uno dentro de esa llamada, cuyo plazo es como mucho 65 s (4 volúmenes × 15 s del helper, más margen);create()sólo vuelve con todosmounted. - Si algo falla: termina el sandbox (salvo
keep_on_failure=True) y lanzaVolumeMountException/VolumeMountErrorconcode(network,iam_denied,not_found,tls,helper_missing,timeout,invalid_pathounknown), oUnimplementedErrorsi la imagen no traeamazon-efs-utils.
Cada valor de volumes= es un EfsVolume: el que devuelve
VolumeStore.create/get, o uno que construyes tú con
EfsVolume(file_system_id=, access_point_id=, read_only=, mount_target_ip=)
(TypeScript: new EfsVolume({ fileSystemId, accessPointId, readOnly,
mountTargetIp })) si ya conoces el access point. Con mount_target_ip,
create() no llama a DescribeMountTargets.
sbx.volumes (Python síncrono), await sbx.volumes() (asíncrono y
TypeScript) lee el estado en vivo de cada ruta (requested, mounting,
mounted, degraded, remounting, unmounted o failed, con
last_error_class/lastErrorClass si falló). reincarnate() vuelve a mandar la sección (y a resolver
las IPs) en el sucesor.
Imagen con amazon-efs-utils¶
volumes= sólo monta en una imagen que traiga el mount helper de EFS
(mount.efs y efs-proxy, del paquete amazon-efs-utils de AL2023) y que
corra con additionalOsCapabilities: ["ALL"] (CAP_SYS_ADMIN). Ninguna de las
imágenes por defecto lo trae: rayito-base, -slim, -caps y -poly no
cambian, y en ellas Health.features.efs_volumes sigue en false (un
create(volumes=...) sobre ellas termina el sandbox y lanza
UnimplementedError). La imagen con efs-utils es una variante aparte,
rayito-base-caps-efs, que publicas tú:
make image-publish-caps-efs BUCKET=amzn-s3-demo-bucket
# o, paso a paso:
rayito image zip image image/rayito-image-efs.zip --sidecar kernel-sidecar --with-efs
rayito image publish --artifact image/rayito-image-efs.zip --with-efs \
--os-capabilities ALL --bucket amzn-s3-demo-bucket --base-image-version 1
--with-efs añade al zip el marcador kernel-sidecar/efs_variant; la capa
condicional del Dockerfile instala entonces amazon-efs-utils-3.1.3
(clavado por versión), vuelve a enlazar /usr/bin/python3 a Python 3.12 (el
paquete trae Python 3.9 y lo reapunta; rayd usa esa ruta) y comprueba que
mount.efs y efs-proxy existen. La región no se hornea: rayd se la pasa al
helper en cada montaje (AWS_REGION del MicroVM), así que la misma imagen
sirve en cualquier región donde la publiques. rayito-base-caps-efs cuenta
como variante caps para mounts= y telemetry= igual que rayito-base-caps.
Coste de la imagen
Medido en AWS real (AWS_API_NOTES.md §16 Q122, AL2023 ARM64): 37 paquetes
más (entre ellos nfs-utils, stunnel, Python 3.9 y systemd, que nadie
arranca), +~198 MB de codeInstallSizeInBytes (almacenamiento de la
imagen), el snapshot de memoria no cambia y la build tarda ≈ 10 s
más. Sin --with-efs no se instala nada ni cambia ningún coste.
Internet y volúmenes¶
Un MicroVM admite un solo conector de egress (medido:
egressNetworkConnectors=[INTERNET_EGRESS, <conector VPC>] es un
ValidationException de AWS, AWS_API_NOTES.md §16 Q131), y un volumen
necesita el conector de tu VPC para llegar al mount target. Por eso el SDK
rechaza, antes de lanzar nada y con InvalidArgumentException/
InvalidArgumentError, un volumes= que:
- no trae
egress=(el sandbox heredaríaINTERNET_EGRESSde la imagen y nunca llegaría al mount target); - incluye
INTERNET_EGRESS(por nombre o por ARN); - trae más de un conector.
Si un sandbox con volumen necesita internet, tiene que salir por tu
VPC: una NAT o un transit gateway en las subredes del conector y un
conector cuyo grupo de seguridad permita esa salida (el de la pila
efs-volumes sólo deja salir NFS). EfsVolumes.check() te dice cuántas de
tus subredes tienen esa ruta; ver
Salida a internet.
Lo que hace rayd con cada volumen¶
Esto es lo que el adaptador de montaje hace en una imagen con
amazon-efs-utils, y lo que midieron las aceptaciones del 2026-10-04
(AWS_API_NOTES.md §16 Q128–Q139):
- Montaje:
mount -t efs -o tls,iam,accesspoint=…con las credenciales del execution role (sinsystemd: p50 313 ms, p95 589 ms), sobre un directorio deraydal que el usuario del sandbox no llega, y después enlazado a tu ruta sin seguir nunca un enlace simbólico. - Desmontaje: cada montaje arranca su propio
efs-proxyyumountno lo para;raydtermina el de cada volumen al desmontarlo y al terminar el sandbox, así que no se acumulan procesos. - Pausa y reanudación: tras pausas de 60 s y 10 min el volumen sigue
igual. Si la pausa cruza la caducidad de las credenciales del rol con
las que se firmó el túnel (≈ 1 h), el volumen respondería
Permission denied:raydlo detecta al reanudar y lo vuelve a montar (medido tras 70 min:mountedotra vez en 0,7–1,3 s,AWS_API_NOTES.md§16 Q139) (o, si no sabe cuándo caducan, prueba el volumen con unstatacotado y lo remonta si falla). Si el remontaje no termina dentro del presupuesto del hook/resume, sigue en segundo plano y el estado del volumen pasa porremountinghastamounted(odegradedcon la causa). Los ficheros que el código tenía abiertos en el volumen antes de un remontaje dan error al usarse: vuelve a abrirlos. - Suspensión: antes de suspender,
raydvacía las escrituras de cada volumen con un plazo (dentro del presupuesto de/suspend, como mucho 5 s).
Escrituras sin vaciar + mount target inalcanzable = datos perdidos
Si al pausar hay escrituras que aún no llegaron a EFS y el mount target
no responde (red caída, grupo de seguridad cambiado), el vaciado no
termina a tiempo, rayd responde a /suspend en su plazo y AWS
termina el MicroVM: lo no vaciado se pierde y pause() lanza
SandboxNotFoundException (medido, AWS_API_NOTES.md §16 Q130). Nada
dentro del guest puede evitarlo. Si un dato importa, ciérralo con
fsync (o sync) antes de pause(); con el mount target accesible y
sin escrituras pendientes, la pausa funciona.
Sólo lectura de verdad¶
EfsVolume(read_only=True) monta con ro, pero eso solo no impide
escribir: efs-proxy escucha en un puerto local y el usuario del
sandbox (uid 1000) puede conectarse a él y hablar NFS por el túnel ya
autenticado, con los permisos IAM del rol (medido, AWS_API_NOTES.md §16
Q133; el kernel del guest no permite filtrar por usuario, Q48). Lo que
hace un volumen de sólo lectura es la política del rol: despliega la
pila con read_only_access_point_arns, que le niega ClientWrite a esos
access points aunque el resto admita escritura (o con allow_write=False
si ningún volumen debe admitirla).
import { EfsVolumes } from "rayito";
const efs = new EfsVolumes({ region: "us-east-1" });
await efs.deploy({
vpcId: "vpc-0123456789abcdef0",
subnetIds: ["subnet-0123456789abcdef0"],
readOnlyAccessPointArns: [
"arn:aws:elasticfilesystem:us-east-1:123456789012:access-point/fsap-0123456789abcdef0",
],
});
Shim E2B: E2B(volume_store=...)¶
client.Volume/client.AsyncVolume (rayito.e2b) son UnimplementedError
hasta que el cliente les liga un VolumeStore (sólo en el constructor del
cliente, nunca por llamada; en Python, el VolumeStore síncrono: un
AsyncVolumeStore es InvalidArgumentException). Ligado, el CRUD es el
mismo que arriba, con los nombres de E2B (Volume.create/connect/list/
get_info/destroy). volume_id/volumeId es el nombre del volumen, el
mismo identificador que reciben connect/get_info/destroy; el
AccessPointId va aparte, en access_point_id/accessPointId.
Sandbox.create(volume_mounts={ruta: Volume|nombre}) monta esos volúmenes
por el mismo camino que volumes= si el cliente trae además
volume_connector_arn=/volumeConnectorArn (el ConnectorArn de la pila):
como un MicroVM sólo admite un conector de egress, ese sandbox sale sólo
por ese conector, nunca por INTERNET_EGRESS, y un
allow_internet_access=True explícito es InvalidArgumentException. Un
Volume se monta con su access_point_id sin llamar a AWS; un nombre de
texto se busca con VolumeStore.get (DescribeAccessPoints). También
necesita execution_role_arn= y la imagen con amazon-efs-utils. Sin
volume_connector_arn falla antes de cualquier llamada a AWS.
from rayito import VolumeStore
from rayito.e2b import E2B
client = E2B(
volume_store=VolumeStore(file_system_id="fs-0123abcd"),
volume_connector_arn="arn:aws:lambda:us-east-1:123456789012:network-connector:rayito-efs",
)
vol = client.Volume.create("datos-agente-7") # sin volume_store: UnimplementedError
sbx = client.Sandbox.create(
"rayito-base-caps-efs",
execution_role_arn="arn:aws:iam::123456789012:role/rayito-execution",
volume_mounts={"/mnt/datos": vol},
)
sbx.kill()
client.Volume.destroy(vol.volume_id)
import { VolumeStore } from "rayito";
import { E2B } from "rayito/e2b";
const client = new E2B({
volumeStore: new VolumeStore({ fileSystemId: "fs-0123abcd" }),
volumeConnectorArn: "arn:aws:lambda:us-east-1:123456789012:network-connector:rayito-efs",
});
const vol = await client.Volume.create("datos-agente-7");
const sbx = await client.Sandbox.create("rayito-base-caps-efs", {
executionRoleArn: "arn:aws:iam::123456789012:role/rayito-execution",
volumeMounts: { "/mnt/datos": vol },
});
await sbx.kill();
await client.Volume.destroy(vol.volumeId);
Errores y solución de problemas¶
| Python | TypeScript | Cuándo | Qué hacer |
|---|---|---|---|
InvalidArgumentException |
InvalidArgumentError |
volumes= vacío, con más de 4 volúmenes, con un valor que no es EfsVolume, con rutas solapadas o fuera de /mnt/·/home/user/, o sin execution_role_arn= |
corrige la forma de la petición |
InvalidArgumentException |
InvalidArgumentError |
volumes= sin egress=, con INTERNET_EGRESS o con más de un conector |
pasa sólo el ConnectorArn de la pila; internet, por tu VPC (Internet y volúmenes) |
UnimplementedError |
UnimplementedError |
la imagen no trae amazon-efs-utils (Health.features.efs_volumes es false: el sandbox ya se terminó) o es una variante no-caps |
publica y usa rayito-base-caps-efs (Imagen con amazon-efs-utils) |
VolumeMountException (code) |
VolumeMountError (code) |
un volumen no montó: iam_denied (política del rol o access point fuera de access_point_arns), network (conector o grupo de seguridad), not_found, tls, timeout, invalid_path (la ruta es un enlace simbólico)… El sandbox ya se terminó (salvo keep_on_failure) |
revisa el rol, el conector o la ruta según code |
VolumeException |
VolumeError |
el sistema de ficheros no tiene ningún mount target available (antes de lanzar) |
despliega la pila o espera a que termine |
VolumeException |
VolumeError |
VolumeStore.create/get/list/destroy falló (IAM, límite de access points, sistema de ficheros no disponible) |
revisa el permiso o el estado del sistema de ficheros |
VolumeNotFoundException |
VolumeNotFoundError |
get/destroy sobre un nombre que no existe |
lista con VolumeStore.list() |
Diferencias con E2B¶
- E2B (
Volume.create/connect/list/get_info/destroy, beta privada) no tiene un plano de datos propio pararead_file/write_file/make_dir/list/removesobre un volumen sin sandbox — Rayito tampoco: esas operaciones sonUnimplementedErroren el shim (rayito.e2b.Volume), porque no hay servicio de plano de control fuera de un MicroVM (SPEC.md§4). Volume.destroyen E2B no borra los datos del directorio;VolumeStore.destroytampoco (sólo el access point). Borrar los datos exige montar el sistema de ficheros desde un sandbox.- E2B no documenta un límite de montajes; Rayito limita a 4 entre
mounts=yvolumes=juntos (research doc §5).
Ver también¶
- Funciones opcionales
- Persistencia (S3) — la alternativa sin VPC ni imagen especial, sin compartir en vivo
- Volúmenes EFS en tu VPC —
EfsVolumes: comprobar, desplegar y borrar - Pilas opcionales —
rayito stack deploy efs-volumes - Plantilla:
infra/efs-volumes.yaml
Fuentes y mediciones
- Estudio de viabilidad completo:
docs/research/2026-10-efs-persistence.md. - Contrato de la API de EFS:
AWS_API_NOTES.md§22, en GitHub. - Decisión de diseño: ADR-018 en
ARCHITECTURE.md. - Campaña de medición:
scripts/measure/efs_volumes.py(plan/run/report/cleanup), preguntas EFS-1..EFS-20; resultados enAWS_API_NOTES.md§16 Q122–Q134. - Amenaza y riesgos residuales:
SECURITY.mdT21.