【Unity、AWS】実践！構築から開発をしてウェブアプリを公開する！

#はじめに

　ローカルで作成したアプリをサーバで公開し、外部から閲覧できるようにするまでの流れを実践を通してまとめました。また、GUIの操作で実装できる部分をコードで行い、実装方法の比較をしました。
　アプリはUnity、サーバミドルウェアはSpringBoot、サーバはAWSを使用しました。

#設計

　今回、作りたいものの設計図です。経験少ないまま作成したものですので、ご容赦ください。

（１）構成図
　AWSの構成と使用するファイルをまとめます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/e0ff1cfe-9969-ba55-2108-6cdb51ff3e8f.png)

（２）ユースケース図
　今回の記事で私が行ったタスクをまとめます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/eb36280e-b037-17b9-9c8a-eb70a104f178.png)

（３）コミュニケーション図
　全体のタスク＆処理の順序をまとめます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/33c45fb2-2bbe-6a18-f8a2-dfa3ab605998.png)

#作成手順

##１．スタックファイル作成

　AWS上でアプリの実行環境を構築します。なるべくAWS portal上で実装せず、コードで実現します。そのため、AWS CloudFormationを使い、yamlファイルから構築します。
　以下、CloudFormationでスタックとして実行するyamlファイルとそのデザイナーです。

```yaml:スタックファイル
Resources:
  ec2:
    Type: 'AWS::EC2::Instance'
    Properties:
      InstanceType: t2.micro
      KeyName: キー名
      ImageId: ami-02892a4ea9bfa2192
      NetworkInterfaces:
        - AssociatePublicIpAddress: 'true'
          DeviceIndex: '0'
          SubnetId: !Ref subnet
          GroupSet:
            - !Ref sg
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-ec2'

  sg:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: allow ssh
      GroupName: !Sub '${AWS::StackName}-sg'
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: クライアントのグローバルIP/32

  vpc:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: 10.0.0.0/16

  subnet:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref vpc
      CidrBlock: 10.0.0.0/24
      AvailabilityZone: ap-northeast-1a

  roottable:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref vpc

  rootToInternet:
    Type: 'AWS::EC2::Route'
    Properties:
      RouteTableId: !Ref roottable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref igw

  igw:
    Type: 'AWS::EC2::InternetGateway'
    Properties: {}

  EC2VPCG3TFW2:
    Type: 'AWS::EC2::VPCGatewayAttachment'
    Properties:
      VpcId: !Ref vpc
      InternetGatewayId: !Ref igw

  EC2SRTA2E350:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      SubnetId: !Ref subnet
      RouteTableId: !Ref roottable
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/43b578a1-407c-28b4-9883-d044f9de5d17.png)

##２．タスク実行

　手順１にて作成したスタックファイルを使用して、AWS portalのCloudFormationのスタックを実行します。
　スタック実行にはCloudFormationのスタック一覧ページから以下の項目を選択します。

|項目|値|
|---|---|
|スタックの作成|新しいリソースを使用（標準）|
|テンプレートソース|テンプレートファイルのアップロード|
|スタック名|任意の名前|

##３．Unityアプリ作成（C#,HLSL）

　Unityアプリを作成します(Unityのバージョン：2020.3.15f2)。　Unityを使うことで、GUI上でアプリの内容をある程度、実装することができ、コードを書く手間を減らすことができます。
　しかし、今回はすべてコードで作成します。コードで多く実装することで、その分GUIで作業する量を減らすことができます。アプリが単純なものであれば、MainCameraにコードをアタッチするだけで、GUI上の作業は終了します。
　以下、実装した内容とコード本文です。

|関数名|処理内容|
|---|---|
|InstantiateObject|空のGameObjectを生成|
|InstantiatePrimitive|PrimitiveTypeのGameObjectを生成|
|InstantiateGuiEnv|EventSystemとCanvasを生成|
|InstantiateButton|ボタンを生成|

```cs:InstanceUtilities.cs
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public static class InstanceUtilities
{
    public static Transform InstantiateObject(string name, Vector3 position)
    {
        var obj = new GameObject(name);
        obj.transform.position = position;
        return obj.transform;
    }
    public static Transform InstantiatePrimitive(PrimitiveType type, string name, Material material, Transform parent, Vector3 localScale, Vector3 position, Vector3 rotation, PhysicMaterial physicMaterial=null, params System.Type[] components)
    {
        var obj = GameObject.CreatePrimitive(type);
        obj.name = name;
        obj.GetComponent<MeshRenderer>().material = material;
        var transform = obj.transform;
        transform.localScale = (type == PrimitiveType.Plane) ? localScale * 0.1f : localScale;
        transform.SetPositionAndRotation(position, Quaternion.Euler(rotation));
        transform.SetParent(parent);
        obj.GetComponent<Collider>().material = physicMaterial;
        foreach (System.Type component in components) obj.AddComponent(component);
        return transform;
    }
     public static Canvas InstantiateGuiEnv(RenderMode renderMode)
    {
        var eventSystemObject = new GameObject("EventSystem", new System.Type[] {typeof(EventSystem), typeof(StandaloneInputModule)});
        var canvasObject = new GameObject("Canvas", new System.Type[] {typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)});
        var canvas = canvasObject.GetComponent<Canvas>();
        canvas.renderMode = renderMode;
        return canvas;
    }
    public static Button InstantiateButton(Canvas canvas, string name, Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot, Vector2 anchoredPosition, Vector2 sizeDelta, string imagePath, UnityEngine.Events.UnityAction call)
    {
        var obj = new GameObject(name, new System.Type[] { typeof(CanvasRenderer), typeof(Image), typeof(Button)});
        obj.transform.SetParent(canvas.transform);
        var rectTransform = obj.GetComponent<RectTransform>();
        rectTransform.anchorMax = anchorMax;
        rectTransform.anchorMin = anchorMin;
        rectTransform.pivot = pivot;
        rectTransform.anchoredPosition = anchoredPosition;
        rectTransform.sizeDelta = sizeDelta;
        var btn = obj.GetComponent<Button>();
        btn.onClick.AddListener(call);
        obj.GetComponent<Image>().sprite = Resources.Load<Sprite>(imagePath);

        return obj.GetComponent<Button>();
    }
}
```

　また、簡単ですが、HLSLにてshaderも作成しました。

```hlsl:map.shader
Shader "Custom/map" {
	Properties {
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200
		
		CGPROGRAM
		#pragma surface surf Standard fullforwardshadows
		#pragma target 3.0

		struct Input {
			float3 worldPos;
		};

		void surf (Input IN, inout SurfaceOutputStandard o) {
			fixed3 u = fixed3(0.8, 0.8, 0.0);
			fixed3 d = fixed3(0.0, 0.0, 0.8);
			o.Albedo = lerp(d, u, (IN.worldPos.y+5)/10);
		}
		ENDCG
	}
	FallBack "Diffuse"
}
```

※参考

https://blog.backham.me/2017/03/31/unity%E3%81%AEugui%E3%81%AErecttransform%E3%81%A7leftrighttopbottom%E3%82%92%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%8B%E3%82%89%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B/

https://www.hanachiru-blog.com/entry/2019/04/21/172208

https://docs.unity3d.com/ja/2018.4/ScriptReference/UI.Button-onClick.html

##４．Unityアプリのビルド

　Unityアプリでの開発が終わったらビルドします。Unityにて、プラットフォームをWebGLに設定し、ビルドします。
　この時、SpringBootアプリで動かすために、Project Settingsにて以下の設定をします。

|項目|値|備考|
|---|---|---|
|Compression Format|Disabled|ビルド時のソースファイルの圧縮するかどうか|
|Strip Engine Code|Disabled|Engine側のコードを削るかどうか|

※「File」>「Build And Run」でローカルホストを立てて動作を確認することができます。
　アプリの説明は省きますが、以下のように正常に表示されました。

![スクリーンショット 2021-09-25 213626.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/eb261075-e7a3-7da6-b248-4df73f90bcaa.png)

※参考

https://issuetracker.unity3d.com/issues/webgl-createprimitive-fails-to-create-required-components

https://forum.unity.com/threads/solved-unity-2020-webgl-doesnt-work-uncaught-syntaxerror-invalid-or-unexpected-token.872581/

##５．SpringBootプロジェクト作成

　SpringBootプロジェクトを作成します。エディターはSpring Tool Suite4を使用しています。
　今回作成したプロジェクトの依存関係とその役割は以下の通りです。

|名前|重要度|役割|
|---|---|---|
|Spring Web|必須|Webアプリの基本的な機能を提供|
|Spring Boot DevTools|補助|補助ツール。ファイル変更でプロジェクトを自動再起動してくれる|
|Thymeleaf|補助|動的なドキュメント生成機能を提供|

##６．Unityアプリの埋め込み（Java,HTML）

　サーバの機能を持つSpringBootアプリを作成します。サーバを立てて、Unityアプリを表示できるようにします。
　この記事では、以下の作業を行いました。

###６－１．UnityアプリをSpringBootプロジェクト内に配置

　Unityプロジェクトのビルドで生成されるファイル(フォルダ)とその配置先は以下の通りです。

|名前|種類|配置先|
|---|---|---|
|index.html|ファイル|プロジェクトフォルダ/src/main/resources/templates|
|Build|フォルダ|プロジェクトフォルダ/src/main/resources/static|
|TemplateData|フォルダ|プロジェクトフォルダ/src/main/resources/static|

###６－２．Unityアプリ内のパスの変更

　マッピングの関係で、Unityアプリのindex.html内のパスを変える必要があります。
　変える個所は以下の通りです。

・Thymeleafの適用

```html:index.html
<html lang="en-us" xmlns:th="http://www.thymeleaf.org">
```

・ソースファイルの参照先の変更

```html:index.html
<link rel="shortcut icon" th:href="@{TemplateData/favicon.ico}">
<link rel="stylesheet" th:href="@{TemplateData/style.css}">
```

・ついでにthymeleafでデータを受け渡しできるようにしてみます。

```html:index.html
<h1 th:text="${date}"></h1>
```

※参考

https://qiita.com/kazuki43zoo/items/e12a72d4ac4de418ee37

###６－３．Javaでマッピングの定義

　ブラウザからリクエストをした際に、Unityアプリが表示されるようにマッピングを定義します。
　以下のJavaプログラムを作成します。

```java:MyController.java
package com.example.demo;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MyController {
	@RequestMapping("/")
	public ModelAndView showApp(ModelAndView mv) {
		Date date = new Date();
		SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日");
		mv.addObject("date", format.format(date));
		mv.setViewName("index");
		return mv;
	}
}
```

　また、Unityアプリがソースファイルに正しくリクエストを送れるように、以下のファイルも作成します。

```java:CustomMapping.java
package com.example.demo;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.MimeMappings;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;

@Component
public class CustomMapping implements WebServerFactoryCustomizer<TomcatServletWebServerFactory>{
	@Override
	public void customize(TomcatServletWebServerFactory factory) {
		MimeMappings mappings = new MimeMappings(MimeMappings.DEFAULT);
		mappings.add("wasm", "application/wasm");
		factory.setMimeMappings(mappings);
	}
}
```

※プロジェクトを「Spring Boot アプリケーション」として実行することで、ローカルホストにて動作を確認することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/b3b2e873-09e9-a716-216f-ec8ca00497fb.png)

デフォルトでは以下のURLにブラウザからアクセスすると表示できます。
http://127.0.0.1:8080/

※参考

https://am1tanaka.hatenablog.com/entry/2020/11/07/113037

https://qiita.com/oohira/items/0767bde61bef6256f568

##７．SpringBootアプリのビルド

　プロジェクトをビルドしてjarファイルを作成します。javaコマンドにて実行できるようにするには、ビルド設定を変える必要があります。
　以下のように、SpringBootプロジェクト内のpom.xmlのbuildタグを変更します。

```xml:pom.xml
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<mainClass>${start-class}</mainClass>
				</configuration>
				<executions>
					<execution>
						<goals>
							<goal>repackage</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
```

　ビルドでは、Maven clean後にMaven buildします。ビルドが成功すると、プロジェクトフォルダ/targetにjarファイルが作成されます。

※参考

https://spring.pleiades.io/spring-boot/docs/2.1.11.RELEASE/maven-plugin/examples/repackage-classifier.html

https://www.webdevqa.jp.net/ja/java/spring-bootjar%e3%83%95%e3%82%a1%e3%82%a4%e3%83%ab%e3%81%ae%e4%bd%9c%e6%88%90metainf-springfactories%e3%81%ab%e8%87%aa%e5%8b%95%e6%a7%8b%e6%88%90%e3%82%af%e3%83%a9%e3%82%b9%e3%81%8c%e8%a6%8b%e3%81%a4%e3%81%8b%e3%82%8a%e3%81%be%e3%81%9b%e3%82%93/827039567/

https://www.aruse.net/entry/2019/07/28/141236

##８．シェルスクリプト作成

　CloudFormationにて作成したEC2にJavaをインストールするためのシェルスクリプトを作成します。シェルスクリプトは必須ではありませんが、今後流用できるようにするために残します。
　以下、作成したシェルスクリプトです。

```bash:install-java.sh
#!/usr/bin/bash
mkdir /tmp/jdk
cd /tmp/jdk
wget https://download.java.net/java/GA/jdk11/9/GPL/openjdk-11.0.2_linux-x64_bin.tar.gz
tar xzvf openjdk-11.0.2_linux-x64_bin.tar.gz
sudo mv jdk-11.0.2 /usr/lib/java/
cd ~
mkdir /tmp/old
cp .bashrc /tmp/old/.bashrc
{
	cat /tmp/old/.bashrc
	echo "export JAVA_HOME=/usr/lib/java/"
	echo "export PATH=\$PATH:\$JAVA_HOME/bin"
}>.bashrc
source .bashrc
java -version
```

　Javaのバージョンは、SpringBootプロジェクト内のJavaを実行できるものである必要があります。今回は、Java11をインストールしました。

※参考

https://devb.hatenablog.com/entry/20101203/1291391256

http://jdk.java.net/archive/

https://qiita.com/yuuman/items/a9e89d7d4899cbdaed44

https://qiita.com/gate9/items/7351557829d7e1e668e9

##９．Javaインストール

　手順8にて作成したシェルスクリプトをEC2で実行します。実行は、シェルスクリプトのあるディレクトリに移動して、以下のコマンドを実行します。

```bash
./install-java.sh
```

##１０．ウェブアプリ実行

　最後にEC2でjarファイルを実行して、ブラウザからアクセスしてみます。

###１０－１．jarファイルの実行
　jarファイルを実行するには、以下のコマンドを実行します。

```bash
source .bashrc
java -jar jarファイル
```

###１０－２．クライアントのブラウザからウェブアプリへのアクセス

　クライアントのブラウザからウェブアプリへアクセスするのですが、１点注意します。
　CloudFormationにて作成したセキュリティグループは、クライアントのグローバルIPから22ポートへの通信のみしか許可していません。サーバのポートは8080ですので、クライアントからアクセスできません。
　そのため、SSHポートフォワーディングを使ってアクセスできるようにします。以下、WindowsのコマンドプロンプトにおけるSSHポートフォワーディングの方法を説明します。
　まず、コマンドプロンプトを起動し、以下のコマンドを実行します。

```bash
ssh -i pemファイル -L 18080:localhost:8080 ec2-user@EC2のグローバルIPアドレス -N
```

次にブラウザを起動し、以下のURLにアクセスします。
http://127.0.0.1:18080/

こうすると、なんとEC2で起動しているウェブアプリにアクセスできます！

※参考

https://www.karakaram.com/notes-on-ssh-local-port-forwarding/

　以上で実践内容は終わりです。

#コードで実践するメリット＆デメリット

　今回、実践を通して感じた、コードでの実装のメリット＆デメリットをまとめます。

〇メリット
(i)サービスやAPIの細かい仕様を知ることができる。
(ii)他者に作業手順を共有しやすい。
(iii)反復作業を自動化しやすい。
(iv)作業環境の自由度が高い。
(v)より細かい制御ができる。

〇デメリット
(i)GUI操作に加えてコードの仕様も学ばなくてはならない。
(ii)作成したコードのエラーに対応しなくてはならない。

#本当はやりたかったこと

　この記事のタイトルからはそれてしまいますが、他にも挑戦したいことがありました。

(i)EC2の代わりにLambdaにデプロイ

断念理由：ビルドに関する知識不足

jarファイルをデプロイしたLambdaがハンドラーを見つけてくれませんでした...
かといって、pom.xmlでビルド方法を変えると実行できませんでした...

※参考

https://aws.amazon.com/jp/premiumsupport/knowledge-center/lambda-troubleshoot-java-classnotfound/

https://stackoverflow.com/questions/58176456/spring-boot-maven-plugin-boot-inf-directory-causing-aws-lambda-application-to

(ii)Cognitoでログイン

断念理由：時間不足

Cognitoの紹介記事は多くあり、簡単に実装できそうでした。

https://qiita.com/fkooo/items/660cab0090a80861155b

#おわりに

　コードでの実装は、デメリットがありますが、解決できればかなり作業が楽になると思います。
少しずつ身に着けていきたいです。