Cloud-init 与 Metadata API 的关系

6 阅读4分钟

📋 核心概念

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脚本          │
│ - 配置网络                   │
└─────────────┘

🔑 关键要点

  1. 同一份数据,两种访问方式

    • Config Drive = 离线快照(实例创建时)
    • Metadata Service = 在线查询(实例运行时)
  2. Cloud-init 的数据源优先级

    Config Drive > Metadata Service > EC2 兼容接口
    
  3. 可动态更新的数据

    • 密码(通过 metadata service 的 password endpoint)
    • Vendor data(可以动态注入)
  4. 不可变数据

    • user_data(实例创建后不变)
    • SSH密钥(实例创建后不变)
  5. 代码关键路径

这就是Nova通过metadata机制实现"基础设施即代码"的核心 - 将配置数据作为API提供给实例内部的cloud-init消费!