📋 核心概念
Cloud-init 是在实例启动时运行的初始化工具,它从 Nova metadata API 获取配置信息来自动配置实例(设置主机名、SSH密钥、网络、运行脚本等)。
它们的关系是:生产者-消费者模式
- Nova Metadata API = 生产者(提供配置数据)
- Cloud-init = 消费者(读取并应用配置)
🎯 两种数据源(Datasource)
Cloud-init 可以从两种方式获取 Nova 提供的元数据:
1️⃣ Metadata Service (HTTP方式)
工作流程:
实例启动
↓
cloud-init 检测到 OpenStack 环境
↓
访问 http://169.254.169.254/openstack/latest/
↓
获取 meta_data.json, user_data, network_data.json 等
↓
应用配置(设置hostname, SSH key, 运行脚本等)
代码实现:
Cloud-init 发起请求:
# 在实例内部
curl http://169.254.169.254/openstack/latest/meta_data.json
Nova 处理请求 - handler.py:98-139:
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
# 1. 获取实例的元数据对象
if CONF.neutron.service_metadata_proxy:
meta_data = self._handle_instance_id_request(req)
else:
meta_data = self._handle_remote_ip_request(req)
# 2. 根据路径查找数据
data = meta_data.lookup(req.path_info)
# 3. 返回响应
return req.response
2️⃣ Config Drive (块设备方式)
工作流程:
Nova创建实例时
↓
生成 Config Drive ISO/VFAT 镜像
↓
作为虚拟磁盘挂载到实例 (/dev/disk/by-label/config-2)
↓
cloud-init 挂载并读取文件
↓
应用配置
代码实现:
Nova 生成 Config Drive - configdrive.py:130-146:
def make_drive(self, path):
"""Make the config drive."""
with utils.tempdir() as tmpdir:
# 写入所有元数据文件
self._write_md_files(tmpdir)
# 生成 ISO9660 或 VFAT 格式
if CONF.config_drive_format == 'iso9660':
self._make_iso9660(path, tmpdir)
elif CONF.config_drive_format == 'vfat':
self._make_vfat(path, tmpdir)
生成的文件结构:
config-2/
├── ec2/
│ ├── latest/
│ │ ├── meta-data.json
│ │ └── user-data
│ └── 2009-04-04/
│ └── ...
└── openstack/
├── latest/
│ ├── meta_data.json # 实例元数据
│ ├── user_data # 用户脚本
│ ├── network_data.json # 网络配置
│ ├── vendor_data.json # 供应商数据
│ └── vendor_data2.json
├── 2012-08-10/
│ └── ...
└── content/
├── 0000 # 注入的文件
└── 0001
元数据生成 - base.py:590-634:
def metadata_for_config_drive(self):
"""Yields (path, value) tuples for metadata elements."""
# 1. 生成 EC2 格式元数据(兼容性)
for version in VERSIONS + ["latest"]:
data = self.get_ec2_metadata(version)
if 'user-data' in data:
filepath = os.path.join('ec2', version, 'user-data')
yield (filepath, data['user-data'])
filepath = os.path.join('ec2', version, 'meta-data.json')
yield (filepath, jsonutils.dump_as_bytes(data['meta-data']))
# 2. 生成 OpenStack 格式元数据
for version in OPENSTACK_VERSIONS + ["latest"]:
# meta_data.json
path = 'openstack/%s/%s' % (version, MD_JSON_NAME)
yield (path, self.lookup(path))
# user_data
if self.userdata_raw is not None:
path = 'openstack/%s/%s' % (version, UD_NAME)
yield (path, self.lookup(path))
# network_data.json
path = 'openstack/%s/%s' % (version, NW_JSON_NAME)
yield (path, self.lookup(path))
# vendor_data.json
path = 'openstack/%s/%s' % (version, VD_JSON_NAME)
yield (path, self.lookup(path))
📦 Cloud-init 使用的关键数据
1. meta_data.json - 实例基本信息
生成代码 - base.py:317-369:
def _metadata_as_json(self, version, path):
metadata = {
'uuid': self.uuid,
'hostname': self._get_hostname(), # cloud-init设置主机名
'name': self.instance.display_name,
'availability_zone': self.availability_zone,
'launch_index': self.instance.launch_index,
'project_id': self.instance.project_id,
}
# SSH公钥 - cloud-init会写入 ~/.ssh/authorized_keys
if self.instance.key_name:
metadata['public_keys'] = {
keypair.name: keypair.public_key,
}
metadata['keys'] = [{
'name': keypair.name,
'type': keypair.type,
'data': keypair.public_key
}]
# 用户自定义元数据
if self.launch_metadata:
metadata['meta'] = self.launch_metadata
# 设备信息(磁盘、网卡等)
metadata['devices'] = self._get_device_metadata(version)
return jsonutils.dump_as_bytes(metadata)
Cloud-init 使用示例:
# cloud-init 读取后设置
hostname: instance-001
fqdn: instance-001.example.com
ssh_authorized_keys:
- ssh-rsa AAAAB3... user@example.com
2. user_data - 用户脚本
用户在创建实例时提供的初始化脚本:
# 创建实例时传入
openstack server create \
--image ubuntu \
--flavor m1.small \
--user-data init-script.sh \
my-instance
代码存储 - base.py:155-158:
if instance.user_data is not None:
# 从数据库中取出(base64编码)
self.userdata_raw = base64.decode_as_bytes(instance.user_data)
else:
self.userdata_raw = None
Cloud-init 处理:
- 如果是
#!/bin/bash开头 → 作为shell脚本执行 - 如果是
#cloud-config开头 → 作为YAML配置解析 - 如果是
#include→ 下载外部配置
示例:
#!/bin/bash
apt-get update
apt-get install -y nginx
systemctl start nginx
3. network_data.json - 网络配置
生成代码 - base.py:488-491:
def _network_data(self, version, path):
if self.network_metadata is None:
return jsonutils.dump_as_bytes({})
return jsonutils.dump_as_bytes(self.network_metadata)
Cloud-init 使用此数据配置网络接口(IP地址、路由、DNS等)。
4. vendor_data.json - 供应商定制数据
代码实现 - base.py:498-528:
def _vendor_data(self, version, path):
if CONF.api.vendordata_providers and \
'StaticJSON' in CONF.api.vendordata_providers:
return jsonutils.dump_as_bytes(
self.vendordata_providers['StaticJSON'].get())
运营商可以注入自定义配置(如监控agent安装脚本)。
🔀 两种方式的选择
Metadata Service 优势:
- ✅ 无需额外磁盘
- ✅ 可动态更新(如密码重置)
- ✅ 实例可以多次查询
Config Drive 优势:
- ✅ 不依赖网络(适合隔离环境)
- ✅ 数据在实例创建时固化
- ✅ 可在无metadata服务的环境使用
代码中如何同时支持两者:
Libvirt Driver 创建实例 - libvirt/driver.py:5418-5428:
# 创建 InstanceMetadata 对象(同一份数据)
inst_md = instance_metadata.InstanceMetadata(
instance,
content=injection_info.files,
extra_md=extra_md,
network_info=injection_info.network_info
)
# 生成 Config Drive
cdb = configdrive.ConfigDriveBuilder(instance_md=inst_md)
with cdb:
cdb.make_drive(config_disk_path)
Metadata Service 处理请求 - base.py:678-702:
def get_metadata_by_instance_id(instance_id, address, ctxt=None):
# 查询数据库获取实例
instance = objects.Instance.get_by_uuid(ctxt, instance_id, ...)
# 返回同一个 InstanceMetadata 对象
return InstanceMetadata(instance, address)
🎬 完整时序图
创建实例时刻:
┌─────────────┐
│ Nova API │ --[1. 接收创建请求 + user_data]-->
└─────────────┘
↓
┌─────────────┐
│ Nova Compute│ --[2. 保存实例到数据库]-->
└─────────────┘
↓
┌─────────────┐
│ Libvirt │ --[3. 生成 InstanceMetadata 对象]-->
└─────────────┘
↓
┌─────────────┐
│ConfigDrive │ --[4. 写入ISO/VFAT镜像]-->
└─────────────┘
↓
┌─────────────┐
│ 虚拟机启动 │
└─────────────┘
实例启动后:
┌─────────────┐
│ cloud-init │ --[方式1: 挂载 /dev/disk/by-label/config-2]-->
│ (实例内) │
└─────────────┘
OR
↓ [方式2: HTTP GET]
┌─────────────────────────────────┐
│ http://169.254.169.254/... │
└─────────────────────────────────┘
↓ [Neutron拦截并转发]
┌─────────────────────────────────┐
│ Nova Metadata Service │
│ - handler.py 处理请求 │
│ - base.py 查询数据库生成数据 │
└─────────────────────────────────┘
↓
┌─────────────┐
│ cloud-init │ --[解析并应用配置]-->
│ - 设置hostname │
│ - 配置SSH密钥 │
│ - 执行user_data脚本 │
│ - 配置网络 │
└─────────────┘
🔑 关键要点
-
同一份数据,两种访问方式:
- Config Drive = 离线快照(实例创建时)
- Metadata Service = 在线查询(实例运行时)
-
Cloud-init 的数据源优先级:
Config Drive > Metadata Service > EC2 兼容接口 -
可动态更新的数据:
- 密码(通过 metadata service 的 password endpoint)
- Vendor data(可以动态注入)
-
不可变数据:
- user_data(实例创建后不变)
- SSH密钥(实例创建后不变)
-
代码关键路径:
- 生成:base.py:InstanceMetadata
- Config Drive:configdrive.py:ConfigDriveBuilder
- HTTP服务:handler.py:MetadataRequestHandler
这就是Nova通过metadata机制实现"基础设施即代码"的核心 - 将配置数据作为API提供给实例内部的cloud-init消费!