图解 Deployment Controller 工作流程

本文基于对 Kubernetes v1.16 的源码阅读,通过流程图描述 Deployment Controller 的工作流程,但也包含相关代码以供参考

上一篇文章《Kubernetes Controller Manager 工作原理》讲解了 Controller Manager 是怎么管理 Controller 的,我们知道 Controller 只需要实现相关事件 handler,无须再关心上层逻辑。本文将基于这篇文章,介绍 Deployment Controller 在接收到来自 Informer 的事件后,做了哪些工作。

Deployment 与控制器模式

在 K8s 中,pod 是最小的资源单位,而 pod 的副本管理是通过 ReplicaSet(RS) 实现的;而 deployment 实则是基于 RS 做了更上层的工作。

这就是 Kubernetes 的控制器模式,顶层资源通过控制下层资源,来拓展新能力。deployment 并没有直接对 pod 进行管理,是通过管理 rs 来实现对 pod 的副本控制。deployment 通过对 rs 的控制实现了版本管理:每次发布对应一个版本,每个版本有一个 rs,在注解中标识版本号,而 rs 再每次根据 pod template 和副本数运行相应的 pod。deployment 只需要保证任何情况下 rs 的状态都在预期,rs 保证任何情况下 pod 的状态都在预期。

K8s 是怎么管理 Deployment 的

了解了 deployment 这一资源在 K8s 中的定位,我们再来看下这个资源如何达到预期状态。

Kubernetes 的 API 和控制器都是基于水平触发的,可以促进系统的自我修复和周期协调。

水平触发这个概念来自硬件的中断,中断可以是水平触发,也可以是边缘触发:

  • 水平触发 : 系统仅依赖于当前状态。即使系统错过了某个事件(可能因为故障挂掉了),当它恢复时,依然可以通过查看信号的当前状态来做出正确的响应。
  • 边缘触发 : 系统不仅依赖于当前状态,还依赖于过去的状态。如果系统错过了某个事件(“边缘”),则必须重新查看该事件才能恢复系统。

Kubernetes 水平触发的 API 实现方式是:控制器监视资源对象的实际状态,并与对象期望的状态进行对比,然后调整实际状态,使之与期望状态相匹配。

水平触发的 API 也叫声明式 API,而监控 deployment 资源对象并确定符合预期的控制器就是 deployment controller,对应的 rs 的控制器就是 rs controller。

Deployment Controller

架构

首先看看 DeploymentController 在 K8s 中的定义:

type DeploymentController struct {
    rsControl     controller.RSControlInterface
    client        clientset.Interface
    eventRecorder record.EventRecorder

    syncHandler func(dKey string) error
    enqueueDeployment func(deployment *apps.Deployment)

    dLister appslisters.DeploymentLister
    rsLister appslisters.ReplicaSetLister
    podLister corelisters.PodLister

    dListerSynced cache.InformerSynced
    rsListerSynced cache.InformerSynced
    podListerSynced cache.InformerSynced

    queue workqueue.RateLimitingInterface
}

主要包括几大块内容:

  • rsControl 是一个 ReplicaSet Controller 的工具,用来对 rs 进行认领和弃养工作;
  • client 就是与 APIServer 通信的 client;
  • eventRecorder 用来记录事件;

  • syncHandler 用来处理 deployment 的同步工作;

  • enqueueDeployment 是一个将 deployment 入 queue 的方法;

  • dListerrsListerpodLister 分别用来从 shared informer store 中获取 资源的方法;

  • dListerSyncedrsListerSyncedpodListerSynced 分别是用来标识 shared informer store 中是否同步过;

  • queue 就是 workqueue,deployment、replicaSet、pod 发生变化时,都会将对应的 deployment 推入这个 queue,syncHandler() 方法统一从 workqueue 中处理 deployment。

工作流程

接下来看看 deployment controller 的工作流程。

deployment controller 利用了 informer 的工作能力,实现了资源的监听,同时与其他 controller 协同工作。主要使用了三个 shared informer —— deployment informer、rs informer、pod informer。

首先 deployment controller 会向三个 shared informer 中注册钩子函数,三个钩子函数会在相应事件到来时,将相关的 deployment 推进 workqueue 中。

deployment controller 启动时会有一个 worker 来控制 syncHandler 函数,实时地将 workqueue 中的 item 推出,根据 item 来执行任务。主要包括:领养和弃养 rs、向 eventRecorder 分发事件、根据升级策略决定如何处理下级资源。

workqueue

首先看看 workqueue,这其实是用来辅助 informer 对事件进行分发的队列。整个流程可以梳理为下图。

可以看到,workqueue 分为了三个部分,一是一个先入先出的队列,由切片来实现;还有两个是名为 dirty 和 processing 的 map。整个工作流程分为三个动作,分别为 add、get、done。

add 是将消息推入队列。消息时从 informer 过来的,其实过来的消息并不是事件全部,而是资源 key,即 namespace/name,以这种形式通知业务逻辑有资源的变更事件过来了,需要拿着这个 key 去 indexer 中获取具体资源。如上图绿色的过程所示,在消息进行之后,先检查 dirty 中是否存在,若已存在,不做任何处理,表明事件已经在队列中,不需要重复处理;若 dirty 中不存在,则将该 key 存入 dirty 中(将其作为 map 的键,值为空结构体),再推入队列一份。

get 是 handle 函数从队列中获取 key 的过程。将 key 从队列中 pop 出来的同时,会将其放入 processing 中,并删除其在 dirty 的索引。这一步的原因是将 item 放入 processing 中标记其正在被处理,同时从 dirty 中删除则不影响后面的事件入队列。

done 则是 handle 在处理完 key 之后,必须执行的一步,相当于给 workqueue 发一个 ack,表明我已经处理完毕,该动作仅仅将其从 processing 中删除。

有了这个小而美的先入先出的队列,我们就可以避免资源的多个事件发生时,从 indexer 中重复获取资源的事情发生了。下面来梳理 deployment controller 的具体流程。

replicaSet 的认领和弃养过程

在 deployment controller 的工作流程中,可以注意到除了 deployment 的三个钩子函数,还有 rs 和 pod 的钩子函数,在 rs 的三个钩子函数中,涉及到了 deployment 对 rs 的领养和弃养过程。

rs 认亲

首先来看 rs 的认亲过程:

在 rs 的三个钩子函数中,都会涉及到认亲的过程。

当监听到 rs 的变化后,会根据 rs 的 ownerReferences 字段找到对应的 deployment 入 queue;若该字段为空,意味着这是个孤儿 rs,启动 rs 认亲机制。

认亲过程首先是遍历所有的 deployment,判断 deployment 的 selector 是否与当前 rs 的 labels 相匹配,找到所有与之匹配的 deployment。

然后判断总共有多少个 deployment,若为 0 个,就直接返回,没有人愿意认领,什么都不做,认领过程结束;若大于 1 个,抛错出去,因为这是不正常的行为,不允许多个 deployment 同时拥有同一个 rs;若有且仅有一个 deployment 与之匹配,那么就找到了愿意领养的 deployment,将其入 queue。

addReplicaSet()updateReplicaSet() 的认领过程类似,这里只展示 addReplicaSet() 的代码:

func (dc *DeploymentController) addReplicaSet(obj interface{}) {
    rs := obj.(*apps.ReplicaSet)

    if rs.DeletionTimestamp != nil {
        dc.deleteReplicaSet(rs)
        return
    }

    if controllerRef := metav1.GetControllerOf(rs); controllerRef != nil {
        d := dc.resolveControllerRef(rs.Namespace, controllerRef)
        if d == nil {
            return
        }
        klog.V(4).Infof("ReplicaSet %s added.", rs.Name)
        dc.enqueueDeployment(d)
        return
    }

    ds := dc.getDeploymentsForReplicaSet(rs)
    if len(ds) == 0 {
        return
    }
    klog.V(4).Infof("Orphan ReplicaSet %s added.", rs.Name)
    for _, d := range ds {
        dc.enqueueDeployment(d)
    }
}

deployment 领养和弃养

deployment 对 rs 的领养和弃养过程,是发生在从 workqueue 中处理 item 的过程,也是找到当前 deployment 拥有的所有 rs 的过程。

该过程会轮询所有的 rs,如果有 owner 且是当前 deployment,再判断 label 是否满足 deployment 的 selector,满足则列入结果;若不满足,启动弃养机制,仅仅将 rs 的 ownerReferences 删除,使其成为孤儿,不做其他事情。这也是为什么修改了 deployment 的 selector 之后,会多一个 replicas!=0 的 rs 的原因。

如果 rs 没有 owner,是个孤儿,判断 label 是否满足 deployment 的 selector,满足条件,则启动领养机制,将其 ownerReferences 设置为当前 deployment,再列入结果。

下面是整个过程的源码:

func (m *BaseControllerRefManager) ClaimObject(obj metav1.Object, match func(metav1.Object) bool, adopt, release func(metav1.Object) error) (bool, error) {
    controllerRef := metav1.GetControllerOf(obj)
    if controllerRef != nil {
        if controllerRef.UID != m.Controller.GetUID() {
            return false, nil
        }
        if match(obj) {
            return true, nil
        }

        if m.Controller.GetDeletionTimestamp() != nil {
            return false, nil
        }
        if err := release(obj); err != nil {
            if errors.IsNotFound(err) {
                return false, nil
            }
            return false, err
        }
        return false, nil
    }

    if m.Controller.GetDeletionTimestamp() != nil || !match(obj) {
        return false, nil
    }
    if obj.GetDeletionTimestamp() != nil {
        return false, nil
    }

    if err := adopt(obj); err != nil {
        if errors.IsNotFound(err) {
            return false, nil
        }
        return false, err
    }
    return true, nil
}

领养和弃养的函数:

func (m *ReplicaSetControllerRefManager) AdoptReplicaSet(rs *apps.ReplicaSet) error {
    if err := m.CanAdopt(); err != nil {
        return fmt.Errorf("can't adopt ReplicaSet %v/%v (%v): %v", rs.Namespace, rs.Name, rs.UID, err)
    }

    addControllerPatch := fmt.Sprintf(
        `{"metadata":{"ownerReferences":[{"apiVersion":"%s","kind":"%s","name":"%s","uid":"%s","controller":true,"blockOwnerDeletion":true}],"uid":"%s"}}`,
        m.controllerKind.GroupVersion(), m.controllerKind.Kind,
        m.Controller.GetName(), m.Controller.GetUID(), rs.UID)
    return m.rsControl.PatchReplicaSet(rs.Namespace, rs.Name, []byte(addControllerPatch))
}

func (m *ReplicaSetControllerRefManager) ReleaseReplicaSet(replicaSet *apps.ReplicaSet) error {
    klog.V(2).Infof("patching ReplicaSet %s_%s to remove its controllerRef to %s/%s:%s",
        replicaSet.Namespace, replicaSet.Name, m.controllerKind.GroupVersion(), m.controllerKind.Kind, m.Controller.GetName())
    deleteOwnerRefPatch := fmt.Sprintf(`{"metadata":{"ownerReferences":[{"$patch":"delete","uid":"%s"}],"uid":"%s"}}`, m.Controller.GetUID(), replicaSet.UID)
    err := m.rsControl.PatchReplicaSet(replicaSet.Namespace, replicaSet.Name, []byte(deleteOwnerRefPatch))
    if err != nil {
        if errors.IsNotFound(err) {
            return nil
        }
        if errors.IsInvalid(err) {
            return nil
        }
    }
    return err
}

rolloutRecreate

如果 deployment 的更新策略是 Recreate,其过程是将旧的 pod 删除,再启动新的 pod。具体过程如下:

首先根据上一步 rs 的领养和弃养过程,获得当前 deployment 的所有 rs,排序找出最新的 rs,将其 pod template 与 deployment 的 pod template 比较,若不一致需要创建新的 rs;

创建新的 rs 的过程为:计算当前 deployment 的 pod template 的 hash 值,将其增加至 rs label 及 selector 中;

对所有旧的 rs 计算出最大的 revision,将其加一,作为新 rs 的 revision,为新的 rs 设置如下注解:

"deployment.kubernetes.io/revision"
"deployment.kubernetes.io/desired-replicas"
"deployment.kubernetes.io/max-replicas"

如果当前 deployment 的 revision 不是最新,将其设为最新;如果需要更新状态,则更新其状态;

将旧的 rs 进行降级,即将其副本数设为 0;

判断当前所有旧的 pod 是否停止,判断条件为 pod 状态为 failed 或 succeed,unknown 或其他所有状态都不是停止状态;若并非所有 pod 都停止了,则退出本次操作,下一个循环再处理;

若所有 pod 都停止了,将新的 rs 进行升级,即将其副本数置为 deployment 的副本数;

最后进行清理工作,比如旧的 rs 数过多时,删除多余的 rs 等。

下面是 rolloutRecreate 的源代码:

func (dc *DeploymentController) rolloutRecreate(d *apps.Deployment, rsList []*apps.ReplicaSet, podMap map[types.UID][]*v1.Pod) error {
    newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false)
    if err != nil {
        return err
    }
    allRSs := append(oldRSs, newRS)
    activeOldRSs := controller.FilterActiveReplicaSets(oldRSs)

    scaledDown, err := dc.scaleDownOldReplicaSetsForRecreate(activeOldRSs, d)
    if err != nil {
        return err
    }
    if scaledDown {
        return dc.syncRolloutStatus(allRSs, newRS, d)
    }

    if oldPodsRunning(newRS, oldRSs, podMap) {
        return dc.syncRolloutStatus(allRSs, newRS, d)
    }

    if newRS == nil {
        newRS, oldRSs, err = dc.getAllReplicaSetsAndSyncRevision(d, rsList, true)
        if err != nil {
            return err
        }
        allRSs = append(oldRSs, newRS)
    }

    if _, err := dc.scaleUpNewReplicaSetForRecreate(newRS, d); err != nil {
        return err
    }

    if util.DeploymentComplete(d, &d.Status) {
        if err := dc.cleanupDeployment(oldRSs, d); err != nil {
            return err
        }
    }

    return dc.syncRolloutStatus(allRSs, newRS, d)
}

rolloutRolling

如果 deployment 的更新策略是 Recreate,其过程是将旧的 pod 删除,再启动新的 pod。具体过程如下:

从上图可以看到,从开始到创建新的 rs 的过程与 rolloutRecreate 过程一致,唯一区别在于,设置新 rs 副本数的过程。在 rolloutRolling 的过程中,新的 rs 的副本数为 deploy.replicas + maxSurge - currentPodCount。代码如下:

func NewRSNewReplicas(deployment *apps.Deployment, allRSs []*apps.ReplicaSet, newRS *apps.ReplicaSet) (int32, error) {
    switch deployment.Spec.Strategy.Type {
    case apps.RollingUpdateDeploymentStrategyType:
        // Check if we can scale up.
        maxSurge, err := intstrutil.GetValueFromIntOrPercent(deployment.Spec.Strategy.RollingUpdate.MaxSurge, int(*(deployment.Spec.Replicas)), true)
        if err != nil {
            return 0, err
        }
        // Find the total number of pods
        currentPodCount := GetReplicaCountForReplicaSets(allRSs)
        maxTotalPods := *(deployment.Spec.Replicas) + int32(maxSurge)
        if currentPodCount >= maxTotalPods {
            // Cannot scale up.
            return *(newRS.Spec.Replicas), nil
        }
        // Scale up.
        scaleUpCount := maxTotalPods - currentPodCount
        // Do not exceed the number of desired replicas.
        scaleUpCount = int32(integer.IntMin(int(scaleUpCount), int(*(deployment.Spec.Replicas)-*(newRS.Spec.Replicas))))
        return *(newRS.Spec.Replicas) + scaleUpCount, nil
    case apps.RecreateDeploymentStrategyType:
        return *(deployment.Spec.Replicas), nil
    default:
        return 0, fmt.Errorf("deployment type %v isn't supported", deployment.Spec.Strategy.Type)
    }
}

然后到了增减新旧 rs 副本数的过程。主要为先 scale up 新 rs,再 scale down 旧 rs。scale up 新 rs 的过程与上述一致;scale down 旧 rs 的过程为先计算一个最大 scale down 副本数,若小于 0 则不做任何操作;然后在 scale down 的时候做了一个优化,先 scale down 不正常的 rs,可以保证先删除那些不健康的副本;最后如果还有余额,再 scale down 正常的 rs。

每次 scale down 的副本数为 allAvailablePodCount - minAvailable,即 allAvailablePodCount - (deploy.replicas - maxUnavailable)。代码如下:

func (dc *DeploymentController) scaleDownOldReplicaSetsForRollingUpdate(allRSs []*apps.ReplicaSet, oldRSs []*apps.ReplicaSet, deployment *apps.Deployment) (int32, error) {
    maxUnavailable := deploymentutil.MaxUnavailable(*deployment)

    // Check if we can scale down.
    minAvailable := *(deployment.Spec.Replicas) - maxUnavailable
    // Find the number of available pods.
    availablePodCount := deploymentutil.GetAvailableReplicaCountForReplicaSets(allRSs)
    if availablePodCount <= minAvailable {
        // Cannot scale down.
        return 0, nil
    }
    klog.V(4).Infof("Found %d available pods in deployment %s, scaling down old RSes", availablePodCount, deployment.Name)

    sort.Sort(controller.ReplicaSetsByCreationTimestamp(oldRSs))

    totalScaledDown := int32(0)
    totalScaleDownCount := availablePodCount - minAvailable
    for _, targetRS := range oldRSs {
        if totalScaledDown >= totalScaleDownCount {
            // No further scaling required.
            break
        }
        if *(targetRS.Spec.Replicas) == 0 {
            // cannot scale down this ReplicaSet.
            continue
        }
        // Scale down.
        scaleDownCount := int32(integer.IntMin(int(*(targetRS.Spec.Replicas)), int(totalScaleDownCount-totalScaledDown)))
        newReplicasCount := *(targetRS.Spec.Replicas) - scaleDownCount
        if newReplicasCount > *(targetRS.Spec.Replicas) {
            return 0, fmt.Errorf("when scaling down old RS, got invalid request to scale down %s/%s %d -> %d", targetRS.Namespace, targetRS.Name, *(targetRS.Spec.Replicas), newReplicasCount)
        }
        _, _, err := dc.scaleReplicaSetAndRecordEvent(targetRS, newReplicasCount, deployment)
        if err != nil {
            return totalScaledDown, err
        }

        totalScaledDown += scaleDownCount
    }

    return totalScaledDown, nil
}

总结

因为 Kubernetes 采用声明式 API,因此对于 Controller 来说,所做的事情就是根据当前事件计算一个预期的状态,并判断当前实际的状态是否满足预期的状态,如果不满足,则采取行动使其靠拢。

本文通过对 Deployment Controller 的工作流程进行分析,虽然做的事情比较繁琐,但是其所做的事情都是围绕 “向预期靠拢” 这个目标展开的。

希望这两篇文章能让你一窥豹斑,了解 Kubernetes Controller Manager 大致的工作流程和实现方式。

2019/12/15 14:37 下午 posted in  Kubernetes

使用 Kubebuilder 创建自定义 K8s AdmissionWebhooks

Kubebuilder 除了可以构建 CRD API 及其 Controller 之外,还能构建 AdmissionWebhooks。这篇文章就来详细分析 Kubebuilder 如何构建 AdmissionWebhooks。

K8s 的 AdmissionWebhooks

首先要知道,在 K8s 里 AdmissionWebhooks 是什么,目的是什么。

先说场景,如果我们需要在 pod 创建出来之前,对其进行配置修改或者检查,这部分工作如果放在 ApiServer 里,需要管理员在 ApiServer 中将其编译成二进制文件,如果配置修改想做成自定义的形式会非常麻烦。而 Admission controllers 就是为这种场景而生的工具,以插件的形式附着到 ApiServer 中,AdmissionWebhooks 就是其中一种准入插件。

K8s 的 AdmissionWebhooks 分两种:MutatingAdmissionWebhookValidatingAdmissionWebhook,二者合起来就是一个特殊类型的 admission controllers,一个处理资源更改,一个处理验证。

在之前一篇文章《[译]深入剖析 Kubernetes MutatingAdmissionWebhook》中,给出了非常详细的 MutatingAdmissionWebhook 的教程,其中主要做了三件事情:

  1. MutatingWebhookConfiguration:MutatingAdmissionWebhook 向 ApiServer 注册的配置;
  2. MutatingAdmissionWebhook 本身:一种插件形式的 admission controller,需要向 ApiServer 注册自己;
  3. Webhook Admission Server:一个附着到 k8s ApiServer 的 http server,接收 ApiServer 的请求。

那么用 Kubebuilder 构建 AdmissionWebhooks 的话,Kubebuilder 会为我们自动生成 Webhook Server,并留下几个函数让我们添加自有逻辑。

创建自定义 AdmissionWebhooks

这里使用一个简单的场景做一个演示,我们自定义一个名为 App 资源,当用户创建一个 App 实例时,我们根据用户的描述创建出一个 Deployment。

然后我们添加一个 MutatingAdmissionWebhook,当用户通过 App 创建 Deployment 时,自动添加一个 sidecar 容器到 Pod 中(这里使用 nginx 作为 sidecar)。

本文所用 kubebuilder 版本为 2.0.1,完整的项目代码可见:https://github.com/zwwhdls/KubeAdmissionWebhookDemo

初始化 API 及 Controller

第一步是创建出 CRD 及其 Controller,几行命令就能搞定:

$ export GO111MODULE=on

$ mkdir $GOPATH/src/zww-app
$ cd $GOPATH/src/zww-app
$ kubebuilder init --domain o0w0o.cn --owner "zwwhdls"

$ kubebuilder create api --group app --version v1 --kind App

我这里做的比较简单,AppSpec 只定义了一个 deploy 属性(就是 appsv1.DeploymentSpec),Controller 中会根据 deploy 属性生成对应的 Deployment:

type AppSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file
    Deploy appsv1.DeploymentSpec `json:"deploy,omitempty"`
}

在完善了 AppSpec 和 Controller 的 Reconcile 函数后,使 Kubebuilder 重新生成代码,并将 config/crd 下的 CRD yaml 应用到当前集群:

make
make install

创建 Webhook Server

接下来就是用 Kubebuilder 来生成 Webhooks 了:

kubebuilder create webhook --group app --version v1 --kind App 

在路径 api/v1 下生成了一个名为 app_webhook.go 的文件。可以看到 Kubebuilder 已经帮你定义了两个变量:

var _ webhook.Defaulter = &App{}
var _ webhook.Validator = &App{}

这两个变量分别表示 MutatingWebhookServer 和 ValidatingWebhookServer,在程序启动的时候,这两个 Server 会 run 起来。

对于 MutatingWebhookServer,Kubebuilder 预留了 Default() 函数,让用户来填写自己的逻辑:

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *App) Default() {
    applog.Info("default", "name", r.Name)

    // TODO(user): fill in your defaulting logic.
}

对于我们希望 Webhook 在资源发生什么样的变化时触发,可以通过这条注释修改:

// +kubebuilder:webhook:path=/mutate-app-o0w0o-cn-v1-app,mutating=true,failurePolicy=fail,groups=app.o0w0o.cn,resources=apps,verbs=create;update,versions=v1,name=mapp.kb.io

对应的参数为:

  • failurePolicy:表示 ApiServer 无法与 webhook server 通信时的失败策略,取值为 "ignore" 或 "fail";
  • groups:表示这个 webhook 在哪个 Api Group 下会收到请求;
  • mutating:这个参数是个 bool 型,表示是否是 mutating 类型;
  • name:webhook 的名字,需要与 configuration 中对应;
  • path:webhook 的 path;
  • resources:表示这个 webhook 在哪个资源发生变化时会收到请求;
  • verbs:表示这个 webhook 在资源发生哪种变化时会收到请求,取值为 “create“, "update", "delete", "connect", 或 "*" (即所有);
  • versions:表示这个 webhook 在资源的哪个 version 发生变化时会收到请求;

对于 ValidatingWebhookServer,Kubebuilder 的处理与 MutatingWebhookServer 一致,这里不再赘述。

方便起见,我只定义了 MutatingWebhookServer 的 Default 函数,为每个 App 类型资源的 pod 注入一个 nginx sidecar 容器:

func (r *App) Default() {
    applog.Info("default", "name", r.Name)
    var cns []core.Container
    cns = r.Spec.Deploy.Template.Spec.Containers

    container := core.Container{
        Name:  "sidecar-nginx",
        Image: "nginx:1.12.2",
    }

    cns = append(cns, container)
    r.Spec.Deploy.Template.Spec.Containers = cns
}

运行 Webhook Server

本文仅分享本地开发测试的调试方案,线上部署方案请参考官方文档

首先需要将 MutatingWebhookConfiguration 稍作修改,使得 ApiServer 能够与 Webhook Server 通信。具体方法如下:

配置 Server Path

第一步,配置 Server Path;将 service 去掉,换成 url: https://<server_ip>:9443/mutate-app-o0w0o-cn-v1-app ,其中 server_ip 是 Webhook Server 的 ip,如果运行在本地,就是本地的 ip。需要注意的是 url 中的 path 要与 app_webhook.go 中定义的保持一致。

配置证书

第二步,配置 caBundle;由于在 Kube 里,所有与 ApiServer 交互的组件都需要与 ApiServer 进行双向 TLS 认证,我们这里需要先手动签发自签名 CA 证书:

$ openssl genrsa -out ca.key 2048
$ openssl req -x509 -new -nodes -key ca.key -subj "/CN=<server_ip>" -days 10000 -out ca.crt
$ openssl genrsa -out server.key 2048
$ cat << EOF >csr.conf
> [ req ]
> default_bits = 2048
> prompt = no
> default_md = sha256
> req_extensions = req_ext
> distinguished_name = dn
> 
> [ dn ]
> C = <country>
> ST = <state>
> L = <city>
> O = <organization>
> OU = <organization unit>
> CN = <server_ip>
> 
> [ req_ext ]
> subjectAltName = @alt_names
> 
> [ alt_names ]
> IP.1 = <server_ip>
> 
> [ v3_ext ]
> authorityKeyIdentifier=keyid,issuer:always
> basicConstraints=CA:FALSE
> keyUsage=keyEncipherment,dataEncipherment
> extendedKeyUsage=serverAuth,clientAuth
> subjectAltName=@alt_names
> EOF
$ openssl req -new -key server.key -out server.csr -config csr.conf
$ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 10000 -extensions v3_ext -extfile csr.conf

证书生成后将 server.keyserver.crt 拷贝到 Kubebuilder 设置的 webhook server 的私钥和证书路径下:

webhook server 的私钥路径:$(TMPDIR)/k8s-webhook-server/serving-certs/tls.key
webhook server 的证书路径:$(TMPDIR)/k8s-webhook-server/serving-certs/tls.crt

注:如果 $(TMPDIR) 为空,则默认路径为 "/tmp/k8s-webhook-server/...",但 android 系统默认路径为 "/data/local/tmp/k8s-webhook-server/..."

而 MutatingWebhookConfiguration 中的 caBundle 为 ca.crt 的 base64 编码结果。最终 yaml 结果为:

apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
  creationTimestamp: null
  name: mutating-webhook-configuration
webhooks:
- clientConfig:
  caBundle: LS0tLS1CRUdJTiBDRVJ...FLS0tLS0=
  url: https://<server_ip>:9443/mutate-app-o0w0o-cn-v1-app
  failurePolicy: Fail
  name: mapp.kb.io
  rules:
    ...

ValidatingWebhookConfiguration 的修改与 MutatingWebhookConfiguration 类似,只需要注意 server path 与 app_webhook.go 中一致即可。两个配置文件都修改好之后在集群中 apply 一下即可。

运行

最后直接在本地运行 CRD Controller 及 Webhook Server:

make run

验证

简单运行一个 app 试试:

apiVersion: app.o0w0o.cn/v1
kind: App
metadata:
  name: app-sample
spec:
  deploy:
    selector:
      matchLabels:
        app: app-sample
    template:
      metadata:
        name: sample
        labels:
          app: app-sample
      spec:
        containers:
          - name: cn
            image: daocloud.io/library/redis:4.0.14-alpine

查看是否已经注入了 sidecar 容器:

$ kubectl apply -f config/samples/app_v1_app.yaml
$ kubectl get app
NAME         AGE
app-sample   43s
$ kubectl get deploy
NAME                READY   UP-TO-DATE   AVAILABLE   AGE
app-sample-deploy   0/1     1            0           43s
$ kubectl get po
NAME                                 READY   STATUS              RESTARTS   AGE
app-sample-deploy-5b5cfb9c9b-z8jk5   0/2     ContainerCreating   0          43s
2019/10/12 18:17 下午 posted in  Kubernetes

使用 Operator Framework 全家桶构建并维护 operator

Operator Framework

Operator Framework 是 CoreOS 开源的一个用于快速开发维护 Operator 的工具包,该框架包含三个主要的项目:

  • Operator SDK: 无需了解复杂的 Kubernetes API 特性,快速构建一个 Operator 应用。
  • Operator Lifecycle Manager (OLM): 帮助安装、更新和管理跨集群的运行中的所有 Operator(以及他们的相关服务)
  • Operator Metering: 收集 operator metric 信息,并生成报告

通过这三个子项目,Operator Framework 完成了 operator 的生成、维护、监控全过程。下面我们逐个了解一下 Operator Framework 全家桶分别是如何工作的。

Operator SDK

Operator-sdk 帮助我们一键生成 operator 的框架,留出部分函数让我们来完成业务逻辑。

安装 operator-sdk

在 MacOS 系统上开发的话只需要一行命令即可安装:

brew install operator-sdk

Linux 系统可以通过二进制源文件安装:

$ RELEASE_VERSION=v0.10.0
$ curl -OJL https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk-${RELEASE_VERSION}-x86_64-linux-gnu
$ chmod +x operator-sdk-${RELEASE_VERSION}-x86_64-linux-gnu && sudo mkdir -p /usr/local/bin/ && sudo cp operator-sdk-${RELEASE_VERSION}-x86_64-linux-gnu /usr/local/bin/operator-sdk && rm operator-sdk-${RELEASE_VERSION}-x86_64-linux-gnu

初始化项目

Operator 是用 Go 语言开发的,所以在开发项目前,需要安装好 Go 语言环境。而 Operator-sdk 初始化项目只需要几行代码:

$ mkdir -p $HOME/projects/
$ cd $HOME/projects/
$ export GO111MODULE=on
$
$ operator-sdk new app-operator --repo github.com/zwwhdls/app-operator --vendor
$ cd app-operator

生成 CRD 及其 controller

Operator-sdk 主要会帮你生成 CRD 文件和其 controller 框架。

生成 CRD 命令的格式:

$ operator-sdk add api --api-version=<自定义资源的 api version> --kind <自定义资源的 kind>
$ operator-sdk add api --api-version=o0w0o.cn/v1 --kind App

然后在项目中会发现多了几个文件,其中有一个 .../app-operator/pkg/apis/o0w0o/v1/app_types.go ,其中帮你创建好了 App 的资源定义:

type AppSpec struct {
}

type AppStatus struct {
}

type App struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   AppSpec   `json:"spec,omitempty"`
    Status AppStatus `json:"status,omitempty"`
}

而你需要做的是定义 AppSpecAppStatus,分别是描述资源的属性和状态。定义好了之后只需要执行下面这条命令,即可在 CRD Yaml 中自动生成你定义的字段。

$ operator-sdk generate k8s

定义好 CR 之后,我们需要告诉 K8s 创建这个资源的时候需要做什么,也就是需要构建 controller。Operator-sdk 可以一键生成 controller 的框架:

$ operator-sdk add controller --api-version=o0w0o.cn/v1 --kind=App
INFO[0000] Generating controller version app.o0w0o.cn/v1 for kind App.
INFO[0000] Created pkg/controller/app/app_controller.go
INFO[0000] Created pkg/controller/add_app.go
INFO[0000] Controller generation complete.

看日志可以知道,Operator 帮我们创建了两个文件。

其中,add_app.go 是用来将 controller 注册进 manager 中的,相当于帮你生成了一个 K8s 的控制器。
app_controller.go 则是真正定义 controller 的地方。在这个文件里,有两个标注了 TODO(User) 的地方,顾名思义,只有这两个地方需要用户自己写。

func add(mgr manager.Manager, r reconcile.Reconciler) error {
    ... 
    // TODO(user): Modify this to be the types you create that are owned by the primary resource
    ...
    return nil
}

// TODO(user): Modify this Reconcile function to implement your Controller logic.  This example creates
func (r *ReconcileApp) Reconcile(request reconcile.Request) (reconcile.Result, error) {
    ...
}

首先我们要知道,operator 的工作模式是基于事件的,监听到特定的事件后,触发对应的操作。那就不难理解,第一个 TODO 就是定义需要监听哪些资源;第二个 TODO 就是定义监听到这些资源的事件后,需要做哪些操作。

部署

Operator 的部署 Yaml 都是 Operator-sdk 帮你生成好的。在完成 Operator 的开发工作后,只需要将 operator.yaml 中的 image 替换掉,再通过如下步骤部署即可。

// 创建 RBAC 相关
$ kubectl create -f deploy/service_account.yaml
$ kubectl create -f deploy/role.yaml
$ kubectl create -f deploy/role_binding.yaml

// 部署 operator CRD
$ kubectl apply -f deploy/crds/app_v1_app_crd.yaml

// 部署 operator
$ kubectl create -f deploy/operator.yaml

Operator Lifecycle Manager

OLM 扩展了Kubernetes,提供了一种陈述式的方式来安装,管理和升级Operator,以及它在集群中所依赖的资源。它还对其管理的组件强制执行一些约束,以确保良好的用户体验。

该项目使用户能够执行以下操作:

  • 将应用程序定义为封装了需求和元数据的一个 Kubernetes 资源
  • 使用依赖项解析方式自动地安装应用程序,或者使用 kubectl/oc(OpenShift Client) 手动安装应用程序
  • 使用不同的批准策略自动升级应用程序

定义的 CR

Resource Short name Owner Description
ClusterServiceVersion csv OLM OLM 管理的 operator 的基本信息等,包括版本信息、其管理的 CRD、必须安装的 CRD 、依赖、安装方式等
InstallPlan ip Catalog 计算要创建的资源列表,以便自动安装或升级CSV
CatalogSource catsrc Catalog 定义应用程序的CSV,CRD和 package 的存储库。
Subscription sub Catalog 记录何时及如何更新 CSV 等信息,相当于订阅需要观察的 CSV
OperatorGroup og OLM 配置与 OperatorGroup 对象在同一名称空间中部署的所有 Operator,以在名称空间列表或群集范围内查看其 CR

ClusterServiceVersion 可以被收集到 CatalogSource,它通过 InstallPlan 解析依赖实现安装的自动化,并且通过 Subscription 保持使用最新版本。

定义的 operator

OLM 定义了两个 operator 来维护自定义 operator 的生命周期,分别为:

  • OLM operator:创建 CSV 中定义的应用,监听 CSV 的变化,并维持其定义的状态。

  • Catalog Operator:解析并安装 CSV 及其依赖的资源;监听 channels 中 CatalogSources 的变化,并将其更新到最新可用版本;

使用过程

在你的 operator 项目中生成 CSV 和 package 的 yaml:

operator-sdk olm-catalog gen-csv --csv-version xx.yy.zz

需要同时生成 CRD 的话,在上面的命令后面加上:

--update-crds

在本地测试 operator 是否合法:https://github.com/operator-framework/community-operators/blob/master/docs/testing-operators.md#manual-testing-on-kubernetes

最后在 GitHub 上 upstream-community-operators 的子文件夹中提 pr 上传自己的 operator 的 csv 及 package 的 yaml 文件。

在集群上部署 operator 只需要定义 Subscription 文件,安装好 OLM 后 apply sub 文件就可以部署了。

operator Metering

通过 operator-sdk 生成的项目,自带了 metrics 接口。operator Metering 本质上是通过提供 metrics 接口,再通过该接口提供的数据整合监控数据,最终生成数据报告。

该项目主要定义了三个 CR:

ReportDataSources: 定义有哪些可用数据
ReportQueries: 定义如何查询 ReportDataSources 中定义的数据
Reports: 根据 ReportQueries 生成数据报告

2019/08/23 17:07 下午 posted in  Kubernetes

使用 KeyCloak 对 Kubernetes 进行统一用户管理

众所周知,K8s 的权限管理体系 (不熟悉的盆友可以跳转至《Kubernetes 安全机制解读》) 中,可以将 RoleBinding 绑定到 ServiceAccount、User、Group 上来实现权限分配。

其中 ServiceAccount 我们经常用到,用来限制某个 pod 的权限;对于 User 和 Group,除了一些特殊的系统 group,并没有具体的资源与之对应,这对于传统项目中的用户管理十分不友好。

上一篇文章中讲到了 OIDC 协议在 K8s 集群中如何工作,这篇文章来具体讲讲如何在 K8s 集群中进行统一用户管理。

准备工作

首先我们需要有一个 Identity Provider 来统一管理 K8s 中的用户,并且提供 OIDC 协议服务,本文采用了 KeyCloak 作为 Identity Provider。

KeyCloak 中的配置

要想实现用户管理,我们需要利用 K8s 中 group 的概念,来对一组用户分配权限,这需要利用 OIDC 协议中的 Claim 概念,来实现 K8s 中用户的分组。

Claim 是 ID Token 中携带的信息,指的是客户端请求的信息范围,如用户名、邮箱等,而这些可以进行扩展用来携带一些用户所属 group 的信息等等。

那么第一步,我们需要扩展 KeyCloak 中的 Claim,如下图:

我们在 Client 中添加了一项 “User Attribute”,并将其加入到 ID Token 中;Multivalued 必须设置为 ON,保证 "groups" Claim 的值为一个 String 数组,其中每个值代表 User 所属的一个分组,一个 User 可以同时属于多个分组,每个值之间使用逗号分隔。

第二步就是为用户设置 "groups" 属性:

一切都设置好之后,可以在 “admin” 这个用户的 ID Token 中看到其 “groups” 属性:

{
    "jti":"9259af9c-8a3d-45ff-94f6-6780f2a79580",
    "exp":1564739637,
    "nbf":0,
    "iat":1564739337,
    "iss":"https://172.16.105.1:8082/auth/realms/hdls",
    "aud":"kubernetes",
    "sub":"f846ddb1-4435-429f-9ce5-faba7a791d43",
    "typ":"ID",
    "azp":"kubernetes",
    "auth_time":0,
    "session_state":"37b1a2ca-1b3b-4c61-ae2c-f8c14818ca6e",
    "acr":"1",
    "email_verified":false,
    "groups":[
        "manager"
    ],
    "preferred_username":"admin"
}

ApiServer 的配置

ApiServer 中留了几个可配置的环境变量,用以支持 OIDC 插件,官方链接

oidc-issuer-url:OIDC Server 的 URL,只接受 https 协议。
oidc-client-id:OIDC Server 中配置的 client_id,具有唯一性。
oidc-username-claim:指明 ID Token 中用以标识用户名称的 claim。
oidc-username-prefix:用户名前缀,“-” 表示没有前缀。
oidc-groups-claim:ID Token 中可以表明用户所在 group 的 claim,要求该 claim 必须是数组形式,所以用户可以属于多个 group。
oidc-groups-prefix:group 前缀。
oidc-ca-file:Identity Provider 的 CA 证书所在路径。

本文的配置参数如下:

    - --oidc-issuer-url=https://172.16.105.1:8082/auth/realms/hdls
    - --oidc-client-id=kubernetes
    - --oidc-username-claim=preferred_username
    - --oidc-username-prefix=-
    - --oidc-groups-claim=groups
    - --oidc-ca-file=/etc/kubernetes/pki/ca.crt

KubeConfig 的配置

作为用户,我们需要通过 Client Application 来访问 API Server,kubectl 显然是首选 Client,让 kubectl 以我们创建的用户 "admin" 的身份来访问 Kubernetes,并通过身份认证,而这需要对 KubeConfig 进行配置,来完成以下几个流程:

  1. 创建一个 kubeconfig user:"admin"。
  2. 为 "admin" 配置上 client-id,client credential,id-token,refresh-token,certficaite 等等属性。
  3. 为 "admin" 创建的一个 user context。
  4. 将它设置为当前 context。

下面的命令可以一键生成配置:

kubectl config set-credentials USER_NAME \
   --auth-provider=oidc \
   --auth-provider-arg=idp-issuer-url=( issuer url ) \
   --auth-provider-arg=client-id=( your client id ) \
   --auth-provider-arg=client-secret=( your client secret ) \
   --auth-provider-arg=refresh-token=( your refresh token ) \
   --auth-provider-arg=idp-certificate-authority=( path to your ca certificate ) \
   --auth-provider-arg=id-token=( your id_token )

Token 的获取方式

ID Token 和 Refresh Token 的生成方式有很多种,其中最简单的方式是使用 curl 进行 Password Grant 方式的身份认证,从而获取想要的 ID Token 和 Refresh Token:

$ curl -k 'https://172.16.105.1:8082/auth/realms/hdls/protocol/openid-connect/token' -d "client_id=kubernetes" -d "client_secret=40dc1fef...c3eeec6" -d "response_type=code token" -d "grant_type=password" -d "username=test" -d "password=dangerous" -d "scope=openid"
{
    "access_token":"eyJhbGciOiJSU...0CMPw",
    "expires_in":300,
    "refresh_expires_in":1800,
    "refresh_token":"eyJhbGciOiJ...W1VUA",
    "token_type":"bearer",
    "id_token":"eyJhbGc...z3TaGJGQ",
    "not-before-policy":0,
    "session_state":"2845e...92ff2",
    "scope":"openid profile email"
}

然而这每次都需要手动生成 Token 并填写进 KubeConfig 中,非常麻烦。好消息是,社区已经有很多可以帮你自动将 Token 写入 KubeConfig 中的工具,非常好用,比如:

  1. kubelogin
  2. k8s-auth-client
  3. k8s-keycloak-oidc-helper
  4. kuberos
  5. k8s-oidc-helper

用户管理

在将一切都配置成功后,我们再来看给用户分配权限。这需要考虑到 K8s 的 RBAC 系统。

RBAC

对 group 为 manager 的用户,我们对其赋予系统自带的 "cluster-admin" 角色,即为 cluster 的管理员权限:

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: keycloak-admin-group
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: Group
  name: manager
  apiGroup: rbac.authorization.k8s.io

我们在 keyCloak 中将 admin 用户 “加入” 到 “manager” 组中:

然后用该用户访问 APIServer :

[root@172-16-105-1 ~]# kubelogin --username=admin --password=dangerous
You got a valid token until 2019-08-03 15:32:42 +0800 CST
[root@172-16-105-1 ~]#
[root@172-16-105-1 ~]# kubectl get cs
NAME                 STATUS    MESSAGE             ERROR
controller-manager   Healthy   ok
scheduler            Healthy   ok
etcd-0               Healthy   {"health":"true"}
[root@172-16-105-1 ~]# kubectl get no
NAME           STATUS   ROLES    AGE   VERSION
172-16-105-1   Ready    master   54d   v1.14.1
172-16-105-2   Ready    <none>   54d   v1.14.1
[root@172-16-105-1 ~]# kubectl get po
No resources found.
[root@172-16-105-1 ~]# kubectl get all
NAME                     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/kubernetes       ClusterIP   10.96.0.1        <none>        443/TCP        54d

可以看到 “admin” 用户有所有资源的权限。

然后,我们再对 group 为 developer 的用户,新建一个角色叫做 "hdls-role",只给他们对 pod 的查看权限:

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: hdls-role
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: hdls-rolebinding
roleRef:
  kind: ClusterRole
  name: hdls-role
  apiGroup: rbac.authorization.k8s.io
subjects:
- kind: Group
  name: developer
  apiGroup: rbac.authorization.k8s.io

在 keyCloak 中将 test 用户 “加入” 到 “developer” 组中:

然后用该用户访问 APIServer :

[root@172-16-105-1 ~]# kubelogin --username=test --password=dangerous
You got a valid token until 2019-08-03 15:40:21 +0800 CST
[root@172-16-105-1 ~]#
[root@172-16-105-1 ~]# kubectl get po
No resources found.
[root@172-16-105-1 ~]# kubectl get no
Error from server (Forbidden): nodes is forbidden: User "test" cannot list resource "nodes" in API group "" at the cluster scope
[root@172-16-105-1 ~]#
[root@172-16-105-1 ~]# kubectl get cs
Error from server (Forbidden): componentstatuses is forbidden: User "test" cannot list resource "componentstatuses" in API group "" at the cluster scope
[root@172-16-105-1 ~]#
[root@172-16-105-1 ~]# kubectl get all
Error from server (Forbidden): replicationcontrollers is forbidden: User "test" cannot list resource "replicationcontrollers" in API group "" in the namespace "default"
Error from server (Forbidden): services is forbidden: User "test" cannot list resource "services" in API group "" in the namespace "default"
Error from server (Forbidden): daemonsets.apps is forbidden: User "test" cannot list resource "daemonsets" in API group "apps" in the namespace "default"
Error from server (Forbidden): deployments.apps is forbidden: User "test" cannot list resource "deployments" in API group "apps" in the namespace "default"
Error from server (Forbidden): replicasets.apps is forbidden: User "test" cannot list resource "replicasets" in API group "apps" in the namespace "default"
Error from server (Forbidden): statefulsets.apps is forbidden: User "test" cannot list resource "statefulsets" in API group "apps" in the namespace "default"
Error from server (Forbidden): horizontalpodautoscalers.autoscaling is forbidden: User "test" cannot list resource "horizontalpodautoscalers" in API group "autoscaling" in the namespace "default"
Error from server (Forbidden): jobs.batch is forbidden: User "test" cannot list resource "jobs" in API group "batch" in the namespace "default"
Error from server (Forbidden): cronjobs.batch is forbidden: User "test" cannot list resource "cronjobs" in API group "batch" in the namespace "default"

test 用户除了能够获取 pod 信息,对于其他资源处处受限。

总结

本文仅仅通过 KeyCloak 和 kubectl 向大家介绍了 K8s 中如何进行用户管理,相应地,如果自己的用户中心实现 OIDC 协议,并且客户端通过 ID Token 以 "bearer token" 的方式来访问 APIServer,就能真正的将 K8s 的权限系统与上层建筑打通。

2019/08/02 15:42 下午 posted in  Kubernetes

Kubernetes 服务发现之 coreDNS

服务发现是 K8s 的一项很重要的功能。K8s 的服务发现有两种方式,一种是将 svc 的 ClusterIP 以环境变量的方式注入到 pod 中;一种就是 DNS,从 1.13 版本开始,coreDNS 就取代了 kube dns 成为了内置的 DNS 服务器。这篇文章就来简单分析一下 coreDNS。

K8s DNS 策略

Kubernetes 中 Pod 的 DNS 策略有四种类型。

  1. Default:Pod 继承所在主机上的 DNS 配置;

  2. ClusterFirst:K8s 的默认设置;先在 K8s 集群配置的 coreDNS 中查询,查不到的再去继承自主机的上游 nameserver 中查询;

  3. ClusterFirstWithHostNet:对于网络配置为 hostNetwork 的 Pod 而言,其 DNS 配置规则与 ClusterFirst 一致;

  4. None:忽略 K8s 环境的 DNS 配置,只认 Pod 的 dnsConfig 设置。

下面主要来了解一下 coreDNS 解析域名的过程。

resolv.conf 文件分析

在部署 pod 的时候,如果用的是 K8s 集群的 DNS,那么 kubelet 在起 pause 容器的时候,会将其 DNS 解析配置初始化成集群内的配置。

比如我创建了一个叫 my-nginx 的 deployment,其 pod 中的 resolv.conf 文件如下:

[root@localhost ~]# kubectl exec -it my-nginx-b67c7f44-hsnpv cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

在集群中 pod 之间互相用 svc name 访问的时候,会根据 resolv.conf 文件的 DNS 配置来解析域名,下面来分析具体的过程。

域名解析的过程

pod 的 resolv.conf 文件主要有三个部分,分别为 nameserver、search 和 option。而这三个部分可以由 K8s 指定,也可以通过 pod.spec.dnsConfig 字段自定义。

nameserver

resolv.conf 文件的第一行 nameserver 指定的是 DNS 服务的 IP,这里就是 coreDNS 的 clusterIP:

[root@localhost ~]# kubectl -n kube-system get svc |grep dns
kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   32d

也就是说所有域名的解析,都要经过 coreDNS 的虚拟 IP 10.96.0.10 进行解析,不论是 Kubernetes 内部域名还是外部的域名。

search 域

resolv.conf 文件的第二行指定的是 DNS search 域。解析域名的时候,将要访问的域名依次带入 search 域,进行 DNS 查询。

比如我要在刚才那个 pod 中访问一个域名为 your-nginx 的服务,其进行的 DNS 域名查询的顺序是:

your-nginx.default.svc.cluster.local. -> your-nginx.svc.cluster.local. -> your-nginx.cluster.local.    

直到查到为止。

options

resolv.conf 文件的第三行指定的是其他项,最常见的是 dnotsdnots 指的是如果查询的域名包含的点 “.” 小于 5,则先走 search 域,再用绝对域名;如果查询的域名包含点数大于或等于 5,则先用绝对域名,再走 search 域。K8s 中默认的配置是 5。

也就是说,如果我访问的是 a.b.c.e.f.g ,那么域名查找的顺序如下:

a.b.c.e.f.g. -> a.b.c.e.f.g.default.svc.cluster.local. -> a.b.c.e.f.g.svc.cluster.local. -> a.b.c.e.f.g.cluster.local.

如果我访问的是 a.b.c.e,那么域名查找的顺序如下:

a.b.c.e.default.svc.cluster.local. -> a.b.c.e.svc.cluster.local. -> a.b.c.e.cluster.local. -> a.b.c.e.

pod 之间的通信

在了解完了域名解析的过程后,再来了解一下 pod 之间的通信。

通过 svc 访问

众所周知,在 K8s 中,Pod 之间通过 svc 访问的时候,会经过 DNS 域名解析,再拿到 ip 通信。而 K8s 的域名全称为 "<service-name>.<namespace>.svc.cluster.local",而我们通常只需将 svc name 当成域名就能访问到 pod,这一点通过上面的域名解析过程并不难理解。

我们来看个例子。有两个 deployment,一个叫 busybox,在 default 这个 namespace 下;一个叫 your-nginx,在 hdls 这个 namespace 下,svc 同名。我们在 busybox 中尝试访问 your-nginx

[root@localhost ~]# kubectl get po
NAME                           READY   STATUS    RESTARTS   AGE
busybox-5bbb5d7ff7-dh68j       1/1     Running   0          8m35s
[root@localhost ~]#
[root@localhost ~]# kubectl exec -it busybox-5bbb5d7ff7-dh68j sh
/ # wget your-nginx
wget: bad address 'your-nginx'
/ #
/ # wget your-nginx.hdls
Connecting to your-nginx.hdls (10.100.3.148:80)
saving to 'index.html'
index.html           100% |*****************************************************|   612  0:00:00 ETA
'index.html' saved
/ #
[root@localhost ~]# kubectl -n hdls get svc
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
your-nginx   ClusterIP   10.100.3.148   <none>        80/TCP    14m

可以看到,当直接用 your-nginx 去访问的时候,提示 bad address,说明域名错了,因为在不同的 namespace 下,所有的 search 域都找过了还是找不到;当用 your-nginx.hdls 去访问的时候,会解析到 10.100.3.148 这个 IP,而这个 IP 正是 your-nginx 的 ClusterIP。

所以,在不同的 namespace 下的 pod 通过 svc 访问的时候,需要在 svc name 后面加上 .<namespace>

pod 的 hostname 与 subdomain

在 K8s 中,如果不指定 pod 的 hostname,其默认为 pod.metadata.name,通过 spec.hostname 字段可以自定义;另外还可以给 pod 设置 subdomain,通过 spec.subdomain 字段。比如下面这个例子:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    name: nginx
spec:
  hostname: domain-test
  subdomain: subdomain-test
  containers:
  - image: nginx
    name: nginx
---
apiVersion: v1
kind: Service
metadata:
  name: subdomain-test
spec:
  selector:
    name: nginx
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP

可以查看这个 pod 的 hostname 和 hosts 文件:

[root@localhost ~]# kubectl get po -owide
NAME                           READY   STATUS    RESTARTS   AGE     IP             NODE           NOMINATED NODE   READINESS GATES
busybox-5bbb5d7ff7-dh68j       1/1     Running   0          112m    10.244.1.246   172-16-105-2   <none>           <none>
nginx                          1/1     Running   0          2m      10.244.1.253   172-16-105-2   <none>           <none>
[root@localhost ~]# kubectl exec -it nginx bash
root@domain-test:/# cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1   localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
fe00::0 ip6-mcastprefix
fe00::1 ip6-allnodes
fe00::2 ip6-allrouters
10.244.1.253    domain-test.subdomain-test.default.svc.cluster.local    domain-test
root@domain-test:/#

在 busybox 容器中访问这个 pod:

[root@localhost ~]# kubectl exec -it busybox-5bbb5d7ff7-dh68j sh
/ # wget domain-test.subdomain-test
Connecting to domain-test.subdomain-test (10.244.1.253:80)
saving to 'index.html'
index.html           100% |*****************************************************|   612  0:00:00 ETA
'index.html' saved
/ #
/ # wget subdomain-test
Connecting to subdomain-test (10.108.213.70:80)
wget: can't open 'index.html': File exists
/ #

可以看到,当访问 domain-test.subdomain-test 解析出来的是 10.244.1.253,这个是 nginx 的 pod ip,而不是 clusterIP;而访问 subdomain-test 时,解析出来的是 10.108.213.70,这是 clusterIP,属于正常的 svc name 途径。

coreDNS Corefile 文件

CoreDNS 实现了应用的插件化,用户可以选择所需的插件编译到可执行文件中;CoreDNS 的配置文件是 Corefile 形式的,下面是个 coreDNS 的 configMap 例子。

[root@localhost ~]# kubectl -n kube-system get cm coredns -oyaml
apiVersion: v1
data:
  Corefile: |
    .:53 {
        errors
        health
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           upstream
           fallthrough in-addr.arpa ip6.arpa
        }
        prometheus :9153
        forward . /etc/resolv.conf
        cache 30
        loop
        reload
        loadbalance
    }
kind: ConfigMap
metadata:
  creationTimestamp: "2019-06-10T03:19:01Z"
  name: coredns
  namespace: kube-system
  resourceVersion: "3380134"
  selfLink: /api/v1/namespaces/kube-system/configmaps/coredns
  uid: 7e845ca2-8b2e-11e9-b4eb-005056b40224

Corefile 文件分析

第一部分:

kubernetes cluster.local in-addr.arpa ip6.arpa {
   pods insecure
   upstream
   fallthrough in-addr.arpa ip6.arpa
}

指明 cluster.local 后缀的域名,都是 kubernetes 内部域名,coredns 会监听 service 的变化来维护域名关系,所以cluster.local 相关域名都在这里解析。

第二部分:

proxy . /etc/resolv.conf

proxy 指 coredns 中没有找到记录,则去 /etc/resolv.conf 中的 nameserver 请求解析,而 coredns 容器中的 /etc/resolv.conf 是继承自宿主机的。
实际效果是如果不是 k8s 内部域名,就会去默认的 dns 服务器请求解析,并返回给 coredns 的请求者。

第三部分:

prometheus:CoreDNS 的监控地址为: http://localhost:9153/metrics ,满足 Prometheus 的格式。

cache:允许缓存

loop:如果找到循环,则检测简单的转发循环并停止 CoreDNS 进程。

reload:允许 Corefile 的配置自动更新。在更改 ConfigMap 后两分钟,修改生效

loadbalance:这是一个循环 DNS 负载均衡器,可以在答案中随机化 A,AAAA 和 MX 记录的顺序。

指定 hosts

有的时候,某个域名的服务在集群外,而我希望在集群内访问到,我们可以在 corefile 中指定 hosts 的方法实现。具体方式是将域名及对应的 ip 以 hosts 插件的方式加入到 corefile 中,如下:

hosts {
    10.244.1.245 other-company.com
    fallthrough
}

其中,10.244.1.245your-nginx 的 pod ip。然后再在上面的 busybox pod 中访问 other-company.com 这个服务,情况如下:

[root@localhost ~]# kubectl exec -it busybox-5bbb5d7ff7-dh68j sh
/ # wget other-company.com
Connecting to other-company.com (10.244.1.245:80)
saving to 'index.html'
index.html           100% |*****************************************************|   612  0:00:00 ETA
'index.html' saved
/ #
2019/07/11 19:11 下午 posted in  Kubernetes