Kubernetes提供了巨大的靈活性和運行各類應用的能力。若是你的應用是雲原生微服務或12要素(12-factor)應用,那麼在Kubernetes中運行它們有可能會相對簡單。html
可是,運行那些沒有明確設計爲在容器化環境中運行的應用程序呢?Kubernetes也能夠處理這些問題,可是設置起來可能會比較麻煩。node
Kubernetes提供的最強大的工具之一是多容器pod(儘管多容器pod在各類狀況下對雲原生應用也頗有用)。爲何要在一個 pod 中運行多個容器?由於多容器pod可讓你在不改變其代碼的狀況下更改應用程序的行爲。nginx
這在各類狀況下都頗有用,特別是對於那些最初沒有被設計成在容器中運行的應用程序來講,這很方便。咱們來看看一個例子。web
Elasticsearch是在容器流行以前誕生的(固然如今在Kubernetes中運行也十分簡單),它能夠當作在虛擬機中運行的傳統Java應用的替代。docker
咱們將Elasticsearch做爲示例應用程序,而後使用多容器pods來加強它。數據庫
如下是十分基本的(非生產環境就緒)Elasticsearch Deployment和服務:json
apiVersion: apps/v1 kind: Deployment metadata: name: elasticsearch spec: selector: matchLabels: app.kubernetes.io/name: elasticsearch template: metadata: labels: app.kubernetes.io/name: elasticsearch spec: containers: - name: elasticsearch image: elasticsearch:7.9.3 env: - name: discovery.type value: single-node ports: - name: http containerPort: 9200 --- apiVersion: v1 kind: Service metadata: name: elasticsearch spec: selector: app.kubernetes.io/name: elasticsearch ports: - port: 9200 targetPort: 9200
discovery.type環境變量是讓它以單個副本運行的必要條件。設計模式
Elasticsearch默認經過HTTP端口9200進行監聽。你能夠經過在集羣中運行另外一個Pod並curl到elasticsearch服務來確認pod工做。api
kubectl run -it --rm --image=curlimages/curl curl \ -- curl http://elasticsearch:9200 { "name" : "elasticsearch-77d857c8cf-mk2dv", "cluster_name" : "docker-cluster", "cluster_uuid" : "z98oL-w-SLKJBhh5KVG4kg", "version" : { "number" : "7.9.3", "build_flavor" : "default", "build_type" : "docker", "build_hash" : "c4138e51121ef06a6404866cddc601906fe5c868", "build_date" : "2020-10-16T10:36:16.141335Z", "build_snapshot" : false, "lucene_version" : "8.6.2", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" }
如今,假設你正在向零信任安全模式發展,你須要對網絡上的全部流量進行加密。若是應用程序沒有原生的TLS支持,你會如何去作?緩存
近期版本的Elasticsearch支持TLS,但它在以前很長一段時間內是一個付費功能。
咱們首先想到的多是用nginx ingress作TLS終止,由於ingress是集羣中路由外部流量的組件。但這並不能知足要求,由於ingress pod和Elasticsearch pod之間的流量可能會在未加密的狀況下經過網絡。
外部流量被路由到Ingress,而後路由到Pod
若是你在Ingress終止TLS,剩下的流量將不會加密。
一個能知足要求的解決方案是在pod上加一個nginx代理容器,經過TLS進行監聽。從用戶到Pod的一路流量都是加密的。
若是在pod中包含一個代理容器,你能夠在Nginx pod中終止TLS。
當你比較當前的設置時,你能夠注意到,在Elasticsearch容器以前,流量一直是加密的。
如下是部署的狀況:
apiVersion: apps/v1 kind: Deployment metadata: name: elasticsearch spec: selector: matchLabels: app.kubernetes.io/name: elasticsearch template: metadata: labels: app.kubernetes.io/name: elasticsearch spec: containers: - name: elasticsearch image: elasticsearch:7.9.3 env: - name: discovery.type value: single-node - name: network.host value: 127.0.0.1 - name: http.port value: '9201' - name: nginx-proxy image: nginx:1.19.5 volumeMounts: - name: nginx-config mountPath: /etc/nginx/conf.d readOnly: true - name: certs mountPath: /certs readOnly: true ports: - name: https containerPort: 9200 volumes: - name: nginx-config configMap: name: elasticsearch-nginx - name: certs secret: secretName: elasticsearch-tls --- apiVersion: v1 kind: ConfigMap metadata: name: elasticsearch-nginx data: elasticsearch.conf: | server { listen 9200 ssl; server_name elasticsearch; ssl_certificate /certs/tls.crt; ssl_certificate_key /certs/tls.key; location / { proxy_pass http://localhost:9201; } }
讓咱們來解讀一下:
因此來自pod外部的請求會經過HTTPS進入9200端口的Nginx,而後轉發到9201端口的Elasticsearch。
你能夠經過在集羣內發出HTTPS請求來確認它是否能夠正常工做。
kubectl run -it --rm --image=curlimages/curl curl \ -- curl -k https://elasticsearch:9200 { "name" : "elasticsearch-5469857795-nddbn", "cluster_name" : "docker-cluster", "cluster_uuid" : "XPW9Z8XGTxa7snoUYzeqgg", "version" : { "number" : "7.9.3", "build_flavor" : "default", "build_type" : "docker", "build_hash" : "c4138e51121ef06a6404866cddc601906fe5c868", "build_date" : "2020-10-16T10:36:16.141335Z", "build_snapshot" : false, "lucene_version" : "8.6.2", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" }
對於自簽名的TLS證書,-k版本是必要的。在生產環境中,你須要使用可信的證書。
快速查看日誌,顯示該請求經過了Nginx代理:
kubectl logs elasticsearch-5469857795-nddbn nginx-proxy | grep curl
10.88.4.127 - - [26/Nov/2020:02:37:07 +0000] "GET / HTTP/1.1" 200 559 "-" "curl/7.73.0-DEV" "-"
你也能夠檢查你是否沒法經過未加密的鏈接鏈接到Elasticsearch:
kubectl run -it --rm --image=curlimages/curl curl \ -- curl http://elasticsearch:9200 <html> <head><title>400 The plain HTTP request was sent to HTTPS port</title></head> <body> <center><h1>400 Bad Request</h1></center> <center>The plain HTTP request was sent to HTTPS port</center> <hr><center>nginx/1.19.5</center> </body> </html>
你已經強制執行了TLS,而無需接觸Elasticsearch代碼或容器鏡像。
代理容器是一種常見的模式
在pod中添加代理容器的作法很常見,以致於它有一個名字:Ambassador模式。
這篇文章中的全部模式在谷歌的一篇優秀論文中都有詳細描述。公衆號後臺回覆【論文】,獲取論文下載地址。
添加基本的TLS支持只是一個開始。這裏有一些其餘的事情你能夠用Ambassador模式來作:
咱們先來了解Kubernetes上pod和容器之間的區別,以便更好地瞭解其底層是如何工做的。
一個傳統的容器(例如由docker run啓動的容器)提供了幾種形式的隔離:
Docker還有其餘一些設置,但這些是最主要的。
底層使用的工具是Linux命名空間和控制組(cgroups)。
控制組是一種用來限制資源的便捷方法,好比一個特定進程可使用的CPU或內存。例如,你能夠說你的進程應該只使用2GB的內存和4個CPU核心中的一個。
命名空間則負責隔離進程以及限制該進程能看到的東西。例如,進程只能看到與它直接相關的網絡數據包,它沒法看到流經網絡適配器的全部網絡數據包。或者你能夠隔離filesystem,讓進程相信它能夠訪問全部的filesystem。
從內核5.6版本開始,有八種命名空間,掛載命名空間是其中之一
有了掛載命名空間,你可讓進程認爲它能夠訪問主機上的全部目錄,而事實上它並無
掛載命名空間被設計爲隔離資源——在本例中是filesystem。
每一個進程均可以看到同一個filesystem,同時還能夠與其餘進程隔離
若是你須要複習一下cgroups和namespaces,這裏有一篇很好的博客文章,深刻探討了一些技術細節:
https://jvns.ca/blog/2016/10/10/what-even-is-a-container/
在Kubernetes上,容器提供了全部形式的隔離,除了網絡隔離。網絡隔離發生在pod層面。換句話說,一個pod中的每一個容器都會有本身的filesystem、進程表等,但它們都會共享同一個網絡命名空間。
讓咱們來看看一個簡單pod容器,以更好地瞭解它是如何工做的。
apiVersion: v1 kind: Pod metadata: name: podtest spec: containers: - name: c1 image: busybox command: ['sleep', '5000'] volumeMounts: - name: shared mountPath: /shared - name: c2 image: busybox command: ['sleep', '5000'] volumeMounts: - name: shared mountPath: /shared volumes: - name: shared emptyDir: {}
咱們將上面的代碼段拆解一下:
你可使用kubectl exec看到卷被掛載在第一個容器上:
kubectl exec -it podtest --container c1 -- sh
該命令將終端會話鏈接到podtest pod中的容器c1。
kubectl exec的--container選項一般縮寫爲-c。
mount | grep shared /dev/vda1 on /shared type ext4 (rw,relatime)
如你所見,一個卷掛載在/shared上——這就是咱們以前建立的shared卷。如今咱們來建立一些文件:
echo "foo" > /tmp/foo echo "bar" > /shared/bar
咱們從第二個容器中檢查相同的文件。首先鏈接到它:
kubectl exec -it podtest --container c2 -- sh
cat /shared/bar bar cat /tmp/foo cat: can't open '/tmp/foo': No such file or directory
如你所見,在shared目錄中建立的文件在兩個容器上都是可用的,但/tmp中的文件卻不可用。這是由於除了卷以外,容器的filesysytem之間是徹底隔離的。
如今咱們來看看網絡和進程隔離。一個很好的方法是使用命令ip link來查看網絡是如何設置的,它能夠顯示Linux系統的網絡設備。讓咱們在第一個容器中執行這個命令:
kubectl exec -it podtest -c c1 -- ip link 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 178: eth0@if179: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue link/ether 46:4c:58:6c:da:37 brd ff:ff:ff:ff:ff:ff
在另外一個容器中執行一樣的命令:
kubectl exec -it podtest -c c2 -- ip link 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 178: eth0@if179: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue link/ether 46:4c:58:6c:da:37 brd ff:ff:ff:ff:ff:ff
你能夠看到兩個容器都有:
由於MAC地址應該是全局惟一的,所以相同的地址清楚地標明,這些Pod共享同一個設備。
如今讓咱們來看看網絡共享的操做吧!咱們先鏈接到第一個容器:
ubectl exec -it podtest -c c1 -- sh
藉助nc啓動一個簡單的網絡監聽器:
nc -lk -p 5000 127.0.0.1 -e 'date'
該命令在端口5000的localhost上啓動一個監聽器,並向任何鏈接的TCP客戶端輸入date命令。
那麼第二個容器能夠鏈接到它嗎?
使用如下命令在第二個容器中打開終端:
kubectl exec -it podtest -c c2 -- sh
如今你能夠驗證第二個容器能夠鏈接到該網絡監聽器,但不能看到nc進程:
telnet localhost 5000 Connected to localhost Sun Nov 29 00:57:37 UTC 2020 Connection closed by foreign host ps aux PID USER TIME COMMAND 1 root 0:00 sleep 5000 73 root 0:00 sh 81 root 0:00 ps aux
經過telnet鏈接,能夠看到date的輸出,證實nc監聽器在工做,可是ps aux(顯示容器上的全部進程)根本沒有顯示nc。這是由於pod內的容器有進程隔離,但沒有網絡隔離。這就解釋了Ambassador模式的工做原理:
接收外部流量的容器就是Ambassador,所以該模式也被稱爲Ambassador模式。
不過有一點很關鍵,要記住:由於網絡命名空間是共享的,因此一個pod中的多個容器不能在同一個端口監聽。
讓咱們來看看多容器pod的一些其餘用例。
假設你已經標準化地使用Prometheus來監控Kubernetes集羣中的全部服務,但你使用的一些應用程序並無原生導出Prometheus指標(如,Elasticsearch)。
你能在不改變你的應用程序代碼的狀況下,將Prometheus指標添加到你的pod中嗎?事實上,你能夠,使用Adapter模式。
對於Elasticsearch的例子,讓咱們在pod中添加一個 "exporter"容器,以Prometheus格式暴露各類Elasticsearch指標。
這並不困難,由於有一個Elasticsearch的開源exporter(你還須要將相關端口添加到服務中):
apiVersion: apps/v1 kind: Deployment metadata: name: elasticsearch spec: selector: matchLabels: app.kubernetes.io/name: elasticsearch template: metadata: labels: app.kubernetes.io/name: elasticsearch spec: containers: - name: elasticsearch image: elasticsearch:7.9.3 env: - name: discovery.type value: single-node ports: - name: http containerPort: 9200 - name: prometheus-exporter image: justwatch/elasticsearch_exporter:1.1.0 args: - '--es.uri=http://localhost:9200' ports: - name: http-prometheus containerPort: 9114 --- apiVersion: v1 kind: Service metadata: name: elasticsearch spec: selector: app.kubernetes.io/name: elasticsearch ports: - name: http port: 9200 targetPort: http - name: http-prometheus port: 9114 targetPort: http-prometheus
一旦應用了這個功能,你就能夠在9114端口找到暴露的指標:
kubectl run -it --rm --image=curlimages/curl curl \ -- curl -s elasticsearch:9114/metrics | head # HELP elasticsearch_breakers_estimated_size_bytes Estimated size in bytes of breaker # TYPE elasticsearch_breakers_estimated_size_bytes gauge elasticsearch_breakers_estimated_size_bytes{breaker="accounting",name="elasticsearch-ss86j"} 0 elasticsearch_breakers_estimated_size_bytes{breaker="fielddata",name="elasticsearch-ss86j"} 0 elasticsearch_breakers_estimated_size_bytes{breaker="in_flight_requests",name="elasticsearch-ss86j"} 0 elasticsearch_breakers_estimated_size_bytes{breaker="model_inference",name="elasticsearch-ss86j"} 0 elasticsearch_breakers_estimated_size_bytes{breaker="parent",name="elasticsearch-ss86j"} 1.61106136e+08 elasticsearch_breakers_estimated_size_bytes{breaker="request",name="elasticsearch-ss86j"} 16440 # HELP elasticsearch_breakers_limit_size_bytes Limit size in bytes for breaker # TYPE elasticsearch_breakers_limit_size_bytes gauge
再次,你已經可以改變你的應用程序的行爲,而無需實際改變你的代碼或容器鏡像。你已經暴露了標準化的Prometheus指標,這些指標能夠被集羣範圍內的工具(如Prometheus Operator使用),從而實現了應用程序和底層基礎設施之間的良好分離。
Tailing logs
接下來,咱們來看看Sidecar模式,在這一模式下你能夠將容器添加到Pod,該pod能夠以某些方式加強應用程序。
Sidecar模式十分通用,能夠應用到不一樣類型的用例中。咱們接下來探索如下sidecar的經典用例:log tailing sidecar。
在容器化環境中,最佳實踐是始終將日誌記錄到標準輸出,這樣能夠集中收集和彙總日誌。但許多舊的應用程序被設計成日誌輸出到文件,而改變這一方式並不是易事。而添加一個log tailing sidecar意味着你不須要更改原有的方式也能夠實現日誌的集中收集和彙總。
咱們繼續以Elasticsearch爲例,這可能會有點彆扭,由於Elasticsearch容器默認是將日誌記錄到標準輸出的(並且讓它記錄到文件也不是件容易的事)。
如下是部署狀況:
apiVersion: apps/v1 kind: Deployment metadata: name: elasticsearch labels: app.kubernetes.io/name: elasticsearch spec: selector: matchLabels: app.kubernetes.io/name: elasticsearch template: metadata: labels: app.kubernetes.io/name: elasticsearch spec: containers: - name: elasticsearch image: elasticsearch:7.9.3 env: - name: discovery.type value: single-node - name: path.logs value: /var/log/elasticsearch volumeMounts: - name: logs mountPath: /var/log/elasticsearch - name: logging-config mountPath: /usr/share/elasticsearch/config/log4j2.properties subPath: log4j2.properties readOnly: true ports: - name: http containerPort: 9200 - name: logs image: alpine:3.12 command: - tail - -f - /logs/docker-cluster_server.json volumeMounts: - name: logs mountPath: /logs readOnly: true volumes: - name: logging-config configMap: name: elasticsearch-logging - name: logs emptyDir: {}
日誌配置文件是一個單獨的ConfigMap,由於它太長了因此這裏沒有包括它。
兩個容器共享相同的volume,名爲logs。Elasticsearch容器將日誌寫入該卷,而日誌容器只是從相應的文件中讀取並輸出到標準輸出。你能夠用kubectl logs指定相應的容器來檢索日誌流:
kubectl logs elasticsearch-6f88d74475-jxdhl logs | head { "type": "server", "timestamp": "2020-11-29T23:01:42,849Z", "level": "INFO", "component": "o.e.n.Node", "cluster.name": "docker-cluster", "node.name": "elasticsearch-6f88d74475-jxdhl", "message": "version[7.9.3], pid[7], OS[Linux/5.4.0-52-generic/amd64], JVM" } { "type": "server", "timestamp": "2020-11-29T23:01:42,855Z", "level": "INFO", "component": "o.e.n.Node", "cluster.name": "docker-cluster", "node.name": "elasticsearch-6f88d74475-jxdhl", "message": "JVM home [/usr/share/elasticsearch/jdk]" } { "type": "server", "timestamp": "2020-11-29T23:01:42,856Z", "level": "INFO", "component": "o.e.n.Node", "cluster.name": "docker-cluster", "node.name": "elasticsearch-6f88d74475-jxdhl", "message": "JVM arguments […]" }
使用sidecar的好處是,流式傳輸到標準輸出並非惟一的選擇。
若是你須要切換到一個自定義的日誌聚合服務,你能夠只改變sidecar容器,而無需改變你的應用程序中任何其餘東西。
其餘sidecar用例
Sidecar有許多用例,日誌容器只是其中一個比較簡單的用例。
如下是你在其餘方面可能用到的一些其餘用例:
到目前爲止,本篇文章所介紹的全部多容器pod的例子都涉及到多個容器同時運行。Kubernetes還提供了運行Init Containers的能力,Init Containers是在 "常規 "容器啓動以前運行完成的容器。
這容許你在你的pod正式啓動以前運行一個初始化腳本。爲何你但願你的準備工做在一個單獨的容器中運行,而不是在你的容器的entrypoint腳本中添加一些初始化?
讓咱們來看看Elasticsearch的一個實際例子。Elasticsearch文檔推薦在生產就緒部署中設置vm.max_map_count的sysctl設置。這在容器化環境中是有問題的,由於沒有容器級的sysctl隔離,任何更改都必須發生在節點級。
在不能自定義Kubernetes節點的狀況下,如何處理這個問題?
一種方法是在特權容器中運行Elasticsearch,這將使Elasticsearch可以改變其主機節點上的系統設置,並改變entrypoint腳本以添加sysctls。但從安全角度來看,這將是很是危險的!若是Elasticsearch服務被入侵,攻擊者將擁有對其主機節點的root權限。你可使用init container來必定程度上下降這個風險:
apiVersion: apps/v1 kind: Deployment metadata: name: elasticsearch spec: selector: matchLabels: app.kubernetes.io/name: elasticsearch template: metadata: labels: app.kubernetes.io/name: elasticsearch spec: initContainers: - name: update-sysctl image: alpine:3.12 command: ['/bin/sh'] args: - -c - | sysctl -w vm.max_map_count=262144 securityContext: privileged: true containers: - name: elasticsearch image: elasticsearch:7.9.3 env: - name: discovery.type value: single-node ports: - name: http containerPort: 9200
pod在特權init container中設置了sysctl,以後Elasticsearch容器按預期啓動。
你仍然在使用一個特權容器,這並非理想狀態,但至少它持續時間很短,因此攻擊面要低得多。
這是Elastic Cloud Operator推薦的方法:
https://www.elastic.co/guide/en/cloud-on-k8s/current/k8s-virtual-memory.html
使用特權init container爲運行pod的節點作準備是一種至關常見的模式。例如,Istio使用init container來設置每次pod運行時的iptables規則。
使用init container的另外一個緣由是以某種方式準備 pod 的filesystem。一個常見的用例是secrets管理。
其餘的init container用例
若是你使用相似HashicCorp Vault這樣的工具來管理secrets,而不是Kubernetes secrets,你能夠在一個init container中檢索secrets,並將它們持久化到一個共享的emptyDir卷。
以下所示:
apiVersion: apps/v1 kind: Deployment metadata: name: myapp labels: app.kubernetes.io/name: myapp spec: selector: matchLabels: app.kubernetes.io/name: myapp template: metadata: labels: app.kubernetes.io/name: myapp spec: initContainers: - name: get-secret image: vault volumeMounts: - name: secrets mountPath: /secrets command: ['/bin/sh'] args: - -c - | vault read secret/my-secret > /secrets/my-secret containers: - name: myapp image: myapp volumeMounts: - name: secrets mountPath: /secrets volumes: - name: secrets emptyDir: {}
如今secret/my-secret secret將在myapp容器的filesystem中可用。
這就是Vault Agent Sidecar Injector等系統工做的基本思路。然而,它們在實踐中至關複雜(結合mutating webhooks、init container和sidecars來隱藏大部分的複雜性)。
此外,還有一些其餘你可能想要使用init container的緣由:
這篇文章涵蓋了至關多的內容,因此這裏有一個表格,列出了一些多容器模式,以及你何時可能要使用它們:
圖片
若是你想深刻研究這個問題,請務必閱讀官方文檔和原始容器設計模式文件:
https://kubernetes.io/docs/concepts/workloads/pods/
https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/45406.pdf