つかびーの技術日記

(情報)工学修士, 元SIer SE, 現Web系 SEの技術blogです。Scala, Java, JS, TS, Python, Ruby, AWS, GCPあたりが好きです。

Lambda連携で学ぶAmazon API Gateway解説!

      2015/12/17

こんにちは、@s_tsukaです。

今回はAmazon API Gateway各種機能、使い方、Lambda連携もろもろを学んで行きます。

Amazon API Gatewayとは

AWSのサービスの一つでその名の通りAPIのGatewayです。通常であれば何らかのFWや言語を使ってAPIを開発すると思います。例えばNode、Spring3、Play2、Spray、Go、Java、Scala、etc。これらにキャッシュやルーティングを変えるためにApacheやNginxの皮を被せることがあるかと思いますが、この皮の超強力なものがAPI Gatewayだと思ってもらえれば良いです。

機能の概要については公式のTOPページが参考になると思います。

https://aws.amazon.com/jp/api-gateway/

Amazon Lambdaとは

AWSのサービスの一つで、コードを実行する基盤です。コードを実行するならば別に物理ホストでもどこかのVPCでもAmazon EC2でも良い訳ですが、それらを本番環境で運用するとなると場合によってはスケーリングやメンテナンスを考慮する必要があります。Lambdaを使えば色々とマネージされているので、それらの考慮の度合いが低くなるあるいは無くなるという恩恵を受けられます。

詳しくはこちらの動画(3分)などが参考になるかと思います。

  • コードの実行基盤を用意したいけど、EC2のようなホストを持ちたく無い(管理したく無い)
  • リクエスト数が朝と夜でかなり異なるので、自動でスケールして欲しい
  • サーバの利用料を減らしたい

と言ったケースに使えるかと思います。

 API GatewayとLambdaの連携

API Gatewayでendpointを用意し、そのバックエンドをLambda Functionで繋ぐことでHTTPを使ってLambdaを動かすことができるようになります。

これは簡単に実現できます。詳細は次の節にて。

blueprint(設計図・テンプレ)からLambda、API連携を速攻作成

API GatewayとLambdaの連携は、実は公式がサンプルを用意してくれています。

そしてそれについて解説しているサイトも既に存在します。

【新機能】Amazon API Gatewayを使ってAWS LambdaをSDKなしでHTTPS越しに操作する

この通りやれば良いのですが、1点だけ注意点があって、API Gatewayは東京リージョンではまだ使えませんので、東京リージョンで作業するとAPI Gateway作成あたりでエラーが出ます。なのでバージニア北部などを選んでおきましょう。おそらくですが、Lambdaを東京で作った場合はLambdaの画面からではなく、API Gatewayの画面から連携設定すると上手く行くかと思います。

(現在は東京リージョンでもAPI Gatewayを利用できるようになりました。http://aws.typepad.com/aws_japan/2015/10/api-gatewaynow-available-in-asia-pacific.html

ここではサンプル作成は上記の記事に任せるとして、そこからさらに発展して行きます。

まずは上記の記事通りサンプルを作成してみてください。

サンプルのオペレーションを一通り実行してみる

上記の記事ではechoしか実行していませんでした。実際にはcreateやlistなど他のオペレーションも用意されています。

console.log('Loading function');

var doc = require('dynamodb-doc');
var dynamo = new doc.DynamoDB();

exports.handler = function(event, context) {
    //console.log('Received event:', JSON.stringify(event, null, 2));

    var operation = event.operation;
    delete event.operation;

    switch (operation) {
        case 'create':
            dynamo.putItem(event, context.done);
            break;
        case 'read':
            dynamo.getItem(event, context.done);
            break;
        case 'update':
            dynamo.updateItem(event, context.done);
            break;
        case 'delete':
            dynamo.deleteItem(event, context.done);
            break;
        case 'list':
            dynamo.scan(event, context.done);
            break;
        case 'echo':
            context.succeed(event);
            break;
        case 'ping':
            context.succeed('pong');
            break;
        default:
            context.fail(new Error('Unrecognized operation "' + operation + '"'));
    }
};

それらも利用してみます。

create

DynamoDBの特定のテーブルにアイテムを登録する操作です。DynamoDBで予めテーブル(sample_table, hashキーは”id”)を作成しておき、以下のbodyでPOSTします。

{
    "operation": "create",
    "TableName": "sample_table",
    "Item": {
        "id": "1",
        "name": "API_TEST"
    }
}

DynamoDBに登録されました。

DynamoDB_·_AWS_コンソール

read

readは取得ですが、対象のキーが必要です。ですので、このようにします。

{
    "operation": "read",
    "TableName": "sample_table",
    "Key": {
        "id": "1"
    }
}

結果はこのようなjsonが返ります。

{
  "Item": {
    "name": "API_TEST",
    "id": "1"
  }
}

 update

UPDATEは更新です。

このようなjsonをbodyにしてAPI callすると

{
    "operation": "update",
    "TableName": "sample_table",
    "Key": {
        "id": "1"
    },
    "UpdateExpression": "set #name = :val1",
    "ExpressionAttributeValues": {
        ":val1": "UPDATED_API_TEST"
    },
    "ExpressionAttributeNames": {
        "#name"  : "name"
    }
}

結果は{}というjsonが返るのですが、ちゃんと更新されています。

DynamoDB_·_AWS_コンソール

テーブルのフィールド名に「name」というDynamoDB予約語を使っているのでExpressionAttributeNamesが必要になって少し複雑ですが・・・。

基本的にbodyに入れて投げるjsonはDynamoDBの仕様と同じですね。

delete

投げるbodyはこれです。

{
    "operation": "delete",
    "TableName": "sample_table",
    "Key": {
        "id": "1"
    }
}

レスポンスは{}が返りますが、正しくきえています。

DynamoDB_·_AWS_コンソール

list

単純ですね。

{
    "operation": "list",
    "TableName": "sample_table"
}

予めcreateで何件か登録しておいた後で叩くとこういうjsonが返ります。

{
  "Count": 3,
  "Items": [
    {
      "name": "API_TEST2",
      "id": "2"
    },
    {
      "name": "API_TEST",
      "id": "1"
    },
    {
      "name": "API_TEST3",
      "id": "3"
    }
  ],
  "ScannedCount": 3
}

 echoとping

echoは既にクラスメソッドのblogでサンプルが出ていたので省略します。

pingも”pong”という文字列が返ってくるだけなので省略します。

よりRESTfulにする(発展)

ここからいよいよAPI Gatewayを把握するために色々とカスタマイズして行きます。

ここまでは単純に作成したサンプルを実行してきましたが、全ての操作はPOSTリクエストで行い、bodyのoperationフィールドによって処理を切り替えていました。しかしRESTに則るならばおそらくこれはNGで、POST, GET, PUT, DELETE (CRUD)によって切り替えるべきです。

早速この切り替えを行って行きます。ここでは簡略化のためにPOSTとGET(1件と一覧)のみサポートします。

設計

まずは対象とするリソース、entityですが以下とします。予めpersonsテーブルもDynamoDBに作成しておきます。

Person(
  id: Int,
  name: String
)

URIはこんな感じです。

POST /api/persons #1件登録
GET /api/persons #一覧取得
GET /api/persons/{id} #1件取得

開発

コードを用意します。操作は上記の通り3つなので、Lambda Functionは3つ用意します。色々と微妙な点はありますが、そこは一旦無視してください。

POST用
console.log('Loading function');

var doc = require('dynamodb-doc');
var dynamo = new doc.DynamoDB();

exports.handler = function(event, context) {
    var dynamoRequest = {
        "TableName": "persons",
        "Item": {
            "id": event.id,
            "name": event.name
        }
    };
    
    dynamo.putItem(dynamoRequest, context.done);
};
(1件)GET用
console.log('Loading function');

var doc = require('dynamodb-doc');
var dynamo = new doc.DynamoDB();

exports.handler = function(event, context) {
    console.log('Received event:', JSON.stringify(event, null, 2));
    var dynamoRequest = {
        "TableName": "persons",
        "Key": {
            "id": event.id.toString()
        }
    };
    
    dynamo.getItem(dynamoRequest, context.done);
};
 (一覧)GET用
console.log('Loading function');

var doc = require('dynamodb-doc');
var dynamo = new doc.DynamoDB();

exports.handler = function(event, context) {
    var dynamoRequest = {
        "TableName": "persons"
    };
    
    dynamo.scan(dynamoRequest, context.done);
};

 

 LambdaとAPI Gatewayの設定

上記のコードをLambdaに登録します。

AWS LambdaAPI Gatewayの登録は少しだけ工夫がいるのでLambdaの画面ではなくAPI Gatewayの画面から行います。

まずはこのまっさらな画面を開きます。

API_GatewayCreateResourceボタンでpersonsリソースを作成します。

 

API_Gateway

personsリソースを選択した状態で再度CreateResourceボタンを押してpersonIdリソースを作成します。ここで、Pathの部分は{id}とします。{}で囲むとそれがURL Parameterだと認識されます。

API_Gateway

リソースの作成が完了したら次はメソッドの登録を行います。/personsの下にはPOSTとGETを、/persons/{id}の下にはGETを作成します。

API_Gateway最後にGET /persons/{id}だけ追加設定を行います。{id}をLambda function側に正しく渡してあげるために設定を行います。

API_GatewayIntegration Requestを選んで以下の画像のようにMappingの設定を行います。

API_GatewayこのあたりのQuery parameter、URL parameterのマッピングについては公式ドキュメントのこのあたりが参考になると思います。

http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html

デプロイ

設定内容が即時反映される訳ではなくて、API Gatewayの設定はステージという単位で区切られています。なので、Deployを行って新たなステージを作成します。

API_GatewayDeployボタンを押してprodステージにデプロイを行います。

ここまでの作業で準備は整いました・・・!いよいよ使ってみましょう!

テスト

折角なのでAPI GatewayのTest機能ではなく、実際のURIを使って試してみましょう。

ステージ画面で払い出されたURIを確認できます。

API_Gateway実際にこのURIを使ってPOST2回とGET(一覧)、GET(1件)を実行してみます。

# POST https://ukdekt9vr9.execute-api.us-east-1.amazonaws.com/prod/persons
# body
{
    "id": "1",
    "name": "tsukaby"
}

# result
{}

# POST https://ukdekt9vr9.execute-api.us-east-1.amazonaws.com/prod/persons
# body
{
    "id": "2",
    "name": "tsukaby2"
}

# result
{}

# GET https://ukdekt9vr9.execute-api.us-east-1.amazonaws.com/prod/persons
# result
{
  "Count": 2,
  "Items": [
    {
      "name": "tsukaby2",
      "id": "2"
    },
    {
      "name": "tsukaby",
      "id": "1"
    }
  ],
  "ScannedCount": 2
}

# GET https://ukdekt9vr9.execute-api.us-east-1.amazonaws.com/prod/persons/1
# result
{
  "Item": {
    "name": "tsukaby",
    "id": "1"
  }
}

ちゃんと動きました!

 まとめ

Lambda functionとAPI Gatewayを連携させる方法を解説しました。また、サンプルを少し改良してRESTfulなAPIを作る方法について解説しました。

API Gatewayの他の機能については別の記事で解説して行きたいと思います。

みなさんもAPI Gateway一度試してみてはいかがでしょうか?

 - クラウドサービス , ,