cloud-init是linux的一个工具,当云主机启动系统,cloud-init可从nova metadata服务或者config drive中获取metadata,用于初始化云主机的操作。
初始化的操作有如下
下载cloud-init
[root@nodes cloud-init]# pwd
/cloud-init
[root@nodes cloud-init]# wget http://ecs-image-utils.oss-cn-hangzhou.aliyuncs.com/cloudinit/ali-cloud-init-latest.tgz
解压缩
[root@nodes cloud-init]# tar -zxvf ali-cloud-init-latest.tgz
下载并配置epel源
[root@nodes cloud-init]# cd /etc/yum.repos.d/
[root@nodes yum.repos.d]# wget http://mirrors.163.com/.help/CentOS7-Base-163.repo
[root@nodes yum.repos.d]# yum install -y epel-release
#为了让epel源不报错,打开baseurl的注释,注释metalink
[root@nodes yum.repos.d]# sed -ri '/^#base/s;(#)(.*);\2;' epel.repo
[root@nodes yum.repos.d]# sed -ri '/^meta/s;^;#;' epel.repo
下载git、python、python-pip、python-devel,并升级pip
[root@nodes yum.repos.d]# yum -y install git python python-pip python-devel
[root@nodes yum.repos.d]# pip install --upgrade pip
安装需要的依赖库
[root@nodes yum.repos.d]# cd /cloud-init/cloud-init-0.7.6a12/
[root@nodes cloud-init-0.7.6a12]# pip install -r requirements.txt
[root@nodes cloud-init-0.7.6a12]# pip install --ignore-installed mycli
[root@nodes cloud-init-0.7.6a12]# pip install docker --ignore-installed chardet
进行tools目录,安装cloud-init
[root@nodes cloud-init-0.7.6a12]# cd tools/
[root@nodes tools]# bash ./deploy.sh centos 7
{
"status_code": 0,
"description": "success"
}
出现success表示安装成功
参考:
Generator | cloud-config.target | 读取配置文件cloud.cfg |
---|---|---|
Local | cloud-init-local.service | 定位“本地”数据源和配置网络 |
Network | cloud-init.service | 读取cloud_init_modules模块的指定配置 |
Config | cloud-config.service | 读取cloud_config_modules模块的指定配置 |
Final | cloud-final.service | 分别读取cloud_final_modules模块的指定配置 |
[root@node ~]# systemctl list-units|grep cloud-
cloud-config.service
cloud-final.service
cloud-init-local.service
cloud-init.service
cloud-config.target
[root@servera ~]# cat /etc/cloud/cloud.cfg
cloud_init_modules:
- migrator
- bootcmd
- write-files
- growpart
- resizefs
- set_hostname
- update_hostname
- update_etc_hosts
- rsyslog
- users-groups
- ssh
cloud_config_modules:
- mounts
- locale
- set-passwords
- yum-add-repo
- package-update-upgrade-install
- timezone
- puppet
- chef
- salt-minion
- mcollective
- disable-ec2-metadata
- runcmd
cloud_final_modules:
- rightscale_userdata
- scripts-per-once
- scripts-per-boot
- scripts-per-instance
- scripts-user
- ssh-authkey-fingerprints
- keys-to-console
- phone-home
- final-message
实现 instance 定制化,cloud-init(或 cloudbase-init)只是故事的一半,metadata service 则是故事的的另一半。两者的分工是:metadata service 为 cloud-init 提供自定义配置数据,cloud-init 完成配置工作。
nova-api-metadata 是 nova-api 的一个子服务,它是 metadata 的提供者,instance 可以通过 nova-api-metadata 的 REST API 来获取 metadata 信息。
nova-api-metadata 运行在控制节点上,服务端口是 8775。
[root@controller ~]# netstat -pautn|grep 8775
tcp 0 0 0.0.0.0:8775 0.0.0.0:* LISTEN 56357/python2
[root@controller ~]# ps aux|grep 56357
nova 56357 1.1 0.6 497336 40816 ? Ss 1月05 12:17 /usr/bin/python2 /usr/bin/nova-api
这里的环境是 devstack,nova-api-metadata 的程序名称就是 nova-api,nova-api-metadata 与常规的 nova-api 服务是合并在一起的。
在 OpenStack 的其他发行版中可能有单独的 nova-api-metadata 进程存在。
[root@controller ~]# cat /etc/nova/nova.conf |grep "^enabled_apis"
enabled_apis=osapi_compute,metadata
osapi_compute 是常规的 nova-api 服务,metadata 就是 nova-api-metadata 服务。
nova-api-metadata 在控制节点上,走 OpenStack 内部管理网络,instance 是无法通过 http://controller_ip:8775 直接访问 metadata service 的,因为网络不通。
那怎么办呢?
答案是:借助 neutron-metadata-agent。
neutron-metadata-agent是运行在网络节点上的,instance 先将 metadata 请求发给 neutron-metadata-agent,
neutron-metadata-agent 再将请求转发到 nova-api-metadata。
那instance 如何将请求发送到 neutron-metadata-agent?
实际上 instance 是不能直接与 neutron-metadata-agent 通信的,因为 neutron-metadata-agent 也是在 OpenStack 内部管理网络上的。
不过好在网络节点上有另外两个组件,dhcp agent 和 l3 agent,它们两兄弟与 instance 可以位于同一 OpenStack network 中,这样就引出了下一个组件: neutron-ns-metadata-proxy。
neutron-ns-metadata-proxy 是由 dhcp-agent 或者 l3-agent 创建的,它是运行在网络节点的 namespace 中。
如果由 dhcp-agent 创建,neutron-ns-metadata-proxy 就运行在 dhcp-agent 所在的 namespace 中。
如果由 l3-agent 创建,neutron-ns-metadata-proxy 就运行在 neutron router 所在的 namespace 中。
“neutron-ns-metadata-proxy” 中间的 ns就是 namespace 的意思。
neutron-ns-metadata-proxy 与 neutron-metadata-agent 通过 unix domain socket 直接相连。
所以整个流程是:
可能大家对于 neutron-ns-metadata-proxy 还会有些疑虑:
既然 dhcp-agent 和 l3-agent 都可以创建和管理 neutron-ns-metadata-proxy,使用的时候该如何选择呢?
简单的说:各有各的使用场景,并且两种方案可以共存。大家不用担心,下面会通过例子详细讨论。
网络 | D-Net |
---|---|
子网 | D-SubNet |
IP 地址分配池 | 开始 172.16.199.10 - 结束 172.16.199.100 |
网络类型 | vxlan |
段ID | 100 |
DHCP 已启用 | 是 |
额外路由 | 无 |
创建一个名为ServerA的云主机
[root@controller ~(keystone_admin)]# openstack server create --flavor web.os --image mywebos --nic net-id=b4e67658-a574-4d38-be7b-c97fa2f9a2bc --availability-zone cpu ServerA
查看ServerA的启动日志
[ 233.983114] cloud-init[769]: 2020-01-05 23:00:18,355 - url_helper.py[WARNING]: Calling 'http://169.254.169.254/2009-04-04/meta-data/instance-id' failed [114/120s]: request error [HTTPConnectionPool(host='169.254.169.254', port=80): Max retries exceeded with url: /2009-04-04/meta-data/instance-id (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x2178690>: Failed to establish a new connection: [Errno 101] Network is unreachable',))]
small image
localhost login:
可以看到出现最多次数的就是这一条信息
一直访问http://169.254.169.254/2009-04-04/meta-data/instance-id
结果都访问失败
169.254.169.254 是个什么地址?
这个地址来源于 AWS,当年亚马逊在设计公有云的时候,为了让 instance 能够访问 metadata,就将 169.254.169.254 这个特殊的 IP 作为 metadata 服务器的地址,instance 启动时就会向 169.254.169.254 请求 metadata。OpenStack 之后也沿用了这个设计。
small image的 cloud-init 显然是没有拿到 metadata 的,这点至少可以从 云主机的hostname没有被设置为ServerA判断出来。
nstance 首先会将 metadata 请求发送给 DHCP agent 或者 L3_agent 管理的 neutron-ns-metadata-proxy。
那目前到底是谁在管理 neutron-ns-metadata-proxy 呢?
先去控制节点上查看一下 neutron-ns-metadata-proxy 的进程。
[root@controller ~]# ps aux|grep metadata-proxy
root 25522 0.0 0.0 112728 984 pts/1 R+ 16:12 0:00 grep --color=auto metadata-proxy
发现没有 neutron-ns-metadata-proxy 在运行!
其原因是
默认配置下,neutron-ns-metadata-proxy 是由 L3_agent 管理的(后面的实验会来做一个让 DHCP 来管理),由于当前 D-Net并没有挂在 Neutron Router 上,所以没有启动 neutron-ns-metadata-proxy。
在路由器上增加D-Net的子网接口
现在可以看到在路由器上的neutron-ns-metadata-proxy了
[root@controller ~]# ps aux|grep ns-metadata
root 27213 0.0 0.0 112728 988 pts/1 R+ 16:24 0:00 grep --color=auto ns-metadata
neutron 116148 0.0 0.0 47960 876 ? Ss 11:12 0:00 haproxy -f /var/lib/neutron/ns-metadata-proxy/56099925-103a-4948-a10d-c554fbd03bf9.conf
重启云主机看看它会发生怎样的变化(如果没变化,重启相关neutron服务)
Red Hat Enterprise Linux Server 7.3 (Maipo)
Kernel 3.10.0-514.10.2.el7.x86_64 on an x86_64
small image
servera login:
从登录提示信息来看主机名不再是localhost了,是servera
查看本机ip地址
[root@servera ~]# ifconfig eth0| head -2|tail -1|awk '{print $2}'
172.16.199.83
serverA访问169.254.169.254测试测试
[root@servera ~]# curl http://169.254.169.254
1.0
2007-01-19
2007-03-01
2007-08-29
2007-10-10
2007-12-15
2008-02-01
2008-09-01
2009-04-04
这里可以直接拿到 metadata。但我们知道 nova-api-metadata 是运行在控制节点上的;
IP并不是
169.254.169.254
,这是怎么实现的呢?下面我们分析一下这个过程。
查看路由表
[root@servera ~]# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.16.199.1 0.0.0.0 UG 100 0 0 eth0
169.254.169.254 172.16.199.1 255.255.255.255 UGH 100 0 0 eth0
172.16.199.0 0.0.0.0 255.255.255.0 U 100 0 0 eth0
查看路由表得知,访问
169.254.169.254
的请求会走172.16.199.1
。
查看网络拓扑图
172.16.199.1
就是VGate
在D-Net
上的 interface IP。这条路由是 OpenStack 自动添加到 serverA中的,这样就将 metadata 的请求转发到Neutron router。
查看VGate路由命名空间的iptables信息
[root@controller ~(keystone_admin)]# ip netns exec qrouter-56099925-103a-4948-a10d-c554fbd03bf9 iptables -t nat -L
Chain neutron-l3-agent-PREROUTING (1 references)
target prot opt source destination
REDIRECT tcp -- anywhere 169.254.169.254 tcp dpt:http redir ports 9697
如果访问目标是169.254.169.254那么就重定向到本机的9697端口
9697是什么?
[root@controller ~(keystone_admin)]# ip netns exec qrouter-56099925-103a-4948-a10d-c554fbd03bf9 bash
[root@controller ~(keystone_admin)]# netstat -npatu
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:9697 0.0.0.0:* LISTEN 116148/haproxy
[root@controller ~(keystone_admin)]# ps aux |grep 116148
neutron 116148 0.0 0.0 47960 1080 ? Ss 11:12 0:00 haproxy -f /var/lib/neutron/ns-metadata-proxy/56099925-103a-4948-a10d-c554fbd03bf9.conf
这是 neutron-ns-metadata-proxy 的监听端口。
169.254.169.254
请求 metadata。编辑 /etc/neutron/dhcp_agent.ini,设置 force_metadata
[root@controller neutron]# cat /etc/neutron/dhcp_agent.ini |grep "force_"
force_metadata=True
重启 dhcp-agent
[root@controller ~]# systemctl restart neutron-dhcp-agent.service
查看neutron-ns-metadata-proxy
#查看网络
[root@controller neutron(keystone_admin)]# openstack network list
+--------------------------------------+-------+--------------------------------------+
| ID | Name | Subnets |
+--------------------------------------+-------+--------------------------------------+
| 36c26924-d000-4f2b-a15f-86561045a67c | pub | 7dd4fa3c-2025-422d-ad49-0c586e7a6f7e |
| b4e67658-a574-4d38-be7b-c97fa2f9a2bc | D-Net | 72b1c3b9-7b96-42c7-b72f-ca7e42d18fea |
+--------------------------------------+-------+--------------------------------------+
#查看路由
[root@controller neutron(keystone_admin)]# openstack router list |awk '{print $2}'
ID
56099925-103a-4948-a10d-c554fbd03bf9
#查看metadata-proxy进程
[root@controller neutron(keystone_admin)]# ps -e -o cmd|grep metadata-proxy
haproxy -f /var/lib/neutron/ns-metadata-proxy/36c26924-d000-4f2b-a15f-86561045a67c.conf
haproxy -f /var/lib/neutron/ns-metadata-proxy/b4e67658-a574-4d38-be7b-c97fa2f9a2bc.conf
haproxy -f /var/lib/neutron/ns-metadata-proxy/56099925-103a-4948-a10d-c554fbd03bf9.conf
从上面可以看到每一个ns-metadata-proxy都对应着一个网络
连接到某个网络的云主机都是通过对应的ns-metadata-proxy进程获取metadata
访问169.254.169.25
[root@servera ~]# curl http://169.254.169.254
1.0
2007-01-19
2007-03-01
2007-08-29
2007-10-10
2007-12-15
2008-02-01
2008-09-01
2009-04-04
查看路由表
[root@servera ~]# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.16.199.1 0.0.0.0 UG 100 0 0 eth0
169.254.169.254 172.16.199.10 255.255.255.255 UGH 100 0 0 eth0
172.16.199.0 0.0.0.0 255.255.255.0 U 100 0 0 eth0
查看得知,现在访问
169.254.169.254
的路由已由之前的 172.16.199.1变为 172.16.199.10。这里的 172.16.199.10 是 dhcp-agent 在D-Net上的 IP,这条路由是由 dhcp-agent 添加进去的。
现在l3-agent 与 dhcp-agent 同时提供 neutron-ns-metadata-proxy 服务,metadata 请求也只会发送给 dhcp-agent。
查看D-SubNet的dhcp命名空间
[root@controller ~]# ip netns exec qdhcp-b4e67658-a574-4d38-be7b-c97fa2f9a2bc ip add |grep "inet "
inet 127.0.0.1/8 scope host lo
inet 172.16.199.10/24 brd 172.16.199.255 scope global tap4ad8306c-7a
inet 169.254.169.254/16 brd 169.254.255.255 scope global tap4ad8306c-7a
[root@controller ~]# ip netns exec qdhcp-b4e67658-a574-4d38-be7b-c97fa2f9a2bc netstat -tlun|grep tcp 0 0 169.254.169.254:80 0.0.0.0:* LISTEN 40589/haproxy
tcp 0 0 169.254.169.254:53 0.0.0.0:* LISTEN 40587/dnsmasq
udp 0 0 169.254.169.254:53 0.0.0.0:* 40587/dnsmasq
从上面可以看到,dhcp-agent 已经将 IP
169.254.169.254
配置到了自己身上。也就是说:
ServerA
访问 metadata 的请求 http://169.254.169.254 实际上是发送到了 dhcp-agent 的 80 端口。而监听 80 端口的正是 dhcp-agent 启动的 neutron-ns-metadata-proxy 进程。
云主机访问169.254.169.254时,数据包走到网关(自己所在dhcp命名空间),然后neutron-ns-metadata-proxy 将请求通过 unix domain socket 发给 neutron-metadata-agent,后者再通过管理网络发给 nova-api-metadata。
到这里,我们已经分别讨论了通过 l3-agent 和 dhcp-agent 访问 metadata 的实现方法。对于 169.254.169.254
:
那么现在有这样一个疑问:nova-api-metadata 是怎么知道应该返回哪个 instance 的 metadata?
ServerA
只是向 169.254.169.254
发送了一个 http 请求;
nova-api-metadata 怎么就知道应该返回 ServerA
的 metadata 呢?
下面我们来详细分析这个问题。
要想从 nova-api-metadata 获得 metadata,需要指定 instance 的 id。
但 instance 刚启动时无法知道自己的 id,所以 http 请求中不会有 instance id 信息,id 是由 neutron-metadata-agent 添加进去的。
针对 l3-agent 和 dhcp-agent 这两种情况在实现细节上有所不同,下面分别讨论。
大致的流程为:
**instance -> neutron-ns-metadata-proxy -> neutron-metadata-agent -> nova-api-metadata**
处理细节说明如下:
① neutron-ns-metadata-proxy 接收到请求,在转发给 neutron-metadata-agent 之前会将 instance ip 和 router id 添加到 http 请求的 head 中,这两个信息对于 l3-agent 来说很容易获得。
② neutron-metadata-agent 接收到请求后,会查询 instance 的 id,具体做法是:
1)通过 router id 找到 router 连接的所有 subnet,然后筛选出 instance ip 所在的 subnet。
2)在 subnet 中找到 instance ip 对应的 port。
3)通过 port 找到对应的 instance 及其 id。
③ neutron-metadata-agent 将 instance id 添加到 http 请求的 head 中,然后转发给 nova-api-metadata,这样 nova-api-metadata 就能返回指定 instance 的 metadata 了。
① neutron-ns-metadata-proxy 在转发请求之前会将 instance ip 和 network id 添加到 http 请求的 head 中,这两个信息对于 dhcp-agent 来说很容易获得。
② neutron-metadata-agent 接收到请求后,会查询 instance 的 id,具体做法是:
1)通过 network id 找到 network 所有的 subnet,然后筛选出 instance ip 所在的 subnet。
2)在 subnet 中找到 instance ip 对应的 port。
3)通过 port 找到对应的 instance 及其 id。
③ neutron-metadata-agent 将 instance id 添加到 http 请求的 head 中,然后转发给 nova-api-metadata,这样 nova-api-metadata 就能返回指定 instance 的 metadata 了。
无论云主机将请求发给 l3-agent 还是 dhcp-agent,nova-api-metadata 最终都能获知 instance 的 id,进而返回正确的 metadata。
从获取 metadata 的流程上看,有一步是至关重要的:instance 必须首先能够正确获取 DHCP IP,否则请求发送不到 169.254.169.254
。
但不是所有环境都会启用 dhcp,更极端的,有些环境可能连 nova-api-metadata 服务都不会启用。
那么 instance 还能获得 metadata 吗?
这就是下面要讨论的主题:config drive。
config-driver就是在没有dhcp来获取ip地址的场景下,云主机也能获取metadata进行初始化操作
有两种方法可以启用config drive:
--config-drive true
(下面实验是采用的这种方法)。force_config_drive = true
,这样部署到此计算节点的 instance 都会使用 config drive。我 config drive 支持两种格式,iso9660 和 vfat,默认是 iso9660,但这会导致 instance 无法在线迁移,必须设置成config_drive_format=vfat
才能在线迁移,这一点需要注意。
配置完成后,重启 nova-compute 服务。
编辑/etc/nova/nova.conf,设置flat_injected
flat_injected 的作用是让 config drive 能够在 instance 启动时将网络配置信息动态注入到操作系统中。
[root@computer ~]# sed -ri '/^#flat_injected/s;(#)(.*=)(.*);\2True;' /etc/nova/nova.conf
[root@computer ~]# systemctl restart openstack-nova-compute.service
编辑user-data被始化脚本
[root@controller ~]# cat /tmp/init.sh
#!/bin/bash
bash -xc "whoami"
bash -xc 'echo 123456 | passwd --stdin root'
bash -xc 'useradd -o -u 0 sa && passwd -d sa'
bash -xc 'sed -i "/^SELINUX/s/enforcing/disabled/g" /etc/selinux/config'
bash -xc 'mkdir /cloud-init && touch /cloud-init/init.md'
bash -xc 'hostnamectl set-hostname ServerA'
创建云主机时指定--config-drive
[root@controller ~(keystone_admin)]# openstack server create --flavor web.os --image mywebos --nic net-id=b4e67658-a574-4d38-be7b-c97fa2f9a2bc --user-data /tmp/init.sh --config-drive True --availability-zone cpu ServerA
在控制台查看启动时的日志消息
#正在执行初始化脚本
Password: [ 149.885277] cloud-init[1011]: + whoami
[ 150.094666] cloud-init[1011]: root
[ 150.264552] cloud-init[1011]: + passwd --stdin root
[ 150.283743] cloud-init[1011]: + echo 123456
[ 151.028468] cloud-init[1011]: Changing password for user root.
[ 151.034823] cloud-init[1011]: passwd: all authentication tokens updated successfully.
[ 151.115120] cloud-init[1011]: + useradd -o -u 0 sa
[ 151.385270] cloud-init[1011]: + passwd -d sa
[ 151.600198] cloud-init[1011]: Removing password for user sa.
[ 151.625189] cloud-init[1011]: passwd: Success
[ 151.679793] cloud-init[1011]: + sed -i '/^SELINUX/s/enforcing/disabled/g' /etc/selinux/config
[ 151.798403] cloud-init[1011]: + mkdir /cloud-init
[ 151.854698] cloud-init[1011]: + touch /cloud-init/init.md
[ 151.943774] cloud-init[1011]: + hostnamectl set-hostname ServerA
[ 154.591424] cloud-init[1011]: 2020-01-07 03:31:50,228 - templater.py[WARNING]: Cheetah not available as the default renderer for unknown template, reverting to the basic renderer.
[ 154.635910] cloud-init[1011]: Cloud-init v. 0.7.6 finished at Tue, 07 Jan 2020 08:31:50 +0000. Datasource DataSourceConfigDriveNet [net,ver=2][source=/dev/sr0]. Up 154.49 seconds
login: timed out after 60 seconds
根据上面最后一条日志信息,挂载设备上去以后查看一下,发现与之前用curl访问169.254.169.254目录内容类似
[root@servera ~]# mount /dev/sr0 /media/
[root@servera ~]# ls /media/
ec2 openstack
[root@servera ~]# ls /media/openstack/
2012-08-10 2013-10-17 2016-06-30 2017-02-22 content
2013-04-04 2015-10-15 2016-10-06 2018-08-27 latest
查看网络配置
[root@servera latest]# pwd
/media/openstack/latest
[root@servera latest]# cat network_data.json |python -m json.tool
{
"links": [
{
"ethernet_mac_address": "fa:16:3e:b4:79:12",
"id": "tap11477c6c-f4",
"mtu": 1450,
"type": "ovs",
"vif_id": "11477c6c-f4ef-434f-a38a-27d6ce9e225a"
}
],
"networks": [
{
"id": "network0",
"ip_address": "172.16.199.26",
"link": "tap11477c6c-f4",
"netmask": "255.255.255.0",
"network_id": "b4e67658-a574-4d38-be7b-c97fa2f9a2bc",
"routes": [
{
"gateway": "172.16.199.1",
"netmask": "0.0.0.0",
"network": "0.0.0.0"
}
],
"services": [],
"type": "ipv4"
}
],
"services": []
}
自己手动配置上ip地址
[root@servera latest]# ifconfig eth0 172.16.199.26 netmask 255.255.255.0
[root@servera latest]# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.16.199.26 netmask 255.255.255.0 broadcast 172.16.199.255
查看user_data文件
[root@servera ~]# cat /media/openstack/latest/user_data
#!/bin/bash
bash -xc "whoami"
bash -xc 'echo 123456 | passwd --stdin root'
bash -xc 'useradd -o -u 0 sa && passwd -d sa'
bash -xc 'sed -i "/^SELINUX/s/enforcing/disabled/g" /etc/selinux/config'
bash -xc 'mkdir /cloud-init && touch /cloud-init/init.md'
bash -xc 'hostnamectl set-hostname ServerA'
发现所有配置都均以生效
[root@servera ~]# hostname
servera
[root@servera ~]# id sa
uid=0(root) gid=0(root) groups=0(root)
[root@servera ~]# ls /cloud-init/
init.md
[root@servera ~]# cat /etc/selinux/config |grep "^SE"
SELINUX=disabled
SELINUXTYPE=targeted
https://yq.aliyun.com/articles/311490?spm=a2c4e.11153940.0.0.596c6aeemEZj3h
https://yq.aliyun.com/articles/311485?spm=a2c4e.11153940.0.0.5f7e200fshe0pU
https://cloud.tencent.com/developer/article/1501295
https://blog.csdn.net/onlyshenmin/article/details/81081786
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。