Volúmenes EFS en tu VPC (experimental)¶
Desde 0.7.0, experimental (Novedades).
La forma rápida y segura de preparar los
volúmenes EFS en una VPC que ya existe: el caso
común, porque muchas cuentas no pueden crear VPCs (una política de la
organización suele denegar ec2:CreateVpc). EfsVolumes comprueba la VPC
sin tocar nada, despliega sólo recursos nuevos dentro de ella y los
borra todos cuando se lo pides.
Experimental
El sistema de ficheros, los grupos de seguridad, el conector, el CRUD de
volúmenes (VolumeStore) y el montaje con Sandbox.create(volumes=...)
son reales, pero montar exige la imagen opcional con amazon-efs-utils
(rayito-base-caps-efs, ver
Imagen con amazon-efs-utils
y Medido en AWS real).
En cinco minutos¶
Necesitas credenciales de AWS con permiso para CloudFormation y EC2 en la cuenta, el id de una VPC existente y de una a tres subredes privadas suyas, cada una en una AZ distinta. Con eso:
- Comprueba la VPC sin crear nada (
check()orayito doctor --efs-vpc-id ... --efs-subnet-ids ...): sección 1. - Despliega la pila (
deploy()orayito stack deploy efs-volumes --param VpcId=... --param SubnetIds=...): unos 5 minutos, sección 2. - Adjunta la salida
CallerPolicyArnal execution role con el que lanzarás los sandboxes, y guardaConnectorArn. - Crea un volumen con
efs.volume_store().create("nombre"). - Monta: publica la imagen con
amazon-efs-utils(make image-publish-caps-efsorayito image publish --with-efs --os-capabilities ALL) y lanza con ella,execution_role_arn=,egress=[ConnectorArn](sólo ese conector: un sandbox con volumen no usaINTERNET_EGRESS, ver Salida a internet) yvolumes={...}(Montar volúmenes).
Para quitarlo todo: destroy(delete_file_system=True), sección
3.
Coste y activación
- Por defecto: apagado. Sin una llamada explícita a
EfsVolumes.deploy(...)(orayito stack deploy efs-volumes) no se crea nada; construirEfsVolumesno llama a AWS. - Activa:
EfsVolumes(...).deploy(vpc_id=..., subnet_ids=[...])en Python onew EfsVolumes(...).deploy({ vpcId, subnetIds })en TypeScript (peer@aws-sdk/client-ec2para la comprobación y@aws-sdk/client-efspara borrar el sistema de ficheros).check()yrayito doctor --efs-vpc-id ... --efs-subnet-ids ...son de sólo lectura. - Recursos y llamadas AWS:
check()sólo lee EC2 (DescribeVpcs,DescribeVpcAttribute,DescribeSubnets,DescribeRouteTables).deploy()crea, etiquetado y dentro de la VPC dada: un sistema de ficheros EFS cifrado en reposo (clave KMS gestionada por AWS), un mount target por subred, dos grupos de seguridad nuevos, unAWS::Lambda::NetworkConnectorde salida a la VPC para MicroVMs, su rol de operador y la políticaRayitoEfsVolumeClient. Nunca modifica la VPC, sus subredes, tablas de rutas, NACLs ni ningún grupo de seguridad existente. - Coste aproximado (us-east-1, consultado 2026-09-11, precios de
EFS): $0 con el sistema de
ficheros vacío; $0,30/GB-mes (Standard) y $0,016/GB-mes tras 30 días
(IA); $0,03/GB leído y $0,06/GB escrito (Elastic Throughput). Mount
targets, access points y ENIs del conector no tienen cargo listado.
Las llamadas
Describe*decheck()son gratis. - IAM: quien despliega necesita CloudFormation sobre la pila,
crear los recursos de arriba (
CAPABILITY_IAM) y losec2:Describe*decheck(); paradelete_file_system,elasticfilesystem:DescribeFileSystems/DescribeMountTargets/DescribeAccessPoints/DeleteAccessPoint/DeleteFileSystem. El execution role del sandbox sólo recibe la política de la salidaCallerPolicyArn:ClientMount(yClientWritesalvoallow_write=Falsey salvo enread_only_access_point_arns) sobre este sistema de ficheros y sus access points, nuncaClientRootAccess. - Cómo apagarla:
destroy()borra la pila (conector, grupos, mount targets, rol y política) y conserva el sistema de ficheros con sus datos;destroy(delete_file_system=True)borra además sus access points y el propio sistema de ficheros: todo lo quedeploy()creó. Si borraste la pila conrayito stack destroy efs-volumes(que siempre lo conserva),delete_file_system("fs-…")borra el que quedó; sólo acepta uno con la etiquetarayito=efs-volumesque le pone la plantilla.
1. Comprueba la VPC (sin crear nada)¶
check() valida que la VPC y las subredes existen, que las subredes son
de esa VPC y están en AZs distintas (EFS admite un mount target por AZ),
que les quedan IPs libres (una para el mount target y al menos una para
una ENI del conector) y si la VPC resuelve DNS. Avisa de una sola AZ (sin
redundancia y con tráfico entre AZs facturado) y te dice cómo saldría a
internet un sandbox. El informe incluye lo que deploy() crearía y su
coste.
from rayito import EfsVolumes
efs = EfsVolumes(region="us-east-1") # (1)!
report = efs.check(
vpc_id="vpc-0123456789abcdef0",
subnet_ids=["subnet-0123456789abcdef0"],
)
for finding in report.findings:
print(finding.level, finding.code, finding.message)
print("deploy crearía:", report.cost.creates)
- Construirlo no llama a AWS.
import { EfsVolumes } from "rayito"; // + npm i @aws-sdk/client-ec2 @aws-sdk/client-efs
const efs = new EfsVolumes({ region: "us-east-1" });
const report = await efs.check({
vpcId: "vpc-0123456789abcdef0",
subnetIds: ["subnet-0123456789abcdef0"],
});
for (const finding of report.findings) {
console.log(finding.level, finding.code, finding.message);
}
| Hallazgo | Nivel | Qué significa |
|---|---|---|
vpc-missing, subnet-missing |
FAIL | la VPC o una subred no existe en esta cuenta y región |
subnet-other-vpc |
FAIL | la subred es de otra VPC |
subnet-same-az |
FAIL | dos subredes en la misma AZ: EFS admite un mount target por AZ |
subnet-free-ips |
FAIL | menos de 2 IPs libres en una subred |
vpc-unavailable, subnet-unavailable |
FAIL | no están en estado available |
vpc-dns-support, vpc-dns-hostnames |
WARN | sin DNS de VPC el nombre del sistema de ficheros no resuelve (montar por IP sí funciona) |
single-az |
WARN | una sola AZ |
internet-egress |
OK | informativo: ver Salida a internet |
2. Despliega¶
deploy() vuelve a correr check() y se niega (sin crear nada) si algún
hallazgo es FAIL.
from rayito import EfsVolumes
efs = EfsVolumes(region="us-east-1")
status = efs.deploy(
vpc_id="vpc-0123456789abcdef0",
subnet_ids=["subnet-0123456789abcdef0"],
)
print(status.outputs["ConnectorArn"], status.outputs["CallerPolicyArn"])
store = efs.volume_store() # (1)!
vol = store.create("datos-agente-7")
- Un
VolumeStoresobre elFileSystemIdde la pila.
import asyncio
from rayito import AsyncEfsVolumes
async def main() -> None:
efs = AsyncEfsVolumes(region="us-east-1")
await efs.deploy(
vpc_id="vpc-0123456789abcdef0",
subnet_ids=["subnet-0123456789abcdef0"],
)
store = await efs.volume_store()
await store.create("datos-agente-7")
asyncio.run(main())
import { EfsVolumes } from "rayito";
const efs = new EfsVolumes({ region: "us-east-1" });
const status = await efs.deploy({
vpcId: "vpc-0123456789abcdef0",
subnetIds: ["subnet-0123456789abcdef0"],
});
console.log(status.outputs.ConnectorArn, status.outputs.CallerPolicyArn);
const store = await efs.volumeStore();
await store.create("datos-agente-7");
Opciones de deploy():
subnet_ids/subnetIds: de 1 a 3 subredes, una por AZ.allow_write=False/allowWrite: false: la política del execution role sólo concedeClientMount(lectura).access_point_arns/accessPointArns: acota la política a esos access points exactos; vacío (por defecto), a cualquier access point de la cuenta y región sobre este sistema de ficheros. Redespliega con la lista nueva cuando añadas un volumen.read_only_access_point_arns/readOnlyAccessPointArns: esos access points se quedan en sólo lectura aunque el resto admita escritura (la política les deniegaClientWrite). Es lo que hace de sólo lectura un volumen: la opciónrodel montaje no basta, ver Sólo lectura de verdad. Por CLI:--param ReadOnlyAccessPointArns=arn:...,arn:....connector_name/connectorName: nombre del conector, único en la cuenta y región (por defectorayito-efs).
Adjunta la salida CallerPolicyArn al execution role que montará los
volúmenes, y pasa ConnectorArn como único conector en egress=[...]
cuando el montaje exista.
Shim E2B¶
El shim usa el mismo VolumeStore:
from rayito import EfsVolumes
from rayito.e2b import E2B
client = E2B(volume_store=EfsVolumes(region="us-east-1").volume_store())
vol = client.Volume.create("datos-agente-7")
Qué crea y por qué es seguro¶
| Recurso (nuevo) | Regla |
|---|---|
| Sistema de ficheros EFS | cifrado en reposo (KMS gestionada por AWS); su política deniega todo sin TLS (aws:SecureTransport), todo montaje sin access point y todo lo que no llegue por un mount target; no tiene ningún Allow, así que un cliente NFS sin IAM no obtiene nada |
| Mount targets (uno por subred) | en el grupo de mount targets |
| Grupo de seguridad de mount targets | sólo TCP 2049 de entrada, y sólo desde el grupo cliente (nunca un CIDR) |
| Grupo de seguridad cliente | usado por el conector; su única salida es TCP 2049 hacia el grupo de mount targets |
AWS::Lambda::NetworkConnector |
salida a la VPC para MicroVMs, en tus subredes, con el grupo cliente |
| Rol de operador del conector | sólo las acciones ec2:*NetworkInterface* que el conector necesita |
Política RayitoEfsVolumeClient |
ClientMount/ClientWrite sobre este sistema de ficheros y sus access points, Deny explícito de ClientWrite sobre ReadOnlyAccessPointArns, nunca ClientRootAccess |
CloudFormation propaga las etiquetas de la pila
(rayito:component=efs-volumes, rayito:managed-by,
rayito:sdk-version y las tuyas de tags=) a los recursos que admiten
etiquetas; el sistema de ficheros lleva además, fija en la plantilla,
rayito=efs-volumes. La VPC, sus subredes, tablas
de rutas, NACLs y grupos existentes no se tocan nunca; el e2e
(tests/e2e/test_m15_efs_volumes_vpc.py) lo comprueba comparando una
foto de todos ellos antes y después.
Salida a internet¶
El conector de esta pila sólo deja salir NFS (2049) hacia los mount
targets: no da salida a internet. Y un MicroVM admite un único conector
de egress: pedir a la vez INTERNET_EGRESS y el conector de la VPC es un
error de validación de AWS (medido el 2026-10-04, AWS_API_NOTES.md §16
Q131). El SDK lo rechaza antes de lanzar nada: Sandbox.create(volumes=...)
sin egress=, con INTERNET_EGRESS o con dos conectores es
InvalidArgumentException/InvalidArgumentError, y el hallazgo
internet-egress de check() lo recuerda. Si un sandbox con volumen
necesita internet:
- tiene que salir por tu VPC: depende de la ruta por defecto de tus
subredes (el
check()cuenta cuántas van a un NAT y cuántas a otra puerta, como un transit gateway) y de un conector propio cuyo grupo de seguridad lo permita (infra/egress-connector.yaml); esta pila no crea ninguno; - con el conector de esta pila el sandbox sí resuelve nombres DNS (los de EFS y los públicos), pero no conecta con nada fuera de los mount targets.
Medido en AWS real¶
Las aceptaciones del 2026-10-04 (AWS_API_NOTES.md §16 Q126–Q139)
desplegaron esta pila en una VPC existente y montaron con la imagen
rayito-base-caps-efs, primero a mano y después por
Sandbox.create(volumes=...) en Python y TypeScript:
| Qué | Resultado |
|---|---|
deploy() / destroy(delete_file_system=True) |
≈ 5 min; la VPC queda idéntica tras destroy(delete_file_system=True) |
| Conector hasta el mount target | ACTIVE; TCP 2049 en 0,1–0,5 s |
mount -t efs -o tls,iam,accesspoint sin systemd |
20 de 20; p50 313 ms, p95 589 ms |
create(volumes=...) |
un volumen de escritura y otro de sólo lectura mounted en 8–17 s (lanzamiento incluido); uid 1000 escribe en uno y recibe Read-only file system en el otro |
| Pausa y reanudación con el volumen montado | datos intactos tras 60 s y 10 min; primera lectura ≤ 0,2 s; tras 70 min (credenciales caducadas), rayd lo remonta solo en /resume: mounted otra vez en 0,7–1,3 s |
| Pausa con el mount target inalcanzable | sin escrituras pendientes, pausa y reanuda; con escrituras pendientes, AWS termina el MicroVM y se pierden |
| Rendimiento | 128 MB/s escribiendo y 585 MB/s leyendo en secuencial; ≈ 17 ms por fichero pequeño |
| Política del sistema de ficheros | sin access point, sin TLS o sin IAM: denegado |
efs-proxy tras umount |
umount no lo para; rayd lo termina al desmontar: 5 de 5 ciclos vuelven a 0 procesos |
uid 1000 y el puerto local de efs-proxy |
puede conectarse, pero con read_only_access_point_arns un montaje NFS directo por ese puerto no puede escribir (Read-only file system); un access point fuera de access_point_arns: iam_denied |
| Punto de montaje que es un enlace simbólico | invalid_path; nada se monta sobre el destino del enlace |
3. Bórralo¶
from rayito import EfsVolumes
efs = EfsVolumes(region="us-east-1")
efs.destroy() # (1)!
efs.destroy(delete_file_system=True) # (2)!
efs.delete_file_system("fs-0123456789abcdef0") # (3)!
- Borra la pila y conserva el sistema de ficheros con sus datos.
- Borra también sus access points y el sistema de ficheros: todo lo
que
deploy()creó. - Tras un
rayito stack destroy efs-volumes: borra el sistema de ficheros que quedó (sólo uno con la etiquetarayito=efs-volumes).
Errores¶
| Python | TypeScript | Cuándo |
|---|---|---|
InvalidArgumentException |
InvalidArgumentError |
ids mal formados, más de 3 subredes, un ARN de access point inválido, o deploy() sobre una VPC con algún FAIL (no se creó nada) |
VolumeException |
VolumeError |
volume_store() sin pila, o delete_file_system sobre uno sin la etiqueta de la pila o que no suelta sus mount targets |
StackException |
StackError |
CloudFormation falló al desplegar o borrar |
Ver también¶
- Volúmenes EFS:
VolumeStore,volumes=y el shim. - Pilas opcionales:
rayito stack deploy|status|destroy. - Plantilla:
infra/efs-volumes.yaml - Contrato de las llamadas a EC2 y EFS:
AWS_API_NOTES.md§22.