From 03c882f3113ee2dfd5a6b163ed81731a784d4ac5 Mon Sep 17 00:00:00 2001 From: Taqi Tahmid Date: Mon, 30 Jun 2025 19:16:14 +0300 Subject: [PATCH] infra: introduce terraform/opentofu for proxmox management - move ansible project within infra - introduce terraform/opentofu for proxmox VM management --- infra/ansible/.gitignore | 1 + infra/ansible/README.md | 110 ++++++++++++++++++ infra/ansible/ansible.cfg | 5 + infra/ansible/inventory/group_vars/all.yaml | 11 ++ infra/ansible/inventory/group_vars/vms.yaml | 22 ++++ .../ansible/inventory/host_vars/proxmox1.yaml | 29 +++++ .../ansible/inventory/host_vars/proxmox2.yaml | 29 +++++ infra/ansible/inventory/hosts.yaml | 51 ++++++++ infra/ansible/playbooks/configure-vms.yaml | 6 + .../playbooks/create-kubernetes-cluster.yaml | 6 + infra/ansible/playbooks/create-vms.yaml | 6 + infra/ansible/playbooks/destroy-vms.yaml | 6 + .../roles/configure-vms/tasks/main.yaml | 11 ++ .../create-kubernetes-cluster/tasks/main.yaml | 97 +++++++++++++++ .../templates/ipAddressPool.yaml.j2 | 8 ++ .../templates/k0sctl.yaml.j2 | 70 +++++++++++ .../ansible/roles/create-vms/tasks/main.yaml | 70 +++++++++++ .../ansible/roles/destroy-vms/tasks/main.yaml | 72 ++++++++++++ infra/terraform/.gitignore | 5 + infra/terraform/README.md | 25 ++++ infra/terraform/proxmox/backend.tf | 14 +++ infra/terraform/proxmox/main.tf | 95 +++++++++++++++ infra/terraform/proxmox/terraform.tfvars | 52 +++++++++ infra/terraform/proxmox/variables.tf | 81 +++++++++++++ 24 files changed, 882 insertions(+) create mode 100644 infra/ansible/.gitignore create mode 100644 infra/ansible/README.md create mode 100644 infra/ansible/ansible.cfg create mode 100644 infra/ansible/inventory/group_vars/all.yaml create mode 100644 infra/ansible/inventory/group_vars/vms.yaml create mode 100644 infra/ansible/inventory/host_vars/proxmox1.yaml create mode 100644 infra/ansible/inventory/host_vars/proxmox2.yaml create mode 100644 infra/ansible/inventory/hosts.yaml create mode 100644 infra/ansible/playbooks/configure-vms.yaml create mode 100644 infra/ansible/playbooks/create-kubernetes-cluster.yaml create mode 100644 infra/ansible/playbooks/create-vms.yaml create mode 100644 infra/ansible/playbooks/destroy-vms.yaml create mode 100644 infra/ansible/roles/configure-vms/tasks/main.yaml create mode 100644 infra/ansible/roles/create-kubernetes-cluster/tasks/main.yaml create mode 100644 infra/ansible/roles/create-kubernetes-cluster/templates/ipAddressPool.yaml.j2 create mode 100644 infra/ansible/roles/create-kubernetes-cluster/templates/k0sctl.yaml.j2 create mode 100644 infra/ansible/roles/create-vms/tasks/main.yaml create mode 100644 infra/ansible/roles/destroy-vms/tasks/main.yaml create mode 100644 infra/terraform/.gitignore create mode 100644 infra/terraform/README.md create mode 100644 infra/terraform/proxmox/backend.tf create mode 100644 infra/terraform/proxmox/main.tf create mode 100644 infra/terraform/proxmox/terraform.tfvars create mode 100644 infra/terraform/proxmox/variables.tf diff --git a/infra/ansible/.gitignore b/infra/ansible/.gitignore new file mode 100644 index 0000000..0f18981 --- /dev/null +++ b/infra/ansible/.gitignore @@ -0,0 +1 @@ +secrets/ \ No newline at end of file diff --git a/infra/ansible/README.md b/infra/ansible/README.md new file mode 100644 index 0000000..52c1297 --- /dev/null +++ b/infra/ansible/README.md @@ -0,0 +1,110 @@ +# Ansible Playbook for Proxmox VM Management + +This Ansible playbook automates the creation, deletion, and configuration of +virtual machines (VMs) on a Proxmox server. + +## Prerequisites + +- Ansible installed on the local machine +- Ansible community.general.proxmox_kvm module +- Access to a Proxmox server with API access enabled +- Python `proxmoxer` library installed (`pip install proxmoxer`) + +## Setup + +1. Clone this repository: + ```sh + git clone https://github.com/TheTaqiTahmid/proxmox_ansible_automation + ``` + +2. Update the `inventory` file with your Proxmox server details: + ```yaml + all: + hosts: + proxmox: + ansible_host: your_proxmox_ip + ansible_user: your_proxmox_user + ansible_password: your_proxmox_password + ``` + In the current example implementation in `inventories/hosts.yaml`, there are + multiple groups depending on the types of hosts. + +3. Add group-related variables to the group file under the `group_vars` directory + and individual host-related variables to the files under the `host_vars` + directory. Ansible will automatically pick up these variables. + +4. Add the following secrets to the ansible-vault: + - proxmox_api_token_id + - proxmox_api_token + - ansible_proxmox_user + - ansible_vm_user + - proxmox_user + - ansible_ssh_private_key_file + - ciuser + - cipassword + + One can create the secret file using the following command: + ```sh + ansible-vault create secrets/vault.yml + ``` + + To encrypt and decrypt the file, use the following commands: + ```sh + ansible-vault encrypt secrets/vault.yml + ansible-vault decrypt secrets/vault.yml + ``` + The password for vault file can be stored in a file or can be provided during + the encryption/decryption process. The password file location can be specified + in the `ansible.cfg` file. + +## Playbooks + +### Create VM + +To create the VMs, run the following command: +```sh +ansible-playbook playbooks/create-vms.yaml +``` +The playbook can be run against specific Proxmox instance using: +```sh +ansible-playbook playbooks/create-vms.yaml --limit proxmox1 +``` + +### Delete VM + +To delete existing VMs, run the following command: +```sh +ansible-playbook playbooks/destroy-vms.yaml +``` + +Similarly the destory playbook can be run against specific Proxmox instance using: +```sh +ansible-playbook playbooks/destroy-vms.yaml --limit proxmox1 +``` + +### Configure VM + +To configure an existing VM, run the following command: +```sh +ansible-playbook playbooks/configure-vms.yaml +``` + +The configuration can be limited to individual VMs using limits: +```sh +ansible-playbook playbooks/configure-vms.yaml --limit vm6 +``` + +## Variables + +The playbooks use the following variables, which can be customized in the +`group_vars/proxmox.yml` file: + +- `vm_id`: The ID of the VM +- `vm_name`: The name of the VM +- `vm_memory`: The amount of memory for the VM +- `vm_cores`: The number of CPU cores for the VM +- `vm_disk_size`: The size of the VM disk + +## Author + +- Taqi Tahmid (mdtaqitahmid@gmail.com) diff --git a/infra/ansible/ansible.cfg b/infra/ansible/ansible.cfg new file mode 100644 index 0000000..5a0d385 --- /dev/null +++ b/infra/ansible/ansible.cfg @@ -0,0 +1,5 @@ +[defaults] +inventory = ./inventory/hosts.yaml +roles_path = ./roles +host_key_checking = False +vault_password_file = ~/.ansible_vault_pass \ No newline at end of file diff --git a/infra/ansible/inventory/group_vars/all.yaml b/infra/ansible/inventory/group_vars/all.yaml new file mode 100644 index 0000000..347565c --- /dev/null +++ b/infra/ansible/inventory/group_vars/all.yaml @@ -0,0 +1,11 @@ +# Proxmox access related variables +proxmox_api_url: "192.168.1.121" + +# Cloud-init image related variables +image_url: "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img" +image_dest: "/tmp/cloud-image.img" +image_format: "qcow2" +storage_name: "local" + +# ansible venv +ansible_venv: "/home/taqi/.venv/ansible/bin/python" \ No newline at end of file diff --git a/infra/ansible/inventory/group_vars/vms.yaml b/infra/ansible/inventory/group_vars/vms.yaml new file mode 100644 index 0000000..0305a57 --- /dev/null +++ b/infra/ansible/inventory/group_vars/vms.yaml @@ -0,0 +1,22 @@ +apt_packages: + - curl + - vim + - htop + +# Kubernetes k0sctl configuration vars +master1_ip: "192.168.1.151" +master1_hostname: "vm6" +master2_ip: "192.168.1.161" +master2_hostname: "vm8" +worker1_ip: "192.168.1.152" +worker1_hostname: "vm7" +worker2_ip: "192.168.1.162" +worker2_hostname: "vm9" + +pod_CIDR: "10.244.0.0/16" +service_CIDR: "10.96.0.0/12" +metallb_ip_range: "192.168.1.201-192.168.1.220" + +k0s_version: "v1.33.2+k0s.0" +metallb_version: "0.15.2" +traefik_version: "36.2.0" \ No newline at end of file diff --git a/infra/ansible/inventory/host_vars/proxmox1.yaml b/infra/ansible/inventory/host_vars/proxmox1.yaml new file mode 100644 index 0000000..b65a766 --- /dev/null +++ b/infra/ansible/inventory/host_vars/proxmox1.yaml @@ -0,0 +1,29 @@ +# VM related variables +vm_list: + - id: 106 + name: "vm6" + memory: 4096 + cores: 2 + disk_size: 30G + ip: "192.168.1.151/24" + gateway: "192.168.1.1" + nameserver1: "192.168.1.145" + nameserver2: "1.1.1.1" + - id: 107 + name: "vm7" + memory: 4096 + cores: 2 + disk_size: 30G + ip: "192.168.1.152/24" + gateway: "192.168.1.1" + nameserver1: "192.168.1.145" + nameserver2: "1.1.1.1" + +# cloud-init variables +node: "homeserver1" +net0: "virtio,bridge=vmbr0" +# disk_name: "local:1000/vm-1000-disk-0.raw,discard=on" +disk_path: "/var/lib/vz/images/1000" +ide2: "local:cloudinit,format=qcow2" +boot_order: "order=scsi0" +scsi_hw: "virtio-scsi-pci" diff --git a/infra/ansible/inventory/host_vars/proxmox2.yaml b/infra/ansible/inventory/host_vars/proxmox2.yaml new file mode 100644 index 0000000..e0bf2bd --- /dev/null +++ b/infra/ansible/inventory/host_vars/proxmox2.yaml @@ -0,0 +1,29 @@ +# VM related variables +vm_list: + - id: 206 + name: "vm8" + memory: 4096 + cores: 2 + disk_size: 30G + ip: "192.168.1.161/24" + gateway: "192.168.1.1" + nameserver1: "192.168.1.145" + nameserver2: "1.1.1.1" + - id: 207 + name: "vm9" + memory: 4096 + cores: 2 + disk_size: 30G + ip: "192.168.1.162/24" + gateway: "192.168.1.1" + nameserver1: "192.168.1.145" + nameserver2: "1.1.1.1" + +# cloud-init template variables +node: "homeserver2" +net0: "virtio,bridge=vmbr0" +# disk_name: "local:2000/vm-2000-disk-0.raw,discard=on" +disk_path: "/var/lib/vz/images/2000" +ide2: "local:cloudinit,format=qcow2" +boot_order: "order=scsi0" +scsi_hw: "virtio-scsi-pci" diff --git a/infra/ansible/inventory/hosts.yaml b/infra/ansible/inventory/hosts.yaml new file mode 100644 index 0000000..c184f8d --- /dev/null +++ b/infra/ansible/inventory/hosts.yaml @@ -0,0 +1,51 @@ +all: + children: + hypervisors: + vms: + +hypervisors: + children: + server1: + server2: + +server1: + hosts: + proxmox1: + ansible_host: 192.168.1.121 + ansible_user: "{{ ansible_proxmox_user }}" + ansible_ssh_private_key_file: "{{ ansible_ssh_private_key_file }}" + +server2: + hosts: + proxmox2: + ansible_host: 192.168.1.122 + ansible_user: "{{ ansible_proxmox_user }}" + ansible_ssh_private_key_file: "{{ ansible_ssh_private_key_file }}" + +vms: + children: + vm_group_1: + vm_group_2: + +vm_group_1: + hosts: + vm6: + ansible_host: 192.168.1.151 + ansible_user: "{{ ansible_vm_user }}" + ansible_ssh_private_key_file: "{{ ansible_ssh_private_key_file }}" + vm7: + ansible_host: 192.168.1.152 + ansible_user: "{{ ansible_vm_user }}" + ansible_ssh_private_key_file: "{{ ansible_ssh_private_key_file }}" + +vm_group_2: + hosts: + vm8: + ansible_host: 192.168.1.161 + ansible_user: "{{ ansible_vm_user }}" + ansible_ssh_private_key_file: "{{ ansible_ssh_private_key_file }}" + vm9: + ansible_host: 192.168.1.162 + ansible_user: "{{ ansible_vm_user }}" + ansible_ssh_private_key_file: "{{ ansible_ssh_private_key_file }}" + diff --git a/infra/ansible/playbooks/configure-vms.yaml b/infra/ansible/playbooks/configure-vms.yaml new file mode 100644 index 0000000..5b65de6 --- /dev/null +++ b/infra/ansible/playbooks/configure-vms.yaml @@ -0,0 +1,6 @@ +- name: Configure Proxmox VMs + hosts: vms + vars_files: + - ../secrets/vault.yaml # Load the encrypted vault file + roles: + - configure-vms \ No newline at end of file diff --git a/infra/ansible/playbooks/create-kubernetes-cluster.yaml b/infra/ansible/playbooks/create-kubernetes-cluster.yaml new file mode 100644 index 0000000..0dc1d29 --- /dev/null +++ b/infra/ansible/playbooks/create-kubernetes-cluster.yaml @@ -0,0 +1,6 @@ +- name: Create Kubernetes Cluster + hosts: vms + vars_files: + - ../secrets/vault.yaml + roles: + - create-kubernetes-cluster diff --git a/infra/ansible/playbooks/create-vms.yaml b/infra/ansible/playbooks/create-vms.yaml new file mode 100644 index 0000000..a61b0b8 --- /dev/null +++ b/infra/ansible/playbooks/create-vms.yaml @@ -0,0 +1,6 @@ +- name: Create Proxmox VMs + hosts: hypervisors + vars_files: + - ../secrets/vault.yaml # Load the encrypted vault file + roles: + - create-vms diff --git a/infra/ansible/playbooks/destroy-vms.yaml b/infra/ansible/playbooks/destroy-vms.yaml new file mode 100644 index 0000000..cb3e266 --- /dev/null +++ b/infra/ansible/playbooks/destroy-vms.yaml @@ -0,0 +1,6 @@ +- name: Destroy Proxmox VMs + hosts: hypervisors + vars_files: + - ../secrets/vault.yaml # Load the encrypted vault file + roles: + - destroy-vms \ No newline at end of file diff --git a/infra/ansible/roles/configure-vms/tasks/main.yaml b/infra/ansible/roles/configure-vms/tasks/main.yaml new file mode 100644 index 0000000..48026ba --- /dev/null +++ b/infra/ansible/roles/configure-vms/tasks/main.yaml @@ -0,0 +1,11 @@ +--- +- name: Update apt cache + ansible.builtin.apt: + update_cache: yes + become: true + +- name: Install necessary packages + ansible.builtin.apt: + name: "{{ apt_packages }}" + state: present + become: true diff --git a/infra/ansible/roles/create-kubernetes-cluster/tasks/main.yaml b/infra/ansible/roles/create-kubernetes-cluster/tasks/main.yaml new file mode 100644 index 0000000..30ab720 --- /dev/null +++ b/infra/ansible/roles/create-kubernetes-cluster/tasks/main.yaml @@ -0,0 +1,97 @@ +- name: Remove known_hosts file if it exists + delegate_to: localhost + run_once: true + ansible.builtin.file: + path: /home/taqi/.ssh/known_hosts + state: absent + +- name: Remove k0ctl lock file if it exists + ansible.builtin.file: + path: /run/lock/k0sctl + state: absent + become: true + +- name: Install k0sctl on host + delegate_to: localhost + ansible.builtin.command: + cmd: "go install github.com/k0sproject/k0sctl@latest" + +- name: Ensure k0sctl is installed on host + delegate_to: localhost + run_once: true + ansible.builtin.command: + cmd: "k0sctl version" + register: k0sctl_version + changed_when: false + +- name: Generate k0sctl configuration file + delegate_to: localhost + run_once: true + ansible.builtin.template: + src: k0sctl.yaml.j2 + dest: /tmp/k0sctl.yaml + when: k0sctl_version is defined + tags: + - generate-k0sctl-config + +- name: Generate MetalLB IP Address Pool configuration file + delegate_to: localhost + run_once: true + ansible.builtin.template: + src: ipAddressPool.yaml.j2 + dest: /tmp/ipAddressPool.yaml + when: k0sctl_version is defined + tags: + - generatemetallb-ippool + - metallb-ippool + +- name: Create Cluster using k0sctl from host + delegate_to: localhost + run_once: true + ansible.builtin.command: + cmd: "k0sctl apply --config /tmp/k0sctl.yaml" + when: k0sctl_version is defined + +- name: Save kubeconfig file on host + delegate_to: localhost + run_once: true + ansible.builtin.shell: + cmd: "cd /tmp && k0sctl kubeconfig > /home/taqi/.kube/k0s_config.yaml" + register: kubeconfig_result + retries: 3 + delay: 5 + until: kubeconfig_result.rc == 0 + when: k0sctl_version is defined + tags: + - generate-kubeconfig + +- name: Apply IP Pool for MetalLB from host + delegate_to: localhost + run_once: true + ansible.builtin.shell: + cmd: "kubectl apply -f /tmp/ipAddressPool.yaml --kubeconfig /home/taqi/.kube/k0s_config.yaml" + register: metallb_ippool_result + retries: 3 + delay: 5 + until: metallb_ippool_result.rc == 0 + when: k0sctl_version is defined + tags: + - metallb-ippool + +- name: Cleanup temporary files + delegate_to: localhost + run_once: true + block: + - name: Remove k0sctl.yaml temporary file + ansible.builtin.file: + path: /tmp/k0sctl.yaml + state: absent + + - name: Remove ipAddressPool.yaml temporary file + ansible.builtin.shell: + cmd: "rm -f /tmp/ipAddressPool.yaml" + delegate_to: localhost + run_once: true + tags: + - cleanup + when: k0sctl_version is defined \ No newline at end of file diff --git a/infra/ansible/roles/create-kubernetes-cluster/templates/ipAddressPool.yaml.j2 b/infra/ansible/roles/create-kubernetes-cluster/templates/ipAddressPool.yaml.j2 new file mode 100644 index 0000000..c0d1ae7 --- /dev/null +++ b/infra/ansible/roles/create-kubernetes-cluster/templates/ipAddressPool.yaml.j2 @@ -0,0 +1,8 @@ +apiVersion: metallb.io/v1beta1 +kind: IPAddressPool +metadata: + name: first-pool + namespace: metallb-system +spec: + addresses: + - "{{ metallb_ip_range }}" \ No newline at end of file diff --git a/infra/ansible/roles/create-kubernetes-cluster/templates/k0sctl.yaml.j2 b/infra/ansible/roles/create-kubernetes-cluster/templates/k0sctl.yaml.j2 new file mode 100644 index 0000000..60449e9 --- /dev/null +++ b/infra/ansible/roles/create-kubernetes-cluster/templates/k0sctl.yaml.j2 @@ -0,0 +1,70 @@ +apiVersion: k0sctl.k0sproject.io/v1beta1 +kind: Cluster +metadata: + name: k0s-cluster +spec: + hosts: + - ssh: + address: "{{ master1_ip }}" + user: "{{ ansible_vm_user }}" + keyPath: "{{ ansible_ssh_private_key_file }}" + role: controller+worker + hostname: "{{ master1_hostname }}" + noTaints: true + - ssh: + address: "{{ master2_ip }}" + user: "{{ ansible_vm_user }}" + keyPath: "{{ ansible_ssh_private_key_file }}" + role: controller+worker + hostname: "{{ master2_hostname }}" + noTaints: true + - ssh: + address: "{{ worker1_ip }}" + user: "{{ ansible_vm_user }}" + keyPath: "{{ ansible_ssh_private_key_file }}" + role: worker + hostname: "{{ worker1_hostname }}" + - ssh: + address: "{{ worker2_ip }}" + user: "{{ ansible_vm_user }}" + keyPath: "{{ ansible_ssh_private_key_file }}" + role: worker + hostname: "{{ worker2_hostname }}" + k0s: + version: "{{ k0s_version }}" + config: + spec: + api: + address: "{{ master1_ip }}" + port: 6443 + k0sApiPort: 9443 + sans: + - "{{ master1_ip }}" + - "{{ master2_ip }}" + - k8s.local + - api.k8s.local + network: + kubeProxy: + mode: iptables + kuberouter: + disabled: false + podCIDR: "{{ pod_CIDR }}" + serviceCIDR: "{{ service_CIDR }}" + provider: kuberouter + extensions: + helm: + concurrencyLevel: 5 + repositories: + - name: metallb + url: https://metallb.github.io/metallb + - name: traefik + url: https://traefik.github.io/charts + charts: + - name: metallb + chartname: metallb/metallb + version: "{{ metallb_version }}" + namespace: metallb-system + - name: traefik + chartname: traefik/traefik + version: "{{ traefik_version }}" + namespace: traefik-system diff --git a/infra/ansible/roles/create-vms/tasks/main.yaml b/infra/ansible/roles/create-vms/tasks/main.yaml new file mode 100644 index 0000000..7503204 --- /dev/null +++ b/infra/ansible/roles/create-vms/tasks/main.yaml @@ -0,0 +1,70 @@ +--- +- name: Download cloud image + get_url: + url: "{{ image_url }}" + dest: "{{ image_dest }}" + use_netrc: yes + +- name: create VMs + delegate_to: localhost + vars: + ansible_python_interpreter: /home/taqi/.venv/ansible/bin/python + community.general.proxmox_kvm: + api_host: "{{ proxmox_api_url }}" + api_user: "{{ proxmox_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token }}" + node: "{{ node }}" + vmid: "{{ item.id }}" + name: "{{ item.name }}" + memory: "{{ item.memory }}" + cores: "{{ item.cores }}" + scsihw: "{{ scsi_hw }}" + boot: "{{ boot_order }}" + net: + net0: "{{ net0 }}" + ipconfig: + ipconfig0: "ip={{ item.ip }},gw={{ item.gateway }}" + ide: + ide2: "{{ ide2 }}" + nameservers: "{{ item.nameserver1 }},{{ item.nameserver2 }}" + ciuser: "{{ ciuser }}" + cipassword: "{{ cipassword }}" + sshkeys: "{{ lookup('file', '/home/taqi/.ssh/homeserver.pub') }}" + loop: "{{ vm_list }}" + +- name: Import disk image + ansible.builtin.shell: | + qm importdisk "{{ item.id }}" "{{ image_dest }}" "{{ storage_name }}" --format "{{ image_format }}" + loop: "{{ vm_list }}" + +- name: Attach disk to VM + ansible.builtin.shell: | + qm set "{{ item.id }}" --scsi0 "{{ storage_name }}:{{ item.id }}/vm-{{ item.id }}-disk-0.{{ image_format }},discard=on" + loop: "{{ vm_list }}" + +- name: Resize disk + ansible.builtin.shell: | + qm resize {{ item.id }} scsi0 {{ item.disk_size }} + loop: "{{ vm_list }}" + +- name: Start VMs + delegate_to: localhost + vars: + ansible_python_interpreter: /home/taqi/.venv/ansible/bin/python + community.general.proxmox_kvm: + api_host: "{{ proxmox_api_url }}" + api_user: "{{ proxmox_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token }}" + node: "{{ node }}" + name: "{{ item.name }}" + state: started + loop: "{{ vm_list }}" + tags: + - start_vms + +- name: Clean up downloaded image + file: + path: "{{ image_dest }}" + state: absent \ No newline at end of file diff --git a/infra/ansible/roles/destroy-vms/tasks/main.yaml b/infra/ansible/roles/destroy-vms/tasks/main.yaml new file mode 100644 index 0000000..bd4b9b3 --- /dev/null +++ b/infra/ansible/roles/destroy-vms/tasks/main.yaml @@ -0,0 +1,72 @@ +- name: Get VM current state + delegate_to: localhost + vars: + ansible_python_interpreter: "{{ ansible_venv }}" + community.general.proxmox_kvm: + api_host: "{{ proxmox_api_url }}" + api_user: "{{ proxmox_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token }}" + name: "{{ item.name }}" + node: "{{ node }}" + state: current + register: vm_state + ignore_errors: yes + loop: "{{ vm_list }}" + loop_control: + index_var: vm_index + tags: + - vm_delete + +- name: Debug VM state + debug: + msg: "VM {{ item.name }} state: {{ vm_state.results[vm_index].status }}" + when: vm_state.results[vm_index] is defined and vm_state.results[vm_index] is succeeded + loop: "{{ vm_list }}" + loop_control: + index_var: vm_index + +- name: Stop VM + delegate_to: localhost + vars: + ansible_python_interpreter: "{{ ansible_venv }}" + community.general.proxmox_kvm: + api_host: "{{ proxmox_api_url }}" + api_user: "{{ proxmox_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token }}" + name: "{{ item.name }}" + node: "{{ node }}" + state: stopped + force: true + when: > + vm_state.results[vm_index] is defined and + vm_state.results[vm_index] is succeeded and + vm_state.results[vm_index].status != 'absent' + loop: "{{ vm_list }}" + loop_control: + index_var: vm_index + tags: + - vm_delete + +- name: Delete VM + delegate_to: localhost + vars: + ansible_python_interpreter: "{{ ansible_venv }}" + community.general.proxmox_kvm: + api_host: "{{ proxmox_api_url }}" + api_user: "{{ proxmox_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token }}" + name: "{{ item.name }}" + node: "{{ node }}" + state: absent + when: > + vm_state.results[vm_index] is defined and + vm_state.results[vm_index] is succeeded and + vm_state.results[vm_index].status != 'absent' + loop: "{{ vm_list }}" + loop_control: + index_var: vm_index + tags: + - vm_delete diff --git a/infra/terraform/.gitignore b/infra/terraform/.gitignore new file mode 100644 index 0000000..2e5cea7 --- /dev/null +++ b/infra/terraform/.gitignore @@ -0,0 +1,5 @@ +.env +.terraform/ +.terraform.lock.hcl +terraform.tfstate +terraform.tfstate.backup diff --git a/infra/terraform/README.md b/infra/terraform/README.md new file mode 100644 index 0000000..b999854 --- /dev/null +++ b/infra/terraform/README.md @@ -0,0 +1,25 @@ +# Terraform Configuration + +> This project uses OpenTofu instead of Terraform. OpenTofu is a fork of +> Terraform that is compatible with Terraform configurations and provides +> similar functionality. + +This directory contains Terraform configurations for managing +infrastructure resources. It includes configurations for Proxmox. + +The plan is to eventually migrate all infrastructure management to Terraform, +including Kubernetes clusters and other resources. Currently, the Proxmox +configuration is fully managed by Terraform, while Kubernetes resources are +managed using Helm charts and kubectl commands. Previously, the Proxmox +configuration was managed using Ansible, but it has been migrated to Terraform +for better consistency and state management. + +The terraform state files are stored in a remote backend, which allows for +collaboration and state management across different environments. The backend +configuration is defined in the `backend.tf` file. The backend is set up to use +minio as the storage backend. + +## Proxmox + +The Proxmox configuration is located in the `proxmox` directory. +It uses the Proxmox provider to manage virtual machines and other resources. diff --git a/infra/terraform/proxmox/backend.tf b/infra/terraform/proxmox/backend.tf new file mode 100644 index 0000000..dbe613a --- /dev/null +++ b/infra/terraform/proxmox/backend.tf @@ -0,0 +1,14 @@ +terraform { + backend "s3" { + bucket = "terraform-state" # Name of the MinIO bucket + key = "proxmox/terraform.tfstate" # Path to the state file in the bucket + endpoint = var.minio_endpoint # MinIO API endpoint + access_key = var.minio_access_key # MinIO access key + secret_key = var.minio_secret_key # MinIO secret key + region = "us-east-1" # Arbitrary region (MinIO ignores this) + skip_credentials_validation = true # Skip AWS-specific credential checks + skip_metadata_api_check = true # Skip AWS metadata API checks + skip_region_validation = true # Skip AWS region validation + use_path_style = true # Use path-style URLs[](http:///) + } +} \ No newline at end of file diff --git a/infra/terraform/proxmox/main.tf b/infra/terraform/proxmox/main.tf new file mode 100644 index 0000000..a143966 --- /dev/null +++ b/infra/terraform/proxmox/main.tf @@ -0,0 +1,95 @@ +terraform { + required_providers { + proxmox = { + source = "bpg/proxmox" + version = "0.78.2" + } + } +} + +provider "proxmox" { + endpoint = var.pm_api_url + api_token = var.pm_api_token + insecure = var.pm_insecure + + ssh { + agent = false + username = var.pm_user + private_key = file(var.pm_ssh_private_key_path) + } +} + +data "local_file" "ssh_public_key" { + filename = var.pm_ssh_public_key_path +} + +resource "proxmox_virtual_environment_vm" "ubuntu_vm" { + for_each = { for vm in var.vms : vm.name => vm } + + name = each.value.name + node_name = each.value.node_name + vm_id = each.value.vm_id + + stop_on_destroy = true + keyboard_layout = "fi" + + initialization { + ip_config { + ipv4 { + address = each.value.ip_address + gateway = each.value.gateway + } + } + + dns { + servers = each.value.dns_servers + } + + datastore_id = "local" + + user_account { + username = var.vm_user_name + password = var.vm_user_password + keys = [trimspace(data.local_file.ssh_public_key.content)] + } + } + + cpu { + cores = each.value.cores + sockets = 1 + } + + memory { + dedicated = each.value.memory + } + + disk { + datastore_id = "local" + file_id = proxmox_virtual_environment_download_file.ubuntu_cloud_image[each.value.node_name].id + interface = "virtio0" + iothread = true + discard = "on" + size = each.value.disk_size + } + + network_device { + bridge = "vmbr0" + } + depends_on = [proxmox_virtual_environment_download_file.ubuntu_cloud_image] +} + +resource "proxmox_virtual_environment_download_file" "ubuntu_cloud_image" { + for_each = toset(var.nodes) + content_type = "iso" + datastore_id = "local" + node_name = each.key + + url = "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img" +} + +data "proxmox_virtual_environment_hosts" "hosts" { + for_each = toset(var.nodes) + node_name = each.key + + depends_on = [proxmox_virtual_environment_vm.ubuntu_vm] +} \ No newline at end of file diff --git a/infra/terraform/proxmox/terraform.tfvars b/infra/terraform/proxmox/terraform.tfvars new file mode 100644 index 0000000..eeb49b4 --- /dev/null +++ b/infra/terraform/proxmox/terraform.tfvars @@ -0,0 +1,52 @@ +# Proxmox configuration = +pm_ssh_public_key_path = "/home/taqi/.ssh/homeserver.pub" +pm_ssh_private_key_path = "/home/taqi/.ssh/homeserver" + +vms = [ + { + name = "vm6" + node_name = "homeserver1" + vm_id = 105 + ip_address = "192.168.1.151/24" + gateway = "192.168.1.1" + dns_servers = ["192.168.1.145", "1.1.1.1"] + cores = 2 + memory = 2048 + disk_size = 20 + }, + { + name = "vm7" + node_name = "homeserver1" + vm_id = 106 + ip_address = "192.168.1.152/24" + gateway = "192.168.1.1" + dns_servers = ["192.168.1.145", "1.1.1.1"] + cores = 2 + memory = 2048 + disk_size = 20 + }, + { + name = "vm8" + node_name = "homeserver2" + vm_id = 205 + ip_address = "192.168.1.161/24" + gateway = "192.168.1.1" + dns_servers = ["192.168.1.145", "1.1.1.1"] + cores = 2 + memory = 2048 + disk_size = 20 + }, + { + name = "vm9" + node_name = "homeserver2" + vm_id = 206 + ip_address = "192.168.1.162/24" + gateway = "192.168.1.1" + dns_servers = ["192.168.1.145", "1.1.1.1"] + cores = 2 + memory = 2048 + disk_size = 20 + } +] + +nodes = ["homeserver1", "homeserver2"] \ No newline at end of file diff --git a/infra/terraform/proxmox/variables.tf b/infra/terraform/proxmox/variables.tf new file mode 100644 index 0000000..2caf3a3 --- /dev/null +++ b/infra/terraform/proxmox/variables.tf @@ -0,0 +1,81 @@ +# variables for minio backend configuration +variable "minio_access_key" { + description = "MinIO access key" + type = string +} + +variable "minio_secret_key" { + description = "MinIO secret key" + type = string +} + +variable "minio_endpoint" { + description = "MinIO API endpoint" + type = string +} + +# Variables for Proxmox configuration +variable "pm_api_url" { + description = "Proxmox API URL" + type = string + sensitive = true +} + +variable "pm_api_token" { + description = "Proxmox password" + type = string + sensitive = true +} + +variable "pm_insecure" { + description = "Allow insecure connections to Proxmox API" + type = bool + default = true +} + +variable "pm_user" { + description = "Proxmox user" + type = string + sensitive = true +} + +variable "pm_ssh_public_key_path" { + description = "Path to SSH public key file" + type = string +} + +variable "pm_ssh_private_key_path" { + description = "Path to SSH private key file" + type = string +} + +variable "vms" { + description = "List of VMs to create" + type = list(object({ + name = string + node_name = string + vm_id = number + ip_address = string + dns_servers = list(string) + gateway = string + cores = number + memory = number + disk_size = number + })) +} + +variable "nodes" { + type = list(string) + default = ["homeserver1", "homeserver2"] +} + +variable "vm_user_name" { + description = "Username for the VM" + type = string +} + +variable "vm_user_password" { + description = "Password for the VM user" + type = string + sensitive = true +} \ No newline at end of file