> ## Documentation Index
> Fetch the complete documentation index at: https://blog.nvim.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Kafka 실습용 Ubuntu VM 준비하기

> virt-install과 cloud-init으로 Ubuntu VM 두 대를 준비하고 Docker와 직접 설치 트랙이 같은 조건에서 시작하도록 점검해요.

> 실습하다 설정이 꼬였을 때, Kafka부터 다시 지우지 않고 깨끗한 VM으로 돌아갈 수 있으면 어떨까요?

Kafka를 처음 실행하는 데는 broker 하나면 충분해요. 하지만 설치 방식, data directory, network 설정이 섞이면 개념을 보는 시간보다 환경을 복구하는 시간이 더 길어질 수 있어요.

그래서 먼저 같은 Ubuntu에서 출발하는 VM 두 대를 준비할게요.

* `kafka-docker`: Ubuntu 안에서 공식 Kafka Docker image를 실행해요.
* `kafka-ubuntu`: Ubuntu에 Java와 Kafka binary archive를 직접 설치해요.

두 환경은 설치 방식만 달라요. 다음 글부터 topic, key, consumer group, offset을 같은 이름과 같은 record로 확인합니다.

<Note title="적용 버전">
  이 VM에서 이어갈 실습은 **Apache Kafka 4.3.1**을 기준으로 해요. VM 준비 절차는 **Ubuntu 26.04 LTS**, **amd64**와 해당 Ubuntu·Docker 공식 문서를 기준으로 해요. 아래의 `2 vCPU`, `4 GiB RAM`, `25 GiB disk`는 이 시리즈가 사용하는 학습용 profile이지 Kafka나 Ubuntu의 일반적인 최소 사양이 아니에요.
</Note>

***

## 완성할 환경부터 그려봐요

```mermaid theme={null}
flowchart TB
    H[CLI Linux host<br />KVM · libvirt · virt-install]
    N[libvirt default network<br />NAT]

    subgraph D[kafka-docker VM]
        DU[Ubuntu 26.04 LTS]
        DE[Docker Engine]
        DK[apache/kafka:4.3.1]
        DU --> DE --> DK
    end

    subgraph U[kafka-ubuntu VM]
        UU[Ubuntu 26.04 LTS]
        J[OpenJDK 21]
        KB[kafka_2.13-4.3.1.tgz]
        UU --> J --> KB
    end

    H --> N
    N --> D
    N --> U
```

처음 네 실습에서는 client도 broker와 같은 VM에서 실행하고 `localhost:9092`로 접속해요. 그래서 기본 NAT network면 충분합니다. 나중에 여러 broker를 서로 다른 VM에 띄울 때 고정된 hostname과 VM 사이의 통신을 다시 설계할 거예요.

***

## CLI host를 먼저 확인해요

이 글은 GUI가 없는 Ubuntu CLI host를 기준으로 해요. `virt-install`과 `virsh`는 VM 안이 아니라 **VM을 실행할 host**에서 사용합니다.

<Steps>
  <Step title="CPU virtualization을 확인해요">
    Ubuntu의 `kvm-ok`는 `cpu-checker` package에 들어 있어요.

    ```bash theme={null}
    sudo apt update
    sudo apt install -y cpu-checker
    kvm-ok
    ```

    Hardware virtualization을 사용할 수 있다는 결과가 나와야 해요. 사용할 수 없다면 BIOS 또는 UEFI에서 Intel VT-x나 AMD-V가 꺼져 있지 않은지 먼저 확인하세요.
  </Step>

  <Step title="KVM, libvirt, virt-install을 설치해요">
    ```bash theme={null}
    sudo apt install -y \
      qemu-kvm \
      libvirt-daemon-system \
      libvirt-clients \
      virtinst \
      osinfo-db
    sudo systemctl enable --now libvirtd
    systemctl is-active libvirtd
    ```

    마지막 명령이 `active`를 출력하면 libvirt service가 요청을 받을 준비가 된 거예요.
  </Step>

  <Step title="현재 사용자에게 VM 관리 권한을 줘요">
    ```bash theme={null}
    sudo adduser "$USER" libvirt
    ```

    이미 `libvirt` group에 속해 있다는 메시지가 나올 수도 있어요. 새로 추가됐다면 로그아웃한 뒤 다시 로그인해야 group 권한이 반영됩니다.

    ```bash theme={null}
    id
    virsh --connect qemu:///system list --all
    ```
  </Step>

  <Step title="default NAT network를 켜요">
    ```bash theme={null}
    if ! virsh --connect qemu:///system net-list --name | grep -qx default; then
      virsh --connect qemu:///system net-start default
    fi

    virsh --connect qemu:///system net-autostart default
    virsh --connect qemu:///system net-info default
    ```

    `Active: yes`와 `Autostart: yes`가 보여야 해요. 두 VM은 이 network에서 사설 address를 받고 host를 거쳐 인터넷에 나갑니다.
  </Step>
</Steps>

<Check title="Host 준비 확인">
  `kvm-ok`가 hardware acceleration을 사용할 수 있다고 알리고, `libvirtd`가 `active`이며, `virsh --connect qemu:///system list --all`과 `net-info default`가 권한 오류 없이 끝나면 VM을 만들 준비가 됐어요.
</Check>

***

## Ubuntu cloud image를 받고 checksum을 확인해요

화면에서 installer 항목을 고르는 대신, 설치가 끝난 Ubuntu 26.04 LTS cloud image에 `cloud-init` 설정을 주입할게요. Host가 ARM이라면 architecture가 맞는 공식 image를 골라야 하며, amd64 image를 그대로 사용하면 안 돼요.

<Steps>
  <Step title="Cloud image와 SHA256SUMS를 받아요">
    ```bash theme={null}
    mkdir -p "$HOME/images/ubuntu-26.04"
    cd "$HOME/images/ubuntu-26.04"

    curl -fSLO \
      https://cloud-images.ubuntu.com/releases/26.04/release/ubuntu-26.04-server-cloudimg-amd64.img
    curl -fSLO \
      https://cloud-images.ubuntu.com/releases/26.04/release/SHA256SUMS
    ```
  </Step>

  <Step title="내려받은 image를 검증해요">
    ```bash theme={null}
    sha256sum -c SHA256SUMS --ignore-missing
    ```

    `ubuntu-26.04-server-cloudimg-amd64.img: OK`가 나오기 전에는 VM disk를 만들지 마세요. `FAILED`가 나오면 손상된 파일을 사용하지 말고 공식 cloud image server에서 다시 받으세요.
  </Step>
</Steps>

***

## 두 VM을 같은 profile로 준비해요

이제 host terminal에서 `cloud-init` 설정과 VM disk를 만든 뒤 `virt-install`로 두 VM을 정의할게요. 두 VM은 같은 base image에서 출발하지만 각각 처음 boot하며 서로 다른 hostname과 machine identity를 만듭니다.

<Steps>
  <Step title="실습 전용 SSH key를 만들어요">
    Host에서 두 VM에 접속할 때 사용할 key예요. 이미 만들었다면 기존 key를 그대로 사용합니다.

    ```bash theme={null}
    install -d -m 0700 "$HOME/.ssh"

    if [ ! -f "$HOME/.ssh/kafka-lab" ]; then
      ssh-keygen -t ed25519 \
        -f "$HOME/.ssh/kafka-lab" \
        -C kafka-lab
    fi
    ```
  </Step>

  <Step title="두 VM의 cloud-init 설정을 만들어요">
    두 VM 모두 `kafka` 계정을 사용하지만 hostname은 다르게 넣어요. Password login은 열지 않고 앞에서 만든 SSH public key만 등록합니다.

    ```bash theme={null}
    install -d -m 0700 "$HOME/kafka-lab/cloud-init"
    KAFKA_LAB_PUBLIC_KEY=$(cat "$HOME/.ssh/kafka-lab.pub")

    for VM_NAME in kafka-docker kafka-ubuntu; do
      cat > "$HOME/kafka-lab/cloud-init/${VM_NAME}-user-data" <<EOF
    #cloud-config
    hostname: ${VM_NAME}
    manage_etc_hosts: true
    ssh_pwauth: false
    users:
      - name: kafka
        groups: [adm, sudo]
        shell: /bin/bash
        lock_passwd: true
        sudo: ["ALL=(ALL) NOPASSWD:ALL"]
        ssh_authorized_keys:
          - ${KAFKA_LAB_PUBLIC_KEY}
    EOF

      cat > "$HOME/kafka-lab/cloud-init/${VM_NAME}-meta-data" <<EOF
    instance-id: ${VM_NAME}-01
    local-hostname: ${VM_NAME}
    EOF
    done
    ```

    이 실습은 IP를 libvirt DHCP lease에서 읽고, VM을 끈 뒤 snapshot을 만들어요. `qemu-guest-agent`가 필요하지 않으므로 첫 boot에서 package를 설치하거나 `systemctl`을 실행하지 않습니다.
  </Step>

  <Step title="두 qcow2 disk를 25 GiB로 만들어요">
    검증한 cloud image를 각 VM의 독립 disk로 복사하고 virtual size를 늘려요. 이 명령을 실행하기 전에 같은 이름의 VM과 disk가 없어야 합니다.

    ```bash theme={null}
    CLOUD_IMAGE="$HOME/images/ubuntu-26.04/ubuntu-26.04-server-cloudimg-amd64.img"

    for VM_NAME in kafka-docker kafka-ubuntu; do
      if sudo test -e "/var/lib/libvirt/images/${VM_NAME}.qcow2"; then
        echo "이미 ${VM_NAME}.qcow2가 있습니다. 덮어쓰지 않고 중단합니다." >&2
        exit 1
      fi

      sudo qemu-img convert \
        -f qcow2 \
        -O qcow2 \
        "$CLOUD_IMAGE" \
        "/var/lib/libvirt/images/${VM_NAME}.qcow2"
      sudo qemu-img resize \
        "/var/lib/libvirt/images/${VM_NAME}.qcow2" \
        25G
      sudo chown libvirt-qemu:kvm \
        "/var/lib/libvirt/images/${VM_NAME}.qcow2"
    done
    ```
  </Step>

  <Step title="CLI 옵션으로 두 VM을 만들어요">
    Host의 `osinfo-db`가 Ubuntu 26.04를 알면 그 profile을 사용하고, 아직 없다면 `generic`으로 넘어가요. Disk와 NIC는 아래에서 `virtio`로 직접 지정하므로 이 fallback도 같은 resource profile을 만듭니다.

    다음 명령은 각 VM에 `2 vCPU`, `4 GiB RAM`, `25 GiB qcow2 disk`, `default` NAT의 `virtio` NIC를 지정해요. `--graphics none`과 `--noautoconsole`이 GUI console을 만들거나 열지 않게 합니다.

    ```bash theme={null}
    if virt-install --osinfo list | grep -qx ubuntu26.04; then
      KAFKA_LAB_OSINFO=ubuntu26.04
    else
      KAFKA_LAB_OSINFO=generic
    fi

    echo "OS profile: $KAFKA_LAB_OSINFO"

    for VM_NAME in kafka-docker kafka-ubuntu; do
      virt-install \
        --connect qemu:///system \
        --name "$VM_NAME" \
        --memory 4096 \
        --vcpus 2 \
        --cpu host-passthrough \
        --osinfo "$KAFKA_LAB_OSINFO" \
        --import \
        --disk "path=/var/lib/libvirt/images/${VM_NAME}.qcow2,format=qcow2,bus=virtio" \
        --network network=default,model=virtio \
        --graphics none \
        --console pty,target.type=serial \
        --noautoconsole \
        --cloud-init \
          "user-data=$HOME/kafka-lab/cloud-init/${VM_NAME}-user-data,meta-data=$HOME/kafka-lab/cloud-init/${VM_NAME}-meta-data"
    done
    ```

    Host resource가 부족하다면 한 VM을 `virsh shutdown`으로 끈 뒤 다른 VM을 실행해도 돼요. 이 profile을 Kafka의 일반적인 최소 사양으로 해석하지는 마세요.
  </Step>

  <Step title="IP를 확인하고 SSH로 접속해요">
    첫 boot에서는 `cloud-init`이 사용자와 SSH 설정을 적용하므로 잠시 기다려야 할 수 있어요. DHCP address가 보이면 각 VM에 접속해 초기화가 끝날 때까지 기다립니다.

    ```bash theme={null}
    virsh --connect qemu:///system list --all
    virsh --connect qemu:///system domifaddr kafka-docker --source lease
    virsh --connect qemu:///system domifaddr kafka-ubuntu --source lease

    KAFKA_DOCKER_IP=$(virsh --connect qemu:///system domifaddr kafka-docker --source lease \
      | awk '$3 == "ipv4" {sub(/\/.*/, "", $4); print $4; exit}')
    KAFKA_UBUNTU_IP=$(virsh --connect qemu:///system domifaddr kafka-ubuntu --source lease \
      | awk '$3 == "ipv4" {sub(/\/.*/, "", $4); print $4; exit}')

    printf 'kafka-docker: %s\nkafka-ubuntu: %s\n' \
      "$KAFKA_DOCKER_IP" \
      "$KAFKA_UBUNTU_IP"

    test -n "$KAFKA_DOCKER_IP" && test -n "$KAFKA_UBUNTU_IP"

    ssh -i "$HOME/.ssh/kafka-lab" "kafka@$KAFKA_DOCKER_IP"
    sudo cloud-init status --wait
    exit

    ssh -i "$HOME/.ssh/kafka-lab" "kafka@$KAFKA_UBUNTU_IP"
    sudo cloud-init status --wait
    ```

    IP 변수가 비어 있어 `test`가 실패하면 잠시 기다린 뒤 이 명령을 다시 실행하세요.
  </Step>
</Steps>

<Info>
  **기존 VM에서 systemctl 인증을 요구한다면**

  `Authenticating as: kafka`가 보이면 해당 `systemctl`은 cloud-init의 root 단계가 아니라 `kafka` login session에서 실행된 거예요. `Ctrl+C`로 취소하세요. 이 실습에서는 `qemu-guest-agent`를 enable하거나 start할 필요가 없습니다.

  기존 VM에서는 아래 명령으로 passwordless sudo와 첫 boot 결과만 확인할 수 있어요.

  ```bash theme={null}
  sudo -n true
  sudo cloud-init status --long
  sudo journalctl -u cloud-final --no-pager -n 80
  ```

  첫 명령이 password prompt 없이 끝나면 cloud-init이 만든 sudo rule은 적용된 거예요. 예전 `runcmd` 때문에 cloud-init 상태가 `error`여도 SSH 접속과 `virsh --source lease` 확인이 된다면 이 실습은 계속 진행할 수 있습니다.
</Info>

<Tip title="한 번에 두 VM이 필요하지는 않아요">
  기초 글에서는 같은 명령을 두 환경에서 차례로 확인해요. `kafka-docker`를 종료한 뒤 `kafka-ubuntu`를 실행해도 됩니다. 세 broker 장애 실습에 들어갈 때 별도의 다중 VM profile을 준비할게요.
</Tip>

***

## 각 VM의 출발 상태를 기록해요

두 VM에 로그인해서 hostname, address, clock, disk를 확인해요. 아래 결과는 복사해 두면 나중에 listener나 disk 문제를 추적할 때 기준점이 됩니다.

```bash theme={null}
hostnamectl --static
. /etc/os-release && echo "$PRETTY_NAME"
uname -m
ip -br address
timedatectl show --property=NTPSynchronized --value
df -h /
```

두 VM에서 확인할 항목은 이렇습니다.

| 확인              | 기대하는 값                         |
| --------------- | ------------------------------ |
| Hostname        | `kafka-docker`, `kafka-ubuntu` |
| Ubuntu          | 두 VM 모두 26.04 LTS              |
| Architecture    | 두 VM 모두 `x86_64`               |
| Clock           | 두 VM 모두 NTP synchronized       |
| Root filesystem | 각 VM의 설치 뒤 여유 공간 기록            |

***

## 설치 방식별 도구를 준비해요

<Tabs>
  <Tab title="Docker">
    `kafka-docker` VM에서 Docker의 공식 apt repository를 사용해 Engine을 설치해요.

    <Steps>
      <Step title="Docker repository key를 등록해요">
        ```bash theme={null}
        sudo apt update
        sudo apt install -y ca-certificates curl
        sudo install -m 0755 -d /etc/apt/keyrings
        sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
          -o /etc/apt/keyrings/docker.asc
        sudo chmod a+r /etc/apt/keyrings/docker.asc
        ```
      </Step>

      <Step title="Docker apt source를 등록해요">
        ```bash theme={null}
        sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
        Types: deb
        URIs: https://download.docker.com/linux/ubuntu
        Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
        Components: stable
        Architectures: $(dpkg --print-architecture)
        Signed-By: /etc/apt/keyrings/docker.asc
        EOF

        sudo apt update
        ```
      </Step>

      <Step title="Docker Engine을 설치하고 확인해요">
        ```bash theme={null}
        sudo apt install -y \
          docker-ce \
          docker-ce-cli \
          containerd.io \
          docker-buildx-plugin \
          docker-compose-plugin

        sudo systemctl is-active docker
        sudo docker run --rm hello-world
        sudo docker version
        ```

        다음 Kafka 글의 Docker 명령은 `sudo docker`를 사용해요. Root 없이 실행하도록 바꾸고 싶다면 Docker의 Linux post-install 안내를 먼저 읽고 권한 범위를 이해하세요.
      </Step>
    </Steps>

    <Warning title="docker group은 root 수준의 권한을 줄 수 있어요">
      단지 `sudo`를 생략하려고 사용자를 `docker` group에 추가하지 마세요. Docker daemon을 제어할 수 있는 계정은 host에서 높은 권한을 얻을 수 있어요. 이 시리즈는 권한 경계를 숨기지 않기 위해 `sudo docker`를 그대로 사용합니다.
    </Warning>
  </Tab>

  <Tab title="Ubuntu 직접 설치">
    `kafka-ubuntu` VM에는 Kafka broker가 지원하는 Java와 archive 검증 도구를 설치해요. Kafka archive 자체는 다음 글에서 받아요.

    <Steps>
      <Step title="Java와 다운로드 도구를 설치해요">
        ```bash theme={null}
        sudo apt update
        sudo apt install -y openjdk-21-jre-headless curl tar coreutils
        ```
      </Step>

      <Step title="실제로 선택된 Java를 확인해요">
        ```bash theme={null}
        java -version
        command -v java
        ```

        Kafka 4.3 broker와 tool은 Java 17 이상이 필요해요. 이 시리즈는 Ubuntu package로 설치한 OpenJDK 21을 사용합니다.
      </Step>
    </Steps>
  </Tab>
</Tabs>

***

## Kafka를 설치하기 전 snapshot을 남겨요

두 VM을 정상 종료한 뒤 host의 `virsh`로 각각 `tooling-ready` snapshot을 만들어요. 아래 snapshot은 `qcow2` disk를 사용하는, 전원이 꺼진 VM을 기준으로 합니다.

```bash theme={null}
for VM_NAME in kafka-docker kafka-ubuntu; do
  virsh --connect qemu:///system shutdown "$VM_NAME"
done

for VM_NAME in kafka-docker kafka-ubuntu; do
  while [ "$(LC_ALL=C virsh --connect qemu:///system domstate "$VM_NAME")" != "shut off" ]; do
    sleep 2
  done
done

virsh --connect qemu:///system list --all
```

두 VM의 상태가 모두 `shut off`가 된 뒤 snapshot을 만드세요.

```bash theme={null}
for VM_NAME in kafka-docker kafka-ubuntu; do
  virsh --connect qemu:///system snapshot-create-as \
    --domain "$VM_NAME" \
    --name tooling-ready \
    --description "Kafka 설치 전 도구 준비 완료"

  virsh --connect qemu:///system snapshot-list "$VM_NAME"
done
```

<Warning title="Snapshot으로 돌아가면 그 뒤의 Kafka data도 사라져요">
  Snapshot restore는 VM disk를 과거 상태로 되돌려요. 이후 만든 topic, record, consumer offset이 필요하다면 먼저 별도로 보존하세요. 다음 글에서 `working-first-record` snapshot을 추가로 만들기 전까지 `tooling-ready`는 Kafka가 전혀 설치되지 않은 복구 지점이에요.
</Warning>

<Check title="두 실습 환경의 출발점 확인">
  `kafka-docker`에서 `sudo docker version`이 성공하고, `kafka-ubuntu`에서 Java 21이 확인되며, 두 VM 모두 `tooling-ready` snapshot을 가지고 있다면 다음 글을 시작할 준비가 됐어요.
</Check>

<Columns cols={2}>
  <Card title="이전 글" icon="arrow-left" href="/messaging/kafka/why-kafka-and-log-based-messaging-exist">
    Kafka가 왜 전달보다 기록을 중심에 두는지 다시 봐요.
  </Card>

  <Card title="다음 글" icon="play" href="/messaging/kafka/run-local-kafka-and-send-first-record">
    두 VM에 Kafka를 실행하고 같은 첫 record를 왕복해요.
  </Card>
</Columns>
