firecracker로 microVM을 생성해 보고, 비즈니스 로직을 만드려고 했다. 다만 내 노트북은 M2로 중첩 가상화를 지원하지 않아서, KVM을 이용할 수 없었다. 그래서 클라우드 가상 머신을 활용하려고 가능한 인스턴스 타입을 알아봤다. 

 

나는 AWS가 가장 친숙하기에 AWS에서 진행하려 했으나 AWS는 기본적으로는 중첩 가상화를 지원하지 않는다. 따라서 베어메탈 인스턴스 타입을 사용해야 kvm을 사용할 수 있다. 그중에 가장 저렴한 것은 c6g.metal다. 64 vcpu, 128GB 메모리로 과분한 스펙이라서 가격이 시간당 2.464$다. 오류를 잡고 테스트를 진행하는 시간이 오래 걸릴 것으로 생각돼서 너무 큰 금액이다. 그래서 나는 azure를 선택했다. 

 

azure는 신규 사용자에게 200$의 크레딧을 제공하고 비교적 저렴한 인스턴스들에서 중첩 가상화가 가능하다. 리스트는 다음 링크에서 확인 가능하다. (공식 답변) 저기에 적혀있는 모든 타입이 가능하지만 일정 이상의 스펙을 가져야 한다고 써있다. 4 vCPU / 12GB of RAM / 128GB of storage 따라서 나는 D4S_v3 인스턴스를 선택했다.

 

여기서 주의해야 할 점은 인스턴스를 생성할 때 보안 유형을 표준으로 변경해야 한다. 이 부분에서 하루 정도 시간을 소비했다. 아래의 저 부분을 꼭 표준으로 변경하자. 아니면 중첩 가상화가 되지 않는다.

 

이제 디스크 사이즈만 128GB로 선택해서 생성해주면 된다. 이제부터는 인스턴스 세팅이다. 

 

인스턴스 셋팅

인스턴스에 페이스쳔 연결 혹은 azure cli 연결로 인스턴스로 들어가 준다. 가장 먼저 sudo su로 루트 권한으로 모든 것을 실행하자!!

 

이제 egrep -c 'vmx|svm' /proc/cpuinfo 명령어를 입력해서 0 이상인지 확인해 준다. 0이라면 인스턴스 타입, 최소 스펙 생성 과정에서 보안 유형이 문제일 수 있으므로 확인해 보자.

 

이제 패키지를 업데이트하고, kvm을 설치해 준다. 

 

apt update
apt install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils

 

전부 설치가 완료되면 kvm-ok를 입력해서, kvm 사용 가능한 지 확인해 보자 아래와 같이 뜨면 문제없다.

 

이제 firecracker를 clone 해준다.

## firecracker 클론
git clone https://github.com/firecracker-microvm/firecracker

 

이제 firecrakcer 공식 페이지에 있는 빠른 시작 가이드를 참고하면 된다. 조금 변경해야 하는 부분도 있는데 하나씩 확인해 보자.

 

ARCH="$(uname -m)"

# 커널 버전 설정
latest=$(wget "http://spec.ccfc.min.s3.amazonaws.com/?prefix=firecracker-ci/v1.11/$ARCH/vmlinux-5.10&list-type=2" -O - 2>/dev/null | grep -oP "(?<=<Key>)(firecracker-ci/v1.11/$ARCH/vmlinux-5\.10\.[0-9]{1,3})(?=</Key>)")

# 커널 바이너리 다운로드
wget "https://s3.amazonaws.com/spec.ccfc.min/${latest}"

# 파일 시스템 다운로드
wget -O ubuntu-24.04.squashfs.upstream "https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.11/${ARCH}/ubuntu-24.04.squashfs"

# 파일 시스템 ssh key 생성
unsquashfs ubuntu-24.04.squashfs.upstream
ssh-keygen -f id_rsa -N ""
cp -v id_rsa.pub squashfs-root/root/.ssh/authorized_keys
mv -v id_rsa ./ubuntu-24.04.id_rsa

# 파일 시스템 ext4 이미지 생성
sudo chown -R root:root squashfs-root
truncate -s 400M ubuntu-24.04.ext4
sudo mkfs.ext4 -d squashfs-root -F ubuntu-24.04.ext4

# firecracker 디렉터리로 이동
mv ubuntu-24.04.ext4  ./firecracker/ubuntu-24.04.ext4
mv vmlinux-5.10.225 ./firecracker/vmlinux-5.10.225
mv id_rsa.pub ./firecracker/id_rsa.pub
mv ubuntu-24.04.id_rsa ./firecracker/ubuntu-24.04.id_rsa
chmod 600 ./firecrakcer/ubuntu-24.04.id_rsa

 

이렇게 까지 하면 microVM이 사용할 커널과 파일 시스템이 생성된다. 문제점은 나는 java와 aws-cli가 필요해서 파일 시스템을 커스텀해야 하는데, 커스텀 과정이 너무 복잡하다. 위에서 다운로드한 파일 시스템을 마운트 해서 java와 aws-cli를 설치하고 마운트를 해제하는 방식을 택하기로 했다. 

 

## 마운트 디렉터리 생성
mkdir /mnt

## 디렉터리 확장
dd if=/dev/zero bs=1M count=2000 >> ubuntu-24.04.ext4

## 확장 적용
resize2fs ./ubuntu-24.04.ext4

## 마운트
mount -o loop ./ubuntu-24.04.ext4 /mnt

 

여기서 파일 시스템을 조금 확장해줘야 한다. 빠른 시작 가이드에서 제공해 주는 파일 시스템 크기는 350MB로 java와 aws-cli를 설치할 수 없다. 따라서 2GB로 늘려주는 작업을 해주고 마운트 해준다. (뒤의 과정을 전부하고 microVM에 접속해서 java와 aws-cli를 설치해야 한다.)

 

ARCH="$(uname -m)"
release_url="https://github.com/firecracker-microvm/firecracker/releases"
latest=$(basename $(curl -fsSLI -o /dev/null -w  %{url_effective} ${release_url}/latest))
curl -L ${release_url}/download/${latest}/firecracker-${latest}-${ARCH}.tgz \
| tar -xz

# 바이너리 파일
mv release-${latest}-$(uname -m)/firecracker-${latest}-${ARCH} firecracker

 

이렇게까지 하면 firecracker 실행 준비는 끝났고 아래의 스크립트를 사용하자. 이 스크립트는 하나의 고정된 IP로 실행하므로 변경이 필요하지만 일단 실행해서 파일 시스템에 필요한 것들을 설치해 주자.

 

#!/bin/bash

set -e

TAP_DEV="tap0"
TAP_IP="172.16.0.1"
MASK_SHORT="/30"
API_SOCKET="/tmp/firecracker.socket"
LOGFILE="./firecracker.log"

# Cleanup previous instances
sudo killall firecracker 2>/dev/null || true
sudo rm -f "${API_SOCKET}"

# Start Firecracker
sudo ./firecracker --api-sock "${API_SOCKET}" &

# Wait for the API socket to be created
while [ ! -S "${API_SOCKET}" ]; do
    sleep 0.1
done

# Setup network interface
sudo ip link del "$TAP_DEV" 2>/dev/null || true
sudo ip tuntap add dev "$TAP_DEV" mode tap
sudo ip addr add "${TAP_IP}${MASK_SHORT}" dev "$TAP_DEV"
sudo ip link set dev "$TAP_DEV" up

# Enable ip forwarding
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -P FORWARD ACCEPT

# Determine host interface
HOST_IFACE=$(ip -j route list default | jq -r '.[0].dev')
echo $HOST_IFACE
# Set up microVM internet access
sudo iptables -t nat -D POSTROUTING -o "$HOST_IFACE" -j MASQUERADE 2>/dev/null || true
sudo iptables -t nat -A POSTROUTING -o "$HOST_IFACE" -j MASQUERADE

# Create log file
touch $LOGFILE

# Set log file
curl -X PUT --unix-socket "${API_SOCKET}" \
    --data "{
        \"log_path\": \"${LOGFILE}\",
        \"level\": \"Debug\",
        \"show_level\": true,
        \"show_log_origin\": true
    }" \
    "http://localhost/logger"

sudo curl -X PUT --unix-socket "$API_SOCKET" \
    --data "{
        \"vcpu_count\": 2,
        \"mem_size_mib\": 1024
    }" "http://localhost/machine-config"

KERNEL="./$(ls vmlinux* | tail -1)"
KERNEL_BOOT_ARGS="console=ttyS0 reboot=k panic=1 pci=off"

ARCH=$(uname -m)

if [ ${ARCH} = "aarch64" ]; then
    KERNEL_BOOT_ARGS="keep_bootcon ${KERNEL_BOOT_ARGS}"
fi

# Set boot source
curl -X PUT --unix-socket "${API_SOCKET}" \
    --data "{
        \"kernel_image_path\": \"${KERNEL}\",
        \"boot_args\": \"${KERNEL_BOOT_ARGS}\"
    }" \
    "http://localhost/boot-source"

ROOTFS="./ubuntu-24.04.ext4"

# Set rootfs
curl -X PUT --unix-socket "${API_SOCKET}" \
    --data "{
        \"drive_id\": \"rootfs\",
        \"path_on_host\": \"${ROOTFS}\",
        \"is_root_device\": true,
        \"is_read_only\": false
    }" \
    "http://localhost/drives/rootfs"

FC_MAC="06:00:AC:10:00:02"

# Set network interface
curl -X PUT --unix-socket "${API_SOCKET}" \
    --data "{
        \"iface_id\": \"net1\",
        \"guest_mac\": \"$FC_MAC\",
        \"host_dev_name\": \"$TAP_DEV\"
    }" \
    "http://localhost/network-interfaces/net1"

# Start microVM
curl -X PUT --unix-socket "${API_SOCKET}" \
    --data "{
        \"action_type\": \"InstanceStart\"
    }" \
    "http://localhost/actions"

# Wait for the VM to be fully booted
sleep 10

ssh -i ./ubuntu-24.04.id_rsa -o StrictHostKeyChecking=no root@172.16.0.2 << EOF
    ip route add default via 172.16.0.1 dev eth0
    echo 'nameserver 8.8.8.8' > /etc/resolv.conf
EOF

echo "VM is ready. You can now SSH into it using:"
echo "ssh -i ./ubuntu-24.04.id_rsa root@172.16.0.2"

# Optionally, you can automatically SSH into the VM:
# ssh -i ./ubuntu-24.04.id_rsa root@172.16.0.2

echo "Use 'root' for both the login and password."
echo "Run 'reboot' to exit."

 

실행하면 아래와 같이 microVM이 생성되고, 접속해 보자.

 

 

아래 명령어를 통해서 접속하자.

ssh -i ./ubuntu-24.04.id_rsa root@172.16.0.2

 

아래의 명령어를 통해서 java21과 aws-cli를 설치할 수 있다.

apt update 

apt install openjdk-21-jre-headless
mkdir -p /usr/share/man/man1
dpkg --configure -a
apt remove --purge openjdk-21-jdk openjdk-21-jre-headless openjdk-21-jre openjdk-21-jdk-headless
apt install openjdk-21-jdk

apt install unzip
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
./aws/install

 

 

이제 reboot을 입력해서 정상적으로 microVM을 종료시켜서 파일시스템에 정합성에 문제없게 한다. 마운트 한 것을 해제한다.

 

umount /mnt

 

이제 스크립트를 수정해서 생성 요청이 올 때 다이내믹하게 IP를 할당해서 여러 개의 microVM을 가동할 수 있도록 만들어보자 전체 스크립트는 아래와 같다. 여기서 플로우는 아래와 같다.

 

1. 랜덤 한 IP 생성

2. 현재 사용하고 있는 IP인지 확인 후 있다면 다시 생성

3. microVM 생성

 

#!/bin/bash

set -euo pipefail

# Function to generate a random SB_ID
generate_random_sb_id() {
    local id
    while true; do
        id=$(( RANDOM % 255 ))  # Generate a random SB_ID (0 to 254)
        if ! ip a | grep -q "176.12.0.${id}"; then
            echo "${id}"
            return
        fi
    done
}

curl_put() {
    local URL_PATH="$1"
    local OUTPUT RC
    OUTPUT="$("${CURL[@]}" -X PUT --data @- "http://localhost/${URL_PATH#/}" 2>&1)"
    RC="$?"
    if [ "$RC" -ne 0 ]; then
        echo "Error: curl PUT ${URL_PATH} failed with exit code $RC, output:"
        echo "$OUTPUT"
        return 1
    fi
    # Error if output doesn't end with "HTTP 2xx"
    if [[ "$OUTPUT" != *HTTP\ 2[0-9][0-9] ]]; then
        echo "Error: curl PUT ${URL_PATH} failed with non-2xx HTTP status code, output:"
        echo "$OUTPUT"
        return 1
    fi
}

start_microvm_with_network() {
    # Firecracker 실행
    sudo rm -f "$API_SOCKET"
    sudo ./firecracker --api-sock "$API_SOCKET" &

    while [ ! -e "$API_SOCKET" ]; do
        echo "FC $SB_ID still not ready..."
        sleep 0.01
    done

    ip link del "$TAP_DEV" 2> /dev/null || true
    ip tuntap add dev "$TAP_DEV" mode tap
    ip link set dev "$TAP_DEV" up
    ip addr add "${TAP_IP}${MASK_SHORT}" dev "$TAP_DEV"
    sysctl -w net.ipv4.conf.${TAP_DEV}.proxy_arp=1 > /dev/null
    sysctl -w net.ipv6.conf.${TAP_DEV}.disable_ipv6=1 > /dev/null

    # 로그 설정
    curl_put "/logger" <<EOF
{
    "level": "Info",
    "log_path": "$LOGFILE",
    "show_level": false,
    "show_log_origin": false
}
EOF

    # 머신 설정
    curl_put "/machine-config" <<EOF
{
    "vcpu_count": 2,
    "mem_size_mib": 1024
}
EOF

    # 부팅 소스 설정
    curl_put "/boot-source" <<EOF
{
    "kernel_image_path": "$KERNEL",
    "boot_args": "$KERNEL_BOOT_ARGS"
}
EOF

   # 루트 파일 시스템 설정
    curl_put "/drives/$SB_ID" <<EOF
{
    "drive_id": "$SB_ID",
    "path_on_host": "$ROOTFS",
    "is_root_device": true,
    "is_read_only": false
}
EOF

    # 네트워크 인터페이스 설정
    curl_put "/network-interfaces/$SB_ID" <<EOF
{
    "iface_id": "$SB_ID",
    "guest_mac": "$FC_MAC",
    "host_dev_name": "$TAP_DEV"
}
EOF

    # microVM 시작
    curl_put '/actions' <<EOF
{
    "action_type": "InstanceStart"
}
EOF

    echo "InstanceStart command executed"

    sleep 10
}

execute_script_in_microvm() {
    # SSH를 통해 S3에서 스크립트 다운로드 및 실행
    ssh -o StrictHostKeyChecking=no -i ./ubuntu-24.04.id_rsa root@${FC_IP} << EOF
        echo "$FC_IP"
EOF
}


SB_ID="${1:-$(generate_random_sb_id)}"

# 필요한 변수 설정
TAP_DEV="tap${SB_ID}"
BUCKET_NAME="your-bucket-name"  # S3 버킷 이름
SCRIPT_PATH="path/toclear/my_script.sh"  # S3에서의 스크립트 경로

MASK_LONG="255.255.255.252"
MASK_SHORT="/30"
FC_IP="$(printf '176.12.0.%s' $(((4 * SB_ID + 1) % 256)))"
TAP_IP="$(printf '176.12.0.%s' $(((4 * SB_ID + 2) % 256)))"
FC_MAC="$(printf '02:FC:00:00:%02X:%02X' $((SB_ID / 256)) $((SB_ID % 256)))"
ROOTFS="./ubuntu-24.04.ext4"

LOGFILE="$PWD/output/fc-sb${SB_ID}-log"
touch "$LOGFILE"

API_SOCKET="/tmp/firecracker-sb${SB_ID}.socket"
CURL=(curl --silent --show-error --header "Content-Type: application/json" --unix-socket "${API_SOCKET}" --write-out "HTTP %{http_code}")

KERNEL_BOOT_ARGS="console=ttyS0 reboot=k panic=1 pci=off ip=${FC_IP}::${TAP_IP}:${MASK_LONG}::eth0:off"
KERNEL="./$(ls vmlinux* | tail -1)"

# Start the microVM with networking
start_microvm_with_network

execute_script_in_microvm

 

실행하면 생성된다. 여기서 시간이 오래 걸렸던 부분은 IP와 MAC 주소를 같게 해 줘야 네트워크가 정상적으로 동작한다는 점이다. 나는 MAC주소는 랜덤 하게 했었는데, 공식 문서에서는 같게 해줘야 한다고 나온다. 

 

공식은 공식 문서에서 찾았다. 

 

스크립트를 실행하기 앞서 아래의 명령어를 입력하여 로그 파일이 생성될 디렉터리를 생성해 주자.

mkdir output

 

이제 가동해서 java와 aws-cli를 사용할 수 있는지 확인하자. 

 

정상적으로 나오는 것을 확인했다. 이제 해당 파일 시스템을 이용해서 microVM을 생성하면 된다.

 

정리

문서가 생각보다 잘 작성돼서 쉬울 것이라고 생각했는데, 어려웠던 부분들이 많았다.

 

1. 필요한 라이브러리 설정 -> 마운트 해서 미리 설치

2. 다이내믹 IP 할당 -> 랜덤 IP 설정 후 중복 확인

 

이제 앞으로 해야 할 것은 해당 스크립트를 실행할 java 애플리케이션을 생성하는 것이 남았다.