これは、なにをしたくて書いたもの?
LocalStackでAWS Lambda、それからAmazon API Gatewayの組み合わせでいろいろ遊んでいるのですが。
1度、Serverless Framewokにも触れた方がいいかなと思い、試してみることにしました。
LocalStackにも、Serverless Framework向けの話があるようですし。
https://localstack.cloud/docs/how-to/serverless/
Serverless Framework
まずはServerless Frameworkとは?というとこからですが。
Serverless: Develop & Monitor Apps On AWS Lambda
Serverless Frameworkは、サーバーレスアプリケーションの開発、デプロイ、トラブルシュートなどのオーバーヘッド、
コストを削減するフレームワークのようです。
Serverless Framework Documentation
OSSで公開されているCLIと、ホストされているダッシュボードで構成されています。
AWS SAMと似たような感じですが、AWS以外にもAzure Functions、Google Cloud Functionsなど
複数のクラウドプロバイダーに対応しているところがポイントです。
Serverless - Infrastructure & Compute Providers
プロジェクトを作成する際に、テンプレートを指定して複数の言語からプロジェクトの構成を選ぶことができるみたいです。
LocalStackとServerless Framework
次に、LocalStackとServerless Frameworkについて。
LocalStackのドキュメントに、Serverless Frameworkに向けた対応が書かれています。
https://localstack.cloud/docs/how-to/serverless/
LocalStack / Serverless Framework
具体的には、LocalStackがServerless Framework向けのプラグインを提供しているので、こちらを使えばよいみたいです。
GitHub - localstack/serverless-localstack: ⚡ Serverless plugin for running against LocalStack
一方で、Serverless Frameworkもオフラインで動作させるためのプラグインを提供しているようです。
視点がLocalStackのようなAWSの多くのサービスをエミュレーションするものの一部として使いたいのか、
Serverless Frameworkの範囲で動作すればいいのかで選ぶんでしょうね。
では、今回はAmazon API Gateway+AWS Lambdaの構成のアプリケーションを、Serverless Frameworkと
LocalStackで使ってみましょう。
環境
今回の環境は、こちら。
LocalStack。
$ localstack --version 0.12.18.3
起動。
$ LAMBDA_EXECUTOR=docker-reuse localstack start
$ aws --version aws-cli/2.2.44 Python/3.8.8 Linux/5.4.0-88-generic exe/x86_64.ubuntu.20 prompt/off
Serverless Frameworkを使うための、Node.js。
$ node -v v14.18.0 $ npm -v 6.14.15
Javaに関するもの。
$ java --version openjdk 11.0.11 2021-04-20 OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04) OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.3 (ff8e977a158738155dc465c6a97ffaf31982d739) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 11.0.11, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-88-generic", arch: "amd64", family: "unix"
Serverless Frameworkをインストールする
では、Serverless Frameworkのインストールから
Setting Up Serverless Framework With AWS
インストール方法はいろいろあるのですが、LocalStackのServerless Frameworkのプラグインがnpm
でインストールする
形態なので、Serverless Frameworkも合わせてnpm
でインストールすることにしました。
というわけで、Serverless FramewokとLocalStackのプラグインをインストール。
$ npm i -g serverless $ npm i -D serverless-localstack
バージョン。
$ serverless --version Framework Core: 2.62.0 (local) Plugin: 5.4.6 SDK: 4.3.0 Components: 3.17.1
LocalStackのServerless Frameworkプラグインのバージョンは、こちら。
$ cat node_modules/serverless-localstack/package.json | jq '._id' "serverless-localstack@0.4.35"
では、こちらのドキュメントに書かれているテンプレートを見つつ
プロジェクトの作成。テンプレートは、aws-java-maven
を選択。
$ serverless create --template aws-java-maven --path serverless-localstack-java
プロジェクトが作成されました。
Serverless: Generating boilerplate... Serverless: Generating boilerplate in "/path/to/serverless-localstack-java" Serverless: Successfully generated boilerplate for template: "aws-java-maven"
ディレクトリ内に移動して
$ cd serverless-localstack-java
できあがったファイルを確認。
$ tree . ├── pom.xml ├── serverless.yml └── src └── main ├── java │ └── com │ └── serverless │ ├── ApiGatewayResponse.java │ ├── Handler.java │ └── Response.java └── resources └── log4j2.xml 6 directories, 6 files
pom.xml
<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.serverless</groupId> <artifactId>hello</artifactId> <packaging>jar</packaging> <version>dev</version> <name>hello</name> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-log4j2</artifactId> <version>1.1.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.8.2</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.8.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.9.10</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.10</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.9.10</version> </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> <configuration> <transformers> <transformer implementation="com.github.edwgiz.mavenShadePlugin.log4j2CacheTransformer.PluginsCacheFileTransformer"> </transformer> </transformers> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>com.github.edwgiz</groupId> <artifactId>maven-shade-plugin.log4j2-cachefile-transformer</artifactId> <version>2.8.1</version> </dependency> </dependencies> </plugin> </plugins> </build> </project>
よく見ると、プロジェクト名とは全然関係ないアーティファクト名などになっているんですね。まあ、テンプレート
そのままなんでしょう。
<groupId>com.serverless</groupId> <artifactId>hello</artifactId> <packaging>jar</packaging> <version>dev</version> <name>hello</name>
serverless.yml
の内容。
$ grep -v '#' serverless.yml service: serverless-localstack-java frameworkVersion: '2' provider: name: aws runtime: java8 lambdaHashingVersion: 20201221 package: artifact: target/hello-dev.jar functions: hello: handler: com.serverless.Handler
src/main/java/com/serverless/Handler.java
package com.serverless; import java.util.Collections; import java.util.Map; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; public class Handler implements RequestHandler<Map<String, Object>, ApiGatewayResponse> { private static final Logger LOG = LogManager.getLogger(Handler.class); @Override public ApiGatewayResponse handleRequest(Map<String, Object> input, Context context) { LOG.info("received: {}", input); Response responseBody = new Response("Go Serverless v1.x! Your function executed successfully!", input); return ApiGatewayResponse.builder() .setStatusCode(200) .setObjectBody(responseBody) .setHeaders(Collections.singletonMap("X-Powered-By", "AWS Lambda & serverless")) .build(); } }
ApiGatewayResponse
やResponse
といったクラスは、Serverless Frameworkなどが提供しているものではなく、
生成されたソースコードの中に含まれているものになります。
src/main/java/com/serverless/ApiGatewayResponse.java
package com.serverless; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Collections; import java.util.Map; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; public class ApiGatewayResponse { private final int statusCode; private final String body; private final Map<String, String> headers; private final boolean isBase64Encoded; public ApiGatewayResponse(int statusCode, String body, Map<String, String> headers, boolean isBase64Encoded) { this.statusCode = statusCode; this.body = body; this.headers = headers; this.isBase64Encoded = isBase64Encoded; } public int getStatusCode() { return statusCode; } public String getBody() { return body; } public Map<String, String> getHeaders() { return headers; } // API Gateway expects the property to be called "isBase64Encoded" => isIs public boolean isIsBase64Encoded() { return isBase64Encoded; } public static Builder builder() { return new Builder(); } public static class Builder { private static final Logger LOG = LogManager.getLogger(ApiGatewayResponse.Builder.class); private static final ObjectMapper objectMapper = new ObjectMapper(); private int statusCode = 200; private Map<String, String> headers = Collections.emptyMap(); private String rawBody; private Object objectBody; private byte[] binaryBody; private boolean base64Encoded; public Builder setStatusCode(int statusCode) { this.statusCode = statusCode; return this; } public Builder setHeaders(Map<String, String> headers) { this.headers = headers; return this; } /** * Builds the {@link ApiGatewayResponse} using the passed raw body string. */ public Builder setRawBody(String rawBody) { this.rawBody = rawBody; return this; } /** * Builds the {@link ApiGatewayResponse} using the passed object body * converted to JSON. */ public Builder setObjectBody(Object objectBody) { this.objectBody = objectBody; return this; } /** * Builds the {@link ApiGatewayResponse} using the passed binary body * encoded as base64. {@link #setBase64Encoded(boolean) * setBase64Encoded(true)} will be in invoked automatically. */ public Builder setBinaryBody(byte[] binaryBody) { this.binaryBody = binaryBody; setBase64Encoded(true); return this; } /** * A binary or rather a base64encoded responses requires * <ol> * <li>"Binary Media Types" to be configured in API Gateway * <li>a request with an "Accept" header set to one of the "Binary Media * Types" * </ol> */ public Builder setBase64Encoded(boolean base64Encoded) { this.base64Encoded = base64Encoded; return this; } public ApiGatewayResponse build() { String body = null; if (rawBody != null) { body = rawBody; } else if (objectBody != null) { try { body = objectMapper.writeValueAsString(objectBody); } catch (JsonProcessingException e) { LOG.error("failed to serialize object", e); throw new RuntimeException(e); } } else if (binaryBody != null) { body = new String(Base64.getEncoder().encode(binaryBody), StandardCharsets.UTF_8); } return new ApiGatewayResponse(statusCode, body, headers, base64Encoded); } } }
src/main/java/com/serverless/Response.java
package com.serverless; import java.util.Map; public class Response { private final String message; private final Map<String, Object> input; public Response(String message, Map<String, Object> input) { this.message = message; this.input = input; } public String getMessage() { return this.message; } public Map<String, Object> getInput() { return this.input; } }
では、こちらをLocalStackにデプロイしていきましょう。
とりあえず、プロジェクトで使うJavaが1.8になっているので、こちらを11に変更しておきましょう。
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
serverless.yml
のruntime
もjava11
に変更しておきます。
provider: name: aws runtime: java11 lambdaHashingVersion: 20201221
次に、LocalStackのServerless Frameworkプラグインの設定を行います。
LocalStack Serverless Plugin / Configuring / Configuration via serverless.yml
最小構成はplugins
にserverless-localstack
を足せばOKなようですが、なんとなくcustom
の方も定義だけは
書いておきました。
frameworkVersion: '2' plugins: - serverless-localstack custom: localstack: provider: name: aws runtime: java11 lambdaHashingVersion: 20201221
あとはなにも考えずにmvn package
して
$ mvn package
デプロイ。
$ serverless deploy
LocalStackへのデプロイが始まり
Serverless: Using serverless-localstack Serverless: Packaging service... Serverless: Creating Stack... Serverless: Checking Stack create progress... ........ Serverless: Stack create finished... Serverless: Uploading CloudFormation file to S3... Serverless: Uploading artifacts... Serverless: Uploading service hello-dev.jar file to S3 (3.38 MB)... Serverless: Validating template... Serverless: Skipping template validation: Unsupported in Localstack Serverless: Updating Stack... Serverless: Checking Stack update progress... ........
完了。
Serverless: Stack update finished... Service Information service: serverless-localstack-java stage: dev region: us-east-1 stack: serverless-localstack-java-dev resources: 6 api keys: None endpoints: functions: hello: serverless-localstack-java-dev-hello layers: None
ただ、これだとAWS Lambda関数はデプロイされているのですが、Amazon API Gatewayなどは作成されません。
$ awslocal apigateway get-rest-apis { "items": [] }
serverless.yml
の変更が必要そうです。
こちらのドキュメントを参照して
Serverless Framework - AWS Lambda Events - REST API (API Gateway v1)
リソース定義を変更。
functions: hello: handler: com.serverless.Handler # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details events: - http: path: /hello method: get
http
をイベントとして設定。
ちなみに、最初の定義はこうでした。
functions: hello: handler: com.serverless.Handler
では、再度デプロイ。
$ serverless deploy Serverless: Using serverless-localstack Serverless: Packaging service... Serverless Error ---------------------------------------- The serverless deployment bucket "serverless-localstack-java-de-serverlessdeploymentbuck-d23aaeee" does not exist. Create it manually if you want to reuse the CloudFormation stack "serverless-localstack-java-dev", or delete the stack if it is no longer required. Get Support -------------------------------------------- Docs: docs.serverless.com Bugs: github.com/serverless/serverless/issues Issues: forum.serverless.com Your Environment Information --------------------------- Operating System: linux Node Version: 14.18.0 Framework Version: 2.62.0 Plugin Version: 5.4.6 SDK Version: 4.3.0 Components Version: 3.17.1
S3バケットがないと言われたので…表示されている名前のバケットを作成して
$ awslocal s3 mb s3://serverless-localstack-java-de-serverlessdeploymentbuck-d23aaeee make_bucket: serverless-localstack-java-de-serverlessdeploymentbuck-d23aaeee
再度デプロイ。
$ serverless deploy
今度はうまくいきました。
Serverless: Using serverless-localstack Serverless: Packaging service... Serverless: Uploading CloudFormation file to S3... Serverless: Uploading artifacts... Serverless: Uploading service hello-dev.jar file to S3 (3.38 MB)... Serverless: Validating template... Serverless: Skipping template validation: Unsupported in Localstack Serverless: Updating Stack... Serverless: Checking Stack update progress... ............. Serverless: Stack update finished... Service Information service: serverless-localstack-java stage: dev region: us-east-1 stack: serverless-localstack-java-dev resources: 11 api keys: None endpoints: http://localhost:4566/restapis/mo8mdxctbu/dev/_user_request_ functions: hello: serverless-localstack-java-dev-hello layers: None
REST APIのIDも表示されているので、こちらを使って
$ REST_API_ID=mo8mdxctbu
LocalStackで動作しているAmazon API Gateway越しにcurl
でアクセスしてみます。
$ curl http://localhost:4566/restapis/$REST_API_ID/dev/_user_request_/hello {"message":"Go Serverless v1.x! Your function executed successfully!","input":{"path":"/hello","headers":{"remote-addr":"172.17.0.1","host":"localhost:4566","user-agent":"curl/7.68.0","accept":"*/*","x-forwarded-for":"172.17.0.1, localhost:4566, 127.0.0.1, localhost:4566","x-localstack-edge":"http://localhost:4566","authorization":"AWS4-HMAC-SHA256 Credential=__internal_call__/20160623/us-east-1/apigateway/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-target, Signature=1234","x-localstack-tgt-api":"apigateway","__apigw_request_region__":"us-east-1"},"multiValueHeaders":{"remote-addr":["172.17.0.1"],"host":["localhost:4566"],"user-agent":["curl/7.68.0"],"accept":["*/*"],"x-forwarded-for":["172.17.0.1, localhost:4566, 127.0.0.1, localhost:4566"],"x-localstack-edge":["http://localhost:4566"],"authorization":["AWS4-HMAC-SHA256 Credential=__internal_call__/20160623/us-east-1/apigateway/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-target, Signature=1234"],"x-localstack-tgt-api":["apigateway"],"__apigw_request_region__":["us-east-1"]},"body":"","isBase64Encoded":false,"httpMethod":"GET","queryStringParameters":{},"multiValueQueryStringParameters":{},"pathParameters":{},"resource":"/hello","requestContext":{"path":"/dev/hello","resourcePath":"/hello","apiId":"mo8mdxctbu","domainPrefix":"mo8mdxctbu","domainName":"mo8mdxctbu.execute-api.localhost.localstack.cloud","accountId":"000000000000","resourceId":"35mudhf87g","stage":"dev","identity":{"accountId":"000000000000","sourceIp":"127.0.0.1","userAgent":"curl/7.68.0"},"httpMethod":"GET","protocol":"HTTP/1.1","requestTime":"2021-10-12T06:40:09.696Z","requestTimeEpoch":1634020809696},"stageVariables":{}}}
OKですね。
あと、remove
でリソースを削除できるはずなのですが…
$ serverless remove
こちらはうまくいかなかったので、今回は置いておくことにしました。
Serverless: Using serverless-localstack Serverless: Getting all objects in S3 bucket... Serverless Error ---------------------------------------- ServerlessError: UnknownError at /path/to/serverless/node_modules/serverless/lib/aws/request.js:225:11 at processTicksAndRejections (internal/process/task_queues.js:95:5) at async persistentRequest (/path/to/serverless/node_modules/serverless/lib/aws/request.js:149:14) For debugging logs, run again after setting the "SLS_DEBUG=*" environment variable. Get Support -------------------------------------------- Docs: docs.serverless.com Bugs: github.com/serverless/serverless/issues Issues: forum.serverless.com Your Environment Information --------------------------- Operating System: linux Node Version: 14.18.0 Framework Version: 2.62.0 Plugin Version: 5.4.6 SDK Version: 4.3.0 Components Version: 3.17.1 Serverless: Removing objects in S3 bucket... Serverless: Removing Stack...
まとめ
Serverless FrameworkをLocalStackで使ってみました。
とはいえ、単純にデプロイしてみただけなので、Serverless Frameworkのほとんどの機能は使えていないわけですが。
ドキュメントを見ているとダッシュボード、CI/CD、モニタリング、テストなどいろいろできそうなのですが、
AWSに閉じるのならまずはAWS SAMあたりを使っておけばよいのかもな、とも。
ちょっといろいろ試しながら、考えていきましょう。