【Docker】コンテナから外部サイトにアクセスできず、Imageのbuildに失敗する

はじめに

こんにちは、SHOJIです。

Ruby on Rails 6 実践ガイド」という書籍をハンズオンで流していたときに遭遇したエラーです。本が悪いわけではなく、環境によっては今後も遭遇するであろう内容なので備忘録として残します。

docker-compose up -d が通らない

書籍の手順に沿ってGitから指定のリポジトリをダウンロードし、 docker-compose up -d したところ、Run bundle install コマンドのところで以下のエラーが発生しました。

Retrying fetcher due to error (2/4): Bundler::HTTPError Could not fetch specs from https://rubygems.org/ Retrying fetcher due to error (3/4): Bundler::HTTPError Could not fetch specs from https://rubygems.org/ Retrying fetcher due to error (4/4): Bundler::HTTPError Could not fetch specs from https://rubygems.org/ Could not fetch specs from https://rubygems.org/ ERROR: Service 'web' failed to build: The command '/bin/sh -c bundle install' returned a non-zero code: 17

調査の流れ

ログから docker-compose.yml で定義しているweb serviceのビルド時に発生していることがわかったので、そこから順番に調査を行いました。

  1. web serviceのbuildを見て、同階層のDockerFileを基にビルドしていることを確認
  2. DockerFileのFROMを見て、使用しているImageとバージョンを確認
  3. docker run -dit [Image name & version] コマンドでImageからクリーンなコンテナを作成
  4. docker exec -it [Container id] /bin/bash コマンドでコンテナにアクセス
  5. ping rubygems.org コマンドで接続できないことを確認
  6. コンテナから抜けて、ホスト側でping rubygems.org コマンドで接続できることを確認
  7. 同じくホスト側でnslookup rubygems.org コマンドでIPアドレスを確認
  8. 再びコンテナに入り、ping [IPアドレス] コマンドで、IPアドレス指定なら接続できることを確認 !DNSに問題がある!
  9. echo 'nameserver 8.8.8.8' >> /etc/resolv.conf && ping rubygems.org コマンドで接続できることを確認
    ※8.8.8.8はGoogleが提供しているDNSサーバ

解決方法

ここまでの調査でDNSサーバの設定ができていないことが原因であることはわかっています。そのため、あまりスマートとは言えない解決方法ですが、DockerFileのエラーの出るコマンドの前に echo 'nameserver 8.8.8.8' >> /etc/resolv.conf && と付けてあげれば解消できます。

おわりに

同じ手順をMacで行うとエラーが出ないんですよね。同じImageからコンテナを作っているのにホスト環境によって結果が違うのが不思議です。いつものごとく、そこまでは調べる時間が惜しいので一旦ここまでで終わりたいと思います。

【読書感想文】ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本

はじめに

こんにちは、SHOJIです。

ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本」を読みました。想像していたよりもさらにずっと良い本でした。

ドメイン駆動設計入門ってどんな本?

プログラマなら避けては通れない?、エリック・エヴァンスのドメイン駆動設計、いわゆるDDDを理解しやすいパターンから解説している本です。

www.amazon.co.jp

避けては通れないと言いつつ、恥ずかしながら僕はまだ原典を読めていません。

原点を読もうとしたのですが、周囲の評判や書籍のレビューを読むにワンクッション挟まないと理解に苦しみそうと感じ、本書を先に読みました。

書籍の構成

目次

  1. ドメイン駆動設計とは
  2. システム固有の値を表現する「値オブジェクト」
  3. ライフサイクルのあるオブジェクト「エンティティ」
  4. 不自然さを解決する「ドメインサービス」
  5. データにまつわる処理を分離する「リポジトリ
  6. ユースケースを実現する「アプリケーションサービス」
  7. 柔軟性をもたらす依存関係のコントロール
  8. ソフトウェアシステムを組み立てる
  9. 複雑な生成処理を行う「ファクトリ」
  10. データの整合性を保つ
  11. アプリケーションを1から組み立てる
  12. ドメインのルールを守る「集約」
  13. 複雑な条件を表現する「仕様」
  14. アーキテクチャ
  15. ドメイン駆動設計のとびらを開こう
  16. ソリューション構成

最初にDDDとは何ぞや?の話があり、その後、DDDにおける重要なキーワードがどのようなものかを解説していくという構成です。

チャプター2の値オブジェクトの説明の時点で、実際の使用を想定したコードが記載されており、章を追うごとに、①新たに解決したい課題、②その解決に役立つDDDの概念、③概念を適用したコードの3点セットで解説がなされるので、非常に理解がしやすいです。

チャプター6までに基本的なキーワードの解説があり、チャプター7からはデザインパターンと組合せてプログラムの設計をブラッシュアップしていきます。最後にはソフトウェアアーキテクチャを取り入れた設計についても触れられています。

読んだ感想

原典を読めていないため、DDDをどの程度網羅しているのかは分かりかねますが、本書単体で見れば非常に良い本でした。

ある程度の経験があれば、自身の経験や有識者の考えに触れる中で持っている自分なりの「良い設計」があると思いますが、それを分かりやすく体系化してくれています(体系化の作業はDDDの功績なのでしょうが、分かりやすさでいうと本書の功績は大きいはずです)。また、僕の場合は、自分なりの「良い設計」では答えを出せていなかったところや、そもそも問題と認識できていなかったところについても触れられており、今後書くコードに活かせる知見がたくさんありました。

新入社員向けにクラスやメソッドをどう設計するのがいいか、といったレクチャーをするときにも使えそうです。後半は経験が少ないと理解は難しいかもしれませんが、前半は取っつきやすいので、リーダブルコードあたりとセットにすれば、プログラマの言う「分かりやすい」がどういうものなのか知ってもらえるんじゃないかと思います。

ただ、書籍内でも触れられていましたが、いささかパターンに偏った構成になっている感じはあって(分かりやすさのために仕方ないとは思います)、このパターンの土台となる思想は原典を読んで理解する必要があるなと思いました。

土台となる考え方というのは、往々にして抽象的で分かりにくいものなので、そういう意味では分かりやすかったことで逆に欠けている部分があるように感じられました。

おわりに

原典は読まないといけないし読むんですけれど、他にも読みたい本が多すぎて、すぐには読めなそうです。

ただ、案件の募集に「DDDに基づいた設計ができること」のような記載がある程度には知ってて当たり前のものになっているので、時間を見つけて少なくとも一回は読んでおかないとまずいなと思っています。

本でいうと読む読む言いながら読めていない本だらけなので、年内に読めたらいい方かな。

【Unity】Barracudaを使用したMachineLearningの組み込み

はじめに

こんにちは、SHOJIです。

今回はUnityで機械学習を用いたゲームを作る際にハマったポイントを備忘録的に残していきます。

画像認識だったので画像処理についても記載するので結構雑多です。

Kerasで作成したモデルをONNXに変換する

  1. 事前にJupyter NotebookやGoogle Colaboratoryで学習済みモデルを作成します。

  2. モデルをSaveModelで保存します。SaveModelでの保存は拡張子を指定せずにmodel.saveを呼び出すことで行えます。

    model.save('saved_model/my_model')
    
    www.tensorflow.org

  3. pip install tf2onnxでONNX変換のためのライブラリをインストールします。

  4. python -m tf2onnx.convert --saved-model my_model --output my_model.onnx コマンドでONNXに変換されます。

Barracuda packageの追加

  1. Unity Editorを起動し、PackageManagerを開きます。

  2. 「+」-「Add package from git URL...」からBarracudaのGitリポジトリhttps://github.com/Unity-Technologies/barracuda-release.git)を指定します。 github.com

推論用スクリプトの追加

github.com

  1. MonoBehaviourを継承したクラスを作成し、ONNXモデルを設定するための変数を用意します。

    public class QuickdrawModel : MonoBehaviour
    {
        [SerializeField]
        private NNModel modelAsset;
    }
    
  2. Modelをロードし、Workerを生成します。

    Model model = ModelLoader.Load(modelAsset);
    IWorker worker = WorkerFactory.CreateWorker(WorkerFactory.Type.Compute, model);
    
  3. Tensorを生成し、モデルの入力データを定義します。 僕の場合は、64*64のグレースケールの画像を入力データとするモデルのため、このようなshapeになります。

    Tensor input = new Tensor(new int[] { 1, 64, 64, 1 }, array);
    

    参考までに、Jupyter notebook でモデルのInputLayerを見るとこのような状態です。

    _________________________________________________________________
     Layer (type)                Output Shape              Param #   
    =================================================================
     input_2 (InputLayer)        [(None, 64, 64, 1)]       0         
    

    画像を1枚ずつ渡すモデルのため、第二引数のarrayは64*64のfloat型配列になっています。

    int size = 64;
    float[,] array = new float[size, size];
    
  4. Tensorをworkerに渡して予測を行い、予測結果を取得します。

    Tensor input = new Tensor(new int[] { 1, 64, 64, 1 }, array);
    worker.Execute(input);
    input.Dispose();
       
    var output = worker.PeekOutput("output_layer_name");
    

    worker.PeekOutputのoutput_layer_nameは、推論結果を出力したいレイヤー名に置き換えてください。

  5. これらの処理を良い感じに整理したら、UnityEditorで適当なGameObjectに適用してInspectorからONNXモデルを設定して実行します。

画像データの加工

OpenCVが使えないので、いくつか自前で前処理を入れる必要がありました。

  1. RGBの範囲を1~255から0~1に変換したい場合

    このような変換をしたい場合は「変換前の範囲の中間値」で割った値に「変換後の最小値」を加えます。 ImageNetの処理もこのようにして変換しています。 https://github.com/keras-team/keras-applications/blob/master/keras_applications/imagenet_utils.py

    例:0~255の値を0~1の範囲に変換する場合は、x / 127.5 + 0となります。

      0~255の値を-1~1の範囲に変換する場合は、x / 127.5 + (-1)となります。

  2. 白黒反転させたい場合

    「値の取りうる最大値」から値を引きます。 例:0~255の値を反転させる場合は、255-xとなります。   0~1の値を反転させる場合は、1-xとなります。

  3. グレースケールに変換したい場合

    0.2989 * R + 0.5870 * G + 0.1140 * Bで算出します。

おわりに

ONNXへの変換と、入力データの形式を合わせるところで若干手間取りましたが、やり方さえ分かってしまえばBarracudaは本当に便利ですね。とても簡単にTensorFlowで作成したモデルを転用できるので、個人でもMachineLearningを組み込んだゲームが作れて夢が広がるなと思いました。

とはいえ、どこにMLを適用するかは難しいですね。ゲームでMLというとNPCの動作の制御とかやってくれると嬉しいですが、、個人で学習データ(理想的なNPCの動作)を集めるのが難しいですし、個人でMLを使うならMLで出来ることから逆算して仕様を考えるのが現実的かなと思います。

【Unity】Firebaseを使用するための手順

はじめに

こんにちは、SHOJIです。

本記事ではUnityとFirebaseを連携するときの設定についてまとめます。

Unity Editorのバージョン

前提として、Unity Editorは「2021.3.1f1」を使用しています。 Unity EditorとFirebase Unity SDKにはバージョンによる相性があるようで、以前にはこのようなことがありました。

deadline-driven-developer.hatenablog.com

この記事を参考にされる場合は、一旦バージョンを合わせて動くことを確認してからバージョンを上げて頂くのが、手間ですが間違いないかと思います。

Firebase Unity SDK のインポート

  1. ここから任意のバージョンのFirebase Unity SDKをダウンロードします。github.com

  2. ダウンロードしたFirebase Unity SDKを展開します。

  3. Unity Editorを開き、「Assets」-「Import Package」-「Custom Package...」から「2」で展開したFirebase Unity SDK内のunitypackageをインポートします。「dotnet3」と「dotnet4」でフォルダが分かれていますが、Unity 2019以降は「dotnet4」の方のパッケージをインポートします。firebase.google.com

  4. どのパッケージをインポートするかは使用する機能次第ですが、僕は以下の4つは入れておいて損はないので必ずインポートし、

    • FirebaseAuth.unitypackage
    • FirebaseAnalytics.unitypackage
    • FirebaseCrashlytics.unitypackage
    • FirebaseMessaging.unitypackage

    加えてデータベース機能も必要な場合はこちらをインポートします。

    • FirebaseDatabase.unitypackage

Firebaseの設定

  1. ブラウザでFirebaseコンソールにアクセスし、新規にプロジェクトを作成します。
  2. Firebaseの「プロジェクト設定」-「パッケージ名」と、Unityの「Project Settings」-「Package Name」は同じにします。
  3. AnalyticsとCrashlyticsはデフォルトで有効になっているのでそのままに。AuthenticationとRealtime Databaseはデフォルトでは有効になっていないため、必要な場合は手動で有効にします。

  4. 機能を有効にしたら「2」の「プロジェクト設定」から google-services.json をダウンロードして、UnityのAssetsフォルダ配下に格納します。

  5. UnityでFirebaseを使用するためのコードを書きます。

    private static Firebase.FirebaseApp firebaseApp;
    private static FirebaseAuth firebaseAuth; // Authenticationを使う場合

    public async Task PrepareFirebase() { try { if (firebaseApp != null) { return true; }

        var dependencyStatus = await Firebase.FirebaseApp.CheckAndFixDependenciesAsync();
    
        if (dependencyStatus == Firebase.DependencyStatus.Available)
        {
            // DefaultInstanceの生成時に副作用として初期化が行われるため、これらのコードを削除してはいけない
            firebaseApp = Firebase.FirebaseApp.DefaultInstance;
            firebaseAuth = FirebaseAuth.DefaultInstance; // Authenticationを使う場合
            Debug.Log($"Firebase fixed dependencies successfully.");
            return true;
        }
        else
        {
            Debug.LogError(String.Format("Could not resolve all Firebase dependencies: {0}", dependencyStatus));
            return false;
        }
    }
    catch (Exception exception)
    {
        Debug.Log(exception.Message);
        return false;
    }
    

    }

    Firebaseの初期化処理はアプリ起動時に一度だけ行えばよいのですが、このメソッドは複数回呼ばれる想定で作っています。これには理由がありまして、Unityで開発する場合、シーンごとにUnity Editorでデバッグすることが多く、シーンごとにFirebaseの初期化処理を行えるようにした方がデバッグがしやすいためです。とはいえ、シーンをまたぐたびに無駄な初期化処理を走らせたくないので、メソッドの冒頭で初期化されているかのチェックを行うようにしています。

Authenticationの設定(匿名認証とGoogle認証のみ記載)

匿名認証

  1. ブラウザでFirebaseコンソールにアクセスし、匿名認証を有効にします。

  2. Unityで匿名認証のためのコードを書きます。

    public static string UserId { private set; get; } = string.Empty;

    public async Task SignInAnonymously() { try { var firebaseUser = await firebaseAuth.SignInAnonymouslyAsync(); UserId = firebaseUser.UserId; return firebaseUser; } catch (Exception exception) { Debug.Log(exception.Message); return null; } }

    僕は同期的に処理したいケースが多いため、awaitして同期的に処理させています。公式のサンプルではContinueWithを繋げて非同期的に処理しています。

Google認証

  1. ブラウザでFirebaseコンソールにアクセスし、匿名認証を有効にします。
  2. Unityでapkファイルをビルドします。
  3. "C:\Program Files\Java\jdk-18.0.2\bin\keytool.exe" -printcert -jarfile [filename].apkコマンドからビルドしたアプリのSHA1証明書を取り出します。 ※Windowsを使用している場合、keytool.exeのパスが通っていないのでJavaフォルダから探してください。
  4. 実行結果のSHA1の値をコピーして、Firebaseの「プロジェクトの設定」-「フィンガープリントを追加」から貼り付けます。
  5. Unityでgoogle-signin-plugin-1.0.4.unitypackageをインポートします。github.com
  6. Google認証のためのコードを書きます。

    public static string UserId { private set; get; } = string.Empty;
    
    public async Task SignInGoogleSilently()
    {
        try
        {
            StartWithSignInGoogle();
            var signIn = await GoogleSignIn.DefaultInstance.SignInSilently();
            var credential = GoogleAuthProvider.GetCredential(signIn.IdToken, null);
            var firebaseUser = await firebaseAuth.SignInWithCredentialAsync(credential);
            UserId = firebaseUser.UserId;
            return firebaseUser;
        }
        catch (Exception exception)
        {
            Debug.Log(exception.Message);
            return await SignInGoogle();
        }
    }
    
    public async Task SignInGoogle()
    {
        try
        {
            StartWithSignInGoogle();
            var signIn = await GoogleSignIn.DefaultInstance.SignIn();
            var credential = GoogleAuthProvider.GetCredential(signIn.IdToken, null);
            var firebaseUser = await firebaseAuth.SignInWithCredentialAsync(credential);
            UserId = firebaseUser.UserId;
            return firebaseUser;
        }
        catch (Exception exception)
        {
            Debug.Log(exception.Message);
            return null;
        }
    }
    
    private void StartWithSignInGoogle()
    {
        if (GoogleSignIn.Configuration == null)
        {
            // Google SignInの設定
            GoogleSignIn.Configuration = new GoogleSignInConfiguration
            {
                RequestIdToken = true,
                // Copy this value from the google-service.json file.
                // oauth_client with type == 3
                WebClientId = "********-***************************.apps.googleusercontent.com"
            };
        }
    }
    

    SignInGoogleSilentlyはサインインの状態を確認して自動でサインインを試みます。 SignInGoogleはGoogleアカウントの候補を表示してパスワードの入力を求めます。

    なお、SignInGoogleSilentlyは自動サインインに失敗した場合はSignInGoogleを呼ぶようにしているため、毎回サインインし直させる事情がない限りはSignInGoogleSilentlyだけあれば事足りるかと思います。

    StartWithSignInGoogleは認証処理の初期設定を行うのですが、Web Client Idに値を設定する必要があります。 こちらはFirebaseの「Authentication」-「ログインプロバイダ」-「Google」-「ウェブSDK構成」-「ウェブ クライアント ID」の値を設定してください。

Realtime Databaseの設定

  1. ブラウザでFirebaseコンソールにアクセスし、Realtime Databaseを有効にします。 あとで設定を変更できるため、最初はテストモードで構いません。

  2. Unityで更新用のコードを書き、更新できることを確認します。

    public void InsertData(string referenceKey, string key, T data)
    {
        var databaseReference = firebaseDatabase.RootReference;
        var json = JsonUtility.ToJson(data);
        databaseReference.Child(referenceKey).Child(key).SetRawJsonValueAsync(json);
    }

    public class Test { public int value = 0;

    public Test(int value)
    {
        this.value = value;
    }
    

    }

    InsertDataのようなメソッドを作ればTestクラスのようなインスタンスを渡すことでデータを登録できます。 referenceKeyはFirebaseにサインインしたときに得られるユーザID、keyは何でも構いません。

  3. Unity側で更新処理を実行したらFirebaseコンソールに戻り、データが登録されていることを確認します。

  4. Firebaseにデータが登録されていたらRealtime Databaseのルールを変更します。 「1」で設定したテストモードは、Realtime Databaseのルールに制限をかけないことを指しており、ここで行うルール変更によってテストモードは終了することになります。(日本語が分かりにくいのですが、Realtime Databaseに本番モード・テストモードが存在するわけではなく、参照・更新のルールがない状態を便宜上テストモードと呼んでいる、ということです) こちらはルールのサンプルです。ルールはデータの構造と合わせる必要があります。 ここでは「2」のInsertDataで更新した場合を想定しています。referenceKeyにユーザIDを設定しているので、構造としてはデータベースの直下にユーザIDが並び、その下にkeyで指定した名称のデータが並びます。

    上記のルールでは「$userId」のデータの読み書きはauth.uidと$userIdが同一の場合のみ行えるとしています。したがって、サインインしているユーザ自身のデータしか見えず触れません。

  5. ルールを変更したら、ユーザ自身のデータ以外は触れないことをUnityでテストコードを書いて試験してください。 試験のコードはここでは割愛させて頂きます。

Release版のアプリのSHA1証明書のフィンガープリント

ここからはGoogle Play Consoleにアプリを登録してからの話になります。 「Google認証」の説明の中で、APKファイルからSHA1証明書を抜き出してFirebaseの「プロジェクトの設定」-「フィンガープリントを追加」に貼り付ける手順がありました。

Unityから端末に直接インストールして動作させる場合はこれでよいのですが、Google Playを経由する場合はGoogle Play Consoleで付与しているSHA1証明書をFirebaseに登録する必要があります。

  1. Google play consoleでアプリをリリースします。内部テストやクローズドテストで問題ありません。
  2. 「設定」-「アプリの完全性」-「アプリの署名」を開き、SHA1証明書をコピーして、Firebaseの「プロジェクトの設定」-「フィンガープリントを追加」に貼り付けます。

おわりに

UnityでFirebaseを使用するための基本設定については以上です。

本当はFirebaseAnalyticsを使ってイベント情報を収集する手順も書きたかったのですが、長くなってしまったので今回は見送ります。FirebaseAnalyticsはあまり落とし穴がないので、別途検索して頂ければ容易に実装できるかと思います。

【Unity】Firebaseを使用すると2回目のPlayでUnity Editorがcrashする

はじめに

こんにちは、SHOJIです。

本記事では、UnityでFirebaseを使用してデバッグ(Play)したときにUnity Editorがcrashする場合の対処方法を記載します。

タイトルがルー大柴みたいになっていますが、同じ現象に悩む方に見つけてもらえるようにした結果なので見逃してください笑

初回起動はクラッシュしないのに2回目以降はクラッシュする

という状況であれば、十中八九この設定で直ります。

Opening two unity editor with same firebase project causing unity crash · Issue #928 · firebase/quickstart-unity · GitHub

#if UNITY_EDITOR
  Firebase.FirebaseDatabase.DefaultInstance.SetPersistenceEnabled(false);
#endif

使用しているのがFirestoreの場合はこうです。

#if UNITY_EDITOR
  FirebaseFirestore.DefaultInstance.Settings.PersistenceEnabled = false;
#endif

なぜクラッシュするのか

原因について言及しているコメントがあります。

以下はDeepL翻訳した文章です。

Windowsでは、永続化データは/<パッケージ名>/<データベース名>に格納されます。このコードを見てください。

問題は、2つのUnityエディタインスタンスがある場合、それらが同じ場所に書き込もうとし、アサートが発生することです。

これはデスクトップでは確かに固有の問題ですが、AndroidiOSでは一度に1つのインスタンスしかないため、そうではありません。

複数インスタンスで同じ場所に書き込もうとして落ちてるとのこと。

なるほど、実機で発生しないのはインスタンスが常に一つだからなんですね。

初回だけ通るという点でキャッシュや一時領域が怪しくはあったのであまり意外性はないかなと思います。

おわりに

この問題は半年前には起きていたのですが、2回目以降のときだけ起きることに気づいておらず、解決方法にたどり着けずにいました。なので今回解決できてようやく快適に開発ができる……と安堵しています。

この記事が同じ事象で悩む方の助けになることを願っています。

【Unity】「Google Play Billing Library バージョン 4 以降にアップグレードしてください」と言われたときの対処方法

はじめに

こんにちは、SHOJIです。

本記事では、Unityで「Google Play Billing Library バージョン 4 以降にアップグレードしてください」と言われたときの対処方法を記載します。

Google Play Billing Libraryが見つからない

ここを把握していなくて少し迷ったのですが、「Google Play Billing Library」はUnityの「In-App Purchasing SDK」に含まれているそうです。

したがって、「In-App Purchasing SDK」を更新すれば解決します。

更新するべきバージョンは、こちらに記載のとおり、4.4.0以降であれば「Google Play Billing Library」のバージョン4が使われています。

Official - Important update: Google Play Billing Library v4 - Unity Forum


更新の仕方ですが、Unity Editorで「Project Settings」-「Services」-「In-App Purchasing」の設定画面を開くと、「Install Latest Version」という、Purchasing Packageを更新してくれるボタンがあります。

こちらをクリックしてしばらく待てば冒頭のエラーは解決するはずです。

【Unity】Ubuntuで古いバージョンのUnityを使用する

はじめに

こんにちは、SHOJIです。

今回の記事は、UbuntuでUnity Hubからインストールできない古いバージョンのUnityを使用する方法です。

コマンドから任意のバージョンをインストールする

  1. Unity download archiveにアクセスする。unity3d.com

  2. Downloads(linux)でUnity Editorを選択する。

  3. 緑色のUnityHubボタンを右クリックし、URLをコピーする。

  4. ローカルのUnityHub.AppImage格納フォルダへ移動する。

  5. ターミナルを起動して./UnityHub.AppImage [手順3でコピーしたURL]を実行する。 例:./UnityHub.AppImage unityhub://2021.3.1f1/3b70a0754835

以上です。