リバースプロキシ
このレシピでは、WordPress、Node.js、Java、Python、Ruby、Go などのアプリケーションの前段で、 Echo をリバースプロキシ兼ロードバランサーとして使う方法を示します。簡単のため、ここでの上流は WebSocket も処理する Go サーバーです。
1) 上流ターゲット URL を特定する
Section titled “1) 上流ターゲット URL を特定する”url1, err := url.Parse("http://localhost:8081")if err != nil { e.Logger.Error("failed to parse url", "error", err) return}url2, err := url.Parse("http://localhost:8082")if err != nil { e.Logger.Error("failed to parse url", "error", err) return}targets := []*middleware.ProxyTarget{ {URL: url1}, {URL: url2},}2) 上流ターゲットでプロキシミドルウェアを設定する
Section titled “2) 上流ターゲットでプロキシミドルウェアを設定する”下のスニペットは round-robin ロードバランシングを使います。
middleware.NewRandomBalancer() も使えます。
e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets)))サブルートにプロキシを設定するには、Echo#Group() を使います。
// Canonicalize the group root so relative WebSocket URLs stay under /blog/.e.Any("/blog", func(c *echo.Context) error { location := "/blog/" if query := c.Request().URL.RawQuery; query != "" { location += "?" + query } return c.Redirect(http.StatusPermanentRedirect, location)})g := e.Group("/blog")g.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ Balancer: middleware.NewRoundRobinBalancer(targets), Rewrite: map[string]string{"/blog/*": "/$1"},}))// Group middleware needs a matching route. The proxy handles the response.g.Any("/*", func(c *echo.Context) error { return echo.ErrNotFound })3) 上流サーバーを起動する
Section titled “3) 上流サーバーを起動する”cookbook/reverse-proxy で、各上流サーバーを別々のターミナルで起動します。
go run ./upstream server1 :8081go run ./upstream server2 :80824) プロキシサーバーを起動する
Section titled “4) プロキシサーバーを起動する”3 つ目のターミナルでも cookbook/reverse-proxy に移動し、プロキシを起動します。
go run .http://localhost:1323 にアクセスすると、HTTP リクエストは “server 1” から、
WebSocket リクエストは “server 2” から配信される Web ページが表示されます。
HTTP
Hello from upstream server server1
WebSocket
Hello from upstream server server2!Hello from upstream server server2!Hello from upstream server server2!グループモードを使う場合は、通常モードのプロキシを停止して次を実行します。
go run . -groupedhttp://localhost:1323/blog/ にアクセスします。プロキシは /blog を除いてリクエストを転送し、ページの相対 WebSocket URL は /blog/ 内に保たれます。
ソースコード
Section titled “ソースコード”上流サーバー
Section titled “上流サーバー”package main
import ( "context" "fmt" "net/http" "os" "time"
"github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" "golang.org/x/net/websocket")
var index = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Upstream Server</title> <style> h1, p { font-weight: 300; } </style> </head> <body> <h1>HTTP</h1> <p> Hello from upstream server %s </p> <h1>WebSocket</h1> <p id="output"></p> <script> var wsURL = new URL('ws', window.location.href) wsURL.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' var ws = new WebSocket(wsURL)
ws.onmessage = function(evt) { var out = document.getElementById('output'); out.innerHTML += evt.data + '<br>'; } </script> </body> </html>`
func main() { name := os.Args[1] port := os.Args[2] e := echo.New()
e.Use(middleware.RequestLogger()) e.Use(middleware.Recover())
e.GET("/", func(c *echo.Context) error { return c.HTML(http.StatusOK, fmt.Sprintf(index, name)) })
// WebSocket handler e.GET("/ws", func(c *echo.Context) error { websocket.Handler(func(ws *websocket.Conn) { defer ws.Close() for { // Write err := websocket.Message.Send(ws, fmt.Sprintf("Hello from upstream server %s!", name)) if err != nil { e.Logger.Error("failed to send message", "error", err) } select { case <-ws.Request().Context().Done(): return case <-time.After(1 * time.Second): continue } } }).ServeHTTP(c.Response(), c.Request()) return nil })
sc := echo.StartConfig{Address: port} if err := sc.Start(context.Background(), e); err != nil { e.Logger.Error("failed to start server", "error", err) }}プロキシサーバー
Section titled “プロキシサーバー”package main
import ( "context" "flag" "net/http" "net/url"
"github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware")
func main() { grouped := flag.Bool("grouped", false, "limit the proxy to the /blog route group") flag.Parse() e := echo.New() e.Use(middleware.RequestLogger()) e.Use(middleware.Recover())
// Setup proxy url1, err := url.Parse("http://localhost:8081") if err != nil { e.Logger.Error("failed to parse url", "error", err) return } url2, err := url.Parse("http://localhost:8082") if err != nil { e.Logger.Error("failed to parse url", "error", err) return } targets := []*middleware.ProxyTarget{ {URL: url1}, {URL: url2}, } if *grouped { configureBlogProxy(e, targets) } else { e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets))) }
sc := echo.StartConfig{Address: ":1323"} if err := sc.Start(context.Background(), e); err != nil { e.Logger.Error("failed to start server", "error", err) }}
// configureBlogProxy limits the proxy middleware to a route group.func configureBlogProxy(e *echo.Echo, targets []*middleware.ProxyTarget) { // Canonicalize the group root so relative WebSocket URLs stay under /blog/. e.Any("/blog", func(c *echo.Context) error { location := "/blog/" if query := c.Request().URL.RawQuery; query != "" { location += "?" + query } return c.Redirect(http.StatusPermanentRedirect, location) }) g := e.Group("/blog") g.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ Balancer: middleware.NewRoundRobinBalancer(targets), Rewrite: map[string]string{"/blog/*": "/$1"}, })) // Group middleware needs a matching route. The proxy handles the response. g.Any("/*", func(c *echo.Context) error { return echo.ErrNotFound })}Echo のセキュリティ更新 (v5.4.0 / v4.16.0)
Section titled “Echo のセキュリティ更新 (v5.4.0 / v4.16.0)”Proxy は Context#Scheme() から X-Forwarded-Proto を設定し、古いスキームヘッダーを削除します。v5 の X-Real-IP は Context#RealIP() から設定されます。Echo の前段に別のプロキシがある場合はスキームと IP の抽出器を設定し、上流へ正しい値を渡してください。