Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
2a29fd1
feat(ref-arch): draft STACKIT AI reference architecture
grubmeshi Aug 11, 2026
4bd8787
feat(ref-arch): add pluggability seams and one-click delivery model
grubmeshi Aug 11, 2026
92591d4
refactor(ref-arch): drop the chat UI from the AI platform core
grubmeshi Aug 11, 2026
2fb6b21
docs(ref-arch): track folding the SKE Starterkit into the AI architec…
grubmeshi Aug 11, 2026
854d238
docs(ref-arch): record install delivery decision and the BBD platform…
grubmeshi Aug 11, 2026
5fd331e
refactor(ref-arch): generalise to a multi-cloud AI platform architecture
grubmeshi Aug 11, 2026
9190999
docs(ref-arch): decide per-tenant model serving token scope
grubmeshi Aug 11, 2026
452e552
docs(ref-arch): make the deployment vs tenancy model explicit
grubmeshi Aug 11, 2026
182a925
docs(ref-arch): resolve bootstrap ordering, request sensitive BB outputs
grubmeshi Aug 11, 2026
d1b1703
docs(ref-arch): compose AI platform as a reusable RA, consolidate clu…
grubmeshi Aug 11, 2026
329b0f0
docs(ref-arch): layer stackit-landingzone options, settle SKE exposure
grubmeshi Aug 11, 2026
4b76bd3
docs(ref-arch): add self-registering SKE cluster block as modules/sta…
grubmeshi Aug 11, 2026
a46b9bc
docs(ref-arch): simplify language across the AI platform branch
grubmeshi Aug 11, 2026
d7e8839
docs(ref-arch): rewrite the AI platform README as a published archite…
grubmeshi Aug 12, 2026
4a1cfd7
feat(kubernetes/ingress): consolidate cert-manager, HAProxy and the C…
grubmeshi Aug 12, 2026
2f3bc4f
feat(stackit/ske): add SKE cluster building block
grubmeshi Aug 12, 2026
06f77bd
feat(kubernetes/platform): add Kubernetes platform registration build…
grubmeshi Aug 12, 2026
b4cd016
feat(stackit/model-serving): finish the module as a publishable build…
grubmeshi Aug 12, 2026
43a7c6d
feat(ref-arch): make stackit-kubernetes orderable as one cluster buil…
grubmeshi Aug 12, 2026
4d47b15
feat: add STACKIT PostgreSQL Flex building block
grubmeshi Aug 12, 2026
1e2b26e
feat(stackit/storage-bucket): add endpoint output and multipart permi…
grubmeshi Aug 12, 2026
de2f47c
docs: add the ai platform directory
grubmeshi Aug 12, 2026
8b60e42
feat(ai): install the LiteLLM gateway with the litellm building block
grubmeshi Aug 12, 2026
08108c6
feat(ai/litellm-team): add the tenant-facing LiteLLM team building block
grubmeshi Aug 12, 2026
aa6db08
feat(ai/azure-openai): register an Azure OpenAI deployment as a LiteL…
grubmeshi Aug 12, 2026
53c23dc
fix(kubernetes/ingress)!: let the caller configure the kubernetes and…
grubmeshi Aug 12, 2026
34240c2
feat(kubernetes/ingress): size every workload for a demonstration clu…
grubmeshi Aug 12, 2026
1c38225
feat(stackit/dns): add STACKIT DNS zone building block
grubmeshi Aug 12, 2026
9eb69d2
feat(stackit/model-serving): add the backplane that mints Model Servi…
grubmeshi Aug 12, 2026
80f9bee
fix(kubernetes/ingress): pin the STACKIT webhook chart to a version t…
grubmeshi Aug 12, 2026
096812a
fix(stackit/postgresflex): grant postgres-flex.admin at folder scope
grubmeshi Aug 12, 2026
08c6ef6
feat(stackit/dns): add record sets to a zone the module does not own
grubmeshi Aug 12, 2026
58f16eb
fix(stackit/dns): grant dns.admin at folder scope
grubmeshi Aug 12, 2026
f505b9c
refactor(stackit-kubernetes): give each cluster a label in the shared…
grubmeshi Aug 12, 2026
7826de5
fix(kubernetes/ingress): separate the certificate domain from the sol…
grubmeshi Aug 13, 2026
67b6e27
docs(stackit-kubernetes): update the integration to the shared-zone d…
grubmeshi Aug 13, 2026
e545314
test(kubernetes/ingress): cover the DNS-01 certificate domain with un…
grubmeshi Aug 13, 2026
7217cd8
refactor(stackit/storage-bucket)!: move the resources into a provider…
grubmeshi Aug 13, 2026
122e5bb
refactor(ske-starterkit): register the e2e platform with the hub module
grubmeshi Aug 13, 2026
d8e4161
fix(stackit/storage-bucket): keep existing buckets across the submodu…
grubmeshi Aug 13, 2026
3bb9af7
feat(stackit/postgresflex): create a database inside an instance that…
grubmeshi Aug 13, 2026
34b4ea4
feat: add shared ClickHouse building block for the AI platform
grubmeshi Aug 13, 2026
593292d
feat: add per-tenant Langfuse building block for the AI platform
grubmeshi Aug 13, 2026
dcb6037
feat(stackit/ske): add the backplane that grants ske.admin at folder …
grubmeshi Aug 13, 2026
6e78243
docs(reference-architectures): redraw who orders the SKE cluster
grubmeshi Aug 13, 2026
8be68b8
feat(ai): pin the Prisma connection pool in both Postgres consumers
grubmeshi Aug 13, 2026
0dee5f4
refactor(stackit/postgresflex)!: split off a provider-free submodule
grubmeshi Aug 13, 2026
e03aa31
fix(ai/litellm-team): stop publishing the virtual key in cleartext
grubmeshi Aug 13, 2026
f3c4e76
feat(ai/litellm): add console single sign-on and protect the five seats
grubmeshi Aug 13, 2026
1994bcb
docs(ai/litellm): drop the untested recovery runbook
grubmeshi Aug 13, 2026
e88e86e
feat(ai/model-access): replace litellm-team with the one tenant-facin…
grubmeshi Aug 13, 2026
871905d
feat(ai/model-access): create the per-tenant Langfuse backends
grubmeshi Aug 13, 2026
1ee8fca
docs(ai/clickhouse): fix the ON CLUSTER placement in the GRANT statement
grubmeshi Aug 14, 2026
ca7ff1a
feat(website): give the AI modules a category of their own
grubmeshi Aug 14, 2026
ec38140
feat(stackit): grant service roles at project scope, not folder scope
grubmeshi Aug 14, 2026
e758c39
feat(ref-arch/stackit-kubernetes): create its own backplane, drop acm…
grubmeshi Aug 14, 2026
8c5bbd2
feat(ai/litellm): accept a client certificate as the cluster credential
grubmeshi Aug 14, 2026
f5ea426
feat(ref-arch/ai-platform): make the AI platform orderable
grubmeshi Aug 14, 2026
b27f978
refactor(stackit/dns)!: split off a provider-free zone submodule
grubmeshi Aug 14, 2026
5b6e27b
feat(ref-arch/stackit-landingzone): add the kubernetes and ai options
grubmeshi Aug 14, 2026
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
6 changes: 5 additions & 1 deletion .agents/references/diagrams.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ in one diagram to a box in another without reading the label.
| 🔌 | network, subnet |
| ☸️ | Kubernetes cluster |
| 🗄️ | container registry |
| 👤 | end user, application team acting as a user |
| 💬 | user-facing chat interface |
| 🚪 | gateway, abstraction layer |
| 📈 | observability, tracing, evaluation |
| ⚙️ | CI/CD wiring |
| 🔀 | git repository |
| 🧠 | model serving / AI API |
Expand All @@ -105,7 +109,7 @@ a concept already listed.

Copy the emoji straight from this table — several of them carry a **U+FE0F variation selector** that
is invisible in the source but decides whether the glyph renders in colour. `☸ ⚙ 🗄 🗂 🛰` default to
*text* presentation and come out as small monochrome symbols without it; `🏢 📁 🔑 🌐 🔌 🔀 🧠 🛬 📦`
*text* presentation and come out as small monochrome symbols without it; `🏢 📁 🔑 🌐 🔌 🔀 🧠 🛬 📦 👤 💬 🚪 📈`
are colour by default. If an icon renders monochrome in the SVG, it is missing the selector.

The **same emoji with a different fill** is deliberate and useful: a green `🔀` repo inside the
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,9 @@ getting-started steps, and shared responsibility matrix.
### Conventions

- Folder name: `<cloud>-<capability>` (e.g. `azure-kubernetes`, `stackit-kubernetes`), with the
architecture itself in `README.md`.
architecture itself in `README.md`. Use `<capability>` alone (e.g. `ai-platform`) when the
architecture is genuinely multi-cloud — it lists several `cloudProviders` and its components are
cloud-agnostic, with each provider contributing only a small provider-specific module.
- Logo: colocate as `logo.png` (or `logo.svg`) in the architecture folder, the same convention
`buildingblock/logo.png` uses. The website generator copies it to
`website/public/assets/reference-architecture-logos/<id>.png` — never add files there by hand,
Expand Down
10 changes: 10 additions & 0 deletions modules/ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
name: AI
description: meshStack integration with AI platforms puts a gateway in front of your model providers, so application teams call one OpenAI-compatible endpoint with a virtual key while the platform team keeps the provider credentials, the budgets and the spend records.
category: ai
benefits:
- Model Gateway
- Virtual Key Issuing
- Budget and Spend Tracking
official: true
---
78 changes: 78 additions & 0 deletions modules/ai/azure-openai/buildingblock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
name: Azure OpenAI Model Backend
supportedPlatforms:
- ai
description: Registers an Azure OpenAI deployment as a model backend on the LiteLLM gateway, scoped to the tenant's team.
# The module talks only to the LiteLLM admin API. It creates nothing in Azure and receives the
# Azure OpenAI endpoint, key and deployment name as static inputs, so there is no cloud-side setup
# to perform ahead of time.
requiresBackplane: false
---

# Azure OpenAI Model Backend Building Block

This building block registers an existing Azure OpenAI deployment as a model entry on the LiteLLM
gateway. It is the Azure entry in the model layer of the AI platform architecture, next to
`stackit/model-serving` for STACKIT, and it is what makes the architecture's claim of two cloud
providers true.

The module is thin on purpose. It creates one `litellm_model` resource and no Azure resources at
all, which is why it declares `requiresBackplane: false` and needs no Azure identity. The platform
team creates the Azure OpenAI resource and its deployment once, outside this module, and passes the
endpoint, the key and the deployment name in as static inputs.

The `team_id` input scopes the entry to one LiteLLM team, so the gateway offers the model to that
tenant alone and attributes its spend to that team. In the AI landing zone the team ID comes from
the platform tenant ID that `ai/model-access` produces, so the application team fills in nothing.

Azure OpenAI addresses a **deployment**, not a model. The provider joins `custom_llm_provider` and
`base_model` into `azure/<base_model>`, so `azure_deployment_name` must carry the Azure deployment
name rather than the underlying model name.

Application teams keep calling the LiteLLM gateway. The Azure credential stays on the gateway, so
the platform team can rotate it without a change on the application side.

The `ncecere/litellm` provider is pinned to exactly `2.0.1`. This is a deliberate exception to the
hub rule that provider constraints use `>=`, and `versions.tf` explains why.

<!-- BEGIN_TF_DOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.12.0 |
| <a name="requirement_litellm"></a> [litellm](#requirement\_litellm) | = 2.0.1 |

## Modules

No modules.

## Resources

| Name | Type |
|------|------|
| [litellm_model.this](https://registry.terraform.io/providers/ncecere/litellm/2.0.1/docs/resources/model) | resource |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_azure_deployment_name"></a> [azure\_deployment\_name](#input\_azure\_deployment\_name) | Name of the model deployment in the Azure OpenAI resource, for example 'gpt-4o'. Azure routes on the deployment name, not on the model name. | `string` | n/a | yes |
| <a name="input_azure_openai_api_key"></a> [azure\_openai\_api\_key](#input\_azure\_openai\_api\_key) | Key of the Azure OpenAI resource. LiteLLM stores it and sends it upstream in the 'api-key' header. | `string` | n/a | yes |
| <a name="input_azure_openai_api_version"></a> [azure\_openai\_api\_version](#input\_azure\_openai\_api\_version) | Azure OpenAI data plane API version. '2024-10-21' is the latest dated GA version of the inference API. | `string` | `"2024-10-21"` | no |
| <a name="input_azure_openai_endpoint"></a> [azure\_openai\_endpoint](#input\_azure\_openai\_endpoint) | Endpoint of the Azure OpenAI resource, for example 'https://my-aoai.openai.azure.com'. LiteLLM calls the deployment under this host. | `string` | n/a | yes |
| <a name="input_litellm_api_base"></a> [litellm\_api\_base](#input\_litellm\_api\_base) | Base URL of the LiteLLM gateway, for example 'https://litellm.example.com'. The provider talks to the admin API under this URL. | `string` | n/a | yes |
| <a name="input_litellm_api_key"></a> [litellm\_api\_key](#input\_litellm\_api\_key) | LiteLLM admin key the provider authenticates with. It needs permission to register models. | `string` | n/a | yes |
| <a name="input_mode"></a> [mode](#input\_mode) | What the deployment is used for. LiteLLM accepts 'chat', 'completion', 'embedding', 'audio\_speech', 'audio\_transcription', 'image\_generation', 'video\_generation', 'batch' and 'rerank'. | `string` | `"chat"` | no |
| <a name="input_model_name"></a> [model\_name](#input\_model\_name) | Name the model is offered under on the LiteLLM gateway. Application teams pass this name in the 'model' field of their requests. | `string` | n/a | yes |
| <a name="input_team_id"></a> [team\_id](#input\_team\_id) | ID of the LiteLLM team the model is registered for. Only that team can call the model. Leave unset to register the model for the whole gateway. | `string` | `null` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_api_base"></a> [api\_base](#output\_api\_base) | OpenAI-compatible base URL of the LiteLLM gateway, including the '/v1' suffix. Calls to this model go here, not to the Azure endpoint. |
| <a name="output_model_id"></a> [model\_id](#output\_model\_id) | ID LiteLLM gave the model entry. |
| <a name="output_model_name"></a> [model\_name](#output\_model\_name) | Name to pass in the 'model' field of a request to the gateway. |
| <a name="output_summary"></a> [summary](#output\_summary) | Summary with the model name and the endpoint to call it on. |
<!-- END_TF_DOCS -->
27 changes: 27 additions & 0 deletions modules/ai/azure-openai/buildingblock/SUMMARY.md.tftpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Azure OpenAI Model: **${model_name}**

## Details

| Property | Value |
|----------|-------|
| **API Base URL** | `${api_base}` |
| **Model name** | `${model_name}` |
| **Azure deployment** | `${deployment_name}` |
| **Azure endpoint** | `${azure_endpoint}` |
| **Azure API version** | `${azure_api_version}` |
| **Model ID** | `${model_id}` |

## Calling the model

Call the LiteLLM gateway, not the Azure endpoint. Send your own virtual key as a bearer token and
pass the model name in the `model` field:

```sh
curl "${api_base}/chat/completions" \
-H "Authorization: Bearer $VIRTUAL_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "${model_name}", "messages": [{"role": "user", "content": "Hello"}]}'
```

The Azure credential stays on the gateway. Your application never sees it and never talks to Azure
directly, so the platform team can rotate the credential without a change on your side.
Binary file added modules/ai/azure-openai/buildingblock/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions modules/ai/azure-openai/buildingblock/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
locals {
# LiteLLM's OpenAI-compatible routes live under '/v1'. The gateway also answers without the
# prefix, but OpenAI client libraries expect it, so the output carries it.
api_base = "${trimsuffix(var.litellm_api_base, "/")}/v1"
}

resource "litellm_model" "this" {
model_name = var.model_name
custom_llm_provider = "azure"

# The provider joins these two into '<custom_llm_provider>/<base_model>' and sends the result to
# LiteLLM. Azure OpenAI addresses a deployment, not a model, so base_model carries the Azure
# deployment name and the request goes to 'azure/<deployment name>'.
base_model = var.azure_deployment_name

model_api_base = var.azure_openai_endpoint
model_api_key = var.azure_openai_api_key
api_version = var.azure_openai_api_version
mode = var.mode

# When a team ID is given, LiteLLM offers the model to that team alone. The gateway keeps one
# entry per tenant, and the spend of each entry is attributed to its team.
team_id = var.team_id
}
26 changes: 26 additions & 0 deletions modules/ai/azure-openai/buildingblock/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
output "model_name" {
value = litellm_model.this.model_name
description = "Name to pass in the 'model' field of a request to the gateway."
}

output "model_id" {
value = litellm_model.this.id
description = "ID LiteLLM gave the model entry."
}

output "api_base" {
value = local.api_base
description = "OpenAI-compatible base URL of the LiteLLM gateway, including the '/v1' suffix. Calls to this model go here, not to the Azure endpoint."
}

output "summary" {
description = "Summary with the model name and the endpoint to call it on."
value = templatefile("${path.module}/SUMMARY.md.tftpl", {
model_name = litellm_model.this.model_name
model_id = litellm_model.this.id
api_base = local.api_base
deployment_name = var.azure_deployment_name
azure_endpoint = var.azure_openai_endpoint
azure_api_version = var.azure_openai_api_version
})
}
4 changes: 4 additions & 0 deletions modules/ai/azure-openai/buildingblock/provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
provider "litellm" {
api_base = var.litellm_api_base
api_key = var.litellm_api_key
}
49 changes: 49 additions & 0 deletions modules/ai/azure-openai/buildingblock/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
variable "litellm_api_base" {
type = string
description = "Base URL of the LiteLLM gateway, for example 'https://litellm.example.com'. The provider talks to the admin API under this URL."
}

variable "litellm_api_key" {
type = string
sensitive = true
description = "LiteLLM admin key the provider authenticates with. It needs permission to register models."
}

variable "azure_openai_endpoint" {
type = string
description = "Endpoint of the Azure OpenAI resource, for example 'https://my-aoai.openai.azure.com'. LiteLLM calls the deployment under this host."
}

variable "azure_openai_api_key" {
type = string
sensitive = true
description = "Key of the Azure OpenAI resource. LiteLLM stores it and sends it upstream in the 'api-key' header."
}

variable "azure_openai_api_version" {
type = string
default = "2024-10-21"
description = "Azure OpenAI data plane API version. '2024-10-21' is the latest dated GA version of the inference API."
}

variable "azure_deployment_name" {
type = string
description = "Name of the model deployment in the Azure OpenAI resource, for example 'gpt-4o'. Azure routes on the deployment name, not on the model name."
}

variable "model_name" {
type = string
description = "Name the model is offered under on the LiteLLM gateway. Application teams pass this name in the 'model' field of their requests."
}

variable "mode" {
type = string
default = "chat"
description = "What the deployment is used for. LiteLLM accepts 'chat', 'completion', 'embedding', 'audio_speech', 'audio_transcription', 'image_generation', 'video_generation', 'batch' and 'rerank'."
}

variable "team_id" {
type = string
default = null
description = "ID of the LiteLLM team the model is registered for. Only that team can call the model. Leave unset to register the model for the whole gateway."
}
15 changes: 15 additions & 0 deletions modules/ai/azure-openai/buildingblock/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
terraform {
required_version = ">= 1.12.0"

required_providers {
litellm = {
source = "ncecere/litellm"
# Exact pin, a deliberate exception to the hub rule that provider constraints use '>='.
# ncecere/litellm is a community provider with a single maintainer, and it has changed
# resource behaviour inside a minor release before: v1.2.0 replaced the id of litellm_key
# with a hash of the key. The same pin is used by modules/ai/model-access, so both modules
# move to a new provider release together, after a review of the changelog.
version = "= 2.0.1"
}
}
}
Loading