C#とAWSのStepFunctionsを試してみたログ
一旦動くところまで作ったけれど、他の言語になりそうなのでとりあえずログとして残しておく。
AWS Toolkit でパッケージすると、LambdaとStepFunctionsの紐づけがわかりやすくて、AWSにデプロイしたあとにも管理しやすいので良いと思ったんだけれどなー。
C# で AWS Lambdaって記事はよく見るけれど、C# で StepFunctions ってのはあんまり見ない。
検索してみるとこの記事がわかりやすかった。
とりあえずやって見よう
環境を作る
Visual StudioにAWS Toolkitが入っていない場合は、Visual Studioの更新機能と更新プログラムから「AWS Toolkit for Visual Studio 2017」をインストールする。
プロジェクトを作る
新しいプロジェクトに、AWS Lambdaというカテゴリができるので、「AWS Serverless Application (.NET Core)」を選択してプロジェクトを作成する。ここでつけたプロジェクト名がLambdaのハンドラー名になるのでちょっと気にしておきたい(まぁ、あとでserverless.templateをいじれば変えられるけれど)。
下の方に .NET Frameworkのバージョンを選択するところがあるけれど、AWS Lambdaでは .net core しか使えないので、ここのドロップダウンはどこにも使われないので無視してOK。
続いて、どのBlueprintを元にプロジェクトを作るかを聞かれるので、「Step Functions Hello World」を選択してFinishボタンを押す。
プロジェクトの構成を確認する
こんな感じのプロジェクトができるので、軽く各ファイルの役割を説明すると
aws-lambda-tools-defaults.json
デプロイに使う設定、AWSのリージョンやデプロイ資源を置くS3のバケットなど設定などを記述する。ここで指定しないものは後でデプロイのときに指定することもできるので、s3-buketやstack-nameを空にしておいて、デプロイ時に指定するみたいな使い方をするみたい。特に変更はいらないと思うのでまずは気にしなくて良い。
serverless.template
Cloud Formation Template っぽい記法で、Lambda や Role、StepFunction などのAWSの各リソースの作成方法を定義する。CloudFormationに不慣れだと読むのが辛いけれど、とりあえずLambdaを増やしたら要素をコピーして追加するぐらいの認識で良いかも。
State.cs
StepFunctionで持ち回るStateの定義、基本的に各ステートで実行されるタスクは、このStateを受け取り、Stateを返すようなメソッドのインターフェイスになる。StateMacnineの内部でもここのステートを参照できる。
state-machine.json
ステートマシーンの定義、各ステートでどんなメソッドを実行するとか、Stateの状態を見て分岐させるとか、数秒ウエイトするみたいな状態とその実行方法、次のステートに移る方法をJSONで定義する。
StepFunctionTasks.cs
各ステートでどんな処理をするのかをC#のメソッドで記述する。メソッドがそれぞれ独立したLambdaに展開される。各メソッドは、State.csで定義されたステートと、実行しているLambdaのContext情報を受け、Stateを更新して次のLambdaにわたす処理を定義する。
State.cs
StepFunctionで持ち回るStateの定義、基本的に各ステートで実行されるタスクは、このStateを受け取り、Stateを返すようなメソッドのインターフェイスになる。StateMacnineの内部でもここのステートを参照できる。
ここで定義したステート定義は内部的に Newtonsoft.Json でシリアライズ/デシリアライズされ、各ステップで利用できる。例えば C# で enum で定義したプロパティーなどを、StateMacnine 側で比較の材料にしたい場合などは数値として渡っていってしまうので、定義する型には気をつけておきたい。
namespace AWSServerless1 { /// <summary> /// The state passed between the step function executions. /// </summary> public class State { /// <summary> /// Input value when starting the execution /// </summary> public string Name { get; set; } /// <summary> /// The message built through the step function execution. /// </summary> public string Message { get; set; } /// <summary> /// The number of seconds to wait between calling the Salutations task and Greeting task. /// </summary> public int WaitInSeconds { get; set; } } }
StepFunctionTasks.cs
各ステートでどんな処理をするのかをC#のメソッドで記述する、メソッドがそれぞれ独立したLambdaに展開される。各メソッドは、State.csで定義されたステートと、実行しているLambdaのContext情報を受け、Stateを更新して次のLambdaにわたす処理を定義する。
この子に処理を全部書くと当たり前のことながら、メンテ不可能になるので適切な設計が必要になる。
DIしたい場合は、コンストラクターでコンテナを作って各メソッドでコンテナから処理を取り出してRunする感じになる。
この例の場合は、GreetingメソッドとSalutationsメソッドがそれぞれ独立したLambdaになり、StepFunctionsのStatemacnineから呼び出される。
using Amazon.Lambda.Core; // Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))] namespace AWSServerless3 { public class StepFunctionTasks { public StepFunctionTasks() { } public State Greeting(State state, ILambdaContext context) { state.Message = "Hello"; if(!string.IsNullOrEmpty(state.Name)) { state.Message += " " + state.Name; } // Tell Step Function to wait 5 seconds before calling state.WaitInSeconds = 5; return state; } public State Salutations(State state, ILambdaContext context) { state.Message += ", Goodbye"; if (!string.IsNullOrEmpty(state.Name)) { state.Message += " " + state.Name; } return state; } } }
Greetingメソッドでは、ステートを更新して次のステップでWaitする時間を動的に変更するために、StateのWaitInSecondsに5を設定している。
serverless.template
Cloud Formation Template っぽい記法で、Lambda や Role、StepFunction などのAWSの各リソースの作成方法を定義する。CloudFormationに不慣れだと読むのが辛いけれど、とりあえずLambdaを増やしたら要素をコピーして追加するぐらいの認識で良いかも。
構成としてはこんな感じになっている。
GreetingTask や SalutationsTask の部分を展開するとこんな感じになっているので、新しいステップで実行したい関数が増えた場合は、このブロックを追加していけば良い。作成時点でのアセンブリ名や名前空間でHandler名が決定されるので、もし途中でアセンブリ名や名前空間、クラス名などを変えたらこっちも一緒に変えてあげたほうが後々わかりやすそう。
"GreetingTask" : { "Type" : "AWS::Lambda::Function", "Properties" : { "Handler" : "AWSServerless1::AWSServerless1.StepFunctionTasks::Greeting", "Role" : {"Fn::GetAtt" : [ "LambdaRole", "Arn"]}, "Runtime" : "dotnetcore2.1", "MemorySize" : 256, "Timeout" : 30, "Code" : { "S3Bucket" : "", "S3Key" : "" } } }, "SalutationsTask" : { "Type" : "AWS::Lambda::Function", "Properties" : { "Handler" : "AWSServerless1::AWSServerless1.StepFunctionTasks::Salutations", "Role" : {"Fn::GetAtt" : [ "LambdaRole", "Arn"]}, "Runtime" : "dotnetcore2.1", "MemorySize" : 256, "Timeout" : 30, "Code" : { "S3Bucket" : "", "S3Key" : "" } } },
state-machine.json
ステートマシーンの定義、各ステートでどんなメソッドを実行するとか、Stateの状態を見て分岐させるとか、数秒ウエイトするみたいな状態とその実行方法、次のステートに移る方法をJSONで定義する。
{ "Comment": "State Machine", "StartAt": "Greeting", "States": { "Greeting": { "Type": "Task", "Resource": "${GreetingTask.Arn}", "Next": "WaitToActivate" }, "WaitToActivate": { "Type": "Wait", "SecondsPath": "$.WaitInSeconds", "Next": "Salutations" }, "Salutations": { "Type": "Task", "Resource": "${SalutationsTask.Arn}", "End": true } } }
ここで定義したものは、AWS上のコンソールからみると、こんな感じのステートマシンとして表示される。
Greeting状態やSalutations状態では、TypeをTaskとしてserverless.templateで定義したLambdaを呼び出している。定義時点では各LambdaのArnは決定できていないので、${GreetingTask.Arn}や${SalutationsTask.Arn}のような変数になっている。
$.ステートに定義されたプロパティー名でステートの情報にアクセスできる。WaitInSecondsWaitToActivate状態では、ステートの状態を元にメソッドを数秒待ちたいので$.WaitInSecondsを参照してWaitしている。
この例では、単に上から下に流れるだけのステートしか定義していので、TaskタイプやWaitタイプしか使っていないけれど、StasteMacnine自体はAmazon States Language(https://states-language.net/spec.html)というDSLで定義されているので、Choiceタイプで条件分岐させたり、Parallelタイプで並列実行したりといったタスクを作ることもできる。
Choiceタイプでは、Lambdaから渡ってきたステート情報を元に分岐をかくことができるけれど、データ型に従った比較メソッドが用意されているのでステートの型と合うように分岐を書く必要がある。
https://dev.classmethod.jp/cloud/aws/aws-step-functions-states-choice/
ステートに日本語を使ったら、AWSにデプロイするときにステートが文字化けしてしまったので、英数字で定義するのが無難みたい。
とりあえずサンプルはココまで
コーディングガイドライン for C# 3.0, 4.0, 5.0
尾崎さんが素晴らしい文書を翻訳してくれました。参考にさせてもらいます!!
aviva SolutionsのC#Coding Guidelinesという文書の日本語版がCodePlexで公開されています。。
→コーディングガイドライン for C# 3.0, 4.0, 5.0
MSDNに、C#のコーディング規則やクラスライブラリ開発のデザインガイドラインは公開されていますが、今回尾崎さんが翻訳された文書は、コーディング規約だけでなく、クラスの設計指針、Visual Studioを利用する場合の環境作成方法などを含め、aviva社内部で採用されているもので理にかなった規則が網羅されています。
C#を対象にしていますが、.NET Frameworkを対象としているので、VBで作成されるプログラムでもほぼそのまま採用できそうなガイドラインで、全ての.NETプログラマーが一度目を通すべきガイドラインになっています。
ストアアプリに含まれるBindableBaseのOnPropertyChangeってプロパティー名渡す必要ないんだ。
遅まきながら、ストアアプリを作ろうかと思ったわけです。
こんなコードを書いたらReSharperが修正したほうがいいよ。と教えてくれた
えっ!?通知元名消しちゃっていいの?と思ってOnPropertyChangedの定義を見たら、
/// <summary> /// プロパティ値が変更されたことをリスナーに通知します。 /// </summary> /// <param name="propertyName">リスナーに通知するために使用するプロパティの名前。 /// この値は省略可能で、 /// <see cref="CallerMemberNameAttribute"/> をサポートするコンパイラから呼び出す場合に自動的に指定できます。</param> protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { var eventHandler = this.PropertyChanged; if (eventHandler != null) { eventHandler(this, new PropertyChangedEventArgs(propertyName)); } }
あー、CallerMemberName属性が付与されているから、コードコメントのように呼び元と同じプロパティー名だったら省略可能なんだね。なるほどねー。久々にC#やるとおろおろする。