API Gateway + AWS Lambda + Spring Boot でCRUD APIを作成してみた

2016.06.02

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

API Gateway + AWS LambdaでデータのCRUDを行うAPIを作成したいという要件は、それなりにあるかと思います。今回はAWS LambdaをJavaで実装したいケースを想定し、Spring Bootを使ってみました。

CRUDを行うデータベースはDynamoDBとし、以下の記事と同じ方法を採っております。
Spring Data DynamoDBでSpringからDynamoDBにアクセスする

実装について

では実装についてです。まずはデータを登録するDynamoDBのテーブルを作成し、その後でソースについて見ていきたいと思います。

1.DynamoDBのテーブル

AWSコンソール上で以下のようにテーブルを作成しました。これは以前の記事と同じです。

  • テーブル名 ・・・ SpringUser
  • プライマリキー ・・・ id

2.build.gradle

Gradleを使用し、build.gradleに以下のように記述しました。

buildscript {
	ext {
		springBootVersion = '1.3.4.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 
	}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot' 

jar {
	baseName = 'LambdaSpringBootApiSample'
	version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
	mavenCentral()
}


dependencies {
	compile('org.springframework.boot:spring-boot-starter')
	compile('com.amazonaws:aws-lambda-java-core:1.1.0')
	compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.7.4'
	compile('com.google.code.gson:gson:2.6.2')
	compile group: 'com.github.derjust', name: 'spring-data-dynamodb', version: '4.2.3'
    compile('org.projectlombok:lombok:1.16.8')
	testCompile('org.springframework.boot:spring-boot-starter-test') 
}


eclipse {
	classpath {
		 containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
		 containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
	}
}

3.Postと登録

API GatewayによるPostメソッド、およびPostされたデータを登録するLambda Functionについてです。

Postメソッド

API GatewayのPostメソッドは以下の様に設定しました。「LambdaSpringBootApiSamplePost」というLambda Functionを呼び出しています。

api-gateway-lambda-spring-post

PostHandlerとLambda

API Gatewayから呼び出されるLambda Functionのソースです。エントリーポイントとなる「handleRequest」メソッド内で、ApplicationContextを作成してSpringアプリケーションを起動しています。

データを登録する主なロジックは「run」メソッドに記述しており、API Gatewayから取得した値を読み込み(45〜51行目)、DynamoDBに登録しています(52・52行目)。

PostHandler.java
package com.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.example.repositories.SpringUserRepository;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;

import lombok.Setter;

@SpringBootApplication
public class PostHandler implements RequestHandler<Object, Object>{
    @Autowired
    private SpringUserRepository repository;
    
    @Setter
    private Object input;
    
    @Setter
    private Context context;
    
    @Override
    public Object handleRequest(Object input, Context context) {
        String args[] = new String[0];
        try (ConfigurableApplicationContext ctx = SpringApplication.run(PostHandler.class, args)) {
            PostHandler app = ctx.getBean(PostHandler.class);
            app.setInput(input);
            app.setContext(context);
            app.run(args);
            return "success.";
        } catch (Exception e) {
            e.printStackTrace();
            context.getLogger().log("error.\n");
            return "error.";
        }
    }
    
    public void run(String... args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        
        Gson gson = new Gson();
        String json = gson.toJson(input);
        
        SpringUser inputUser = mapper.readValue(json, SpringUser.class);
        SpringUser registerUser = new SpringUser(inputUser.getFirstName(), inputUser.getLastName());
        repository.save(registerUser);
    }
}

上記のソースを含めてjarを作成し、Lambda Functionとして登録します。

api-gateway-lambda-spring-post-handler

4.Putと更新

API GatewayによるPutメソッド、およびPutされたデータを更新するLambda Functionについてです。以降は上記のPostとあまり変わりないので、スクリーンショットとソースの表示のみで詳細な説明は省きます。

Putメソッド

api-gateway-lambda-spring-put

PutHandlerとLambda

データを更新するLambda Functionのソースです。

PutHandler.java
package com.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.example.repositories.SpringUserRepository;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;

import lombok.Setter;

@SpringBootApplication
public class PutHandler implements RequestHandler<Object, Object>{
    @Autowired
    private SpringUserRepository repository;
    
    @Setter
    private Object input;
    
    @Setter
    private Context context;
    
    @Override
    public Object handleRequest(Object input, Context context) {
        String args[] = new String[0];
        try (ConfigurableApplicationContext ctx = SpringApplication.run(PutHandler.class, args)) {
            PutHandler app = ctx.getBean(PutHandler.class);
            app.setInput(input);
            app.setContext(context);
            app.run(args);
            return "success.";
        } catch (Exception e) {
            e.printStackTrace();
            context.getLogger().log("error.\n");
            return "error.";
        }
    }

    public void run(String... args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        
        Gson gson = new Gson();
        String json = gson.toJson(input);
        
        SpringUser inputUser = mapper.readValue(json, SpringUser.class);
        SpringUser updateUser = repository.findOne(inputUser.getId());
        updateUser.setFirstName(inputUser.getFirstName());
        updateUser.setLastName(inputUser.getLastName());
        repository.save(updateUser);
    }
}

Lambda Functionは以下となります。

api-gateway-lambda-spring-put-handler

5.Getと取得

API GatewayによるGetメソッド、およびデータを取得するLambda Functionについてです。

Getメソッド

api-gateway-lambda-spring-get

GetHandlerとLambda

データを取得するLambda Functionのソースです。取得したデータを返却するところが、上記のソースとは異なっています。

GetHandler.java
package com.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.example.repositories.SpringUserRepository;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.Setter;

@SpringBootApplication
public class GetHandler implements RequestHandler<Object, Object>{
    @Autowired
    private SpringUserRepository repository;
    
    @Setter
    private Object input;
    
    @Setter
    private Context context;
    
    @Override
    public Object handleRequest(Object input, Context context) {
        String args[] = new String[0];
        try (ConfigurableApplicationContext ctx = SpringApplication.run(GetHandler.class, args)) {
            GetHandler app = ctx.getBean(GetHandler.class);
            app.setInput(input);
            app.setContext(context);
            String result = app.run(args);
            return result;
        } catch (Exception e) {
            e.printStackTrace();
            context.getLogger().log("error.\n");
            return "error.";
        }
    }

    public String run(String... args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        Iterable<SpringUser> users = repository.findAll();
        String json = mapper.writeValueAsString(users);
        return json;
    }
}

api-gateway-lambda-spring-get-handler

4.Deleteと削除

API GatewayによるDeleteメソッド、およびデータを削除するLambda Functionについてです。

Deleteメソッド

api-gateway-lambda-spring-delete

DeleteHandlerとLambda

データを削除するLambda Functionのソースです。

DeleteHandler.java
package com.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.example.repositories.SpringUserRepository;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;

import lombok.Setter;

@SpringBootApplication
public class DeleteHandler implements RequestHandler<Object, Object>{
    @Autowired
    private SpringUserRepository repository;
    
    @Setter
    private Object input;
    
    @Setter
    private Context context;
    
    @Override
    public Object handleRequest(Object input, Context context) {
        String args[] = new String[0];
        try (ConfigurableApplicationContext ctx = SpringApplication.run(DeleteHandler.class, args)) {
            DeleteHandler app = ctx.getBean(DeleteHandler.class);
            app.setInput(input);
            app.setContext(context);
            app.run(args);
            return "success.";
        } catch (Exception e) {
            e.printStackTrace();
            context.getLogger().log("error.\n");
            return "error.";
        }
    }

    public void run(String... args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        
        Gson gson = new Gson();
        String json = gson.toJson(input);
        
        SpringUser inputUser = mapper.readValue(json, SpringUser.class);
        SpringUser deleteUser = repository.findOne(inputUser.getId());
        repository.delete(deleteUser);
    }
}

api-gateway-lambda-spring-delete-handler

6.DynamoDB周りの共通処理

最後にDynamoDBにCRUDするための共通処理についてです。

DynamoDBConfig

DynamoDBへ接続するためのクラスです。以前の記事と大体同じなのですが、クレデンシャルを実行環境から取得するよう「EnvironmentVariableCredentialsProvider」メソッドを使用するように変更しています。

DynamoDBConfig.java
package com.example;

import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
 
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.EnvironmentVariableCredentialsProvider;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
 
@Configuration
@EnableDynamoDBRepositories(basePackages = "com.example.repositories")
public class DynamoDBConfig {
    @Value("${amazon.dynamodb.endpoint}")
    private String amazonDynamoDBEndpoint;
    
    @Bean
    public AmazonDynamoDB amazonDynamoDB() {
        AmazonDynamoDB amazonDynamoDB = new AmazonDynamoDBClient(amazonAWSCredentials());
        if (!StringUtils.isEmpty(amazonDynamoDBEndpoint)) {
            amazonDynamoDB.setEndpoint(amazonDynamoDBEndpoint);
        }
        return amazonDynamoDB;
    }

    @Bean
    public AWSCredentials amazonAWSCredentials() {
        return new EnvironmentVariableCredentialsProvider().getCredentials();
    }
}

またEndpointを定義ファイルから取得しており、その定義ファイルも載せておきます。

application.yml
amazon:
  dynamodb:
    endpoint: https://dynamodb.ap-northeast-1.amazonaws.com

SpringUser

CRUDを行う対象のデータを表す、SpringUserクラスです。

SpringUser.java
package com.example;
 
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAutoGeneratedKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;
 
@DynamoDBTable(tableName = "SpringUser")
public class SpringUser {
     
    private String id;
    private String firstName;
    private String lastName;
     
    public SpringUser(){
         
    }
     
    public SpringUser(String firstName, String lastName){
        this.firstName = firstName;
        this.lastName = lastName;
    }
     
    @DynamoDBHashKey
    @DynamoDBAutoGeneratedKey
    public String getId()
    {
      return id;
    }
 
    @DynamoDBAttribute
    public String getFirstName()
    {
      return firstName;
    }
 
    @DynamoDBAttribute
    public String getLastName()
    {
      return lastName;
    }
     
    public void setId(String id){
        this.id = id;
    }
     
    public void setFirstName(String firstName){
        this.firstName = firstName;
    }
     
    public void setLastName(String lastName){
        this.lastName = lastName;
    }
}

SpringUserRepository

データのCRUDを実際に行うリポジトリクラスです。

SpringUserRepository.java
package com.example.repositories;

import org.socialsignin.spring.data.dynamodb.repository.EnableScan;
import org.springframework.data.repository.CrudRepository;

import com.example.SpringUser;

@EnableScan
public interface SpringUserRepository extends CrudRepository<SpringUser, String> {
}

動作確認

では実際にAPI Gatewayを叩いて動作を確認してみます。

登録

API GatewayにPostして、データを登録してみます。

$ curl https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/test -v -X POST -d "{\"firstName\":\"Ichiro\", \"lastName\":\"Test\"}"
(中略)
< HTTP/1.1 200 OK
(中略)
$ curl https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/test -v -X POST -d "{\"firstName\":\"Jiro\", \"lastName\":\"Test\"}"
(中略)
< HTTP/1.1 200 OK
DynamoDB

api-gateway-lambda-spring-dynamodb-post

更新

Putにて上記DynamoDBの最初のデータを更新してみます。

$ curl https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/test -v -X PUT -d "{\"id\":\"f8fd8d22-dee5-422b-a587-fdc11a61fe85\", \"firstName\":\"Seiya\", \"lastName\":\"Test\"}"
(中略)
< HTTP/1.1 200 OK
DynamoDB

api-gateway-lambda-spring-dynamodb-put

取得

Getにてデータを取得してみます。

$ curl https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/test -v -X GET
(中略)
"[{\"id\":\"f8fd8d22-dee5-422b-a587-fdc11a61fe85\",\"firstName\":\"Seiya\",\"lastName\":\"Test\"},{\"id\":\"03faef7c-5547-4ded-8d1a-9c97923b2eda\",\"firstName\":\"Ichiro\",\"lastName\":\"Test\"}]"

削除

Deleteにてデータを削除してみます。

$ curl https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/test -v -X DELETE -d "{\"id\":\"f8fd8d22-dee5-422b-a587-fdc11a61fe85\"}"
(中略)
< HTTP/1.1 200 OK
DynamoDB

api-gateway-lambda-spring-dynamodb-delete

まとめ

簡単なサンプルですが、Javaを使ってAPI Gateway + AWS Lambda でAPIを実現することができました。何かの時に参考になれば幸いです。