티스토리 뷰

로컬 서버(Laptop)에서 테스트를 위해 VM을 특정한 환경(예를 들어 kubernetes, ceph와 같은)으로 구성해야할 경우가 종종있다. 이때 마다 불필요하게 반복된 작업을 해야할 경우가 있었다. 이를 Terraform을 통해 코드화 하여 한번 구성해 놓으면 이후에도 손쉽게 환경을 구성할수 있도록 해보고자 했다. 검색해보니 아쉽게도 official하게 제공되지는 않지만 libvirt-provider가 있어 이를 활용해 KVM환경에서 VM을 생성 및 관리하는 방법에 대해 알아보고 테스트 했던 내용을 기반으로 기술해보고자 한다.

그럼, Terraform을 이용하여 KVM환경에서 VM을 어떻게 생성하는지 알아보도록 하자.

우선, 실행해보았던 환경은 다음과 같다.

jacob@jacob-laptop:~/workspaces/tf-virt$ tf --version
Terraform v0.12.20
+ provider.libvirt (unversioned)
jacob@jacob-laptop:~$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="19.10 (Eoan Ermine)"

libvirt provider 를 위한 plugin 설치

아래 링크를 참고하여 terraform을 libvirt provider로 사용할수 있도록 도와주는 module을 다운로드 받고 plugin으로 사용할수 있도록 한다. (참고로 경로는 .terraform.d/plugins/ 로 복사하면 된다.)

Hashicorp에서 정식 지원하는 것은 아닌것으로 보이며 suse engineer가 개발 및 유지보수를 진행중인 것으로 보이며 기본적인 기능은 동작되기에 사용하는데 무리는 없는것으로 보인다.

실제 설치를 위해서는 다음 명령어들을 사용할 수 있다.

wget https://github.com/dmacvicar/terraform-provider-libvirt/releases/download/v0.6.1/terraform-provider-libvirt-0.6.1+git.1578064534.db13b678.Ubuntu_18.04.amd64.tar.gz
mkdir ~/.terraform.d/plugins/
mv terraform-provider-libvirt ~/.terraform.d/plugins

만약 아래와 같은 permission 문제에 당면했을 경우 (참고로 ubuntu 사용자일 경우 발생될 수 있다.) 아래의 링크를 참고하여
/etc/libvirtd/qemu.conf의 설정을 변경한다.

Error: virError(Code=38, Domain=7, Message='Failed to connect socket to '/var/run/libvirt/libvirt-sock': Permission denied')

  on main.tf line 1, in provider "libvirt":
   1: provider "libvirt" {

위 링크를 종합해 보면 /etc/libvirtd/qemu.conf 내에 아래와 같은 설정을 추가하고 libvirtd 를 재시작하면 된다.

user = root
group = root
security_driver = "none"

만약 terraform 0.13 이상의 버전을 사용하는 경우라면 아래링크를 참고하여 home directory가 아닌 실제 terraform 이 실행되는 경로에 .terraform/plugins/registry.terraform.io/dmacvicar/libvirt/0.6.2/linux_amd64 경로에 해당 binary를 복사해놓고 init을 수행한다.

Terraform을 이용한 resource 생성

각 resource를 생성하기전에 다음 항목에 대한 이해를 하고 진행하자.

  • domain : VM 생성에 사용 (volume 생성과 함께 사용되어야 함)
  • network : 네트워크 생성에 사용
  • volume : 디스크 생성에 사용
  • cloud-init : VM이 최초 실행되면서 실행되는 스크립트(ex. root 패스워드 변경, network interface ip 설정 등)

network

네트워크 생성은 아래와 같이 mode를 지정하여 생성할수 있으며

resource "libvirt_network" "pubnet" {
  name = "pub-net"
  mode = "nat"
  addresses = ["172.16.101.0/24"]
  dns {
    enabled = true
    local_only = true
  }
}

생성과 함께 domain 즉, VM에 attach 시켜줄수 있다.

resource "libvirt_domain" "vmname" {
...
  network_interface {
    network_id = libvirt_network.pubnet.id
    hostname   = "centos7"
    addresses  = ["${var.vm_ipaddr}"]
    wait_for_lease = true
  }
...
}

multi network_interface

다수의 interface를 추가할수 있으며 다수를 추가할 경우 각 network_interface를 위와 같이 선언해주어야 한다.

resource "libvirt_domain" "db1" {
  name   = "db1"
  memory = "1024"
  vcpu   = 1

  network_interface {
    network_name = "default"
  }

  network_interface {
    network_id = libvirt_network.test_network.id
  }
...

NOTE
interface를 다수 생성하다보면 device(일반적으로 bridge)가 중복되어 생성이 실패되는 경우가
종종 있다. 이와 같은 경우를 대비하여 특정한 device(ex. br10과 같이 쉽게 사용안될만한)를
지정하는것이 좋다.

또한 eth0이외에 network interface를 추가했을 경우 ip를 addresses 항목에 추가했음에도
지정한 IP가 아닌 다른 IP를 가져오는 경우가 있다. 이와 같은 이유는 다음과 같이 hostsfile내에 지정되어 있지 않기 때문이다.

root@jacob-laptop:/var/lib/libvirt/dnsmasq# ls -alh
total 40K
drwxr-xr-x 2 root root 4.0K  2월 24 17:28 .
drwxr-xr-x 7 root root 4.0K  2월 24 09:48 ..
-rw-r--r-- 1 root root    0  2월 24 17:27 default.addnhosts
-rw------- 1 root root  623  2월 24 09:48 default.conf
-rw-r--r-- 1 root root    0  2월 24 17:27 default.hostsfile
-rw-r--r-- 1 root root    0  2월 24 17:28 int-net.addnhosts
-rw------- 1 root root  656  2월 24 17:28 int-net.conf
-rw-r--r-- 1 root root    0  2월 24 17:28 int-net.hostsfile
-rw-r--r-- 1 root root    0  2월 24 17:28 pub-net.addnhosts
-rw------- 1 root root  621  2월 24 17:28 pub-net.conf
-rw-r--r-- 1 root root  126  2월 24 17:28 pub-net.hostsfile
-rw-r--r-- 1 root root    0  2월 19 10:21 virbr0.status
-rw-r--r-- 1 root root  246  2월 24 17:28 virbr7.macs
-rw-r--r-- 1 root root  432  2월 24 17:28 virbr7.status
-rw-r--r-- 1 root root  246  2월 24 17:28 virbr8.macs
-rw-r--r-- 1 root root  425  2월 24 17:28 virbr8.status
root@jacob-laptop:/var/lib/libvirt/dnsmasq# cat pub-net.hostsfile 
52:54:00:3e:10:11,172.16.101.100,centos-0
52:54:00:f2:61:0b,172.16.101.101,centos-1
52:54:00:0a:1e:fc,172.16.101.102,centos-2

관련이유는 좀더 확인이 필요해 보인다.

NOTE
참고로 기존에 생성되어 있던 network를 불러와서 사용하고자 할 경우 network_id가 아닌
network_name을 사용하면 된다. 관련 script를 참고하길 바란다.

network_interface {
  network_name = "existing_network"
  addresses = ["${var.existing_network_subnet}${count.index}"]
  wait_for_lease = false
}

volume

아래와 같이 volume을 생성할수 있다.
참고로 직접 source내에 url을 추가하여 디스크 다운로드부터 수행해볼수도 있다.

resource "libvirt_volume" "emptyvol" {
  name = "empty_vol"
  size = 8589934592
}

resource "libvirt_volume" "rootvol" {
  name = "root_vol"
  pool = "default"
  format = "qcow2"
  source = "/var/lib/libvirt/images/centos7.qcow2"
}

resource "libvirt_domain" "centos" {
...
  disk {
    volume_id = libvirt_volume.rootvol.id
  }

  disk {
    volume_id = libvirt_volume.emptyvol.id
  } 
...

앞서 url을 통해 다운로드를 받아 진행하는 방식을 소개하였고 또다른 방식인
기존에 존재하는 이미지를 복사하여 사용하는 방식은 다음과 같다.

resource "libvirt_volume" "root" {
  name = "rootdisk"
  # 존재하는 image 이름
  base_volume_name = "CentOS-1901.qcow2"
  # qcow image가 존재하는 pool
  base_volume_pool = "default"
  # 실제 image가 생성될 pool
  pool = "kvm-images"
  format = "qcow2"
}

VM 생성

아래와 같이 libvirt_domain을 이용하여 VM을 생성할수 있다.

resource "libvirt_domain" "centos" {
  count = "2"

  name = "centos7-${count.index}"
  memory = "4096"
  vcpu = 2
  cloudinit = libvirt_cloudinit_disk.cloudinit.id

  disk {
    volume_id = libvirt_volume.rootvol.id
  }

또한 count.index를 통해 다수의 VM을 생성할 수도 있다.

NOTE
참고로 volume 생성시 url을 통한 다운로드는 매번 다운로드에 시간이 소요될수 있으니
하나를 사전에 받아두고 해당 disk를 source로 지정하여 사용하기를 권장한다.
혹은 base_volume_* 을 사용하여 이미지를 복사하는방법도 권장한다.

cloud-init

아래와 같이 cloud init 용 volume을 생성하고 libvirt_domain내에서 이를 불러오도록 한다.
당시 cloud_init.cfg로 명명한 파일은 사전에 동일 경로에 생성되어 있어야 한다.

data "template_file" "user_data" {
  template = file("${path.module}/cloud_init.cfg")
}

resource "libvirt_cloudinit_disk" "cloudinit" {
  name = "cloudinit.iso"
  user_data = data.template_file.user_data.rendered
}

resource "libvirt_domain" "centos" {
  ...

  cloudinit = libvirt_cloudinit_disk.cloudinit.id

참고용으로 cloud_init.cfg 내용도 첨부한다.

#cloud-config
ssh_pwauth: true
users:
  - default
  - name: jacob
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: users
chpasswd:
  list: |
    root:root123
  expire: False

참고로 multi interface 항목에서도 이야기 했듯이 interface를 다수 설정은 가능하나 실제 interface를 OS상에서 enable 하거나 ip를 할당하는 방법은 cloud-init을 이용해야 한다.

다음과 같은 cfg파일을 생성하고
(network_config.cfg에는 network 지시어는 빼야한다. https://bugs.launchpad.net/cloud-init/+bug/1679602)

jacob@jacob-laptop:~/workspace/tf-jacob$ cat network_config.cfg 
version: 2
ethernets:
  eth0:
    dhcp4: no
  eth1:
    dhcp4: no

volume 생성시 아래와 같은 cloud-init용 volume을 생성하면 된다.

data "template_file" "network_config" {
  template = file("${path.module}/network_config.cfg")
}

resource "libvirt_cloudinit_disk" "cloudinit" {
  name = "cloudinit.iso"
  user_data = data.template_file.user_data.rendered
  network_config = data.template_file.network_config.rendered
}

다만 알아두어야 할 것은 eth0 interface에 대한 IP설정은 domain상에 설정한 addresses의 값을 참조하나
그외의 interface는 IP설정이 지정한 addresses의 값이 참조되지 않는다. 해당 이유는 아직 확인중이다.

NOTE
가변적인 network interface의 IP를 설정하고자 할 경우 다음과 같이 network_config를 좀더 상세히 작성하면 된다.

data "template_file" "network_config" {
  count = length(var.vm_names)
  template = file("${path.module}/templates/network_config.cfg")

  vars = {
    deploy_addr = "${var.deploy_addr}"
    monitor_addr = "${var.monitor_addr}"
    storage_addr = "${var.storage_addr}"
    internal_addr = "${var.internal_addr}"
    external_addr = "${var.external_addr}${count.index}"
    ip_num  = "${var.vm_num}${count.index}"
    netmask = "24"
    gateway = "192.168.10.1"
  }
}

실제 지정된 network_config.cfg파일을 확인해보면

version: 2
ethernets:
  eth0:
    addresses:
      - ${deploy_addr}.${ip_num}/24
  eth1:
    addresses:
      - ${storage_addr}.${ip_num}/24
  eth2:
    addresses:
      - ${monitor_addr}.${ip_num}/24
  eth3:
    addresses:
      - ${internal_addr}.${ip_num}/24
  eth4:
    addresses:
      - ${external_addr}/${netmask}
    gateway4: ${gateway}
    nameservers:
      addresses: 8.8.8.8

remote_exec 를 활용한 commands 수행

아래와 같이 remote-exec 를 활용해 yum install을 수행해볼수 있다.
현재는 password 방식으로 지정해놓았으며 이를 ssh-key를 활용하는것을 권장한다.

resource "libvirt_domain" "centos" {
  ...

  connection {
    type = "ssh"
    user = "root"
    password = "root123"
    #private_key = "${file("~/.ssh/id_rsa")}"
    host = var.vm_ipaddr

  }

  provisioner "remote-exec" {
    inline = [
      "yum install vim -y"
    ]
  }
}

위에서 진행했던 example들을 아래 gitlab repo에 public으로 open해 놓았으니 참고할것.

Remote KVM 연결

아래와 같은 원격에서 동작중인 kvm 에 연결하여 사용할 수도 있다.
물론 이와 같이 원격 연결을 사용하기 위해서는 ssh key를 사전에 원격 kvm에 등록해 놓아야 한다.

provider "libvirt" {
  uri = "qemu+ssh://jacobbaek@192.168.10.10/system"
}

NOTE
참고할 것은 아래와 같이 source를 terraform을 실행하는 서버(laptop과 같은)의 경로를 입력하는 것이
사용상 좋다. 이유는 다운로드 주소를 넣을 경우 이를 다운로드하기 위한 시간이 다수 소요되고
각 volume마다 이미지를 받게 되어 불필요한 시간이 소요되기 때문이다.

참고

근래(2020.08월경) Terraform이 0.13이 release되면서 terraform init시 에러가 발생된다.

Error while installing hashicorp/libvirt: provider registry
registry.terraform.io does not have a provider named
registry.terraform.io/hashicorp/libvirt

다음 링크를 참고하여 0.13에 맞도록 설정을 변경한다.

아래와 같이 경로를 찾아 생성하고 terraform-provider-libvirt(0.6.2 이상 필수) 파일을 복사를 한다.

[root@jacob-kvm bins]# find ~/. -name registry.terraform.io
/root/./tf-taco/.terraform/plugins/registry.terraform.io
[root@jacob-kvm bins]# cp terraform-provider-libvirt ~/tf-taco/.terraform/plugins/registry.terraform.io/dmacvicar/libvirt/0.6.2/linux_amd64/

이후 main.tf 혹은 (필자의 경우 versions.tf)에 terraform에 대한 기본정보가 포함되어 있는데 이를 아래와 같이 수정한다.

[root@jacob-kvm tf-taco]# cat versions.tf 
terraform {
 required_version = ">= 0.13"
  required_providers {
    libvirt = {
      source  = "dmacvicar/libvirt"
      version = "0.6.2"
    }
  }
}

이후 정상적으로 terraform init이 가능하다.

참고사이트

댓글
댓글쓰기 폼