├── .gitignore ├── README.md ├── ansible ├── ansible.cfg ├── files │ ├── authorized_keys │ └── sshd_config_secured ├── install-qemu-guest-agent.yaml ├── provision.yaml └── upgrade.yaml └── tf ├── ct ├── main.tf ├── var.tf └── versions.tf └── vm ├── main.tf ├── var.tf └── versions.tf /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | 11 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 12 | # .tfvars files are managed as part of configuration and so should be included in 13 | # version control. 14 | # 15 | # example.tfvars 16 | 17 | # Ignore override files as they are usually used to override resources locally and so 18 | # are not checked in 19 | override.tf 20 | override.tf.json 21 | *_override.tf 22 | *_override.tf.json 23 | 24 | # Include override files you do wish to add to version control using negated pattern 25 | # 26 | # !example_override.tf 27 | 28 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 29 | # example: *tfplan* 30 | 31 | *plan* 32 | *.lock.hcl 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # proxmox-automation 2 | 3 | Automations for Proxmox using Terraform and Ansible. Can be used to setup and provision containers and virtual machines. Read [my post](https://vanmieghem.io/automating-proxmox-with-terraform-ansible/) for more information. 4 | 5 | ### Usage 6 | 7 | 1. Clone the repo and `cd proxmox-automation/tf/ct` (for a container) 8 | 2. Install `ansible` and `terraform` (on a Mac `brew install ansible terraform`) 9 | 3. Configure the variables in `var.tf` and add your public keys to `ansible/files/authorized_keys`. To provision multiple resources, add more hostnames and IP addresses to the defined list in `var.tf`. 10 | 4. `export PM_PASS='your-PVE-password'` 11 | 5. `terraform init` (this should pull in the Terraform Proxmox provider and configure the Terraform project) 12 | 6. `terraform plan -out plan` 13 | 7. `terraform apply` 14 | 15 | 16 | 17 | ### Creating a cloud-init VM template 18 | 19 | On the pve host: 20 | 21 | 1. `apt install cloud-init` 22 | 2. Create a template VM (in this case Ubuntu 20.04): 23 | ``` 24 | wget http://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img 25 | export VM_ID="9000" 26 | qm create 9000 --memory 2048 --net0 virtio,bridge=vmbr0 --sockets 1 --cores 2 --vcpu 2 -hotplug network,disk,cpu,memory --agent 1 --name cloud-init-focal --ostype l26 27 | qm importdisk $VM_ID focal-server-cloudimg-amd64.img local-lvm 28 | qm set $VM_ID --scsihw virtio-scsi-pci --virtio0 local-lvm:vm-$VM_ID-disk-0 29 | qm set $VM_ID --ide2 local-lvm:cloudinit 30 | qm set $VM_ID --boot c --bootdisk virtio0 31 | qm set $VM_ID --serial0 socket 32 | qm template $VM_ID 33 | rm focal-server-cloudimg-amd64.img 34 | ``` 35 | 36 | -------------------------------------------------------------------------------- /ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | host_key_checking = false 3 | ssh_timeout = 20 -------------------------------------------------------------------------------- /ansible/files/authorized_keys: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIM... key 2 | -------------------------------------------------------------------------------- /ansible/files/sshd_config_secured: -------------------------------------------------------------------------------- 1 | # Package generated configuration file 2 | # See the sshd_config(5) manpage for details 3 | 4 | # What ports, IPs and protocols we listen for 5 | Port 22 6 | # Use these options to restrict which interfaces/protocols sshd will bind to 7 | #ListenAddress :: 8 | #ListenAddress 0.0.0.0 9 | Protocol 2 10 | # HostKeys for protocol version 2 11 | HostKey /etc/ssh/ssh_host_rsa_key 12 | HostKey /etc/ssh/ssh_host_dsa_key 13 | HostKey /etc/ssh/ssh_host_ecdsa_key 14 | HostKey /etc/ssh/ssh_host_ed25519_key 15 | #Privilege Separation is turned on for security 16 | UsePrivilegeSeparation yes 17 | 18 | # Lifetime and size of ephemeral version 1 server key 19 | KeyRegenerationInterval 3600 20 | ServerKeyBits 1024 21 | 22 | # Logging 23 | SyslogFacility AUTH 24 | LogLevel INFO 25 | 26 | # Authentication: 27 | LoginGraceTime 120 28 | #PermitRootLogin no 29 | StrictModes yes 30 | 31 | RSAAuthentication yes 32 | PubkeyAuthentication yes 33 | #AuthorizedKeysFile %h/.ssh/authorized_keys 34 | 35 | # Don't read the user's ~/.rhosts and ~/.shosts files 36 | IgnoreRhosts yes 37 | # For this to work you will also need host keys in /etc/ssh_known_hosts 38 | RhostsRSAAuthentication no 39 | # similar for protocol version 2 40 | HostbasedAuthentication no 41 | # Uncomment if you don't trust ~/.ssh/known_hosts for RhostsRSAAuthentication 42 | #IgnoreUserKnownHosts yes 43 | 44 | # To enable empty passwords, change to yes (NOT RECOMMENDED) 45 | PermitEmptyPasswords no 46 | 47 | # Change to yes to enable challenge-response passwords (beware issues with 48 | # some PAM modules and threads) 49 | ChallengeResponseAuthentication no 50 | 51 | # Change to no to disable tunnelled clear text passwords 52 | PasswordAuthentication no 53 | 54 | # Kerberos options 55 | #KerberosAuthentication no 56 | #KerberosGetAFSToken no 57 | #KerberosOrLocalPasswd yes 58 | #KerberosTicketCleanup yes 59 | 60 | # GSSAPI options 61 | #GSSAPIAuthentication no 62 | #GSSAPICleanupCredentials yes 63 | 64 | X11Forwarding yes 65 | X11DisplayOffset 10 66 | PrintMotd no 67 | PrintLastLog yes 68 | TCPKeepAlive yes 69 | #UseLogin no 70 | 71 | #MaxStartups 10:30:60 72 | #Banner /etc/issue.net 73 | 74 | # Allow client to pass locale environment variables 75 | AcceptEnv LANG LC_* 76 | 77 | Subsystem sftp /usr/lib/openssh/sftp-server 78 | 79 | # Set this to 'yes' to enable PAM authentication, account processing, 80 | # and session processing. If this is enabled, PAM authentication will 81 | # be allowed through the ChallengeResponseAuthentication and 82 | # PasswordAuthentication. Depending on your PAM configuration, 83 | # PAM authentication via ChallengeResponseAuthentication may bypass 84 | # the setting of "PermitRootLogin without-password". 85 | # If you just want the PAM account and session checks to run without 86 | # PAM authentication, then enable this but set PasswordAuthentication 87 | # and ChallengeResponseAuthentication to 'no'. 88 | UsePAM yes 89 | -------------------------------------------------------------------------------- /ansible/install-qemu-guest-agent.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | gather_facts: false 4 | become: yes 5 | 6 | tasks: 7 | - name: update apt cache 8 | command: apt update 9 | 10 | - name: add universe repo 11 | command: add-apt-repository universe 12 | 13 | - name: update apt cache 14 | command: apt update 15 | 16 | - name: install qemu guest agent 17 | apt: 18 | name: qemu-guest-agent 19 | state: present 20 | 21 | - name: upgrade all packages 22 | command: apt upgrade -y 23 | 24 | - name: reboot host 25 | reboot: 26 | -------------------------------------------------------------------------------- /ansible/provision.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | become: yes 4 | tasks: 5 | - name: Update and upgrade apt packages 6 | become: yes 7 | ignore_errors: yes 8 | failed_when: "'FAILED' in command_result.stderr" 9 | apt: 10 | upgrade: yes 11 | update_cache: yes 12 | cache_valid_time: 86400 #One day 13 | 14 | - name: Make sure we have a 'wheel' group 15 | group: 16 | name: wheel 17 | state: present 18 | 19 | - name: Allow 'wheel' group to have passwordless sudo 20 | lineinfile: 21 | dest: /etc/sudoers 22 | state: present 23 | regexp: '^%wheel' 24 | line: '%wheel ALL=(ALL) NOPASSWD: ALL' 25 | validate: 'visudo -cf %s' 26 | 27 | - name: Add sudoers users to wheel group 28 | user: 29 | name: notroot 30 | groups: wheel 31 | append: yes 32 | state: present 33 | createhome: yes 34 | shell: /bin/bash 35 | 36 | - name: Create necessary folders 37 | file: 38 | path: "{{ item }}" 39 | recurse: yes 40 | state: directory 41 | with_items: 42 | - /home/notroot/.ssh/ 43 | 44 | - name: Copy Secured SSHD Configuration 45 | copy: src=sshd_config_secured dest=/etc/ssh/sshd_config owner=root group=root mode=0644 46 | #sudo: yes 47 | 48 | - name: Disable IPv6 49 | sysctl: 50 | name: "{{ item }}" 51 | value: '1' 52 | sysctl_set: yes 53 | state: present 54 | with_items: 55 | - net.ipv6.conf.all.disable_ipv6 56 | - net.ipv6.conf.default.disable_ipv6 57 | - net.ipv6.conf.lo.disable_ipv6 58 | 59 | - name: SSHD Restart 60 | service: name=sshd state=restarted enabled=yes 61 | #sudo: yes 62 | 63 | - name: Copy keys and profiles 64 | copy: 65 | src: "{{ item.src }}" 66 | dest: "{{ item.dest }}" 67 | owner: notroot 68 | group: notroot 69 | mode: 0400 70 | with_items: 71 | - { src: 'authorized_keys', dest: '/home/notroot/.ssh/authorized_keys' } -------------------------------------------------------------------------------- /ansible/upgrade.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: lxc-all 3 | become: yes 4 | tasks: 5 | - name: Update and upgrade apt packages 6 | become: true 7 | apt: 8 | upgrade: yes 9 | update_cache: yes 10 | cache_valid_time: 86400 #One day -------------------------------------------------------------------------------- /tf/ct/main.tf: -------------------------------------------------------------------------------- 1 | provider "proxmox" { 2 | pm_api_url = var.proxmox_host["pm_api_url"] 3 | pm_user = var.proxmox_host["pm_user"] 4 | pm_tls_insecure = true 5 | } 6 | 7 | 8 | resource "proxmox_lxc" "container" { 9 | count = length(var.hostnames) 10 | hostname = var.hostnames[count.index] 11 | target_node = var.proxmox_host["target_node"] 12 | vmid = var.vmid + count.index 13 | cores = 1 14 | memory = 2048 15 | 16 | ostemplate = "local:vztmpl/ubuntu-20.04-standard_20.04-1_amd64.tar.gz" 17 | ostype = "ubuntu" 18 | unprivileged = "true" 19 | start = "true" 20 | 21 | 22 | rootfs { 23 | storage = "local-lvm" 24 | size = var.rootfs_size 25 | } 26 | swap = 0 27 | 28 | 29 | 30 | # Mountpoints don't work yet in 2.6.6 unfortunately. 31 | /* mountpoint { 32 | key = "1" 33 | #slot = 0 34 | storage = "/tank/downloads" 35 | mp = "/downloads" 36 | #size = "100G" #this is required 37 | }*/ 38 | 39 | network { 40 | ip = format("%s/24", var.ips[count.index]) 41 | name ="eth0" 42 | bridge = "vmbr0" 43 | # firewall = "true" # this does not work at the moment 44 | gw = cidrhost(format("%s/24", var.ips[count.index]), 1) 45 | } 46 | 47 | ssh_public_keys = file(var.ssh_keys["pub"]) 48 | 49 | #creates ssh connection to check when the CT is ready for ansible provisioning 50 | connection { 51 | host = var.ips[count.index] 52 | user = var.user 53 | private_key = file(var.ssh_keys["priv"]) 54 | agent = false 55 | timeout = "3m" 56 | } 57 | 58 | provisioner "remote-exec" { 59 | # Leave this here so we know when to start with Ansible local-exec 60 | inline = [ "echo 'Cool, we are ready for provisioning'"] 61 | } 62 | 63 | provisioner "local-exec" { 64 | working_dir = "../../ansible/" 65 | command = "ansible-playbook -u ${var.user} --key-file ${var.ssh_keys["priv"]} -i ${var.ips[count.index]}, provision.yaml" 66 | } 67 | } 68 | 69 | 70 | -------------------------------------------------------------------------------- /tf/ct/var.tf: -------------------------------------------------------------------------------- 1 | variable "proxmox_host" { 2 | type = map 3 | default = { 4 | pm_api_url = "https://10.0.42.50:8006/api2/json" 5 | pm_user = "root@pam" 6 | target_node = "pve" 7 | } 8 | } 9 | 10 | variable "vmid" { 11 | default = 300 12 | description = "Starting ID for the CTs" 13 | } 14 | 15 | 16 | variable "hostnames" { 17 | description = "Containers to be created" 18 | type = list(string) 19 | default = ["prod-ct"] 20 | } 21 | 22 | 23 | variable "rootfs_size" { 24 | description = "Root filesystem size in GB" 25 | default = "2G" 26 | } 27 | 28 | variable "ips" { 29 | description = "IPs of the containers, respective to the hostname order" 30 | type = list(string) 31 | default = ["10.0.42.83"] 32 | } 33 | 34 | variable "user" { 35 | default = "root" 36 | description = "Ansible user used to provision the container" 37 | } 38 | 39 | variable "ssh_keys" { 40 | type = map 41 | default = { 42 | pub = "~/.ssh/id_ed25519-pwless.pub" 43 | priv = "~/.ssh/id_ed25519-pwless" 44 | } 45 | } -------------------------------------------------------------------------------- /tf/ct/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | proxmox = { 4 | source = "Telmate/proxmox" 5 | version = "2.6.6" 6 | } 7 | } 8 | required_version = ">= 0.14" 9 | } 10 | -------------------------------------------------------------------------------- /tf/vm/main.tf: -------------------------------------------------------------------------------- 1 | provider "proxmox" { 2 | pm_api_url = var.proxmox_host["pm_api_url"] 3 | pm_user = var.proxmox_host["pm_user"] 4 | pm_tls_insecure = true 5 | } 6 | 7 | resource "proxmox_vm_qemu" "prox-vm" { 8 | count = length(var.hostnames) 9 | name = var.hostnames[count.index] 10 | target_node = var.proxmox_host["target_node"] 11 | vmid = var.vmid + count.index 12 | full_clone = true 13 | clone = "cloud-init-focal" 14 | 15 | cores = 2 16 | sockets = 1 17 | vcpus = 2 18 | memory = 2048 19 | balloon = 2048 20 | boot = "c" 21 | bootdisk = "virtio0" 22 | 23 | scsihw = "virtio-scsi-pci" 24 | 25 | onboot = false 26 | agent = 1 27 | cpu = "kvm64" 28 | numa = true 29 | hotplug = "network,disk,cpu,memory" 30 | 31 | network { 32 | bridge = "vmbr0" 33 | model = "virtio" 34 | } 35 | 36 | ipconfig0 = "ip=${var.ips[count.index]}/24,gw=${cidrhost(format("%s/24", var.ips[count.index]), 1)}" 37 | 38 | disk { 39 | #id = 0 40 | type = "virtio" 41 | storage = "local-lvm" 42 | size = "5G" 43 | } 44 | 45 | os_type = "cloud-init" 46 | 47 | #creates ssh connection to check when the CT is ready for ansible provisioning 48 | connection { 49 | host = var.ips[count.index] 50 | user = var.user 51 | private_key = file(var.ssh_keys["priv"]) 52 | agent = false 53 | timeout = "3m" 54 | } 55 | 56 | provisioner "remote-exec" { 57 | # Leave this here so we know when to start with Ansible local-exec 58 | inline = [ "echo 'Cool, we are ready for provisioning'"] 59 | } 60 | 61 | provisioner "local-exec" { 62 | working_dir = "../../ansible/" 63 | command = "ansible-playbook -u ${var.user} --key-file ${var.ssh_keys["priv"]} -i ${var.ips[count.index]}, provision.yaml" 64 | } 65 | 66 | provisioner "local-exec" { 67 | working_dir = "../../ansible/" 68 | command = "ansible-playbook -u ${var.user} --key-file ${var.ssh_keys["priv"]} -i ${var.ips[count.index]}, install-qemu-guest-agent.yaml" 69 | } 70 | } 71 | 72 | 73 | -------------------------------------------------------------------------------- /tf/vm/var.tf: -------------------------------------------------------------------------------- 1 | variable "proxmox_host" { 2 | type = map 3 | default = { 4 | pm_api_url = "https://10.0.42.50:8006/api2/json" 5 | pm_user = "root@pam" 6 | target_node = "pve" 7 | } 8 | } 9 | 10 | variable "vmid" { 11 | default = 400 12 | description = "Starting ID for the CTs" 13 | } 14 | 15 | 16 | variable "hostnames" { 17 | description = "VMs to be created" 18 | type = list(string) 19 | default = ["prod-vm", "staging-vm", "dev-vm"] 20 | } 21 | 22 | variable "rootfs_size" { 23 | default = "2G" 24 | } 25 | 26 | variable "ips" { 27 | description = "IPs of the VMs, respective to the hostname order" 28 | type = list(string) 29 | default = ["10.0.42.80", "10.0.42.81", "10.0.42.82"] 30 | } 31 | 32 | variable "ssh_keys" { 33 | type = map 34 | default = { 35 | pub = "~/.ssh/id_ed25519-pwless.pub" 36 | priv = "~/.ssh/id_ed25519-pwless" 37 | } 38 | } 39 | 40 | variable "ssh_password" {} 41 | 42 | variable "user" { 43 | default = "notroot" 44 | description = "User used to SSH into the machine and provision it" 45 | } -------------------------------------------------------------------------------- /tf/vm/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | proxmox = { 4 | source = "Telmate/proxmox" 5 | version = "2.6.6" 6 | } 7 | } 8 | required_version = ">= 0.14" 9 | } 10 | --------------------------------------------------------------------------------