20240510-cluster_fu.md (26916B)
1 I am always looking for ways to improve my home setup and one of the things I had been wanting to do for a while was to put together a dedicated homelab. 2 3 My desktop computer is a bit of a workhorse so it has always been used for creating virtual machines (VMs), running [OCI][1] containers such as [Docker][2] or [Podman][3], [LXC][4] containers, and testing various warez. 4 5 Recently I acquired four ThinkCentre M900 Tiny devices. After dedicating one to become another 'production' system running [Proxmox][5], I decided to use two as my homelab. Now, I could have put Proxmox on these two as well then [created a cluster][6], but where is the fun in that? I felt like I didn't know enough about how clustering is done on Linux so set out to build my own from scratch. The end goal was to have the ability to create LXC containers and VMs, which I can then use for testing and running OCI containers along with tools such as [Kubernetes][7]. 6 7 ## base camp 8 I get on well with [Debian][8] as a server operating system so installed the latest version (12 "_Bookworm_" at time of writing) on my two nodes, which I am naming _pigley_ and _goatley_ (IYKYK). 9 10 For the cluster I opted to use [Pacemaker][9] as the resource manager and [Corosync][10] for the cluster communication, with [Glusterfs][11] for shared storage. 11 12 Now before everyone writes in saying how two node clusters are not advised, due to quorum issues or split-brain scenarios, I have thought about it. 13 14 I didn't want to use the forth ThinkCentre in this homelab, I have other plans for that. Instead I opted to use a spare RaspberryPi (named _krieger_) as a Corosync quorum device. This (should) counteract the issues seen in a two node cluster. 15 16 Ideally for Glusterfs I would configure _krieger_ as an arbiter device, however in order to get the same version of `glusterfs-server` (10.3 at time of writing) on Raspbian I had to add the testing repo. Unfortunately I couldn't get the `glusterd` service to start. The stable repo only offered `glusterfs-server` version 9.2-1 at the time, which was incompatible with 10.3-5 on _pigley_ and _goatley_. 17 18 I decided to forgo the Glusterfs arbiter, while there is a risk of split-brain this is only a lab environment. 19 20 After provisioning _pigley_ and _goatley_ I installed the required packages 21 ``` 22 apt-get install pcs corosync-qdevice glusterfs-server 23 ``` 24 25 * `pcs` - pacemaker/cluster configuration system. This package will install 26 * `pacemaker` 27 * `corosync` 28 * `corosync-qdevice` - for the quorum device 29 * `glusterfs-storage` 30 31 According to the documentation it is advisable to disable Pacemaker from automatic startup for now 32 ``` 33 systemctl disable pacemaker 34 ``` 35 36 On _krieger_ I installed the `corosync-qnetd` package 37 ``` 38 apt-get install pcs corosync-qnetd 39 ``` 40 41 ## share and share alike 42 On _pigley_ and _goatley_ I created a partition on the main storage device and formatted it with XFS, created a mount point, and mounted the partition 43 ``` 44 mkfs.xfs -i size=512 /dev/sda3 45 mkdir -p /data/glusterfs/lab 46 mount /dev/sda3 /data/glusterfs/lab 47 ``` 48 49 Next I had to ensure _pigley_ and _goatley_ could talk to each other. To make things easy I put the IP addresses in _/etc/hosts_, then using the `gluster` tool confirmed connectivity 50 ``` 51 systemctl start glusterd 52 systemctl enable glusterd 53 gluster peer probe pigley 54 gluster peer status 55 ``` 56 57 I opted to configure a replica volume, keeping the data on 2 bricks (as that's all I have) 58 ``` 59 gluster volume create lab0 replica 2 pigley.home.lab:/data/glusterfs/lab/brick0 \ 60 goatley.home.lab:/data/glusterfs/lab/brick0 61 gluster volume start lab0 62 gluster volume info 63 ``` 64 65 The data isn't accessed directly in the brick directories, so I mounted the Glusterfs volume on a new mountpoint on both systems 66 ``` 67 mkdir /labfs && \ 68 mount -t glusterfs <hostname>:/lab0 /labfs 69 ``` 70 71 To test the replication was working I created a few empty files on one of the systems 72 ``` 73 touch /labfs/{a,b,c} 74 ``` 75 76 Then checked they existed on the other system 77 ``` 78 ls -l /labfs 79 ``` 80 81 And they did! Win win. 82 83 I did experience an issue when adding the _/labfs_ mount in _/etc/fstab_, as it would try to mount before the `glusterd` service was running. To workaround this I included the `noauto` and `x-systemd.automount` options to my _/etc/fstab_ entry 84 ``` 85 localhost:/lab0 /labfs glusterfs defaults,_netdev,noauto,x-systemd.automount 0 0 86 ``` 87 88 ## start your engine 89 Now the `corosync` config. On both nodes I created _/etc/corosync/corosync.conf_ 90 ``` 91 cluster_name: lab 92 crypto_cipher: none >> crypto_cipher: aes256 93 crypto_hash: none >> crypto_hash: sha1 94 nodelist { 95 node { 96 name: pigley 97 nodeid: 1 98 ring0_addr: 192.168.1.8 99 } 100 node { 101 name: goatley 102 nodeid: 2 103 ring0_addr: 192.168.1.9 104 } 105 } 106 ``` 107 108 On one node I had to generate an authkey using `corosync-keygen`, then copied it (_/etc/corosync/authkey_) to the other node. I could then add the authkey to my cluster and restart the cluster services on each node 109 ``` 110 pcs cluster authkey corosync /etc/corosync/authkey --force 111 systemctl restart corosync && systemctl restart pacemaker 112 ``` 113 114 The cluster takes a short while to become clean so I monitored it using `pcs status`. The output below shows everything (except [STONITH][12]) is looking good 115 ``` 116 Cluster name: lab 117 118 WARNINGS: 119 No stonith devices and stonith-enabled is not false 120 121 Status of pacemakerd: 'Pacemaker is running' (last updated 2023-10-24 21:40:57 +01:00) 122 Cluster Summary: 123 * Stack: corosync 124 * Current DC: pigley (version 2.1.5-a3f44794f94) - partition with quorum 125 * Last updated: Tue Mar 26 11:37:06 2024 126 * Last change: Tue Mar 26 11:36:23 2024 by hacluster via crmd on pigley 127 * 2 nodes configured 128 * 0 resource instances configured 129 130 Node List: 131 * Online: [ goatley pigley ] 132 133 Full List of Resources: 134 * No resources 135 136 Daemon Status: 137 corosync: active/enabled 138 pacemaker: active/enabled 139 pcsd: active/enabled 140 ``` 141 142 STONITH, or "Shoot The Other Node In The Head", is used for fencing failed cluster nodes. As this is a test lab I am disabling it but may spend some time configuring it in the future 143 ``` 144 pcs property set stonith-enabled=false 145 ``` 146 147 ## the votes are in 148 As mentioned, I want to use a third system as a quorum device. This means that it casts deciding votes to protect against split-brain yet isn't part of the cluster, so doesn't have to be capable of running any resources. 149 150 While I used an authkey to authenticate _pigley_ and _goatley_ in the cluster, for _krieger_ I had to use password authentication. On _pigley_ I set the `hacluster` user's password 151 ``` 152 passwd hacluster 153 ``` 154 155 On _krieger_ I set the same password and started the quorum device 156 ``` 157 passwd hacluster 158 pcs qdevice setup model net --enable --start 159 ``` 160 161 Back on _pigley_ I then authenticated _krieger_ and specified it as the quorum device 162 ``` 163 pcs host auth krieger 164 pcs quorum device add model net host=krieger algorithm=ffsplit 165 ``` 166 167 The output of `pcs quorum device status` shows the QDevice information 168 ``` 169 Qdevice information 170 ------------------- 171 Model: Net 172 Node ID: 1 173 Configured node list: 174 0 Node ID = 1 175 1 Node ID = 2 176 Membership node list: 1, 2 177 178 Qdevice-net information 179 ---------------------- 180 Cluster name: lab 181 QNetd host: krieger:5403 182 Algorithm: Fifty-Fifty split 183 Tie-breaker: Node with lowest node ID 184 State: Connected 185 ``` 186 187 On _krieger_ the output of `pcs qdevice status net` shows similar information 188 ``` 189 QNetd address: *:5403 190 TLS: Supported (client certificate required) 191 Connected clients: 2 192 Connected clusters: 1 193 Cluster "lab": 194 Algorithm: Fifty-Fifty split (KAP Tie-breaker) 195 Tie-breaker: Node with lowest node ID 196 Node ID 2: 197 Client address: ::ffff:192.168.1.9:52060 198 Configured node list: 1, 2 199 Membership node list: 1, 2 200 Vote: ACK (ACK) 201 Node ID 1: 202 Client address: ::ffff:192.168.1.8:43106 203 Configured node list: 1, 2 204 Membership node list: 1, 2 205 Vote: No change (ACK) 206 ``` 207 208 ## build something 209 Now my cluster is up and running I can start creating resources. The first thing I wanted to get running were some VMs. 210 211 I installed `qemu` on _pigley_ and _goatley_ 212 ``` 213 apt-get install qemu-system-x86 libvirt-daemon-system virtinst 214 ``` 215 216 Before creating a VM I made sure the default network was started, and set it to auto start 217 ``` 218 virsh net-list --all 219 virsh net-start default 220 virsh net-autostart default 221 ``` 222 223 I uploaded a Debian ISO to _pigley_ then used `virt-install` to create a VM 224 ``` 225 virt-install --name testvm \ 226 --memory 2048 \ 227 --vcpus=2 \ 228 --cdrom=/labfs/debian-12.1.0-amd64-netinst.iso \ 229 --disk path=/labfs/testvm.qcow2,size=20,format=qcow2 \ 230 --os-variant debian11 \ 231 --network network=default \ 232 --graphics=spice \ 233 --console pty,target_type=serial -v 234 ``` 235 236 The command waits until the system installation is completed, so from my workstation I used `virt-viewer` to connect to the VM and run through the Debian installer 237 ``` 238 virt-viewer --connect qemu+ssh://pigley/system --wait testvm 239 ``` 240 241 Once the installation is complete and the VM has been rebooted I can add it as a resource to the cluster. First the VM (or VirtualDomain in `libvirt` speak) has to be shutdown and the configuration XML saved to a file 242 ``` 243 virsh shutdown testvm 244 virsh dumpxml testvm > /labfs/testvm.xml 245 pcs resource create testvm VirtualDomain \ 246 config=/labfs/testvm.xml \ 247 migration_transport=ssh \ 248 meta \ 249 allow-migrate=true 250 ``` 251 252 To allow the resource to run on any of the cluster nodes the `symmetric-cluster` option has to be set to `true` (I am not bothering with specific resource rules at this time). Then I can enable the resource 253 ``` 254 pcs property set symmetric-cluster=true 255 pcs resource enable testvm 256 ``` 257 258 Watching `pcs resource` I can see that the VM has started on _goatley_ 259 ``` 260 * testvm (ocf:heartbeat:VirtualDomain): Started goatley 261 ``` 262 263 On _goatley_ I can check that the VM is running with `virsh list` 264 ``` 265 Id Name State 266 -------------------------- 267 1 testvm running 268 ``` 269 270 To connect from my workstation I can use `virt-viewer` again 271 ``` 272 virt-viewer --connect qemu+ssh://goatley/system --wait testvm 273 ``` 274 275 Now I can really test the cluster by moving the VM from _goatley_ to _pigley_ with one command from either node 276 ``` 277 pcs resource move testvm pigley 278 ``` 279 280 The VM is automatically shutdown and restarted on _pigley_, now the output of `pcs resource` shows 281 ``` 282 * testvm (ocf:heartbeat:VirtualDomain): Started pigley 283 ``` 284 285 Successfully clustered! 286 287 Most of the VMs I create will probably be accessed remotely via `ssh`. The VM network on the cluster is not directly accessible from my workstation, I have to `ProxyJump` through whichever node is running the VM (this is by design) 288 ``` 289 ssh -J pigley testvm 290 ``` 291 292 Unless I check the resource status I won't always know which node the VM is on, so I came up with a workaround. 293 294 The `libvirt` network sets up `dnsmasq` for local name resolution, so by setting the first `nameserver` on _pigley_ and _goatley_ to `192.168.122.1` (my virtual network) each node could resolve the hostnames of the VirtualDomains that are running on them. I set this in _dhclient.conf_ 295 ``` 296 prepend domain-name-servers 192.168.122.1; 297 ``` 298 299 On my workstation I made use of the tagging capability and `Match` function in SSH to find which node a VM is on 300 ``` 301 Match tagged lab exec "ssh pigley 'ping -c 1 -W 1 %h 2>/dev/null'" 302 ProxyJump pigley 303 304 Match tagged lab 305 ProxyJump goatley 306 ``` 307 308 When I want to connect to a VM I specify a tag with the `-P` flag 309 ``` 310 ssh -P lab root@testvm 311 ``` 312 313 My SSH config will then use the first `Match` to ping that hostname on _pigley_ and if the VM is running on _pigley_ it will succeed, making _pigley_ the proxy. If the `ping` fails SSH will go to the second `Match` and use _goatley_ as the proxy. 314 315 ## this cloud is just my computer 316 Now that my cluster is up and running I want to be able to interact with it remotely. 317 318 For personal ethical reasons I opted to use [OpenTofu][13] instead of [Terraform][14]. OpenTofu's command `tofu` is a drop-in replacement, simply swap `terraform` for `tofu`. OpenTofu doesn't have a module for managing Pacemaker cluster resources but it does have a `libvirt` module, so I started there. 319 320 I created an OpenTofu configuration using the provider [dmacvicar/libvirt][15]. Unfortunately I had issues trying to get it to connect over SSH. 321 322 My OpenTofu configuration for testing looked like this 323 ``` 324 terraform { 325 required_providers { 326 libvirt = { 327 source = "dmacvicar/libvirt" 328 version = "0.7.1" 329 } 330 } 331 } 332 333 provider "libvirt" { 334 uri = "qemu+ssh://pigley/system" 335 } 336 337 resource "libvirt_domain" "testvm" { 338 name = "testvm" 339 } 340 ``` 341 342 After initialising (`tofu init`) I ran `tofu plan` and got an error 343 ``` 344 Error: failed to dial libvirt: ssh: handshake failed: ssh: no authorities for hostname: pigley:22 345 ``` 346 347 This was remedied by setting the `known_hosts_verify` option to `ignore` 348 ``` 349 ... 350 351 provider "libvirt" { 352 uri = "qemu+ssh://pigley/system?known_hosts_verify=ignore" 353 } 354 355 ... 356 ``` 357 358 Another `tofu plan` produced another error 359 ``` 360 Error: failed to dial libvirt: ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publickey], no supported methods remain 361 ``` 362 363 At first it I thought it was due to using an SSH agent for my key, so I created a dedicated passphrase-less SSH keypair and specified the file in the `uri` 364 ``` 365 ... 366 367 provider "libvirt" { 368 uri = "qemu+ssh://pigley/system?keyfile=/home/pyratebeard/.ssh/homelab_tofu.key&known_hosts_verify=ignore" 369 } 370 371 ... 372 ``` 373 374 This produced the same error again. After some Github issue digging I found mention of setting the [sshauth][16] option to `privkey`. The default is supposedly `agent,privkey` but as I found it isn't picking up my agent, even with `$SSH_AUTH_SOCK` set. I set the option in the `uri` 375 ``` 376 ... 377 378 provider "libvirt" { 379 uri = "qemu+ssh://pigley/system?keyfile=/home/pyratebeard/.ssh/homelab_tofu.key&known_hosts_verify=ignore&sshauth=privkey" 380 } 381 382 ... 383 ``` 384 385 Finally it worked! Until I tried to apply the plan with `tofu apply` 386 ``` 387 Error: error while starting the creation of CloudInit's ISO image: exec: "mkisofs": executable file not found in $PATH 388 ``` 389 390 This was easily fixed by installing the `cdrtools` package on my workstation 391 ``` 392 pacman -S cdrtools 393 ``` 394 395 After creating a new VM I want to automatically add it as a cluster resource. To do this I chose to use [Ansible][17] and so that I don't have to run two lots of commands I wanted to use Ansible to deploy my OpenTofu configuration. Ansible does have a [terraform module][18] but there is not yet one for OpenTofu. A workaround to this is to create a symlink for the `terraform` command 396 ``` 397 sudo ln -s /usr/bin/tofu /usr/bin/terraform 398 ``` 399 400 Ansible will never know the difference! The task in the playbook looks like this 401 ``` 402 - name: "tofu test" 403 community.general.terraform: 404 project_path: '~src/infra_code/libvirt/debian12/' 405 state: present 406 force_init: true 407 delegate_to: localhost 408 ``` 409 410 That ran successfully so I started to expand my OpenTofu config so it would actually build a VM. 411 412 413 In order to not have to go through the ISO install every time, I decided to use the Debian cloud images then make use of [Cloud-init][19] to apply any changes when the new VM is provisioned. Trying to keep it similar to a "real" cloud seemed like a good idea. 414 415 ``` 416 terraform { 417 required_providers { 418 libvirt = { 419 source = "dmacvicar/libvirt" 420 version = "0.7.1" 421 } 422 } 423 } 424 425 provider "libvirt" { 426 uri = "qemu+ssh://pigley/system?keyfile=/home/pyratebeard/.ssh/homelab_tofu.key&known_hosts_verify=ignore&sshauth=privkey" 427 } 428 429 variable "vm_name" { 430 type = string 431 description = "hostname" 432 default = "testvm" 433 } 434 435 variable "vm_vcpus" { 436 type = string 437 description = "number of vcpus" 438 default = 2 439 } 440 441 variable "vm_mem" { 442 type = string 443 description = "amount of memory" 444 default = "2048" 445 } 446 447 variable "vm_size" { 448 type = string 449 description = "capacity of disk" 450 default = "8589934592" # 8G 451 } 452 453 resource "libvirt_volume" "debian12-qcow2" { 454 name = "${var.vm_name}.qcow2" 455 pool = "labfs" 456 source = "http://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2" 457 format = "qcow2" 458 } 459 460 resource "libvirt_volume" "debian12-qcow2" { 461 name = "${var.vm_name}.qcow2" 462 pool = "labfs" 463 format = "qcow2" 464 size = var.vm_size 465 base_volume_id = libvirt_volume.base-debian12-qcow2.id 466 } 467 468 data "template_file" "user_data" { 469 template = "${file("${path.module}/cloud_init.cfg")}" 470 vars = { 471 hostname = var.vm_name 472 } 473 } 474 475 resource "libvirt_cloudinit_disk" "commoninit" { 476 name = "commoninit.iso" 477 pool = "labfs" 478 user_data = "${data.template_file.user_data.rendered}" 479 } 480 481 resource "libvirt_domain" "debian12" { 482 name = var.vm_name 483 memory = var.vm_mem 484 vcpu = var.vm_vcpus 485 486 network_interface { 487 network_name = "default" 488 wait_for_lease = true 489 } 490 491 disk { 492 volume_id = "${libvirt_volume.debian12-qcow2.id}" 493 } 494 495 cloudinit = "${libvirt_cloudinit_disk.commoninit.id}" 496 497 console { 498 type = "pty" 499 target_type = "serial" 500 target_port = "0" 501 } 502 } 503 ``` 504 505 The _cloud\_init.cfg_ configuration is very simple at the moment, only setting the hostname for DNS to work and creating a new user 506 ``` 507 #cloud-config 508 ssh_pwauth: false 509 510 preserve_hostname: false 511 hostname: ${hostname} 512 513 users: 514 - name: pyratebeard 515 ssh_authorized_keys: 516 - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICSluiY54h5FlGxnnXqifWPnfvKNIh1/f0xf0yCThdqV 517 sudo: ['ALL=(ALL) NOPASSWD:ALL'] 518 shell: /bin/bash 519 groups: wheel 520 ``` 521 522 Out of (good?) habit I tested this with `tofu` before running it with Ansible, and I hit another issue (getting tiring isn't it?!) 523 ``` 524 Error: error creating libvirt domain: internal error: process exited while connecting to monitor: ... Could not open '/labfs/debian-12-genericcloud-amd64.qcow2': Permission denied 525 ``` 526 527 OpenTofu wasn't able to write out the _qcom2_ file due to AppArmor. I attempted to give permission in _/etc/apparmor.d/usr.lib.libvirt.virt-aa-helper_ yet that didn't seem to work. Instead I set the following line in _/etc/libvirt/qemu.conf_ and restarted `libvirtd` on _pigley_ and _goatley_ 528 ``` 529 security_device = "none" 530 ``` 531 532 That "fixed" my issue and I successfully built a new VM. Now I could try it with Ansible. 533 534 My playbook applies the infrastructure configuration, then creates a cluster resource using the same `virsh` and `pcs` commands as I used earlier 535 536 ``` 537 - hosts: pigley 538 gather_facts: true 539 become: true 540 pre_tasks: 541 - name: "load vars" 542 ansible.builtin.include_vars: 543 file: vars.yml 544 tags: always 545 546 tasks: 547 - name: "create vm" 548 community.general.terraform: 549 project_path: '{{ tofu_project }}' 550 state: present 551 complex_vars: true 552 variables: 553 vm_name: "{{ vm_name }}" 554 vm_vcpus: "{{ vm_vcpus }}" 555 vm_mem: "{{ vm_mem }}" 556 vm_size: "{{ vm_size }}" 557 force_init: true 558 delegate_to: localhost 559 560 - name: "shutdown vm & dumpxml" 561 ansible.builtin.shell: | 562 virsh shutdown {{ vm_name }} && \ 563 virsh dumpxml {{ vm_name }} > /labfs/{{ vm_name }}.xml 564 565 - name: "create cluster resource" 566 ansible.builtin.shell: | 567 pcs resource create {{ vm_name }} VirtualDomain \ 568 config=/labfs/{{ vm_name }}.xml \ 569 migration_transport=ssh \ 570 meta \ 571 allow-migrate=true 572 ``` 573 574 This is not the most elegant solution of adding the cluster resource, yet seems to be the only way of doing it. 575 576 The _vars.yml_ file which is loaded at the beginning lets me define some options for the VM 577 ``` 578 vm_os: "debian12" # shortname as used in opentofu dir hierarchy 579 vm_name: "testvm" 580 vm_vcpus: "2" 581 vm_mem: "2048" 582 vm_size: "8589934592" # 8G 583 584 ## location of opentofu project on local system 585 tofu_project: "~src/infra_code/libvirt/{{ vm_os }}/" 586 ``` 587 588 When the playbook runs the VM variables defined in _vars.yml_ override anything configured in my OpenTofu project. This means that once my infrastructure configuration is crafted I only have to edit the _vars.yml_ file and run the playbook. I can add more options to _vars.yml_ as I expand the configuration. 589 590 When the playbook completes I can SSH to my new VM without knowing where in the cluster it is running, or even knowing the IP thanks to the SSH tag and the `libvirt` DNS 591 ``` 592 ssh -P lab pyratebeard@testvm 593 ``` 594 595 ## contain your excitement 596 For creating LXC (not LXD) containers the only (active and working) OpenTofu/Terraform modules I could find were for Proxmox or [Incus][20]. I have not been able to look into the new Incus project at the time of writing so for now I went with using Ansible's [LXC Container][21] module. 597 598 On _pigley_ I installed LXC and the required Python package for use with Ansible 599 ``` 600 apt-get install lxc python3-lxc 601 ``` 602 603 I opted not to configure unprivileged containers at this time, this is a lab after all. 604 605 After a quick test I decided to not use the default LXC bridge, instead I configured it to use the existing "default" network configured with `libvirt`. This enabled me to use the same SSH tag method for logging in as the nameserver would resolve the LXC containers as well. The alternative was to configure my own DNS as I can't use two separate nameservers for resolution. 606 607 In _/etc/default/lxc-net_ I switched the bridge option to `false` 608 ``` 609 USE_LXC_BRIDGE="false" 610 ``` 611 612 In _/etc/lxc/default.conf_ I set the network link to the `libvirt` virtual device 613 ``` 614 lxc.net.0.link = virbr0 615 ``` 616 617 Then I restarted the LXC network service 618 ``` 619 systemctl restart lxc-net 620 ``` 621 622 To use my Glusterfs mount for the LXC containers I had to add the `lxc.lxcpath` configuration to _/etc/lxc/lxc.conf_ 623 ``` 624 lxc.lxcpath = /labfs/ 625 ``` 626 627 I tested this by manually creating an LXC container 628 ``` 629 lxc-create -n testlxc -t debian -- -r bookworm 630 ``` 631 632 Which resulted in a ACL error 633 ``` 634 Copying rootfs to /labfs/testlxc/rootfs...rsync: [generator] set_acl: sys_acl_set_file(var/log/journal, ACL_TYPE_ACCESS): Operation not supported (95) 635 ``` 636 637 The fix for this was to mount _/labfs_ with the `acl` option in _/etc/fstab_ 638 ``` 639 localhost:/lab0 /labfs glusterfs defaults,_netdev,noauto,x-systemd.automount,acl 0 0 640 ``` 641 642 With Ansible, creating a new container is straight forward 643 ``` 644 - name: Create a started container 645 community.general.lxc_container: 646 name: testlxc 647 container_log: true 648 template: debian 649 state: started 650 template_options: --release bookworm 651 ``` 652 653 Once it is created I could connect with the same SSH tag I used with the VMs 654 ``` 655 ssh -P lab root@testlxc 656 ``` 657 658 This wouldn't let me in with the default build configuration, I am expected to set up a new user as I did with the VM cloud image. There is no way (that I know of) to use Cloud-init to do this with Ansible. Thankfully the Ansible LXC module has a `container_command` option, which allows specified commands to run inside the container on build. 659 660 I adjusted my playbook task to create a new user and included a task to load variables from a file 661 ``` 662 - hosts: pigley 663 gather_facts: true 664 become: true 665 pre_tasks: 666 - name: "load vars" 667 ansible.builtin.include_vars: 668 file: vars.yml 669 tags: always 670 671 tasks: 672 - name: Create a started container 673 community.general.lxc_container: 674 name: "lxc-{{ lxc_name }}" 675 container_log: true 676 template: "{{ lxc_template }}" 677 state: started 678 template_options: "--release {{ lxc_release }}" 679 container_command: | 680 useradd -m -d /home/{{ username }} -s /bin/bash -G sudo {{ username }} 681 [ -d /home/{{ username }}/.ssh ] || mkdir /home/{{ username }}/.ssh 682 echo {{ ssh_pub_key }} > /home/{{ username }}/.ssh/authorized_keys 683 ``` 684 685 With the variables stored in _vars.yml_ 686 ``` 687 lxc_template: "debian" 688 lxc_release: "bookworm" 689 lxc_name: "testlxc" 690 username: "pyratebeard" 691 ssh_pub_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICSluiY54h5FlGxnnXqifWPnfvKNIh1/f0xf0yCThdqV" 692 ``` 693 694 Now I can log in with SSH 695 ``` 696 ssh -P lab pyratebeard@testlxc 697 ``` 698 699 With a similar command as the VM resource creation I tested adding the LXC container to the cluster 700 ``` 701 pcs resource create testlxc ocf:heartbeat:lxc \ 702 container=testlxc 703 config=/labfs/testlxc/config \ 704 op monitor timeout="20s" interval="60s" OCF_CHECK_LEVEL="0" 705 ``` 706 707 This created the resource, which I could migrate between hosts as before 708 ``` 709 pcs resource move testlxc goatley 710 ``` 711 712 I updated my playbook to include the resource creation task 713 ``` 714 - name: "create cluster resource" 715 ansible.builtin.shell: | 716 pcs resource create {{ lxc_name }} ocf:heartbeat:lxc \ 717 container={{ lxc_name }} 718 config=/labfs/{{ lxc_name }}/config \ 719 op monitor timeout="20s" interval="60s" OCF_CHECK_LEVEL="0" 720 ``` 721 722 And done! My homelab is now ready to use. I am able to quickly create Virtual Machines and LXC containers as well as accessing them via SSH without caring which cluster node they are on. 723 724 After testing all of the VM and container creations I made a small change to my SSH config to discard host key fingerprint checking. I set `StrictHostKeyChecking` to `no`, which stops the host key fingerprint accept prompt, then set the `UserKnownHostsFile` to _/dev/null_ so that fingerprints don't get added to my usual known hosts file. 725 ``` 726 Match tagged lab exec "ssh pigley 'ping -c 1 -W 1 %h 2>/dev/null'" 727 ProxyJump pigley 728 StrictHostKeyChecking no 729 UserKnownHostsFile /dev/null 730 731 Match tagged lab 732 ProxyJump goatley 733 StrictHostKeyChecking no 734 UserKnownHostsFile /dev/null 735 ``` 736 737 It is fun having my own little cloud, building it has certainly taught me a lot and hopefully will continue to do so as I improve and expand my OpenTofu and Ansible code. 738 739 If you are interested keep an eye on my [playbooks][22] and [infra_code][23] repositories. 740 741 [1]: https://opencontainers.org/ 742 [2]: https://www.docker.com/resources/what-container/ 743 [3]: https://docs.podman.io/en/latest/ 744 [4]: https://linuxcontainers.org/lxc/introduction/ 745 [5]: https://www.proxmox.com/en/ 746 [6]: https://pve.proxmox.com/wiki/Cluster_Manager 747 [7]: https://kubernetes.io/ 748 [8]: https://debian.org 749 [9]: http://clusterlabs.org/pacemaker 750 [10]: http://corosync.github.io/ 751 [11]: https://www.gluster.org/ 752 [12]: https://en.wikipedia.org/wiki/STONITH 753 [13]: https://opentofu.org 754 [14]: https://terraform.io/ 755 [15]: https://github.com/dmacvicar/terraform-provider-libvirt 756 [16]: https://github.com/dmacvicar/terraform-provider-libvirt/issues/886#issuecomment-986423116 757 [17]: https://www.ansible.com/ 758 [18]: https://docs.ansible.com/ansible/latest/collections/community/general/terraform_module.html 759 [19]: https://cloud-init.io/ 760 [20]: https://linuxcontainers.org/incus/ 761 [21]: https://docs.ansible.com/ansible/latest/collections/community/general/lxc_container_module.html 762 [22]: https://git.pyratebeard.net/playbooks/ 763 [23]: https://git.pyratebeard.net/infra_code/