cloud-demo/docs/QUICKSTART.md

276 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Quickstart: manual cluster (no Terraform)
Gets a real k3s cluster running today, by hand, so you can learn/practice
Flux and GitOps immediately instead of waiting on the Terraform/libvirt
provider work in `docs/SETUP.md` to be sorted out. One throwaway VM, created
directly with `virt-install` rather than the `dmacvicar/libvirt` Terraform
provider. When the Terraform track is ready, tear this down (last section)
and provision the "real" 3-node cluster properly instead.
Shares steps 14 of `docs/SETUP.md` as prerequisites — do those first if you
haven't:
- Step 1: KVM/libvirt installed on the T630.
- Step 2: the unprivileged `k8s` user exists, with `~/.ssh/id_ed25519`
generated.
- Step 3: `openssl rand -hex 32` isn't needed here (no agents joining, so no
cluster token) — skip it.
- Step 4: `cloud-demo` pushed to Forgejo, with the `flux-write` token
generated.
Everything below runs as `k8s` on the T630 (`sudo -iu k8s`).
## 1. Create the VM
No sudo needed anywhere in this guide — `k8s`'s `libvirt`/`kvm` group
membership (step 2) is what authorizes talking to libvirt at all, via
polkit. But group membership alone doesn't solve everything: system libvirt
runs actual VMs as a separate, restricted `libvirt-qemu` user, not as
`k8s` — so a disk image sitting under `k8s`'s home directory (mode 700 by
default) would fail at boot, since that user can't read into `k8s`'s home
at all. The fix is to let libvirt manage the disk storage itself, inside
its own pool, rather than pointing at a raw path in `~` — pool operations
go through libvirtd's API, so it's libvirtd (already running with the
right privileges) that handles the file ownership, not `k8s` directly.
Every `virsh`/`virt-install` command below uses `-c qemu:///system` /
`--connect qemu:///system` explicitly, rather than relying on the
`LIBVIRT_DEFAULT_URI` env var — for a non-root user that env var is what
picks system vs. the per-user `qemu:///session` instance, and it's easy for
it to be set in one terminal and not another, silently causing commands to
operate on the wrong (session) libvirt instance where nothing you created
system-side is visible. Explicit `-c` on every command avoids that
ambiguity entirely.
**Debian's `libvirt-daemon-system` does *not* auto-create a `default`
storage pool** the way some other distros' packaging does (it does
auto-create the `default` *network*, just left inactive) — check and fix
both before doing anything else:
```sh
virsh -c qemu:///system pool-list --all
virsh -c qemu:///system net-list --all
```
If `pool-list` comes back empty:
```sh
virsh -c qemu:///system pool-define-as default dir --target /var/lib/libvirt/images
virsh -c qemu:///system pool-build default
virsh -c qemu:///system pool-start default
virsh -c qemu:///system pool-autostart default
```
If `net-list` shows `default` as `inactive`:
```sh
virsh -c qemu:///system net-start default
virsh -c qemu:///system net-autostart default
```
Download the cloud image somewhere `k8s` can read it — this location
itself doesn't need to be qemu-readable, since it's only ever read by
`virsh` (running as `k8s`), never directly by the VM:
```sh
curl -L -o /tmp/noble-base.img \
https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
```
Import it into the `default` pool, then create a copy-on-write overlay on
top of it, sized up to 20G:
```sh
virsh -c qemu:///system vol-create-as default k3s-manual-base.qcow2 \
--capacity "$(stat -c%s /tmp/noble-base.img)" --format qcow2
virsh -c qemu:///system vol-upload --pool default k3s-manual-base.qcow2 /tmp/noble-base.img --sparse
rm /tmp/noble-base.img
virsh -c qemu:///system vol-create-as default k3s-manual.qcow2 20G --format qcow2 \
--backing-vol k3s-manual-base.qcow2 --backing-vol-format qcow2
virsh -c qemu:///system vol-list --pool default # should list both volumes
```
Write the cloud-init user-data — paste in the contents of
`~/.ssh/id_ed25519.pub` where marked:
```sh
mkdir -p ~/vms
cat > ~/vms/k3s-manual-user-data.yaml <<'EOF'
#cloud-config
hostname: k3s-manual
manage_etc_hosts: true
users:
- name: k3s
groups: sudo
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- PASTE ~/.ssh/id_ed25519.pub CONTENTS HERE
package_update: true
packages:
- curl
runcmd:
- curl -sfL https://get.k3s.io | sh -s - server
- mkdir -p /home/k3s/.kube
- k3s kubectl config view --raw > /home/k3s/.kube/config
- chown -R k3s:k3s /home/k3s/.kube
EOF
```
This is a single, standalone server — no join token, no agents, no custom
network (uses libvirt's default NAT network + DHCP). A k3s server node
schedules workloads on itself by default, so this alone is a complete,
usable cluster.
The cloud-init user-data file itself is fine living under `~/vms` — unlike
the disk, it's only ever read client-side by `virt-install` (as `k8s`),
which then hands the rendered seed data to libvirtd over the API. Note
`$HOME` rather than `~` in the `--cloud-init` argument below — `~` only
expands at the start of a shell word, not after `=` inside one, so
`user-data=~/vms/...` would be passed to virt-install literally with the
tilde still in it.
`--os-variant ubuntu22.04` below is deliberately not `ubuntu24.04` (the
actual image) — `osinfo-db` on Debian 12 may not recognize the newer
variant name yet. Check what yours knows about with
`osinfo-query os | grep -i ubuntu`; the hint mostly just tunes libvirt's
domain defaults (virtio devices, clock, etc.), so a close-enough variant
against a cloud-init `--import` like this is harmless:
```sh
virt-install \
--connect qemu:///system \
--name k3s-manual \
--memory 2048 \
--vcpus 2 \
--disk vol=default/k3s-manual.qcow2 \
--import \
--os-variant ubuntu22.04 \
--network network=default \
--cloud-init user-data=$HOME/vms/k3s-manual-user-data.yaml \
--graphics none \
--noautoconsole
```
## 2. Find its IP and get kubectl talking to it
Give cloud-init ~2 minutes to finish installing k3s after the VM boots.
```sh
virsh -c qemu:///system domifaddr k3s-manual # note the IP under the default network
```
If you haven't installed `kubectl` on the T630 yet (as `k8s`, no sudo
needed):
```sh
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
mkdir -p ~/.local/bin
mv kubectl ~/.local/bin/
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
export PATH="$HOME/.local/bin:$PATH"
```
```sh
mkdir -p ~/.kube
ssh k3s@<VM_IP> sudo cat /etc/rancher/k3s/k3s.yaml \
| sed "s/127.0.0.1/<VM_IP>/" > ~/.kube/config-manual
export KUBECONFIG=~/.kube/config-manual
kubectl get nodes # expect 1 Ready node
```
## 3. Bootstrap Flux against Forgejo
If you haven't installed the Flux CLI yet (no sudo needed):
```sh
FLUX_VERSION=$(curl -s https://api.github.com/repos/fluxcd/flux2/releases/latest | grep tag_name | cut -d '"' -f4 | sed 's/^v//')
curl -L -o /tmp/flux.tar.gz "https://github.com/fluxcd/flux2/releases/download/v${FLUX_VERSION}/flux_${FLUX_VERSION}_linux_amd64.tar.gz"
tar -xzf /tmp/flux.tar.gz -C ~/.local/bin flux
rm /tmp/flux.tar.gz
flux --version
```
Same repo, same token, same target path as the full guide would use —
nothing about this is cluster-specific:
```sh
flux check --pre --kubeconfig ~/.kube/config-manual
flux bootstrap git \
--url=https://git.boglabob.com/codegit/cloud-demo \
--branch=main \
--path=clusters/homelab \
--username=codegit \
--password=<FLUX_WRITE_TOKEN> \
--token-auth \
--kubeconfig ~/.kube/config-manual
```
## 4. Continue with the shared steps
From here, `docs/SETUP.md` steps 810 and 1213 apply exactly as written,
regardless of how the cluster was created — verifying the podinfo GitOps
loop, registering the Forgejo Actions runner, the Dashboard, remote kubectl
access. Just use `~/.kube/config-manual` as the kubeconfig throughout
instead of the Terraform-provisioned one.
**Step 11 (exposing apps through Caddy) does not apply as-written**,
though — it assumes the Terraform track's static `10.20.30.x` addressing.
This VM is on libvirt's `default` network instead, using DHCP:
```sh
virsh -c qemu:///system domifaddr k3s-manual # note the IP and MAC address
```
DHCP leases are "sticky" to a MAC address in practice, but not guaranteed
fixed the way the Terraform track's static IPs are — pin it explicitly so
it can't change later:
```sh
virsh -c qemu:///system net-update default add ip-dhcp-host \
"<host mac='<VM_MAC>' ip='<VM_IP>'/>" --live --config
```
Then in Caddy's config, use that IP directly rather than
`docs/Caddyfile.example`'s multi-node placeholder blocks:
```
podinfo.boglabob.com {
reverse_proxy http://<VM_IP>:80
}
hello.boglabob.com {
reverse_proxy http://<VM_IP>:80
}
```
## Tearing this down
Once the Terraform track in `docs/SETUP.md` is ready and you `tofu apply`
the real 3-node cluster, remove this one:
```sh
# as k8s
export LIBVIRT_DEFAULT_URI=qemu:///system
virsh destroy k3s-manual # stop it
virsh undefine k3s-manual --remove-all-storage # delete VM + its overlay disk
virsh vol-delete --pool default k3s-manual-base.qcow2 # the base image isn't
# attached to the VM
# directly, so it
# needs its own delete
rm ~/.kube/config-manual
rm -rf ~/vms
```
Nothing else needs cleaning up — Flux's state lived entirely inside that
VM's cluster and goes away with it. The Forgejo repo, deploy tokens, and
the Forgejo Actions runner registration are all cluster-independent and
carry over to the real cluster unchanged; just re-run `flux bootstrap`
against its kubeconfig once it's up.