3.1.3 Provisioning API
到目前为止,根据我们所介绍的步骤,可以假设每个服务器和交换机都已启动并运行,但是仍然需要做一些工作为配置栈中的下一层准备裸金属集群,本质上是在图 11 所示的混合云的左右两边之间建立对应关系。如果你问自己"谷歌会怎么做?"这就减少了为裸金属边缘云设置类似 GCP API 的任务。这个 API 主要包含了 Kubernetes API,但不仅提供了使用 Kubernetes 的方法,还包括了管理 Kubernetes 的调用。
简而言之,这个"管理 Kubernetes"的任务就是把一组相互连接的服务器和交换机变成一个完全实例化的 Kubernetes 集群。对于初学者来说,API 需要提供一种在每个物理集群上安装和配置 Kubernetes 的方法,包括指定运行哪个版本的 Kubernetes,选择正确的容器网络接口(CNI)插件(虚拟网络适配器)组合,以及将 Kubernetes 连接到本地网络(以及可能需要的任何 VPN)。这一层还需要提供一种方法来设置访问和使用每个 Kubernetes 集群的帐户(和相关凭据),以及管理部署在给定集群上的独立项目的方法(例如为多个应用程序管理名称空间)。
例如,Aether 目前使用 Rancher 管理裸金属集群上的 Kubernetes, Rancher 的一个集中化实例负责管理所有边缘站点。这将产生如图 16 所示的配置,为了强调 Rancher 的范围,显示了多个边缘集群。虽然图中没有显示,但 GCP 提供的 API,就像 Rancher 一样,也跨越了多个物理站点(例如,us-west1-a
,europe-north1-b
,asia-south2-c
,等等)。
图 16. 混合云中的配置,包括一个用于管理运行在多个裸金属集群上的 Kubernetes 的 API 层。
最后需要指出的是,虽然我们经常把 Kubernetes 当作全行业标准来对待,但事实并非如此,每个云供应商都提供自己的定制版本:
- Microsoft Azure 提供了 Azure Kubernetes Service (AKS)
- AWS 提供了 Amazon Elastic Kubernetes Service (EKS)
- Google Cloud 提供了 Google Kubernetes Engine (GKE)
- Aether 边缘云运行 Rancher 认证的 Kubernetes 版本 (RKE)
虽然 CNCF(云原生计算基金会,负责管理 Kubernetes 项目的开源组织)对这些版本和其他版本的 Kubernetes 进行了认证,但这只是建立了一致性基线。每个版本都可以在此基础上进行改进,这些改进通常以附加特性的形式提供并控制 Kubernetes 集群。我们在云管理层的工作是为运营商提供一种管理这种异构性的方法。正如我们将在第 3.2 节中看到的,这是基础设施即代码层解决的主要挑战。
3.1.4 配置虚拟机
通过考虑提供虚拟机(VM)的含义,我们结束了对配置物理机所需步骤的讨论。当我们向 AKS、EKS 或 GKE 请求 Kubernetes 集群时,这是"幕后"发生的事情,因为超大规模云服务商可以选择将 Kubernetes 服务分层放在他们的基础设施即服务(IaaS)之上。我们正在构建的边缘云也需要类似的东西吗?
不一定。因为我们的目标是支持一组精心策划的边缘服务,为企业用户提供价值,而不是支持不受信任的第三方启动他们想要的任何应用程序的容器即服务(Container-as-a-Service),所以不需要"作为服务"来管理 VM。但是,我们仍然可能希望使用 VM 作为一种将 Kubernetes 的工作负载隔离在有限数量的物理服务器上的方法。这可以作为配置的一个步骤,类似于连接和引导物理机,但使用 KVM 和 Proxmox 等虚拟化机制来完成,而不需要类似 OpenStack 这样成熟的 IaaS 机制。然后,这些 VM 将被记录为 NetBox 和本节介绍的其他工具中的一级云资源,与物理机器没有区别。
考虑到 Kubernetes 允许我们在单个集群上部署多个应用程序,为什么要这样做呢?这个问题没有固定答案。一个原因是支持细粒度的资源隔离,从而可以(a)确保每个 Kubernetes 应用能够获取完成工作所需的处理器、内存和存储资源,(b)减少应用程序之间的信息泄露风险。例如,假设除了 SD-Fabric、SD-RAN 和 SD-Core 工作负载(默认情况下)运行在每个边缘站点之外,我们还想运行一个或多个其他边缘应用,比如在 2.3 节中介绍的 OpenVINO 平台。为了确保这些应用程序之间不存在干扰,可以为每个应用专门部署一个物理服务器子集。物理分区是共享物理集群的粗粒度方法。通过实例化 VM,能够在多个应用之间"分割"一个或多个服务器,这为运维人员分配资源提供了更大的灵活性,通常意味着更少的总体资源需求。请注意,还有其他方法可以指定如何在应用程序之间共享集群资源(我们将在第 4.4 节中看到),但是配置(provisioning)层是可以解决这个问题的一个选项。
3.2 基础设施即代码(Infrastructure-as-Code)
刚刚介绍的 Kubernetes 配置接口包含可编程 API、命令行接口(CLI)和图形用户界面(GUI)。如果你尝试了本书推荐的任何教程,可能会使用后两种教程中的一种。然而,对于运维部署来说,让运维人员与 CLI 或 GUI 交互是有问题的,不仅因为人类容易出错,还因为几乎不可能始终如一地重复一系列配置步骤。能够持续重复这个过程是下一章所介绍的生命周期管理的核心。
解决方案是以声明式语法定义基础架构,包含需要实例化的 Kubernetes 集群信息(例如,一部分运行在裸金属上的边缘集群,一部分在 GCP 中实例化),以及相关配置信息,然后自动化调用可编程 API。这是"基础设施即代码"的本质,正如前面说的,我们使用 Terraform 作为开源示例。
由于 Terraform 规范是声明式的,所以理解的最佳方法是浏览特定示例。这样做的目的不是记录 Terraform(对更详细的内容感兴趣的人可以使用在线文档和循序渐进的教程),而是建立关于该层在管理云方面所扮演角色的直觉。
延伸阅读:
为了理解示例,关于 Terraform 配置语言,其主要内容在于提供了一种方法(1)为不同类型的资源指定模板(这些是.tf
文件),(2)为这些资源模板的特定实例填充变量(这些是.tfvars
文件)。然后给定一组.tf
和.tfvars
文件,Terraform 实现两阶段过程。第一阶段,基于执行的前一个计划以来发生的变化构建执行计划。第二阶段,Terraform 执行一系列任务,使底层基础设施符合最新定义的规格说明。请注意,目前我们的工作是编写这些规格文件,并将它们签入配置存储库(Config Repo)。在第 4 章中,Terraform 将作为 CI/CD 流水线的一部分被调用。
现在来看具体的文件。在最上层,运维人员定义了计划合并到基础设施中的供应商(provider) 集合。我们可以认为每个供应商对应于一个云后端,提供了图 16 中介绍的相应配置 API。在我们的示例中,只展示两个供应商: Rancher 管理的边缘集群和 GCP 管理的集中式集群。注意,示例文件为每个供应商声明了一组相关变量(例如url
、access-key
),这些变量由下面介绍的特定实例的变量文件"填充"。
terraform { required_version = ">= 0.13" required_providers { rancher2 = { source = "rancher/rancher2" version = "= 1.15.1" } google = { source = "hashicorp/google" version = "~> 3.65.0" } null = { source = "hashicorp/null" version = "~> 2.1.2" } } } variable "rancher" { description = "Rancher credential" type = object({ url = string access_key = string secret_key = string }) } variable "gcp_config" { description = "GCP project and network configuration" type = object({ region = string compute_project = string network_project = string network_name = string subnet_name = string }) } provider "rancher2" { api_url = var.rancher.url access_key = var.rancher.access_key secret_key = var.rancher.secret_key } provider "google" { # Provide GCP credential using GOOGLE_CREDENTIALS environment variable project = var.gcp_config.compute_project region = var.gcp_config.region }
下一步是为我们希望配置的实际集群集填充详细信息(定义值)。让我们来看两个示例,对应于刚才指定的两个供应商。第一个显示了由 GCP 托管的集群(名为amp-gcp
),托管 AMP 工作负载。(类似的有一个sdcore-gcp
托管 SD-Core 实例。)Terraform 通过给特定集群分配相关标签(例如,env = "production"
)和管理堆栈的其他层(根据相关标签有选择的采取不同的操作)之间建立联系,我们将在第 4.4 节中看到使用这些标签的示例。
cluster_name = "amp-gcp" cluster_nodes = { amp-us-west2-a = { host = "10.168.0.18" roles = ["etcd", "controlplane", "worker"] labels = [] taints = [] }, amp-us-west2-b = { host = "10.168.0.17" roles = ["etcd", "controlplane", "worker"] labels = [] taints = [] }, amp-us-west2-c = { host = "10.168.0.250" roles = ["etcd", "controlplane", "worker"] labels = [] taints = [] } } cluster_labels = { env = "production" clusterInfra = "gcp" clusterRole = "amp" k8s = "self-managed" backup = "enabled" }
第二个示例展示了一个在 Site X 上实例化的边缘集群(名为ace-X
)。在示例代码中可以看到,这是一个由 5 个服务器和 4 个交换机(两个叶交换机和两个脊交换机)组成的裸金属集群。每个设备的地址必须与 3.1 节中介绍的硬件配置阶段分配的地址相匹配。理想情况下,该节中介绍的 NetBox(以及相关的)工具链将自动生成 Terraform 变量文件,但在实践中,通常仍然需要手动输入数据。
cluster_name = "ace-X" cluster_nodes = { leaf1 = { user = "terraform" private_key = "~/.ssh/id_rsa_terraform" host = "10.64.10.133" roles = ["worker"] labels = ["node-role.aetherproject.org=switch"] taints = ["node-role.aetherproject.org=switch:NoSchedule"] }, leaf2 = { user = "terraform" private_key = "~/.ssh/id_rsa_terraform" host = "10.64.10.137" roles = ["worker"] labels = ["node-role.aetherproject.org=switch"] taints = ["node-role.aetherproject.org=switch:NoSchedule"] }, spine1 = { user = "terraform" private_key = "~/.ssh/id_rsa_terraform" host = "10.64.10.131" roles = ["worker"] labels = ["node-role.aetherproject.org=switch"] taints = ["node-role.aetherproject.org=switch:NoSchedule"] }, spine2 = { user = "terraform" private_key = "~/.ssh/id_rsa_terraform" host = "10.64.10.135" roles = ["worker"] labels = ["node-role.aetherproject.org=switch"] taints = ["node-role.aetherproject.org=switch:NoSchedule"] }, server-1 = { user = "terraform" private_key = "~/.ssh/id_rsa_terraform" host = "10.64.10.138" roles = ["etcd", "controlplane", "worker"] labels = [] taints = [] }, server-2 = { user = "terraform" private_key = "~/.ssh/id_rsa_terraform" host = "10.64.10.139" roles = ["etcd", "controlplane", "worker"] labels = [] taints = [] }, server-3 = { user = "terraform" private_key = "~/.ssh/id_rsa_terraform" host = "10.64.10.140" roles = ["etcd", "controlplane", "worker"] labels = [] taints = [] }, server-4 = { user = "terraform" private_key = "~/.ssh/id_rsa_terraform" host = "10.64.10.141" roles = ["worker"] labels = [] taints = [] }, server-5 = { user = "terraform" private_key = "~/.ssh/id_rsa_terraform" host = "10.64.10.142" roles = ["worker"] labels = [] taints = [] } } cluster_labels = { env = "production" clusterInfra = "bare-metal" clusterRole = "ace" k8s = "self-managed" coreType = "4g" upfType = "up4" }
最后一块拼图是填写关于如何实例化每个 Kubernetes 集群的其余细节。本例中,我们只展示用于配置边缘集群的特定于 RKE 模块,如果你理解 Kubernetes,那么其中大部分细节都很简单。例如,该模块指定每个边缘集群应该加载calico
和multus
CNI 插件,还定义了如何调用kubectl
根据这些规范配置 Kubernetes。也许对 SCTPSupport 的引用会比较陌生,这表明特定的 Kubernetes 集群是否需要支持 SCTP, SCTP 是一种面向电信的网络协议,并不包含在普通 Kubernetes 部署中,但 SD-Core 需要。
terraform { required_providers { rancher2 = { source = "rancher/rancher2" } null = { source = "hashicorp/null" version = "~> 2.1.2" } } } resource "rancher2_cluster" "cluster" { name = var.cluster_config.cluster_name enable_cluster_monitoring = false enable_cluster_alerting = false labels = var.cluster_labels rke_config { kubernetes_version = var.cluster_config.k8s_version authentication { strategy = "x509" } monitoring { provider = "none" } network { plugin = "calico" } services { etcd { backup_config { enabled = true interval_hours = 6 retention = 30 } retention = "72h" snapshot = false } kube_api { service_cluster_ip_range = var.cluster_config.k8s_cluster_ip_range extra_args = { feature-gates = "SCTPSupport=True" } } kubelet { cluster_domain = var.cluster_config.cluster_domain cluster_dns_server = var.cluster_config.kube_dns_cluster_ip fail_swap_on = false extra_args = { cpu-manager-policy = "static" kube-reserved = "cpu=500m,memory=256Mi" system-reserved = "cpu=500m,memory=256Mi" feature-gates = "SCTPSupport=True" } } kube_controller { cluster_cidr = var.cluster_config.k8s_pod_range service_cluster_ip_range = var.cluster_config.k8s_cluster_ip_range extra_args = { feature-gates = "SCTPSupport=True" } } scheduler { extra_args = { feature-gates = "SCTPSupport=True" } } kubeproxy { extra_args = { feature-gates = "SCTPSupport=True" proxy-mode = "ipvs" } } } addons_include = ["https://raw.githubusercontent.com/k8snetworkplumbingwg/multus-cni/release-3.7/images/multus-daemonset.yml"] addons = var.addon_manifests } } resource "null_resource" "nodes" { triggers = { cluster_nodes = length(var.nodes) } for_each = var.nodes connection { type = "ssh" bastion_host = var.bastion_host bastion_private_key = file(var.bastion_private_key) bastion_user = var.bastion_user user = each.value.user host = each.value.host private_key = file(each.value.private_key) } provisioner "remote-exec" { inline = [<<EOT ${rancher2_cluster.cluster.cluster_registration_token[0].node_command} \ ${join(" ", formatlist("--%s", each.value.roles))} \ ${join(" ", formatlist("--taints %s", each.value.taints))} \ ${join(" ", formatlist("--label %s", each.value.labels))} EOT ] } } resource "rancher2_cluster_sync" "cluster-wait" { cluster_id = rancher2_cluster.cluster.id provisioner "local-exec" { command = <<EOT kubectl set env daemonset/calico-node \ --server ${yamldecode(rancher2_cluster.cluster.kube_config).clusters[0].cluster.server} \ --token ${yamldecode(rancher2_cluster.cluster.kube_config).users[0].user.token} \ --namespace kube-system \ IP_AUTODETECTION_METHOD=${var.cluster_config.calico_ip_detect_method} EOT } }
还有其他一些松耦合端点需要绑定,例如定义用于连接边缘集群到 GCP 中对应节点的 VPN,但是上面示例足以说明基础设施即代码在云管理堆栈中所扮演的角色。关键是 Terraform 处理的所有事情都可以由人工运维人员在后端配置 API 上通过一系列 CLI 命令(或 GUI 点击)来完成,但经验表明,这种方法容易出错,而且难以重复。从声明式语言开始并自动生成正确的 API 调用序列是克服这个问题的一种经过验证的方法。
最后请注意这样一个事实: 虽然我们现在为云基础设施定义了一个声明性规范,我们称之为 Aether 平台,但这些规范文件是我们在配置存储库中签入的一个软件工件。这就是我们所说的"基础架构即代码": 基础架构规范被签入到存储库中,并像任何其他代码一样接受版本控制。这个存储库反过来为下一章介绍的生命周期管理流水线提供了输入。3.1 节中介绍的物理配置步骤发生在流水线的"外部"(这就是为什么我们不只是将资源配置加入生命周期管理),但将资源配置看作生命周期管理的"阶段 0"是比较公平的定义。
3.3 平台定义
定义系统架构(在我们的例子中是混合云的管理框架)的艺术在于决定在平台中包含什么以及在平台上运行的应用程序之间划清界限。对于 Aether,我们决定在平台中包含 SD-Fabric(以及 Kubernetes),而 SD-Core 和 SD-RAN 被视为应用程序,尽管这三者都是作为基于 Kubernetes 的微服务实现的。这个决定的后果是 SD-Fabric 被初始化为本章介绍的配置系统的一部分(与 NetBox、Ansible、Rancher 和 Terraform 扮演角色共同完成),而 SD-Core 和 SD-RAN 是基于第 4 章介绍的应用级机制部署。
可能还有其他边缘应用作为 Kubernetes 工作负载运行,这使情况变得更加复杂,因为从他们的角度来看,所有 Aether 组件(包括 SD-Core 和 SD-RAN 实现的 5G 连接)都假定是平台的一部分。换句话说,Aether 划了两条线,一条划分了 Aether 基础平台(Kubernetes 加上 SD-Fabric),另一条划分了 Aether PaaS(包括运行在平台上的 SD-Core 和 SD-RAN,加上管理整个系统的 AMP)。"基础平台"和"PaaS"之间的区别很细微,但本质上分别对应于软件堆栈和托管服务。
从某些方面来说,这只是一个术语问题,当然也很重要,不过与我们的讨论相关的是,由于有多个重叠机制,因此我们有不止一种方法来解决遇到的每个工程问题,从而很容易对可分离关注点实现不必要的合并而结束。明确、一致的界定什么是平台、什么是应用是健全的整体设计的先决条件。同样重要的是要认识到内部工程决策(例如,使用什么机制来部署给定组件)和外部可见的体系架构决策(例如,通过公共 API 公开什么功能)之间的区别。