How-tos

For Performant Swift Serverless actions…

Share this post:

While coding and drafting “Mobile app with a Serverless Backend”, We came up with an idea to use Swift on the server-side for the iOS app (it’s an interesting use case).So, that it will cater the full stack swift developers out there. The same is true for the Android version of the app as well.
Here’s an architectural interpretation of what we wanted to achieve,

As an initial step, I started exploring Swift actions under Functions (FaaS) on IBM Cloud. My first challenge was to authenticate the user through AppID service and this should entirely happen on the server-side in a Swift action. I figured out that there is an introspect API endpoint which will validate the token your pass. This is good, can I use external packages like SwiftyJSON inside a Swift Cloud Functions action? This will be answered soon.

Here’s the action to validate a token

/***************
 ** Validate an user access token through Introspect endpoint of
 ** App ID Service on IBM Cloud.
 ***************/
import Foundation
import Dispatch
import SwiftyJSON


func main(args: [String:Any]) -> [String:Any]  {
    
    var args: [String:Any] = args
    let str = ""
    var result: [String:Any] = [
        "status": str,
        "isactive": str
    ]
    
    guard let requestHeaders = args["__ow_headers"] as! [String:Any]?,
        let authorizationHeader = requestHeaders["authorization"] as? String
        else {
            print("Error: Authorization headers missing.")
            result["ERROR"] = "Authorization headers missing."
            return result
    }
    
    guard let authorizationComponents = authorizationHeader.components(separatedBy: " ") as [String]?,
        let bearer = authorizationComponents[0] as? String, bearer == "Bearer",
        let accessToken = authorizationComponents[1] as? String,
        let idToken = authorizationComponents[2] as? String
        else {
            print("Error: Authorization header is malformed.")
            result["ERROR"] = "Authorization header is malformed."
            return result
    }
    guard let username = args["services.appid.clientId"] as? String,
        let password = args["services.appid.secret"] as? String,
        let tenantid = args["tenantid"] as? String
        else{
            print("Error: missing a required parameter for basic Auth.")
            result["ERROR"] = "missing a required parameter for basic Auth."
            return result
    }
    let loginString = username+":"+password
    let loginData = loginString.data(using: String.Encoding.utf8)!
    let base64LoginString = loginData.base64EncodedString()
    let headers = [
        "content-type": "application/x-www-form-urlencoded",
        "authorization": "Basic \(base64LoginString)",
        "cache-control": "no-cache",
        ]
    let postData = "tenantid=\(tenantid)&token=\(accessToken)"
    
    var request = URLRequest(url: URL(string: (args["services.appid.url"] as? String)! + "/introspect")! as URL,
                             cachePolicy: .useProtocolCachePolicy,
                             timeoutInterval: 10.0)
    request.httpMethod = "POST"
    request.allHTTPHeaderFields = headers
    request.httpBody = postData.data(using: .utf8)
    
    let semaphore = DispatchSemaphore(value: 0)
    let sessionConfiguration = URLSessionConfiguration.default;
    let urlSession = URLSession(
        configuration:sessionConfiguration, delegate: nil, delegateQueue: nil)
    let dataTask = urlSession.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
        guard let data = data, error == nil else {
            print("Error: \(String(describing: error?.localizedDescription))")
            return
        }
        if let httpStatus = response as? HTTPURLResponse {
            if httpStatus.statusCode == 200
            {
                let responseString = String(data: data, encoding: .utf8)
                guard let data = responseString?.data(using: String.Encoding.utf8),
                    let dictionary = try? JSONSerialization.jsonObject(with: data, options: []) as? [String:Bool]
                    else {
                        return
                }
                if let myDictionary = dictionary
                {
                    print(" isActive : \(myDictionary["active"]!)")
                    result = [
                        "status": String(httpStatus.statusCode),
                        "isactive": myDictionary["active"]!
                    ]
                }
            }
            else
            {
                print("Unexpected response:\(httpStatus.statusCode)")
                print("\(httpStatus)")
                result["ERROR"] = httpStatus
            }
        }
        print("operation concluded")
        semaphore.signal()
    })
    
    dataTask.resume()
    _ = semaphore.wait(timeout: DispatchTime.distantFuture)
    
    
    if(result["isactive"] != nil && result["isactive"]! as! Bool)
    {
        
        let parsedAccessToken = parseToken(from: accessToken)["payload"]
        let parsedIdToken = parseToken(from: idToken)["payload"]
        
        var _accessToken = ""
        var _idToken = ""
        
        if let accessTokenString = parsedAccessToken.rawString() {
            _accessToken = accessTokenString
        } else {
            print("ERROR: accessTokenString is nil")
        }
        
        if let idTokenString = parsedIdToken.rawString() {
            _idToken = idTokenString
        } else {
            print("ERROR: idTokenString is nil")
        }
        args["_accessToken"] = _accessToken
        args["_idToken"] = _idToken
        return args
    }
        
    else{
        result["ERROR"] = "Invalid Token or the token has expired"
        return result
    }
}

extension String{
    
    func base64decodedData() -> Data? {
        let missing = self.characters.count % 4
        
        var ending = ""
        if missing > 0 {
            let amount = 4 - missing
            ending = String(repeating: "=", count: amount)
        }
        let base64 = self.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") + ending
        return Data(base64Encoded: base64, options: Data.Base64DecodingOptions())
    }
}


func parseToken(from tokenString:String) -> JSON {
    print("parseToken")
    var json = JSON([:])
    let tokenComponents = tokenString.components(separatedBy: ".")
    
    guard tokenComponents.count == 3 else {
        print("ERROR: Invalid access token format")
        return json
    }
    
    let jwtHeaderData = tokenComponents[0].base64decodedData()
    let jwtPayloadData = tokenComponents[1].base64decodedData()
    let jwtSignature = tokenComponents[2]
    
    guard jwtHeaderData != nil && jwtPayloadData != nil else {
        print("ERROR: Invalid access token format")
        return json
    }
    
    let jwtHeader = JSON(data: jwtHeaderData!)
    let jwtPayload = JSON(data: jwtPayloadData!)
    
    json["header"] = jwtHeader
    json["payload"] = jwtPayload
    json["signature"] = JSON(jwtSignature)
    return json
}

You can find other actions here.

Clone the code from by running the following command on a terminal or download the code from the github repo,

 git clone https://github.com/IBM-Cloud/serverless-followupapp-ios.git

From the architecture diagram, you should have figured out that once we are authenticated the next steps are to add the user through one of the actions and then save the feedback provided by the user along with his device id (to send push notifications). There is a trigger associated with feedback table so that once a new feedback is added to the table, the trigger will be triggered to send the feedback to Watson tone analyser and the output tone is passed to Cloudant to map and send the associated message as a push notification to the feedback provider/customer.

This is all good and interesting. What will be the execution time for this flow to complete?

Ok, let’s see the execution time of one action and also, how to improve the performance?

If you observe, the initial serverless action call took 5.51 secs to complete and subsequent calls are faster. So, what exactly is the reason? Here’s what IBM Cloud Functions documentation says

When you create a Swift action with a Swift source file, it has to be compiled into a binary before the action is run. Once done, subsequent calls to the action are much faster until the container that holds your action is purged. This delay is known as the cold-start delay.

How to overcome this and make our Swift Cloud Functions actions performant from the word go by avoiding cold-start delay.

To avoid the cold-start delay, you can compile your Swift file into a binary and then upload to Cloud Functions in a zip file. As you need the OpenWhisk scaffolding, the easiest way to create the binary is to build it within the same environment it runs in.

See the following steps:

  • Run an interactive Swift action container by using the following command:

docker run — rm -it -v “$(pwd):/owexec” openwhisk/action-swift-v3.1.1 bash

  • Copy the source code and prepare to build it.
cp /owexec/{PATH_TO_DOWNLOADED_CODE}/ValidateToken.swift /swift3Action/spm-build/main.swift
cat /swift3Action/epilogue.swift >> /swift3Action/spm-build/main.swift
echo '_run_main(mainFunction:main)' >> /swift3Action/spm-build/main.swift
  • (Optional) Create the Package.swift file to add dependencies.
swift import PackageDescription
let package = Package( name: "Action",
dependencies: [
.Package(url: "https://github.com/apple/example-package-deckofplayingcards.git", majorVersion: 3),
.Package(url: "https://github.com/IBM-Swift/CCurl.git", "0.2.3"),
.Package(url: "https://github.com/IBM-Swift/Kitura-net.git", "1.7.10"),
.Package(url: "https://github.com/IBM-Swift/SwiftyJSON.git", "15.0.1"),
.Package(url: "https://github.com/watson-developer-cloud/swift-sdk.git", "0.16.0")
])

This example adds swift-watson-sdk and example-package-deckofplayingcards dependencies. Notice that CCurl, Kitura-net, and SwiftyJSON are provided in the standard Swift action so you can include them in your own Package.swift.

  • Copy Package.swift to spm-build directory
cp /owexec/Package.swift /swift3Action/spm-build/Package.swift
  • Change to the spm-build directory
cd /swift3Action/spm-build
  • Compile your Swift Action.
swift build -c release
  • Create the zip archive.
zip /owexec/{PATH_TO_DOWNLOADED_CODE}/ValidateToken.zip .build/release/Action
  • Exit the Docker container.
exit

You can see that ValidateToken.zip is created in the same directory as ValidateToken.swift.

  • Upload it to OpenWhisk with the action name authvalidate:
wsk action update authvalidate ValidateToken.zip -- kind swift:3.1.1
  • To check how much faster it is, run
 wsk action invoke authvalidate -- blocking

Refer this link for installing Cloud Functions standalone CLI

The time that it took for the action to run is in the “duration” property and compare to the time it takes to run with a compilation step in the ValidateToken action.

So, what will be the performance improvement when I run an action with a .swift file for the first time and an action with a binary file.

Here’s the answer,

auth-validate is an action created with .swift file and authvalidate1 is an action created with a binary file.

Don’t forget to refer our other solution tutorials covering this end-to-end usecases and scenarios.

More How-tos stories

Speed up your WordPress with IBM Cloud

WordPress is one of the most popular content management systems available, but the many websites and blogs that use it experience issues with speed. At IBM Cloud, there are several solutions that can help alleviate some of these issues and allow you to have a better and faster WordPress experience.

Continue reading

Container Native Monitoring Insights with Elastic on IBM Cloud

Introduction to container native monitoring In this blog post, we will discuss how Elastic easily deploys with the IBM Cloud Kubernetes Service (IKS). This provides full visibility of your containerized workloads and operational consistency with container deployments in a multi-cloud architecture. We will deploy a Kubernetes cluster in IBM Cloud and layer in the Elastic […]

Continue reading

Build, Deploy, and Scale Real-World Solutions on IBM Cloud

IBM Cloud Solution Tutorials give you step-by-step instructions to create applications running on virtual servers, Kubernetes, Cloud Foundry, and serverless architectures. The tutorials also cover topics ranging from web and mobile applications, chatbots, IoT, and machine learning and analytics.

Continue reading