いよいよ Envoy の肝である xDS サーバーを使いたいと思います。 まず手始めにエンドポイントを設定できる Endpoint Discovery Service (EDS) を提供するサービスを実装します。
xDS おさらい
Envoy では gRPC または REST API 経由で動的に設定変更できます。 これは Envoy にリクエストを送るのではなく、Envoy が API を提供するサーバーに繋ぎにいきます。 API には、エンドポイントの設定を提供する Endpoint Discovery Service (EDS) や、クラスタの設定を提供する Cluster Discovery Service (CDS) などがあります。 これらをまとめてxDS APIと呼びます。
xDS API のデータ構造は Protocol Buffer によって定義されています。 v2 API referenceも大体はこの Protocol Buffer に基づきます。 また Envoy 起動時にロードする静的な設定も、ほとんどの設定項目がこの構造体の定義に基づきます。
xDS API を提供するサーバーをコントロールプレーンと呼び、Envoy 側をデータプレーンと呼びます。
構成図
今回構築する構成は以下の図のとおりです。
基本的なクラスタ構成は前回の記事と似ています。 Envoy はバーチャルホストに基づいて、ユーザーからのリクエストを 2 つのクラスタに分岐します。
前回と違う点は、Envoy は起動時に転送先のエンドポイント(アドレスとポート)を知りません。 かわりに EDS API からエンドポイントの設定をロードして、接続先ホストのアドレスやポートを知ります。
エンドポイントを保持する EDS API は、接続する Envoy の ID に基づいた適切なエンドポイントを返します(今回はとりあえず固定です)。
Go の XDS サーバー実装
Envoy のコントロールプレーンは、Envoy のプロトコルにしたがった API を提供します。 公式で Protocol Buffer が提供されてますが、go-control-plane という公式の便利 Go 実装があります。
go-config-plane は xDS API が使えるだけでなく、キャッシュ機能などの実運用に必要な機能もあります。
クラスタを起動
前回と同じく nginx クラスタと httpd クラスタをあらかじめ起動します。
$ docker run --rm --name nginx1 nginx
$ docker run --rm --name nginx2 nginx
$ docker run --rm --name httpd1 httpd
$ docker run --rm --name httpd2 httpd
エンドポイントを知るために雑なシェル芸を叩きます。 ここで取得した IP アドレスを後ほど xDS API で利用します。
$ for h in nginx1 nginx2 httpd1 httpd2; do docker inspect $h | jq -r '.[].NetworkSettings.IPAddress'; done
Envoy を起動
まずは Envoy の設定ファイルをゴシゴシと書きます。
# /tmp/envoy/envoy.yaml
node:
id: node0
cluster: cluster.local
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 80 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
stat_prefix: ingress_http
route_config:
name: route
virtual_hosts:
- name: nginx_service
domains: ["nginx.local"]
routes:
- match: { prefix: "/" }
route: { cluster: nginx_cluster }
- name: httpd_service
domains: ["httpd.local"]
routes:
- match: { prefix: "/" }
route: { cluster: httpd_cluster }
http_filters:
- name: envoy.router
clusters:
- name: nginx_cluster
connect_timeout: 0.25s
lb_policy: ROUND_ROBIN
type: EDS
eds_cluster_config:
eds_config:
api_config_source:
api_type: GRPC
grpc_services:
envoy_grpc:
cluster_name: xds_cluster
- name: httpd_cluster
connect_timeout: 0.25s
lb_policy: ROUND_ROBIN
type: EDS
eds_cluster_config:
eds_config:
api_config_source:
api_type: GRPC
grpc_services:
envoy_grpc:
cluster_name: xds_cluster
- name: xds_cluster
connect_timeout: 0.25s
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: xds_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: 127.0.0.1, port_value: 20000 }
node
セクション
前回にはなかった node
セクションには、Envoy の識別子を書きます。
node
セクションは xDS API を利用する場合は必須となります。
cluster
セクション
前回と同くhttpd_cluster
とnginx_cluster
をclusters
セクションに記述します。
前回と違いエンドポイントを与えるのではなく、EDS を利用することを宣言します。
EDS を利用するには、type: EDS
を指定して、eds_cluster_config
に EDS の設定を記述します。
EDS の接続先もまた、clusters
以下に定義します。
ここに EDS のクラスタ名 xds_cluster
を宣言します。
xds_cluster
自体は前回と同じようにload_assignment
で明示的にエンドポイントを与えます。
この xds_cluster
を、上記の eds_cluster_config
内で指定します。
以上で Envoy の設定はおわりです。 試しに Envoy を起動してみます。 すると xds_cluster に接続を試みますが、まだ xDS API に接続できないためリトライし続けます。
$ docker run \
--name envoy --rm --network host \
-v /tmp/envoy:/etc/envoy \
envoyproxy/envoy:v1.9.0
xDS API の実装
さて本題の xDS API を実装します。
基本的に以下のコードをコピペすれば動きます。
ただしgithub.com/envoyproxy/go-control-plane
は最近 API の変更が入ったので v0.6.6 を使ってください。
package main
import (
"flag"
"fmt"
"log"
"net"
"os"
api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
"github.com/envoyproxy/go-control-plane/envoy/api/v2/endpoint"
"github.com/envoyproxy/go-control-plane/pkg/cache"
xds "github.com/envoyproxy/go-control-plane/pkg/server"
"google.golang.org/grpc"
)
// NodeHash interfaceの実装。Envoyの識別子から文字列をかえすハッシュ関数を実装する。
type hash struct{}
func (hash) ID(node *core.Node) string {
if node == nil {
return "unknown"
}
return node.Cluster + "/" + node.Id
}
var upstreams = map[string][]struct {
Address string
Port uint32
}{
// ここはコンテナのアドレス
"nginx_cluster": {{"172.17.0.2", 80}, {"172.17.0.3", 80}},
"httpd_cluster": {{"172.17.0.4", 80}, {"172.17.0.5", 80}},
}
// スナップショットを返す。構造体の形はProtocol Bufferの定義と同じ。
func defaultSnapshot() cache.Snapshot {
var resources []cache.Resource
for cluster, ups := range upstreams {
eps := make([]endpoint.LocalityLbEndpoints, len(ups))
for i, up := range ups {
eps[i] = endpoint.LocalityLbEndpoints{
LbEndpoints: []endpoint.LbEndpoint{{
Endpoint: &endpoint.Endpoint{
Address: &core.Address{
Address: &core.Address_SocketAddress{
SocketAddress: &core.SocketAddress{
Address: up.Address,
PortSpecifier: &core.SocketAddress_PortValue{PortValue: up.Port},
},
},
},
},
}},
}
}
assignment := &api.ClusterLoadAssignment{
ClusterName: cluster,
Endpoints: eps,
}
resources = append(resources, assignment)
}
return cache.NewSnapshot("0.0", resources, nil, nil, nil)
}
func run(listen string) error {
// xDSの結果をキャッシュとして設定すると、いい感じにxDS APIとして返してくれる。
snapshotCache := cache.NewSnapshotCache(false, hash{}, nil)
server := xds.NewServer(snapshotCache, nil)
// NodeHashで返ってくるハッシュ値とその設定のスナップショットをキャッシュとして覚える
err := snapshotCache.SetSnapshot("cluster.local/node0", defaultSnapshot())
if err != nil {
return err
}
// gRCPサーバーを起動してAPIを提供
grpcServer := grpc.NewServer()
api.RegisterEndpointDiscoveryServiceServer(grpcServer, server)
lsn, err := net.Listen("tcp", listen)
if err != nil {
return err
}
return grpcServer.Serve(lsn)
}
func main() {
var listen string
flag.StringVar(&listen, "listen", ":20000", "listen port")
flag.Parse()
log.Printf("Starting server with -listen=%s", listen)
err := run(listen)
if err != nil {
fmt.Println(os.Stderr, err)
os.Exit(1)
}
}
pkg/cache
パッケージや pkg/server
パッケージは、xDS API を提供するための高位なライブラリです。
これらは xDS の設定を内部でキャッシュして、Envoy が渡す識別子に紐づく適切な設定を xDS API で返します。
デフォルトでスナップショット形式のキャッシュ実装が用意されてます。 これは Envoy 識別子と設定値を内部で持ち、Envoy 識別子に対応する設定値を返します。 キャッシュは interface として定義されてるので、必要ならばキャッシュを自前で実装できます。 もちろん高級すぎて使いにくいという人は、ナイーブなライブラリも用意されてます。
さあ、必要なライブラリをインストールしてこのコードをgo run
すればコントロールプレーンは起動します。
Envoy がコントロールプレーンに接続できると、繰り返し出力されてた警告が収まるはずです。
うまくいけば xDS API を使って適切なエンドポイントを取得できているでしょう。
試しに以下のコマンドを打ってみてください。 それぞれ httpd クラスタと nginx クラスタに接続できれば成功です。
$ curl -H'Host: httpd.local' 127.0.0.1
$ curl -H'Host: nginx.local' 127.0.0.1
おわりに
コードだらけの記事になってしまいましたが、なんとなく xDS API で何ができるか伝われば幸いです。 そして Envoy では通常の HTTP プロキシ以上の事ができるのがなんとなくわかったと思います。 これを応用すれば、マイクロサービスに必要なサービスディスカバリやネットワーク制御も Envoy でできることがわかるでしょう。
まだまだ Envoy を探求して、他のトピックについてもまた記事を書きたいと思います!