ついにLambdaでVB.NETが!!AWS Lambda の Custom Runtimes を利用してVB.NETを実行してみる #reinvent

サーバーレス開発部@大阪の岩田です。 この記事は AWS Lambda Custom Runtimes芸人 Advent Calendar 2018 の 8日目 です。

はじめに

Microsoftが大好きな皆様、Lambdaで.NETは使っていますか?? 昨年の今頃、re:Invent2017にてLambdaの.NET Core対応が発表されました。 この発表を聞いて歓喜したMSファンも多かったのではないでしょうか?

しかし、この.NET Core対応には1つ大きな問題がありました。 対応している言語がC#のみなのです!! 泣く泣くLambdaの利用を諦めたVB.NET派の方が多数いらっしゃったことと想像します。 しかし、今年のre:Invent2018でCustom Runtimesの機能が発表されました。 ついにLambdaでVB.NETを動かせる日が来たのです!!

本エントリではVB.NET on .NET Coreのカスタムランタイムを作成し、LambdaでVB.NETのコードを動かしてみます!

環境

開発環境は下記の通りです。

  • macOS High Sierra Darwin Kernel Version 17.7.0
  • .NET Core 2.1.301

カスタムランタイム用のレイヤー作成

それでは早速ランタイムを作成していきます。 まずはプロジェクトの雛形を作成します。

dotnet new console -lang VB -o bootstrap

作成されたProgram.vbを下記の通り修正します。動作させるためだけのコードなので、もし実際に使う場合は適宜戻り値のチェックやエラーハンドリングを追加して下さい。

Imports System
Imports System.Linq
Imports System.Reflection
Imports System.Net.Http
Imports System.Text
Imports System.Threading.Tasks


Class Program
    Public Shared Sub Main(ByVal args As String())
        MainAsync().GetAwaiter().GetResult()
    End Sub

    Private Shared Async Function MainAsync() As Task
        Dim path As String = System.IO.Path.Combine(Environment.GetEnvironmentVariable("LAMBDA_TASK_ROOT"), Environment.GetEnvironmentVariable("_HANDLER"))
        Dim asm As Assembly = Assembly.LoadFrom(path)
        Dim myType As Type = asm.[GetType]("handler.Handler")
        Dim handler As MethodInfo = myType.GetMethod("Main")
        Dim obj = Activator.CreateInstance(myType)
        Dim endPoint = Environment.GetEnvironmentVariable("AWS_LAMBDA_RUNTIME_API")
        Dim client = New HttpClient()

        While True
            Dim response = Await client.GetAsync("http://" & endPoint & "/2018-06-01/runtime/invocation/next")
            Dim requestId = response.Headers.GetValues("Lambda-Runtime-Aws-Request-ID").FirstOrDefault()
            Dim json = Await response.Content.ReadAsStringAsync()
            Dim res = handler.Invoke(obj, New Object() {json})
            Dim content = New StringContent(res.ToString(), Encoding.UTF8)
            response = Await client.PostAsync("http://" & endPoint & "/2018-06-01/runtime/invocation/" & requestId & "/response", content)
        End While
    End Function
End Class

ポイントは

Dim myType As Type = asm.[GetType]("handler.Handler")
Dim handler As MethodInfo = myType.GetMethod("Main")

の部分です。 Lambda functionとしてデプロイされたdllから動的にアセンブリを読み込んでメソッドを呼び出します。

実装できたら自己完結型として実行ファイルを作成します。 自己完結型とすることで、.NET Coreのランタイムまで含めて実行ファイルを発行することができます。

dotnet publish -c Release -f netcoreapp2.1 -r linux-x64

作成できたらLambdaへのデプロイ用にZIPに固めます。 bootstrapに実行権限を付けるのを忘れないで下さい。

cd bin/Release/netcoreapp2.1/linux-x64/publish/
chmod +x bootstrap
zip -r bootstrap.zip .

ZIPができたらLambdaのレイヤーを作成します。 「互換性のあるランタイム」を指定しないのがポイントです。

Lambda function用のdll作成

次にLambda function用のコードをVB.NETで実装し、dllを作成します。

dotnet new classlib -lang VB -o handler

生成されたClass1.vbをHandler.vbにリネームし、コードを修正します。

Public Class Handler

  Public Function Main(ByVal json as String)
    return json
  End Function

End Class

引数で受け取った文字列をそのまま返すだけの処理です。 前述の通りbootstrapではアセンブリを動的に読み込んでメソッドを呼び出しているので、クラス名やメソッド名はbootstrap側で指定しているものと合わせる必要があります。

実装できたらビルドします。

dotnet build -c Release -r linux-x64

作成されたhandler.dllをzipに圧縮します

bin/Release/netstandard2.0/linux-x64/
zip handler.zip handler.dll 

Lambda functionの作成

最後にLambda functionを作成します。 ランタイムには「独自のランタイム使用する」を選択します。

レイヤーとして先ほど作成したレイヤーを指定します。

関数コードとして作成したhandler.zipをアップロードし、ハンドラにhandler.dllを指定して保存します。

試してみる

ここまでできたら適当なテストイベントを設定してテストを実行してみます。

ついに、、、VB.NETのコードがLambdaで動きました!!

まとめ

VB.NETで実装したコードをカスタムランタイム上で動作させてみました。 カスタムランタイムの実装ではbootstrapをシェルスクリプトで実装している例も多いのですが、bootstrapも含めてVB.NET縛りで実装できて満足です。

.NETのメリットの1つとして、複数の言語から自分の好きな言語を選択して開発できるという点がありますが、今回紹介したカスタムレイヤーの上で Lambda functionごとにC#やF#を使い分けて遊ぶのも良いかもしれません。

VB.NET派、F#派の方の参考になれば幸いです。