Cloud-init是Ubuntu系统中用于云实例初始化配置的核心工具,它在虚拟机或云服务器首次启动时自动执行用户数据(user-data)脚本,完成主机名设置、SSH密钥注入、软件包安装、网络配置等一系列操作。自定义cloud-init配置文件的关键在于理解其YAML格式的用户数据结构,掌握模块优先级和执行顺序,并通过日志定位调试问题。下面直接从配置文件编写、模块详解、常见调试方法三个维度把这件事讲透。
一、cloud-init的工作原理与配置文件位置
Cloud-init在系统启动的早期阶段(initramfs之后、systemd之前)运行,读取位于/etc/cloud/cloud.cfg.d/目录下的所有.cfg文件以及/etc/cloud/cloud.cfg主配置文件。它支持多种数据源(datasource),包括NoCloud、OpenStack、AWS、Azure、VMware等,不同平台的数据来源不同,但最终都会生成一份user-data文件供cloud-init解析执行。
在本地测试或自定义场景下,最常用的是NoCloud数据源。你需要在ISO镜像或虚拟机中放置一个名为"meta-data"和"user-data"的文件到/var/lib/cloud/seed/nocloud-net/目录下。主配置文件/etc/cloud/cloud.cfg定义了模块执行顺序、数据源列表和全局参数,而具体的初始化逻辑则写在user-data中。
# /etc/cloud/cloud.cfg 关键配置示例
datasource_list: [ NoCloud ]
datasource:
NoCloud:
seedfrom: /var/lib/cloud/seed/nocloud-net/理解这个路径和结构是一切自定义的基础。很多人卡在第一步,就是因为不知道cloud-init去哪里找配置文件,导致写了半天脚本系统根本不执行。
二、user-data文件的YAML结构与核心模块
User-data文件必须以#cloud-config开头,这是cloud-init识别的标志。整个文件是YAML格式,缩进必须严格使用空格(不能用Tab)。它由多个模块组成,每个模块负责一类初始化任务。下面按常用程度逐一说明。
1. hostname和fqdn模块:设置主机名和完整域名
#cloud-config hostname: my-ubuntu-server fqdn: my-ubuntu-server.example.com manage_etc_hosts: true
manage_etc_hosts设为true时,cloud-init会自动把主机名写入/etc/hosts文件,省去手动编辑的麻烦。
2. ssh_authorized_keys模块:注入SSH公钥
#cloud-config ssh_authorized_keys: - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDK... user@host - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... user@host
这个模块直接把公钥写入/home/ubuntu/.ssh/authorized_keys(默认用户名根据镜像而定,Ubuntu通常是ubuntu)。注意权限和属主,cloud-init会自动处理,但如果你后续手动修改要小心别破坏。
3. runcmd模块:执行任意Shell命令
#cloud-config runcmd: - [ apt-get, update ] - [ apt-get, install, -y, nginx ] - [ systemctl, enable, nginx ] - [ systemctl, start, nginx ] - echo "Hello from cloud-init" > /tmp/cloud-init-test.txt
runcmd是最灵活的模块,每条命令用数组形式写(避免YAML解析问题)。命令按顺序执行,前一条失败会影响后续,但默认不会中断整个流程。如果需要确保某条命令失败时停止,可以用"|| exit 1"的方式。
4. write_files和runcmd配合:创建自定义配置文件
#cloud-config
write_files:
- path: /etc/nginx/conf.d/custom.conf
content: |
server {
listen 80;
server_name example.com;
root /var/www/html;
}
permissions: '0644'
- path: /etc/myapp/config.yaml
content: |
database:
host: localhost
port: 5432
owner: root:rootwrite_files模块可以直接在目标系统上创建文件,支持内容模板、权限设置、所有者指定。这在部署应用时非常实用,比用runcmd里的echo重定向要可靠得多,因为它能精确控制文件属性。
5. packages和package_update模块:软件包管理
#cloud-config package_update: true package_upgrade: true packages: - curl - wget - git - vim - htop
package_update会先执行apt-get update,package_upgrade会做dist-upgrade。如果你的镜像比较旧,建议都设为true,但要注意升级可能引入兼容性问题,生产环境建议锁定版本。
6. users模块:创建用户和设置密码
#cloud-config
users:
- name: deployuser
groups: sudo, docker
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- ssh-rsa AAAAB3... deploy@workstation这里要特别注意,cloud-init默认会锁定root账户(passwd: '!'),如果你需要启用root登录,必须显式设置。密码可以用chpasswd模块单独设置。
7. chpasswd和expire模块:密码策略
#cloud-config
chpasswd:
list: |
deployuser:MyStr0ngP@ss!
expire: falsechpasswd的list字段支持多行,每行是"用户名:密码"格式。expire设为false表示密码永不过期。生产环境强烈建议配合SSH密钥使用,尽量避免明文密码。
三、多云数据(#include)与合并策略
当user-data内容较多时,可以用#include指令引入外部文件,cloud-init会在解析时将其内容合并进来。这对于版本管理和模块化配置非常有用。
#cloud-config #include - http://192.168.1.100/cloud-config/base.yaml - /var/lib/cloud/scripts/per-instance/custom.yaml
合并策略由/etc/cloud/cloud.cfg中的merge_how配置决定,支持dict(字典合并)、list(列表追加)、str(字符串拼接)等方式。默认情况下,顶层键是dict合并,runcmd等列表字段是追加。如果你需要覆盖而非追加,可以在外部文件中使用相同键名并设置特定的合并行为。
四、cloud-init调试的核心方法
调试cloud-init是运维中的高频需求,因为配置写错了系统不会报明显错误,只是默默不执行。以下是经过实战验证的调试手段。
1. 查看日志文件
Cloud-init的运行日志在/var/log/cloud-init.log和/var/log/cloud-init-output.log。前者记录cloud-init自身的执行过程,后者记录runcmd等模块的标准输出和错误输出。这两个文件是排查问题的第一手资料。
# 实时查看日志 tail -f /var/log/cloud-init.log # 查看上次启动的详细输出 cat /var/log/cloud-init-output.log
2. 使用cloud-init命令行工具
# 分析配置文件语法是否正确 cloud-init schema --config-file user-data.yaml # 模拟执行(不实际修改系统) cloud-init init --local # 查看当前实例状态 cloud-init status # 清理并重新运行(慎用,会重置所有cloud-init状态) cloud-init clean cloud-init init
cloud-init schema是个非常好用的工具,它会验证你的YAML文件是否符合cloud-init的schema定义,能提前发现缩进错误、字段名拼写错误等问题。cloud-init status会显示当前处于哪个阶段(init-local、init-network、config、final等),帮助你判断执行到了哪一步。
3. 手动触发模块执行
# 只运行特定模块 cloud-init modules --mode=config # 查看所有可用模块及其状态 cloud-init modules --mode=config --list
这个功能在你只想测试某个模块而不想重启整个实例时特别有用。比如你改了write_files的内容,不需要重启机器,直接运行上面的命令就能看到效果。
4. 检查数据源和元数据
# 查看cloud-init识别到的数据源
cloud-init query -f '{{ds.name}}'
# 查看实例ID
cloud-init query -f '{{instance_id}}'
# 查看所有可用的元数据
cloud-init query -f '{{v1.cloud_name}}'很多时候配置不生效不是YAML写错了,而是数据源没正确识别。比如在VMware环境下用了NoCloud数据源,cloud-init根本不会去读你放的user-data文件。这时候需要在/etc/cloud/cloud.cfg.d/99-vmware.cfg中正确配置VMware数据源。
五、常见踩坑点与最佳实践
1. YAML缩进错误是头号杀手。YAML对缩进极其敏感,多一个空格少一个空格都会导致解析失败。建议用cloud-init schema先校验,编辑器开启显示空格和Tab的功能。
2. runcmd中的命令要考虑执行时机。有些命令依赖网络(如apt-get),但在init-local阶段网络可能还没起来。如果需要网络,应该放在init-network或config阶段,或者在runcmd前加等待网络的逻辑。
3. 避免在user-data中写过于复杂的逻辑。Cloud-init不是配置管理工具,它适合做初始化的一次性操作。复杂的持续配置应该交给Ansible、Puppet等工具。cloud-init做好"第一次启动"的事就够了。
4. 版本兼容性要注意。不同Ubuntu版本(18.04、20.04、22.04、24.04)的cloud-init版本不同,支持的模块和字段有差异。写配置时要确认目标版本的文档,避免使用已废弃的字段。
5. 敏感信息不要明文写在user-data里。如果配置文件要上传到版本仓库或共享,密码、密钥等敏感内容用cloud-init的encrypt模块加密,或者通过其他安全渠道注入。
6. 测试时用cloud-localds工具生成ISO。这是验证配置最方便的方式,不需要每次都重新部署云实例。
# 安装工具 sudo apt install cloud-image-utils # 生成包含user-data和meta-data的ISO镜像 cloud-localds seed.img user-data meta-data # 用这个ISO启动虚拟机测试
六、总结
Cloud-init的自定义与调试本质上是三件事:写对YAML格式的user-data、理解模块执行顺序和数据源匹配、善用日志和命令行工具定位问题。掌握这三点,你就能在Ubuntu云环境中实现高效的自动化初始化。不要把cloud-init当成万能配置工具,它的定位是"首次启动初始化",用对场景才能发挥最大价值。遇到问题先看日志、再校验schema、最后查数据源,这套流程能解决绝大多数故障。
