tech.guitarrapc.cóm

Technical updates

AzureFunctions の Precompiled Functions を試してみる

2017/1/6 に Precompiled Functions がサポートされました!! この対応により、C# Scripting (.csx) に比べてかなり Azure Functions が書きやすくなります。早速Precompiled の利用とこれまでの.csxとの違いを見てみましょう。

buchizo.wordpress.com

目次

Precompiled Function の概要

「ビルド済みのdll」 と 「dllを使うことを明示した function.json」 を設置することで、.csx で発生していたコンパイルがスキップされ指定したメソッドをエントリポイントとして関数が実行されます。

2017/1/13 現在、.NET Framework v4.6 (Full Desktop) がdllの対象となります。このバージョンは Azure App Service の制限となるので、将来の.NET Framework 対応はそれ待ちです。

なお、.NET Core 対応 はされていません。すでに Issue に上がっていますが、Microsoft.Azure .WebJobs をはじめとする ライブラリやAPIの解決が待たれる状態です。これができると、AWS Lambda とのマルチクラウドにおけるコード運用も可能になるので、かなりいいのですが..。

github.com

github.com

実際、最新の 1.1.2 を試してみても、ね?

さて、改めてPrecompiled Functions を使う理由ってなんでしょうか? 個人的には、.csx で解消できなかった IDE 支援の下でのC#の記述、dllコンパイルによる挙動のわかりやすさ、ローカル環境での動作検証の容易さがその使う理由です。

Pros

ザクッとあげます。要は普通にC# が Visual Studio などのIDE支援で書けるのって嬉しいですね、デプロイ不要でローカルデバッグできるのいいですね。ということです。

メリット一覧 内容
IDE のフル支援が受けられる Visual Studio や VS Code をはじめとする任意のIDEでいつも通りC#を書けます。.csx では、インテリセンスをはじめとしたIDE支援が制限された中で書くことが強いられていたため、普通にかけるのは嬉しいものです。
ローカル動作の確認 dllにコンパイル済みなので、些細なミスによるコンパイル失敗は完全に避けられます。些細なのですが、; 忘れだったり結構IDEに頼っていることを自覚させられる生活から解放されます。*1
一定の動作保証 ローカルで動作確認した上でCI/CDするため、おおよそ意図通り動くことが期待できます。*2
Cons

WebUI での編集ができない。の一言です。AWS Lambda における Java や C# (.NET Core) と同じです。

実際、Precompiled Functions は WebUI 上でみてもコードは表示されず文字化け状態で表示されます。*3

Precompiled Functions を作成してみる

ミニマムコードサンプルはWikiにあります。

github.com

今回は、ミニマムコードにない Logger を含めてやってみましょう。コードはいつも通り、Github に挙げておきました。PrecompileFunctions.sln にソースをいれてあります。

github.com

コード全体像は次の通りです。

gist.github.com

このプロジェクトをビルドすればルート直下にPreCompileEnvironmentVariables フォルダを作成して、ビルド済みdll、function.json が一緒に配置されます。ちなみに、nuget パッケージを一切追加しない状態だと次のようなエラーが出ます。そこで、必要なパッケージを追加して解決しています。

テストすると、上手く動きましたね?

エントリポイントのメソッドを含めるクラスの注意

dll を配置した場合のエントリポイントは、function.json に記述した次の2文で指定します。

  "scriptFile": "PreCompileEnvironmentVariables.dll",
  "entryPoint": "PreCompileEnvironmentVariables.MyFunction.Run",
キー 設定する内容
scriptFile コンパイルしたエントリポイントとなるdll を指定します。
entryPoint NameSpace.Class.Methodの形式でエントリポイントを指定します。

例では、MyFunctionクラスのRunメソッドをエントリポイントに指定しています。このRunメソッドを含む MyFunctionクラスには、Runメソッド以外を含めるとエントリポイントを見つけられないようです。つまり、インスタンスフィールドなどは書かず、Runメソッドだけ書いてください。

ロガーについて

ロガーを利用するには Microsoft.Azure.WebJobs nugetパッケージの追加をしましょう。ミニマムコードのRunメソッドシグネチャに TraceWriter がありませんが、実は渡ってきています。このnugetパッケージがあれば、.csx 同様にTraceWriter を実装せずともロガーとして利用できます。

これにより、エントリポイントのメソッドシグネチャはpublic static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)のように書けます。これでこれまで通り、log.Info("なにかログ") とするだけでロギングできて便利!

HttpRequestMessage の拡張メソッドについて

.csx と同じように HttpRequestMessage の拡張メソッドを利用するため、Microsoft.AspNet.WebApi.Core nuget パッケージの追加をしましょう。これにより、HttpResponseMessage CreateResponse<T>(this HttpRequestMessage request, HttpStatusCode statusCode, T value) などが利用可能になります。

Precompiled Functions をデプロイする

いよいよ Precompiled Functions をデプロイしましょう。Github による CD でやってみます。

従来の deployment.cmd に msbuild のセクションを追加しました。diffはこの通りです。

gist.github.com

上手くデプロイされるとFunctions がビルド後に展開され、WebUI からも見えるはずです。

ただし、w3wp.exe による dll ロック問題があるので、2度目以降のデプロイ時には Kudu コンソールで w3wp.exe をkill、WebUIでのRestart をしてあげてください。

この問題はすでに報告済みなので、解決を待ちましょう。*4

github.com

Precompiled Functions の課題

いくつか使いにくいポイントが残っています。

ビルド

今回、.slnの配置、ビルド生成物の配置をいじっていますが、CI -> CD の連携に工夫がいるのはちょっとまだ使いにくいかと思います。もう少しいい感じでデプロイできるといいのですが...。

DLLロック

現状では、Web Apps の API経由、Kudu API 経由などで w3wp やサイトのリスタートが必要です。結構ヤなポイントなので、これは解消しないと使いにくいです。コンテナベースになれば手っ取り早いのですが...。

.NET Core 対応

いずれするでしょう。待ちです。

まとめ

かなりいい感じで利用できると思います。dll の文字化けが表示されるのは愛嬌ということで。

が、dllロックは結構厄介なので、これが解消するのを待って本番に入れたいところです。AWS Lambda が主軸なのですが、サブとして Azure Functions は優秀なので、.NET Core 対応も待ち望ましいですね。

*1:その自覚別にうれしくないです

*2:環境依存の原因で動かないことはあり得るのでそこはしょうがない

*3:まるでdll をエディタで開いた時のような画面ですね

*4:Kudu Deployment Script でやるかと思いましたが、まぁ待機で。

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

2016 年の人気記事ランキングを出してみた

今年もアクセスランキングを Google Analytics から出してくれるサービスで出してみます。

blog.shibayan.jp

目次

ランキング

相変わらず PowerShell な記事がヒットするようです。リアルタイム通信という名の当時のWebにおけるほげもげ記事は現時点で本ブログ最大のブックマークなのでほむり。

順位 記事 公開年
1 PowerShell での文字列出力について考える - tech.guitarrapc.cóm 2014
2 PowerShellの Out-File と Set-Content あるいは Out-File -Append と Add-Content の違い - tech.guitarrapc.cóm 2014
3 PowerShellで日付書式にカスタム書式パターンを指定する - tech.guitarrapc.cóm 2013
4 PowerShellでリモートPCの操作を行うに為にEnable-PSRemotingをするための準備 - tech.guitarrapc.cóm 2013
5 そろそろ PowerShell の一次配列の罠と回避について一言いっておくか - tech.guitarrapc.cóm 2015
6 PowerShell のモジュール詳解とモジュールへのコマンドレット配置手法を考える - tech.guitarrapc.cóm 2013
7 リアルタイム通信で利用されるプロトコルと手法 - tech.guitarrapc.cóm 2015
8 PowerShell でディレクトリ構造を保ったまま特定のファイルをコピーする(1) - tech.guitarrapc.cóm 2014
9 PowerShell の コマンドレット例外を取得する - tech.guitarrapc.cóm 2013
10 PowerShellで文字列の比較をする際のTips - tech.guitarrapc.cóm 2013

アクセス数的には、2015年よりも 100,000 余り伸びています。しかし2016年の記事が一件もヒットしないのはむむぅ。

2016年の記事に絞ったところ、はてなブックマークに準じたようなそうじゃないような... なんとなくそうだろうなぁという順番になりました。とりあえず意図通り記事の変遷につながっているようなので、引き続きこの感じで。

順位 記事 公開 2016年公開記事におけるPV割合
1 https://tech.guitarrapc.com//entry/2016/04/25/060920 2016/04/25 17.18%
2 https://tech.guitarrapc.com//entry/2016/03/26/070012 2016/03/26 13.12%
3 https://tech.guitarrapc.com//entry/2016/01/07/100248 2016/01/07 6.29%
4 https://tech.guitarrapc.com//entry/2016/04/11/070745 2016/04/11 5.35%
5 https://tech.guitarrapc.com//entry/2016/05/15/105054 2016/05/15 4.53%
6 https://tech.guitarrapc.com//entry/2016/08/24/044850 2016/08/24 4.26%
7 https://tech.guitarrapc.com//entry/2016/04/14/135520 2016/04/14 3.98%
8 https://tech.guitarrapc.com//entry/2016/03/15/011802 2016/03/15 3.64%
9 https://tech.guitarrapc.com//entry/2016/08/29/092502 2016/08/29 3.49%
10 https://tech.guitarrapc.com//entry/2016/05/28/173049 2016/05/28 3.36%

まとめ

幾分思うところもありますが、少しでも皆様のお役に立っている側面があるなら嬉しい限りです。

某なんとか審査のために、正確なPVをとりたいため はてなブログPro や Google Analytics をみるのですが、やはり Google Analytics のほうが分析が楽でいいですねぇ。

Azure Functions で Asssembly.Location が正しくかえって来ない問題の対処

Azure Functions が6日ほど前に更新されて1.0.10690になってから、以下のエラーが発生する場合があります。

Can't create a metadata reference to an assembly without location.
  at Microsoft.CodeAnalysis.MetadataReference.CreateFromAssemblyInternal(Assembly assembly, MetadataReferenceProperties properties, DocumentationProvider documentation)
  at Microsoft.CodeAnalysis.Scripting.ScriptOptions.CreateReferenceFromAssembly(Assembly assembly)
  at Microsoft.CodeAnalysis.Scripting.ParameterValidationHelpers.<>c__DisplayClass4_0`2.<SelectChecked>b__0(T item)
  at System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext()
  at Microsoft.CodeAnalysis.Scripting.ParameterValidationHelpers.AddRangeChecked[T](ArrayBuilder`1 builder, IEnumerable`1 items, String parameterName)
  at Microsoft.CodeAnalysis.Scripting.ParameterValidationHelpers.ToImmutableArrayChecked[T](IEnumerable`1 items, String parameterName)
  at Microsoft.CodeAnalysis.Scripting.ScriptOptions.WithReferences(IEnumerable`1 references)
  at Microsoft.CodeAnalysis.Scripting.ScriptOptions.WithReferences(IEnumerable`1 references)
  at Submission#0.<EvaluateCSharpAsync>d__3.MoveNext() in D:\home\site\wwwroot\CSharpScripting.csx:line 47

今回はこの発生原因と対処についてです。

目次

発生原因

最新バージョン 1.0.10690 で入った以下のコミットが原因です。

github.com

ということで、残念ながら現在 Azure Functions をコンソールから最新バージョン (1.0~) にするとエラーの起こっているバージョンになります。

現在のバージョンの確認

現在問題が起こるのかは、1.0未満なら Azure Functions の画面から確認できます。

1.0 、あるいはもっと正確に知りたい場合 Kudu を使います。

github.com

batコマンドだとこうですね。

type d:\local\config\applicationhost.config | findstr virtual | findstr Functions

表示された結果にバージョンが埋まっています。この場合なら 1.0.10690 となります。

<virtualDirectory path="/" physicalPath="D:\Program Files (x86)\SiteExtensions\Functions\1.0.10690" />

PowerShell でやりたい場合はこんな感じでどうでしょうか。

gist.github.com

問題が出るコード

Assembly.Load が正しい値を返さないのが問題です。Issue がすでに立っており、解決待ちステータスです。*1

github.com

今回私がひっかかったのが、Roslyn を使ったコード評価で独自dll を参照に追加するときでした。typeof(EnumerableExtensions).Assembly, の部分で独自dll のアセンブリを返していますがここでCan't create a metadata reference....エラーが出ます。

private static readonly Assembly[] DefaultReferences =
{
    typeof(Enumerable).Assembly,
    typeof(List<string>).Assembly,
    typeof(System.Net.Http.HttpClient).Assembly,
    typeof(EnumerableExtensions).Assembly,
};

https://github.com/guitarrapc/AzureFunctionsIntroduction/blob/master/CSharpScripting.csx#L39

対処

2つのワークアラウンドが提示されています。

  1. Azure Functions のバージョンをひとつ前に固定する
  2. 独自dllをFunctionフォルダ\bin ではない場所に配置する

今回私は 2の独自dll のパスを変更しています。修正PRは次のものです。

github.com

ワークアラウンドを見てみましょう。

(非推奨) Azure Functions のバージョンをひとつ前に固定する。

私個人としては非推奨です。

  1. バージョン追従がマニュアルになる
  2. 通常のフローではないバージョン管理が必要になる
  3. バージョン固定時に既存のFunction からFunctionが全部消える

特に、Function が全部消えるのは痛恨の罠です。Function Url というか code が作り直しになるので、外部連携している場合 URL 張り直しです。*2

一見コード修正もいらず最高かと思いましたがちょっと苦しいですね。

(推奨) 独自dllをFunctionフォルダ\bin ではない場所に配置する。

私個人としては推奨です。

過去には独自dll は {Function名}\bin\独自dll.dll 固定だったのですがどうやら制約はなくなったようです。reference には残っているので修正してほしいですね。

If you need to reference a private assembly, you can upload the assembly file into a bin folder relative to your function and reference it by using the file name (e.g. #r "MyAssembly.dll"). For information on how to upload files to your function folder, see the following section on package management.

docs.microsoft.com

修正も参照先アセンブリ #r の変更のみなので非常に容易です。

github.com

修正後

PRマージ前の問題発生中はエラーが返っていました。

一方、ワークアラウンド2の対処で修正後は問題なくAssembly を読んでコード評価しています。よかったです。

まとめ

おそらく近日中に修正されると思うので心待ちにしましょう。

Azure は、AzureFunctions などのAppService チームの中の人がふつーに Twitter で補足、アドバイスをくださり 神がかっています。*3

というのは別にしても、熱意と対応の速さは素晴らしく、真摯になっている姿勢を心から尊敬するとともに見習いたいと思います。

*1:正しく直す前に修正する場合に備えて、Revert コミットも準備してくれています : https://github.com/Azure/azure-webjobs-sdk-script/pull/1074

*2:これは Azure Functions の設計上の難しポイントでもあります。API Gateayなどが必要なのはこういう問題の対処です

*3:神が降臨したとTwitter リプライのたびに拝んでいます

.NET Core on Lambda の CI を組んでみる

パッケージの利用、ローカルテスト、ビルド、Visual Studioからのデプロイまで来たのでローカル開発は問題なくできそうですね。ただCIがないままではチーム開発がしにくいです。

tech.guitarrapc.com

tech.guitarrapc.com

tech.guitarrapc.com

そこで今回は、.NET Core on Lambda をいい感じで CIで回す方法についてみていきます。

目次

先人の知恵

先人の知恵があります。Circle CI での例ですが、今回のベースになっています。

blog.shibayan.jp

今回は、Circle CI と Visual Studio Team Service (VSTS) を試してみましょう。

circleci.com

www.visualstudio.com

デプロイ方法

Cloud Formation や Terraform はちょっと重厚すぎる、Serverless Framework ほどではない。ぐらいの温度感で行きたいので、dotnet lambda を用いましょう。

dotnet lambda に関しては、コードをみるのもいいでしょう。

github.com

default-settings.json について

dotnet lambbda コマンドで渡す内容を事前に default-settings.json に定義しておけばずいぶんとシンプルになります。

今回はデフォルトのものにfunction-namefunction-role の 2パラメータを追加しています。この2つを足すことで、function名、roleが指定可能になりCIが捗ります。*1

  "function-name": "GithubWebhook",
  "function-role": "arn:aws:iam::ACCOUNTID:role/service-role/lambda_exec_role"

gist.github.com

.Net Core 1.0 の構築

CircleCI は Ubuntu 環境です。.NET Core は入っていないので入れましょう。

CI環境 .NET Core有無 (インストールが必要)
CircleCI なし (Ubuntu12 or 14)
VSTS Hosted Windows あり
VSTS Hosted Linux あり
VSTS Agent なし (環境に依存します)

Ubuntu それぞれの環境構築コマンドは以下にまとまっています。

Download .NET (Linux, macOS, and Windows)

ただし、ここのバージョンは .NET Core 1.1 です。現状の AWS Lambda が .NET Core 1.0 なので、対象パッケージを変えます。ちなみにこのままやると以下のようなエラーに会います。

github.com

.NET Core 1.0 の最新パッケージバージョンは以下のURLで確認できます。

Download .NET (Linux, macOS, and Windows)

Circle CI

Circle CI のアカウントを持っていない場合は作ってしまいます。リポジトリ側と Circle CI でそれぞれ設定しておくことがあるので見てみましょう。

(リポジトリ側作業) circle.yml の作成

CircleCI のビルドタスクは、circle.yml で記述できます。これをリポジトリの直下においておけば、CircleCI でビルドが走った時に自動的に利用されてるので記述しましょう。

今回は、circle.yml に以下のタスクを記述します。

  1. .net core 1.0 環境の構築
  2. dotnet restore のビルド前実行
  3. dotnet build によるコンパイルテスト
  4. dotnet test による実行テスト
  5. dotnet lambda deploy-function による AWS Lambda へのデプロイ

ざくっと書きました。

gist.github.com

CircleCI は、Ubuntu 14 で行きます。

(CircleCI作業) 環境変数

AWS Lambda で、環境変数にリポジトリへ入れたくない情報を入れました。CircleCI でテストするにあたり、同様にこれらの情報を環境変数に入れます。

qiita.com

(CircleCI作業) AWS Lambda への デプロイIAM User Access/Secret

AWS に、dotnet lambda deploy-function でデプロイするにあたり、Circle CIのAWS Permissions に 適切な Permission が付与された IAM User の Access Key/Secret Key を取得、設定します。ACCOUNT_ID にはご自分のIDをどうぞ。

今回は、Lambda Function の新規作成、更新を踏まえて以下の Permission にしました。

gist.github.com

ビルド実行

あとは、git push でトリガーされます。

dotnet lambda deploy-function をみてもいい感じですね。

Executing publish command
... invoking 'dotnet publish', working folder '/home/ubuntu/AWSLambdaCSharpIntroduction/AWSLambdaCSharpIntroduction/SlackSlashCommandWebhook/SlackSlashCommandWebhook/bin/Release/netcoreapp1.0/publish'
... publish: Publishing SlackSlashCommandWebhook for .NETCoreApp,Version=v1.0
... publish: Project LambdaShared (.NETStandard,Version=v1.6) was previously compiled. Skipping compilation.
... publish: Project SlackSlashCommandWebhook (.NETCoreApp,Version=v1.0) will be compiled because expected outputs are missing
... publish: Compiling SlackSlashCommandWebhook for .NETCoreApp,Version=v1.0
... publish: /home/ubuntu/AWSLambdaCSharpIntroduction/AWSLambdaCSharpIntroduction/SlackSlashCommandWebhook/SlackSlashCommandWebhook/Function.cs(25,35): warning CS1998: This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
... publish: Compilation succeeded.
... publish:     1 Warning(s)
... publish:     0 Error(s)
... publish: Time elapsed 00:00:01.6428709
... publish:  
... publish: publish: Published to /home/ubuntu/AWSLambdaCSharpIntroduction/AWSLambdaCSharpIntroduction/SlackSlashCommandWebhook/SlackSlashCommandWebhook/bin/Release/netcoreapp1.0/publish
... publish: Published 1/1 projects successfully
Changed permissions on published dll (chmod +r SlackSlashCommandWebhook.dll).
Changed permissions on published dll (chmod +r Amazon.Lambda.Core.dll).
Changed permissions on published dll (chmod +r Amazon.Lambda.Serialization.Json.dll).
Changed permissions on published dll (chmod +r Amazon.Lambda.TestUtilities.dll).
Changed permissions on published dll (chmod +r Newtonsoft.Json.dll).
Changed permissions on published dll (chmod +r System.Runtime.Serialization.Primitives.dll).
Changed permissions on published dll (chmod +r LambdaShared.dll).
Zipping publish folder /home/ubuntu/AWSLambdaCSharpIntroduction/AWSLambdaCSharpIntroduction/SlackSlashCommandWebhook/SlackSlashCommandWebhook/bin/Release/netcoreapp1.0/publish to /home/ubuntu/AWSLambdaCSharpIntroduction/AWSLambdaCSharpIntroduction/SlackSlashCommandWebhook/SlackSlashCommandWebhook/bin/Release/netcoreapp1.0/SlackSlashCommandWebhook.zip
... publish:   adding: SlackSlashCommandWebhook.dll (deflated 59%)
... publish:   adding: SlackSlashCommandWebhook.pdb (deflated 46%)
... publish:   adding: Amazon.Lambda.Core.dll (deflated 57%)
... publish:   adding: Amazon.Lambda.Serialization.Json.dll (deflated 56%)
... publish:   adding: Amazon.Lambda.TestUtilities.dll (deflated 61%)
... publish:   adding: Newtonsoft.Json.dll (deflated 60%)
... publish:   adding: System.Runtime.Serialization.Primitives.dll (deflated 48%)
... publish:   adding: LambdaShared.dll (deflated 60%)
... publish:   adding: LambdaShared.pdb (deflated 43%)
... publish:   adding: SlackSlashCommandWebhook.deps.json (deflated 72%)
... publish:   adding: SlackSlashCommandWebhook.runtimeconfig.json (deflated 22%)
Created publish archive (/home/ubuntu/AWSLambdaCSharpIntroduction/AWSLambdaCSharpIntroduction/SlackSlashCommandWebhook/SlackSlashCommandWebhook/bin/Release/netcoreapp1.0/SlackSlashCommandWebhook.zip).
Updating code for existing function 

VSTS

Windows / Linux ともに、Hosted Agent では2つが障壁となってデプロイできません。これらは自前環境に Agent を入れることで回避ができます。

問題 Agent の場合の解決方法
~/.aws/config が作成できない 作成してください。こんなことができないようになっているのは仕方ないとはいえ、見るところ間違えてますかねぇ。
VSTS UI 上の Environment Variables がコードに渡らない OS の環境変数に入れてください。こんなことができな(略

ということで、VSTS で AWS Lambda をCI する場合は、hosted agent ではなく、自前エージェントでどうぞ。

~/.aws/config が作成できない

単純にこれをやるだけなんですが、厳しい...です。

docs.aws.amazon.com

結果、default profile が見つからず dotnet lambda が実行できない、と。

******************************************************************************
Starting: Run echo
******************************************************************************
==============================================================================
Task         : Command Line
Description  : Run a command line with arguments
Version      : 1.1.1
Author       : Microsoft Corporation
Help         : [More Information](https://go.microsoft.com/fwlink/?LinkID=613735)
==============================================================================
/bin/echo '[default]' > ~/.aws/config
'[default]' > ~/.aws/config
******************************************************************************
Finishing: Run echo
******************************************************************************
******************************************************************************
Starting: Run ls
******************************************************************************
==============================================================================
Task         : Command Line
Description  : Run a command line with arguments
Version      : 1.1.1
Author       : Microsoft Corporation
Help         : [More Information](https://go.microsoft.com/fwlink/?LinkID=613735)
==============================================================================
/bin/ls ~/.aws/config
/bin/ls: cannot access '~/.aws/config': No such file or directory
/bin/ls failed with return code: 2
/bin/ls failed with error: /bin/ls failed with return code: 2
******************************************************************************
Finishing: Run ls
******************************************************************************```

Error retrieving configuration for function GithubWebhook: Profile default cannot be found
/usr/bin/dotnet failed with return code: 255
/usr/bin/dotnet failed with error: /usr/bin/dotnet failed with return code: 255
VSTS UI 上の Environment Variables がコードに渡らない

単純にC#コードから、Environemnt.GetVariable() で取得できると思ったんですが... CircleCI でできてVSTS Hosted Agent でできないのは何かやり方おかしいんですかねぇ。

www.visualstudio.com

Hosted agent では、環境依存を環境変数に逃がしている場合に、.Tests でコードを通せなくなっています。

==============================================================================
Task         : .NET Core (PREVIEW)
Description  : Build, test and publish using dotnet core command-line.
Version      : 0.1.0
Author       : Microsoft Corporation
Help         : [More Information](https://go.microsoft.com/fwlink/?linkid=832194)
==============================================================================
/usr/bin/dotnet test /opt/vsts/work/1/s/AWSLambdaCSharpIntroduction/GithubWebhook/GithubWebhook.Tests/project.json --configuration release
    GithubWebhook.Tests.FunctionTest.TestValidSnsGithubWebhookMessage [FAIL]
SUMMARY: Total: 1 targets, Passed: 0, Failed: 1.
Project LambdaShared (.NETStandard,Version=v1.6) was previously compiled. Skipping compilation.
Project GithubWebhook (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation.
Project GithubWebhook.Tests (.NETCoreApp,Version=v1.0) will be compiled because inputs were modified
Compiling GithubWebhook.Tests for .NETCoreApp,Version=v1.0
Compilation succeeded.
    0 Warning(s)
    0 Error(s)
Time elapsed 00:00:01.1756032
 
xUnit.net .NET CLI test runner (64-bit .NET Core ubuntu.16.04-x64)
  Discovering: GithubWebhook.Tests
  Discovered:  GithubWebhook.Tests
  Starting:    GithubWebhook.Tests
GitHub WebHook triggered
      System.AggregateException : One or more errors occurred. (An invalid request URI was provided. The request URI must either be an absolute URI or BaseAddress must be set.)
      ---- System.InvalidOperationException : An invalid request URI was provided. The request URI must either be an absolute URI or BaseAddress must be set.
      Stack Trace:
           at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
           at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
        /opt/vsts/work/1/s/AWSLambdaCSharpIntroduction/GithubWebhook/GithubWebhook.Tests/FunctionTest.cs(21,0): at GithubWebhook.Tests.FunctionTest.TestValidSnsGithubWebhookMessage()
        ----- Inner Stack Trace -----
           at System.Net.Http.HttpClient.PrepareRequestMessage(HttpRequestMessage request)
           at System.Net.Http.HttpClient.SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
           at System.Net.Http.HttpClient.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
           at System.Net.Http.HttpClient.PostAsync(Uri requestUri, HttpContent content, CancellationToken cancellationToken)
        /opt/vsts/work/1/s/AWSLambdaCSharpIntroduction/GithubWebhook/GithubWebhook/Function.cs(81,0): at GithubWebhook.Function.<FunctionHandlerAsync>d__1.MoveNext()
  Finished:    GithubWebhook.Tests
=== TEST EXECUTION SUMMARY ===
   GithubWebhook.Tests  Total: 1, Errors: 0, Failed: 1, Skipped: 0, Time: 0.545s
Dotnet command failed with non-zero exit code: 1.

まとめ

CircleCI は流石、圧倒的な構築の容易さがあります。VSTS は、まぁシカタナイ(? 見るところがおかしいような気がするのですが...)

.NET Core on AWS Lambda は、.NET Core なだけあっていい感じに Linux 上でビルドできるので、CircleCI便利です。オススメ。

今回の内容もリポジトリに置いておきます 。

github.com

*1:省くと初めて作成するときに対話プロンプトで聞かれて困ったります