🚀 Construyendo un motor de ejecución Multi-SSH

Cuando ejecutar un comando SSH deja de ser trivial

Ejecutar un comando remoto en un servidor Linux desde C# es una de esas tareas que parecen completamente resueltas desde el principio. Durante mucho tiempo, el enfoque clásico funciona sin problemas: se abre una conexión SSH, se ejecuta un comando, se captura la salida y se cierra la conexión. Es simple, directo y suficiente para scripts aislados o automatizaciones puntuales.

Sin embargo, ese modelo empieza a romperse en cuanto el sistema evoluciona.

Cuando entran en juego múltiples servidores, sesiones persistentes, logs en tiempo real o interacción con procesos remotos, la complejidad deja de ser accidental y pasa a ser estructural. La lógica SSH comienza a duplicarse en distintas partes del código, el control de errores se vuelve inconsistente y las conexiones empiezan a abrirse y cerrarse sin una estrategia clara.

En ese punto, el problema deja de ser técnico en el sentido más básico. Ya no se trata de ejecutar comandos, sino de cómo organizar esa capacidad dentro de una arquitectura que pueda escalar sin degradarse.


El problema real: diseñar una capa de ejecución remota sostenible

Ejecutar comandos remotos no es complicado. Lo complejo es hacerlo de forma consistente, mantenible y extensible dentro de una aplicación que evoluciona con el tiempo.

La pregunta relevante deja de ser cómo ejecutar un comando por SSH en C#, y pasa a ser cómo diseñar una capa de ejecución remota que no contamine el resto del sistema. Es decir, cómo evitar que detalles de infraestructura terminen mezclándose con lógica de negocio, interfaces de usuario o flujos de aplicación.

Ese es el punto de partida de Kalyptus.Enginer: no resolver la ejecución en sí, sino encapsularla de forma que el sistema siga siendo comprensible cuando crece.


Qué es Kalyptus.Enginer

Kalyptus.Enginer es una librería en C# orientada a proporcionar una capa de ejecución remota limpia sobre SSH. Su objetivo no es competir con herramientas de orquestación como Kubernetes o Ansible, sino cubrir un espacio intermedio: el de las aplicaciones .NET que necesitan ejecutar comandos en servidores Linux de forma controlada.

Se trata, en esencia, de un componente runtime. No define infraestructura, no orquesta despliegues complejos y no introduce modelos declarativos. Su responsabilidad es mucho más acotada: ejecutar comandos remotos de forma estructurada y predecible.

Esta limitación es deliberada. Al reducir el alcance, la librería puede centrarse en resolver correctamente un problema específico sin arrastrar complejidad innecesaria.


Separación de responsabilidades como base de la arquitectura

Uno de los pilares del diseño es la separación clara entre lo que se ejecuta, cómo se ejecuta y cuándo se ejecuta.

Kalyptus.Commands  -> QUÉ ejecutar
Kalyptus.Enginer   -> CÓMO ejecutarlo
Client / UI        -> CUÁNDO y POR QUÉ ejecutarlo

Esta división evita un problema muy habitual en sistemas de este tipo: la mezcla de lógica de infraestructura con lógica de aplicación. Cuando esa frontera no existe, los comandos, las conexiones SSH y las decisiones de negocio acaban entrelazándose, dificultando tanto el mantenimiento como la evolución.

Al aislar estas responsabilidades, cada capa puede evolucionar de forma independiente. Los comandos no dependen del transporte, la ejecución no conoce reglas de negocio y la interfaz no necesita gestionar detalles técnicos innecesarios.


El papel de las conexiones SSH en la arquitectura

Una de las decisiones más relevantes del sistema es tratar las conexiones SSH como recursos persistentes y no como operaciones efímeras.

En muchos diseños, cada comando implica abrir y cerrar una conexión. Aunque esto simplifica la implementación inicial, introduce costes significativos a medida que aumenta la carga: latencia adicional, sobrecarga de autenticación y una experiencia poco fluida en interfaces que requieren múltiples interacciones con el mismo servidor.

Kalyptus.Enginer adopta un enfoque distinto. Las conexiones se mantienen activas y se reutilizan entre ejecuciones. El ciclo de vida deja de estar implícito en cada comando y pasa a ser una responsabilidad explícita del sistema.

Este cambio conceptual permite trabajar con servidores como contextos activos, donde es posible ejecutar múltiples operaciones sin incurrir en el coste de reconexión constante.


Organización interna del sistema

La arquitectura se estructura en capas bien definidas, donde cada componente tiene una responsabilidad concreta.

Aplicación
    ↓
Servicios (Docker, etc.)
    ↓
ICommandExecutor
    ↓
CommandExecutor
    ↓
SshConnectionManager
    ↓
Sesiones SSH persistentes

Una decisión clave es que el componente encargado de ejecutar comandos (CommandExecutor) no gestiona directamente las conexiones. Esa responsabilidad recae en SshConnectionManager, que actúa como intermediario entre la ejecución y el estado de las sesiones.

Esta separación mejora la testabilidad, reduce el acoplamiento y evita efectos secundarios difíciles de rastrear.


La abstracción de ejecución

El núcleo del sistema es la interfaz ICommandExecutor, que define las distintas formas de ejecutar comandos remotos.

public interface ICommandExecutor
{
    Task<EngineCommandResult> ExecuteAsync(...);
    Task<EngineCommandResult> ExecuteStreamingAsync(...);
    Task<EngineCommandResult> ExecuteInteractiveAsync(...);
}

La interfaz es intencionadamente simple. En lugar de cubrir todos los posibles escenarios, se centra en tres modos fundamentales: ejecución estándar, streaming de salida e interacción con procesos remotos.

Esta simplicidad permite mantener un contrato estable y facilita la sustitución del mecanismo de transporte en el futuro sin afectar a las capas superiores.


La importancia de los resultados estructurados

Uno de los aspectos más prácticos del diseño es la forma en la que se modelan los resultados de ejecución.

public class EngineCommandResult
{
    public bool Success { get; set; }
    public int ExitCode { get; set; }
    public string StandardOutput { get; set; }
    public string StandardError { get; set; }
}

En lugar de devolver simplemente una cadena de texto, el sistema proporciona información explícita sobre el resultado del comando. Esto permite construir lógica más robusta en capas superiores, ya sea para logging, auditoría o toma de decisiones automáticas.

En sistemas de infraestructura, estos detalles no son accesorios; son esenciales para operar de forma fiable.


Extensión mediante servicios de alto nivel

El diseño también permite construir capas adicionales sin modificar el núcleo del sistema. Un ejemplo claro es la integración con Docker.

En lugar de acoplar directamente la lógica Docker al manejo de SSH, se introducen servicios específicos que utilizan el executor como dependencia. De este modo, la construcción de comandos y su ejecución permanecen desacopladas.

Este enfoque facilita la extensión del sistema sin comprometer su coherencia interna.


Cuándo aplicar este enfoque

Este tipo de arquitectura resulta especialmente útil en aplicaciones .NET que necesitan interactuar con infraestructura de forma programática. Es habitual en herramientas internas, plataformas de gestión de servidores o sistemas de automatización controlada.

Sin embargo, no pretende sustituir soluciones de orquestación completas. Cuando el problema requiere modelos declarativos, ejecución distribuida compleja o gestión de clústeres, herramientas como Kubernetes, Terraform o Ansible operan en un nivel distinto.

Kalyptus.Enginer se sitúa deliberadamente por debajo de ese nivel, como un componente reutilizable dentro de sistemas más amplios.


Conclusión

En última instancia, este problema no trata de SSH ni de ejecución remota. Trata de cómo diseñar sistemas que sigan siendo comprensibles a medida que crecen.

Ejecutar comandos es sencillo. Mantener una arquitectura limpia cuando aumentan los servidores, los flujos y los casos de uso es lo realmente difícil.

Kalyptus.Enginer propone una forma de abordar ese problema mediante separación de responsabilidades, control explícito del ciclo de vida y abstracciones bien definidas.

En sistemas de infraestructura, ese tipo de decisiones no son opcionales. Son las que determinan si el sistema podrá evolucionar o acabará colapsando bajo su propia c