initial template v1

This commit is contained in:
CC
2026-05-20 23:24:33 +01:00
parent b45851e888
commit 7f84f1b2b6
7 changed files with 303 additions and 45 deletions

View File

@@ -2,6 +2,15 @@ name: Deploy VM and App
on:
workflow_dispatch:
inputs:
tfvars_file:
description: "Which tfvars file to use"
required: true
default: "single.tfvars.example"
type: choice
options:
- single.tfvars.example
- multi.tfvars.example
jobs:
terraform:
@@ -14,22 +23,24 @@ jobs:
- uses: hashicorp/setup-terraform@v3
- name: Select tfvars
run: cp "${{ inputs.tfvars_file }}" terraform.tfvars
- name: Terraform init
run: terraform init
- name: Terraform apply
run: terraform apply -auto-approve
- name: Write inventory from output
- name: Write inventory
run: |
IP=$(terraform output -raw vm_ipv4_address)
mkdir -p ../ansible/inventory
printf '[app]\n%s ansible_user=cloud\n' "$IP" > ../ansible/inventory/hosts.ini
terraform output -json vm_ipv4_addresses | jq -r '
to_entries[] | "[app]\n\(.value) ansible_user=cloud"
' > ../ansible/inventory/hosts.ini
- name: Write tags file
run: |
TAGS=$(terraform output -json vm_tags)
echo "$TAGS" > ../terraform/vm_tags.json
- name: Write tags
run: terraform output -json vm_tags > ../ansible/vm_tags.json
deploy:
needs: terraform

145
README.md Normal file
View File

@@ -0,0 +1,145 @@
# VM Service Template
This repository is a starter template for deploying one or more service VMs in Proxmox using Terraform, then configuring and launching applications with Ansible and Docker Compose.
It is designed to work with a shared Terraform module stored in a separate repository. This repo acts as the wrapper/root module and provides the service-specific inputs, deployment workflow, and app stack files.
## What this repo does
- Creates one or more Proxmox VMs from a reusable Terraform module.
- Uses cloud-init and QEMU guest agent in the template VM.
- Writes the VM IP address into Ansible inventory.
- Tags VMs with Terraform, Docker, service name, and IP metadata.
- Deploys an application stack with Ansible and Docker Compose.
## Repository layout
```text
terraform/ Terraform wrapper for the shared Proxmox module.
compose/ Docker Compose application files.
ansible/ Playbook and role used to configure and deploy the app.
.gitea/ Workflow files for apply and deploy.
```
## Requirements
- Terraform installed locally or in CI.
- Access to the Proxmox API.
- A shared Proxmox VM module in a separate repository.
- Ansible installed for application deployment.
- Docker and Docker Compose available on the target VM.
## How it works
1. Terraform reads the instance configuration from `terraform.tfvars`.
2. Terraform calls the shared Proxmox module to create the VM.
3. Terraform outputs the VM IP address and tags.
4. The workflow writes the IP into `ansible/inventory/hosts.ini`.
5. Ansible connects to the VM and deploys the Compose stack.
## Single instance
Use `instance_mode = "single"` and define one `instance` object.
Example:
```hcl
instance_mode = "single"
instance = {
service_name = "grafana"
vm_name = "grafana-01"
node_name = "pop"
app_port = 3000
app_image = "grafana/grafana:latest"
vm_tags = ["monitoring"]
}
```
## Multi instance
Use `instance_mode = "multi"` and define an `instances` map.
Example:
```hcl
instance_mode = "multi"
instances = {
grafana = {
service_name = "grafana"
vm_name = "grafana-01"
node_name = "pop"
app_port = 3000
app_image = "grafana/grafana:latest"
vm_tags = ["monitoring"]
}
caddy = {
service_name = "caddy"
vm_name = "caddy-01"
node_name = "pop"
app_port = 80
app_image = "caddy:latest"
vm_tags = ["proxy"]
}
}
```
## Defaults
The template supports module defaults for items like:
- datastore
- BIOS
- machine type
- SSH key
- bridge
- CPU, RAM, and disk size
Override only what you need in the service repo.
## Running locally
```bash
cd terraform
cp single.tfvars.example terraform.tfvars
terraform init
terraform apply
```
After Terraform finishes, run:
```bash
cd ..
ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/deploy.yml
```
## Workflow trigger
This template is intended to be run after the repository is created from it.
The usual flow is:
- create a new repo from this template,
- update the tfvars file,
- trigger the workflow manually with `workflow_dispatch`,
- or push the first commit to start deployment.
## Notes
- The shared Proxmox module should live in a separate repository.
- The service repo should contain the wrapper, not the internal Proxmox resource logic.
- VM IP tags are generated after Terraform apply.
- If the VM disk is resized manually in Proxmox, Terraform may see drift on the next run.
## Example use case
This template works well for service VMs such as:
- Grafana
- Caddy
- Jellyfin
- Code Server
- Monitoring or proxy VMs

View File

@@ -1,20 +1,25 @@
module "vm" {
source = "git::https://your-git-server/infra/modules/proxmox_ubuntu_cloudinit.git//?ref=v1.0.0"
vm_name = var.vm_name
node_name = var.node_name
node_datastore = var.node_datastore
bridge = var.bridge
vm_cpu = var.vm_cpu
vm_ram = var.vm_ram
vm_size = var.vm_size
vm_bios = var.vm_bios
vm_machine = var.vm_machine
vm_tags = local.all_tags
vm_user_sshkey = var.vm_user_sshkey
}
locals {
ip_tag = "ip-${replace(module.vm.vm_ipv4_address, ".", "-")}"
all_tags = concat(var.vm_tags, ["terraform", "docker"], [local.ip_tag])
instance_map = var.instance_mode == "single" ? {
main = var.instance
} : var.instances
}
module "vm" {
for_each = local.instance_map
source = "git::https://tea.charcarservices.uk/CC/TerraformModules.git//proxmox_ubuntu_cloudinit_template?ref=main"
vm_name = each.value.vm_name
node_name = each.value.node_name
node_datastore = var.vm_defaults.node_datastore
bridge = var.vm_defaults.bridge
vm_cpu = coalesce(try(each.value.vm_cpu, null), var.vm_defaults.vm_cpu)
vm_ram = coalesce(try(each.value.vm_ram, null), var.vm_defaults.vm_ram)
vm_size = coalesce(try(each.value.vm_size, null), var.vm_defaults.vm_size)
vm_bios = var.vm_defaults.vm_bios
vm_machine = var.vm_defaults.vm_machine
vm_tags = concat(
try(each.value.vm_tags, []),
["terraform", "docker", each.value.service_name]
)
vm_user_sshkey = var.vm_defaults.vm_user_sshkey
}

View File

@@ -0,0 +1,21 @@
instance_mode = "multi"
instances = {
grafana = {
service_name = "grafana"
vm_name = "grafana-01"
node_name = "pop"
app_port = 3000
app_image = "grafana/grafana:latest"
vm_tags = ["monitoring"]
}
caddy = {
service_name = "caddy"
vm_name = "caddy-01"
node_name = "pop"
app_port = 80
app_image = "caddy:latest"
vm_tags = ["proxy"]
}
}

View File

@@ -1,11 +1,20 @@
output "vm_name" {
value = var.vm_name
}
output "vm_ipv4_address" {
value = module.vm.vm_ipv4_address
output "vm_ipv4_addresses" {
value = {
for k, m in module.vm : k => m.vm_ipv4_address
}
}
output "vm_tags" {
value = local.all_tags
value = {
for k, m in module.vm : k => concat(
try(local.instance_map[k].vm_tags, []),
["terraform", "docker", local.instance_map[k].service_name, "ip-${replace(m.vm_ipv4_address, ".", "-")}"]
)
}
}
output "service_names" {
value = {
for k, cfg in local.instance_map : k => cfg.service_name
}
}

View File

@@ -0,0 +1,21 @@
instance_mode = "single"
instance = {
service_name = "grafana"
vm_name = "grafana-01"
node_name = "pop"
app_port = 3000
app_image = "grafana/grafana:latest"
vm_tags = ["monitoring"]
}
vm_defaults = {
node_datastore = "hlst"
vm_bios = "ovmf"
vm_machine = "q35"
vm_user_sshkey = "ssh-ed25519 AAAA..."
bridge = "vmbr0"
vm_cpu = 2
vm_ram = 4096
vm_size = "20G"
}

View File

@@ -1,13 +1,59 @@
variable "vm_name" { type = string }
variable "node_name" { type = string }
variable "node_datastore" { type = string }
variable "bridge" { type = string, default = "vmbr0" }
variable "vm_cpu" { type = number, default = 2 }
variable "vm_ram" { type = number, default = 4096 }
variable "vm_size" { type = string, default = "20G" }
variable "vm_bios" { type = string, default = "ovmf" }
variable "vm_machine" { type = string, default = "q35" }
variable "vm_tags" { type = list(string), default = ["terraform"] }
variable "vm_user_sshkey" { type = string }
variable "app_name" { type = string, default = "myservice" }
variable "app_port" { type = number, default = 8080 }
variable "instance_mode" {
type = string
default = "single"
}
variable "instance" {
description = "Single instance definition"
type = object({
service_name = string
vm_name = string
node_name = string
vm_cpu = optional(number)
vm_ram = optional(number)
vm_size = optional(string)
app_port = number
app_image = string
vm_tags = optional(list(string))
})
default = null
}
variable "instances" {
description = "Multiple instance definitions"
type = map(object({
service_name = string
vm_name = string
node_name = string
vm_cpu = optional(number)
vm_ram = optional(number)
vm_size = optional(string)
app_port = number
app_image = string
vm_tags = optional(list(string))
}))
default = {}
}
variable "vm_defaults" {
type = object({
node_datastore = string
vm_bios = string
vm_machine = string
vm_user_sshkey = string
bridge = string
vm_cpu = number
vm_ram = number
vm_size = string
})
default = {
node_datastore = "hlst"
vm_bios = "ovmf"
vm_machine = "q35"
vm_user_sshkey = ""
bridge = "vmbr0"
vm_cpu = 1
vm_ram = 2048
vm_size = "20G"
}
}