commit be328f47266150dab16fe65d28b9c7a12a51d8c1 Author: CC Date: Wed May 20 22:26:32 2026 +0000 Initial commit diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..74c32db --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,57 @@ +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: + runs-on: ubuntu-latest + defaults: + run: + working-directory: terraform + steps: + - uses: actions/checkout@v4 + + - 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 + run: | + mkdir -p ../ansible/inventory + terraform output -json vm_ipv4_addresses | jq -r ' + to_entries[] | "[app]\n\(.value) ansible_user=cloud" + ' > ../ansible/inventory/hosts.ini + + - name: Write tags + run: terraform output -json vm_tags > ../ansible/vm_tags.json + + deploy: + needs: terraform + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Ansible + run: | + python3 -m pip install --upgrade pip + pip install ansible community.docker + + - name: Deploy app + run: ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/deploy.yml diff --git a/README.md b/README.md new file mode 100644 index 0000000..bccc673 --- /dev/null +++ b/README.md @@ -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 + diff --git a/compose/.env b/compose/.env new file mode 100644 index 0000000..4f9dbfa --- /dev/null +++ b/compose/.env @@ -0,0 +1,2 @@ +APP_NAME=myservice +APP_PORT=8080 diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml new file mode 100644 index 0000000..973dc04 --- /dev/null +++ b/compose/docker-compose.yml @@ -0,0 +1,9 @@ +services: + app: + image: your-image:latest + container_name: ${APP_NAME} + restart: unless-stopped + ports: + - "${APP_PORT}:8080" + environment: + - TZ=Europe/London diff --git a/terraform/backend.tf b/terraform/backend.tf new file mode 100644 index 0000000..80d5841 --- /dev/null +++ b/terraform/backend.tf @@ -0,0 +1,18 @@ +terraform { + backend "s3" { + bucket = "terraform" + key = "template/terraform.tfstate" + access_key = "GK242d456c0692a9d4cc102206" + secret_key = "1d7e22b7a8892cb11b569017659aa511b37b53287c4d1699c310d9f8ac76df09" + region = "garage" + endpoints = { + s3 = "http://192.168.10.109:3900" + } + skip_credentials_validation = true + skip_requesting_account_id = true + skip_metadata_api_check = true + skip_region_validation = true + use_path_style = true + use_lockfile = true +} +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..c2d633a --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,25 @@ +locals { + 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 +} diff --git a/terraform/multi.tfvars.example b/terraform/multi.tfvars.example new file mode 100644 index 0000000..a93000b --- /dev/null +++ b/terraform/multi.tfvars.example @@ -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"] + } +} diff --git a/terraform/output.tf b/terraform/output.tf new file mode 100644 index 0000000..79095bd --- /dev/null +++ b/terraform/output.tf @@ -0,0 +1,20 @@ +output "vm_ipv4_addresses" { + value = { + for k, m in module.vm : k => m.vm_ipv4_address + } +} + +output "vm_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 + } +} diff --git a/terraform/providers.tf b/terraform/providers.tf new file mode 100644 index 0000000..6d64c1f --- /dev/null +++ b/terraform/providers.tf @@ -0,0 +1,33 @@ +terraform { + required_providers { + proxmox = { + source = "bpg/proxmox" + version = "0.106.0" + #url = https://registry.terraform.io/providers/bpg/proxmox/latest/docs/guides/clone-vm + } + aws = { + source = "hashicorp/aws" + version = "6.38.0" + } + } +} + +provider "proxmox" { + endpoint = var.pm_api_url + api_token = var.pm_api_token + insecure = true + + # === FIX THIS === + ssh { + agent = true + username = "root" + password = "Ishimaru17" + } +} + +provider "aws" { + region = "garage" + access_key = "GK242d456c0692a9d4cc102206" + secret_key = "1d7e22b7a8892cb11b569017659aa511b37b53287c4d1699c310d9f8ac76df09" +# shared_credentials_files = ["$HOME/.aws/credentials"] +} \ No newline at end of file diff --git a/terraform/single.tfvars.example b/terraform/single.tfvars.example new file mode 100644 index 0000000..2b46128 --- /dev/null +++ b/terraform/single.tfvars.example @@ -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" +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..496ab55 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,59 @@ +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" + } +}