UnityでAWSを使うのはゼンゼンコワクナイヨー(シカモ無料デイケルヨ!)

エンジニアですと、AWSをご存知の方ってとっても多いと思います。
ただ、UnityでAWSを使うのって、個人開発だとけっこー敷居が高いと思うんですよね。

ということで、本記事はUnityでAWSを使うのはゼンゼンコワクナイヨーっていうアレです。(アレってなんだ)

ナンデAWS?

筆者はUnityでソシャゲを作ったのですが、最初に使っていたParseというmBaaSが急にサービス終了したためNCMBに乗り換え、以降はNCMBをメインに使っています。

mBaaSのサービスってすごく便利ではあるんですが、安定性や拡張性・使い勝手の面でやや難がある場合が多々あります。
例えばNCMBですと、無料プランの次が月額3万円で、リクエスト数がチョットでもはみ出すと極端にコストが増大したり、ちょくちょくサーバに繋がらなかったりで結構悩まされがち。
(と文句を言いつつも、NCMBは構築がとにかく楽。死ぬほど楽。「とりあえず作ってみる」系のアプリやサービスで引き続き使う予定デス。)

それと比べて、AWSのドコが良いかっていうと、

  • 利用者が非常に多く、情報が得やすい(NCMB、全然情報無い・・・)
  • 「一定レベル」の信頼性(落ちる時は落ちますけどね)
  • スケーリングが自由自在
  • さまざまな種類のAWSサービスが日々リリースされている
  • セキュリティを強固にできる
  • mBaaS関連は意外と無料枠が広い

などなど。

反面、

  • 従量課金&コスト算出に慣れが必要なため、適当に使っていると高くつく
  • 設定が複雑だったりする。学習コストがかかる
  • サービスの種類が多すぎて把握できない

などのデメリットもあるので、ご留意をば。

AWS無料枠ってどんくらい?

参考までに、ゲームでよく使いそうなところをざっくりとご紹介。

Cognito

ユーザ認証の仕組みを提供するCognito。
各種SNSアカウントでの認証やパスワードリマインダーなども備える非常に強力なサービスでゴワス。

Cognito User Poolを使う場合、5万MAUまで無料。
(自分のゲーム、そんなMAUおらんです・・・)

DynamoDB

高速なNoSQLデータベース。
ロギングなどにうってつけ。

データ容量は25GBまで無料で、読み書きはそれぞれ25ユニットまで無料。

ユニットってのがわかりづらいですが、ものすごくシンプルに説明すると
「1秒につき1リクエスト+1リクエストあたり4KB以下のデータを扱うことのできる権利=1ユニット」
です。

25ユニットってことは、4KB以下のちっちゃいデータを扱うのであれば、1秒間に25リクエストまで無料なわけですね。
ちなみに料金の計算は1時間区切りなので、継続して25ユニットを越えない限り無料で使えます。

なお、ユニットは4KB単位のため、仮に15KBのデータを扱う場合は1リクエストで4ユニット消費しちゃうので要注意。

S3

セキュアなファイルストレージです。
ファイル単位で細かなアクセス制限ができたり、配置したファイルを暗号化しておくことなども可能です。

UnityだとサーバにAssetBundleを置いたりすると思いますが、残念ながらS3の無料枠はAWSアカウント登録後12ヶ月間のみ。
12ヵ月間の無料枠は、毎月容量5GB、ファイルダウンロード2万回、ファイルアップロード2000回とやや少なめ。

ただ、S3は無駄遣いしなければ割と安価です。
ムダなコンテンツは置かないようにしたり、DLしたファイルをキャッシュするなど、節約を心掛けてコストは抑えましょー。

どうしてもコストを0にしたければ、ストレージだけ他サービス(例えばGoogleDriveとか、Dropbox)を使うのもアリでしょう。
※その場合、セキュリティには気をつけましょー

Lambda

サーバ側でスクリプトが実行できます。

APIみたいに叩いて実行できるのに加え、スケジュールを組んで実行させることもできるので、毎週のランキング集計+報酬付与とかのバッチ処理も可能。
JavaScriptやC#など、複数言語に対応しているのも魅力です。

無料枠は月のリクエスト回数最大100万回、コンピューティング時間が320万秒。
(コンピューティング時間=処理にかかった時間の合計です)
短時間の処理をチョロっとやるだけであれば、問題無く無料枠で収まりますね。

SNS

リモートPUSHができます。
月100万件まで無料。プッシュプッシュ。

UnityでAWSどう使うん?

Amazon謹製のUnityプラグイン(SDK)があります。
http://docs.aws.amazon.com/ja_jp/mobile/sdkforunity/developerguide/what-is-unity-plugin.html

もっと具体的に!

今回はサクッと試すということで、Cognito Identity Pool・S3・DynamoDBの組み合わせをご紹介。

Cognito Identity Poolでセキュリティのために認証情報を取得、それを使ってS3からファイル取得、DynamoDBにログ書き込みといった感じです。

まずはAWS側でCognito Identity Pool・S3・DynamoDB各種サービスを設定します。(ハマりどころがあるので、後述)
その後、↑のSDKをUnityプロジェクトに放り込んで、チュートリアルに従いセットアップ。
あとは↓のクラスを使えばS3とDynamoDBにサクっと接続できちゃいます。

SDKのメソッドを直で叩いても問題なく値はとれますが、スレッドを気にしないといけないのでコルーチン化してみました。

なお、ものすごく基本的なトコロしか実装していませんので、ご参考程度に。

AWSConnector.cs
using System;
using System.IO;
using System.Collections;
using UnityEngine;
using Amazon;
using Amazon.CognitoIdentity;
using Amazon.S3;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;

public class AWSConnector
{
    const string IDENTITY_POOL_ID = "Cognito Identity PoolのID";
    const string BUCKET_NAME = "S3バケット名";
    static RegionEndpoint COGNITO_REGION = RegionEndpoint.APNortheast1; // リージョン。てきぎへんこー
    static RegionEndpoint S3_REGION = RegionEndpoint.APNortheast1; // リージョン。てきぎへんこー
    static RegionEndpoint DYNAMO_REGION = RegionEndpoint.APNortheast1; // リージョン。てきぎへんこー

    CognitoAWSCredentials credentials;

    public AWSConnector()
    {
        // Amazon Cognito 認証情報プロバイダーを初期化します
        credentials = new CognitoAWSCredentials(
            IDENTITY_POOL_ID,
            COGNITO_REGION
        );
    }

    /// <summary>
    /// S3からテキストデータを取得するコルーチンです
    /// </summary>
    /// <returns>The text data coroutine.</returns>
    /// <param name="fileName">File name.</param>
    /// <param name="isForceDownload">If set to <c>true</c> is force download.</param>
    /// <param name="onFinished">On finished.</param>
    public IEnumerator GetTextFromS3Coroutine(string fileName, bool isForceDownload, Action<bool, string> onFinished)
    {
        var s3Client = new AmazonS3Client(credentials, S3_REGION);

        var cachePath = string.Format("{0}/{1}", Application.temporaryCachePath, fileName);
        if (!isForceDownload && File.Exists(cachePath))
        {
            onFinished(true, File.ReadAllText(cachePath));
            yield break;
        }

        var dirName = Path.GetDirectoryName(cachePath);
        if (!Directory.Exists(dirName))
        {
            Directory.CreateDirectory(dirName);
        }

        var isFinished = false;
        var result = false;
        var resultText = "";
        s3Client.GetObjectAsync(BUCKET_NAME, fileName, (responseObject) =>
            {
                isFinished = true;

                if (null != responseObject.Exception)
                {
                    resultText = responseObject.Exception.ToString();
                    return;
                }

                result = true;
                var response = responseObject.Response;
                if (null != response.ResponseStream)
                {
                    using (var reader = new StreamReader(response.ResponseStream))
                    {
                        resultText = reader.ReadToEnd();
                    }
                }
                else
                {
                    resultText = "";
                }
            });

        while (!isFinished)
        {
            yield return new WaitForSecondsRealtime(0.1f);
        }

        if (result && resultText.Length > 0)
        {
            File.WriteAllText(cachePath, resultText);
        }

        onFinished(true, resultText);
    }

    /// <summary>
    /// DynamoDBにデータを保存するコルーチンです
    /// </summary>
    /// <returns>The to dynamo DBC oroutine.</returns>
    /// <param name="data">Data.</param>
    /// <param name="onFinished">On finished.</param>
    /// <typeparam name="T">The 1st type parameter.</typeparam>
    public IEnumerator PutToDynamoDBCoroutine<T>(T data, Action<bool, string> onFinished)
    {
        var dynamoClient = new AmazonDynamoDBClient(credentials, DYNAMO_REGION);
        var context = new DynamoDBContext(dynamoClient);

        var result = false;
        var resultText = "";
        var isFinished = false;
        context.SaveAsync<T>(data, (resultObject) =>
            {
                isFinished = true;

                if (resultObject.Exception == null)
                {
                    result = true;
                }
                else
                {
                    resultText = resultObject.Exception.ToString();
                }
            });

        while (!isFinished)
        {
            yield return new WaitForSecondsRealtime(0.1f);
        }

        onFinished(result, resultText);
    }
}

DynamoDBに放り込むデータモデル(前述のPutToDynamoDBCoroutine()に渡すオブジェクト)はこんな感じ。
プロパティ名やデータ型はDynamoDB側のテーブル定義と合わせましょう。

PlayLog.cs
using System;
using Amazon.DynamoDBv2.DataModel;

[DynamoDBTable("DynamoDBのテーブル名")]
public class PlayLog
{
    [DynamoDBHashKey]
    public string id { get; set; }

    [DynamoDBProperty]
    public string userId { get; set; }

    [DynamoDBProperty]
    public DateTime createdAt { get; set; }
}

初めてだとハマるであろうことを先に解説

Cognito Identity Pool・S3・DynamoDBの基本設定は、公式チュートリアルやWebの情報に従えばサクッとできると思います。
どこでハマるかっていうと、たぶんIAM周り。

IAMって?

権限の管理サービスです。
コレのお陰でセキュリティを重視したサービスを組み立てることが可能です。

今回はCognito Identity Poolで認証情報を得て、それをS3やDynamoDBに接続するために使うのですが、IAMでS3&DynamoDBに接続するための権限を与えておかないと、認証情報を使ってもサービスに接続できません。

IAMでロールを設定する

AWSからIAMを選択し、ロールを設定しましょう。

ロールが複数ある!?

Cognitoで設定を進めていくと、IAMに2種類のロールができています。
ロールはそれぞれ

  • ○○Auth_Role Cognito User Poolでログイン認証したユーザのロール
  • ○○Unauth_Role Cognito Identity Poolでアプリ認証だけした場合のロール(今回使うヤツ)

ですので、ご注意をば。

設定の流れ

ロールの設定はこんな感じ。
まず、○○Unauth_Roleを選択。
スクリーンショット 2017-12-06 15.24.29.png

続いて、インラインポリシーの追加。
スクリーンショット 2017-12-06 15.26.45.png

そして、カスタムポリシーを追加します。(内容は後述)
スクリーンショット 2017-12-06 15.27.19.png

カスタムポリシーのサンプル

参考ということで、前述のプログラムを実行するためのカスタムポリシーを記載しておきます。
ポリシーの名前はテキトーにつけてください。

S3のカスタムポリシー(ダウンロードのみ)
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": "S3 BucketのARN"
        }
    ]
}
DynamoDBのカスタムポリシー(書き込みのみ)
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "dynamodb:DescribeTable",
                "dynamodb:UpdateItem",
                "dynamodb:PutItem"
            ],
            "Resource": [
                "DynamoDB 対象テーブルのARN"
            ]
        }
    ]
}

まとめ

各サービスの初期設定とか、途中めっちゃ端折ったのに長くなってしまいました。
なんだかんだでAWSはややこしいってコトですかねぇ。

最初からAWS使っとけば、ゲームの規模が大きくなっても安心ってのは間違いないです。
スケールアウトも自由自在、サービスの種類も多種多用ですので。

ただ、mBaaSにも素晴らしいものはたくさんあるので、自分のゲームには何が適しているか、求めているモノは何かを考えつつ導入を検討しましょー。