CLOVER🍀

That was when it all began.

MySQL 8.0.21以降で、レプリケーションの用語が変わっていっている(Master → Source、Slave → Replica)という話

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

世の中、"master"や"slave"という単語が違う表現に置き換えられていっていますが、そういえばMySQLの
レプリケーションでもこのあたりの単語を使っていましたがどうなったのかな?と思って見てみたら。

ちょっとずつ変わっていっているみたいなので、置き換えられた単語でレプリケーションを構成してみることに
しました。

あと、レプリケーションでの通信を非SSL/TLSで行う場合の認証でもちょっとハマったので、そちらもメモとして。

単語の変化

MySQL 8.0.21でドキュメントが、8.0.22以降で実際に使うシンタックスが変化していっているみたいです。

方針的には、"master"が"source"へ、"slave"が"replica"へ。
あとは"whitelist"が"allowlist"へ、"blacklist"が"blocklist"へ、という感じみたいですね。

In the documentation for MySQL 8.0.21, we have started changing the term “master” to “source”, the term “slave” to “replica”, the term “whitelist” to “allowlist”, and the term “blacklist” to “blocklist”. There are currently no changes to the product's syntax, so these terms are still present in the documentation where the current code requires their use. See the blog post MySQL Terminology Updates for more information.

MySQL :: MySQL 8.0 Release Notes :: Changes in MySQL 8.0.21 (2020-07-13, General Availability)

From MySQL 8.0.22, the statements START SLAVE, STOP SLAVE, SHOW SLAVE STATUS, SHOW SLAVE HOSTS and RESET SLAVE are deprecated. The following aliases should be used instead:

MySQL :: MySQL 8.0 Release Notes :: Changes in MySQL 8.0.22 (2020-10-19, General Availability)

From MySQL 8.0.23, the statement CHANGE MASTER TO is deprecated. The alias CHANGE REPLICATION SOURCE TO should be used instead. The parameters for the statement also have aliases that replace the term MASTER with the term SOURCE.

MySQL :: MySQL 8.0 Release Notes :: Changes in MySQL 8.0.23 (2021-01-18, General Availability)

Incompatible Change: From MySQL 8.0.26, new aliases or replacement names are provided for most remaining identifiers that contain the terms “master”, which is changed to “source”;

MySQL :: MySQL 8.0 Release Notes :: Changes in MySQL 8.0.26 (2021-07-20, General Availability)

レプリケーションに関するドキュメント

レプリケーションに関するドキュメントは、このあたりを参照します。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 17.1.3.4 GTID を使用したレプリケーションのセットアップ

MySQL :: MySQL 8.0 リファレンスマニュアル :: 13.4.1 ソースサーバーを制御する SQL ステートメント

MySQL :: MySQL 8.0 リファレンスマニュアル :: 13.4.2 レプリケーションサーバーを制御するための SQL ステートメント

GTIDを使ったレプリケーションで組むことにします。

環境

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

MySQL。

mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.27    |
+-----------+
1 row in set (0.00 sec)

MySQLサーバーは2台用意し、以下の役割とします。

  • Source … 172.17.0.2
  • Replica … 172.17.0.3

Sourceは以前で言うMaster、Replicaは以前で言うSlaveです。

Sourceのセットアップ

では、まずはSource側のセットアップから。

レプリケーションまわりで、必要な設定はこのあたり。

## gtid
gtid_mode = on
enforce_gtid_consistency = on

## binary log
log-bin = mysql-bin
sync_binlog = 1
binlog_expire_logs_seconds = 864000
binlog_checksum=NONE

## replication
server_id = 1

レプリケーション用のユーザーを作成。

mysql> create user repl@'%' identified by 'password';
Query OK, 0 rows affected (0.04 sec)

mysql> grant replication slave on *.* to 'repl'@'%';
Query OK, 0 rows affected (0.02 sec)

grant文に関しては、"slave"という単語がまだ残っているようです("replica"には置き換えられませんでした)。

GRANT ステートメント / MySQL によってサポートされる権限

これで、Sourceのセットアップは終わりです。

まあ、ここまでは単語の変化は見えませんね。

ちなみに、show master statusについても単語は変わっていないみたいです。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 13.7.7.23 SHOW MASTER STATUS ステートメント

mysql> show master status\G
*************************** 1. row ***************************
             File: mysql-bin.000002
         Position: 660
     Binlog_Do_DB: 
 Binlog_Ignore_DB: 
Executed_Gtid_Set: c39bd721-35b2-11ec-9fc4-0242ac110002:1-2
1 row in set (0.00 sec)

Replicaのセットアップ

続いて、Replicaを設定します。

MySQLの設定は、こちら。Sourceとはserver_idが異なり、それからread_onlyがonになっています。

## gtid
gtid_mode = on
enforce_gtid_consistency = on

## binary log
log-bin = mysql-bin
sync_binlog = 1
binlog_expire_logs_seconds = 864000
binlog_checksum=NONE

## replication
server_id = 2
read_only = on

レプリケーションでの通信にSSL/TLSを使う場合、change replication source to文を使ってSourceの情報を設定する際に、
source_sslを1にします。

mysql> change replication source to source_host = '172.17.0.2', source_port = 3306, source_ssl = 1, source_auto_position = 1;
Query OK, 0 rows affected (0.23 sec)

レプリケーションでの通信をSSL/TLS化しない場合は、Sourceに対して先ほど作成したレプリケーション用のユーザーで
接続し、--get-server-public-keyオプションを使ってRSA公開鍵を取得します。
※もしくは、レプリケーションの通信をSSL/TLSにします

$ mysql --ssl-mode=DISABLED -urepl -p -h172.17.0.2 --get-server-public-key
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 8.0.27 MySQL Community Server - GPL

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

接続したら、特になにか行う必要はないので切断します。

mysql> exit
Bye

そして、change replication source to文でSourceの情報を設定します。

mysql> change replication source to source_host = '172.17.0.2', source_port = 3306, source_auto_position = 1;
Query OK, 0 rows affected (0.13 sec)

SSL/TLSを有効、無効のいずれかの方法でchange replication source to文を実行したら、start replica文で
レプリケーションを開始します。

mysql> start replica user = 'repl' password = 'password';
Query OK, 0 rows affected (0.04 sec)

レプリケーションのステータス確認。

mysql> show replica status\G
*************************** 1. row ***************************
             Replica_IO_State: Waiting for source to send event
                  Source_Host: 172.17.0.2
                  Source_User: repl
                  Source_Port: 3306
                Connect_Retry: 60
              Source_Log_File: mysql-bin.000002
          Read_Source_Log_Pos: 660
               Relay_Log_File: 6502f56a3e70-relay-bin.000002
                Relay_Log_Pos: 867
        Relay_Source_Log_File: mysql-bin.000002
           Replica_IO_Running: Yes
          Replica_SQL_Running: Yes
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Source_Log_Pos: 660
              Relay_Log_Space: 1075
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Source_SSL_Allowed: Yes
           Source_SSL_CA_File: 
           Source_SSL_CA_Path: 
              Source_SSL_Cert: 
            Source_SSL_Cipher: 
               Source_SSL_Key: 
        Seconds_Behind_Source: 0
Source_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 0
               Last_SQL_Error: 
  Replicate_Ignore_Server_Ids: 
             Source_Server_Id: 1
                  Source_UUID: c39bd721-35b2-11ec-9fc4-0242ac110002
             Source_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
    Replica_SQL_Running_State: Replica has read all relay log; waiting for more updates
           Source_Retry_Count: 86400
                  Source_Bind: 
      Last_IO_Error_Timestamp: 
     Last_SQL_Error_Timestamp: 
               Source_SSL_Crl: 
           Source_SSL_Crlpath: 
           Retrieved_Gtid_Set: c39bd721-35b2-11ec-9fc4-0242ac110002:1-2
            Executed_Gtid_Set: c39bd721-35b2-11ec-9fc4-0242ac110002:1-2
                Auto_Position: 1
         Replicate_Rewrite_DB: 
                 Channel_Name: 
           Source_TLS_Version: 
       Source_public_key_path: 
        Get_Source_public_key: 0
            Network_Namespace: 
1 row in set (0.01 sec)

確認

最後に、レプリケーションが動作していることを確認します。

Sourceに接続して、データベースとユーザーを作成。

mysql> create database example;
Query OK, 1 row affected (0.03 sec)

mysql> create user kazuhira@localhost identified by 'password';
Query OK, 0 rows affected (0.03 sec)

mysql> create user kazuhira@'%' identified by 'password';
Query OK, 0 rows affected (0.02 sec)

mysql> grant all privileges on example.* to 'kazuhira'@localhost;
Query OK, 0 rows affected, 1 warning (0.02 sec)

Warning (Code 1285): MySQL is started in --skip-name-resolve mode; you must restart it without this switch for this grant to work
mysql> grant all privileges on example.* to 'kazuhira'@'%';
Query OK, 0 rows affected (0.02 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.01 sec)

作成したユーザーで、Sourceにログインしてテーブル作成。

$ mysql -ukazuhira -p example
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 8.0.27 MySQL Community Server - GPL

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> create table t(c int);
Query OK, 0 rows affected (0.11 sec)

Replicaで確認してみます。

$ mysql -ukazuhira -p example
Enter password: 
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 15
Server version: 8.0.27 MySQL Community Server - GPL

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show tables;
+-------------------+
| Tables_in_example |
+-------------------+
| t                 |
+-------------------+
1 row in set (0.00 sec)

OKですね。

Replica側は、Read Onlyです。

mysql> create table t2(c2 int);
ERROR 1290 (HY000): The MySQL server is running with the --read-only option so it cannot execute this statement

これで、確認できました、と。

RSA公開鍵の取得について

Replicaの構築している中で、RSA公開鍵の取得がありました。

これを飛ばすと、以下のようなエラーを見ることになります。非SSL/TLS接続での時に発生するみたいです。

2021-10-25T14:10:38.866725Z 15 [ERROR] [MY-010584] [Repl] Slave I/O for channel '': error connecting to master 'repl@172.17.0.2:3306' - retry-time: 60 retries: 1 message: Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection. Error_code: MY-002061

これを回避するには、RSA公開鍵をサーバーから取得するか、1度サーバーにログインしてキャッシュを作成する
必要があるみたいです。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 6.4.1.2 SHA-2 プラガブル認証のキャッシュ

日々の覚書: MySQL 8.0のcaching_sha2_password + 非SSL接続が転ける

8.0の最初からこんな感じでしたっけ…?

今回、RSA公開鍵の取得をしない場合にsource_sslを1にする方法も書きましたが、これは通信の暗号化のみを
意味します。

レプリケーション接続用の SOURCE_SSL=1 | MASTER_SSL=1 を設定し、それ以上の SOURCE_SSL_xxx | MASTER_SSL_xxx オプションを設定しないことは、暗号化接続のコマンドオプション で説明されているように、クライアント用の --ssl-mode=REQUIRED の設定に対応します。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 17.3.1 暗号化接続を使用するためのレプリケーションの設定

--ssl-mode=VERIFY_CAや--ssl-mode=VERIFY_IDENTITY相当を行うにはもう少し設定が必要みたいですが、
そちらは今回はパスです…。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 6.3.1 暗号化接続を使用するための MySQL の構成

Spring Cloud Function AWS AdapterとLocalStackを使って、Amazon API Gateway+AWS Lambdaを構成してみる

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

前にSpring Cloud Function AWS Adapterを使って、AWS Lambda関数をLocalStackにデプロイしてみました。

Spring Cloud Function AWS AdapterでAWS Lambda関数を作成して、LocalStackにデプロイしてみる - CLOVER🍀

Spring Cloud Function AWS Adapterには、Amazon API Gatewayと組み合わせるための機能もあるみたいなので、
こちらを試してみることにしました。

SpringBootApiGatewayRequestHandler

Spring Cloud Function AWS Adapterのドキュメントには、プラットフォーム固有の機能に関する記述があります。

Spring Cloud Function Reference Documentation / AWS Adapter / Platform Specific Features

Amazon API Gatewayとのプロキシ統合も、そのひとつです。
org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandlerというクラスが
提供されています。

Spring Cloud Function Reference Documentation / AWS Adapter / HTTP and API Gateway

なんですけど、よく見るとdeprecatedなんですけどね。

https://github.com/spring-cloud/spring-cloud-function/blob/v3.1.4/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootApiGatewayRequestHandler.java

まあ、今回はドキュメントに載っている情報をそのまま使うとしましょう。

環境

今回の環境は、こちら。

LocalStack。

$ localstack --version
0.12.19.1

起動。

$ LAMBDA_EXECUTOR=docker-reuse localstack start

アプリケーションを作成する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-89-generic", arch: "amd64", family: "unix"

Spring Cloud Functionを使ったプロジェクトそのものは、JARファイルを作ることしかしないので、LocalStack側に
Amazon API Gateway等の構築が必要です。

これには、Terraformを使うことにします。

$ terraform version
Terraform v1.0.9
on linux_amd64

合わせて、AWS Lambda関数のデプロイもTerraformで行います。

プロジェクトを作成する

まずは、Spring Cloud Functionを使うプロジェクトを作成します。

$ curl -s https://start.spring.io/starter.tgz \
  -d dependencies=cloud-function,webflux \
  -d groupId=org.littlewings \
  -d artifactId=hello-spring-cloud-function-api-gateway-with-lambda \
  -d packageName=org.littlewings.spring.cloudfuntion.aws \
  -d bootVersion=2.5.6 \
  -d javaVersion=11 \
  -d type=maven-project \
  -d baseDir=hello-spring-cloud-function-api-gateway-with-lambda | tar zxvf -
$ cd hello-spring-cloud-function-api-gateway-with-lambda

含まれるソースコードは、いったん削除。

$ rm src/main/java/org/littlewings/spring/cloudfuntion/aws/DemoApplication.java src/test/java/org/littlewings/spring/cloudfuntion/aws/DemoApplicationTests.java

依存関係とMavenプラグインの設定を、このあたりの情報を見て調整します。

Spring Cloud Function Reference Documentation / AWS Adapter / Build file setup

Spring Cloud Function Reference Documentation / AWS Adapter / Notes on JAR Layout

https://github.com/spring-cloud/spring-cloud-function/blob/v3.1.4/spring-cloud-function-samples/function-sample-aws/pom.xml

こんな感じになりました。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.littlewings</groupId>
    <artifactId>hello-spring-cloud-function-api-gateway-with-lambda</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>11</java.version>
        <spring-cloud.version>2020.0.4</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-function-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-function-adapter-aws</artifactId>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-events</artifactId>
            <version>3.10.0</version>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.2.1</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <dependencies>
                    <dependency>
                        <groupId>org.springframework.boot.experimental</groupId>
                        <artifactId>spring-boot-thin-layout</artifactId>
                        <version>1.0.27.RELEASE</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <configuration>
                    <createDependencyReducedPom>false</createDependencyReducedPom>
                    <shadedArtifactAttached>true</shadedArtifactAttached>
                    <shadedClassifierName>aws</shadedClassifierName>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

ポイントは、この依存関係角追加と

     <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-function-adapter-aws</artifactId>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-events</artifactId>
            <version>3.10.0</version>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.2.1</version>
            <scope>provided</scope>
        </dependency>

Mavenプラグインの設定調整、追加ですね。

     <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <dependencies>
                    <dependency>
                        <groupId>org.springframework.boot.experimental</groupId>
                        <artifactId>spring-boot-thin-layout</artifactId>
                        <version>1.0.27.RELEASE</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <configuration>
                    <createDependencyReducedPom>false</createDependencyReducedPom>
                    <shadedArtifactAttached>true</shadedArtifactAttached>
                    <shadedClassifierName>aws</shadedClassifierName>
                </configuration>
            </plugin>
        </plugins>

AWS Lambda関数を作成して、LocalStackにデプロイする

次に、AWS Lambda関数に相当するクラスを作成します。

src/main/java/org/littlewings/spring/cloudfuntion/aws/App.java

package org.littlewings.spring.cloudfuntion.aws;

import java.util.Map;
import java.util.function.Function;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import reactor.core.publisher.Mono;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }

    // @Bean
    // public Function<Mono<APIGatewayProxyRequestEvent>, Mono<APIGatewayProxyResponseEvent> upper(ObjectMapper mapper) {

    @Bean
    public Function<Mono<Map<String, Object>>, Mono<Map<String, Object>>> upper() {
        return request ->
                request.map(r -> Map.of("result", r.get("message").toString().toUpperCase()));
    }
}

関数は、AWS Lambdaとして単純にデプロイした時と同様、messageキーに対応するメッセージを大文字にして
resultとして返すだけにしています。

あと、こちらを見てちょっと勘違いしていたのですが

Spring Cloud Function Reference Documentation / AWS Adapter / HTTP and API Gateway

コメントアウトしていますが、作成する関数自体のリクエスト、レスポンスはAPIGatewayProxyRequestEventや
APIGatewayProxyResponseEventである必要はないみたいです。

    // @Bean
    // public Function<Mono<APIGatewayProxyRequestEvent>, Mono<APIGatewayProxyResponseEvent> upper(ObjectMapper mapper) {

むしろ、そう書いていたらうまくいかなかったです…。

パッケージング。

$ mvn package

-aws.jarとなっているJARファイルをデプロイします。

$ ll -h target
合計 21M
drwxrwxr-x 8 xxxxx xxxxx 4.0K 10月 25 01:25 ./
drwxr-xr-x 7 xxxxx xxxxx 4.0K 10月 25 01:25 ../
drwxrwxr-x 3 xxxxx xxxxx 4.0K 10月 25 01:25 classes/
drwxrwxr-x 3 xxxxx xxxxx 4.0K 10月 25 01:25 generated-sources/
drwxrwxr-x 3 xxxxx xxxxx 4.0K 10月 25 01:25 generated-test-sources/
-rw-rw-r-- 1 xxxxx xxxxx  21M 10月 25 01:25 hello-spring-cloud-function-api-gateway-with-lambda-0.0.1-SNAPSHOT-aws.jar
-rw-rw-r-- 1 xxxxx xxxxx  12K 10月 25 01:25 hello-spring-cloud-function-api-gateway-with-lambda-0.0.1-SNAPSHOT.jar
-rw-rw-r-- 1 xxxxx xxxxx 4.2K 10月 25 01:25 hello-spring-cloud-function-api-gateway-with-lambda-0.0.1-SNAPSHOT.jar.original
drwxrwxr-x 2 xxxxx xxxxx 4.0K 10月 25 01:25 maven-archiver/
drwxrwxr-x 3 xxxxx xxxxx 4.0K 10月 25 01:25 maven-status/
drwxrwxr-x 2 xxxxx xxxxx 4.0K 10月 25 01:25 test-classes/

デプロイおよびAmazon API Gatewayの構築などは、Terraformで行います。

terraform/main.tfというファイルに各種リソースの定義を行いますが、こちらは最後にまた載せます。

概要としては、Amazon API GatewayでREST APIとして作成し、AWS Lambda関数とプロキシ統合します。
path-partは、リクエストされた内容をすべてAWS Lambda関数に転送するように設定します。

前に書いた、こちらの内容をほぼ使う感じですね。

Terraformで、LocalStackにAmazon API Gateway+AWS Lambdaの環境を構成してみる - CLOVER🍀

注意点としては、タイムアウト(timeout)を伸ばしておかないと、起動しきれずにタイムアウトしてしまいます。

resource "aws_lambda_function" "lambda" {
  filename      = "../target/hello-spring-cloud-function-api-gateway-with-lambda-0.0.1-SNAPSHOT-aws.jar"
  function_name = "hello_spring_cloud_function_api_gateway_with_lambda"
  role          = aws_iam_role.lambda_role.arn
  handler       = "org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler"
  runtime       = "java11"
  timeout       = 30
  memory_size   = 1024
}

では、initしてapply。

$ terraform init
$ terraform apply -auto-approve

REST APIのIDを取得して

$ REST_API_ID=`awslocal apigateway get-rest-apis --query 'items[0].id' --output text`

動作確認。

$ curl -XPOST -H 'Content-Type: application/json' http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/upper -d '{"message": "Hello World"}'
{"result":"HELLO WORLD"}

OKですね。パスについては、どのリクエストも同じAWS Lambda関数に転送するようになっているので、
なにを指定しても大丈夫です。

これで、確認したいことは動作確認できました、と。

まとめ

Spring Cloud Function AWS Adapterを使って、AWS Lambda関数とAmazon API Gatewayのプロキシ統合を構成して
みました。

今回は全リクエストを単一のAWS Lambda関数に転送しているのですが、これをSpring Web MVCのような
ルーティングをするのは、いろいろ調べましたがちょっとできなさそうでした。
※HTTPヘッダーなどでのルーティングはできそうですが

他の手段もありそうなので、そのうち試してみるとしましょう。

オマケ

省略しておいた、Terraformのリソース定義全体を載せておきます。

terraform/main.tf

terraform {
  required_version = "1.0.9"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.63.0"
    }
  }
}

provider "aws" {
  access_key                  = "mock_access_key"
  region                      = "us-east-1"
  s3_force_path_style         = true
  secret_key                  = "mock_secret_key"
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  endpoints {
    apigateway     = "http://localhost:4566"
    cloudformation = "http://localhost:4566"
    cloudwatch     = "http://localhost:4566"
    dynamodb       = "http://localhost:4566"
    es             = "http://localhost:4566"
    firehose       = "http://localhost:4566"
    iam            = "http://localhost:4566"
    kinesis        = "http://localhost:4566"
    lambda         = "http://localhost:4566"
    route53        = "http://localhost:4566"
    redshift       = "http://localhost:4566"
    s3             = "http://localhost:4566"
    secretsmanager = "http://localhost:4566"
    ses            = "http://localhost:4566"
    sns            = "http://localhost:4566"
    sqs            = "http://localhost:4566"
    ssm            = "http://localhost:4566"
    stepfunctions  = "http://localhost:4566"
    sts            = "http://localhost:4566"
  }
}

## API Gateway
resource "aws_api_gateway_rest_api" "rest_api" {
  name = "my-rest-api"
}

resource "aws_api_gateway_resource" "proxy_resource" {
  rest_api_id = aws_api_gateway_rest_api.rest_api.id
  parent_id   = aws_api_gateway_rest_api.rest_api.root_resource_id
  path_part   = "{proxy+}"
}

resource "aws_api_gateway_method" "method" {
  rest_api_id   = aws_api_gateway_rest_api.rest_api.id
  authorization = "NONE"
  http_method   = "ANY"
  resource_id   = aws_api_gateway_resource.proxy_resource.id
}

resource "aws_api_gateway_integration" "integration" {
  rest_api_id             = aws_api_gateway_rest_api.rest_api.id
  resource_id             = aws_api_gateway_resource.proxy_resource.id
  http_method             = aws_api_gateway_method.method.http_method
  integration_http_method = "ANY"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.lambda.invoke_arn
}

resource "aws_api_gateway_deployment" "deployment" {
  rest_api_id = aws_api_gateway_rest_api.rest_api.id

  triggers = {
    redeployment = sha1(jsonencode([
      aws_api_gateway_rest_api.rest_api.body,
      aws_api_gateway_resource.proxy_resource.id,
      aws_api_gateway_method.method.id,
      aws_api_gateway_integration.integration.id,
    ]))
  }

  lifecycle {
    create_before_destroy = true
  }

  depends_on = [aws_api_gateway_integration.integration]
}

resource "aws_api_gateway_stage" "stage" {
  deployment_id = aws_api_gateway_deployment.deployment.id
  rest_api_id   = aws_api_gateway_rest_api.rest_api.id
  stage_name    = "Prod"
}

## Lambda
data "aws_caller_identity" "current" {}

data "aws_region" "current" {}

resource "aws_lambda_permission" "lambda" {
  statement_id  = "AllowExecutionFromApiGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.lambda.function_name
  principal     = "apigateway.amazonaws.com"

  source_arn = "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.rest_api.id}/*/${aws_api_gateway_method.method.http_method}${aws_api_gateway_resource.proxy_resource.path}"
}

resource "aws_lambda_function" "lambda" {
  filename      = "../target/hello-spring-cloud-function-api-gateway-with-lambda-0.0.1-SNAPSHOT-aws.jar"
  function_name = "hello_spring_cloud_function_api_gateway_with_lambda"
  role          = aws_iam_role.lambda_role.arn
  handler       = "org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler"
  runtime       = "java11"
  timeout       = 30
  memory_size   = 1024
}

# IAM
resource "aws_iam_role" "lambda_role" {
  name               = "MyLambdaRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}