こんにちは、金融ソリューション事業部の孫です。 シリーズの最初の記事( Part1 )では、 Kubernetes の強力な機能を活用するためにEKS(Elastic Kubernetes Service)をどのように設定するかについて詳しく説明しました。 EKSの設定が成功した後、ゲームのインフラでよく使われるAgonesとOpen Matchをインストールしました。 また、公式デモでテストを行い、インストールが正しく行われたことを確認しました。 Kubernetes に基づくAgonesとOpen Matchという2つの コンポーネント について、理解していない方がいらっしゃるかもしれません。 そのため、Part2では、まずAgonesとOpen Matchの基本的な概念を簡単に紹介します。 次に、実践でマッチングシステムのデモを作成し、どのようにAgonesとOpen Matchを組み合わせて効率的で柔軟なDedicated Serverの管理とマッチングを実現するかを示します。 Agonesの紹介 OpenMatchの紹介 OpenMatchのマッチメイカーを作成する一般的なフロー OpenMatchとAgonesの統合 実践:ゲームマッチングシステムのデモ作成 マッチングルールの定義 事前準備 マッチング関数の作成 GameFrontend Director Match Profilesの作成 Agones Allocator ServiceによるOpenMatchの統合 AgonesのAllocate機能のパッケージ化 Allocator ServiceパッケージをOpenMatchに統合 MatchFunction デプロイと動作確認 ローカルにおいてDockerImageのコンパイル DockerImageをAmazon ECRにアップロード モジュールをEKSにデプロイ 動作確認 終わりに 参考 Agonesの紹介 Agonesは、 Google Cloudと Ubisoft が共同で開発されて、 Ubisoft 内の大規模な マルチプレイヤー オンラインゲーム(MMO)で利用されているソリューションです。 また、プログラミングが OSS で公開されている為安全に独自ネットワーク内で動作させることが可能です。 Agonesは、 Kubernetes の特性を活用してゲームサーバーを効率的に、そしてスケーラブルに運用および管理する方法を提供します。 Agonesの主要な コンポーネント には、GameServer、Fleetがあります。 Agonesでは、開発者は簡単な Kubernetes のコマンドを用いてGameServerを作成および管理することが可能で、これによりゲームサーバーの管理の複雑さが大幅に低減されます。 Agonesがゲームサーバーのライフサイクルを管理する中で、以下の6つのステージを定義しています。 Agonesはゲームサーバーのステージに応じて、適切な処理を実行します。 Scheduled (予定):GameServerがスケジュールされ、Nodeに割り当てられる Requested (要求): Kubernetes のPodが作成され、GameServerが作成される Starting (起動中):GameServerが起動し、プレイヤーがゲームに接続できる状態になる前の準備状態 Ready (準備完了):GameServerがアクティブ状態で、プレイヤーが接続できる Allocated (割り当て済み):プレイヤーがGameServerに接続し、リソースが確保されている Shutdown (シャットダウン):すべてのプレイヤーが切断され、GameServerがシャットダウンする OpenMatchの紹介 OpenMatchは、Frontend API 、Backend API 、Query API 、Functionなど、複数の コンポーネント から成り立っています。 これらの コンポーネント はそれぞれが独自の役割を果たしながら協調して働き、マッチングシステムを構築します。 OpenMatchのマッチングフローは以下のとおりです。 プレイヤーがFrontend API にマッチングリク エス トを送信する Frontend API はそのリク エス トを内部の状態でストアに保存する マッチング関数がQuery API を使用して状態ストアから条件に合うプレイヤーを問い合わせする マッチング関数がBackend API にマッチング結果を返す Backend API がプレイヤーにマッチング結果を返す OpenMatchのマッチメ イカ ーを作成する一般的なフロー OpenMatchのマッチメ イカ ーを作成するには主に三つのステップがあります。 マッチングルールを定義する マッチング関数を作成する マッチメ イカ ーの設定および運用を行う まず、マッチングルールを定義します。 このルールはマッチングロジックを反映したもので、プレイヤーのレベル、地域、スキルなどを含めます。 次に、マッチング関数を作成します。 この関数はQuery API を使用してマッチングルールに合致するプレイヤーを問い合わせ、そのマッチング結果をBackend API に返します。 最後に、マッチメ イカ ーの設定と運用を行います。OpenMatchは多くの設定オプションを提供しており、それらはニーズに応じて設定できます。 OpenMatchとAgonesの統合 OpenMatchとAgonesの統合は、効率的なゲームマッチングシステムを構築する上での重要な部分であり、主に二つのプロセスが関与しています。 OpenMatchのマッチング関数からAgonesのGameServerを呼びだすところ GameServerのライフサイクルを管理するところ OpenMatchはプレイヤーのマッチングを担当し、一方AgonesはGameServerのライフサイクルの管理を担当します。 これら二つの組み合わせにより、プレイヤーのニーズに応じてGameServerを動的に作成および割り当てることができます。 OpenMatchのマッチング関数内で、Agones SDK を通じて新しいGameServerを作成できます。 しかし、ほとんどの場合新しいGameServerを作成するだけではなく既存のGameServerをスケジュールし、割り当てることがより重要です。 GameServerのパフォーマンス、負荷、地理的な位置などを評価し、最適なGameServerを見つける必要があります。 適切なGameServerを見つけたら、そのアドレスをプレイヤーに返します、プレイヤーはそのアドレスを使用してGame Serverに接続しゲーム体験を始めます。 ここで終わりではありませんが、GameServerの状態を監視し、必要に応じて調整する必要があります。 例えば、GameServerの負荷が高すぎる場合、新しいGameServerを作成して負荷を分散できます。 一方、GameServerのプレイヤー数が減少した場合、それをシャットダウンしてリソースを節約することも可能です。 実践:ゲームマッチングシステムのデモ作成 先に紹介したOpenMatch マッチメーカー の作成プロセスに従って、デモを作成し始めます。 マッチングルールの定義 このデモでは、ユーザーのスキルレベルとレイテンシを基にスコアを算出し、同一リージョン内でスコアが近いユーザーをマッチングするというルールを実装します。 それぞれのマッチングルームは4人のプレイヤーで構成され、スコアが近いユーザー同士は一緒になります。 以下では、このマッチングの詳細や手順、そして適用する アルゴリズム について具体的に説明します。 チケット詳細 Ticket Details チケットは以下図のGameFrontendによって作成され、OpenMatchのFrontendにプッシュされる情報です。 チケットにはプレイヤーに関する情報が含まれており、マッチングの際に使用されます。 以下「GameFrontend」、「Director」、「MatchFunction」章の実装で利用されます。 今回のデモでは、チケット詳細には以下の要素が含まれています。 タグ tag :タグを使ってチケットを分類することが可能で、それによりマッチングシステムはより効率的に対応するキューを見つけることができる 今回はゲームモード Game Mode という設計を前提に実装するため、タグはmode.sessionとする リージョン Region :これはプレイヤーがいる地理的な地域を示しているが、今回はap-northeast-1、ap-northeast-3とする スキルレベル Skill Level :これはプレイヤーのスキルレベルを示しており、0.0から2.0の範囲で設定される レイテンシ Latency :これはプレイヤーのネットワーク遅延を示している ※ほとんどの人は0に近いですが、ネットワークの信頼性をシミュレートするために、一部の人は無限大に設定されている マッチング機能の基準 MatchFunction Criteria 今回のデモでは、マッチングの基準を以下に定義します。 以下「Director」、「MatchFunction」章の実装で利用されます。 まずはプレイヤーの地理的な地域とゲームモードを基準に、チケットプールを作成する 次に、各プレイヤーに対して score = skill - (latency / 1000.0) の アルゴリズム を使用してスコアを算出する そして、スコアに基づいてルームにプレイヤーを配置する 高スキル、低レイテンシのユーザーは同じルームに割り当てられる 1つのマッチに参加できるプレイヤーの上限は、4人と定められている ディレクタープロファイル Director Profiles ディレクタープロファイルはディレクターが生成するオブジェクトで、マッチのリク エス トに使用されます。 以下「Director」章の実装で利用されます。 今回のデモでは、ディレクターは5秒ごとにプロファイルを生成し、マッチをリク エス トする。 また、ディレクターはプレイヤーの地理的な地域に応じて、対応する地理的な地域のGameServerをプレイヤーに割り当てる 事前準備 OpenMatchの リポジトリ をローカル環境にクローンする ベースとなるコードは、tutorials/matchmaker101のパスに存在する git clone https://github.com/googleforgames/open-match.git Golang による実装のため、適切な IDE を設定する この記事では、 Visual Studio Code にGo plugin(v.39.0)をインストールした環境で開発を進めた Docker環境を準備する 対象の環境でDockerをセットアップするには、 Dockerのインストール のドキュメントを参照してください マッチング関数の作成 Openmatch公式ドキュメントの Tutorial をベースに、ステップ バイス テップでGameFrontend、Director、MatchFunctionといった コンポーネント を設計・作成します。 ※TutorialはOpenmatchの フレームワーク プログラムで、マッチングロジックは上記のルールに従って独自に実装する必要があります。 以下の図は、Openmatch の全体的な アーキテクチャ で、赤く囲まれた部分は独自に実装すべき部分です。 GameFrontend チケット生成関数を実装する ※ベースコード: https://github.com/googleforgames/open-match/blob/main/tutorials/matchmaker101/frontend/ticket.go 「マッチングルールの定義」章で定義したチケット詳細 Ticket Details のルールに従って、 mode.session という名前のタグを定義して、次にランダムにスキルとレイテンシの値を設定します。 リージョンの設定については、具体的なユーザーがどこから接続するかは確定していないため、外部から値を取得するように定義します。この情報はクライアントから取得する必要があります。 # ticket.go import( "open-match.dev/open-match/pkg/pb" // 必要なパッケージ追加 "math/rand" "time" ) func makeTicket(region string) *pb.Ticket { modes := []string{"mode.session"} ticket := &pb.Ticket{ SearchFields: &pb.SearchFields{ Tags: modes, DoubleArgs: CreateDoubleArgs(), StringArgs: map[string]string{ "region": region, }, }, } return ticket } func CreateDoubleArgs() map[string]float64 { rand.Seed(time.Now().UTC().UnixNano()) skill := 2 * rand.Float64() latency := 50.0 * rand.ExpFloat64() return map[string]float64{ "skill": skill, "latency": latency, } } echo フレームワーク を使用してFrontendのAPIServerを実装する ※ベースコード: https://github.com/googleforgames/open-match/blob/main/tutorials/matchmaker101/frontend/main.go この API ServerのURLは: GET /play/:region で、regionパラメータを持っています。 # frontend/main.go import( "github.com/labstack/echo" ) type matchResponce struct { IP string `json:"ip"` Port string `json:"port"` Skill string `json:"skill"` Latency string `json:"latency"` Region string `json:"region"` } var fe pb.FrontendServiceClient var matchRes = &matchResponce{} func main() { //:(ベースコートを省略する) // fe = pb.NewFrontendServiceClient(conn)以降のコードをコメントアウト e := echo.New() e.GET("/play/:region", handleGetMatch) e.Start(":80") } func handleGetMatch(c echo.Context) error { // Create Ticket. region := c.Param("region") req := &pb.CreateTicketRequest{ Ticket: makeTicket(region), } matchRes.Skill = fmt.Sprintf("%f", req.Ticket.SearchFields.DoubleArgs["skill"]) matchRes.Latency = fmt.Sprintf("%f", req.Ticket.SearchFields.DoubleArgs["latency"]) matchRes.Region = req.Ticket.SearchFields.StringArgs["region"] resp, err := fe.CreateTicket(context.Background(), req) if err != nil { log.Fatalf("Failed to CreateTicket, got %v", err) return c.JSON(http.StatusInternalServerError, matchRes) } // Polling TicketAssignment. deleteOnAssign(fe, resp) return c.JSON(http.StatusOK, matchRes) } func deleteOnAssign(fe pb.FrontendServiceClient, t *pb.Ticket) { //:(ベースコートを省略する) if got.GetAssignment() != nil { log.Printf("Ticket %v got assignment %v", got.GetId(), got.GetAssignment()) conn := got.GetAssignment().Connection slice := strings.Split(conn, ":") matchRes.IP = slice[0] matchRes.Port = slice[1] break } } Director Match Profilesの作成 ※ベースコード: https://github.com/googleforgames/open-match/blob/main/tutorials/matchmaker101/director/profile.go 「マッチングルールの定義」章で定義した マッチング機能の基準 MatchFunction Criteria のチケットプール作成ルールに従って、 TagPresentFilters および StringEqualsFilter を定義します。 「マッチングルールの定義」章で定義したディレクタープロファイル Director Profiles のゲームサーバー選択ルールに従って、 profile.Extensions を定義します。 # profile.go type AllocatorFilterExtension struct { Labels map[string]string `json:"labels"` Fields map[string]string `json:"fields"` } func generateProfiles() []*pb.MatchProfile { var profiles []*pb.MatchProfile regions := []string{"ap-northeast-1", "ap-northeast-3"} for _, region := range regions { profile := &pb.MatchProfile{ Name: fmt.Sprintf("profile_%s", region), Pools: []*pb.Pool{ { Name: "pool_mode_" + region, TagPresentFilters: []*pb.TagPresentFilter{ {Tag: "mode.session"}, }, StringEqualsFilters: []*pb.StringEqualsFilter{ {StringArg: "region", Value: region}, }, }, }, } // build filter extensions filter := AllocatorFilterExtension{ Labels: map[string]string{ "region": region, }, Fields: map[string]string{ "status.state": "Ready", }, } // to protobuf Struct labelsStruct := &structpb.Struct{Fields: make(map[string]*structpb.Value)} for key, value := range filter.Labels { labelsStruct.Fields[key] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: value}} } fieldsStruct := &structpb.Struct{Fields: make(map[string]*structpb.Value)} for key, value := range filter.Fields { fieldsStruct.Fields[key] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: value}} } // put data to the protobuf Struct filterStruct := &structpb.Struct{Fields: map[string]*structpb.Value{ "labels": {Kind: &structpb.Value_StructValue{StructValue: labelsStruct}}, "fields": {Kind: &structpb.Value_StructValue{StructValue: fieldsStruct}}, }} // to google.protobuf.Any object filterAny, err := ptypes.MarshalAny(filterStruct) if err != nil { panic(err) } profile.Extensions = map[string]*any.Any{ "allocator_filter": filterAny, } profiles = append(profiles, profile) } return profiles } Agones Allocator ServiceによるOpenMatchの統合 Agonesを統合し、マッチング結果に対応するGameServerアドレスを割り当てます。 プレイヤーはこのアドレスを通じて対応するGameServerに接続します。 Agones Allocate機能の実装は独自のものであり、OpenMatchの チュートリアル には基礎となるコードが存在しません。 そのため、directorフォルダの下に新規ファイルとしてallocator_director.goを作成します。 AgonesのAllocate機能のパッケージ化 Agonesが提供する Allocator Service を使って対応するGameServerを取得します。 デフォルトのクライアント証明書を取得する Allocator Service はmTLS認証モードを使用しており、これにより証明書を使用してサービスに接続することは必須になります。 証明書は既にhelmインストール時に作成されています。 独自の証明書を使用することも可能で、具体的な設定は こちら を参照してください。 下記のコマンドを実行してデフォルトのクライアント証明書を取得する ※筆者は Mac の環境でコマンドを実行しています。 Linux の環境であれば、 base64 -D の代わりに base64 -d コマンドを使用してください。 # MACのコマンド kubectl get secret allocator-client.default -n default -ojsonpath="{.data.tls\.crt}" | base64 -D > "client.crt" kubectl get secret allocator-client.default -n default -ojsonpath="{.data.tls\.key}" | base64 -D > "client.key" kubectl get secret allocator-tls-ca -n agones-system -ojsonpath="{.data.tls-ca\.crt}" | base64 -D > "tls-ca.crt" 取得したクライアント証明書を配置します。 上記でダウンロードした証明書を特定のパスに保存し、Pathとして定義します。 ここでは、 allocator/certfile ディレクト リに保存することを想定しています。 # agones_allocator.go const ( KeyFilePath = "allocator/certfile/client.key" CertFilePath = "allocator/certfile/client.crt" CaCertFilePath = "allocator/certfile/tls-ca.crt" ) Allocator Serviceのクライアントを作成する 外部からAgonesのAllocate機能を呼びだす必要がある場合、 NewAgonesAllocatorClient を呼びだすとクライアントが生成されます。 ※ご注意: コードが長くなりすぎないように、エラー処理に関連するコードは削除しています。 # agones_allocator.go type AgonesAllocatorClientConfig struct { KeyFile string CertFile string CaCertFile string AllocatorServiceHost string AllocatorServicePort int Namespace string MultiCluster bool } type AgonesAllocatorClient struct { Config *AgonesAllocatorClientConfig DialOpts grpc.DialOption } func NewAgonesAllocatorClient() (*AgonesAllocatorClient, error) { config := &AgonesAllocatorClientConfig{ KeyFile: KeyFilePath, CertFile: CertFilePath, CaCertFile: CaCertFilePath, AllocatorServiceHost: AllocatorServiceHost, AllocatorServicePort: AllocatorServicePort, Namespace: "default", MultiCluster: false, } cert, err = ioutil.ReadFile(config.CertFile) key, err = ioutil.ReadFile(config.KeyFile) ca, err = ioutil.ReadFile(config.CaCertFile) dialOpts, err := createRemoteClusterDialOption(cert, key, ca) return &AgonesAllocatorClient{ Config: config, DialOpts: dialOpts, }, nil } func createRemoteClusterDialOption(clientCert, clientKey, caCert []byte) (grpc.DialOption, error) { cert, err := tls.X509KeyPair(clientCert, clientKey) tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true} if len(caCert) != 0 { tlsConfig.RootCAs = x509.NewCertPool() if !tlsConfig.RootCAs.AppendCertsFromPEM(caCert) { return nil, errors.New("only PEM format is accepted for server CA") } } return grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), nil } Allocateのメイン関数を実装する この関数では、まずgrpc( grpc.Dial ) プロトコル を使用して Allocator Service に接続します。 次に、GameServerの選択ルール( assignmentGroup.Assignment.Extensions )を取得します。 このルールに基づいて対応するGameServerを取得し、最終的に各Assignmentの address フィールドにアドレスを付与します。 ※ご注意: コードが長くなりすぎないように、エラー処理に関連するコードは削除しています。 # agones_allocator.go func (c *AgonesAllocatorClient) Allocate(req *pb.AssignTicketsRequest) error { conn, err := grpc.Dial(fmt.Sprintf("%s:%d", c.Config.AllocatorServiceHost, c.Config.AllocatorServicePort), c.DialOpts) defer conn.Close() grpcClient := pb_agones.NewAllocationServiceClient(conn) for _, assignmentGroup := range req.Assignments { filterAny := assignmentGroup.Assignment.Extensions["allocator_filter"] filter := &structpb.Struct{} if err := ptypes.UnmarshalAny(filterAny, filter); err != nil { panic(err) } request := &pb_agones.AllocationRequest{ Namespace: c.Config.Namespace, GameServerSelectors: []*pb_agones.GameServerSelector{ { MatchLabels: filter.Fields["labels"], }, }, MultiClusterSetting: &pb_agones.MultiClusterSetting{ Enabled: c.Config.MultiCluster, }, } resp, err := grpcClient.Allocate(context.Background(), request) if len(resp.GetPorts()) > 0 { address := fmt.Sprintf("%s:%d", resp.Address, resp.Ports[0].Port) assignmentGroup.Assignment.Connection = address } } return nil } Allocator ServiceパッケージをOpenMatchに統合 上記のOpenMatchの アーキテクチャ 図に基づき、Agonesサービスを呼び出してGameServerを取得する コンポーネント はDirectorです。 そのため、呼び出しコードをDirectorに統合する必要があります。 ※ベースコード: https://github.com/suecideTech/try-openmatch-agones/blob/master/OpenMatch/mod_matchmaker101/director/main.go GameServerのassgin関数を修正します。 ここでは、上記で作成した Allocator Service パッケージを使用して実際のGameServerアドレスを取得します。 元のコードでは GameServerAllocations を使ってGameServerアドレスを取得していますが、これはAgonesが推奨している方法ではなく、また後期の拡張にも適していません。 そのため、公式に推奨されている Allocator Service をラップしてGameServerを取得するようにしました。 # director/main.go func assign(be pb.BackendServiceClient, matches []*pb.Match) error { for _, match := range matches { ticketIDs := []string{} for _, t := range match.GetTickets() { ticketIDs = append(ticketIDs, t.Id) } aloReq := &pb.AssignTicketsRequest{ Assignments: []*pb.AssignmentGroup{ { TicketIds: ticketIDs, Assignment: &pb.Assignment{ Extensions: match.Extensions, }, }, }, } client, err := allocator.NewAgonesAllocatorClient() client.Allocate(aloReq) if _, err := be.AssignTickets(context.Background(), aloReq); err != nil { return fmt.Errorf("AssignTickets failed for match %v, got %w", match.GetMatchId(), err) } log.Printf("Assigned server %v to match %v", conn, match.GetMatchId()) } return nil } MatchFunction ユーザーマッチングルールを実装します。 ※ベースコード: https://github.com/suecideTech/try-openmatch-agones/blob/master/OpenMatch/mod_matchmaker101/matchfunction/mmf/matchfunction.go 「マッチングルールの定義」章で定義した マッチング機能の基準 MatchFunction Criteria のユーザーマッチングルールに従って、まずユーザーのスコアを計算し、スコアの大きさに基づいて4人部屋を割り当てます。 # matchfunction.go const ( matchName = "basic-matchfunction" ticketsPerPoolPerMatch = 4 ) func (s *MatchFunctionService) Run(req *pb.RunRequest, stream pb.MatchFunction_RunServer) error { //:(ベースコートを省略する) //poolTickets, err := matchfunction.QueryPools(stream.Context(), s.queryServiceClient, req.GetProfile().GetPools()) p := req.GetProfile() tickets, err := matchfunction.QueryPool(stream.Context(), s.queryServiceClient, p.GetPools()[0]) //:(ベースコートを省略する) idPrefix := fmt.Sprintf("profile-%v-time-%v", p.GetName(), time.Now().Format("2006-01-02T15:04:05.00")) proposals, err := makeMatches(req.GetProfile(), idPrefix, tickets) //:(ベースコートを省略する) } func (s *MatchFunctionService) makeMatches(ticketsPerPoolPerMatch int, profile *pb.MatchProfile, idPrefix string, tickets []*pb.Ticket) ([]*pb.Match, error) { if len(tickets) < ticketsPerPoolPerMatch { return nil, nil } ticketScores := make(map[string]float64) for _, ticket := range tickets { ticketScores[ticket.Id] = score(ticket.SearchFields.DoubleArgs["skill"], ticket.SearchFields.DoubleArgs["latency"]) } sort.Slice(tickets, func(i, j int) bool { return ticketScores[tickets[i].Id] > ticketScores[tickets[j].Id] }) var matches []*pb.Match count := 0 for len(tickets) >= ticketsPerPoolPerMatch { matchTickets := tickets[:ticketsPerPoolPerMatch] tickets = tickets[ticketsPerPoolPerMatch:] var matchScore float64 for _, ticket := range matchTickets { matchScore += ticketScores[ticket.Id] } eval, err := anypb.New(&pb.DefaultEvaluationCriteria{Score: matchScore}) if err != nil { log.Printf("Failed to marshal DefaultEvaluationCriteria into anypb: %v", err) return nil, fmt.Errorf("Failed to marshal DefaultEvaluationCriteria into anypb: %w", err) } newExtensions := map[string]*anypb.Any{"evaluation_input": eval} newExtensions for k, v := range origExtensions { newExtensions[k] = v } matches = append(matches, &pb.Match{ MatchId: fmt.Sprintf("%s-%d", idPrefix, count), MatchProfile: profile.GetName(), MatchFunction: matchName, Tickets: matchTickets, Extensions: newExtensions, }) count++ } return matches, nil } func score(skill, latency float64) float64 { return skill - (latency / 1000.0) } ここまでで、マッチング機能とGameServerのケジューリング機能の実装が完了しました。 次に、作成したモジュールをそれぞれEKSにアップロードし、デモとしてテストします。 具体的に完成したデモの アーキテクチャ は、以下の図の通りです。 デプロイと動作確認 ローカルにおいてDockerImageの コンパイル GameFrontend # OpenMatch/mod_matchmaker101/frontend/Dockerfile docker build -t localimage/mod_frontend:0.1 . Director # OpenMatch/mod_matchmaker101/director/Dockerfile docker build -t localimage/mod_director:0.1 . MatchFunction # OpenMatch/mod_matchmaker101/matchfunction/Dockerfile docker build -t localimage/mod_matchfunction:0.1 . DockerImageを Amazon ECRにアップロード EKSでDockerImageを取得する際、ローカルのイメージにアクセスできないため、イメージを Amazon ECRサービスにアップロードする必要があります。 ※ Amazon ECRはイメージを保管するための専用レポジトリで、Docker Hubなどと同様のサービスがあります。 Amazon ECRでプライベートイメージ リポジトリ を作成する具体的な方法については、 ECRでプライベートリポジトリを作成する を参照してください。 以下の名称の Amazon ECRプライベートイメージ リポジトリ をそれぞれ作成する Frontendの リポジトリ 名:mod_frontend Directorの リポジトリ 名:mod_director MatchFunctionの リポジトリ 名:mod_matchfunction ローカルのイメージを Amazon ECRにアップロードする # Frontend docker tag localimage/mod_frontend:0.1 {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_frontend:0.1 docker push {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_frontend:0.1 # Director docker tag localimage/mod_director:0.1 {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_director:0.1 docker push {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_director:0.1 # MatchFunction: docker tag localimage/mod_matchfunction:0.1 {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_matchfunction:0.1 docker push {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_matchfunction:0.1 モジュールをEKSにデプロイ デプロイ yaml ファイル内のimageアドレスを、上記で作成した Amazon ECRのアドレスに変更します。 # Frontend: ## yaml file path ## OpenMatch/mod_matchmaker101/frontend/frontend.yaml image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_frontend:0.1 # Director ## yaml file path ## OpenMatch/mod_matchmaker101/director/director.yaml image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_director:0.1 # MatchFunction: ## yaml file path ## OpenMatch/mod_matchmaker101/matchfunction/matchfunction.yaml image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_matchfunction:0.1 各 Yaml ファイルを以下のように修正します。 # Frontend.yaml ## yaml file path ## OpenMatch/mod_matchmaker101/frontend/frontend.yaml ## KindをDeploymentに変更し、HTTP LBサービスを追加します apiVersion: v1 kind: Service metadata: name: frontend-endpoint annotations: service.alpha.kubernetes.io/app-protocols: '{"http":"HTTP"}' labels: app: frontend spec: type: NodePort selector: app: frontend ports: - port: 80 protocol: TCP name: http targetPort: frontend --- apiVersion: apps/v1 kind: Deployment metadata: name: frontend namespace: default labels: app: frontend spec: replicas: 1 selector: matchLabels: app: frontend template: metadata: labels: app: frontend spec: containers: - name: frontend image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_frontend:0.1 imagePullPolicy: Always ports: - name: frontend containerPort: 80 # Director.yaml ## yaml file path ## OpenMatch/mod_matchmaker101/director/director.yaml apiVersion: v1 kind: Pod metadata: name: director namespace: openmatch-poc spec: containers: - name: director image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_director:0.1 imagePullPolicy: Always hostname: director # MatchFunction.yaml ## yaml file path ## OpenMatch/mod_matchmaker101/matchfunction/matchfunction.yaml apiVersion: v1 kind: Pod metadata: name: matchfunction namespace: openmatch-poc labels: app: openmatch component: matchfunction spec: containers: - name: matchfunction image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_MatchFunction:0.1 imagePullPolicy: Always ports: - name: grpc containerPort: 50502 --- kind: Service apiVersion: v1 metadata: name: matchfunction namespace: openmatch-poc labels: app: openmatch component: matchfunction spec: selector: app: openmatch component: matchfunction clusterIP: None type: ClusterIP ports: - name: grpc protocol: TCP port: 50502 それぞれの yaml ファイルをEKSに適用し、マッチングシステムをデプロイします。 # GameFrontend kubectl apply -f frontend.yaml # Director kubectl apply -f director.yaml # MatchFunction kubectl apply -f matchfunction.yaml 動作確認 以下の図に示すように、 frontend.yaml で作成されたServiceに接続し、マッチングシステムをテストします。 この frontend-endpoint サービスにローカルでもアクセスできるようにするため、 Kubernetes のPortForwadering機能を使用します。 Frontendサービスをローカルに マッピング する 下図の⑤エリアです。 # Frontend サービスをローカルの8081ポートにマッピングします kubectl port-forward services/frontend-ednpoint 8081:80 8つの新しいターミナルを作成して、8名のプレイヤーがFrontendサービスに接続するのをシミュレートする 下図の①エリアで4名(ap-northeast-1)+4名(ap-northeast-3)のプレイヤーが接続するのをシミュレートします。 # Get: /Frontend/play/regionname ## 4名(ap-northeast-1) curl 127.0.0.1:8081/play/ap-northeast-1 ## 4名(ap-northeast-3) curl 127.0.0.1:8081/play/ap-northeast-3 エラーの有無を確認するために、matchfunction/director/frontendモジュールのログ情報を出力する 下図の②〜④エリアです。 # matchfuntion log kubectl logs --tail 4 matchfunction -n openmatch-poc # director log kubectl logs --tail 4 director -n openmatch-poc # frontend log kubectl logs --tail 4 deployments/frontend 上記の手順に従って、8名のプレイヤーがマッチングを開始するシミュレーションを行います。 確認ポイントは次の通りです。 ②〜④エリアのログにはエラー出力がありません。 ①の8名のクライアント全員がIP、Port情報を正常に取得します。 また、前の4名のプレイヤーは同じグループにマッチされるため、その IP:Port アドレスは同じです。 後の4名のプレイヤーも同じグループにマッチされ、その IP:Port アドレスも同じです。 ⑥エリアでは、GameServerのステータスを確認し、2台のサーバーがAllocated状態にあることを確認します。 終わりに これまでに、AgonesとOpen Matchを使用して高可用性と拡張性、スケジューリングが可能なマッチングシステムを構築しました。 次に、 Part3 ではUnrealEngineを使用してGameClientを開発し、このマッチングシステムに接続する方法を説明します。 これにより、マッチング機能を持ち、Agonesを使用してDedicated Serverをスケジューリングする マルチプレイ ゲームの開発を完了します。 引き続き、お楽しみにしてください! 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ(Web3/メタバース/AI) 参考 https://agones.dev/site/docs/overview/ https://open-match.dev/site/docs/ 執筆: @chen.sun 、レビュー: @yamashita.yuki ( Shodo で執筆されました )