커스텀 실행기와 함께 libvirt 사용하기

Tier: Free, Premium, Ultimate Offering: GitLab.com, Self-managed

libvirt를 사용하여 커스텀 실행기 드라이버는
실행하는 각 작업마다 새로운 디스크와 VM을 생성하며,
작업이 완료된 후 디스크와 VM은 삭제됩니다.

이 예시는 libvirt를 GitLab Runner 실행기로 추가하기 위한
커뮤니티 기여 !464에서 영감을 받았습니다.

이 문서는 libvirt 설정 방법을 설명하려고 하지 않으며,
이는 주제에서 벗어나기 때문에 포함되지 않습니다. 그러나,
이 드라이버는 GCP Nested Virtualization에서 테스트되었으며,
여기에는 브리지 네트워킹을 사용하여 libvirt 설정하는 방법에 대한
세부 정보도 포함되어 있습니다.
이 예시는 libvirt 설치 시 제공되는 default 네트워크를 사용하므로,
이 네트워크가 실행되고 있는지 확인하세요.

이 드라이버는 각 VM이 전용 IP 주소를 가져야 하므로 브리지 네트워킹을 요구합니다.
이것은 GitLab Runner가 내부에서 SSH를 통해 명령을 실행할 수 있게 합니다.
SSH 키는 다음 명령을 사용하여 생성할 수 있습니다.

의존성을 매번 다운로드하지 않도록 하기 위해 기본 디스크 VM 이미지를 생성합니다.
다음 예시에서는
virt-builder를 사용하여
디스크 VM 이미지를 생성합니다.

virt-builder debian-11 \  
    --size 8G \  
    --output /var/lib/libvirt/images/gitlab-runner-base.qcow2 \  
    --format qcow2 \  
    --hostname gitlab-runner-bullseye \  
    --network \  
    --install curl \  
    --run-command 'curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | bash' \  
    --run-command 'curl -s "https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh" | bash' \  
    --run-command 'useradd -m -p "" gitlab-runner -s /bin/bash' \  
    --install gitlab-runner,git,git-lfs,openssh-server \  
    --run-command "git lfs install --skip-repo" \  
    --ssh-inject gitlab-runner:file:/root/.ssh/id_rsa.pub \  
    --run-command "echo 'gitlab-runner ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers" \  
    --run-command "sed -E 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"net.ifnames=0 biosdevname=0\"/' -i /etc/default/grub" \  
    --run-command "grub-mkconfig -o /boot/grub/grub.cfg" \  
    --run-command "echo 'auto eth0' >> /etc/network/interfaces" \  
    --run-command "echo 'allow-hotplug eth0' >> /etc/network/interfaces" \  
    --run-command "echo 'iface eth0 inet dhcp' >> /etc/network/interfaces"  

위의 명령은 이전에 지정된 모든 필수 소프트웨어를 설치합니다.

virt-builder는 자동으로 루트 암호를 설정하며,
이는 마지막에 인쇄됩니다. 본인이 암호를 지정하고 싶다면,
--root-password password:$SOME_PASSWORD를 전달하면 됩니다.

구성

다음은 libvirt에 대한 GitLab Runner 구성의 예입니다:

concurrent = 1
check_interval = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "libvirt-driver"
  url = "https://gitlab.com/"
  token = "xxxxx"
  executor = "custom"
  builds_dir = "/home/gitlab-runner/builds"
  cache_dir = "/home/gitlab-runner/cache"
  [runners.custom_build_dir]
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]
  [runners.custom]
    prepare_exec = "/opt/libvirt-driver/prepare.sh" # VM을 생성하기 위한 Bash 스크립트 경로입니다.
    run_exec = "/opt/libvirt-driver/run.sh" # VM 내부에서 SSH를 통해 스크립트를 실행하기 위한 Bash 스크립트 경로입니다.
    cleanup_exec = "/opt/libvirt-driver/cleanup.sh" # VM 및 디스크를 삭제하기 위한 Bash 스크립트 경로입니다.

기본

각 단계 (prepare, run, 및 cleanup)는

다른 스크립트 전반에서 사용되는 변수를 생성하기 위해 아래의 기본 스크립트를 사용합니다.

이 스크립트는 다른 스크립트와 동일한 디렉토리에 있어야 하며, 이 경우 /opt/libivirt-driver/에 있어야 합니다.

#!/usr/bin/env bash

# /opt/libvirt-driver/base.sh

VM_IMAGES_PATH="/var/lib/libvirt/images"
BASE_VM_IMAGE="$VM_IMAGES_PATH/gitlab-runner-base.qcow2"
VM_ID="runner-$CUSTOM_ENV_CI_RUNNER_ID-project-$CUSTOM_ENV_CI_PROJECT_ID-concurrent-$CUSTOM_ENV_CI_CONCURRENT_PROJECT_ID-job-$CUSTOM_ENV_CI_JOB_ID"
VM_IMAGE="$VM_IMAGES_PATH/$VM_ID.qcow2"

_get_vm_ip() {
    virsh -q domifaddr "$VM_ID" | awk '{print $4}' | sed -E 's|/([0-9]+)?$||'
}

준비

준비 스크립트:

  • 디스크를 새 경로로 복사합니다.

  • 복사된 디스크에서 새 VM을 설치합니다.

  • VM이 IP를 획득할 때까지 대기합니다.

  • VM에서 SSH가 응답할 때까지 대기합니다.

#!/usr/bin/env bash

# /opt/libvirt-driver/prepare.sh

currentDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
source ${currentDir}/base.sh # 기본 스크립트에서 변수를 가져옵니다.

set -eo pipefail

# 오류를 트랩하고 시스템 실패로 표시합니다.
trap "exit $SYSTEM_FAILURE_EXIT_CODE" ERR

# 작업에 사용할 기본 디스크를 복사합니다.
qemu-img create -f qcow2 -b "$BASE_VM_IMAGE" "$VM_IMAGE" -F qcow2

# VM 설치
virt-install \
    --name "$VM_ID" \
    --os-variant debian11 \
    --disk "$VM_IMAGE" \
    --import \
    --vcpus=2 \
    --ram=2048 \
    --network default \
    --graphics none \
    --noautoconsole

# VM이 IP를 획득할 때까지 대기
echo 'VM이 IP를 획득할 때까지 대기 중입니다.'
for i in $(seq 1 30); do
    VM_IP=$(_get_vm_ip)

    if [ -n "$VM_IP" ]; then
        echo "VM의 IP: $VM_IP"
        break
    fi

    if [ "$i" == "30" ]; then
        echo 'VM이 시작되기를 30초 동안 기다렸습니다. 종료 중...'
        # GitLab Runner에 시스템 실패로 알려주어야 하며, 
        # 재시도해야 합니다.
        exit "$SYSTEM_FAILURE_EXIT_CODE"
    fi

    sleep 1s
done

# ssh가 사용 가능해지기를 기다립니다
echo "sshd가 사용 가능해지기를 기다리는 중"
for i in $(seq 1 30); do
    if ssh -i /root/.ssh/id_rsa -o StrictHostKeyChecking=no gitlab-runner@$VM_IP >/dev/null 2>/dev/null; then
        break
    fi

    if [ "$i" == "30" ]; then
        echo 'sshd가 시작되기를 30초 동안 기다렸습니다. 종료 중...'
        # GitLab Runner에 시스템 실패로 알려주어야 하며, 
        # 재시도해야 합니다.
        exit "$SYSTEM_FAILURE_EXIT_CODE"
    fi

    sleep 1s
done

실행

이 스크립트는 GitLab Runner에 의해 생성된 스크립트를 실행하며, SSH를 통해 STDIN을 통해 VM으로 스크립트 내용을 전송합니다.

#!/usr/bin/env bash

# /opt/libvirt-driver/run.sh

currentDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
source ${currentDir}/base.sh # 기본 스크립트에서 변수를 가져옵니다.

VM_IP=$(_get_vm_ip)

ssh -i /root/.ssh/id_rsa -o StrictHostKeyChecking=no gitlab-runner@$VM_IP /bin/bash < "${1}"
if [ $? -ne 0 ]; then
    # 변수를 사용하여 종료하여 GitLab CI에서 빌드를 실패로 만듭니다.
    exit "$BUILD_FAILURE_EXIT_CODE"
fi

정리

이 스크립트는 VM을 제거하고 디스크를 삭제합니다.

#!/usr/bin/env bash

# /opt/libvirt-driver/cleanup.sh

currentDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
source ${currentDir}/base.sh # 기본 스크립트에서 변수를 가져옵니다.

set -eo pipefail

# VM 종료.
virsh shutdown "$VM_ID"

# VM 정의 해제.
virsh undefine "$VM_ID"

# VM 디스크 삭제.
if [ -f "$VM_IMAGE" ]; then
    rm "$VM_IMAGE"
fi