Cloudflare R2¶
Vision General¶
Cloudflare R2 es el servicio de almacenamiento de objetos de GDI Latam. Es compatible con la API S3 de AWS, lo que permite usar boto3 (Python) para interactuar con el.
Se utiliza para almacenar:
- PDFs oficiales firmados (documentos finales)
- PDFs pendientes de firma (borradores enviados a firma)
- Assets del sistema (logos, isologos de organizaciones)
Buckets¶
| Bucket | Proposito | Acceso |
|---|---|---|
tenant-<id>-oficial |
PDFs firmados y oficiales | GDI-Backend (lectura/escritura) |
tenant-<id>-tosign |
PDFs pendientes de firma (temporales) | GDI-Backend (lectura/escritura) |
gdi-assets |
Logos e imagenes de organizaciones | GDI-BackOffice-Back (escritura), Frontends (lectura) |
Convenciones de naming
Los nombres de bucket siguen el patron tenant-<schema_name>-<tipo>. Ejemplo: para el schema 200_muni, los buckets son tenant-test-oficial y tenant-test-tosign.
Credenciales¶
Las credenciales de R2 se almacenan como variables de entorno. Nunca en codigo fuente.
| Variable | Descripcion | Donde se usa |
|---|---|---|
CF_R2_ENDPOINT |
Endpoint S3-compatible | GDI-Backend |
CF_R2_ACCESS_KEY_ID |
Access Key ID | GDI-Backend |
CF_R2_SECRET_ACCESS_KEY |
Secret Access Key | GDI-Backend |
CF_R2_SIGN_EXPIRATION |
Expiracion presigned URLs (seg) | GDI-Backend |
CF_R2_SIGN_EXPIRATION |
Expiracion de presigned URLs (segundos) | GDI-Backend |
Formato del endpoint:
Seguridad
Las credenciales R2 solo deben estar en variables de entorno del servidor. Nunca commitear credenciales al repositorio.
Acceso via S3 API (boto3)¶
GDI-Backend usa boto3 para interactuar con R2. La configuracion del cliente es:
import boto3
r2_client = boto3.client(
"s3",
endpoint_url=os.getenv("CF_R2_ENDPOINT"),
aws_access_key_id=os.getenv("CF_R2_ACCESS_KEY_ID"),
aws_secret_access_key=os.getenv("CF_R2_SECRET_ACCESS_KEY"),
region_name="auto", # R2 no usa regiones, pero boto3 lo requiere
)
Operaciones Principales¶
Subir un PDF:
r2_client.put_object(
Bucket="tenant-test-oficial",
Key="200_muni/official/2025/01/IF-2025-0001234-MUNI.pdf",
Body=pdf_bytes,
ContentType="application/pdf",
)
Generar URL firmada (presigned) para descarga:
url = r2_client.generate_presigned_url(
"get_object",
Params={
"Bucket": "tenant-test-oficial",
"Key": "200_muni/official/2025/01/IF-2025-0001234-MUNI.pdf",
},
ExpiresIn=600, # 10 minutos
)
Descargar un PDF:
response = r2_client.get_object(
Bucket="tenant-test-tosign",
Key="200_muni/tosign/draft-uuid.pdf",
)
pdf_bytes = response["Body"].read()
Eliminar un objeto:
Estructura de Keys (Paths)¶
Los objetos en R2 se organizan con la siguiente estructura de keys:
Bucket Oficial (tenant-*-oficial)¶
Ejemplos:
200_muni/official/2025/01/IF-2025-0001234-MUNI.pdf
200_muni/official/2025/01/DICT-2025-0000089-MUNI.pdf
200_muni/official/2025/02/RES-2025-0000001-MUNI.pdf
Bucket ToSign (tenant-*-tosign)¶
Ejemplos:
200_muni/tosign/a1b2c3d4-e5f6-7890-abcd-ef1234567890.pdf
200_muni/tosign/f9e8d7c6-b5a4-3210-fedc-ba0987654321.pdf
Bucket Assets (gdi-assets)¶
Ejemplos:
Flujo de Documentos¶
graph LR
subgraph Creacion
A["Backend genera PDF<br/>(via PDFComposer)"] --> B["Sube a bucket tosign"]
end
subgraph Firma
B --> C["Notary descarga de tosign"]
C --> D["Notary firma PDF"]
D --> E["Backend sube a bucket oficial"]
end
subgraph Consulta
E --> F["Backend genera presigned URL"]
F --> G["Frontend descarga PDF"]
end
- Creacion: El Backend genera un PDF via PDFComposer y lo sube al bucket
tosign - Firma: Cuando se inicia el proceso de firma, el Backend descarga de
tosign, envia a Notary para firmar, y sube el resultado aoficial - Consulta: El Frontend solicita un PDF al Backend, que genera una presigned URL temporal para la descarga directa desde R2
Multi-Tenant¶
Cada organizacion (tenant) tiene sus propios buckets con un prefijo de schema en las keys:
# Tenant 200_muni
tenant-test-oficial/200_muni/official/...
tenant-test-tosign/200_muni/tosign/...
# Tenant 200_municipio
tenant-municipio-oficial/200_municipio/official/...
tenant-municipio-tosign/200_municipio/tosign/...
El codigo en services/storage/cloudflare.py obtiene el cliente R2 correcto usando:
Aislamiento de datos
El schema_name se usa como prefijo en las keys de R2 para asegurar que un tenant no pueda acceder a los archivos de otro. Este patron es consistente con el multi-tenant de la base de datos.
CORS¶
Para que los frontends puedan descargar PDFs directamente desde R2 (via presigned URLs), se requiere configurar CORS en los buckets.
Configuracion de CORS recomendada (via Cloudflare Dashboard):
[
{
"AllowedOrigins": [
"https://tu-frontend.tu-dominio.com",
"https://tu-backoffice.tu-dominio.com",
"http://localhost:3003",
"http://localhost:3013"
],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
}
]
Presigned URLs
En la practica, GDI usa presigned URLs para la mayoria de descargas. Las presigned URLs no requieren CORS porque la autenticacion esta embebida en la URL misma. CORS solo es necesario si se hacen requests directos desde el navegador con headers de autenticacion.
Monitoreo¶
Verificar Conectividad¶
Desde el Backend, se puede verificar la conexion a R2 listando los buckets:
Verificar Objetos¶
# Via AWS CLI (compatible con R2)
aws s3 ls s3://tenant-test-oficial/ \
--endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com \
--profile r2
Limites¶
| Limite | Valor |
|---|---|
| Tamano maximo de objeto | 5 GB (multipart upload) |
| Tamano maximo de upload simple | 100 MB |
| Operaciones Class A (escritura) | Gratis primeras 1M/mes |
| Operaciones Class B (lectura) | Gratis primeras 10M/mes |
| Storage | Gratis primeros 10 GB/mes |
| Egress | Gratis (sin cargos de salida) |
Costos
Cloudflare R2 no cobra por egress (trafico de salida), lo que lo hace significativamente mas economico que AWS S3 para servir PDFs a usuarios finales.