tech.guitarrapc.cóm

Technical updates

.NET Core on Lambda で Unity Cloud Build のWebhook処理とLambda をネスト実行する

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

結果こんな通知がくるようにします。

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

目次

Unity Cloud Build とは

Unity Cloud Build は、Unity の SaaS型 CI サービスです。

βのころからずっと触っていましたが、なかなか癖が強いのとUnity ビルド自体がマシンパワー必要なのに対してビルド環境がそこまで強くない、UIが使いにくい、アクセス制御が乏しいなどと難しさをずっと感じていました。しかし、ここ3か月の進化は正当に順当に進んでおり少なくともUIやグループ制御もいい感じになってきました。

加えてビルド状態がWebhookで通知できるようになってことで、他基盤との連携がしやすくなりました。

blogs.unity3d.com

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

とはいえ、このままではほかの基盤と連携するには Webhook を受けて解釈する必要があります。こういったイベントベースの連携には FaaS がまさに向いています。AzureFunctions でも API Gateway(やSNS) + AWS Lambda、あるいは Cloud Functions が格好の例でしょう。

今回行うのはまさにこの、Slack 以外のサービス基盤と Webhook を使って連携することです。連携したいサービスは Chatwork、連携を中継するのは API Gateway と AWS Lambda です。*1

全体像

まずは今回の仕組みで利用する構成です。構成要素は以下の通りです。

  1. Unity Collaborate
  2. Unity Cloud Build
  3. Amazon API Gateway
  4. AWS Lambda
  5. Chatwork

全体図です。

簡単に見ていきましょう。

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メッセージフォーマット の仕様はドキュメント化されています。

Unity Cloud Build

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を追加します。

Steam VR Plugin

デフォルトシーンにある main cameraを削除して、SteamVR Plugin の CameraRig を追加します。

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

Unity Editor に表示された ServicesタブでUnity Collaborate を有効化、Collab から 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) と呼ばれるやつです。

New for AWS Lambda – Environment Variables and Serverless Application Model (SAM) | AWS News Blog

github.com

この流れで SAM にするとコンポーネントが増えてしまいます。リトライ回りやフロー化という意味ではStep Functions とかも面白いのですが、今回はシンプルに行きましょう。

ふつーに API Gateway + Lambda とします。

(AWS 側設定) Lambda から Lambda の呼び出しのIAM Role作成

通常のLambda 単独実行ならば、いわゆる lambda_exec_role があれば実行できます。Managed Policy の AWSLambdaExecute がそれですが、こんなデフォルトポリシーですね。

{
  "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 の AWSLambdaRole がそれにあたります。

{
    "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 などを記述します

gist.github.com

入力される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 です。この利用には少し注意点があります。AmazonLambdaClient クラスはAmazon.Lambda.Toolsパッケージで入るように見えます。しかし実際のところは、Amazon.Lambda.Toolsが依存しているAWSSDK.Lambda が本体です。

このため、project.jsonAWSSDK.Lambda を参照しないと、コンパイルが通っても実行時エラーになります。AmazonLambdaClientクラスを利用しない限り出会わないため気付くのが遅れやすいくて苦しかったです。

実行時エラーになる例

{
  "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.jsonAWSSDK.Lambda も追加してください。もちろん AWSSDK.* なパッケージはすでに .NET Core 対応されているので安心です。*7

AWS SDK for .NET Status Update for .NET Core Support | AWS Developer Tools Blog

{
  "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 だった場合に、body : Webhokで送信されたJSON となるように整形します。

整形は、いつも通りIntegration Request > Body Mapping Templates で行います。

パラメータ
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 にあげておきます。

github.com

*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 にきっちり間に合わせていて素晴らしいです。