CLOVER🍀

That was when it all began.

Infinispan Server 12.1で、Hot Rod Client(RemoteCacheManagerAdmin)からCacheを作成する

これは、なにをしたくて書いたもの?

InfinispanのHot Rod Clientには、Cacheの作成、削除などを行う管理APIがあります。

今までも何回か使っていたのですが、よく忘れるので単体でメモしておくことにしました。

RemoteCacheManagerAdminインターフェース

RemoteCacheManagerAdminというインターフェースを使うことで、Infinispan ServerにおけるCacheの作成、削除などが
できます。

RemoteCacheManagerAdmin (Infinispan JavaDoc 12.1.10.Final API)

クラスタ構成のInfinispan Serverであっても、各Nodeに一気にCacheを作れたりするので便利です。
各Nodeそれぞれに対して、XMLファイルを修正しなくてすみますからね。
もっともCLIで操作した場合も同じように各NodeにCacheの定義を反映してくれるのですが、APIで書けた方が
便利な時もあるかな、と。

ちなみに、CLIであっても管理APIであっても、作成したCacheの情報はクラスタに新しいNodeが参加した場合、
そのCacheの情報もコピーされます。この点でも便利です。

ドキュメントとしては、このあたりに記載があります。

Deploying and Configuring Infinispan Servers / Creating Remote Caches with Hot Rod Clients

Hot Rod Java Clients / Creating Remote Caches with Hot Rod Clients

書かれている内容は同じなのですが。

RemoteCacheManagerAdminではなく、RemoteCacheConfigurationBuilder#configurationでCacheの定義を行う方法も
紹介されているのですが、こちらはパスします…。

RemoteCacheConfigurationBuilder (Infinispan JavaDoc 12.1.10.Final API)

環境

今回の環境は、こちらです。

$ 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.2 (ea98e05a04480131370aa0c110b8c54cf726c06f)
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-81-generic", arch: "amd64", family: "unix"

Infinispan Serverは12.1.7.Finalを使い、172.17.0.2〜4の3 Node用意します。

準備

各Nodeには、adminグループに属するユーザーと、applicationグループに属するユーザーをそれぞれ作成しておきます。

$ bin/cli.sh user create -g admin -p password admin-user
$ bin/cli.sh user create -g application -p password app-user

各グループは、ClusterRoleMapperを意識しています。

Deploying and Configuring Infinispan Servers / User Roles and Permissions

Cacheの定義などは行いません。

Maven依存関係などはこちら。

    <dependencies>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-client-hotrod</artifactId>
            <version>12.1.7.Final</version>
        </dependency>

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-core</artifactId>
            <version>12.1.7.Final</version>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.7.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.7.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.20.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

今回のお題には、最低限infinispan-client-hotrodが必要です。infinispan-coreはなくてもRemoteCacheManagerAdmin
使うことはできるのですが、あった方がCacheの定義をするには便利かなと思います。理由は後述します。

動作確認はテストコードで行うので、テスト用のライブラリも追加しておきます。

テストコードの雛形

テストコードの雛形は、こちら。

src/test/java/org/littlewings/remote/admin/CreateCacheTest.java

package org.littlewings.remote.admin;

import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.RemoteCacheManagerAdmin;
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class CreateCacheTest {

    private static String createUri(String userName, String password) {
        return String.format("hotrod://%s:%s@172.17.0.2:11222,172.17.0.3:11222,172.17.0.4:11222", userName, password);
    }

    // ここに、テストコードを書く
}

createUriというメソッドは、接続するユーザー名とパスワードを使って、Infinispan Serverへ接続するためのURI
作成するメソッドです。こちらを接続情報とします。

RemoteCacheManagerAdminを使ってCacheを作成してみる

では、RemoteCacheManagerAdminを使ってCacheを作成してみます。

使用例はこちら。

    @Test
    public void createCacheByAdminUser() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            RemoteCache<String, String> cache = admin.createCache("distCache", cacheConfiguration);

            IntStream.rangeClosed(1, 100).forEach(i -> cache.put("key" + i, "value" + i));
            assertThat(cache.size()).isEqualTo(100);

            admin.removeCache("distCache");
        }
    }

RemoteCacheManagerAdminは、RemoteCacheManager#administrationで取得できます。
なお、接続にはadminグループに属しているユーザーを使用しています。

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

Cacheの作成はRemoteCacheManagerAdmin#createCacheで行えばよいのですが

            RemoteCache<String, String> cache = admin.createCache("distCache", cacheConfiguration);

この時に作成するCacheの定義情報が必要です。

Cacheの定義情報はStringXMLStringConfigurationから作成でき、依存関係もinfinispan-client-hotrodだけで済みますが
Cacheの定義を文字列で用意することになり、まあ面倒です。この作り方は、最後に載せたいと思います。

もうひとつは、Embedded CacheでのConfigurationを使う方法です。こちらを使うとAPIでCache定義を組み立てられるの
ですが、infinispan-coreが必要になります。依存関係にinfinispan-coreを追加したのはこれが理由ですね。

こんな感じでCacheの定義情報を作成します。infinispan-client-hotrodの範囲外であることがわかるようにFQCN
クラス名を書いています。

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

Cacheの種類としては、Distributed Cacheとしました。

これでクラスタに参加する各NodeにCacheが作成され、すぐに使うことができます。

            IntStream.rangeClosed(1, 100).forEach(i -> cache.put("key" + i, "value" + i));
            assertThat(cache.size()).isEqualTo(100);

RemoteCacheManagerAdminを使うと、Cacheの削除も可能です。

            admin.removeCache("distCache");

なお、Cacheの作成にはADMIN権限が必要です。このため、applicationグループに属したユーザーでは
RemoteCacheManagerAdmin#createCacheの呼び出しに失敗します。

    @Test
    public void createCacheByApplicationUser() {
        String uri = createUri("app-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            assertThatThrownBy(() -> admin.createCache("distCache", cacheConfiguration))
                    .hasMessageContaining("org.infinispan.commons.CacheException: java.lang.SecurityException: ISPN000287: Unauthorized access: subject 'Subject with principal(s): [app-user, RolePrincipal{name='application'},")
                    .hasMessageContaining("' lacks 'ADMIN' permission")
                    .isInstanceOf(HotRodClientException.class);
        }
    }

ClusterRoleMapperADMIN権限が使えるのは、ALLが割り当てられているadminグループしかありません。

RemoteCacheManagerAdmin#getOrCreateCacheを使う

RemoteCacheManagerAdmin#createCacheでCacheの作成が可能なことはわかりましたが、作成対象のCacheがすでに
存在している場合は例外がスローされます。

    @Test
    public void recreateCache() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            assertThat(admin.createCache("distCache", cacheConfiguration))
                    .isNotNull()
                    .isInstanceOf(RemoteCache.class);

            assertThatThrownBy(() -> admin.createCache("distCache", cacheConfiguration))
                    .hasMessage("org.infinispan.commons.CacheConfigurationException: ISPN000507: Cache distCache already exists")
                    .isInstanceOf(HotRodClientException.class);

            admin.removeCache("distCache");
        }
    }

これを避ける場合は、RemoteCacheManagerAdmin#getOrCreateCacheを使えばOKです。

    @Test
    public void createOrGetCache() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            assertThat(admin.createCache("distCache", cacheConfiguration))
                    .isNotNull()
                    .isInstanceOf(RemoteCache.class);
            assertThat(admin.getOrCreateCache("distCache", cacheConfiguration))
                    .isNotNull()
                    .isInstanceOf(RemoteCache.class);

            admin.removeCache("distCache");

            assertThat(admin.getOrCreateCache("distCache", cacheConfiguration))
                    .isNotNull()
                    .isInstanceOf(RemoteCache.class);

            admin.removeCache("distCache");
        }
    }

対象のCacheがなければ作成し、すでに存在していればそれを使うという動作になります。

最初からこれを使ったらいいのでは、という話もありますね。

複数のCacheを作成してみる

ここまでDistributed Cacheばかり作っていましたが、さらにReplicated Cacheも作ってみましょう。

    @Test
    public void createVariousCache() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration distCacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            org.infinispan.configuration.cache.Configuration replCacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.REPL_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();


            RemoteCache<String, String> distCache = admin.getOrCreateCache("distCache", distCacheConfiguration);
            RemoteCache<String, String> replCache = admin.getOrCreateCache("replCache", replCacheConfiguration);

            IntStream.rangeClosed(1, 100).forEach(i -> distCache.put("key" + i, "value" + i));
            assertThat(distCache.size()).isEqualTo(100);

            IntStream.rangeClosed(1, 100).forEach(i -> replCache.put("key" + i, "value" + i));
            assertThat(replCache.size()).isEqualTo(100);

            admin.removeCache("distCache");
            admin.removeCache("replCache");
        }
    }

まあ、ちょっとしたオマケ的な感じですが…。

で、ここまでCacheを作成してきましたが、Infinispan Server側にどのように存在するかは特に書いてきませんでした。

Cacheを作成しても、infinispan.xmlそのものは変わりません。

server/conf/infinispan.xml

   <cache-container name="default" statistics="true">
      <transport cluster="${infinispan.cluster.name:cluster}" stack="${infinispan.cluster.stack:tcp}" node-name="${infinispan.node.name:}"/>
      <security>
         <authorization/>
      </security>
   </cache-container>

動的に作成したCacheは、server/data/caches.xmlファイルに記載されます。

server/data/caches.xml

<?xml version="1.0"?>
<infinispan xmlns="urn:infinispan:config:12.1">
    <cache-container>
        <replicated-cache mode="SYNC" remote-timeout="17500" name="replCache" statistics="true">
            <encoding>
                <key media-type="application/x-protostream"/>
                <value media-type="application/x-protostream"/>
            </encoding>
            <locking concurrency-level="1000" acquire-timeout="15000" striping="false"/>
            <state-transfer timeout="60000"/>
        </replicated-cache>
        <distributed-cache mode="SYNC" remote-timeout="17500" name="distCache" statistics="true">
            <encoding>
                <key media-type="application/x-protostream"/>
                <value media-type="application/x-protostream"/>
            </encoding>
            <locking concurrency-level="1000" acquire-timeout="15000" striping="false"/>
            <state-transfer timeout="60000"/>
        </distributed-cache>
    </cache-container>
</infinispan>

ConfigurationBuilderでCacheの定義を行った時はほぼ最低限の設定しか行っていなかったのに、いろいろと
デフォルト値が埋め込まれていますね。

また、最初にも書きましたが、クラスタに新しいNodeが参加した場合は、このファイルも新しいNodeに展開されます。

文字列でCache定義を行う

最後に、Cacheの定義を文字列で作成します。ドキュメントに記載されていたのは、この方法ですね。

    @Test
    public void createCacheByString() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            String cacheConfigurationAsString =
                    "        <distributed-cache name=\"distCache\">\n" +
                            "            <encoding>\n" +
                            "                <key media-type=\"application/x-protostream\"/>\n" +
                            "                <value media-type=\"application/x-protostream\"/>\n" +
                            "            </encoding>\n" +
                            "        </distributed-cache>";

            org.infinispan.commons.configuration.XMLStringConfiguration cacheConfiguration =
                    new org.infinispan.commons.configuration.XMLStringConfiguration(cacheConfigurationAsString);

            RemoteCache<String, String> distCache = admin.getOrCreateCache("distCache", cacheConfiguration);

            IntStream.rangeClosed(1, 100).forEach(i -> distCache.put("key" + i, "value" + i));
            assertThat(distCache.size()).isEqualTo(100);

            admin.removeCache("distCache");

            assertThatThrownBy(() -> admin.getOrCreateCache("distributedCache", cacheConfiguration))
                    .hasMessageContaining("org.infinispan.commons.CacheConfigurationException: ISPN005031: The supplied configuration for cache 'distributedCache' is missing a named configuration for it:")
                    .isInstanceOf(HotRodClientException.class);
        }
    }

ポイントはこちらで、Cache定義の断片をXMLStringConfigurationとして作成します。

            String cacheConfigurationAsString =
                    "        <distributed-cache name=\"distCache\">\n" +
                            "            <encoding>\n" +
                            "                <key media-type=\"application/x-protostream\"/>\n" +
                            "                <value media-type=\"application/x-protostream\"/>\n" +
                            "            </encoding>\n" +
                            "        </distributed-cache>";

            org.infinispan.commons.configuration.XMLStringConfiguration cacheConfiguration =
                    new org.infinispan.commons.configuration.XMLStringConfiguration(cacheConfigurationAsString);

これをRemoteCacheManagerAdmin#createCacheRemoteCacheManagerAdmin#getOrCreateCacheに渡せば
OKです。

            RemoteCache<String, String> distCache = admin.getOrCreateCache("distCache", cacheConfiguration);

この方法であればinfinispan-client-hotrodの依存関係に含まれる、infinispan-commonsがあれば依存関係としては
十分です。つまり、Hot Rod Clientのみで実行することができます。

ちなみに、XML定義のCache名と作成するCacheの名前が異なる場合は、例外がスローされるので注意しましょう。。

            assertThatThrownBy(() -> admin.getOrCreateCache("distributedCache", cacheConfiguration))
                    .hasMessageContaining("org.infinispan.commons.CacheConfigurationException: ISPN005031: The supplied configuration for cache 'distributedCache' is missing a named configuration for it:")
                    .isInstanceOf(HotRodClientException.class);

これでひととおり確認できた感じですね。

こちらのチュートリアルを見ていると、Configuration Templateも定義できそうですが、今回はパスします。

https://github.com/infinispan/infinispan-simple-tutorials/tree/main/infinispan-remote/cache-admin-api

Embedded CacheのConfigurationは、どう使われているの?

オマケ的にですが。

こちらがRemoteCacheManagerAdminインターフェースの実装です。

https://github.com/infinispan/infinispan/blob/12.1.7.Final/client/hotrod-client/src/main/java/org/infinispan/client/hotrod/impl/RemoteCacheManagerAdminImpl.java

Embedded CacheのConfigurationを渡した時にはどうなるのかな?と思ったのですが、結局文字列に変換されているので
最終的にはXML定義をInfinispan Serverに送ってCacheを作成するみたいですね。

https://github.com/infinispan/infinispan/blob/12.1.7.Final/client/hotrod-client/src/main/java/org/infinispan/client/hotrod/impl/RemoteCacheManagerAdminImpl.java#L61

まとめ

InfinispanのHot Rod ClientのRemoteCacheManagerAdminを使って、Infinispan Server上にCacheを動的に作成して
みました。

時々使っているのですが、本当によく忘れるのでいい機会かな、と。

今回作成したソースコードは、こちらに置いています。

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-create-cache

LocalStackの提供するAWS SAMを使って、LocalStackにAmazon API Gateway+AWS Lambdaをデプロイしてみる

これは、なにをしたくて書いたもの?

前に、LocalStackの提供するAWS CLIを使ってLocalStackを操作してみました。

LocalStackの提供するAWS CLIを使って、LocalStackを操作する - CLOVER🍀

こちらと類似のもので、LocalStackの提供するAWS SAM(aws-sam-cli-local)もあったので、こちらを試してみることに
しました。

GitHub - localstack/aws-sam-cli-local: Simple wrapper around AWS SAM CLI for use with LocalStack

これはLocalStackの提供するAWS CLIawslocal)と似たようなもので、エンドポイントをLocalStackに向けた
AWS SAMのラッパーです。

環境

今回の環境は、こちら。

$ localstack --version
0.12.17.5


$ python3 -V
Python 3.8.10


$ aws --version
aws-cli/2.2.35 Python/3.8.8 Linux/5.4.0-81-generic exe/x86_64.ubuntu.20 prompt/off


$ awslocal --version
aws-cli/2.2.35 Python/3.8.8 Linux/5.4.0-81-generic exe/x86_64.ubuntu.20 prompt/off

LocalStack自体のインストールは割愛します。AWS Lambdaの実行には、docker-reuseを指定して起動。

$ LAMBDA_EXECUTOR=docker-reuse localstack start

aws-sam-cli-localをインストールして、Amazon API GatewayAWS Lambdaを動かしてみる

まずはaws-sam-cli-localをインストール。

$ pip3 install aws-sam-cli-local==1.1.0.1

AWS SAMのラッパーなのですが、aws-sam-cli-localの依存関係に含まれているので明示的にインストールしなくても
大丈夫です。自分でインストールしていても問題ありません。

バージョン。

$ samlocal --version
SAM CLI, version 1.30.0

では、こちらに沿ってAmazon API GatewayAWS LambdaのHello Worldアプリケーションを作成して、LocalStackに
デプロイしてみましょう。

チュートリアル: Hello World アプリケーションのデプロイ - AWS Serverless Application Model

init。

$ samlocal init

アプリケーション名はsam-app、ランタイムはPython 3.8とします。

    -----------------------
    Generating application:
    -----------------------
    Name: sam-app
    Runtime: python3.8
    Dependency Manager: pip
    Application Template: hello-world
    Output Directory: .
    
    Next steps can be found in the README file at ./sam-app/README.md

生成されたディレクトリに移動。

$ cd sam-app

こんな感じにディレクトリツリーができています。

$ tree
.
├── README.md
├── __init__.py
├── events
│   └── event.json
├── hello_world
│   ├── __init__.py
│   ├── app.py
│   └── requirements.txt
├── template.yaml
└── tests
    ├── __init__.py
    ├── integration
    │   ├── __init__.py
    │   └── test_api_gateway.py
    ├── requirements.txt
    └── unit
        ├── __init__.py
        └── test_handler.py

5 directories, 13 files

Lambda関数。

hello_world/app.py

import json

# import requests


def lambda_handler(event, context):
    """Sample pure Lambda function

    Parameters
    ----------
    event: dict, required
        API Gateway Lambda Proxy Input Format

        Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format

    context: object, required
        Lambda Context runtime methods and attributes

        Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html

    Returns
    ------
    API Gateway Lambda Proxy Output Format: dict

        Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
    """

    # try:
    #     ip = requests.get("http://checkip.amazonaws.com/")
    # except requests.RequestException as e:
    #     # Send some context about this error to Lambda Logs
    #     print(e)

    #     raise e

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world",
            # "location": ip.text.replace("\n", "")
        }),
    }

Amazon API Gatewayを通したレスポンス形式に沿っていますね。

テンプレート。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app

  Sample SAM Template for sam-app

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.8
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

ビルド。

$ samlocal build

次はデプロイなのですが、今回はS3バケットを先に作成しておきます。

$ awslocal s3 mb s3://my-bucket

デプロイ。AWS CloudFormationのスタック名は任意の名称で。--regionを明示しているのはAWS_REGION環境変数
設定していないからのようです。aws configureもしていないのですが、実施していたらよいのかもしれませんね。
--s3-bucketでは、先ほど作成したS3バケットを指定。

$ samlocal deploy --stack-name my-stack --region us-east-1 --s3-bucket my-bucket

実行結果。

CloudFormation outputs from deployed stack
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                                        
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 HelloWorldApi                                                                                                                                              
Description         API Gateway endpoint URL for Prod stage for Hello World function                                                                                           
Value               https://uvpkw1p552.execute-api.us-east-1.amazonaws.com/Prod/hello/                                                                                         

Key                 HelloWorldFunction                                                                                                                                         
Description         Hello World Lambda Function ARN                                                                                                                            
Value               arn:aws:lambda:us-east-1:000000000000:function:my-stack-HelloWorldFunction-23bdfa27                                                                        

Key                 HelloWorldFunctionIamRole                                                                                                                                  
Description         Implicit IAM Role created for Hello World function                                                                                                         
Value               arn:aws:iam::000000000000:role/cf-my-stack-HelloWorldFunctionRole                                                                                          
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

あっさり作成されました。これで、LocalStack上にデプロイされています。

Amazon API Gatewayのエンドポイント、AWS LambdaのARN、IAMロールのARNが得られます。

awslocalでも、作成されたAmazon API GatewayREST APIのidを確認。

$ awslocal apigateway get-rest-apis

REST APIのidは環境変数に保存しておきましょう。

$ REST_API_ID=.....

ステージは、Prod以外にもStageがあるようです。

$ awslocal apigateway get-stages --rest-api-id $REST_API_ID
{
    "item": [
        {
            "deploymentId": ".....",
            "stageName": "Stage",
            "description": "",
            "cacheClusterEnabled": false,
            "methodSettings": {},
            "variables": {}
        },
        {
            "deploymentId": ".....",
            "stageName": "Prod",
            "description": "",
            "cacheClusterEnabled": false,
            "methodSettings": {},
            "variables": {},
            "tags": {}
        }
    ]
}

LocalStack上にデプロイされたLambda関数を、Amazon API Gateway越しに呼び出してみます。

$ curl http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/hello
{"message": "hello world"}

LocalStackで動作させた時のAmazon API GatewayのURLのルールは、こちらに記載があります。

LocalStack / Invoking API Gateway

使えそうですね。あっさり確認OKです。

動作確認というと、sam local invokeをしてみては?という話もあるのですが、これをやるんだったら素のAWS SAMを
そのまま使えばよいのではと思うので書きません。
※動かしはしました…というか、動かした瞬間に「そのまま使うのと同じでは」と思いました…

aws-sam-cli-localの仕組み

最初に書いた通り、aws-sam-cli-localAWS SAMのラッパーです。

aws-sam-cli-localPythonスクリプト内で、エンドポイントをLocalStack向けに設定してsamコマンドを
呼び出しているものです。

https://github.com/localstack/aws-sam-cli-local/blob/64a41c2c8e46a584c6fb4b13abfc8bb62e136d48/bin/samlocal

ハマったところ

実は、ひとつだけハマったところがありまして。

sam buildした後に、sam deploy --guidedをサジェストされるのですが。

こちらに素直に従うと

$ samlocal deploy --guided --region us-east-1

S3バケット名が解決できなくてエラーになります。

Error: Unable to upload artifact HelloWorldFunction referenced by CodeUri parameter of HelloWorldFunction resource.
Parameter validation failed:
Invalid bucket name "!Ref SamCliSourceBucket": Bucket name must match the regex "^[a-zA-Z0-9.\-_]{1,255}$" or be an ARN matching the regex "^arn:(aws).*:(s3|s3-object-lambda):[a-z\-0-9]*:[0-9]{12}:accesspoint[/:][a-zA-Z0-9\-.]{1,63}$|^arn:(aws).*:s3-outposts:[a-z\-0-9]+:[0-9]{12}:outpost[/:][a-zA-Z0-9\-]{1,63}[/:]accesspoint[/:][a-zA-Z0-9\-]{1,63}$"

これを回避したくて、先にS3バケットを作成したのでした…。