A project to help define architecture logically as code, and generate living, interactive diagrams.
Run npx aac-cli demo
to quickly see this project in action!
- Introduction
- Design Goals
- High-Level Requirements
- Technical Notes
- Examples
- Other Ideas
- Alternatives
- Technical Design
- The CLI
- API Documentation
- Developer Guide
- Creating a Release
- TODO
We define infrastructure as code, configuration as code, and more - but what about architecture? The goal of this project is to allow teams to collaborate on architecture as code, and build up-to-date, interactive diagrams from existing systems or assets.
To quickly demo this project, just run:
npx aac-cli demo
The goals of this project are to allow a team to define architecture declaratively, as simple yaml
files, and have interactive diagrams and visualisations generated automatically.
At a high level:
- Developers and tech leads should be able to collaborate on architecture using standard practices such as pull-requests, code reviews, etc.
- The 'toil' of generating the diagrams and artifacts should be eliminated.
- The system should be simple enough to very quickly define an architecture at a high-level, then layer on details as needed.
- The system should allow 'multidimensional' models, allowing logical architecture to be defined, but also allow the viewer to 'dive' into lower level details, such as physical or network infrastructure if needed.
- The system should allow UI developers to rapidly create 'styles' for renderings, or custom renderers for typed entities.
A team should be able to use a yaml
file to represent logical or physical architecture. There are three types of entities:
Components
A component is any logical or physical entity. It might be a system, such as an ESB, a server, a switch, and storage service, and so on.
Containers
A conatainer is any logical grouping of components. It could be a logical container (such as 'Front End Services'), or a technical container (such as an AWS region).
Connections
A connection is a relationship between components or containers. It could be logical, such as a dependency between a service and a database, or a technical connection, such as a potential HTTP connection between a mobile app and a web server, via a gateway.
Level
A 'level' is an optional concept which may represent a 'zoom' of the diagram. For example, level 1 in a diagram might be the high level architecture. Level 2 might be the specific services and systems, level 3 might be the physical infrastructure on which an instance resides. By providing levels, diagrams should be able to be 'zoomed' as needed.
Aspect
An 'aspect' is a 'view' of the system. For example, the infrastructure aspect might only show physical and network infrastructure for the on-premise data center, which the 'cloud' aspect might show only the cloud components.
The in-memory structure may be best represented as a graph.
Containers, components and connections should be rendered in a simple 'boxes and lines' fashion.
It should be possible to give a container, component or connection a 'type', from a pre-defined library. For example, a specific type might be an 'EC2 Instance'. Each type has a schema, of required and optional fields. Each type should also have an associated 'renderer', which allows the type to be rendered in a specific way (for example, an EC2 instance might be rendered with the AWS EC2 logo, with the instance name at the lower side and the instance type at the upper right side).
It should be possible to associate arbitrary metadata with entities, to allow custom rendering if needed.
It should be possible to specify 'levels'. Levels would
Below are a few examples, highlighting a proposed format.
The code might look like this:
title: Three Tier Application
author: Dave Kerr
---
container:
id: channels
name: Channels
container:
id: services
name: Services
container:
id: databases
name: Databases
component:
name: Mobile App (iOS)
container: channels
component:
name: Mobile App (Android)
container: channels
component:
name: Website
container: channels
component:
id: api_gateway
name: API Gateway
component:
name: Application Server
container: services
component:
name: Application Database
container: databases
connection:
from: channels
via:
- api_gateway
to: services
connection:
from: services
to: databases
The resulting diagram should be rendered as something like this:
Some additional features would would be useful:
- Creating a structure from Terraform files
- Creating a model from an external structure, such as AWS
- Layering data from an APM such as New Relic in real-time
- WebGL rendering
Enforce policies. Go to AWS, highlight any resource which is not part of an architecture schema. Allows existing environments to be audited, e.g show me resources which don’t fit into my defined architecture. This is potentially quite a useful feature, allowing an existing cloud set up to be audited against a well defined architectural spec.
Current solutions which are similar in nature:
- Structurizr: Create diagrams based on Markdown or AsciiDoc
- Mingrammer: Very cool, uses Python as the syntax
- PlantUML: Old school visuals but supports flow diagrams etc
- Mermaid: Gantts, flowcharts, diagrams in a simple text format
- Parser: The parser will read the yaml file, validate it for syntax and structure, then produce an in-memory model. Suggested implementation is a Node.js module which can be run in-memory to validate the structure, and load into an in-memory representation, or as a cli to vlidate from the commandline. This will allow validation to happen as part of a CI/CD process, to allow Pull Requests on the structure to be validated.
- Visualiser: The visualiser will use the parser to build the in-memory model, which will be rendered to an HTML5 Canvas (or perhaps in 3D). Suggested implementation is a simple HTML SPA.
model.yaml --> validator --> warnings, errors, model.json
model.json --> inflator --> warnings, errors, model-inflated.json
model-inflated --> renderer --> visualisation
Other options:
- A CLI to render a PNG of the diagram, potentially highlighting a 'diff' for a pull request.
A model is simply the yaml
representation of an architecture. This representation is designed to be as easy as possible to be created and maintained by a human. Therefore, we want to provide a simple, declarative schema to define architectures.
However, to render a model, we must first build an intermediate structure, which is the 'compiled model'. The reason that the model must be compiled is that there are elements of a model which do not have to be defined in the yaml
, but which are still required to be rendered. This compiled model is built with the compile
function. The compiler will:
- Validate that the model is syntactically and semantically correct
- Assign an
id
to every entity which does not have one explicitly defined - Build a graph of entities, starting from an artificial
root
entity
The following features of the compiler are not yet completed:
- Link entities together, for ease of traversal (e.g. child entities have a
parent
reference, parent entities have achildren
reference. NOTE: This brings in a small challenge which is that the compiled model cannot be serialized as a POJO, due to potential circular references. One simple approach to deal with this would be to unlink the model by replacingparent
withparentId
,children
withchildrenIds
and so on. This would be straightforward to read in the serialized compiled model, and straightforward to re-link after loading the model.
Some ideas for the CLI interface:
aac validate # validate a model
aac render # render a model
aac import # create a model from an external structure
aac diff # compare models, or a model to an external structure
- Consider yargs rather than commander
- Consider inquirer for cli guidance
The key APIs are documented below.
Validates the structure of a model YAML, and returns the in-memory representation of the model:
const modelYaml = fs.readFileSync('model.yaml', 'utf8');
const { model, errors, warnings } = validate({ modelYaml });
console.log(`${errors.length} errors`);
console.log(`${warnings.length} warnings`);
console.log(`Model: ${JSON.stringify(model, null, 2)}`);
Common tasks can be run from the makefile
Command | Usage |
---|---|
make test |
Lint and test the code. |
make circle |
Install the CircleCI CLI and run the CircleCI build locally. |
To create a release:
cd aac-cli
npm run release && git push --follow-tags && npm publish
-
aac ./some/file.yaml
shows no output - it should demand a command -
meta
field on anything, just key value pairs. Use it as a 'dumping ground' (e.g.platform: as400
) when building models, until you find a structured location for it. When rendered, available as a tooltip. - styling should be handled with a separate css file, which can use classes, selectors etc. Regardless of whether the renderer is HTML, CSS provides a known framework for styling.
-
aac demo
- creates a three-tier architecture, renders and watches it, with an informative message. One command to show all the good stuff. -
validate
should give a unique id to all elements