Compare commits

...

3 Commits

Author SHA1 Message Date
Lukas Greve
6076b096f1 update the README to reflect recent changes
add script to automatically add SSH key pair to main.tf files, for deployments that do require it
2025-10-18 13:19:22 +02:00
Lukas Greve
91e23f0765 move up files to one level and erase default public key 2025-10-18 13:18:32 +02:00
Lukas Greve
f5e85371e4 Move simler example to a new repository 2025-10-18 12:14:55 +02:00
20 changed files with 204 additions and 441 deletions

332
README.md
View File

@@ -11,16 +11,16 @@ The folder *multiple* contains two subfolders, one with shared modules and the o
The idea is to reuse modules across multiple virtual machines and operating systems.
```
./multiple:
.:
environments shared_modules
./multiple/environments:
./environments:
cloud_init.yaml ubuntu-cloud-server-2404-bios
./multiple/environments/ubuntu-cloud-server-2404-bios:
./environments/ubuntu-cloud-server-2404-bios:
ubuntu-cloud-server-2404-bios.tf
./multiple/shared_modules:
./shared_modules:
cloud-init.tf domain.tf network.tf outputs.tf pool.tf provider.tf variables.tf volume.tf
```
@@ -29,320 +29,92 @@ cloud-init.tf domain.tf network.tf outputs.tf pool.tf provider.tf variable
- [QEMU](https://www.qemu.org/)
- [libvirt](https://libvirt.org/)
- [Terraform provider for Libvirt](https://github.com/dmacvicar/terraform-provider-libvirt)
- An SSH key pair to connect to machines that are deployed using cloud-init
## Assumptions
Your Linux x86_64-based machine has at least 4 GB of available memory and 2 CPUs.
- Your Linux x86_64-based machine has at least 4 GB of available memory and 2 CPUs
## How to use it
- Clone this repository
- Go to folder *example*
- Execute the following commands, which will download and install the required Terraform provider if not already present
- Run the following to generate a public key pair
```
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of dmacvicar/libvirt from the dependency lock file
- Using previously-installed dmacvicar/libvirt v0.8.3
Terraform has been successfully initialized!
[...]
$ ssh-keygen -t rsa -b 4096 -f ~/.ssh/terraform_key -C "terraform-deployment"
```
- The following command will plan the deployment, describing actions that will be taken when applied
- Make the script executable
```
$ chmod +x update_ssh_keys.sh
```
- Run the script (it will use terraform_key by default), and will update all `main.tf` file so that they use the previously generated key:
```
$ ./update_ssh_keys.sh
```
> Alternatively, you can use your own public key and update it manually in the `main.tf` deployment file
- Navigate to one of the available deployment
```
$ cd environments/ubuntu-cloud-server-2404-bios/
```
- Initialize your terraform environment
```
$ terraform init
```
- Plan the deployment
```
$ terraform plan
[...]
Terraform will perform the following actions
# A cloud-init ISO disk is created, which provides pre-configured settings and scripts that are applied to a cloud-native disk image during its initial boot. Without it, no user would be created and it would not be possible to log into the virtual machine
+ resource "libvirt_cloudinit_disk" "commoninit" {
+ name = "commoninit.iso"
+ pool = "ubuntu-bios"
[...]
}
# The libvirt domain or virtual machine will be created
+ resource "libvirt_domain" "domain" {
+ cloudinit = (known after apply)
[...]
}
# Here, a libvirt pool to store the virtual machine disk image will be created. It should be possible to use the default one
+ resource "libvirt_pool" "ubuntu-bios" {
[...]
+ name = "ubuntu-bios"
+ type = "dir"
+ target {
+ path = "/tmp/ubuntu-bios"
}
}
# A qcow2 disk volume will be created and stored in the previously created pool, based on a Ubuntu noble (24.04) hosted cloud image
+ resource "libvirt_volume" "ubuntu-qcow2" {
+ format = "qcow2"
[...]
# The plan summaries the action to be taken, which in this case is about creating resources
Plan: 4 to add, 0 to change, 0 to destroy.
```
- The last command will carry out the plan
- Deploy
```
$ terraform apply
[...]
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
# The actions are carried out
libvirt_pool.ubuntu-bios: Creating...
libvirt_pool.ubuntu-bios: Creation complete after 0s
[...]
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
$ terraform deploy
```
- Identify the created machine
- Identify the name of the machine, which requires elevated privileges
```
$ sudo virsh list --all
Id Name State
---------------------------------------------
10 ubuntu-cloud-server-2404-0 running
# virsh list --all
Id Name State
--------------------------------------------
2 u24-bios-0 running
```
- Determine its IP address
- Fetch IP address
```
$ sudo virsh domifaddr ubuntu-cloud-server-2404-0
# virsh domifaddr u24-bios-0
```
- Connect to the machine with the user `groot`
```
$ ssh groot@10.17.3.107
```
```
Name MAC address Protocol Address
----------------------------------------------------------------
vnet3 52:54:00:e2:51:c0 ipv4 192.168.122.24/24
groot@ubuntu:~$
```
- Connect to the machine
- Logout
```
$ ssh root@192.168.122.24
[...]
# Use the password defined in the cloud-init.cfg file
root@192.168.122.24's password:
Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.8.0-71-generic x86_64)
[...]
System information as of Tue Aug 26 10:40:49 UTC 2025
System load: 0.0 Processes: 113
Usage of /: 67.4% of 2.35GB Users logged in: 0
Memory usage: 5% IPv4 address for ens3: 192.168.122.24
Swap usage: 0%
$ exit
```
- Exit the virtual machine
```
root@ubuntu$ exit
```
- To destroy the virtual machine, execute the following command
- Destroy the machine
```
$ terraform destroy
[...]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
- destroy
Terraform will perform the following actions:
# libvirt_cloudinit_disk.commoninit will be destroyed
- resource "libvirt_cloudinit_disk" "commoninit" {
[...]
# libvirt_domain.domain[0] will be destroyed
- resource "libvirt_domain" "domain" {
- arch = "x86_64" -> null
[...]
}
}
# libvirt_pool.ubuntu2 will be destroyed
- resource "libvirt_pool" "ubuntu2" {
- allocation = 798310400 -> null
- available = 16019255296 -> null
[...]
}
}
# libvirt_volume.ubuntu-qcow2 will be destroyed
- resource "libvirt_volume" "ubuntu-qcow2" {
[...]
}
Plan: 0 to add, 0 to change, 4 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
libvirt_domain.domain[0]: Destroying... [id=611d5ede-e4b4-4ca5-ad83-83030942a6b5]
libvirt_domain.domain[0]: Destruction complete after 0s
libvirt_cloudinit_disk.commoninit: Destroying... [id=/tmp/cluster_storage2/commoninit.iso;5f4e08ef-ad51-484f-a9f2-c926f582974a]
libvirt_volume.ubuntu-qcow2: Destroying... [id=/tmp/cluster_storage2/ubuntu-qcow2]
libvirt_cloudinit_disk.commoninit: Destruction complete after 0s
libvirt_volume.ubuntu-qcow2: Destruction complete after 0s
libvirt_pool.ubuntu2: Destroying... [id=dbd62f8b-5d09-4e96-87e2-88e95c582896]
libvirt_pool.ubuntu2: Destruction complete after 0s
Destroy complete! Resources: 4 destroyed.
```
## Explanations
Let's take a look inside the *ubuntu-cloud-server-2404-bios* folder, which contains two files, *ubuntu-cloud-server-2404-bios.tf* and *cloud_init.cfg*
The first file *ubuntu-cloud-server-2404-bios.tf* contains the main configuration for the Terraform deployment.
- It starts by defining the required Terraform version and provider
```
terraform {
required_version = ">= 0.13"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "0.8.3"
}
}
}
```
- The specific provider is defined here
```
provider "libvirt" {
uri = "qemu:///system"
}
```
> The [connection URI](https://libvirt.org/uri.html#qemu-qemu-and-kvm-uris) of the libvirt instance can be defined. One could for instance specific a libvirt instance that is hosted remotely
- A libvirt pool, to store the virtual machine image, is created:
```
resource "libvirt_pool" "ubuntu-bios" {
name = "ubuntu-bios"
type = "dir"
target {
path = "/tmp/ubuntu-bios"
}
}
```
- The cloud-init user data will be fetched from a specific file whose path has to be declared:
```
data "template_file" "user_data" {
template = file("${path.module}/cloud_init.cfg")
}
```
- The ISO cloud-init disk will be created:
```
resource "libvirt_cloudinit_disk" "commoninit" {
name = "commoninit.iso"
user_data = data.template_file.user_data.rendered
pool = libvirt_pool.ubuntu-bios.name
}
```
- Perhaps the most important, the domain will be created:
> Values can be adjusted, such as memory or vCPU counts. In the examples, multiple virtio-based device hardware are created, such as a virtio-gpu.
```
resource "libvirt_domain" "domain" {
count = 1
name = "ubuntu-cloud-server-2404-${count.index}"
memory = "4092"
vcpu = 2
cloudinit = libvirt_cloudinit_disk.commoninit.id
cpu {
mode = "host-model"
}
disk {
volume_id = libvirt_volume.ubuntu-qcow2.id
}
console {
type = "pty"
target_port = "0"
target_type = "virtio"
}
video {
type = "virtio"
}
tpm {
backend_type = "emulator"
backend_version = "2.0"
}
network_interface {
network_name = "default"
}
}
```
## Resources

View File

@@ -0,0 +1,22 @@
terraform {
required_version = ">= 0.13"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "0.8.3"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}
module "shared_modules" {
source = "../../shared_modules"
vm_name = "f42-bios"
image_location = "https://download.fedoraproject.org/pub/fedora/linux/releases/42/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-42-1.1.x86_64.qcow2"
ssh_key = ""
enable_cloudinit = true
}

View File

@@ -0,0 +1,22 @@
terraform {
required_version = ">= 0.13"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "0.8.3"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}
module "shared_modules" {
source = "../../shared_modules"
vm_name = "u24-bios"
image_location = "/var/lib/libvirt/images/noble-server-cloudimg-amd64.img"
ssh_key = ""
enable_cloudinit = true
}

View File

@@ -0,0 +1,28 @@
terraform {
required_version = ">= 0.13"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "0.8.3"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}
module "shared_modules" {
source = "../../shared_modules"
vm_name = "u24-uefi"
image_location = "/var/lib/libvirt/images/noble-server-cloudimg-amd64.img"
ssh_key = ""
enable_cloudinit = true
# ---- OPTIONAL UEFI SETTINGS ----------------------------------------------
uefi_firmware = "/usr/share/edk2/x64/OVMF_CODE.4m.fd"
uefi_nvram_template = "/usr/share/edk2/x64/OVMF_VARS.4m.fd"
uefi_nvram_file_suffix = "-uefi"
# ----------------------------------------------------------------
}

View File

@@ -1,18 +0,0 @@
#cloud-config
# vim: syntax=yaml
# examples:
# https://cloudinit.readthedocs.io/en/latest/topics/examples.html
---
ssh_pwauth: true
disable_root: false
chpasswd:
list: |
root:password
expire: false
users:
- name: ubuntu
sudo: ALL=(ALL) NOPASSWD:ALL
groups: users, admin
home: /home/ubuntu
shell: /bin/bash
lock_passwd: false

View File

@@ -1,71 +0,0 @@
terraform {
required_version = ">= 0.13"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "0.8.3"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}
resource "libvirt_pool" "ubuntu-bios" {
name = "ubuntu-bios"
type = "dir"
target {
path = "/tmp/ubuntu-bios"
}
}
resource "libvirt_volume" "ubuntu-bios" {
name = "ubuntu-bios-${count.index}"
pool = libvirt_pool.ubuntu-bios.name
source = "/var/lib/libvirt/images/noble-server-cloudimg-amd64.img"
format = "qcow2"
count = 1
}
resource "libvirt_cloudinit_disk" "commoninit" {
name = "commoninit.iso"
user_data = templatefile("${path.module}/cloud_init.yaml", {})
}
resource "libvirt_domain" "domain" {
count = 1
name = "ubuntu-cloud-server-2404-${count.index}"
memory = "4092"
vcpu = 2
cloudinit = libvirt_cloudinit_disk.commoninit.id
cpu {
mode = "host-model"
}
disk {
volume_id = element(libvirt_volume.ubuntu-bios.*.id, count.index)
}
console {
type = "pty"
target_port = "0"
target_type = "virtio"
}
video {
type = "virtio"
}
tpm {
backend_type = "emulator"
backend_version = "2.0"
}
network_interface {
network_name = "default"
}
}

View File

@@ -1,22 +0,0 @@
terraform {
required_version = ">= 0.13"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "0.8.3"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}
module "shared_modules" {
source = "../../shared_modules"
vm_name = "f42-bios"
image_location = "https://download.fedoraproject.org/pub/fedora/linux/releases/42/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-42-1.1.x86_64.qcow2"
ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDMSuVlvOsMqx9qOrKKB1295FjCf2QhHfR1qola9brGkUcFL9dAztG2qdQnpiuPQ4OJpkedrO3C/ixEw1MLTL8l12SvYy/Q9QFguwylp35Nbw1p8h7jrX1FcNLRYltxkMgVhCs1InT5m0lf56bu1h7JfsMs7Ovsy3lU5OdK4h2MysTSKOLctsE4jDJ+XbJYQzj4rbfB/U7/9ple366cGl6xlaHxVfI4BUFWUOiVU4HWvZjrOM5fqPt+AUFRx1l2D7hLUZgOdVQwgO8GFn0sCyCIw0NCXbDn/H05pvWtTUPnyhj5TiseF8qW1byrrT5G8saxwvx8nbIK2tpPfKFdIiL7aj9bYQdltn1knJtvk3hpTPy4QvAbaoGfnfrPAsyU1A/CTw9SD/idvDT2wt1hVsm8EsnpovF7WT5z22fcgoFLDo+QCQrp7t1Wx0/Djay2nThi3FO3N051y5fQWoKOvTsm+rRhrzpDoc+Wtrtss3ua54qnQxHRx3YC0M5Xl9DINkwrcunbZBhozsDG2DzX9qcyzJsSfm9Zt5yM2lpcq+dGPRO1wedw4ogoOpobRr9Cja9W/lJvxmjgIiHz2HbSFPtk/VGjL6M7aQor/GDNN3ugSsfUoTTmNaS9+lWeg+tQWcFUPhYQtQB4/gHQ2u7+mQ0H3hVybsIKIh5XBpAdHQ7pww=="
enable_cloudinit = true
}

View File

@@ -1,22 +0,0 @@
terraform {
required_version = ">= 0.13"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "0.8.3"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}
module "shared_modules" {
source = "../../shared_modules"
vm_name = "u24-bios"
image_location = "/var/lib/libvirt/images/noble-server-cloudimg-amd64.img"
ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDMSuVlvOsMqx9qOrKKB1295FjCf2QhHfR1qola9brGkUcFL9dAztG2qdQnpiuPQ4OJpkedrO3C/ixEw1MLTL8l12SvYy/Q9QFguwylp35Nbw1p8h7jrX1FcNLRYltxkMgVhCs1InT5m0lf56bu1h7JfsMs7Ovsy3lU5OdK4h2MysTSKOLctsE4jDJ+XbJYQzj4rbfB/U7/9ple366cGl6xlaHxVfI4BUFWUOiVU4HWvZjrOM5fqPt+AUFRx1l2D7hLUZgOdVQwgO8GFn0sCyCIw0NCXbDn/H05pvWtTUPnyhj5TiseF8qW1byrrT5G8saxwvx8nbIK2tpPfKFdIiL7aj9bYQdltn1knJtvk3hpTPy4QvAbaoGfnfrPAsyU1A/CTw9SD/idvDT2wt1hVsm8EsnpovF7WT5z22fcgoFLDo+QCQrp7t1Wx0/Djay2nThi3FO3N051y5fQWoKOvTsm+rRhrzpDoc+Wtrtss3ua54qnQxHRx3YC0M5Xl9DINkwrcunbZBhozsDG2DzX9qcyzJsSfm9Zt5yM2lpcq+dGPRO1wedw4ogoOpobRr9Cja9W/lJvxmjgIiHz2HbSFPtk/VGjL6M7aQor/GDNN3ugSsfUoTTmNaS9+lWeg+tQWcFUPhYQtQB4/gHQ2u7+mQ0H3hVybsIKIh5XBpAdHQ7pww=="
enable_cloudinit = true
}

View File

@@ -1,28 +0,0 @@
terraform {
required_version = ">= 0.13"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "0.8.3"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}
module "shared_modules" {
source = "../../shared_modules"
vm_name = "u24-uefi"
image_location = "/var/lib/libvirt/images/noble-server-cloudimg-amd64.img"
ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDMSuVlvOsMqx9qOrKKB1295FjCf2QhHfR1qola9brGkUcFL9dAztG2qdQnpiuPQ4OJpkedrO3C/ixEw1MLTL8l12SvYy/Q9QFguwylp35Nbw1p8h7jrX1FcNLRYltxkMgVhCs1InT5m0lf56bu1h7JfsMs7Ovsy3lU5OdK4h2MysTSKOLctsE4jDJ+XbJYQzj4rbfB/U7/9ple366cGl6xlaHxVfI4BUFWUOiVU4HWvZjrOM5fqPt+AUFRx1l2D7hLUZgOdVQwgO8GFn0sCyCIw0NCXbDn/H05pvWtTUPnyhj5TiseF8qW1byrrT5G8saxwvx8nbIK2tpPfKFdIiL7aj9bYQdltn1knJtvk3hpTPy4QvAbaoGfnfrPAsyU1A/CTw9SD/idvDT2wt1hVsm8EsnpovF7WT5z22fcgoFLDo+QCQrp7t1Wx0/Djay2nThi3FO3N051y5fQWoKOvTsm+rRhrzpDoc+Wtrtss3ua54qnQxHRx3YC0M5Xl9DINkwrcunbZBhozsDG2DzX9qcyzJsSfm9Zt5yM2lpcq+dGPRO1wedw4ogoOpobRr9Cja9W/lJvxmjgIiHz2HbSFPtk/VGjL6M7aQor/GDNN3ugSsfUoTTmNaS9+lWeg+tQWcFUPhYQtQB4/gHQ2u7+mQ0H3hVybsIKIh5XBpAdHQ7pww=="
enable_cloudinit = true
# ---- OPTIONAL UEFI SETTINGS ----------------------------------------------
uefi_firmware = "/usr/share/edk2/x64/OVMF_CODE.4m.fd"
uefi_nvram_template = "/usr/share/edk2/x64/OVMF_VARS.4m.fd"
uefi_nvram_file_suffix = "-uefi"
# ----------------------------------------------------------------
}

80
update_ssh_keys.sh Executable file
View File

@@ -0,0 +1,80 @@
#!/bin/bash
# Script to automatically update SSH keys in all main.tf files
# This script looks for terraform_key (or terraform_key.pub) in ~/.ssh directory
# Function to display usage
usage() {
echo "Usage: $0 [ssh_key_name]"
echo " ssh_key_name: Name of the SSH key pair (default: terraform_key)"
echo ""
echo "Example:"
echo " $0 # Uses default 'terraform_key'"
echo " $0 my_custom_key # Uses 'my_custom_key' and 'my_custom_key.pub'"
exit 1
}
# Set the SSH key name (default to terraform_key)
SSH_KEY_NAME="${1:-terraform_key}"
# Expand the home directory properly
HOME_DIR="${HOME:-/home/$(whoami)}"
SSH_KEY_PATH="$HOME_DIR/.ssh/$SSH_KEY_NAME"
SSH_KEY_PUB_PATH="$HOME_DIR/.ssh/$SSH_KEY_NAME.pub"
# Check if SSH key exists
if [ ! -f "$SSH_KEY_PATH" ] && [ ! -f "$SSH_KEY_PUB_PATH" ]; then
echo "Error: SSH key '$SSH_KEY_NAME' not found in $HOME_DIR/.ssh/"
echo "Please generate your SSH key first:"
echo " ssh-keygen -t rsa -b 4096 -f $HOME_DIR/.ssh/$SSH_KEY_NAME"
exit 1
fi
# Check if public key exists specifically (required for reading)
if [ ! -f "$SSH_KEY_PUB_PATH" ]; then
echo "Error: SSH public key '$SSH_KEY_NAME.pub' not found in $HOME_DIR/.ssh/"
exit 1
fi
# Get the public key content (remove any trailing whitespace)
PUBLIC_KEY=$(cat "$SSH_KEY_PUB_PATH" | tr -d '\n')
# Validate that we got a valid SSH key
if [[ ! "$PUBLIC_KEY" =~ ^ssh-[a-z]+[[:space:]]+[A-Za-z0-9+/]*[=]{0,3} ]]; then
echo "Error: Invalid SSH public key format detected"
exit 1
fi
echo "Found SSH public key:"
echo "$PUBLIC_KEY"
echo ""
# Find all main.tf files and update them
MAIN_TF_FILES=$(find . -name "main.tf" -type f)
if [ -z "$MAIN_TF_FILES" ]; then
echo "No main.tf files found!"
exit 1
fi
echo "Updating SSH key in the following files:"
echo "$MAIN_TF_FILES"
echo ""
# Replace the ssh_key line in all main.tf files using # as delimiter
echo "Replacing SSH key in all main.tf files..."
for file in $MAIN_TF_FILES; do
sed -i "s#ssh_key = \".*\"#ssh_key = \"$PUBLIC_KEY\"#g" "$file"
done
# Verify the replacement worked
echo ""
echo "Verification:"
for file in $MAIN_TF_FILES; do
echo "File: $file"
grep "ssh_key =" "$file" | head -1
done
echo ""
echo "SSH key has been successfully updated in all main.tf files!"
echo "Backup files are saved with timestamp suffixes."