はじめに
クラウド上にアプリケーションを構築する際の設計手法として、「サーバーレスアーキテクチャー」の注目度が上がっています。
ガートナー社の発表では、『企業が注視すべき、プラットフォームを実現する主要なテクノロジ』として紹介され、今後2〜5年以内にメインストリームになると予測されています。
ガートナー、「先進テクノロジのハイプ・サイクル:2017年」を発表
本稿では、モバイル・バックエンドとしてのサーバーレス適用について、以下のような簡易なサンプルアプリを作成して考察します。
・サーバーレス実行環境としてIBM Cloud Functionsを利用する
・サーバーサイドSwiftでコードを書く 〜バイオリズム診断〜
・iOSアプリからREST APIアクセスしてみる
<ご注意>
本稿は2017年8月時点の情報に基づいており、現在の情報と異なっている可能性があります。
本稿の内容は執筆者独自の見解であり、所属企業における立場、戦略、意見を代表するものではありません。
<2017年12月8日 記事修正>
以下に伴い、文言を修正しました。
・IBM OpenWhiskはIBM Cloud Functionsに名称変更されました。
・IBM BluemixはIBM Cloudブランドに統合されました。
サンプルアプリの完成イメージ


前提環境
- Swift 3.1
- Xcode 8.3
IBM Cloud Functionsとは
IBM Cloud上で提供されているイベント駆動型コード実行サービスです。
メリット
コードを実行している間だけサーバーリソースが自動的に割り当てられ、課金は実行時間とメモリ量にて従量計算されます。
このためサーバーを常時起動させるIaaSやPaaSに比べてコストメリットがあります。
IaaSやPaaSと対比して、Function as a Service = FaaS とも言います。
さらに、管理する対象がコードだけとなり、サーバーの存在を意識せず、運用監視の手間を軽減できることから「サーバーレス」というわけです。
概念
Cloud Functionsでは、何らかの処理を起動するイベントのことを「トリガー」、実行される処理を「アクション」と呼んでいます。
以下は、Cloud Functionsの公式ドキュメントからの引用です。
トリガーとは
トリガーは、ユーザーが明示的に発生させることも、ユーザーの代わりに外部イベント・ソースによって発生させることもできます。
フィード は、Cloud Functions によってコンシューム可能なトリガー・イベントを発生させるように外部イベント・ソースを構成するための便利な方法です。
フィードの例として、以下があります。
- データベースの文書に追加または変更があるたびにトリガー・イベントを発生させる Cloudant データ変更フィード。
- Git リポジトリーへのコミットごとにトリガー・イベントを発生させる Git フィード。
アクションとは
アクションは、JavaScript 関数、Swift 関数、Python 関数、または Java メソッドとして、または、Docker コンテナーにパッケージした実行可能なカスタム・プログラムとして、作成できます。
例えば、アクションを使用して、イメージ内の顔を検出したり、データベース変更に応答したり、一連の API 呼び出しを集約したり、ツイートを投稿したりできます。
アクションは、トリガーと関連付けて起動できるほか、REST API等によってアクションを直接起動することも可能です。
複数のアクションおよびフィードをまとめた「パッケージ」という機能もあります。
- Watsonとの連携
- Cloudantデータベースとの連携
- Push通知
- Slackとの連携 などが一例です。他にも様々なパッケージが提供されています。
その他の詳しい情報はCloud Functionsの公式ドキュメントをご参照ください。
Cloud FunctionsとiOSアプリとの連携
準備
Cloud Functionsの利用にはIBM Cloudアカウントが必要です。
まだお持ちでない方は、簡単にIBM Cloudライト・アカウントを取得することができますので、こちらを参照してください。
https://www.ibm.com/cloud-computing/jp/ja/bluemix/lite-account/
Cloud FunctionsアクションをサーバーサイドSwiftで書いてみる
IBM Cloudダッシュボードのハンバーガーメニューから「機能」を選択し、Cloud Functionsのトップ画面に遷移すると、そのまますぐにアクションの作成を開始できます。
アクション作成時に、ランタイム(≒言語)を指定します。
サーバーサイドSwiftが使えるのはIBM Cloudの特徴の一つです。
今回はiOSアプリとの連携を試すのですから、当然Swift一択です!
コード編集方法
2種類あります。
【方法1】Cloud Functionsの「開発ビュー」にて、ブラウザ上でコードを編集する。
【方法2】ローカルで編集したコードを、コマンドラインインターフェイス(CLI)でアップロードする。※iOS SDKも提供されています。
なおデプロイについては、公式ドキュメントで以下のように記載されていますので、アクション実行時にコードがサーバーにデプロイされる、という理解が正しいようです。
アクションは、トリガーが発生するとすぐにデプロイされて実行されます。
今回は、Swift on LinuxのブラウザベースのREPL、IBM Swift Sandboxを利用してコーディング&デバッグを行い、出来たコードをCloud Functionsの開発ビューに貼り付けました。


サーバーサイドSwiftのコード
エントリーポイントのmain
と、引数および戻り値の型[String:Any]
は、アクションでの決まりごとです。
import Foundation
// アクションのエントリーポイント
func main(args: [String: Any]) -> [String: Any] {
// パラメーター簡易チェック
if args["year"] as? String == nil || args["month"] as? String == nil || args["day"] as? String == nil {
return ["error" : "parameter error"]
}
// パラメーター取得
let year = Int(args["year"] as! String)!
let month = Int(args["month"] as! String)!
let day = Int(args["day"] as! String)!
// 身体
var biorhythmKind = Biorhythm.Body
let bodyRawValue = calcBiorhythm(kind: biorhythmKind, year: year, month: month, day: day)
let bodyDic = ["text" : arrange(kind: biorhythmKind, value: bodyRawValue), "value" : String(bodyRawValue)]
let body = ["body": bodyDic]
// 感情
biorhythmKind = Biorhythm.Emotion
let emoRawValue = calcBiorhythm(kind: biorhythmKind, year: year, month: month, day: day)
let emoDic = ["text" : arrange(kind: biorhythmKind, value: emoRawValue), "value" : String(emoRawValue)]
let emotion = ["emotion": emoDic]
// 知性
biorhythmKind = Biorhythm.Intelligence
let intelliRawValue = calcBiorhythm(kind: biorhythmKind, year: year, month: month, day: day)
let intelliDic = ["text" : arrange(kind: biorhythmKind, value: intelliRawValue), "value" : String(intelliRawValue)]
let intelligence = ["intelligence": intelliDic]
return ["biorhythm" : [body, emotion, intelligence] as Any]
}
// バイオリズム列挙体
enum Biorhythm: Int {
case Body = 0
case Emotion
case Intelligence
// バイオリズム種別名 ※子どもにも読める漢字にしてみた
var kindName: String {
switch self {
case .Body:
return "体"
case .Emotion:
return "心"
case .Intelligence:
return "頭"
}
}
// バイオリズム周期
var cycle: Int {
switch self {
case .Body:
return 23
case .Emotion:
return 28
case .Intelligence:
return 33
}
}
}
// バイオリズム計算
func calcBiorhythm(kind: Biorhythm, year: Int, month: Int, day: Int) -> Double {
// 誕生日のDateを生成
let calendar = Calendar(identifier: .gregorian)
let date = calendar.date(from: DateComponents(year: year, month: month, day:day))
// 今日までの日数計算
let days = calcDays(fromDate: date)
// 計算
let a = Double(days) * 2.0 * Double.pi
let b = a / Double(kind.cycle)
return sin(b)
}
// 今日までの日数計算
func calcDays(fromDate: Date?) -> Int {
let retInterval: Double! = fromDate?.timeIntervalSinceNow
let ret = (retInterval/86400) * -1
return Int(floor(ret)) // n日
}
// 編集
func arrange(kind: Biorhythm, value: Double) -> String {
var retString = kind.kindName + "のバイオリズム:"
let dispValue = floor(value * 100) / 100
retString += String(dispValue)
return retString
}
今回初めてサーバーサイドをSwiftで書いてみて、とても楽しかったのですが、残念ながら2017年8月時点では以下の注意点があります…
<注意点>
Swift on Linux用のFoundationは開発途上です。
今回、実装途中に調べて知ったのですが、URLSession関連が未完成でした。
Githubのこちらのページで、最新のステータスを確認することができます。
https://github.com/apple/swift-corelibs-foundation/blob/master/Docs/Status.md
iOSアプリからアクセスしてみる
作成したアクションにiOSアプリからREST APIでアクセスしてみます。
iOSアプリのコード
import UIKit
import SwiftyJSON
class ViewController: UIViewController {
@IBOutlet weak var button: UIButton!
@IBOutlet weak var datePicker: UIDatePicker!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
/// ボタンTapのAction
///
/// - Parameter sender: sender
@IBAction func buttonTupped(_ sender: Any) {
let ymd = self.splitYmd(date: self.datePicker.date)
self.callApi(year: ymd.year, month: ymd.month, day: ymd.day)
}
/// 年月日分割
///
/// - Parameter date: 指定日
/// - Returns: 年、月、日のタプル
func splitYmd(date: Date) -> (year: Int, month: Int, day: Int) {
let calendar = Calendar.current
let year = calendar.component(.year, from: date)
let month = calendar.component(.month, from: date)
let day = calendar.component(.day, from: date)
return (year: year, month: month, day: day)
}
/// Cloud FunctionsアクションのREST-API連携
///
/// - Parameters:
/// - year: 年
/// - month: 月
/// - day: 日
func callApi(year: Int, month: Int, day: Int) {
// API呼び出し準備(リクエストヘッダ)
let auth = "Basic 認証文字列"
let url = "https://openwhisk.ng.bluemix.net/api/v1/namespaces/xxxxxxxx%40xx.xxx.com_dev/actions/biorhythm-action?blocking=true"
guard let destURL = URL(string: url) else {
print ("url is NG: " + url) // debug
return
}
var request = URLRequest(url: destURL)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(auth, forHTTPHeaderField: "Authorization")
// リクエストボディ
let params: [String: Any] = [
"year": String(year),
"month": String(month),
"day": String(day)
]
do {
// activityIndicator始動
self.activityIndicator.startAnimating()
self.button.isEnabled = false
self.datePicker.isEnabled = false
request.httpBody = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
let task:URLSessionDataTask = URLSession.shared.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) -> Void in
if error == nil, let _data = data {
// APIレスポンス:正常
let json = JSON(data: _data)
print(json) // debug
// JSON読み込み
let body = json["response"]["result"]["biorhythm"].arrayValue[0]["body"]
let emotion = json["response"]["result"]["biorhythm"].arrayValue[1]["emotion"]
let intelligence = json["response"]["result"]["biorhythm"].arrayValue[2]["intelligence"]
if let bodyText = body["text"].string, let bodyValue = body["value"].string, let emotionText = emotion["text"].string, let emotionValue = emotion["value"].string, let intelliText = intelligence["text"].string, let intelliValue = intelligence["value"].string {
// 画面制御はmainQueueで行う
OperationQueue.main.addOperation(
{
defer {
// activityIndicator停止
self.activityIndicator.stopAnimating()
self.button.isEnabled = true
self.datePicker.isEnabled = true
}
let message = bodyText + " " + self.getIcon(value: bodyValue) + "\n\n"
+ emotionText + " " + self.getIcon(value: emotionValue) + "\n\n"
+ intelliText + " " + self.getIcon(value: intelliValue)
// アラート表示
self.showAlart(resultText: message)
}
)
} else if let error = json["response"]["result"]["error"].string {
// Cloud Functionsアクション内のパラメータチェックエラー
fatalError("API error: " + error)
} else {
// JSON読み込みエラー(想定外のエラー)
fatalError("JSON parse error")
}
} else {
// APIレスポンス:エラー
print(error.debugDescription)
}
})
task.resume()
} catch {
// API呼び出しエラー
print("API call error:\(error)")
return
}
}
/// アイコン取得
///
/// - Parameter value: バイオリズム値
/// - Returns: アイコン
func getIcon(value: String) -> String {
guard let dValue = Double(value) else {
fatalError("Value convert error")
}
// バイオリズムの数値は-1.0から1.0の間の小数。百倍して判定しアイコンを決定する
switch Int(floor(dValue * 100)) {
case (-100)..<(-60):
return "😰"
case (-60)..<(-20):
return "😥"
case (-20)..<(20):
return "😑"
case 20..<60:
return "☺️"
default:
return "😁"
}
}
/// レスポンス文字列をUIAlertControllerで表示する
///
/// - Parameter resultText: 表示テキスト
func showAlart(resultText: String!) {
let alert = UIAlertController(title: nil, message: resultText, preferredStyle: UIAlertControllerStyle.alert)
let close = UIAlertAction(title: "OK", style: UIAlertActionStyle.cancel, handler: {
(action: UIAlertAction!) in
})
alert.addAction(close)
self.present(alert, animated: true, completion: nil)
}
}
JSONパーサー・ライブラリとしてSwiftyJSONを利用しています。
アクションのREST APIエンドポイントおよびBasic認証文字列は、Cloud Functions開発ビューの「RESTエンドポイントの表示」で確認できます。
レスポンスのサンプル
{
"annotations" : [
{
"key" : "limits",
"value" : {
"timeout" : 60000,
"logs" : 10,
"memory" : 256
}
},
{
"key" : "path",
"value" : "xxxxxxxx@xx.xxx.com_dev\/biorhythm-action"
}
],
"duration" : 22,
"name" : "biorhythm-action",
"response" : {
"result" : {
"biorhythm" : [
{
"body" : {
"value" : "0.269796771156971",
"text" : "体のバイオリズム:0.26"
}
},
{
"emotion" : {
"value" : "0.781831482468066",
"text" : "心のバイオリズム:0.78"
}
},
{
"intelligence" : {
"value" : "-0.281732556841373",
"text" : "頭のバイオリズム:-0.29"
}
}
]
},
"success" : true,
"status" : "success"
},
"end" : 1502605503728,
"version" : "0.0.8",
"namespace" : "xxxxxxxx@xx.xxx.com_dev",
"publish" : false,
"start" : 1502605503706,
"activationId" : "d3b1a28aa8a8441483eea972a3d7df22",
"logs" : [
],
"subject" : "xxxxxxxx@xx.xxx.com"
}
"response" > "result"配下に、"biorhythm"という要素があります。
これが、私が作ったCloud Functionsアクションのレスポンス内容です。
デメリット
上記の通り、IBM Cloud Functionsサービスの利用開始はとても簡単で、アクションのコードをアップロードするだけで、簡単にサーバーサイドを実装できました。
その反面、(IBM Cloud Functionsに限らず)サーバーレスには以下のようなデメリットがあります。
デメリット1:
デプロイ可能なコードの最大サイズや、一回の実行にかかる時間の制限(タイムアウト制限)があり、重い処理には向きません。
デメリット2:
同時実行数や秒当たりの実行数に上限があり、コードが100%実行される保証がありません。
デメリット3:
イベント発生のたび動的にリソースが割り当てられる、という特性上、イベント発生の間隔が短い場合はプーリング処理されますが、間隔が長い場合は初回の実行に時間がかかる傾向があります。
今回の検証結果とまとめ
今回サンプルアプリで試してみて、やはり『間隔が長い場合は初回の実行に時間がかかる傾向』は明らかに感じられました。
平均処理時間(実行ログの集計)
実行間隔が空いた場合 | 間隔を空けない場合 | |
---|---|---|
平均時間 | 3.49s | 21ms |
レスポンス性を求められる機能へのサーバーレス適用については、UX観点で注意が必要かもしれません。
一方で、今回は試すことができませんでしたが、サーバーからモバイルへのPush通知は、一番適用しやすいユースケースであると思われました。
一例としては、
・データベースの更新をトリガーとしてアクションを実行し、Push通知
・定期的に外部サービスの情報を取得し、何らかの条件に合致した場合にPush通知
などが考えられます。
現時点での私見になりますが、今回のサンプルアプリによる検証の結果から、以下のような感想を持ちました。
- 基幹系アプリと比べると、モバイルアプリはサーバーレスのデメリットを許容できるケースが多く、サーバーレスを適用しやすいと言える。
- 特に、モバイルへのPush通知については、サーバーレスのメリットを最大限に享受できそうである。
- ただし、モバイルからのリクエスト処理にサーバーレスを使うことは、UX観点で注意が必要である。
おまけ
バイオリズム診断のWeb版も作ってみましたので、良かったら試してみてください。
もちろんバックエンドはIBM Cloud Functionsです。
バイオリズム診断
- バイオリズムは、プラスが好調で1.0が最高、マイナスが不調で-1.0が最悪、0付近は好不調の切り替わる要注意日です。
- ただし、医学的な裏付けはないようですので、占い程度と捉えてください。