the existing infrastructure

This commit is contained in:
Platform team
2026-11-02 09:55:39 +00:00
commit b2c2460a52
21 changed files with 537 additions and 0 deletions
@@ -0,0 +1,10 @@
owner: carol
description: Second CI build agent
template: ubuntu-24.04
cpu: 4
memory_gb: 8
disks:
- size_gb: 100
network:
vlan: 110
backup: false
+12
View File
@@ -0,0 +1,12 @@
# A new VM in dev: the file lists its keys in the schema's order, with the requester as its owner.
kind: create
params: { env: dev, name: build-agent-02 }
owner: carol
values:
description: Second CI build agent
template: ubuntu-24.04
cpu: 4
memory_gb: 8
disks: [{ size_gb: 100 }]
network: { vlan: 110 }
backup: false
@@ -0,0 +1,17 @@
# Front-end for the shop. Pinned to VLAN 120 by the firewall team.
owner: bob
description: Shop front-end
template: ubuntu-24.04
cpu: 8 # bumped in INC-4411
memory_gb: 32
disks:
- size_gb: 80
tier: ssd
network:
vlan: 120
ip: 10.0.12.11
backup: true
backup_policy: daily
tags:
cost_center: "4711"
app: shop
+5
View File
@@ -0,0 +1,5 @@
# The resize action on web01: only cpu and memory_gb change; comments and layout stay.
kind: action
key: prod/web01
action: resize
actionInputs: { cpu: 8, memory_gb: 32 }
+65
View File
@@ -0,0 +1,65 @@
apiVersion: quai.dev/v1beta1
kind: ResourceType
metadata:
name: vm
title: Virtual machine (Terraform)
description: One YAML file per VM under envs/<env>/vms/. Each environment is a Terraform stack that turns those files into module instances.
icon: server
category: Compute
version: 1.0.0
tags: [terraform, brownfield]
spec:
locate:
strategy: file
path: envs/{env}/vms/{name}.yaml
params:
- name: env
title: Environment
type: enum
discover: true # offer the environments that exist in the repository
- name: name
title: VM name
description: Becomes the file name and the Terraform for_each key.
pattern: "[a-z][a-z0-9-]{1,30}"
# The contract CI validates every VM file against; the form is generated from it.
schema: catalog/schemas/vm.schema.json
generate: { kind: identity }
ownership: { field: owner }
stack: envs/{env}
address: module.vm["{name}"]
deletion: approval
ui:
order: [description, template, cpu, memory_gb, disks, network, backup, backup_policy, tags]
fields:
template: { immutable: true }
network.vlan: { immutable: true }
backup_policy: { visibleIf: "has(inputs.backup) && inputs.backup" }
approval:
rules:
- name: cost
when: params.env == 'prod' && plan.cost.monthlyDelta > 500
approvers: [group:finops]
- name: production
when: params.env == 'prod'
approvers: [group:platform-admins]
- name: large-vm
# Only when a VM becomes large, not on every change to an already large one.
when: inputs.cpu > 8 && !(has(previous.cpu) && previous.cpu > 8)
approvers: [group:finops]
actions:
- name: resize
title: Resize
icon: maximize
inputs:
- { name: cpu, title: vCPUs, type: integer, min: 1, max: 32, required: true }
- { name: memory_gb, title: Memory (GB), type: integer, min: 1, max: 256, required: true }
set:
cpu: ${{ action.cpu }}
memory_gb: ${{ action.memory_gb }}
- name: extend-disk
title: Extend system disk
icon: hard-drive
inputs:
- { name: size_gb, title: Size (GB), type: integer, min: 10, max: 4096, required: true }
set:
disks.0.size_gb: ${{ action.size_gb }}
+21
View File
@@ -0,0 +1,21 @@
# Example repository
An existing Terraform repository with Quai definitions added: the demo (the
compose stack seeds it into Gitea) and the fixture of the backend's tests and the
end-to-end scenarios.
- `envs/<env>/vms/*.yaml`: one file per VM. Each environment is a Terraform
stack (`envs/<env>/main.tf`) that reads them with `fileset()` and
`yamldecode()` and instantiates `modules/vm` once per file.
- `catalog/schemas/vm.schema.json`: the contract between the VM files and the
module. CI validates every file against it; Quai generates its form from it.
- `.quai/types/vm.yaml`: what Quai needs to know about the layout: where VM
files live, which schema they follow, who owns them, which changes need
approval, and the day-2 actions.
- `blueprints/`: catalog items built from the `vm` type: a single VM sized from
t-shirt sizes, and a web server with its database server.
- `pipelines/`: Azure Pipelines that plan `envs/prod` on pull requests (as build
validation) and apply it on main, each reporting to Quai with the `quai report`
step template of `deploy/runners/azure-pipelines`.
Quai edits the YAML files and nothing else.
+68
View File
@@ -0,0 +1,68 @@
apiVersion: quai.dev/v1beta1
kind: Blueprint
metadata:
name: app-servers
title: Application servers
description: A web server and its database server in one environment, requested and approved together. Built on the designer canvas.
icon: layers
category: Applications
version: 1.0.0
owners: [team:platform]
tags: [terraform, vm]
spec:
inputs:
type: object
required: [environment, app]
additionalProperties: false
properties:
environment: { type: string, title: Environment, enum: [dev, test, prod], default: dev }
app: { type: string, title: Application, pattern: "^[a-z][a-z0-9]{1,20}$" }
webCpu: { type: integer, title: Web server vCPUs, minimum: 1, maximum: 16, default: 2 }
withDatabase: { type: boolean, title: Add a database server, default: true }
dbDiskGb: { type: integer, title: Database data disk (GB), minimum: 50, maximum: 4096, default: 200 }
ui:
order: [environment, app, webCpu, withDatabase, dbDiskGb]
fields:
dbDiskGb: { visibleIf: "inputs.withDatabase" }
resources:
- id: web
type: { name: vm }
params:
env: ${{ inputs.environment }}
name: ${{ inputs.app + '-web01' }}
values:
template: ubuntu-24.04
cpu: ${{ inputs.webCpu }}
memory_gb: ${{ inputs.webCpu * 2 }}
disks: [{ size_gb: 50 }]
network: { vlan: 110 }
dependsOn: [db]
- id: db
when: inputs.withDatabase
type: { name: vm }
params:
env: ${{ inputs.environment }}
name: ${{ inputs.app + '-db01' }}
values:
template: rhel-9
cpu: 4
memory_gb: 16
disks:
- { size_gb: 100, tier: ssd }
- { size_gb: "${{ inputs.dbDiskGb }}", tier: ssd }
network: { vlan: 130 }
backup: true
backup_policy: "${{ inputs.environment == 'prod' ? 'hourly' : 'daily' }}"
approval:
rules:
- name: production
when: inputs.environment == 'prod'
approvers: [group:platform-admins]
actions:
- name: scale-web
title: Scale the web server
icon: maximize
inputs:
- { name: cpu, title: vCPUs, type: integer, min: 1, max: 16, required: true }
apply:
web: { action: resize, inputs: { cpu: "${{ action.cpu }}", memory_gb: "${{ action.cpu * 2 }}" } }
+66
View File
@@ -0,0 +1,66 @@
apiVersion: quai.dev/v1beta1
kind: Blueprint
metadata:
name: linux-vm
title: Linux virtual machine
description: A VM in the Terraform repository, sized from t-shirt sizes. The vRA classic, now a pull request.
icon: server
category: Compute
version: 2.0.0
owners: [team:platform]
tags: [terraform, vm]
spec:
inputs:
type: object
required: [environment, name, os, size]
additionalProperties: false
properties:
environment: { type: string, title: Environment, enum: [dev, test, prod], default: dev }
name: { type: string, title: VM name, pattern: "^[a-z][a-z0-9-]{1,30}$" }
os:
type: string
title: Operating system
enum: [ubuntu-24.04, rhel-9]
default: ubuntu-24.04
size:
type: string
title: Size
description: small = 2 vCPU / 4 GB, medium = 4 / 16, large = 16 / 64
enum: [small, medium, large]
default: small
diskGb: { type: integer, title: System disk (GB), minimum: 20, maximum: 2048, default: 50 }
vlan: { type: integer, title: VLAN, enum: [110, 120, 130], default: 110 }
ui:
order: [environment, name, os, size, diskGb, vlan]
fields:
os: { immutable: true }
resources:
- id: vm
type: { name: vm } # a type in this repository
params:
env: ${{ inputs.environment }}
name: ${{ inputs.name }}
values:
template: ${{ inputs.os }}
cpu: "${{ inputs.size == 'small' ? 2 : inputs.size == 'medium' ? 4 : 16 }}"
memory_gb: "${{ inputs.size == 'small' ? 4 : inputs.size == 'medium' ? 16 : 64 }}"
disks: [{ size_gb: "${{ inputs.diskGb }}", tier: standard }]
network: { vlan: "${{ inputs.vlan }}" }
backup: ${{ inputs.environment == 'prod' }}
approval:
rules:
- name: large-vm-cost
when: inputs.size == 'large'
approvers: [group:finops]
actions:
- name: resize
title: Resize
icon: maximize
description: Change the VM size. The VM restarts to apply it.
inputs:
- { name: size, title: New size, type: enum, options: [small, medium, large], required: true }
apply:
vm:
values:
cpu: "${{ action.size == 'small' ? 2 : action.size == 'medium' ? 4 : 16 }}"
memory_gb: "${{ action.size == 'small' ? 4 : action.size == 'medium' ? 16 : 64 }}"
+57
View File
@@ -0,0 +1,57 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/vm.schema.json",
"title": "Virtual machine",
"description": "Contract between the VM files in envs/*/vms/ and modules/vm. CI validates every file against it; Quai renders its form from it.",
"type": "object",
"required": ["template", "cpu", "memory_gb", "disks", "network"],
"additionalProperties": false,
"properties": {
"owner": { "type": "string", "title": "Owner" },
"description": { "type": "string", "title": "Description", "maxLength": 200 },
"template": {
"type": "string",
"title": "OS template",
"enum": ["ubuntu-24.04", "rhel-9", "windows-2022"],
"default": "ubuntu-24.04"
},
"cpu": { "type": "integer", "title": "vCPUs", "minimum": 1, "maximum": 32, "default": 2 },
"memory_gb": { "type": "integer", "title": "Memory (GB)", "minimum": 1, "maximum": 256, "default": 4 },
"disks": {
"type": "array",
"title": "Disks",
"minItems": 1,
"maxItems": 8,
"items": { "$ref": "#/$defs/disk" },
"default": [{ "size_gb": 50, "tier": "standard" }]
},
"network": {
"type": "object",
"title": "Network",
"required": ["vlan"],
"additionalProperties": false,
"properties": {
"vlan": { "type": "integer", "title": "VLAN", "enum": [110, 120, 130], "default": 110 },
"ip": { "type": "string", "title": "Static IP", "format": "ipv4", "description": "Leave empty for DHCP." }
}
},
"backup": { "type": "boolean", "title": "Backup", "default": true },
"backup_policy": { "type": "string", "title": "Backup policy", "enum": ["daily", "hourly"], "default": "daily" },
"tags": {
"type": "object",
"title": "Tags",
"additionalProperties": { "type": "string" }
}
},
"$defs": {
"disk": {
"type": "object",
"required": ["size_gb"],
"additionalProperties": false,
"properties": {
"size_gb": { "type": "integer", "title": "Size (GB)", "minimum": 10, "maximum": 4096 },
"tier": { "type": "string", "title": "Tier", "enum": ["standard", "ssd"], "default": "standard" }
}
}
}
}
+22
View File
@@ -0,0 +1,22 @@
# Stack for the dev environment. Every file in vms/ becomes one module
# instance, keyed by its file name. Quai only ever edits the YAML.
locals {
vm_files = fileset("${path.module}/vms", "*.yaml")
vms = { for f in local.vm_files : trimsuffix(f, ".yaml") => yamldecode(file("${path.module}/vms/${f}")) }
}
module "vm" {
source = "../../modules/vm"
for_each = local.vms
name = each.key
environment = "dev"
template = each.value.template
cpu = each.value.cpu
memory_gb = each.value.memory_gb
disks = each.value.disks
vlan = each.value.network.vlan
ip = try(each.value.network.ip, null)
backup_policy = try(each.value.backup, false) ? try(each.value.backup_policy, "daily") : null
tags = merge(try(each.value.tags, {}), { owner = try(each.value.owner, "unknown") })
}
+10
View File
@@ -0,0 +1,10 @@
owner: bob
description: CI build agent
template: ubuntu-24.04
cpu: 8
memory_gb: 16
disks:
- size_gb: 100
network:
vlan: 110
backup: false
+22
View File
@@ -0,0 +1,22 @@
# Stack for the prod environment. Every file in vms/ becomes one module
# instance, keyed by its file name. Quai only ever edits the YAML.
locals {
vm_files = fileset("${path.module}/vms", "*.yaml")
vms = { for f in local.vm_files : trimsuffix(f, ".yaml") => yamldecode(file("${path.module}/vms/${f}")) }
}
module "vm" {
source = "../../modules/vm"
for_each = local.vms
name = each.key
environment = "prod"
template = each.value.template
cpu = each.value.cpu
memory_gb = each.value.memory_gb
disks = each.value.disks
vlan = each.value.network.vlan
ip = try(each.value.network.ip, null)
backup_policy = try(each.value.backup, false) ? try(each.value.backup_policy, "daily") : null
tags = merge(try(each.value.tags, {}), { owner = try(each.value.owner, "unknown") })
}
+13
View File
@@ -0,0 +1,13 @@
owner: dana
description: Orders database (PostgreSQL)
template: rhel-9
cpu: 12
memory_gb: 64
disks:
- size_gb: 100
tier: ssd
- size_gb: 1000 # data volume
tier: ssd
network: { vlan: 130, ip: 10.0.13.20 }
backup: true
backup_policy: hourly
+10
View File
@@ -0,0 +1,10 @@
# Created before Quai. No owner on record: only platform admins can change it.
description: Partner file drop (to be retired)
template: rhel-9
cpu: 2
memory_gb: 4
disks:
- size_gb: 200
network:
vlan: 110
backup: false
+17
View File
@@ -0,0 +1,17 @@
# Front-end for the shop. Pinned to VLAN 120 by the firewall team.
owner: bob
description: Shop front-end
template: ubuntu-24.04
cpu: 4 # bumped in INC-4411
memory_gb: 16
disks:
- size_gb: 80
tier: ssd
network:
vlan: 120
ip: 10.0.12.11
backup: true
backup_policy: daily
tags:
cost_center: "4711"
app: shop
+22
View File
@@ -0,0 +1,22 @@
# Stack for the test environment. Every file in vms/ becomes one module
# instance, keyed by its file name. Quai only ever edits the YAML.
locals {
vm_files = fileset("${path.module}/vms", "*.yaml")
vms = { for f in local.vm_files : trimsuffix(f, ".yaml") => yamldecode(file("${path.module}/vms/${f}")) }
}
module "vm" {
source = "../../modules/vm"
for_each = local.vms
name = each.key
environment = "test"
template = each.value.template
cpu = each.value.cpu
memory_gb = each.value.memory_gb
disks = each.value.disks
vlan = each.value.network.vlan
ip = try(each.value.network.ip, null)
backup_policy = try(each.value.backup, false) ? try(each.value.backup_policy, "daily") : null
tags = merge(try(each.value.tags, {}), { owner = try(each.value.owner, "unknown") })
}
View File
+17
View File
@@ -0,0 +1,17 @@
# Illustrative only: swap in vsphere_virtual_machine, proxmox_vm_qemu,
# kubevirt_virtual_machine or a cloud provider's instance resource.
resource "terraform_data" "vm" {
input = {
name = "${var.environment}-${var.name}"
template = var.template
cpu = var.cpu
memory_mb = var.memory_gb * 1024
disks = var.disks
vlan = var.vlan
ip = var.ip
backup_policy = var.backup_policy
tags = var.tags
}
}
output "id" { value = terraform_data.vm.id }
+24
View File
@@ -0,0 +1,24 @@
variable "name" { type = string }
variable "environment" { type = string }
variable "template" { type = string }
variable "cpu" { type = number }
variable "memory_gb" { type = number }
variable "disks" {
type = list(object({
size_gb = number
tier = optional(string, "standard")
}))
}
variable "vlan" { type = number }
variable "ip" {
type = string
default = null
}
variable "backup_policy" {
type = string
default = null
}
variable "tags" {
type = map(string)
default = {}
}
+31
View File
@@ -0,0 +1,31 @@
# Applies envs/prod when main changes and reports the apply to Quai: spec.runner.pipelines
# names it (kind: apply), and requests merged to main wait for it.
trigger:
branches:
include: [main]
paths:
include: [envs/prod, modules]
pr: none
resources:
repositories:
- repository: quai
type: github
name: damhau/quai
endpoint: github # a GitHub service connection
variables:
- group: quai # quaiUrl, quaiRepository and the secret quaiRunToken
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
- script: |
set -euo pipefail
terraform -chdir=envs/prod init -input=false
terraform -chdir=envs/prod apply -input=false -auto-approve
displayName: terraform apply
- template: deploy/runners/azure-pipelines/quai-report.yml@quai
parameters: { stack: envs/prod, kind: apply }
+28
View File
@@ -0,0 +1,28 @@
# Plans envs/prod on each pull request to main and reports the plan to Quai: a "Build
# validation" branch policy runs it, and spec.runner.pipelines names it (kind: plan).
trigger: none
pr: none # run by the branch policy
resources:
repositories:
- repository: quai
type: github
name: damhau/quai
endpoint: github # a GitHub service connection
variables:
- group: quai # quaiUrl, quaiRepository and the secret quaiRunToken
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
- script: |
set -euo pipefail
terraform -chdir=envs/prod init -input=false
terraform -chdir=envs/prod plan -input=false -out=tfplan
terraform -chdir=envs/prod show -json tfplan > plan.json
displayName: terraform plan
- template: deploy/runners/azure-pipelines/quai-report.yml@quai
parameters: { stack: envs/prod, kind: plan, plan: plan.json }