Linux 基础体系 · 第 83/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux cloud-init:实例初始化、Metadata、用户数据、幂等和安全

1. cloud-init 解决什么问题

cloud-init 是运行在云主机或虚拟机内部的初始化框架。它在系统首次启动或实例身份发生变化时读取数据源(datasource)提供的实例信息和用户配置,然后以 root 身份执行一组初始化模块。

它解决的不是“启动一个服务”这么单一的问题,而是把以下过程连接起来:

  1. 云平台创建虚拟机;
  2. 云平台向虚拟机提供实例元数据和用户数据;
  3. 虚拟机识别数据源,获取网络、主机名、SSH 公钥等信息;
  4. cloud-init 生成用户、写入文件、安装软件、执行命令;
  5. 初始化状态被记录,后续启动根据状态决定哪些模块是否再次运行;
  6. 运维系统(例如 Ansible)接管持续配置、变更和修复。

这里有三个容易混淆的对象:

  • 实例(instance):云平台创建出来的一台虚拟机及其身份。
  • Metadata:描述实例的结构化元数据,例如实例 ID、主机名、网络接口和 SSH 公钥。
  • User-data:用户提交给实例的初始化输入,可以是 cloud-config、Shell 脚本或 MIME 多段消息。

cloud-init 不是虚拟机镜像本身,也不是云平台 API。它是来宾操作系统内部的一个初始化消费者。云平台负责提供数据,镜像中的 cloud-init 负责解释和执行数据。

2. 从启动到初始化:组件和数据流

一个典型数据流如下:

flowchart TD
    A[云平台创建实例] --> B[实例启动]
    B --> C[cloud-init-local]
    C --> D[识别 DataSource]
    D --> E[读取 Metadata]
    D --> F[读取 User-data]
    E --> G[生成实例状态]
    F --> G
    G --> H[cloud-init init]
    H --> I[网络和基础配置]
    I --> J[cloud-config modules]
    J --> K[cloud-final]
    K --> L[packages/write_files/users]
    K --> M[runcmd/scripts]
    L --> N[初始化完成]
    M --> N

在使用 systemd 的主流发行版中,通常可以看到以下服务:

systemctl status cloud-init-local.service
systemctl status cloud-init.service
systemctl status cloud-config.service
systemctl status cloud-final.service

这些服务对应的阶段通常是:

2.1 cloud-init-local

该阶段尽可能在网络完全可用前运行,主要任务包括:

  • 识别数据源;
  • 读取本地数据源;
  • 生成或准备网络配置;
  • 处理实例身份的早期信息。

它不能假设所有网络访问都已经成功。因此,依赖远端 HTTP 服务、软件仓库或 DNS 的操作不应放在只适合早期阶段的位置。

2.2 cloud-init

该阶段处理基础初始化,例如:

  • 主机名;
  • 网络配置;
  • 数据源元数据;
  • 用户配置的准备;
  • SSH 主机密钥等系统基础内容。

具体模块和顺序受发行版、cloud-init 版本及配置影响,不能把模块列表当作跨版本不变的规范。

2.3 cloud-config

该阶段运行 cloud-config 模块,例如:

  • 创建用户;
  • 安装软件包;
  • 写入文件;
  • 配置 SSH;
  • 设置时区;
  • 配置磁盘和挂载点。

2.4 cloud-final

该阶段通常是网络和包管理器可用之后的最后初始化阶段,常见任务包括:

  • 运行 runcmd 生成的脚本;
  • 运行脚本目录中的用户脚本;
  • 执行需要完整系统环境的初始化操作。

cloud-init 完成并不等价于“系统所有服务都健康”。它只表示初始化阶段结束。应用服务仍需通过 systemd、监控和健康检查验证。

可以使用以下命令观察状态:

cloud-init status
cloud-init status --wait

典型输出可能是:

status: done

在初始化尚未完成时,cloud-init status --wait 会等待,而不是立即返回。自动化系统可以据此避免在 cloud-final 尚未完成时立刻执行后续配置。

3. DataSource、Metadata 和 User-data

3.1 DataSource 是什么

DataSource 是 cloud-init 获取初始化输入的实现。常见数据源包括:

  • 公有云实例元数据服务;
  • OpenStack metadata service;
  • VMware 等虚拟化平台提供的数据;
  • NoCloud 本地目录、磁盘或 ISO;
  • ConfigDrive;
  • DigitalOcean、LXD 等平台特定实现。

数据源决定了 cloud-init 去哪里读数据以及如何识别实例。不同云平台的 Metadata URL、认证方式、网络行为和字段并不相同,不能把某一家云的路径写成所有环境通用接口。

可以在实例内查看数据源和实例数据:

cloud-id
cloud-init query ds
cloud-init query instance_id
cloud-init query local-hostname

某些发行版或 cloud-init 版本还会提供:

cloud-init query --all

输出字段和可查询范围与版本有关。不要直接假设某个字段在所有平台都存在。

3.2 Metadata

Metadata 是描述实例的输入数据,常见内容包括:

  • 实例 ID;
  • 区域、可用区、项目或租户信息;
  • 本地主机名;
  • 网卡、地址、路由和 DNS 信息;
  • 实例关联的 SSH 公钥;
  • 平台特定标签;
  • 临时凭证或令牌的访问入口。

Metadata 通常由实例内的 HTTP 服务、虚拟设备或本地挂载介质提供。它不是普通的互联网网站,而是实例身份的一部分。

Metadata 的一个关键作用是提供 instance-idcloud-init 用它区分“这是同一个实例的再次启动”还是“一个新的实例身份”。这直接影响幂等和模块是否再次执行。

3.3 User-data

User-data 是创建实例时传给实例的用户输入。最常见格式是以如下标记开始的 YAML:

#cloud-config
hostname: app-01

#cloud-config 不是 YAML 的业务字段,而是一个内容类型标记。它告诉 cloud-init 按 cloud-config 语法解析后续内容。

常见 user-data 类型有:

cloud-config

#cloud-config
packages:
  - curl
  - nginx

Shell 脚本

#!/bin/bash
set -eu
echo "initialized" > /var/lib/example/initialized

MIME 多段消息

当需要同时提供 cloud-config、Shell 脚本和其他内容时,可以使用 MIME。云平台通常提供生成或上传 MIME user-data 的方式,但具体命令因平台而异。

User-data 通常具备以下属性:

  • 执行权限高,很多操作由 root 执行;
  • 内容可能保存在本机;
  • 内容可能出现在日志或调试输出中;
  • 其生命周期、大小限制和编码方式由云平台及数据源共同决定;
  • 修改 user-data 是否触发重新执行,取决于实例生命周期和实例 ID,不应默认认为“改了就会再次运行”。

因此,user-data 更接近“实例启动时注入的初始化程序”,而不是安全的配置中心。

4. cloud-config 的生命周期和模块频率

cloud-init 会把初始化工作拆成多个模块。模块通常具有执行频率。常见频率概念包括:

  • per-instance:每个实例身份执行一次;
  • per-boot:每次启动执行;
  • per-always:每次满足阶段条件时执行;
  • per-once:全局只执行一次,具体含义受模块实现和版本影响。

常见模块有:

  • users-groups:创建和修改用户;
  • cc_package_update_upgrade_install:包更新、升级和安装;
  • cc_write_files:写入文件;
  • cc_runcmd:生成待执行的命令脚本;
  • cc_scripts_user:执行用户脚本;
  • cc_set_hostname:设置主机名;
  • cc_ssh:处理 SSH 相关初始化。

一个重要细节是:runcmd 通常不是在解析 YAML 的那一刻直接执行命令。cc_runcmd 会把命令转换并写入脚本,之后由脚本执行模块在最终阶段运行。

例如:

#cloud-config
runcmd:
  - [sh, -c, 'printf "%s\n" initialized > /var/lib/example/state']

使用数组形式可以减少 Shell 解析歧义。若使用字符串:

runcmd:
  - echo "$HOME" > /tmp/home

它通常会经过 Shell 解释,变量展开、重定向、管道和特殊字符都会生效。字符串形式更灵活,但注入风险和引号错误也更大。

模块顺序由 cloud-init 配置决定,而不是简单按 YAML 字段顺序执行。比如需要先安装软件,再写入其配置并启动服务时,应使用能表达依赖关系的模块或显式命令,而不能仅依赖配置文件中字段的排列顺序。

5. 一个可运行的初始化示例

下面的 cloud-config 创建一个系统用户,安装 Nginx,写入一个静态页面,并启用服务:

#cloud-config

package_update: true
packages:
  - nginx

users:
  - name: deploy
    gecos: Deployment user
    groups: [adm]
    shell: /bin/bash
    sudo: ["ALL=(ALL) NOPASSWD:ALL"]
    lock_passwd: true
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... deploy@example

write_files:
  - path: /var/www/html/index.html
    owner: root:root
    permissions: "0644"
    content: |
      cloud-init initialized this instance

runcmd:
  - [systemctl, enable, --now, nginx]

这个示例的因果关系是:

  1. package_update: true 要求先刷新包索引;
  2. packages 请求安装 Nginx;
  3. users 创建 deploy 用户并写入公钥;
  4. write_files 写入网页文件;
  5. runcmd 请求 systemd 启用并立即启动 Nginx。

但它仍有几个生产边界:

  • package_update 可能消耗网络和时间;
  • 包仓库不可用时,安装阶段可能失败或等待;
  • sudo: NOPASSWD 会扩大该用户的权限,不能因为方便就默认使用;
  • 示例中的公钥必须替换为真实公钥,不能把私钥放进 user-data;
  • systemctl enable --now nginx 只说明 systemd 接受了启动请求,仍需检查服务状态和端口。

验证:

cloud-init status --wait
systemctl is-enabled nginx
systemctl is-active nginx
curl -fsS http://127.0.0.1/

预期结果分别类似于:

enabled
active
cloud-init initialized this instance

如果 curl 失败,应继续检查:

systemctl status nginx --no-pager
journalctl -u nginx -b --no-pager
ss -lntp

5.1 使用 NoCloud 在本地测试

为了不依赖具体云厂商,可以使用 NoCloud 数据源进行测试。准备两个文件:

user-data

#cloud-config
write_files:
  - path: /tmp/cloud-init-test
    permissions: "0644"
    content: |
      hello from cloud-init
runcmd:
  - [sh, -c, 'date -Is >> /tmp/cloud-init-test']

meta-data

instance-id: iid-local-test-001
local-hostname: cloud-init-test

如果系统安装了 cloud-localds,可以生成 NoCloud 镜像:

cloud-localds seed.img user-data meta-data

随后把 seed.img 作为虚拟机的附加介质启动。cloud-localds 的安装包名称和可用性随发行版变化;它不是 cloud-init 在所有系统中的必备命令。

启动后检查:

cat /tmp/cloud-init-test

可能得到:

hello from cloud-init
2025-01-01T12:34:56+00:00

这个实验可以验证三件事:

  1. user-data 被识别为 cloud-config;
  2. instance-id 被写入实例状态;
  3. runcmd 在最终阶段执行,而不是仅仅被解析。

如果把 meta-data 中的 instance-id 改成另一个值再启动,cloud-init 可能把它识别为新的实例身份,从而重新执行 per-instance 模块。这正是实例身份与幂等行为之间的联系。

6. 幂等:重复执行后系统状态不再变化

6.1 形式化定义

设系统状态为 SS,初始化操作为 FF。如果重复执行一次和执行多次得到相同目标状态:

F(F(S))=F(S)F(F(S)) = F(S)

则称 FF 对该状态是幂等的。

例如:

mkdir -p /opt/example

第一次创建目录,第二次发现目录已存在,最终状态相同,因此通常是幂等的。

相反:

echo "$(date -Is)" >> /var/log/example-init.log

每次执行都会追加一行,因而:

F(F(S))F(S)F(F(S)) \ne F(S)

这不是幂等操作。

6.2 cloud-init 的幂等不是“所有命令自动幂等”

cloud-init 通过实例状态和模块频率避免重复运行某些模块,但它无法改变命令本身的副作用。

例如:

#cloud-config
runcmd:
  - [sh, -c, 'echo initialized >> /tmp/state']

如果该命令由于模块配置、实例 ID 变化、人工清理状态或脚本被再次调用而执行两次,文件会多出两行。cloud-init 的“per-instance”状态控制与业务命令的幂等性是两个不同层次:

  • cloud-init 层面:模块是否再次调度;
  • 命令层面:命令再次执行后是否改变结果;
  • 外部系统层面:包仓库、DNS、云 API、数据库等副作用是否可重复。

6.3 对比一个非幂等和幂等实现

非幂等:

runcmd:
  - [sh, -c, 'echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf']

执行两次会出现重复配置。重复配置不一定立刻失败,但会降低可读性,并使删除和审计困难。

一种更稳妥的写法是让文件内容由完整目标状态决定:

write_files:
  - path: /etc/sysctl.d/99-example.conf
    owner: root:root
    permissions: "0644"
    content: |
      net.ipv4.ip_forward = 1

runcmd:
  - [sysctl, --system]

这里的推导是:

  1. 配置文件的目标内容固定;
  2. write_files 每次都将文件收敛到该内容;
  3. sysctl --system 将目标内容加载到内核;
  4. 重复执行不会继续追加同一行。

sysctl --system 仍可能受到其他配置文件覆盖,因此验证结果比“命令执行成功”更重要:

sysctl net.ipv4.ip_forward

6.4 文件、用户和包的幂等边界

以下形式通常更接近幂等操作:

#cloud-config
packages:
  - nginx

users:
  - name: deploy
    lock_passwd: true

包管理器会判断软件包是否已经安装,用户模块也会判断用户是否存在。不过以下情况仍可能破坏预期:

  • 包仓库中版本发生变化;
  • 软件包安装脚本本身带有外部副作用;
  • 用户已存在但 UID、组或 shell 不符合目标;
  • 手工修改了 cloud-init 管理的文件;
  • 发行版的包管理器处于锁定状态;
  • 模块只在首次实例初始化时运行,后续配置变更并不会自动收敛。

因此,cloud-init 更适合“首次引导和基础成像”,而不是替代持续配置管理。Ansible Playbook 应负责后续的目标状态收敛,并通过 Inventory 区分环境、通过滚动变更控制影响范围。

7. 实例状态、重启和镜像复制

cloud-init 会在本机保存状态。常见路径包括:

/var/lib/cloud/
├── instance -> instances/<instance-id>
├── instances/
├── sem/
└── seed/

具体目录结构与发行版和版本有关,但通常可以在以下目录看到日志、缓存和实例状态:

sudo ls -la /var/lib/cloud/
sudo ls -la /var/lib/cloud/instance/

常见日志:

sudo less /var/log/cloud-init.log
sudo less /var/log/cloud-init-output.log

cloud-init.log 更偏向 cloud-init 自身的处理日志;cloud-init-output.log 常包含初始化命令的标准输出和标准错误。敏感数据可能因此进入日志。

7.1 普通重启

普通重启通常不会让所有 per-instance 模块重新执行,因为实例 ID 没有变化,模块的完成标记仍然存在。

这并不意味着所有内容都不会再次执行。per-boot 或 per-always 模块可能仍会运行。

7.2 复制镜像

制作模板镜像时,如果把源实例的实例状态一起复制,新的虚拟机可能继承旧实例的:

  • 实例 ID;
  • 模块完成标记;
  • SSH 主机密钥;
  • 主机名;
  • 网络配置;
  • 日志和缓存。

因此,制作镜像前通常需要清理实例状态和机器身份。常见命令为:

sudo cloud-init clean --logs

部分版本支持在清理后自动重启:

sudo cloud-init clean --logs --reboot

该操作具有破坏性:它会删除 cloud-init 的状态和日志,使后续启动可能重新执行初始化。必须先确认当前系统确实处于可重新初始化状态,并且模板中的用户、SSH、网络和应用配置不会造成重复或安全问题。

不要把“删除 /var/lib/cloud”当作无条件等价方案。不同版本的数据、缓存和权限细节可能不同;优先使用本机版本提供的 cloud-init clean --help 检查支持的参数。

7.3 误判失败的典型原因

一个常见误判是:修改 user-data 后重启实例,发现配置没有生效,于是认为 cloud-init 失效。实际可能是:

  1. user-data 只在创建实例时注入;
  2. cloud-init 发现原 instance-id 未变化;
  3. 对应模块已有完成标记;
  4. 因而没有重新执行。

这时不要直接删除状态目录。应先判断自己是在测试“首次初始化”、 “配置变更”还是“实例重建”。生产环境中,临时删除状态并重跑初始化可能重复创建用户、重启服务、改写网络或覆盖配置。

8. YAML、Shell 和执行顺序的风险

8.1 YAML 类型问题

YAML 不是“看起来像配置文件的文本”,它有自己的类型规则。建议对关键值显式加引号:

users:
  - name: deploy
    shell: "/bin/bash"
    sudo:
      - "ALL=(ALL) NOPASSWD:ALL"

权限字段尤其应写成字符串:

permissions: "0600"

否则某些解析器可能把它当作数字处理,导致前导零丢失或语义改变。

8.2 Shell 引号问题

以下命令意图是把变量作为一个整体传给脚本:

runcmd:
  - [sh, -c, 'useradd --comment "service account" appuser']

如果变量来自 Metadata、标签或外部输入,不能直接拼接到 Shell 字符串中。应优先使用参数数组,或先进行严格校验。云平台标签不是天然可信的 Shell 代码。

8.3 bootcmdruncmd 不等价

bootcmd 适合非常早期、每次启动相关的操作,执行时网络和文件系统环境可能还不完整;runcmd 适合最终阶段的命令,但它本身通常只是生成脚本,实际执行由后续模块完成。

例如,不应把依赖软件包安装结果的命令放到早期阶段:

bootcmd:
  - [systemctl, restart, nginx]

此时 Nginx 可能还没有安装,或者 systemd、网络和挂载状态尚未达到预期。

应根据依赖关系选择阶段,并在执行后验证结果,而不是仅凭 YAML 字段名称判断时机。

9. Metadata 服务与安全边界

9.1 Metadata 不是普通可信 API

实例内的进程通常都可能访问 Metadata 服务。若某个 Web 应用存在 SSRF 漏洞,攻击者可能借助该漏洞访问 Metadata,并进一步取得:

  • 实例身份信息;
  • SSH 公钥等配置;
  • 云平台临时凭证;
  • 角色或服务账号权限。

不同云平台提供的防护不同,例如请求令牌、特定请求头、TTL 限制或网络隔离。不能假设所有平台都使用相同的 Metadata 认证机制。

安全设计应同时依赖:

  • 云平台提供的 Metadata 防护;
  • 最小权限的实例角色;
  • 应用层 SSRF 防护;
  • 出站网络控制;
  • 凭证短期化和轮换;
  • 不让应用进程拥有不必要的云 API 权限。

仅仅修改应用代码中的 Metadata URL,不足以形成完整边界。

9.2 不要把长期密钥直接放入 user-data

以下做法风险很高:

#cloud-config
write_files:
  - path: /root/.aws/credentials
    content: |
      aws_access_key_id = ...
      aws_secret_access_key = ...

原因包括:

  • user-data 可能被云平台保存;
  • 本机可能保留 /var/lib/cloud/instance/user-data.txt
  • 调试日志可能包含命令参数和输出;
  • 具有 root 权限的本机用户可以读取;
  • 镜像、快照和备份可能间接复制这些内容。

更合理的流程是:

  1. 通过实例角色或工作负载身份取得短期凭证;
  2. 使用 Secret 管理系统按需拉取;
  3. 赋予文件最小权限,例如 0600
  4. 避免把 Secret 放进命令行参数;
  5. 防止日志输出 Secret;
  6. 定期轮换并验证撤销路径。

如果必须通过 write_files 写入敏感文件:

#cloud-config
write_files:
  - path: /etc/example/token
    owner: root:root
    permissions: "0600"
    content: |
      token-value

这只能减少本机文件暴露,不能消除 user-data、日志、云平台控制面和镜像备份中的暴露风险。

9.3 SSH 初始化的边界

使用公钥登录时,私钥应只保存在管理员或密钥系统中。user-data 中可以放公钥:

users:
  - name: deploy
    lock_passwd: true
    ssh_authorized_keys:
      - ssh-ed25519 AAAA...

不应把密码明文写入:

chpasswd:
  list: |
    deploy:PlainTextPassword

即使某些版本允许密码哈希,也要考虑 user-data 的存储和日志风险。更稳妥的是禁用密码登录、使用密钥或短期认证机制,并通过 SSH 服务实际配置验证:

sshd -T | grep -E 'passwordauthentication|permitrootlogin'

disable_root: truessh_pwauth: false 等 cloud-init 配置对发行版默认配置的影响可能不同,必须检查最终的 sshd_configsshd_config.d 和发行版预设文件。

10. 诊断:从阶段、输入和状态三条线排查

初始化失败不能只看一条日志。应按以下顺序缩小范围。

10.1 先确认是否完成

cloud-init status --long
systemctl list-dependencies cloud-final.service

如果仍在运行:

cloud-init status --wait

10.2 再看服务日志

journalctl -u cloud-init-local.service -b --no-pager
journalctl -u cloud-init.service -b --no-pager
journalctl -u cloud-config.service -b --no-pager
journalctl -u cloud-final.service -b --no-pager

常见失败路径:

  • DataSource 未识别;
  • Metadata 网络不可达;
  • user-data YAML 解析失败;
  • 包管理器被另一个进程锁定;
  • DNS 或软件仓库不可用;
  • 命令返回非零退出码;
  • 服务启动后立即退出;
  • 用户脚本权限或解释器错误。

10.3 验证输入是否正确到达

sudo sed -n '1,160p' /var/lib/cloud/instance/user-data.txt
sudo ls -la /var/lib/cloud/instance/
cloud-id
cloud-init query instance_id

读取 user-data 可能暴露 Secret,只应由有权限的管理员执行,并避免把输出复制到工单、聊天或公共日志中。

10.4 验证语法

在安全测试环境中,可使用:

cloud-init schema --config-file ./user-data

典型成功输出依版本而不同,可能表示配置通过验证;失败时会指出 YAML 结构或字段问题。schema 主要检查格式和字段,不会验证软件仓库、网络、服务启动等运行时条件。

10.5 分析耗时

许多版本支持:

cloud-init analyze show
cloud-init analyze blame

blame 可以帮助发现包安装、网络等待或某个脚本耗时过长。它不能证明应用已经健康,只能说明 cloud-init 阶段的时间分布。

11. 生产中的边界:cloud-init 与 Ansible 如何分工

适合放入 cloud-init 的内容通常具有以下特征:

  • 只需要在实例首次引导时完成;
  • 与实例身份强相关;
  • 为后续配置管理准备基础条件;
  • 失败后容易通过重建实例恢复;
  • 不包含长期密钥;
  • 不需要复杂的条件分支和持续收敛。

例如:

  • 设置初始主机名;
  • 创建运维用户并写入公钥;
  • 安装 Ansible、监控代理或基础包;
  • 配置基础磁盘挂载;
  • 写入指向 Secret 系统的引导配置。

适合放入 Ansible 的内容通常是:

  • 持续配置;
  • 多次变更;
  • 需要 Inventory、分组变量和条件逻辑;
  • 需要滚动更新;
  • 需要审计、回滚和跨主机协调;
  • 需要对当前状态反复收敛。

一个常见的生产链路是:

云平台创建实例
    ↓
cloud-init 完成基础引导
    ↓
实例注册到 Inventory 或控制面
    ↓
Ansible 执行 Playbook
    ↓
监控、容量、变更和应急流程接管

两者不要同时管理同一份配置文件。比如 cloud-init 用 write_files/etc/example/app.conf,Ansible 又管理同一文件,最终结果取决于执行顺序,故障时也难以判断责任边界。

更稳妥的做法是:

  • cloud-init 只写入最小引导配置;
  • Ansible 负责完整配置;
  • Secret 由专门系统提供;
  • 应用通过健康检查验证;
  • 变更通过滚动策略控制,而不是在 user-data 中无条件重启所有服务。

12. 常见误解和反例

误解一:user-data 是一次性脚本,所以不用考虑幂等

错误。初始化脚本可能因实例身份变化、镜像清理、人工重跑或故障恢复而再次执行。所有会修改系统的操作都应明确重复执行后的结果。

误解二:cloud-init 返回成功就代表应用正常

错误。cloud-init 只报告初始化阶段状态。应继续验证:

systemctl is-active example.service
curl -fsS http://127.0.0.1:8080/health

误解三:重启就会重新读取最新 user-data

错误。云平台可能只在创建实例时设置 user-data,cloud-init 也可能因为实例 ID 和完成标记而跳过模块。

误解四:Metadata 只有主机名等无害信息

错误。Metadata 可能提供临时凭证或访问令牌。应用 SSRF、开放代理和过宽实例角色都可能把它转化为权限提升路径。

误解五:复制虚拟机磁盘就是制作模板

错误。必须处理实例状态、SSH 主机密钥、机器身份、日志、缓存和网络配置,否则多个实例可能拥有重复身份或跳过初始化。

13. 交付前检查

在把 cloud-init 配置用于生产前,至少应验证:

  1. 数据源是否明确,Metadata 访问是否满足平台安全要求;
  2. user-data 是否通过版本匹配的 schema 检查;
  3. YAML 中的权限、布尔值、字符串和 Shell 引号是否正确;
  4. 初始化命令是否满足幂等条件;
  5. 包管理器、DNS、仓库不可用时的失败表现;
  6. /var/log/cloud-init*/var/lib/cloud/instance 是否会暴露敏感数据;
  7. 模板镜像是否执行了适当的状态清理;
  8. cloud-init 是否只负责首次引导,持续配置是否交给 Ansible;
  9. cloud-init status 成功后,服务健康检查是否仍然通过;
  10. 失败时能否通过日志、实例重建或回滚恢复,而不是依赖手工删除状态目录。

cloud-init 的核心价值是把“实例创建”和“操作系统进入可管理状态”连接起来。它的可靠性不来自一段很长的启动脚本,而来自清晰的数据来源、明确的阶段边界、可重复的目标状态、可观测的执行记录和受控的权限范围。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。