Memorystoreを使ってGAE/FEで動くSpringbootのセッションを管理する

## はじめに

[Memorystore](https://cloud.google.com/memorystore/) にGAE/FE(Java runtime)で動くSpringbootのセッションを管理させてみましたのでまとめます。

オープンソースのRedisとMemorystoreの差異は [Memorystoreリファレンスのこのへん](https://cloud.google.com/memorystore/docs/redis/redis-overview#differences_between_managed_and_open_source_redis) に記載されています。
はたしてすんなり動くのか？

## TL;DR

[Springのリファレンス](https://docs.spring.io/spring-session/docs/current/reference/html5/#api-redisoperationssessionrepository-sessiondestroyedevent) の通り下記の指定をすれば動きます。

```java
@Bean
public static ConfigureRedisAction configureRedisAction() {
	return ConfigureRedisAction.NO_OP;
}
```

Memorystoreでは [CONFIGコマンドがブロックされている](https://cloud.google.com/memorystore/docs/reference/redis-configs#blocked) ため、この対応が必要になるようです。
Springのリファレンスによると上記の結果 [`SessionDeletedEvent`](https://docs.spring.io/spring-session/docs/current/api/org/springframework/session/events/SessionDeletedEvent.html) と [`SessionExpiredEvent`](https://docs.spring.io/spring-session/docs/current/api/org/springframework/session/events/SessionExpiredEvent.html
) は取れなくなるとのこと。

上記以外の注意点として、MemorystoreとGAEは [同一プロジェクト、同一リージョン、同一ネットワークにないと接続できません](https://cloud.google.com/memorystore/docs/redis/networking?hl=ja)。

## 試してみる

上記を試してみます。

### 事前準備

まず下記が無ければ用意します。

- JDK
- Maven
- 適当なGCPプロジェクト
- [Cloud SDK](https://cloud.google.com/sdk/docs/overview?hl=ja#installing_cloud_sdk)
- [適当なSpringbootサンプルソース](https://github.com/tnagao3000/hello-springboot-with-session-on-gae)

Cloud SDKは [使用するアカウントとプロジェクトを指定してログイン](https://cloud.google.com/sdk/docs/initializing?hl=ja) しておきます。
また適当なSpringbootサンプルソースは[GitHubにあげました](https://github.com/tnagao3000/hello-springboot-with-session-on-gae)。


### Memorystoreを起動する

GCPのウェブUIのMemorystoreのところでインスタンスを作成します。


設定は初期値の通りでいいですが、使用するプロジェクトでもし過去にGAEをデプロイした
ことがある場合は、[GAEとMemorystoreのリージョンを合わせておく必要があります](https://cloud.google.com/memorystore/docs/redis/connect-redis-instance-flex)。

GAEのリージョンはウェブUIのダッシュボードの右上か [コマンドライン(describeのlocationId)](https://cloud.google.com/sdk/gcloud/reference/app/describe) で確認できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/26808/34cb15d2-1408-8674-8cae-69609016cd5e.png)

Memorystoreの起動が完了すると次の通りインスタンスに割り当てられたIPが確認できます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/26808/77539062-9522-f144-4168-c0b73dd3e359.png)


### 適当なSpringbootサンプルソースについて

適当なサンプルは[GitHubにあげました](https://github.com/tnagao3000/hello-springboot-with-session-on-gae)。
以下サンプルの補足です。

#### サンプル画面

次の通り訪問回数をセッションに保存して表示するだけの画面を作りました。

```java:HelloSpringbootController.java

@RestController
public class HelloSpringbootController {

	@RequestMapping({ "/" })
	public String index(HttpSession session) {

		Integer visits = (Integer) session.getAttribute("visits-by-session-key");
		visits = visits == null ? 1 : ++visits;
		session.setAttribute("visits-by-session-key", visits);

		return "visits by session => " + visits;
	}
}
```

画面を呼び出すと次のような感じ。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/26808/af8b4cc3-cec5-bfc1-da2e-bfa18f21fac0.png)

#### プロファイルの切替

ローカルと本番(GAE)のプロファイルの切替は app.yaml で環境変数 `SPRING_PROFILES_ACTIVE` を指定して行いました。
ローカルは指定無し、本番は'prod'を指定しています。

```yaml:appengine/app.yaml
runtime: java
env: flex

handlers:
  - url: /.*
    script: this field is required, but ignored

resources:
  cpu: 1
  memory_gb: 2

# Specify the active profile of spring for production environment by environment variable.
env_variables:
  SPRING_PROFILES_ACTIVE: 'prod'
```

なお本サンプルで切り替えているのは [application.yml](https://github.com/tnagao3000/hello-springboot-with-session-on-gae/blob/master/src/main/resources/application.yml) と [application-prod.yml](https://github.com/tnagao3000/hello-springboot-with-session-on-gae/blob/master/src/main/resources/application-prod.yml) だけです。

#### RedisのIPを指定する

先ほどのMemorystore起動時に割り当てられたIPを application-prod.yml で指定します。

```yaml:resources/application-prod.yml
# Setting for production environment
spring:
  redis:
    host: 10.0.0.3
    port: 6379
```

#### ConfigureRedisActionを指定する

次のような感じ。

```java:HelloSpringbootSessionConfig.java
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 60)
public class HelloSpringbootSessionConfig {

	@Bean
	public ConfigureRedisAction configureRedisAction() {
		return ConfigureRedisAction.NO_OP;
	}

	// イベントが飛んでくるかどうかを確認
	@EventListener
	public void confirmEvent(ApplicationEvent event) {
		System.out.println(LocalDateTime.now().toString() + " --> " + event.getClass().getSimpleName());
	}

}
```

#### 修正点まとめ

素のSpringbootアプリをGAE + Memorystoreに載せるのに必要な修正点をまとめると次の通り。

| 修正（または追加）対象                 |補足                                    | 
|:------------------------------------|:---------------------------------------|
| [pom.xml](https://github.com/tnagao3000/hello-springboot-with-session-on-gae/blob/master/pom.xml#L51) |[appengine-maven-plugin](https://cloud.google.com/appengine/docs/flexible/java/using-maven#adding_the_app_engine_maven_and_eclipse_jetty_maven_plugins)を追加             |
| [app.yaml](https://github.com/tnagao3000/hello-springboot-with-session-on-gae/blob/master/src/main/appengine/app.yaml)  |[GAEの設定](https://cloud.google.com/appengine/docs/flexible/java/configuring-your-app-with-app-yaml) |
| [HelloSpringbootSessionConfig.java](https://github.com/tnagao3000/hello-springboot-with-session-on-gae/blob/master/src/main/java/com/example/demo/HelloSpringbootSessionConfig.java#L20)   |ConfigureRedisAction.NO_OPを指定 |
| [application-prod.yml](https://github.com/tnagao3000/hello-springboot-with-session-on-gae/blob/master/src/main/resources/application-prod.yml) |MemorystoreのIPを指定 |


### GAEを初期化する

もし該当プロジェクトで過去にGAEを使用したことがない場合は、まず最初にGAEで使用するリージョンを指定します。
次の通りウェブUIで行うか、または [コマンドライン](https://cloud.google.com/sdk/gcloud/reference/app/create) で行います。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/26808/ac00b39e-d9be-3cf7-a7e1-266da3f84b86.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/26808/b0091b58-ec9c-d6b3-a054-5450b4ba7331.png)

ここではGAEのリージョンの指定まで出来ればよいです。
GAEのリージョンはMemorystoreと合わせておく必要があります。
またGAEのリージョンは一度設定すると変更できません。

### GAEにデプロイする

最後に [`> mvn appengine:deploy`](https://cloud.google.com/appengine/docs/flexible/java/maven-reference) でデプロイします。

```
> mvn appengine:deploy
[INFO] Scanning for projects...
[INFO]
[INFO] ----------< com.example:hello-springboot-with-session-on-gae >----------
[INFO] Building hello-springboot-with-session-on-gae 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] >>> appengine-maven-plugin:1.3.2:deploy (default-cli) > package @ hello-springboot-with-session-on-gae >>>
[INFO]
[INFO] --- maven-resources-plugin:3.0.2:resources (default-resources) @ hello-springboot-with-session-on-gae ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO] Copying 0 resource
(以下略)
```

以上でGAEにSpringbootアプリがデプロイされました。

### アプリにアクセスしてみる

GCPのウェブUIからリンクをたどるか、または [コマンドライン](https://cloud.google.com/sdk/gcloud/reference/app/browse) でブラウザを開きます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/26808/adefcbda-e547-829a-6268-8a6c0bff7695.png)

以上で動作が確認できます。

参考まで[イベントが飛んで来たらわかるようにしました](https://github.com/tnagao3000/hello-springboot-with-session-on-gae/blob/df3d09cd30d736c807832d48f00dd7cb952256fc/src/main/java/com/example/demo/HelloSpringbootSessionConfig.java#L25)。
ログを見るとリファレンス記載の通り `SessionDeletedEvent` と `SessionExpiredEvent` は飛んでないことが確認できます。

## いやオレはイベント使うんじゃ・・という場合

Memorystoreでは[CONFIGコマンドはブロック](https://cloud.google.com/memorystore/docs/reference/redis-configs#blocked)されていますが、[一部のconfig値はウェブUIほかで設定可能](https://cloud.google.com/memorystore/docs/reference/redis-configs#modifiable_configuration_parameters)です。
これをちょっと試してみました。

[Springのリファレンス](https://docs.spring.io/spring-session/docs/current/reference/html5/#api-redisoperationssessionrepository-sessiondestroyedevent)を元に次の通り**notify-keyspace-events**に**Egx** をセットして・・

![image.png](https://qiita-image-store.s3.amazonaws.com/0/26808/a6142fd2-1ef8-70c2-49d4-c7d627d078f3.png)

そしてブラウザでアクセス後にログを見てみると次のような感じ。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/26808/f8a76aac-2840-a32d-7c83-c5ae03789f0f.png)

※ 今回試した環境ではGAEインスタンスが２台あり、`SessionCreatedEvent` と `SessionExpiredEvent` は２台とも受け取っているようでした。
※ また`SessionDeletedEvent`はまったく見当たりませんでした。

以上です。