Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 3 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand All @@ -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.
23 changes: 23 additions & 0 deletions diagrams/08-costos-y-finops.mmd
Original file line number Diff line number Diff line change
@@ -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"]
81 changes: 80 additions & 1 deletion docs/08-costos-y-finops.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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`.
Expand Down
3 changes: 3 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 91 additions & 0 deletions examples/finops.rs
Original file line number Diff line number Diff line change
@@ -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()
);
}
}
Loading