Cloud Init
目前大部分公有云(openstack, AWS, Aliyun)都在使用 cloud-init, 已经成为虚拟机元数据管理和 OS 系统配置初始化的事实标准.最早 cloud-init 由 ubuntu 的母公司 Canonical 开发。主要思想是当用户首次创建虚拟机时,将前台设置的主机名,密码或者秘钥等存入 metadata server(顾名思义,存放元数据的服务器)。在 openstack 环境下当 cloud-init 随虚拟机启动而运行时,通过 http 协议访问 metadata server,获取这些信息并修改主机配置。完成系统的环境初始化。本文以 openstack + centos7 + cloud-init 0.7.9 为例,分两篇介绍基本概念,工作原理和源码解读。
如下是一些 cloud-init 的关键信息: 源码: https://github.com/cloud-init/cloud-init 文档: https://cloudinit.readthedocs.io/en/latest/ 配置文件: /etc/cloud/cloud.cfg 日志:/var/log/cloud-init.log 存放关键数据的目录: /var/lib/cloud/
基本概念和相关文件目录
datasources
cloud-init 将 openstack, AWS, Aliyun 等众多云平台抽象成数据源,使用统一的接口适配所有平台。 具体地,openstack 下获取数据的方法是访问http://169.254.169.254 下的 userdata 和 metadata
userdata
可以是文件,shell 脚本或者 cloud-init 配置文件。租户可以在前台输入。 具体的格式,请参考 https://cloudinit.readthedocs.io/en/latest/topics/format.html 如下是一个 shell 脚本的 userdata, openstack 下用它来初始化 root 用户的密码
[root@ecs-test-wangbo log]# curl http://169.254.169.254/openstack/2015-10-15/user_data
#!/bin/bash
echo 'root:$6$O9wyDQ$LYGAz6V/dy66Ve8eJkeATAbXOwjkWGpLbr4QoxkH8iQ0nsLa7.n3lzSlOer7Okb2RD8FObkP3RRPHEKS2xGip0' | chpasswd -e;
metadata#
包含主机名,实例 id 和其他服务器相关的信息
[root@ecs-test-wangbo log]# curl http://169.254.169.254/openstack/2015-10-15/meta_data.json | python -m json.tool
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1297 100 1297 0 0 2452 0 --:--:-- --:--:-- --:--:-- 2456
{
"availability_zone": "cn-east-2a",
"hostname": "ecs-test-wangbo.novalocal",
"launch_index": 0,
"meta": {
"charging_mode": "0",
"image_name": "CentOS 7.3 64bit",
"metering.cloudServiceType": "hws.service.type.ec2",
"metering.image_id": "643d831d-a69c-433f-803f-065e0e2e2911",
"metering.imagetype": "gold",
"metering.resourcespeccode": "c1.medium.linux",
"metering.resourcetype": "1",
"os_bit": "64",
"os_type": "Linux",
"vpc_id": "8428b86b-7ec1-4b31-97aa-d6a0db2c3aab"
},
"name": "ecs-test-wangbo",
"project_id": "9b4e13cdeb7c4c6ebb929a4a503e51a2",
"random_seed": "zUPkPLRFdLufLlYMnqQcrcp/nob7nJecOH/Nsv+hjDcNz1oYLvW/6qHgbL/++Kqvt0DLbkgpxRCO+lnqZfdOMadnZUHabdzB5LjEqOBFnk22RewmAh2sITOD1QPqmsvEssAOlz39FrUtK7+0XHIFclwYZIk8XtXPQa9L1lM9v76Y3+cZI18m1V0E+He1qLQzfyfu4qYYc8ER4YcGS2T2L/cZIRT9o20vwrLO7Ut2d98uitHhUphfVMVANG2DkZDK3U4DgR4M8q7jFMl5a3oR370XVYS7XqAn8YopCFXH1wAKCzipbzVL+JXiq9W6xFNvDvnK+bKdMVHf7ge+zUmRG6LnPpBqFLUT03qyhrErTLrV6mjpw3u4+uAX/Okn8NoWB0eEqly/VSgR3eAF4v/Ga2GylUSRCXcHH5Ss0RvbcXmF0NeGcMKH1QueBd9wBRxygBJsXZS0ZKfd+T13cA22TwqWLw4UFj3gu08/NE51A9sZu9quM6HL+XWOdqcOtN1T3aJVp6w2uisAfJCNEVq7Ftr2k0SsJdVHWw6dseDc3kOCC0Q+JPZh8deGF28/v+dykZH7yyHhFZCQZqHGarrd+QPwTzc3mdzzV/BL+GeNbYXKQXf6Uibv5s9usI6QwF+sfOg6AKtyUICWTDGgKRL1FUaDR3zF4sGIftO0AZ49vhI=",
"uuid": "e9f15094-8157-4c78-96a1-674cbaf26baf"
}
stage
cloud-init 对系统的配置分为四个阶段, 内部叫 stage。 分别是 local, network,config, final
modules
具体的定制化配置是由模块完成。每个模块根据获取的元数据和配置文件完成相关配置 cloud.cfg 里的部分配置:
cloud_init_modules:// 定义init阶段需要执行的模块
-migrator // 迁移老的cloud-init数据为新的
-bootcmd // 启动时执行相关命令
-write-files // 根据cloud.cfg的配置写数据到文件里
-growpart // 扩展分区到硬盘的大小 ,默认对根分区执行。 它需要调用“growpart”或者”gpart“
-resizefs // resize文件系统,适配新的大小。 默认对根目录执行
-set_hostname // 根据元数据设置主机名
-update_hostname // 更新主机名,适用于当用户自定义主机名时
-update_etc_hosts
-rsyslog
-users-groups // 根据cloud.cfg的配置创建用户组和用户
-ssh // 配置sshd
handlers
用于具体处理 userdata.目前有四类默认的 handler: boot hook, cloud config ,shell script, upstart job
frequencies
handler/module 的运行频率, 目前有三个有效值: once-per-instance always once
/var/lib/cloud/目录解读
该目录主要保存元数据和其他一些运行时需要的信息
/var/lib/cloud/data 文件夹存放具体的数据源,主机名和实例 ID result.json 记录了一些数据源信息 status.json 记录了每个 stage 运行的开始时间,结束时间和遇到的 error
/var/lib/cloud/data
├── instance-id
├── previous-datasource
├── previous-hostname
├── previous-instance-id
├── result.json
└── status.json
/var/lib/cloud/instance 存放元数据和其他一些缓存文件
/var/lib/cloud/instance
├── boot-finished // 记录cloud-ini运行完的时间
├── cloud-config.txt
├── datasource
├── handlers
├── obj.pkl // 缓存文件
├── scripts
│ └── part-001 // userdata为shell脚本,解析后归档到此
├── sem // 该目录用来存放各模块执行时的锁文件
├── user-data.txt // 从数据源获取的user-data
├── user-data.txt.i
├── vendor-data.txt
└── vendor-data.txt.i
工作原理
前面提到 cloudinit 分四个阶段执行,具体它们以服务的形式注册到系统中按如下顺序依次运行: local - cloud-init-local.service nework - cloud-init.service config - cloud-config.service final - cloud-final.service 每个具体的服务中对应的命令如下,都只运行一次,没有常驻进程 可以看到执行的都是 /usr/bin/cloud-init 这个文件,但参数不一样 所有 debug 日志全部默认输出到/var/log/cloud-init.log
[root@ecs-test-wangbo ~]# grep ExecStart /lib/systemd/system/cloud-*.service
/lib/systemd/system/cloud-config.service:ExecStart=/usr/bin/cloud-init modules --mode=config
/lib/systemd/system/cloud-final.service:ExecStart=/usr/bin/cloud-init modules --mode=final
/lib/systemd/system/cloud-init-local.service:ExecStart=/usr/bin/cloud-init init --local
/lib/systemd/system/cloud-init-local.service:ExecStart=/bin/touch /run/cloud-init/network-config-ready
/lib/systemd/system/cloud-init.service:ExecStart=/usr/bin/cloud-init init
[root@ecs-test-wangbo ~]#
[root@ecs-test-wangbo ~]# grep "Cloud-init v. 0.7.9 running" /var/log/cloud-init.log
2017-10-18 09:57:50,309 - util.py[DEBUG]: Cloud-init v. 0.7.9 running 'init-local' at Wed, 18 Oct 2017 01:57:50 +0000. Up 9.28 seconds.
2017-10-18 09:57:56,036 - util.py[DEBUG]: Cloud-init v. 0.7.9 running 'init' at Wed, 18 Oct 2017 01:57:56 +0000. Up 15.03 seconds.
2017-10-18 09:58:43,771 - util.py[DEBUG]: Cloud-init v. 0.7.9 running 'modules:config' at Wed, 18 Oct 2017 01:58:43 +0000. Up 62.72 seconds.
2017-10-18 09:58:44,165 - util.py[DEBUG]: Cloud-init v. 0.7.9 running 'modules:final' at Wed, 18 Oct 2017 01:58:44 +0000. Up 63.11 seconds.
[root@ecs-test-wangbo ~]#
local 阶段
此时 instance 尝试从 ConfigDrive 等本地源获取信息。在 openstack 环境下是不存在的,然后 cloud-init 检查系统是否有默认网卡,有则配置为 dhcp, 并写入 /etc/sysconfig/network-scripts/ifcfg-eth0. 只有配置了 dhcp, 该网卡有了 ip, 才能进一步连接 metadataserver 获取元数据 因为使用了 dhcp, 所有也会从 dhcp server 获取 dns 配置并写入/etc/resolv.conf。 这个和 vpc 里配置的 dns 服务器是一致的
2017-10-18 09:57:50,500 - main.py[DEBUG]: No local datasource found
2017-10-18 09:57:50,500 - util.py[DEBUG]: Reading from /sys/class/net/eth0/carrier (quiet=False)
2017-10-18 09:57:50,500 - util.py[DEBUG]: Reading from /sys/class/net/eth0/dormant (quiet=False)
2017-10-18 09:57:50,500 - util.py[DEBUG]: Reading from /sys/class/net/eth0/operstate (quiet=False)
2017-10-18 09:57:50,500 - util.py[DEBUG]: Read 5 bytes from /sys/class/net/eth0/operstate
2017-10-18 09:57:50,500 - util.py[DEBUG]: Reading from /sys/class/net/eth0/address (quiet=False)
2017-10-18 09:57:50,501 - util.py[DEBUG]: Read 18 bytes from /sys/class/net/eth0/address
2017-10-18 09:57:50,501 - stages.py[DEBUG]: applying net config names for {'version': 1, 'config': [{'subnets': [{'type': 'dhcp'}], 'type': 'physical', 'name': 'eth0', 'mac_address': 'fa:16:3e:b0:e3:5d'}]}
network 阶段#
此时 instance 已经有自己的 ip, 然后搜索所有网路源如下
2017-10-18 09:57:56,102 - __init__.py[DEBUG]: Searching for network data source in: [u'DataSourceNoCloudNet', u'DataSourceAzureNet', u'DataSourceAltCloud', u'DataSourceOVFNet', u'DataSourceMAAS', u'DataSourceGCE', u'DataSourceOpenStack', u'DataSourceEc2', u'DataSourceCloudStack', u'DataSourceBigstep', u'DataSourceNone']
最终通过访问 169.254.169.254 成功获取 openstack 下的数据,
2017-10-18 09:58:31,288 - __init__.py[DEBUG]: Seeing if we can get any data from <class 'cloudinit.sources.DataSourceOpenStack.DataSourceOpenStack'>
2017-10-18 09:58:31,289 - url_helper.py[DEBUG]: [0/1] open 'http://169.254.169.254/openstack' with {'url': 'http://169.254.169.254/openstack', 'headers': {'User-Agent': 'Cloud-Init/0.7.9'}, 'allow_redirects': True, 'method': 'GET', 'timeout': 10.0} configuration
2017-10-18 09:58:31,952 - url_helper.py[DEBUG]: Read from http://169.254.169.254/openstack (200, 50b) after 1 attempts
2017-10-18 09:58:31,952 - DataSourceOpenStack.py[DEBUG]: Using metadata source: 'http://169.254.169.254
下面日志表明抓取数据成功,并写入 /var/lib/cloud/instances/e9f15094-8157-4c78-96a1-674cbaf26baf
2017-10-18 09:58:43,120 - util.py[DEBUG]: Crawl of openstack metadata service took 11.168 seconds
其他步骤如下:
- 解析 userdata,并执行
- 按 cloud.cfg 里的配置顺序,依次运行各模块
config 阶段#
执行一些配置模块。
final 阶段#
此时大部分定制化已经完成, 这里只是一些简单的收尾工作 比如 final-message 模块,只是在日志里打印 cloud-init 启动结束
2017-10-18 09:58:44,236 - handlers.py[DEBUG]: start: modules-final/config-final-message: running config-final-message with frequency always
2017-10-18 09:58:44,236 - helpers.py[DEBUG]: Running config-final-message using lock (<cloudinit.helpers.DummyLock object at 0x1c81750>)
2017-10-18 09:58:44,236 - util.py[DEBUG]: Reading from /proc/uptime (quiet=False)
2017-10-18 09:58:44,236 - util.py[DEBUG]: Read 12 bytes from /proc/uptime
2017-10-18 09:58:44,240 - util.py[DEBUG]: Cloud-init v. 0.7.9 finished at Wed, 18 Oct 2017 01:58:44 +0000. Datasource DataSourceOpenStack [net,ver=2]. Up 63.25 seconds
2017-10-18 09:58:44,240 - util.py[DEBUG]: Writing to /var/lib/cloud/instance/boot-finished - wb: [420] 51 bytes
2017-10-18 09:58:44,241 - handlers.py[DEBUG]: finish: modules-final/config-final-message: SUCCESS: config-final-message ran successfully
cloud-init 源码结构
大部分代码存放于 /lib/python2.7/site-packages/cloudinit
├── cmd // 所有命令的主入口
├── config // 各种模块文件
├── distros // 各OS具体操作实现(比如安装软件,写文件)
│ └── parsers
├── filters // 日志相关的过滤
├── handlers // 处理userdata的具体实现
├── mergers // 辅助函数
├── net // 网络配置的通用操作
├── reporting // 通用的类,用于报告各种事件
└── sources // openstack, aliyun等数据源的类实现
└── helpers
└── vmware
└── imc
运行的主入口
local, network, config, final 三个不同阶段通过不同的参数,传递给主程序.比如 local 的具体命令行为 /usr/bin/cloud-init init --local.首先主入口是 cmd/main.py, 解析命令行参数
defmain(sysv_args=None):
if sysv_args isnotNone:
parser = argparse.ArgumentParser(prog=sysv_args[0])
sysv_args = sysv_args[1:]
else:
parser = argparse.ArgumentParser()
local, init 会走入 main_init这个函数, local 主要是寻找本地源(比如 configdriver), init 阶段是寻找网络源(比如通过 http 消息获取 metadata)
当前华为云的裸金属镜像使用 configdriver 这种方式,原理如下:
物理机启动 minios, 下载镜像和 metadata 给一块硬盘分区,将镜像 dd 写入第一分区 在该硬盘的最后位置,生成一个分区,写入 metadata minios 重启系统,让物理机从新的硬盘引导 OS 启动后,里面的 cloud-init 会挂载分区 /dev/sr0 类似这样的 mount 后将读取普通文件一样获取 metadata
main_init主要做的事情如下:
- 读取配置文件
- 初始化日志信息
- 根据配置初始化运行时相关的目录和权限
- 如果是寻找网络源的过程,检查是否已经存在信息,有则提前退出,无则继续第五步
- 根据当前 Cloud-init 所支持的 datasource 列表, 逐个搜索,看是否可以获取元数据。 所有全部数据源都检查后没有找到数据且命令行没有设置
--force, cloud-init 会提前推出, 否则继续第六步。 - 配置网络, local 阶段会自动设置 eth0 为 dhcp 模式,用自动获取 ip, 这样在 init 阶段(有时也叫 network)时网络源才能正常工作。
- 如果元数据里有 userdata, 则程序开始解析并运行
- 重读配置文件,获取该阶段需要运行的模块
- 根据待运行的模块重新配置日志输出
- 执行 8 步获取的模块列表
常见模板介绍
config 文件下包含所有模块,通过名字很容易识别其对应的功能。 比如 cc_set_hostname.py 用于创建虚拟机时设置主机名。 ``cc_update_hostname.py`用于每次启动时更新主机名。 模块都根据对应的配置项执行, 同时每个模块有自己固定的运行频率(per isntance, per always 等)
cc_set_hostname.py
只在创建虚拟机时运行一次, 如果 perserve_hostname 为 false, 则模块不运行。 从 metadata 里提取 hostname, 然后运行对应 OS 下的设置主机名命令
cc_update_hostname.py
每次虚拟机重启都会运行一次(包括第一次新建虚拟机), 如果 perserve_hostname 为 true, 则模块不运行。
- 首先检查是否存在/var/lib/cloud/data/previous-hostname 文件,有则对比当前 OS 的主机名, 如果不一样认为管理员维护主机名。提前退出
- 发现当前元数据和当前 OS 的主机名不一样,则直接更新
- 将最新的主机名写入 previous-hostname
cc_growpart.py
每次虚拟机重启都会运行一次(包括第一次新建虚拟机), 它会调整分区, 实现自动扩容,默认对根盘所有的虚拟磁盘执行。若要正常工作,还需要安装 cloud-utils-growpart 等辅助软件包
cc_resizefs.py
每次虚拟机重启都会运行一次(包括第一次新建虚拟机), 它主要配置 growpart, 对文件系统扩容。 growpart 主要针对磁盘。 不同的文件系统,调用不同的命令
def_resize_btrfs(mount_point, devpth):
return('btrfs','filesystem','resize','max', mount_point)
def_resize_ext(mount_point, devpth):
return('resize2fs', devpth)
def_resize_xfs(mount_point, devpth):
return('xfs_growfs', devpth)
def_resize_ufs(mount_point, devpth):
return('growfs', devpth)
# Do not use a dictionary as these commands should be able to be used
# for multiple filesystem types if possible, e.g. one command for
# ext2, ext3 and ext4.
RESIZE_FS_PREFIXES_CMDS =[
('btrfs', _resize_btrfs),
('ext', _resize_ext),
('xfs', _resize_xfs),
('ufs', _resize_ufs),
]
数据源实现
sources下面是每个数据源的具体实现,openstack, aliyun 这些都继承 __init__.py中的元类 DataSource. 这个 metaclass 实现了一些通用的操作。
openstack 中通过访问 http://169.254.169.254获取信息
aliyun 通过 http://100.100.100.200获取
configdirve 查找 /dev/sr0,/dev/sr1,/dev/cd0,/dev/cd1等设备,有则 mount 后访问文件
配置文件
cloud-init 采用 yaml 格式的文件。yaml 格式的具体说明参见 http://www.ruanyifeng.com/blog/2016/07/yaml.html 特别注意:yaml 不支持 tab 键,支持多个空格,但相同层级的元素左侧对齐。 cloud-init 对布尔有特殊处理。 如下, true, 1, on, yes 均认为是 true
TRUE_STRINGS =('true','1','on','yes')
FALSE_STRINGS =('off','0','no','false')
Reference
-
No backlinks found.