
1. 项目缘起为什么要在openEuler上折腾OpenStack镜像最近在给一个内部私有云平台做技术栈升级底层虚拟化平台从老旧的KVM管理工具切换到了OpenStack。平台选型定了虚拟机的操作系统镜像就成了第一个要啃的硬骨头。客户那边对安全性和可控性要求极高指定了要使用国产的openEuler 22.03 LTS作为基础操作系统。市面上现成的、针对OpenStack优化好的openEuler镜像几乎没有就算有也不敢直接用——谁知道里面有没有夹带私货或者配置不符合我们的安全基线所以自己动手从零开始制作一个干净、合规、高性能的OpenStack镜像就成了必须完成的任务。这件事听起来就是qemu-img转个格式但真做起来坑是一个接一个。镜像做出来能启动只是第一步更重要的是要让它能在OpenStack环境下“活得舒服”云初始化cloud-init能不能正确注入主机名、密钥磁盘驱动和网卡驱动是不是最优性能镜像尺寸是不是够精简上传到Glance会不会太慢这些细节直接关系到后续成百上千台虚拟机的创建效率和运行稳定性。今天我就把这次从零制作openEuler 22.03 OpenStack镜像的完整过程、踩过的坑以及最终打磨出的最佳实践毫无保留地分享出来。无论你是刚开始接触OpenStack的运维还是需要为特定发行版定制镜像的开发者这篇指南都能让你少走弯路。2. 核心原理与工具链选型镜像不只是“一个文件”在动手之前我们必须搞清楚OpenStack镜像到底是什么以及为什么不能直接用从官网下载的ISO。OpenStack期望的镜像通常是一个包含可启动操作系统、并预装了特定“云环境”代理程序的磁盘文件。它的核心目标是实现虚拟机的自动化初始化。2.1 OpenStack镜像的核心要求一个合格的OpenStack镜像至少需要满足以下几个条件文件格式 必须是QCOW2QEMU Copy-On-Write格式。这是OpenStack Glance镜像服务最推荐、也是性能最好的格式。它支持稀疏文件节省存储空间、快照链以及后端存储如Ceph的高级特性。云初始化支持 这是灵魂所在。虚拟机首次启动时OpenStack Nova计算服务会通过配置驱动ConfigDrive或者元数据服务Metadata Service将实例的元数据如主机名、IP地址、SSH公钥、用户数据脚本等传递给虚拟机。镜像内部必须有一个服务通常是cloud-init来接收并处理这些数据完成系统的个性化配置。没有它创建出来的所有虚拟机都一个样无法自动化。虚拟化驱动优化 为了在KVM虚拟化环境下获得最佳I/O性能镜像内应该使用半虚拟化驱动VirtIO。这包括磁盘驱动 使用virtio_blk或virtio_scsi而不是模拟的IDE或SATA控制器。网卡驱动 使用virtio_net而不是模拟的e1000或rtl8139。气球驱动可选virtio_balloon用于动态调整内存。串口控制台 配置好ttyS0即VirtIO控制台这样我们才能通过OpenStack的“控制台”功能看到虚拟机的启动日志这在排错时至关重要。SSH服务器与安全配置 默认启用SSH服务sshd并且通常建议禁用密码登录仅允许密钥认证这符合云环境的安全最佳实践。精简与通用性 镜像应该尽可能小删除不必要的软件包、缓存和日志以加快上传和下载速度。同时它应该是一个“通用”镜像不包含特定于某台机器的配置如固定的IP地址、主机名、网络管理器持久化配置等。2.2 我们的工具链选择与理由基于以上要求我们选择了一套经典且稳定的工具链虚拟化平台 QEMU/KVM。这是Linux下事实标准的虚拟化方案与OpenStack底层完全一致能保证最大的兼容性。系统安装 使用官方openEuler 22.03 LTS的ISO文件通过virt-install命令进行自动化、无交互的安装。这比手动在图形界面点击要可靠和可重复得多。镜像操作 核心工具是qemu-img用于创建、转换和调整镜像文件。virt-customize来自libguestfs-tools套件是一个神器它可以在不启动虚拟机的情况下直接挂载镜像文件并向其中注入文件、安装软件包、运行脚本极大地提高了定制效率。云初始化 openEuler 22.03 官方仓库已经提供了cloud-init包我们直接安装并配置即可。为什么不使用Docker或chroot因为它们无法处理内核安装、引导加载程序GRUB2配置等需要完整虚拟硬件环境才能完成的操作。QEMU虚拟机是最接近生产环境的沙箱。3. 实战第一步准备构建环境与自动化安装脚本构建环境最好是一台干净的、安装了KVM的Linux主机可以是物理机也可以是一台性能足够的虚拟机。我这里使用的是另一台openEuler 22.03的宿主机。3.1 环境准备与依赖安装首先确保构建主机满足基本条件并安装所有必要的工具。# 1. 检查CPU是否支持虚拟化对于物理机 egrep -c (vmx|svm) /proc/cpuinfo # 输出大于0即可 # 2. 安装KVM及相关管理工具 sudo dnf install -y qemu-kvm libvirt virt-install libvirt-client libvirt-daemon virt-manager virt-viewer # 3. 启动libvirtd服务并设置开机自启 sudo systemctl enable --now libvirtd # 4. 安装镜像操作神器 libguestfs-tools # 它提供了 virt-customize, virt-sysprep, virt-resize 等命令 sudo dnf install -y libguestfs-tools # 5. 下载 openEuler 22.03 LTS 官方ISO # 建议从国内镜像站下载速度更快 wget https://repo.openeuler.org/openEuler-22.03-LTS/ISO/x86_64/openEuler-22.03-LTS-x86_64-dvd.iso # 下载完成后验证一下SHA256校验和确保文件完整3.2 编写自动化安装的Kickstart文件手动安装无法保证一致性我们必须使用Kickstart实现无人值守安装。创建一个名为openeuler-ks.cfg的文件# 文件名openeuler-ks.cfg # 语言和键盘 lang en_US.UTF-8 keyboard us # 时区 timezone Asia/Shanghai --utc # 根密码这里仅为示例生产环境应使用加密密码或后续通过cloud-init修改 rootpw --plaintext a_strong_password_should_be_changed # 禁用首次启动的配置向导非常重要 firstboot --disable # 使用文本模式安装无图形界面 text # 跳过安装介质检查加快速度 cdrom # 清除所有分区并初始化磁盘 zerombr clearpart --all --initlabel # 使用自动分区方案创建一个LVM卷组 autopart --typelvm # 引导加载程序安装位置 bootloader --locationmbr --boot-drivevda # 网络配置安装时启用网卡但使用DHCP。最终网络由cloud-init管理。 network --onbooton --devicelink --bootprotodhcp --noipv6 # 防火墙和SELinux配置根据需求调整云镜像通常先放宽松 firewall --disabled selinux --disabled # 软件包选择安装最小化服务器环境并包含“标准系统工具”组确保有ifconfig等命令 %packages --ignoremissing ^minimal-environment standard %end # 安装后脚本这里可以执行一些最基础的设置 %post --log/root/ks-post.log # 禁用不必要的服务加快启动 systemctl disable firewalld systemctl disable tuned # 确保网络服务启用使用NetworkManager systemctl enable NetworkManager # 清理安装缓存减小镜像体积 dnf clean all rm -rf /var/cache/dnf/* # 创建一个标志文件便于后续识别 touch /.build-by-ks %end这个Kickstart文件做了几件关键事1) 最小化安装2) 使用LVM自动分区更灵活3) 禁用首次启动向导和防火墙由cloud-init接管4) 清理缓存。注意这里的根密码是明文的仅用于构建阶段。最终我们会通过cloud-init禁用密码登录所以问题不大但最好在构建完成后立即修改或使用加密密码。3.3 使用virt-install启动自动化安装现在我们使用virt-install命令基于Kickstart文件启动一个临时的虚拟机来完成安装。# 创建一个20GB的QCow2格式的原始磁盘镜像 qemu-img create -f qcow2 /var/lib/libvirt/images/openeuler-raw.qcow2 20G # 使用virt-install启动安装过程 sudo virt-install \ --nameopeneuler-builder \ --vcpus2 \ --memory4096 \ --disk path/var/lib/libvirt/images/openeuler-raw.qcow2,formatqcow2,size20,busvirtio \ --network networkdefault,modelvirtio \ --graphics none \ --console pty,target_typeserial \ --location/path/to/your/openEuler-22.03-LTS-x86_64-dvd.iso \ --extra-argsinst.ksfile:///run/install/openeuler-ks.cfg consolettyS0,115200n8 \ --initrd-inject./openeuler-ks.cfg \ --noautoconsole参数拆解与避坑点--graphics none和--console pty 我们使用串口控制台这是无头headless服务器安装的标准做法也方便我们通过virsh console命令查看安装日志。--location 指定ISO文件路径。这里使用--location而非--cdrom是为了配合--extra-args向安装内核传递参数。--extra-args 这是核心。inst.ks指定了Kickstart文件的位置我们通过--initrd-inject将其注入到了虚拟机的初始内存盘initrd中。consolettyS0,115200n8设置了内核的串口控制台参数必须与--console选项匹配。--initrd-inject 将Kickstart文件放入虚拟机的initrd使其在安装早期即可被访问。--noautoconsole 不自动连接控制台因为安装是自动的。命令执行后虚拟机开始启动并自动安装。你可以通过以下命令观察安装进度sudo virsh console openeuler-builder安装完成后虚拟机会自动关闭。此时原始的openeuler-raw.qcow2镜像已经包含了一个最小化的、可启动的openEuler系统。4. 镜像定制与“云化”改造得到一个原始系统镜像只是开始接下来才是让它蜕变为“云镜像”的关键步骤。我们将使用virt-customize在不启动虚拟机的情况下直接对镜像文件进行手术刀式的修改。4.1 安装cloud-init并完成基础配置首先我们需要在镜像里安装cloud-init并对其进行正确配置使其能够从OpenStack获取元数据。# 使用 virt-customize 安装 cloud-init 并进行初步配置 sudo virt-customize -a /var/lib/libvirt/images/openeuler-raw.qcow2 \ --run-command dnf install -y cloud-init cloud-utils-growpart \ --run-command systemctl enable cloud-init-local cloud-init cloud-config cloud-final \ --mkdir /var/lib/cloud/seed/nocloud \ --copy-in ./99-disable-network-config.cfg:/etc/cloud/cloud.cfg.d/ \ --run-command ln -sf /dev/null /etc/systemd/network/99-default.link这里有几个关键操作安装软件包 除了cloud-init还安装了cloud-utils-growpart这个工具允许系统在启动时自动扩展根分区以填满虚拟磁盘对于按需分配磁盘的云环境非常有用。启用服务 确保cloud-init的各个阶段服务都开机自启。创建种子目录 虽然OpenStack通常用ConfigDrive或元数据服务但创建这个目录是良好实践。注入自定义配置 这是第一个大坑。OpenStack的网络配置能力很强通过Neutron我们不应该让cloud-init再去尝试配置网络否则容易冲突。我们创建一个配置文件99-disable-network-config.cfg内容如下# 文件名99-disable-network-config.cfg network: config: disabled这个配置告诉cloud-init“不要动网络配置”网络交给OpenStack的Neutron和实例内部的NetworkManager或systemd-networkd来处理。处理默认链路规则 删除或屏蔽99-default.link防止systemd根据网卡MAC地址重命名网卡例如将eth0重命名为ens3确保网卡名称在OpenStack中保持稳定通常是eth0。4.2 配置SSH与安全加固云环境安全至关重要默认的密码SSH登录是巨大的风险。sudo virt-customize -a /var/lib/libvirt/images/openeuler-raw.qcow2 \ --run-command sed -i s/^#*PasswordAuthentication.*/PasswordAuthentication no/ /etc/ssh/sshd_config \ --run-command sed -i s/^#*PermitRootLogin.*/PermitRootLogin prohibit-password/ /etc/ssh/sshd_config \ --run-command echo \AuthorizedKeysFile .ssh/authorized_keys\ /etc/ssh/sshd_config \ --run-command systemctl enable sshdPasswordAuthentication no 彻底禁用密码认证强制使用密钥。PermitRootLogin prohibit-password 允许root用户通过密钥登录根据你的安全策略也可以设置为no完全禁止root登录通过普通用户sudo。确保AuthorizedKeysFile配置正确cloud-init会将OpenStack中注入的SSH公钥写入这个文件。4.3 优化内核与驱动适配虚拟化环境为了让虚拟机在KVM下跑得更快我们需要确保使用VirtIO驱动并优化内核参数。sudo virt-customize -a /var/lib/libvirt/images/openeuler-raw.qcow2 \ --run-command dnf install -y kernel kernel-devel acpid \ --run-command grub2-mkconfig -o /boot/grub2/grub.cfg \ --run-command dracut -f /boot/initramfs-$(uname -r).img $(uname -r) \ --run-command systemctl enable acpid确保安装了与当前内核版本对应的kernel-devel有些驱动编译可能需要。重新生成GRUB配置和initramfs镜像确保新的内核和驱动被正确包含。安装并启用acpid高级电源管理接口守护进程这样在OpenStack界面上执行“关机”操作时虚拟机可以正常响应ACPI信号而不是被强制断电。关于驱动的一个深坑 openEuler 22.03 的内核默认已经包含了virtio_net,virtio_blk,virtio_pci,virtio_console等驱动。但为了万无一失特别是确保控制台ttyS0可用我们需要检查并修改引导参数。# 检查当前的GRUB命令行参数 sudo virt-customize -a /var/lib/libvirt/images/openeuler-raw.qcow2 \ --run-command cat /proc/cmdline # 输出可能类似BOOT_IMAGE/vmlinuz-5.10.0-xxx root/dev/mapper/openeuler-root ro rhgb quiet # 我们需要修改GRUB配置添加串口控制台参数 sudo virt-customize -a /var/lib/libvirt/images/openeuler-raw.qcow2 \ --run-command grubby --update-kernelALL --argsconsolettyS0,115200n8 \ --run-command grubby --update-kernelALL --remove-argsrhgb quietgrubby是一个很好的修改GRUB参数的工具。我们为所有内核添加consolettyS0,115200n8这样内核日志就会输出到串口OpenStack的控制台才能看到。移除了rhgb图形化启动和quiet安静模式让启动日志更详细便于调试。4.4 系统清理与通用化Sysprep这是制作镜像的最后一步也是最容易出问题的一步。目的是移除所有实例特有的信息让镜像变成一个“模板”。# 使用 virt-sysprep 进行通用化处理 sudo virt-sysprep -a /var/lib/libvirt/images/openeuler-raw.qcow2 \ --operations customize,hostname,logfiles,machine-id,net-hostname,net-hwaddr,ssh-hostkeys,udev-persistent-net,tmp-files,utmp \ --hostname localhost.localdomain \ --run-command truncate -s 0 /etc/machine-id \ --run-command rm -f /var/lib/dbus/machine-id ln -s /etc/machine-id /var/lib/dbus/machine-id \ --run-command rm -f /etc/ssh/ssh_host_* \ --run-command rm -rf /tmp/* /var/tmp/* \ --run-command rm -f /var/log/*.log /var/log/*.log-* \ --run-command dnf clean all rm -rf /var/cache/dnf/*每一步的操作意图和风险--operations 指定要执行的操作集合。customize是自定义脚本hostname重置主机名logfiles清空日志machine-id和net-hwaddr是重中之重。machine-id陷阱 Linux系统的/etc/machine-id必须是唯一的。如果镜像里保留了一个固定的ID那么所有从这个镜像启动的虚拟机都会有相同的ID会导致很多依赖此ID的服务如Docker、Journal日志出现冲突。我们必须清空它truncate -s 0系统在首次启动时会生成一个新的。网络硬件地址MACnet-hwaddr操作会删除/etc/udev/rules.d/70-persistent-net.rules等文件中记录的旧MAC地址与网卡名的绑定规则防止新虚拟机继承旧的网卡名绑定。SSH主机密钥 删除ssh_host_*密钥每个实例在首次启动sshd服务时会自动生成独一无二的密钥这是安全必需。清理工作 清空临时文件、日志和DNF缓存进一步缩小镜像体积。警告virt-sysprep的--operations参数顺序很重要且有些操作有依赖关系。务必先阅读man virt-sysprep。我强烈建议在操作前先对镜像做一个备份因为sysprep是不可逆的。5. 最终打磨压缩、转换与验证经过sysprep的镜像已经是一个“通用”镜像了但我们可以做得更好。5.1 压缩与转换格式原始的QCOW2文件可能因为稀疏文件特性在宿主机上看起来不大但直接上传可能效率不高。我们进行压缩和优化。# 1. 使用 virt-sparsify 进行“稀疏化”处理回收镜像内部的未使用空间 # 这步需要额外磁盘空间因为它会创建一个新的镜像文件 sudo virt-sparsify --compress /var/lib/libvirt/images/openeuler-raw.qcow2 /var/lib/libvirt/images/openeuler-sparse.qcow2 # 2. 检查压缩后的镜像信息 qemu-img info /var/lib/libvirt/images/openeuler-sparse.qcow2 # 关注 virtual size (虚拟大小如20G) 和 disk size (实际占用物理空间可能只有1-2G) # 3. 可选但推荐将镜像转换为“流优化”格式 # 这种格式在上传到Glance时支持直接流式传输无需先下载整个镜像到Glance节点速度更快。 qemu-img convert -f qcow2 -O qcow2 -c -o compat0.10 /var/lib/libvirt/images/openeuler-sparse.qcow2 /var/lib/libvirt/images/openeuler-22.03-openstack.qcow2virt-sparsify 它会读取镜像文件系统将全零块或已删除文件占用的块标记为“稀疏”从而减小物理文件大小。qemu-img convert-c 进行压缩进一步减小文件体积。-o compat0.10 指定QCOW2版本为较老的0.10以获得更好的兼容性一些老版本的OpenStack或QEMU可能不支持更高的版本。最终得到的openeuler-22.03-openstack.qcow2就是我们准备上传的镜像。5.2 本地验证镜像在投入生产环境前强烈建议在本地启动一次这个最终镜像验证cloud-init和基本功能。# 创建一个验证用的临时镜像避免污染原镜像 cp /var/lib/libvirt/images/openeuler-22.03-openstack.qcow2 /var/lib/libvirt/images/openeuler-test.qcow2 # 创建一个简单的cloud-init元数据文件 cat /tmp/meta-data EOF instance-id: test-instance-001 local-hostname: test-vm EOF cat /tmp/user-data EOF #cloud-config password: changeme chpasswd: { expire: False } ssh_pwauth: True EOF # 创建一个包含元数据和用户数据的ISO文件模拟OpenStack的ConfigDrive genisoimage -output /tmp/config-drive.iso -volid cidata -joliet -rock /tmp/meta-data /tmp/user-data # 启动测试虚拟机 sudo virt-install \ --nameopeneuler-test \ --vcpus1 \ --memory1024 \ --disk path/var/lib/libvirt/images/openeuler-test.qcow2,formatqcow2,busvirtio \ --disk path/tmp/config-drive.iso,devicecdrom \ --network networkdefault,modelvirtio \ --graphics none \ --console pty,target_typeserial \ --import \ --noautoconsole # 连接控制台观察启动过程 sudo virsh console openeuler-test在控制台日志中你应该能看到cloud-init在运行并应用你的user-data例如设置密码。登录后检查主机名是否变成了test-vmdf -h查看根分区是否自动扩展到了整个虚拟磁盘ip addr查看网卡是否正常获取了IP来自libvirt的default网络/var/log/cloud-init.log是否有错误如果一切正常恭喜你一个高质量的openEuler 22.03 OpenStack镜像就制作完成了。6. 上传至OpenStack Glance及后续优化建议最后一步就是将镜像上传到你的OpenStack环境中。# 使用OpenStack CLI命令上传 openstack image create \ --file /var/lib/libvirt/images/openeuler-22.03-openstack.qcow2 \ --disk-format qcow2 \ --container-format bare \ --property hw_disk_busvirtio \ --property hw_vif_modelvirtio \ --min-disk 20 \ --min-ram 1024 \ --public \ openEuler 22.03 LTS Cloud参数解释--min-disk和--min-ram 设置实例启动所需的最小资源引导用户合理选择规格。--property 设置镜像属性强制使用VirtIO驱动确保性能。--public 根据你的权限可以设置为--shared或--private。上传完成后在Horizon界面或通过openstack image list查看镜像状态确认状态为active后就可以用它来启动实例了。一些进阶优化建议使用Packer自动化 上述步骤可以完全用HashiCorp Packer编写成模板实现一键构建、版本化管理这是团队协作和CI/CD的最佳实践。集成安全基线 在%post阶段或virt-customize时运行安全合规脚本如根据等保要求配置密码策略、审计规则等。预装监控代理 如果公司统一使用Prometheus Node Exporter、Zabbix Agent等可以预装并配置好但注意不要写入具体的服务器地址可以通过cloud-init的user-data在启动时注入配置。制作不同规格镜像 可以制作“最小化”、“开发版”、“桌面版”等不同软件包集合的镜像满足不同场景需求。制作一个“能用”的镜像不难但制作一个“好用”、“安全”、“高效”的云镜像需要对这些细节有深刻的理解和把控。整个过程就像打磨一件工艺品每一步的精心处理最终都会在成百上千台虚拟机的稳定运行中得到回报。希望这份超详细的指南能帮你避开我踩过的所有坑顺利打造出属于你自己的完美openEuler云镜像。