diff --git a/Cargo.toml b/Cargo.toml index cf3a0a8..8aff8f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,10 @@ path = "examples/soluciones/managed_services_nivel_3.rs" name = "serverless" path = "examples/serverless.rs" +[[example]] +name = "finops" +path = "examples/finops.rs" + [[example]] name = "serverless_nivel_1" path = "examples/soluciones/serverless_nivel_1.rs" diff --git a/ROADMAP.md b/ROADMAP.md index 8ddba36..a436c45 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -49,7 +49,7 @@ esperada es: | 05 | Identidad y accesos | implemented | | 06 | Servicios manejados | implemented | | 07 | Serverless | implemented | -| 08 | Costos y FinOps | draft | +| 08 | Costos y FinOps | implemented | | 09 | AWS en la práctica | planned | | 10 | GCP en la práctica | planned | @@ -74,6 +74,5 @@ esperada es: ## Siguiente paso natural -Completar el milestone `08. Costos y FinOps` con el flujo restante: modelo Rust -mínimo, capítulo narrativo, diagrama Mermaid, ejemplos progresivos, ejercicios, -soluciones y análisis de costos. +Completar el milestone `08. Costos y FinOps` con ejercicios graduados, +soluciones compilables y análisis de costos sin precios vivos de proveedor. diff --git a/diagrams/08-costos-y-finops.mmd b/diagrams/08-costos-y-finops.mmd new file mode 100644 index 0000000..00bd058 --- /dev/null +++ b/diagrams/08-costos-y-finops.mmd @@ -0,0 +1,23 @@ +flowchart TD + A["Recurso cloud"] --> B["Unidad de costo"] + B --> C["Dueño y propósito"] + C --> D["Visibilidad"] + D --> E["Presupuesto y alerta"] + E --> F["Tradeoff de optimización"] + F --> G["Decisión responsable"] + + D --> H{"¿Se puede atribuir?"} + H -->|"sí"| E + H -->|"no"| I["Hallazgo: baja visibilidad"] + + E --> J{"¿Tiene límite de elasticidad?"} + J -->|"sí"| F + J -->|"no"| K["Hallazgo: elasticidad sin límite"] + + C --> L{"¿Tiene propósito?"} + L -->|"sí"| D + L -->|"no"| M["Hallazgo: falta propósito"] + + B --> N{"¿Está casi sin uso en producción?"} + N -->|"no"| C + N -->|"sí"| O["Hallazgo: recurso idle"] diff --git a/docs/08-costos-y-finops.md b/docs/08-costos-y-finops.md index c891eaf..7f455f9 100644 --- a/docs/08-costos-y-finops.md +++ b/docs/08-costos-y-finops.md @@ -4,8 +4,10 @@ - **Semestre:** 5 - **Estado:** implemented - **Milestone:** 08. Costos y FinOps -- **Issues:** #29, #30 +- **Issues:** #29, #30, #31 - **Módulo Rust:** `src/finops.rs` +- **Diagrama:** `diagrams/08-costos-y-finops.mmd` +- **Ejemplo:** `examples/finops.rs` ## Concepto @@ -117,6 +119,19 @@ externas: El módulo no debe intentar calcular precios reales. Su función es pedagógica: hacer visibles las variables que explican por qué aparece un costo. +## Lectura del modelo + +`FinOpsProfile` junta cuatro preguntas que suelen quedar separadas: + +1. **Qué se consume:** categoría de costo, ambiente y patrón de uso. +2. **Por qué se consume:** propósito, criticidad e intención de optimización. +3. **Quién lo cuida:** dueño y control de presupuesto. +4. **Cómo se observa:** visibilidad, unidad económica y límite de elasticidad. + +El modelo prefiere nombres explícitos sobre fórmulas. Un estudiante no necesita +memorizar precios para detectar que un gasto sin dueño, sin presupuesto, sin +unidad económica y sin límite de elasticidad todavía no está gobernado. + ## Cómo leer el módulo Rust El módulo `finops` empieza con un perfil explícito: @@ -182,6 +197,70 @@ assert!(profile.evaluate().findings().contains( )); ``` +El objetivo del modelo no es decidir si un recurso es caro o barato. El objetivo +es obligar a explicar qué valor compra, qué señal lo vuelve atribuible y qué +tradeoff aparece antes de optimizarlo. + +## Diagrama + +El diagrama del capítulo vive en `diagrams/08-costos-y-finops.mmd`. Resume la +lectura principal: + +```text +recurso -> unidad de costo -> dueño/proposito -> visibilidad -> presupuesto -> tradeoff -> decision +``` + +La rama crítica del diagrama aparece cuando una señal no puede atribuirse o +cuando un recurso elástico no tiene límite. En ambos casos, el problema no es +solo económico: también es de arquitectura y operación. + +El diagrama separa tres tipos de lectura: + +1. **Lectura normal:** el costo tiene unidad, dueño, propósito, señal, + presupuesto y decisión explícita. +2. **Lectura de gobierno:** si el costo no se puede atribuir, el primer trabajo + no es ahorrar; es volverlo explicable. +3. **Lectura de elasticidad:** si un recurso puede crecer sin límite, la + arquitectura debe declarar qué protege a la factura, a las dependencias y al + producto. + +## Ejemplo ejecutable + +El ejemplo `examples/finops.rs` compara un costo de producción atribuido y +gobernable contra invocaciones de desarrollo que crecen sin dueño, presupuesto +ni unidad económica. También incluye un recurso productivo casi sin uso para +mostrar que "barato" y "gobernado" no son sinónimos. + +```bash +cargo run --example finops +``` + +Salida esperada: + +```text +academy-api: costo atribuido y gobernable +preview-workers: 5 hallazgos educativos +legacy-worker: 1 hallazgos educativos +``` + +El ejemplo no consulta proveedores ni precios actuales. Su intención es mostrar +qué señales deben existir antes de abrir una calculadora o una factura real. + +## Ejemplos progresivos + +El capítulo usa tres niveles de lectura: + +| Nivel | Escenario | Señal principal | Aprendizaje | +|-------|-----------|-----------------|-------------| +| Básico | API de producción con presupuesto y unidad económica | Sin hallazgos | El costo puede defenderse porque tiene dueño, valor y control | +| Intermedio | Workers de preview con invocaciones crecientes | Elasticidad sin límite | La elasticidad necesita presupuesto, atribución y límite explícito | +| Avanzado | Worker productivo casi inactivo | Recurso idle | Un recurso puede ser estable y aun así revelar desperdicio | + +Estos escenarios son deliberadamente pequeños. La intención no es simular una +factura completa, sino entrenar la pregunta correcta antes de comparar +proveedores: qué decisión técnica explica el consumo y qué acción responsable +permite tomar. + ## Decisiones registradas - El perfil principal se llama `FinOpsProfile`. diff --git a/examples/README.md b/examples/README.md index 5b6d14e..bd1bbe3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -23,6 +23,9 @@ Los ejemplos progresivos de cada capítulo vivirán aquí: delegada, estado durable y recuperación probada. - `serverless.rs`: compara un flujo serverless acotado con una función que revela riesgos de retry, estado y observabilidad. +- `finops.rs`: compara un costo de producción atribuido contra invocaciones de + desarrollo sin dueño, presupuesto ni unidad económica y un recurso productivo + casi sin uso. - `soluciones/serverless_nivel_1.rs`: solución del ejercicio básico de handler de cola acotado. - `soluciones/serverless_nivel_2.rs`: solución del ejercicio intermedio de diff --git a/examples/finops.rs b/examples/finops.rs new file mode 100644 index 0000000..8194d33 --- /dev/null +++ b/examples/finops.rs @@ -0,0 +1,91 @@ +use rust_cloud::finops::{ + BudgetControl, CostCategory, CostVisibility, ElasticityLimit, Environment, FinOpsCriticality, + FinOpsFinding, FinOpsProfile, FinOpsRequirements, OptimizationIntent, UsagePattern, +}; + +fn main() { + let academy_api = FinOpsProfile::new( + "academy-api", + FinOpsRequirements { + category: CostCategory::Compute, + environment: Environment::Production, + usage_pattern: UsagePattern::Steady, + criticality: FinOpsCriticality::High, + elasticity: ElasticityLimit::Bounded { max_units: 20 }, + visibility: CostVisibility::UnitEconomics, + budget_control: BudgetControl::ForecastAndReview, + optimization_intent: OptimizationIntent::ReduceWaste, + owner: "equipo academy", + purpose: "servir rutas de aprendizaje a estudiantes", + unit_economics: "costo por estudiante activo", + }, + ) + .unwrap(); + + let preview_workers = FinOpsProfile::new( + "preview-workers", + FinOpsRequirements { + category: CostCategory::Invocations, + environment: Environment::Development, + usage_pattern: UsagePattern::Growing, + criticality: FinOpsCriticality::Medium, + elasticity: ElasticityLimit::Unbounded, + visibility: CostVisibility::Aggregate, + budget_control: BudgetControl::None, + optimization_intent: OptimizationIntent::None, + owner: "", + purpose: "ejecutar previews automáticos", + unit_economics: "", + }, + ) + .unwrap(); + + let legacy_worker = FinOpsProfile::new( + "legacy-worker", + FinOpsRequirements { + category: CostCategory::Compute, + environment: Environment::Production, + usage_pattern: UsagePattern::Idle, + criticality: FinOpsCriticality::Low, + elasticity: ElasticityLimit::NotElastic, + visibility: CostVisibility::Attributed, + budget_control: BudgetControl::BudgetWithOwner, + optimization_intent: OptimizationIntent::ReduceWaste, + owner: "equipo academy", + purpose: "mantener compatibilidad temporal con cargas antiguas", + unit_economics: "costo por ejecución heredada", + }, + ) + .unwrap(); + + print_evaluation(&academy_api); + let preview_evaluation = preview_workers.evaluate(); + assert!( + preview_evaluation + .findings() + .contains(&FinOpsFinding::UnboundedElasticity("preview-workers"),) + ); + print_evaluation(&preview_workers); + + let legacy_evaluation = legacy_worker.evaluate(); + assert!( + legacy_evaluation + .findings() + .contains(&FinOpsFinding::IdleProductionResource("legacy-worker"),) + ); + print_evaluation(&legacy_worker); +} + +fn print_evaluation(profile: &FinOpsProfile) { + let evaluation = profile.evaluate(); + + if evaluation.is_low_risk() { + println!("{}: costo atribuido y gobernable", profile.name()); + } else { + println!( + "{}: {} hallazgos educativos", + profile.name(), + evaluation.findings().len() + ); + } +}