今回は、Unity開発に欠かせなくなってきたUnity Cloud Buildのビルド通知をAWS Lambda (.NET Core) でいい感じに処理することを考えてみます。手始めに、他のチャット基盤 (Chatwork) への通知に取り組んでみましょう。
結果こんな通知がくるようにします。

Zapier連携があればもっと楽ちんだったのですがシカタナイですねぇ。

- Unity Cloud Build とは
- 全体像
- Unity Cloud Build の Webhook API 仕様
- (Unity 側設定) Unity プロジェクトの Collaborate 設定
- (Unity 側設定) Unity プロジェクトの Cloud Build 設定
- (AWS 側設定) Lambdaの連携方法
- (AWS 側設定) Lambda から Lambda の呼び出しのIAM Role作成
- (AWS 側設定) Lambda の構成
- Lambda の作成
- (AWS 側設定) API Gateway の設定
- ビルドテスト
- まとめ
Unity Cloud Build とは
Unity Cloud Buildは、UnityのSaaS型CIサービスです。
βのころからずっと触っていましたが、なかなか癖が強いのとUnityビルド自体がマシンパワー必要なのに対してビルド環境がそこまで強くない、UIが使いにくい、アクセス制御が乏しいなどと難しさをずっと感じていました。しかし、ここ3か月の進化は順当に進んでおり少なくともUIやグループ制御もいい感じになってきました。

加えてビルド状態がWebhookで通知できるようになってことで、他基盤との連携がしやすくなりました。
https://blogs.unity3d.com/jp/2016/09/07/webhooks-and-slack-notifications-in-unity-cloud-build/

Slackがデフォルトでワンポチ連携できるのもトレンドに沿っててなるほど感。

とはいえ、ほかの基盤と連携するにはWebhookを受ける必要があります。こういったイベントベースの連携にはFaaSがまさに向いています。AzureFunctionsでもAPI Gateway(やSNS) + AWS Lambda、あるいはCloud Functionsが格好の例でしょう。
今回行うのはまさにこの、Slack以外のサービス基盤とWebhookを使って連携することです。連携したいサービスはChatwork、連携を中継するのはAPI GatewayとAWS Lambdaです。*1
全体像
まずは今回の仕組みで利用する構成です。構成要素は以下の通りです。
全体図です。

簡単に見ていきましょう。
Unity Collaborate
Unityのソースをチームで共有するための仕組みで、今回はUnity Cloud Buildへのソース、イベント発火起点として利用します。

Unity Cloud Build
今回の肝となるCIです。やりたいことは、ここで発生したビルドイベントのWebhookを経由した他サービスとのイベント連携です。今回のイベント連携終着点はChatworkへの通知ですね。

Unity Cloud Buildの通知先がSlackなのであれば、Cloud Buildの通知先にビルトインされているので、API GatewayやLambdaを使わず簡単に飛ばせます。仕組みは単純にUnity Collaborate -> Unity Cloud Build -> Slack、シンプルですね。


Amazon API Gateway -> Amazon Lambda -> Chatwork
Amazon API GatewayはWebhookを受けてLambdaに流しこむためのプロキシとしての役割を担います。
AWS Lambdaは、イベント連携の基盤です。どのように連携するかをコードで定義します。言語はC#(.NET Core)を使ってみます。
最後に、AWS LambdaからChatworkにビルド情報を送信します。

Unity Cloud Build の Webhook API 仕様
さて、Lambdaで解析するUnity Cloud Buildから送られてくるWebhookメッセージフォーマットの仕様はドキュメント化されています。
application/jsonで送られてくるJSONフォーマットは次のものです。
{ "projectName": "My Project", "buildTargetName": "Mac desktop 32-bit build", "projectGuid": "0895432b-43a2-4fd3-85f0-822d8fb607ba", "orgForeignKey": "13260", "buildNumber": 14, "buildStatus": "queued", "startedBy": "Build User <builduser@domain.com>", "platform": "standaloneosxintel", "links": { "api_self": { "method": "get", "href": "/api/orgs/my-org/projects/my-project/buildtargets/mac-desktop-32-bit-build/builds/14" }, "dashboard_url": { "method": "get", "href": "https://build.cloud.unity3d.com" }, "dashboard_project": { "method": "get", "href": "/build/orgs/stephenp/projects/assetbundle-demo-1" }, "dashboard_summary": { "method": "get", "href": "/build/orgs/my-org/projects/my-project/buildtargets/mac-desktop-32-bit-build/builds/14/summary" }, "dashboard_log": { "method": "get", "href": "/build/orgs/my-org/projects/my-project/buildtargets/mac-desktop-32-bit-build/builds/14/log" } } }
さぁこれで全体の仕組み、メッセージフォーマットがわかったので、API Gatewayで受けてLambdaで好きなようにいじれますね。Unity側の設定、AWS側の設定と順にみていきましょう。
(Unity 側設定) Unity プロジェクトの Collaborate 設定
Unity Cloud Buildのビルド連携は、Unity Collaborate経由が一番楽です。GitHub空の連携では、Submoduleやビルド依存関係(dllがビルド時生成とか) など細かい制御が非常に面倒です。*2
今回はUnityのVRプロジェクト*3をビルドする体で進めます。
適当にUnityで新規プロジェクトを3Dで作成して、SteamVR Pluginを追加します。
デフォルトシーンにあるmain cameraを削除して、SteamVR PluginのCameraRigを追加します。

続いて、メニューバー > Windows > Servicesを開きます。

Unity Editorに表示されたServicesタブでUnity Collaborateを有効化、Servicesから Publish now!します。

Uploadが終わるのを待ちます。

(Unity 側設定) Unity プロジェクトの Cloud Build 設定
Upload後は、Cloud Buildを有効化して、


環境に合わせてビルド設定を組みます。*4

ビルド設定が追加されると、自動的にビルドが開始します。Unity Collaborateでpublishしたら自動的にUnity Cloud Buildも走るように設定できるので非常に楽ちんですね。*5

ビルド完了もUnity上から確認できる上に、Cloud BuildのWebへのリンクもあるのでWeb上でも確認できます。このあたりの連携は非常に便利です。うれしさあります。

![]()
さて、これでWebhookを使ってビルド通知を流す下準備ができました。次はAWS側の設定をやります。
(AWS 側設定) Lambdaの連携方法
AWS側で必要なのが、AWS Lambdaの構成 -> API Gatewayの構築です。いわゆるAWS Serverless Application Model(SAM) と呼ばれるやつです。
https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md
この流れでSAMにするとコンポーネントが増えてしまいます。リトライ回りやフロー化という意味ではStep Functionsとかも面白いのですが、今回はシンプルに行きましょう。
ふつーにAPI Gateway + Lambdaとします。
(AWS 側設定) Lambda から Lambda の呼び出しのIAM Role作成
通常のLambda単独実行ならば、いわゆるlambda_exec_roleがあれば実行できます。Managed Policyのlambda_exec_roleがそれですが、こんなデフォルトポリシーですね。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:*" ], "Resource": "arn:aws:logs:*:*:*" }, { "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject" ], "Resource": "arn:aws:s3:::*" } ] }
しかし、Lambdaから別のLambdaを呼ぶにはlambda:InvokeFunction権限が必要です。Managed Policyのlambda:InvokeFunctionがそれにあたります。
{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": [ "lambda:InvokeFunction" ], "Resource": ["*"] }] }
ということで、IAMにlambda_collaborate_roleを作っておきましょう。

(AWS 側設定) Lambda の構成
Unity Cloud BuildのWebhookを受けて実行するLambdaを作成します。また、LambdaからSendToChatwork Lambdaを呼び出します。*6
Lambda のコード
ざくっと行きます。
Function.csが、今回のAWS Lambda本体コード。Chatworkでメッセージが読みやすいようにいい感じに整形するUnityCloudBuildWebhook.csはJSONからクラスへのデシリアライズ定義ChatNotification.csは、以前作成したChatworkへの送信Lambdaに渡すクラス定義project.jsonに、今回利用するコンポーネントaws-lambda-tools-defaults.jsonには、先ほどのIAM Roleなどを記述
https://gist.github.com/guitarrapc/05dc97af22d0ef06e25e262a90f0b08b
入力されるJSONについて
API Gatewayでbody : Webhokで送信されたJSONとなるように整形します。こうすることで、body経由で入力があったのかどうかも含めてシチュエーション対応が柔軟にできます。
そのため、Lambdaで受けるJSONは次のフォーマットになります。
{ "body": { "projectName": "My Project", "buildTargetName": "Mac desktop 32-bit build", "projectGuid": "0895432b-43a2-4fd3-85f0-822d8fb607ba", "orgForeignKey": "13260", "buildNumber": 14, "buildStatus": "queued", "startedBy": "Build User <builduser@domain.com>", "platform": "standaloneosxintel", "links": { "api_self": { "method": "get", "href": "/api/orgs/my-org/projects/my-project/buildtargets/mac-desktop-32-bit-build/builds/14" }, "dashboard_url": { "method": "get", "href": "https://build.cloud.unity3d.com" }, "dashboard_project": { "method": "get", "href": "/build/orgs/stephenp/projects/assetbundle-demo-1" }, "dashboard_summary": { "method": "get", "href": "/build/orgs/my-org/projects/my-project/buildtargets/mac-desktop-32-bit-build/builds/14/summary" }, "dashboard_log": { "method": "get", "href": "/build/orgs/my-org/projects/my-project/buildtargets/mac-desktop-32-bit-build/builds/14/log" } } } }
Lambda から Lambda の呼び出しにおける project.json に注意
今回LambdaからLambdaを呼び出しました。この時利用するのが、Amazon.Lambda.AmazonLambdaClientです。この利用には少し注意点があります。Amazon.Lambda.AmazonLambdaClientクラスはAmazon.Lambda.AmazonLambdaClientパッケージで入るように見えます。しかし実際のところは、Amazon.Lambda.AmazonLambdaClientが依存しているAmazon.Lambda.AmazonLambdaClientが本体です。

このため、project.jsonでproject.jsonを参照しないと、コンパイルが通っても実行時エラーになります。project.jsonクラスを利用しない限り出会わないため気付くのが遅れやすいくて苦しかったです。
実行時エラーになる例
{ "version": "1.0.0-*", "buildOptions": { "emitEntryPoint": true }, "dependencies": { "Microsoft.NETCore.App": { "type": "platform", "version": "1.0.0" }, "Amazon.Lambda.Core": "1.0.0*", "Amazon.Lambda.Serialization.Json": "1.0.1", "Amazon.Lambda.Tools": { "type": "build", "version": "1.0.0-preview1" }, "Newtonsoft.Json": "9.0.1", "LambdaShared": "1.0.0-*" }, "tools": { "Amazon.Lambda.Tools": "1.0.0-preview1" }, "frameworks": { "netcoreapp1.0": { "imports": "dnxcore50" } } }
エラーメッセージ
{ "errorType": "FileNotFoundException", "errorMessage": "Could not load file or assembly 'AWSSDK.Core, Version=3.3.0.0, Culture=neutral, PublicKeyToken=885c28607f98e604'. The system cannot find the file specified.", "stackTrace": [ "at UnityCloudBuildNotificationProxy.Function.FunctionHandler(Object input, ILambdaContext context)", "at lambda_method(Closure , Stream , Stream , ContextInfo )" ], "cause": { "errorType": "FileNotFoundException", "errorMessage": "'AWSSDK.Core, Version=3.3.0.0, Culture=neutral, PublicKeyToken=885c28607f98e604' not found in the deployment package or in the installed Microsoft.NETCore.App.", "stackTrace": [ "at AWSLambda.Internal.Bootstrap.LambdaAssemblyLoadContext.Load(AssemblyName assemblyName)", "at System.Runtime.Loader.AssemblyLoadContext.ResolveUsingLoad(AssemblyName assemblyName)", "at System.Runtime.Loader.AssemblyLoadContext.Resolve(IntPtr gchManagedAssemblyLoadContext, AssemblyName assemblyName)" ] } }
AWSSDK.Lambda を参照追加する
対策は容易です。project.jsonにproject.jsonも追加してください。もちろんproject.jsonなパッケージはすでに .NET Core対応されているので安心です。*7
https://aws.amazon.com/jp/blogs/developer/aws-sdk-for-net-status-update-for-net-core-support/
{ "version": "1.0.0-*", "buildOptions": { "emitEntryPoint": true }, "dependencies": { "Microsoft.NETCore.App": { "type": "platform", "version": "1.0.0" }, "Amazon.Lambda.Core": "1.0.0*", "Amazon.Lambda.Serialization.Json": "1.0.1", "Amazon.Lambda.Tools": { "type": "build", "version": "1.0.0-preview1" }, "Newtonsoft.Json": "9.0.1", "AWSSDK.Lambda": "3.3.2.4", "LambdaShared": "1.0.0-*" }, "tools": { "Amazon.Lambda.Tools": "1.0.0-preview1" }, "frameworks": { "netcoreapp1.0": { "imports": "dnxcore50" } } }
環境変数
今回は、通知先のChatwork RoomIdを決め打ってしまっています。これは環境変数に設定しまいます。

Debug実行対応
ローカルデバッグ、Circle CIでのデバッグ実行においてAWS Lambdaを呼び出ししているため、環境変数にAWS認証を設定しておきましょう。

これらが設定されていれば、xUnitで作成したUnit Testも通ります。

Lambda の作成
コードがかけてIAMも用意できたら、Visual StudioやCIでデプロイします。これでUnityCloudBuildNotificationProxy Lambdaが生成されます。

テストも通っていればokですね。
(AWS 側設定) API Gateway の設定
POSTを受けるようにします。

バックエンドは先ほど作成したUnityCloudBuildNotificationProxy Lambdaです。

JSON のフォーマット
コンテンツタイプがapplication/jsonだった場合に、application/jsonとなるように整形します。
整形は、いつも通りIntegration Request > Integration Requestで行います。
| パラメータ | 値 |
|---|---|
| Content-Type | application/json |
| Mapping | { "body": $input.json("$") } |

これでokです。
ビルドテスト
さぁ長くなりました。Unity Cloud Buildでビルドしてみると...?

うまく通知されましたね。

Lambdaの実行をCloud Watch Logsで確認しても上手くいっています。

まとめ
Unity Cloud Buildは、Unity開発をするにあたって欠かせない存在になってきています。こういったWebhookのサポートもありどんどん使いやすくなっているのでぜひ活用していくといいですね。
Unity操作や細かい注意を書いたので長くなりましたが、実はやってる作業はこれまでのAWS Lambdaの記事とあまり変わりません。今回のコードもGitHubにあげておきます。
*1:AzureFunctionsでもほとんど変わりません。楽ちん!
*2:正直、現状CircleCIやVSTSを含めたふつーのSaaS型CIに比べてコナレテいるとは言いが難いかなぁと感じています
*3:SteamVR Pluginを足しただけのモック
*4:VRで今ならAlways Use Latest 5.5が機能への追随ができるので望ましいでしょう。5.6を選択できるようになってほしいですね
*5:GitHubなどでももちろん可能です
*6:Lambdaのネスト実行
*7:このあたりAWS .NETチームは昨年から準備を進めて、今年の.NET Core GA -> .NET Core on AWS Lambdaにきっちり間に合わせていて素晴らしいです。