Amazon ECS Service Connectを使用して、gRPCのリクエストが各ECSタスクに振り分けられるかを確かめる
通常のHTTP APIでも同じ挙動になる
使用する言語、ライブラリ
- Go 1.22
- google.golang.org/grpc v1.63.0
Service Connectなし
まずはService Connectを使用せず、gRPCのリクエストが1つのサーバー側ECSタスクにリクエストし続けることを確かめる
検証するaws構成
クライアント側はAWS Cloud Mapを使用してサーバー側ECSタスクを名前解決する
1. gRPCサーバーをECSサービスとして起動する
サーバーは自身のECSタスクのprivate IPアドレスを返す
特にclient side load balancingの設定等はしていない
ECSタスクは2つ起動する
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"google.golang.org/grpc"
pb "google.golang.org/grpc/examples/features/proto/echo"
)
type ecServer struct {
pb.UnimplementedEchoServer
}
func (s *ecServer) UnaryEcho(_ context.Context, req *pb.EchoRequest) (*pb.EchoResponse, error) {
metadataURL := os.Getenv("ECS_CONTAINER_METADATA_URI_V4")
if metadataURL == "" {
log.Println("ECS_CONTAINER_METADATA_URI_V4 environment variable is not set")
return &pb.EchoResponse{}, nil
}
resp, err := http.Get(metadataURL + "/task")
if err != nil {
log.Printf("failed to get metadata: %v\n", err)
return &pb.EchoResponse{}, nil
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("failed to read response body: %v\n", err)
return &pb.EchoResponse{}, nil
}
var metadata struct {
Containers []struct {
Name string `json:"name"`
Networks []struct {
IPv4Addresses []string `json:"IPv4Addresses"`
} `json:"networks"`
} `json:"containers"`
}
if err := json.Unmarshal(body, &metadata); err != nil {
log.Printf("failed to parse JSON: %v\n", err)
return &pb.EchoResponse{}, nil
}
myIP := ""
for _, container := range metadata.Containers {
if container.Name == "server" && len(container.Networks) > 0 && len(container.Networks[0].IPv4Addresses) > 0 {
myIP = container.Networks[0].IPv4Addresses[0]
break
}
}
return &pb.EchoResponse{Message: fmt.Sprintf("%s (at %s)", req.Message, myIP)}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterEchoServer(s, &ecServer{})
log.Printf("serving on :50051\n")
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
起動した2つのECSタスクのprivate IPアドレスは以下になった
- 10.0.1.174
- 10.0.17.182
なおECSタスクのコンテナヘルスチェックは設定していない
2. gRPCクライアントをECSサービスとして起動する
クライアントはgRPCサーバーに一定間隔でリクエストを送り、レスポンスを標準出力する
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
ecpb "google.golang.org/grpc/examples/features/proto/echo"
)
func main() {
conn, err := grpc.NewClient(
os.Getenv("GRPC_TARGET"),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
hwc := ecpb.NewEchoClient(conn)
for i := 0; i < 100; i++ {
r, err := hwc.UnaryEcho(context.Background(), &ecpb.EchoRequest{Message: "hello"})
if err != nil {
log.Fatalf("could not invoke: %v", err)
}
fmt.Println(r.Message)
time.Sleep(5 * time.Second)
}
}
ECSタスク定義のリクエスト先環境変数 GRPC_TARGETにはCloud MapのDNSとサーバー側ポート(ここでは50051)を設定する
e.g.) server.grpc.local:50051
3. クライアント側ECSタスクのログを確認する
1つのサーバー側ECSタスクにリクエストし続けることが確認できる
Service Connectあり
次にService Connectを使用し、gRPCのリクエストが2つのサーバー側ECSタスクに振り分けられてリクエストされるかを確かめる
検証するaws構成
クライアント側はService Connect経由でサーバーにリクエストする
1. サーバー側ECSサービスにService Connectを設定しサービス更新する
起動した2つのECSタスクのprivate IPアドレスは以下になった
- 10.0.2.102
- 10.0.26.24
2. クライアント側ECSタスク定義のリクエスト先を更新する
環境変数 GRPC_TARGETのDNSにService ConnectのDNS(ここではserver-50051-tcp.grpc.local)を設定し新しくリビジョン作成する