Skip to content
Open
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ as a stable production hosting.
- [Amazon Web Services / EKS](docs/vendor-eks.md)
- [UpCloud Kubernetes Service / UKS](docs/vendor-uks.md)

### Architecture model (C4)

A [C4 model](c4/) of the platform, written in Structurizr DSL. It covers the
delivery toolchain, the shared cluster services, the containers that make up a
single project environment, and a deployment view for each of the four vendors
listed above.

This is the base model — no client-specific detail. Client workspaces
[extend it](c4/extensions/README.md) instead of forking it, so they inherit
changes made here.

Browse it locally. Docker is the only prerequisite:

```bash
cd c4 && docker compose up
```

Then open <http://localhost:8080>. See [c4/README.md](c4/README.md) for the
full layout and for exporting the diagrams to Mermaid or PlantUML.

## How it works in practice

All infrastructure configuration is based on Git, a deployment is triggered automatically when pushing code to Github.
Expand Down
9 changes: 9 additions & 0 deletions c4/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Structurizr runtime files.
# workspace.json holds hand-tuned diagram layouts — if you drop `autolayout`
# from a view and arrange it manually, un-ignore this file and commit it.
workspace.json
.structurizr/

# Generated diagram exports
export/
.export-check/
112 changes: 112 additions & 0 deletions c4/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Silta — C4 architecture model

A base-level C4 model of the Silta platform, written in
[Structurizr DSL](https://docs.structurizr.com/dsl). One model, thirteen
views, no client-specific detail — client workspaces extend this one rather
than forking it.

## Run it locally

Docker is the only prerequisite — nothing is installed on your machine.

```bash
cd c4 && LOCAL_UID=$(id -u) LOCAL_GID=$(id -g) docker compose up
```

Then open <http://localhost:8080>. You land on the workspace: diagrams on one
tab, the prose from `docs/` on another.

Structurizr re-reads `workspace.dsl` on every page load, so the loop is edit a
`.dsl` file, refresh the browser. No restart, no rebuild.

`Ctrl-C` to stop, or `docker compose down` from another terminal.

### If you prefer plain docker

```bash
docker run -it --rm -p 8080:8080 \
--user "$(id -u):$(id -g)" \
-v "$PWD:/usr/local/structurizr" \
structurizr/structurizr local
```

### Two things that will otherwise catch you out

**Use `structurizr/structurizr local`, not `structurizr/lite`.** As of 2026 the
`lite` image is a stub that prints a migration notice and exits 0. The
container disappears before it binds the port, so the symptom is a connection
refused on 8080 that looks like a networking problem and is not one.

**The `--user` mapping matters.** Running writes `workspace.json` and
`.structurizr/` into this folder. Without it they are created as root and you
will need sudo to delete them. Both are gitignored; `workspace.json` is where
hand-tuned diagram layouts live, so if you ever drop `autolayout` from a view
and arrange it by hand, un-ignore it and commit it.

## Export it

```bash
./export.sh # Mermaid, into export/
./export.sh plantuml # or plantuml, c4plantuml, dot, json, ...
```

## Check it

```bash
docker run --rm -v "$PWD:/workspace" -w /workspace structurizr/structurizr validate --workspace workspace.dsl
```

## What is in here

| Path | |
| --- | --- |
| `workspace.dsl` | Entry point. Includes everything else. |
| `model/people.dsl` | The six audiences the model is drawn for. |
| `model/external-systems.dsl` | Everything Silta depends on but does not operate. |
| `model/toolchain.dsl` | The CI-side of Silta: orb, CLI, charts, images, docs. |
| `model/platform.dsl` | Shared cluster services from the `silta-cluster` chart. |
| `model/project-environment.dsl` | The containers inside one Helm release. |
| `model/dashboard.dsl` | The Silta Dashboard. |
| `model/relationships.dsl` | Every relationship, in one readable file. |
| `deployment/*.dsl` | One deployment environment per cloud: GKE, EKS, AKS, UKS. |
| `views/views.dsl` | View definitions. |
| `views/styles.dsl` | Element and relationship styling. |
| `docs/` | Prose rendered next to the diagrams by Structurizr. |
| `extensions/` | How to build a client workspace on top of this one. |

## Views

**Landscape** — everything and everyone, one page.

**System context** (4) — the cluster platform, a project environment, the
delivery toolchain, the dashboard.

**Container** (4) — the same four, opened up.

**Deployment** (4) — GKE, EKS, AKS and UKS. These are the point of the
baseline: same model, four different infrastructure realities.

Component-level (L3) and dynamic views are not in the baseline yet. The
deployment pipeline and the HTTP request path are the two obvious candidates —
both are currently only documented as PNGs on the docs site.

## Conventions

- Grey elements are outside Silta's control.
- Dashed borders mean optional — enabled per cluster or per project.
- Identifiers are flat, so every element in `model/` is directly addressable
from a client workspace.

## Sources

The model was derived from the sibling Silta repositories — `charts`,
`silta-circleci`, `silta-cli`, `silta-dashboard`, `silta-images`,
`wunder-silta-cluster`, `fastly-terraform` — and from the documentation in
`../docs`, published at <https://wunderio.github.io/silta>.

It is hand-written, not generated. When those repositories change, this model
has to be changed with them.

It deliberately sits outside `../docs`: that directory is the Docusaurus
source, where every `.md` file becomes a published page. A Structurizr
workspace is not a set of pages, so it lives here instead.
18 changes: 18 additions & 0 deletions c4/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Browse the model at http://localhost:8080
#
# LOCAL_UID=$(id -u) LOCAL_GID=$(id -g) docker compose up
#
# Structurizr re-reads workspace.dsl on every page load, so edit a .dsl file
# and refresh. It writes workspace.json and .structurizr/ next to this file;
# both are gitignored — see the note in README.md if you want to keep
# hand-tuned diagram layouts.
services:
structurizr:
image: structurizr/structurizr
command: local
ports:
- "8080:8080"
volumes:
- .:/usr/local/structurizr
# Keeps generated files owned by you rather than root.
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
97 changes: 97 additions & 0 deletions c4/deployment/aks.dsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# ---------------------------------------------------------------------------
# Deployment: Azure AKS
#
# Differences from the GKE reference that matter when reading this diagram:
# * two ingress options — keep Traefik behind an Azure Load Balancer for the
# built-in cluster domain, and optionally route exposed customer domains
# through an existing Application Gateway via AGIC;
# * silta-shared can be Azure Blob through csi-rclone, or the azurefile-csi
# driver for faster I/O and instant cross-pod consistency (case-insensitive
# filenames, and the storage request is enforced);
# * kubenet + Calico is required for NetworkPolicy support, and cannot be
# changed after the cluster is created.
# ---------------------------------------------------------------------------

deploymentEnvironment "AKS" {

deploymentNode "GitHub" "Source of truth and identity provider" "SaaS" {
softwareSystemInstance github
}

deploymentNode "CircleCI Cloud" "Runs the pipeline for every push" "SaaS" {
deploymentNode "Job executor" "Remote Docker executor" "silta-cicd container" {
aksCli = containerInstance cli
containerInstance orb
containerInstance builderImage
}
}

deploymentNode "Edge" "Public entry point for production domains" "Azure Front Door / CDN" {
aksCdn = softwareSystemInstance cdn
}

deploymentNode "Microsoft Azure" "One subscription and resource group per cluster environment" "Azure" {

aksDns = infrastructureNode "Azure DNS" "Zones for the cluster domain and customer domains." "Azure DNS"

aksLb = infrastructureNode "Azure Load Balancer" "Standard L4 load balancer with a static public IP, fronting the Traefik ingress service for the built-in cluster domain." "Azure Load Balancer"

aksAppGw = infrastructureNode "Application Gateway" "Optional L7 entry point for exposed customer domains, driven by the AGIC add-on watching in-cluster Ingress resources. Requires VNet peering, a route table association, and the gateway subnet allow-listed in nginx realipfrom, noauthips and the release NetworkPolicy." "Application Gateway + AGIC" "Optional"

aksAcr = infrastructureNode "Azure Container Registry" "Project images. CI authenticates with a service principal held in a CircleCI Context." "ACR"

aksBlob = infrastructureNode "Azure Blob Storage" "Default backend for the silta-shared storage class via csi-rclone." "Blob Storage"

aksFiles = infrastructureNode "Azure Files" "Alternative RWX backend via the azurefile-csi driver. Chosen for projects with many files; cannot be switched on an existing deployment." "Azure Files" "Optional"

aksDisk = infrastructureNode "Azure Disk" "Default block storage class for database and search volumes." "Azure Disk CSI"

deploymentNode "AKS cluster" "kubenet networking with Calico network policy" "Kubernetes" {

aksApi = infrastructureNode "Kubernetes API server" "Managed control plane. CI authenticates with a service principal (tenant, app id, password) from a CircleCI Context." "AKS control plane"

deploymentNode "silta-cluster namespace" "Installed once per cluster from the silta-cluster chart" "Kubernetes namespace" {
aksIngress = containerInstance ingressController
containerInstance certManager
aksCsi = containerInstance csiRclone
containerInstance downscaler
containerInstance deploymentRemover
aksJump = containerInstance sshJumpServer
containerInstance sshKeyServer
containerInstance controllerSidecars
containerInstance hubAgent
}

deploymentNode "Project namespace" "One namespace per repository" "Kubernetes namespace" {
deploymentNode "Helm release" "One release per git branch, deployed with cluster.type=aks" "Helm 3" {
containerInstance varnish
aksNginx = containerInstance webserver
containerInstance waf
aksApp = containerInstance appRuntime
containerInstance shell
aksDb = containerInstance database
containerInstance searchEngine
containerInstance cacheStore
containerInstance mailTrap
aksJobs = containerInstance scheduledJobs
aksShared = containerInstance sharedFiles
containerInstance releasePolicy
}
}
}
}

aksCdn -> aksLb "Forwards cache misses to the origin" "HTTPS"
aksDns -> aksLb "Resolves built-in cluster hostnames to" "DNS"
aksDns -> aksAppGw "Resolves exposed customer domains to, when the gateway is used" "DNS"
aksLb -> aksIngress "Load balances inbound HTTP/HTTPS to" "TCP 80/443"
aksAppGw -> aksNginx "Routes exposed-domain traffic straight to the release, bypassing Traefik" "HTTP"
aksCli -> aksApi "Applies the Helm release through" "HTTPS"
aksCli -> aksAcr "Pushes project images to" "HTTPS"
aksAcr -> aksApp "Is pulled by the kubelet to start" "HTTPS"
aksCsi -> aksBlob "Mounts containers as RWX volumes from" "rclone over HTTPS"
aksFiles -> aksShared "Backs, when azurefile-csi is chosen over csi-rclone" "SMB"
aksDisk -> aksDb "Provides block storage to" "CSI"
aksJobs -> aksBlob "Writes nightly backups and reference data to" "rclone over HTTPS"
aksJump -> aksLb "Is published through a separate frontend of" "TCP 22"
}
93 changes: 93 additions & 0 deletions c4/deployment/eks.dsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# ---------------------------------------------------------------------------
# Deployment: Amazon EKS
#
# Differences from the GKE reference that matter when reading this diagram:
# * ingress-nginx replaces Traefik, and the ingress ELB speaks the PROXY
# protocol so the real client IP survives;
# * csi-rclone points at an S3 bucket with a dedicated IAM user;
# * EBS gp2 is the default block storage class, EFS the optional RWX one;
# * the SSH jump server needs one Elastic IP per subnet on its NLB;
# * there is no NLB path for HTTP/HTTPS ingress yet — this is a known gap.
# ---------------------------------------------------------------------------

deploymentEnvironment "EKS" {

deploymentNode "GitHub" "Source of truth and identity provider" "SaaS" {
softwareSystemInstance github
}

deploymentNode "CircleCI Cloud" "Runs the pipeline for every push" "SaaS" {
deploymentNode "Job executor" "Remote Docker executor" "silta-cicd container" {
eksCli = containerInstance cli
containerInstance orb
containerInstance builderImage
}
}

deploymentNode "Edge" "Public entry point for production domains" "Amazon CloudFront" {
eksCdn = softwareSystemInstance cdn
}

deploymentNode "Amazon Web Services" "One AWS account per cluster environment" "AWS" {

eksDns = infrastructureNode "Route 53" "Hosted zones for the cluster domain and customer domains." "Route 53"

eksLb = infrastructureNode "Elastic Load Balancer" "Fronts the ingress-nginx controller. PROXY protocol is enabled on both the service annotation and the controller config so client IPs reach the pods." "ELB"

eksSshLb = infrastructureNode "Network Load Balancer" "TCP passthrough for the SSH jump server, with source-IP stickiness, client-IP preservation and one Elastic IP allocation per subnet." "NLB"

eksEcr = infrastructureNode "Elastic Container Registry" "Project images. CI authenticates with 'aws ecr get-login-password'; nodes pull via an instance role." "ECR"

eksS3 = infrastructureNode "S3" "Bucket behind the silta-shared storage class, accessed by a dedicated IAM user with a least-privilege bucket policy." "S3"

eksEbs = infrastructureNode "EBS (gp2)" "Default block storage class for database and search volumes, provisioned by the Amazon EBS CSI driver add-on." "EBS"

eksEfs = infrastructureNode "EFS" "Managed NFS, used where the nfs-subdir provisioner is preferred over csi-rclone." "EFS" "Optional"

deploymentNode "EKS cluster" "Amazon VPC CNI for NetworkPolicy, EBS CSI driver add-on, IAM role attached to the worker nodes" "Kubernetes" {

eksApi = infrastructureNode "Kubernetes API server" "Managed control plane. CI authenticates with AWS credentials from a CircleCI Context." "EKS control plane"

deploymentNode "silta-cluster namespace" "Installed once per cluster from the silta-cluster chart" "Kubernetes namespace" {
eksIngress = containerInstance ingressController
containerInstance certManager
eksCsi = containerInstance csiRclone
containerInstance downscaler
containerInstance deploymentRemover
eksJump = containerInstance sshJumpServer
containerInstance sshKeyServer
containerInstance controllerSidecars
containerInstance hubAgent
}

deploymentNode "Project namespace" "One namespace per repository" "Kubernetes namespace" {
deploymentNode "Helm release" "One release per git branch, deployed with cluster.type=aws" "Helm 3" {
containerInstance varnish
eksNginx = containerInstance webserver
containerInstance waf
eksApp = containerInstance appRuntime
containerInstance shell
eksDb = containerInstance database
containerInstance searchEngine
containerInstance cacheStore
containerInstance mailTrap
eksJobs = containerInstance scheduledJobs
eksFiles = containerInstance sharedFiles
containerInstance releasePolicy
}
}
}
}

eksCdn -> eksLb "Forwards cache misses to the origin" "HTTPS"
eksDns -> eksLb "Resolves cluster and project hostnames to" "DNS"
eksLb -> eksIngress "Load balances inbound HTTP/HTTPS to" "TCP + PROXY protocol"
eksSshLb -> eksJump "Publishes" "TCP 22"
eksCli -> eksApi "Applies the Helm release through" "HTTPS"
eksCli -> eksEcr "Pushes project images to" "HTTPS"
eksEcr -> eksApp "Is pulled by the kubelet to start" "HTTPS"
eksCsi -> eksS3 "Mounts the bucket as RWX volumes from" "rclone over HTTPS"
eksEbs -> eksDb "Provides block storage to" "CSI"
eksEfs -> eksFiles "Backs, where NFS is preferred" "NFS"
eksJobs -> eksS3 "Writes nightly backups and reference data to" "rclone over HTTPS"
}
Loading
Loading