harmony/adr/002-hexagonal-architecture.md
Jean-Gabriel Gill-Couture 62a554fac7 feat(modules/opnsense): refactor and add upgrade functionality
Refactor OPNSense module to use a mod.rs structure and add an OPNsenseLaunchUpgrade score for launching firmware upgrades.
2025-02-21 11:24:04 -05:00

238 lines
14 KiB
Markdown

# Architecture Decision Record: Adoption of Hexagonal Architecture
Proposed by : Jean-Gabriel Gill-Couture
Date : September 1st 2024
## Status
Proposed
## Context
Our infrastructure orchestrator project involves complex interactions between core business logic and various external systems (hardware interfaces, network protocols, etc.). We need an architectural pattern that allows for clear separation of concerns, flexibility, and ease of testing.
## Decision
We have decided to adopt Hexagonal Architecture (also known as Ports and Adapters) for structuring our application.
## Rationale
1. Separation of Concerns: Hexagonal Architecture clearly separates core domain logic from external concerns, which is crucial given our diverse set of external interfaces.
2. Flexibility: The ports and adapters model allows us to easily swap or add new implementations for different hardware or protocols without affecting the core logic.
3. Testability: The architecture facilitates easier testing by allowing us to mock external dependencies through port interfaces or virtualization.
4. Alignment with Project Requirements: The structure aligns well with our need to interact with various external systems (IPMI, Redfish, AMT, etc.) and our plan to use event-driven patterns with eg. NATS.
5. Future-proofing: As we plan to expand into day 2 operations and policy management, Hexagonal Architecture provides a clear path for growth without compromising existing structure.
6. Compatibility with DDD: While providing more structure than pure DDD, Hexagonal Architecture allows us to incorporate DDD principles in modeling our core domain.
## Consequences
Positive:
- Clear structure for organizing complex interactions with external systems.
- Improved testability of core business logic.
- Easier to adapt to new requirements or change external implementations.
Negative:
- Initial overhead in setting up the architecture.
- Potential for over-engineering if not carefully managed.
## Alternatives Considered
- Pure Domain-Driven Design: Excellent for modeling complex domains but provides less guidance on structuring external interactions.
- Clean Architecture: Similar benefits to Hexagonal Architecture but with more prescribed layers that may be overly complex for our needs.
- Traditional Layered Architecture: Simpler but less flexible for our complex external interactions.
## Conversation log
This ADR was written in large part by Claude 3.5 Sonnet, here is the conversation leading up to the current ADR :
> **User** (This conversation was started talking about the project at a higher level)
>
> I am pretty happy with my progress so far. I was previously digging into various libraries for low-level management to help me decide which language and tech stack to choose.
>
> I settled down on Rust because :
> - I know it best
> - The tools provide a great DX
> - The language is rapidly rising and gaining endorsement from major entities such as US government, Microsoft and many others for low level work
> - The missing libraries for IPMI and such are not that big of a deal : we can always write small programs in go or python that expose an api and interact with them from my Rust program without loosing too much productivity
>
>
> Now I am working on my architecture. I am quite experienced with Domain Driven Design, Gang of Four Design patterns, SOLID principles, etc.
>
> Though I am a fan of more organic, less strict architectural style. I think Hexagonal architecture migh be a good fit but it's been a long time since I studied it.
>
> There will be some CQRS like aspects to the architecture when the project gains maturity as we will be subscribing to policies channel for day 2 operations much like RedHat Open Cluster Management (aka Advanced Cluster Management). We will likely use Nats to distribute messages across our many clusters on various updates channels such as "firewall-standard", "single-node-k8s", "fully-ha-okd" and stuff like that. Design is still very early and may evolve.
>
> For now, please help me come up with a fitting architectural style for the first module :
>
> Cluster bootstraping, installation and configuration. This will do all of these :
>
> - Install and configure firewall OS (opnsense)
> - discover all available hardware in LAN (port scanning, discover brand through mac address, try logging in with default credentials, build an inventory by scanning with ipmi or redfish or vendor specific protocol such as amt, discover network interfaces and Storage Drives, identify switches, their brand, models, available ports)
> - Build the network topology (which cable from which host is plugged in which switch/firewall port)
> - Assign roles to each hardware component (automatically assign the control plane role to hosts with around 16GB of RAM and only 1-2 hard drives, automatically assign storage role to those with fast network, lots of storage available and other requirements, automatically assign firewall role to those with lots of nics but lower RAM and CPU power, automatically assign worker role to thos with lots of CPU and RAM, fast network but low storage, etc)
> - Initialize the cluster, build and launch a PXE server, reboot the bootstrap node using ipmi/redfish and force it to use the proper PXE/iPXE settings, then make sure the bootstrapping process works properly, reboot nodes in the right order, etc.
> - Make sure the cluster is installed properly, run sanity checks, hardware stress tests, etc
> - Handoff the cluster management to the cluster itself, the programm will first live on an external device but then be transferred to the cluster
> - Once living inside the cluster, continuously monitor and manage the cluster, making sure it is up to date, healthy and following the policies that it is subscribed to
> **Claude 3.5 Sonnet**
>
> Based on your project description and requirements, I would recommend an architecture that combines elements of Hexagonal Architecture (also known as Ports and Adapters) with some aspects of Clean Architecture and Event-Driven Architecture. This approach will provide the flexibility and modularity you need while allowing for easy integration of different protocols and technologies. Here's a high-level overview of how this architecture could look:
>
> 1. Core Domain Layer:
> - Contains the core business logic and domain models
> - Implements use cases and domain services
> - Is independent of any external frameworks or libraries
>
> 2. Application Layer:
> - Orchestrates the use cases
> - Implements CQRS-like patterns for handling commands and queries
> - Defines interfaces (ports) for external dependencies
>
> 3. Infrastructure Layer:
> - Implements the adapters for various external systems and protocols (IPMI, Redfish, AMT, etc.)
> - Handles data persistence, messaging (NATS), and external API calls
>
> 4. Presentation Layer:
> - Handles user interactions (CLI, API, etc.)
> - Translates external requests into application commands/queries
>
> 5. Cross-Cutting Concerns:
> - Logging, error handling, configuration management
>
> Here's a more detailed breakdown of how this could be structured for your cluster bootstrapping module:
>
> ```
> src/
> ├── domain/
> │ ├── models/
> │ │ ├── hardware.rs
> │ │ ├── network.rs
> │ │ ├── cluster.rs
> │ │ └── role.rs
> │ ├── services/
> │ │ ├── discovery_service.rs
> │ │ ├── role_assignment_service.rs
> │ │ └── cluster_initialization_service.rs
> │ └── repositories/
> │ ├── hardware_repository.rs
> │ └── network_repository.rs
> ├── application/
> │ ├── use_cases/
> │ │ ├── discover_hardware.rs
> │ │ ├── build_network_topology.rs
> │ │ ├── assign_roles.rs
> │ │ ├── initialize_cluster.rs
> │ │ └── perform_health_checks.rs
> │ ├── ports/
> │ │ ├── hardware_discovery.rs
> │ │ ├── network_management.rs
> │ │ ├── pxe_server.rs
> │ │ └── cluster_management.rs
> │ └── event_bus.rs
> ├── infrastructure/
> │ ├── adapters/
> │ │ ├── ipmi_adapter.rs
> │ │ ├── redfish_adapter.rs
> │ │ ├── amt_adapter.rs
> │ │ ├── opnsense_adapter.rs
> │ │ └── nats_adapter.rs
> │ ├── repositories/
> │ │ ├── in_memory_hardware_repository.rs
> │ │ └── in_memory_network_repository.rs
> │ └── services/
> │ ├── pxe_server_impl.rs
> │ └── cluster_management_impl.rs
> ├── presentation/
> │ ├── cli/
> │ │ └── commands.rs
> │ └── api/
> │ └── routes.rs
> └── main.rs
> ```
>
> Key architectural decisions:
>
> 1. Use of interfaces (traits in Rust) in the application layer to define ports, allowing for easy swapping of implementations.
>
> 2. Event-driven approach using an event bus for loose coupling between components.
>
> 3. Separation of concerns: each layer has a specific responsibility.
>
> 4. Use of the Repository pattern for data access, allowing for easy switching between in-memory, database, or API-based storage.
>
> 5. Implementation of adapters in the infrastructure layer to interact with external systems.
>
> Implementation strategy:
>
> 1. Start with implementing the core domain models and services.
>
> 2. Define the use cases in the application layer, using interfaces for external dependencies.
>
> 3. Implement the infrastructure adapters one by one, starting with the most critical ones (e.g., hardware discovery).
>
> 4. Create a simple CLI or API in the presentation layer to trigger the use cases.
>
> 5. Use dependency injection (can be manual in Rust) to wire everything together in your `main.rs`.
>
> This architecture allows for:
>
> - Easy testing: You can mock the interfaces for unit testing.
> - Flexibility: You can easily add new adapters or swap out implementations.
> - Scalability: As your project grows, you can add new use cases and domain services without affecting the overall structure.
> - Clear separation of concerns: Each part of your system has a well-defined responsibility.
>
> Remember, this is a starting point. As you develop your system, you may need to adjust the architecture to fit your specific needs. The key is to maintain loose coupling between components and keep your core domain logic isolated from external concerns.
> **User**
>
> Explain me more about hexagonal architecture and why it is more fitting than a pure DDD architecture.
> **Claude 3.5 Sonnet**
>
> Certainly. Let's dive deeper into Hexagonal Architecture and compare it with a pure Domain-Driven Design (DDD) approach.
>
> Hexagonal Architecture, also known as Ports and Adapters Architecture, was proposed by Alistair Cockburn. The main idea behind this architecture is to put the business logic at the center of the application and keep it isolated from external concerns.
>
> Key aspects of Hexagonal Architecture:
>
> 1. Core Domain: The center of the hexagon contains the business logic, free from dependencies on external systems or frameworks.
>
> 2. Ports: These are interfaces that define how the core domain interacts with the outside world. They are divided into:
> - Driving Ports (Primary): Used by external actors to interact with the application.
> - Driven Ports (Secondary): Used by the application to interact with external systems.
>
> 3. Adapters: These are implementations of the ports. They translate between the external world and the core domain.
>
> Why Hexagonal Architecture might be more fitting than pure DDD for your project:
>
> 1. Clear Separation of Concerns:
> While DDD focuses on modeling the domain, Hexagonal Architecture provides a clear structure for separating the domain from external concerns. This is particularly useful in your case, where you're dealing with various hardware interfaces, network protocols, and external systems.
>
> 2. Flexibility in Implementation:
> Your project involves interacting with multiple external systems (IPMI, Redfish, AMT, etc.). Hexagonal Architecture allows you to easily swap or add new implementations without affecting the core domain logic.
>
> 3. Testability:
> The port interfaces make it easy to mock external dependencies, allowing you to test your core logic in isolation. This is crucial for a complex system like yours where you need to ensure the reliability of your cluster management logic.
>
> 4. Adaptability to Change:
> As your project evolves and potentially needs to support new hardware or protocols, Hexagonal Architecture makes it easier to add new adapters without modifying the core domain.
>
> 5. Technology Independence:
> Your core logic remains independent of the specific technologies used for hardware management or networking. This allows you to change these technologies if needed without rewriting your business logic.
>
> 6. Alignment with Your Multi-Language Approach:
> You mentioned using small programs in Go or Python for certain tasks. Hexagonal Architecture's adapter concept aligns well with this approach, allowing you to integrate these external programs seamlessly.
>
> 7. Event-Driven Compatibility:
> The ports in Hexagonal Architecture can easily accommodate event-driven patterns, which aligns well with your plan to use NATS for message distribution.
>
> 8. Gradual Migration and Expansion:
> As your system grows to include day 2 operations and policy management, Hexagonal Architecture provides a clear path for expansion without compromising the existing structure.
>
> While pure DDD is excellent for modeling complex domains, Hexagonal Architecture provides additional structural benefits that are particularly relevant to your infrastructure orchestration project. It gives you a clear place for all the different components of your system - from core cluster management logic to hardware interfacing code.
>
> That said, it's important to note that Hexagonal Architecture and DDD are not mutually exclusive. You can (and should) use DDD principles to model your core domain within the Hexagonal Architecture structure. This combination gives you the best of both worlds: a rich, well-modeled domain at the core, with a clear structure for interfacing with the complex technical landscape of hardware and network management.