adding backend as a service with lambda + api gateway + dynamodb

This commit is contained in:
Dheeraj Mummareddy 2018-03-05 20:51:32 -05:00
parent af6973884f
commit b2dd36f607
14 changed files with 579 additions and 9 deletions

View File

@ -49,7 +49,9 @@
<slf4j.version>1.7.21</slf4j.version>
<logback.version>1.1.7</logback.version>
<aws-lambda-core.version>1.1.0</aws-lambda-core.version>
<aws-java-sdk-dynamodb.version>1.11.289</aws-java-sdk-dynamodb.version>
<aws-lambda-log4j.version>1.0.0</aws-lambda-log4j.version>
<aws-lambda-java-events.version>2.0.1</aws-lambda-java-events.version>
<jackson.version>2.8.5</jackson.version>
</properties>
<modules>

View File

@ -35,6 +35,26 @@
<artifactId>aws-lambda-java-core</artifactId>
<version>${aws-lambda-core.version}</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-dynamodb</artifactId>
<version>${aws-java-sdk-dynamodb.version}</version>
<exclusions>
<exclusion>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
</exclusion>
<exclusion>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-kms</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
<version>${aws-lambda-java-events.version}</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-log4j</artifactId>

View File

@ -21,7 +21,7 @@
# THE SOFTWARE.
#
service: lambda-info-http-endpoint
service: serverless-services
frameworkVersion: ">=1.2.0 <2.0.0"
@ -41,9 +41,62 @@ package:
artifact: target/serverless.jar
functions:
currentTime:
handler: com.iluwatar.serverless.api.LambdaInfoApiHandler
lambdaInfoApi:
handler: com.iluwatar.serverless.faas.api.LambdaInfoApiHandler
events:
- http:
path: info
method: get
savePerson:
handler: com.iluwatar.serverless.baas.api.SavePersonApiHandler
events:
- http:
path: api/person
method: post
cors: true
integration: lambda-proxy
getPerson:
handler: com.iluwatar.serverless.baas.api.FindPersonApiHandlerr
events:
- http:
path: api/person/{id}
method: get
cors: true
integration: lambda-proxy
resources:
Resources:
DynamoDbTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: persons
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
DynamoDBIamPolicy:
Type: AWS::IAM::Policy
DependsOn: DynamoDbTable
Properties:
PolicyName: lambda-dynamodb
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:Query
- dynamodb:Scan
Resource: arn:aws:dynamodb:*:*:table/persons
Roles:
- Ref: IamRoleLambdaExecution

View File

@ -0,0 +1,79 @@
package com.iluwatar.serverless.baas.api;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* abstract dynamodb handler
* @param <T> - serializable collection
*/
public abstract class AbstractDynamoDbHandler<T extends Serializable> {
private DynamoDBMapper dynamoDbMapper;
private ObjectMapper objectMapper;
public AbstractDynamoDbHandler() {
this.initAmazonDynamoDb();
this.objectMapper = new ObjectMapper();
}
private void initAmazonDynamoDb() {
AmazonDynamoDB amazonDynamoDb = AmazonDynamoDBClientBuilder
.standard()
.withRegion(Regions.US_EAST_1)
.build();
this.dynamoDbMapper = new DynamoDBMapper(amazonDynamoDb);
}
protected DynamoDBMapper getDynamoDbMapper() {
return this.dynamoDbMapper;
}
protected ObjectMapper getObjectMapper() {
return objectMapper;
}
public void setDynamoDbMapper(DynamoDBMapper dynamoDbMapper) {
this.dynamoDbMapper = dynamoDbMapper;
}
protected Map<String, String> headers() {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
return headers;
}
/**
* API Gateway response
*
* @param statusCode - status code
* @param body - Object body
* @return - api gateway proxy response
*/
protected APIGatewayProxyResponseEvent apiGatewayProxyResponseEvent(Integer statusCode, T body) {
APIGatewayProxyResponseEvent apiGatewayProxyResponseEvent =
new APIGatewayProxyResponseEvent().withHeaders(headers());
try {
apiGatewayProxyResponseEvent
.withStatusCode(statusCode)
.withBody(getObjectMapper()
.writeValueAsString(body));
} catch (JsonProcessingException jsonProcessingException) {
throw new RuntimeException(jsonProcessingException);
}
return apiGatewayProxyResponseEvent;
}
}

View File

@ -0,0 +1,29 @@
package com.iluwatar.serverless.baas.api;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.iluwatar.serverless.baas.model.Person;
import org.apache.log4j.Logger;
/**
* find person from persons collection
* Created by dheeraj.mummar on 3/5/18.
*/
public class FindPersonApiHandler extends AbstractDynamoDbHandler
implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private static final Logger LOG = Logger.getLogger(FindPersonApiHandler.class);
private static final Integer SUCCESS_STATUS_CODE = 200;
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent,
Context context) {
LOG.info(apiGatewayProxyRequestEvent.getPathParameters());
Person person = this.getDynamoDbMapper().load(Person.class, apiGatewayProxyRequestEvent
.getPathParameters().get("id"));
return apiGatewayProxyResponseEvent(SUCCESS_STATUS_CODE, person);
}
}

View File

@ -0,0 +1,39 @@
package com.iluwatar.serverless.baas.api;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.iluwatar.serverless.baas.model.Person;
import org.apache.log4j.Logger;
import java.io.IOException;
/**
* save person into persons collection
* Created by dheeraj.mummar on 3/4/18.
*/
public class SavePersonApiHandler extends AbstractDynamoDbHandler
implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private static final Logger LOG = Logger.getLogger(SavePersonApiHandler.class);
private static final Integer CREATED_STATUS_CODE = 201;
private static final Integer BAD_REQUEST_STATUS_CODE = 400;
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent
apiGatewayProxyRequestEvent, Context context) {
APIGatewayProxyResponseEvent apiGatewayProxyResponseEvent;
Person person;
try {
person = getObjectMapper().readValue(apiGatewayProxyRequestEvent.getBody(), Person.class);
getDynamoDbMapper().save(person);
apiGatewayProxyResponseEvent = apiGatewayProxyResponseEvent(CREATED_STATUS_CODE, person);
} catch (IOException ioException) {
LOG.error("unable to parse body", ioException);
apiGatewayProxyResponseEvent = apiGatewayProxyResponseEvent(BAD_REQUEST_STATUS_CODE, null);
}
return apiGatewayProxyResponseEvent;
}
}

View File

@ -0,0 +1,119 @@
package com.iluwatar.serverless.baas.model;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBDocument;
import java.io.Serializable;
/**
* Address Object
* Created by dheeraj.mummarareddy on 3/4/18.
*/
@DynamoDBDocument
public class Address implements Serializable {
private static final long serialVersionUID = 6760844284799736970L;
private String addressLineOne;
private String addressLineTwo;
private String city;
private String state;
private String zipCode;
@DynamoDBAttribute(attributeName = "addressLineOne")
public String getAddressLineOne() {
return addressLineOne;
}
public void setAddressLineOne(String addressLineOne) {
this.addressLineOne = addressLineOne;
}
@DynamoDBAttribute(attributeName = "addressLineTwo")
public String getAddressLineTwo() {
return addressLineTwo;
}
public void setAddressLineTwo(String addressLineTwo) {
this.addressLineTwo = addressLineTwo;
}
@DynamoDBAttribute(attributeName = "city")
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
@DynamoDBAttribute(attributeName = "state")
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
@DynamoDBAttribute(attributeName = "zipCode")
public String getZipCode() {
return zipCode;
}
public void setZipCode(String zipCode) {
this.zipCode = zipCode;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Address address = (Address) o;
if (addressLineOne != null ? !addressLineOne.equals(address.addressLineOne) :
address.addressLineOne != null) {
return false;
}
if (addressLineTwo != null ? !addressLineTwo.equals(address.addressLineTwo) :
address.addressLineTwo != null) {
return false;
}
if (city != null ? !city.equals(address.city) : address.city != null) {
return false;
}
if (state != null ? !state.equals(address.state) : address.state != null) {
return false;
}
return zipCode != null ? zipCode.equals(address.zipCode) : address.zipCode == null;
}
@Override
public int hashCode() {
int result = addressLineOne != null ? addressLineOne.hashCode() : 0;
result = 31 * result + (addressLineTwo != null ? addressLineTwo.hashCode() : 0);
result = 31 * result + (city != null ? city.hashCode() : 0);
result = 31 * result + (state != null ? state.hashCode() : 0);
result = 31 * result + (zipCode != null ? zipCode.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Address{"
+ "addressLineOne='" + addressLineOne + '\''
+ ", addressLineTwo='" + addressLineTwo + '\''
+ ", city='" + city + '\''
+ ", state='" + state + '\''
+ ", zipCode='" + zipCode + '\''
+ '}';
}
}

View File

@ -0,0 +1,102 @@
package com.iluwatar.serverless.baas.model;
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;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serializable;
/**
* Person Request
* Created by dheeraj.mummarareddy on 3/4/18.
*/
@DynamoDBTable(tableName = "persons")
public class Person implements Serializable {
private static final long serialVersionUID = -3413087924608627075L;
private String id;
private String firstName;
private String lastName;
private Address address;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
@DynamoDBHashKey(attributeName = "id")
@DynamoDBAutoGeneratedKey
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@DynamoDBAttribute(attributeName = "firstName")
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
@DynamoDBAttribute(attributeName = "lastName")
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@DynamoDBAttribute(attributeName = "address")
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Person person = (Person) o;
if (firstName != null ? !firstName.equals(person.firstName) : person.firstName != null) {
return false;
}
if (lastName != null ? !lastName.equals(person.lastName) : person.lastName != null) {
return false;
}
return address != null ? address.equals(person.address) : person.address == null;
}
@Override
public int hashCode() {
int result = firstName != null ? firstName.hashCode() : 0;
result = 31 * result + (lastName != null ? lastName.hashCode() : 0);
result = 31 * result + (address != null ? address.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Person{"
+ "id='" + id + '\''
+ ", firstName='" + firstName + '\''
+ ", lastName='" + lastName + '\''
+ ", address=" + address
+ '}';
}
}

View File

@ -20,7 +20,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.serverless;
package com.iluwatar.serverless.faas;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

View File

@ -20,7 +20,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.serverless;
package com.iluwatar.serverless.faas;
import java.io.Serializable;

View File

@ -20,13 +20,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.serverless.api;
package com.iluwatar.serverless.faas.api;
import com.iluwatar.serverless.ApiGatewayResponse;
import com.iluwatar.serverless.faas.ApiGatewayResponse;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.iluwatar.serverless.LambdaInfo;
import com.iluwatar.serverless.faas.LambdaInfo;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;

View File

@ -0,0 +1,49 @@
package com.illuwatar.serverless.baas.api;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.iluwatar.serverless.baas.api.FindPersonApiHandler;
import com.iluwatar.serverless.baas.api.SavePersonApiHandler;
import com.iluwatar.serverless.baas.model.Person;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.Collections;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* Created by dheeraj.mummar on 3/5/18.
*/
@RunWith(MockitoJUnitRunner.class)
public class FindPersonApiHandlerTest {
private FindPersonApiHandler findPersonApiHandler;
@Mock
private DynamoDBMapper dynamoDbMapper;
@Before
public void setUp() {
this.findPersonApiHandler = new FindPersonApiHandler();
this.findPersonApiHandler.setDynamoDbMapper(dynamoDbMapper);
}
@Test
public void handleRequest() {
findPersonApiHandler.handleRequest(apiGatewayProxyRequestEvent(), mock(Context.class));
verify(dynamoDbMapper, times(1)).load(Person.class, "37e7a1fe-3544-473d-b764-18128f02d72d");
}
private APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent() {
return new APIGatewayProxyRequestEvent()
.withPathParamters(Collections
.singletonMap("id", "37e7a1fe-3544-473d-b764-18128f02d72d"));
}
}

View File

@ -0,0 +1,78 @@
package com.illuwatar.serverless.baas.api;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.iluwatar.serverless.baas.api.SavePersonApiHandler;
import com.iluwatar.serverless.baas.model.Address;
import com.iluwatar.serverless.baas.model.Person;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import static org.mockito.Mockito.*;
/**
* Created by dheeraj.mummar on 3/4/18.
*/
@RunWith(MockitoJUnitRunner.class)
public class SavePersonApiHandlerTest {
private SavePersonApiHandler savePersonApiHandler;
@Mock
private DynamoDBMapper dynamoDbMapper;
private ObjectMapper objectMapper = new ObjectMapper();
@Before
public void setUp() {
this.savePersonApiHandler = new SavePersonApiHandler();
this.savePersonApiHandler.setDynamoDbMapper(dynamoDbMapper);
}
@Test
public void handleRequestSavePersonSuccessful() throws JsonProcessingException {
Person person = newPerson();
APIGatewayProxyResponseEvent apiGatewayProxyResponseEvent =
this.savePersonApiHandler
.handleRequest(apiGatewayProxyRequestEvent(objectMapper.writeValueAsString(person)), mock(Context.class));
verify(dynamoDbMapper, times(1)).save(person);
Assert.assertNotNull(apiGatewayProxyResponseEvent);
Assert.assertEquals(new Integer(201), apiGatewayProxyResponseEvent.getStatusCode());
}
@Test
public void handleRequestSavePersonException() {
APIGatewayProxyResponseEvent apiGatewayProxyResponseEvent =
this.savePersonApiHandler
.handleRequest(apiGatewayProxyRequestEvent("invalid sample request"), mock(Context.class));
Assert.assertNotNull(apiGatewayProxyResponseEvent);
Assert.assertEquals(new Integer(400), apiGatewayProxyResponseEvent.getStatusCode());
}
private APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent(String body) {
APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent = new APIGatewayProxyRequestEvent();
return apiGatewayProxyRequestEvent.withBody(body);
}
private Person newPerson() {
Person person = new Person();
person.setFirstName("Thor");
person.setLastName("Odinson");
Address address = new Address();
address.setAddressLineOne("1 Odin ln");
address.setCity("Asgard");
address.setState("country of the Gods");
address.setZipCode("00001");
person.setAddress(address);
return person;
}
}

View File

@ -20,7 +20,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.serverless.api;
package com.iluwatar.serverless.faas.api;
import com.amazonaws.services.lambda.runtime.Context;
import org.junit.jupiter.api.Test;