一、背景与场景

说实话,很多团队在虚拟化环境里折腾手工建虚拟机,鼠标点来点去,配置IP、装软件,一个两个还行,一旦要批量创建几十台,或者每次部署环境都要重复同样操作,那真是又慢又容易出错。尤其是一些做测试环境、开发环境自动化的同学,经常被“环境不一致”搞得头大。这么一看,把虚拟机部署这件事做成自动化流程,就特别有必要了。

这个场景常见的地方比如:

  • 开发团队要快速拉起一套和线上一样的测试环境;
  • CI/CD流水线需要临时创建虚拟机跑测试任务;
  • 运维给新项目分配资源,希望一键生成虚拟机并配置好应用。

大家通常会用两种工具:Terraform负责“基础设施即代码”,比如定义网络、虚拟机规格、磁盘这些;Ansible负责“配置管理”,比如装软件、改配置文件、启动服务。两者结合起来,正是自动化部署KVM虚拟机的经典套路。

二、技术栈说明

本文所有示例会分别使用Terraform和Ansible两种技术栈,但每个代码片段都会明确标注使用的技术栈名称。Terraform版本采用1.5以上,Ansible采用core 2.14以上,操作系统为CentOS 7/8或Ubuntu 20.04,KVM宿主机已经配置好libvirt和qemu。

三、用Terraform定义KVM虚拟机

3.1 为什么先提Terraform

Terraform能让你用代码描述“想要什么样子的虚拟机”,比如几核CPU、多大内存、用什么镜像、网络怎么连。写完代码后执行terraform apply,它就会去调用libvirt的API,把虚拟机创建出来。这样做的好处是:过程可重复、配置版本化、不易出错。

3.2 安装Provider与配置

首先,需要安装Terraform的libvirt provider。在项目目录创建main.tf文件,内容如下:

# 技术栈:Terraform HCL
# 定义provider,source指向官方libvirt插件
terraform {
  required_providers {
    libvirt = {
      source = "dmacvicar/libvirt"
      version = "0.7.1"
    }
  }
}

# 配置连接本地的libvirt daemon(默认qemu:///system)
provider "libvirt" {
  uri = "qemu:///system"
}

3.3 定义虚拟机资源

接下来定义一个KVM虚拟机,包括CPU、内存、磁盘、网络和cloud-init配置(用于自动设置IP和主机名)。以下是一个完整示例:

# 技术栈:Terraform HCL
# 创建一个存储池,存放镜像文件
resource "libvirt_pool" "default" {
  name = "default"
  type = "dir"
  path = "/var/lib/libvirt/images"
}

# 上传一个Ubuntu 20.04的qcow2镜像到存储池
resource "libvirt_volume" "ubuntu_qcow2" {
  name   = "ubuntu-focal.qcow2"
  pool   = libvirt_pool.default.name
  source = "https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img"
  format = "qcow2"
}

# 创建虚拟机的磁盘,使用上面的镜像作为后端,并设置大小
resource "libvirt_volume" "vm_disk" {
  name           = "vm-disk.qcow2"
  base_volume_id = libvirt_volume.ubuntu_qcow2.id
  pool           = libvirt_pool.default.name
  size           = 21474836480 # 20GB
}

# 定义cloud-init的配置数据(网络、主机名、用户密码等)
resource "libvirt_cloudinit_disk" "commoninit" {
  name      = "commoninit.iso"
  pool      = libvirt_pool.default.name
  network_config = data.template_file.network_config.rendered
  user_data      = data.template_file.user_data.rendered
}

# 使用template_file渲染cloud-init模板(需要先定义data模板)
data "template_file" "user_data" {
  template = file("${path.module}/cloud_init.cfg")
  vars = {
    hostname = "myvm01"
    user     = "ubuntu"
    password = "ubuntu123"
  }
}

data "template_file" "network_config" {
  template = file("${path.module}/network_config.cfg")
  vars = {
    ip_address = "192.168.122.100/24"
    gateway    = "192.168.122.1"
    dns        = "8.8.8.8"
  }
}

# 最后创建虚拟机
resource "libvirt_domain" "vm" {
  name   = "myvm01"
  memory = "2048"
  vcpu   = 2

  disk {
    volume_id = libvirt_volume.vm_disk.id
  }

  network_interface {
    network_name   = "default"   # 使用libvirt默认NAT网络
    hostname       = "myvm01"
    addresses      = ["192.168.122.100"]
  }

  cloudinit = libvirt_cloudinit_disk.commoninit.id

  # 保证ssh能通过密钥登录,这里只是演示,生产建议使用密钥
  console {
    type        = "pty"
    target_port = "0"
    target_type = "serial"
  }
}

注意,上面示例中引用了两个模板文件cloud_init.cfgnetwork_config.cfg,需要放在同目录下。cloud_init.cfg示例:

#cloud-config
# 技术栈:cloud-init配置
users:
  - name: ${user}
    lock_passwd: false
    passwd: ${password}
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash

hostname: ${hostname}
manage_etc_hosts: true

network_config.cfg:

#cloud-config
# 技术栈:cloud-init网络配置
network:
  version: 2
  ethernets:
    ens3:
      addresses:
        - ${ip_address}
      gateway4: ${gateway}
      nameservers:
        addresses:
          - ${dns}

执行terraform initterraform apply后,Terraform就会自动创建虚拟机。如果中途出错,用terraform destroy一键清理,非常干净。

3.4 Terraform小结

通过Terraform,我们把虚拟机的基础设施(CPU、内存、网络、存储)全部代码化了。这避免了手工点击libvirt管理界面,也方便后续用版本控制管理这些配置。

四、用Ansible进行配置管理

虚拟机建好了,里面只装了一个纯净的Ubuntu系统,接下来需要安装Nginx、配置防火墙、部署代码等。这些任务交给Ansible再合适不过。

4.1 准备工作:配置主机清单

Ansible需要知道要管理哪些机器,所以先写一个inventory.ini文件:

# 技术栈:Ansible Inventory
[webservers]
myvm01 ansible_host=192.168.122.100 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa

测试连通性:

ansible webservers -m ping -i inventory.ini

4.2 编写Playbook:安装Nginx并配置

创建一个playbookdeploy_nginx.yml,内容如下:

---
# 技术栈:Ansible Playbook (YAML)
- name: 部署Nginx到Web服务器
  hosts: webservers
  become: yes                # 需要sudo权限
  vars:
    nginx_port: 8080         # 自定义端口
    custom_index: "<h1>Hello from Ansible!</h1>"

  tasks:
    - name: 更新apt缓存
      apt:
        update_cache: yes
        cache_valid_time: 3600
      when: ansible_os_family == "Debian"

    - name: 安装Nginx
      apt:
        name: nginx
        state: present

    - name: 修改Nginx监听端口
      lineinfile:
        path: /etc/nginx/sites-available/default
        regexp: 'listen\s+80;'
        line: '    listen {{ nginx_port }} default_server;'
      notify: restart nginx

    - name: 创建自定义index.html
      copy:
        content: "{{ custom_index }}"
        dest: /var/www/html/index.html
        mode: '0644'

    - name: 确保Nginx服务启动并开机自启
      service:
        name: nginx
        state: started
        enabled: yes

  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted

执行命令:

ansible-playbook -i inventory.ini deploy_nginx.yml

Ansible会自动连接到虚拟机,依次执行每个任务。如果某任务失败,它会停在出错的地方,方便排查。

4.3 Ansible的扩展能力

除了装软件,你还可用Ansible去添加用户、同步文件、管理系统服务、甚至执行SQL脚本。所有的操作都是幂等的,即多次运行结果一致,不会重复造成问题。

五、从Terraform到Ansible:整合持续交付流程

上面两步是分开做的:先Terraform建虚拟机,再Ansible配置。在实际的持续交付流水线里,我们希望一条命令完成全部。可以写一个Shell脚本或Makefile来串联:

#!/bin/bash
# 技术栈:Shell脚本
set -e

echo "=== 第一步:使用Terraform创建KVM虚拟机 ==="
cd terraform/
terraform init
terraform apply -auto-approve

# 获取虚拟机的IP地址,假设从Terraform output获取
VM_IP=$(terraform output -raw vm_ip)
echo "虚拟机IP地址: $VM_IP"

echo "=== 第二步:使用Ansible配置虚拟机 ==="
cd ../ansible/
# 动态生成inventory文件,或者直接使用变量
ansible-playbook -i "${VM_IP}," -u ubuntu --private-key ~/.ssh/id_rsa deploy_nginx.yml

这样,将Terraform和Ansible脚本放在同一个git仓库里,持续集成系统(比如Jenkins、GitLab CI)一触发,就能自动拉起虚拟机并部署应用,实现“基础设施+配置管理”的全自动化。

六、技术优缺点分析

优点

  • 一致性强:代码定义的环境,每次创建结果都一样,减少“我的环境可以你的不行”的扯皮。
  • 可版本控制:所有配置文件放入git,变更可追溯,回滚也方便。
  • 效率高:批量创建、并行配置,几分钟完成以前半天的工作。
  • 易于集成:Terraform和Ansible都是主流DevOps工具,能无缝接入CI/CD流水线。

缺点

  • 学习曲线:需要同时掌握HCL和YAML两种语言,对新手有一定门槛。
  • 依赖外部插件:Terraform的libvirt provider不是官方维护,有些版本可能不兼容。
  • 调试复杂:全自动化出问题时,不如手工操作容易定位原因,需要熟悉日志和状态文件。
  • 资源消耗:Terraform的状态文件管理不当可能造成资源泄露;Ansible在规模大时执行速度变慢。

七、注意事项

  1. 权限问题:KVM宿主机上运行Terraform需要用户有libvirt组权限(通常添加到libvirt组)。
  2. 网络规划:确保虚拟机IP不与宿主机或其他虚拟机冲突,建议使用独立的NAT网络或桥接网络。
  3. 密钥管理:cloud-init中设置密码虽然方便,但生产环境应该使用SSH密钥,并且把私钥妥善保管。
  4. 状态文件安全:Terraform的terraform.tfstate包含敏感信息(如IP、密码),最好远程存储在加密的后端(如S3、Consul)。
  5. Ansible幂等性:写playbook时注意每个任务最好是幂等的,避免重复运行导致错误。
  6. 镜像缓存:大镜像多次上传会浪费带宽和时间,建议在Terraform中设置base_volume_id复用已有镜像,或者预先下载到宿主机。

八、总结

通过本文,我们从零开始,用Terraform定义并创建了KVM虚拟机,再用Ansible完成了软件配置,最后整合成一条自动化流水线。整个过程不复杂,但真正落地后,能极大提升环境交付的速度和可靠性。不管你是刚接触虚拟化的小白,还是资深运维,这套思路都值得一试。以后只需要改改代码,跑一下脚本,一个完整的虚拟机环境就诞生了。这就是“基础设施即代码”的魅力所在。