iOS アプリ開発者のための AWS Lambda 入門

2014.11.28

AWS を MBaaS として使う時がやってきた!

先日開催された re:Invent で発表された目玉のサービスといえば…やっぱり AWS Lambda (以降 Lambda) でしょう!

AWS Mobile SDK を直接使う上で障壁だったのは、提供されている各サービスが疎結合であったところです。データの読み書き、ファイルのアップロード・ダウンロード、プッシュ通知、認証システム…モバイル向けのアプリに必要な機能はひと通り揃っていましたが、各サービスを組み合わせて使うには自前でサーバーを用意したりクライアント側でガッツリ実装したりする必要があったため、MBaaS として利用するにはハードルが高いという課題がありました。

そこで Lambda の登場です。Lambda はそれぞれ独立していた Amazon S3、 Amazon SNS、 Amazon Dynamo DB といったサービスを自由に繋ぎあわせることを可能にします。サーバーレス・クロスプラットフォームで!Lambda は今まで AWS を MBaaS として利用する上で欠如していた部分を埋めてくれるサービスと言えるでしょう。

ということで、本記事では Lambda をモバイルから利用する試みにチャレンジしていきたいと思います。なお、Lambda の技術情報は続々と公開中ですので、こちらも合わせてチェックしていただければと思います。

AWS Lambda は現在(2014/11/28)は Preview として提供されています。お手持ちの AWS アカウントで Lambda を利用するには次のページから申請する必要があります。 AWS Lambda | Preview Contact Form

例えば…

Lambda を活用すると、クライアント側においては AWS Mobile SDK を組み込むだけで実現できます。

ファイルのアップロードを通知する

S3 にファイルがアップロードされたときに SNS を通してプッシュ通知を送る機能が実現できます。送る相手は自分だけではなく、自由にフィルターをかけ、複数のユーザーに通知することができます。

DynamoDB のテーブルのレコードのサブスクライブ

DynamoDB Streams を使って、テーブルのレコードの更新のイベントをトリガに SNS でプッシュ配信を行う機能が実現できます。

ポイントは、モバイルから直接 SNS を叩く必要がない点です。モバイル側の IAM ロールには SNS の Publish 権限、また他のユーザーの情報(デバイストークンなど)へのアクセス権限を付与する必要が無くなります。よりセキュアなサービスが構築できるはずです。

DynamoDB Streams は現在(2014/11/28)は Preview として提供されています。お手持ちの AWS アカウントで DynamoDB Streams を利用するには次のページから申請する必要があります。 Amazon DynamoDB Streams Preview

ファイルとデータを同時に格納する

モバイルからファイルを S3 に直接アップロードしつつ、ファイルに関連したデータを DynamoDB のテーブルのレコードに格納する機能が実現できます。

AWS Lambda を使ってみよう

今回は、上記の1つ目の例にあたるS3 のバケットにファイルがアップロードされたらモバイルにプッシュ通知する機能を構築してみます。

手順

次の手順で構築していきます。

  1. S3 にバケットを作成する
  2. SNS に Topic を作成する
  3. Cognito で認証済みのモバイルから SNS に Endpoint を登録する
  4. Lambda ファンクションを作成する
  5. バケットにファイルをアップロードするとモバイルに通知される!

S3 にバケットを作成する

まずは S3 に適当なバケットを作成します。

lambda-ios01

SNS に Topic を作成する

次に SNS の Topic を作成します。

まず初めに、SNS の Application を作成します。登録手順については以下の記事を参考にしてください。モバイルプッシュを iOS で利用するには APNs を利用するための Certificate を登録し、p12 ファイルを準備するなど事前準備が必要です。

Application の作成が終わったら、次に Topic を作成します。右側「Create And Add」のメニューに「Create New Topic」があるので選択します。

lambda-ios02

Topic Name を適当に決めて作成します。

lambda-ios03

作成できたら「Topic ARN」の値をメモしておきます。これは iOS アプリ側の実装と Lambda ファンクションの実装で使用します。

lambda-ios04

Cognito で認証済みのモバイルから SNS に Endpoint を登録する

今回は、モバイルから直接 SNS の Endpoint の作成、ならびに Topic への追加を行います。Endpoint とは、通知先のクライアントを一意に識別するためのものです。SNS Mobile Push については、デバイストークンが格納されているものという認識でOKです。

SNS の Endpoint をモバイルから直接作成するには、Cognito と STS を利用して一時的な AWS Credential を取得する必要があります。Cognito を iOS から利用する方法は以下の記事にまとめていますので、こちらを参考に Management Console から Cognito identity pool を作成してください。今回は Facebook を利用した認証は使わないので、その部分は省いてしまって構いません。

Cognito identity pool が作成できたら、iOS アプリの実装に移ります。AWS Mobile SDK は CocoaPods でインストールできるので、Podfile に以下を追加してください。

Podfile

pod 'AWSiOSSDKv2'

次に AppDelegate に以下の処理を追加します。

AppDelegate.m

#import "AppDelegate.h"

// 適宜、自分の環境の値に変更してください
static NSString* const kAccountId = @"xxxxxxxxxxxx";
static NSString* const kIdentityPoolId = @"us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
static NSString* const kUnAuthArn = @"arn:aws:iam::xxxxxxxxxxxx:role/Cognito_SampleAppUnauth_DefaultRole";
static NSString* const kAuthArn = @"arn:aws:iam::xxxxxxxxxxxx:role/Cognito_SampleAppAuth_DefaultRole";
static NSString* const kSNSApplicationArn = @"arn:aws:sns:us-east-1:xxxxxxxxxxxx:app/APNS_SANDBOX/SampleApp";
static NSString* const kSNSTopicArn = @"arn:aws:sns:us-east-1:xxxxxxxxxxxx:SampleAppTopic";

@implementation AppDelegate

...省略...

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
    NSString  *tokenString = [[[[deviceToken description] stringByReplacingOccurrencesOfString:@"<"withString:@""]
                               stringByReplacingOccurrencesOfString:@">" withString:@""]
                               stringByReplacingOccurrencesOfString: @" " withString: @""];
    NSLog(@"token : %@", tokenString);
    
    // Cognitoを使ってSNSにEndpointを登録する
    [[[self getCredentials] continueWithSuccessBlock:^id(BFTask *task) {
        if (task.isCancelled) {
            NSLog(@"Cancelled");
            return [BFTask cancelledTask];
        } else if (task.error) {
            NSLog(@"Error: %@", task.error);
            return [BFTask cancelledTask];
        } else {
            NSLog(@"Success: %@", task.result);
        }
        return [self registerEndpoint:tokenString];
    }] continueWithSuccessBlock:^id(BFTask *task) {
        if (task.isCancelled) {
            NSLog(@"Cancelled");
        } else if (task.error) {
            NSLog(@"Error: %@", task.error);
        } else {
            NSLog(@"Success: %@", task.result);
        }
        return nil;
    }];
}

- (BFTask*)getCredentials
{
    // Cognitoを使って一時的なAWS Credentialsを取得する
    AWSCognitoCredentialsProvider *credentialsProvider = [AWSCognitoCredentialsProvider
                                                          credentialsWithRegionType:AWSRegionUSEast1
                                                          accountId:kAccountId
                                                          identityPoolId:kIdentityPoolId
                                                          unauthRoleArn:kUnAuthArn
                                                          authRoleArn:kAuthArn];
    AWSServiceConfiguration *configuration = [AWSServiceConfiguration configurationWithRegion:AWSRegionUSEast1
                                                                          credentialsProvider:credentialsProvider];
    [AWSServiceManager defaultServiceManager].defaultServiceConfiguration = configuration;
    return [credentialsProvider getIdentityId];
}

- (BFTask*)registerEndpoint:(NSString*)token
{
    // SNSにEndpointを登録する
    BFTaskCompletionSource* taskCompletionSource = [BFTaskCompletionSource new];
    AWSSNSCreatePlatformEndpointInput *request = [AWSSNSCreatePlatformEndpointInput new];
    request.platformApplicationArn = kSNSApplicationArn;
    request.token = token;
    AWSSNS *sns = [AWSSNS defaultSNS];
    [[[sns createPlatformEndpoint:request] continueWithBlock:^id(BFTask *task) {
        // エラーが発生した場合は中断
        if (task.error) {
            [taskCompletionSource setError:task.error];
            return [BFTask cancelledTask];
        }
        // 対象のTopicにEndpointを登録する
        AWSSNSCreateEndpointResponse *response = task.result;
        AWSSNSSubscribeInput *subscribeRequest = [AWSSNSSubscribeInput new];
        subscribeRequest.topicArn = kSNSTopicArn;
        subscribeRequest.endpoint = response.endpointArn;
        subscribeRequest.protocol = @"application";
        return [sns subscribe:subscribeRequest];
    }] continueWithBlock:^id(BFTask *task) {
        if (task.error) {
            [taskCompletionSource setError:task.error];
        } else {
            [taskCompletionSource setResult:@(YES)];
        }
        return nil;
    }];
    
    return [taskCompletionSource task];
}

@end

解説します。まず application:didFinishLaunchingWithOptions メソッドと application:didRegisterUserNotificationSettings メソッドは通常通りの Remote Notification の認証処理です。認証完了後 application:didRegisterForRemoteNotificationsWithDeviceToken メソッドが呼ばれるので、そのメソッド内で Cognito 経由で一時的な AWS Credential を得る getCredentials メソッドを呼んでいます。このメソッドが完了後 registerEndpoint メソッドを実行し、Endpoint の登録ならびに先ほど作成した Topic への登録を処理しています *1

ここまでで iOS アプリ側の実装は終わりです。実行すると SNS の Topic に Endpoint が登録されるはずです。

lambda-ios05

AWS Lambda

ようやく Lambda の登場です!今回は「特定の S3 のバケットにファイルがアップロードされたらモバイルにプッシュ通知する」という Lambda ファンクションを実装します。

まずは「Get Started Now」をクリックしましょう。

lambda-ios06

次に Lambda ファンクションを作成する画面が表示されます。Name は Description は適当に入力してください。

lambda-ios07

Code entry type は「Edit code inline」を選択します。コードは次のようにします。このコードでは、イベントの情報から S3 に追加されたコンテンツを特定し、ファイル名を SNS の Topic に向けて Publish しています。

console.log('Loading event');

var aws = require('aws-sdk');
var s3 = new aws.S3({apiVersion: '2006-03-01'});
var arn = 'arn:aws:sns:us-east-1:xxxxxxxxxxxx:SampleAppTopic';
var sns = new aws.SNS({region: 'us-east-1', params: {TopicArn: arn}});
 
exports.handler = function(event, context) {
    console.log('Received event:');
    console.log(JSON.stringify(event, null, '  '));
    // S3オブジェクトを取得
    var bucket = event.Records[0].s3.bucket.name;
    var key = event.Records[0].s3.object.key;
    // SNSのTopicにPublish
    sns.publish({Message: 'Added ' + key + ' at ' + bucket}, function (err, data) {
        if (err) {
            console.log('Error');
            context.done('error', err);
        } else {
            console.log('Done');
            context.done(null,'');
        }
    });
};

Handler name は識別できる名前であれば何でも構いません。Role name は「Create / Select Role」をクリックし、新規 Role を作成します。

lambda-ios08

Role のポリシーは以下のように設定しましょう。デフォルト設定に加えて、SNS の Publish を許可しています。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:*",
        "sns:Publish"
      ],
      "Resource": "*"
    }
  ]
}

ここまでできたら「Create Lambda Function」をクリックして作成完了です。ですが、今のままでは S3 のイベントをフックしていないので、その設定を追加します。「Configue event source」をクリックしましょう。

lambda-ios09-2

対象となる S3 のバケット名、S3 が Lambda に対するイベントを通知するために使用する IAM Role を設定します。ここで設定する IAM Role のポリシーは次の通りです。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Resource": [
        "*"
      ],
      "Action": [
        "lambda:InvokeFunction"
      ]
    }
  ]
}

最後に「Submit」をクリックして完了です。これで全ての設定が終わりました!

実行

それでは実行してみます。先ほど作成した S3 バケットにファイルをアップロードすると…

lambda-ios11

通知されます!

lambda-ios12

まとめ

以上、モバイル + Lambda 連係の事始めでした。今後もモバイルと Lambda を連係させるための情報を公開していきたいと思っておりますので、よろしくお願いします!ぜひ皆さんも Lambda を使ってみましょう!:)

参考

脚注

  1. 今回は簡易的な実装ですが、Endpoint や Topic の登録処理の実装方法はベストプラクティスがあります。