likes
comments
collection
share

aws sgp 失效分析

作者站长头像
站长
· 阅读数 43

一、security groups for pod 工作机制

Amazon EKS 集群有两个在 Kubernetes 控制平面上运行的新组件:与您的集群相关联的 Amazon Virtual Private Cloud (Amazon VPC) 的mutating webhook和resource controller。Webhook负责向需要安全组的Pod 添加限制和请求。控制器负责管理网络接口与那些Pod相关联。为了促进此功能,每个工作节点将与一个主干网络接口和多个分支网络接口相关联。主干接口充当附加到实例的标准网络接口。然后,VPC 资源控制器将分支接口关联到主干接口。这增加了每个实例可以连接的网络接口的数量。由于安全组是用网络接口指定的,我们现在能够将需要特定安全组的Pod调度到分配给工作节点的额外的网络接口上。让我们将此功能的工作原理分解为以下3个阶段。

第一个阶段:node初始化和广播分支接口限制

aws sgp 失效分析

一旦在Amazon VPC CNI 插件上启用配置变量,IP address management daemon(ipamd) 将向支持的实例类型添加 Kubernetes 标签。然后,VPC资源控制器会将分支网络接口作为集群中节点上的扩展资源进行发布。分支接口容量是对已存在的实例类型的辅助IP地址限制的补充。例如,c5.4xlarge 可以继续将最多 234个辅助IP地址分配给标准网络接口和最多54个分支网络接口。ENI主干/分支在大多数基于实例系列AWS Nitro上可用,包括 m5、m6g、c5、c6g、r5、r6g、g4 和 p3。如果不需要使用特定安全组隔离您的工作负载,则无需进行任何更改即可使用共享ENI上的辅助 IP 地址继续运行它们。

第二阶段 调度pod到node上

aws sgp 失效分析

对于确实需要特定安全组的workloads,我们采用了Kubernetes 原生方法并添加了新的自定义资源定义(CRD)。集群管理员可以通过 SecurityGroupPolicy CRD 指定将哪些安全组分配给 pod。在命名空间内,您可以根据Pod标签或根据与Pod关联的服务帐户的标签来选择 Pod。对于任何匹配的 pod,您还可以定义要应用的安全组 ID。

Webhook 会监视SecurityGroupPolicy自定义资源的任何更改,并自动将匹配的Pod与扩展资源请求一起注入,以便将Pod调度到具有可用分支网络接口容量的节点上。一旦pod被调度,资源控制器将创建一个分支接口并将其附加到主干接口。成功附加后,控制器会使用分支接口详细信息向pod对象添加注释。

VPC资源控制器需要EC2权限才能根据集群中pod的要求修改VPC资源。为了简化这一过程,我们创建了一个 AWS 托管策略:AmazonEKSVPCResourceController。鉴于控制器在 Kubernetes 控制平面上运行,您需要将此策略附加到与集群关联的 IAM角色以便利用将安全组应用于pod。

第三阶段 设置pod网络协议栈

aws sgp 失效分析

在此阶段,VPC CNI 插件为pod设置网络。该插件查询ipamd以读取分支网络接口详细信息,然后查询 Kubernetes ApiServer以读取 pod 注释。一旦pod注释可用,CNI将从中继接口创建一个Virtual LAN(vlan)设备。该设备仅供此分支接口pod 使用,不与主机上的任何其他 pod 共享。然后,CNI 将使用vlan设备创建一个包含默认路由的路由表,并将pod的主机虚拟以太网设备(veth) 端关联到该接口。最后,CNI 插件添加 iptables 规则,以便所有流入此主机veth和vlan 的流量都将使用此路由表。

二、交流沟通

与厂商沟通以后,厂商反馈:VPC Resource Controller从v1.14.0升级到v1.1.5后,升级前创建的SecurityGroupPolicy返回为not find,issuer: github.com/aws/amazon-… 报错如下:

Webhook couldn't find SGP definition: GroupVersionResource or GroupKind didn't match. Will allow regular pods creation.

厂商怀疑是webhook的cache失效导致,根据上文报错推断出可能是因为自己的controller-runtime的包太旧了导致的,刚好有一个相关的issuer被修复了, issuer内容如下:

Currently, even the simplest controller (like github.com/kubernetes-…) will make a lot of API server calls at startup to setup the restmapping. In my relatively small cluster, this results in 75 API server calls which, with rate limiting, takes over 5s.

These calls are unnecessary in many cases - I don't need to get discovery info for my acme CRD when I am creating a TokenReview controller.

There is an option, WithLazyDiscovery, that sounds like a solution, but it really just defers when we query the fully discovery listing until we start the client.

It would be great to have an option that is per-resource lazy - so if we only read TokenReview we don't request all of the other groups. Even better, we can make core types baked in - I think its a safe assumption that the RestMapping for Pod, Service, etc is never changing (?)

见链接github.com/kubernetes-…

三、对controller-runtime 修复的相应代码进行分析

restMapper主要是为了解决把GVK转化为GVR。

首先提供了一个mapper的struct


// mapper is a RESTMapper that will lazily query the provided
// client for discovery information to do REST mappings.
type mapper struct {
   mapper      meta.RESTMapper
   client      *discovery.DiscoveryClient
   knownGroups map[string]*restmapper.APIGroupResources
   apiGroups   map[string]*metav1.APIGroup

   // mutex to provide thread-safe mapper reloading.
   mu sync.RWMutex
}

mapper实现lazy query的代码主要体现在这个方法上addKnownGroupAndReload,方法内容如下


func (m *mapper) addKnownGroupAndReload(groupName string, versions ...string) error {
   // If no specific versions are set by user, we will scan all available ones for the API group.
   // This operation requires 2 requests: /api and /apis, but only once. For all subsequent calls
   // this data will be taken from cache.
   if len(versions) == 0 {
      apiGroup, err := m.findAPIGroupByName(groupName)
      if err != nil {
         return err
      }
      for _, version := range apiGroup.Versions {
         versions = append(versions, version.Version)
      }
   }

   m.mu.Lock()
   defer m.mu.Unlock()

   // Create or fetch group resources from cache.
   groupResources := &restmapper.APIGroupResources{
      Group:              metav1.APIGroup{Name: groupName},
      VersionedResources: make(map[string][]metav1.APIResource),
   }
   if _, ok := m.knownGroups[groupName]; ok {
      groupResources = m.knownGroups[groupName]
   }

   // Update information for group resources about versioned resources.
   // The number of API calls is equal to the number of versions: /apis/<group>/<version>.
   groupVersionResources, err := m.fetchGroupVersionResources(groupName, versions...)
   if err != nil {
      return fmt.Errorf("failed to get API group resources: %w", err)
   }
   for version, resources := range groupVersionResources {
      groupResources.VersionedResources[version.Version] = resources.APIResources
   }

   // Update information for group resources about the API group by adding new versions.
   // Ignore the versions that are already registered.
   for _, version := range versions {
      found := false
      for _, v := range groupResources.Group.Versions {
         if v.Version == version {
            found = true
            break
         }
      }

      if !found {
         groupResources.Group.Versions = append(groupResources.Group.Versions, metav1.GroupVersionForDiscovery{
            GroupVersion: metav1.GroupVersion{Group: groupName, Version: version}.String(),
            Version:      version,
         })
      }
   }

逻辑如下:

  1. 如果用户没有指定版本,我们将会扫描所有可用的verions;
  2. 这个操作会发起2个请求/api 和 /apis,但只会发起一次;
  3. 后续的请求获取这些数据将会走缓存。
转载自:https://juejin.cn/post/7236713712627236919
评论
请登录