原文:https://venilnoronha.io/hand-crafting-a-sidecar-proxy-like-istio
做者:Venil Noronha
譯者:邱世達
審閱:孫海洲
複製代碼
sidecar代理模式是一個重要的概念,它容許Istio爲服務網格中運行的服務提供路由、度量、安全和其餘功能。在這篇文章中,我將解釋爲Istio提供支持的關鍵技術,同時還將向您展現一種構建簡單的HTTP流量嗅探sidecar代理的方法。html
服務網格的實現一般依賴於sidecar代理,這些代理使得服務網格可以控制、觀察和加密保護應用程序。sidecar代理是反向代理,全部流量在到達目標服務以前流過它。代理將分析流經本身的流量並生成有用的統計信息,並且還能提供靈活的路由功能。此外,代理還可使用mTLS來加密保護應用程序流量。golang
在這篇文章中,咱們將構建一個簡單的sidecar代理,它能夠嗅探HTTP流量並生成統計信息,例如請求大小,響應狀態等。而後,咱們將在Kubernetes Pod中部署HTTP服務,配置sidecar代理,並檢查生成的統計信息。docker
Istio依靠Envoy來代理網絡流量。Envoy代理被打包爲一個容器,並部署在一個Pod中的服務容器旁邊。在這篇文章中,咱們將使用Golang來構建一個能夠嗅探HTTP流量的微型代理。json
咱們的代理須要在TCP端口上偵聽傳入的HTTP請求,而後將它們轉發到目標地址。由於在咱們的例子中,代理和服務都駐留在同一個Pod中,因此目標主機能夠經過環回IP地址(即,127.0.0.1)進行尋址。可是,咱們仍然須要一個端口號來標識目標服務。ubuntu
const ( proxyPort = 8000 servicePort = 80 )複製代碼
如今,咱們能夠開始編寫代理的骨架代碼了。代理將偵聽proxyPort
上的請求並將請求轉發給servicePort
。代理將在服務每一個請求後最終打印統計信息。api
// Create a structure to define the proxy functionality. type Proxy struct{} func (p *Proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Forward the HTTP request to the destination service. res, duration, err := p.forwardRequest(req) // Notify the client if there was an error while forwarding the request. if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } // If the request was forwarded successfully, write the response back to // the client. p.writeResponse(w, res) // Print request and response statistics. p.printStats(req, res, duration) } func main() { // Listen on the predefined proxy port. http.ListenAndServe(fmt.Sprintf(":%d", proxyPort), &Proxy{}) }複製代碼
代理最重要的部分是它轉發請求的能力。咱們首先在代理實現中定義此功能。安全
func (p *Proxy) forwardRequest(req *http.Request) (*http.Response, time.Duration, error) { // Prepare the destination endpoint to forward the request to. proxyUrl := fmt.Sprintf("http://127.0.0.1:%d%s", servicePort, req.RequestURI) // Print the original URL and the proxied request URL. fmt.Printf("Original URL: http://%s:%d%s\n", req.Host, servicePort, req.RequestURI) fmt.Printf("Proxy URL: %s\n", proxyUrl) // Create an HTTP client and a proxy request based on the original request. httpClient := http.Client{} proxyReq, err := http.NewRequest(req.Method, proxyUrl, req.Body) // Capture the duration while making a request to the destination service. start := time.Now() res, err := httpClient.Do(proxyReq) duration := time.Since(start) // Return the response, the request duration, and the error. return res, duration, err }複製代碼
如今咱們獲得了代理請求的響應,讓咱們定義將其寫回客戶端的邏輯。bash
func (p *Proxy) writeResponse(w http.ResponseWriter, res *http.Response) { // Copy all the header values from the response. for name, values := range res.Header { w.Header()[name] = values } // Set a special header to notify that the proxy actually serviced the request. w.Header().Set("Server", "amazing-proxy") // Set the status code returned by the destination service. w.WriteHeader(res.StatusCode) // Copy the contents from the response body. io.Copy(w, res.Body) // Finish the request. res.Body.Close() }複製代碼
代理的最後一部分是打印統計信息。讓咱們繼續將其實現。網絡
func (p *Proxy) printStats(req *http.Request, res *http.Response, duration time.Duration) { fmt.Printf("Request Duration: %v\n", duration) fmt.Printf("Request Size: %d\n", req.ContentLength) fmt.Printf("Response Size: %d\n", res.ContentLength) fmt.Printf("Response Status: %d\n\n", res.StatusCode) }複製代碼
至此,咱們已經構建了一個功能齊全的HTTP流量嗅探代理。app
Istio打包了Envoy並將其做爲sidecar容器運行在服務容器旁邊。讓咱們構建一個代理容器鏡像,運行上面的Go代碼來模仿Istio的運行模式。
# Use the Go v1.12 image for the base. FROM golang:1.12 # Copy the proxy code to the container. COPY main.go . # Run the proxy on container startup. ENTRYPOINT [ "go" ] CMD [ "run", "main.go" ] # Expose the proxy port. EXPOSE 8000複製代碼
要構建代理容器鏡像,咱們能夠簡單地執行如下Docker命令:
$ docker build -t venilnoronha/amazing-proxy:latest -f Dockerfile .複製代碼
咱們須要設置Pod網絡以確保sidecar代理可以接收全部應用程序的流量,以便它能夠對其進行分析並轉發到所需的目標。實現此目的的一種方法是要求用戶將全部客戶端請求地址指向代理端口,同時將代理配置爲指向目標服務端口。這使用戶體驗變得複雜。更好更透明的方法是使用Linux內核中的Netfilter/iptables組件。
爲了更好地理解,讓咱們列出Kubernetes向Pod公開的網絡接口。
$ kubectl run -i --rm --restart=Never busybox --image=busybox -- sh -c "ip addr" 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 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 174: eth0@if175: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue link/ether 02:42:ac:11:00:05 brd ff:ff:ff:ff:ff:ff inet 172.17.0.5/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever複製代碼
如您所見,Pod能夠訪問至少2個網絡接口,即lo
和eth0
。lo
接口表示環回地址,eth0
表示以太網。這裏要注意的是這些是虛擬的而不是真正的接口。
iptables
最簡單的用途是將一個端口映射到另外一個端口。咱們能夠利用它來透明地將流量路由到咱們的代理。Istio正是基於這個確切的概念來創建它的Pod網絡。
這裏的想法是將eth0
接口上的服務端口(80
)映射到代理端口(8000
)。這將確保每當容器嘗試經過端口80
訪問服務時,來自容器外部的流量就會路由到代理。如上圖所示,咱們讓lo
接口將Pod內部流量直接路由到目標服務,即沒有跳轉到代理服務。
Kubernetes容許在Pod運行普通容器以前運行init容器
。Istio使用init容器來設置Pod網絡,以便設置必要的iptables規則。這裏,讓咱們作一樣的事情來將Pod外部流量路由到代理。
#!/bin/bash
# Forward TCP traffic on port 80 to port 8000 on the eth0 interface.
iptables -t nat -A PREROUTING -p tcp -i eth0 --dport 80 -j REDIRECT --to-port 8000
# List all iptables rules.
iptables -t nat --list
複製代碼
咱們如今可使用此初始化腳本建立Docker容器鏡像。
# Use the latest Ubuntu image for the base.
FROM ubuntu:latest
# Install the iptables command.
RUN apt-get update && \
apt-get install -y iptables
# Copy the initialization script into the container.
COPY init.sh /usr/local/bin/
# Mark the initialization script as executable.
RUN chmod +x /usr/local/bin/init.sh
# Start the initialization script on container startup.
ENTRYPOINT ["init.sh"]
複製代碼
要構建Docker鏡像,只需執行如下命令:
$ docker build -t venilnoronha/init-networking:latest -f Dockerfile .
複製代碼
咱們已經構建了一個代理和一個init容器來創建Pod網絡。如今是時候進行測試了。爲此,咱們將使用httpbin容器做爲服務。
Istio自動注入init容器和代理。可是,對於咱們的實驗,能夠手動製做Pod yaml。
apiVersion: v1
kind: Pod
metadata:
name: httpbin-pod
labels:
app: httpbin
spec:
initContainers:
- name: init-networking
image: venilnoronha/init-networking
securityContext:
capabilities:
add:
- NET_ADMIN
privileged: true
containers:
- name: service
image: kennethreitz/httpbin
ports:
- containerPort: 80
- name: proxy
image: venilnoronha/amazing-proxy
ports:
- containerPort: 8000
複製代碼
咱們已經設置了具備root
權限的init容器,並將proxy
和service
配置爲普通容器。要在Kubernetes集羣上部署它,咱們能夠執行如下命令:
$ kubectl apply -f httpbin.yaml
複製代碼
爲了測試部署,咱們首先肯定Pod的ClusterIP。爲此,咱們能夠執行如下命令:
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
httpbin-pod 2/2 Running 0 21h 172.17.0.4 minikube
複製代碼
咱們如今須要從Pod外部生成流量。爲此,我將使用busybox
容器經過curl
發出HTTP請求。
首先,咱們向httpbin服務發送一個GET請求。
$ kubectl run -i --rm --restart=Never busybox --image=odise/busybox-curl \
-- sh -c "curl -i 172.17.0.4:80/get?query=param"
HTTP/1.1 200 OK
Content-Length: 237
Content-Type: application/json
Server: amazing-proxy
複製代碼
而後,再發送一個POST請求。
$ kubectl run -i --rm --restart=Never busybox --image=odise/busybox-curl \
-- sh -c "curl -i -X POST -d 'body=parameters' 172.17.0.4:80/post"
HTTP/1.1 200 OK
Content-Length: 317
Content-Type: application/json
Server: amazing-proxy
複製代碼
最後,向/status
端點發送一個GET請求。
$ kubectl run -i --rm --restart=Never busybox --image=odise/busybox-curl \
-- sh -c "curl -i http://172.17.0.4:80/status/429"
HTTP/1.1 429 Too Many Requests
Content-Length: 0
Content-Type: text/html; charset=utf-8
Server: amazing-proxy
複製代碼
請注意,咱們將請求發送到端口80
,即服務端口而不是代理端口。iptables
規則確保首先將其路由到代理,而後將請求轉發給服務。此外,咱們還看到了額外的請求頭Server: amazing-proxy
,這個請求頭是咱們手動實現的的代理自動加上的。
如今咱們來看看代理生成的統計數據。爲此,咱們能夠運行如下命令:
$ kubectl logs httpbin-pod --container="proxy"
Original URL: http://172.17.0.4:80/get?query=param
Proxy URL: http://127.0.0.1:80/get?query=param
Request Duration: 1.979348ms
Request Size: 0
Response Size: 237
Response Status: 200
Original URL: http://172.17.0.4:80/post
Proxy URL: http://127.0.0.1:80/post
Request Duration: 2.026861ms
Request Size: 15
Response Size: 317
Response Status: 200
Original URL: http://172.17.0.4:80/status/429
Proxy URL: http://127.0.0.1:80/status/429
Request Duration: 1.191793ms
Request Size: 0
Response Size: 0
Response Status: 429
複製代碼
如您所見,咱們確實看到代理的結果與咱們生成的請求相匹配。
本文中,咱們實現了一個簡單的HTTP流量嗅探代理,使用init容器將其嵌入Kubernetes Pod與原有服務無縫鏈接。並且,咱們也瞭解了iptables
是如何提供靈活的網絡,以便在處理代理時提供優良的用戶體驗的。最重要的是,咱們已經學會了關於Istio實現的一些關鍵概念。