직장 생활을 끝낸 지 만 8년 하고도 5개월이 지났다. 짧다면 짧고 길다면 긴 그 시간 동안 가장 아쉬운 것 중 하나는 일을 하면서 처음 접하는 기술, 더 깊이 있는 경험이 필요할 때 기술적인 테스트를 수행할 서버와 네트워크 환경을 제공하는 곳이 사라졌다는 점이다. 그나마 다행(?)인 것은 개인이 구비하기는 불가능에 가까운 Sun, IBM, HP 등 비 x86_64 기반의 유닉스 장비와 운영체제가 자취를 감추고 있고 x86_64 계열의 가상화 환경이 대세로 굳어졌다는 것이다. 그래서 그러한 아쉬움은 이제 거의 사라졌다고 봐도 무방할 듯 하다. 마음만 먹으면 얼마든지 훌륭한 홈랩(Home Lab) 구축이 가능해졌기 때문이다.
지난 주 배송받은 GenMachine 미니 PC에 Proxmox VE를 설치하고 기존에 사용하고 있던 Proxmox VE 클러스터에 조인(Join) 시켰다. 그 기념으로 홈랩 환경을 하루 날을 잡아 청소하고 네트워크 구성도 변경했다. 꼬박 물리적인 환경 정비에만 8시간 이상 걸렸다. 그리고 이런저런 테스트로 너덜너덜해진 가상서버들을 다시 설치하고 환경을 구성할 때 마다 고생이 이만저만이 아니었는데 차일피일 미루고 있던 테라폼(Terraform)을 사용하여 IaC(Infra as Code)로 인프라를 자동으로 구축할 수 있는 환경을 구성했다. 여로 종류의 가상머신과 응용프로그램 그리고 쿠버네티스 설치까지 자동화하여 단시간에 테스트환경을 재구축 할 수 있도록 한 것이다.
그렇게 구축한 홈랩(Home Lab)을 소개하고 잊으면 찾아볼 수 있도록 IaC 구현의 기본 과정을 기록으로 남기고자 한다.
taeho’s Home Lab Configuration
필자의 홈랩은 공유기와 스위칭허브를 제외하면 모두 램 32G가 장착된 PC 4대로 구성되어 있다. 그리고 구성도에는 없지만 백업용 NAS(시놀로지)와 집 밖에서 홈랩에 접근하기 위한 SSL-VPN 전용 공유기 2대가 더 있다.

이 구성도에서 알 수 있듯 4대의 장비는 Proxmox VE가 설치된 prxmx, prxmx2, prxmx3 세대의 호스트와 스토리지 용도로 사용되는 시놀로지NAS 1대다. 3대의 Proxmox VE 호스트는 blogger 라는 클러스터로 묶여 있고 각 호스트는 인터넷과 연결된 192.x 네트워크 인터페이스와 클러스터링 전용 10.x 네트워크 인터페이스를 2개씩 갖고 있다. 시놀로지 NAS도 마찬가지다.

그리고 prxmx, prxmx2, prxmx2는 Proxmox VE에서 제공되는 SDN 기능을 사용해 172.16.0.0/24 IP 대역을 갖는 VXLAN으로 구성된 가상화 네트워크를 갖고 있다. 쉽게 이야기하자면 AWS의 VPC 또는 Subnet과 비슷하다고 할 수 있다. 이 네트워크는 클러스터 네트워크(10.0.0.0/24)에 터널 형태로 구성된다. 다음과 같이 Proxmox VE 클러스터의 Datacenter의 SDN 메뉴에서 확인할 수 있다.

그리고 이 172.x 네트워크를 인터넷과 연결하기 위한 방화벽(OpenWRT) 가상머신을 리눅스 컨테이너(LXC)를 사용해 prxmx 호스트에 생성했고 192.x(wan구간), 172.x(가상머신구간, SDN), 10.x(proxmox cluster network) 세 개의 네트워크 인터페이스를 추가하여 이 방화벽을 홈랩 네트워크의 게이트웨이로 사용하여 10.x의 호스트와 172.x의 VM에 접속할 수 있고 VM에서 인터넷에 연결할 수 있도록 룰셋을 넣었다.
그리고 1개의 쿠버네티스 마스터 노드(컨트롤 플레인) VM과 3개의 워커 노드 VM을 생성하여 172.x 네트워크에 연결했다.
가상머신 생성할 때 사용할 템플릿 생성하기
필자는 우분투 공식사이트에서 제공하는 클라우드 이미지(Ubuntu 24.04)를 다운로드 받아 공통의 데이터(계정, SSH 공개키, 필수 설치 프로그램, 디스크 구성 등)을 추가하여 템플릿 이미지를 생성한 다음 IaC 코드로 4개의 VM이 각각 필요로 하는 구성을 정의하여 두고 필요할 때 손쉽게 재구축(배포)할 수 있도록 했다.
먼저 다음의 쉘 스크립트를 통해 클라우드 용 이미지에 계정, SSH 공개키, SSH 서비스 활성화 등 공통적인 환경정보 주입한다.
#!/bin/bash
# --- [설정] 파일명과 스토리지 이름을 확인하세요 ---
TEMPLATE_ID=9000
TEMPLATE_NAME="ubuntu-2404-template-nas"
# 1. 미리 다운로드 받아 둔 클라우드 이미지 (우분투24.04) 파일명
SOURCE_FILE="noble-server-cloudimg-ubuntu2404-amd64.img"
# 2. Proxmox 스토리지 및 네트워크 설정. "synology"라는 이름으로 마운트 된 NFS 스토리지 임. xvnet1이 SDN 네트워크임
STORAGE="synology"
BRIDGE="xvnet1"
# ---------------------------------------------------
# 3. 우분투 클라우드 버전 이미지 파일 존재 여부 확인
if [ ! -f "$SOURCE_FILE" ]; then
echo "[Error] 현재 폴더에 '$SOURCE_FILE' 파일이 없습니다!"
exit 1
fi
# 4. 템플릿을 수정할 때 기존 템플릿 삭제
echo "[-] 기존 템플릿($TEMPLATE_ID) 정리 중..."
if qm status $TEMPLATE_ID >/dev/null 2>&1; then
CURRENT_NAME=$(qm config $TEMPLATE_ID | grep "name:" | awk '{print $2}')
if [ "$CURRENT_NAME" == "$TEMPLATE_NAME" ]; then
qm stop $TEMPLATE_ID >/dev/null 2>&1
qm destroy $TEMPLATE_ID --purge >/dev/null 2>&1
echo " -> 삭제 완료."
else
echo "[!] 경고: ID $TEMPLATE_ID 이름 불일치 ($CURRENT_NAME). 중단합니다."
exit 1
fi
else
echo "[-] 기존 템플릿 없음. 계속 진행."
fi
# 5. 작업용 임시 이미지 파일 생성
WORK_FILE="working_template.img"
echo "[-] 원본 파일을 보호하기 위해 복사 중..."
cp "$SOURCE_FILE" "$WORK_FILE"
# 6. 이미지 수정 (Guest Agent 및 SSH 설정)
echo "[-] QEMU Guest Agent 및 SSH 설정 중..."
# [주의] 콤마 뒤에 공백이 있으면 절대 안 됩니다!
virt-customize -a "$WORK_FILE" --install qemu-guest-agent,openssh-server
virt-customize -a "$WORK_FILE" --run-command "systemctl enable ssh"
virt-customize -a "$WORK_FILE" --run-command "sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config.d/60-cloudimg-settings.conf || true"
virt-customize -a "$WORK_FILE" --run-command "echo 'PasswordAuthentication yes' > /etc/ssh/sshd_config.d/99-force-password.conf"
# 7. 네트워크가 VXLAN등 SDN 인 경우 MTU 1450 설정 (VXLAN 최적화)
# 로컬에 임시 yaml 파일을 만들고 이미지 내부로 밀어넣습니다.
cat <<EOF > mtu_config.yaml
network:
version: 2
ethernets:
id0:
match:
name: "e*"
mtu: 1450
EOF
virt-customize -a "working_template.img" --upload mtu_config.yaml:/etc/netplan/99-custom-mtu.yaml
rm mtu_config.yaml # 임시 파일 삭제
# 8. 빈 VM 생성
echo "[-] VM 생성 중..."
# [주의] virtio,bridge 사이에 공백이 없어야 함
qm create $TEMPLATE_ID --name "$TEMPLATE_NAME" --memory 2048 --cores 2 --net0 model=virtio,bridge="$BRIDGE"
# 9. 이미지를 NAS 스토리지로 임포트
echo "[-] 이미지를 NAS($STORAGE)로 임포트 중..."
qm importdisk $TEMPLATE_ID "$WORK_FILE" $STORAGE
# 10. 하드웨어 및 Cloud-Init 설정
echo "[-] 하드웨어 설정 및 Cloud-Init 드라이브 생성..."
qm set $TEMPLATE_ID --scsihw virtio-scsi-pci --scsi0 ${STORAGE}:${TEMPLATE_ID}/vm-${TEMPLATE_ID}-disk-0.raw,discard=on,ssd=1 || \
qm set $TEMPLATE_ID --scsihw virtio-scsi-pci --scsi0 ${STORAGE}:${TEMPLATE_ID}/vm-${TEMPLATE_ID}-disk-0.qcow2,discard=on,ssd=1
qm set $TEMPLATE_ID --ide2 $STORAGE:cloudinit
# 11. 부팅 순서 및 시리얼 콘솔 설정
qm set $TEMPLATE_ID --boot c --bootdisk scsi0
qm set $TEMPLATE_ID --serial0 socket --vga serial0
# 12. 템플릿 변환
echo "[-] 템플릿으로 변환 중..."
qm template $TEMPLATE_ID
# 13. 임시 파일 삭제
rm "$WORK_FILE"
echo "========================================================"
echo "[!] 완료! 템플릿($TEMPLATE_ID) 생성 성공."
echo "========================================================"
이 스크립트는 Proxmox VE 호스트에 콘솔 등으로 접속해서 템플릿을 생성할 때 사용할 클라우드 이미지(Ubuntu 24.04)가 있는 경로로 이동한 다음 실행한다.
가상머신(VM) 템플릿을 생성할 때 사용되는 리눅스 운영체제의 이미지는 일반 이미지와는 다르게 이미 생성된 VM의 이미지다. (부팅하며 설치되는 이미지가 아님을 기억하자.
그리고 10 단계에서 Cloud-init 설정과정이 나온다. 스크립트 내의qm, virt-customize 명령은 모두 Proxmox에서 제공하는 명령어다.
이 스크립트는 클라우드 이미지를 Proxmox VE에서 사용할 가상머신 템플릿으로 변환하고 공통환경 정보를 주입하며 클라우드 이미지에 기본적으로 설치되어 VM이 최초 부팅 될 때 초기단계에서 실행되는 Cloud-init 프로그램이 운영체제 템플릿 이미지와 함게 배포되는 Cloud-init 드라이브에서 각각의 VM마다 다르게 적용될 고유의 정보들을 읽어올 수 있도록 Cloud-init 드라이브를 설정하는 코드다. (VM이 리부팅 될 때 반복적으로 실행되는 코드들도 있긴 함)
이 스크립트를 실행하면 Proxmox노드에 다음과 같이 템플릿이 생성된다.

템플릿에는 운영체제가 설치된 디스크 이미지(.raw 파일)와 Cloudinit 드라이브 이미지 2개가 포함되어 있다. 그리고 템플릿은 템플릿을 생성하기 위해 접속한 Proxmox VE 호스트에서만 보인다. 다른 호스트에서는 보이지 않아 의아했는데 그게 맞다고 한다.
IaC 코드 작성하고 배포하기
템플릿 VM이 준비되었다면 IaC 코드를 작성하고 배포할 차례다. 일반적으로 IaC 코드는 VSCode와 같은 통합개발환경(IDE)에서 사용하게 된다. VSCode에 IaC (Infra as Code) 솔루션의 대표주자인 테라폼(Terraform) 플러그인을 설치하면 린터(Linter)와 포맷터(Formatter) 등을 알아서 해주기 때문에 오류를 극적으로 줄일 수 있다. 게다가 계속 수정되는 IaC 코드의 버전관리를 위해Git과 연동해 버전관리도 가능하기 때문에 VSCode와 같은 IDE를 필수적으로 사용하는 것이 바람직하다.
먼저 Terraform (Windows 용 실행파일 1개)을 다운로드 받아 압축을 풀고 적당한 폴더에 두고 시스템 환경변수 (path)에 해당 경로를 등록한 다음 IaC 코드를 작성할 경로에서 VSCode를 실행시킨다. 그리고 다음과 같이 IaC 코드를 작성한다. 코드 중에 Proxmox VE가 설치된 호스트의 주소가 보이는 것이 핵심 중 하나다. 이 주소와 토큰 ID, 시크릿 토큰이 보이면 어떻게 동작하는지 확~ 이해가 되기 시작한다.

terraform {
required_providers {
proxmox = {
source = "telmate/proxmox"
version = "3.0.1-rc3"
}
}
}
provider "proxmox" {
pm_api_url = "https://192.168.219.199:8006/api2/json"
pm_api_token_id = "root@pam!terraform"
pm_api_token_secret = "d3b8853c-65***5b-cece1a8c8524"
pm_tls_insecure = true
}
# ==========================================
# 1. K8s Master Node (Prxmx 노드에 생성)
# ==========================================
resource "proxmox_vm_qemu" "k8s-m" {
name = "k8s-m"
vmid = 201 # ID 고정
target_node = "prxmx" # 1번 물리서버
clone = "ubuntu-2404-template-nas"
full_clone = true
cores = 2
sockets = 1
memory = 4096
agent = 1
# 디스크 설정
scsihw = "virtio-scsi-pci"
disks {
scsi {
scsi0 {
disk {
size = "20G" # 마스터는 디스크 좀 더 크게
storage = "local-lvm"
iothread = true
}
}
}
ide {
ide2 {
cloudinit {
storage = "local-lvm" # 여기에 저장하라고 명시
}
}
}
}
# 네트워크 (SDN)
network {
model = "virtio"
bridge = "xvnet1"
}
# IP 설정 (.10)
os_type = "cloud-init"
ipconfig0 = "ip=172.16.0.10/24,gw=172.16.0.254"
nameserver = "8.8.8.8"
ciuser = "taeho"
cipassword = "***"
sshkeys = <<EOF
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAmP+Pf3GZ04r2j0OGNBUJloSsuyxQPKlqfJIcij1ng7oNXNVvT/tZXJ/hkAkVaVwhSwGgKKNTTt7o+QmnIBleRSWBHNQ8woaVAV0IVvQ6T8U<중간생략>4xElS2p3oVWfPRR8PJemBl8ssoMmuYoHXpUNXFaveUG/1XGTmnV/gT3rpBCPrMK/HxYFQncGnQ8va9GAVRVR+ujBr/cVgcF8vLZx+4TnVUMBzEM3kcpBWxN1a07q5ckLMmP6Bw== rsa 2048-072422
EOF
}
# ==========================================
# 2. K8s Worker 1 (prxmx 노드에 생성)
# ==========================================
resource "proxmox_vm_qemu" "k8s-w1" {
name = "k8s-w1"
vmid = 202
target_node = "prxmx" # 1번 물리서버 (이름 확인 필요!)
clone = "ubuntu-2404-template-nas"
full_clone = true
cores = 2
sockets = 1
memory = 4096
agent = 1
scsihw = "virtio-scsi-pci"
disks {
scsi {
scsi0 {
disk {
size = "10G"
storage = "local-lvm" # prxmx2에도 local-lvm이 있어야 함
iothread = true
}
}
}
ide {
ide2 {
cloudinit {
storage = "local-lvm" # 여기에 저장하라고 명시
}
}
}
}
network {
model = "virtio"
bridge = "xvnet1"
}
# IP 설정 (.11)
os_type = "cloud-init"
ipconfig0 = "ip=172.16.0.11/24,gw=172.16.0.254"
nameserver = "8.8.8.8"
ciuser = "taeho"
cipassword = "****"
sshkeys = <<EOF
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAmP+Pf3GZ04r2j0OGNBUJloSsuyxQPKlqfJIcij1ng7oNXNVvT/tZXJ/hkAkVaVwhSwGgKKNTTt7o+QmnIBleRSWBHNQ8woaVAV0IVv<중간생략>ElS2p3oVWfPRR8PJemBl8ssoMmuYoHXpUNXFaveUG/1XGTmnV/gT3rpBCPrMK/HxYFQncGnQ8va9GAVRVR+ujBr/cVgcF8vLZx+4TnVUMBzEM3kcpBWxN1a07q5ckLMmP6Bw== rsa 2048-072422
EOF
}
# ==========================================
# 3. K8s Worker 2 (prxmx2 노드에 생성)
# ==========================================
resource "proxmox_vm_qemu" "k8s-w2" {
name = "k8s-w2"
vmid = 203
target_node = "prxmx2" # 2번 물리서버 (이름 확인 필요!)
clone = "ubuntu-2404-template-nas"
full_clone = true
cores = 2
sockets = 1
memory = 4096
agent = 1
scsihw = "virtio-scsi-pci"
disks {
scsi {
scsi0 {
disk {
size = "10G"
storage = "local-lvm" # prxmx3에도 local-lvm이 있어야 함
iothread = true
}
}
}
ide {
ide2 {
cloudinit {
storage = "local-lvm" # 여기에 저장하라고 명시
}
}
}
}
network {
model = "virtio"
bridge = "xvnet1"
}
# IP 설정 (.12)
os_type = "cloud-init"
ipconfig0 = "ip=172.16.0.12/24,gw=172.16.0.254"
nameserver = "8.8.8.8"
ciuser = "taeho"
cipassword = "****"
sshkeys = <<EOF
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAmP+Pf3GZ04r2j0OGNBUJloSsuyxQPKlqfJIcij1ng7oNXNVvT/tZXJ/hkAkVaVwhSwGgKKNTTt7o+QmnIBleRSWBHNQ8woaVAV0IVvQ6T8URp<중간생략>S2p3oVWfPRR8PJemBl8ssoMmuYoHXpUNXFaveUG/1XGTmnV/gT3rpBCPrMK/HxYFQncGnQ8va9GAVRVR+ujBr/cVgcF8vLZx+4TnVUMBzEM3kcpBWxN1a07q5ckLMmP6Bw== rsa 2048-072422
EOF
}
# ==========================================
# 3. K8s Worker 3 (prxmx3 노드에 생성)
# ==========================================
resource "proxmox_vm_qemu" "k8s-w3" {
name = "k8s-w3"
vmid = 204
target_node = "prxmx3" # 3번 물리서버 (이름 확인 필요!)
clone = "ubuntu-2404-template-nas"
full_clone = true
cores = 2
sockets = 1
memory = 4096
agent = 1
scsihw = "virtio-scsi-pci"
disks {
scsi {
scsi0 {
disk {
size = "10G"
storage = "local-lvm" # prxmx3에도 local-lvm이 있어야 함
iothread = true
}
}
}
ide {
ide2 {
cloudinit {
storage = "local-lvm" # 여기에 저장하라고 명시
}
}
}
}
network {
model = "virtio"
bridge = "xvnet1"
}
# IP 설정 (.12)
os_type = "cloud-init"
ipconfig0 = "ip=172.16.0.13/24,gw=172.16.0.254"
nameserver = "8.8.8.8"
ciuser = "taeho"
cipassword = "****"
sshkeys = <<EOF
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAmP+Pf3GZ04r2j0OGNBUJloSsuyxQPKlqfJIcij1ng7oNXNVvT/tZXJ/hkAkVaVwhSwGgKKNTTt7o+QmnIBleRSWBHNQ8woaVAV0IVvQ6T<중간생략>NXFaveUG/1XGTmnV/gT3rpBCPrMK/HxYFQncGnQ8va9GAVRVR+ujBr/cVgcF8vLZx+4TnVUMBzEM3kcpBWxN1a07q5ckLMmP6Bw== rsa 2048-072422
EOF
}
이 IaC 코드를 main.tf 라는 이름으로 저장하고 VSCode의 터미널에서 terraform plan 명령을 실행하고 에러가 없으면 terraform apply 명령을 실행한다. 실행 후 Proxmox VE의 관리 콘솔을 보면 Proxmox VE 호스트 1대에서 가상서버 4개가 생성되어 각 노드로 배포되고 VM이 실행되는 신기한 과정을 볼 수 있다.
배포된 VM은 자동으로 실행된다. 그리고 정상적으로 배포되어 실행되었다면 바로 템플릿 생성 시 주입한 ID와 SSH Keyt로 원격 로그인이 가능하다.
방화벽 (OpenWRT) 설정하기
Proxmox VE 클러스터에 생성된 SDN에 가상머신이 만들어져 연결되면 가상머신에서 인터넷 접속이 안되고 집의 기본 네트워크인 192.168.219.0/24 네트워크에서 SDN 네트워크인 172.x/24 네트워크의 가상머신에 접속이 불가능 하다. 이 두 네트워크를 연결해 가상머신에서 인터넷 접속이 가능하게 하려면 SDN 네트워크(172.x)와 바깥의 네트워크(192.x) 사이에서 라우팅과 상호 접근통제를 해줄 머신이 필요한데 방화벽을 설치해 트래픽도 제어하고 라우팅도 할 수 있도록 해주는 것이 좋다. Proxmox VE에서 제공하는 자체 기능을 사용할 수도 있긴 한데 홈랩이고 공부도 할 겸 OpenWRT를 가상머신 형태로 설치해 방화벽을 구성해 사용하고 있다.
필자의 홈랩에 있는 방화벽(OpenWRT)에는 모두 3개의 인터페이스를 연결했다. 이름만 봐도 어떤 네트워크인지 짐작이 갈 듯 하다.

OpenWRT는 Zone 개념이 있는 방화벽이다. Zone은 다음과 같이 기본 구성을 적용했다.

그런데 위 화면처럼 정책을 넣어주면 가상머신에서 인터넷 연결이 안돼 패키지 설치, 시간동기과, 인터넷 접속이 불가하다. 다음과 같이 개별적으로 통신이 가능하도록 방화벽 정책을 “traffic Rules” 메뉴에서 넣어주면 된다.

이렇게 구성하면 Proxmox VE 클러스터에 SDN으로 구성한 홈랩 네트워크에 필요한 가상머신의 템플릿을 손쉽게 구성하여 관리하고 문제가 발생해도 쉽고 빠르게 재구축 할 수 있다.
다음 포스트에서는 Kubespray와 Ansible을 사용해 IaC로 구성한 VM 4 대에 쿠버네티스를 편리하고 쉽게 배포하는 과정을 설명하고자 한다.
#Proxmox_VE_Cluster #홈랩 #SDN #IaC #terraform #IaC
정말 공감합니다. 저도 비슷한 경험을 많이 했어요. 홈랩 구축으로 이런 고민 해결하고 싶네요!
항상 실행 중이어야 하는 서비스는 오라클 클라우드(평생무료)에 올려두고 이따금씩 켜서 사용하는 테스트 시스템만 홈랩으로 구축하는게 좋죠.