caddy & grpc(3) 為 caddy 新增一個 反向代理外掛
caddy-grpc 為 caddy 新增一個 反向代理外掛
專案地址:https://github.com/yhyddr/caddy-grpc
前言
上一次我們學習瞭如何在 Caddy 中擴充套件自己想要的外掛。部落格中只提供了大致框架。這一次,我們來根據具體外掛 caddy-grpc
學習。
選取它的原因是,它本身是一個獨立的應用,這裡把它做成了一個 Caddy 的外掛。或許你有進一步理解到 Caddy 的良好設計。
外掛作用
該外掛的目的與Improbable-eng/grpc-web/go/grpcwebproxy目的相同,但作為 Caddy 中介軟體外掛而不是獨立的Go應用程式。
而這個專案的作用又是什麼呢?
這是一個小型反向代理,可以使用gRPC-Web協議支援現有的gRPC伺服器並公開其功能,允許從瀏覽器中使用gRPC服務。
特徵:
- 結構化記錄(就是 log 啦)代理請求到stdout(標準輸出)
- 可除錯的 HTTP 埠(預設埠
8080
)- Prometheus監視代理請求(
/metrics
在除錯端點上)- Request(
/debug/requests
)和連線跟蹤端點(/debug/events
)- TLS 1.2服務(預設埠
8443
):
- 具有啟用客戶端證書驗證的選項
- 安全(純文字)和TLS gRPC後端連線:
- 使用可自定義的CA證書進行連線
其實意思就是,把這一個反向代理做到了 caddy 伺服器的中介軟體中。
使用
在你需要的時候,可以通過
example.com
grpc localhost:9090
第一行example.com是要服務的站點的主機名/地址。 第二行是一個名為grpc的指令,其中可以指定後端gRPC服務端點地址(即示例中的localhost:9090)。 (注意:以上配置預設為TLS 1.2到後端gRPC服務)
Caddyfile 語法
grpc backend_addr { backend_is_insecure backend_tls_noverify backend_tls_ca_files path_to_ca_file1 path_to_ca_file2 }
backend_is_insecure
預設情況下,代理將使用TLS連線到後端,但是如果後端以明文形式提供服務,則需要新增此選項
backend_tls_noverify
預設情況下,要驗證後端的TLS。如果不要驗證,則需要新增此選項
backend_tls_ca_files
用於驗證後端證書的PEM證書鏈路徑(以逗號分隔)。 如果為空,將使用 host 主機CA鏈。
原始碼
目錄結構
caddy-grpc
├── LICENSE
├── README.md
├── proxy // 代理 grpc proxy 的功能實現
│ ├── DOC.md
│ ├── LICENSE.txt
│ ├── README.md
│ ├── codec.go
│ ├── director.go
│ ├── doc.go
│ └── handler.go
├── server.go // Handle 邏輯檔案
└── setup.go // 安裝檔案
Setup.go
按照我們上次進行的 外掛編寫的順序來看,如果不記得,請看:如何為 caddy 新增外掛擴充套件
首先看 安裝的 setup.go 檔案
init func
func init() {
caddy.RegisterPlugin("grpc", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
可以知道,該外掛 註冊的 是 http 伺服器,名字叫 grpc
setup func
然後我們看到最重要的 setup 函式,剛才提到的使用方法中,負責分析 caddyfile 中的選項的正是它。它也會將分析到的 directive 交由 Caddy 的 controller 來配置自己這個外掛
// setup configures a new server middleware instance.
func setup(c *caddy.Controller) error {
for c.Next() {
var s server
if !c.Args(&s.backendAddr) { //loads next argument into backendAddr and fail if none specified
return c.ArgErr()
}
tlsConfig := &tls.Config{}
tlsConfig.MinVersion = tls.VersionTLS12
s.backendTLS = tlsConfig
s.backendIsInsecure = false
//check for more settings in Caddyfile
for c.NextBlock() {
switch c.Val() {
case "backend_is_insecure":
s.backendIsInsecure = true
case "backend_tls_noverify":
s.backendTLS = buildBackendTLSNoVerify()
case "backend_tls_ca_files":
t, err := buildBackendTLSFromCAFiles(c.RemainingArgs())
if err != nil {
return err
}
s.backendTLS = t
default:
return c.Errf("unknown property '%s'", c.Val())
}
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
s.next = next
return s
})
}
return nil
}
我們注意到 依舊是 c.Next() 起手,用來讀取配置檔案,實際上這裡,它讀取了 grpc 這個 token 並進行下一步
- 然後我們看到,緊跟著 grpc 讀取的是 監聽地址。
if !c.Args(&s.backendAddr) { //loads next argument into backendAddr and fail if none specified
return c.ArgErr()
}
這裡正好對應 在 caddyfile 中的配置 grpc localhost:9090
注意 c.Next(), c.Args(), c.NextBlock(), 都是讀取 caddyfile 中的配置的函式,在caddy 中我們稱為 token
- 另外是注意到 tls 的配置,前面有提到,該服務是開啟 tls 1.2 的服務的
tlsConfig := &tls.Config{}
tlsConfig.MinVersion = tls.VersionTLS12
s.backendTLS = tlsConfig
s.backendIsInsecure = false
- 然後是上面所說的 caddyfile 語法中的配置讀取
//check for more settings in Caddyfile
for c.NextBlock() {
switch c.Val() {
case "backend_is_insecure":
s.backendIsInsecure = true
case "backend_tls_noverify":
s.backendTLS = buildBackendTLSNoVerify()
case "backend_tls_ca_files":
t, err := buildBackendTLSFromCAFiles(c.RemainingArgs())
if err != nil {
return err
}
s.backendTLS = t
default:
return c.Errf("unknown property '%s'", c.Val())
}
}
可以看到是通過 c.NextBlock()
來進行每一個新 token 的分析,使用 c.Val() 讀取之後進行不同的配置。
- 最後,別忘了我們要把它加入 整個 caddy 的中介軟體中去
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
s.next = next
return s
})
server.go
下面進行第二步。
struct
首先檢視這一個外掛最核心的結構。即儲存了哪些資料
type server struct {
backendAddr string
next httpserver.Handler
backendIsInsecure bool
backendTLS *tls.Config
wrappedGrpc *grpcweb.WrappedGrpcServer
}
- backendAddr 是 grpc 服務的監聽地址
- next 是下一個外掛的 Handler 的處理
- backendIsInsecure 和 backendTLS 都是後臺服務是否啟用了不同的安全策略。
- wrappedGrpc 是這個外掛的關鍵,它實現的是 grpcweb protocol,來讓 grpc 服務能夠被瀏覽器訪問。
serveHTTP
我們上次的文章中,這是第二重要的部分, serveHTTP 的實現代表著具體的功能。上一次我們的內容只有用來傳遞給下一個 Handle 的邏輯
func (g gizmoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
return g.next.ServeHTTP(w, r)
}
現在我們來看 這個 grpc 中添加了什麼邏輯吧。
// ServeHTTP satisfies the httpserver.Handler interface.
func (s server) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
//dial Backend
opt := []grpc.DialOption{}
opt = append(opt, grpc.WithCodec(proxy.Codec()))
if s.backendIsInsecure {
opt = append(opt, grpc.WithInsecure())
} else {
opt = append(opt, grpc.WithTransportCredentials(credentials.NewTLS(s.backendTLS)))
}
backendConn, err := grpc.Dial(s.backendAddr, opt...)
if err != nil {
return s.next.ServeHTTP(w, r)
}
director := func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) {
md, _ := metadata.FromIncomingContext(ctx)
return metadata.NewOutgoingContext(ctx, md.Copy()), backendConn, nil
}
grpcServer := grpc.NewServer(
grpc.CustomCodec(proxy.Codec()), // needed for proxy to function.
grpc.UnknownServiceHandler(proxy.TransparentHandler(director)),
/*grpc_middleware.WithUnaryServerChain(
grpc_logrus.UnaryServerInterceptor(logger),
grpc_prometheus.UnaryServerInterceptor,
),
grpc_middleware.WithStreamServerChain(
grpc_logrus.StreamServerInterceptor(logger),
grpc_prometheus.StreamServerInterceptor,
),*/ //middleware should be a config setting or 3rd party middleware plugins like for caddyhttp
)
// gRPC-Web compatibility layer with CORS configured to accept on every
wrappedGrpc := grpcweb.WrapServer(grpcServer, grpcweb.WithCorsForRegisteredEndpointsOnly(false))
wrappedGrpc.ServeHTTP(w, r)
return 0, nil
}
- 首先是 grpc 的配置部分,如果你瞭解 grpc ,你就會知道這是用來配置 grpc 客戶端的選項。這裡為我們的客戶端增添了 Codec 編解碼和不同的安全策略選項。
//dial Backend
opt := []grpc.DialOption{}
opt = append(opt, grpc.WithCodec(proxy.Codec()))
if s.backendIsInsecure {
opt = append(opt, grpc.WithInsecure())
} else {
opt = append(opt, grpc.WithTransportCredentials(credentials.NewTLS(s.backendTLS)))
}
backendConn, err := grpc.Dial(s.backendAddr, opt...)
if err != nil {
return s.next.ServeHTTP(w, r)
}
- 然後是設定了 grpc 伺服器的選項
director := func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) {
md, _ := metadata.FromIncomingContext(ctx)
return metadata.NewOutgoingContext(ctx, md.Copy()), backendConn, nil
}
grpcServer := grpc.NewServer(
grpc.CustomCodec(proxy.Codec()), // needed for proxy to function.
grpc.UnknownServiceHandler(proxy.TransparentHandler(director)),
/*grpc_middleware.WithUnaryServerChain(
grpc_logrus.UnaryServerInterceptor(logger),
grpc_prometheus.UnaryServerInterceptor,
),
grpc_middleware.WithStreamServerChain(
grpc_logrus.StreamServerInterceptor(logger),
grpc_prometheus.StreamServerInterceptor,
),*/ //middleware should be a config setting or 3rd party middleware plugins like for caddyhttp
)
- 最後是使用 grpcweb.WrapServer 來實現 web 服務的呼叫
// gRPC-Web compatibility layer with CORS configured to accept on every
wrappedGrpc := grpcweb.WrapServer(grpcServer, grpcweb.WithCorsForRegisteredEndpointsOnly(false))
wrappedGrpc.ServeHTTP(w, r)
Proxy
注意到,在上文中使用了 proxy.TransparentHandler 這是在 proxy 的 handler.go 中定義的函式。用來實現 gRPC 服務的代理。這裡涉及到 關於 gRPC 的互動的實現,重點是 Client 和 Server 的 stream 傳輸,與本文關係不大,有興趣可以下來了解。
結語
思考一下把這個作為 Caddy 的外掛帶來了什麼?
是不是一瞬間獲得了很多可以擴充套件的配置?
而不是將 Caddy 中想要的一些外掛的功能做到 最開始說的那個獨立應用的專案中。
如果你也在做 HTTP 服務,還在眼饞 Caddy 中的一些功能和它的生態,就像這樣接入吧。
它還涉及到了 grpc-web ,如果有興趣,可以擴充套件學習一下
grpc-web client implementations/examples:
Vue.js
GopherJS
參考
caddy:https://github.com/caddyserver/caddy
如何寫中介軟體:https://github.com/caddyserver/caddy/wiki/Writing-a-Plugin:-HTTP-Middleware
caddy-grpc外掛:https://github.com/pieterlouw/caddy-g