2017年8月17日 星期四

透過 AWS Lambda 開發 Serverless Framework(二):開發 RESTful API

從前一篇文章中,可以看出 Lambda 是一種以事件來觸發的架構
理論上因為 RESTful API 也是以事件觸發的,所以應該也有機會變成 Lambda
不過 JAX-RS 的結構又跟 Lambda 的結構不太一樣,所以到底行不行呢?

不過如果不行的話,我就不會寫這篇了 XDDDD

要用 Lambda 來跑 RESTful API,基本一定需要的就是要結合 AWS API Gateway 和 Lambda
也就是要在 API Gateway 上定義 RESTful API 的每個 API endpoint,然後讓 Lambda 以 API Gateway 作為 Trigger。
而實務上,要做這些事情手動相當麻煩~所幸網路上也有不少開源的專案可以輔助這些事情。

但網路上相關的開源專案其實有很多種,這裡會使用的組合是 jrestless + serverless。
如果想要參考其他選擇,另外還有 Lambada Framework [3],以及 [3] 最底下 Other projects 那邊講到的其他類似的專案。

jrestless 和 serverless 的功能

首先先談談為何我們會需要 jrestless 跟 serverless。

先回憶一下 Lambda 的程式碼結構吧!
我們必須要有一個實作 RequestHandler 介面的物件,裡面負責接收 Trigger 送來的事件
但一般來說,我們使用 JAX-RS 開發 RESTful API 時,並沒有像是 RequestHandler 這樣的統一入口
這時要怎麼把 RESTful API 轉成能夠相容 Lambda 的模式呢?
jrestless 能夠幫忙的地方就在這裡,它建好了跟 API Gateway 溝通的樣板,我們只需要額外把自定義的部份加上去即可
其他 Request 和 Response 的部份,jrestless 會幫忙做好轉換。

接著,在程式碼的部份處理好以後,實際上要放上 Lambda 時還有一些蠻麻煩的事情要做
也就是需要先去 API Gateway 上面設定 API endpoint。
endpoint 設定好以後,就是要把程式上傳到 Lambda,而這些有點繁雜的工作,都是 serverless 可以幫忙處理的。

環境準備

首先,電腦上需要有 serverless 的環境,因為 serverless 是用 command line 控制的。
具體來說,可以參考 serverless 官網 [2] 裡的文件,只要先裝了 npm 以後,就可以用下述的指令安裝 serverless。

# Installing the serverless cli
npm install -g serverless
RESTful API 的撰寫

假設現在我們的 RESTful API 程式碼如下:

package com.example.test;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Path("/api")
public class SampleResource {
    private static final Logger log = LoggerFactory.getLogger(SampleResource.class);
    
    @GET
    @Path("/test")
    @Produces(MediaType.TEXT_PLAIN)
    public Response test() {
        log.info("Test is requested.");
        return Response.ok()
            .entity("Test is requested")
            .build();
    }
}

也就是說,假設我們只有一個 RESTful API,這個 API 的網址是 /api/test
然後它會回覆 HTTP statuscode 200,以及 Response Body 是 “Test is requested”。

Maven 設定

首先我們需要先將 pom.xml 調整成以下的樣子。

PS. 如果是使用 Gradle,就請參考 jrestless 在 Github 上的文件吧 XD

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>test</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>test</name>
	<url>http://maven.apache.org</url>

	<properties>
		<jersey.version>2.23</jersey.version>
		<slf4j.version>1.7.25</slf4j.version>
		<logback.version>1.1.11</logback.version>
		<jrestless.version>0.5.1</jrestless.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>1.8</maven.compiler.source>
		<maven.compiler.target>1.8</maven.compiler.target>
	</properties>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.glassfish.jersey</groupId>
				<artifactId>jersey-bom</artifactId>
				<version>${jersey.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<dependencies>
		<!-- Jersey -->
		<dependency>
			<groupId>org.glassfish.jersey.containers</groupId>
			<artifactId>jersey-container-grizzly2-http</artifactId>
		</dependency>
		<dependency>
			<groupId>org.glassfish.jersey.containers</groupId>
			<artifactId>jersey-container-servlet-core</artifactId>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>jul-to-slf4j</artifactId>
			<version>${slf4j.version}</version>
		</dependency>
		<!-- AWS Lambda -->
		<dependency>
			<groupId>com.amazonaws</groupId>
			<artifactId>aws-lambda-java-core</artifactId>
			<version>1.1.0</version>
		</dependency>
		<dependency>
			<groupId>com.amazonaws</groupId>
			<artifactId>aws-lambda-java-log4j</artifactId>
			<version>1.0.0</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>log4j-over-slf4j</artifactId>
			<version>${slf4j.version}</version>
		</dependency>
		<!-- jrestless which package JAX-RS as AWS Lambda -->
		<dependency>
			<groupId>com.jrestless.aws</groupId>
			<artifactId>jrestless-aws-gateway-handler</artifactId>
			<version>${jrestless.version}</version>
		</dependency>
		<!-- Logging -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>${slf4j.version}</version>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>${logback.version}</version>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-core</artifactId>
			<version>${logback.version}</version>
		</dependency>
		<!-- Unit Test -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
			<scope>test</scope>
		</dependency>
	</dependencies>
	
	<build>
		<plugins>
			<!-- Using the Apache Maven Shade plugin to package the jar "This plugin 
				provides the capability to package the artifact in an uber-jar, including 
				its dependencies and to shade - i.e. rename - the packages of some of the 
				dependencies." Link: https://maven.apache.org/plugins/maven-shade-plugin/ -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-shade-plugin</artifactId>
				<version>2.3</version>
				<configuration>
					<createDependencyReducedPom>false</createDependencyReducedPom>
				</configuration>
				<executions>
					<execution>
						<phase>package</phase>
						<goals>
							<goal>shade</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

	<repositories>
		<!-- Repository for jrestless. -->
		<repository>
			<id>jcenter</id>
			<url>http://jcenter.bintray.com</url>
		</repository>
	</repositories>
</project>

其中比較主要的部份是加入了 Lambda 和 jrestless 的套件,然後打包時改用 shade plugin(會打包成 JAR 而不是 WAR)
並且因為 jrestless 並沒有發佈在 Maven Central Repository,因此需要加入 jcenter 這個 Repository。

而 Java 版本的問題,因為 Lambda 目前只支援 Java 8,因此一定要設定 Java 版本是 1.8。

Request Handler

Maven 設定好以後,接著需要寫 Lambda 用的 Request Handler
不過比較不同的是,這裡只需要繼承 jrestless 的 GatewayRequestObjectHandler
然後做一點 Jetty 的設定即可。

package com.example.test;

import java.io.IOException;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;

import org.glassfish.jersey.server.ResourceConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;

import com.jrestless.aws.gateway.handler.GatewayRequestAndLambdaContext;
import com.jrestless.aws.gateway.handler.GatewayRequestObjectHandler;
import com.jrestless.aws.gateway.io.GatewayResponse;
import com.jrestless.core.container.io.JRestlessContainerRequest;

public class RequestHandler extends GatewayRequestObjectHandler {
  private static final Logger log = LoggerFactory.getLogger(RequestHandler.class);
  
  public RequestHandler() {
    // bridge java.util.logging (used by Jersey) to SLF4J which will use log4j
    SLF4JBridgeHandler.removeHandlersForRootLogger();
    SLF4JBridgeHandler.install();
    // configure the application with the resource
    ResourceConfig config = new ResourceConfig()
        .register(SampleResource.class)
        // For testing only.
        .register(new LoggingRequestFilter())
        .packages("com.example.test");
    init(config);
    start();
  }
  
  private class LoggingRequestFilter implements ContainerRequestFilter {
    private Logger log = LoggerFactory.getLogger(LoggingRequestFilter.class);
    @Override
    public void filter(ContainerRequestContext request) throws IOException {
      log.debug("baseUri: {}", request.getUriInfo().getBaseUri());
      log.debug("requestUri: {}", request.getUriInfo().getRequestUri());
    }
  }
  
  @Override
  protected void beforeHandleRequest(
      GatewayRequestAndLambdaContext request,
      JRestlessContainerRequest containerRequest) {
    log.info("Start to handle request: {}", request.getGatewayRequest());
  }

  @Override
  protected GatewayResponse onRequestSuccess(
      GatewayResponse response,
      GatewayRequestAndLambdaContext request,
      JRestlessContainerRequest containerRequest) {
    log.info("Request handled successfully: {}", response);
    return response;
  }
}

在上述程式碼中,我們實際上是在 Request Handler 被初始化時啟動了一個 Jetty 的服務
然後在 Jetty 上註冊我們寫的 RESTful API。

其他部份就只是 jrestless 的範例擷取過來的,就是在各個階段多印一些 log 出來,以表示我們可以做哪些額外的控制。

部署 RESTful API 到 AWS

最開始的時候提到,我們會使用 serverless 來進行部署
因此我們需要寫一個 serverless.yml 的檔案,用來設定 serverless 的部署方法。

service: test-api

provider:
    name: aws
    runtime: java8
    stage: dev
    region: us-east-1
    memorySize: 256
    timeout: 30
    deploymentBucket:
        name: ${self:provider.region}.lambda.deployment
    role: arn:aws:iam::XXXXXXXXXXXX:role/OOOOOOOOOOOOO

package:
    artifact: target/test-0.0.1-SNAPSHOT.jar

functions:
    api:
        handler: com.example.test.RequestHandler
        events:
            - http:
                path: api/test
                method: any

其中 provider.role 的設定是要在 AWS IAM 裡先建好對應權限的角色,而需要什麼權限就要看這個 RESTful API 的需求
或者如果想讓 serverless 自己幫忙產生新的 Role,可以把 provider.role 移除,serverless 就會自動建立。

PS. 當然,如果要讓 serverless 自動建立,那在電腦裡設定的 AWS credential 必須要有足夠的權限才行。

provider.deploymentBucket 是指定 S3 的空間,用來存放 CloudFormation 使用的 YML。
serverless 會依照我們設定的 functions 先產生一份要給 CloudFormation 用來建置 API Gateway 的 YML
然後將這份 YML 放在這裡指定的位置,以這個例子來說,是「us-east-1.lambda.deployment」這個 bucket。
同樣地,如果這個設定不給的話,serverless 會自動在 S3 上建立一個 bucket 來放 YML。

最後,最重要的地方在於 functions 這個區塊,裡面要設定所有 RESTful API 的 endpoint 和接受的 HTTP Method。

全部設定完以後,使用以下的 Maven 和 serverless 指令,就可以讓 Maven 打包 jar、並讓 serverless 把打包好的 jar 上傳到 Lambda。

mvn clean package
serverless deploy

如果執行正常的話,會產生類似下述的訊息,其中會告訴我們最後建立好的網址是什麼。

Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..........
Serverless: Stack update finished...
Service Information
service: test-api
stage: dev
region: us-east-1
api keys:
  None
endpoints:
  ANY - https://6tsdvrger0.execute-api.us-east-1.amazonaws.com/dev/api/test
functions:
  api: test-api-dev-api
Serverless: Removing old service versions...

然後就可以用上面寫到的連結,去存取部署好的 API 了。

參考資料
  1. jrestless
  2. serverless
  3. Lambada Framework
  4. jrestless examples

沒有留言: