generated from CC/VMServiceTemplate
Initial commit
This commit is contained in:
57
.gitea/workflows/deploy.yml
Normal file
57
.gitea/workflows/deploy.yml
Normal file
@@ -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
|
||||||
145
README.md
Normal file
145
README.md
Normal 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
|
||||||
|
|
||||||
2
compose/.env
Normal file
2
compose/.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
APP_NAME=myservice
|
||||||
|
APP_PORT=8080
|
||||||
9
compose/docker-compose.yml
Normal file
9
compose/docker-compose.yml
Normal file
@@ -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
|
||||||
18
terraform/backend.tf
Normal file
18
terraform/backend.tf
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
25
terraform/main.tf
Normal file
25
terraform/main.tf
Normal file
@@ -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
|
||||||
|
}
|
||||||
21
terraform/multi.tfvars.example
Normal file
21
terraform/multi.tfvars.example
Normal 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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
20
terraform/output.tf
Normal file
20
terraform/output.tf
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
33
terraform/providers.tf
Normal file
33
terraform/providers.tf
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
21
terraform/single.tfvars.example
Normal file
21
terraform/single.tfvars.example
Normal 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"
|
||||||
|
}
|
||||||
59
terraform/variables.tf
Normal file
59
terraform/variables.tf
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user