OpenShift——大杂烩

2019年11月10日 阅读数:212
这篇文章主要向大家介绍OpenShift——大杂烩,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

 理解OpenShift(1):网络之 Router 和 Routephp

理解OpenShift(2):网络之 DNS(域名服务)css

理解OpenShift(3):网络之 SDNhtml

理解OpenShift(4):用户及权限管理前端

理解OpenShift(5):从 Docker Volume 到 OpenShift Persistent Volumevue

 https://www.cnblogs.com/sammyliu/p/10013461.htmlnode

** 本文基于 OpenShift 3.11,Kubernetes 1.11 进行测试 ***mysql

 

1. OpenShift 为何须要 Router 和 Route?

顾名思义,Router 是路由器,Route 是路由器中配置的路由。OpenShift 中的这两个概念是为了解决从集群外部(就是从除了集群节点之外的其它地方)访问服务的需求。不晓得为何OpenShift 要将Kubernetes 中的 Ingress 改成 Router,我却是以为 Ingress 名字更贴切。linux

从外部经过 router 和从内部经过 servide 访问 pod 中的应用两个过程的简单的示意图以下:ios

上图中,某个应用的三个pod 分别位于 node1,node2 和 node3 上。OpenShift 中有三层IP地址概念:c++

  • pod 本身的 IP 地址,能够类比为 OpenStack 中虚拟机的固定IP。它只有在集群内才有意义。
  • service 的 IP 地址。Service 一般有 ClusterIP,这也是一种集群内部的IP 地址。
  • 应用的外部 IP 地址,能够类比为OpenStack 中的浮动IP,或者IDC IP(和浮动IP 之间是NAT 映射关系)。

所以,要从集群外部访问 pod 中的应用,无非两种方式:

  • 一种是利用一个代理(proxy),把外部 IP 地址转化为后端的 Pod IP 地址。这就是 OpenShift router/route 的思路。OpenShift 中的 router 服务,是一个运行在特定节点(一般是基础架构节点)上的集群基础服务,由集群管理员负责建立和管理。它能够有多个副本(pod)。router 中可有多个 route,每一个 route 能经过外部HTTP 请求的域名找出其后端的 pod 列表,并进行网络包的转发。也就是将pod 中的应用暴露到外网域名,使得用户能够外面经过域名访问到应用。这其实是一种七层负载均衡器。OpenShift 默认采用 HAProxy 来实现,固然也支持其它实现,好比 F5.
  • 另外一种是将服务直接暴露到集群外。这种方式具体会在『服务 Service』那一篇文章中详细解释。

2. OpenShift 如何利用 HAProxy 实现 router 和 route?

2.1 Router 部署

使用 ansible 采用默认配置部署 OpenShift 集群时,在集群 Infra 节点上,会以 Host networking 方式运行一个 HAProxy 的 pod,它会在全部网卡的 80 和 443 端口上进行监听。

[root@infra-node3 cloud-user]# netstat -lntp | grep haproxy
tcp        0      0 127.0.0.1:10443         0.0.0.0:*               LISTEN      583/haproxy         
tcp        0      0 127.0.0.1:10444         0.0.0.0:*               LISTEN      583/haproxy         
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      583/haproxy         
tcp        0      0 0.0.0.0:443             0.0.0.0:*               LISTEN      583/haproxy

其中,172.0.0.1 上的 10443 和 10444 是HAproxy 本身使用的。下文会有解释。

所以,在每一个 infra 节点上,只能有一个 HAProxy pod,由于这些端口只能被占用一次。若是调度器找不到知足要求的节点,则router 服务的调度就会失败:

0/7 nodes are available: 2 node(s) didn't have free ports for the requested pod ports, 5 node(s) didn't match node selector

OpenShift HAProxy Router 支持两种部署方式:

  • 一种是常见的单Router 服务部署,它有一个或多个实例(pod),分布在多个节点上,负责整个集群上部署的服务的对外访问。
  • 另外一种是分片(sharding)部署。此时,会有多个 Router 服务,每一个Router 服务负责指定的若干project,二者之间采用标签(label)进行映射。这是为了解决单个 Router 的性能不够问题而提出的解决方案。

OpenShift 提供了 oc adm router 命令来建立 router 服务。

建立router:

复制代码
[root@master1 cloud-user]# oc adm router router2 --replicas=1 --service-account=router
info: password for stats user admin has been set to J3YyPjlbqf
--> Creating router router2 ...
    warning: serviceaccounts "router" already exists
    clusterrolebinding.authorization.openshift.io "router-router2-role" created
    deploymentconfig.apps.openshift.io "router2" created
    service "router2" created
--> Success
复制代码

详细的部署方法请参见官方文档 https://docs.openshift.com/container-platform/3.11/install_config/router/default_haproxy_router.html。

2.2 Router pod 中的 HAProxy 进程

在 Router 服务的每一个 pod 之中,openshift-router 进程启动了一个 haproy 进程:

UID        PID  PPID  C STIME TTY          TIME CMD
1000000+     1     0  0 Nov21 ?        00:14:27 /usr/bin/openshift-router
1000000+ 16011     1  0 12:42 ?        00:00:00 /usr/sbin/haproxy -f /var/lib/haproxy/conf/haproxy.config -p /var/lib/haproxy/run/haproxy.pid -x /var/lib/haproxy/run/haproxy.sock -sf 16004

查看 haproxy 使用的配置文件(只是部分):

复制代码
global
  maxconn 20000
  daemon
  ca-base /etc/ssl
  crt-base /etc/ssl
 。。。。  

defaults
  maxconn 20000

  # Add x-forwarded-for header.

  # server openshift_backend 127.0.0.1:8080
  errorfile 503 /var/lib/haproxy/conf/error-page-503.http

。。。
  timeout http-request 10s
  timeout http-keep-alive 300s

  # Long timeout for WebSocket connections.
  timeout tunnel 1h

frontend public
    
  bind :80
  mode http
  tcp-request inspect-delay 5s
  tcp-request content accept if HTTP
  monitor-uri /_______internal_router_healthz

  # Strip off Proxy headers to prevent HTTpoxy (https://httpoxy.org/)
  http-request del-header Proxy

  # DNS labels are case insensitive (RFC 4343), we need to convert the hostname into lowercase
  # before matching, or any requests containing uppercase characters will never match.
  http-request set-header Host %[req.hdr(Host),lower]

  # check if we need to redirect/force using https.
  acl secure_redirect base,map_reg(/var/lib/haproxy/conf/os_route_http_redirect.map) -m found
  redirect scheme https if secure_redirect

  use_backend %[base,map_reg(/var/lib/haproxy/conf/os_http_be.map)]

  default_backend openshift_default

# public ssl accepts all connections and isn't checking certificates yet certificates to use will be
# determined by the next backend in the chain which may be an app backend (passthrough termination) or a backend
# that terminates encryption in this router (edge)
frontend public_ssl
    
  bind :443
  tcp-request  inspect-delay 5s
  tcp-request content accept if { req_ssl_hello_type 1 }

  # if the connection is SNI and the route is a passthrough don't use the termination backend, just use the tcp backend
  # for the SNI case, we also need to compare it in case-insensitive mode (by converting it to lowercase) as RFC 4343 says
  acl sni req.ssl_sni -m found
  acl sni_passthrough req.ssl_sni,lower,map_reg(/var/lib/haproxy/conf/os_sni_passthrough.map) -m found
  use_backend %[req.ssl_sni,lower,map_reg(/var/lib/haproxy/conf/os_tcp_be.map)] if sni sni_passthrough

  # if the route is SNI and NOT passthrough enter the termination flow
  use_backend be_sni if sni

  # non SNI requests should enter a default termination backend rather than the custom cert SNI backend since it
  # will not be able to match a cert to an SNI host
  default_backend be_no_sni

。。。
backend be_edge_http:demoprojectone:jenkins mode http option redispatch option forwardfor balance leastconn timeout server 4m timeout check 5000ms http-request set-header X-Forwarded-Host %[req.hdr(host)] http-request set-header X-Forwarded-Port %[dst_port] http-request set-header X-Forwarded-Proto http if !{ ssl_fc } http-request set-header X-Forwarded-Proto https if { ssl_fc } http-request set-header X-Forwarded-Proto-Version h2 if { ssl_fc_alpn -i h2 } http-request add-header Forwarded for=%[src];host=%[req.hdr(host)];proto=%[req.hdr(X-Forwarded-Proto)];proto-version=%[req.hdr(X-Forwarded-Proto-Version)] cookie 4376ea64d7d0abf11209cfe5f7cca1e7 insert indirect nocache httponly secure server pod:jenkins-1-84nrt:jenkins:10.128.2.13:8080 10.128.2.13:8080 cookie 8669a19afc9f0fed6824feb9fb1cf4ac weight 256 。。。
复制代码

为了简单期间,上面只是配置文件的部份内容,它主要包括三种类型:

  • 全局配置,好比最大链接数 maxconn,超时时间 timeout 等;以及front部分,即前端配置,HAProxy 默认会在 443 和 80 两个端口上分别监听外部 https 和 http 请求。
  • backend,即每一个服务的后端配置,里面有不少关键内容,好比后端协议(mode)、负载均衡方法(balance)、后端列表(server,这里是pod,包括其IP 地址和端口)、证书等。

所以,OpenShift 的路由器功能须要能对这三部分进行管理和控制。

关于负载均衡器和 HAProxy 的详细介绍,能够参考 Neutron 理解 (7): Neutron 是如何实现负载均衡器虚拟化的 这篇文章。

2.3 全局配置管理

要指定或修改 HAProxy 的全局配置,OpenShift 有提供两种方式:

(1)第一种是使用 oc adm router 命令在建立 router 时候指定各类参数,好比 --max-connections 用于设置最大链接数。好比:

oc adm router --max-connections=200000 --ports='81:80,444:443' router3

建立出来的HAProxy 的 maxconn 将是 20000,router3 这个服务对外暴露出来的端口是 81 和 444,可是 HAProxy pod 的端口依然是 80 和 443.

(2)经过设置 dc/<dc router名> 的环境变量来设置 router 的全局配置。

在官方文档 https://docs.openshift.com/container-platform/3.4/architecture/core_concepts/routes.html#haproxy-template-router 中有完整的环境变量列表。好比运行如下命令后,

 oc set env dc/router3 ROUTER_SERVICE_HTTPS_PORT=444 ROUTER_SERVICE_HTTP_PORT=81 STATS_PORT=1937

router3 会从新部署,新部署的HAProxy 的 https 监听端口是 444,http 监听端口是 80,统计端口是 1937.

 2.4 OpenShift passthrough 类型的 route 与 HAProxy backend

(1)经过OpenShift Console 或者 oc 命令建立一条 route,它将 sit 项目的 jenkins 服务暴露到域名 sitjenkins.com.cn:

在界面上建立 route:

结果:

复制代码
Name:                   sitjenkins.com.cn
Namespace:              sit
Labels:                 app=jenkins-ephemeral
                        template=jenkins-ephemeral-template
Annotations:            <none>
Requested Host:         sitjenkins.com.cn
Path:                   <none>
TLS Termination:        passthrough
Endpoint Port:          web

Service:        jenkins
Weight:         100 (100%)
Endpoints:      10.128.2.15:8080, 10.131.0.10:8080 
复制代码

这里,service name 起了一个中介做用,把 route 和服务的端点(也就是pod)链接了起来。

(2)router 服务的两个 pod 中的 HAProxy 进程的配置文件中多了一个backend:

复制代码
# Secure backend, pass through
backend be_tcp:sit:sitjenkins.com.cn
  balance source

  hash-type consistent
  timeout check 5000ms}
  server pod:jenkins-1-bqhfj:jenkins:10.128.2.15:8080 10.128.2.15:8080 weight 256 check inter 5000ms
  server pod:jenkins-1-h2fff:jenkins:10.131.0.10:8080 10.131.0.10:8080 weight 256 check inter 5000ms
复制代码

其中,这些后端 server 其实就是 pod,它们是 openshift 经过步骤(1)中的 service name 找到的。balance 是负载均衡策略,后文会解释。

(3)文件 /var/lib/haproxy/conf/os_sni_passthrough.map 中多了一条记录

sh-4.2$ cat /var/lib/haproxy/conf/os_sni_passthrough.map
^sitjenkins\.com\.cn(:[0-9]+)?(/.*)?$ 1

(4)文件 /var/lib/haproxy/conf/os_tcp_be.map 中多了一条记录

sh-4.2$ cat /var/lib/haproxy/conf/os_tcp_be.map
^sitjenkins\.com\.cn(:[0-9]+)?(/.*)?$ be_tcp:sit:sitjenkins.com.cn

(5)HAProxy 根据上面的 map 文件为该条 route 选择第(2)步中增长的 backend的逻辑以下

复制代码
frontend public_ssl  #解释:前端协议 https,

  bind :443  ##前端端口 443
  tcp-request  inspect-delay 5s
  tcp-request content accept if { req_ssl_hello_type 1 }

  # if the connection is SNI and the route is a passthrough don't use the termination backend, just use the tcp backend
  # for the SNI case, we also need to compare it in case-insensitive mode (by converting it to lowercase) as RFC 4343 says
  acl sni req.ssl_sni -m found ##检查 https request 支持 sni
  acl sni_passthrough req.ssl_sni,lower,map_reg(/var/lib/haproxy/conf/os_sni_passthrough.map) -m found ##检查经过 sni 传来的 hostname 在 os_sni_patthrough.map 文件中
  use_backend %[req.ssl_sni,lower,map_reg(/var/lib/haproxy/conf/os_tcp_be.map)] if sni sni_passthrough ##从 oc_tcp_be.map 中根据 sni hostname 获取 backend name

  # if the route is SNI and NOT passthrough enter the termination flow
  use_backend be_sni if sni

  # non SNI requests should enter a default termination backend rather than the custom cert SNI backend since it
  # will not be able to match a cert to an SNI host
  default_backend be_no_sni
复制代码

(6)HAPorxy 进程会重启,从而应用修改了的配置文件。

理解(5)中的脚本须要的一些背景知识:

从上面的蓝色注释中,咱们能看到 HAProxy 进程经过 https 请求中经过 SNI 传入的域名 sitjenkins.com.cn ,在 os_tcp_be.map 文件中获取到了 backend 名称 be_tcp:sit:sitjenkins.com.cn,这样就和(2)步骤中的 backend 对应上了。

OpenShift 的 router 使用的 HAProxy 采用基于域名的负载均衡路由方式,示例以下,具体说明请参加官方文档。

2.5 OpenShift edge 和 re-encrypt 类型的 route 与 HAProxy

HAProxy 前端:前端依然是在 443 端口监听外部 HTTPS 请求

frontend public_ssl
  bind :443
..... # if the route is SNI and NOT passthrough enter the termination flow use_backend be_sni if sni

可是,当 TLS 终止类型不是 passthrough (edge 或者 re-encrypt)时,会使用backend be_sni。

backend be_sni
  server fe_sni 127.0.0.1:10444 weight 1 send-prox

而这个后端是由本机的 127.0.0.1:10444 提供服务,所以又转到了前端 fe_sni:

复制代码
frontend fe_sni
  # terminate ssl on edge
  bind 127.0.0.1:10444 ssl no-sslv3 crt /var/lib/haproxy/router/certs/default.pem crt-list /var/lib/haproxy/conf/cert_config.map accept-proxy
  mode http
。。。。。。

  # map to backend
  # Search from most specific to general path (host case).
  # Note: If no match, haproxy uses the default_backend, no other
  #       use_backend directives below this will be processed.
  use_backend %[base,map_reg(/var/lib/haproxy/conf/os_edge_reencrypt_be.map)]

  default_backend openshift_default
复制代码

map 映射文件:

sh-4.2$ cat /var/lib/haproxy/conf/os_edge_reencrypt_be.map
^edgejenkins\.com\.cn(:[0-9]+)?(/.*)?$ be_edge_http:sit:jenkins-edge

Edge 类型 route 的 HAProxy 后端:

复制代码
backend be_edge_http:sit:jenkins-edge
  mode http
  option redispatch
  option forwardfor
  balance leastconn

  timeout check 5000ms
  .....
  server pod:jenkins-1-bqhfj:jenkins:10.128.2.15:8080 10.128.2.15:8080 cookie 71c6bd03732fa7da2f1b497b1e4c7993 weight 256 check inter 5000ms
  server pod:jenkins-1-h2fff:jenkins:10.131.0.10:8080 10.131.0.10:8080 cookie fa8d7fb72a46958a7add1406e6d26cc8 weight 256 check inter 5000ms
复制代码

Re-encrypt 类型 route 的 HAProxy 后端:

复制代码
# Plain http backend or backend with TLS terminated at the edge or a
# secure backend with re-encryption.
backend be_secure:sit:reencryptjenkins.com.cn
  mode http
。。。。

    http-request set-header X-Forwarded-Host %[req.hdr(host)]
    http-request set-header X-Forwarded-Port %[dst_port]
    http-request set-header X-Forwarded-Proto http if !{ ssl_fc }
    http-request set-header X-Forwarded-Proto https if { ssl_fc }
    http-request set-header X-Forwarded-Proto-Version h2 if { ssl_fc_alpn -i h2 }

  server pod:jenkins-1-bqhfj:jenkins:10.128.2.15:8080 10.128.2.15:8080 cookie ... weight 256 ssl verifyhost jenkins.sit.svc verify required ca-file /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt check inter 5000ms #与后端的链路采用 ssl 加密,而且要检查hostname
  server pod:jenkins-1-h2fff:jenkins:10.131.0.10:8080 10.131.0.10:8080 cookie ... weight 256 ssl verifyhost jenkins.sit.svc verify required ca-file /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt check inter 5000ms
复制代码

这里能够看出来从新使用密钥对链接进行加密,可是不知道为什么 mode 依然是 http,而不是 https。

 2.6 设置和修改 route 配置

route 配置主要有如下几个比较重要的:

(1)SSL 终结方式。共三种:

 

  • edge:TLS 在 router 上被终结,而后非SSL网络包被转发给后端 pod。所以须要在 router 上安装 TLS 证书。不安装的话,会使用 router 的默认证书。
  • passthrough:加密网络包直接被发给 pod,router 上不作TLS 终结,由于不须要在 router 上配置证书或密钥。
  • Re-encryption:是 edge 的一种变种。首先 router 上会使用一个证书作 TSL 终结,而后使用另外的证书再进行加密,而后发给后端 pod。所以,整个网络路径都是加密的。

设置:

(2)负载均衡策略。也有三种:

 

  • roundrobin:根据权重轮流使用全部后端。
  • leastconn:选择最少链接的后端接收请求。
  • source:将源IP进行哈希,确保来自同一个源IP的请求发给同一个后端。
设置:
  • 要修改整个 router 的负载均衡策略,可以使用 ROUTER_TCP_BALANCE_SCHEME 环境变量,为该 router 的全部 passthrough 类型的 route设置负载均衡策略,使用 ROUTER_LOAD_BALANCE_ALGORITHM 为其它类型的 route 设置策略。
  • 可使用 haproxy.router.openshift.io/balance 为某个 route 设置负载均衡策略。

 举例:

 

  • 设置整个 router 的环境变量:oc set env dc/router ROUTER_TCP_BALANCE_SCHEME=roundrobin
改完之后,该 router 实例会从新部署,全部 passthrough 的 route 都是 roundrobin 类型的了。默认为 source 类型。
  • 修改某个 route 的负载均衡的策略:oc edit route aaaa.svc.cluster.local
修改完成后,HAProxy 中对应该 route 的 backend 中的 balance 值会被修改成 leastconn。

2.7 一个 route 将流量分给多个后端服务

该功能经常使用于一些开发测试流程,好比作A/B 测试。

在下面的配置中,有一个应用三个版本的部署,前端一个 route,各服务使用不一样的权重。

下面是 HAProxy 配置文件中的 backend 配置,采用 roundrobin 负载均衡模式:

 

3. OpenShift router 服务如何实现高可用?

OpenShift router 服务支持两种高可用模式。

3.1 单 router 服务多副本,并利用和DNS/LB 实现高可用

这种模式只部署一个 router 服务,它支持集群的全部对外暴露的服务。要实现HA,须要设置副本数(replicas)大于1,使得会在超过一台服务器上建立pod,而后再经过DNS轮询或者四层负载均衡。 

由于 router/pod 中的 HAProxy 要实现本地配置文件,所以实际上它们是有状态容器。OpenShift 采用 etcd 做为配置的统一存储,openshift-router 进程应该是采起某种机制(被通知或定时拉取)从 etcd 中获取 router 和 route 的配置,而后再修改本地的配置文件,再重启 HAPorxy 进程来应用新修改了的配置文件。 要深刻了解这里面的工做原理,能够去看源代码。

3.2 多 router 服务经过分片(sharding)实现高可用

这种模式下,管理员须要建立和部署多个 router 服务,每一个router 服务支持一个或几个 project/namespace。router 和 project/namespace 之间的映射使用标签(label)来实现。具体的配置请参考官网 https://docs.openshift.com/container-platform/3.11/install_config/router/default_haproxy_router.html。实际上,和一些产品(好比mysql,memedcache)的分片功能相似,该功能更多地是为了解决性能问题,而没法彻底解决高可用问题。

4. 常见问题如何排查?

从上面的分析能够看出,要使得 router 和 route 都正常工做,至少要确保如下几个环节都是没问题的:

  1. 客户端使用 route 中配置的域名和端口来访问服务。
  2. DNS 能将域名解析到目标 router 所在的服务器(在使用分片配置时比较复杂,尤为须要注意)。
  3. 若有采用另外的四层负载均衡器的话,它得配置正确、工做正常。
  4. HAProxy 能经过域名匹配到正确的backend。
  5. router 和 route 的配置被正确地反映到了 HAProxy 的配置文件中了。
  6. HAProxy 进程重启了,从而读取了新修改的配置文件。
  7. 后端 pod 列表正确,而且至少有一个 pod 正常工做。

若是您看到以下的错误页面,则说明上面的第3到7点至少有一处不能正常功能。此时,进行有针对性的排查便可。

 

理解OpenShift(2):网络之 DNS(域名服务)

理解OpenShift(1):网络之 Router 和 Route

理解OpenShift(2):网络之 DNS(域名服务)

理解OpenShift(3):网络之 SDN

理解OpenShift(4):用户及权限管理

理解OpenShift(5):从 Docker Volume 到 OpenShift Persistent Volume

 

** 本文基于 OpenShift 3.11,Kubernetes 1.11 进行测试 ***

 

OpenShift 集群中,至少有三个地方须要用到 DNS:

  • 一是Pod 中的应用经过域名访问外网的时候,须要DNS来解析外网的域名
  • 二是在集群内部(pod 中或者宿主机上)经过服务的域名来访问集群内服务的时候,这也是一般所说的服务发现功能,须要经过服务域名来先发现(获取其IP地址)再使用该服务
  • 三是从集群外部经过域名访问部署在OpenShift pod 中的服务的时候,须要DNS来解析服务的外网域名

本文就从这三点出发,解释 OpenShift 是如何实现这三种DNS功能的。

1. OpenShift 中的DNS 相关组件及其配置

1.1 Pod 中的 DNS 配置

在Linux 系统上,当一个应用经过域名链接远端主机时,DNS 解析会经过系统调用来进行,好比 getaddrinfo()。和任何Linux 操做系统同样,Pod 的 DNS 定义在 resolv.conf 文件中,其示例以下:

sh-4.2$ cat /etc/resolv.conf 
nameserver 172.22.122.9
search dev.svc.cluster.local svc.cluster.local cluster.local exampleos.com
options ndots:5

其中,

  • nameserver 字段是 pod 所在的宿主机的主网卡的IP 地址。也就是说 pod 中发起的全部DNS 查询请求都会被转发到运行在宿主机的 53 端口上的DNS服务器上。
  • search 字段指定当解析一个非FQDN域名时被附加的搜索域(search domain)列表。其解释以下:
域名(Domain Name)分为两种,一种是绝对域名(Absolute Domain Name,也称为 Fully-Qualified Domain Name,简称 FQDN),另外一种是相对域名(Relative Domain Name,也称为 Partially Qualified Domain Name,简称PQDN)。FQDN 是完整域名,它可以惟一地在DNS名字空间中肯定一个记录。好比最高级别的域名A包括子域名B它又包括子域名C,那么FQDN 是 C.B.A.,好比 cs.widgetopia.edu.。 有时候咱们也会使用PQDN,它是不彻底的、模糊的。
FQDN 能被直接到 DNS 名字服务器中查询;而 PQDN 须要先转化为FQDN 再进行查询。其作法是将 PQDN 附加一个搜索域名(search domain)来生成一个 FQDN。在域名系统中,域名结尾是不是『.』被用来区分 FQDN 和 PQDN。好比  apple.com. 表示一个Apple公司的 FQDN,而 apple 则表示一个 PQDN,它的FQDN 多是  apple.cs.widgetopia.edu.; apple.com 仍然是一个 PQDN,它的FQDN 多是  apple.com.cs.widgetopia.edu.。
  • options ndots:5

默认地,许多DNS 解析器若是发现被解析的域名中有任何的点(.)就把它当作一个 FQDN 来解析;若是域名中没有任何点,就把它当作 PQDN 来处理,而且会加上系统的默认domain name 和最后的点,来组成 FQDN。若是没有指定默认的 domain name (经过 domain 字段)或查询失败,则会将 search 字段的第一个值当作默认domain name,若是解析不成功,则依次往下试,直到有一个成功或者所有失败为止。

这个行为是经过 options ndots 来指定的,其默认值为1,这意味着只要被解析域名中有任何一个点(.),那么它就会被当作 FQDN,而不会附加任何 search domain,直接用来查询。OpenShift 环境中,这个值被设置为 5。这意味着,只要被解析域名中包含不超过五个点,该域名就会被当作PQDN,而后挨个使用 search domain,来组装成 FQDN 来作DNS查询。若是所有不成功过,则会尝试将它直接做为 FQDN 来解析。

 

所以,这某些场景中,pod 中的DNS 查询速度会下降应用的性能。解决方法主要有两种,要么直接使用 FQDN,要么减少 ndots 的值,具体请查看 Kubernetes 和 DNS 的有关文档。

1.2 Pod 所在宿主机上的 DNS 配置及服务

1.2.1 resolv.conf 文件

[root@node2 cloud-user]# cat /etc/resolv.conf
# nameserver updated by /etc/NetworkManager/dispatcher.d/99-origin-dns.sh
# Generated by NetworkManager
search cluster.local exampleos.com
nameserver 172.22.122.9

在部署环境时,会在每一个节点上部署 /etc/NetworkManager/dispatcher.d/99-origin-dns.sh 文件。每当节点上的 NetworkManager 服务启动时,该文件会被运行。它的任务包括:

 

  • 建立 dnsmasq 配置文件 :
    • node-dnsmasq.conf (在个人 3.11 版本环境上没有建立该文件,见下文分析)
    • origin-dns.conf  
    • origin-upstream-dns.conf
  • 当 NetworkManager 服务启动时启动 dnsmasq 服务
  • 设置宿主机的全部默认路由 IP 为 Dnsmasq 的侦听IP
  • 修改 /etc/resolv.conf,设置搜索域,以及将宿主机的默认 IP 做为 nameserver
  • 建立 /etc/origin/node/resolv.conf

也就是说,宿主机上的 DNS 请求也会转到本机上的 53 端口。

1.2.2 dnsmasq 及其配置

宿主机上的 53 端口上,dnsmasq 服务在route 默认路由的全部IP的53端口上侦听。其中一个负责接受并处理宿主机上全部pod 中以及宿主机上的全部DNS查询服务。

tcp 0 0 10.128.2.1:53 0.0.0.0:* LISTEN 906/dnsmasq 
tcp 0 0 172.17.0.1:53 0.0.0.0:* LISTEN 906/dnsmasq 
tcp 0 0 172.22.122.9:53 0.0.0.0:* LISTEN 906/dnsmasq

这些 IP 地址和默认路由IP 地址是符合的:

10.128.0.0      0.0.0.0         255.252.0.0     U     0      0        0 tun0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0 172.22.122.0 0.0.0.0 255.255.255.0 U 100 0 0 eth0 172.30.0.0 0.0.0.0 255.255.0.0 U 0 0 0 tun0

dnsmasq 服务的配置目录为 /etc/dnsmasq.d。其中有两个配置文件(具体含义请查阅有关文档):

复制代码
[root@node2 dnsmasq.d]# cat origin-dns.conf 
no-resolv
domain-needed
no-negcache
max-cache-ttl=1
enable-dbus
dns-forward-max=10000
cache-size=10000
bind-dynamic
min-port=1024
except-interface=lo
# End of config
复制代码

文件 origin-upstream-dns.conf 中定义了上游(upstream) DNS 名字服务器:

[root@node2 dnsmasq.d]# cat origin-upstream-dns.conf 
server=172.22.122.3
server=172.22.122.2
server=172.22.122.4

这些上游服务器的地址是从 DHCP 服务器中获取到的(个人OpenShift 环境搭建在OpenStack虚拟机中。前两个地址是OpenStack neutron 网络的 DNSmasq 地址,最后一个是单独搭建的 bind9  DNS 服务器地址)。

在早期版本中(个人OpenShift版本是 3.11),还有一个配置文件 node-dnsmasq.conf :

server=/in-addr.arpa/127.0.0.1
server=/cluster.local/127.0.0.1

这意味着全部以 cluster.local 和 in-addr.arpa 结尾的域名,都会被转到 127.0.0.1:53 上被解析。而其它的解析请求,会被转到在 origin-upstream-dns.conf  中定义的上游 DNS 服务器。

个人3.11版本环境中并无生成该文件。从代码 https://github.com/openshift/origin/blob/master/pkg/dns/dnsmasq.go 看,OpenShift 中的 dnsmasq 在启动时会自动添加这两条记录:

而 dnsIP 和 dnsDomain 应该是在 /etc/origin/node/node-config.yaml 中的以下配置:

dnsBindAddress: 127.0.0.1:53
dnsDomain: cluster.local
从 dnsmasq 日志中也能看到相关记录:
Dec  3 14:10:57 dnsmasq[29595]: using nameserver 127.0.0.1#53 for domain in-addr.arpa
Dec  3 14:10:57 dnsmasq[29595]: using nameserver 127.0.0.1#53 for domain cluster.local

从上面的分析可见,在 node 节点上的 dnsmasq,其实只是一个DNS 查询转发器(转到上游DNS 服务器或者本机上的 SkyDns)和结果缓存器,它自己并不保存域名的原始记录。

1.2.3 SkyDNS 及其配置

关于 SkyDNS:它是一个开源的构建在 etcd 之上的分布式服务宣告(announcement)和发现(discovery)服务。利用它,能够经过 DNS 查询来发现可用的服务。其开源社区的地址是 https://github.com/skynetservices/skydns。社区版本的 SkyDns 将记录保存在 etcd 中,在作查询时从etcd 获取数据并封装成 DNS 结果格式给客户端。

SkyDNS 的 server 部分支持被做为库文件使用,此时能够为其实现其它后端。在OpenShift 中并无采用默认的 etcd 后端,而是基于 OpenShift API 服务实现了新的后端,其代码在https://github.com/openshift/origin/blob/master/pkg/dns/ 。SkyDns 调用 OpenShift API 服务来获取主机名、IP地址等信息,而后封装成标准 DNS 记录并返回给查询客户端。 

在 127.0.0.1:53 上,包装在 openshift 进程中的 SkyDNS 在侦听。
tcp        0      0 127.0.0.1:53            0.0.0.0:*               LISTEN      17182/openshift 

Node 节点上的 SkyDN 要么从cache 中直接回答 DNS 查询,要么调用 OpenShift API 服务来获取数据再返回。

1.3 Master 节点上的 DNS 服务

resolv.conf 文件同Node 节点上的:

[root@master1 cloud-user]# cat /etc/resolv.conf 
# nameserver updated by /etc/NetworkManager/dispatcher.d/99-origin-dns.sh
# Generated by NetworkManager
search cluster.local haihangyun.cn exampleos.com
nameserver 172.22.122.5

dnsmasq 在多个IP 地址的 53 端口上侦听,为本机上的以及本机上Pod 中的DNS查询服务:

udp        0      0 10.128.0.1:53           0.0.0.0:*                           866/dnsmasq         
udp        0      0 172.17.0.1:53           0.0.0.0:*                           866/dnsmasq         
udp        0      0 172.22.122.5:53         0.0.0.0:*                           866/dnsmasq 

和 Node 节点不一样,Master 节点上有两个SkyDns 进程。一个在 127.0.0.1:53 侦听,负责本机上的集群内服务的DNS查询,由于 Master 节点同时承担 node 节点的角色:

udp        0      0 127.0.0.1:53            0.0.0.0:*                           11700/openshift 
Dec  3 14:50:41 dnsmasq[10607]: using nameserver 127.0.0.1#53 for domain cluster.local
Dec  3 14:50:41 dnsmasq[10607]: using nameserver 127.0.0.1#53 for domain in-addr.arpa

另外一个是在全部网卡的 8053 端口上侦听,这是由于Master 还具备 master api 角色:

udp        0      0 0.0.0.0:8053            0.0.0.0:*                           15096/openshift 

对于这个 SkyDns 进程的做用尚不清楚,还需进一步研究。从已有资料上看看,全部节点上都须要安装 SkyDns,并组成一个分布式集群。由于 Master 节点上的 53 端口被另外一个 SkyDns 进程占用,所以换到了端口8053。

2. DNS 查询流程

2.1 pod 内的应用经过域名访问外网服务器的DNS查询流程

流程示意图如最上面图中的 1 和 2.1 部分所示。

dnsmasq 日志:

复制代码
Nov 21 11:03:44 dnsmasq[17788]: using nameserver 172.22.122.3#53
Nov 21 11:03:44 dnsmasq[17788]: using nameserver 172.22.122.2#53
Nov 21 11:03:44 dnsmasq[17788]: using nameserver 172.22.122.4#53
Nov 21 11:03:49 dnsmasq[17788]: query[A] www.sina.com from 172.22.122.13
Nov 21 11:03:49 dnsmasq[17788]: forwarded www.sina.com to 172.22.122.4
Nov 21 11:03:49 dnsmasq[17788]: forwarded www.sina.com to 172.22.122.2
Nov 21 11:03:49 dnsmasq[17788]: forwarded www.sina.com to 172.22.122.3
Nov 21 11:03:49 dnsmasq[17788]: reply spool.grid.sinaedge.com is 124.228.42.248
复制代码

能看到 node 上的 dnsmasq 直接将查询请求转发给上游 DNS 名字服务器。由于存在多个名字服务器,因此是依次查询,直到成功为止。从日志看,其查询顺序和配置文件中的顺序是相反的。

2.2 Pod 内应用经过服务域名查找其IP 地址

流程示意图如上图中的 1 + 2.2 + 3 部分所示。

日志实例:

(1)从一个 pod 中 ping registry-console服务的域名 registry-console.default.svc.cluster.local。

(2)Node宿主机(IP 地址为 172.22.122.13)上的 dnsmasq 收到该查询。

(3)dnsmasq 将查询转到 127.0.0.1:53 上的 SkyDns 服务。

(4)SkyDNS 作查询。SkyDNS 能接收的域名格式:<prefix>.<service_name>.<namespace>.(svc|endpoints|pod).<base>,这意味着它支持查询服务(svc)、端点(endpoints)和 pod 的 DNS信息。

查询结果:

复制代码
[root@node2 cloud-user]# nsenter -t 4216 -n dig mybank.dev.svc.cluster.local          

;; QUESTION SECTION:
;mybank.dev.svc.cluster.local.  IN      A

;; ANSWER SECTION:
mybank.dev.svc.cluster.local. 30 IN     A       172.30.162.172

;; Query time: 1 msec
;; SERVER: 172.22.122.9#53(172.22.122.9)
;; WHEN: Mon Dec 03 11:43:01 CST 2018
;; MSG SIZE  rcvd: 62
复制代码

dnsmasq 日志:

Dec  3 14:19:44 dnsmasq[29595]: query[A] mybank.dev.svc.cluster.local from 10.128.2.128
Dec  3 14:19:44 dnsmasq[29595]: forwarded mybank.dev.svc.cluster.local to 127.0.0.1
Dec  3 14:19:44 dnsmasq[29595]: reply mybank.dev.svc.cluster.local is 172.30.162.172

(5)其它实验:查询服务的全部端点

查询结果:

复制代码
[root@node2 cloud-user]# nsenter -t 4216 -n dig jenkins.dev.endpoints.cluster.local

;; QUESTION SECTION:
;jenkins.dev.endpoints.cluster.local. IN        A

;; ANSWER SECTION:
jenkins.dev.endpoints.cluster.local. 30 IN A    10.128.2.81
jenkins.dev.endpoints.cluster.local. 30 IN A    10.131.1.70
复制代码

dnsmasq 日志:

Dec  3 14:20:48 dnsmasq[29595]: query[A] jenkins.dev.endpoints.cluster.local from 10.128.2.128
Dec  3 14:20:48 dnsmasq[29595]: forwarded jenkins.dev.endpoints.cluster.local to 127.0.0.1
Dec  3 14:20:48 dnsmasq[29595]: reply jenkins.dev.endpoints.cluster.local is 10.128.2.81
Dec  3 14:20:48 dnsmasq[29595]: reply jenkins.dev.endpoints.cluster.local is 10.131.1.70

(6)查询 pod

待查询的pod域名的格式为 <IP_with_dashes>.<namespace>.pod.<base>,SkyDns 会返回其IP 地址,但我没明白这么作的场景和价值,也许是确认pod是否存在?

查询结果:

复制代码
[root@node2 cloud-user]# nsenter -t 4216 -n dig 172-30-162-172.dev.pod.cluster.local

;; QUESTION SECTION:
;172-30-162-172.dev.pod.cluster.local. IN A

;; ANSWER SECTION:
172-30-162-172.dev.pod.cluster.local. 30 IN A   172.30.162.172

;; Query time: 1 msec
;; SERVER: 172.22.122.9#53(172.22.122.9)
;; WHEN: Mon Dec 03 13:32:05 CST 2018
;; MSG SIZE  rcvd: 70
复制代码

dnsmasq 日志:

Dec  3 14:22:24 dnsmasq[29595]: query[A] 172-30-162-172.dev.pod.cluster.local from 10.128.2.128
Dec  3 14:22:24 dnsmasq[29595]: forwarded 172-30-162-172.dev.pod.cluster.local to 127.0.0.1
Dec  3 14:22:24 dnsmasq[29595]: reply 172-30-162-172.dev.pod.cluster.local is 172.30.162.172

(7)对比 FQDN 和 PQDN

这个 PQDN 被加上了搜索域名再进行查询,能返回正确的IP地址:

[root@node2 cloud-user]# nsenter -t 4216 -n ping mybank.dev.svc
PING mybank.dev.svc.cluster.local (172.30.162.172) 56(84) bytes of data.

而这个 FQDN 被直接作DNS查询,结果查询失败,未能获取IP地址:

[root@node2 cloud-user]# nsenter -t 4216 -n ping mybank.dev.svc.
ping: mybank.dev.svc.: Name or service not known

2.3 从外网经过服务域名访问pod 中运行的服务

 

能够看出,该过程当中只涉及到外部DNS将服务的公共域名解析为 OpenShift Router 所在节点的公网地址,后面 HAProxy 做为代理,直接经过 IP 访问pod,并将结果返回客户端。

 

参考文档:

 

理解OpenShift(3):网络之 SDN

理解OpenShift(1):网络之 Router 和 Route

理解OpenShift(2):网络之 DNS(域名服务)

理解OpenShift(3):网络之 SDN

理解OpenShift(4):用户及权限管理

理解OpenShift(5):从 Docker Volume 到 OpenShift Persistent Volume

 

** 本文基于 OpenShift 3.11,Kubernetes 1.11 进行测试 ***

 

1. 概况

为了OpenShift 集群中 pod 之间的网络通讯,OpenShift 以插件形式提供了三种符合Kubernetes CNI 要求的 SDN实现:

  • ovs-subnet:ovs-subnet 实现的是一种扁平网络,未实现租户之间的网络隔离,这意味着全部租户之间的pod 均可以互访,这使得该实现没法用于绝大多数的生产环境。
  • ovs-multitenant:基于 OVS 和 VxLAN 等技术实现了项目(project)之间的网络隔离。
  • ovs-networkpolicy:介于ovs-subnet 和 ovs-multitenant 之间的一种实现。考虑到 ovs-multitenant  只是实现了项目级别的网络隔离,这种隔离粒度在一些场景中有些过大,用户无法作更精细的控制,这种需求致使了ovs-networkpolicy的出现。默认地,它和ovs-subnet 同样,全部租户之间都没有网络隔离。可是,管理员能够经过定义 NetworkPolicy 对象来精细地进行网络控制。能够粗略地将它类比为OpenStack neutron 中的neutron 网络防火墙和Nova安全组。具体请查阅有关文档。

当使用 ansible 部署 OpenShift 时,默认会启用ovs-subnet,可是能够在部署完成后修改成其它两种实现。本文中的说明都是针对 ovs-multitenant。

1.1 OpenShift 集群的网络设计

要部署一个OpenShift 生产环境,主要的网络规划和设计以下图所示:

 

 

节点角色类型:

  • Master 节点:只承担 Master 角色,可不也能够承担Node 角色。主要运行 API 服务、controller manager 服务、etcd 服务、web console 服务等。
  • Infra 节点:做为 Node 角色,经过设置并应用节点标签,只用于部署系统基础服务,包括Registry、Router、Prometheus 以及 EFK 等。
  • Node 节点:做为 Node 角色,用于运行用户业务系统的Pod。

网络类型:

  • 外部网络:这是一个外部网络,用于从外部访问集群。和该网络链接的服务器或组件须要被分配公网IP地址才能被从外部访问。从内部访问外网中的服务时,好比DNS或者镜像仓库,能够经过NAT实现,而无需公网IP地址。
  • 管理网络:这是一个内部网络,用于集群内部 API 访问。
  • IPMI网络:这是一个内部网络,用于管理物理服务器。
  • SDN网络:这是一个内部网络,用于集群内部Pod 之间的通讯,承载 VxLAN Overlay 流量。
  • 存储网络:这是一个内部网络,用于各节点访问基于网络的存储。

在PoC 或开发测试环境中,管理/SDN/存储网络能够合并为一个网络。

1.2 Node节点中的网络

 

节点上的主要网络设备:

  • br0:OpenShift 建立和管理的 Open vSwitch 网桥, 它会使用 OpenFlow 规则来实现网络隔离和转发。
  • vethXXXXX:veth 对,它负责将 pod 的网络命名空间链接到 br0 网桥。
  • tun0 :一OVS 内部端口,它会被分配本机的 pod 子网的网关IP 地址,用于OpenShift pod 以及Docker 容器与集群外部的通讯。iptables 的 NAT 规则会做用于tun0。
  • docker0:Docker 管理和使用的 linux bridge 网桥,经过 veth 对将不受 OpenShift 管理的Docker 容器的网络地址空间链接到 docker0 上。
  • vovsbr/vlinuxbr:将 docker0 和 br0 链接起来的 veth 对,使得Docker 容器能和 OpenShift pod 通讯,以及经过 tun0 访问外部网络
  • vxlan0:一OVS VXLAN 隧道端点,用于集群内部 pod 之间的网络通讯。

2. 实现

2.1 pod 网络整体设置流程

Pod 网络整体设置流程以下(来源:OpenShift源码简析之pod网络配置(上)):

简单说明:

  • OpenShift 使用运行在每一个节点上的 kubelet 来负责pod 的建立和管理,其中就包括网络配置部分。
  • 当 kubelet 接受到 pod 建立请求时,会首先调用docker client 来建立容器,而后再调用 docker api接口启动上一步中建立成功的容器。kubelet 在建立 pod 时是先建立一个 infra 容器,配置好该容器的网络,而后建立真正用于业务的应用容器,最后再把业务容器的网络加到infra容器的网络命名空间中,至关于业务容器共享infra容器的网络命名空间。业务应用容器和infra容器共同组成一个pod。
  • kubelet 使用 CNI 来建立和管理Pod网络(openshift在启动kubelet时传递的参数是--netowrk-plugin=cni)。OpenShift 实现了 CNI 插件(由 /etc/cni/net.d/80-openshift-network.conf 文件指定),其二进制文件是 /opt/cni/bin/openshift-sdn 。所以,kubelet 经过 CNI 接口来调用 openshift sdn 插件,而后具体作两部分事情:一是经过 IPAM 获取 IP 地址,二是设置 OVS(其中,一是经过调用 ovs-vsctl 将 infra 容器的主机端虚拟网卡加入 br0,二是调用 ovs-ofctl 命令来设置规则)。

2.2 OVS 网桥 br0 中的规则

本部份内容主要引用自 OVS 在云项目中的使用

流量规则表:

 

  • table 0: 根据输入端口(in_port)作入口分流,来自VXLAN隧道的流量转到表10并将其VXLAN VNI 保存到 OVS 中供后续使用,从tun0过阿里的(来自本节点或进本节点来作转发的)流量分流到表30,将剩下的即本节点的容器(来自veth***)发出的流量转到表20;
  • table 10: 作入口合法性检查,若是隧道的远端IP(tun_src)是某集群节点的IP,就认为是合法,继续转到table 30去处理;
  • table 20: 作入口合法性检查,若是数据包的源IP(nw_src)与来源端口(in_port)相符,就认为是合法的,设置源项目标记,继续转到table 30去处理;若是不一致,便可能存在ARP/IP欺诈,则认为这样的的数据包是非法的;
  • table 30: 数据包的目的(目的IP或ARP请求的IP)作转发分流,分别转到table 40~70 去处理;
  • table 40: 本地ARP的转发处理,根据ARP请求的IP地址,从对应的端口(veth)发出;
  • table 50: 远端ARP的转发处理,根据ARP请求的IP地址,设置VXLAN隧道远端IP,并从隧道发出;
  • table 60: Service的转发处理,根据目标Service,设置目标项目标记和转发出口标记,转发到table 80去处理;
  • table 70: 对访问本地容器的包,作本地IP的转发处理,根据目标IP,设置目标项目标记和转发出口标记,转发到table 80去处理;
  • table 80: 作本地的IP包转出合法性检查,检查源项目标记和目标项目标记是否匹配,或者目标项目是不是公开的,若是知足则转发;(这里实现了 OpenShift 网络层面的多租户隔离机制,其实是根据项目/project 进行隔离,由于每一个项目都会被分配一个 VXLAN VNI,table 80 只有在网络包的VNI和端口的VNI tag 相同才会对网络包进行转发)
  • table 90: 对访问远端容器的包,作远端IP包转发“寻址”,根据目标IP,设置VXLAN隧道远端IP,并从隧道发出;
  • table 100: 作出外网的转出处理,将数据包从tun0发出。

 

备注一些经常使用的操做命令:

  • 查询OVS 流表: ovs-ofctl -O OpenFlow13 dump-flows br0
  • 查询OVS设备: ovs-vsctl show
  • 查看OVS网桥: ovs-ofctl -O OpenFlow13 show br0
  • 查看路由表:route -n
  • 在容器中运行命令:nsenter -t <容器的PiD> -n ip a
  • 查询 iptables NAT 表:iptables -t nat -S

3. 流程

3.1 同一个节点上的两个pod 之间的互访

访问:pod 1 (ip:10.131.1.150)访问 pod2(10.131.1.152)

网络路径::pod1的eth0 → veth12 → br0 → veth34 → pod2的eth0。 

OVS 流表:

复制代码
table=0, n_packets=14631632, n_bytes=1604917617, priority=100,ip actions=goto_table:20
table=20, n_packets=166585, n_bytes=12366463, priority=100,ip,in_port=96,nw_src=10.131.1.152 actions=load:0xbe3127->NXM_NX_REG0[],goto_table:21
table=21, n_packets=14671413, n_bytes=1606835395, priority=0 actions=goto_table:30
table=30, n_packets=8585493, n_bytes=898571869, priority=200,ip,nw_dst=10.131.0.0/23 actions=goto_table:70
table=70, n_packets=249967, n_bytes=16177300, priority=100,ip,nw_dst=10.131.1.152 actions=load:0xbe3127->NXM_NX_REG1[],load:0x60->NXM_NX_REG2[],goto_table:80
table=80, n_packets=0, n_bytes=0, priority=100,reg0=0xbe3127,reg1=0xbe3127 actions=output:NXM_NX_REG2[]
table=80, n_packets=0, n_bytes=0, priority=0 actions=drop #不合法的包会被丢弃
复制代码

表 20 会判断包类型(IP)、源地址(nw_src)、进来端口的ID(96),将其对应的 VNI ID(这里是 0xbe3127,十进制是12464423)保存在 REG0 中。这意味着全部经过OVS 端口进入OVS br0 网桥的来自pod 的网络包都会被打上对口对应的VNID 标签。集群中全部项目对应的 VNID 可使用 oc get netnamespaces 命令查到:

复制代码
[root@master1 cloud-user]# oc get netnamespaces
NAME                                NETID      EGRESS IPS
cicd                                16604171   []
default                             0          []
demoproject2                        16577323   []
demoprojectone                      1839630    []
dev                                 12464423   []
复制代码

表 70 会根据目的地址,也就是目的 pod 的地址,将网络包的目的出口标记(这里为 0x60,十进制为96)保存到REG2,同时设置其项目的 VNI ID 到 REG1(这里是0xbe3127).

根据端口的ID 96 找到veth网络设备:

96(veth0612e07f): addr:66:d0:c3:e3:be:cf
     config:     0
     state:      0
     current:    10GB-FD COPPER
     speed: 10000 Mbps now, 0 Mbps max

查找其对应的容器中的网卡。

[root@node1 cloud-user]# ip link  | grep veth0612e07f
443: veth0612e07f@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1400 qdisc noqueue master ovs-system state UP mode DEFAULT 

这与pod2容器中的 eth0 正好吻合:

3: eth0@if443: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1400 qdisc noqueue state UP 
    link/ether 0a:58:0a:83:01:98 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.131.1.152/23 brd 10.131.1.255 scope global eth0
       valid_lft forever preferred_lft forever

表80 会检查报的来源 VNI ID (REG0)和目的端口的 VNI ID (REG1),将相符的合法的包转发到表70 设置的出口,以完成转发。

3.2 不一样节点上的两个pod 之间的互访

网络路径:节点1上的Pod1的eth0→veth1→br0→vxlan0→ 节点1的eth0网卡→ 节点2的eth0网卡→vxlan0→br0→veth1→ Pod3的eth0流表:

发送端(node1)的OVS 流表:

table=0, n_packets=14703186, n_bytes=1612904326, priority=100,ip actions=goto_table:20
table=20, n_packets=167428, n_bytes=12428845, priority=100,ip,in_port=96,nw_src=10.131.1.152 actions=load:0xbe3127->NXM_NX_REG0[],goto_table:21
table=21, n_packets=14736461, n_bytes=1613954556, priority=0 actions=goto_table:30
table=30, n_packets=1143761, n_bytes=1424533777, priority=100,ip,nw_dst=10.128.0.0/14 actions=goto_table:90
table=90, n_packets=0, n_bytes=0, priority=100,ip,nw_dst=10.128.2.0/23 actions=move:NXM_NX_REG0[]->NXM_NX_TUN_ID[0..31],set_field:172.22.122.9->tun_dst,output:1
  • 表21 一样是将源pod 的 VNI ID 保存在 REG0 中。
  • 表30 会判断目的地址是否是集群的大的 pod 的 IP CIDR。
  • 表90 会设置 VNI ID 为以前保存在 REG0 中的值,而后根据目的地址的网段(这里是 10.128.2.0/23),计算出其所在的节点的IP 地址(这里是 172.22.122.9)并设置为tun_dst,而后发到 vxlan0,它会负责根据提供的信息来作VXLAN UDP 包封装。

接收端(node2)的OVS 流表: 

table=0, n_packets=1980863, n_bytes=1369174876, priority=200,ip,in_port=1,nw_src=10.128.0.0/14 actions=move:NXM_NX_TUN_ID[0..31]->NXM_NX_REG0[],goto_table:10
table=10, n_packets=0, n_bytes=0, priority=100,tun_src=172.22.122.8 actions=goto_table:30
table=30, n_packets=16055284, n_bytes=1616511267, priority=200,ip,nw_dst=10.128.2.0/23 actions=goto_table:70
table=70, n_packets=248860, n_bytes=16158751, priority=100,ip,nw_dst=10.128.2.128 actions=load:0xbe3127->NXM_NX_REG1[],load:0x32->NXM_NX_REG2[],goto_table:80
table=80, n_packets=0, n_bytes=0, priority=100,reg0=0xbe3127,reg1=0xbe3127 actions=output:NXM_NX_REG2[]
  • 表0 会将发送到保存在 NXM_NX_TUN_ID[0..31] 中的源 VNI ID 取出来保存到 REG0.
  • 表10 会检查包的来源节点的地址。
  • 表30 会检查包的目的地址是否是本机上 pod 的网段。
  • 表70 会根据目的地址,将目的 VNI ID 保存到 REG1,将目的端口 ID 保存到 REG2
  • 表80 会检查目的 VNI ID 和源 VNI ID,若是相符的话,则将包转发到保存在 REG2 中的目的端口ID 指定的端口。而后包就会经过 veth 管道进入目的 pod。

3.3 pod 内访问外网

网络路径:PodA的eth0 → vethA → br0 → tun0 → 经过iptables实现SNAT → 物理节点的 eth0  → 互联网

NAT:将容器发出的IP包的源IP地址修改成宿主机的 eth0 网卡的IP 地址。

OVS 流表:

table=0, n_packets=14618128, n_bytes=1603472372, priority=100,ip actions=goto_table:20
table=20, n_packets=0, n_bytes=0, priority=100,ip,in_port=17,nw_src=10.131.1.73 actions=load:0xfa9a3->NXM_NX_REG0[],goto_table:21
table=21, n_packets=14656675, n_bytes=1605262241, priority=0 actions=goto_table:30
table=30, n_packets=73508, n_bytes=6820206, priority=0,ip actions=goto_table:100
table=100, n_packets=44056, n_bytes=3938540, priority=0 actions=goto_table:101
table=101, n_packets=44056, n_bytes=3938540, priority=0 actions=output:2

表20 会检查 IP 包的来源端口和IP 地址,并将源项目的 VNI ID 保存到 REG0.

表101 会将包发送到端口2 即 tun0. 而后被 iptables 作 NAT 而后发送到 eth0.

3.4 外网访问 pod

由于 Infra 节点上的 HAproxy 容器采用了 host-network 模式,所以它是直接使用宿主机的 eth0 网卡的。

下面是宿主机的路由表:

复制代码
[root@infra-node1 /]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.22.122.1    0.0.0.0         UG    100    0        0 eth0
10.128.0.0      0.0.0.0         255.252.0.0     U     0      0        0 tun0
169.254.169.254 172.22.122.1    255.255.255.255 UGH   100    0        0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0
172.22.122.0    0.0.0.0         255.255.255.0   U     100    0        0 eth0
172.30.0.0      0.0.0.0         255.255.0.0     U     0      0        0 tun0
复制代码

从 HAProxy 容器内出来目的地址为业务pod(ip:10.128.2.128)的网络包,根据上面的路由表,其下一跳是 tun0,也就是说它又进入了 OVS 网桥 br0. 对应的 OVS 流表规则为:

ip,in_port=2 actions=goto_table:30
ip,nw_dst=10.128.0.0/14 actions=goto_table:90
ip,nw_dst=10.128.2.0/23 actions=move:NXM_NX_REG0[]->NXM_NX_TUN_ID[0..31],set_field:172.22.122.9->tun_dst,output:1

可见它最终又被发到了端口1 即 vxlan0,它会负责作 vxlan 封包,并经过 eth0 网卡发出去。 

3.5 汇总

整体来讲,OVS 中的OpenFlow流表根据网络包的目的地址将其分为四类来处理:

  • 到本地pod的,直接在 br0 中转发。
  • 到本集群pod 的,通过 br0 后发到 vxlan0,封装为 vxlan udp 包经物理网卡发到对方节点。
  • 到本地不受OpenShift SDN管理的docker容器的,还未具体研究。
  • 到集群外的,通过 br0 后发到 tun0,通过 iptables 作SNAT,而后经物理网卡发出。

3.6. 项目(project)级别的网络隔离

3.6.1 原理

OpenShift 中的网络隔离是在项目(project)级别实现的。OpenShfit 默认的项目 『default』的 VNID (Virtual Network ID)为0,代表它是一个特权项目,由于它能够发网络包到其它全部项目,也能接受其它全部项目的pod发来的网络包。这从 table 80 的规则上能够看出来,若是来源项目的 VNID (reg0)或目标项目的 VNID(reg1)为0,都会容许包转发到pod 的端口:

table=80, n_packets=8244506, n_bytes=870316191, priority=200,reg0=0 actions=output:NXM_NX_REG2[]
table=80, n_packets=13576848, n_bytes=1164951315, priority=200,reg1=0 actions=output:NXM_NX_REG2[]

其它全部项目都会有一个非0的 VNID。在 OpenShift ovs-multitenant 实现中,非0 VNID 的项目之间的网络是不通的。

从一个本地 pod 发出的全部网络流量,在它进入 OVS 网桥时,都会被打上它所经过的 OVS 端口ID相对应的 VNID。port:VNID 映射会在pod 建立时经过查询master 上的 etcd 来肯定。从其它节点经过 VXLAN发过来的网络包都会带有发出它的pod 所在项目的 VNID。

根据上面的分析,OVS 网桥中的 OpenFlow 规则会阻止带有与目标端口上的 VNID 不一样的网络包的投递(VNID 0 除外)。这就保证了项目之间的网络流量是互相隔离的。

可使用下面的命令查看namespace 的 NETID 也就是 VNID:

在个人环境里面,default 项目默认就是 global的,我还把 cicd 项目设置为 gloabl 的了,由于它也须要访问其它项目。

3.6.2 实验

下图显示了两个项目之间的三种网络状态:

  • 左图显示的是默认状态:SIT 项目和 Dev 项目之间的 pod 没法访问。根据前面对 OVS 流表的分析,表80 会检查IP 包的来源Pod的项目 VNI ID 和目标Pod的项目 VNI ID。若是二者不符合,这些IP网络包就会被丢弃。
  • 中间图显示的是打通这两个项目的网络:经过运行 oc adm pod-network join-projects 命令,将两个项目链接在一块儿,结果就是 DEV 项目的 VNI ID 变成了 SIT 项目的 VNI ID。这时候两个项目中的 pod 网络就通了。
  • 右图显示的是分离这两个项目的网络:经过运行 oc adm pod-network isolate-projects 命令,将两个项目分离,其结果是 DEV 项目被分配了新的 VNI ID。此时两个项目中的pod 又不能互通了。

3.7 CluserIP 类型的 Service

OpenShift Serivce 有多种类型,默认的和最经常使用的是 ClusterIP 类型。每一个这种类型的Service,建立时都会被从一个子网中分配一个IP地址,在集群内部可使用该IP地址来访问该服务,进而访问到它后端的pod。所以,Service 其实是用于OpenShift 集群内部的四层负载均衡器,它是基于 iptables 实现的。

接下来我以 mybank 服务为例进行说明,它的  ClusterIP 是  172.30.162.172,服务端口是8080;它有3个后端 10.128.2.128:8080,10.131.1.159:8080,10.131.1.160:8080。

宿主机上的路由表:

复制代码
[root@node1 cloud-user]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.22.122.1    0.0.0.0         UG    100    0        0 eth0
10.128.0.0      0.0.0.0         255.252.0.0     U     0      0        0 tun0   #3.7.1 中会用到
169.254.169.254 172.22.122.1    255.255.255.255 UGH   100    0        0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0
172.22.122.0    0.0.0.0         255.255.255.0   U     100    0        0 eth0   #3.7.1 中会用到
172.30.0.0      0.0.0.0         255.255.0.0     U     0      0        0 tun0   #3.7.2 中会用到
复制代码

3.7.1 从宿主机上访问服务

每当建立一个 service 后,OpenShift 会在集群的每一个节点上的 iptables 中添加如下记录:

复制代码
-A KUBE-SERVICES -d 172.30.162.172/32 -p tcp -m comment --comment "dev/mybank:8080-tcp cluster IP" -m tcp --dport 8080 -j KUBE-SVC-3QLA52JX7QFEEEC5

-A KUBE-SVC-3QLA52JX7QFEEEC5 -m comment --comment "dev/mybank:8080-tcp" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-AWPSVWBUXH7A2CLB
-A KUBE-SVC-3QLA52JX7QFEEEC5 -m comment --comment "dev/mybank:8080-tcp" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-ESYZLBFGDE6MOHX2
-A KUBE-SVC-3QLA52JX7QFEEEC5 -m comment --comment "dev/mybank:8080-tcp" -j KUBE-SEP-ENPHHSSNP6FR7JJI

-A KUBE-SEP-AWPSVWBUXH7A2CLB -p tcp -m comment --comment "dev/mybank:8080-tcp" -m tcp -j DNAT --to-destination 10.128.2.128:8080

-A KUBE-SVC-3QLA52JX7QFEEEC5 -m comment --comment "dev/mybank:8080-tcp" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-ESYZLBFGDE6MOHX2

-A KUBE-SEP-ENPHHSSNP6FR7JJI -p tcp -m comment --comment "dev/mybank:8080-tcp" -m tcp -j DNAT --to-destination 10.131.1.160:8080

复制代码
  • 第1条:检查目的IP地址以及端口,添加comment
  • 第2到5条:以随机分配(random)方式将流量平均地转发到三条规则上
  • 第6条:第一条转发规则经过DNAT 将目的IP地址和端口修改成第一个endpoint 的IP 和地址,第7和8条相同

DNAT 后,根据路由表,下一跳将是 tun0,也就是说它会进入 OVS 网桥 br0。在进入网桥以前,若是是从pod 中发出的网络包,还会进行SNAT,将其源IP地址修改成 tun0 的IP 地址。其目的是使得返回包能回到tun0,而后能经过反SNAT 操做,将目的IP地址由 tun0 的IP 修改成原来的源IP。具体见下文的分析。

-A OPENSHIFT-MASQUERADE -s 10.128.0.0/14 -m comment --comment "masquerade pod-to-service and pod-to-external traffic" -j MASQUERADE

而后,进入网桥。在网桥中,会检查目的地址。若是是本地 pod 网段内的,那么将直接转发给对应的pod;若是是远端pod的,那么转发到 vxlan0 再经过 VXLAN 网络发到对方节点。这过程跟上面说明的过程就差很少了,再也不赘述。

3.7.2 从 pod 中访问 service

从某个 pod 中访问同一个 service。IP 包从 br0 的某个端口进入 OVS,而后执行如下流表规则:

table=30, n_packets=14212117, n_bytes=1219709382, priority=100,ip,nw_dst=172.30.0.0/16 actions=goto_table:60
table=60, n_packets=0, n_bytes=0, priority=100,ip,nw_dst=172.30.162.172,nw_frag=later actions=load:0xbe3127->NXM_NX_REG1[],load:0x2->NXM_NX_REG2[],goto_table:80
table=60, n_packets=0, n_bytes=0, priority=100,tcp,nw_dst=172.30.162.172,tp_dst=8080 actions=load:0xbe3127->NXM_NX_REG1[],load:0x2->NXM_NX_REG2[],goto_table:80 table=80, n_packets=0, n_bytes=0, priority=100,reg0=0xbe3127,reg1=0xbe3127 actions=output:NXM_NX_REG2[]

从 table60 能够看出,OVS 流表给该网络包设置的出口端口为2,即 tun0,由于要去作NAT。出去后,即开始 iptables NAT 过程,也就是 3.7.1 中的过程。最后仍是要回到 OVS br0,再走到 vxlan0,经过 VXLAN 隧道发到目标pod 所在的宿主机。该过程示意图以下:

对于返回的网络包,其目的地址是源pod 宿主机上的 tun0,即左图中的 10.131.0.1/23. 数据包到达左图中的 br0 后,首先要出 tun0,由于要去作NAT:

table=30, n_packets=1214735, n_bytes=1135728626, priority=300,ip,nw_dst=10.131.0.1 actions=output:2

根据这篇文章(https://superuser.com/questions/1269859/linux-netfilter-how-does-connection-tracking-track-connections-changed-by-nat),发送阶段 iptables 在作 SNAT 时会利用 conntrack 记录此次修改(在/proc/net/nf_conntrack 中);在如今回复包返回的时候,会自动地作相反SNAT操做(相似DNAT),将包的目的IP地址(tun0的IP地址)修改成原来的源IP地址即源pod地址。

/proc/net/nf_conntrack 文件的有关记录:

ipv4     2 tcp      6 70 TIME_WAIT src=10.131.0.1 dst=10.131.1.72 sport=56862 dport=8080 src=10.131.1.72 dst=10.131.0.1 sport=8080 dport=56862 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 zone=0 use=2

作完De-SNAT后,根据路由表,它又会回到 tun0, OVS 根据流表,会根据目的pod IP 地址对它进行转发,使得它回到原来的出发pod。

 

参考文档:

 

理解OpenShift(4):用户及权限管理

理解OpenShift(1):网络之 Router 和 Route

理解OpenShift(2):网络之 DNS(域名服务)

理解OpenShift(3):网络之 SDN

理解OpenShift(4):用户及权限管理

理解OpenShift(5):从 Docker Volume 到 OpenShift Persistent Volume

 

** 本文基于 OpenShift 3.11,Kubernetes 1.11 进行测试 ***

 

OpenShift 支持 RBAC(Role Based Access Controll),基于角色的访问控制。它涉及诸多概念,本文尝试着作一些概念上的梳理,不少细节还须要进一步研究。

1. 主要概念及其之间的联系

1.1 用户(User)

我试着把一个OpenShift 环境中的全部用户分为三大类:

  • 应用用户:部署在集群之中的应用本身的用户。通常来讲每一个应用都有本身的用户管理系统,与平台无关。也有一些应用,好比 Jenkins,支持与OpenShift 用户系统集成,也就是Jenkins容许用户在经过了OpenShift 用户认证后对其进行访问。这部分不是本文的讨论范围以内。
  • OpenShift 用户:访问OpenShift 资源的用户。根据其特征,又将其分为三个子类:
    • Regular user:表明一个天然人用户,好比部署应用的一个开发者。
    • System user:OpenShift 系统用户,大部分在集群建立时被自动建立,好比每一个node都有一个system user。由于这部分主要和OpenShift自身系统相关,与通常用户关系不太大,所以本文不会具体介绍这部分。
    • Service account:服务帐户。这是跟一个项目关联的特殊系统用户,每一个用户被一个 ServiceAccount 对象表示,一般是指 pod 中运行主进程的用户。后文会有具体介绍。
  • 操做系统用户:访问操做系统资源的用户,又分为容器内的操做系统用户,和宿主机上的操做系统用户。

1.2 身份(Identity)与认证(Authentication)

认证是确认用户身份是否合法的过程。这能够有多种实现方式,好比用户名/密码、令牌(token)、证书等。不一样类型的用户有不一样的身份管理方式:

  • 对于 regular user,每一个用户有一个身份(identity)用于认证。OpenShift  以插件形式支持多种 identity provider,好比在测试环境中经常使用的 htpasswd,生产环境中经常使用的 LDAP 等。这些 provider 中会保存用户身份信息,好比用户名和密码。useridentitymapping 对象将 user 对象和 identity 对象联系在一块儿。
  • 对于 service account,一方面它须要访问 OpenShift 集群资源好比 API 和内部镜像仓库中的镜像,另外一方面它可能须要访问 pod 中和宿主机上的操做系统资源,好比宿主机上的文件或网络。对于前者,每一个 service account 使用 secret 来进行身份认证,包括用户 API 访问的 token 和用于从镜像仓库拉取代码的 secret。对于后者,原本有 user namespace(用户命名空间)来支持,可是彷佛OpenShift 还不支持该功能。

1.3 角色(Role)/权限(Permission)与受权(Authorization)

受权是对被认证了的用户访问受控制的资源的权限进行控制。按照资源类型,OpenShift 将受权管理方式分为两大类:

  • 对于 OpenShift 集群资源,好比 pod,deployment,route 等,经过 role (角色)进行控制。从范围(scope)上分,role 可分为集群角色(clusterRole)和项目角色(role)。每一个角色定义了受控制的对象(subject)、容许的操做(verb)和范围(集群仍是项目)。用户(user)和角色(role/clusterRole)之间经过 rolebinding/clusterrolebinding 对象进行绑定。
  • 对于操做系统资源,这只针对服务帐户。宿主机上的用户访问宿主机上的资源,这由宿主机操做系统进行控制。pod 中的用户(serviceaccount)访问pod内和宿主机上操做系统资源,由 scc(security context constraints)进行控制。

2. 身份 (Identity) 与认证(Authentication)

就像人的身份证同样,identity 是一个 user 的身份信息,该信息用于用户认证。OpenShift 本身并无实现用户身份信息库,而是经过灵活支持多种 identity provider,来实现各类不一样的身份管理方案。每种实现都以不一样的方式保存着用户的身份信息,好比保存在LDAP 中,保存在  htpasswd 文件中,保存在 OpenStack Keystone 中。

 

所以,OpenShift 对 user 身份的校验是经过这些配置了的 identity provider 进行的。所以,还须要 OpenShift user 和这些 provider 里面的 identity 的映射关系。OpenShfit 支持四种映射管理,claim,lookup,generate,add。具体请参考官网 https://docs.openshift.com/container-platform/3.11/install_config/configuring_authentication.html#identity-providers_parameters

上图中以 htpasswd 这种 identity provider 为例,说明了从零建立一个 openshift user,在 htpasswd 中建立 user 及其密码,而后建立 openshift identity 对象以及 useridentitymapping 对象的过程。

用户获取用于身份认证的token的过程:

  • 全部申请 OAuth token 的请求都要发到 <master>/oauth/authorize和<master>/oauth/token。每一个申请 OAuth token 的请求中都必须带有 OAuth client 标识,这是一个 OAuthClient  对象。具体请参阅官方文档。
  • OpenShift master 节点上内置有一个 OAuth server。用户从 OAuth 获取 token 后再用它去访问 API 就能够认证经过了。当一个 user 申请一个 OAuth token 时,OAuth 使用配置的 identity provider 去肯定该申请用户的身份。它在肯定该用户所映射到的 identity后,会为该用户建立一个 token,而后返回该token。

3. 角色(Role)和受权(Authorization)

前文说了,角色用于控制对 OpenShift 资源的访问权限,它分为项目角色和集群角色。

OpenShift 系统默认会建立不少的集群角色。经常使用的角色的简单描述以下: 

admin
  • project manager
  • 若是用于本地 rolebinding,那么用户将能查看和修改所在项目中的全部资源
basic-user
  • user 能够获取关于项目和用户的基本信息
cluster-admin
  • 用户能够在任何项目中作任何操做(超级用户)
  • 如何用于本地 rolebinding,那么用户将拥有所在project的全部权限,包括控制quota和role
cluster-status
  • 用户能够获取集群的基本状态信息
edit
  • 用户能够修改项目中的大部分对象;可是不能查看或修改 role 和 rolebinding
self-provisioner
  • 全部用户的默认role,能够建立本身的project
view
  • 用户能够查看项目中的大部分对象,除了 role 和 rolebinding
  • 用户不能作任何修改任何对象 

能够查看全部角色,好比 system:router 角色:

可使用 oc adm policy 命令向用户或用户授予角色:

user、group、role、rolebinding 之间的关系:

更多对 role 的说明,可参见官方文档。 

4. Service Account 用户

 

OpenShift 的 service account 比较复杂,和不少概念都有关联。官方文档在  https://docs.openshift.com/container-platform/3.11/dev_guide/service_accounts.html。该文档对为何须要这个概念的说明是:当一个天然人用户访问 OpenShfit API 时,OpenShift 对它进行用户认证和权限控制。可是,有时候作操做的并非天然人用户,好比:

  • Replication Controller 调用 API 去建立或者删除 pod
  • 容器中的应用调用 API
  • 外部应用调用 API 去进行监控或者整合

为了这种访问,OpenShift 创造了 Service Account (简称sa)概念。每一个项目(project)默认都会自动建立3个sa user: 

Service Account Usage

builder

用于 build pod。它被授予了 system:image-builder 角色,所以被容许向内部镜像仓库中的任意 image stream 上push 镜像。

deployer

用于 deployment pod。被授予了 system:deployer 角色,所以被容许查看和修改集群中的 RC 及其 pod。

default

当一个 pod 没有显式指定 service account 默认都会使用该 sa user。

4.1 身份

sa 利用 secret(token)来保存其身份信息。默认状况下,每一个 sa 用户都有两种token,即访问 OpenShift API 的token和访问内部镜像仓库的token。以系统默认的 『builder』 sa user 为例,它包含一个用于拉取镜像的token secret,两个访问API 的token secret,三个secret 中只有两个能被以卷的形式挂接给pod:

能够按需求修改一个 sa 帐户的这些token。可参考 https://docs.openshift.com/enterprise/3.0/dev_guide/image_pull_secrets.html 为service account 建立和添加新的用于拉取镜像的token secret。具体请参考官方文档。

其中,sa 的 API token 会被挂接到 pod 的 目录的 token 文件,从而使得 pod 中的应用能够读取该 token 去访问 OpenShift API:

image pull secret 是如何挂载到 pod 的,我尚未找到。并且在 pod spec 中,只看到 API token secret 的 mountpoint,而并无 imagePullSecret 的 mountpoint:

4.2 权限 - 访问OpenShift 集群资源的权限

和天然人 user 相似,对 sa 用户访问OpenShift 集群资源的权限控制是经过 role 进行的。前面的表格代表,项目的两个默认 sa builder 和 deployer 都被授予了相应的 role,从而能访问指定的资源。而默认的 sa 用户,只被授予了 /system:image-puller 角色。这意味着默认的 sa 用户只能拉取镜像,而不能访问集群其它资源。

下图是项目 stage 中的 rolebinding:

用户组 system:serviceaccounts:stage 中包括该项目中的全部 sa 用户。利用 default sa user 来作下实验。先获取其 API token,而后登陆进 OpenShift 集群:

调用 API 获取 pod,结果失败:

向 default sa user 授予 cluster-reader 角色:

而后就能够作集群资源查询操做了。 

4.3 权限 - 访问系统资源的权限

pod 中的应用除了有访问 OpenShift API 和内部镜像仓库以外,还有一些系统资源访问要求。好比:

  • 要求以任意用户甚至是 root 来运行 pod 中的主进程
  • 要求访问宿主机上的文件系统
  • 要求访问宿主机上的网络

对于这些操做系统资源的访问权限,OpenShift 利用 scc 来进行控制。这就要求:

  • 在 scc 中进行权限控制。这部分在后面介绍。
  • 在 servie account 和 sa 之间创建联系。每一个 scc 都有指定使用它的用户列表。全部经过身份认证了的用户都只在 restricted 这个 scc 的用户列表之中,包括 service account。所以,pod 默认使用的是 restricted scc。要使它使用其它的scc,就要将它的 service account user 加入到要使用的 scc 的用户列表之中。这在 scc 部分具体介绍。

5. Security Context Constraint(SCC)

前面说过,SCC 用于控制访问系统资源的权限,那说明只有  service account  才须要使用 scc。没在文档中看到天然人用户 user 使用 scc 的例子。

Linux 中的权限控制非常复杂,这里就不说明了,我本身也没怎么弄清楚。OpenShift scc 将系统权限分为几大类,具体见上图中的『权限』部分,而后能够建立 scc 对象来精细地控制对每种权限的控制。

5.1 OpenShift默认的 scc

由于这很是繁琐,所以 OpenShift 默认建立了几个典型的 scc,列表以下。上图中的『系统预约义scc』部分有简要说明,这里再也不重复。

其中,若是 pod 所使用的 scc 的 runAsUser 策略被设置为了 MustRunAsRange,那么 pod 所使用的 uid 和 supplemental-group id 必须在某区间以内。

集群管理员能够在 scc 中设置区间,好比:

可是,OpenShift 默认提供的 scc 都没有设置该区间,而是使用 pod 所在的 project 中定义的区间以内。好比:

复制代码
[root@master2 cloud-user]# oc describe project dev
Name:                   dev
Created:                3 weeks ago
Labels:                 <none>
Annotations:            openshift.io/description=
                        openshift.io/display-name=
                        openshift.io/requester=demo
                        openshift.io/sa.scc.mcs=s0:c16,c0
                        openshift.io/sa.scc.supplemental-groups=1000000000/10000
                        openshift.io/sa.scc.uid-range=1000000000/10000
复制代码

每一个 project 会被分配不一样的 ID 区间。当未指定 uid 和 supplemental gid 时,会默认使用区间的最小值。

每一个 pod 中,运行主进程的用户都有三个属性:

  • uid 即 user id,上面例子中uid 为 100000000,使用的是 project 定义的 uid 区间的第一个值。其策略分为三种:
    • MustRunAs - 须要配置一个区间,要么在 project 中配置,要么在 scc 中指定。默认使用区间的第一个值。指定特定值的话,会检查是否在该区间内。
    • MustRunAsNonRoot - 要求指定非 root user id,不提供默认值。
    • RunAsAny - 容许全部用户,不提供默认值。
  • gid 即用户的primary group id(主组ID)。上面例子中 gid 为 0,组名字为 root。从 https://github.com/kubernetes/enhancements/issues/213 上看,目前 Kubernetes 还不支持设置 gid,其值固定为 0.
  • fsGroup 定义 pod 的 文件系统 group ID,用于块存储,好比 Ceph RDB 和 iSCSI。supplementalGroups ID 用于共享存储,也是组/group ID 的一种,用于共享存储,好比 NFS 和 GlusterFS。上面例子中的 supplementalGroup ID 为 10000000,取的也是默认值。这两种 group ID 支持 MustRunAs 和 RunAsAny 两种策略。会在下一篇存储部分详细介绍。

备注:Supplemental group(附加组)也是Linux 组的一种。当一个进程运行在 Linux 中时,它会拥有一个 UID,一个 GID,以及一个或多个附加组ID。具体请查阅Linux 相关文档。

若是某 scc 设置的 RunAsUser 策略为 MustRunAsRange,它会要求配置合法的 uid 区间(要么在 project 中配置,要么在 scc 中指定)。若是你要指定特定的 uid ,你能够在 pod 定义 yaml 中使用 securityContext: runAsUser <uid> 来指定特定的 user id,可是该 id 必须在区间以内。不然会报错:

Error from server (Forbidden): error when creating "testcontainervolume-restricted.yaml": pods "test-pod-volume-restricted2" is forbidden: unable to validate against any security context constraint: [spec.containers[0].securityContext.securityContext.runAsUser: Invalid value: 65534: must be in the ranges: [1000000000, 1000009999]]

5.2 容器使用的默认 scc

根据建立 pod 的用户不一样,pod 使用不一样的默认 scc:

  • 非 cluster admin 角色的 openshift 用户建立的没有显式指定 service account 和 scc 的 pod,其默认使用的 sa user 为 default,默认使用的 scc 为 restricted。
  • cluster admin 用户,根据 scc 优先级,anyuid 将是这种 pod 默认使用的 scc。

SCC 优先级:一个 sa user 能够被加到多的 scc 当中。当一个 service account user 有多个 SCC 可用时,其 scc 按照下面的规则排序

  • 最高优先级的scc排在最前面。默认地,对于 cluster admin 角色的用户的 pod,anyuid scc 会被授予最高优先级,排在最前面。这会容许集群管理员可以以任意 user 运行 pod,而不须要指定 pod 的 SecurityContext 中的 RunAsUser 字段。
  • 若是优先级相同,scc 将从限制最高的向限制最少的顺序排序。
  • 若是优先级和限制都同样,那么将按照名字排序。

每一个 scc 有其用户/用户组列表。以 restricted 为例,全部经过身份验证的用户都在列表中;而 anyuid,只有 cluster-admins 用户组中的用户在里面。

  

5.3 修改容器使用的scc

要受权 pod 有除了 restricted 定义的权限以外的权限,主要有两种作法:

  • 一是将其 service account 放到目标 scc 的用户列表中。此时建议建立一个新的 service account,而不要使用已有的,考虑到对现有 pod 的影响。好比由于 registry 和 router 服务的 pod 须要使用 host networkinig 网络模式,所以有为它们建立单独的 sa user 而且加入到了 hostnetwork scc 的用户列表中。

  • 二是将 service account 加入到目标 scc 的用户组中。

官方建议采用第一种。一个很经常使用的例子是运行要使用 root 用户的容器。不少的Docker 镜像都使用的是 root 用户。可是,openshift restricted scc 不容许使用 root 用户,而要使用一个用户区间内的用户:

修复步骤:

$ oc create serviceaccount useroot
$ oc adm policy add-scc-to-user anyuid -z useroot
$ oc patch dc/myAppNeedsRoot --patch '{"spec":{"template":{"spec":{"serviceAccountName": "useroot"}}}}'

备注:

  • 第二个语句中的 -z userroot 用于指定当前project 中的 sa user,它和使用 system:serviceaccount:<project>:userroot 效果相同。
  • 在 pod 中设置 service account,其属性名字为 serviceAccount,不是 dc 中的 serviceAccount。

说明:

  • 第一步建立名为 userroot 的 sa user
  • 第二步将该 sa user 加入到 anyuid scc 的 user 列表中
  • 第三步在应用的 DeploymentConfig 中指定 serviceAccountName 为 userroot

 

理解OpenShift(5):从 Docker Volume 到 OpenShift Persistent Volume

理解OpenShift(1):网络之 Router 和 Route

理解OpenShift(2):网络之 DNS(域名服务)

理解OpenShift(3):网络之 SDN

理解OpenShift(4):用户及权限管理

理解OpenShift(5):从 Docker Volume 到 OpenShift Persistent Volume

 

** 本文基于 OpenShift 3.11,Kubernetes 1.11 进行测试 ***

1. 从 Docker Volume 到 OpenShift/Kubernetes Persistent Volume

1.1 Docker 容器层(Container layer)

Docker 镜像是不可修改的。使用一Docker 镜像启动一个容器实例后,Docker 会在镜像层之上添加一个可读写的容器层(Container layer)。容器中全部新增或修改的数据都保存在该容器层之中。在容器实例被删除后,该层也会随之被自动删除,所以全部写入的或修改的数据都会丢失。具体可阅读Docker 相关文档,好比 https://docs.docker.com/v17.09/engine/userguide/storagedriver/imagesandcontainers/

1.2 Docker Volume

在容器的可写层中保存数据是可能的,可是有一些缺点:

  • 当容器实例不在运行时,数据不会被保存下来,所以数据是易失性的,不是持久性的。
  • 很难将容器中的数据弄到容器外面,若是其它进行须要访问它的话。
  • 容器的可写层和容器所在的宿主机紧耦合,数据没法被移动到其它宿主机上。
  • 向容器的可写层中写入数据须要经过存储驱动(storage driver,好比AUFS,Brtfs,OverlayFS等)来管理文件系统。存储驱动利用Linux内核提供联合文件系统(union file system),这会下降IO性能。

为了解决以上问题,Docker 提供了 Volume (卷)功能。本质上,一个数据卷(data volume)是 Docker 容器所在宿主机上的一个目录或文件,它被挂载(mount)进容器。Docker 卷具备本身独立的生命周期,可使用 Docker volume 命令独立地被建立和管理。在容器实例被删除后,卷依然存在,所以卷中的数据会被保留,从而实现数据持久化。并且,数据卷直接将数据写入宿主机文件系统,性能相比容器的可写层有提升。

Docker 提供三种方式将宿主机文件或文件夹挂载到容器中:

  • volume(卷):卷保存在宿主机上由Docker 管理的文件系统中,一般在 /var/lib/docker/volumes/ 目录下。
  • bind mount(绑定挂载):被挂载的文件或文件夹能够在宿主机上文件系统的任何地方。
  • tmpfs volume:数据保存在宿主机内存中,而不写入磁盘。

 

三种方式各自有合适的场景,一般建议使用 Docker Volume。Docker Volume 还支持经过各类卷插件(volume plugin),接入各类外置存储。本质上,都是存储插件将存储的卷挂载到Docker宿主机上的某个目录,而后Docker 将目录在挂载给容器。

 

更详细信息,请阅读 https://docs.docker.com/v17.09/engine/admin/volumes/#good-use-cases-for-tmpfs-mounts 等官方文档。

1.3 Kubernetes/OpenShift Volume

OpenShift 利用 Kubernetes 的存储机制来实现其 Volume 功能。和Docker volume 概念相似,本质上,一个 K8S Volume 也是一个能被Pod 中的容器访问的目录。至于该目录是怎么来的,后端介质是什么,内容是什么,则是由所使用的具体卷类型(volume type)决定的。Kubernetes Volume 支持多种存储类型:

关于 K8S Volume 概念的更多信息,请阅读相关文档。

1.3.1 K8S NFS Volume 示例

下面以 Glusterfs Volume 为例介绍 K8S Volume 的使用:

(1)OpenShift 管理员在集群中建立一个 endpoints 对象,指向 Glusterfs 服务器的 IP 地址。在个人测试环境中,由两台服务器提供Glusterfs服务。

(2)存储管理员在 Glusterfs 上建立卷 glustervol1.
(3)开发工程师 建立一个 pod,使用 glusterfs 类型的 volume。 
(4)Pod 运行后,OpenShift 会先将 Glusterfs 卷挂载到宿主机上 ,成为宿主机上的一个目录
172.20.80.7:glusterfsvol1 on /var/lib/origin/openshift.local.volumes/pods/bd8914b5-00d9-11e9-a6cf-fa163eae8505/volumes/kubernetes.io~glusterfs/glustervol1 type fuse.glusterfs (rw,relatime,user_id=0,group_id=0,default_permissions,allow_other,max_read=131072)

(5)而后,宿主机上的这个目录会经过 Docker bind mounted 挂载进容器 

(6)Pod 中的进程使用所挂载的 /var/volume 目录进行数据读写。

1.3.2 K8S/OpenShfit Volume 使用方式总结

从上面过程能够看出,使用卷的过程须要至少有存储工程师和开发人员。要使用某种卷,开发人员须要了解后端存储的具体配置信息。 

可是实际上,存储信息对于应用开发人员来讲,实际上是不须要可见的。他们只关心有没有知足要求的存储可用,而不须要关心后端是什么存储。

为了解耦存储供给和存储使用(pod中的存储定义),Kubernetes 建立了两个概念:PV (Persistent Volume)和 PVC (Persistent Volume Claim)这些概念。

1.4 Kubernetes/OpenShift Persistent Volume

1.4.1 概念

 

  • PV:Persistent Volume。由 OpenShfit 管理员建立,后端是各类类型的存储,好比 AWS EBS,GCE Disk,NFS 等。管理员能够建立多个PV,造成一个存储池,供开发人员使用。
  • StorageClass:在须要动态建立 PV 时由 OpenShfit 管理员建立。 管理员利用 StorageClass 来描述他们所提供的存储的类型(classes)。Storage class 向管理员提供了一种方式,用于描述他们所提供的存储的信息。不一样的class 可映射到不一样的 SLA,备份策略,等等。每一个 StorageClass 包括 provisoner、parameters、reclaimPolicy 字段。
  • PVC:Persistent Volume Claim。由开发人员建立,一个实例表示对某种存储资源的一个申请。每当开发人员建立一个新的PVC后,Kubernetes 会在已有的PV 池中进行搜索,找到一个最佳匹配的PV 来使用。PVC 中只包含通用的存储需求,好比访问模式(AccessModes)、容量(request)等,而不须要关心后端存储的具体信息。 Pod 经过 PVC 使用 PV,PV 由实际的存储系统提供物理存储。

以 Glusterfs 为例,这是各类概念之间对照图(来源: http://blog.leifmadsen.com/blog/2017/09/19/persistent-volumes-with-glusterfs/):

根据 PV 的不一样建立方式,又能够分为静态建立PV 和 动态建立PV两种方式。前面一种PV由OpenShift 管理员手工建立,后者一种的PV由系统自动建立。具体可参考后面的两个例子。

1.4.2 PV 的生命周期

 

PV 的生命周期包括四大部分:
  • 供给:分为静态供给和动态供给。
    • 静态供给是指管理员会预先建立好必定数目的PV,每一个PV 包含供集群使用的真实后端存储的详细信息,这些PV造成一个持久化卷的资源池。
    • 动态供给是集群管理员预先建立 StorageClass,而后PVC申请StorageClass,而后集群会动态建立PV,供PVC消费。  动态卷供给是 Kubernetes独有的功能,这一功能容许按需建立存储建。在此以前,集群管理员须要事先在集群外由存储提供者或者云提供商建立存储卷,成功以后再建立PersistentVolume对象,才可以在kubernetes中使用。动态卷供给能让集群管理员没必要进行预先建立存储卷,而是随着用户需求进行建立。
  • 绑定:用户在部署容器应用时会定义PVC,其中会声明所需的存储资源的特性,如大小和访问方式。K8S 的一个控制器(controller)会负责在PV 资源池中寻找匹配的PV,并将PVC与目标PV 进行对接。这是PV和PVC的状态将变成 Bound,即绑定状态。PV 和 PVC 之间的绑定是1:1的,这意味着PVC对PV的占据是独占的、排它的。
  • 使用:Pod 经过使用 PVC 来经过卷(volume)来使用后端存储(storage)资源。Pod 和它要使用的 PVC 必须在同一个 project 中。
    • 集群为 pod 在同一个 project 中定位到 PVC。
    • 经过 PVC 找到 PV。若是没找到且存在合适的StorageClass,则自动建立一个PV。
    • 存储卷挂载到宿主机,而后被 pod 使用。
  • 释放:当应用再也不使用存储时,能够删除PVC,此时PV的状态为 released,即释放。Kubernetes 支持使用保护模式(Storage Object in Use Protection)。当该功能启用后,若是用户删除被一个正被pod 使用着的 PVC,该 PVC 不会立刻被删除,而是会推迟到 pod 再也不使用该PVC时。若是用户删除PV,它也不会被立刻删除,而是会等到该PV再也不绑定到PVC 的时候。是否启用了该保护,能够从 PV 和 PVC 的 finalizers:   - kubernetes.io/pvc-protection 上看出来。
  • 回收:当 PV 的状态变为 released,K8S 会根据 PV 定义的回收策略回收持久化卷。
    • retain:保留数据,人工回收持久化卷。
    • recycle:经过执行 rm -rf 删除卷上全部数据。目前只有 NFS 和 host path 支持。
    • delete:动态地删除后端存储。须要底层 iaas 支持,目前 AWS EBS,GCE PD 和 OpenStack Cinder 支持。 

2. 静态建立PV示例及Volume 权限(以NFS为例)

2.1 静态建立PV的流程

(1)存储管理员准备 NFS 环境

网上有不少关于NFS安装步骤的文章,这里再也不重复。个人测试环境上,NFS 服务器的IP 地址为 172.20.80.4,它暴露了三个文件夹供客户端使用:

 

(2)OpenShift 管理员建立 PV, 后端使用上述 NFS 存储的 

(3)开发人员建立一个 PVC,使用上一步骤中建立的PV。该 PVC实例会存在于某个project 之中,而PV则是在集群范围内共享的。

(4)NFS folder4 文件夹被挂载到Pod 所在的宿主机上。

172.20.80.4:/mnt/folder4 on /var/lib/origin/openshift.local.volumes/pods/863e9b2d-01a0-11e9-a6cf-fa163eae8505/volumes/kubernetes.io~nfs/pv-folder4-2 type nfs4 (rw,relatime,vers=4.1,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.22.122.8,local_lock=none,addr=172.20.80.4)

(5)宿主机上的目录被 bind mounted 给容器,成为其 /var/volume 文件夹。

2.2 Pod volume 权限

2.2.1 NFS 权限控制

每种存储后端都有其本身的权限管理方式。在NFS 中,在 /etc/exports 文件中国年,可使用如下原语来设置每一个将被共享出来的文件夹的权限:

 

  • 读写模式
    •   ro:共享目录只读;
    •   rw:共享目录可读可写;
  • 用户管理
    •   all_squash:全部访问用户都映射为匿名用户或用户组;
    •   no_all_squash(默认):访问用户先与本机用户匹配,匹配失败后再映射为匿名用户或用户组;
    •   root_squash(默认):未来访的root用户映射为匿名用户或用户组;
    •   no_root_squash:来访的root用户保持root账号权限;
    •   anonuid=<UID>:指定匿名访问用户的本地用户UID,默认为nfsnobody(65534);
    •   anongid=<GID>:指定匿名访问用户的本地用户组GID,默认为nfsnobody(65534);
  • 端口管理
    •   secure(默认):限制客户端只能从小于1024的tcp/ip端口链接服务器;
    •   insecure:容许客户端从大于1024的tcp/ip端口链接服务器;
  • 数据写入方式
    •   sync:将数据同步写入内存缓冲区与磁盘中,效率低,但能够保证数据的一致性;
    •   async:将数据先保存在内存缓冲区中,必要时才写入磁盘;
    •   wdelay(默认):检查是否有相关的写操做,若是有则将这些写操做一块儿执行,这样能够提升效率;
    •   no_wdelay:如有写操做则当即执行,应与sync配合使用;
  • 文件夹权限
    •   subtree_check(默认) :若输出目录是一个子目录,则nfs服务器将检查其父目录的权限;
    •   no_subtree_check :即便输出目录是一个子目录,nfs服务器也不检查其父目录的权限,这样能够提升效率;

NFS 用户认证及权限控制基于 RPC。在 NFS 3 和 4 版本中,最经常使用的认证机制是 AUTH_Unix。客户端系统上的 uid 和 gid 经过 RPC 调用传到 NFS 端,而后这些 id 所拥有的权限会被校验,以肯定可否访问目标资源。所以,客户端和服务器端上的 uid 和 gid 必须相同。同时,可使用一些设置来作特定处理:

  • all_squash:将全部用户和组都映射为匿名用户和组。默认为 nfsnobody 用户(id 为 65534)和 nfsnodbody 组(id 为 65534)。也能够经过 anonuid 和 anongid 指定。
  • no_all_squash:访问用户先与本机用户经过 id 进行匹配,若是有 id 相同的用户则匹配成功,若匹配失败后再映射为匿名用户或用户组。这是默认选项。
  • root_squash:未来访的 root 用户(id 为 0)映射为匿名用户。这是默认选型,可使用 no_root_squash 不进行这种映射,而保持为 root 用户。

在咱们当前的例子中,folder4 的文件夹权限为 /mnt/folder4 172.22.122.0/24(insecure,rw,sync,no_root_squash,no_all_squash)。这表示它:

  • 容许 172.22.122.0/24 网段的客户端访问(备注:这个网段其实是宿主机所在的网段,而不是Pod 网段,由于 NFS 其实是被挂载给宿主机的,而后再 bind mount 给容器的)。
  • insecure:容许经过端口号大于 1024 的 tcp 链接访问它。
  • no_root_squash:保持客户端 root 用户,将其映射为服务器端 root 用户。理论上这是一种危险的配置。
  • no_all_squash:先将经过 PRC 传入的 uid 和 gid 在本地进行匹配。成功则使用 NFS 服务器上的同id 的用户或组;不然使用匿名用户或组。

NFS 上的 folder4 的目录权限为:drwxr--r-x 2 nfsnobody nfsnobody 4096 Dec 17 10:11 folder4。这意味着 nfsnobody 用户能够对它作读写,其它用户(包括nfsnobody组内的用户和其它用户)都只能读。

Pod 中的用户 id 为:uid=1001(default) gid=0(root) groups=0(root)。

查询共享目录OK。

写入失败:Permission denied。这是由于本地用户 uid 1001 在 NFS 服务器上有匹配的用户 (cloud-user2:x:1001:1001::/home/cloud-user2:/bin/bash),而该用户并无 folder4 文件夹写权限。

2.2.2 几种权限作法

从上面对 NFS 权限控制原理的分析能够看出有几种方式来保证Pod 中的用户的写入成功。

(1)将 NFS 暴露出来的文件夹的全部者修改成 nfsnobody:nfsnobody,而后在文件夹上设置 all_squash,这会将全部客户端 uid 和 gid 映射为NFS服务器端的 nfsnobody 用户和 nfsnobdy 组。

在 pod 中的 id: uid=1001(default) gid=0(root) groups=0(root)

在 pod 中写入文件,而后在 NFS 上查看:

可见是uid 和 gid 都是映射成功了的。

(2)上述方法一将全部客户端的用户都映射为 nfsnobody:nfsnobody,这有了统一性,可是也消灭了独特性。有时候还须要保留客户端上的已知uid。此时会在 NFS 共享的文件夹上设置 no_all_squash,这样会先作匹配找到两地都有的user,匹配不成功则走步骤(1)中的作法。

这种状况下,若是匹配成功,则NFS 会对服务器端的同 uid 和 gid 的用户的权限进行校验。一般状况下,NFS 服务器端匹配到的用户不会是 nfsnobdy,根据文件夹上的权限设置,此时Pod 中是没法写入文件的。这就是 2.2.1 中说描述的场景的结果。此时有两种处理方式:

(a)将文件夹上 other user 加上写权限。这种作法比较简单粗暴,权限暴露过大,不推荐使用。

chmod o+w folder5 -R

(b)使用 supplemental group id

Linux 系统中, supplemental group ID 是进程所拥有的附加组的一个集合。在 Linux 上,文件系统的用户(user)、组(group)的 ID,连同辅助组(supplementary group)的ID,一块儿肯定对文件系统的操做权限,包括打开(open)、修改全部者(change ownership)、修改权限(permission)。具体请阅读相关 Linux 文档。

首先修改NFS 文件夹的 gid 为某个数值,好比下面的命令修改gid为 2000(这里其实是 gid,不是 supplemental gid。gid 对文件夹有意义,而 supplemental gid 对文件夹无心义而对进程有意义)。

chown :2000 folder4 -R

而后在 pod 上进行配置,使得 Pod 中的主进程的辅助组id 为这里所设置的gid。

2.2.3 设置Pod 中主进程的 uid 和 supplemental gid

(1)Pod 的 uid,gid 和 supplemental gid

Kubernets 目前还不支持设置 gid,所以全部 pod 中运行主进程的 gid 都是 0。

对一个非 cluster admin role 用户启动的 pod,它的默认 service account  为 restricted。它要求 uid 必须在指定的区间内,而它本身并无指定用户id 区间:

此时 pod 的 uid 区间受pod 所在的 project 上的定义的相应 annotation 限制:

此时pod 中的 uid 和 suppenmental gid 以下图所示:(备注:与前面的例子中的 uid 不一样,是由于前面的 pod 是 cluster admin user 启用的,所以 pod 的 scc 为 anyuid):

在不显式指定 uid 和 supplemental gid 的状况下,会使用区间的最小值做为默认值。

(2)修改 Pod 的 uid

根据前面对 NFS 权限管理的分析,能够将 Pod 中的 uid 修改成 nfsnobody 对应的 uid,这样Pod 就会具备 NFS 共享目录的写入权限。可是,默认的 nfsnobdy 的 uid 为 65534,这个 uid 并不在service account restricted 容许的 uid 区间 [1000000000,1000009999] 以内,所以没法将 uid 设置为 65534.

此时,能够基于 restricted scc 建立一个新的 scc,别的配置不变,除了将 RunAsUser 策略修改成 RunAsAny 之外。此时,就能够在 Pod 中指定 uid 为 65534 了。

 

新的scc:

 

pod 中指定 uid:

   

pod 的 uid:

挂载的文件夹可写。操做成功。

(3)修改 supplementantal gid

由于 uid 会和太多因素关联,因此直接修改 uid 这种作法比较重。除了 uid 外,Pod 中还能够:

  • 设置 fsGroup,它主要面向向块存储
  • 设置 suppmental gid,它主要面向共享文件系统

由于 Glusterfs 是共享文件存储,所以需设置辅助组id。具体步骤包括:

  • 修改 pod 的 suppemental gid,其作法与修改 uid 的作法相似,再也不重复。
  • 修改 NFS 文件夹的 group 权限,加上 w 和 x,并设置其 gid 为 pod 所使用的 suppemental gid。 

这两,在NFS客户端(pod)和服务器端(文件夹)上经过 group id 将把权限打通了。 

更详细说明,请阅读 OpenShift 官方文档 https://docs.okd.io/latest/install_config/persistent_storage/pod_security_context.html

3. 动态建立PV示例(以Clusterfs 为例)

3.1 流程概述

3.1.1 从 OpenShift 角度看

下图展现了从 OpenShift 角度看的动态建立PV的流程。在步骤 3.2,当开发人员建立好PVC之后,OpenShift 会在当前StorageClass中查找知足要求的 StorageClass。一旦找到,就会根据PVC中的配置自动建立一个PV,并调用StorageClass中的 storage provisioner 自动建立一个存储volume。在开发人员建立使用该 PVC 的 Pod 后,存储卷就会被挂载给Pod 所在的宿主机,而后经过 bind mounted 被挂载给Pod。

 

这么作的好处是显而易见的,好比:

  • 集群管理员不须要预先准备好PV
  • PV的容量和PVC的容量是同样的,这样就不会存在存储浪费。
  • 在删除PVC时,PV 会被自动删除,存储卷也会被自动删除。

另外一方面,OpenShift 会为每一个PVC 在后端存储上建立一个卷。这样,在有大量PVC时,存储中将出现大量的小容量卷,这对某些存储会产生至关大的压力,特别是对于一些传统存储。这些存储可能就不能知足现代容器云平台对存储的要求了。

3.1.2 从存储角度看

由于 Glusterfs 自己不提供 REST API,所以须要在它前面部署一个Proxy。Heketi 就是一种开源的这种Proxy,它的项目地址是 https://github.com/heketi/heketi。它暴露Gluster Volume 操做的REST API,并经过 SSH 来运行 Glusterfs 命令,完成各类卷相关的操做,好比建立,映射等。OpenShift 经过调用 Heketi API 来实现 Gluesterfs 卷的动态建立和管理。 

 3.2 示例

(1)OpenShift 管理员建立 StorageClass

每一个 StorageClass 会包含几个属性:

  • provisioner:指定建立PV所使用的存储插件(volume plugin)。Kubernets/OpenShift 带有它们所支持的存储插件。
  • parameters:后端存储的各类参数。
  • reclaimPolicy:存储空间回收策略。

关于StorageClass的详细说明,请阅读 https://kubernetes.io/docs/concepts/storage/storage-classes/

(2)开发人员建立一个PVC

其中关键的一项是在 storageClassName 中制定 StorageClass 的名称。

(3)OpenShfit 自动建立一个PV,以及其它资源。

OpenShfit 会根据 StorageClass 及 PVC 中的有关属性,动态建立一个 PV。 

以及 Service:

及其 Endpoints:

OpenShift 是经过该 service 调用 storage provisioner 的。

(4)Volume plugin 会自动地建立存储卷

 Heketi 在 Glusterfs 中建立改卷的过程大体以下:

(a)Glusterfs 系统初始化时会为每一个物理磁盘建立一个 Volume Group:

 

pvcreate --metadatasize=128M --dataalignment=256K '/dev/vde'
vgcreate --autobackup=n vg_c04281d30edfa285bb51f0f323ab7690 /dev/vde
(b)Heketi 调用 glusterfs 命令建立一个 volume
复制代码
gluster --mode=script volume create vol_e22dc22f335de8f8c90f7c66028edf37 172.20.80.7:/var/lib/heketi/mounts/vg_c04281d30edfa285bb51f0f323ab7690/brick_97d37975df78714e2e0bfea850a9e4aa/brick
mkdir -p /var/lib/heketi/mounts/vg_c04281d30edfa285bb51f0f323ab7690/brick_97d37975df78714e2e0bfea850a9e4aa
lvcreate --autobackup=n --poolmetadatasize 8192K --chunksize 256K --size 1048576K --thin vg_c04281d30edfa285bb51f0f323ab7690/tp_97d37975df78714e2e0bfea850a9e4aa --virtualsize 1048576K --name brick_97d37975df78714e2e0bfea850a9e4aa
mkfs.xfs -i size=512 -n size=8192 /dev/mapper/vg_c04281d30edfa285bb51f0f323ab7690-brick_97d37975df78714e2e0bfea850a9e4aa 
mount -o rw,inode64,noatime,nouuid /dev/mapper/vg_c04281d30edfa285bb51f0f323ab7690-brick_97d37975df78714e2e0bfea850a9e4aa /var/lib/heketi/mounts/vg_c04281d30edfa285bb51f0f323ab7690/brick_97d37975df78714e2e0bfea850a9e4aa
复制代码

#这个目录是在 Glusterfs 节点上实际保存数据的目录 

mkdir /var/lib/heketi/mounts/vg_c04281d30edfa285bb51f0f323ab7690/brick_97d37975df78714e2e0bfea850a9e4aa/brick

#该命令会目录的 gid 修改成前述第(3)步中的 gid

chown :2000 /var/lib/heketi/mounts/vg_c04281d30edfa285bb51f0f323ab7690/brick_97d37975df78714e2e0bfea850a9e4aa/brick
chmod 2775 /var/lib/heketi/mounts/vg_c04281d30edfa285bb51f0f323ab7690/brick_97d37975df78714e2e0bfea850a9e4aa/brick

 (5)开发人员建立一个使用上述PVC的 Pod

(6)Pod 启动时,系统会

 

首先 Glusters volume 被挂接到pod所在的宿主机上:
[root@node2 cloud-user]# mount  | grep gluster
172.20.80.7:vol_e22dc22f335de8f8c90f7c66028edf37 on /var/lib/origin/openshift.local.volumes/pods/5d97c7db-ff75-11e8-8b3e-fa163eae8505/volumes/kubernetes.io~glusterfs/pvc-10438bac-ff75-11e8-8b3e-fa163eae8505 type fuse.glusterfs (rw,relatime,user_id=0,group_id=0,default_permissions,allow_other,max_read=131072)

而后该宿主机目录做为一个 mountpoint 被挂载给容器:

 
挂载的目录(最后一行):
 
 Pod 中的 glusterfs 挂载点:
172.20.80.7:vol_e22dc22f335de8f8c90f7c66028edf37 on /var/volume type fuse.glusterfs (rw,relatime,user_id=0,group_id=0,default_permissions,allow_other,max_read=131072)

查看用户,它有id 为 2000 辅助组。

 
该 gid 和 Glusterfs 上的文件夹目录的权限相同,这样就能够确保对存储的访问没有权限问题。
 
这里能够看出来有对 gid/supplemental gid 有管理。 系统有对 PV 分配 gid,而后该 gid 会成为 Pod 主进程的 supplemetnal group ID,同时还会被设置为后端实际存储目录的 gid。这么作,相对手工建立PV 的流程,有一些简化。
 
参考连接: