logo
작성일

HW4. Vagrant와 Ansible로 HTCondor 클러스터 자동 구축

Authors
  • avatar
    Name
    seren-wib
    Twitter
Contents

HW3에서는 HTCondor 클러스터를 손으로 하나하나 만들었다. 이번엔 교재 7장대로 그 과정을 자동화해 봤다. 4장에서 만든 VM 3대를 다 지우고, Vagrant로 같은 이름·IP의 VM 3대를 새로 만든 다음, Ansible 플레이북 하나로 HTCondor를 설치하고 설정까지 끝냈다. 뒤쪽에는 7장 연습문제 답을 정리했다.

0. 실습 환경

HW2·HW3와 같은 환경이다. Apple M3 맥에서는 x86용 VirtualBox VM을 쓸 수 없어서, UTM으로 Ubuntu를 띄우고 그 안에서 KVM/libvirt를 하이퍼바이저로 쓴다. Vagrant와 Ansible은 이 Ubuntu 호스트에서 돌린다.

macOS (Apple M3)
└─ UTM (Apple Virtualization, 중첩 가상화)
   └─ Ubuntu 24.04 ARM64  (호스트 yoosung-vm, 강의의 "Ubuntu 호스트") ← Vagrant·Ansible 실행
      └─ KVM + libvirt   (VirtualBox 대체, vagrant-libvirt 플러그인)
         ├─ main   Rocky Linux 10.2 aarch64  192.168.56.101  main.cloud.org   (central manager + submit)
         ├─ work1  Rocky Linux 10.2 aarch64  192.168.56.102  work1.cloud.org  (execute)
         └─ work2  Rocky Linux 10.2 aarch64  192.168.56.103  work2.cloud.org  (execute)
교재 (슬라이드)이 환경
Ubuntu 호스트 x86_64, 계정 cloudman@abeliaUbuntu 24.04.5 aarch64, 계정 yoosung@yoosung-vm
Vagrant 프로바이더 VirtualBoxlibvirt(KVM), vagrant-libvirt 0.12.2 플러그인
Vagrant 설치: HashiCorp apt 저장소저장소에 arm64 리눅스 패키지가 없어서 RubyGems의 vagrant 2.4.9 gem으로 설치 (버전은 슬라이드와 같음)
박스 bento/rockylinux-10Rocky 공식 rockylinux/10 libvirt arm64 박스 (10.2-20260525.0)
게스트 NIC eth0(NAT), eth1(host-only), 클러스터 인터페이스 enp0s8enp3s0(관리망 192.168.121.0/24 NAT), enp4s0(클러스터망 192.168.56.0/24) → NETWORK_INTERFACE = enp4s0
SSH: 127.0.0.1의 포워딩 포트 2222~2224관리망 IP(192.168.121.x)의 22번 포트로 바로 접속. 포트 포워딩 필요 없음
HTCondor 버전 명시 없음condor-25.14.1-1.el10.aarch64 (25.0 저장소)

자동화 흐름

5장에서 손으로 했던 일을 두 도구가 나눠 맡는다. Vagrant는 Vagrantfile 하나로 VM 3대를 만들고(프로비저닝) 호스트명·IP·SSH 키까지 넣어 준다. Ansible은 플레이북(htcondor.yaml) 하나로 그 3대에 HTCondor를 설치하고 역할별 설정 파일을 배포한다. 둘 다 Ubuntu 호스트에서 SSH로 VM을 다룬다.

단계도구입력 파일결과
1. VM 프로비저닝Vagrant + vagrant-libvirt~/vrepo/Vagrantfilemain·work1·work2 생성, 호스트명, 192.168.56.101~103, 노드별 SSH 키
2. VM 구성Ansible/etc/ansible/hosts, ~/Ansible/htcondor.yamlSELinux·방화벽·hosts·패키지·HTCondor 설정·서비스 재시작

SSH 접속 구조

과제 조건이 "모든 과정은 SSH로 VM에 접속해서 진행"이라 접속 경로를 정리해 뒀다. 접속은 세 겹이다.

  • 맥 → Ubuntu 호스트: SSH 키 접속. HW3와 같다.
  • Ubuntu 호스트 → VM: vagrant ssh <노드>를 썼다. 이건 vagrant up이 만든 노드별 개인키와 관리망 IP로 ssh를 실행하는 거고, 그 설정은 뒤에 나오는 vagrant ssh-config로 볼 수 있다. 캡처에 나오는 [vagrant@main ~]$ 프롬프트가 이 접속이다.
  • Ansible: Ansible도 SSH로 동작한다. 인벤토리의 ansible_user=vagrant와 ansible_ssh_private_key_file로 각 노드에 접속해 모듈을 실행한다.

VM 콘솔은 문제 4를 진단할 때 virsh console로 한 번 들어간 것 말고는 안 썼다.

1. Vagrant로 VM 3대 만들기

기존 VM 삭제

4장에서 virt-clone으로 만든 main·work1·work2를 디스크·NVRAM까지 지웠다. 세 도메인은 완전 복제본이라(backing 체인 없음) --remove-all-storage로 디스크까지 지워도 원본에 영향이 없다. 복제 원본인 rocky-base는 4장에서 만든 3대에 들어가지 않으니 남겨 뒀다.

virsh -c qemu:///system list --all
virsh -c qemu:///system undefine main  --nvram --remove-all-storage
virsh -c qemu:///system undefine work1 --nvram --remove-all-storage
virsh -c qemu:///system undefine work2 --nvram --remove-all-storage
virsh -c qemu:///system list --all
virsh -c qemu:///system vol-list images
기존 VM 삭제

main·work1·work2를 도메인·디스크·NVRAM까지 지우고 rocky-base만 남음

Vagrant 설치

슬라이드 방식: HashiCorp apt 저장소

먼저 슬라이드 13쪽대로 GPG 키와 저장소를 등록했다. 슬라이드의 –o는 하이픈이 아니라 EN DASH(U+2013)라서 그대로 붙여 넣으면 gpg가 –o를 파일 이름으로 보고 실패한다. 그래서 -o로 직접 쳤다.

wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com noble main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update
sudo apt install vagrant
HashiCorp 저장소 등록

HashiCorp 저장소 등록과 apt update

문제 1. 저장소에 arm64용 Vagrant가 없음

apt install vagrant가 vagrant:amd64를 받아 오다가 package architecture (amd64) does not match system (arm64)로 실패했다.

apt 설치 실패

apt로는 amd64 패키지만 받아져서 arm64 시스템에서 설치 실패

찾아보니 저장소 패키지 인덱스에는 vagrant가 amd64·i686으로만 있고, releases.hashicorp.com에도 linux_amd64 zip뿐이었다. Ubuntu 자체 아카이브의 vagrant 패키지는 23.10(mantic)이 마지막이라 24.04(noble)에는 아예 없다. 대신 HashiCorp가 같은 버전을 RubyGems에도 올려 두고 있었다. Vagrant가 Ruby로 만들어져 있어서 가능한 방법이다.

RubyGems로 설치

Ruby와 빌드 도구를 깔고 gem으로 설치했다. libarchive-tools는 vagrant가 박스를 풀 때 쓰는 bsdtar고, libvirt-dev는 다음 단계 플러그인을 컴파일할 때 필요하다. 공식 설치 경로가 아니라서 "outside of the official installers" 경고가 뜨지만 동작에는 문제가 없었다. 설치된 버전은 슬라이드와 같은 2.4.9다.

sudo rm /etc/apt/sources.list.d/hashicorp.list && sudo apt clean
sudo apt install -y ruby-full ruby-dev build-essential libarchive-dev libarchive-tools zlib1g-dev libssl-dev pkg-config libvirt-dev
ruby --version; gem --version; bsdtar --version
sudo gem install vagrant --no-document
vagrant --version
Ruby 설치

Ruby 3.2.3, RubyGems 3.4.20, bsdtar 설치

gem install vagrant

gem install vagrant: gem 54개 설치 후 Vagrant 2.4.9

libvirt 프로바이더 플러그인

이 환경엔 VirtualBox가 없으니까 Vagrant가 KVM/libvirt로 VM을 만들도록 vagrant-libvirt 플러그인을 깔았다. 슬라이드 12쪽의 "Vagrant가 지원하는 도구" 중 프로바이더를 libvirt로 고른 셈이다.

vagrant plugin install vagrant-libvirt
vagrant plugin list
vagrant-libvirt

vagrant-libvirt 0.12.2 플러그인 설치

vagrant init

vagrant init이 만든 기본 Vagrantfile은 주석을 빼면 config.vm.box = "base" 한 줄뿐이다.

mkdir vrepo && cd vrepo && vagrant init
ls
grep -v "^ *#" Vagrantfile | grep -v "^ *$"
vagrant init

vagrant init과 기본 Vagrantfile

문제 2. 스토리지 풀 충돌

슬라이드 16쪽처럼 기본 Vagrantfile로 vagrant up을 해 봤더니, 박스 base를 찾기도 전에 libvirt 에러가 났다.

Call to virStoragePoolDefineXML failed: ... Storage source conflict with pool: 'images'
스토리지 풀 충돌

기본 Vagrantfile로 up: Storage source conflict with pool: 'images'

플러그인은 /var/lib/libvirt/images에 default라는 이름의 풀을 새로 정의하려고 하는데, HW2에서 같은 경로에 images라는 풀을 이미 만들어 둬서 libvirt가 거부한 거다. Vagrantfile의 libvirt 프로바이더에 storage_pool_name = "images"를 지정해서 기존 풀을 쓰게 했다.

Rocky Linux 10 박스 추가

문제 3. 박스 다운로드 404

슬라이드의 bento/rockylinux-10은 parallels·utm·virtualbox·vmware용만 있고 libvirt용이 없어서 Rocky 공식 rockylinux/10 박스를 골랐다. 그런데 vagrant box add가 404로 실패했다.

vagrant box add rockylinux/10 --provider libvirt        # → 404
박스 404

Vagrant Cloud를 거친 다운로드가 404

Vagrant Cloud 박스 메타데이터는 dl.rockylinux.org/.../Rocky-10-Vagrant-Libvirt-10.0-20250609.1.aarch64.box로 리다이렉트하는데, Rocky 미러에는 10.2(20260525.0) 파일만 남아 있고 10.0 파일은 지워져 있었다. 미러 디렉터리에서 최신 파일 URL을 찾아 이름을 붙여 직접 추가했다.

vagrant box add --name rockylinux/10 https://dl.rockylinux.org/pub/rocky/10/images/aarch64/Rocky-10-Vagrant-Libvirt.latest.aarch64.vagrant.libvirt.box
vagrant box list
박스 목록

미러 URL로 추가한 박스 목록, 박스 파일(box.img·metadata.json·Vagrantfile)과 박스 내장 Vagrantfile

박스 이미지에 pci=nomsi 넣기

HW2에서 확인했듯이 이 중첩 가상화 환경에서는 Rocky 게스트 커널에 pci=nomsi가 없으면 virtio 인터럽트를 못 받아서 부팅 중에 멈춘다. 그래서 박스 이미지를 qemu-nbd로 붙여서 부트 엔트리(/boot/loader/entries/*.conf)의 options 줄에 인자를 추가하는 스크립트를 만들었다. 첫 vagrant up 전에 한 번만 하면, 이후 만들어지는 VM 디스크가 전부 이 이미지를 backing으로 쓴다.

box-nomsi.sh
#!/bin/bash
# box-nomsi.sh <box.img>
# qcow2 박스 이미지의 커널 명령줄에 pci=nomsi 추가 (qemu-nbd로 오프라인 수정)
set -e
IMG="$1"; [ -f "$IMG" ] || { echo "usage: $0 <box.img>"; exit 1; }
sudo modprobe nbd max_part=16
sudo qemu-nbd --disconnect /dev/nbd0 >/dev/null 2>&1 || true
sudo qemu-nbd --connect=/dev/nbd0 "$IMG"
sleep 2; sudo partprobe /dev/nbd0 2>/dev/null || true; sleep 1
lsblk -o NAME,SIZE,FSTYPE,LABEL /dev/nbd0
M=$(mktemp -d)
cleanup() { sudo umount "$M/boot" 2>/dev/null; sudo umount "$M" 2>/dev/null; rmdir "$M" 2>/dev/null; sudo qemu-nbd --disconnect /dev/nbd0 >/dev/null 2>&1; }
trap cleanup EXIT
ROOT=""; BOOT=""
for p in /dev/nbd0p*; do
  fs=$(lsblk -no FSTYPE "$p"); [ -z "$fs" ] && continue
  [ "$fs" = vfat ] && continue
  sudo mount -o ro "$p" "$M"
  if [ -d "$M/etc" ] && [ -d "$M/boot" ]; then ROOT="$p"; fi
  if [ -d "$M/loader/entries" ]; then BOOT="$p"; fi
  sudo umount "$M"
done
echo "root=$ROOT boot=$BOOT"
sudo mount "$ROOT" "$M"
[ -n "$BOOT" ] && sudo mount "$BOOT" "$M/boot"
# /boot/loader/entries는 root 전용(0700)이라 glob을 sudo 안에서 펼침
show() { sudo bash -c "grep -h '^options' '$M'/boot/loader/entries/*.conf; cat '$M/etc/kernel/cmdline' 2>/dev/null; grep '^GRUB_CMDLINE_LINUX=' '$M/etc/default/grub'; true"; }
echo "--- before"; show
sudo bash -c "sed -i '/^options/{/pci=nomsi/!s/\$/ pci=nomsi/}' '$M'/boot/loader/entries/*.conf"
sudo bash -c "[ -f '$M/etc/kernel/cmdline' ] && sed -i '/pci=nomsi/!s/\$/ pci=nomsi/' '$M/etc/kernel/cmdline' || true"
sudo sed -i '/^GRUB_CMDLINE_LINUX=/{/pci=nomsi/!s/"$/ pci=nomsi"/}' "$M/etc/default/grub" 2>/dev/null || true
echo "--- after"; show
sync; cleanup; trap - EXIT
echo "done: pci=nomsi added to $IMG"
~/bin/box-nomsi.sh ~/.vagrant.d/boxes/rockylinux-VAGRANTSLASH-10/0/arm64/libvirt/box.img
pci=nomsi 추가

박스 이미지 파티션(EFI·BOOT·rocky)과 커널 options 줄의 before/after

Vagrantfile 작성

슬라이드 18쪽 구조(NODE_COUNT, main 정의, times 반복으로 work1·work2 정의)는 그대로 두고 VirtualBox 전용 부분만 libvirt용으로 바꿨다.

  • 박스·프로바이더: config.vm.box = "rockylinux/10", provider "libvirt". modifyvm 대신 cpus·memory, VM 이름은 title.
  • 문제 해결용 옵션: storage_pool_name = "images"는 기존 풀과의 충돌 해결(문제 2), machine_virtual_size = 10은 박스 메타데이터 오류 보정(문제 4).
  • aarch64 UEFI 부팅: machine_type/machine_arch, 시큐어 부트 없는 AAVMF loader, 노드별 nvram. VirtualBox용 기본값인 cirrus 비디오·PS/2 마우스는 aarch64에서 안 되니까 graphics_type = "none"에 virtio 키보드.
  • 네트워크: private_network는 libvirt__network_name: "cloud56"으로 4장에서 만든 192.168.56.0/24 네트워크를 재사용한다. 관리망 IP로 바로 SSH하니까 forwarded_port는 뺐고, 박스에 들어 있는 rsync 공유 폴더도 안 써서 껐다.
  • 오타: 슬라이드 마지막 줄 End는 Ruby 문법 오류라 end로 썼다.
Vagrantfile
NODE_COUNT=2

Vagrant.configure("2") do |config|
  config.vm.box = "rockylinux/10"
  config.vm.synced_folder ".", "/vagrant", disabled: true

  config.vm.provider "libvirt" do |v|
    v.storage_pool_name = "images"
    v.machine_virtual_size = 10
    v.machine_type = "virt"
    v.machine_arch = "aarch64"
    v.cpu_mode = "host-passthrough"
    v.loader = "/usr/share/AAVMF/AAVMF_CODE.no-secboot.fd"
    v.graphics_type = "none"
    v.input type: "keyboard", bus: "virtio"
    v.cpus = 1
    v.memory = 1024
  end

  config.vm.define "main" do |main|
    main.vm.hostname = "main.cloud.org"
    main.vm.network "private_network", ip: "192.168.56.101", libvirt__network_name: "cloud56"
    main.vm.provider "libvirt" do |v|
      v.title = "Main(vagrant)"
      v.nvram = "/var/lib/libvirt/qemu/nvram/vrepo_main_VARS.fd"
    end
  end

  NODE_COUNT.times do |i|
    node_id = "work#{i+1}"
    ip_address = "192.168.56.10#{i+2}"
    config.vm.define node_id do |node|
      node.vm.hostname = "#{node_id}.cloud.org"
      node.vm.network "private_network", ip: ip_address, libvirt__network_name: "cloud56"
      node.vm.provider "libvirt" do |v|
        v.title = "Work#{i+1}(vagrant)"
        v.nvram = "/var/lib/libvirt/qemu/nvram/vrepo_#{node_id}_VARS.fd"
      end
    end
  end
end
Vagrantfile

작성한 Vagrantfile 최초본 (machine_virtual_size는 문제 4를 해결하면서 추가)

vagrant up

문제 4. 세 VM 모두 dracut 비상 셸

첫 vagrant up에서 세 VM이 Waiting for domain to get an IP address...에서 안 넘어갔다. 커널은 돌고 있는데(CPU 유휴) DHCP 요청이 없어서 virsh console로 들어가 보니 sh-5.2# 비상 셸이었다. /run/initramfs/rdsosreport.txt에는 루트 UUID 장치 대기 타임아웃과 Could not boot.가 찍혀 있었고, blkid에는 /dev/vda: PTTYPE="PMBR"뿐, 파티션이 하나도 없었다.

dracut 비상 셸

1차 up의 콘솔: dracut 타임아웃, blkid에 파티션 없음

원인은 박스 메타데이터였다. 박스의 metadata.json에는 virtual_size: 5(GB)라고 돼 있는데 실제 box.img는 10 GiB다. vagrant-libvirt는 메타데이터 값대로 5 GiB짜리 VM 디스크를 만들었고, 디스크 끝을 넘어가는 GPT 파티션을 커널이 버리면서 루트 파일시스템을 못 찾은 거다. Rocky 박스 쪽 오류라서 Vagrantfile에 machine_virtual_size = 10을 넣고 다시 올렸다.

vagrant up                      # 1차: 비상 셸
virsh -c qemu:///system console vrepo_main
vagrant destroy -f
grep -n machine_virtual_size Vagrantfile
vagrant up                      # 2차: 성공
destroy와 디스크 크기 수정

vagrant destroy -f와 디스크 크기 옵션 추가

디스크가 10 GiB로 만들어지니까 약 40초 만에 세 VM이 다 부팅됐다. 슬라이드 19쪽처럼 노드별 SSH 키를 교체하고 호스트명·네트워크까지 설정하는 게 보인다.

2차 vagrant up

2차 vagrant up: 노드별 SSH 키 교체, "Machine booted and ready!", 호스트명·네트워크 설정

노드 확인

vagrant status로 3대가 running인 걸 보고, vagrant ssh로 노드마다 들어가서 호스트명과 주소를 확인했다. enp3s0(192.168.121.x)은 vagrant-libvirt가 SSH용으로 만든 NAT 관리망이고, enp4s0에 슬라이드와 같은 192.168.56.101~103이 붙었다.

vagrant status
vagrant ssh main      # hostname; ip -br addr; exit   (work1, work2도 같음)
노드 확인

세 노드의 hostname과 ip -br addr

vagrant 계정의 키와 ssh-config

vagrant up이 노드마다 새 키 쌍을 만들어 .vagrant/machines/<노드>/libvirt/private_key에 뒀다(슬라이드의 virtualbox 디렉터리 자리). vagrant ssh-config는 이 키와 관리망 IP를 보여 주는데, 이걸 Ansible 인벤토리에 그대로 쓴다.

tree .vagrant/ -L 4 -a
vagrant ssh-config
.vagrant 트리

.vagrant/ 트리: 노드별 private_key

vagrant ssh-config

vagrant ssh-config: HostName은 관리망 IP, Port 22, IdentityFile

2. Ansible로 HTCondor 구성하기

Ansible 설치와 인벤토리

Ubuntu 패키지의 ansible(core 2.16.3)에는 플레이북에서 쓰는 ansible.posix 컬렉션이 같이 들어 있다.

sudo apt install -y ansible
ansible --version
sudo mkdir -p /etc/ansible
sudo cp /mnt/share/VM/share/ansible-hosts /etc/ansible/hosts   # 공유 폴더에 미리 써 둔 파일
cat /etc/ansible/hosts
ansible 설치

ansible 설치, 버전, ansible.posix 1.5.4

인벤토리는 슬라이드처럼 [cluster]·[main]·[worker] 세 그룹이고 호스트 하나가 한 줄이다. 키 경로만 이 환경에 맞게 바꿨다.

/etc/ansible/hosts
[cluster]
main.cloud.org ansible_user=vagrant ansible_ssh_private_key_file=/home/yoosung/vrepo/.vagrant/machines/main/libvirt/private_key
work1.cloud.org ansible_user=vagrant ansible_ssh_private_key_file=/home/yoosung/vrepo/.vagrant/machines/work1/libvirt/private_key
work2.cloud.org ansible_user=vagrant ansible_ssh_private_key_file=/home/yoosung/vrepo/.vagrant/machines/work2/libvirt/private_key

[main]
main.cloud.org ansible_user=vagrant ansible_ssh_private_key_file=/home/yoosung/vrepo/.vagrant/machines/main/libvirt/private_key

[worker]
work1.cloud.org ansible_user=vagrant ansible_ssh_private_key_file=/home/yoosung/vrepo/.vagrant/machines/work1/libvirt/private_key
work2.cloud.org ansible_user=vagrant ansible_ssh_private_key_file=/home/yoosung/vrepo/.vagrant/machines/work2/libvirt/private_key
인벤토리

/etc/ansible/hosts

호스트 통신 확인

Ubuntu 호스트의 /etc/hosts에 세 노드를 추가하고 ping으로 이름 해석과 192.168.56.0/24 통신을 확인했다.

printf "%s\n" "192.168.56.101 main.cloud.org main" "192.168.56.102 work1.cloud.org work1" "192.168.56.103 work2.cloud.org work2" | sudo tee -a /etc/hosts
ping -c 2 main.cloud.org; ping -c 2 work1.cloud.org; ping -c 2 work2.cloud.org
ping

/etc/hosts 추가와 세 노드 ping

ansible cluster -m ping은 처음 돌렸을 때 세 노드의 SSH 호스트 키 확인 질문이 한꺼번에 떠서 진행이 안 됐다. ssh-keyscan으로 호스트 키를 미리 known_hosts에 넣고 다시 돌리니 세 노드 모두 pong이 왔다.

ssh-keyscan main.cloud.org work1.cloud.org work2.cloud.org >> ~/.ssh/known_hosts
ansible cluster -m ping
ansible ping

ssh-keyscan과 ansible cluster -m ping: 세 노드 SUCCESS/pong

플레이북 작성

슬라이드 34~38쪽의 플레이 세 개(cluster 공통 설치, main 설정, worker 설정)를 그대로 옮기고 두 군데만 바꿨다. NETWORK_INTERFACE는 이 환경의 클러스터 인터페이스 enp4s0으로 바꿨고, cluster 플레이에 firewalld를 끄는 태스크를 추가했다(문제 5 해결용이라 1차 실행 땐 없었다). 들여쓰기는 공백 2칸이고 탭은 안 썼다.

htcondor.yaml
---
- name: HTCondor Installation
  hosts: cluster
  become: yes
  become_user: root

  tasks:
    - name: Disable SELinux
      ansible.posix.selinux:
        state: permissive
        policy: targeted

    - name: Stop and disable firewalld
      ansible.builtin.service:
        name: firewalld
        state: stopped
        enabled: no

    - name: Modify /etc/hosts file
      ansible.builtin.blockinfile:
        path: /etc/hosts
        block: |
          192.168.56.101 main.cloud.org     main
          192.168.56.102 work1.cloud.org    work1
          192.168.56.103 work2.cloud.org    work2

    - name: Ensure OpenSSH fully update
      ansible.builtin.dnf:
        name:
          - openssh-server
          - openssh-clients
          - openssl
          - openssl-libs
        state: latest

    - name: Install wget package
      ansible.builtin.dnf:
        name: wget
        state: present

    - name: Install EPEL
      ansible.builtin.dnf:
        name: epel-release
        state: present

    - name: Install HTCondor Repository for EL10
      ansible.builtin.dnf:
        name: https://research.cs.wisc.edu/htcondor/repo/25.0/htcondor-release-current.el10.noarch.rpm
        state: present
        disable_gpg_check: yes

    - name: Install condor
      ansible.builtin.dnf:
        name: condor
        state: present

- name: HTCondor Main Node Setup
  hosts: main
  become: yes
  become_user: root

  tasks:
    - name: Remove old configuration file
      ansible.builtin.file:
        path: /etc/condor/config.d/condor_config.local
        state: absent

    - name: Creating a configuration file
      ansible.builtin.copy:
        dest: /etc/condor/config.d/condor_config.local
        content: |
          UID_DOMAIN = cloud.org
          ALLOW_READ = *
          ALLOW_WRITE = 192.168.56.*
          CONDOR_ADMIN = root@$(FULL_HOSTNAME)
          CONDOR_HOST = main.cloud.org
          IN_HIGHPORT = 9999
          IN_LOWPORT = 9000
          REQUIRE_LOCAL_CONFIG_FILE = False
          NETWORK_INTERFACE = enp4s0
          DAEMON_LIST = MASTER, SCHEDD, COLLECTOR, NEGOTIATOR
          SEC_DEFAULT_AUTHENTICATION_METHODS = CLAIMTOBE
          SEC_CLIENT_AUTHENTICATION_METHODS = CLAIMTOBE
        mode: '0644'

    - name: Restart HTCondor
      ansible.builtin.service:
        name: condor
        state: restarted
        enabled: yes

- name: HTCondor Worker Node Setup
  hosts: worker
  become: yes
  become_user: root

  tasks:
    - name: Remove old configuration file
      ansible.builtin.file:
        path: /etc/condor/config.d/condor_config.local
        state: absent

    - name: Creating a configuration file
      ansible.builtin.copy:
        dest: /etc/condor/config.d/condor_config.local
        content: |
          UID_DOMAIN = cloud.org
          ALLOW_READ = *
          ALLOW_WRITE = 192.168.56.*
          CONDOR_ADMIN = root@$(FULL_HOSTNAME)
          CONDOR_HOST = main.cloud.org
          IN_HIGHPORT = 9999
          IN_LOWPORT = 9000
          REQUIRE_LOCAL_CONFIG_FILE = False
          NETWORK_INTERFACE = enp4s0
          DAEMON_LIST = MASTER, STARTD
          SEC_DEFAULT_AUTHENTICATION_METHODS = CLAIMTOBE
          SEC_CLIENT_AUTHENTICATION_METHODS = CLAIMTOBE
        mode: '0644'

    - name: Restart HTCondor
      ansible.builtin.service:
        name: condor
        state: restarted
        enabled: yes
플레이북 1

htcondor.yaml 1~40행: HTCondor Installation 플레이 (SELinux, firewalld(추가), /etc/hosts, OpenSSH, wget)

플레이북 2

htcondor.yaml 41~91행: EPEL, HTCondor 저장소, condor 설치, Main Node Setup 플레이

플레이북 3

htcondor.yaml 92~125행: Worker Node Setup 플레이

플레이북 실행

firewalld 태스크를 넣기 전 첫 실행은 세 노드 모두 ok=12 changed=9 failed=0으로 끝났다.

ansible-playbook --syntax-check htcondor.yaml
ansible-playbook htcondor.yaml
1차 실행

1차 ansible-playbook: PLAY RECAP ok=12 changed=9 failed=0

문제 5. 플레이북은 성공했는데 condor_status가 비어 있음

그런데 main에서 condor_status를 치니 아무것도 안 나왔다.

빈 condor_status

1차 실행 뒤 main의 condor_status가 비어 있음

워커의 /var/log/condor/StartLog를 보니 main의 9618 포트로 연결이 안 되고 있었다.

attempt to connect to <192.168.56.101:9618> failed: No route to host
TCP connection to collector main.cloud.org failed

Rocky 박스는 firewalld가 켜진 채로 오고, 기본 영역에 ssh·cockpit·dhcpv6-client만 열려 있어서 HTCondor 포트(9618)가 막혀 있었다. 슬라이드 플레이북엔 방화벽 단계가 없다. 5장 실습에서는 설치 스크립트가 9618을 열어 줬고 그 뒤에 firewalld를 손으로 껐으니까 문제가 안 됐던 거다.

vagrant ssh main
condor_status                                                   # 빈 출력
ansible worker  -b -m shell -a "tail -3 /var/log/condor/StartLog"
ansible cluster -b -m shell -a "firewall-cmd --state; firewall-cmd --list-services"
ansible-playbook htcondor.yaml                                  # firewalld 태스크 추가 후
문제 5 진단

워커 StartLog의 "No route to host"와 세 노드의 firewalld 상태

cluster 플레이에 ansible.builtin.service로 firewalld를 stopped·enabled: no로 만드는 태스크를 넣고 다시 돌렸다. firewalld 중지와 condor 설정·재시작만 changed고 나머지는 전부 ok로 지나갔다(ok=13 changed=4). 멱등성이 실제로 보이는 장면이라 연습문제 8번 답에도 썼다.

2차 실행

2차 ansible-playbook: PLAY RECAP ok=13 changed=4 failed=0

최종 결과

main에서 condor_status를 보면 slot1@work1.cloud.org, slot1@work2.cloud.org가 Unclaimed/Idle로 나오고 Total 2다. 슬라이드 40쪽과 같은 클러스터가 자동으로 만들어졌다. Arch는 이 환경이라 aarch64다.

vagrant ssh main
condor_status
최종 condor_status

최종 condor_status: 워커 2대 Unclaimed Idle, Total 2

슬라이드 복붙 함정

슬라이드를 그대로 복사해 붙여 넣으면 실패하게 특수문자가 들어 있다고 해서 PDF 텍스트를 검사해 봤다. 아래 항목이 명령을 깨뜨리니까 손으로 다시 쳐야 한다.

쪽내용결과
13sudo gpg --dearmor –o ...의 –o가 EN DASH(U+2013)gpg가 –o를 입력 파일 이름으로 해석해서 실패. -o로 입력
18Vagrantfile 마지막 줄이 end가 아니라 EndRuby는 대소문자를 구분해서 블록이 안 닫히고 문법 오류. end로 입력
30sudo vi /etc/host/etc/hosts

3. 연습문제

Ch7-3. 데이터 센터에 쓸 수 있는 다른 자동화 툴

자동화 툴은 하는 일에 따라 두 부류로 나뉜다. 서버·네트워크·스토리지 같은 자원을 만들어 주는 프로비저닝 도구(이번 실습의 Vagrant)와, 이미 있는 서버 안의 패키지·설정·서비스를 원하는 상태로 맞추는 구성 관리 도구(Ansible)다. 슬라이드 8~9쪽의 IaC 툴 목록을 기준으로 조사했다.

도구종류 / 방식장점단점
Terraform (HashiCorp)프로비저닝, 선언형, 불변 인프라. HCL 언어. 에이전트 없음AWS·Azure·GCP·OpenStack·vSphere 등 프로바이더가 많아 여러 클라우드를 한 코드로 관리. plan으로 바뀔 내용을 미리 보고 apply. 상태 파일로 실제 자원과 코드를 대조. 커뮤니티가 가장 큼서버 내부 설정은 못 해서 구성 관리 도구와 같이 써야 함. 상태 파일을 잃거나 여럿이 동시에 쓰면 사고가 남. 2023년 BSL로 라이선스가 바뀌어 OpenTofu 포크가 생김
AWS CloudFormation프로비저닝, 선언형, 불변. JSON/YAML 템플릿. AWS 전용(Closed)AWS 서비스를 가장 빨리·깊게 지원하고 추가 비용이 없음. 스택 단위로 생성·변경·삭제하고 실패하면 자동 롤백. IAM과 통합AWS 밖에서는 못 씀. 템플릿이 길고 장황함. 배포가 느리고 에러 메시지가 불친절. 커뮤니티가 작음
OpenStack Heat프로비저닝, 선언형, 불변. HOT(YAML) 템플릿. OpenStack 전용사설 클라우드(OpenStack)에 기본으로 들어 있어 추가 설치가 없음. CloudFormation 호환 템플릿도 받음OpenStack 밖에서는 못 씀. 사용자와 자료가 적고(커뮤니티 Small) 성숙도가 낮음
Pulumi프로비저닝, 선언형 모델을 일반 언어(Python·TypeScript·Go 등)로 작성별도 DSL을 안 배워도 되고 반복문·함수·테스트 같은 언어 기능을 그대로 씀. Terraform 프로바이더를 재사용상태 저장은 Pulumi Cloud 유료 서비스를 쓰는 게 기본(Must-have). 커뮤니티가 작고, 코드가 자유로운 만큼 팀마다 스타일이 달라지기 쉬움
Chef구성 관리, 절차형, 가변 인프라. Ruby DSL. 서버–에이전트오래돼서 안정적이고 자료가 많음(커뮤니티 Large). Ruby를 그대로 쓸 수 있어 복잡한 로직도 자유롭게 표현Ruby와 Chef 서버를 알아야 해서 배우기 어려움. 모든 노드에 에이전트(chef-client)와 중앙 서버가 필요. 절차형이라 "어떻게"를 일일이 적어야 함
Puppet구성 관리, 선언형, 가변. 자체 DSL. 마스터–에이전트"어떤 상태여야 하는가"만 적으면 됨. 노드 상태 보고·감사 기능이 좋아 규정 준수 관리에 강함. 커뮤니티 Large자체 DSL과 마스터 서버를 배워야 함. 에이전트가 주기적으로 당겨 오는 방식이라 즉시 실행(ad-hoc)에 약함. 작은 환경에는 무거움
SaltStack (Salt)구성 관리 + 원격 실행, 선언형(YAML). 마스터–미니언(ZeroMQ), salt-ssh로 에이전트 없이도 가능메시지 버스로 수천 대에 명령을 동시에 빠르게 보냄. 상태 적용과 원격 실행을 한 도구로. 이벤트 기반 자동 대응(리액터)grains·pillar·state 같은 고유 개념이 많아 진입 장벽이 있음. 마스터·미니언 설치가 필요. Ansible보다 커뮤니티가 작음

정리하면 자원을 만드는 단계에서는 특정 클라우드에 묶인 CloudFormation·Heat보다 여러 환경을 한 코드로 다루는 Terraform이 범용적이고, 익숙한 프로그래밍 언어를 쓰고 싶으면 Pulumi가 대안이다. 서버 내부를 맞추는 단계에서는 Chef·Puppet·Salt가 모두 에이전트와 중앙 서버를 둬서 대규모·장기 운영에 강하지만 도입 비용이 크다. Ansible은 에이전트 없이 SSH만으로 동작하니까 이번 실습처럼 작은 클러스터를 빨리 구성하기에 알맞다.

실제 데이터 센터에서는 한 도구로 다 하기보다 "Terraform(또는 Vagrant)으로 만들고 Ansible(또는 Puppet)로 설정한다"처럼 프로비저닝 도구와 구성 관리 도구를 짝지어 쓰는 게 일반적이다. 이번 과제의 Vagrant + Ansible 조합이 그 축소판이다.

Ch7-8. Ansible의 멱등성과 그 중요성

멱등성(idempotence)은 같은 연산을 한 번 하든 여러 번 하든 결과가 같은 성질이다(슬라이드 27쪽). Ansible에서는 플레이북을 몇 번을 돌려도 시스템이 같은 상태에 머문다는 뜻이다.

이게 되는 이유는 Ansible 모듈이 "명령을 실행"하는 게 아니라 "원하는 상태"를 받아서, 먼저 현재 상태를 확인하고 다를 때만 바꾸기 때문이다. 예를 들어 dnf: name: condor, state: present는 condor가 없으면 설치하고 이미 있으면 아무것도 안 한다. blockinfile은 표시 주석으로 자기가 넣은 블록을 찾아서 두 번 추가하지 않는다. 그래서 실행 결과가 changed(바꿨음)와 ok(이미 그 상태라 손대지 않음)로 나뉜다.

이번 실습이 바로 그 예다. 1차 실행에서는 세 노드 모두 ok=12 changed=9로 설치와 설정이 이뤄졌고, firewalld 태스크 하나를 넣고 다시 돌렸을 땐 ok=13 changed=4였다. 바뀐 4개는 새로 넣은 firewalld 중지, 설정 파일 삭제·생성, condor 재시작뿐이고, SELinux·hosts·패키지 설치 같은 나머지는 이미 원하는 상태라 그냥 지나갔다. 멱등성이 없었다면 두 번째 실행에서 /etc/hosts에 같은 세 줄이 또 붙고 패키지를 다시 받는 식으로 상태가 흐트러졌을 거다.

멱등성이 중요한 이유는 이렇다.

  • 안전한 재실행: 네트워크 끊김 같은 걸로 중간에 실패해도 처음부터 다시 돌리면 된다. 어디까지 됐는지 사람이 따질 필요가 없으니까, 수작업에서 생기는 인적 오류(슬라이드 3·5쪽 KT 장애 사례처럼 명령 하나 빠뜨리는 일)가 줄어든다.
  • 여러 대를 같은 상태로: 100대에 같은 플레이북을 돌리면 설치 시점이나 이전 상태와 상관없이 다 같은 상태가 된다. 시간이 지나면서 서버마다 설정이 달라지는 구성 표류(configuration drift)도 정기 실행으로 되돌릴 수 있다.
  • 코드로서의 인프라: 플레이북이 "절차"가 아니라 "시스템이 어떤 상태여야 하는가"의 기록이 되니까, 버전 관리하고 리뷰하고 다른 환경에 재사용할 수 있다(슬라이드 7쪽 IaC의 특징).
  • 변경 추적: changed만 보면 이번 실행이 실제로 뭘 바꿨는지 알 수 있어서, 바뀌면 안 되는 서버에 변경이 생겼는지도 바로 드러난다. --check 모드로 적용 없이 확인만 할 수도 있다.

다만 모든 태스크가 저절로 멱등적인 건 아니다. shell·command 모듈은 매번 실행되고, 이번 플레이북의 service: state: restarted도 돌릴 때마다 재시작하니까 일부러 멱등적이지 않게 쓴 셈이다. 이런 태스크는 creates: 조건을 달거나, 설정 파일이 바뀌었을 때만 재시작하는 핸들러(notify)로 바꿔야 플레이북 전체가 멱등적이 된다.