From 45a704709a7f220ba4c1817a355d967733dd435a Mon Sep 17 00:00:00 2001 From: CC Date: Fri, 22 May 2026 21:59:40 +0100 Subject: [PATCH] tested n8n template --- ansible/playbooks/docker_copy.yml | 8 + .../playbooks/roles/docker/defaults/main.yml | 15 ++ .../roles/docker/tasks/docker_copy.yml | 17 +++ .../roles/docker/tasks/docker_destroy.yml | 27 ++++ .../roles/docker/tasks/docker_install.yml | 23 +++ ansible/playbooks/roles/docker/tasks/main.yml | 17 +++ ansible/playbooks/tags.yml | 22 +++ compose/.env | 2 - compose/docker-compose.yml | 9 -- compose/n8n/.env | 17 +++ compose/n8n/docker-compose.yml | 22 +++ terraform/backend.tf | 2 +- terraform/main.tf | 30 +++- .../modules/proxmox_ansible_inventory/main.tf | 26 ++++ .../proxmox_ansible_inventory/outputs.tf | 7 + .../proxmox_ansible_inventory/variables.tf | 13 ++ .../proxmox_ubuntu_cloudinit_clone/main.tf | 123 +++++++++++++++ .../proxmox_ubuntu_cloudinit_clone/output.tf | 3 + .../providers.tf | 13 ++ .../variables.tf | 140 ++++++++++++++++++ .../proxmox_ubuntu_cloudinit_template/main.tf | 126 ++++++++++++++++ .../output.tf | 3 + .../providers.tf | 13 ++ .../variables.tf | 137 +++++++++++++++++ terraform/modules/proxmox_vm_data/main.tf | 17 +++ terraform/modules/proxmox_vm_data/outputs.tf | 7 + .../modules/proxmox_vm_data/variables.tf | 15 ++ terraform/output.tf | 43 ++++-- terraform/single.tfvars.example | 20 +-- terraform/variables.tf | 65 ++++---- workflows/deploy.yml | 78 ++++++++++ workflows/destroy_module.yml | 36 +++++ 32 files changed, 1027 insertions(+), 69 deletions(-) create mode 100644 ansible/playbooks/docker_copy.yml create mode 100644 ansible/playbooks/roles/docker/defaults/main.yml create mode 100644 ansible/playbooks/roles/docker/tasks/docker_copy.yml create mode 100644 ansible/playbooks/roles/docker/tasks/docker_destroy.yml create mode 100644 ansible/playbooks/roles/docker/tasks/docker_install.yml create mode 100644 ansible/playbooks/roles/docker/tasks/main.yml create mode 100644 ansible/playbooks/tags.yml delete mode 100644 compose/.env delete mode 100644 compose/docker-compose.yml create mode 100644 compose/n8n/.env create mode 100644 compose/n8n/docker-compose.yml create mode 100644 terraform/modules/proxmox_ansible_inventory/main.tf create mode 100644 terraform/modules/proxmox_ansible_inventory/outputs.tf create mode 100644 terraform/modules/proxmox_ansible_inventory/variables.tf create mode 100644 terraform/modules/proxmox_ubuntu_cloudinit_clone/main.tf create mode 100644 terraform/modules/proxmox_ubuntu_cloudinit_clone/output.tf create mode 100644 terraform/modules/proxmox_ubuntu_cloudinit_clone/providers.tf create mode 100644 terraform/modules/proxmox_ubuntu_cloudinit_clone/variables.tf create mode 100644 terraform/modules/proxmox_ubuntu_cloudinit_template/main.tf create mode 100644 terraform/modules/proxmox_ubuntu_cloudinit_template/output.tf create mode 100644 terraform/modules/proxmox_ubuntu_cloudinit_template/providers.tf create mode 100644 terraform/modules/proxmox_ubuntu_cloudinit_template/variables.tf create mode 100644 terraform/modules/proxmox_vm_data/main.tf create mode 100644 terraform/modules/proxmox_vm_data/outputs.tf create mode 100644 terraform/modules/proxmox_vm_data/variables.tf create mode 100644 workflows/deploy.yml create mode 100644 workflows/destroy_module.yml diff --git a/ansible/playbooks/docker_copy.yml b/ansible/playbooks/docker_copy.yml new file mode 100644 index 0000000..deddd87 --- /dev/null +++ b/ansible/playbooks/docker_copy.yml @@ -0,0 +1,8 @@ +- hosts: all + become: true + vars: + folder_name: n8n + + roles: + - role: docker + config_flavor: copy diff --git a/ansible/playbooks/roles/docker/defaults/main.yml b/ansible/playbooks/roles/docker/defaults/main.yml new file mode 100644 index 0000000..80f2098 --- /dev/null +++ b/ansible/playbooks/roles/docker/defaults/main.yml @@ -0,0 +1,15 @@ +docker_comparisons: + env: strict + labels: strict + +docker_image_name_mismatch: recreate + +docker_state: started + +docker_restart_policy: unless-stopped + +docker_pull: "missing" + +gather_facts: true + +config_flavor: none \ No newline at end of file diff --git a/ansible/playbooks/roles/docker/tasks/docker_copy.yml b/ansible/playbooks/roles/docker/tasks/docker_copy.yml new file mode 100644 index 0000000..57bcffd --- /dev/null +++ b/ansible/playbooks/roles/docker/tasks/docker_copy.yml @@ -0,0 +1,17 @@ +--- +# Copy directory recursively to remote host + +- name: Copy project directory to remote + ansible.builtin.synchronize: + src: ../compose/{{ folder_name }} + dest: /home/cloud/ + mode: push + + +- name: Start Compose stack + community.docker.docker_compose_v2: + project_src: /home/cloud/{{ folder_name }} + build: always + pull: always + state: present + diff --git a/ansible/playbooks/roles/docker/tasks/docker_destroy.yml b/ansible/playbooks/roles/docker/tasks/docker_destroy.yml new file mode 100644 index 0000000..b52c01a --- /dev/null +++ b/ansible/playbooks/roles/docker/tasks/docker_destroy.yml @@ -0,0 +1,27 @@ +- name: Get running containers + docker_host_info: + containers: yes + register: docker_info + +- name: Stop running containers + docker_container: + name: "{{ item }}" + state: stopped + loop: "{{ docker_info.containers | map(attribute='Id') | list }}" + +- name: Remove Stoped docker containers + shell: | + docker rm $(docker ps -a -q); + when: docker_info.containers != 0 + +- name: Get details of all images + docker_host_info: + images: yes + verbose_output: yes + register: image_info + +- name: Remove all images + docker_image: + name: "{{ item }}" + state: absent + loop: "{{ image_info.images | map(attribute='Id') | list }}" \ No newline at end of file diff --git a/ansible/playbooks/roles/docker/tasks/docker_install.yml b/ansible/playbooks/roles/docker/tasks/docker_install.yml new file mode 100644 index 0000000..b7396de --- /dev/null +++ b/ansible/playbooks/roles/docker/tasks/docker_install.yml @@ -0,0 +1,23 @@ +- name: Install gpg + ansible.builtin.apt: + name: gpg + +- name: Add Docker repository key + ansible.builtin.apt_key: + url: https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg + keyring: /etc/apt/trusted.gpg.d/docker.gpg + +- name: Add Docker repository + ansible.builtin.apt_repository: + # Use HTTP to enable apt-cacher + repo: deb http://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable + filename: docker + +- name: Install Docker + ansible.builtin.apt: + name: "{{ item }}" + loop: + - docker-ce + - docker-ce-cli + - containerd.io + diff --git a/ansible/playbooks/roles/docker/tasks/main.yml b/ansible/playbooks/roles/docker/tasks/main.yml new file mode 100644 index 0000000..4ee4577 --- /dev/null +++ b/ansible/playbooks/roles/docker/tasks/main.yml @@ -0,0 +1,17 @@ +--- +- name: Docker Install + include_tasks: docker_install.yml + when: config_flavor == "install" + +- name: Docker Stop & Destroy + include_tasks: docker_destroy.yml + when: config_flavor == "destroy" + + +- name: Docker Transfer Compose to Remote Host + include_tasks: docker_copy.yml + when: config_flavor == "copy" + +##### +# You need to set up each docker playbook to a config flavor or look for an input module and assign config_flavor to it +###### \ No newline at end of file diff --git a/ansible/playbooks/tags.yml b/ansible/playbooks/tags.yml new file mode 100644 index 0000000..9d3f8e7 --- /dev/null +++ b/ansible/playbooks/tags.yml @@ -0,0 +1,22 @@ +--- +- name: Update Proxmox VM tags + hosts: all + gather_facts: false + + vars_files: + - ../terraform/vm_data.yml + + tasks: + - name: Update tags on each VM + community.proxmox.proxmox_kvm: + api_user: "{{ lookup('env', 'PROXMOX_USER') }}" + api_token_id: "{{ lookup('env', 'PROXMOX_TOKEN_ID') }}" + api_token_secret: "{{ lookup('env', 'PROXMOX_TOKEN_SECRET') }}" + api_host: "{{ lookup('env', 'PROXMOX_HOST') }}" + validate_certs: true + node: "{{ item.value.node_name }}" + name: "{{ item.value.vm_name }}" + state: present + update: true + tags: "{{ item.value.tags }}" + loop: "{{ vm_tag_data | dict2items }}" diff --git a/compose/.env b/compose/.env deleted file mode 100644 index 4f9dbfa..0000000 --- a/compose/.env +++ /dev/null @@ -1,2 +0,0 @@ -APP_NAME=myservice -APP_PORT=8080 diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml deleted file mode 100644 index 973dc04..0000000 --- a/compose/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - app: - image: your-image:latest - container_name: ${APP_NAME} - restart: unless-stopped - ports: - - "${APP_PORT}:8080" - environment: - - TZ=Europe/London diff --git a/compose/n8n/.env b/compose/n8n/.env new file mode 100644 index 0000000..d9c1928 --- /dev/null +++ b/compose/n8n/.env @@ -0,0 +1,17 @@ +APP_NAME=n8n +APP_PORT=5678 +# DOMAIN_NAME and SUBDOMAIN together determine where n8n will be reachable from +# The top level domain to serve from +DOMAIN_NAME=charcarservices.uk + +# The subdomain to serve from +SUBDOMAIN=nein + +# The above example serve n8n at: https://n8n.example.com + +# Optional timezone to set which gets used by Cron and other scheduling nodes +# New York is the default value if not set +GENERIC_TIMEZONE=Europe/London + +# The email address to use for the TLS/SSL certificate creation +SSL_EMAIL=user@example.com \ No newline at end of file diff --git a/compose/n8n/docker-compose.yml b/compose/n8n/docker-compose.yml new file mode 100644 index 0000000..52a72c1 --- /dev/null +++ b/compose/n8n/docker-compose.yml @@ -0,0 +1,22 @@ +services: + n8n: + image: docker.n8n.io/n8nio/n8n + restart: always + ports: + - "5678:5678" + environment: + - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true + - N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME} + - N8N_PORT=5678 + - N8N_PROTOCOL=https + - NODE_ENV=production + - WEBHOOK_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/ + - GENERIC_TIMEZONE=${GENERIC_TIMEZONE} + - TZ=${GENERIC_TIMEZONE} + volumes: + - ./n8n_data:/home/node/.n8n + - ./local-files:/files + +volumes: + n8n_data: + traefik_data: diff --git a/terraform/backend.tf b/terraform/backend.tf index 80d5841..81d8d16 100644 --- a/terraform/backend.tf +++ b/terraform/backend.tf @@ -1,7 +1,7 @@ terraform { backend "s3" { bucket = "terraform" - key = "template/terraform.tfstate" + key = "n8n/terraform.tfstate" access_key = "GK242d456c0692a9d4cc102206" secret_key = "1d7e22b7a8892cb11b569017659aa511b37b53287c4d1699c310d9f8ac76df09" region = "garage" diff --git a/terraform/main.tf b/terraform/main.tf index c2d633a..e2a5875 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -2,11 +2,25 @@ locals { instance_map = var.instance_mode == "single" ? { main = var.instance } : var.instances + + vm_created = { + for k, v in local.instance_map : + k => { + service_name = v.service_name + vm_name = v.vm_name + node_name = v.node_name + ipv4_address = module.vm-n8n[k].vm_ipv4_address + vm_tags = concat( + try(v.vm_tags, []), + ["terraform", "docker", v.service_name, "ip-${replace(module.vm-n8n[k].vm_ipv4_address, ".", "-")}"] + ) + } + } } -module "vm" { +module "vm-n8n" { for_each = local.instance_map - source = "git::https://tea.charcarservices.uk/CC/TerraformModules.git//proxmox_ubuntu_cloudinit_template?ref=main" + source = "./modules/proxmox_ubuntu_cloudinit_clone" vm_name = each.value.vm_name node_name = each.value.node_name @@ -23,3 +37,15 @@ module "vm" { ) vm_user_sshkey = var.vm_defaults.vm_user_sshkey } + +module "inventory" { + source = "./modules/proxmox_ansible_inventory" + filename = "${abspath("${path.root}/..")}/ansible/inventory/inventory.yml" + instances = local.vm_created +} + +module "vm_data" { + source = "./modules/proxmox_vm_data" + filename = "${abspath("${path.root}/..")}/terraform/vm_data.yml" + instances = local.vm_created +} diff --git a/terraform/modules/proxmox_ansible_inventory/main.tf b/terraform/modules/proxmox_ansible_inventory/main.tf new file mode 100644 index 0000000..6c262d8 --- /dev/null +++ b/terraform/modules/proxmox_ansible_inventory/main.tf @@ -0,0 +1,26 @@ +locals { + inventory = { + all = { + vars = { + ansible_user = "cloud" + } + children = { + for svc in distinct([for k, v in var.instances : v.service_name]) : + svc => { + hosts = { + for k, v in var.instances : + v.vm_name => { + ansible_host = v.ipv4_address + } + if v.service_name == svc + } + } + } + } + } +} + +resource "local_file" "inventory" { + filename = var.filename + content = yamlencode(local.inventory) +} diff --git a/terraform/modules/proxmox_ansible_inventory/outputs.tf b/terraform/modules/proxmox_ansible_inventory/outputs.tf new file mode 100644 index 0000000..0ab7d6c --- /dev/null +++ b/terraform/modules/proxmox_ansible_inventory/outputs.tf @@ -0,0 +1,7 @@ +output "filename" { + value = local_file.inventory.filename +} + +output "content" { + value = local_file.inventory.content +} diff --git a/terraform/modules/proxmox_ansible_inventory/variables.tf b/terraform/modules/proxmox_ansible_inventory/variables.tf new file mode 100644 index 0000000..ea3837f --- /dev/null +++ b/terraform/modules/proxmox_ansible_inventory/variables.tf @@ -0,0 +1,13 @@ +variable "filename" { + description = "Path to write the inventory.yml file" + type = string +} + +variable "instances" { + description = "Normalized instance map keyed by instance key" + type = map(object({ + service_name = string + vm_name = string + ipv4_address = string + })) +} 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..75ce03f --- /dev/null +++ b/terraform/modules/proxmox_ubuntu_cloudinit_clone/main.tf @@ -0,0 +1,123 @@ +locals { + selected_instance = one([ + for cfg in var.instance_configs : + cfg if cfg.crispy_name == var.node_name + ]) +} + +resource "proxmox_virtual_environment_file" "cloud_config" { + content_type = "snippets" + datastore_id = "local" + node_name = "pop" + + source_raw { + file_name = "vm.cloud-config.yaml" # The name of the snippet file + data = <<-EOF + #cloud-config + hostname: ${var.vm_name} + + package_update: true + package_upgrade: true + + system_info: + default_user: + groups: [ docker ] + + users: + - default + - name: cloud + groups: + - sudo + - docker + shell: /bin/bash + ssh-authorized-keys: + - "${var.vm_user_sshkey}" # Inject user's SSH key + sudo: ALL=(ALL) NOPASSWD:ALL + + packages: + - qemu-guest-agent + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + - unattended-upgrades + + runcmd: + - systemctl enable qemu-guest-agent + - mkdir -p /etc/apt/keyrings + - 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 + - systemctl enable docker + - systemctl start docker + - 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 + ] + } +} + + + + + + + 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..55d1be1 --- /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.38.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..f6af510 --- /dev/null +++ b/terraform/modules/proxmox_ubuntu_cloudinit_clone/variables.tf @@ -0,0 +1,140 @@ +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 = "9002" }, + { crispy_name = "crackle", vmid = "9000" }, + { 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 "vm_user_sshkey" { + description = "Admin user SSH key of the VM" + type = string + default = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDRlWaLBt/qmWY01Cd6jN/YxLnlT+6lg+evEdN/dIajirdTj1rCbAdlG3WYvo+4BpN17HK3/eGQpGUMbgI/8MVd8YPODcD34gaNX0w2v66BwHx+S6BZUpz5T2IoQT0JtSv/TtFICoff5gXdNRpfd4eWsmTioEqLA6oToJLE4dn3jvAzFi9y7fyLqvuoQMmPidYYJjGT30eiULtXNspoEP+GmuWmVEu+znzMWaKDWKdOsii4Cv1aWCRKSDDRzDBrZI2mP+Vm4HDQBdgDYRw4ehumMDtfaSjyJCnrk691bIM+wxzICuIEecg5kq5HcUPvo2mFyWPAEXb5xlXnuopYEBd7 Generated By NeoServer" +} + + +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" +} \ No newline at end of file diff --git a/terraform/modules/proxmox_ubuntu_cloudinit_template/main.tf b/terraform/modules/proxmox_ubuntu_cloudinit_template/main.tf new file mode 100644 index 0000000..1bc862d --- /dev/null +++ b/terraform/modules/proxmox_ubuntu_cloudinit_template/main.tf @@ -0,0 +1,126 @@ +locals { + selected_instance = one([ + for cfg in var.instance_configs : + cfg if cfg.crispy_name == var.node_name + ]) +} + +resource "proxmox_virtual_environment_file" "cloud_config" { + content_type = "snippets" + datastore_id = "local" + node_name = "pop" + + source_raw { + file_name = "vm.cloud-config.yaml" # The name of the snippet file + data = <<-EOF + #cloud-config + hostname: ${var.vm_name} + + package_update: true + package_upgrade: true + + system_info: + default_user: + groups: [ docker ] + + users: + - default + - name: cloud + groups: + - sudo + - docker + shell: /bin/bash + ssh-authorized-keys: + - "${var.vm_user_sshkey}" # Inject user's SSH key + sudo: ALL=(ALL) NOPASSWD:ALL + + packages: + - qemu-guest-agent + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + - unattended-upgrades + + runcmd: + - systemctl enable qemu-guest-agent + - mkdir -p /etc/apt/keyrings + - 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 + - systemctl enable docker + - systemctl start docker + - reboot + EOF + } +} + +resource "proxmox_virtual_environment_vm" "ubuntu_22_minimal_template" { + 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 + + disk { + datastore_id = var.node_datastore # Datastore to hold the disk + interface = "scsi0" # Primary disk interface + size = var.vm_size + file_id = "local:iso/ubuntu-22.04-minimal-cloudimg-amd64.img" + } + + efi_disk { + datastore_id = var.node_datastore + type = "4m" + } + + 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 + } + + 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 + } + + operating_system { + type = "l26" # Linux 2.6+ kernel + } + + lifecycle { + ignore_changes = [ # Ignore initialization section after first depoloyment for idempotency + initialization + ] + } +} + + + + + + + diff --git a/terraform/modules/proxmox_ubuntu_cloudinit_template/output.tf b/terraform/modules/proxmox_ubuntu_cloudinit_template/output.tf new file mode 100644 index 0000000..30d83b4 --- /dev/null +++ b/terraform/modules/proxmox_ubuntu_cloudinit_template/output.tf @@ -0,0 +1,3 @@ +output "vm_ipv4_address" { + value = proxmox_virtual_environment_vm.ubuntu_22_minimal_template.ipv4_addresses[1][0] +} \ No newline at end of file diff --git a/terraform/modules/proxmox_ubuntu_cloudinit_template/providers.tf b/terraform/modules/proxmox_ubuntu_cloudinit_template/providers.tf new file mode 100644 index 0000000..55d1be1 --- /dev/null +++ b/terraform/modules/proxmox_ubuntu_cloudinit_template/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.38.0" + } + } +} diff --git a/terraform/modules/proxmox_ubuntu_cloudinit_template/variables.tf b/terraform/modules/proxmox_ubuntu_cloudinit_template/variables.tf new file mode 100644 index 0000000..c5f1b5a --- /dev/null +++ b/terraform/modules/proxmox_ubuntu_cloudinit_template/variables.tf @@ -0,0 +1,137 @@ +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 "instance_configs" { + type = list(object({ + crispy_name = string + vmid = string + })) + default = [ + { crispy_name = "snap", vmid = "9002" }, + { crispy_name = "crackle", vmid = "9000" }, + { crispy_name = "pop", vmid = "9001" } + ] +} + +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 "vm_user_sshkey" { + description = "Admin user SSH key of the VM" + type = string + default = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDRlWaLBt/qmWY01Cd6jN/YxLnlT+6lg+evEdN/dIajirdTj1rCbAdlG3WYvo+4BpN17HK3/eGQpGUMbgI/8MVd8YPODcD34gaNX0w2v66BwHx+S6BZUpz5T2IoQT0JtSv/TtFICoff5gXdNRpfd4eWsmTioEqLA6oToJLE4dn3jvAzFi9y7fyLqvuoQMmPidYYJjGT30eiULtXNspoEP+GmuWmVEu+znzMWaKDWKdOsii4Cv1aWCRKSDDRzDBrZI2mP+Vm4HDQBdgDYRw4ehumMDtfaSjyJCnrk691bIM+wxzICuIEecg5kq5HcUPvo2mFyWPAEXb5xlXnuopYEBd7 Generated By NeoServer" +} + + +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" +} \ 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/output.tf b/terraform/output.tf index 79095bd..2330874 100644 --- a/terraform/output.tf +++ b/terraform/output.tf @@ -1,20 +1,41 @@ output "vm_ipv4_addresses" { + description = "Map of instance key to VM IPv4 address" 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, ".", "-")}"] - ) + for k, m in module.vm-n8n : k => m.vm_ipv4_address } } output "service_names" { + description = "Map of instance key to service name" value = { - for k, cfg in local.instance_map : k => cfg.service_name + 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/single.tfvars.example b/terraform/single.tfvars.example index 2b46128..7705723 100644 --- a/terraform/single.tfvars.example +++ b/terraform/single.tfvars.example @@ -1,21 +1,11 @@ instance_mode = "single" instance = { - service_name = "grafana" - vm_name = "grafana-01" + service_name = "n8n" + vm_name = "n8n-01" node_name = "pop" - app_port = 3000 - app_image = "grafana/grafana:latest" - vm_tags = ["monitoring"] + app_port = 5678 + app_image = "docker.n8n.io/n8nio/n8n" + vm_tags = ["agentic"] } -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 index 496ab55..7555c66 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -1,40 +1,44 @@ variable "instance_mode" { - type = string - default = "single" + 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" { - 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)) + 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 + default = null + nullable = true + description = "Used only when instance_mode = single." } 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)) + 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 = {} + default = {} + nullable = false + description = "Used only when instance_mode = multiple." } + variable "vm_defaults" { type = object({ node_datastore = string @@ -44,16 +48,19 @@ variable "vm_defaults" { bridge = string vm_cpu = number vm_ram = number - vm_size = string + vm_size = number }) default = { node_datastore = "hlst" vm_bios = "ovmf" vm_machine = "q35" - vm_user_sshkey = "" + 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 = 1 - vm_ram = 2048 - vm_size = "20G" + vm_cpu = 2 + vm_ram = 4096 + vm_size = 20 } } + +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" } diff --git a/workflows/deploy.yml b/workflows/deploy.yml new file mode 100644 index 0000000..69f81e5 --- /dev/null +++ b/workflows/deploy.yml @@ -0,0 +1,78 @@ +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 + 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" + + - 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: Run playbook + run: | + ansible-playbook ansible/playbooks/docker_copy.yml -i ansible/inventory/inventory.yml -u cloud --private-key ~/.ssh/vlans_rsa + + - 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 diff --git a/workflows/destroy_module.yml b/workflows/destroy_module.yml new file mode 100644 index 0000000..b6eaf38 --- /dev/null +++ b/workflows/destroy_module.yml @@ -0,0 +1,36 @@ +name: Terraform Destroy + +on: + workflow_dispatch: + inputs: + working_directory: + description: "Working directory for the module" + required: false + default: "terraform" + type: string + +jobs: + destroy: + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + working-directory: ${{ inputs.working_directory }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - 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