LoginSignup
10
6

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-08-13

はじめに

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

ガートナー社の発表では、『企業が注視すべき、プラットフォームを実現する主要なテクノロジ』として紹介され、今後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付近は好不調の切り替わる要注意日です。
  • ただし、医学的な裏付けはないようですので、占い程度と捉えてください。
10
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6