the existing infrastructure
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
@@ -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 }}
|
||||
@@ -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.
|
||||
@@ -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 }}" } }
|
||||
@@ -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 }}"
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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") })
|
||||
}
|
||||
@@ -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
|
||||
@@ -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") })
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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") })
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 = {}
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user