7 de julio de 2026
Un pipeline de finanzas a 0 €/mes: S3, Go y un dedup atómico
#aws #go #dynamodb #serverless
Quería mis operaciones de DEGIRO y MyInvestor en un único sitio sin darle a ningún servicio mis credenciales bancarias. El resultado es finance-tracker: subes el CSV mensual del bróker a S3 y sale consolidado, deduplicado y trazado en CloudWatch. Coste operativo: 0 €/mes.
flowchart LR
U[Subir CSV] --> S3[("S3 pending/")]
S3 -->|ObjectCreated| L["Lambda · Go arm64"]
PS[("Parameter Store · KMS")] -.->|Service Account| L
L -->|"PutItem (condicional)"| DDB[("DynamoDB · fuente de verdad")]
L -.->|mirror, no-fatal| GS[("Google Sheets")]
L --> P[("S3 processed/")]
L -.->|"≥2 err / 5 min"| SNS[("SNS · email")]
Event-driven, no scraping programado
El instinto inicial es un cron que se loguea en cada bróker y scrapea. Eso significa guardar credenciales en la nube y mantener scrapers frágiles cada vez que un portal cambia su HTML. Así que lo invertí: la subida es el trigger.
s3://…/pending/<plataforma>/<archivo>.csv
│ s3:ObjectCreated (prefix=pending/, suffix=.csv)
▼
Lambda (Go, ARM64)
Cero credenciales en la nube, nada que programar, nada que scrapear. La
plataforma se lee del prefix de la key — pending/degiro/ despacha al parser
registrado para degiro.
Lo interesante: dedup atómica
Resubir el mismo CSV no debe crear operaciones duplicadas. En vez de leer-y-luego- escribir (una race esperando a pasar), dejo que DynamoDB lo imponga con una escritura condicional:
_, err := ddb.PutItem(ctx, &dynamodb.PutItemInput{
TableName: aws.String(table),
Item: item,
ConditionExpression: aws.String("attribute_not_exists(id)"),
})
var cfe *types.ConditionalCheckFailedException
if errors.As(err, &cfe) {
// ya ingerido — se omite, no es error
}
El id determinista (derivado de los campos estables de la operación) más
attribute_not_exists(id) hace la ingesta idempotente en la capa de base de
datos. Los reintentos salen gratis; sin lock, sin lectura, sin race.
Dual-write con un mirror no-fatal
DynamoDB es la fuente de verdad — queries server-side, ediciones atómicas,
aislamiento por usuario, sin rate limits. La Google Sheet se mantiene solo como
mirror legible, así que un fallo de Sheets es un WARN, nunca una ejecución
fallida:
PutItema DynamoDB (condicional) — tiene que ir bien.- Append a Sheets — best effort, se loguea si falla.
- Mover el archivo a
processed/. Si algún paso falla, se queda enpending/para reintento manual, conrequest_id/bucket/keyen los logs.
Por qué Go sobre ARM64
Go compila a un binario estático → se despliega un ZIP, no un contenedor: sin factura de ECR, menor cold start, builds sin Docker. ARM64 (Graviton) es ~20% más barato que x86_64, y el tipado estático es justo lo que quieres tocando números financieros.
Todo cabe en la capa gratuita — eventos S3, unas pocas invocaciones de Lambda al mes, DynamoDB on-demand, Parameter Store para el único secreto del service account de Google (cifrado con KMS, gratis hasta 10k parámetros). De ahí los 0 €/mes.
IAM escrito a mano, no la política genérica
Misma idea que la dedup: la garantía vive en la infra, no en el código. La
Lambda no lleva una S3CrudPolicy genérica — lleva permisos escritos a mano y
pegados a los prefixes:
s3:GetObject+s3:DeleteObjectsolo enpending/*.s3:PutObjectsolo enprocessed/*.ssm:GetParametersolo enfinance-tracker/*, ykms:Decryptsolo sobrealias/aws/ssm.
El Service Account de Google nunca toca el repo, ni las env vars, ni los logs: vive cifrado en Parameter Store y se descifra en el cold start. Y el deploy no guarda claves AWS de larga vida — GitHub Actions asume un role vía OIDC. Cero credenciales bancarias en la nube, cero secretos long-lived en CI.
Qué pasa cuando falla
Un pipeline que corre una o dos veces al mes es justo el que se te olvida hasta que se rompe. Así que el fallo es ruidoso y el estado, recuperable:
- Si cualquier paso peta, el CSV se queda en
pending/. No hay estado a medias que limpiar — corriges y resubes; la dedup hace el reintento idempotente. - Cada ejecución emite JSON estructurado con
request_id,platform,added,duplicates. - Una alarma salta con ≥2 errores en 5 minutos y publica a SNS; me llega un email. Otro email por cada ejecución que añade operaciones nuevas.
Nada de esto es código de aplicación: es una condition expression, una policy de IAM y una alarma de CloudWatch.
La lección que se repite: empuja las garantías difíciles hacia la infraestructura. El dedup no es lógica de aplicación que pueda equivocarme — es una condition expression que la base de datos impone cada vez.