初めに
「タスク管理を作ってみました」ということでSaas型のシステムを作るに当たって必要なことを記事にまとめて共有してコミュニティに貢献したいなと思います。Saasサービスを作って公開し、パフォーマンスがよい状態に保ちながら、随時アップデートを続け、アップデート時に品質を落とさないためには広範な知識が必要です。この記事のそれぞれの内容は公式ドキュメントやブログやSlideShareなどで非常に良い情報はあるのですが、経験が無いと全体像がなかなか掴みづらいと思います。この記事では実際にタスク管理を作ってみたときに発生した課題とその解決方法について実例を交えながら説明し様々な技術スタックを紹介していきます。
想定読者
・Saasで何かしらのサービスを作りたいと思っている初級プログラマ
・趣味レベルからしっかりとした質のサービスが提供できるレベルへステップアップしたい人
・Saasサービスの提供に必要な技術の全体像を知りたい人
・エンジニアが何をやっているかよくわからない経営者
あたりを想定読者にしております。細かい詳細まで書くと記事が長くなりすぎるので簡潔に概要を紹介しています。詳細はリンク先を参照してみてください。
この記事で触れるツール、サービス、技術などのリストです。
・Visutal Studio
・Management Studio
・機能のUIデザイン
・WEBサイトの作成
・JavaScriptフレームワーク
・CSSフレームワーク
・認証
・ログ
・通知
・オブジェクトマッパー
・データアクセス層
・SQL Database
・全文検索
・BigQuery
・Redis(キャッシュ)
・Blob(ファイルの保存)
・Notification Hubs(モバイルへの通知)
・SignalR(双方向通信、チャットとか)
これらの理解があって使いこなすことで本格的なSaasアプリケーションが作れるようになっていきます。
どんなシステムか?
上記の技術を利用してどんなタスク管理を作ったのか?簡単に概要を解説します。ざっくりいうと巷にあるタスク管理の良いところを集めた感じで作ってみました。カレンダー+タスク管理+秘書BOTという感じで機能を提供しています。タスク管理部分はこんな機能があります。
・かんばん
・タスク一覧
・ガントチャート
・コメント一覧
・担当者別カレンダー
・プロジェクトカレンダー
・年間カレンダー
ガントチャート。ちょっとまだ機能が足りないので進化させたい。親子タスクはもちろん、孫、ひ孫タスクも対応してます。
タスクへのコメントを新着順でチェックしたいときはこういうFacebookみたいなSNSチックなビューで見たいですよね。
担当別のカレンダー。管理者が負荷を管理するときにはこのビューが見やすいです。
ある程度の規模のイベントの準備(セミナー開催、舞台、映画撮影など)の時には年間カレンダーだと管理しやすいです。
映像制作、TV業界などではエクセルでこういうのを作って管理している人が多かったりします↓
https://frame-illust.com/?p=13559
タスク管理に無い機能としてデフォルトでカレンダーもあります。
Googleカレンダー、Office365のカレンダーと連携できます。通常業務を考えるとタスクの管理は会議や外出など予定を見ながら調整したいのでカレンダーがないと不便です。
さらにLINE,Facebookの秘書BOTも作ってみました。BOT経由でタスクの追加、完了ができます。
簡単ですが全体像はこんな感じです。
Saasに必要な技術スタック
こういったシステムを作っていくにはどうすれば良いのか?開発ツール、クラウド、外部APIの組み合わせで作成できます。
ツール
まずはツールが必要です。開発ツールとして私は以下を使いました。ほぼこの2つでいけます。
1. Visual Studio
2. Management Studio
Visual Studioは個人なら無料で利用できます。
https://visualstudio.microsoft.com/ja/downloads/
Management Studioも無料で利用できます。
https://docs.microsoft.com/ja-jp/sql/ssms/download-sql-server-management-studio-ssms?view=sql-server-ver15
クラウドサービスと外部API
- Azure (WebSites,Blob,Database,SignalR...etc)
- GCP (GoogleCalendar,BigQuery...etc)
- LINE API
- Facebook Messenger API
あたりを活用しています。
Webサイトの作成
まずはSaasということでWebサイトを作成する必要があります。私はC#+ASP.NET Coreで構築しました。
他のフレームワークでも良いでしょう。Python+Djangoやruby on railsなどいろいろあります。
Webアプリのデプロイ
Azure Appサービスを利用するとWEBアプリを簡単にデプロイできます。マイクロソフトが提供しているASP.NET Coreだけでなく、Node.js、Python、PHP、Javaなど様々な言語・フレームワークのWEBアプリをデプロイ可能です。
ドメインと証明書
Saasサービスを提供するならばおそらく独自ドメインでの提供になるでしょう。ドメインは私はGoDaddyを利用しています。世界標準なのでMicrosoft365や他のサービスでの連携機能が充実しています。
パフォーマンス
パフォーマンスの良いシステムを構築する知識をしっかりと頭に入れてシステムを構築します。
コンピューターサイエンスの基礎の理解、それぞれの言語の特性、ライブラリの適切な使用、などを丁寧に行いハイパフォーマンスなシステムを構築します。また自分が利用するクラウドのベストプラクティスも知っておきましょう。
パフォーマンス向上で知っておくべきコンピューターサイエンスの基礎知識とその実践
WEBアプリケーションのパフォーマンスをUPするために知っておくべき技術と知識
世界で通用するエンジニアになるための高度な技術記事(英語)
Azure アプリケーション アーキテクチャ ガイド
内容はほぼ同じですがこちらの記事では画像や表などを使用してより丁寧に説明しています。
パフォーマンス向上で知っておくべきコンピューターサイエンスの基礎知識とその実践
UI層(ブラウザ表示)
UI層の構築においては以下を検討しましょう。
・JavaScriptフレームワーク
・CSSフレームワーク
・機能のUIデザイン
JavaScriptフレームワーク
フロントエンドはJavaScriptの上に何を乗せるかになりますが私はTypeScriptを使用しました。フレームワークはjQuery、Vue.js, Angularなど好きなので良いと思います。
Reactが今は人気が上昇中です。Angularは機能がいろいろ増え過ぎた結果、機能過剰に陥って今後は使いたくないという意見が多いようです。
https://note.com/erukiti/n/na654ad7bd9bb
https://note.com/rdlabo/n/neb8f70d1c874
React,Vueあたりがお薦めです。が、数年後にまた変わってるかもしれないのでJavaScriptフレームワークの辛いところです。prototype.jsとかありましたよね。その後にすぐjQueryが来たので廃れてしまいました。
(※自分はフロントのフレームワークのアップデートに引きずられるのが嫌でテンプレートだけあれば良いので自分で必要な分のフレームワークを自作しています。)
CSSフレームワーク
CSSフレームワークはいろいろあるので良さそうなのを選択しましょう。
https://webdesign-trends.net/entry/11473
https://eng-entrance.com/css-framework
https://cyclo.jp/archives/blog/2301/
https://uxmilk.jp/75765
どちらでも良いと思います。フレームワークはどちらでも良いですが、sassは非常に便利で共通部分の部品化が可能なので自分は導入しておいてとても助かりました。
$color-primary:#5167B8;@mixindisplay_flex(){display:-webkit-flex;display:-moz-flex;display:-ms-flex;display:-o-flex;display:flex;}
@import"Mixin.scss";.top-bar-panel.user-name-panel{background-color:$color-primary;@includedisplay_flex();}
変数を宣言したり、ブラウザの差異を吸収するコードを書けます。
機能のUIデザイン
機能のUIデザインについてプログラマ視点からまとめてみました。Saasアプリケーションで提供する際に必要な基本的な機能は以下の感じになります。
・追加、更新、削除、閲覧
・一覧、詳細ページ
・フィルタ&ソート
・ビューの分割
・ページング
・検索
あたりをどういったUIで実現するかを考える必要があります。こういった機能をどうUIに落とし込むかについて記事を書いたのでQiitaでも共有します。
プログラマのための機能のUIデザイン
元記事です↓
グループウェアを再定義し素晴らしいUIについて考える
素晴らしいUIデザインその2(レフトメニューについて)
WEBアプリケーション層(ASP.NET Core)
Webアプリの動作部分はMVCで組みます。実際にWEBアプリを組むに当たって
・認証
・ログ
・通知
・オブジェクトマッピング
・データアクセスレイヤー
あたりをどうするかは課題です。
認証
ASP.NETが認証の各機能を提供してくれています。それを活用しましょう。
ログ
ASP.NETがDIの機能を提供しています。DIにログ機能をインジェクションしていく感じです。
https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1
割と移り変わりがあるので検索キーワードでリンクを提供します。
https://www.google.com/search?q=asp.net+log+library&oq=asp.net+log+library
私はBigQueryにログを保存するようにしています。ログデータはサイズが大きくなりがちです。BigQueryは何といっても安いので助かります。
通知
例えばタスクを追加した時に、プロジェクトのメンバーに通知を送る機能を考えます。プロジェクトのメンバーが20人いると20人分のメールボックスor秘書BOTに通知を送ることになります。メール送信&LINE API呼び出しで1人当たり500ミリ秒の処理時間がかかるとすると順次実行すると20人だと10秒もかかることになります。ブラウザで保存ボタンを押してPOSTされた時にこの処理を実行すると保存ボタンを押してからレスポンスが戻ってくるまで10秒待たされることになりUXが悪くなります。
このようなときは通知の送信処理を別で実行するように設計しましょう。
・Azure Functionなど別のサービスで実行
・ASP.NET Core上でバックグラウンドサービスを走らせ、そのサービスに処理を移譲
するように設計することで保存ボタンのレスポンスを素早く返すことができ、UXへの悪影響を防ぐことができます。
Azure Function
Azure Functionはサーバーレスで処理を実行できるサービスです。Amazon Lambdaと似たサービスになります。
https://azure.microsoft.com/ja-jp/services/functions/
https://docs.microsoft.com/ja-jp/azure/azure-functions/
Saasアプリケーションを例にすると
・通知の送信(UX向上のため)
・タスクの詳細のテキストにあるURLのページのOpenGraphの画像を定期的に取得する
・指定時刻が来たら記事を公開する
・不要になったデータを削除する
・毎日1時にログテーブルを作成する
などをアプリケーションに実装したいシーンはあると思います。そういった際にAzure Functionで処理を実行すると良いでしょう。
Azure Service Bus
サービスバスを使用することでキューとしてデータを保存し、順次別のプログラムで処理をすることが可能です。
https://azure.microsoft.com/ja-jp/services/service-bus/
https://docs.microsoft.com/ja-jp/azure/service-bus-messaging/service-bus-dotnet-get-started-with-queues
POST時にサービスバスへデータを追加→Azure Functionを1分毎に実行しサービスバスからデータを取り出して処理
という感じで別サービスでの通知処理を実行できます。例えば
POST時に通知先のユーザーID一覧をサービスバスへ追加→Azure FunctionでユーザーID一覧を取り出し通知を送信
という感じになるでしょう。
バックグラウンドサービスを作って実行
C#でバックグラウンドサービスを作成します。完全には動かないかもですがこんな感じの実装になります。
publicabstractclassServiceCommand{publicDateTimeOffsetScheduleTime{get;set;}=DateTimeInfo.GetNow();publicDateTimeOffset?CommandStartTime{get;set;}publicDateTimeOffset?CommandEndTime{get;set;}publicabstractvoidExecute();publicvoidWriteInfo(Stringmessage){#if DEBUG
Console.WriteLine(message);#endif
}protectedvoidSetDatabaseSetting(Databasedatabase){vardb=database;db.CommandCreated+=Database_CommandCreated;}protectedvoidDatabase_CommandCreated(objectsender,CommandCreatedEventArgse){varcm=e.Command;cm.CommandTimeout=150;}}publicclassServiceCommandExecuteResult{publicStringName{get;set;}publicDateTimeOffsetStartTime{get;set;}publicTimeSpanDuration{get;set;}publicServiceCommandExecuteResult(ServiceCommandcommand){this.Name=command.GetType().Name;this.StartTime=command.CommandStartTime.Value;this.Duration=command.CommandEndTime.Value-command.CommandStartTime.Value;}publicoverridestringToString(){returnString.Format("{0} (StartTime={1}, Duration={2}ms)",this.Name,this.StartTime.ToString("MM/dd HH:mm:ss.fffffff"),this.Duration.TotalMilliseconds);}}publicenumBackgroundCommandServiceState{Ready,Executing,Suspend,}publicclassBackgroundCommandService{privateThread_Thread=null;privateAutoResetEvent_AutoResetEvent=newAutoResetEvent(true);privateInt32_ThreadSleepSecondsPerCommand=0;privateConcurrentQueue<ServiceCommand>_CommandList=newConcurrentQueue<ServiceCommand>();privateServiceCommand_CurrentCommand=null;privateList<ServiceCommand>_PreviousCommandList=newList<ServiceCommand>();privateDateTimeOffset_PreviousResetTime=DateTimeInfo.GetNow();privateInt64_ExecutedCommandCount=0;privateInt64_ExecutedSeconds=0;privateBoolean_IsStarted=false;privateInt64_IsSuspend=0;publicStringName{get;privateset;}publicBackgroundCommandServiceStateState{get{if(_IsStarted){if(_IsSuspend==1){returnBackgroundCommandServiceState.Suspend;}else{returnBackgroundCommandServiceState.Executing;}}else{returnBackgroundCommandServiceState.Ready;}}}publicInt64ExecutedCommandCount{get{returnInterlocked.Read(ref_ExecutedCommandCount);}}publicInt64ExecutedSeconds{get{returnInterlocked.Read(ref_ExecutedSeconds);}}publicInt32CommandCount{get{return_CommandList.Count;}}publicBackgroundCommandService(Stringname,Int32threadSleepSecondsPerCommand){this.Name=name;_ThreadSleepSecondsPerCommand=threadSleepSecondsPerCommand;}publicvoidStartThread(){if(_Thread!=null){thrownewInvalidOperationException();}_Thread=newThread(()=>this.Start());_Thread.Name=String.Format("BackgroundService({0})",this.Name);_Thread.IsBackground=true;_Thread.Priority=ThreadPriority.BelowNormal;_Thread.Start();_IsStarted=true;}privatevoidStart(){while(true){varl=newList<ServiceCommand>();while(_CommandList.TryDequeue(outvarcm)){if(cm==null){continue;}l.Add(cm);}varnow=DateTimeInfo.GetNow();DateTimeOffset?minNextStartTime=null;foreach(varcminl){if(cm.ScheduleTime>now){_CommandList.Enqueue(cm);if(minNextStartTime==null||minNextStartTime>cm.ScheduleTime){minNextStartTime=cm.ScheduleTime;}}try{varsw=Stopwatch.StartNew();cm.CommandStartTime=DateTimeInfo.GetNow();_CurrentCommand=cm;cm.Execute();cm.CommandEndTime=DateTimeInfo.GetNow();sw.Stop();Interlocked.Increment(ref_ExecutedCommandCount);Interlocked.Add(ref_ExecutedSeconds,sw.ElapsedTicks/TimeSpan.TicksPerSecond);if(_PreviousResetTime.Day!=now.Day){Interlocked.Exchange(ref_ExecutedCommandCount,0);Interlocked.Exchange(ref_ExecutedSeconds,0);}_PreviousResetTime=now;}catch(Exceptionex){cm.CommandEndTime=DateTimeInfo.GetNow();try{varbl=HignullLog.Current.AddAsync(ex,false);}catch{}}finally{_CurrentCommand=null;}if(_ThreadSleepSecondsPerCommand>0){Thread.Sleep(_ThreadSleepSecondsPerCommand);}}_PreviousCommandList=l;if(minNextStartTime.HasValue){varts=minNextStartTime.Value-DateTimeInfo.GetNow();_AutoResetEvent.WaitOne((Int32)ts.TotalMilliseconds);}else{_AutoResetEvent.WaitOne();}}}publicvoidSuspend(){Interlocked.Exchange(ref_IsSuspend,1);}publicvoidResume(){Interlocked.Exchange(ref_IsSuspend,0);}publicvoidAddCommand(ServiceCommandcommand){if(_Thread==null){return;}if(_IsSuspend==1){return;}_CommandList.Enqueue(command);_AutoResetEvent.Set();}publicvoidAddCommand(IEnumerable<ServiceCommand>commandList){if(_Thread==null){return;}if(_IsSuspend==1){return;}foreach(varcommandincommandList){_CommandList.Enqueue(command);}_AutoResetEvent.Set();}}
アプリケーションの初期化処理時に別スレッドを開始します。
publicclassBackgroundService{publicstaticreadonlyBackgroundCommandServiceDefaultProcess=newBackgroundCommandService("Default",0);}publicclassStartup{publicvoidConfigure(IApplicationBuilderapp,IWebHostEnvironmentenv){//--省略BackgroundService.DefaultProcess.StartThread();}}
各コントローラーでCommandを作ってサービスに渡します。こうすることで処理をバックグラウンドサービスで実行し、クライアントにレスポンスを早く返すことが可能です。
publicclassSendNotificationCommand{publicoverridevoidExecute(){//通知の送信処理...}}publicclassTaskApiController{publicasyncTask<TaskRecord>Add(){vardc=newTaskRepository();vartaskCD=awaitdc.Add(.....);BackgroundService.DefaultProcess.AddCommand(newSendNotificationCommand());returnrTask;}}
この方法の利点は他のサービス(Azure Function,Service Busなど)を使用しないのでお金がかからないということと、WEBサイト内の実装になるので配置の手間も軽減できるというメリットがあります。
以上のようにいくつかの時間がかかる処理はこういった形で別スレッドや別サービスにFIFOで処理を投げられるような設計にすることが必要です。Azure FunctionやService Busを利用する、同じプロセス内で別スレッドで実行する、などの方法でシステムのアーキテクチャを設計しましょう。
オブジェクトマッパー
DB⇔Model⇔API Resultでオブジェクトのプロパティの値のマッピングは面倒な処理です。なのでAutoMapperなどのマッピングライブラリを導入することになると思います。私はHigLabo.Mapperというライブラリを作りそれを利用しています。2020年8月現在、世界最速です。詳しくはこちら↓
C#で世界最速のMapperライブラリを作ってみた(AutoMapperなどよりも3倍-10倍ほど高速)
CodeProjectの元記事(英語)
https://www.codeproject.com/Articles/5275388/HigLabo-Mapper-Creating-Fastest-Object-Mapper-in-t
ExpressionTreeを動的に活用し世界最速の速度を実現しています。
DBアクセスレイヤー
データアクセスレイヤーはEntityFrameworkなどあります。ORMapperの利用も良いでしょう。
私自身はデータアクセスレイヤーもDbSharpというツール&ライブラリを作ってストアドプロシージャを簡単に呼び出せるようにしてあります。
CreateProcedureHi_App_Task_Add(@TaskCDUniqueIdentifier,@TitleNVarChar(100),@CreateUserCDUniqueIdentifier,@CreateTimeDateTimeOffset(7),@ProjectCDUniqueIdentifier,@StateCDUniqueIdentifier,@UserCDUniqueIdentifier,@StartDateDate,@DueDateDate,@DescriptionNVarChar(max),@TimeStampTimestampoutput,@TagListNvarchar255Tablereadonly)AsinsertintoDTask(TaskCD,Title,CreateUserCD,CreateTime,UpdateUserCD,UpdateTime//--省略--,Description)values(@TaskCD,@Title,@CreateUserCD,@CreateTime,@CreateUserCD,@CreateTime//--省略--,@Description)insertintoDTaskTag(TagCD,TagText,TaskCD)selectNEWID(),Value,@TaskCDfrom@TagListGo
というストアドをDBに作ってDbSharpApplicationでインポートして
そのあとC#のクラスを生成します。そうすると
varsp=new();sp.TaskCD=Guid.NewGuid();sp.Title="Qiitaに記事を書く";//--省略varaffectedRecordCount=awaitsp.ExecuteNonQuery();
という感じで簡単にストアドプロシージャを呼び出せます。非常に簡単にDBアクセスのコードが書けるようになります。ツールのダウンロードはこちらから↓
https://github.com/higty/higlabo/tree/master/NetStandard/Tools/DbSharp/Compiled
コミュニティに貢献ということでオープンソースです。SQLサーバー、MySQLに対応しています。是非使ってみてください。
https://github.com/higty/higlabo
https://github.com/higty/higlabo/tree/master/NetStandard/Tools/DbSharp
詳しい内部の設計はこちらです(英語)↓
https://www.codeproject.com/Articles/776811/DbSharp-DAL-Generator-Tool-on-NET-Core
データベース
データベースはAzure SQL Databaseで作りました。Azure SQL Databaseの魅力はこちらです。
・AIが推奨インデックスを提案してくれる
・複数のDBの運用時にコストを最小化できる
・簡単に全文検索を導入できる
AIが推奨インデックスを提案
Azureのポータルから簡単にインデックスを適用できます。利用パターンから推奨インデックスを提案してくれるので何も考える必要がありません。非常に重宝します。
DBコストの最小化
複数のDBを運用している場合、エラスティックプールを活用するとコストを削減できます。
https://docs.microsoft.com/ja-jp/azure/azure-sql/database/elastic-pool-overview
全文検索
like検索はデータ量が多くなるとパフォーマンスが出ません。その場合、全文検索を導入することでパフォーマンスを向上することができます。SQLデータベースでは非常に簡単に全文検索機能の導入が可能です。全文検索を導入することで圧倒的なパフォーマンスと関連度や類義語検索などの機能を提供できます。
https://www.slideshare.net/dahatake/azure-search-114345418
https://news.mynavi.jp/article/zeroazure-16/
https://docs.microsoft.com/ja-jp/sql/relational-databases/search/full-text-search?view=sql-server-ver15
ECサイトのSaasを提供するとして、膨大な商品データの検索機能を提供する場合などにこの機能を活用すると良いでしょう。
Webサイトの公開
作成したWebサイトをクラウドにデプロイして公開する必要があります。Visual StudioからAzureへ簡単に公開できます。
個人的にはマイクロソフトが提供するツール&サービスでの開発環境はとても生産性が高いです。
・開発ツール(Visual Studio)
・クラウド(Azure)
・プログラミング言語(C#)
の3つを持っている会社は世界でマイクロソフトだけなのでWEBアプリの開発では各ツールとサービスの連携が非常にスムーズです。AI系はもう1歩という感じですが。
BigQuery
私はログデータの保存にはBigQueryを使用しています。料金が安いというのが一番大きな理由です。BigQueryには日付ごとのテーブルの作成の機能があります。日付の異なるテーブルをselect時にまとめて取得することも可能です。
SELECT*FROM`hignull.Log.WebAccessLog_*`whereRequestTime>'2020-08-20 15:00:00.00 UTC'orderbyRequestTimedescLIMIT1000
BigQueryはレコード数が億とかになっても数秒でselectできます。ただし数件のレコード取得でも1秒ちょっと時間がかかります。クエリ結果をキャッシュすると2回目以降は一瞬で返ってきます。ログデータは不変です。うまくクエリのパラメータを設計しましょう。例えば現在時刻が15時15分時にUIのリンクで
・直近1時間のログ(変化あり)
・14時-15時のログ(不変)
・13時-14時のログ(不変)
というリンクを表示するように画面設計します。14時-15時のログは不変なのでクエリ結果をキャッシュしても良いはずです。こうすることで初回以外は高速でログデータを表示させることが可能になるでしょう。
Redis
Redisを利用していくつかの便利な仕組みをアプリケーションに導入しパフォーマンス向上とスケーラビリティの向上を実現しましょう。
・データキャッシュ
・コンテンツキャッシュ(HTMLをキャッシュする)
・期限付きキャッシュ
・キュー
・Publish,Subscribe
・データのインポート/エクスポート
などの機能があります。
Blob
ユーザーのプロフィール画像やタスクにファイルをアップロードして共有したいなどの機能を実装したい場合にはBlobを使用しましょう。
https://azure.microsoft.com/ja-jp/services/storage/blobs/
https://docs.microsoft.com/ja-jp/azure/storage/blobs/storage-blobs-introduction
Notification Hub
自分が担当のタスクが他のユーザーによって追加された場合などにモバイルへの通知をしたい、といった場合にはNotification Hubsを使用しましょう。
https://azure.microsoft.com/ja-jp/services/notification-hubs/
https://docs.microsoft.com/ja-jp/azure/notification-hubs/notification-hubs-push-notification-overview
Nugetでパッケージをダウンロードしてこんな感じでJSON文字列を作ってモバイルへ送信できます。
varcl=newNotificationHubClient(....);varr=newAndroidNotificationRecord();r.Title=title;r.Message=messsage;r.Body=messsage;r.ChannelCD=channelCD;String[]tagList=newString[]{"Tag1","Tag2"};varjson=r.GetJsonPayload(MobileNotificationType.Generic);varresult=awaitclient.SendFcmNativeNotificationAsync(json,tagList);
Azure SignalR Service
SignalRを利用すると双方向通信を実現できます。
https://azure.microsoft.com/ja-jp/services/signalr-service/
https://docs.microsoft.com/en-us/azure/azure-signalr/signalr-overview
https://news.mynavi.jp/article/zeroazure-33/
例えば
・サーバーからブラウザへ通知を送りたい
・ブラウザのダッシュボードのグラフをリアルタイム更新したい
・チャットアプリケーションを作りたい
などの機能を実現するのにSignalRは最適です。
Saasアプリケーションでいくつかの例を挙げると
・タスクにコメントがついたら通知を送る
・オークションサービスで他のユーザーが自分より高い値段をつけたら通知を送る
・リアルタイムのオンラインゲームを実装する
など様々なシーンで双方向通信を行いたいというニーズはあるかと思います。
Azure SignalR Serviceではサーバーのリソースを簡単に増減することが可能です。WEBサイト上にホストする形だとスケーラビリティに問題が出てきますが、Azure SignalR Serviceでは外部のサーバーで双方向通信を実現するためWEBサイトとは別でサーバーのリソースの設定が可能になっています。
またこのアーキテクチャになっていることでAzure Functionと組み合わせてサーバーレスで双方向通信アプリケーションの作成が可能になります。
https://tech-lab.sios.jp/archives/17849
https://blog.shibayan.jp/entry/20190126/1548494039
https://blog.xin9le.net/entry/2019/04/29/235529
外部API
自分のSaasアプリケーションで外部のAPIと連携したい場合もあると思います。代表的なのは
・LINE API
・Facebook Messenger API
・Googleカレンダー
・Microsoft365 Graph API
などです。
Azure Bot Service
LINEやFacebookのAPIを活用してBOTを作成したい場合、AzureにはAzure Bot Serviceというサービスがあります。
https://azure.microsoft.com/ja-jp/services/bot-service/
https://docs.microsoft.com/ja-jp/azure/bot-service/?view=azure-bot-service-4.0
https://news.mynavi.jp/article/zeroazure-31/
Azure FunctionやWEBサイトでBOTを作ることも可能です。私はAzure Bot Serviceでは自分の要件が実現できなかったため、BOT用のWebサイトを構築して処理を全て自分で実装しました。その際にLINE,FacebookのAPI用のライブラリを作ったので共有します。
https://github.com/higty/higlabo/tree/master/NetStandard/HigLabo.Bot.Line
https://github.com/higty/higlabo/tree/master/NetStandard/HigLabo.Bot.Facebook
Nugetから使用できます。2,3年前に作ったままなので新しいAPIが未対応かもしれません。要望や質問あれば連絡ください。
Googleカレンダー、Microsoft365 Graph API
GoogleカレンダーやGraph APIについてはNugetで公式ライブラリがあり、割と簡単に呼び出すことができます。
DevOps
Saas型でサービスを提供する場合、継続的に機能を進化させていく必要があります。作成したWEBアプリはGitHubなどで管理していると思いますが、masterにコミットしたら自動でデプロイしてくれるようにすることで少ない手間で継続的なアップデートができるようになります。CI/CDを実現するためのサービスを紹介します。
GitHub Action
GitHubのCI/CDです。
https://github.co.jp/features/actions
https://docs.github.com/ja/actions
https://www.ritolab.com/entry/206
2020年現在、GitHubはソースコード管理のツールとしてデファクトスタンダードだと思いますが、そのGitHubのCI/CDなので今後どんどん広まってデファクトスタンダードになっていくと思われます。
Azure DevOps
私はAzure DevOpsを使用しています。2016年くらいだとGitHubのプライベートレポジトリが有料だったのでAzure DevOpsを使用していました。CI/CDの機能もこちらにあったのでAzure DevOpsでソースの管理とCI/CDのパイプラインを作って運用しています。Azure DevOpsはプライベートレポジトリでも無料なのでそこが良いですね。そのうちGitHub Actionsに乗り換えることになるのか…今後要検討です。
WebSitesのSwap
さてSaasのサービスを進化させていくに当たって更新が必要なのはWEBアプリとDBのスキーマです。このチャプターではWEBアプリについての機能を解説していきます。
Azure Webサイトにはデプロイスロットという概念があり、複数の異なるバージョンのWEBアプリを配置することができます。
https://docs.microsoft.com/ja-jp/azure/app-service/deploy-staging-slots
https://blog.memobog.net/2018/07/22/webapps-swap/
https://qiita.com/yuhattor/items/db00e1406fd64ed74740
https://maku.blog/p/bog7iq8/
・myapp.azurewebsites.net(プロダクション環境)
・myapp-staging.azurewebsites.net(ステージング環境)
という2つのスロットを用意してSwapすると入れ替えできます。こうすることでもし入れ替え後に問題が発生した場合にすぐに元に戻すことが可能です。戻せる準備をしてリスクヘッジをすることでSaasサービスを進化させていくことが可能になります。
その他のAppサービスの細かい機能の解説です↓
https://azure.github.io/AppService/2020/05/15/Robust-Apps-for-the-cloud.html
デプロイスロットのもう1つの機能としてリクエストの一部を分割して送信することができます。
この画面の右端にTrafficという項目があります。例えばstagingの%を10%に設定すると、Azureがリクエストの90%をproduction環境へ、残りの10%をstaging環境へ振り分けてくれます。FacebookやTwitterが良くやっているように、新機能が一部のユーザーにリリースされたというニュースがあります。例えばこんな感じの記事です↓
https://www.itmedia.co.jp/news/articles/2004/27/news127.html
それと同様のことを実現できます。
Selenium
Saasサービスで新しい機能を追加してデプロイした時に、デグレードは避けたいところです。Saasサービスで1日に10回とかデプロイするとなると手動でのテストは現実的ではありません。Seleniumを使用するとプログラムによる自動テストを行うことができます。
https://www.selenium.dev/documentation/ja/
https://tanuhack.com/selenium/
http://ichannel.tokyo/technoracle/selenium%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%83%BC%E3%82%92%E6%93%8D%E4%BD%9C%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95-%E5%9F%BA%E6%9C%AC%E7%B7%A8/3976/
Seleniumでリグレッションテストを行える環境を準備しておくことで、自信をもってSaasを進化させていくことが可能です。
CI/CDのまとめ
以上を組み合わせてCI/CDを実現します。私が取っている手順は
1. masterにコミット→Staging環境
2. SeleniumでStaging環境へテストを実行
3. Swapで入れ替え
という感じでやっています。
DBスキーマの更新
このチャプターではDBスキーマの更新について説明します。テーブルの列の追加、更新、削除を行う場合はWEBアプリの作り方によってはWebアプリのコードも変更の必要があります。
私はWEBアプリからのDBの処理は全てストアドプロシージャを使用しています。パフォーマンス、セキュリティ面でベターなのと、スキーマ更新時にWEBアプリの変更が不要に出来るのでそういう設計にしています。ストアドプロシージャを使ってDBのインターフェースを定義し、一度作ったストアドプロシージャのインターフェースを不変にして運用します。そうすることでWEBアプリを止めることなくDBを更新できるようになります。
詳しいやり方はこちらに書いたので参考にしてみてください。
データベースのテーブルとストアドプロシージャのバージョン管理方法の紹介
DB無停止でスキーマ更新可能なテーブルとストアドのバージョン管理方法の紹介
ORマッパーでWEBアプリ中にSQLを埋め込む場合は別途方法を考える必要があるでしょう。DBスキーマの更新は少し難しいものになるかもしれません。
まとめ
この記事ではSaasサービスを継続的に進化させていくために必要ないろいろな知識について解説しました。
・WEBサイト
・UIフレームワーク
・データベース
・キャッシュ
・双方向通信
・モバイル通知
・DevOps
・UIテスト
個人でしっかりとしたSaasサービスを提供するためにはこういった知識が必要です。経営者の観点からはSaasサービスを提供するにはこれらの技術セットをもった人材を揃える必要があります。一人で全てできるフルスタックエンジニアがいると良いですが、そのような優秀な人材は見つからないのでおそらく3人―10人くらいで揃えることになるのかなと思われます。
Hignull
これらの技術スタックとクラウドサービスで実際に作ってみたタスク管理+カレンダー+予約管理+秘書BOTのサービスが以下になります。
https://www.hignull.com/
コミュニティへの貢献ということで最近フリープランを追加しました。
既存のタスク管理システムで
・Googleカレンダーとタスク管理で予定とタスクが別管理でやりづらさがある
・ガントチャートで孫タスクが使えない
・いろいろな切り口のビューでタスクを見たい(かんばん、一覧、ガントチャート、担当別、コメント一覧など)
・電車で移動中などにタスクを秘書BOTから簡単に登録したい
などなど不満がある人は是非使ってみてください。
現状はタスク管理ベースのサービスですが今後はチャット機能を実装予定です。カレンダーとタスク管理が分かれて不便なのと同様、チャットとタスク管理が分かれて不便なのを解消できるようなシステムを目指しています。
今回はタスク管理を例に解説しましたが、ECサイト、チャットサービス、経費管理システム、HR系のサービスなどといったSaasサービスを提供する際にも同様のことを考慮する必要があります。この記事がSaasサービスを改善したいと思っている全ての人の参考になれば幸いです。