commit 1e37f5e92175e50568f683969b7d49a9e8a2da0e Author: CC Date: Tue Sep 15 14:44:43 2026 +0000 Initial commit diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..54de3f7 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,89 @@ +name: Deploy VM and App + +on: + push: + + 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-ansible-deploy: + runs-on: ubuntu-latest + env: + PROXMOX_VE_API_TOKEN: https://api.staging.example.com + PROXMOX_VE_ENDPOINT: true + GITEA_BASE_URL: https://api.staging.example.com + GITEA_TOKEN: true + AWS_ACCESS_KEY_ID: https://api.staging.example.com + AWS_SECRET_ACCESS_KEY: true + AWS_REGION: garage + TF_VAR_vm_ssh_userkey: ${{ secrets.VM_USER_SSHKEY }} + + steps: + - uses: actions/checkout@v4 + + - name: Check files & Select tfvars + shell: bash + run: | + #rm ansible/inventory/inventory.yml + mkdir -p ansible/inventory + cd terraform + #rm vm_data.yml + cp "${{ inputs.tfvars_file || 'single.tfvars.example' }}" terraform.tfvars + + - uses: hashicorp/setup-terraform@v4 + + - name: Check path + run: pwd + + - name: Terraform init + run: terraform init + working-directory: "terraform" + + - name: Terraform apply + run: terraform apply -auto-approve + working-directory: "terraform" + +#### Needs review / determine if worth keeping +# - name: Install Ansible +# shell: bash +# run: | +# sudo apt-get update +# sudo apt-get install -y ansible rsync + +# - name: Set up SSH +# run: | +# mkdir -p ~/.ssh +# echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/vlans_rsa +# chmod 600 ~/.ssh/vlans_rsa +# cat > ~/.ssh/config <<'EOF' +# Host * +# StrictHostKeyChecking no +# UserKnownHostsFile=/dev/null +# EOF + +# - name: Configure Git +# run: | +# git config user.name "git-bot" +# git config user.email "got-bot@text.com" + +# - name: Commit and push to Gitea +# run: | +# git remote set-url origin https://$GITEA_USERNAME:${{ secrets.GIT_BOT_TOKEN }}@tea.charcarservices.uk/CC/N8N.git +# git add terraform/vm_data.yml ansible/inventory/inventory.yml +# git diff --cached --quiet || git commit -m "chore: update terraform outputs" +# git push origin HEAD:main +# env: +# GITEA_USERNAME: git-bot # or your bot account + +# - name: Run playbook +# run: | +# ansible-playbook ansible/playbooks/docker_copy.yml -i ansible/inventory/inventory.yml -u cloud --private-key ~/.ssh/vlans_rsa diff --git a/.gitea/workflows/destroy_module.yml b/.gitea/workflows/destroy_module.yml new file mode 100644 index 0000000..22a56f5 --- /dev/null +++ b/.gitea/workflows/destroy_module.yml @@ -0,0 +1,51 @@ +name: Terraform Destroy + +on: + workflow_dispatch: + inputs: + working_directory: + description: "Working directory for the module" + required: false + default: "terraform" + type: string + + tfvars_file: + description: "Which tfvars file to use" + required: true + default: "single.tfvars.example" + type: choice + options: + - single.tfvars.example + - multi.tfvars.example + +jobs: + destroy: + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + working-directory: ${{ inputs.working_directory }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check files & Select tfvars + shell: bash + run: | + cp "${{ inputs.tfvars_file || 'single.tfvars.example' }}" terraform.tfvars + cat terraform.tfvars + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v4 + + - name: Terraform Init + run: terraform init + + - name: Terraform Destroy Plan + run: terraform plan -destroy -out=tfplan + continue-on-error: false + + - name: Terraform Destroy + run: terraform destroy -auto-approve diff --git a/.gitea/workflows/test_deploy.yml b/.gitea/workflows/test_deploy.yml new file mode 100644 index 0000000..8229cc5 --- /dev/null +++ b/.gitea/workflows/test_deploy.yml @@ -0,0 +1,82 @@ +name: Terraform Deploy + +on: + workflow_dispatch: + inputs: + instance_mode: + description: "Instance mode" + required: true + default: "single" + type: choice + options: + - single + - multi + + service_name: + description: "Service name (e.g. n8n)" + required: false + default: "" + + vm_name: + description: "VM name (e.g. n8n-01)" + required: false + default: "" + + node_name: + description: "Proxmox node (e.g. pop)" + required: false + default: "" + + app_port: + description: "App port (e.g. 5678)" + required: false + default: "5678" + + vm_tags: + description: "VM tags, comma-separated (e.g. agentic,prod)" + required: false + default: "agentic" + +jobs: + terraform: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Build tfvars + env: + VM_USER_SSHKEY: ${{ secrets.VM_USER_SSHKEY }} + APP_PORT: ${{ github.event.inputs.app_port }} + VM_TAGS: ${{ github.event.inputs.vm_tags }} + run: | + # Convert comma-separated tags to HCL list + TAGS_HCL=$(echo "$VM_TAGS" | tr ',' '\n' | sed 's/^/"/;s/$/"/' | paste -sd ',' | sed 's/^/[/;s/$/]/') + + cat > ci.tfvars < { + service_name = v.service_name + vm_name = v.vm_name + node_name = v.node_name + ipv4_address = module.proxmox_ubuntu_cloudinit_clone[k].vm_ipv4_address + vm_tags = concat( + try(v.vm_tags, []), + ["terraform", "docker", v.service_name, "ip-${replace(module.proxmox_ubuntu_cloudinit_clone[k].vm_ipv4_address, ".", "-")}"] + ) + } + } +} \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..1d86fce --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,22 @@ +module "proxmox_ubuntu_cloudinit_clone" { + for_each = local.instance_map + source = "./modules/proxmox_ubuntu_cloudinit_clone" + + vm_name = each.value.vm_name + service_name = each.value.service_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 + compose_file_content = filebase64("${path.root}/../compose/docker-compose.yml") + env_file_content = filebase64("${path.root}/../compose/.env") +} \ No newline at end of file diff --git a/terraform/modules/proxmox_ubuntu_cloudinit_clone/locals.tf b/terraform/modules/proxmox_ubuntu_cloudinit_clone/locals.tf new file mode 100644 index 0000000..f790d82 --- /dev/null +++ b/terraform/modules/proxmox_ubuntu_cloudinit_clone/locals.tf @@ -0,0 +1,6 @@ +locals { + selected_instance = one([ + for cfg in var.instance_configs : + cfg if cfg.crispy_name == var.node_name + ]) +} \ No newline at end of file diff --git a/terraform/modules/proxmox_ubuntu_cloudinit_clone/main.tf b/terraform/modules/proxmox_ubuntu_cloudinit_clone/main.tf new file mode 100644 index 0000000..72fccac --- /dev/null +++ b/terraform/modules/proxmox_ubuntu_cloudinit_clone/main.tf @@ -0,0 +1,132 @@ +resource "proxmox_virtual_environment_file" "cloud_config" { + content_type = "snippets" + datastore_id = "hlst" + node_name = var.node_name + + source_raw { + file_name = "${var.vm_name}_vm.cloud-config.yaml" + data = <<-EOF + #cloud-config + hostname: ${var.vm_name} + + package_update: true + package_upgrade: true + + system_info: + default_user: + groups: [ cloud ] + + users: + - default + - name: cloud + groups: + - sudo + - docker + shell: /bin/bash + ssh-authorized-keys: + - "${var.vm_user_sshkey}" + sudo: ALL=(ALL) NOPASSWD:ALL + + packages: + - qemu-guest-agent + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + - unattended-upgrades + - rsync + - nano + + bootcmd: + - mkdir -p /home/cloud/${var.service_name} + - chown cloud:cloud /home/cloud/${var.service_name} + + write_files: + - path: /home/cloud/${var.service_name}/docker-compose.yml + owner: cloud:cloud + permissions: '0644' + encoding: b64 + content: ${var.compose_file_content} + + - path: /home/cloud/${var.service_name}/.env + owner: cloud:cloud + permissions: '0600' + encoding: b64 + content: ${var.env_file_content} + + runcmd: + - systemctl enable qemu-guest-agent + - mkdir -p /etc/apt/keyrings + - apt-add-repository ppa:fish-shell/release-3 + - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + - echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + - apt-get update + - apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin fish + - systemctl enable docker + - systemctl start docker + - chown -R cloud:cloud /home/cloud/${var.service_name} + - cd /home/cloud/${var.service_name} + - sudo -u cloud docker compose up -d + - reboot + EOF + } +} + +resource "proxmox_virtual_environment_vm" "ubuntu_22_minimal_clone" { + name = var.vm_name # VM name + node_name = var.node_name # Proxmox node to deploy the VM + tags = var.vm_tags # Optional VM tags for categorization + + agent { + enabled = true # Enable the QEMU guest agent + } + + stop_on_destroy = true # Ensure VM is stopped gracefully when destroyed + + + clone { + vm_id = local.selected_instance.vmid # ID of the source template + node_name = local.selected_instance.crispy_name # Node of the source template + } + + bios = var.vm_bios # BIOS type (e.g., seabios or ovmf) + machine = var.vm_machine # Machine type (e.g., q35) + + cpu { + cores = var.vm_cpu # Number of CPU cores + type = "host" # Use host CPU type for best compatibility/performance + } + + memory { + dedicated = var.vm_ram # RAM in MB + } + + disk { + datastore_id = var.node_datastore # Datastore to hold the disk + interface = "scsi0" # Primary disk interface + size = var.vm_size + } + + + initialization { + user_data_file_id = proxmox_virtual_environment_file.cloud_config.id # Link the cloud-init file + datastore_id = var.node_datastore + interface = "scsi1" # Separate interface for cloud-init + ip_config { + ipv4 { + address = "dhcp" # Get IP via DHCP + } + } + } + + network_device { + bridge = var.bridge # Use the default bridge + } + + # lifecycle { + # ignore_changes = [ # Ignore initialization section after first depoloyment for idempotency + # initialization + # ] + # } +} \ No newline at end of file diff --git a/terraform/modules/proxmox_ubuntu_cloudinit_clone/output.tf b/terraform/modules/proxmox_ubuntu_cloudinit_clone/output.tf new file mode 100644 index 0000000..dadef05 --- /dev/null +++ b/terraform/modules/proxmox_ubuntu_cloudinit_clone/output.tf @@ -0,0 +1,3 @@ +output "vm_ipv4_address" { + value = proxmox_virtual_environment_vm.ubuntu_22_minimal_clone.ipv4_addresses[1][0] +} \ No newline at end of file diff --git a/terraform/modules/proxmox_ubuntu_cloudinit_clone/providers.tf b/terraform/modules/proxmox_ubuntu_cloudinit_clone/providers.tf new file mode 100644 index 0000000..d48bb1b --- /dev/null +++ b/terraform/modules/proxmox_ubuntu_cloudinit_clone/providers.tf @@ -0,0 +1,13 @@ +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.64.0" + } + } +} diff --git a/terraform/modules/proxmox_ubuntu_cloudinit_clone/variables.tf b/terraform/modules/proxmox_ubuntu_cloudinit_clone/variables.tf new file mode 100644 index 0000000..9dae83d --- /dev/null +++ b/terraform/modules/proxmox_ubuntu_cloudinit_clone/variables.tf @@ -0,0 +1,157 @@ +variable "pm_api_url" { default = "https://192.168.10.201:8006/api2/json" } +variable "pm_api_token" { default = "terraform@pve!provider=760580c4-5c1f-462b-986a-dd244c6c95f2" } + +variable "storage" { default = "hlst" } +variable "bridge" { default = "vmbr0" } + +variable "os_type" { + default = "alpine" +} + +variable "instance_configs" { + type = list(object({ + crispy_name = string + vmid = string + })) + default = [ + { crispy_name = "snap", vmid = "902" }, + { crispy_name = "crackle", vmid = "901" }, + { crispy_name = "pop", vmid = "900" } + ] +} + +variable "clone_count" { + type = number + default = 1 +} + +variable "vm_count" { default = 1 } +variable "name_prefix" { default = "dev" } +variable "vm_ram" { default = 2048 } +variable "vm_cpu" { default = 1 } +variable "vm_size" { default = 10 } +variable "vm_bios" { + description = "Type of BIOS used for the VM" + type = string + default = "ovmf" +} + +variable "vm_machine" { + description = "Type of machine used for the VM" + type = string + default = "q35" +} + +variable "vm_tags" { + description = "Tags for the VM" + type = list(any) + default = ["test", "terraform"] +} + +variable "ipconfig0" { default = "ip=dhcp" } + +variable "access_key" { + type = string + default = "GK242d456c0692a9d4cc102206" +} + +variable "secret_key" { + type = string + default = "1d7e22b7a8892cb11b569017659aa511b37b53287c4d1699c310d9f8ac76df09" +} + +variable "region" { + type = string + default = "garage" +} + +variable "endpoints_s3" { + type = string + description = "S3 endpoint" + default = "http://192.168.10.109:3909" +} + +variable "skip_credentials_validation" { + type = bool + default = true +} + +variable "skip_requesting_account_id" { + type = bool + default = true +} + +variable "skip_metadata_api_check" { + type = bool + default = true +} + +variable "skip_region_validation" { + type = bool + default = true +} + +variable "use_path_style" { + type = bool + default = true +} + +variable "use_lockfile" { + type = bool + default = true +} + +variable "vm_name" { + description = "Hostname of the VM" + type = string + default = "Lab" +} + +variable "service_name" { + description = "Folder name of the services" + type = string + default = "Lab" +} + +variable "vm_user_sshkey" { + description = "Admin user SSH key of the VM" + type = string + default = "" +} + + +variable "node_name" { + description = "Proxmox host for the VM" + type = string + default = "pop" +} + +variable "node_datastore" { + description = "Datastore used for VM storage" + type = string + default = "hlst" +} + +variable "vm_template" { + description = "Template of the VM" + type = string + default = "ubuntu-cloud" +} + + +variable "vm_user" { + description = "Admin user of the VM" + type = string + default = "cloud" +} + +variable "compose_file_content" { + description = "Rendered contents of docker-compose.yml to write to the VM" + type = string +} + +variable "env_file_content" { + description = "Rendered contents of .env to write to the VM" + type = string + sensitive = true +} \ No newline at end of file diff --git a/terraform/modules/proxmox_vm_data/main.tf b/terraform/modules/proxmox_vm_data/main.tf new file mode 100644 index 0000000..b1a0368 --- /dev/null +++ b/terraform/modules/proxmox_vm_data/main.tf @@ -0,0 +1,17 @@ +locals { + vm_data = { + vm_tag_data = { + for k, v in var.instances : + k => { + node_name = v.node_name + vm_name = v.vm_name + tags = v.vm_tags + } + } + } +} + +resource "local_file" "vm_data" { + filename = var.filename + content = yamlencode(local.vm_data) +} diff --git a/terraform/modules/proxmox_vm_data/outputs.tf b/terraform/modules/proxmox_vm_data/outputs.tf new file mode 100644 index 0000000..c6c849f --- /dev/null +++ b/terraform/modules/proxmox_vm_data/outputs.tf @@ -0,0 +1,7 @@ +output "filename" { + value = local_file.vm_data.filename +} + +output "content" { + value = local_file.vm_data.content +} diff --git a/terraform/modules/proxmox_vm_data/variables.tf b/terraform/modules/proxmox_vm_data/variables.tf new file mode 100644 index 0000000..a523f07 --- /dev/null +++ b/terraform/modules/proxmox_vm_data/variables.tf @@ -0,0 +1,15 @@ +variable "filename" { + description = "Path to write the vm_data.yml file" + type = string +} + +variable "instances" { + description = "Normalized instance map keyed by instance key" + type = map(object({ + service_name = string + vm_name = string + node_name = string + ipv4_address = string + vm_tags = list(string) + })) +} 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..e4e5905 --- /dev/null +++ b/terraform/output.tf @@ -0,0 +1,41 @@ +output "vm_ipv4_addresses" { + description = "Map of instance key to VM IPv4 address" + value = { + for k, m in module.proxmox_ubuntu_cloudinit_clone : k => m.vm_ipv4_address + } +} + +output "service_names" { + description = "Map of instance key to service name" + value = { + for k, v in local.instance_map : k => v.service_name + } +} + +output "vm_names" { + description = "Map of instance key to VM name" + value = { + for k, v in local.instance_map : k => v.vm_name + } +} + +output "vm_tag_data" { + description = "Map of instance key to node, VM name, and final tags" + value = { + for k, v in local.vm_created : k => { + node_name = v.node_name + vm_name = v.vm_name + tags = v.vm_tags + } + } +} + +# output "inventory_file" { +# description = "Path to the generated Ansible inventory" +# value = module.inventory.filename +# } + +# output "vm_data_file" { +# description = "Path to the generated VM data file" +# value = module.vm_data.filename +# } diff --git a/terraform/providers.tf b/terraform/providers.tf new file mode 100644 index 0000000..79529ed --- /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.64.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..7705723 --- /dev/null +++ b/terraform/single.tfvars.example @@ -0,0 +1,11 @@ +instance_mode = "single" + +instance = { + service_name = "n8n" + vm_name = "n8n-01" + node_name = "pop" + app_port = 5678 + app_image = "docker.n8n.io/n8nio/n8n" + vm_tags = ["agentic"] +} + diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..87b6b73 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,66 @@ +variable "instance_mode" { + type = string + description = "single or multiple" + + validation { + condition = contains(["single", "multiple"], var.instance_mode) + error_message = "instance_mode must be either single or multiple." + } +} + +variable "instance" { + type = object({ + service_name = string + vm_name = string + node_name = string + vm_cpu = optional(number) + vm_ram = optional(number) + vm_size = optional(string) + vm_tags = optional(list(string)) + }) + default = null + nullable = true + description = "Used only when instance_mode = single." +} + +variable "instances" { + type = map(object({ + service_name = string + vm_name = string + node_name = string + vm_cpu = optional(number) + vm_ram = optional(number) + vm_size = optional(string) + vm_tags = optional(list(string)) + })) + default = {} + nullable = false + description = "Used only when instance_mode = multiple." +} + + +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 = number + }) + default = { + node_datastore = "hlst" + vm_bios = "ovmf" + vm_machine = "q35" + vm_user_sshkey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDRlWaLBt/qmWY01Cd6jN/YxLnlT+6lg+evEdN/dIajirdTj1rCbAdlG3WYvo+4BpN17HK3/eGQpGUMbgI/8MVd8YPODcD34gaNX0w2v66BwHx+S6BZUpz5T2IoQT0JtSv/TtFICoff5gXdNRpfd4eWsmTioEqLA6oToJLE4dn3jvAzFi9y7fyLqvuoQMmPidYYJjGT30eiULtXNspoEP+GmuWmVEu+znzMWaKDWKdOsii4Cv1aWCRKSDDRzDBrZI2mP+Vm4HDQBdgDYRw4ehumMDtfaSjyJCnrk691bIM+wxzICuIEecg5kq5HcUPvo2mFyWPAEXb5xlXnuopYEBd7 Generated By NeoServer" + bridge = "vmbr0" + vm_cpu = 2 + vm_ram = 8192 + vm_size = 100 + } +} + +variable "pm_api_url" { default = "https://192.168.10.201:8006/api2/json" } +variable "pm_api_token" { default = "terraform@pve!provider=760580c4-5c1f-462b-986a-dd244c6c95f2" }