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

はじめに

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

ガートナー社の発表では、『企業が注視すべき、プラットフォームを実現する主要なテクノロジ』として紹介され、今後2〜5年以内にメインストリームになると予測されています。
ガートナー、「先進テクノロジのハイプ・サイクル:2017年」を発表

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

・サーバーレス実行環境としてIBM Cloud Functionsを利用する
・サーバーサイドSwiftでコードを書く 〜バイオリズム診断〜
・iOSアプリからREST APIアクセスしてみる

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

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

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

スクリーンショット 2017-08-13 15.24.41.png

スクリーンショット 2017-08-13 15.25.05.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の公式ドキュメントからの引用です。

トリガーとは

トリガーは、ユーザーが明示的に発生させることも、ユーザーの代わりに外部イベント・ソースによって発生させることもできます。
フィード は、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一択です!:wink:

コード編集方法

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

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

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

今回は、Swift on LinuxのブラウザベースのREPL、IBM Swift Sandboxを利用してコーディング&デバッグを行い、出来たコードをCloud Functionsの開発ビューに貼り付けました。

スクリーンショット 2017-08-13 10.04.08.png

スクリーンショット 2017-08-12 21.38.36.png

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

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

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

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を利用しています。

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

レスポンスのサンプル

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

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

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

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

今回の検証結果とまとめ

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

平均処理時間(実行ログの集計)

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

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

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

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

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

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

おまけ

バイオリズム診断のWeb版も作ってみましたので、良かったら試してみてください。
もちろんバックエンドはIBM Cloud Functionsです。
バイオリズム診断

  • バイオリズムは、プラスが好調で1.0が最高、マイナスが不調で-1.0が最悪、0付近は好不調の切り替わる要注意日です。
  • ただし、医学的な裏付けはないようですので、占い程度と捉えてください。

『 Swift 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。