英文好的可以直接閱讀原文:引用原文(英文):https://learnk8s.io/sidecar-containers-patterns
TL;TR:k8s patterns包含了雲原生架構中各種的最佳實踐,這里面繞不開用的最多的就是pod下多容器的pattern,也是k8s與swarm區別最大的地方。利用好這些pattern可以在不修改任何代碼的情況下實現不同的行為比如TLS加固。
k8s把最小單位從容器上升到了pod是它設計的核心思想,這種設計帶來了與原生docker容器無法比擬的優勢,我們知道容器利用了linux下的各種命名空間用來隔離各種資源,但是pod作為多個容器的上一層,它可以利用命名空間是的這些容器共享某些資源從而達到親緣性,比如共用網絡、共用存儲空間實現unionfile等。
示例: 一個安全的http服務
如何利用pod下多容器模式如何實現一個ElasticSearch服務的強化:
apiVersion: apps/v1
kind: Deployment
metadata:
name: elasticsearch
spec:
selector:
matchLabels:
es.test: elasticsearch
template:
metadata:
labels:
es.test: 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:
es.test: elasticsearch
ports:
port: 9200
targetPort: 9200
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"
}
現在的訪問是明文的,那么如何方便的使用多容器pod來實現TLS加固傳輸呢,如果你想到用ingress(通常用來路由外部流量到pod),這里從ingress到pod之間還是未加密的如下圖:

那么滿足zero-trust的辦法就是給這個pod加入一個nginx代理tls加密流量如下圖:

增加一個nginx容器代理tls流量
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; } }
前面的配置中我們利用用service可以讓curl明文的訪問es的接口,而這個配置中改為用nginx代理了9200,es只對localhost暴露9201,也就是從pod以外是訪問不到es了。nginx在9200端口監聽了https請求並轉發給http的9200本地的端口給ES。

代理容器是最常用的一種Pattern
這種添加一個代理容器到一個pod的解決方式稱之為:Ambassador Pattern
本文中所有模式都可以在google的研究文稿中找到詳細的論述:https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/45406.pdf
添加基本的TLS還只是開始,除此之外還可以用這個模式做到以下幾點:
- 如果你希望集群上的流量都用tls證書加密,你可以在所有pod上加上一個nginx代理。或者你也可以更進一步使用mutual TLS來保證所有認證的請求已經被很好的加密,正如lstio 和linkerd在service meshes中做的。
- 在OAuth認證中也可以使用nginx代理來保證所有請求是被jwt驗證的。
- 用在連接外部的數據庫,比如一些不支持TLS或者舊版本的數據庫時這也是很方便的方式。
暴露一個標准的接口用來度量
假設你已經熟悉如何使用 Prometheus來監控所有集群中的服務,但是你也正在使用一些原生並不支持 Prometheus度量的服務,比如Elasticsearch。
如何在不更改代碼的情況下添加 Prometheus度量?
Adapter Pattern 適配模式
以ES為例,我們可以添加一個 exporter容器以Prometheus的格式暴露ES的度量。這非常簡單,用一個開源的exporter for es 即可:
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
通過這種方式可以更廣泛的使用prometheus度量從而達到更好的應用與基礎架構的分離。
日志跟蹤 / Sidecar Pattern
邊車模式,我一直把他想想成老式三輪摩托車的副座,它始終與摩托車主題保持一致並提供各種輔助功能,實現方式也是添加容器來曾強pod中應用。邊車最經典的應用就是日志跟蹤。
在容器化的環境中最標准的做法是標准輸出日志到一個中心化的收集器中用於分析和管理。但是很多老的應用是將日志寫入文件,而更改日志輸出有時候是一件困難的事。
那么添加一個日志跟蹤的邊車就意味着你可能不必去更改日志代碼。回到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: {}
這里的logs容器就是sidecar的一個具體實現,現實中可以使用具體的日志收集器代替比如filebeat。當app持續寫入數據時,邊車中的日志收集程序會不斷的以只讀的形式收集日志,這里的logs邊車就把寫入文件的logs變為標准輸出而不需要修改任何代碼。
其他邊車模式常用的場景
- 實時的重啟ConfigMaps而不需要重啟pod
- 從Hashcorp Vault注入秘鑰
- 添加一個本地的redis作為一個低延遲的內存緩存服務
在pod前的准備工作中使用Init Containers
k8s除了提供多容器外還提供了一種叫做初始化容器的功能,顧名思義它就是在pods 容器啟動前工作的容器,我一般把它當做job這樣的概念,一般場景中init containers這些容器在執行完后就不再運行了處於pause狀態,這里特別要注意的是它的執行會嚴格按照編排的從上至下的順序逐一初始化,這種順序也是實現初始化工作不可缺少的。下面還是以ES為例子:
ES 文檔中建議生產中設置vm.max_map_count這個sysctl屬性。
這就帶來了一個問題,這個屬性只能在節點級別才可以被修改,容器級別是沒有做到隔離。
所以在不修改k8s代碼的情況下你不得不使用特權級別來運行es已達到修改的目的,而這也不是你所希望的,
因為他會帶來很嚴重的安全問題。
那么使用Init Containers就可以很好地解決這個問題,做法就是只在初始化容器中提權修改設置,那么后面的es只是普通容器就可以運行。如下:
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
除了上面這種常見的做法外初始化容器還可以這么用,當你HashicCorp Vault 來管理secrets而不是k8s 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: {}
更多的初始化容器應用場景
- 你希望在運行app前跑數據庫的遷移腳本
- 從外部讀取/拉取一個超大文件時可以避免容器臃腫
總結
這些pattern非常巧妙地用很小的代價非侵入的解決現實中常見的問題,這里要特別說明的是除了初始化容器會在運行后暫停不占用資源外,pods中增加的容器都是吃資源的,實際使用中我們不希望因為解決一個小問題反倒拖累整個pod,所以在邊車這類容器組件的選擇上要慎重,要足夠的高效輕量,常見的像nginx、go寫的大部分組件就是一個很好的選擇,java寫的就呵呵了。
如果希望挖掘更多多容器的設計細節可以查看官方文檔:https://kubernetes.io/docs/concepts/workloads/pods/,還有google的容器設計論文:https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/45406.pdf