OpenShift——大雜燴

 理解OpenShift(1):網絡之 Router 和 Routephp

理解OpenShift(2):網絡之 DNS(域名服務)css

理解OpenShift(3):網絡之 SDNhtml

理解OpenShift(4):用戶及權限管理前端

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

 http://www.javashuo.com/article/p-qobayhbc-ch.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 的流程,有一些簡化。
 
參考連接:
相關文章
相關標籤/搜索