EnvoyのExternal Processingのgoの実装サンプル紹介
EnvoyProxyのExternal Processingを利用した実装サンプルの紹介をします。 まだalpha版であり、実装サンプルが少ないため情報資源として残しています。
以下のサンプルを作成しました。実装の詳細はコードを追うとわかります。この記事では概説と実装時に躓くポイントを紹介します。
3つのサーバーで構成されています。
go run
で実行)これらを、Docker NetworkとHost Networkに配置することで動作確認のしやすさを確立しています。
External Processing Server(以後ext_proc)はホストマシンで実行すると、Docker Network内のEnvoyから見てhost.docker.internal
でアクセスできます。
これを利用するとext_procはホストでgo run
で実行するだけで済むため開発効率がすべてDocker Networkに閉じ込める場合と比較して上昇します。
実装
最初のサンプルは、何もしないサンプルです。検証を進めていく上でこれがかなり重要な役割を果たします。 External Processing Serverの開発を進めていくとわかりますが、HTTPのRequest/ResponseをHTTPの仕様通りに実装しなければ、Envoy側がexitして停止します。
また、External Processing ServerのResponse
は省略不可です。
これを指定しない場合、ブラウザでアクセスするとResponseが返ってくるまでコネクションを維持し続けます(ローディング状態になる)。
必ずレスポンスを指定する必要があります。
resp := &pb.ProcessingResponse{} switch value := req.Request.(type) { case *pb.ProcessingRequest_RequestHeaders: resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_RequestHeaders{}, // 省略不可 } break case *pb.ProcessingRequest_RequestBody: resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_RequestBody{}, // 省略不可 } break case *pb.ProcessingRequest_ResponseHeaders: resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_ResponseHeaders{}, // 省略不可 } break case *pb.ProcessingRequest_ResponseBody: resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_ResponseBody{}, // 省略不可 } break default: logrus.Debug(fmt.Sprintf("Unknown Request type %v\n", value)) } if err := processServer.Send(resp); err != nil { logrus.Debug(fmt.Sprintf("send error %v", err)) }
実装
ResponseBodyを書き換えるサンプルです。curlでenvoyproxyに対してGETリクエストを投げるとext_procサーバーで書き換えられた値が返ってきます。
ResponseHeaderを取得したタイミングで、ModeOverrideを利用し、ResponseBodyをext_procサーバーで処理するようにenvoy.yamlに記述されたもの方法から処理方法を上書きします。
これによりProcessingRequest_ResponseBody
節の方に処理が流れ、ResponseBodyを加工できます。
ただし、この方法はcurl -I
などHEAD
MethodでリクエストするとResponseBodyを期待していないためEnvoy側がexitする問題が発生します。
他にも、304 Not Modified
もResponseBodyを期待していないため、単純んなブラウザリロードでEnvoy側が停止する問題があります。
したがって、ResponseBodyを書き換える条件をかなり厳密に定義する必要があります。
switch value := req.Request.(type) { case *pb.ProcessingRequest_RequestHeaders: resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_RequestHeaders{}, } break case *pb.ProcessingRequest_RequestBody: resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_RequestBody{}, } break case *pb.ProcessingRequest_ResponseHeaders: resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_ResponseHeaders{ ResponseHeaders: &pb.HeadersResponse{ Response: &pb.CommonResponse{ HeaderMutation: &pb.HeaderMutation{ RemoveHeaders: []string{ "content-encoding", // ExternalProcessingが自動で付与するHeader。場合によってはブラウザで表示されない }, }, }, }, }, ModeOverride: &v3alpha.ProcessingMode{ // envoy.yamlで指定したprocessing_modeを書き換える ResponseBodyMode: v3alpha.ProcessingMode_BUFFERED, // BUFFEREDにすることでProcessingRequest_ResponseBodyの節を通る様になる }, } break case *pb.ProcessingRequest_ResponseBody: resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_ResponseBody{ ResponseBody: &pb.BodyResponse{ Response: &pb.CommonResponse{ BodyMutation: &pb.BodyMutation{ Mutation: &pb.BodyMutation_Body{ Body: []byte("Rewrite value !"), // 書き換える }, }, }, }, }, ModeOverride: &v3alpha.ProcessingMode{}, } break default: logrus.Debug(fmt.Sprintf("Unknown Request type %v\n", value)) }
実装
たとえばnginxからEnvoyに乗り換える際に404ページをどうするのか、という問題を解決する一つの解を紹介しています。
ほとんどのユースケースとして、ブラウザからアクセスした際にユーザーに404ページが表示されれば良い、であるためResponseBodyを返す対象は
RequestのHTTP MethodがGET
かつ、UpstreamのResponse Status Codeが404
である場合と限定されます。
Request/ResponseのBody、Headerの書き換え処理はgoroutineによって処理のスコープが別れているため、context
を利用して値を共有する必要があります。
それ以外の場合は、01のサンプルで示したように「何もしない」処理を実施します。
より条件を厳しくしたり、:path
単位で返却するResponseBodyを分けるなど、実装次第でさまざまな要求を実現できます。
switch value := req.Request.(type) { case *pb.ProcessingRequest_RequestHeaders: httpMethod, _ := share.GetHeaderValue(value.RequestHeaders.Headers.Headers, ":method") ctx = context.WithValue(ctx, HTTP_METHOD, httpMethod) resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_RequestHeaders{}, } break case *pb.ProcessingRequest_RequestBody: resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_RequestBody{}, } break case *pb.ProcessingRequest_ResponseHeaders: status, _ := share.GetHeaderValue(value.ResponseHeaders.Headers.Headers, ":status") httpMethod := (ctx).Value(HTTP_METHOD).(string) if httpMethod == "GET" && status == "404" { // Request MethodがGETかつ、Response Headerの:statusが404であるときにResponse Bodyを上書きする resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_ResponseHeaders{ ResponseHeaders: &pb.HeadersResponse{ Response: &pb.CommonResponse{ HeaderMutation: &pb.HeaderMutation{ RemoveHeaders: []string{ "content-encoding", }, }, }, }, }, ModeOverride: &v3alpha.ProcessingMode{ ResponseBodyMode: v3alpha.ProcessingMode_BUFFERED, }, } } else { resp = &pb.ProcessingResponse{ // 条件を満たさない場合は何もしない Response: &pb.ProcessingResponse_ResponseHeaders{}, } } break case *pb.ProcessingRequest_ResponseBody: httpMethod := (ctx).Value(HTTP_METHOD).(string) if httpMethod == "GET" { // 404かつGETのときしか来ないので、それに甘えてやや条件がゆるい(実装をサボってる) resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_ResponseBody{ ResponseBody: &pb.BodyResponse{ Response: &pb.CommonResponse{ BodyMutation: &pb.BodyMutation{ Mutation: &pb.BodyMutation_Body{ Body: []byte(HTML_404), // 404のHTMLを返す }, }, }, }, }, ModeOverride: &v3alpha.ProcessingMode{}, } } else { resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_ResponseBody{}, } } break default: logrus.Debug(fmt.Sprintf("Unknown Request type %v\n", value)) }
実装
Upstreamのステータスコードに対してResponseBodyを書き換えるようにするサンプル。 まだまだ詰が甘い部分がたくさんありますが、404以外のざっくりとした発展形として紹介しています。
htmlMap := map[string]string{ // サーバー起動時にキャッシュする "403": share.ReadFile("public/403.html"), "404": share.ReadFile("public/404.html"), "500": share.ReadFile("public/500.html"), "503": share.ReadFile("public/503.html"), } // 中略 switch value := req.Request.(type) { case *pb.ProcessingRequest_RequestHeaders: httpMethod, _ := share.GetHeaderValue(value.RequestHeaders.Headers.Headers, ":method") requestPath, _ := share.GetHeaderValue(value.RequestHeaders.Headers.Headers, ":path") ctx = context.WithValue(ctx, HTTP_METHOD, httpMethod) // Request時のHTTP MethodをResponse時の処理に対して共有する resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_RequestHeaders{}, } break case *pb.ProcessingRequest_RequestBody: resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_RequestBody{}, } break case *pb.ProcessingRequest_ResponseHeaders: responseStatus, _ := share.GetHeaderValue(value.ResponseHeaders.Headers.Headers, ":status") httpMethod := (ctx).Value(HTTP_METHOD).(string) // Request時のHTTP Methodを取得する html := "" for status, resHtml := range htmlMap { // 該当するstatus codeが探索する if responseStatus == status { html = resHtml } } ctx = context.WithValue(ctx, RES_CONTENT, html) // ResponseHeaderとResponseBodyで情報を共有する if httpMethod == "GET" && html != "" { resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_ResponseHeaders{ ResponseHeaders: &pb.HeadersResponse{ Response: &pb.CommonResponse{ HeaderMutation: &pb.HeaderMutation{ RemoveHeaders: []string{ "content-encoding", }, SetHeaders: []*core.HeaderValueOption{ { Header: &core.HeaderValue{ Key: "content-type", Value: "text/html", }, }, { Header: &core.HeaderValue{ Key: "content-length", Value: strconv.Itoa(len(html)), }, }, }, }, }, }, }, ModeOverride: &v3alpha.ProcessingMode{ ResponseBodyMode: v3alpha.ProcessingMode_BUFFERED, }, } } else { resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_ResponseHeaders{}, } } break case *pb.ProcessingRequest_ResponseBody: httpMethod := (ctx).Value(HTTP_METHOD).(string) html := (ctx).Value(RES_CONTENT).(string) if httpMethod == "GET" && html != "" { resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_ResponseBody{ ResponseBody: &pb.BodyResponse{ Response: &pb.CommonResponse{ BodyMutation: &pb.BodyMutation{ Mutation: &pb.BodyMutation_Body{ Body: []byte(html), }, }, }, }, }, ModeOverride: &v3alpha.ProcessingMode{}, } } else { resp = &pb.ProcessingResponse{ Response: &pb.ProcessingResponse_ResponseBody{}, } } break default: logrus.Debug(fmt.Sprintf("Unknown Request type %v\n", value)) }
External ProcessingのServer実装はHTTPのRequest/Responseの仕様に対して正確かつ忠実に実装する必要があります。
さもなくばExtProcのサーバーではなく、Envoy側が停止するためラフにHEAD
リクエストを飛ばして停止する可能性が十分にありうる諸刃の剣です。
ユースケースを厳密にし、実装することでこの問題は回避できますがまだまだ安心して使える状況ではないと考えられます。
alpha版であるため状況を注視しつつ、今後に大いに期待したい機能です。