Kubernetes Service

Kubernetes Pod 是有生命周期的,它们可以被创建,也可以被销毁,然而一旦被销毁生命就永远结束。 通过 ReplicationController 能够动态地创建和销毁 Pod(例如,需要进行扩缩容,或者执行 滚动升级)。 每个 Pod 都会获取它自己的 IP 地址,即使这些 IP 地址不总是稳定可依赖的。 这会导致一个问题:在 Kubernetes 集群中,如果一组 Pod(称为 backend)为其它 Pod (称为 frontend)提供服务,那么那些 frontend 该如何发现,并连接到这组 Pod 中的哪些 backend 呢?

关于 Service

Kubernetes Service 定义了这样一种抽象:一个 Pod 的逻辑分组,一种可以访问它们的策略 —— 通常称为微服务。 这一组 Pod 能够被 Service 访问到,通常是通过 Label Selector(查看下面了解,为什么可能需要没有 selector 的 Service)实现的。

举个例子,考虑一个图片处理 backend,它运行了3个副本。这些副本是可互换的 —— frontend 不需要关心它们调用了哪个 backend 副本。 然而组成这一组 backend 程序的 Pod 实际上可能会发生变化,frontend 客户端不应该也没必要知道,而且也不需要跟踪这一组 backend 的状态。 Service 定义的抽象能够解耦这种关联。

对 Kubernetes 集群中的应用,Kubernetes 提供了简单的 Endpoints API,只要 Service 中的一组 Pod 发生变更,应用程序就会被更新。 对非 Kubernetes 集群中的应用,Kubernetes 提供了基于 VIP 的网桥的方式访问 Service,再由 Service 重定向到 backend Pod。

定义 Service

一个 Service 在 Kubernetes 中是一个 REST 对象,和 Pod 类似。 像所有的 REST 对象一样, Service 定义可以基于 POST 方式,请求 apiserver 创建新的实例。 例如,假定有一组 Pod,它们对外暴露了 9376 端口,同时还被打上 "app=MyApp" 标签。

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376

上述配置将创建一个名称为 “my-service” 的 Service 对象,它会将请求代理到使用 TCP 端口 9376,并且具有标签 "app=MyApp" 的 Pod 上。 这个 Service 将被指派一个 IP 地址(通常称为 “Cluster IP”),它会被服务的代理使用(见下面)。 该 Service 的 selector 将会持续评估,处理结果将被 POST 到一个名称为 “my-service” 的 Endpoints 对象上。

需要注意的是, Service 能够将一个接收端口映射到任意的 targetPort。 默认情况下,targetPort 将被设置为与 port 字段相同的值。 可能更有趣的是,targetPort 可以是一个字符串,引用了 backend Pod 的一个端口的名称。 但是,实际指派给该端口名称的端口号,在每个 backend Pod 中可能并不相同。 对于部署和设计 Service ,这种方式会提供更大的灵活性。 例如,可以在 backend 软件下一个版本中,修改 Pod 暴露的端口,并不会中断客户端的调用。

Kubernetes Service 能够支持 TCP 和 UDP 协议,默认 TCP 协议。

没有 selector 的 Service

Servcie 抽象了该如何访问 Kubernetes Pod,但也能够抽象其它类型的 backend,例如:

  • 希望在生产环境中使用外部的数据库集群,但测试环境使用自己的数据库。
  • 希望服务指向另一个 Namespace 中或其它集群中的服务。
  • 正在将工作负载转移到 Kubernetes 集群,和运行在 Kubernetes 集群之外的 backend。

在任何这些场景中,都能够定义没有 selector 的 Service :

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376

由于这个 Service 没有 selector,就不会创建相关的 Endpoints 对象。可以手动将 Service 映射到指定的 Endpoints:

kind: Endpoints
apiVersion: v1
metadata:
  name: my-service
subsets:
  - addresses:
      - ip: 1.2.3.4
    ports:
      - port: 9376

注意:Endpoint IP 地址不能是 loopback(127.0.0.0/8)、 link-local(169.254.0.0/16)、或者 link-local 多播(224.0.0.0/24)。

访问没有 selector 的 Service,与有 selector 的 Service 的原理相同。请求将被路由到用户定义的 Endpoint(该示例中为 1.2.3.4:9376)。

ExternalName Service 是 Service 的特例,它没有 selector,也没有定义任何的端口和 Endpoint。 相反地,对于运行在集群外部的服务,它通过返回该外部服务的别名这种方式来提供服务。

kind: Service
apiVersion: v1
metadata:
  name: my-service
  namespace: prod
spec:
  type: ExternalName
  externalName: my.database.example.com

当查询主机 my-service.prod.svc.CLUSTER时,集群的 DNS 服务将返回一个值为 my.database.example.com 的 CNAME 记录。 访问这个服务的工作方式与其它的相同,唯一不同的是重定向发生在 DNS 层,而且不会进行代理或转发。 如果后续决定要将数据库迁移到 Kubernetes 集群中,可以启动对应的 Pod,增加合适的 Selector 或 Endpoint,修改 Service 的 type。

VIP 和 Service 代理

在 Kubernetes 集群中,每个 Node 运行一个 kube-proxy 进程。kube-proxy 负责为 Service 实现了一种 VIP(虚拟 IP)的形式,而不是 ExternalName 的形式。 在 Kubernetes v1.0 版本,代理完全在 userspace。在 Kubernetes v1.1 版本,新增了 iptables 代理,但并不是默认的运行模式。 从 Kubernetes v1.2 起,默认就是 iptables 代理。

在 Kubernetes v1.0 版本,Service 是 “4层”(TCP/UDP over IP)概念。 在 Kubernetes v1.1 版本,新增了 Ingress API(beta 版),用来表示 “7层”(HTTP)服务。

userspace 代理模式

这种模式,kube-proxy 会监视 Kubernetes master 对 Service 对象和 Endpoints 对象的添加和移除。 对每个 Service,它会在本地 Node 上打开一个端口(随机选择)。 任何连接到“代理端口”的请求,都会被代理到 Service 的backend Pods 中的某个上面(如 Endpoints 所报告的一样)。 使用哪个 backend Pod,是基于 Service 的 SessionAffinity 来确定的。 最后,它安装 iptables 规则,捕获到达该 Service 的 clusterIP(是虚拟 IP)和 Port 的请求,并重定向到代理端口,代理端口再代理请求到 backend Pod。

网络返回的结果是,任何到达 Service 的 IP:Port 的请求,都会被代理到一个合适的 backend,不需要客户端知道关于 Kubernetes、Service、或 Pod 的任何信息。

默认的策略是,通过 round-robin 算法来选择 backend Pod。 实现基于客户端 IP 的会话亲和性,可以通过设置 service.spec.sessionAffinity 的值为 "ClientIP" (默认值为 "None")。

userspace代理模式下Service概览图

iptables 代理模式

这种模式,kube-proxy 会监视 Kubernetes master 对 Service 对象和 Endpoints 对象的添加和移除。 对每个 Service,它会安装 iptables 规则,从而捕获到达该 Service 的 clusterIP(虚拟 IP)和端口的请求,进而将请求重定向到 Service 的一组 backend 中的某个上面。 对于每个 Endpoints 对象,它也会安装 iptables 规则,这个规则会选择一个 backend Pod。

默认的策略是,随机选择一个 backend。 实现基于客户端 IP 的会话亲和性,可以将 service.spec.sessionAffinity 的值设置为 "ClientIP" (默认值为 "None")。

和 userspace 代理类似,网络返回的结果是,任何到达 Service 的 IP:Port 的请求,都会被代理到一个合适的 backend,不需要客户端知道关于 Kubernetes、Service、或 Pod 的任何信息。 这应该比 userspace 代理更快、更可靠。然而,不像 userspace 代理,如果初始选择的 Pod 没有响应,iptables 代理能够自动地重试另一个 Pod,所以它需要依赖 readiness probes

iptables代理模式下Service概览图

多端口 Service

很多 Service 需要暴露多个端口。对于这种情况,Kubernetes 支持在 Service 对象中定义多个端口。 当使用多个端口时,必须给出所有的端口的名称,这样 Endpoint 就不会产生歧义,例如:

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
    selector:
      app: MyApp
    ports:
      - name: http
        protocol: TCP
        port: 80
        targetPort: 9376
      - name: https
        protocol: TCP
        port: 443
        targetPort: 9377

选择自己的 IP 地址

在 Service 创建的请求中,可以通过设置 spec.clusterIP 字段来指定自己的集群 IP 地址。 比如,希望替换一个已经已存在的 DNS 条目,或者遗留系统已经配置了一个固定的 IP 且很难重新配置。 用户选择的 IP 地址必须合法,并且这个 IP 地址在 service-cluster-ip-range CIDR 范围内,这对 API Server 来说是通过一个标识来指定的。 如果 IP 地址不合法,API Server 会返回 HTTP 状态码 422,表示值不合法。

为何不使用 round-robin DNS?

一个不时出现的问题是,为什么我们都使用 VIP 的方式,而不使用标准的 round-robin DNS,有如下几个原因:

  • 长久以来,DNS 库都没能认真对待 DNS TTL、缓存域名查询结果
  • 很多应用只查询一次 DNS 并缓存了结果
    • 就算应用和库能够正确查询解析,每个客户端反复重解析造成的负载也是非常难以管理的

我们尽力阻止用户做那些对他们没有好处的事情,如果很多人都来问这个问题,我们可能会选择实现它。

服务发现

Kubernetes 支持2种基本的服务发现模式 —— 环境变量和 DNS。

环境变量

当 Pod 运行在 Node 上,kubelet 会为每个活跃的 Service 添加一组环境变量。 它同时支持 Docker links兼容 变量(查看 makeLinkVariables)、简单的 {SVCNAME}_SERVICE_HOST 和 {SVCNAME}_SERVICE_PORT 变量,这里 Service 的名称需大写,横线被转换成下划线。

举个例子,一个名称为 "redis-master" 的 Service 暴露了 TCP 端口 6379,同时给它分配了 Cluster IP 地址 10.0.0.11,这个 Service 生成了如下环境变量:

REDIS_MASTER_SERVICE_HOST=10.0.0.11
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_PORT=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11

这意味着需要有顺序的要求 —— Pod 想要访问的任何 Service 必须在 Pod 自己之前被创建,否则这些环境变量就不会被赋值。DNS 并没有这个限制。

DNS

一个可选(尽管强烈推荐)集群插件 是 DNS 服务器。 DNS 服务器监视着创建新 Service 的 Kubernetes API,从而为每一个 Service 创建一组 DNS 记录。 如果整个集群的 DNS 一直被启用,那么所有的 Pod 应该能够自动对 Service 进行名称解析。

例如,有一个名称为 "my-service" 的 Service,它在 Kubernetes 集群中名为 "my-ns" 的 Namespace 中,为 "my-service.my-ns" 创建了一条 DNS 记录。 在名称为 "my-ns" 的 Namespace 中的 Pod 应该能够简单地通过名称查询找到 "my-service"。 在另一个 Namespace 中的 Pod 必须限定名称为 "my-service.my-ns"。 这些名称查询的结果是 Cluster IP。

Kubernetes 也支持对端口名称的 DNS SRV(Service)记录。 如果名称为 "my-service.my-ns" 的 Service 有一个名为 "http" 的 TCP 端口,可以对 "_http._tcp.my-service.my-ns" 执行 DNS SRV 查询,得到 "http" 的端口号。

Kubernetes DNS 服务器是唯一的一种能够访问 ExternalName 类型的 Service 的方式。 更多信息可以查看DNS Pod 和 Service

Headless Service

有时不需要或不想要负载均衡,以及单独的 Service IP。 遇到这种情况,可以通过指定 Cluster IP(spec.clusterIP)的值为 "None" 来创建 Headless Service。

这个选项允许开发人员自由寻找他们自己的方式,从而降低与 Kubernetes 系统的耦合性。 应用仍然可以使用一种自注册的模式和适配器,对其它需要发现机制的系统能够很容易地基于这个 API 来构建。

对这类 Service 并不会分配 Cluster IP,kube-proxy 不会处理它们,而且平台也不会为它们进行负载均衡和路由。 DNS 如何实现自动配置,依赖于 Service 是否定义了 selector。

配置 Selector

对定义了 selector 的 Headless Service,Endpoint 控制器在 API 中创建了 Endpoints 记录,并且修改 DNS 配置返回 A 记录(地址),通过这个地址直接到达 Service 的后端 Pod上。

不配置 Selector

对没有定义 selector 的 Headless Service,Endpoint 控制器不会创建 Endpoints 记录。 然而 DNS 系统会查找和配置,无论是:

  • ExternalName 类型 Service 的 CNAME 记录
    • 记录:与 Service 共享一个名称的任何 Endpoints,以及所有其它类型

发布服务 —— 服务类型

对一些应用(如 Frontend)的某些部分,可能希望通过外部(Kubernetes 集群外部)IP 地址暴露 Service。

Kubernetes ServiceTypes 允许指定一个需要的类型的 Service,默认是 ClusterIP 类型。

Type 的取值以及行为如下:

  • ClusterIP:通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的 ServiceType。
  • NodePort:通过每个 Node 上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。通过请求 <NodeIP>:<NodePort>,可以从集群的外部访问一个 NodePort 服务。
  • LoadBalancer:使用云提供商的负载局衡器,可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和 ClusterIP 服务。
  • ExternalName:通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如, foo.bar.example.com)。 没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持。

NodePort 类型

如果设置 type 的值为 "NodePort",Kubernetes master 将从给定的配置范围内(默认:30000-32767)分配端口,每个 Node 将从该端口(每个 Node 上的同一端口)代理到 Service。该端口将通过 Service 的 spec.ports[*].nodePort 字段被指定。

如果需要指定的端口号,可以配置 nodePort 的值,系统将分配这个端口,否则调用 API 将会失败(比如,需要关心端口冲突的可能性)。

这可以让开发人员自由地安装他们自己的负载均衡器,并配置 Kubernetes 不能完全支持的环境参数,或者直接暴露一个或多个 Node 的 IP 地址。

需要注意的是,Service 将能够通过 <NodeIP>:spec.ports[*].nodePort 和 spec.clusterIp:spec.ports[*].port 而对外可见。

LoadBalancer 类型

使用支持外部负载均衡器的云提供商的服务,设置 type 的值为 "LoadBalancer",将为 Service 提供负载均衡器。 负载均衡器是异步创建的,关于被提供的负载均衡器的信息将会通过 Service 的 status.loadBalancer 字段被发布出去。

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376
      nodePort: 30061
  clusterIP: 10.0.171.239
  loadBalancerIP: 78.11.24.19
  type: LoadBalancer
status:
  loadBalancer:
    ingress:
      - ip: 146.148.47.155

来自外部负载均衡器的流量将直接打到 backend Pod 上,不过实际它们是如何工作的,这要依赖于云提供商。 在这些情况下,将根据用户设置的 loadBalancerIP 来创建负载均衡器。 某些云提供商允许设置 loadBalancerIP。如果没有设置 loadBalancerIP,将会给负载均衡器指派一个临时 IP。 如果设置了 loadBalancerIP,但云提供商并不支持这种特性,那么设置的 loadBalancerIP 值将会被忽略掉。

AWS 内部负载均衡器

在混合云环境中,有时从虚拟私有云(VPC)环境中的服务路由流量是非常有必要的。 可以通过在 Service 中增加 annotation 来实现,如下所示:

[...]
metadata: 
    name: my-service
    annotations: 
        service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0
[...]

在水平分割的 DNS 环境中,需要两个 Service 来将外部和内部的流量路由到 Endpoint 上。

AWS SSL 支持

对运行在 AWS 上部分支持 SSL 的集群,从 1.3 版本开始,可以为 LoadBalancer 类型的 Service 增加两个 annotation:

    metadata:
      name: my-service
      annotations:
        service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012

第一个 annotation 指定了使用的证书。它可以是第三方发行商发行的证书,这个证书或者被上传到 IAM,或者由 AWS 的证书管理器创建。

    metadata:
      name: my-service
      annotations:
         service.beta.kubernetes.io/aws-load-balancer-backend-protocol: (https|http|ssl|tcp)

第二个 annotation 指定了 Pod 使用的协议。 对于 HTTPS 和 SSL,ELB 将期望该 Pod 基于加密的连接来认证自身。

HTTP 和 HTTPS 将选择7层代理:ELB 将中断与用户的连接,当转发请求时,会解析 Header 信息并添加上用户的 IP 地址(Pod 将只能在连接的另一端看到该 IP 地址)。

TCP 和 SSL 将选择4层代理:ELB 将转发流量,并不修改 Header 信息。

外部 IP

如果外部的 IP 路由到集群中一个或多个 Node 上,Kubernetes Service 会被暴露给这些 externalIPs。 通过外部 IP(作为目的 IP 地址)进入到集群,打到 Service 的端口上的流量,将会被路由到 Service 的 Endpoint 上。 externalIPs 不会被 Kubernetes 管理,它属于集群管理员的职责范畴。

根据 Service 的规定,externalIPs 可以同任意的 ServiceType 来一起指定。 在上面的例子中,my-service 可以在 80.11.12.10:80(外部 IP:端口)上被客户端访问。

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 9376
  externalIPs: 
    - 80.11.12.10

不足之处

为 VIP 使用 userspace 代理,将只适合小型到中型规模的集群,不能够扩展到上千 Service 的大型集群。 查看 最初设计方案 获取更多细节。

使用 userspace 代理,隐藏了访问 Service 的数据包的源 IP 地址。 这使得一些类型的防火墙无法起作用。 iptables 代理不会隐藏 Kubernetes 集群内部的 IP 地址,但却要求客户端请求必须通过一个负载均衡器或 Node 端口。

Type 字段支持嵌套功能 —— 每一层需要添加到上一层里面。 不会严格要求所有云提供商(例如,GCE 就没必要为了使一个 LoadBalancer 能工作而分配一个 NodePort,但是 AWS 需要 ),但当前 API 是强制要求的。

未来工作

未来我们能预见到,代理策略可能会变得比简单的 round-robin 均衡策略有更多细微的差别,比如 master 选举或分片。 我们也能想到,某些 Service 将具有 “真正” 的负载均衡器,这种情况下 VIP 将简化数据包的传输。

我们打算为 L7(HTTP)Service 改进我们对它的支持。

我们打算为 Service 实现更加灵活的请求进入模式,这些 Service 包含当前 ClusterIP、NodePort 和 LoadBalancer 模式,或者更多。

VIP 的那些骇人听闻的细节

对很多想使用 Service 的人来说,前面的信息应该足够了。 然而,有很多内部原理性的内容,还是值去理解的。

避免冲突

Kubernetes 最主要的哲学之一,是用户不应该暴露那些能够导致他们操作失败、但又不是他们的过错的场景。 这种场景下,让我们来看一下网络端口 —— 用户不应该必须选择一个端口号,而且该端口还有可能与其他用户的冲突。 这就是说,在彼此隔离状态下仍然会出现失败。

为了使用户能够为他们的 Service 选择一个端口号,我们必须确保不能有2个 Service 发生冲突。 我们可以通过为每个 Service 分配它们自己的 IP 地址来实现。

为了保证每个 Service 被分配到一个唯一的 IP,需要一个内部的分配器能够原子地更新 etcd 中的一个全局分配映射表,这个更新操作要先于创建每一个 Service。 为了使 Service能够获取到 IP,这个映射表对象必须在注册中心存在,否则创建 Service 将会失败,指示一个 IP 不能被分配。 一个后台 Controller 的职责是创建映射表(从 Kubernetes 的旧版本迁移过来,旧版本中是通过在内存中加锁的方式实现),并检查由于管理员干预和清除任意 IP 造成的不合理分配,这些 IP 被分配了但当前没有 Service 使用它们。

IP 和 VIP

不像 Pod 的 IP 地址,它实际路由到一个固定的目的地,Service 的 IP 实际上不能通过单个主机来进行应答。 相反,我们使用 iptables(Linux 中的数据包处理逻辑)来定义一个虚拟IP地址(VIP),它可以根据需要透明地进行重定向。 当客户端连接到 VIP 时,它们的流量会自动地传输到一个合适的 Endpoint。 环境变量和 DNS,实际上会根据 Service 的 VIP 和端口来进行填充。

Userspace

作为一个例子,考虑前面提到的图片处理应用程序。 当创建 backend Service 时,Kubernetes master 会给它指派一个虚拟 IP 地址,比如 10.0.0.1。 假设 Service 的端口是 1234,该 Service 会被集群中所有的 kube-proxy 实例观察到。 当代理看到一个新的 Service, 它会打开一个新的端口,建立一个从该 VIP 重定向到新端口的 iptables,并开始接收请求连接。

当一个客户端连接到一个 VIP,iptables 规则开始起作用,它会重定向该数据包到 Service代理 的端口。 Service代理 选择一个 backend,并将客户端的流量代理到 backend 上。

这意味着 Service 的所有者能够选择任何他们想使用的端口,而不存在冲突的风险。 客户端可以简单地连接到一个 IP 和端口,而不需要知道实际访问了哪些 Pod。

Iptables

再次考虑前面提到的图片处理应用程序。 当创建 backend Service 时,Kubernetes master 会给它指派一个虚拟 IP 地址,比如 10.0.0.1。 假设 Service 的端口是 1234,该 Service会被集群中所有的 kube-proxy 实例观察到。 当代理看到一个新的 Service, 它会安装一系列的 iptables 规则,从 VIP 重定向到 per-Service 规则。 该 per-Service 规则连接到 per-Endpoint 规则,该 per-Endpoint 规则会重定向(目标 NAT)到 backend。

当一个客户端连接到一个 VIP,iptables 规则开始起作用。一个 backend 会被选择(或者根据会话亲和性,或者随机),数据包被重定向到这个 backend。 不像 userspace 代理,数据包从来不拷贝到用户空间,kube-proxy 不是必须为该 VIP 工作而运行,并且客户端 IP 是不可更改的。 当流量打到 Node 的端口上,或通过负载均衡器,会执行相同的基本流程,但是在那些案例中客户端 IP 是可以更改的。

API 对象

在 Kubernetes REST API 中,Service 是 top-level 资源。关于 API 对象的更多细节可以查看:Service API 对象

更多信息

阅读 使用 Service 连接 Frontend 到 Backend

原文:https://k8smeetup.github.io/docs/concepts/services-networking/service/

译者:shirdrn

K8S中文社区微信公众号

Kubernetes 应用连接到 Service

Kubernetes 连接容器模型

既然有了一个持续运行、可复制的应用,我们就能够将它暴露到网络上。在讨论 Kubernetes 网络连接的方式之前,非常值得我们同 Docker中 “正常” 方式的网络进行一番对比。

默认情况下,Docker 使用私有主机网络,仅能与同在一台机器上的容器间通信。为了实现容器的跨节点通信,必须在机器自己的 IP 上为这些容器分配端口,为容器进行端口转发或者代理。

多个开发人员之间协调端口的使用很难做到规模化,那些难以控制的集群级别的问题,都会交由用户自己去处理。 Kubernetes 假设 Pod 可与其它 Pod 通信,不管它们在哪个主机上。 我们给 Pod 分配属于自己的集群私有 IP 地址,所以没必要在 Pod 或映射到的容器的端口和主机端口之间显式地创建连接。 这表明了在 Pod 内的容器都能够连接到本地的每个端口,集群中的所有 Pod 不需要通过 NAT 转换就能够互相看到。 文档的剩余部分将详述,如何在一个网络模型之上运行可靠的服务。

该指南使用一个简单的 Nginx server 来演示证明谈及的概念。同样的原则也体现在一个更加完整的 Jenkins CI 应用 中。

在集群中暴露 Pod

我们在之前的示例中已经做过,然而再让我重试一次,这次聚焦在网络连接的视角。 创建一个 Nginx Pod,指示它具有一个容器端口的说明:

run-my-nginx.yaml 
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80

这使得可以从集群中任何一个节点来访问它。检查节点,该 Pod 正在运行:

$ kubectl create -f ./run-my-nginx.yaml
$ kubectl get pods -l run=my-nginx -o wide
NAME                        READY     STATUS    RESTARTS   AGE       IP            NODE
my-nginx-3800858182-jr4a2   1/1       Running   0          13s       10.244.3.4    kubernetes-minion-905m
my-nginx-3800858182-kna2y   1/1       Running   0          13s       10.244.2.5    kubernetes-minion-ljyd

检查 Pod 的 IP 地址:

$ kubectl get pods -l run=my-nginx -o yaml | grep podIP
    podIP: 10.244.3.4
    podIP: 10.244.2.5

应该能够通过 ssh 登录到集群中的任何一个节点,使用 curl 也能调通所有 IP 地址。 需要注意的是,容器不会使用该节点上的 80 端口,也不会使用任何特定的 NAT 规则去路由流量到 Pod 上。 这意味着可以在同一个节点上运行多个 Pod,使用相同的容器端口,并且可以从集群中任何其他的 Pod 或节点上使用 IP 的方式访问到它们。 像 Docker 一样,端口能够被发布到主机节点的接口上,但是出于网络模型的原因应该从根本上减少这种用法。

如果对此好奇,可以获取更多关于 如何实现网络模型 的内容。

创建 Service

我们有 Pod 在一个扁平的、集群范围的地址空间中运行 Nginx 服务,可以直接连接到这些 Pod,但如果某个节点死掉了会发生什么呢? Pod 会终止,Deployment 将创建新的 Pod,使用不同的 IP。这正是 Service 要解决的问题。

Kubernetes Service 从逻辑上定义了运行在集群中的一组 Pod,这些 Pod 提供了相同的功能。 当创建时,每个 Service 被分配一个唯一的 IP 地址(也称为 clusterIP)。 这个 IP 地址与一个 Service 的生命周期绑定在一起,当 Service 存在的时候它也不会改变。 可以配置 Pod 使它与 Service 进行通信,Pod 知道,与 Service 通信将被自动地负载均衡到该 Service 中的某些 Pod 上。

可以使用 kubectl expose 为 2个 Nginx 副本创建一个 Service:

$ kubectl expose deployment/my-nginx
service "my-nginx" exposed

这等价于使用 kubectl create -f 命令创建,对应如下的 yaml 文件:

nginx-svc.yaml 
apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    run: my-nginx

上述规格将创建一个 Service,对应具有标签 run: my-nginx 的 Pod,目标 TCP 端口 80,并且在一个抽象的 Service 端口(targetPort:容器接收流量的端口;port:抽象的 Service 端口,可以使任何其它 Pod 访问该 Service 的端口)上暴露。 查看 Service API 对象 了解 Service 定义支持的字段列表。

$ kubectl get svc my-nginx
NAME       CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
my-nginx   10.0.162.149   <none>        80/TCP    21s

正如前面所提到的,一个 Service 由一组 backend Pod 组成。这些 Pod 通过 endpoints 暴露出来。 Service Selector 将持续评估,结果被 POST 到一个名称为 my-nginx 的 Endpoint 对象。 当 Pod 终止后,它会自动从 Endpoint 中移除,新的能够匹配上 Service Selector 的 Pod 将自动地被添加到 Endpoint 中。 检查该 Endpoint,注意到 IP 地址与在第一步创建的 Pod 是相同的。

$ kubectl describe svc my-nginx
Name:                my-nginx
Namespace:           default
Labels:              run=my-nginx
Selector:            run=my-nginx
Type:                ClusterIP
IP:                  10.0.162.149
Port:                <unset> 80/TCP
Endpoints:           10.244.2.5:80,10.244.3.4:80
Session Affinity:    None
No events.

$ kubectl get ep my-nginx
NAME       ENDPOINTS                     AGE
my-nginx   10.244.2.5:80,10.244.3.4:80   1m

现在能够从集群中任意节点上,通过 curl 请求 Nginx Service <CLUSTER-IP>:<PORT> 。 注意 Service IP 完全是虚拟的,它从来没有走过网络,如果对它如何工作的原理好奇,可以阅读更多关于 服务代理 的内容。

访问 Service

Kubernetes 支持两种主要的服务发现模式 —— 环境变量和 DNS。前者在单个节点上可用使用,然而后者需要 kube-dns 集群插件

环境变量

当 Pod 在 Node 上运行时,kubelet 会为每个活跃的 Service 添加一组环境变量。这会有一个顺序的问题。想了解为何,检查正在运行的 Nginx Pod 的环境变量(Pod 名称将不会相同):

$ kubectl exec my-nginx-3800858182-jr4a2 -- printenv | grep SERVICE
KUBERNETES_SERVICE_HOST=10.0.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443

注意,还没有谈及到 Service。这是因为创建副本先于 Service。 这样做的另一个缺点是,调度器可能在同一个机器上放置所有 Pod,如果该机器宕机则所有的 Service 都会挂掉。 正确的做法是,我们杀掉 2 个 Pod,等待 Deployment 去创建它们。 这次 Service 会 先于 副本存在。这将实现调度器级别的 Service,能够使 Pod 分散创建(假定所有的 Node 都具有同样的容量),还有正确的环境变量:

$ kubectl scale deployment my-nginx --replicas=0; kubectl scale deployment my-nginx --replicas=2;

$ kubectl get pods -l run=my-nginx -o wide
NAME                        READY     STATUS    RESTARTS   AGE     IP            NODE
my-nginx-3800858182-e9ihh   1/1       Running   0          5s      10.244.2.7    kubernetes-minion-ljyd
my-nginx-3800858182-j4rm4   1/1       Running   0          5s      10.244.3.8    kubernetes-minion-905m

可能注意到,Pod 具有不同的名称,因为它们被杀掉后并被重新创建。

$ kubectl exec my-nginx-3800858182-e9ihh -- printenv | grep SERVICE
KUBERNETES_SERVICE_PORT=443
MY_NGINX_SERVICE_HOST=10.0.162.149
KUBERNETES_SERVICE_HOST=10.0.0.1
MY_NGINX_SERVICE_PORT=80
KUBERNETES_SERVICE_PORT_HTTPS=443

DNS

Kubernetes 提供了一个 DNS 插件 Service,它使用 skydns 自动为其它 Service 指派 DNS 名字。 如果它在集群中处于运行状态,可以通过如下命令来检查:

$ kubectl get services kube-dns --namespace=kube-system
NAME       CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
kube-dns   10.0.0.10    <none>        53/UDP,53/TCP   8m

如果没有在运行,可以 启用它。 本段剩余的内容,将假设已经有一个 Service,它具有一个长久存在的 IP(my-nginx),一个为该 IP 指派名称的 DNS 服务器(kube-dns 集群插件),所以可以通过标准做法,使在集群中的任何 Pod 都能与该 Service 通信(例如:gethostbyname)。 让我们运行另一个 curl 应用来进行测试:

$ kubectl run curl --image=radial/busyboxplus:curl -i --tty
Waiting for pod default/curl-131556218-9fnch to be running, status is Pending, pod ready: false
Hit enter for command prompt

然后,按回车并执行命令 nslookup my-nginx:

[ root@curl-131556218-9fnch:/ ]$ nslookup my-nginx
Server:    10.0.0.10
Address 1: 10.0.0.10

Name:      my-nginx
Address 1: 10.0.162.149

Service 安全

到现在为止,我们只在集群内部访问了 Nginx server。在将 Service 暴露到 Internet 之前,我们希望确保通信信道是安全的。对于这可能需要:

  • https 自签名证书(除非已经有了一个识别身份的证书)
  • 使用证书配置的 Nginx server
  • 使证书可以访问 Pod 的秘钥

可以从 Nginx https 示例 获取所有上述内容,简明示例如下:

$ make keys secret KEY=/tmp/nginx.key CERT=/tmp/nginx.crt SECRET=/tmp/secret.json
$ kubectl create -f /tmp/secret.json
secret "nginxsecret" created
$ kubectl get secrets
NAME                  TYPE                                  DATA      AGE
default-token-il9rc   kubernetes.io/service-account-token   1         1d
nginxsecret           Opaque                                2         1m

现在修改 Nginx 副本,启动一个使用在秘钥中的证书的 https 服务器和 Servcie,都暴露端口(80 和 443):

nginx-secure-app.yaml 
apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  type: NodePort
  ports:
  - port: 8080
    targetPort: 80
    protocol: TCP
    name: http
  - port: 443
    protocol: TCP
    name: https
  selector:
    run: my-nginx
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 1
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      volumes:
      - name: secret-volume
        secret:
          secretName: nginxsecret
      containers:
      - name: nginxhttps
        image: bprashanth/nginxhttps:1.0
        ports:
        - containerPort: 443
        - containerPort: 80
        volumeMounts:
        - mountPath: /etc/nginx/ssl
          name: secret-volume

关于 nginx-secure-app manifest 值得注意的点如下:

  • 它在相同的文件中包含了 Deployment 和 Service 的规格
  • Nginx server 处理 80 端口上的 http 流量,以及 443 端口上的 https 流量,Nginx Service 暴露了这两个端口。
  • 每个容器访问挂载在 /etc/nginx/ssl 卷上的秘钥。这需要在 Nginx server 启动之前安装好。
$ kubectl delete deployments,svc my-nginx; kubectl create -f ./nginx-secure-app.yaml

这时可以从任何节点访问到 Nginx server。

$ kubectl get pods -o yaml | grep -i podip
    podIP: 10.244.3.5
node $ curl -k https://10.244.3.5
...
<h1>Welcome to nginx!</h1>

注意最后一步我们是如何提供 -k 参数执行 curl命令的,这是因为在证书生成时,我们不知道任何关于运行 Nginx 的 Pod 的信息,所以不得不在执行 curl 命令时忽略 CName 不匹配的情况。 通过创建 Service,我们连接了在证书中的 CName 与在 Service 查询时被 Pod使用的实际 DNS 名字。 让我们从一个 Pod 来测试(为了简化使用同一个秘钥,Pod 仅需要使用 nginx.crt 去访问 Service):

curlpod.yaml 
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: curl-deployment
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: curlpod
    spec:
      volumes:
      - name: secret-volume
        secret:
          secretName: nginxsecret
      containers:
      - name: curlpod
        command:
        - sh
        - -c
        - while true; do sleep 1; done
        image: radial/busyboxplus:curl
        volumeMounts:
        - mountPath: /etc/nginx/ssl
          name: secret-volume
$ kubectl create -f ./curlpod.yaml
$ kubectl get pods -l app=curlpod
NAME                               READY     STATUS    RESTARTS   AGE
curl-deployment-1515033274-1410r   1/1       Running   0          1m
$ kubectl exec curl-deployment-1515033274-1410r -- curl https://my-nginx --cacert /etc/nginx/ssl/nginx.crt
...
<title>Welcome to nginx!</title>
...

暴露 Service

对我们应用的某些部分,可能希望将 Service 暴露在一个外部 IP 地址上。 Kubernetes 支持两种实现方式:NodePort 和 LoadBalancer。 在上一段创建的 Service 使用了 NodePort,因此 Nginx https 副本已经就绪,如果使用一个公网 IP,能够处理 Internet 上的流量。

$ kubectl get svc my-nginx -o yaml | grep nodePort -C 5
  uid: 07191fb3-f61a-11e5-8ae5-42010af00002
spec:
  clusterIP: 10.0.162.149
  ports:
  - name: http
    nodePort: 31704
    port: 8080
    protocol: TCP
    targetPort: 80
  - name: https
    nodePort: 32453
    port: 443
    protocol: TCP
    targetPort: 443
  selector:
    run: my-nginx

$ kubectl get nodes -o yaml | grep ExternalIP -C 1
    - address: 104.197.41.11
      type: ExternalIP
    allocatable:
--
    - address: 23.251.152.56
      type: ExternalIP
    allocatable:
...

$ curl https://<EXTERNAL-IP>:<NODE-PORT> -k
...
<h1>Welcome to nginx!</h1>

让我们重新创建一个 Service,使用一个云负载均衡器,只需要将 my-nginx Service 的 Type 由 NodePort 改成 LoadBalancer。

$ kubectl edit svc my-nginx
$ kubectl get svc my-nginx
NAME       CLUSTER-IP     EXTERNAL-IP        PORT(S)               AGE
my-nginx   10.0.162.149   162.222.184.144    80/TCP,81/TCP,82/TCP  21s

$ curl https://<EXTERNAL-IP> -k
...
<title>Welcome to nginx!</title>

在 EXTERNAL-IP 列指定的 IP 地址是在公网上可用的。CLUSTER-IP 只在集群/私有云网络中可用。

注意,在 AWS 上类型 LoadBalancer 创建一个 ELB,它使用主机名(比较长),而不是 IP。 它太长以至于不能适配标准 kubectl get svc 的输出,事实上需要通过执行 kubectl describe service my-nginx 命令来查看它。 可以看到类似如下内容:

$ kubectl describe service my-nginx
...
LoadBalancer Ingress:   a320587ffd19711e5a37606cf4a74574-1142138393.us-east-1.elb.amazonaws.com
...

进一步阅读

Kubernetes 也支持联合 Service,能够跨多个集群和云提供商,提供逐步增长的可用性,更好的容错和服务的可伸缩性。 查看 联合 Service 用户指南 获取更进一步信息。

下一步

了解更多关于 Kubernetes 的特性,有助于在生产环境中可靠地运行容器

原文:https://k8smeetup.github.io/docs/concepts/services-networking/connect-applications-service/

译者:shirdrn

K8S中文社区微信公众号

Kubernetes 管理集群中的TLS认证

概览

每个 Kubernetes 集群都有一个集群根证书颁发机构(CA)。 集群中的组件通常使用 CA 来验证 API server 的证书,由API服务器验证 kubelet 客户端证书等。为了支持这一点,CA 证书包被分发到集群中的每个节点,并作为一个 sercret 附加分发到默认 service account 上。 或者,您的工作负载可以使用此 CA 建立信任。 您的应用程序可以使用类似于 ACME草案 的协议,使用 certificates.k8s.io API 请求证书签名。

集群中的 TLS 信任

让 Pod 中运行的应用程序信任集群根 CA 通常需要一些额外的应用程序配置。您将需要将 CA 证书包添加到 TLS 客户端或服务器信任的 CA 证书列表中。 例如,您可以使用 golang TLS 配置通过解析证书链并将解析的证书添加到 tls.Config 结构中的 Certificates字段中,CA 证书捆绑包将使用默认服务账户自动加载到 pod 中,路径为 /var/run/secrets/kubernetes.io/serviceaccount/ca.crt。 如果您没有使用默认服务账户,请请求集群管理员构建包含您有权访问使用的证书包的 configmap。

请求认证

以下部分演示如何为通过 DNS 访问的 Kubernetes 服务创建 TLS 证书。

步骤0. 下载安装SSL

下载 cfssl 工具:https://pkg.cfssl.org/

步骤1. 创建证书签名请求

通过运行以下命令生成私钥和证书签名请求(或CSR):

$ cat <<EOF | cfssl genkey - | cfssljson -bare server
{
  "hosts": [
    "my-svc.my-namespace.svc.cluster.local",
    "my-pod.my-namespace.pod.cluster.local",
    "172.168.0.24",
    "10.0.34.2"
  ],
  "CN": "my-pod.my-namespace.pod.cluster.local",
  "key": {
    "algo": "ecdsa",
    "size": 256
  }
}
EOF

172.168.0.24 是 service 的 cluster IP,my-svc.my-namespace.svc.cluster.local 是 service 的 DNS 名称, 10.0.34.2 是 Pod 的 IP, my-pod.my-namespace.pod.cluster.local 是 pod 的 DNS 名称,您可以看到以下输出:

2017/03/21 06:48:17 [INFO] generate received request
2017/03/21 06:48:17 [INFO] received CSR
2017/03/21 06:48:17 [INFO] generating key: ecdsa-256
2017/03/21 06:48:17 [INFO] encoded CSR

此命令生成两个文件;它生成包含 PEM 编码的 pkcs #10 认证请求的 server.csr,以及包含仍然要创建的证书的 PEM 编码密钥的 server-key.pem。

步骤2. 创建证书签名请求对象以发送到 Kubernetes API

使用以下命令创建 CSR yaml 文件,并发送到 API server:

$ cat <<EOF | kubectl create -f -
apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
  name: my-svc.my-namespace
spec:
  groups:
  - system:authenticated
  request: $(cat server.csr | base64 | tr -d '\n')
  usages:
  - digital signature
  - key encipherment
  - server auth
EOF

请注意,在步骤1中创建的 server.csr 文件是 base64 编码并存储在 .spec.request 字段中。 我们还要求提供 “数字签名”,“密钥加密” 和 “服务器身份验证” 密钥用途的证书。 我们 这里 支持列出的所有关键用途和扩展的关键用途,以便您可以使用相同的 API 请求客户端证书和其他证书。

在 API server 中可以看到这些 CSR 处于 pending 状态。执行下面的命令您将可以看到:

$ kubectl describe csr my-svc.my-namespace
Name:                   my-svc.my-namespace
Labels:                 <none>
Annotations:            <none>
CreationTimestamp:      Tue, 21 Mar 2017 07:03:51 -0700
Requesting User:        yourname@example.com
Status:                 Pending
Subject:
        Common Name:    my-svc.my-namespace.svc.cluster.local
        Serial Number:
Subject Alternative Names:
        DNS Names:      my-svc.my-namespace.svc.cluster.local
        IP Addresses:   172.168.0.24
                        10.0.34.2
Events: <none>

步骤3. 获取证书签名请求

批准证书签名请求是通过自动批准过程完成的,或由集群管理员一次性完成。有关这方面涉及的更多信息,请参见下文。

步骤4. 下载签名并使用

CSR 被签署并获得批准后,您应该看到以下内容:

$ kubectl get csr
NAME                  AGE       REQUESTOR               CONDITION
my-svc.my-namespace   10m       yourname@example.com    Approved,Issued

您可以通过运行以下命令下载颁发的证书并将其保存到 server.crt 文件中:

$ kubectl get csr my-svc.my-namespace -o jsonpath='{.status.certificate}' \
    | base64 -d > server.crt

现在您可以将 server.crt 和server-key.pem 作为键对来启动 HTTPS 服务器。

批准证书签名请求

Kubernetes 管理员(具有适当权限)可以使用 kubectl certificate approve 和kubectl certificate deny 命令手动批准(或拒绝)证书签名请求。但是,如果您打算大量使用此 API,则可以考虑编写自动化的证书控制器。

如果上述机器或人类使用 kubectl,批准者的作用是验证 CSR 满足如下两个要求:

  1. CSR 的主体控制用于签署 CSR 的私钥。这解决了伪装成授权主体的第三方的威胁。在上述示例中,此步骤将验证该 pod 控制了用于生成 CSR 的私钥。
  2. CSR 的主体被授权在请求的上下文中执行。这解决了我们加入群集的我们不期望的主体的威胁。在上述示例中,此步骤将是验证该 pod 是否被允许加入到所请求的服务中。

当且仅当满足这两个要求时,审批者应该批准 CSR,否则拒绝 CSR。

##关于批准许可的警告

批准 CSR 的能力决定谁信任群集中的谁。这包括 Kubernetes API 信任的人。批准 CSR 的能力不能过于广泛和轻率。在给予本许可之前,应充分了解上一节中提到的挑战和发布特定证书的后果。有关证书与认证交互的信息,请参阅 此处

给集群管理员的一个建议

本教程假设将签名者设置为服务证书 API。Kubernetes controller manager 提供了一个签名者的默认实现。 要启用它,请将 --cluster-signature-cert-file 和 --cluster-signing-key-file 参数传递给 controller manager,并配置具有证书颁发机构的密钥对的路径。

来源:https://k8smeetup.github.io/docs/tasks/tls/managing-tls-in-a-cluster/

译者:jimmysong

K8S中文社区微信公众号

Kubernetes Network Policy

网络策略说明一组 Pod 之间是如何被允许互相通信,以及如何与其它网络 Endpoint 进行通信。 NetworkPolicy 资源使用标签来选择 Pod,并定义了一些规则,这些规则指明允许什么流量进入到选中的 Pod 上。

前提条件

网络策略通过网络插件来实现,所以必须使用一种支持 NetworkPolicy 的网络方案 —— 非 Controller 创建的资源,是不起作用的。

隔离的与未隔离的 Pod

默认 Pod 是未隔离的,它们可以从任何的源接收请求。 具有一个可以选择 Pod 的网络策略后,Pod 就会变成隔离的。 一旦 Namespace 中配置的网络策略能够选择一个特定的 Pod,这个 Pod 将拒绝任何该网络策略不允许的连接。(Namespace 中其它未被网络策略选中的 Pod 将继续接收所有流量)

NetworkPolicy 资源

查看 API参考 可以获取该资源的完整定义。

下面是一个 NetworkPolicy 的例子:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: test-network-policy
  namespace: default
spec:
  podSelector:
    matchLabels:
      role: db
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          project: myproject
    - podSelector:
        matchLabels:
          role: frontend
    ports:
    - protocol: TCP
      port: 6379

将上面配置 POST 到 API Server 将不起任何作用,除非选择的网络方案支持网络策略。

必选字段:像所有其它 Kubernetes 配置一样, NetworkPolicy 需要 apiVersion、kind 和 metadata这三个字段,关于如何使用配置文件的基本信息,可以查看 这里这里 和 这里

spec:NetworkPolicy spec 具有在给定 Namespace 中定义特定网络的全部信息。

podSelector:每个 NetworkPolicy 包含一个 podSelector,它可以选择一组应用了网络策略的 Pod。由于 NetworkPolicy 当前只支持定义 ingress 规则,这个 podSelector 实际上为该策略定义了一组 “目标Pod”。示例中的策略选择了标签为 “role=db” 的 Pod。一个空的 podSelector 选择了该 Namespace 中的所有 Pod。

ingress:每个NetworkPolicy 包含了一个白名单 ingress 规则列表。每个规则只允许能够匹配上 from和 ports配置段的流量。示例策略包含了单个规则,它从这两个源中匹配在单个端口上的流量,第一个是通过namespaceSelector 指定的,第二个是通过 podSelector 指定的。

因此,上面示例的 NetworkPolicy:

  1. 在 “default” Namespace中 隔离了标签 “role=db” 的 Pod(如果他们还没有被隔离)
  2. 在 “default” Namespace中,允许任何具有 “role=frontend” 的 Pod,连接到标签为 “role=db” 的 Pod 的 TCP 端口 6379
  3. 允许在 Namespace 中任何具有标签 “project=myproject” 的 Pod,连接到 “default” Namespace 中标签为 “role=db” 的 Pod 的 TCP 端口 6379

查看 NetworkPolicy 入门指南 给出的更进一步的例子。

默认策略

通过创建一个可以选择所有 Pod 但不允许任何流量的 NetworkPolicy,你可以为一个 Namespace 创建一个 “默认的” 隔离策略,如下所示:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector:

这确保了即使是没有被任何 NetworkPolicy 选中的 Pod,将仍然是被隔离的。

可选地,在 Namespace 中,如果你想允许所有的流量进入到所有的 Pod(即使已经添加了某些策略,使一些 Pod 被处理为 “隔离的”),你可以通过创建一个策略来显式地指定允许所有流量:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all
spec:
  podSelector:
  ingress:
  - {}

下一步

了解 声明网络策略

译者:shirdrn / 原文地址

K8S中文社区微信公众号

Kubernetes 垃圾收集

Kubernetes 垃圾收集器的角色是删除指定的对象,这些对象曾经有但以后不再拥有 Owner 了。

注意:垃圾收集是 beta 特性,在 Kubernetes 1.4 及以上版本默认启用。

Owner 和 Dependent

一些 Kubernetes 对象是其它一些的 Owner。例如,一个 ReplicaSet 是一组 Pod 的 Owner。具有 Owner 的对象被称为是 Owner 的 Dependent。每个 Dependent 对象具有一个指向其所属对象的 metadata.ownerReferences 字段。

有时,Kubernetes 会自动设置 ownerReference 的值。例如,当创建一个 ReplicaSet 时,Kubernetes 自动设置 ReplicaSet 中每个 Pod 的 ownerReference 字段值。在 1.6 版本,Kubernetes 会自动为一些对象设置 ownerReference 的值,这些对象是由 ReplicationController、ReplicaSet、StatefulSet、DaemonSet 和 Deployment 所创建或管理。

也可以通过手动设置 ownerReference 的值,来指定 Owner 和 Dependent 之间的关系。

这有一个配置文件,表示一个具有 3 个 Pod 的 ReplicaSet:

apiVersion: extensions/v1beta1
kind: ReplicaSet
metadata:
  name: my-repset
spec:
  replicas: 3
  selector:
    matchLabels:
      pod-is-for: garbage-collection-example
  template:
    metadata:
      labels:
        pod-is-for: garbage-collection-example
    spec:
      containers:
      - name: nginx
        image: nginx

如果创建该 ReplicaSet,然后查看 Pod 的 metadata 字段,能够看到 OwnerReferences 字段:

kubectl create -f https://k8s.io/docs/concepts/abstractions/controllers/my-repset.yaml
kubectl get pods --output=yaml

输出显示了 Pod 的 Owner 是名为 my-repset 的 ReplicaSet:

apiVersion: v1
kind: Pod
metadata:
  ...
  ownerReferences:
  - apiVersion: extensions/v1beta1
    controller: true
    blockOwnerDeletion: true
    kind: ReplicaSet
    name: my-repset
    uid: d9607e19-f88f-11e6-a518-42010a800195
  ...

控制垃圾收集器删除 Dependent

当删除对象时,可以指定是否该对象的 Dependent 也自动删除掉。自动删除 Dependent 也称为 级联删除。Kubernetes 中有两种 级联删除 的模式:background 模式和 foreground 模式。

如果删除对象时,不自动删除它的 Dependent,这些 Dependent 被称作是原对象的 孤儿。

Background 级联删除

在 background 级联删除 模式下,Kubernetes 会立即删除 Owner 对象,然后垃圾收集器会在后台删除这些 Dependent。

Foreground 级联删除

在 foreground 级联删除 模式下,根对象首先进入 “删除中” 状态。在 “删除中” 状态会有如下的情况:

  • 对象仍然可以通过 REST API 可见
  • 会设置对象的 deletionTimestamp 字段
  • 对象的 metadata.finalizers 字段包含了值 “foregroundDeletion”

一旦被设置为 “删除中” 状态,垃圾收集器会删除对象的所有 Dependent。垃圾收集器删除了所有 “Blocking” 的 Dependent(对象的 ownerReference.blockOwnerDeletion=true)之后,它会删除 Owner 对象。

注意,在 “foreground 删除” 模式下,Dependent 只有通过 ownerReference.blockOwnerDeletion 才能阻止删除 Owner 对象。在 Kubernetes 1.7 版本中将增加 admission controller,基于 Owner 对象上的删除权限来控制用户去设置 blockOwnerDeletion 的值为 true,所以未授权的 Dependent 不能够延迟 Owner 对象的删除。

如果一个对象的ownerReferences 字段被一个 Controller(例如 Deployment 或 ReplicaSet)设置,blockOwnerDeletion 会被自动设置,没必要手动修改这个字段。

设置级联删除策略

通过为 Owner 对象设置 deleteOptions.propagationPolicy 字段,可以控制级联删除策略。可能的取值包括:“orphan”、“Foreground” 或 “Background”。

对很多 Controller 资源,包括 ReplicationController、ReplicaSet、StatefulSet、DaemonSet 和 Deployment,默认的垃圾收集策略是 orphan。因此,除非指定其它的垃圾收集策略,否则所有 Dependent 对象使用的都是 orphan 策略。

下面是一个在后台删除 Dependent 对象的例子:

kubectl proxy --port=8080
curl -X DELETE localhost:8080/apis/extensions/v1beta1/namespaces/default/replicasets/my-repset \
-d '{"kind":"DeleteOptions","apiVersion":"v1","propagationPolicy":"Background"}' \
-H "Content-Type: application/json"

下面是一个在前台删除 Dependent 对象的例子:

kubectl proxy --port=8080
curl -X DELETE localhost:8080/apis/extensions/v1beta1/namespaces/default/replicasets/my-repset \
-d '{"kind":"DeleteOptions","apiVersion":"v1","propagationPolicy":"Foreground"}' \
-H "Content-Type: application/json"

下面是一个孤儿 Dependent 的例子:

kubectl proxy --port=8080
curl -X DELETE localhost:8080/apis/extensions/v1beta1/namespaces/default/replicasets/my-repset \
-d '{"kind":"DeleteOptions","apiVersion":"v1","propagationPolicy":"Orphan"}' \
-H "Content-Type: application/json"

kubectl 也支持级联删除。 通过设置 --cascade 为 true,可以使用 kubectl 自动删除 Dependent 对象。设置 --cascade 为 false,会使 Dependent 对象成为孤儿 Dependent 对象。--cascade 的默认值是 true。

下面是一个例子,使一个 ReplicaSetDependent 对象成为孤儿 Dependent:

kubectl delete replicaset my-repset --cascade=false

已知的问题

  • 1.7 版本,垃圾收集不支持 自定义资源,比如那些通过 CustomResourceDefinition 新增,或者通过 API server 聚集而成的资源对象。

其它已知的问题

原文地址:https://k8smeetup.github.io/docs/concepts/workloads/controllers/garbage-collection/

译者:shirdrn

K8S中文社区微信公众号

Pod 安全策略

PodSecurityPolicy 类型的对象能够控制,是否可以向 Pod 发送请求,该 Pod 能够影响被应用到 Pod 和容器的 SecurityContext。 查看 Pod 安全策略建议 获取更多信息。

什么是 Pod 安全策略?

Pod 安全策略 是集群级别的资源,它能够控制 Pod 运行的行为,以及它具有访问什么的能力。 PodSecurityPolicy对象定义了一组条件,指示 Pod 必须按系统所能接受的顺序运行。 它们允许管理员控制如下方面:

控制面 字段名称
已授权容器的运行 privileged
为容器添加默认的一组能力 defaultAddCapabilities
为容器去掉某些能力 requiredDropCapabilities
容器能够请求添加某些能力 allowedCapabilities
控制卷类型的使用 volumes
主机网络的使用 hostNetwork
主机端口的使用 hostPorts
主机 PID namespace 的使用 hostPID
主机 IPC namespace 的使用 hostIPC
主机路径的使用 allowedHostPaths
容器的 SELinux 上下文 seLinux
用户 ID runAsUser
配置允许的补充组 supplementalGroups
分配拥有 Pod 数据卷的 FSGroup fsGroup
必须使用一个只读的 root 文件系统 readOnlyRootFilesystem

Pod 安全策略 由设置和策略组成,它们能够控制 Pod 访问的安全特征。这些设置分为如下三类:

  • 基于布尔值控制:这种类型的字段默认为最严格限制的值。
  • 基于被允许的值集合控制:这种类型的字段会与这组值进行对比,以确认值被允许。
  • 基于策略控制:设置项通过一种策略提供的机制来生成该值,这种机制能够确保指定的值落在被允许的这组值中。

RunAsUser

  • MustRunAs - 必须配置一个 range。使用该范围内的第一个值作为默认值。验证是否不在配置的该范围内。
  • MustRunAsNonRoot - 要求提交的 Pod 具有非零 runAsUser 值,或在镜像中定义了 USER 环境变量。不提供默认值。
  • RunAsAny - 没有提供默认值。允许指定任何 runAsUser 。

SELinux

  • MustRunAs - 如果没有使用预分配的值,必须配置 seLinuxOptions。默认使用 seLinuxOptions。验证 seLinuxOptions。
  • RunAsAny - 没有提供默认值。允许任意指定的 seLinuxOptions ID。

SupplementalGroups

  • MustRunAs - 至少需要指定一个范围。默认使用第一个范围的最小值。验证所有范围的值。
  • RunAsAny - 没有提供默认值。允许任意指定的 supplementalGroups ID。

FSGroup

  • MustRunAs - 至少需要指定一个范围。默认使用第一个范围的最小值。验证在第一个范围内的第一个 ID。
  • RunAsAny - 没有提供默认值。允许任意指定的 fsGroup ID。

控制卷

通过设置 PSP 卷字段,能够控制具体卷类型的使用。当创建一个卷的时候,与该字段相关的已定义卷可以允许设置如下值:

  1. azureFile
  2. azureDisk
  3. flocker
  4. flexVolume
  5. hostPath
  6. emptyDir
  7. gcePersistentDisk
  8. awsElasticBlockStore
  9. gitRepo
  10. secret
  11. nfs
  12. iscsi
  13. glusterfs
  14. persistentVolumeClaim
  15. rbd
  16. cinder
  17. cephFS
  18. downwardAPI
  19. fc
  20. configMap
  21. vsphereVolume
  22. quobyte
  23. photonPersistentDisk
  24. projected
  25. portworxVolume
  26. scaleIO
  27. storageos
  28. * (allow all volumes)

对新的 PSP,推荐允许的卷的最小集合包括:configMap、downwardAPI、emptyDir、persistentVolumeClaim、secret 和 projected。

主机网络

  • HostPorts, 默认为 empty。HostPortRange 列表通过 min(包含) and max(包含) 来定义,指定了被允许的主机端口。

允许的主机路径

  • AllowedHostPaths 是一个被允许的主机路径前缀的白名单。空值表示所有的主机路径都可以使用。

许可

包含 PodSecurityPolicy 的 许可控制,允许控制集群资源的创建和修改,基于这些资源在集群范围内被许可的能力。

许可使用如下的方式为 Pod 创建最终的安全上下文:

  1. 检索所有可用的 PSP。
  2. 生成在请求中没有指定的安全上下文设置的字段值。
  3. 基于可用的策略,验证最终的设置。

如果某个策略能够匹配上,该 Pod 就被接受。如果请求与 PSP 不匹配,则 Pod 被拒绝。

Pod 必须基于 PSP 验证每个字段。

创建 Pod 安全策略

下面是一个 Pod 安全策略的例子,所有字段的设置都被允许:

apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
  name: permissive
spec:
  seLinux:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  runAsUser:
    rule: RunAsAny
  fsGroup:
    rule: RunAsAny
  hostPorts:
  - min: 8000
    max: 8080
  volumes:
  - '*'

下载示例文件可以创建该策略,然后执行如下命令:

$ kubectl create -f ./psp.yaml
podsecuritypolicy "permissive" created

获取 Pod 安全策略列表

获取已存在策略列表,使用 kubectl get:

$ kubectl get psp
NAME        PRIV   CAPS  SELINUX   RUNASUSER         FSGROUP   SUPGROUP  READONLYROOTFS  VOLUMES
permissive  false  []    RunAsAny  RunAsAny          RunAsAny  RunAsAny  false           [*]
privileged  true   []    RunAsAny  RunAsAny          RunAsAny  RunAsAny  false           [*]
restricted  false  []    RunAsAny  MustRunAsNonRoot  RunAsAny  RunAsAny  false           [emptyDir secret downwardAPI configMap persistentVolumeClaim projected]

修改 Pod 安全策略

通过交互方式修改策略,使用 kubectl edit:

$ kubectl edit psp permissive

该命令将打开一个默认文本编辑器,在这里能够修改策略。

删除 Pod 安全策略

一旦不再需要一个策略,很容易通过 kubectl 删除它:

$ kubectl delete psp permissive
podsecuritypolicy "permissive" deleted

启用 Pod 安全策略

为了能够在集群中使用 Pod 安全策略,必须确保如下:

  1. 启用 API 类型 extensions/v1beta1/podsecuritypolicy(仅对 1.6 之前的版本)
  2. 启用许可控制器 PodSecurityPolicy
  3. 定义自己的策略

使用 RBAC

在 Kubernetes 1.5 或更新版本,可以使用 PodSecurityPolicy 来控制,对基于用户角色和组的已授权容器的访问。访问不同的 PodSecurityPolicy 对象,可以基于认证来控制。基于 Deployment、ReplicaSet 等创建的 Pod,限制访问 PodSecurityPolicy 对象,Controller Manager 必须基于安全 API 端口运行,并且不能够具有超级用户权限。

PodSecurityPolicy 认证使用所有可用的策略,包括创建 Pod 的用户,Pod 上指定的服务账户(service acount)。当 Pod 基于 Deployment、ReplicaSet 创建时,它是创建 Pod 的 Controller Manager,所以如果基于非安全 API 端口运行,允许所有的 PodSecurityPolicy 对象,并且不能够有效地实现细分权限。用户访问给定的 PSP 策略有效,仅当是直接部署 Pod 的情况。更多详情,查看 PodSecurityPolicy RBAC 示例,当直接部署 Pod 时,应用 PodSecurityPolicy 控制基于角色和组的已授权容器的访问 。

原文地址:https://k8smeetup.github.io/docs/concepts/policy/pod-security-policy/

译者:shirdrn

K8S中文社区微信公众号