Amazon S3 と Amazon CloudFront を活用したグローバル向けゲームビルドバイナリ共有環境の構築

2024-05-01
デベロッパーのためのクラウド活用方法

Author : 森下 真孝

ゲームなみなさんこんにちは、Game Solutions Architect の森下です。
この投稿では、ゲームのビルドバイナリをグローバルに共有する方法として、Amazon S3 と Amazon CloudFront をご紹介します。

AWS for Games

AWS for Games はより早い開発、よりスマートな運営、そしてより楽しいゲームへの成長という Build、Run、Grow の 3 つの柱に沿ってサポートします。今回は Build の柱 クラウドゲーム開発 のお話になります。

このクラウドレシピ (ハンズオン記事) を無料でお試しいただけます »

毎月提供されるクラウドレシピのアップデート情報とともに、クレジットコードを受け取ることができます。 


概要

ゲーム開発では、開発者や QA 担当者の間でビルドバイナリを共有する場面が発生します。しかし、ゲームのビルドバイナリは数 GB ~ 数十 GB になることも多く、保存領域と転送帯域の確保が課題になります。また、ビルドバイナリは機密性が高いため、セキュリティについての考慮も必要です。

さらに、開発したゲームをグローバル展開する場合、各ローカライズバージョンの動作確認を現地のチームに任せるという方法がよく取られます。この際、ビルドバイナリを世界各地でダウンロードする際に、転送速度が問題になる場合もあります。

本稿では、コスト効率に優れたストレージサービスである Amazon S3 と、グローバルなコンテンツ配信サービスである Amazon CloudFront を活用し、ゲーム開発向けのグローバルかつセキュアなビルドバイナリ共有環境を構築します。


構築するアーキテクチャ

本投稿で構築するアーキテクチャは 2023 年度 GIC の公演 Game production in the Cloud の Build Sharing Soulution を参考にします。

今回構築するアーキテクチャ図を次に示します。

以下、このアーキテクチャにおいて重要な 2 つのポイントを解説します。

Amazon S3 によるビルドバイナリの管理

ビルドバイナリの管理には、Amazon S3 を活用します。Amazon S3 はコスト効率に優れたオブジェクトストレージサービスで、特にゲームビルドのような、大きな単一のファイルを管理する上で非常に優れたソリューションになります。

今回構築するアーキテクチャでは、ビルドサーバーとして稼働している Amazon EC2 上から直接ビルドバイナリのアップロードを行います。ビルドサーバーには Amazon S3 への Permission を有するロールを付与します。

Amazon CloudFront 署名付き URL の活用

このアーキテクチャでは、Amazon CloudFront を活用することでグローバル対応を行います。Amazon CloudFront は、世界中に分散した 600 以上(2024/4 投稿時点)の Point of Presence (PoP) を経由してコンテンツを配信することで、レイテンシーを低減することができるサービスです。

* 2024 年 4 月時点

ただし、コンテンツにアクセスできるのは認証されたユーザーに限る必要があります。そうしないと、S3 にアップロードされている開発中のゲームビルドを誰でもダウンロードできるようになってしまいます。

本稿では、コンテンツへのアクセスの際に RSA 暗号鍵で署名された 署名付き URL を必須とすることでアクセスを制限します。署名付き URL は期限付きのアクセス許可を付与されたコンテンツへの URL です。その署名付き URL が正しい RSA 暗号鍵で署名されたものであれば、誰でもファイルにアクセスすることができます。

本投稿では、署名付き URL の配布をサーバーレスで行います。具体的には、AWS Lambda で署名付き URL を発行する関数を作成し、Amazon API Gateway で API を公開します。API へのアクセスは、Amazon Cognito によるユーザー認証を必須とします。


手順

以降より、アーキテクチャの構築手順をまとめます。

注意: 本投稿の手順を手元のアカウントで試す場合は AWS の利用料金が発生します。リソースの削除忘れには十分ご注意ください。


1. Amazon S3 バケットの作成

まずは、Amazon S3 バケットを作成します。

マネジメントコンソールで Amazon S3 の画面を開き「バケットの作成」をクリックします。

バケットの設定は以下のようにします。

  • AWS リージョン : ap-northeast-1
  • バケット名 : 任意のバケット名を入力します。全世界でユニークでなければならないので、日付やランダムな文字列を組み合わせましょう。

それ以外は全てデフォルトのままで「バケットを作成」をクリックします。


2. Amazon CloudFront の設定

次に、Amazon S3 のコンテンツを配信する Amazon CloudFront ディストリビューションの設定を行います。

RSA 暗号鍵の作成

まず初めに、Amazon CloudFront の署名付き URL を発行するための RSA 暗号鍵を作成します。

以下のコマンドを、AWS CloudShell などで実行してください。ローカルの PC でも構いません。

openssl genrsa -out private_key.pem 2048
openssl rsa -pubout -in private_key.pem -out public_key.pem

作成したキーのうち、秘密鍵は AWS Secrets Manager に保存し、AWS Lambda で暗号化に利用します。公開鍵は Amazon CloudFront に登録し、Amazon CloudFront 内部で利用されます。以下の手順で、暗号鍵の各種設定と Amazon CloudFront ディストリビューションの作成を同時に行います。

AWS Secrets Manager への登録

AWS Lambda で利用する秘密鍵を、AWS Secrets Manager に登録します。

AWS Secrets Manager の画面を開き、「新しいシークレットを保存する」をクリックしてください。

シークレットのタイプその他のシークレットのタイプ とします。その後、キー/値のペア で プレーンテキスト を選択し、秘密鍵の内容をコピペします(すでに入力されている文字列は全て上書きしてください)

クリックすると拡大します

シークレットの名前binary_share_secret とします。

それ以外は全てデフォルトのまま作成してください。

Amazon CloudFront で公開鍵を登録

Amazon CloudFront の画面を開き、左側のメニューから「キー管理」>「パブリックキー」>「パブリックキーを作成」を選択します。

  • 名前 : secret
  • キー : public_key.pem の内容をコピペ

クリックすると拡大します

次に、左側のメニューから「キー管理」>「キーグループ」>「キーグループを作成」を選択します。

  • 名前 : secret_keygroup
  • パブリックキー : 先ほど作成した secret を選択

クリックすると拡大します

Amazon CloudFront ディストリビューションの作成

Amazon CloudFront の画面で、左側のメニューから「ディストリビューション」を選択し、「ディストリビューションを作成」をクリックします。

ディストリビューションの設定は以下の通りとしてください。

  • Origin domain : S3 バケットの名前
  • オリジンアクセス : Origin access control settings

Origin access control は、右にある「Create new OAC」ボタンをクリックし、全てデフォルトのまま Create をクリックしてください。(名前が 64 文字を超えるとエラーになるため、その場合は修正してください)

クリックすると拡大します

ビューワーのアクセスを制限するでは「Yes」を選択し、キーグループを追加で先ほど作成した secret_keygroup を選択します。

クリックすると拡大します

また、ウェブアプリケーションファイアウォール (WAF) の項目では セキュリティ保護を有効にしないでください を選択します。

クリックすると拡大します

それ以外は全てデフォルトで「ディストリビューションを作成」をクリックします。

ディストリビューションの作成後、上部に 「S3 バケットポリシーを更新する必要があります」 というポップアップが出ています。右の「ポリシーをコピー」をクリックしてクリップボードにコピーした後「S3 バケットの権限に移動してポリシーを更新する」リンクをクリックします。

クリックすると拡大します

S3 バケットポリシーの追加

作成したディストリビューションからのアクセスを許可するように、S3 のバケットポリシーを変更します。

バケットポリシー」の項目から「編集」をクリックし、先ほどクリップボードにコピーした内容をコピペしてください。

クリックすると拡大します

なお、上の手順でポリシーをコピーし忘れた場合は、以下のステートメントの S3_BUCKET_ARNCLOUD_FRONT_DISTRIBUTION_ARN を置き換えてコピペを行なってください。

{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "AllowCloudFrontServicePrincipal",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "{S3_BUCKET_ARN}",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": "{CLOUD_FRONT_DISTRIBUTION_ARN}"
                }
            }
        }
    ]
}

3. Lambda 関数の作成

次に、署名付き URL を発行する Lambda 関数を作成します。

マネジメントコンソールで Lambda の画面を開き「関数の作成」をクリックします。

関数の設定は以下のようにします。

  • 関数名 : get_presigned_url
  • ランタイム : Node.js 20.x

それ以外は全てデフォルトで「関数の作成」をクリックします。

コードの入力・デプロイ

Lambda 関数の内容は以下の通りとします。ただし、CLOUD_FRONT_URLPUBLIC_KEYPAIR_ID を正しい値に置き換える必要があります。

import {
  SecretsManagerClient,
  GetSecretValueCommand
} from "@aws-sdk/client-secrets-manager";
import crypto from 'crypto';

const secretsManager = new SecretsManagerClient({region: 'ap-northeast-1'});
const replacementChars = {'+':'-', '=':'_', '/':'~'}

export const getSecretValue = async (secretName = 'binary_share_secret') => {
  const client = new SecretsManagerClient();
  const response = await client.send(
    new GetSecretValueCommand({
      SecretId: secretName,
    }),
  );
  
  if (response.SecretString) {
    return response.SecretString;
  }

  if (response.SecretBinary) {
    return response.SecretBinary;
  }
};

export const handler = async (event) => {
  let expiration = Date.now() + (60 * 30 * 1000);
  let baseUrl = 'CLOUD_FRONT_URL';
  let keyPairId = 'PUBLIC_KEYPAIR_ID';
  let objectName = event.queryStringParameters.object_name;
  
  let queryUrl = baseUrl + "/" + objectName;

  let cannedPolicy = {
    "Statement":[
      {
        "Resource": queryUrl,
        "Condition":{
          "DateLessThan":{
            "AWS:EpochTime": expiration
          }
        }
      }
    ]
  };
  cannedPolicy = JSON.stringify(cannedPolicy);

  let encodedPolicy = new Buffer.from(cannedPolicy).toString("base64");
  encodedPolicy = encodedPolicy.replace(/[+=/]/g, m => replacementChars[m]);

  const signer = crypto.createSign('RSA-SHA1');
  signer.update(cannedPolicy);
  let signedPolicy = signer.sign(await getSecretValue(), 'base64');
  signedPolicy = signedPolicy.replace(/[+=/]/g, m => replacementChars[m]);

  const paramDelimiter = (queryUrl.indexOf('?') === -1) ? '?' : '&';
  const cfSignedUrl = `${queryUrl}${paramDelimiter}Expires=${expiration}&Signature=${signedPolicy}&Key-Pair-Id=${keyPairId}`;

  const response = {
    statusCode: 200,
    body: JSON.stringify(cfSignedUrl),
  };

  return response;
}

CLOUD_FRONT_URL は Amazon CloudFront ディストリビューションの画面で確認できるドメイン名になります。横のコピーボタンをクリックすると https から始まる URL をコピーできます。

クリックすると拡大します

PUBLIC_KEYPAIR_ID は、Amazon CloudFront パブリックキーの画面で確認できる ID になります。

クリックすると拡大します

関数のコードを入力したら、Deploy ボタンも忘れず押してください。

クリックすると拡大します

AWS Secrets Manager へのアクセス許可を追加

AWS Secrets Manager にあるシークレットキーに、Lambada 関数からアクセスできるようにします。

まず、Lambda 関数の実行ロールの ARN を調べます。Lambda 関数の「設定」からロール名をクリックし、IAM の画面でロール ARN を控えます。

クリックすると拡大します

AWS Secrets Manager で作成したシークレットの画面で、「リソースのアクセス許可」>「許可を編集」をクリックします。

クリックすると拡大します

入力欄に以下のコードを入力します。ただし、arn:aws:iam::123456789012:role/MyRole の部分は控えた IAM ロールの ARN に置き換えてください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::123456789012:role/MyRole"
            },
            "Action": "secretsmanager:GetSecretValue",
            "Resource": "*"
        }
    ]
}

4. Cognito の設定

次に Cognito ユーザープールを作成します。

ユーザープールの作成

マネジメントコンソールで Cognito の画面を開き「ユーザープールを作成」をクリックします。

ユーザープールの設定は以下のようにします。設定項目が多いのでご注意ください。
なお、説明で触れていない項目に関しては全てデフォルトとします。

ステップ 1 : サインインエクスペリエンスを設定

 「ユーザー名」と「E メール」のチェックボックスをオン

クリックすると拡大します

ステップ 2 セキュリティ要件を設定

 「多要素認証」の項目で「MFA なし」を選択

クリックすると拡大します

ステップ 3 サインアップエクスペリエンスを設定

全てデフォルトのままとします。

ステップ 4 メッセージ配信を設定

E メールプロバイダー」で「Cognito で E メールを送信」を選択します。

クリックすると拡大します

ステップ 5 アプリケーションを統合

  • ユーザープール名: developer
  • ホストされた認証ページ: 「Cognito のホストされた UI を使用」をオンにします
  • Cognito ドメイン: 任意の文字列とします。グローバルで一意にする必要があります
  • アプリケーションクライアント名: build-binary-share
  • 許可されているコールバック URL: http://localhost (httpsではないので注意)
  • 高度なアプリケーションクライアントの設定 欄を開き「OAuth 2.0 許可タイプ」を「暗黙的な付与」のみとします

クリックすると拡大します

クリックすると拡大します

クリックすると拡大します

許可されているコールバックに http://localhost を指定していますが、これにより、Cognito の 認証ページからローカルに立ち上げたダウンロードアプリへ認証情報を引き継いでリダイレクトされるようになります。

あとは「次へ」をクリックしてユーザープールの作成を完了させます。

Cognito ユーザーの作成

動作確認用の Cognito ユーザーを作成します。

作成した developer ユーザープールの画面を開き「ユーザー」の欄で「ユーザーを作成」をクリックします。

クリックすると拡大します

ユーザーの設定は以下のようにします。

  • 招待メッセージ : 「E メールで招待を送信」を選択
  • ユーザー名 : 任意のものを入力
  • E メールアドレス : メールを受け取りたいアドレスを入力
  • 仮パスワード : 「パスワードの生成」を選択

クリックすると拡大します

入力したメールに Your temporary password というタイトルでメールが来ます。本文に仮パスワードが記載されているので控えておきましょう (メール本文末尾のピリオドはパスワードではないのでご注意ください)

認証ページの確認

設定は以上ですが、念のためユーザーが作成できているかを確認します。

developer ユーザープールの画面から「アプリケーションの統合 」> 「アプリクライアントと分析」と遷移し、binary-share-client をクリックします。

〜〜〜

クリックすると拡大します

アプリケーションクライアントの画面に遷移後「ホストされた UI を表示」をクリックします。

クリックすると拡大します

Cognito の認証画面に遷移するので、ユーザー名と仮パスワードを入力します。初回ログイン時はパスワードの変更を求められるので任意のパスワードを入力します。

正しく認証できれば、ブラウザのエラー画面に遷移するかと思います。まだ localhost にダウンロード用のアプリケーションサーバを立てていないので、これは正常な動作になります。


5. Amazon API Gateway の設定

API Gateway のエンドポイントを作成し、Lambda 関数のトリガーとして設定します。

エンドポイントの作成

マネジメントコンソールで Lambda の画面を開きます。

Lambda 関数 get_presigned_url の画面で「関数の概要」から「トリガーを追加」をクリックします。

クリックすると拡大します

トリガーの設定画面では以下のようにします。

  • ソースを選択 : 「API Gateway」を選択
  • Create a new API のラジオボタンをオン
  • Security :「Create JWT authorizer」を選択
  • Identity source : $request.header.Authorization
  • Issuer : https://cognito-idp.ap-northeast-1.amazonaws.com/{ユーザープールID}
  • Audience : binary-share-client のクライアントID

ユーザープール ID と クライアント ID は cognito の画面で確認可能です。

クリックすると拡大します

クリックすると拡大します

設定が終わったら「追加」をクリックします。

CORS の設定

次に、CORS の設定を行います。この設定をしておかないと、localhost に立てたダウンロード用のアプリケーションから API の呼び出しができません。

マネジメントコンソールで API Gateway の画面を開き、先ほど作成した get_presigned_url-API の画面を開きます。

まず、API に紐づくメソッドを GET だけに変更します。左のメニューから「Routes」を選択し、ルートの「ANY」を選択した後、「ルートの詳細」にある「編集」ボタンをクリックします。

クリックすると拡大します

メソッドの設定を ANY から GET に変更して「保存」をクリックします。

クリックすると拡大します

次に、左のメニューから「CORS」をクリックして、「設定」をクリックします。

(画像は設定後の画面です)

クリックすると拡大します

CORS の設定は以下の通りとします。(追加ボタンを押す必要があります)

  • Access-Control-Allow-Origin: *
  • Access-Control-Allow-Headers: authorization
  • Access-Control-Allow-Methods: *

それ以外はデフォルトのままとして「保存」をクリックします。


6. ビルドマシンの立ち上げ

ビルドマシンを想定して EC2 インスタンスを立ち上げます。本投稿においてゲームのビルド作成は本筋ではないため、あくまでも仮想のビルドマシンとします。

インスタンスの設定

マネジメントコンソールで EC2 の画面を開き「インスタンスを起動」をクリックします。

インスタンスタイプが t2.micro(無料利用枠の対象) であることを確認します。

基本的な設定は何も変えずに「高度な詳細」を開きます。「IAM インスタンスプロフィール」の横にある「新しい IAM プロファイルの作成」をクリックします。

クリックすると拡大します

ビルドマシン用 IAM ロールの作成

IAM ロールの設定画面に遷移するので「ロールを作成」をクリックします。

IAM ロールの設定は以下の通りとします。

  • サービスまたはユースケース : EC2
  • 許可ポリシー : 検索欄に AmazonS3FullAccess と入力しチェックボックスをオンにします
  • ロール名 : BuildServerRole

クリックすると拡大します

最後に「ロールを作成」をクリックします。

ロールのアタッチ

EC2 インスタンスの設定画面に戻り「IAM インスタンスプロフィール」の項目で先ほど作ったロール BuildServerRole を選択します。

インスタンスの設定は以上です。「インスタンスを起動」ボタンをクリックしてください。


7. 簡易版ダウンロードアプリの立ち上げ

開発者がビルドバイナリをダウンロードする際に利用する web アプリケーションをローカルに立ち上げます。こちらも、本投稿の本筋ではないため、非常に簡易なもので済ませてしまいます。

以下の javascript コードをローカルに保存し、node で実行してください。例えば server.js として保存したなら node server.js とします。80 番ポートで web サーバーが起動します。

{APIエンドポイントのID} の部分を、作成した API Gateway エンドポイントの ID に置き換える必要があることに注意してください。

const http = require("http");
const port = 80;

const _html = `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>

<label for="objectName">Object Name:</label>
<input type="text" id="objectName" name="objectName" placeholder="Enter object name">
<button onclick="sendApiRequest()">Get</button>
<br>
<label for="presignedUrl">Presigned URL:</label>
<textarea id="presignedUrl" rows="1" cols="50" readonly></textarea>

<script>
    function sendApiRequest() {
        const apiEndpoint = 'https://{APIエンドポイントのID}.execute-api.ap-northeast-1.amazonaws.com/default/get_presigned_url';
        const idToken = window.location.hash.substring(1).split('&').find(param => param.startsWith('id_token=')).split('=')[1];

        // オブジェクト名を取得
        const objectName = document.getElementById('objectName').value;

        // Authorization ヘッダーに id_token をセット
        const headers = new Headers();
        headers.append('Authorization', 'Bearer ' + idToken);

        // APIリクエストパラメータにオブジェクト名を追加
        const queryParams = new URLSearchParams();
        queryParams.append('object_name', objectName);

        // API呼び出し
        const url = apiEndpoint + '?' + queryParams.toString();
        fetch(url, {
            method: 'GET',
            headers: headers,
            mode: 'cors'
        })
        .then(response => response.json())
        .then(data => {
            const presignedUrl = data;
            document.getElementById('presignedUrl').value = presignedUrl;
            console.log('API Response:', data);
        })
        .catch(error => console.error('API Error:', error));
    }
</script>

</body>
</html>
`

const server = http.createServer((request, response) => {
    response.writeHead(200, {
      "Content-Type": "text/html"
    });

    const responseMessage = _html;
    response.end(responseMessage);
});

server.listen(port);
console.log(`The server has started and is listening on port number: ${port}`);

エンドポイントの ID は API Gateway の画面で確認可能です。

(黒塗りにしている部分に表示されているものが ID)

クリックすると拡大します

構築の手順は以上になります ! お疲れ様でした。


使ってみる

アップロード

まずは、ビルドサーバーに見立てた EC2 からファイルをアップロードしてみます。とはいえ、本投稿では touch コマンドで作成したファイルをビルドバイナリとみなして作業を行います。

Amazon EC2 への接続

まず、EC2 に接続します。ビルドサーバーインスタンスを選択し「接続」をクリックします。

クリックすると拡大します

EC2 Instance Connect が選択されていることを確認し、そのままの設定で「接続」をクリックします。

以下のような CLI が開かれれば接続成功です。

クリックすると拡大します

ビルドの作成・アップロード

それでは、ビルドを模したファイルを作成します。以下のコマンドを実行してください。

touch awesome_game_build

次に、以下のコマンドで Amazon S3 へアップロードを行います。

aws s3 cp awesome_game_build s3://{Amazon S3のバケット名}/awesome_game_build

アップロードできているかどうか確認します。マネジメントコンソール上で Amazon S3 のバケットを開き、ファイルが存在するかを確認してください。

クリックすると拡大します

ビルドのダウンロード

改めて 3. Cognito の設定 で確認した認証ページを開き認証を行います。localhost にダウンロードアプリの web サーバーが立ち上がっている場合、ダウンロードページにアクセスできます。

Object Name」にオブジェクト名を入力して「Get」ボタンをクリックすると署名付き URL を取得することができます。

クリックすると拡大します

署名付き URL が有効であることを確認しましょう。ブラウザで署名付き URL を開いた際にファイルがダウンロードできれば成功です!


後片付け

構築したリソースの後片付けを行います。削除するリソースは以下の通りです。

  • Amazon CloudFront ディストリビューションの削除 : Amazon CloudFront の画面からディストリビューションを無効化後削除します。
  • AWS Secrets Manager の削除 : AWS Secrets Manager の画面から作成したシークレットを削除します。
  • Amazon S3 バケットの削除 : Amazon S3 の画面からバケットを削除します。バケットは空にしないと削除できないので注意してください
  • Lambda 関数の削除 : AWS Lambda の画面から関数を削除します
  • API Gateway エンドポイントの削除 : Amazon API Gateway の画面からエンドポイントを削除します
  • Cognito ユーザープールの削除 : Amazon Cognito の画面から ユーザープールを削除します
  • EC2 インスタンスの終了: Amazon EC2 の画面から立ち上げた EC2 インスタンスを終了します。

まとめ

今回は、ゲーム開発におけるビルドバイナリの共有環境を Amazon S3 上に構築し、Amazon CloudFront でグローバルにアクセスする方法について解説しました。Amazon S3 はコスト効率とセキュリティに優れたストレージサービスであり、ビルドバイナリのような単一で大きいファイルを管理する上では非常に優れたソリューションとなり得ます。Amazon CloudFront はグローバルにコンテンツを配信するサービスで、離れた位置にあるデータにも高速にアクセスすることが可能になります。

今回解説した内容が、皆様のゲーム開発の助けになれば幸いです。


builders.flash メールメンバーへ登録することで
AWS のベストプラクティスを毎月無料でお試しいただけます

筆者プロフィール

森下 真孝
アマゾン ウェブ サービス ジャパン合同会社
ソリューションアーキテクト

国内のモバイルゲーム開発会社でサーバーサイドからクライアントサイドまでを担当。
ゲーム業界向けのソリューションアーキテクトとしてゲーム開発に携わるお客様をご支援しております。

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する