サーバーレスとiOSアプリの連携 〜IBM Cloud Functionsを使ってサーバーサイドSwiftで試してみる

# はじめに

クラウド上にアプリケーションを構築する際の設計手法として、「サーバーレスアーキテクチャー」の注目度が上がっています。

ガートナー社の発表では、『企業が注視すべき、プラットフォームを実現する主要なテクノロジ』として紹介され、今後２〜５年以内にメインストリームになると予測されています。
[ガートナー、「先進テクノロジのハイプ・サイクル：2017年」を発表](https://www.gartner.co.jp/press/html/pr20170823-01.html)

本稿では、モバイル・バックエンドとしてのサーバーレス適用について、以下のような簡易なサンプルアプリを作成して考察します。

・サーバーレス実行環境として[IBM Cloud Functions](https://console.bluemix.net/openwhisk/)を利用する
・サーバーサイドSwiftでコードを書く 〜[バイオリズム](https://ja.wikipedia.org/wiki/%E3%83%90%E3%82%A4%E3%82%AA%E3%83%AA%E3%82%BA%E3%83%A0)診断〜
・iOSアプリからREST APIアクセスしてみる

**＜ご注意＞**
本稿は2017年8月時点の情報に基づいており、現在の情報と異なっている可能性があります。
本稿の内容は執筆者独自の見解であり、所属企業における立場、戦略、意見を代表するものではありません。

**＜2017年12月8日 記事修正＞**
以下に伴い、文言を修正しました。
・IBM OpenWhiskは**IBM Cloud Functions**に名称変更されました。
・IBM Bluemixは**IBM Cloud**ブランドに統合されました。

# サンプルアプリの完成イメージ

<img width="326" alt="スクリーンショット 2017-08-13 15.24.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/122917/1bad77cd-736e-4b22-f8dc-c4bc6af0700d.png">

<img width="327" alt="スクリーンショット 2017-08-13 15.25.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/122917/0fd30424-2410-b7fd-0f3b-b3c5a8f81851.png">

**前提環境**

- Swift 3.1
- Xcode 8.3

# IBM Cloud Functionsとは

IBM Cloud上で提供されているイベント駆動型コード実行サービスです。

## メリット

コードを実行している間だけサーバーリソースが自動的に割り当てられ、**課金は実行時間とメモリ量にて従量計算されます。**

このためサーバーを常時起動させるIaaSやPaaSに比べてコストメリットがあります。
IaaSやPaaSと対比して、Function as a Service = FaaS とも言います。

さらに、管理する対象がコードだけとなり、**サーバーの存在を意識せず、運用監視の手間を軽減できることから「サーバーレス」というわけです。**

## 概念

Cloud Functionsでは、何らかの処理を起動するイベントのことを「トリガー」、実行される処理を「アクション」と呼んでいます。

以下は、[Cloud Functionsの公式ドキュメント](https://console.bluemix.net/docs/openwhisk/index.html#cloud-functions-)からの引用です。

**トリガーとは**

> トリガーは、ユーザーが明示的に発生させることも、ユーザーの代わりに外部イベント・ソースによって発生させることもできます。
> フィード は、Cloud Functions によってコンシューム可能なトリガー・イベントを発生させるように外部イベント・ソースを構成するための便利な方法です。
> フィードの例として、以下があります。
> 
> - データベースの文書に追加または変更があるたびにトリガー・イベントを発生させる Cloudant データ変更フィード。
> - Git リポジトリーへのコミットごとにトリガー・イベントを発生させる Git フィード。

**アクションとは**

> アクションは、JavaScript 関数、Swift 関数、Python 関数、または Java メソッドとして、または、Docker コンテナーにパッケージした実行可能なカスタム・プログラムとして、作成できます。
> 例えば、アクションを使用して、イメージ内の顔を検出したり、データベース変更に応答したり、一連の API 呼び出しを集約したり、ツイートを投稿したりできます。

アクションは、トリガーと関連付けて起動できるほか、REST API等によってアクションを直接起動することも可能です。

複数のアクションおよびフィードをまとめた「パッケージ」という機能もあります。

- Watsonとの連携
- Cloudantデータベースとの連携
- Push通知
- Slackとの連携　　などが一例です。他にも様々なパッケージが提供されています。

その他の詳しい情報は[Cloud Functionsの公式ドキュメント](https://console.bluemix.net/docs/openwhisk/index.html#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一択です！:wink:

### コード編集方法

２種類あります。
【方法１】Cloud Functionsの「開発ビュー」にて、ブラウザ上でコードを編集する。
【方法２】ローカルで編集したコードを、コマンドラインインターフェイス（CLI）でアップロードする。※iOS SDKも提供されています。

なおデプロイについては、公式ドキュメントで以下のように記載されていますので、アクション実行時にコードがサーバーにデプロイされる、という理解が正しいようです。

> アクションは、トリガーが発生するとすぐにデプロイされて実行されます。

今回は、Swift on LinuxのブラウザベースのREPL、[IBM Swift Sandbox](https://swift.sandbox.bluemix.net/#/repl)を利用してコーディング＆デバッグを行い、出来たコードをCloud Functionsの開発ビューに貼り付けました。

<img width="1280" alt="スクリーンショット 2017-08-13 10.04.08.png" src="https://qiita-image-store.s3.amazonaws.com/0/122917/912f8463-21ba-7213-bddf-e6748b457d48.png">

<img width="1280" alt="スクリーンショット 2017-08-12 21.38.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/122917/325235f9-db9b-1575-582e-5fa61a0b8b7d.png">

### サーバーサイドSwiftのコード

エントリーポイントの`main`と、引数および戻り値の型`[String:Any]`は、アクションでの決まりごとです。

```swift:Swift

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アプリのコード

```swift:ViewController.swift

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](https://github.com/SwiftyJSON/SwiftyJSON)を利用しています。

アクションのREST APIエンドポイントおよびBasic認証文字列は、Cloud Functions開発ビューの「RESTエンドポイントの表示」で確認できます。

### レスポンスのサンプル

```json:JSON

{
  "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に限らず）サーバーレスには以下のようなデメリットがあります。

**デメリット１：**
デプロイ可能なコードの最大サイズや、一回の実行にかかる時間の制限（タイムアウト制限）があり、重い処理には向きません。

**デメリット２：**
同時実行数や秒当たりの実行数に上限があり、コードが100％実行される保証がありません。

**デメリット３：**
イベント発生のたび動的にリソースが割り当てられる、という特性上、イベント発生の間隔が短い場合はプーリング処理されますが、間隔が長い場合は初回の実行に時間がかかる傾向があります。

# 今回の検証結果とまとめ

今回サンプルアプリで試してみて、やはり『間隔が長い場合は初回の実行に時間がかかる傾向』は明らかに感じられました。

**平均処理時間（実行ログの集計）**

| | 実行間隔が空いた場合 | 間隔を空けない場合 |
|:-:|:-:|:-:|
| 平均時間 | 3.49s | 21ms  |

レスポンス性を求められる機能へのサーバーレス適用については、UX観点で注意が必要かもしれません。

一方で、今回は試すことができませんでしたが、**サーバーからモバイルへのPush通知は、一番適用しやすいユースケースである**と思われました。

一例としては、
・データベースの更新をトリガーとしてアクションを実行し、Push通知
・定期的に外部サービスの情報を取得し、何らかの条件に合致した場合にPush通知
などが考えられます。

現時点での私見になりますが、今回のサンプルアプリによる検証の結果から、以下のような感想を持ちました。

- 基幹系アプリと比べると、モバイルアプリはサーバーレスのデメリットを許容できるケースが多く、サーバーレスを適用しやすいと言える。
- 特に、モバイルへのPush通知については、サーバーレスのメリットを最大限に享受できそうである。
- ただし、モバイルからのリクエスト処理にサーバーレスを使うことは、UX観点で注意が必要である。

# おまけ

バイオリズム診断のWeb版も作ってみましたので、良かったら試してみてください。
もちろんバックエンドはIBM Cloud Functionsです。
[バイオリズム診断](https://y-someya.au-syd.mybluemix.net/biorhythm/)

- [バイオリズム](https://ja.wikipedia.org/wiki/%E3%83%90%E3%82%A4%E3%82%AA%E3%83%AA%E3%82%BA%E3%83%A0)は、プラスが好調で1.0が最高、マイナスが不調で-1.0が最悪、0付近は好不調の切り替わる要注意日です。
- ただし、医学的な裏付けはないようですので、占い程度と捉えてください。