OpenStack metadata简介
OpenStack虚拟机通过cloud-init完成初始化配置,比如网卡配置、hostname、初始化密码以及密钥注入等。cloud-init是运行在虚拟机内部的一个进程,它通过datasources获取虚拟机的配置信息,即metadata。cloud-init支持从不同的数据源获取metadata,因而实现了不同的datasources驱动,不同的datasource实现原理不一样。比较常用的datasources主要有以下两种:
ConfigDriver: Nova把所有配置信息写入到本地的一个raw文件中,然后通过cdrom形式挂载到虚拟机中。此时在虚拟机内部可以看到类似/dev/sr0(注:sr代表 scsi + rom)的虚拟设备。cloud-init只需要读取/dev/sr0文件信息即可获取虚拟机配置信息。
Metadata: Nova在本地启动一个HTTP metadata服务,虚拟机通过HTTP访问该metadata服务获取相关的虚拟机配置信息。
ConfigDriver的实现原理比较简单,本文不再介绍。这里重点介绍Metadata,主要解决以下两个问题:
Nova Metadata服务启动在nova-api控制节点上,虚拟机内部租户网络和宿主机的物理网络是不通的,虚拟机如何访问Nova的Metadata服务。
假设问题1已经解决,那么Nova Metadata服务如何知道是哪个虚拟机发起的请求。
02
Metadata服务配置
2.1 Nova配置
Nova的metadata服务名称为nova-api-metadata,不过通常会把服务与nova-api服务合在一起启动:
另外虚拟机访问Nova的Metadata服务需要Neutron转发,原因后面讲,这里只需要注意在nova.conf配置:
2.2 Neutron配置
前面提到虚拟机访问Nova的Metadata服务需要Neutron转发,可以通过l3-agent转发,也可以通过dhcp-agent转发,如何选择需要根据实际情况:
通过l3-agent转发,则虚拟机所在的网络必须关联了router。
通过dhcp-agent转发,则虚拟机所在的网络必须开启dhcp功能。
Metadata默认是通过l3-agent转发的,不过由于在实际情况下,虚拟机的网络通常都会开启dhcp功能,但不一定需要router,因此我更倾向于选择通过dhcp-agent转发,配置如下:
本文接下来的所有内容均基于以上配置环境。
03
虚拟机如何访问Metadata服务
3.1 从虚拟机访问Metadata服务说起
cloud-init访问metadata服务的URL地址是http://169.254.169.254,这个IP很特别,主要是效仿了AWS的Metadata服务地址,它的网段是169.254.0.0/16,这个IP段其实是保留的,即IPv4 Link Local Address,它和私有IP(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)类似,不能用于互联网路由,通常只用于直连网络。如Windows操作系统DHCP获取IP失败,就会自动配置为169.254.0.0/16网段的一个IP。
那AWS为什么选择169.254.169.254这个IP呢,这是因为选择Link Local IP可以避免与用户的IP冲突,至于为什么选择169.254.169.254这个IP而不是169.254.0.0/24的其它IP,大概是为了好记吧。
另外AWS还有几个很有趣的地址:
169.254.169.253: DNS服务。
169.254.169.123: NTP服务。
更多关于169.254.169.254信息,可以参考whats-special-about-169-254-169-254-ip-address-for-aws。
OpenStack虚拟机也是通过http://169.254.169.254获取虚拟机的初始化配置信息:
从以上输出可见从metadata服务中我们获取了虚拟机的uuid、name、project id、availability_zone、hostname等。
虚拟机怎么通过访问169.254.169.254这个地址就可以获取Metadata信息呢,我们首先查看下虚拟机的路由表:
我们可以看到169.254.169.254的下一跳为10.0.0.66。10.0.0.66这个IP是什么呢?我们通过Neutron的port信息查看下:
可看到10.0.0.66正好是网络2c4b658c-f2a0-4a17-9ad2-c07e45e13a8a的dhcp地址,可以进一步验证:
由此,我们可以得出结论,OpenStack虚拟机访问169.254.169.254会路由到虚拟机所在网络的DHCP地址,DHCP地址与虚拟机IP肯定是可以互通的,从而解决了虚拟机内部到宿主机外部的通信问题。那DHCP又如何转发到Nova Metadata服务呢,下一节将介绍如何解决这个问题.
3.2 Metadata请求第一次转发
前面介绍了虚拟机访问Metadata服务地址169.254.169.254,然后转发到DHCP地址。我们知道Neutron的DHCP port被放到了namespace中,我们不妨进入到虚拟机所在网络的namespace:
首先查看该namespace的路由:
从路由表中看出169.254.0.0/16是从网卡tap1332271e-0d发出去的,我们查看网卡地址信息:
我们发现,169.254.169.254其实是配在网卡tap1332271e-0d的一个虚拟IP。虚拟机能够访问169.254.169.254这个地址也就不足为奇了。需要注意的是,本文的metadata转发配置是通过dhcp-agent实现的,如果是l3-agent,则169.254.169.254是通过iptables转发。
我们能够访问curl http://169.254.169.254,说明这个地址肯定开放了80端口:
从输出中看,所在的环境除了开启了DHCP服务(53端口),确实监听了80端口,进程pid为11334/haproxy。
我们看到haproxy这个进程就可以猜测是负责请求的代理与转发,即OpenStack虚拟机首先会把请求转发到DHCP所在namespace haproxy监听的端口80。
问题又来了,DHCP所在的namespace网络仍然和Nova Metadata是不通的,那haproxy如何转发请求到Nova Metadata服务呢,我们下一节介绍。
3.3 Metadata请求第二次转发
前面我们介绍了OpenStack虚拟机访问http://169.254.169.254会被转发到DHCP所在namespace的haproxy监听的80端口中。但是,namespace中仍然无法访问Nova Metadata服务。
为了研究解决办法,我们首先看下这个haproxy进程信息:
其中2c4b658c-f2a0-4a17-9ad2-c07e45e13a8a.conf配置文件部分内容如下:
我们发现haproxy绑定的端口为80,后端地址为一个文件/opt/stack/data/neutron/metadata_proxy。后端不是一个IP/TCP地址,那必然是一个UNIX Socket文件:
因此我们得出结论,haproxy进程会把OpenStack虚拟机Metadata请求转发到本地的一个socket文件中。
NIX Domain Socket是在socket架构上发展起来的用于同一台主机的进程间通讯(IPC),它不需要经过网络协议栈实现将应用层数据从一个进程拷贝到另一个进程,有点类似于Unix管道(pipeline)。
问题又来了:
我们从haproxy配置看,监听的地址是0.0.0.0:80,那如果有多个网络同时都监听80端口岂不是会出现端口冲突吗?
socket只能用于同一主机的进程间通信,如果Nova Metadata服务与Neutron dhcp-agent不在同一个主机,则显然还是无法通信。
第一个问题其实前面已经解决了,haproxy是在虚拟机所在网络的DHCP namespace中启动的,不同的namespace网络栈相互隔离,因此不会存在端口冲突。我们可以验证:
关于第二个问题,显然还需要一层转发,具体内容请看下一小节内容。
另外需要注意的是,新版本的OpenStack是直接使用haproxy代理转发的,在一些老版本中则使用neutron-ns-metadata-proxy进程负责转发,实现的代码位于neutron/agent/metadata/namespace_proxy.py:
大家可能对请求URL为169.254.169.254有疑问,怎么转发给自己呢? 这是因为这是一个UNIX Domain Socket请求,其实这个URL只是个参数占位,填什么都无所谓,这个请求相当于:
3.4 Metadata请求第三次转发
前面说到,haproxy会把Metadata请求转发到本地的一个socket文件中,那么,到底是哪个进程在监听/opt/stack/data/neutron/metadata_proxysocket文件呢?我们通过lsof查看下:
可见neutron-metadata-agent监听了这个socket文件,相当于haproxy把Metadata服务通过socket文件转发给了neutron-metadata-agent服务。
neutron-metadata-agent初始化代码如下:
进一步验证了neutron-metadata-agent监听了/opt/stack/data/neutron/metadata_proxysocket文件。
由于neutron-metadata-agent是控制节点上的进程,因此和Nova Metadata服务肯定是通的, OpenStack虚拟机如何访问Nova Metadata服务问题基本就解决了。
即一共需要三次转发。
但是Nova Metadata服务如何知道是哪个虚拟机发送过来的请求呢?换句话说,如何获取该虚拟机的uuid,我们将在下一章介绍。
04
Metadata服务如何获取虚拟机信息
前一章介绍了OpenStack虚拟机如何通过169.254.169.254到达Nova Metadata服务,那到达之后如何判断是哪个虚拟机发送过来的呢?
其实OpenStack是通过neutron-metadata-agent获取虚拟机的uuid的。我们知道,在同一个Neutron network中,即使有多个subnet,也不允许IP重复,即通过IP地址能够唯一确定Neutron的port信息。而neutron port会设置device_id标识消费者信息,对于虚拟机来说,即虚拟机的uuid。
因此neutron-metadata-agent通过network uuid以及虚拟机ip即可获取虚拟机的uuid。
不知道大家是否还记得在haproxy配置文件中存在一条配置项:
即haproxy转发之前会把network id添加到请求头部中,而IP可以通过HTTP的头部X-Forwarded-For中获取。因此neutron-metadata-agent具备获取虚拟机的uuid以及project id(租户id)条件,我们可以查看neutron-metadata-agent获取虚拟机uuid以及project id实现,代码位于neutron/agent/metadata/agent.py:
如果谁都可以伪造Metadata请求获取任何虚拟机的metadata信息,显然是不安全的,因此在转发给Nova Metadata服务之前,还需要发一个secret:
metadata_proxy_shared_secret需要管理员配置,然后组合虚拟机的uuid生成一个随机的字符串作为key。
最终,neutron-metadata-agent会把虚拟机信息放到头部中,发送到Nova Metadata服务的头部信息如下:
此时Nova Metadata就可以通过虚拟机的uuid查询metadata信息了,代码位于nova/api/metadata/base.py:
05
外部如何获取虚拟机metadata
前面已经介绍了OpenStack虚拟机从Nova Metadata服务获取metadata的过程。有时候我们可能需要调试虚拟机的metadata信息,验证传递的数据是否正确,而又嫌麻烦不希望进入虚拟机内部去调试。有什么方法能够直接调用nova-api-metadata服务获取虚拟机信息呢。
根据前面介绍的原理,我写了两个脚本实现:
第一个Python脚本sign_instance.py用于生成secret:
第二个bash脚本get_metadata.py实现获取虚拟机metadata:
其中metadata_server为Nova Metadata服务地址。用法如下:
通过如上脚本就可以轻松在外部获取任一虚拟机的metadata,对于虚拟机cloud-init初始化失败时调试特别方便。
05
总结
最后通过一张工作流图总结整个过程如下: