gw-08 step 01 — A minimal xDS control plane driving a real Envoy
Goal
Build an xDS server with go-control-plane that serves a listener,
route, cluster, and endpoints to a real Envoy, and watch config flow
from your Go process into the live proxy.
Code — src/go/snapshot.go
package cp
import (
cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
"github.com/envoyproxy/go-control-plane/pkg/cache/types"
"github.com/envoyproxy/go-control-plane/pkg/cache/v3"
"github.com/envoyproxy/go-control-plane/pkg/resource/v3"
)
// MakeSnapshot builds a consistent set of {cluster, endpoints, route,
// listener} at a version. Everything references "playback".
func MakeSnapshot(version string, endpoints []string) *cache.Snapshot {
const clusterName = "playback"
cl := &cluster.Cluster{
Name: clusterName,
ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS},
EdsClusterConfig: &cluster.Cluster_EdsClusterConfig{
EdsConfig: &core.ConfigSource{ // endpoints come via EDS (ADS)
ConfigSourceSpecifier: &core.ConfigSource_Ads{Ads: &core.AggregatedConfigSource{}},
},
},
LbPolicy: cluster.Cluster_LEAST_REQUEST, // P2C (gw-06)
}
eds := makeEndpoints(clusterName, endpoints) // []*endpoint.ClusterLoadAssignment
rt := makeRoute("local_route", clusterName) // *route.RouteConfiguration
ls := makeHTTPListener("listener_0", "local_route", 10000) // *listener.Listener
snap, _ := cache.NewSnapshot(version, map[resource.Type][]types.Resource{
resource.ClusterType: {cl},
resource.EndpointType: {eds},
resource.RouteType: {rt},
resource.ListenerType: {ls},
})
return snap
}
func makeEndpoints(clusterName string, addrs []string) *endpoint.ClusterLoadAssignment {
var lbs []*endpoint.LbEndpoint
for _, a := range addrs {
host, port := splitHostPort(a)
lbs = append(lbs, &endpoint.LbEndpoint{
HostIdentifier: &endpoint.LbEndpoint_Endpoint{Endpoint: &endpoint.Endpoint{
Address: &core.Address{Address: &core.Address_SocketAddress{
SocketAddress: &core.SocketAddress{
Address: host,
PortSpecifier: &core.SocketAddress_PortValue{PortValue: port},
}}},
}},
})
}
return &endpoint.ClusterLoadAssignment{
ClusterName: clusterName,
Endpoints: []*endpoint.LocalityLbEndpoints{{LbEndpoints: lbs}},
}
}
Code — src/go/server.go (the xDS gRPC server)
package cp
import (
"context"
"net"
clusterservice "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3"
discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
endpointservice "github.com/envoyproxy/go-control-plane/envoy/service/endpoint/v3"
listenerservice "github.com/envoyproxy/go-control-plane/envoy/service/listener/v3"
routeservice "github.com/envoyproxy/go-control-plane/envoy/service/route/v3"
"github.com/envoyproxy/go-control-plane/pkg/cache/v3"
"github.com/envoyproxy/go-control-plane/pkg/server/v3"
"google.golang.org/grpc"
)
func Run(ctx context.Context, snapCache cache.SnapshotCache, addr string) error {
srv := server.NewServer(ctx, snapCache, nil)
grpcServer := grpc.NewServer()
// Register the ADS endpoint (aggregates all xDS on one stream).
discovery.RegisterAggregatedDiscoveryServiceServer(grpcServer, srv)
// (Per-type services can also be registered for non-ADS Envoys.)
endpointservice.RegisterEndpointDiscoveryServiceServer(grpcServer, srv)
clusterservice.RegisterClusterDiscoveryServiceServer(grpcServer, srv)
routeservice.RegisterRouteDiscoveryServiceServer(grpcServer, srv)
listenerservice.RegisterListenerDiscoveryServiceServer(grpcServer, srv)
lis, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return grpcServer.Serve(lis)
}
Code — main.go
func main() {
snapCache := cache.NewSnapshotCache(true, cache.IDHash{}, nil) // ADS=true
const nodeID = "edge-envoy-1"
snap := cp.MakeSnapshot("v1", []string{"127.0.0.1:9001", "127.0.0.1:9002"})
_ = snap.Consistent() // verify clusters/endpoints/routes line up
_ = snapCache.SetSnapshot(context.Background(), nodeID, snap)
cp.Run(context.Background(), snapCache, ":18000")
}
Envoy bootstrap (points Envoy at your control plane)
envoy-bootstrap.yaml:
node: { id: edge-envoy-1, cluster: edge }
dynamic_resources:
ads_config:
api_type: GRPC
transport_api_version: V3
grpc_services: [{ envoy_grpc: { cluster_name: xds_cluster } }]
cds_config: { ads: {}, resource_api_version: V3 }
lds_config: { ads: {}, resource_api_version: V3 }
static_resources:
clusters:
- name: xds_cluster
type: STRICT_DNS
typed_extension_protocol_options: # h2 to the control plane
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config: { http2_protocol_options: {} }
load_assignment:
cluster_name: xds_cluster
endpoints: [{ lb_endpoints: [{ endpoint: { address: { socket_address: { address: 127.0.0.1, port_value: 18000 }}}}]}]
admin: { address: { socket_address: { address: 127.0.0.1, port_value: 9901 }}}
go run . # control plane on :18000
func-e run -c envoy-bootstrap.yaml # Envoy fetches config via ADS
# two mock origins:
python3 -m http.server 9001 & python3 -m http.server 9002 &
# Traffic flows through Envoy (listener on :10000) to the origins:
curl -v localhost:10000/
# See what Envoy ACTUALLY applied (and the version it ACKed):
curl -s localhost:9901/config_dump | jq '.. | .version_info? // empty' | sort -u
curl -s localhost:9901/clusters # endpoints Envoy got via EDS
Tasks
- Build the control plane, boot Envoy against it, and curl through Envoy to the origins — config came entirely from your Go process.
- In Envoy's
/config_dump, find theversion_infoand confirm it matches the"v1"you set (Envoy ACKed it). - Break the snapshot on purpose (route references a nonexistent
cluster); confirm
snap.Consistent()catches it, or that Envoy NACKs and/config_dumpstays on the last good version.
Acceptance
- A request through Envoy reaches an origin using only dynamically- pushed config (no static routes/clusters).
- You can read the applied version from Envoy's admin and tie it to your snapshot version.
- A bad snapshot is rejected (consistency check or NACK), and Envoy keeps serving last-known-good.
Discussion prompts
- Where is the control plane relative to the request path, and what
happens to live traffic if you
killyour Go process? (Last-known- good.) - Why does the snapshot bundle clusters + endpoints + routes + listeners at one version? What goes wrong if endpoints update without the cluster?
- What does a NACK mean operationally, and why must you alert on it?