電通総研のブログ - TECH PLAY

TECH PLAY

電通総研

電通総研 の技術ブログ

全867件

こんにちは、ISID 金融ソリューション事業部の岡崎です。 ゲームや動画などを作成する際に、ユーザーが違和感なくコンテンツを使用するためには、 フレームごとの描画速度(フレームレート)を意識し、パフォーマンスを担保し続ける必要があります。 今回はUE5でパフォーマンスを担保するために必要な、プロファイリングのワークフローの説明を行います。 はじめに UEでは、制作したプロジェクトの性能を測るためにプロファイリングという作業を行います。 プロファイリングは、主にパフォーマンス改善を目的として実施します。例えばコマンドや専用のアプリケーションを使用して、描画に遅延が起こっていないか、不要な処理をしているBlueprintがないか、などを調べます。 今回の記事では、UEの基礎的なプロファイリングの方法( GPU 編)について紹介していきます。 CPUに関しては記事が長くなるので、次の記事でご紹介します。 検証環境/ツール OS: Windows 11 pro GPU : NVIDIA GeForce RTX 4070 Ti Game Engine: Unreal Engine 5.2.0 実装手順 「stat unit」コマンドの実施 Drawsが多い場合の対処方法 2-1. 対象メッシュの確認、削除 2-2. Naniteの設定(UE5の場合) 1. 「stat unit」コマンドの実施 まず初めに、「stat unit」コマンドについて説明していきます。 UEではさまざまな方法でプロジェクトのプロファイリングを行う方法があります。 「stat unit」はプロジェクト全体を俯瞰し、大体どの辺に致命的なバグや遅延の原因があるかなどを調べるのに適したコマンドになります。 「stat unit」コマンドを使用するためには、 デバッグ したいプロジェクトを開き、画面下部よりアウトプットタブを押下し、 コマンド入力スペースに「stat unit」を記述します。 画面上に画像のようなログが出てきます。 主要な項目の説明をします。 Frame : 画面を描画するのにかかった処理時間。60FPSのゲームを作成する場合は、16.6ms以下になるように調整しないといけない Game : リアルタイムで動作するゲームのロジックやアニメーションにかかるCPUの処理時間 Draw : 何を描画するか、選択や計算をするためにかかるCPUの処理時間 GPU Time : メッシュや各マテリアルの描画や、ライティングなどにかかる GPU の処理時間 Draws : 描画する必要のあるメッシュの個数。 GPU Timeに影響を与える またGameなどの数値は、ゲームをプレイしている最中にBlueprintなどに連動して数値が変動するので、 ゲームをプレイしてキャ ラク ターの操作などを行った際の数値も確かめる必要があります。 今回は GPU 編なので、「stat unit」の結果から、 「Draw」、「Draws」の数値を調査、修正を行っていきます。 その他の項目のCPUに関わる箇所は次の記事で紹介するので、本記事では割愛します。 2. Drawsが多い場合の対処方法 Drawsの数値が大きい場合は、メッシュが大量に配置されていたり、ポリゴン数の多すぎる複雑なメッシュが配置されてしまっている可能性があります。 今回は、16.6msを超えているため、正常時は緑に表示される数字が赤文字になっています。 この Drawsの数値が大きいように感じたので調査していきます。 まずはレベル上にどのようなメッシュやポリゴンがあるか確認していきます。 2-1. 対象メッシュの確認、削除 画面上部のツールから、オーディット>統計 を選びます。 すると別画面でレベル内に配置されているメッシュのポリゴン数を確認することが出来ます。 「Tris」と書かれている部分がポリゴン数で、「Sum Tris」と書かれている部分が合計のポリゴン数になります。 今回の統計では、「SM_Movehuman」というオブジェクトの「Count」が「31,834」になっており、「Sum Tris」の数値も非常に高くなっています。 一般的にポリゴン数は下記の 閾値 を参考にすると良いとされています。 2000から3000までが理想 5000を超えると重たくなり始める 10000を超えると問題が起こり始める このためDrawの数値が大きくなっていることが考えられます。 プロジェクトに戻って確認してみると、 実はこのレベルには大量の「SM_Movehuman」のアクターを配置していたことがわかりました。 もちろんこのプロジェクトは処理を重くすることを目的に作成しているので簡単に発見できましたが、 実際のプロジェクトでは、ここまで単純に問題を発見することはなかなか出来ません。 しかし、統計を見ながらレベルに配置してあるオブジェクトの個数や、ポリゴン数を確認し、大体のあたりをつけるのにはとても有効な手段です。 今回のように、オブジェクトが大量にあり「Draws」の数値が非常に高い場合の対処法は大きく3つあります。 無駄なオブジェクトの削除 UE5で追加された「Nanite」を使用 オクルージョンカリングの設定 ※オクルージョンカリングの設定方法は本記事では説明しません。次回以降の記事で紹介する予定です。 まずは無駄なオブジェクトの削除を試します。 本プロジェクトでは、「SM_Movehuman」のアクターをすべて削除してみます。 「stat unit」の値を確認してみます。 「Draws」の値が下がり、「Draw」にかかる処理速度も短くなっていることが確認できます。 アクター削除前 アクター削除後 また、本プロジェクトでは割愛しますが、統計内の「Count」の数を下げなくとも、そもそものポリゴン数を少ないものを用意するのも有効です。 もし配置してあるオブジェクトが全て必要なもので、削除できない場合は次の手段を試します。 2-2. Naniteの設定(UE5の場合) Naniteとは まず Nanite を簡単に説明をします。 Nanite とは、UE5から搭載された、3Dモデルを効率的にかつ高速に レンダリング してくれるシステムで、LODを自動で行ってくれます。 LODというのは Level of Detail の略で、ひとつのオブジェクトに対し、複数の粗さの違うメッシュを作成し、 カメラが遠くにあるときには粗く軽いメッシュを使用し、近くにあるときは細かく正確なメッシュを使用する描画方法です。 これによりユーザーに違和感やメッシュの粗さを感じさせずに、描画速度を上げることができます。 Naniteが搭載される前は、これらの複数のメッシュを用意し、距離を計算し配置する作業は手作業で行っていましたが、 LODがNaniteによって自動化され、手作業でやるよりさらに効率的に レンダリング できるようになりました。 Naniteの設定方法 Naniteはスタティックメッシュにのみ使用できます。(スケルタルメッシュなどには設定できないので注意が必要) 設定したいスタティックメッシュを開きます。 今回はレベル内に配置してある大量の人型のオブジェクトに対し、Naniteの設定を行っていきます。 スタティックメッシュを開いたら詳細パネルより、Naniteの設定 > Naniteサポートの有効化 をオンにします。 スタティックメッシュを保存し、レベルに配置するためのアクタークラスBlueprintを作成します。 コンポーネント 内にスタティックメッシュを追加し、詳細画面から先ほどNaniteの設定をしたスタティックメッシュを選択します。 あとは、このアクターをレベル上に配置して負荷に変化があるか検証していきます。 Naniteによる負荷軽減の検証 PCGを利用して人型のアクターを大量に配置したレベルを用意し、Naniteを設定したものと、していないものでどれだけ数値に差が出るか調査しました。 それぞれのゲーム上で「stat unit」を行います。 (今回の記事ではPCGに関しての説明は割愛いたします。) Naniteを有効にしていない場合 Naniteを有効にしている場合 Drawsの数値が劇的に減少され( 18143 → 559 )、Frameにかかる時間も16ms ほど短縮できています。 今回はデフォルトで用意されている人型のメッシュに対しNaniteの設定を行いましたが、 ZBrush などで作成した複雑でポリゴン数の多いオブジェクトなどに対してNaniteの設定を行うことで、劇的に描画速度を向上できます。 おわりに 今回は、UE5を利用してプロファイリングの方法の GPU 部分に関して、 ボトルネック になっている箇所を探し、対策する方法などをまとめました。 今回は検証用に作成したプロジェクトだったため、単純明快に問題の発見と解決ができましたが、 実際のプロジェクトでは、より複雑に原因が入り組んでいることが想像できます。 実践を積みながら、プロファイリングについてもっと詳しくなっていきたいです。 本プロジェクトでは、まだCPU側に ボトルネック がありFrameの数値が140msと、非常に遅くなっているので、 次の記事ではCPUに関するプロファイルング方法をご紹介していきます。 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ(Web3/メタバース/AI) 執筆: @okazaki.wataru 、レビュー: @kano.nanami ( Shodo で執筆されました )
こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター セキュリティグループの平岡です。 私は社内の SOC サービスの研究開発メンバーとして、Azureの Microsoft Defender for Cloud周りの調査を専門に行っています。 最近は、主に推奨事項の発出条件やセキュリティアラートの調査をしたり、顧客開発環境のセキュリティ監視などを行うための仕組みを作っています。 SOC活動を通じて、是非読者の皆さんに試していただきたいと思った情報を、このテックブログで紹介したいと思います。どうぞよろしくお願いします。 さて、今回は、 Azure Lighthouseを利用して他 サブスクリプション のセキュリティ情報を集約する方法 を紹介します。 そもそもどんなシチュエーションでこの設定が役に立つのか? Azureポータル上では、 Microsoft Defender for Cloud等のサービスを通じて、自分の サブスクリプション 内のセキュリティ情報を見ることが出来ます。 使用している サブスクリプション が一つだけなら、セキュリティ情報の管理はそれほど難しくないかもしれません。 しかし、大きな企業になると社内で沢山の サブスクリプション を立てて、それを部署や案件単位で管理していることが多いのではないでしょうか。このような状況であれば、一つの サブスクリプション で、他の サブスクリプション や、異なるテナント間のリソースのセキュリティ情報を集約して、監視することが出来たらとても楽ですよね。 そこで使っていただきたいのが Azure Lighthouse です。 Azure lighthouseとは・・・Azure Lighthouseは、複数のAzure ADテナントの管理を一か所のテナントで行えるようにするものです。これにより、Azureを払い出している部署が、各部署や案件単位で利用するテナントを効率的に構築および提供する事ができるようになります。また、Azure Lighthouse自体の利用料金はかかりません。 Azure Lighthouse とは https://learn.microsoft.com/ja-jp/azure/lighthouse/overview それでは、早速設定してみます。 今回は委任先の サブスクリプション に対して、委任元の サブスクリプション のセキュリティ情報を集約できるようにします。 1. Azure Lighthouseのデプロイを実施します (1) Azure Lighthouseのデプロイはポータルからはできない(※ 2023/6/1現在)ため、 Github のARMテンプレートを使用します。テンプレートはリンクから選択します。 https://github.com/Azure/Azure-Lighthouse-samples/ (2) README.md内の"Deploy to Azure buttons"から、下記画像のテンプレートを選択します。 Name : Azure Lighthouse - Subscription Deployment Description : onboard a subscription 2. Azureのカスタムデプロイ画面に遷移後、必須項目を埋めます    サブスクリプション : 委任元の サブスクリプション リージョン :デプロイ場所 Msp Offer Name : 委任の名称 Msp Offer Description : 委任の説明 Managed By Tenant ID : 委任先のテナントID Authorizations : 委任内容を json 形式で記述  [     {         "principalId": "<ユーザー等のオブジェクト ID>",         "principalIdDisplayName": "<任意の名前>",         "roleDefinitionId": "<委任する組み込みロール ID>"     }  ] 委任先のテナントIDは Azure Acrive Directory>概要 を参照します。 principalId(グループまたはユーザーのオブジェクトID)は Azure Active Directory >グループ(ユーザー) を参照します。 roleDefinitionId(委任する組み込みロールID)は 下記ドキュメントから必要な権限を選択します。 Azure 組み込みロール: https://learn.microsoft.com/ja-jp/azure/role-based-access-control/built-in-roles 3.必須項目の入力完了後、”確認と作成”ボタンを押下しデプロイを完了します 4.委任先のテナントで、ポータルの設定に遷移します ポータルの設定画面は右上の歯車ボタンを押下します 5.既定の サブスクリプション フィルターのプルダウンを“すべての ディレクト リ”および“すべての サブスクリプション ”に設定します 設定は以上です。 集約先の サブスクリプション で Microsoft Defender for Cloud画面を見てみましょう。 集約元の サブスクリプション の推奨情報やセキュリティ警告が反映されています。 サブスクリプション を行き来しなくても、一つの サブスクリプション でセキュリティ情報が見られるようになり、管理がしやすくなりました。 <推奨事項画面> <セキュリティ警告画面> 終わりに 今回紹介したAzure Lighthouseを利用することで、一つの サブスクリプション に複数の サブスクリプション の情報を集約することができます。 複数の サブスクリプション の管理をしている方は是非利用してみてください。 私たちは一緒に働いてくれる仲間を募集しています! セキュリティエンジニア 執筆: @hiraoka.eri 、レビュー: @fukutake.hiroaki ( Shodo で執筆されました )
こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター セキュリティグループの平岡です。 私は社内の SOC サービスの研究開発メンバーとして、Azureの Microsoft Defender for Cloud周りの調査を専門に行っています。 最近は、主に推奨事項の発出条件やセキュリティアラートの調査をしたり、顧客開発環境のセキュリティ監視などを行うための仕組みを作っています。 SOC活動を通じて、是非読者の皆さんに試していただきたいと思った情報を、このテックブログで紹介したいと思います。どうぞよろしくお願いします。 さて、今回は、 Azure Lighthouseを利用して他 サブスクリプション のセキュリティ情報を集約する方法 を紹介します。 そもそもどんなシチュエーションでこの設定が役に立つのか? Azureポータル上では、 Microsoft Defender for Cloud等のサービスを通じて、自分の サブスクリプション 内のセキュリティ情報を見ることが出来ます。 使用している サブスクリプション が一つだけなら、セキュリティ情報の管理はそれほど難しくないかもしれません。 しかし、大きな企業になると社内で沢山の サブスクリプション を立てて、それを部署や案件単位で管理していることが多いのではないでしょうか。このような状況であれば、一つの サブスクリプション で、他の サブスクリプション や、異なるテナント間のリソースのセキュリティ情報を集約して、監視することが出来たらとても楽ですよね。 そこで使っていただきたいのが Azure Lighthouse です。 Azure lighthouseとは・・・Azure Lighthouseは、複数のAzure ADテナントの管理を一か所のテナントで行えるようにするものです。これにより、Azureを払い出している部署が、各部署や案件単位で利用するテナントを効率的に構築および提供する事ができるようになります。また、Azure Lighthouse自体の利用料金はかかりません。 Azure Lighthouse とは https://learn.microsoft.com/ja-jp/azure/lighthouse/overview それでは、早速設定してみます。 今回は委任先の サブスクリプション に対して、委任元の サブスクリプション のセキュリティ情報を集約できるようにします。 1. Azure Lighthouseのデプロイを実施します (1) Azure Lighthouseのデプロイはポータルからはできない(※ 2023/6/1現在)ため、 Github のARMテンプレートを使用します。テンプレートはリンクから選択します。 https://github.com/Azure/Azure-Lighthouse-samples/ (2) README.md内の"Deploy to Azure buttons"から、下記画像のテンプレートを選択します。 Name : Azure Lighthouse - Subscription Deployment Description : onboard a subscription 2. Azureのカスタムデプロイ画面に遷移後、必須項目を埋めます    サブスクリプション : 委任元の サブスクリプション リージョン :デプロイ場所 Msp Offer Name : 委任の名称 Msp Offer Description : 委任の説明 Managed By Tenant ID : 委任先のテナントID Authorizations : 委任内容を json 形式で記述  [     {         "principalId": "<ユーザー等のオブジェクト ID>",         "principalIdDisplayName": "<任意の名前>",         "roleDefinitionId": "<委任する組み込みロール ID>"     }  ] 委任先のテナントIDは Azure Acrive Directory>概要 を参照します。 principalId(グループまたはユーザーのオブジェクトID)は Azure Active Directory >グループ(ユーザー) を参照します。 roleDefinitionId(委任する組み込みロールID)は 下記ドキュメントから必要な権限を選択します。 Azure 組み込みロール: https://learn.microsoft.com/ja-jp/azure/role-based-access-control/built-in-roles 3.必須項目の入力完了後、”確認と作成”ボタンを押下しデプロイを完了します 4.委任先のテナントで、ポータルの設定に遷移します ポータルの設定画面は右上の歯車ボタンを押下します 5.既定の サブスクリプション フィルターのプルダウンを“すべての ディレクト リ”および“すべての サブスクリプション ”に設定します 設定は以上です。 集約先の サブスクリプション で Microsoft Defender for Cloud画面を見てみましょう。 集約元の サブスクリプション の推奨情報やセキュリティ警告が反映されています。 サブスクリプション を行き来しなくても、一つの サブスクリプション でセキュリティ情報が見られるようになり、管理がしやすくなりました。 <推奨事項画面> <セキュリティ警告画面> 終わりに 今回紹介したAzure Lighthouseを利用することで、一つの サブスクリプション に複数の サブスクリプション の情報を集約することができます。 複数の サブスクリプション の管理をしている方は是非利用してみてください。 私たちは一緒に働いてくれる仲間を募集しています! セキュリティエンジニア 執筆: @hiraoka.eri 、レビュー: @fukutake.hiroaki ( Shodo で執筆されました )
はじめに こんにちは。X イノベーション 本部の藤川です。 私は「aiuola(アイウォーラ)」という エンタープライズ アプリケーションプラットフォームの開発において、プロダクトマネージャーをしています。まれにアーキテクト、 デベロッパ ーなど別の帽子を被って 開発プロセス に参加しています。 今回の記事では、aiuolaが採用している アジャイル 開発プロセス について私たちの現在の着地点をスプリントイベントを中心に紹介します。 ちなみにaiuolaとは「 エンタープライズ アプリケーションでお客様に感動を」というキャッチフレーズで立ち上げた開発プロジェクトです。エンプラ系アプリってなんかあれじゃん、結局大したことないでしょという眉唾100%の皆様、ご安心ください。 私たちの想いを体現した エンタープライズ アプリケーションは「Ci*X(サイクロス)」というブランドでリリースしております。 ご利用中のお客様からは「マニュアルを参照せずに使える業務アプリって初めて!」「 経理 部門に配属以来最も大きな感動体験です!」と評判です。 会計プロダクトの検討をされている方はぜひこちらをご覧ください。 グループ経営管理ソリューション サイクロスシリーズ aiuolaの取り組み aiuola とは aiuolaは当社の エンタープライズ アプリケーションの共通プラットフォームです。 エンタープライズ アプリで求められる認証・認可やUIライブラリを提供しています。 2023年現在、aiuolaを活用したいくつかのプロダクト群をリリースしご好評をいただいております。 体制 aiuolaの開発体制は初期からの変遷を経て、以下のような体制となっています。 本チームではサービス運用のミッションを持っていないためインフラチームは有していません。 (各プロダクト側にインフラチームがいて、我々と協力してお客様のサクセスを支えています) 初期の頃はフロントエンジニア4名、専任デザイナーが1名という体制でしたが、デザインシステムが固まってからは概ね上記の体制で活動しています。 当チームのバックエンドエンジニアは単一の ユースケース (機能)の開発を担当し、機能単位のフロント実装も行います。 一方でフロントエンジニアはUI コンポーネント の開発やフロントエンド実装指針にフォーカスを当てて活動しています。 スプリントイベント aiuolaの開発は2週間のスプリントで実施しています。 スプリント期間の過ごし方をスプリントイベントを中心に図にしてみました。 ご覧いただくとお分かりかと思いますが、すごくオーソドックスな スクラム 開発プロセス を採用しています。 なお、2020年以降、aiuolaの開発はリモートスタイルでの開発となっています。 スプリントイベントを含めて、すべての活動をリモート環境で実施しています。飲み会以外はね!😉。 デイリーミーティング 主に前日タスクでの課題とその日の活動予定を共有しますが、slackで簡単なワークレポートを投稿するルールにしているので、困りごとへの対応、レビュー担当者への アサイ ン、QAメンバーへのテスト依頼などが目的になります。 大体、15分〜20分ぐらいの時間でその名の通り、毎日実施しています。 スプリントプランニング スプリントで実施するタスクを バックログ から決定します。 スプリントチームはQAチームを含めて4つのサブチームが存在するので、3段階のスプリントプランニングステージを設けています。 プレスプリントプランニング 各サブチームにて次スプリントタスクの選定とポーカー サブリーダーによるスプリントプランニング サブチーム間の作業すり合わせ 全員でのスプリントプランニング スプリントタスクの共有 従来は3だけですべてを実施していたのですが、チーム体制が大きくなるにつれ、全員を拘束してしまう時間が増えてしまいました。 貴重なエンジニアリング時間を十分に確保することを目的に3段階のフェーズを設けることにしました。 プロダクトリーダー会 プロダクトファミリーで紹介した通り、aiuolaを活用するプロダクトは複数あります。 各プロダクトも日々開発を進めています。プロダクト間で期待する要望、対応スケジュールのすり合わせは重要です。 プロダクトリーダー会ではそのようなプロダクトをまたがった調整を行います。 リリース内容発表会 リリース内容発表会ではスプリントにおいてリリースできる機能をチーム内で共有し、紹介し、功績を称える会となります。 私たちの開発対象は エンタープライズ アプリケーションのため、単一の機能が有するフィーチャーは比較的大規模になります。そのためスプリントの中で1つの機能を完全に作り上げることができません。 リリース内容発表会ではそのリリースに含めることができた機能を対象とします。 それ以外にもQAチームから品質の取り組みに関する発表も含まれることがあります。 スプリントリリース スプリントリリースはミーティングではなく、前スプリントの成果をリリースするタスクとなります。 aiuolaとしてのリリースはスプリントの成果物を各プロダクトチームに提供する作業です。 プロダクトファミリーの市場投入サイクルはaioulaを含めてすべて6ヶ月単位となります。そのため、スプリントリリースは開発ベータ版としてプロダクトチームに成果物を提供することを指します。 レトロスペクティブ レトロスペクティブはその名の通り、スプリントの最後に行う振り返り会です。 開発の改善に役立つア イデア や課題を議論し、スプリントの中で挑戦すべきことを議論します。 従来はオフラインで実施していましたが、オンライン主体の 開発プロセス に変わってからはMiroを活用しております。 終わりに 今回の記事では私たちが開発している エンタープライズ アプリケーションプラットフォーム「aiuola」の 開発プロセス について非常に簡単ですがその一端をご紹介しました。 プロダクトファミリーにて紹介したaiuolaプラットフォームを活用した各プロダクトも積極的な機能強化を行っています。 それぞれのプロダクトはここで紹介したプロセスをコピーして適用しているわけではなく、プロダクト特性やチーム体制に応じた開発スタイルを採用し、独自に発展を遂げています。もちろん私たちaiuolaチームも現状に満足しているわけではなく、より効果的な取り組みにチャレンジしています。 いずれのプロダクトでも、共に新たなチャレンジに挑んでくれる仲間を探していますので、ご興味を持っていただいた方は是非とも採用ページをご覧ください。 また、本チームの開発メンバーがアプリケーション アーキテクチャ をテーマとしてTech Playに登壇します。 こちらも是非ご参加いただけると幸いです。 Tech Play URL 私たちは一緒に働いてくれる仲間を募集しています! プロダクトプラットフォーム開発エンジニア/新規プロダクト開発エンジニア 次世代会計プロダクト開発ITアーキテクト(Ci*Xシリーズ) 次世代会計プロダクトDevOpsエンジニア(Ci*Xシリーズ) 執筆: @fujikawa.kenji 、レビュー: @wakamoto.ryosuke ( Shodo で執筆されました )
はじめに こんにちは。X イノベーション 本部の藤川です。 私は「aiuola(アイウォーラ)」という エンタープライズ アプリケーションプラットフォームの開発において、プロダクトマネージャーをしています。まれにアーキテクト、 デベロッパ ーなど別の帽子を被って 開発プロセス に参加しています。 今回の記事では、aiuolaが採用している アジャイル 開発プロセス について私たちの現在の着地点をスプリントイベントを中心に紹介します。 ちなみにaiuolaとは「 エンタープライズ アプリケーションでお客様に感動を」というキャッチフレーズで立ち上げた開発プロジェクトです。エンプラ系アプリってなんかあれじゃん、結局大したことないでしょという眉唾100%の皆様、ご安心ください。 私たちの想いを体現した エンタープライズ アプリケーションは「Ci*X(サイクロス)」というブランドでリリースしております。 ご利用中のお客様からは「マニュアルを参照せずに使える業務アプリって初めて!」「 経理 部門に配属以来最も大きな感動体験です!」と評判です。 会計プロダクトの検討をされている方はぜひこちらをご覧ください。 グループ経営管理ソリューション サイクロスシリーズ aiuolaの取り組み aiuola とは aiuolaは当社の エンタープライズ アプリケーションの共通プラットフォームです。 エンタープライズ アプリで求められる認証・認可やUIライブラリを提供しています。 2023年現在、aiuolaを活用したいくつかのプロダクト群をリリースしご好評をいただいております。 体制 aiuolaの開発体制は初期からの変遷を経て、以下のような体制となっています。 本チームではサービス運用のミッションを持っていないためインフラチームは有していません。 (各プロダクト側にインフラチームがいて、我々と協力してお客様のサクセスを支えています) 初期の頃はフロントエンジニア4名、専任デザイナーが1名という体制でしたが、デザインシステムが固まってからは概ね上記の体制で活動しています。 当チームのバックエンドエンジニアは単一の ユースケース (機能)の開発を担当し、機能単位のフロント実装も行います。 一方でフロントエンジニアはUI コンポーネント の開発やフロントエンド実装指針にフォーカスを当てて活動しています。 スプリントイベント aiuolaの開発は2週間のスプリントで実施しています。 スプリント期間の過ごし方をスプリントイベントを中心に図にしてみました。 ご覧いただくとお分かりかと思いますが、すごくオーソドックスな スクラム 開発プロセス を採用しています。 なお、2020年以降、aiuolaの開発はリモートスタイルでの開発となっています。 スプリントイベントを含めて、すべての活動をリモート環境で実施しています。飲み会以外はね!😉。 デイリーミーティング 主に前日タスクでの課題とその日の活動予定を共有しますが、slackで簡単なワークレポートを投稿するルールにしているので、困りごとへの対応、レビュー担当者への アサイ ン、QAメンバーへのテスト依頼などが目的になります。 大体、15分〜20分ぐらいの時間でその名の通り、毎日実施しています。 スプリントプランニング スプリントで実施するタスクを バックログ から決定します。 スプリントチームはQAチームを含めて4つのサブチームが存在するので、3段階のスプリントプランニングステージを設けています。 プレスプリントプランニング 各サブチームにて次スプリントタスクの選定とポーカー サブリーダーによるスプリントプランニング サブチーム間の作業すり合わせ 全員でのスプリントプランニング スプリントタスクの共有 従来は3だけですべてを実施していたのですが、チーム体制が大きくなるにつれ、全員を拘束してしまう時間が増えてしまいました。 貴重なエンジニアリング時間を十分に確保することを目的に3段階のフェーズを設けることにしました。 プロダクトリーダー会 プロダクトファミリーで紹介した通り、aiuolaを活用するプロダクトは複数あります。 各プロダクトも日々開発を進めています。プロダクト間で期待する要望、対応スケジュールのすり合わせは重要です。 プロダクトリーダー会ではそのようなプロダクトをまたがった調整を行います。 リリース内容発表会 リリース内容発表会ではスプリントにおいてリリースできる機能をチーム内で共有し、紹介し、功績を称える会となります。 私たちの開発対象は エンタープライズ アプリケーションのため、単一の機能が有するフィーチャーは比較的大規模になります。そのためスプリントの中で1つの機能を完全に作り上げることができません。 リリース内容発表会ではそのリリースに含めることができた機能を対象とします。 それ以外にもQAチームから品質の取り組みに関する発表も含まれることがあります。 スプリントリリース スプリントリリースはミーティングではなく、前スプリントの成果をリリースするタスクとなります。 aiuolaとしてのリリースはスプリントの成果物を各プロダクトチームに提供する作業です。 プロダクトファミリーの市場投入サイクルはaioulaを含めてすべて6ヶ月単位となります。そのため、スプリントリリースは開発ベータ版としてプロダクトチームに成果物を提供することを指します。 レトロスペクティブ レトロスペクティブはその名の通り、スプリントの最後に行う振り返り会です。 開発の改善に役立つア イデア や課題を議論し、スプリントの中で挑戦すべきことを議論します。 従来はオフラインで実施していましたが、オンライン主体の 開発プロセス に変わってからはMiroを活用しております。 終わりに 今回の記事では私たちが開発している エンタープライズ アプリケーションプラットフォーム「aiuola」の 開発プロセス について非常に簡単ですがその一端をご紹介しました。 プロダクトファミリーにて紹介したaiuolaプラットフォームを活用した各プロダクトも積極的な機能強化を行っています。 それぞれのプロダクトはここで紹介したプロセスをコピーして適用しているわけではなく、プロダクト特性やチーム体制に応じた開発スタイルを採用し、独自に発展を遂げています。もちろん私たちaiuolaチームも現状に満足しているわけではなく、より効果的な取り組みにチャレンジしています。 いずれのプロダクトでも、共に新たなチャレンジに挑んでくれる仲間を探していますので、ご興味を持っていただいた方は是非とも採用ページをご覧ください。 また、本チームの開発メンバーがアプリケーション アーキテクチャ をテーマとしてTech Playに登壇します。 こちらも是非ご参加いただけると幸いです。 Tech Play URL 私たちは一緒に働いてくれる仲間を募集しています! プロダクトプラットフォーム開発エンジニア/新規プロダクト開発エンジニア 次世代会計プロダクト開発ITアーキテクト(Ci*Xシリーズ) 次世代会計プロダクトDevOpsエンジニア(Ci*Xシリーズ) 執筆: @fujikawa.kenji 、レビュー: @wakamoto.ryosuke ( Shodo で執筆されました )
こんにちは。X(クロス) イノベーション 本部 クラウド イノベーション センターの柴田です。 本記事ではTerraformでコードを変更していないリソースが known after apply となってしまう場合の回避策をご紹介します。 前提 問題となるコードの例 原因 回避策 おわりに 参考 前提 この記事は以下のTerraformのバージョンを前提とします。 新しいバージョンのTerraformでは本記事と異なる挙動をする可能性があります。 $ terraform version Terraform v1.5.3 on linux_amd64 + provider registry.terraform.io/hashicorp/aws v5.7.0 問題となるコードの例 以下のTerraformコードを例に考えてみましょう。 resource "aws_s3_bucket" "sample" { bucket_prefix = "sample-" tags = { Project = "My Project A" } } data "aws_iam_policy_document" "sample" { statement { effect = "Allow" actions = [ "s3:*" ] resources = [ aws_s3_bucket.sample.arn ] } } resource "aws_iam_policy" "sample" { name = "SampleBucketFullAccess" path = "/" policy = data.aws_iam_policy_document.sample.json } このTerraformコードを terraform apply します。 $ terraform apply Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create <= read (data resources) Terraform will perform the following actions: # data.aws_iam_policy_document.sample will be read during apply # (config refers to values not yet known) <= data "aws_iam_policy_document" "sample" { + id = (known after apply) + json = (known after apply) + statement { + actions = [ + "s3:*", ] + effect = "Allow" + resources = [ + (known after apply), ] } } # aws_iam_policy.sample will be created + resource "aws_iam_policy" "sample" { + arn = (known after apply) + id = (known after apply) + name = "SampleBucketFullAccess" + name_prefix = (known after apply) + path = "/" + policy = (known after apply) + policy_id = (known after apply) + tags_all = (known after apply) } # aws_s3_bucket.sample will be created + resource "aws_s3_bucket" "sample" { + acceleration_status = (known after apply) + acl = (known after apply) + arn = (known after apply) + bucket = (known after apply) + bucket_domain_name = (known after apply) + bucket_prefix = "sample-" + bucket_regional_domain_name = (known after apply) + force_destroy = false + hosted_zone_id = (known after apply) + id = (known after apply) + object_lock_enabled = (known after apply) + policy = (known after apply) + region = (known after apply) + request_payer = (known after apply) + tags = { + "Project" = "My Project A" } + tags_all = { + "Project" = "My Project A" } + website_domain = (known after apply) + website_endpoint = (known after apply) } Plan: 2 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes aws_s3_bucket.sample: Creating... aws_s3_bucket.sample: Creation complete after 1s [id=sample-20230713121614963200000001] data.aws_iam_policy_document.sample: Reading... data.aws_iam_policy_document.sample: Read complete after 0s [id=55239551] aws_iam_policy.sample: Creating... aws_iam_policy.sample: Creation complete after 1s [id=arn:aws:iam::************:policy/SampleBucketFullAccess] Apply complete! Resources: 2 added, 0 changed, 0 destroyed. 次にS3 バケット aws_s3_bucket.sample の Project タグの値を変更します。 resource "aws_s3_bucket" "sample" { bucket_prefix = "sample-" tags = { - Project = "My Project A" + Project = "My Project B" } } 変更後のTerraformコードに対して terraform plan を実行します。 $ terraform plan aws_s3_bucket.sample: Refreshing state... [id=sample-20230713121614963200000001] aws_iam_policy.sample: Refreshing state... [id=arn:aws:iam::************:policy/SampleBucketFullAccess] Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: ~ update in-place <= read (data resources) Terraform will perform the following actions: # data.aws_iam_policy_document.sample will be read during apply # (depends on a resource or a module with changes pending) <= data "aws_iam_policy_document" "sample" { + id = (known after apply) + json = (known after apply) + statement { + actions = [ + "s3:*", ] + effect = "Allow" + resources = [ + "arn:aws:s3:::sample-20230713121614963200000001", ] } } # aws_iam_policy.sample will be updated in-place ~ resource "aws_iam_policy" "sample" { id = "arn:aws:iam::************:policy/SampleBucketFullAccess" name = "SampleBucketFullAccess" ~ policy = jsonencode( { - Statement = [ - { - Action = "s3:*" - Effect = "Allow" - Resource = "arn:aws:s3:::sample-20230713121614963200000001" }, ] - Version = "2012-10-17" } ) -> (known after apply) tags = {} # (4 unchanged attributes hidden) } # aws_s3_bucket.sample will be updated in-place ~ resource "aws_s3_bucket" "sample" { id = "sample-20230713121614963200000001" ~ tags = { ~ "Project" = "My Project A" -> "My Project B" } ~ tags_all = { ~ "Project" = "My Project A" -> "My Project B" } # (10 unchanged attributes hidden) # (3 unchanged blocks hidden) } Plan: 0 to add, 2 to change, 0 to destroy. すると、変更がないはずのIAM policy aws_iam_policy.sample の policy が known after apply となってしまいました。 原因 これはData Sourceの仕様によるものです。 Data Resource Dependencies には以下のように記述されています。 Data resources have the same dependency resolution behavior as defined for managed resources . Setting the depends_on meta-argument within data blocks defers reading of the data source until after all changes to the dependencies have been applied. In order to ensure that data sources are accessing the most up to date information possible in a wide variety of use cases, arguments directly referencing managed resources are treated the same as if the resource was listed in depends_on . 要約すると以下のとおりです。 Data Sourceでは depends_on に記載されたリソースのすべての変更が適用されるまで読み取りは延期される。 Data Sourceの引数が他のリソースを直接参照している場合、参照先のリソースがData Sourceの depends_on に含まれている場合と同じように扱われる。 つまり Data Sourceが直接参照しているリソースに変更がある場合、それらの変更が適用されたあと、Data Sourceの読み取りが再実行される ということです。 改めて先ほどの例を考えてみましょう。Terraformが以下のように判断していたことがわかります。 aws_s3_bucket.sample のコードの変更が検出される。 data.aws_iam_policy_document.sample は aws_s3_bucket.sample を直接参照しているため、 depends_on に aws_s3_bucket.sample が含まれている場合と同じように扱われる。 depends_on に含まれる aws_s3_bucket.sample の変更が検出されたため、その変更が適用されたあとで data.aws_iam_policy_document.sample の再読み取りを行う必要があると判断される。 3をうけて aws_iam_policy.sample の policy が更新されると判断される。 回避策 Data Resource Dependencies には以下のように記述されています。 This behavior can be avoided when desired by indirectly referencing the managed resource values through a local value , unless the data resource itself has custom conditions . どうやら Local Value を間に挟むことで先ほどの事象を回避できるようです。 試してみましょう。先ほどのTerraformコードを以下のように変更します。 provider "aws" { region = "ap-northeast-1" } resource "aws_s3_bucket" "sample" { bucket_prefix = "sample-" tags = { Project = "My Project B" } } locals { s3_bucket_arn = aws_s3_bucket.sample.arn } data "aws_iam_policy_document" "sample" { statement { effect = "Allow" actions = [ "s3:*" ] resources = [ local.s3_bucket_arn ] } } resource "aws_iam_policy" "sample" { name = "SampleBucketFullAccess" path = "/" policy = data.aws_iam_policy_document.sample.json } 変更後のTerraformコードに対して terraform plan を実行します。 $ terraform plan aws_s3_bucket.sample: Refreshing state... [id=sample-20230713121614963200000001] data.aws_iam_policy_document.sample: Reading... data.aws_iam_policy_document.sample: Read complete after 0s [id=55239551] aws_iam_policy.sample: Refreshing state... [id=arn:aws:iam::************:policy/SampleBucketFullAccess] Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: ~ update in-place Terraform will perform the following actions: # aws_s3_bucket.sample will be updated in-place ~ resource "aws_s3_bucket" "sample" { id = "sample-20230713121614963200000001" ~ tags = { ~ "Project" = "My Project A" -> "My Project B" } ~ tags_all = { ~ "Project" = "My Project A" -> "My Project B" } # (10 unchanged attributes hidden) # (3 unchanged blocks hidden) } Plan: 0 to add, 1 to change, 0 to destroy. S3 バケット aws_s3_bucket.sample 以外の変更が表示されないことを確認できました。 おわりに 本記事ではTerraformでコードを変更していないリソースが known after apply となってしまう場合の回避策をご紹介しました。 この記事がこの問題に悩んでいる方のお役に立てば幸いです。 最後までお読みいただき、ありがとうございました。 参考 Data Sources - Configuration Language | Terraform | HashiCorp Developer date source referencing managed resource proposes unnecessary changes under 0.14 · Issue #27171 · hashicorp/terraform 私たちは一緒に働いてくれる仲間を募集しています! クラウドアーキテクト 執筆: @shibata.takao 、レビュー: 寺山 輝 (@terayama.akira) ( Shodo で執筆されました )
こんにちは。X(クロス) イノベーション 本部 クラウド イノベーション センターの柴田です。 本記事ではTerraformでコードを変更していないリソースが known after apply となってしまう場合の回避策をご紹介します。 前提 問題となるコードの例 原因 回避策 おわりに 参考 前提 この記事は以下のTerraformのバージョンを前提とします。 新しいバージョンのTerraformでは本記事と異なる挙動をする可能性があります。 $ terraform version Terraform v1.5.3 on linux_amd64 + provider registry.terraform.io/hashicorp/aws v5.7.0 問題となるコードの例 以下のTerraformコードを例に考えてみましょう。 resource "aws_s3_bucket" "sample" { bucket_prefix = "sample-" tags = { Project = "My Project A" } } data "aws_iam_policy_document" "sample" { statement { effect = "Allow" actions = [ "s3:*" ] resources = [ aws_s3_bucket.sample.arn ] } } resource "aws_iam_policy" "sample" { name = "SampleBucketFullAccess" path = "/" policy = data.aws_iam_policy_document.sample.json } このTerraformコードを terraform apply します。 $ terraform apply Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create <= read (data resources) Terraform will perform the following actions: # data.aws_iam_policy_document.sample will be read during apply # (config refers to values not yet known) <= data "aws_iam_policy_document" "sample" { + id = (known after apply) + json = (known after apply) + statement { + actions = [ + "s3:*", ] + effect = "Allow" + resources = [ + (known after apply), ] } } # aws_iam_policy.sample will be created + resource "aws_iam_policy" "sample" { + arn = (known after apply) + id = (known after apply) + name = "SampleBucketFullAccess" + name_prefix = (known after apply) + path = "/" + policy = (known after apply) + policy_id = (known after apply) + tags_all = (known after apply) } # aws_s3_bucket.sample will be created + resource "aws_s3_bucket" "sample" { + acceleration_status = (known after apply) + acl = (known after apply) + arn = (known after apply) + bucket = (known after apply) + bucket_domain_name = (known after apply) + bucket_prefix = "sample-" + bucket_regional_domain_name = (known after apply) + force_destroy = false + hosted_zone_id = (known after apply) + id = (known after apply) + object_lock_enabled = (known after apply) + policy = (known after apply) + region = (known after apply) + request_payer = (known after apply) + tags = { + "Project" = "My Project A" } + tags_all = { + "Project" = "My Project A" } + website_domain = (known after apply) + website_endpoint = (known after apply) } Plan: 2 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes aws_s3_bucket.sample: Creating... aws_s3_bucket.sample: Creation complete after 1s [id=sample-20230713121614963200000001] data.aws_iam_policy_document.sample: Reading... data.aws_iam_policy_document.sample: Read complete after 0s [id=55239551] aws_iam_policy.sample: Creating... aws_iam_policy.sample: Creation complete after 1s [id=arn:aws:iam::************:policy/SampleBucketFullAccess] Apply complete! Resources: 2 added, 0 changed, 0 destroyed. 次にS3 バケット aws_s3_bucket.sample の Project タグの値を変更します。 resource "aws_s3_bucket" "sample" { bucket_prefix = "sample-" tags = { - Project = "My Project A" + Project = "My Project B" } } 変更後のTerraformコードに対して terraform plan を実行します。 $ terraform plan aws_s3_bucket.sample: Refreshing state... [id=sample-20230713121614963200000001] aws_iam_policy.sample: Refreshing state... [id=arn:aws:iam::************:policy/SampleBucketFullAccess] Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: ~ update in-place <= read (data resources) Terraform will perform the following actions: # data.aws_iam_policy_document.sample will be read during apply # (depends on a resource or a module with changes pending) <= data "aws_iam_policy_document" "sample" { + id = (known after apply) + json = (known after apply) + statement { + actions = [ + "s3:*", ] + effect = "Allow" + resources = [ + "arn:aws:s3:::sample-20230713121614963200000001", ] } } # aws_iam_policy.sample will be updated in-place ~ resource "aws_iam_policy" "sample" { id = "arn:aws:iam::************:policy/SampleBucketFullAccess" name = "SampleBucketFullAccess" ~ policy = jsonencode( { - Statement = [ - { - Action = "s3:*" - Effect = "Allow" - Resource = "arn:aws:s3:::sample-20230713121614963200000001" }, ] - Version = "2012-10-17" } ) -> (known after apply) tags = {} # (4 unchanged attributes hidden) } # aws_s3_bucket.sample will be updated in-place ~ resource "aws_s3_bucket" "sample" { id = "sample-20230713121614963200000001" ~ tags = { ~ "Project" = "My Project A" -> "My Project B" } ~ tags_all = { ~ "Project" = "My Project A" -> "My Project B" } # (10 unchanged attributes hidden) # (3 unchanged blocks hidden) } Plan: 0 to add, 2 to change, 0 to destroy. すると、変更がないはずのIAM policy aws_iam_policy.sample の policy が known after apply となってしまいました。 原因 これはData Sourceの仕様によるものです。 Data Resource Dependencies には以下のように記述されています。 Data resources have the same dependency resolution behavior as defined for managed resources . Setting the depends_on meta-argument within data blocks defers reading of the data source until after all changes to the dependencies have been applied. In order to ensure that data sources are accessing the most up to date information possible in a wide variety of use cases, arguments directly referencing managed resources are treated the same as if the resource was listed in depends_on . 要約すると以下のとおりです。 Data Sourceでは depends_on に記載されたリソースのすべての変更が適用されるまで読み取りは延期される。 Data Sourceの引数が他のリソースを直接参照している場合、参照先のリソースがData Sourceの depends_on に含まれている場合と同じように扱われる。 つまり Data Sourceが直接参照しているリソースに変更がある場合、それらの変更が適用されたあと、Data Sourceの読み取りが再実行される ということです。 改めて先ほどの例を考えてみましょう。Terraformが以下のように判断していたことがわかります。 aws_s3_bucket.sample のコードの変更が検出される。 data.aws_iam_policy_document.sample は aws_s3_bucket.sample を直接参照しているため、 depends_on に aws_s3_bucket.sample が含まれている場合と同じように扱われる。 depends_on に含まれる aws_s3_bucket.sample の変更が検出されたため、その変更が適用されたあとで data.aws_iam_policy_document.sample の再読み取りを行う必要があると判断される。 3をうけて aws_iam_policy.sample の policy が更新されると判断される。 回避策 Data Resource Dependencies には以下のように記述されています。 This behavior can be avoided when desired by indirectly referencing the managed resource values through a local value , unless the data resource itself has custom conditions . どうやら Local Value を間に挟むことで先ほどの事象を回避できるようです。 試してみましょう。先ほどのTerraformコードを以下のように変更します。 provider "aws" { region = "ap-northeast-1" } resource "aws_s3_bucket" "sample" { bucket_prefix = "sample-" tags = { Project = "My Project B" } } locals { s3_bucket_arn = aws_s3_bucket.sample.arn } data "aws_iam_policy_document" "sample" { statement { effect = "Allow" actions = [ "s3:*" ] resources = [ local.s3_bucket_arn ] } } resource "aws_iam_policy" "sample" { name = "SampleBucketFullAccess" path = "/" policy = data.aws_iam_policy_document.sample.json } 変更後のTerraformコードに対して terraform plan を実行します。 $ terraform plan aws_s3_bucket.sample: Refreshing state... [id=sample-20230713121614963200000001] data.aws_iam_policy_document.sample: Reading... data.aws_iam_policy_document.sample: Read complete after 0s [id=55239551] aws_iam_policy.sample: Refreshing state... [id=arn:aws:iam::************:policy/SampleBucketFullAccess] Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: ~ update in-place Terraform will perform the following actions: # aws_s3_bucket.sample will be updated in-place ~ resource "aws_s3_bucket" "sample" { id = "sample-20230713121614963200000001" ~ tags = { ~ "Project" = "My Project A" -> "My Project B" } ~ tags_all = { ~ "Project" = "My Project A" -> "My Project B" } # (10 unchanged attributes hidden) # (3 unchanged blocks hidden) } Plan: 0 to add, 1 to change, 0 to destroy. S3 バケット aws_s3_bucket.sample 以外の変更が表示されないことを確認できました。 おわりに 本記事ではTerraformでコードを変更していないリソースが known after apply となってしまう場合の回避策をご紹介しました。 この記事がこの問題に悩んでいる方のお役に立てば幸いです。 最後までお読みいただき、ありがとうございました。 参考 Data Sources - Configuration Language | Terraform | HashiCorp Developer date source referencing managed resource proposes unnecessary changes under 0.14 · Issue #27171 · hashicorp/terraform 私たちは一緒に働いてくれる仲間を募集しています! クラウドアーキテクト 執筆: @shibata.takao 、レビュー: 寺山 輝 (@terayama.akira) ( Shodo で執筆されました )
こんにちは、X(クロス) イノベーション 本部 クラウド イノベーション センターの田村です。 2023 年 5 月の Microsoft Build にて統合分析プラットフォーム Microsoft Fabric が発表されました。 Microsoft Fabric は現在プレビュー中ですが、既存のサービスにはない機能追加や多くのアップデートが予定されており、 Microsoft のデータ領域ビジネスにおいて今後注目すべきサービスです。 本記事では、 Microsoft Fabric の概要とサービス検証として実施した勤怠データ分析の内容をご紹介します。 Microsoft Fabric の概要 One Lake Synapse Data Engineering Data Factory Power BI サービス検証:勤怠データの分析 概要 検証シナリオ:勤怠データ分析 検証アーキテクチャ Microsoft Fabric サブスクリプションの購入 Microsoft Fabric ワークスペースの作成 レイクハウスの作成 勤怠データの格納 ノートブックによるデータの整形 データフローによるデータの変換・加工 SQL エンドポイントによる分析 Power BI レポートの作成 サービス検証結果 一気通貫のデータ分析 データ分析の経験が浅いユーザーへのサポート 各機能の操作感 まとめ Microsoft Fabric の概要 Microsoft Fabric は、データ分析に必要な要素を 1 つの統合環境で提供する SaaS 形式のソリューションです。 従来 Microsoft の クラウド サービスでデータ分析をする場合は、Azure Data Factory や Azure Synapse Analytics といった PaaS を組み合わせた アーキテクチャ を設計する手法が一般的でした。 Microsoft Fabric では、データの蓄積/加工/変換/分析/可視化を単一の基盤上で実現できるため、ユーザーは統一された UI 上でシームレスな分析を実施できると謳われています。 これにより、ユーザーは 1 つのサービス内でデータ分析やデータ管理に関する作業をすべて完結でき、分析リソースのサイジングといった管理負担の軽減が期待できます。 Microsoft Fabric の主要な構成要素について述べます。 記事のボリュームを考慮し、今回は技術検証にて扱った要素について簡潔に説明しますので、他にも興味がある方は こちら のリンクをご参照ください。 (出典) Microsoft Fabric とは One Lake Microsoft Fabric の環境に 1 つ作成される SaaS 形式のデータレイクです。 Microsoft Fabric の各機能で利用されるデータを一元的に管理する基盤であり、ショートカットと呼ばれる機能を利用するとさまざまなサービスから手軽にデータを移動できます。 現時点では Microsoft Fabric 内部/ Amazon S3 /Azure Data Lake Storage との連携が実装されており、今後も拡大されていくと思われます。 OneLake のショートカット Synapse Data Engineering レイクハウスと呼ばれる構造化/非構造化データを管理・分析できるデータストアに格納されたデータに対する処理を行う機能です。 具体的には Apache Spark クラスタ による分析やノートブックによる対話的な分析( Python /R/ Scala )、データパイプラインによるデータの移動/変換/結合等が実施できます。 Microsoft Fabric のデータ エンジニアリングとは Data Factory 既存のデータ統合サービスである Azure Data Factory と、 Excel や Power BI 等に搭載されている Power Query を組み合わせたデータ統合機能です。 さまざまなデータソースからデータを取り込み、柔軟な処理が可能です。 Microsoft Fabric の Data Factory とは Power BI Microsoft Power Platform にて提供されている BI ツールです。 Microsoft Fabric に可視化 コンポーネント として統合されています。 Power BI とは サービス検証:勤怠データの分析 概要 7 月上旬あたりで Microsoft Fabric のキャッチアップと チュートリアル の実施がある程度進んだので、手持ちのデータを使用して Microsoft Fabric での分析に着手しました。 分析作業を通じて、下記の項目について検証することを目的とします。 Microsoft Fabric のみでデータ分析を 一気通貫 で実施できるか データ分析の経験があまりないユーザーをサポートする機能が現時点でどの程度実装されているか Microsoft Fabric の操作感(Power BI や Data Factory など統合されたサービスがこれまで通り利用できるか) 検証シナリオ:勤怠データ分析 ISID では日々の勤怠入力を自社ソリューションである POSITIVE で実施しています。 POSITIVE - 人事給与、勤怠システム ISID 社内で利用している POSITIVE では、入力した勤怠情報を Excel 形式でダウンロードできます。 そこで、自分の勤怠データが記録された Excel ファイルを Microsoft Fabric で分析し、可視化までの工程を 一気通貫 で検証してみることにしました。 検証 アーキテクチャ アーキテクチャ と検証手順は下記の通りです。 Microsoft Fabric サブスクリプション を購入する Microsoft Fabric ワークスペース を作成する Synapse Data Engineering にてデータストアとなるレイクハウスを作成する ローカルから Excel 形式の勤怠データをレイクハウスへ格納する ノートブック を作成し、 Excel 形式の勤怠データを整形して再度レイクハウスへ格納する 整形された勤怠データをデータフローにて変換・加工し、再度レイクハウスへ格納する レイクハウスから SQL エンドポイントを作成し分析を実施する 変換後のデータおよび分析結果を Power BI にて可視化する Microsoft Fabric サブスクリプション の購入 Microsoft 365 もしくは Azure 上から購入できます。 今回は Azure 上から購入しました。 手順は簡単かつドキュメントの内容をなぞるだけですので、本記事では割愛します。 Microsoft Fabric サブスクリプションを購入する Microsoft Fabric ワークスペース の作成 ワークスペース とは、 Microsoft Fabric で作成したさまざまなコレクション(レイクハウス、データフロー、BI レポート etc...)をまとめて管理するものです。 Microsoft Fabric で作業を開始する際には、内容を問わずはじめに実施する工程となります。 手順としては ワークスペース 名を入力して保存するだけですので、こちらについても詳細は割愛します。 厳密には ワークスペース に対する権限設定などもありますが、今回は検証用 ワークスペース のため実施していません。 ワークスペースの作成 レイクハウスの作成 レイクハウスは、大容量のデータを蓄積するデータレイクとデータの取り込みや分析に特化したデータウェアハウスを統合したものです。 一般的に構造化/半構造化/非構造化データを 1 箇所で管理・分析することが可能で、 Microsoft Fabric では多様なデータソースからのデータ格納とデータに対するさまざまな変換・統合処理や SQL や Spark エンジンを利用した分析をサポートしています。 また Microsoft Fabric におけるレイクハウスの特徴として、格納されたデータを自動で検出し、分析に適した形式でテーブルとして登録する機能も用意されています。 作成自体は非常に簡単で、 Microsoft Fabric ワークスペース の[+ 新規]より[レイクハウス]を選択し任意の名前を設定して完了です。 勤怠データの格納 検証に必要なデータを作成したレイクハウスへ格納します。 対象となるのは、私の 2023 年 6 月分の勤怠実績を記録した Excel データ(ファイル名:Attendance_202306.xlsx)です。 作成直後のレイクハウスには 構造化データが管理される Tables とデータレイクのようにさまざまなソースから連携された生データが管理される Files というセクションが用意されています。 データ格納をする際はデータフローやパイプラインを利用する方法や、One Lake のショートカット機能などさまざまな選択肢がありますが、今回はシンプルに手動でアップロードしました。 アップロードが完了すると データ形式 によってはプレビュー表示が可能ですが、2023 年 7 月時点で Excel データはサポート外のようです。 ノートブックによるデータの整形 生データの時点である程度構造化されていればよいのですが、検証で扱う勤怠データは分析用にフォーマットされたものではないため、 変換や統合処理の前にデータを整形する必要があります。 そこで、今回は Spark エンジンが接続されたノートブックによるデータの整形を実施しました。 ほかの選択肢としてデータフロー(Power Query)も検討しましたが、そちらはこの後の作業であるデータの変換・加工処理で利用しています。 ノートブックはレイクハウス UI の[ノートブックを開く]より作成できます。 ノートブックの作成と同時に Spark エンジンも用意されるため、ユーザーは言語を選択して処理を記述していくだけです。 私は Python を選択しましたが、その他に Scala / C# /R/Spark SQL がサポートされています。 データ整形の内容としては、 Excel 形式の勤怠データをより必要なデータを抽出し、 Python のデータフレームを利用してデータを再配置しています。 抽出したデータは下記の通りです。 日付 勤務区分(出勤/法定休日/有給など) 勤務形態(出社/テレワーク/顧客先など) 勤務開始時間 勤務終了時間 勤務時間 休憩時間 また、整形後のデータはレイクハウス内でプレビュー表示させたかったので、 CSV 形式のファイルとして Files セクションに格納しています。 整形後のデータは下記の通りです(ファイル名:Attendance. csv )。 データフローによるデータの変換・加工 整形したデータに対して、変換・加工処理を実施し分析の準備を行います。 前述したノートブックでまとめて実施してもよかったのですが、他の機能も触ってみたいという思いでデータフローによる作業としました。 データフローは Microsoft Fabric の UI より Data Factory を選択し、[Dataflow Gen2(プレビュー)]より作成できます。 作成後はレイクハウスに格納されている整形後の勤怠データ(Attendance. csv )を読み込み、必要な処理を実施します。 今回実施した主な処理内容は下記の通りです。 勤務開始/終了時間のデータ型を文字列型へ変更する 定時を表現する列を追加する 勤務時間と定時を計算し、残業時間を表現する列を追加する 勤務区分に休日出勤が含まれる場合は、勤務時間を残業時間として追加する 実際のデータフロー画面は下画像のようになります。 ノートブックと異なり、コード(Power Query)を記述できなくとも GUI 上の操作で基本的なデータの変換・加工が可能です。 もちろん、必要に応じて直接 Power Query を書くこともできます。 また、処理のサマリがダイアグラムビュー(画像内の青枠部分)に記録され処理結果は画像下部にプレビューされるので、処理に対する結果を即時把握できます。 データ変換が完了したら、画像右下の[データの同期先]よりレイクハウスへの同期設定を行うことで、分析用に最適化された形式で Tables セクションへ格納されます。 また、その際の スキーマ 定義やデータ構造の作成などはすべて自動で実施されます。 データフローからレイクハウスへの同期が完了すると、Tables セクションでテーブルの内容をプレビューできます。 今回は変換後のデータであることを明示するために、attendance_transform というテーブル名としました。 SQL エンドポイントによる分析 データフローから同期されたテーブルに対して分析を実施します。 SQL エンドポイントと呼ばれる機能を利用することで、レイクハウス内のテーブルに対する SQL クエリ発行やリレーションシップの構成が可能です。 レイクハウスの UI より、 SQL エンドポイントへ遷移します。 SQL エンドポイントでは 3 つのペインが用意されており、それぞれ下記の作業が可能です。 データ: スキーマ 定義に含まれるテー ブルデー タの確認や SQL クエリの管理 クエリ: T-SQL クエリの発行 モデル:テーブル間のリレーションシップ構成や Power BI におけるメジャーの設定が可能 今回はクエリとモデルのペインにて、それぞれ分析作業を実施しました。 主な分析内容は下記の通りです。 SQL クエリを利用した分析 勤務時間、残業時間、勤務区分、勤務形態の集計 勤務形態における勤務状況の傾向分析 1 か月間における勤務状況の傾向分析 モデル 勤務日における出社/テレワークの割合を算出 今回は 1 か月分の勤怠データのみであったためそこまで複雑な分析ではありませんが、関連データの追加やデータ期間を拡大することでさらに充実した分析が可能です。 Power BI レポートの作成 最終工程として、分析結果を Power BI で可視化します。 SQL エンドポイント UI の[新しいレポート]より、Power BI レポートの作成画面へ遷移します。 分析結果を反映した Power BI レポートは下画像の通りです。 レポート内の各ビジュアルで分析結果を可視化しています。 勤務時間と残業時間および勤務日に対するテレワークの割合をカード上にテキスト形式で表示 1 か月間における勤務時間の推移と定時に対する残業時間の推移を棒グラフで表示 勤務時間が長い日や期間を表形式で表示 勤務形態(出社 or テレワーク)の違いで平均勤務時間にどのような影響があるか散布図で表示 サービス検証結果 勤怠データの分析シナリオを通じた Microsoft Fabric の検証結果をまとめます。 検証項目は下記の通りです(再掲)。 Microsoft Fabric のみでデータ分析を 一気通貫 で実施できるか データ分析の経験があまりないユーザーをサポートする機能が現時点でどの程度実装されているか Microsoft Fabric の操作感(Power BI や Data Factory など統合されたサービスがこれまで通り利用できるか) その他利用時の注意点 一気通貫 のデータ分析 データの格納から可視化まですべて Microsoft Fabric で完結できました。 従来のように分析ごとに用途に応じて PaaS を組み合わせたり、インフラを確保する必要がなく、統一された UI の元で作業ができることはメリットであると言えます。 今回はローカルデータを中心とした分析シナリオでしたが、 機械学習 やリアルタイム分析など幅広いシナリオに対応できるサービスであるという印象です。 一方で、分析シナリオによっては不要な機能や コンポーネント があることも事実です。 Microsoft Fabric の利用が最適となる分析シナリオは何か、という点については、金銭面や学習コストの観点も含めて今後のアップデートを追っていく必要があります。 データ分析の経験が浅いユーザーへのサポート 今回の検証で触れた機能の中では、データフローの GUI によるデータ変換処理は比較的ユーザーに易しい設計という印象を受けました。 ですが、それ以外の部分については継続的な強化が必要と感じています。 Microsoft Fabric は「誰もがデータを管理および分析してアクションにつながる インサイト を得られる」というコンセプトを掲げていますが、現状の機能構成はデータ変換や加工・分析等の処理設計をユーザーの知識に依存しています。 Microsoft Fabric - データ分析 今回の検証においては、ノートブックを利用したデータの前処理や SQL エンドポイントによる分析で必要となるコードやクエリの作成は、未経験のユーザーには難易度が高い作業です。 誰もがデータを管理および分析するためにはこのような作業に対するサポート機能が必要で、具体的には下記のような内容を期待します。 Copilot 機能による各種コードやクエリの自動生成 One Lake のショートカット機能強化(連携先の拡張など) ドキュメントや チュートリアル の充実 Microsoft によると、Azure OpenAI と連携した Copilot 機能が近日公開予定となっておりますので、そちらを注視したいと思います。 各機能の操作感 主観になりますが、既存の PaaS や SaaS から統合された機能については違和感なく操作できました。 ノートブックによる対話形式でのデータ操作やデータフロー周辺の操作は、UI 自体が統合元の Microsoft 製品と大きく変わらないため、それらの経験がある方であれば抵抗なく利用できるかと思います。 まとめ 本記事では、 Microsoft Fabric の概要とサービス検証として実施した勤怠データ分析の内容についてご紹介しました。 Microsoft Fabric は発表されて間もないソリューションであり、ロードマップでは今後も多くのアップデートが予定されておりますので、引き続き注視したいと思います。 私たちは一緒に働いてくれる仲間を募集しています! クラウドアーキテクト 執筆: @tamura.kohei 、レビュー: @yamada.y ( Shodo で執筆されました )
こんにちは、X(クロス) イノベーション 本部 クラウド イノベーション センターの田村です。 2023 年 5 月の Microsoft Build にて統合分析プラットフォーム Microsoft Fabric が発表されました。 Microsoft Fabric は現在プレビュー中ですが、既存のサービスにはない機能追加や多くのアップデートが予定されており、 Microsoft のデータ領域ビジネスにおいて今後注目すべきサービスです。 本記事では、 Microsoft Fabric の概要とサービス検証として実施した勤怠データ分析の内容をご紹介します。 Microsoft Fabric の概要 One Lake Synapse Data Engineering Data Factory Power BI サービス検証:勤怠データの分析 概要 検証シナリオ:勤怠データ分析 検証アーキテクチャ Microsoft Fabric サブスクリプションの購入 Microsoft Fabric ワークスペースの作成 レイクハウスの作成 勤怠データの格納 ノートブックによるデータの整形 データフローによるデータの変換・加工 SQL エンドポイントによる分析 Power BI レポートの作成 サービス検証結果 一気通貫のデータ分析 データ分析の経験が浅いユーザーへのサポート 各機能の操作感 まとめ Microsoft Fabric の概要 Microsoft Fabric は、データ分析に必要な要素を 1 つの統合環境で提供する SaaS 形式のソリューションです。 従来 Microsoft の クラウド サービスでデータ分析をする場合は、Azure Data Factory や Azure Synapse Analytics といった PaaS を組み合わせた アーキテクチャ を設計する手法が一般的でした。 Microsoft Fabric では、データの蓄積/加工/変換/分析/可視化を単一の基盤上で実現できるため、ユーザーは統一された UI 上でシームレスな分析を実施できると謳われています。 これにより、ユーザーは 1 つのサービス内でデータ分析やデータ管理に関する作業をすべて完結でき、分析リソースのサイジングといった管理負担の軽減が期待できます。 Microsoft Fabric の主要な構成要素について述べます。 記事のボリュームを考慮し、今回は技術検証にて扱った要素について簡潔に説明しますので、他にも興味がある方は こちら のリンクをご参照ください。 (出典) Microsoft Fabric とは One Lake Microsoft Fabric の環境に 1 つ作成される SaaS 形式のデータレイクです。 Microsoft Fabric の各機能で利用されるデータを一元的に管理する基盤であり、ショートカットと呼ばれる機能を利用するとさまざまなサービスから手軽にデータを移動できます。 現時点では Microsoft Fabric 内部/ Amazon S3 /Azure Data Lake Storage との連携が実装されており、今後も拡大されていくと思われます。 OneLake のショートカット Synapse Data Engineering レイクハウスと呼ばれる構造化/非構造化データを管理・分析できるデータストアに格納されたデータに対する処理を行う機能です。 具体的には Apache Spark クラスタ による分析やノートブックによる対話的な分析( Python /R/ Scala )、データパイプラインによるデータの移動/変換/結合等が実施できます。 Microsoft Fabric のデータ エンジニアリングとは Data Factory 既存のデータ統合サービスである Azure Data Factory と、 Excel や Power BI 等に搭載されている Power Query を組み合わせたデータ統合機能です。 さまざまなデータソースからデータを取り込み、柔軟な処理が可能です。 Microsoft Fabric の Data Factory とは Power BI Microsoft Power Platform にて提供されている BI ツールです。 Microsoft Fabric に可視化 コンポーネント として統合されています。 Power BI とは サービス検証:勤怠データの分析 概要 7 月上旬あたりで Microsoft Fabric のキャッチアップと チュートリアル の実施がある程度進んだので、手持ちのデータを使用して Microsoft Fabric での分析に着手しました。 分析作業を通じて、下記の項目について検証することを目的とします。 Microsoft Fabric のみでデータ分析を 一気通貫 で実施できるか データ分析の経験があまりないユーザーをサポートする機能が現時点でどの程度実装されているか Microsoft Fabric の操作感(Power BI や Data Factory など統合されたサービスがこれまで通り利用できるか) 検証シナリオ:勤怠データ分析 ISID では日々の勤怠入力を自社ソリューションである POSITIVE で実施しています。 POSITIVE - 人事給与、勤怠システム ISID 社内で利用している POSITIVE では、入力した勤怠情報を Excel 形式でダウンロードできます。 そこで、自分の勤怠データが記録された Excel ファイルを Microsoft Fabric で分析し、可視化までの工程を 一気通貫 で検証してみることにしました。 検証 アーキテクチャ アーキテクチャ と検証手順は下記の通りです。 Microsoft Fabric サブスクリプション を購入する Microsoft Fabric ワークスペース を作成する Synapse Data Engineering にてデータストアとなるレイクハウスを作成する ローカルから Excel 形式の勤怠データをレイクハウスへ格納する ノートブック を作成し、 Excel 形式の勤怠データを整形して再度レイクハウスへ格納する 整形された勤怠データをデータフローにて変換・加工し、再度レイクハウスへ格納する レイクハウスから SQL エンドポイントを作成し分析を実施する 変換後のデータおよび分析結果を Power BI にて可視化する Microsoft Fabric サブスクリプション の購入 Microsoft 365 もしくは Azure 上から購入できます。 今回は Azure 上から購入しました。 手順は簡単かつドキュメントの内容をなぞるだけですので、本記事では割愛します。 Microsoft Fabric サブスクリプションを購入する Microsoft Fabric ワークスペース の作成 ワークスペース とは、 Microsoft Fabric で作成したさまざまなコレクション(レイクハウス、データフロー、BI レポート etc...)をまとめて管理するものです。 Microsoft Fabric で作業を開始する際には、内容を問わずはじめに実施する工程となります。 手順としては ワークスペース 名を入力して保存するだけですので、こちらについても詳細は割愛します。 厳密には ワークスペース に対する権限設定などもありますが、今回は検証用 ワークスペース のため実施していません。 ワークスペースの作成 レイクハウスの作成 レイクハウスは、大容量のデータを蓄積するデータレイクとデータの取り込みや分析に特化したデータウェアハウスを統合したものです。 一般的に構造化/半構造化/非構造化データを 1 箇所で管理・分析することが可能で、 Microsoft Fabric では多様なデータソースからのデータ格納とデータに対するさまざまな変換・統合処理や SQL や Spark エンジンを利用した分析をサポートしています。 また Microsoft Fabric におけるレイクハウスの特徴として、格納されたデータを自動で検出し、分析に適した形式でテーブルとして登録する機能も用意されています。 作成自体は非常に簡単で、 Microsoft Fabric ワークスペース の[+ 新規]より[レイクハウス]を選択し任意の名前を設定して完了です。 勤怠データの格納 検証に必要なデータを作成したレイクハウスへ格納します。 対象となるのは、私の 2023 年 6 月分の勤怠実績を記録した Excel データ(ファイル名:Attendance_202306.xlsx)です。 作成直後のレイクハウスには 構造化データが管理される Tables とデータレイクのようにさまざまなソースから連携された生データが管理される Files というセクションが用意されています。 データ格納をする際はデータフローやパイプラインを利用する方法や、One Lake のショートカット機能などさまざまな選択肢がありますが、今回はシンプルに手動でアップロードしました。 アップロードが完了すると データ形式 によってはプレビュー表示が可能ですが、2023 年 7 月時点で Excel データはサポート外のようです。 ノートブックによるデータの整形 生データの時点である程度構造化されていればよいのですが、検証で扱う勤怠データは分析用にフォーマットされたものではないため、 変換や統合処理の前にデータを整形する必要があります。 そこで、今回は Spark エンジンが接続されたノートブックによるデータの整形を実施しました。 ほかの選択肢としてデータフロー(Power Query)も検討しましたが、そちらはこの後の作業であるデータの変換・加工処理で利用しています。 ノートブックはレイクハウス UI の[ノートブックを開く]より作成できます。 ノートブックの作成と同時に Spark エンジンも用意されるため、ユーザーは言語を選択して処理を記述していくだけです。 私は Python を選択しましたが、その他に Scala / C# /R/Spark SQL がサポートされています。 データ整形の内容としては、 Excel 形式の勤怠データをより必要なデータを抽出し、 Python のデータフレームを利用してデータを再配置しています。 抽出したデータは下記の通りです。 日付 勤務区分(出勤/法定休日/有給など) 勤務形態(出社/テレワーク/顧客先など) 勤務開始時間 勤務終了時間 勤務時間 休憩時間 また、整形後のデータはレイクハウス内でプレビュー表示させたかったので、 CSV 形式のファイルとして Files セクションに格納しています。 整形後のデータは下記の通りです(ファイル名:Attendance. csv )。 データフローによるデータの変換・加工 整形したデータに対して、変換・加工処理を実施し分析の準備を行います。 前述したノートブックでまとめて実施してもよかったのですが、他の機能も触ってみたいという思いでデータフローによる作業としました。 データフローは Microsoft Fabric の UI より Data Factory を選択し、[Dataflow Gen2(プレビュー)]より作成できます。 作成後はレイクハウスに格納されている整形後の勤怠データ(Attendance. csv )を読み込み、必要な処理を実施します。 今回実施した主な処理内容は下記の通りです。 勤務開始/終了時間のデータ型を文字列型へ変更する 定時を表現する列を追加する 勤務時間と定時を計算し、残業時間を表現する列を追加する 勤務区分に休日出勤が含まれる場合は、勤務時間を残業時間として追加する 実際のデータフロー画面は下画像のようになります。 ノートブックと異なり、コード(Power Query)を記述できなくとも GUI 上の操作で基本的なデータの変換・加工が可能です。 もちろん、必要に応じて直接 Power Query を書くこともできます。 また、処理のサマリがダイアグラムビュー(画像内の青枠部分)に記録され処理結果は画像下部にプレビューされるので、処理に対する結果を即時把握できます。 データ変換が完了したら、画像右下の[データの同期先]よりレイクハウスへの同期設定を行うことで、分析用に最適化された形式で Tables セクションへ格納されます。 また、その際の スキーマ 定義やデータ構造の作成などはすべて自動で実施されます。 データフローからレイクハウスへの同期が完了すると、Tables セクションでテーブルの内容をプレビューできます。 今回は変換後のデータであることを明示するために、attendance_transform というテーブル名としました。 SQL エンドポイントによる分析 データフローから同期されたテーブルに対して分析を実施します。 SQL エンドポイントと呼ばれる機能を利用することで、レイクハウス内のテーブルに対する SQL クエリ発行やリレーションシップの構成が可能です。 レイクハウスの UI より、 SQL エンドポイントへ遷移します。 SQL エンドポイントでは 3 つのペインが用意されており、それぞれ下記の作業が可能です。 データ: スキーマ 定義に含まれるテー ブルデー タの確認や SQL クエリの管理 クエリ: T-SQL クエリの発行 モデル:テーブル間のリレーションシップ構成や Power BI におけるメジャーの設定が可能 今回はクエリとモデルのペインにて、それぞれ分析作業を実施しました。 主な分析内容は下記の通りです。 SQL クエリを利用した分析 勤務時間、残業時間、勤務区分、勤務形態の集計 勤務形態における勤務状況の傾向分析 1 か月間における勤務状況の傾向分析 モデル 勤務日における出社/テレワークの割合を算出 今回は 1 か月分の勤怠データのみであったためそこまで複雑な分析ではありませんが、関連データの追加やデータ期間を拡大することでさらに充実した分析が可能です。 Power BI レポートの作成 最終工程として、分析結果を Power BI で可視化します。 SQL エンドポイント UI の[新しいレポート]より、Power BI レポートの作成画面へ遷移します。 分析結果を反映した Power BI レポートは下画像の通りです。 レポート内の各ビジュアルで分析結果を可視化しています。 勤務時間と残業時間および勤務日に対するテレワークの割合をカード上にテキスト形式で表示 1 か月間における勤務時間の推移と定時に対する残業時間の推移を棒グラフで表示 勤務時間が長い日や期間を表形式で表示 勤務形態(出社 or テレワーク)の違いで平均勤務時間にどのような影響があるか散布図で表示 サービス検証結果 勤怠データの分析シナリオを通じた Microsoft Fabric の検証結果をまとめます。 検証項目は下記の通りです(再掲)。 Microsoft Fabric のみでデータ分析を 一気通貫 で実施できるか データ分析の経験があまりないユーザーをサポートする機能が現時点でどの程度実装されているか Microsoft Fabric の操作感(Power BI や Data Factory など統合されたサービスがこれまで通り利用できるか) その他利用時の注意点 一気通貫 のデータ分析 データの格納から可視化まですべて Microsoft Fabric で完結できました。 従来のように分析ごとに用途に応じて PaaS を組み合わせたり、インフラを確保する必要がなく、統一された UI の元で作業ができることはメリットであると言えます。 今回はローカルデータを中心とした分析シナリオでしたが、 機械学習 やリアルタイム分析など幅広いシナリオに対応できるサービスであるという印象です。 一方で、分析シナリオによっては不要な機能や コンポーネント があることも事実です。 Microsoft Fabric の利用が最適となる分析シナリオは何か、という点については、金銭面や学習コストの観点も含めて今後のアップデートを追っていく必要があります。 データ分析の経験が浅いユーザーへのサポート 今回の検証で触れた機能の中では、データフローの GUI によるデータ変換処理は比較的ユーザーに易しい設計という印象を受けました。 ですが、それ以外の部分については継続的な強化が必要と感じています。 Microsoft Fabric は「誰もがデータを管理および分析してアクションにつながる インサイト を得られる」というコンセプトを掲げていますが、現状の機能構成はデータ変換や加工・分析等の処理設計をユーザーの知識に依存しています。 Microsoft Fabric - データ分析 今回の検証においては、ノートブックを利用したデータの前処理や SQL エンドポイントによる分析で必要となるコードやクエリの作成は、未経験のユーザーには難易度が高い作業です。 誰もがデータを管理および分析するためにはこのような作業に対するサポート機能が必要で、具体的には下記のような内容を期待します。 Copilot 機能による各種コードやクエリの自動生成 One Lake のショートカット機能強化(連携先の拡張など) ドキュメントや チュートリアル の充実 Microsoft によると、Azure OpenAI と連携した Copilot 機能が近日公開予定となっておりますので、そちらを注視したいと思います。 各機能の操作感 主観になりますが、既存の PaaS や SaaS から統合された機能については違和感なく操作できました。 ノートブックによる対話形式でのデータ操作やデータフロー周辺の操作は、UI 自体が統合元の Microsoft 製品と大きく変わらないため、それらの経験がある方であれば抵抗なく利用できるかと思います。 まとめ 本記事では、 Microsoft Fabric の概要とサービス検証として実施した勤怠データ分析の内容についてご紹介しました。 Microsoft Fabric は発表されて間もないソリューションであり、ロードマップでは今後も多くのアップデートが予定されておりますので、引き続き注視したいと思います。 私たちは一緒に働いてくれる仲間を募集しています! クラウドアーキテクト 執筆: @tamura.kohei 、レビュー: @yamada.y ( Shodo で執筆されました )
こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター セキュリティグループの耿です。 AWS のマネジメントコンソールへのユーザー認証ではMFAの利用が一般的になってきましたが、アクセスキー利用時にMFAを必須とするには少しコツがいるので、この記事でまとめてみます。 ちなみに Security Hub のコン トロール IAM.19 「すべての IAM ユーザーに対して MFA を有効にする必要があります」を満たすには、マネジメントコンソールのユーザーだけではなく、アクセスキーで利用するIAMユーザーに対してもMFAの設定をしなければなりません。 前提:IAMユーザーの状態 通常通りポリシーをつけても、MFA不要でアクセスできてしまう MFAを必須にする方法1:ユーザーポリシーに条件を追加する MFAを必須にする方法2:スイッチロールを利用し、コードの取得を簡単にする Tips:OSSツールAWSumeで簡単にスイッチロール まとめ 前提:IAMユーザーの状態 IAMユーザーを作成し、MFAを設定した状態を前提として、この後の話を進めます。(IAMユーザー・アクセスキーを配布するケースは事前にMFAを設定しておくことはできませんが、この記事では割愛します。) このユーザーに対してアクセスキーを発行しておきます。 ~/.aws/credentials に test-user というプロファイル名で、アクセスキーとシークレットアクセスキーを保存しておきます。 [test-user] aws_access_key_id = <アクセスキーID> aws_secret_access_key = <シークレットアクセスキー> region = ap-northeast-1 通常通りポリシーをつけても、MFA不要でアクセスできてしまう このユーザーに対して、以下のようなポリシーを付けてみます。 この記事では一貫して ec2:DescribeRegions を例としていますが、一般的なリソースアクセスの許可に適宜置き換えて考えてください。 { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Allow ", " Action ": " ec2:DescribeRegions ", " Resource ": " * " } ] } このユーザーを利用し DescribeRegions API をコールすると、MFAを設定しているにも関わらず、MFAコードを入力しなくてもアクセスが成功してしまいました。 Security Hub のIAM.19コン トロール をクリアするだけであれば、このようにMFAを有効にするだけで良いのですが、それだとMFAを利用しなくても権限を使用できてしまいます。せっかくMFAを有効にするのですから、MFAを利用しないと API アクセスできないようにきっちり設定したいですね。 MFAを必須にする方法1:ユーザーポリシーに条件を追加する MFAの利用を必須とする方法の1つ目として、IAMユーザーのポリシーを次のように書き換えます。 Condition 句を追加し、 aws:MultiFactorAuthPresent を利用してMFAを行ったことを条件とします。 { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Allow ", " Action ": " ec2:DescribeRegions ", " Resource ": " * ", " Condition ": { " Bool ": { " aws:MultiFactorAuthPresent ": " true " } } } ] } 再び DescribeRegions API をコールすると、MFAを利用していないので今度はちゃんとエラーとなります。 MFAを利用するためには STS の GetSessionToken API を呼び出し、一時的な認証情報を取得します。 このとき --serial-number オプションには設定した MFA デ バイス の識別子(ARN)を、 --token-code にはMFA トーク ンを指定します。 発行された一時的な認証情報を使用するために ~/.aws/credentials に新しくプロファイルを作成しても良いですが、ここではシンプルに 環境変数 に設定します。 ( 環境変数 に設定された AWS 認証情報は、 認証情報ファイルよりも優先されます 。) そして 環境変数 に設定した認証情報を利用してもらうために、 AWS CLI の --profile オプションをつけずに DescribeRegions API をコールすると、再びアクセスできるようになりました。 以上の方法でMFAの利用を必須にはできましたが、 GetSessionToken API を呼び出して一時的な認証情報を取得する 取得した認証情報を利用するように設定する といった手間がかかってしまいます。 MFAを必須にする方法2:スイッチロールを利用し、コードの取得を簡単にする 上で説明した手間を省くために、スイッチロールを利用してMFAを強制する方法があります。 IAMロールを1つ作成します。 信頼ポリシー では同じアカウントからの AssumeRole を許可すると同時に、 aws:MultiFactorAuthPresent を利用してMFAを行ったことを条件とします。 { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Allow ", " Principal ": { " AWS ": " arn:aws:iam::<アカウントID>:root " } , " Action ": " sts:AssumeRole ", " Condition ": { " Bool ": { " aws:MultiFactorAuthPresent ": " true " } } } ] } このロールの 許可ポリシー は、上で作成したユーザーと同じとします。 { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Allow ", " Action ": " ec2:DescribeRegions ", " Resource ": " * " } ] } そして元のIAMユーザーの許可ポリシーから DescribeRegions の許可を除外し、作成した IAMロールへの AssumeRole のみを許可します。( aws:MultiFactorAuthPresent の条件も不要です。) { " Version ": " 2012-10-17 ", " Statement ": [ { " Action ": " sts:AssumeRole ", " Resource ": " arn:aws:iam::<アカウントID>:role/<ロール名> ", " Effect ": " Allow " } ] } スイッチロールを簡単にするために、 ~/.aws/config に以下のように追記します。 [profile test-user] の部分は ~/.aws/credentials に記載したプロファイル名 test-user と名前を合わせる必要があります。 [profile test-user] region = ap-northeast-1 [profile my-test-role] region = ap-northeast-1 source_profile = test-user role_arn = arn:aws:iam::<アカウントID>:role/<スイッチロール用のロール名> mfa_serial = arn:aws:iam::<アカウントID>:mfa/<デバイス名> DescribeRegions API を呼び出す時に、スイッチロールした先のプロファイル名(ここでは my-test-role )を指定すると、そのままMFAコードを尋ねられます。入力すると API 実行が成功します。 この方法の良いところは、実行したい API をそのままコールすればよく、デ バイス の識別子(ARN)も ~/.aws/config にあらかじめ保存してあるので毎回入力する必要がないことです。 Tips: OSS ツールAWSumeで簡単にスイッチロール MFAを強制するスイッチロールを簡単にするツールとして、 OSS の AWSume もおすすめです。 必要なロール・ポリシー構成は「方法2」と同じで、ローカルで AWS SDK を利用する時にも一時的なクレデンシャル情報を簡単なコマンドでセットアップできたりするので便利です。 セットアップ 後、 awsume <プロファイル名> で一時的な認証情報を取得でき、MFAが必要な場合はここで尋ねられます。そのあとは、一時的な認証情報を利用して API コールできるようになります。 まとめ これまで説明したパターンを図にまとめてみました。 IAMアクセスキーは漏えいしないように細心の注意を払うことが大前提ですが、万が一漏えいしたとしてもすぐに使えないように、MFAで二重のガードをしておきましょう。 私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。 セキュリティエンジニア 執筆: @kou.kinyo 、レビュー: 寺山 輝 (@terayama.akira) ( Shodo で執筆されました )
こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター セキュリティグループの耿です。 AWS のマネジメントコンソールへのユーザー認証ではMFAの利用が一般的になってきましたが、アクセスキー利用時にMFAを必須とするには少しコツがいるので、この記事でまとめてみます。 ちなみに Security Hub のコン トロール IAM.19 「すべての IAM ユーザーに対して MFA を有効にする必要があります」を満たすには、マネジメントコンソールのユーザーだけではなく、アクセスキーで利用するIAMユーザーに対してもMFAの設定をしなければなりません。 前提:IAMユーザーの状態 通常通りポリシーをつけても、MFA不要でアクセスできてしまう MFAを必須にする方法1:ユーザーポリシーに条件を追加する MFAを必須にする方法2:スイッチロールを利用し、コードの取得を簡単にする Tips:OSSツールAWSumeで簡単にスイッチロール まとめ 前提:IAMユーザーの状態 IAMユーザーを作成し、MFAを設定した状態を前提として、この後の話を進めます。(IAMユーザー・アクセスキーを配布するケースは事前にMFAを設定しておくことはできませんが、この記事では割愛します。) このユーザーに対してアクセスキーを発行しておきます。 ~/.aws/credentials に test-user というプロファイル名で、アクセスキーとシークレットアクセスキーを保存しておきます。 [test-user] aws_access_key_id = <アクセスキーID> aws_secret_access_key = <シークレットアクセスキー> region = ap-northeast-1 通常通りポリシーをつけても、MFA不要でアクセスできてしまう このユーザーに対して、以下のようなポリシーを付けてみます。 この記事では一貫して ec2:DescribeRegions を例としていますが、一般的なリソースアクセスの許可に適宜置き換えて考えてください。 { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Allow ", " Action ": " ec2:DescribeRegions ", " Resource ": " * " } ] } このユーザーを利用し DescribeRegions API をコールすると、MFAを設定しているにも関わらず、MFAコードを入力しなくてもアクセスが成功してしまいました。 Security Hub のIAM.19コン トロール をクリアするだけであれば、このようにMFAを有効にするだけで良いのですが、それだとMFAを利用しなくても権限を使用できてしまいます。せっかくMFAを有効にするのですから、MFAを利用しないと API アクセスできないようにきっちり設定したいですね。 MFAを必須にする方法1:ユーザーポリシーに条件を追加する MFAの利用を必須とする方法の1つ目として、IAMユーザーのポリシーを次のように書き換えます。 Condition 句を追加し、 aws:MultiFactorAuthPresent を利用してMFAを行ったことを条件とします。 { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Allow ", " Action ": " ec2:DescribeRegions ", " Resource ": " * ", " Condition ": { " Bool ": { " aws:MultiFactorAuthPresent ": " true " } } } ] } 再び DescribeRegions API をコールすると、MFAを利用していないので今度はちゃんとエラーとなります。 MFAを利用するためには STS の GetSessionToken API を呼び出し、一時的な認証情報を取得します。 このとき --serial-number オプションには設定した MFA デ バイス の識別子(ARN)を、 --token-code にはMFA トーク ンを指定します。 発行された一時的な認証情報を使用するために ~/.aws/credentials に新しくプロファイルを作成しても良いですが、ここではシンプルに 環境変数 に設定します。 ( 環境変数 に設定された AWS 認証情報は、 認証情報ファイルよりも優先されます 。) そして 環境変数 に設定した認証情報を利用してもらうために、 AWS CLI の --profile オプションをつけずに DescribeRegions API をコールすると、再びアクセスできるようになりました。 以上の方法でMFAの利用を必須にはできましたが、 GetSessionToken API を呼び出して一時的な認証情報を取得する 取得した認証情報を利用するように設定する といった手間がかかってしまいます。 MFAを必須にする方法2:スイッチロールを利用し、コードの取得を簡単にする 上で説明した手間を省くために、スイッチロールを利用してMFAを強制する方法があります。 IAMロールを1つ作成します。 信頼ポリシー では同じアカウントからの AssumeRole を許可すると同時に、 aws:MultiFactorAuthPresent を利用してMFAを行ったことを条件とします。 { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Allow ", " Principal ": { " AWS ": " arn:aws:iam::<アカウントID>:root " } , " Action ": " sts:AssumeRole ", " Condition ": { " Bool ": { " aws:MultiFactorAuthPresent ": " true " } } } ] } このロールの 許可ポリシー は、上で作成したユーザーと同じとします。 { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Allow ", " Action ": " ec2:DescribeRegions ", " Resource ": " * " } ] } そして元のIAMユーザーの許可ポリシーから DescribeRegions の許可を除外し、作成した IAMロールへの AssumeRole のみを許可します。( aws:MultiFactorAuthPresent の条件も不要です。) { " Version ": " 2012-10-17 ", " Statement ": [ { " Action ": " sts:AssumeRole ", " Resource ": " arn:aws:iam::<アカウントID>:role/<ロール名> ", " Effect ": " Allow " } ] } スイッチロールを簡単にするために、 ~/.aws/config に以下のように追記します。 [profile test-user] の部分は ~/.aws/credentials に記載したプロファイル名 test-user と名前を合わせる必要があります。 [profile test-user] region = ap-northeast-1 [profile my-test-role] region = ap-northeast-1 source_profile = test-user role_arn = arn:aws:iam::<アカウントID>:role/<スイッチロール用のロール名> mfa_serial = arn:aws:iam::<アカウントID>:mfa/<デバイス名> DescribeRegions API を呼び出す時に、スイッチロールした先のプロファイル名(ここでは my-test-role )を指定すると、そのままMFAコードを尋ねられます。入力すると API 実行が成功します。 この方法の良いところは、実行したい API をそのままコールすればよく、デ バイス の識別子(ARN)も ~/.aws/config にあらかじめ保存してあるので毎回入力する必要がないことです。 Tips: OSS ツールAWSumeで簡単にスイッチロール MFAを強制するスイッチロールを簡単にするツールとして、 OSS の AWSume もおすすめです。 必要なロール・ポリシー構成は「方法2」と同じで、ローカルで AWS SDK を利用する時にも一時的なクレデンシャル情報を簡単なコマンドでセットアップできたりするので便利です。 セットアップ 後、 awsume <プロファイル名> で一時的な認証情報を取得でき、MFAが必要な場合はここで尋ねられます。そのあとは、一時的な認証情報を利用して API コールできるようになります。 まとめ これまで説明したパターンを図にまとめてみました。 IAMアクセスキーは漏えいしないように細心の注意を払うことが大前提ですが、万が一漏えいしたとしてもすぐに使えないように、MFAで二重のガードをしておきましょう。 私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。 セキュリティエンジニア 執筆: @kou.kinyo 、レビュー: 寺山 輝 (@terayama.akira) ( Shodo で執筆されました )
こんにちは、金融ソリューション事業部の孫です。 前回の Part1 記事に続きまして Part2 では、OpenMatchとAgonesを使用して、柔軟性がありスケーラブルなゲームマッチングとゲームサーバー管理システムの構築方法を詳しく説明しました。 この記事(Part3)では、UnrealEngineを利用して、オンラインマルチプレーヤーゲームのデモを完成させます。 さらに、 Part2 で開発したマッチメイキングサービスをUEクライアント(UnrealEngine GameClient)に統合します。 全体 アーキテクチャ は以下の図の通りです。 UEクライアントの実装について、前の 記事 で作成したデモを基にさらに拡張します。 プレーヤーマッチング機能とサーバー管理機能を追加することで、この度のクライアントの実現を目指します。 実施手順 1.前記事のデモにマッチング機能の追加 マッチング機能のボタンの追加 Frontend APIの呼びだす機能実装 2. Agones SDKを呼び出してGameServerの終了機能の実装 ユーザー数の管理機能の追加 Agones SDKを利用してGameServerの終了実装 3.パッケージ化したUEサーバーでAgones Fleetの作成 4.動作確認 終わりに 参考 実施手順 以下の順序でシステムを構築します: 前記事 のデモにマッチング機能の追加 Agones SDK を呼び出してGameServerの終了機能の実装 パッケージ化したUEサーバーでAgones Fleetの作成 動作確認 1. 前記事 のデモにマッチング機能の追加 この前の記事 UE5ネットワーク同期のC++実装例 では、プレーヤーが「Play」ボタンをクリックすると、UEゲームサーバーに接続してゲーム体験を開始する機能をすでに完成させています。 次に、マッチング機能のボタンを追加し、このボタンをクリックするとOpenMatchの Frontend API を呼び出してマッチング要求をリク エス トします。 マッチング機能のボタンの追加 以下の図のように、テキストエリア(Region入力用)とボタン(「Match」)をUnrealEngineのBluePrint Widget UIに配置します。 Frontend API の呼びだす機能実装 以下のモジュールをxxx.Build.csファイルに追加する 今回では、 Agones / HTTP / Json / JsonUtilities の4つのモジュールを使用します。 各モジュールは次のような役割を果たします。 Agones モジュールは Agones の SDK を使うために使用する HTTP モジュールは Frontend API の http リク エス トを送信するために使用する Json 、 JsonUtilities モジュールは http からの返却 Json データを解析するために使用する //xxx.Build.cs using UnrealBuildTool; //:(省略) PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "EnhancedInput", "Agones", "HTTP", "Json", "JsonUtilities" }); //:(省略) LoginHUDWidget.h を編集する まずは、配置したWidgetsをバインドすることから始めます。 ※注意:BluePrint内の Widget 名は LoginHUDWidget.h ファイル内の変数名と同じでなければならず、 meta = (BindWidget) タグを追加して Widget をバインドします それを完了したら、httpモジュールをincludeした上でコールバック関数を作成します。 //LoginHUDWidget.h // "Http.h"ヘッダーファイルの追加 #include "Http.h" UCLASS() class TEST_DESERVER_API ULoginHUDWidget : public UUserWidget { GENERATED_BODY() public: //:(省略) UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UEditableText* regionName; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UTextBlock* matchLabel; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UButton* matchBtn; private: FHttpModule* Http; void OnFetchGameServerResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); } LoginHUDWidget.cpp にマッチングボタンのクリックロジックを実装する //LoginHUDWidget.cpp // "Json.h", "JsonUtilities.h"ヘッダーファイルの追加 #include "Json.h" #include "JsonUtilities.h" // 構造関数内で関連コントロールとHttpモジュールを初期化します // MatchボタンにOnMatchmakingButtonClickedトリガー関数をバインドします void ULoginHUDWidget::NativePreConstruct() { Super::NativePreConstruct(); //:(省略) matchLabel->SetText(FText::FromString("Match")); FScriptDelegate MatchmakingDelegate; MatchmakingDelegate.BindUFunction(this, "OnMatchmakingButtonClicked"); matchBtn->OnClicked.Add(MatchmakingDelegate); //:(省略) Http = &FHttpModule::Get(); } // OnMatchmakingButtonClickedトリガー関数の処理ロジックを追加します void ULoginHUDWidget::OnMatchmakingButtonClicked() { statusLabel->SetText(FText::FromString("Start Matching!! Please wait")); TSharedRef<IHttpRequest, ESPMode::ThreadSafe> FetchGameServerHttpRequest = Http->CreateRequest(); FString frontendUrl = "127.0.0.1:8081/play/" + FString(regionName->GetText().ToString()); FetchGameServerHttpRequest->SetVerb("GET"); FetchGameServerHttpRequest->SetURL(frontendUrl); FetchGameServerHttpRequest->SetHeader("Content-Type", "application/json"); FetchGameServerHttpRequest->OnProcessRequestComplete().BindUObject(this, &ULoginHUDWidget::OnFetchGameServerResponse); FetchGameServerHttpRequest->ProcessRequest(); }; // Httpのコールバック関数の処理ロジックを追加します void ULoginHUDWidget::OnFetchGameServerResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { if (bWasSuccessful) { TSharedPtr<FJsonObject> JsonObject; TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString()); if (FJsonSerializer::Deserialize(Reader, JsonObject)) { FString IpAddress = JsonObject->GetStringField("ip"); FString Port = JsonObject->GetStringField("port"); FString LevelName = IpAddress + ":" + Port; statusLabel->SetText(FText::FromString(LevelName)); playBtn->SetVisibility(ESlateVisibility::Visible); } } } // OnPlayGameButtonClickedトリガー関数の処理ロジックを変更します // OpenMatchから返されたGameServerアドレスを接続ターゲットとして設定します void ULoginHUDWidget::OnPlayGameButtonClicked() { //:(省略) FString LevelName = statusLabel->GetText().ToString(); //:(省略) } 2. Agones SDK を呼び出してGameServerの終了機能の実装 すべてのプレイヤーがオフラインになった場合、AgonesSDKを呼び出して利用済のGameServerを削除した後、新しいGameServerを再作成します。 ユーザー数の管理機能の追加 UnrealEngine Gamemodeクラスにおいて PlayerNum 変数を追加して現在のプレーヤー数を保存します。 また、少なくとも1人のプレーヤーがこのGameServerに接続したことがあることを判断するため、 HasPlayerConnected のBool変数を追加します。 前の 記事 で、UnrealEngineによく使われるサーバー側の関数について既にいくつか紹介しました。 今回はその中の PostLogin 、 Logout 関数を用いることで、プレーヤー数の簡易的な統計データが取得します。 //xxxGameMode.h public: //:(省略) virtual void PostLogin(APlayerController* NewPlayer) override virtual void Logout(AController* Exiting) override private: //:(省略) int32 PlayerNum; bool HasPlayerConnected; //xxxGameMode.cpp // 構造関数で変数を初期化します xxxGameMode::xxxGameMode(){ //:(省略) int32 PlayerNum = 0; bool HasPlayerConnected = false; } void xxxGameMode::PostLogin(APlayerController* NewPlayer) { Super::PostLogin(NewPlayer); PlayerNum++; HasPlayerConnected = true; if (!HasPlayerConnected) { HasPlayerConnected = true; GetWorldTimerManager().SetTimer(CountDownPlayerNumHandle, this, &xxxGameMode::TickCount, 1.0f, true); } } void xxxGameMode::Logout(AController* Exiting) { Super::Logout(Exiting); PlayerNum--; } Agones SDK を利用してGameServerの終了実装 UnrealEngine Gamemodeクラスにおいて、Agones SDK のShutdown()関数を呼び出してGameServerがシャットダウンされます。 ※Agonesは一 定量 のGameServerを維持し、GameServerが閉じられると新たなGameServerの生成がトリガーされます。 //xxxGameMode.h public: //:(省略) virtual void Tick(float DeltaTime) override private: //:(省略) //Agones SDKの処理結果に対応するレスポンス関数 //成功処理後のレスポンス関数 void HandleShutdownSuccess(const FEmptyResponse& Response); //成功失敗時のレスポンス関数 void HandleShutdownError(const FAgonesError& Error); void TickCount(); //xxxGameMode.cpp void Atest_DEServerGameMode::HandleShutdownSuccess(const FEmptyResponse& Response) {   //デモのため、実際の処理を行いません UE_LOG(LogTemp, Log, TEXT("Game server successfully shutdown")); } void Atest_DEServerGameMode::HandleShutdownError(const FAgonesError& Error) { //デモのため、実際の処理を行いません UE_LOG(LogTemp, Error, TEXT("shutting down failed: %s"), *Error.ErrorMessage); } void xxxGameMode::TickCount() { Super::Tick(DeltaTime); if (HasPlayerConnected && PlayerNum <= 0) { FShutdownDelegate SuccessDelegate; SuccessDelegate.BindUFunction(this, FName("HandleShutdownSuccess")); FAgonesErrorDelegate ErrorDelegate; ErrorDelegate.BindUFunction(this, FName("HandleShutdownError")); GetWorldTimerManager().ClearTimer(CountDownPlayerNumHandle); AgonesSDK->Shutdown(SuccessDelegate, ErrorDelegate); } } 3.パッケージ化したUEサーバーでAgones Fleetの作成 UnrealEngine Editorを使用して、 Linux プラットフォーム向けのDedicated Serverをパッケージ化します。 以下の図のように、ターゲットプラットフォーム Linux -> Development -> Linux(server) を選択し、パッケージします。 ※ Windows OSではもしかするとターゲットプラットフォームの選択肢に Linux が存在しないかもしれません。 これは、 Linux プラットフォームのサポートが設定されていないためです。 具体的な設定方法については、 こちら を参照してください。 パッケージ化が完了したら、 Part2 で各モジュールをデプロイしたのと同じ手順で、EKSにGameServerをデプロイします。 ローカルにおいてDockerImageを コンパイル する 下記のDockerfileをパッケージ化の際に指定された保存先のルート ディレクト リに配置し、Dockerイメージ作成コマンドを実行します。 ## DockerFile ## 「AgonesOMServer」を実際のプロジェクト名に置き換えます FROM ubuntu:20.04 RUN apt-get update && apt-get install -y \ libxcursor1 \ libxrandr2 \ libxinerama1 \ libxi6 \ libgl1-mesa-glx \ && rm -rf /var/lib/apt/lists/* RUN addgroup --gid 1000 gameserver && \ adduser --gid 1000 --uid 1000 --shell /usr/sbin/nologin --home /home/gameserver --gecos "" --disabled-login --disabled-password gameserver COPY --chown=gameserver:gameserver ./LinuxServer /home/gameserver/LinuxServer RUN chmod -R 770 /home/gameserver/LinuxServer WORKDIR /home/gameserver/LinuxServer USER gameserver EXPOSE 7777/udp ENTRYPOINT ["/home/gameserver/LinuxServer/「AgonesOMServer」.sh"] ## Docker image build command $ docker build -t localimage/ue_server:0.1 . DockerImageを Amazon ECRにアップロードする Part2 と同様に、ue_serverという名前の Amazon ECRプライベー トリポジ トリを新規作成します。 次に、Dockerイメージを Amazon ECRにアップロードします。 $ docker tag localimage/ue_server:0.1 {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/ue_server:0.1 $ docker push {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/ue_server:0.1 Amazon EKSにおいてAgones Fleetを作成する ローカルでAgones Fleetの作成用のfleet. yaml ファイルを作成します。 ## fleet.yaml --- apiVersion: "agones.dev/v1" kind: Fleet metadata: name: fleet-ap-northeast-1 spec: replicas: 2 scheduling: Packed strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 25% template: metadata: namespace: meta-poc labels: region: ap-northeast-1 spec: players: initialCapacity: 4 ports: - name: default containerPort: 7654 health: initialDelaySeconds: 30 periodSeconds: 60 template: metadata: namespace: meta-poc labels: region: ap-northeast-1 spec: containers: - name: ue5-server image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/ue_server:0.1 resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "1Gi" cpu: "1" fleet. yaml ファイルを Amazon EKSに適用します。 $ kubectl apply -f fleet.yaml 4.動作確認 UnrealEngine Editorを使用して4つのクライアントを起動する 具体的な操作手順は、以下の図のとおり New Editor Window(PIE) -> Number Of Players: 4 -> Play Standalone を設定する 以下のコマンドでAgones GameServerのステータス監視を起動する $ watch kubectl get gs OpenMatch Frontend Serviceをローカルに マッピング する # OpenMatch Frontend サービスがローカルの8081ポートにマッピングされます kubectl port-forward services/frontend-ednpoint 8081:80 マッチング機能をテストする 確認ポイント: 4つのクライアントが同じGameServerアドレスにマッチングされていること GameServerのステータスがAllocatedになっていること DedicatedServerへの接続をテストする 確認ポイント: Playボタンをクリックした後、Dedicated Serverに成功したこと Characterの頭上に表示されているNickNameが、クライアントが入力したNickNameと同じであること AgonesによるGameServerのシャットダウンをテストする 確認ポイント: 全てのGameClientを閉じた後、以前Allocated状態だったサーバーが削除されること その代わりに新たにReady状態のGameServerが作成されること 終わりに これで、マッチング機能を備え、Agonesで Unreal Engine DedicatedServerをスケジューリングするオンライン マルチプレイヤー ゲームの作成が完了しました。 この一連の記事では、EKSを使って Kubernetes の特性を活用し、 Unreal Engine のDedicated Serverを管理する方法について深く探求しました。 その中で、マッチングシステムとしてOpenMatchを使用し、その強力な拡張性と設定性を活用してニーズに合ったマッチングルールをカスタマイズしました。同時に、Agonesを用いてゲームサーバーのスケジューリングを行い、効率的で安定したサーバー管理を実現しました。 次は、さらなるゲームに関連するインフラ設計のソリューションを見つけ出し、ゲーム体験をさらに向上させる方法を継続に探求します。 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ(Web3/メタバース/AI) 参考 https://docs.unrealengine.com/5.2/ja/linux-game-development-in-unreal-engine/ https://agones.dev/site/docs/reference/fleet/ 執筆: @chen.sun 、レビュー: @yamashita.yuki ( Shodo で執筆されました )
こんにちは、金融ソリューション事業部の孫です。 シリーズの最初の記事( 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 で執筆されました )
こんにちは!金融ソリューション事業部の孫です。 以前の 記事 では Unreal Engine Dedicated Serverの構築方法について紹介しました。 今回は続きの記事として、以下の3部で、 AWS が提供するEKSを使用してマッチメイキング機能を持つAgonesでGameServerを運用する環境の構築プロセスを説明します。 なお、EKSは Kubernetes の クラウド サービスの一つであり、同様のものとして「 Google のGKE」や「 Microsoft の AKS 」などが存在します。 以下3記事(Part)に分けて解説します。 Part1 :環境設定(「EKS環境構築」と「Agones/Open Matchのインストール」)、および動作確認 Part2 :マッチメイキングサービスのカスタマイズ処理を実装します Part3 :UnrealEngineを使用したGameServerとGameClientを用意します、Part2で開発したマッチメイキングサービスをGameClientに統合します。 はじめに 実行環境 実施手順 1.EKSの環境構築 2.Agonesのインストール 3.Openmatchのインストール 4.動作確認 Agonesデモのデプロイと動作確認 Open Matchデモのデプロイと動作確認 終わりに 参考 はじめに 現代のゲーム業界では、Dedicated Serverが重要な要素となっています。ある マルチプレイ ゲームの裏側では、大量のDedicated Serverを所有しており、プレイヤーに安定かつ効率的なサービスを提供します。 これらのサーバーの管理とスケジューリングは、プレイヤーのゲーム体験に直接影響する重要な問題です。この問題を解決するために、Agonesは登場し、Dedicated Serverの管理とスケジューリングをよりシンプルにするソリューションを提供します。 しかし、専用サーバーの管理とスケジューリングだけでは不十分で、これらのサーバーを効率的に利用するためにはマッチングシステムが必要です。 例えば、同じ地域のプレイヤーが最も近いゲームサーバーにマッチングされることで、彼らのゲーム体験を向上させることを望んでいます。そのため、Open Matchは、柔軟でスケーラブルなマッチングシステムとして生まれました。 したがって、AgonesとOpenMatchは相互補完的な存在です。一方で、AgonesはDedicated Serverの管理とスケジューリングを行い、サーバーの使用効率を向上させます。 一方で、Open Matchは効率的なマッチング アルゴリズム を通じて、プレイヤーを最も適したサーバーにマッチングします。 これら二つは、ゲームに対して柔軟でスケーラブルで効率的なインフラスト ラク チャを提供します。 これから、このようなインフラスト ラク チャを作成する方法を3記事に分けてステップ バイス テップで説明します。 実行環境 kubectl (v1.27.1) Kubernetes クラスタ ーを操作するための コマンドライン ツール eksctl (0.139.0) EKS クラスタ ーを制御するために使用する コマンドライン ツール AWS CLI (2.11.17) Amazon EKS など AWS のサービスを操作するための コマンドライン ツール 実施手順 以下の手順で環境を構築し、テストを行います。 EKSの環境構築 Agonesのインストール Openmatchのインストール 動作確認 1.EKSの環境構築 EKSの構築方法については、 AWS 公式ドキュメントの 「Amazon EKS の開始方法」 に従って以下の手順を実施します。 EKS展開用のネットワーク環境を作成する Kubernetes クラスタ を作成する クラスタ のノードグループを作成する これから、EKSの構築を実施します。 a.EKS展開用のネットワーク環境を作成する 今回は検証目的のため、 公式サイト で推奨されるネットワーク アーキテクチャ (PrivateとPublicの構造)は使用せず、2つのPublicネットワークのみを設定します。 実施結果を確認します。 VPC CIDR: 192.168.0.0/16 Subnet: $ aws ec2 describe-subnets \ --filters "Name=vpc-id,Values=[VPC ID]" \ --query 'Subnets[*].{SubnetId: SubnetId,AvailabilityZone: AvailabilityZone,CidrBlock: CidrBlock}' \ --output table -------------------------------------------------------------------- | DescribeSubnets | +------------------+------------------+----------------------------+ | AvailabilityZone | CidrBlock | SubnetId | +------------------+------------------+----------------------------+ | ap-northeast-1a | 192.168.0.0/24 | パブリックサブネットID1 | | ap-northeast-1c | 192.168.1.0/24 | パブリックサブネットID2 | +------------------+------------------+----------------------------+ b. Kubernetes クラスタ を作成する ここでは、 クラスタ を作成する方法として「eksctl」と「マネジメントコンソール」の2つの選択肢があります。 初心者がステップ バイス テップで進めたい場合は後者のマネジメントコンソールを使用することをおすすめしますが、今回は手間を省略するためにeksctlコマンドを使用して クラスタ を作成します。 以下のコマンドを使用して クラスタ を作成しますが、「 クラスタ 名」・「パブリックサブネットID1」・「パブリックサブネットID2」はそれぞれ実際の クラスタ 名とパブリックサブネットIDに置き換えてください # コマンドガイドライン ## https://eksctl.io/usage/vpc-configuration/#use-existing-vpc-other-custom-configuration eksctl create cluster --name 「クラスタ名」\ --region ap-northeast-1 \ --without-nodegroup \ --vpc-public-subnets=[パブリックサブネットID1],[パブリックサブネットID2] c. クラスタ のノードグループを作成する NodeGroupを AWS コンソールを介して作成する場合、まず一連のロールを設定する必要があります。手続きを簡略化するために、今回も引き続きeksctlコマンドを使用してNodeGroupを作成します。 以下のコマンドを使用してNodeGroupを作成します。「 クラスタ 名」「ノードグループ名」はそれぞれ希望する名前に置き換えてください。 ※次にOpen Matchをインストールしますが、インストールコマンドを簡略化するために、デフォルトの設定を使用します。このため、最低でも3つ以上のCPUが必要となりますので、今回はt3.xlargeタイプのサーバを選択します。 # コマンドガイドライン ## https://eksctl.io/usage/managing-nodegroups/ eksctl create nodegroup \ --cluster 「クラスタ名」 \ --region ap-northeast-1 \ --name 「ノードグループ名」 \ --node-type t3.xlarge \ --nodes 2 \ --nodes-min 2 \ --nodes-max 2 ここまでは、EKSの環境設定が完了しました。 次に、AgonesとOpen Matchをインストールします。 2.Agonesのインストール Agonesをインストールする方法については、基本的には Agonesの公式ドキュメンテーション に従います。 インストール方法としては、 yaml インストールとHelmインストールの2つの方法がありますが、以下の理由からHelmを使用してAgonesをインストールする方法を選択しました。 ① Helmを使用すると、 Kubernetes アプリケーションのデプロイを再現可能で一貫性のある形で行うことができます。これにより、運用上の一貫性と信頼性を確保する ② Helmはチャートと呼ばれるパッケージ形式を使用するため、チーム間での共有や再利用が容易になる ③ 複雑なアプリケーションを構成するためのパラメータ化された設定を提供します。これにより、カスタマイズや再構成が容易になる ④ Helmはデプロイのバージョン管理、つまりリリースの管理を容易にします。これにより、バージョン間の移行がスムーズに行う Agonesのインストールコマンドは以下のとおりです。 [my-release]はリリースの名前で、適宜置き換えてください。 # 公式のstable Helmリポジトリを追加 helm repo add agones https://agones.dev/chart/stable # Helmを使ってAgonesをインストール helm install [my-release] --namespace agones-system agones/agones 上記のコマンドを使用すると、公式のstable Helm リポジトリ を追加し、Helmを使ってAgonesをインストールできます。[my-release]の部分にはインストールするリリースの名前を指定します。 また、Agonesは agones-system という名前の空間にインストールされます。 3.Openmatchのインストール Openmatchの公式ドキュメンテーション に従ってOpenmatchをインストールします。 OpenMatchのインストールコマンドは以下のとおりです。 [my-release]はリリースの名前で、適宜置き換えてください。 # Open MatchのHelmリポジトリを追加 helm repo add open-match https://open-match.dev/chart # Helmを使ってOpen Matchをインストール helm install [my-release] open-match/open-match --namespace open-match --set open-match-core.enabled=true 上記のコマンドを使用すると、Open MatchのHelm リポジトリ を追加し、Helmを使ってOpen Matchをインストールできます。 また、Open Matchはopen-matchという 名前空間 にインストールされ、 open-match-core が有効になります。 4.動作確認 環境の構築が完了したので、AgonesとOpen Matchが正しくインストールされていることを確認するために、AgonesのデモとOpen Matchのデモを使用してテストを行います。 環境の構築が完了している場合は、以下の手順を実行してテストを行ってください。 Agonesデモのデプロイと動作確認 Agones公式ドキュメントの ゲームサーバ作成手順 に従ってサンプルゲームサーバーをデプロイします。これにより、Agonesが正常に動作するかを確認できます。 サンプルゲームサーバーをデプロイする kubectl create -f https://raw.githubusercontent.com/googleforgames/agones/release-1.32.0/examples/simple-game-server/gameserver.yaml GameServerの状態を確認する kubectl get gs 確認ポイントとして、GameServerのステータスが Ready になっていることを確認してください。 $ kubectl get gs NAME STATE ADDRESS PORT NODE AGE simple-game-server-h4h2w Ready ec2-18-182-6-144.ap-northeast-1.compute.amazonaws.com 7393 ip-192-168-0-130.ap-northeast-1.compute.internal 18s Open Matchデモのデプロイと動作確認 Openmatch 公式デモの作成手順 に従ってOpen Matchのデモをデプロイします。これにより、Open Matchが正常に動作するかを確認できます。 サンプルマッチメイキングフロントエンドとバックエンドをデプロイする kubectl create namespace open-match-demo kubectl apply --namespace open-match-demo \ -f https://open-match.dev/install/v1.7.0/yaml/02-open-match-demo.yaml マッチメイキングフロントエンドとバックエンドの状態を確認する kubectl get pods -n open-match-demo 上記の手順に従い、Open Matchのサンプルマッチメイキングでフロントエンドとバックエンドをデプロイし、 kubectl get pods -n open-match-demo コマンドでポッドの状態を確認します。 確認ポイントとして、 ① すべてのポッドのステータスが Running になっていることを確認する $ kubectl get pods -n open-match-demo NAME READY STATUS RESTARTS AGE om-demo-7bf887b848-75nvn 1/1 Running 0 35s om-function-58b954cf5f-4gqr4 1/1 Running 0 35s om-function-58b954cf5f-ll6pz 1/1 Running 0 35s om-function-58b954cf5f-nqrks 1/1 Running 0 35s ② PortForward を使用してデモページにアクセスできることを確認する $ kubectl port-forward --namespace open-match-demo service/om-demo 51507:51507 Forwarding from 127.0.0.1:51507 -> 51507 Forwarding from [::1]:51507 -> 51507 終わりに 以上で、EKSの環境構築およびAgones/Open Matchのインストールプロセスの説明が完了しました。また、公式デモを使用してテストを行いました。 次に、マッチメイキングサービスの構築とGameClientの実装について詳しく説明します。 Part2 と Part3 では、マッチメイキングサービスのカスタマイズ方法とGameClientの開発について取り上げます。 引き続き、お楽しみにしてください! 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ(Web3/メタバース/AI) 参考 https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/getting-started-console.html https://open-match.dev/site/docs/ https://agones.dev/site/docs/ 執筆: @chen.sun 、レビュー: @yamashita.yuki ( Shodo で執筆されました )
こんにちは!金融ソリューション事業部の孫です。 以前の 記事 では Unreal Engine Dedicated Serverの構築方法について紹介しました。 今回は続きの記事として、以下の3部で、 AWS が提供するEKSを使用してマッチメイキング機能を持つAgonesでGameServerを運用する環境の構築プロセスを説明します。 なお、EKSは Kubernetes の クラウド サービスの一つであり、同様のものとして「 Google のGKE」や「 Microsoft の AKS 」などが存在します。 以下3記事(Part)に分けて解説します。 Part1 :環境設定(「EKS環境構築」と「Agones/Open Matchのインストール」)、および動作確認 Part2 :マッチメイキングサービスのカスタマイズ処理を実装します Part3 :UnrealEngineを使用したGameServerとGameClientを用意します、Part2で開発したマッチメイキングサービスをGameClientに統合します。 はじめに 実行環境 実施手順 1.EKSの環境構築 2.Agonesのインストール 3.Openmatchのインストール 4.動作確認 Agonesデモのデプロイと動作確認 Open Matchデモのデプロイと動作確認 終わりに 参考 はじめに 現代のゲーム業界では、Dedicated Serverが重要な要素となっています。ある マルチプレイ ゲームの裏側では、大量のDedicated Serverを所有しており、プレイヤーに安定かつ効率的なサービスを提供します。 これらのサーバーの管理とスケジューリングは、プレイヤーのゲーム体験に直接影響する重要な問題です。この問題を解決するために、Agonesは登場し、Dedicated Serverの管理とスケジューリングをよりシンプルにするソリューションを提供します。 しかし、専用サーバーの管理とスケジューリングだけでは不十分で、これらのサーバーを効率的に利用するためにはマッチングシステムが必要です。 例えば、同じ地域のプレイヤーが最も近いゲームサーバーにマッチングされることで、彼らのゲーム体験を向上させることを望んでいます。そのため、Open Matchは、柔軟でスケーラブルなマッチングシステムとして生まれました。 したがって、AgonesとOpenMatchは相互補完的な存在です。一方で、AgonesはDedicated Serverの管理とスケジューリングを行い、サーバーの使用効率を向上させます。 一方で、Open Matchは効率的なマッチング アルゴリズム を通じて、プレイヤーを最も適したサーバーにマッチングします。 これら二つは、ゲームに対して柔軟でスケーラブルで効率的なインフラスト ラク チャを提供します。 これから、このようなインフラスト ラク チャを作成する方法を3記事に分けてステップ バイス テップで説明します。 実行環境 kubectl (v1.27.1) Kubernetes クラスタ ーを操作するための コマンドライン ツール eksctl (0.139.0) EKS クラスタ ーを制御するために使用する コマンドライン ツール AWS CLI (2.11.17) Amazon EKS など AWS のサービスを操作するための コマンドライン ツール 実施手順 以下の手順で環境を構築し、テストを行います。 EKSの環境構築 Agonesのインストール Openmatchのインストール 動作確認 1.EKSの環境構築 EKSの構築方法については、 AWS 公式ドキュメントの 「Amazon EKS の開始方法」 に従って以下の手順を実施します。 EKS展開用のネットワーク環境を作成する Kubernetes クラスタ を作成する クラスタ のノードグループを作成する これから、EKSの構築を実施します。 a.EKS展開用のネットワーク環境を作成する 今回は検証目的のため、 公式サイト で推奨されるネットワーク アーキテクチャ (PrivateとPublicの構造)は使用せず、2つのPublicネットワークのみを設定します。 実施結果を確認します。 VPC CIDR: 192.168.0.0/16 Subnet: $ aws ec2 describe-subnets \ --filters "Name=vpc-id,Values=[VPC ID]" \ --query 'Subnets[*].{SubnetId: SubnetId,AvailabilityZone: AvailabilityZone,CidrBlock: CidrBlock}' \ --output table -------------------------------------------------------------------- | DescribeSubnets | +------------------+------------------+----------------------------+ | AvailabilityZone | CidrBlock | SubnetId | +------------------+------------------+----------------------------+ | ap-northeast-1a | 192.168.0.0/24 | パブリックサブネットID1 | | ap-northeast-1c | 192.168.1.0/24 | パブリックサブネットID2 | +------------------+------------------+----------------------------+ b. Kubernetes クラスタ を作成する ここでは、 クラスタ を作成する方法として「eksctl」と「マネジメントコンソール」の2つの選択肢があります。 初心者がステップ バイス テップで進めたい場合は後者のマネジメントコンソールを使用することをおすすめしますが、今回は手間を省略するためにeksctlコマンドを使用して クラスタ を作成します。 以下のコマンドを使用して クラスタ を作成しますが、「 クラスタ 名」・「パブリックサブネットID1」・「パブリックサブネットID2」はそれぞれ実際の クラスタ 名とパブリックサブネットIDに置き換えてください # コマンドガイドライン ## https://eksctl.io/usage/vpc-configuration/#use-existing-vpc-other-custom-configuration eksctl create cluster --name 「クラスタ名」\ --region ap-northeast-1 \ --without-nodegroup \ --vpc-public-subnets=[パブリックサブネットID1],[パブリックサブネットID2] c. クラスタ のノードグループを作成する NodeGroupを AWS コンソールを介して作成する場合、まず一連のロールを設定する必要があります。手続きを簡略化するために、今回も引き続きeksctlコマンドを使用してNodeGroupを作成します。 以下のコマンドを使用してNodeGroupを作成します。「 クラスタ 名」「ノードグループ名」はそれぞれ希望する名前に置き換えてください。 ※次にOpen Matchをインストールしますが、インストールコマンドを簡略化するために、デフォルトの設定を使用します。このため、最低でも3つ以上のCPUが必要となりますので、今回はt3.xlargeタイプのサーバを選択します。 # コマンドガイドライン ## https://eksctl.io/usage/managing-nodegroups/ eksctl create nodegroup \ --cluster 「クラスタ名」 \ --region ap-northeast-1 \ --name 「ノードグループ名」 \ --node-type t3.xlarge \ --nodes 2 \ --nodes-min 2 \ --nodes-max 2 ここまでは、EKSの環境設定が完了しました。 次に、AgonesとOpen Matchをインストールします。 2.Agonesのインストール Agonesをインストールする方法については、基本的には Agonesの公式ドキュメンテーション に従います。 インストール方法としては、 yaml インストールとHelmインストールの2つの方法がありますが、以下の理由からHelmを使用してAgonesをインストールする方法を選択しました。 ① Helmを使用すると、 Kubernetes アプリケーションのデプロイを再現可能で一貫性のある形で行うことができます。これにより、運用上の一貫性と信頼性を確保する ② Helmはチャートと呼ばれるパッケージ形式を使用するため、チーム間での共有や再利用が容易になる ③ 複雑なアプリケーションを構成するためのパラメータ化された設定を提供します。これにより、カスタマイズや再構成が容易になる ④ Helmはデプロイのバージョン管理、つまりリリースの管理を容易にします。これにより、バージョン間の移行がスムーズに行う Agonesのインストールコマンドは以下のとおりです。 [my-release]はリリースの名前で、適宜置き換えてください。 # 公式のstable Helmリポジトリを追加 helm repo add agones https://agones.dev/chart/stable # Helmを使ってAgonesをインストール helm install [my-release] --namespace agones-system agones/agones 上記のコマンドを使用すると、公式のstable Helm リポジトリ を追加し、Helmを使ってAgonesをインストールできます。[my-release]の部分にはインストールするリリースの名前を指定します。 また、Agonesは agones-system という名前の空間にインストールされます。 3.Openmatchのインストール Openmatchの公式ドキュメンテーション に従ってOpenmatchをインストールします。 OpenMatchのインストールコマンドは以下のとおりです。 [my-release]はリリースの名前で、適宜置き換えてください。 # Open MatchのHelmリポジトリを追加 helm repo add open-match https://open-match.dev/chart # Helmを使ってOpen Matchをインストール helm install [my-release] open-match/open-match --namespace open-match --set open-match-core.enabled=true 上記のコマンドを使用すると、Open MatchのHelm リポジトリ を追加し、Helmを使ってOpen Matchをインストールできます。 また、Open Matchはopen-matchという 名前空間 にインストールされ、 open-match-core が有効になります。 4.動作確認 環境の構築が完了したので、AgonesとOpen Matchが正しくインストールされていることを確認するために、AgonesのデモとOpen Matchのデモを使用してテストを行います。 環境の構築が完了している場合は、以下の手順を実行してテストを行ってください。 Agonesデモのデプロイと動作確認 Agones公式ドキュメントの ゲームサーバ作成手順 に従ってサンプルゲームサーバーをデプロイします。これにより、Agonesが正常に動作するかを確認できます。 サンプルゲームサーバーをデプロイする kubectl create -f https://raw.githubusercontent.com/googleforgames/agones/release-1.32.0/examples/simple-game-server/gameserver.yaml GameServerの状態を確認する kubectl get gs 確認ポイントとして、GameServerのステータスが Ready になっていることを確認してください。 $ kubectl get gs NAME STATE ADDRESS PORT NODE AGE simple-game-server-h4h2w Ready ec2-18-182-6-144.ap-northeast-1.compute.amazonaws.com 7393 ip-192-168-0-130.ap-northeast-1.compute.internal 18s Open Matchデモのデプロイと動作確認 Openmatch 公式デモの作成手順 に従ってOpen Matchのデモをデプロイします。これにより、Open Matchが正常に動作するかを確認できます。 サンプルマッチメイキングフロントエンドとバックエンドをデプロイする kubectl create namespace open-match-demo kubectl apply --namespace open-match-demo \ -f https://open-match.dev/install/v1.7.0/yaml/02-open-match-demo.yaml マッチメイキングフロントエンドとバックエンドの状態を確認する kubectl get pods -n open-match-demo 上記の手順に従い、Open Matchのサンプルマッチメイキングでフロントエンドとバックエンドをデプロイし、 kubectl get pods -n open-match-demo コマンドでポッドの状態を確認します。 確認ポイントとして、 ① すべてのポッドのステータスが Running になっていることを確認する $ kubectl get pods -n open-match-demo NAME READY STATUS RESTARTS AGE om-demo-7bf887b848-75nvn 1/1 Running 0 35s om-function-58b954cf5f-4gqr4 1/1 Running 0 35s om-function-58b954cf5f-ll6pz 1/1 Running 0 35s om-function-58b954cf5f-nqrks 1/1 Running 0 35s ② PortForward を使用してデモページにアクセスできることを確認する $ kubectl port-forward --namespace open-match-demo service/om-demo 51507:51507 Forwarding from 127.0.0.1:51507 -> 51507 Forwarding from [::1]:51507 -> 51507 終わりに 以上で、EKSの環境構築およびAgones/Open Matchのインストールプロセスの説明が完了しました。また、公式デモを使用してテストを行いました。 次に、マッチメイキングサービスの構築とGameClientの実装について詳しく説明します。 Part2 と Part3 では、マッチメイキングサービスのカスタマイズ方法とGameClientの開発について取り上げます。 引き続き、お楽しみにしてください! 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ(Web3/メタバース/AI) 参考 https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/getting-started-console.html https://open-match.dev/site/docs/ https://agones.dev/site/docs/ 執筆: @chen.sun 、レビュー: @yamashita.yuki ( Shodo で執筆されました )
こんにちは、金融ソリューション事業部の孫です。 シリーズの最初の記事( 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 で執筆されました )
こんにちは、金融ソリューション事業部の孫です。 前回の Part1 記事に続きまして Part2 では、OpenMatchとAgonesを使用して、柔軟性がありスケーラブルなゲームマッチングとゲームサーバー管理システムの構築方法を詳しく説明しました。 この記事(Part3)では、UnrealEngineを利用して、オンラインマルチプレーヤーゲームのデモを完成させます。 さらに、 Part2 で開発したマッチメイキングサービスをUEクライアント(UnrealEngine GameClient)に統合します。 全体 アーキテクチャ は以下の図の通りです。 UEクライアントの実装について、前の 記事 で作成したデモを基にさらに拡張します。 プレーヤーマッチング機能とサーバー管理機能を追加することで、この度のクライアントの実現を目指します。 実施手順 1.前記事のデモにマッチング機能の追加 マッチング機能のボタンの追加 Frontend APIの呼びだす機能実装 2. Agones SDKを呼び出してGameServerの終了機能の実装 ユーザー数の管理機能の追加 Agones SDKを利用してGameServerの終了実装 3.パッケージ化したUEサーバーでAgones Fleetの作成 4.動作確認 終わりに 参考 実施手順 以下の順序でシステムを構築します: 前記事 のデモにマッチング機能の追加 Agones SDK を呼び出してGameServerの終了機能の実装 パッケージ化したUEサーバーでAgones Fleetの作成 動作確認 1. 前記事 のデモにマッチング機能の追加 この前の記事 UE5ネットワーク同期のC++実装例 では、プレーヤーが「Play」ボタンをクリックすると、UEゲームサーバーに接続してゲーム体験を開始する機能をすでに完成させています。 次に、マッチング機能のボタンを追加し、このボタンをクリックするとOpenMatchの Frontend API を呼び出してマッチング要求をリク エス トします。 マッチング機能のボタンの追加 以下の図のように、テキストエリア(Region入力用)とボタン(「Match」)をUnrealEngineのBluePrint Widget UIに配置します。 Frontend API の呼びだす機能実装 以下のモジュールをxxx.Build.csファイルに追加する 今回では、 Agones / HTTP / Json / JsonUtilities の4つのモジュールを使用します。 各モジュールは次のような役割を果たします。 Agones モジュールは Agones の SDK を使うために使用する HTTP モジュールは Frontend API の http リク エス トを送信するために使用する Json 、 JsonUtilities モジュールは http からの返却 Json データを解析するために使用する //xxx.Build.cs using UnrealBuildTool; //:(省略) PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "EnhancedInput", "Agones", "HTTP", "Json", "JsonUtilities" }); //:(省略) LoginHUDWidget.h を編集する まずは、配置したWidgetsをバインドすることから始めます。 ※注意:BluePrint内の Widget 名は LoginHUDWidget.h ファイル内の変数名と同じでなければならず、 meta = (BindWidget) タグを追加して Widget をバインドします それを完了したら、httpモジュールをincludeした上でコールバック関数を作成します。 //LoginHUDWidget.h // "Http.h"ヘッダーファイルの追加 #include "Http.h" UCLASS() class TEST_DESERVER_API ULoginHUDWidget : public UUserWidget { GENERATED_BODY() public: //:(省略) UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UEditableText* regionName; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UTextBlock* matchLabel; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UButton* matchBtn; private: FHttpModule* Http; void OnFetchGameServerResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); } LoginHUDWidget.cpp にマッチングボタンのクリックロジックを実装する //LoginHUDWidget.cpp // "Json.h", "JsonUtilities.h"ヘッダーファイルの追加 #include "Json.h" #include "JsonUtilities.h" // 構造関数内で関連コントロールとHttpモジュールを初期化します // MatchボタンにOnMatchmakingButtonClickedトリガー関数をバインドします void ULoginHUDWidget::NativePreConstruct() { Super::NativePreConstruct(); //:(省略) matchLabel->SetText(FText::FromString("Match")); FScriptDelegate MatchmakingDelegate; MatchmakingDelegate.BindUFunction(this, "OnMatchmakingButtonClicked"); matchBtn->OnClicked.Add(MatchmakingDelegate); //:(省略) Http = &FHttpModule::Get(); } // OnMatchmakingButtonClickedトリガー関数の処理ロジックを追加します void ULoginHUDWidget::OnMatchmakingButtonClicked() { statusLabel->SetText(FText::FromString("Start Matching!! Please wait")); TSharedRef<IHttpRequest, ESPMode::ThreadSafe> FetchGameServerHttpRequest = Http->CreateRequest(); FString frontendUrl = "127.0.0.1:8081/play/" + FString(regionName->GetText().ToString()); FetchGameServerHttpRequest->SetVerb("GET"); FetchGameServerHttpRequest->SetURL(frontendUrl); FetchGameServerHttpRequest->SetHeader("Content-Type", "application/json"); FetchGameServerHttpRequest->OnProcessRequestComplete().BindUObject(this, &ULoginHUDWidget::OnFetchGameServerResponse); FetchGameServerHttpRequest->ProcessRequest(); }; // Httpのコールバック関数の処理ロジックを追加します void ULoginHUDWidget::OnFetchGameServerResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { if (bWasSuccessful) { TSharedPtr<FJsonObject> JsonObject; TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString()); if (FJsonSerializer::Deserialize(Reader, JsonObject)) { FString IpAddress = JsonObject->GetStringField("ip"); FString Port = JsonObject->GetStringField("port"); FString LevelName = IpAddress + ":" + Port; statusLabel->SetText(FText::FromString(LevelName)); playBtn->SetVisibility(ESlateVisibility::Visible); } } } // OnPlayGameButtonClickedトリガー関数の処理ロジックを変更します // OpenMatchから返されたGameServerアドレスを接続ターゲットとして設定します void ULoginHUDWidget::OnPlayGameButtonClicked() { //:(省略) FString LevelName = statusLabel->GetText().ToString(); //:(省略) } 2. Agones SDK を呼び出してGameServerの終了機能の実装 すべてのプレイヤーがオフラインになった場合、AgonesSDKを呼び出して利用済のGameServerを削除した後、新しいGameServerを再作成します。 ユーザー数の管理機能の追加 UnrealEngine Gamemodeクラスにおいて PlayerNum 変数を追加して現在のプレーヤー数を保存します。 また、少なくとも1人のプレーヤーがこのGameServerに接続したことがあることを判断するため、 HasPlayerConnected のBool変数を追加します。 前の 記事 で、UnrealEngineによく使われるサーバー側の関数について既にいくつか紹介しました。 今回はその中の PostLogin 、 Logout 関数を用いることで、プレーヤー数の簡易的な統計データが取得します。 //xxxGameMode.h public: //:(省略) virtual void PostLogin(APlayerController* NewPlayer) override virtual void Logout(AController* Exiting) override private: //:(省略) int32 PlayerNum; bool HasPlayerConnected; //xxxGameMode.cpp // 構造関数で変数を初期化します xxxGameMode::xxxGameMode(){ //:(省略) int32 PlayerNum = 0; bool HasPlayerConnected = false; } void xxxGameMode::PostLogin(APlayerController* NewPlayer) { Super::PostLogin(NewPlayer); PlayerNum++; HasPlayerConnected = true; if (!HasPlayerConnected) { HasPlayerConnected = true; GetWorldTimerManager().SetTimer(CountDownPlayerNumHandle, this, &xxxGameMode::TickCount, 1.0f, true); } } void xxxGameMode::Logout(AController* Exiting) { Super::Logout(Exiting); PlayerNum--; } Agones SDK を利用してGameServerの終了実装 UnrealEngine Gamemodeクラスにおいて、Agones SDK のShutdown()関数を呼び出してGameServerがシャットダウンされます。 ※Agonesは一 定量 のGameServerを維持し、GameServerが閉じられると新たなGameServerの生成がトリガーされます。 //xxxGameMode.h public: //:(省略) virtual void Tick(float DeltaTime) override private: //:(省略) //Agones SDKの処理結果に対応するレスポンス関数 //成功処理後のレスポンス関数 void HandleShutdownSuccess(const FEmptyResponse& Response); //成功失敗時のレスポンス関数 void HandleShutdownError(const FAgonesError& Error); void TickCount(); //xxxGameMode.cpp void Atest_DEServerGameMode::HandleShutdownSuccess(const FEmptyResponse& Response) {   //デモのため、実際の処理を行いません UE_LOG(LogTemp, Log, TEXT("Game server successfully shutdown")); } void Atest_DEServerGameMode::HandleShutdownError(const FAgonesError& Error) { //デモのため、実際の処理を行いません UE_LOG(LogTemp, Error, TEXT("shutting down failed: %s"), *Error.ErrorMessage); } void xxxGameMode::TickCount() { Super::Tick(DeltaTime); if (HasPlayerConnected && PlayerNum <= 0) { FShutdownDelegate SuccessDelegate; SuccessDelegate.BindUFunction(this, FName("HandleShutdownSuccess")); FAgonesErrorDelegate ErrorDelegate; ErrorDelegate.BindUFunction(this, FName("HandleShutdownError")); GetWorldTimerManager().ClearTimer(CountDownPlayerNumHandle); AgonesSDK->Shutdown(SuccessDelegate, ErrorDelegate); } } 3.パッケージ化したUEサーバーでAgones Fleetの作成 UnrealEngine Editorを使用して、 Linux プラットフォーム向けのDedicated Serverをパッケージ化します。 以下の図のように、ターゲットプラットフォーム Linux -> Development -> Linux(server) を選択し、パッケージします。 ※ Windows OSではもしかするとターゲットプラットフォームの選択肢に Linux が存在しないかもしれません。 これは、 Linux プラットフォームのサポートが設定されていないためです。 具体的な設定方法については、 こちら を参照してください。 パッケージ化が完了したら、 Part2 で各モジュールをデプロイしたのと同じ手順で、EKSにGameServerをデプロイします。 ローカルにおいてDockerImageを コンパイル する 下記のDockerfileをパッケージ化の際に指定された保存先のルート ディレクト リに配置し、Dockerイメージ作成コマンドを実行します。 ## DockerFile ## 「AgonesOMServer」を実際のプロジェクト名に置き換えます FROM ubuntu:20.04 RUN apt-get update && apt-get install -y \ libxcursor1 \ libxrandr2 \ libxinerama1 \ libxi6 \ libgl1-mesa-glx \ && rm -rf /var/lib/apt/lists/* RUN addgroup --gid 1000 gameserver && \ adduser --gid 1000 --uid 1000 --shell /usr/sbin/nologin --home /home/gameserver --gecos "" --disabled-login --disabled-password gameserver COPY --chown=gameserver:gameserver ./LinuxServer /home/gameserver/LinuxServer RUN chmod -R 770 /home/gameserver/LinuxServer WORKDIR /home/gameserver/LinuxServer USER gameserver EXPOSE 7777/udp ENTRYPOINT ["/home/gameserver/LinuxServer/「AgonesOMServer」.sh"] ## Docker image build command $ docker build -t localimage/ue_server:0.1 . DockerImageを Amazon ECRにアップロードする Part2 と同様に、ue_serverという名前の Amazon ECRプライベー トリポジ トリを新規作成します。 次に、Dockerイメージを Amazon ECRにアップロードします。 $ docker tag localimage/ue_server:0.1 {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/ue_server:0.1 $ docker push {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/ue_server:0.1 Amazon EKSにおいてAgones Fleetを作成する ローカルでAgones Fleetの作成用のfleet. yaml ファイルを作成します。 ## fleet.yaml --- apiVersion: "agones.dev/v1" kind: Fleet metadata: name: fleet-ap-northeast-1 spec: replicas: 2 scheduling: Packed strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 25% template: metadata: namespace: meta-poc labels: region: ap-northeast-1 spec: players: initialCapacity: 4 ports: - name: default containerPort: 7654 health: initialDelaySeconds: 30 periodSeconds: 60 template: metadata: namespace: meta-poc labels: region: ap-northeast-1 spec: containers: - name: ue5-server image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/ue_server:0.1 resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "1Gi" cpu: "1" fleet. yaml ファイルを Amazon EKSに適用します。 $ kubectl apply -f fleet.yaml 4.動作確認 UnrealEngine Editorを使用して4つのクライアントを起動する 具体的な操作手順は、以下の図のとおり New Editor Window(PIE) -> Number Of Players: 4 -> Play Standalone を設定する 以下のコマンドでAgones GameServerのステータス監視を起動する $ watch kubectl get gs OpenMatch Frontend Serviceをローカルに マッピング する # OpenMatch Frontend サービスがローカルの8081ポートにマッピングされます kubectl port-forward services/frontend-ednpoint 8081:80 マッチング機能をテストする 確認ポイント: 4つのクライアントが同じGameServerアドレスにマッチングされていること GameServerのステータスがAllocatedになっていること DedicatedServerへの接続をテストする 確認ポイント: Playボタンをクリックした後、Dedicated Serverに成功したこと Characterの頭上に表示されているNickNameが、クライアントが入力したNickNameと同じであること AgonesによるGameServerのシャットダウンをテストする 確認ポイント: 全てのGameClientを閉じた後、以前Allocated状態だったサーバーが削除されること その代わりに新たにReady状態のGameServerが作成されること 終わりに これで、マッチング機能を備え、Agonesで Unreal Engine DedicatedServerをスケジューリングするオンライン マルチプレイヤー ゲームの作成が完了しました。 この一連の記事では、EKSを使って Kubernetes の特性を活用し、 Unreal Engine のDedicated Serverを管理する方法について深く探求しました。 その中で、マッチングシステムとしてOpenMatchを使用し、その強力な拡張性と設定性を活用してニーズに合ったマッチングルールをカスタマイズしました。同時に、Agonesを用いてゲームサーバーのスケジューリングを行い、効率的で安定したサーバー管理を実現しました。 次は、さらなるゲームに関連するインフラ設計のソリューションを見つけ出し、ゲーム体験をさらに向上させる方法を継続に探求します。 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ(Web3/メタバース/AI) 参考 https://docs.unrealengine.com/5.2/ja/linux-game-development-in-unreal-engine/ https://agones.dev/site/docs/reference/fleet/ 執筆: @chen.sun 、レビュー: @yamashita.yuki ( Shodo で執筆されました )
はじめに 前提 この記事の対象とする読者の方 スポットインスタンスについて 2つのシグナル 2分前中断シグナル 再調整推奨シグナル スポットインスタンスのリソース確保 マネージド型ノードグループの利用 Mixed Instance Policyの利用 Cluster Autoscalerを有効にする。 ノードグループのAZごとの分散 Priority Expander スポットインスタンスの中断に備える ワークロードを停止させないために考慮すること Graceful Shutdownされるようになっている。 Podが必要な数を維持したままドレインされる。 Podが正常であるという状態を適切に定義する。 Pod Readiness Gate Podの可用性を考慮したスケジュール まとめ おわりに はじめに こんにちは!2年目にもかかわらずフレックス制度を存分に活用して、普段8:30 – 16:30で業務をしている クラウド イノベーション センターの石井大樹です。 Amazon EKS(以下EKS)にて、スポット インスタンス は利用されていますか? スポット インスタンス は安いけど、いきなりシャットダウンしちゃうから、使い勝手が悪いんでしょう?など、漠然とした不安から利用をためらっていたりはしないでしょうか。 そのぼんやりとした不安を払しょくし、スポット インスタンス 利用を検討する足がかりになれば、と思い今回は記事にまとめさせていただきました。 前提 この記事の対象とする読者の方 Kubernetes クラスタ をEKSで運用している方 EKS環境でスポット インスタンス を利用しようとしている方 既にEKS環境でスポット インスタンス を利用している方 ワークロードを維持する方法に関心がある方 スポット インスタンス について スポット インスタンス は、通常のオンデマンド インスタンス に比べて最大90%の割引で利用できるオプションです。 しかし、オンデマンド インスタンス と異なり、 AWS 側のリソースキャパシティの都合により、 インスタンス の利用が中断される可能性があります。 2つのシグナル 先ほどスポット インスタンス は、「 AWS 側の都合により中断される可能性がある」とお話しいたしました。しかし、実際には2つのシグナルが発出された後に中断が実行されます。このシグナルを利用することで、スポット インスタンス の中断の影響を最小限に収めることができます。 2分前中断シグナル スポット インスタンス の終了の2分前に通知されるものです。この通知を受け取って2分後に、スポット インスタンス は終了します。 再調整推奨シグナル 対象スポット インスタンス の中断可能性の高まりが検知された場合、2分前シグナルを受け取る前に通知されるシグナルです。このシグナルを受け取ったタイミングで必要な処理が行われるように事前に準備することで、スポット インスタンス を安定して安全に運用できます。 Amazon EC2 AutoScalingグループのCapacity Rebalancingが有効化されている場合、再調整推奨シグナルを受け取ると、 Amazon EC2 AutoScalingは、代替となる新しい インスタンス の起動を試みます。その 新しい インスタンス が起動した後、既存の インスタンス はシャットダウンされます。 なお、EKSノードグループのAutoScalingグループでCapacity Rebalancingを有効化したい場合は、後述するマネージド型ノードグループを利用することで自動的に有効化されます。セルフマネージド型ノードグループで利用をする際は、手動で設定をする必要があります。 また、セルフマネージド型ノードグループにて、再調整推奨シグナルを受け、後述するGraceful Shutdownなどのアクションの実行をするには、 AWS Node Termination Handler などの導入が必要です。セルフマネージド型ノードグループには、 シグナルをキャッチしてノード上のPodの安全な待避を行う仕組みがないためです。 ※注意 この再調整推奨シグナルは2分前中断シグナルよりも前に通知されるということを保証していません。予期せぬリソースの需要が AWS 側で生まれた場合は、2分前中断シグナルと同時に通知を受け取り、必要な対処ができずにワークロードが中断される可能性があります。 再調整推奨シグナルについて スポット インスタンス のリソース確保 せっかくスポット インスタンス の利用を開始したのにも関わらず、スポット インスタンス のリソースが十分に確保できずにワークロードを中断してしまっては元も子もありません。この章では、スポット インスタンス を利用しながら十分なリソースを確保し続ける方法をこちらでご紹介します。 マネージド型ノードグループの利用 EKSでは、マネージド型ノードグループを利用することにより、様々な恩恵を開発者は受けることができます。この恩恵はスポット インスタンス の利用時も例外ではありません。これを利用することにより、EKSでスポット インスタンス を利用する際に AWS がベストプ ラク ティスとして定めた内容を自動的に設定してくれます。 以下が対象の設定内容の一部分です。 配分戦略を capacity-optimized にする。 これにより、中断確率の低いスポット インスタンス が配分されます。 Capacity Rebalancing を有効化。 これにより、 再調整推奨シグナル を受けたときに、EKSは自動的に中断可能性の低いスポットキャパシティプールからスポット インスタンス を起動し、そのノードが準備完了になったら、既存のスポット インスタンス のドレインをしてくれます。 Mixed Instance Policyの利用 複数のEC2 インスタンス タイプをノードグループで利用できるようにすることで、スポット インスタンス を利用できる確率が高まります。利用候補となる インスタンス タイプが増えるためです。しかし、注意することが1点あります。それは、vCPU・メモリが同等の インスタンス タイプであるべきということです。 例えば、 CPU: 2vCPU , メモリ: 4GiB の c5a.large を利用する際は、CPU: 2vCPU , 、メモリ: 4GiB の c5d.large 等と利用されるべきということです。 vCPU・メモリが異なる インスタンス タイプを混在させると、Cluster Autoscalerが正常にリソースの計算ができなくなり、意図しないスケールにつながってしまいます。 Cluster Autoscalerを有効にする。 Cluster Autoscalerは Kubernetes クラスタ を自動的に調整してくれる非常に有効なエコシステムです。このエコシステムは導入のみでも十分な効果を発揮しますが、以下に紹介する機能を利用することでさらなる効果が期待できます。 ※Auto Scalingグループの最大・最少ノード数は尊重されます。そのため、Cluster Autoscalerは最大ノード数以上・最少ノード数以下にはスケールできません。 ノードグループのAZごとの分散 ノードグループを、利用しているリージョンのAZを跨るように作成するようにします。利用候補となる インスタンス が増えるためです。そうすることで、スポット インスタンス を利用できる確率を高めることができます。 Priority Expander しかし、どれだけ対策を練っても、スポット インスタンス が確保できない可能性があります。そんなときは、オンデマンド インスタンス を代わりに確保するようにしましょう。値段は割引がされていない定価で利用することになりますが、ワークロードを中断させないことが最優先です。 Cluster AutoscalerのPriority Expander を利用することで、スポット インスタンス が見つからない際は、オンデマンド インスタンス のノードグループをスケールさせることで不足したリソースを補填できます。 以下が設定例です。 正規表現 で spot に該当するノードグループが優先してスケールされるように設定しています。 apiVersion : v1 kind : ConfigMap metadata : name : cluster-autoscaler-priority-expander namespace : kube-system data : priorities : |- 50 : - .*spot.* 1 : - .*ondemand.* スポット インスタンス の中断に備える ここまでは、できるだけスポット インスタンス を多くの割合で利用する方法をご紹介いたしました。 ご紹介した通り、スポット インスタンス は中断させられてしまうものです。この中断の際にワークロードの実体であるPodに対してなんにも考慮に入れていない場合、ワークロードは中断してしまいます。 ここからはスポット インスタンス が切り替わる際にもワークロードを安全に保つための方法をご紹介します。 ワークロードを停止させないために考慮すること ワークロードを中断させないために考慮する必要があることが4つあります。 PodはGraceful Shutdownされるようになっている。 Podは必要な数を維持したままドレインされる。 Podが正常であるという状態を適切に定義されている。 Podの可用性を考慮してスケジュールされるようになっている。 Graceful Shutdownされるようになっている。 再調整推奨シグナル を受け取り、EKSがノードのドレインを開始して、瞬時にPodが削除されてしまった場合、問題が発生する可能性があります。その問題について理解するには、まずPodの削除が始まると何が起こるかを理解する必要があります。 Podの削除が始まると以下が行われます。 Podの終了処理 ServiceからのPodの切り離し これらが、問題の生じない順序で行われたら良いのですが、 Kubernetes ではこれらは個別のプロセスとして別々に行われるので、それは保証されません。 順序が異なる場合、以下のような問題が生じます。 リク エス トを受け付けたが、処理するPodが削除されてしまい、エラーになる。 また、Podが最後のリク エス トを受け付けたはいいが、処理している間にPodが終了されてしまった場合はどうなるでしょうか。このような問題が生じます。 リク エス トの処理が終わっていないにもかかわらずPodが削除されてしまい、エラーになる。 このような状況が生じ、ワークロードに影響を及ぼさないためにもPodがGraceful Shutdownをするように適切に設定しなければいけません。 この問題を解決するには2つの対策が必要です。 以下では、2つの対策・Podのライフサイクルを語る上では非常に重要な用語を用いて解説します。 SIGTERM :終了処理を開始するように命令するシグナル SIGKILL :コンテナを強制的にシャットダウンするように命令するシグナル terminationGracePeriodSeconds : deletionTimeStampが設定されてから何秒でSIGKILLをおくるかの設定 - preStopと、SIGTERMはこの時間内に終わらせることが必要です。 preStop : Podが終了する前に実行される処理 preStop ライフサイクルフックで、 SIGTERM が、サービスから切り離されたあとに送られるように待機させる。 アプリケーション側でSIGTERMシグナルを適切にハンドリングして、リク エス トの処理が終了した後でアプリが終了するように実装する。 terminationGracePeriodSeconds を十分な時間設定して、 SIGKILL が、リク エス トの処理+ SIGTERM ハンドリング処理が終わったのちにPodに送られるよう待機させる。 図で説明すると、以下のような時系列に処理が行われるようにすることで、Graceful Shutdownは達成できます。 Podが必要な数を維持したままドレインされる。 再調整推奨シグナル が出た際などの、ノードがPodをドレインする必要が生じた際、全てのPodを一度に停止してしまったらどのようなことが起こり得るでしょうか。 リク エス トを受け付けるPodがなくなり、サーバーサイドエラーになってしまい、ワークロードに障害が生じます。 このような状況を防ぐために PodDisruptionBudget、通称PDB という機能が存在しています。これは、ノードがPodをドレインする際に、Podが一度に停止できる数を制限できます。 これは、最大停止数 maxUnavailable , 最少起動数 minAvailable を、レプリカ数または割合を指定することで適用できます。 apiVersion : policy/v1beta1 kind : PodDisruptionBudget metadata : name : my-pdb spec : minAvailable : 2 # ここには最小起動数を指定します。数値または割合を指定できます。 #maxUnavailable: 1 またはこのように、最大停止数を指定できます。 selector : matchLabels : app : my-app # ここには、この PDB を適用する Pod のラベルを指定します。 Podが正常であるという状態を適切に定義する。 無事新しいスポット インスタンス にPodがスケジュールされたとしても考慮しなければいけないことがあります。それは、Podがリク エス トを受け付けられるかのヘルスチェックを正確にすることです。 Podの準備ができていないにも関わらず、 トラフィック をPodにルーティングした場合、Podはリク エス トを処理できずにエラーになってしまいます。これでは、ワークロードが中断してしまうため、非常に問題です。 これを解決するのは、Readiness Probeです。 Readiness Probeは、httpGetを用いて特定のパスの状態をチェックするなどして、アプリケーションがリク エス トを正常に処理できる状態になっているかを確認します。これにより、DB接続や時間のかかる起動プロセスが全て完了している、つまり トラフィック を受け付ける準備が整ったときにだけ、 トラフィック がPodにルーティングされます。 Pod Readiness Gate AWS Load Balancer Controllerを利用している場合は、 AWS LoadBalancer Controller Pod Readiness Gate を有効にすることを強くお勧めします。仮に、ローリングアップデートが非常に高速に行われてしまい、PodのLoad Balancerターゲットグループへの登録が遅れた場合はどうなるでしょうか。Load Balancerはバックエンドに、リク エス トを送信できる正常なPodがないと判断し、リク エス トを送信せずにワークロードが中断してしまいます。 しかし、なぜこのようなことが生じてしまうのでしょうか。それは、Load Balancerが正常であると判断するタイミングと、 Kubernetes 内でPodが正常であると判断されるタイミングに差があるからです。 このような問題が生じることをPod Readiness Gate機能を利用することで防ぐことができます。この機能は、Pod Readiness Probeに、「Load Balancerのターゲットグループへ登録されていること」という条件を追加できます。この新たな条件により、Podが正常な状態として判断される前に、そのPodがLoad Balancerのターゲットグループに正しく登録されていることが確認されます。 この結果、ローリングアップデートが行われて新しいPodがデプロイされる際、すべてのPodがLoad Balancerに適切に登録されてからリク エス トを処理し始めることが保証されます。 Podの可用性を考慮したスケジュール 次は、Podの可用性について考えてみましょう。先ほど、ノードがPodをドレインする際に PDB を設定することにより、ワークロードを保つために必要な最低限のPod数を指定しました。 しかし、仮に災害や電源の問題など、何らかで、一部のノードが全く利用できなくなった場合、Pod Disruption Budgets ( PDB ) 設定だけでは、Podの最少数を維持できなくなってしまう可能性があります。 例えば、1つのノードに全てのPodがスケジュールされていたとします。その1つのノードがダウンした場合、サービスは停止することになります。 こうしたシナリオを考慮すると、Podを複数のノード、さらに アベイラビリティ ゾーン(以下AZ)に分散する方針が重要になってきます。これにより、単一のノード・AZで問題が発生した場合でも、他のAZに展開されたPodがワークロードを維持できます。 そのため、 Kubernetes の podAntiAffinity や、 topologySpreadConstraints の設定を使用して、Podの分散を制御するべきです。 podAntiAffinityを利用した場合: apiVersion : apps/v1 kind : Deployment metadata : name : myapp spec : replicas : 8 selector : matchLabels : app : myapp template : metadata : labels : app : myapp spec : containers : - name : myapp image : myapp:1.0.0 affinity : podAntiAffinity : preferredDuringSchedulingIgnoredDuringExecution : - weight : 100 podAffinityTerm : labelSelector : matchLabels : app : myapp topologyKey : topology.kubernetes.io/zone topologySpreadConstraintsを利用した場合: apiVersion : apps/v1 kind : Deployment metadata : name : myapp spec : replicas : 8 selector : matchLabels : app : myapp template : metadata : labels : app : myapp spec : containers : - name : myapp image : myapp:1.0.0 topologySpreadConstraints : - maxSkew : 1 topologyKey : topology.kubernetes.io/zone whenUnsatisfiable : DoNotSchedule labelSelector : matchLabels : app : myapp 上記の YAML 設定は、podAntiAffinityとtopologySpreadConstraintsをそれぞれ利用した場合どのようにAZ間の分散を実現するかの例です。 podAntiAffinityの設定は、同じAZ内に同じアプリケーションのPodが配置されることを避けることを示しています。これは preferredDuringSchedulingIgnoredDuringExecution オプションを使用して、スケジューリング時には 可能な限り 考慮されます。 ここでは、topologyKeyを topology.kubernetes.io/zone に設定しているため、同じAZ内ではなく異なるAZにPodが配置されます。 ※ requiredDuringSchedulingIgnoredDuringExecution は利用しないでください。同じAZでのスケジュールが不可になるため、Podの最大数がAZ数に制限されてしまいます。 2つ目の例では、topologySpreadConstraintsを用いて、Podが均等に分散されるよう制約を加えています。ここでは maxSkew を 1 に設定しており、これは任意の2つのAZ間でのPodの最大の数量差を示します。 つまり、各AZのPodの数が1つだけ異なる場合にのみPodのスケジューリングを許可します。topologyKeyは topology.kubernetes.io/zone に設定され、Podは異なるAZに均等に分散されます。 そして whenUnsatisfiable: DoNotSchedule により、制約を満たすことができない場合には新たなPodのスケジューリングが行われません。 このようにして、Podの配置を細かく制御し、ノードやAZの障害からアプリケーションを保護できます。これらの設定を適切に使用することで、災害や電源の問題など、予期しないイベントが発生した場合でも、ワークロードの中断を回避する、または最小限に抑えることができます。 まとめ 今回、お話しさせていただいた内容は以下です。 スポット インスタンス のリソース確保 マネージド型ノードグループの利用 Mixed Instance Policyの利用 Cluster Autoscalerの利用 ノードグループのAZごとの分散 Priority Expanderの利用 スポット インスタンス の中断に備える PodはGraceful Shutdownされるようになっている。 Podは必要な数を維持したままドレインされる。 Podが正常であるという状態を適切に定義されているか。 Podの可用性を考慮してスケジュールされるようになっている。 これらを実践することで、よりスポット インスタンス を安全に効率的にご利用いただけます。また、スポット インスタンス を利用しない場合でも、後半の「スポット インスタンス の中断に備える」の部分を実践していただくことで、より堅牢にサービスを維持していただくことができます。 おわりに X(クロス) イノベーション 本部 クラウド イノベーション センターでは、新卒・キャリア採用問わず共に働いてくれる仲間を探しています。 本記事で紹介した私の働き方や、 クラウド を中心とした業務にご興味をお持ちの方は、ぜひ採用ページよりご応募ください。 執筆: @taiki_ishii 、レビュー: 柴田 崇夫 (@shibata.takao) / 寺山 輝 (@terayama.akira) ( Shodo で執筆されました )
はじめに 前提 この記事の対象とする読者の方 スポットインスタンスについて 2つのシグナル 2分前中断シグナル 再調整推奨シグナル スポットインスタンスのリソース確保 マネージド型ノードグループの利用 Mixed Instance Policyの利用 Cluster Autoscalerを有効にする。 ノードグループのAZごとの分散 Priority Expander スポットインスタンスの中断に備える ワークロードを停止させないために考慮すること Graceful Shutdownされるようになっている。 Podが必要な数を維持したままドレインされる。 Podが正常であるという状態を適切に定義する。 Pod Readiness Gate Podの可用性を考慮したスケジュール まとめ おわりに はじめに こんにちは!2年目にもかかわらずフレックス制度を存分に活用して、普段8:30 – 16:30で業務をしている クラウド イノベーション センターの石井大樹です。 Amazon EKS(以下EKS)にて、スポット インスタンス は利用されていますか? スポット インスタンス は安いけど、いきなりシャットダウンしちゃうから、使い勝手が悪いんでしょう?など、漠然とした不安から利用をためらっていたりはしないでしょうか。 そのぼんやりとした不安を払しょくし、スポット インスタンス 利用を検討する足がかりになれば、と思い今回は記事にまとめさせていただきました。 前提 この記事の対象とする読者の方 Kubernetes クラスタ をEKSで運用している方 EKS環境でスポット インスタンス を利用しようとしている方 既にEKS環境でスポット インスタンス を利用している方 ワークロードを維持する方法に関心がある方 スポット インスタンス について スポット インスタンス は、通常のオンデマンド インスタンス に比べて最大90%の割引で利用できるオプションです。 しかし、オンデマンド インスタンス と異なり、 AWS 側のリソースキャパシティの都合により、 インスタンス の利用が中断される可能性があります。 2つのシグナル 先ほどスポット インスタンス は、「 AWS 側の都合により中断される可能性がある」とお話しいたしました。しかし、実際には2つのシグナルが発出された後に中断が実行されます。このシグナルを利用することで、スポット インスタンス の中断の影響を最小限に収めることができます。 2分前中断シグナル スポット インスタンス の終了の2分前に通知されるものです。この通知を受け取って2分後に、スポット インスタンス は終了します。 再調整推奨シグナル 対象スポット インスタンス の中断可能性の高まりが検知された場合、2分前シグナルを受け取る前に通知されるシグナルです。このシグナルを受け取ったタイミングで必要な処理が行われるように事前に準備することで、スポット インスタンス を安定して安全に運用できます。 Amazon EC2 AutoScalingグループのCapacity Rebalancingが有効化されている場合、再調整推奨シグナルを受け取ると、 Amazon EC2 AutoScalingは、代替となる新しい インスタンス の起動を試みます。その 新しい インスタンス が起動した後、既存の インスタンス はシャットダウンされます。 なお、EKSノードグループのAutoScalingグループでCapacity Rebalancingを有効化したい場合は、後述するマネージド型ノードグループを利用することで自動的に有効化されます。セルフマネージド型ノードグループで利用をする際は、手動で設定をする必要があります。 また、セルフマネージド型ノードグループにて、再調整推奨シグナルを受け、後述するGraceful Shutdownなどのアクションの実行をするには、 AWS Node Termination Handler などの導入が必要です。セルフマネージド型ノードグループには、 シグナルをキャッチしてノード上のPodの安全な待避を行う仕組みがないためです。 ※注意 この再調整推奨シグナルは2分前中断シグナルよりも前に通知されるということを保証していません。予期せぬリソースの需要が AWS 側で生まれた場合は、2分前中断シグナルと同時に通知を受け取り、必要な対処ができずにワークロードが中断される可能性があります。 再調整推奨シグナルについて スポット インスタンス のリソース確保 せっかくスポット インスタンス の利用を開始したのにも関わらず、スポット インスタンス のリソースが十分に確保できずにワークロードを中断してしまっては元も子もありません。この章では、スポット インスタンス を利用しながら十分なリソースを確保し続ける方法をこちらでご紹介します。 マネージド型ノードグループの利用 EKSでは、マネージド型ノードグループを利用することにより、様々な恩恵を開発者は受けることができます。この恩恵はスポット インスタンス の利用時も例外ではありません。これを利用することにより、EKSでスポット インスタンス を利用する際に AWS がベストプ ラク ティスとして定めた内容を自動的に設定してくれます。 以下が対象の設定内容の一部分です。 配分戦略を capacity-optimized にする。 これにより、中断確率の低いスポット インスタンス が配分されます。 Capacity Rebalancing を有効化。 これにより、 再調整推奨シグナル を受けたときに、EKSは自動的に中断可能性の低いスポットキャパシティプールからスポット インスタンス を起動し、そのノードが準備完了になったら、既存のスポット インスタンス のドレインをしてくれます。 Mixed Instance Policyの利用 複数のEC2 インスタンス タイプをノードグループで利用できるようにすることで、スポット インスタンス を利用できる確率が高まります。利用候補となる インスタンス タイプが増えるためです。しかし、注意することが1点あります。それは、vCPU・メモリが同等の インスタンス タイプであるべきということです。 例えば、 CPU: 2vCPU , メモリ: 4GiB の c5a.large を利用する際は、CPU: 2vCPU , 、メモリ: 4GiB の c5d.large 等と利用されるべきということです。 vCPU・メモリが異なる インスタンス タイプを混在させると、Cluster Autoscalerが正常にリソースの計算ができなくなり、意図しないスケールにつながってしまいます。 Cluster Autoscalerを有効にする。 Cluster Autoscalerは Kubernetes クラスタ を自動的に調整してくれる非常に有効なエコシステムです。このエコシステムは導入のみでも十分な効果を発揮しますが、以下に紹介する機能を利用することでさらなる効果が期待できます。 ※Auto Scalingグループの最大・最少ノード数は尊重されます。そのため、Cluster Autoscalerは最大ノード数以上・最少ノード数以下にはスケールできません。 ノードグループのAZごとの分散 ノードグループを、利用しているリージョンのAZを跨るように作成するようにします。利用候補となる インスタンス が増えるためです。そうすることで、スポット インスタンス を利用できる確率を高めることができます。 Priority Expander しかし、どれだけ対策を練っても、スポット インスタンス が確保できない可能性があります。そんなときは、オンデマンド インスタンス を代わりに確保するようにしましょう。値段は割引がされていない定価で利用することになりますが、ワークロードを中断させないことが最優先です。 Cluster AutoscalerのPriority Expander を利用することで、スポット インスタンス が見つからない際は、オンデマンド インスタンス のノードグループをスケールさせることで不足したリソースを補填できます。 以下が設定例です。 正規表現 で spot に該当するノードグループが優先してスケールされるように設定しています。 apiVersion : v1 kind : ConfigMap metadata : name : cluster-autoscaler-priority-expander namespace : kube-system data : priorities : |- 50 : - .*spot.* 1 : - .*ondemand.* スポット インスタンス の中断に備える ここまでは、できるだけスポット インスタンス を多くの割合で利用する方法をご紹介いたしました。 ご紹介した通り、スポット インスタンス は中断させられてしまうものです。この中断の際にワークロードの実体であるPodに対してなんにも考慮に入れていない場合、ワークロードは中断してしまいます。 ここからはスポット インスタンス が切り替わる際にもワークロードを安全に保つための方法をご紹介します。 ワークロードを停止させないために考慮すること ワークロードを中断させないために考慮する必要があることが4つあります。 PodはGraceful Shutdownされるようになっている。 Podは必要な数を維持したままドレインされる。 Podが正常であるという状態を適切に定義されている。 Podの可用性を考慮してスケジュールされるようになっている。 Graceful Shutdownされるようになっている。 再調整推奨シグナル を受け取り、EKSがノードのドレインを開始して、瞬時にPodが削除されてしまった場合、問題が発生する可能性があります。その問題について理解するには、まずPodの削除が始まると何が起こるかを理解する必要があります。 Podの削除が始まると以下が行われます。 Podの終了処理 ServiceからのPodの切り離し これらが、問題の生じない順序で行われたら良いのですが、 Kubernetes ではこれらは個別のプロセスとして別々に行われるので、それは保証されません。 順序が異なる場合、以下のような問題が生じます。 リク エス トを受け付けたが、処理するPodが削除されてしまい、エラーになる。 また、Podが最後のリク エス トを受け付けたはいいが、処理している間にPodが終了されてしまった場合はどうなるでしょうか。このような問題が生じます。 リク エス トの処理が終わっていないにもかかわらずPodが削除されてしまい、エラーになる。 このような状況が生じ、ワークロードに影響を及ぼさないためにもPodがGraceful Shutdownをするように適切に設定しなければいけません。 この問題を解決するには2つの対策が必要です。 以下では、2つの対策・Podのライフサイクルを語る上では非常に重要な用語を用いて解説します。 SIGTERM :終了処理を開始するように命令するシグナル SIGKILL :コンテナを強制的にシャットダウンするように命令するシグナル terminationGracePeriodSeconds : deletionTimeStampが設定されてから何秒でSIGKILLをおくるかの設定 - preStopと、SIGTERMはこの時間内に終わらせることが必要です。 preStop : Podが終了する前に実行される処理 preStop ライフサイクルフックで、 SIGTERM が、サービスから切り離されたあとに送られるように待機させる。 アプリケーション側でSIGTERMシグナルを適切にハンドリングして、リク エス トの処理が終了した後でアプリが終了するように実装する。 terminationGracePeriodSeconds を十分な時間設定して、 SIGKILL が、リク エス トの処理+ SIGTERM ハンドリング処理が終わったのちにPodに送られるよう待機させる。 図で説明すると、以下のような時系列に処理が行われるようにすることで、Graceful Shutdownは達成できます。 Podが必要な数を維持したままドレインされる。 再調整推奨シグナル が出た際などの、ノードがPodをドレインする必要が生じた際、全てのPodを一度に停止してしまったらどのようなことが起こり得るでしょうか。 リク エス トを受け付けるPodがなくなり、サーバーサイドエラーになってしまい、ワークロードに障害が生じます。 このような状況を防ぐために PodDisruptionBudget、通称PDB という機能が存在しています。これは、ノードがPodをドレインする際に、Podが一度に停止できる数を制限できます。 これは、最大停止数 maxUnavailable , 最少起動数 minAvailable を、レプリカ数または割合を指定することで適用できます。 apiVersion : policy/v1beta1 kind : PodDisruptionBudget metadata : name : my-pdb spec : minAvailable : 2 # ここには最小起動数を指定します。数値または割合を指定できます。 #maxUnavailable: 1 またはこのように、最大停止数を指定できます。 selector : matchLabels : app : my-app # ここには、この PDB を適用する Pod のラベルを指定します。 Podが正常であるという状態を適切に定義する。 無事新しいスポット インスタンス にPodがスケジュールされたとしても考慮しなければいけないことがあります。それは、Podがリク エス トを受け付けられるかのヘルスチェックを正確にすることです。 Podの準備ができていないにも関わらず、 トラフィック をPodにルーティングした場合、Podはリク エス トを処理できずにエラーになってしまいます。これでは、ワークロードが中断してしまうため、非常に問題です。 これを解決するのは、Readiness Probeです。 Readiness Probeは、httpGetを用いて特定のパスの状態をチェックするなどして、アプリケーションがリク エス トを正常に処理できる状態になっているかを確認します。これにより、DB接続や時間のかかる起動プロセスが全て完了している、つまり トラフィック を受け付ける準備が整ったときにだけ、 トラフィック がPodにルーティングされます。 Pod Readiness Gate AWS Load Balancer Controllerを利用している場合は、 AWS LoadBalancer Controller Pod Readiness Gate を有効にすることを強くお勧めします。仮に、ローリングアップデートが非常に高速に行われてしまい、PodのLoad Balancerターゲットグループへの登録が遅れた場合はどうなるでしょうか。Load Balancerはバックエンドに、リク エス トを送信できる正常なPodがないと判断し、リク エス トを送信せずにワークロードが中断してしまいます。 しかし、なぜこのようなことが生じてしまうのでしょうか。それは、Load Balancerが正常であると判断するタイミングと、 Kubernetes 内でPodが正常であると判断されるタイミングに差があるからです。 このような問題が生じることをPod Readiness Gate機能を利用することで防ぐことができます。この機能は、Pod Readiness Probeに、「Load Balancerのターゲットグループへ登録されていること」という条件を追加できます。この新たな条件により、Podが正常な状態として判断される前に、そのPodがLoad Balancerのターゲットグループに正しく登録されていることが確認されます。 この結果、ローリングアップデートが行われて新しいPodがデプロイされる際、すべてのPodがLoad Balancerに適切に登録されてからリク エス トを処理し始めることが保証されます。 Podの可用性を考慮したスケジュール 次は、Podの可用性について考えてみましょう。先ほど、ノードがPodをドレインする際に PDB を設定することにより、ワークロードを保つために必要な最低限のPod数を指定しました。 しかし、仮に災害や電源の問題など、何らかで、一部のノードが全く利用できなくなった場合、Pod Disruption Budgets ( PDB ) 設定だけでは、Podの最少数を維持できなくなってしまう可能性があります。 例えば、1つのノードに全てのPodがスケジュールされていたとします。その1つのノードがダウンした場合、サービスは停止することになります。 こうしたシナリオを考慮すると、Podを複数のノード、さらに アベイラビリティ ゾーン(以下AZ)に分散する方針が重要になってきます。これにより、単一のノード・AZで問題が発生した場合でも、他のAZに展開されたPodがワークロードを維持できます。 そのため、 Kubernetes の podAntiAffinity や、 topologySpreadConstraints の設定を使用して、Podの分散を制御するべきです。 podAntiAffinityを利用した場合: apiVersion : apps/v1 kind : Deployment metadata : name : myapp spec : replicas : 8 selector : matchLabels : app : myapp template : metadata : labels : app : myapp spec : containers : - name : myapp image : myapp:1.0.0 affinity : podAntiAffinity : preferredDuringSchedulingIgnoredDuringExecution : - weight : 100 podAffinityTerm : labelSelector : matchLabels : app : myapp topologyKey : topology.kubernetes.io/zone topologySpreadConstraintsを利用した場合: apiVersion : apps/v1 kind : Deployment metadata : name : myapp spec : replicas : 8 selector : matchLabels : app : myapp template : metadata : labels : app : myapp spec : containers : - name : myapp image : myapp:1.0.0 topologySpreadConstraints : - maxSkew : 1 topologyKey : topology.kubernetes.io/zone whenUnsatisfiable : DoNotSchedule labelSelector : matchLabels : app : myapp 上記の YAML 設定は、podAntiAffinityとtopologySpreadConstraintsをそれぞれ利用した場合どのようにAZ間の分散を実現するかの例です。 podAntiAffinityの設定は、同じAZ内に同じアプリケーションのPodが配置されることを避けることを示しています。これは preferredDuringSchedulingIgnoredDuringExecution オプションを使用して、スケジューリング時には 可能な限り 考慮されます。 ここでは、topologyKeyを topology.kubernetes.io/zone に設定しているため、同じAZ内ではなく異なるAZにPodが配置されます。 ※ requiredDuringSchedulingIgnoredDuringExecution は利用しないでください。同じAZでのスケジュールが不可になるため、Podの最大数がAZ数に制限されてしまいます。 2つ目の例では、topologySpreadConstraintsを用いて、Podが均等に分散されるよう制約を加えています。ここでは maxSkew を 1 に設定しており、これは任意の2つのAZ間でのPodの最大の数量差を示します。 つまり、各AZのPodの数が1つだけ異なる場合にのみPodのスケジューリングを許可します。topologyKeyは topology.kubernetes.io/zone に設定され、Podは異なるAZに均等に分散されます。 そして whenUnsatisfiable: DoNotSchedule により、制約を満たすことができない場合には新たなPodのスケジューリングが行われません。 このようにして、Podの配置を細かく制御し、ノードやAZの障害からアプリケーションを保護できます。これらの設定を適切に使用することで、災害や電源の問題など、予期しないイベントが発生した場合でも、ワークロードの中断を回避する、または最小限に抑えることができます。 まとめ 今回、お話しさせていただいた内容は以下です。 スポット インスタンス のリソース確保 マネージド型ノードグループの利用 Mixed Instance Policyの利用 Cluster Autoscalerの利用 ノードグループのAZごとの分散 Priority Expanderの利用 スポット インスタンス の中断に備える PodはGraceful Shutdownされるようになっている。 Podは必要な数を維持したままドレインされる。 Podが正常であるという状態を適切に定義されているか。 Podの可用性を考慮してスケジュールされるようになっている。 これらを実践することで、よりスポット インスタンス を安全に効率的にご利用いただけます。また、スポット インスタンス を利用しない場合でも、後半の「スポット インスタンス の中断に備える」の部分を実践していただくことで、より堅牢にサービスを維持していただくことができます。 おわりに X(クロス) イノベーション 本部 クラウド イノベーション センターでは、新卒・キャリア採用問わず共に働いてくれる仲間を探しています。 本記事で紹介した私の働き方や、 クラウド を中心とした業務にご興味をお持ちの方は、ぜひ採用ページよりご応募ください。 執筆: @taiki_ishii 、レビュー: 柴田 崇夫 (@shibata.takao) / 寺山 輝 (@terayama.akira) ( Shodo で執筆されました )
金融ソリューション事業部の石沢です。ふだんは所属部門の様々なプロジェクトの支援をしています。今回の記事では、当社でやっている 「伴走型研修」 を受講者の立場で紹介します。資格試験については会社の補助で制限なく受験できるのですが、それでも勉強するのは大変ですよね。 くじけずにやりきる ために今回は伴走型研修というものを利用して、無事に資格合格したというお話です。 受験のきっかけ 今回挑戦したのはタイトルにある通り、 AWS Certified Solutions Architect - Associate(以下SAA)です。さすがに説明は不要だと思いますが、詳細は AWS さんのサイトをご確認ください。 AWS Certified Solutions Architect – Associate 認定 自分は、より初心者向けの AWS Certified Cloud Practitionerという資格を3年前に取得していました。この資格の有効期限が切れるので、再受験して維持するのか、より上位の資格にチャレンジを悩んでいたところでした。悩んだポイントは 実務としての AWS を利用したシステム構築経験がない SAAは中級向けの資格であり、難易度はそこそこ高いらしい 割と忙しいので受験を先延ばしにしたり、挫折することになりそう です。組織的にはそれなりの職級にいるので、別に上司から資格を取れと言われるわけでもありません。どうしよっかなー、失敗したら恥ずかしいしパスしようかなー……。 と悩んでいたところに、伴走型研修の案内が来たのでした。 伴走型研修とは もともと社では AWS 認定資格受験希望者に対して学習コンテンツや学習用環境、資格受験費用の全額補助が提供されているのですが、さらに昨年から任意参加型の「伴走型研修」も利用できるようになっていました。 伴走型研修では、以下のサポートを受けることができます(2023年現在の情報) キックオフウェビナーでの、学習の進め方についての全体ガイダンス 講師による個別面談(スキルや状況に応じた学習計画についてアド バイス してもらえる) 毎週30分のミニ勉強会(試験問題解説や情報共有など) 学習中につまづいたポイントや不明点に関する相談や質疑応答など 私はSAAコースを選択しましたが、 AWS Certified Solutions Architect - Professionalコースも選択可能です。心強い! 実際にやったこと キックオフウェビナー 資格の概要に始まり、学習方法についてひととおり説明していただき助かりました。インターネットにたくさんの情報が公開されているのですが、書いてある内容はバラバラなので、まとめて確認し質疑応答できるのはかなり助かる! 個別面談 キックオフ後、講師による個別の1on1で学習スケジュールと資格受験日程を決めました。受講の目的や AWS 経験などをアド バイス してもらえます。私の場合はだいたい基礎知識学習1カ月+問題集などを利用した試験対策1カ月の2カ月コースが良さそうということでスケジュールを組みました。また挫折防止のために、試験を予約したほうが良いとのこと。えーっと思いながらも従順に2カ月後の試験予約もしてしまいます。デッドラインが決まって身が引き締まります。 なお、この伴走型研修では学習自体は各自が自由に進めることになります(座学の講義があるわけではない)。おすすめコンテンツなどを紹介いただきながら、書籍、 AWS が公開しているコンテンツやUdemy講座を中心のカリキュラムを自分で選択することになります。私自身は学習期間にあまりまとまった時間が取れない見込みだったので、基礎知識の学習についても書籍やドキュメントなどの読み込みではなく、Udemyの講座でキャッチアップすることにしました(スキマ時間を活用するため)。 毎週30分のミニ勉強会 技術資格の勉強は、わりと孤独なものになりがちです。ですが、伴走型研修では毎週30分のオンライン勉強会があるので孤立感が軽減されていて良かったです。業務時間内でも30分という時間なので調整しやすく、欠席をしても録画でキャッチアップすることができるようになっています。勉強会では講師の出す例題を皆で解いたり、理解しにくい点について解説を受けたりしました。 受講者チャネルでのフォローアップ 上記以外には、受講者の参加するコミュニケーションチャンネル( Microsoft Teamsの専用Teamでのチャット)でのフォローアップがあります。学習コンテンツでわからなかった点や、模擬試験集の解説で腑に落ちない点などについて質問をできます。しっかり活用しました。 自習 伴走型研修で伴走してもらいながら、実際に自分でやった事は以下のとおりです。いずれも個人の費用負担はありません。 ハンズオン用の AWS 学習環境の取得(社内で簡単なフォームに入力するだけ。各種ガード設定付きです) いくつかの AWS 提供ドキュメントの通読(読んでおくと良いとアド バイス されたもの) Udemy:【ベストセラー完全日本語化】AWS 認定ソリューションアーキテクト アソシエイト SAA-C03 対応 2022 最新版 書籍での学習を断念し、こちらの講座をUdemy for Business枠で受講。また講座内のハンズオンは極力自分でも手を動かしています。27時間を少しスピードを上げて視聴しています Udemy:【SAA-C03版】AWS 認定ソリューションアーキテクト アソシエイト模擬試験問題集(6回分390問) 試験対策として有名なコースですね。当初想像していた問題よりこちらの模擬試験のほうが難しく感じ、また最初はまったく正解できず焦りましたが、復習をしっかりやって知識を補完していきました 試験受験 最後は試験の受験です。自宅だと集中しにくいのでテストセンターで受験し、無事に1回で合格できました。 よかったこと 伴走型研修に参加して一番良かったのは、途中でくじけずに完走して予定通りに資格取得が完了したことです。伴走型研修を利用せずに個人学習のみでチャレンジしていたら、おそらくもっと時間がかかったと思います(ズルズルと試験日を延ばしていたはず)。もしかしたら、途中であきらめてしまったかもしれません。 なお、毎週の勉強会で講師から 「進捗どうですか、質問はいつでもどうぞ」 と言われていたのが自分にとっては先延ばし防止効果がありました。別に進捗報告の必要はなく単なる声掛けだったのですが、勉強が出来ていない週は「ウッ」となって、あわててやっていました。 加えて、世の中にあふれている資格情報に惑わされなかったという点も自分にとっては大きかったです。試験日が近づくにつれて、つい 銀の弾丸 を求めてネットをさまよってしまい「〇〇をやればよい」とか「〇〇だけでは不十分だ」といったブログ記事を見てしまい不安になってしまうのですが、信頼できる講師と話ができるので不安もだいぶ緩和されたと思います。 まとめ 伴走型研修というものを使って、 AWS 実務経験なしでもくじけずに資格取得が出来て良かった という話でした。個人の学習で詰まっている人は、伴走者を見つけると良いかもしれません。 もちろん当社に入社していただければ、ここで紹介した伴走型研修そのものに参加することもできます。興味があればぜひ以下のページもご覧ください! 私たちは一緒に働いてくれる仲間を募集しています! ISID 募集職種一覧 執筆: Ishizawa Kento (@kent) 、レビュー: @sato.taichi ( Shodo で執筆されました )