1. 项目概述与核心价值
最近在给公司的私有云平台做镜像标准化,选型定在了openEuler 22.03 LTS。这个活儿听起来简单,不就是做个系统镜像嘛,但真上手了才发现,从零开始为OpenStack这样的云平台制作一个“开箱即用”、性能稳定且符合安全基线要求的镜像,里头的门道可不少。这不仅仅是装个系统、打个包那么简单,它涉及到系统初始化配置、云平台驱动集成、安全加固、性能调优等一系列琐碎但至关重要的步骤。一个制作精良的镜像,能极大提升后续虚拟机实例的创建速度和运行稳定性,减少运维的重复劳动;而一个粗糙的镜像,则可能成为后续各种“灵异事件”的源头。今天,我就把自己从零开始,制作openEuler 22.03 for OpenStack镜像的完整过程、踩过的坑以及总结的最佳实践,毫无保留地分享出来。无论你是刚开始接触云平台运维,还是想优化现有的镜像流水线,相信这篇近万字的实操记录都能给你带来直接的参考价值。
2. 镜像制作的整体设计与思路拆解
在动手之前,我们必须先想清楚要做一个什么样的镜像,以及OpenStack对镜像有哪些“隐形”要求。盲目开始往往意味着中途要不断返工。
2.1 明确镜像的最终形态与核心需求
我们的目标不是做一个能在物理机上安装的ISO,而是制作一个QCOW2格式的磁盘镜像文件。这个文件将被上传到OpenStack的Glance镜像服务中,作为创建虚拟机(实例)的“模板”。因此,它需要满足几个核心需求:
- 云就绪:系统必须能适应云环境的动态特性。比如,IP地址、主机名通常由云平台通过DHCP或Metadata服务动态分配和注入,而不是在镜像中写死。
- 驱动兼容:虚拟机在Hypervisor(如KVM)上运行,需要对应的虚拟化驱动(如virtio)来保证磁盘、网卡等设备的高性能。
- 轻量高效:镜像体积要尽可能小,以节省存储空间和加快下载、缓存速度。这意味着需要清理不必要的缓存、日志和软件包。
- 安全基线:默认配置应符合基本的安全要求,例如禁用root密码SSH登录、配置合理的防火墙策略(虽然云平台安全组是主要防线,但镜像内仍可做基础设置)。
- 可维护性:镜像中的软件源应配置正确,方便后续在实例内部进行软件更新和管理。
基于这些需求,我选择的方案是:在一台临时虚拟机(作为构建机)中,使用qemu-img创建一块虚拟磁盘,然后通过virt-install或qemu-system工具,将openEuler 22.03 LTS系统安装到这块虚拟磁盘中,接着进行一系列“云化”和优化配置,最后将磁盘导出为QCOW2格式。这种方法隔离性好,可重复性强。
2.2 工具链选型与准备工作
工欲善其事,必先利其器。以下是整个流程中需要用到的核心工具及其作用:
- 构建环境:一台安装有KVM/QEMU的Linux主机(物理机或虚拟机均可)。我使用的是另一台CentOS 7的服务器,它本身支持虚拟化。你需要确保
qemu-kvm、libvirt等包已安装,并且CPU支持虚拟化(egrep -c ‘(vmx|svm)’ /proc/cpuinfo输出大于0)。 - 核心命令:
qemu-img:用于创建、转换磁盘镜像格式。我们用它来创建初始的raw格式磁盘,并最终转换为qcow2格式。virt-install:一个封装了libvirt的命令行工具,能极大地简化从ISO启动并安装系统到指定磁盘的过程。比手动配置qemu-system-x86_64要方便得多。guestfish/virt-customize:这是一套“神器”,可以在不启动虚拟机的情况下,直接对磁盘镜像文件进行读写、注入文件、执行命令等操作。在配置阶段会频繁使用。virt-sysprep:同样是libguestfs工具集的一员,用于对已安装好的系统镜像进行“通用化”清理,例如清除SSH主机密钥、清理日志、清除用户信息等,这对于制作模板镜像至关重要。
- 软件源:准备好openEuler 22.03 LTS的ISO安装文件。同时,为了在构建过程中和最终镜像内部都能高速安装软件,建议在构建机内部搭建一个本地镜像源,或者配置好可靠的国内开源镜像站(如华为云、清华、阿里云的openEuler镜像源)。
注意:构建机本身最好有充足的空间(至少50GB空闲),因为过程中会产生多个磁盘镜像副本。网络也要畅通,以便下载必要的工具和软件包。
3. 核心细节解析与实操要点
这一部分,我们深入到几个最容易出问题,也最影响镜像质量的关键环节,看看具体怎么做,以及为什么要这么做。
3.1 虚拟磁盘创建与初始安装的“坑”
安装系统听起来是基础操作,但为云镜像安装系统有特殊要求。
首先,创建磁盘。我使用命令qemu-img create -f raw /var/lib/libvirt/images/openEuler-22.03.raw 10G创建了一个10GB的raw格式原始磁盘。为什么先用raw格式?因为在安装和初始操作阶段,raw格式性能更好,兼容性问题最少。等到所有配置都完成后,我们再把它压缩转换成qcow2。
接下来是使用virt-install进行无人值守安装。这是第一个关键点。如果像平时一样手动交互安装,效率太低且无法自动化。我们需要使用Kickstart(对于openEuler,它继承自RHEL系,支持很好)或AutoYast(对于SUSE系)这样的应答文件。我编写了一个精简的Kickstart文件(ks.cfg),核心内容包括:
- 分区方案:我采用了云环境常见的简单分区:一个大的根分区(
/)和一个交换分区(swap)。不需要单独的/boot,因为云实例通常不需要复杂的多系统引导。 - 软件包选择:只安装
@base和@core这两个最基础的包组,以及一个至关重要的包:cloud-init。cloud-init是云镜像的“灵魂”,它负责在实例首次启动时,从OpenStack的Metadata服务获取并应用网络配置、主机名、SSH密钥、用户数据等。没有cloud-init的镜像,在OpenStack里就是个“半成品”。 - 网络与防火墙:在Kickstart中配置网络为DHCP,并暂时禁用防火墙(
firewalld)和SELinux。因为在云环境中,网络由云平台管理,安全主要由安全组控制,镜像内过于严格的默认设置可能导致实例初始化失败。 - root密码:设置一个临时密码,或者留空(不推荐)。最佳实践是在
cloud-init配置中禁止密码登录,仅使用SSH密钥。
然后,使用如下命令启动自动化安装:
virt-install \ --name openEuler-builder \ --memory 2048 \ --vcpus 2 \ --disk path=/var/lib/libvirt/images/openEuler-22.03.raw,format=raw \ --network network=default \ --os-type linux \ --os-variant openeuler22.03 \ --location /path/to/openEuler-22.03-LTS-x86_64-dvd.iso \ --extra-args “inst.ks=file:///path/to/ks.cfg console=tty0 console=ttyS0,115200n8” \ --graphics none \ --noautoconsole这里有几个要点:--graphics none和console参数配置是为了支持串口控制台,这在无图形界面的服务器环境和云平台中非常重要。--noautoconsole让安装后台进行。
3.2 驱动与内核模块:性能的基石
安装完系统后,镜像还无法在云平台发挥最佳性能。OpenStack底层默认使用KVM虚拟化,其高性能的虚拟设备(如网卡、磁盘)遵循virtio标准。因此,我们必须确保镜像内包含了对应的驱动。
- 磁盘驱动(virtio-blk):幸运的是,openEuler 22.03 LTS的内核默认已经编译了
virtio_blk驱动,所以对于根文件系统,一般无需额外操作。 - 网卡驱动(virtio-net):同样,内核也包含了
virtio_net。但为了确保万无一失,可以在镜像中显式安装kernel-modules-extra包,它包含了更多不常用的内核模块。 - 半虚拟化驱动(virtio-pci):这是
virtio设备的底层PCI驱动,也必须存在。
如何检查?我们可以使用guestfish工具,在不启动虚拟机的情况下“潜入”磁盘镜像内部进行检查和操作:
guestfish --ro -a /var/lib/libvirt/images/openEuler-22.03.raw -i lsmod | grep virtio如果发现缺少关键驱动,就需要在镜像内部安装对应的kernel-module包,并确保initramfs镜像也包含了这些驱动。更新initramfs的命令需要在镜像内执行:dracut -f –add-drivers “virtio virtio_pci virtio_blk virtio_net”。
3.3 Cloud-Init的深度配置:让镜像“活”起来
cloud-init的配置是云镜像制作的重中之重。它的配置文件通常位于/etc/cloud/cloud.cfg及其cloud.cfg.d/子目录下。我们需要定制这个文件,使其更好地适配OpenStack和我们的运维习惯。
一个常见的优化配置片段(我们可以将其制作成一个文件,用virt-customize注入到镜像的/etc/cloud/cloud.cfg.d/99_openstack.cfg中)如下:
# 设置默认用户为 openeuler (openEuler的默认用户名) system_info: default_user: name: openeuler lock_passwd: true # 锁定密码,禁止密码登录 gecos: openEuler Cloud User groups: [wheel, adm] sudo: [“ALL=(ALL) NOPASSWD:ALL”] # 为方便,配置sudo无需密码,生产环境应收紧 shell: /bin/bash ssh_authorized_keys: [] # 密钥由cloud-init从metadata服务获取后注入 # 配置数据源,OpenStack使用ConfigDrive和NoCloud(HTTP Metadata) datasource_list: [‘ConfigDrive’, ‘NoCloud’] datasource: ConfigDrive: dsmode: local NoCloud: fs_label: “config-2” # 这是OpenStack挂载metadata的卷标签 # 禁用不必要的数据源,加速启动 disable_root: true # 禁用root登录 ssh_pwauth: false # 禁用SSH密码认证,强制使用密钥 manage_etc_hosts: localhost # 如何管理/etc/hosts preserve_hostname: false # 不保留镜像中的主机名,使用metadata提供的 cloud_init_modules: - migrator - bootcmd - write-files - growpart - resizefs - set_hostname - update_hostname - update_etc_hosts - users-groups - ssh cloud_config_modules: - mounts - ssh-import-id - locale - set-passwords - timezone - disable-ec2-metadata - runcmd cloud_final_modules: - scripts-per-once - scripts-per-boot - scripts-per-instance - scripts-user - ssh-authkey-fingerprints - keys-to-console - final-message关键解释:
lock_passwd: true和ssh_pwauth: false是重要的安全设置,强制使用SSH密钥认证。datasource_list指定了cloud-init从哪里获取元数据。OpenStack通常同时提供ConfigDrive(一个模拟的配置光盘)和HTTP Metadata服务,两者都配置上兼容性最好。growpart和resizefs这两个模块非常实用!它们允许实例启动时,自动扩展根分区以填满创建实例时分配的系统盘大小。比如你用这个10G的镜像创建了一个100G根盘的实例,有了这个配置,根文件系统会自动扩容到100G,无需手动操作。
4. 实操过程与核心环节实现
下面,我将把整个制作过程串联起来,形成一个可一键执行的脚本化流程(为了可读性,省略了部分错误检查)。
4.1 阶段一:基础系统安装
准备环境与资源:
# 假设工作目录为 /data/image_build ISO_PATH=“/data/isos/openEuler-22.03-LTS-x86_64-dvd.iso” IMAGE_NAME=“openEuler-22.03” IMAGE_RAW=“${IMAGE_NAME}.raw” IMAGE_QCOW2=“${IMAGE_NAME}.qcow2” KS_FILE=“./ks.cfg” # 安装必要工具(在构建机上) sudo yum install -y qemu-img libvirt virt-install libguestfs-tools-c sudo systemctl start libvirtd创建磁盘与Kickstart文件:
# 创建20G的raw磁盘(比最终镜像大,留出操作空间) qemu-img create -f raw ${IMAGE_RAW} 20G # 编写ks.cfg文件(内容参考3.1节,此处略) cat > ${KS_FILE} << ‘EOF’ # … Kickstart配置内容 … EOF执行无人值守安装:
sudo virt-install \ --name ${IMAGE_NAME}-builder \ --memory 4096 \ --vcpus 2 \ --disk path=${IMAGE_RAW},format=raw \ --network network=default,model=virtio \ --os-type linux \ --os-variant openeuler22.03 \ --location ${ISO_PATH} \ --initrd-inject ${KS_FILE} \ --extra-args “inst.ks=file:///ks.cfg inst.repo=file:///mnt/iso console=tty0 console=ttyS0,115200n8” \ --graphics none \ --noautoconsole \ --wait -1安装完成后,虚拟机会自动关闭。使用
sudo virsh undefine ${IMAGE_NAME}-builder清理临时虚拟机定义。
4.2 阶段二:镜像定制与云化配置
现在,我们有了一个安装了基础系统和cloud-init的raw磁盘镜像。接下来进行深度定制。
安装必要软件包与驱动:
# 使用 virt-customize 进行批量操作 sudo virt-customize -a ${IMAGE_RAW} \ --run-command ‘dnf makecache’ \ --install qemu-guest-agent,cloud-init,cloud-utils-growpart,bash-completion,acpid,net-tools \ --update \ --selinux-relabelqemu-guest-agent:安装QEMU Guest Agent,它运行在实例内部,可以向宿主机报告实例的IP、主机名、磁盘使用情况等信息,在OpenStack Horizon控制台能看到这些信息,也支持在线调整磁盘。cloud-utils-growpart:这是growpart模块的依赖,确保分区扩容功能正常。acpid:用于响应虚拟机的电源操作(如软关机)。
注入Cloud-Init配置:
# 将前面写好的99_openstack.cfg注入 sudo virt-customize -a ${IMAGE_RAW} \ --upload 99_openstack.cfg:/etc/cloud/cloud.cfg.d/配置SSH服务:
sudo virt-customize -a ${IMAGE_RAW} \ --run-command “sed -i ‘s/^#PermitRootLogin.*/PermitRootLogin no/’ /etc/ssh/sshd_config” \ --run-command “sed -i ‘s/^PasswordAuthentication.*/PasswordAuthentication no/’ /etc/ssh/sshd_config” \ --run-command “systemctl enable sshd cloud-init cloud-config cloud-final cloud-init-local”这里直接修改了SSH配置,禁止root登录和密码认证,并确保相关服务开机自启。
4.3 阶段三:清理与通用化
这是制作模板镜像最关键的一步,目的是消除镜像的唯一性信息,避免由此引发的网络冲突、主机名混淆等问题。
使用virt-sysprep进行深度清理:
sudo virt-sysprep -a ${IMAGE_RAW} \ --operations defaults,-ssh-userdir \ --network \ --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”–operations defaults:执行一系列默认清理操作,如清除日志文件、临时文件、yum缓存等。–network:清除网络设备持久化规则(如70-persistent-net.rules),防止网卡MAC地址绑定。–hostname:重置主机名。- 清理
machine-id:这个ID在系统内应该是唯一的。如果不清理,所有从这个镜像创建的虚拟机都会有相同的machine-id,可能导致某些依赖此ID的应用程序(如Docker、某些集群软件)出现问题。dbus的机器ID是/etc/machine-id的软链接,也需要处理。
清理Shell历史与缓存:
sudo virt-customize -a ${IMAGE_RAW} \ --run-command “rm -rf /var/cache/dnf/* /tmp/* /var/tmp/*” \ --run-command “rm -f /root/.bash_history /home/*/.bash_history” \ --run-command “journalctl –rotate && journalctl –vacuum-time=1s” # 清理系统日志
4.4 阶段四:格式转换与压缩
最后,我们将优化后的raw镜像转换为OpenStack推荐的qcow2格式,并进行空间压缩。
# 将raw格式转换为qcow2格式,并进行稀疏文件优化 qemu-img convert -c -O qcow2 ${IMAGE_RAW} ${IMAGE_QCOW2} # 检查最终镜像信息 qemu-img info ${IMAGE_QCOW2}-c参数表示进行压缩,可以显著减小镜像文件体积。转换完成后,原始的raw文件可以删除。现在得到的openEuler-22.03.qcow2就是我们的最终产品了。
5. 常见问题与排查技巧实录
制作过程中难免会遇到各种问题,这里记录几个典型的“坑”和解决方法。
5.1 实例启动后网络不通或无法获取IP
这是最常见的问题之一。
排查思路:
- 检查cloud-init日志:实例启动后,立即通过VNC控制台或日志查看
/var/log/cloud-init.log和/var/log/cloud-init-output.log。关注是否有datasource识别错误、网络配置应用失败等信息。 - 检查网络服务:确认
network.service或NetworkManager服务是否正常运行。openEuler 22.03默认使用NetworkManager。检查/etc/sysconfig/network-scripts/下的网卡配置文件是否被正确生成。 - 检查驱动:确认
virtio_net内核模块已加载 (lsmod | grep virtio)。
- 检查cloud-init日志:实例启动后,立即通过VNC控制台或日志查看
我的踩坑记录: 有一次制作镜像时,为了精简,我移除了
NetworkManager,只保留了network-scripts。结果在某个OpenStack版本上,因为metadata路由配置方式不同,实例无法通过169.254.169.254获取元数据。教训是:除非有充分把握,否则不要随意替换或移除发行版默认的网络管理工具。openEuler与NetworkManager集成度更好,保持默认即可。快速修复命令(在镜像定制阶段注入):
sudo virt-customize -a ${IMAGE_RAW} \ --run-command “systemctl enable NetworkManager” \ --run-command “echo ‘NOZEROCONF=yes’ >> /etc/sysconfig/network” # 避免某些情况下产生169.254.0.0/16的无效地址
5.2 Cloud-Init执行失败,用户密钥未注入
表现为使用SSH密钥对无法登录实例。
排查思路:
- 检查metadata服务:在实例内部,尝试
curl -s http://169.254.169.254/openstack/latest/meta_data.json。如果无法连接或返回错误,说明实例无法访问OpenStack的metadata服务。检查安全组规则(是否放行了实例对169.254.169.254的访问?)、以及底层网络(如Neutron的dhcp-agent是否配置了enable_isolated_metadata = True)。 - 检查cloud-init配置:确认
/etc/cloud/cloud.cfg中ssh_pwauth是否为false,以及datasource_list是否包含ConfigDrive, NoCloud。 - 检查公钥文件:查看
/home/openeuler/.ssh/authorized_keys文件是否存在且内容正确。cloud-init会将metadata中的公钥写入此文件。
- 检查metadata服务:在实例内部,尝试
一个隐蔽的坑:SELinux。如果SELinux处于强制模式(Enforcing),且
.ssh目录或authorized_keys文件的上下文不正确,可能导致SSH服务拒绝读取密钥。可以在cloud-init的runcmd模块中添加命令临时放宽限制,或在镜像中设置为宽容模式(setenforce 0),但更好的做法是确保文件上下文正确。在镜像定制时,可以执行restorecon -Rv /home/openeuler/.ssh。
5.3 镜像上传后创建实例非常慢
- 可能原因:
- 镜像格式:如果上传的是raw格式,Glance会先将其转换为qcow2(如果后端存储支持的话),首次创建实例时会有一个转换过程。最佳实践是上传前就转换为qcow2。
- 镜像体积过大:虽然我们做了清理,但如果初始安装的软件包太多,或者包含了调试符号、文档等,镜像体积会很大。使用
virt-sparsify工具可以进一步“瘦身”,它能够识别并丢弃磁盘镜像中未使用的块。sudo virt-sparsify –compress ${IMAGE_QCOW2} ${IMAGE_QCOW2}.sparse mv ${IMAGE_QCOW2}.sparse ${IMAGE_QCOW2} - Glance后端存储性能:如果使用文件系统后端,且磁盘IO性能差,也会影响速度。这属于平台运维层面问题。
5.4 实例根分区未自动扩容
即使配置了growpart和resizefs,有时分区也不会扩容。
- 排查步骤:
- 检查cloud-init日志,看
growpart和resizefs模块是否执行,是否有报错。 - 检查内核是否支持在线扩容。对于xfs文件系统,需要内核和
xfsprogs版本支持。 - 最常见原因:分区表类型。
growpart工具对MBR分区表支持较好,但对GPT分区表,需要确保磁盘末尾有足够的空闲空间。在创建镜像磁盘时,如果分区没有对齐到扇区末尾,可能会留出几兆的空间,导致growpart认为没有空间可扩。解决方案:在Kickstart分区时,使用part / –fstype=“xfs” –ondisk=vda –size=8192 –grow中的–grow参数,让根分区占据所有剩余空间,而不是指定一个固定值。
- 检查cloud-init日志,看
制作一个高质量的云镜像,是一个融合了系统知识、云平台理解和运维经验的细致活。从最开始的自动化安装,到中间的驱动、服务配置,再到最后的深度清理和优化,每一步都需要仔细考量。上面分享的流程和配置,是我经过多次测试和线上验证后总结出来的,已经能够稳定地产出符合生产环境要求的openEuler 22.03镜像。当然,根据不同的业务场景,你可能还需要在镜像中预装监控Agent、日志采集组件、或者特定的业务运行环境,这些都可以在virt-customize的–install和–upload阶段轻松加入。希望这份超详细的指南能帮你少走弯路,高效地构建出自己的“黄金镜像”。