Spring Boot+Spring SessionでスケーラブルなステートフルWebアプリが簡単につくれるよ〜

今回は、Spring BootとSpring Sessionを使用してスケーラブルなステートフルWebアプリ（HTTPセッションを使うWebアプリ）を作ってみるぞ〜 :punch:

# システム構成のイメージ

今回は・・・

* Webサーバー兼ロードバランサーとして[Nginx](https://nginx.org/)
* アプリケーションサーバーとして[Spring Boot](http://projects.spring.io/spring-boot/)(Embedded Tomcat)
* セッションストアとしてKVS(Key Value Store)の有名どころである[Redis](http://redis.io/)

を使用し、Nginx、Spring Boot(Embedded Tomcat)、Redisをそれぞれ[Docker](https://www.docker.com/)コンテナ上で動かします。
なお、今回はRedisは１台構成にさせてもらいます。実際のシステムを１台構成で動かすことはあり得ませんが・・・ :sweat: （Master/Slave構成によるクラスタ化は次回の宿題ということで・・・:sweat_smile:）

![spring-session-demo.png](https://qiita-image-store.s3.amazonaws.com/0/117313/f56724e5-184c-a46b-c31a-9a487dcfa55f.png)

# フレームワーク構成

Spring Bootで扱うセッション情報を透過的にRedisに保存するために、[Spring Session](http://projects.spring.io/spring-session/)のRedis実装を使います。ちなみに・・・Spring SessionのRedis実装は、[Spring Data Redis](http://projects.spring.io/spring-data-redis/) + [Jedis](https://github.com/xetorthio/jedis)を使ってRedisの操作を行う仕組みになっています。

![spring-session-demo-fw.png](https://qiita-image-store.s3.amazonaws.com/0/117313/bfa22bfa-95bb-8cf3-7e00-fc2873b2272f.png)

> **Note:**
>
> 今回はセッションストアとしてRedisを使用しますが、Spring Sessionは「[Hazelcast](https://hazelcast.com/)実装」「[MongoDB](https://www.mongodb.com/jp)実装」「[Pivotal GemFire](https://pivotal.io/jp/big-data/pivotal-gemfire)実装」「JDBC実装」「`ConcurrentHashMap`実装(テスト用)」も提供しています。

# 作るアプリ

作るアプリは・・・ショボいです（ってかアプリとは呼べないけど）・・・:sweat_smile: なお、この投稿で説明する内容の完成品は、[GitHub](https://github.com/kazuki43zoo/qiita-materials/tree/master/spring-boot/spring-session-demo)で公開しています。

## Helloエンドポイント

http://localhost:10080/greeting/hello?name=kazuki43zoo にアクセスしたタイミングで、セッションに「名前(?name=xxx)」「セッション作成時間」「セッション作成インスタンス名」を格納します。

* 初回アクセス → AP1で処理(Echo by ap1) ※セッション生成

<img width="840" alt="spring-session-endpoint-hello-1.png" src="https://qiita-image-store.s3.amazonaws.com/0/117313/c2e1bcb5-aeb4-ee44-a74e-b6519eb35055.png">

* ２回目のアクセス → AP2で処理(Echo by ap2) ※Redisからセッション復元

<img width="840" alt="spring-session-endpoint-hello-2.png" src="https://qiita-image-store.s3.amazonaws.com/0/117313/a0cab3ec-f6dc-9da7-f27c-5910fef59b99.png">

* 3回目のアクセス → AP3で処理(Echo by ap3) ※Redisからセッション復元

<img width="840" alt="spring-session-endpoint-hello-3.png" src="https://qiita-image-store.s3.amazonaws.com/0/117313/bd2e107a-b102-2533-7364-fd3c098691cf.png">

* 4回目のアクセス → 再びAP1で処理(Echo by ap1) ※Redisからセッション復元

<img width="840" alt="spring-session-endpoint-hello-4.png" src="https://qiita-image-store.s3.amazonaws.com/0/117313/7957027b-8ecf-ada1-6a9e-a728270d7572.png">

## Goodbyeエンドポイント

http://localhost:10080/greeting/goodbye にアクセスするとセッションを破棄（`HttpSession#invalid`）します。

<img width="840" alt="spring-session-endpoint-goodbye.png" src="https://qiita-image-store.s3.amazonaws.com/0/117313/1867cb14-2cd0-07e8-e835-f916e646384c.png">

再度Helloエンドポイントにアクセスすると・・・別のセッションが作成されます。（セッション作成時間がかわります）

<img width="840" alt="spring-session-endpoint-hello-5.png" src="https://qiita-image-store.s3.amazonaws.com/0/117313/6adb7613-bbeb-23ff-7d40-a43602f36bd4.png">


# 動作確認環境

* Docker for Mac 1.12.0-a(Build: 11213)
* Nginx 1.11.5
* Redis 3.2.5
* Spring Boot 1.4.2.RELEASE (Embedded Tomcat 8.5.6)
* Spring Session 1.2.2.RELEAE
* Spring Data Redis 1.7.5.RELEASE
* Jedis 2.8.2

# 各種プロダクトのインストール

## Docker

以下のページやインターネットに転がっている情報をもとにインストールしてください！！

* https://docs.docker.com/docker-for-mac/
* https://docs.docker.com/docker-for-windows/

## Medis

Redisのクライアントツールをインストールしましょう！！ 私は[Medis](https://github.com/luin/medis)というツールを使いましたが、なんでもOKです。コマンドLoveな方は標準でインストールされる「redis-cli」でいいでしょう。

# プロジェクトの作成と起動

[SPRING INITIALIZR](https://start.spring.io/)を使ってプロジェクトを作成しましょう。Dependenciesには、「Web」「Session」「Redis」を選びます。先ほど紹介システム構成で動かす前に、作成したプロジェクトをMavenコマンドを使って動かしてみたいと思います。

## Redisの起動

まず、Docker上でRedisを起動します。

```
$ docker run -d --name redis -p 6379:6379 redis
```

## Spring Bootアプリの起動

次に、Spring BootアプリをMavenコマンドを使って起動します。

```
$ ./mvnw clean spring-boot:run
...
2016-11-13 23:53:11.945  INFO 80852 --- [           main] o.s.c.support.DefaultLifecycleProcessor  : Starting beans in phase 2147483647
2016-11-13 23:53:11.952  INFO 80852 --- [           main] s.a.ScheduledAnnotationBeanPostProcessor : No TaskScheduler/ScheduledExecutorService bean found for scheduled processing
2016-11-13 23:53:11.994  INFO 80852 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-11-13 23:53:11.999  INFO 80852 --- [           main] c.example.SpringSessionDemoApplication   : Started SpringSessionDemoApplication in 2.659 seconds (JVM running for 6.016)
```

起動確認ができたら、Ctrl + C でSpring Bootを停止しましょう。

## Redisの停止と削除

動作確認用に作成したRedisも削除しておきましょう。

```
$ docker rm -f redis
redis
```


# Spring Bootアプリの作成

とりあえずMavneでの起動確認ができたので、いよいよアプリを作っていきましょう！！

## セッションストアの指定

Spring Sessionのセッションストアを指定しましょう。Mavenでの起動確認した時に、実は以下のようなWARNログが出ていることに気がつきましたか？ WARNなので設定がなくても動くみたいなのですが、WARNログが出ているのは気持ち悪いので使うセッションストア(今回は`redis`)を指定します。

```
2016-11-13 23:53:11.319  WARN 80852 --- [ost-startStop-1] o.s.b.a.s.RedisSessionConfiguration      : Spring Session store type is mandatory: set 'spring.session.store-type=redis' in your configuration
```

```properties:src/main/resources/application.properties
spring.session.store-type=redis
```

> **Note:**
>
> 2017/1/8 追記
> ちなみに・・・Spring Boot 1.5から`store-type`を指定しないとエラーになるので、今から明示的に指定するようにしておきましょう！！

## Redisの接続先の指定

Dockerのリンク機能を使うと、他のコンテナで起動したプロセスの「IPアドレス」と「ポート番号」が環境変数に設定されます。Spring BootからRedisに接続する際は、Dockerのリンク機能を使い、環境変数に設定されたIPアドレスとポート番号を使用して接続するようにします。

```properties:src/main/resources/application.properties
spring.redis.host=${REDIS_PORT_6379_TCP_ADDR:localhost}
spring.redis.port=${REDIS_PORT_6379_TCP_PORT:6379}
```

> **Note:**
>
> IPアドレスは「(コンテナ名)\_PORT\_(コンテナ内で起動したポート番号)\_TCP\_ADDR」に、ポート番号は「(コンテナ名)\_PORT\_(コンテナ内で起動したポート番号)\_TCP_PORT」に設定されます。 例えばRedisのコンテナ名が「redis」の場合は、それぞれ「REDIS_PORT_6379_TCP_ADDR」と「REDIS_PORT_6379_TCP_PORT」という環境変数に値が設定されます。

## テスト時のセッションストアの指定

今回はテストは作りませんが、テストをスキップする設定を加えずにjarファイルを作成する(`$ ./mvnw package`する)と、Redisが起動していないとエラーになってしまいます。単体テストを行う時にRedisが必要じゃない場合は、セッションストアをインメモリ（`ConcurrentHashMap`）実装に切り替えておくのがよいでしょう。

```properties:src/test/resources/application.properties
spring.session.store-type=hash_map
```

## Redisに格納するセッション情報のシリアライズ方法の変更

今回は、Redisに格納するセッション情報をJSON形式のデータにシリアライズするようにしたいと思います。なお、デフォルトだとJava標準のシリアライズの仕組み(`java.io.Serializable`)が使われます。

```java
package com.example;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;

@Configuration
public class HttpSessionConfig {
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}
```

> **Note:**
>
> Java標準の仕組み(java.io.Serializable)は実データに対してシリアライズ後のデータサイズの増加率が多い＋パフォーマンスにも難点があり、別のソリューションが必要になるケースも少なくないようです。この問題を解決するソリューションのひとつとして使われるのが、セッション情報をJSONデータに変換して共有する方法になります。なお、最近リリースされたSpring Security 4.2では、Spring Securityが提供するクラスをJSONデータに変換するためのコンポーネントが提供されています。詳しくは・・・「[Spring Security 4.2 主な変更点](http://qiita.com/kazuki43zoo/items/ef6cc6d05d68a8cb7a0e)」をごらんください！！



## セッションスコープのBeanの作成

今回は、セッションスコープのBeanを作成してセッションに情報を格納してみます。

```java
package com.example;

import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;

import java.io.Serializable;
import java.util.Date;

@SessionScope
@Component
public class GreetingInfo implements Serializable {

    private static final long serialVersionUID = 8048097948251750715L;
    private String name;
    private Date createdAt;
    private String createdBy;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }

    public String getCreatedBy() {
        return createdBy;
    }

    public void setCreatedBy(String createdBy) {
        this.createdBy = createdBy;
    }

}
```

## エンドポイントの作成

HelloエンドポイントとGoodbyeエンドポイントを作成します。

```java
package com.example;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;
import java.util.Date;
import java.util.Optional;

@RestController
@RequestMapping("/greeting")
public class GreetingRestController {
    private final String instanceName;
    private final GreetingInfo greetingInfo;

    public GreetingRestController(@Value("${instance.name:ap}") String instanceName, GreetingInfo greetingInfo) {
        this.instanceName = instanceName;
        this.greetingInfo = greetingInfo;
    }

    @GetMapping("/hello")
    public String hello(@RequestParam Optional<String> name) {
        if (greetingInfo.getName() == null) {
            greetingInfo.setName(name.orElse("anonymous"));
            greetingInfo.setCreatedAt(new Date());
            greetingInfo.setCreatedBy(instanceName);
        }
        return "Hi " + greetingInfo.getName() + ". Enter at " + greetingInfo.getCreatedAt() + " by " + greetingInfo.getCreatedBy() + " (Echo by " + instanceName + ")";
    }

    @GetMapping("/goodbye")
    public String goodbye(HttpSession session) {
        Optional<String> name = Optional.ofNullable(greetingInfo.getName());
        session.invalidate();
        return "Goodbye " + name.orElse("anonymous") + " (Echo by " + instanceName + ")";
    }

}
```

# Dockerfileの作成

NginxとSpring Boot用のDockerfileを作成しましょう。なお、RedisはDocker Hubにある[Redisのイメージ](https://hub.docker.com/_/redis/)をそのまま使います。


## Nginx用のconfファイルとDockerfileの作成

NginxからSpring Boot(ap1, ap2, ap3)へのプロキシ（ロードバランス）設定を行います。

```text:nginx/default.conf
upstream spring-boot {
    server ap1:8080;
    server ap2:8080;
    server ap3:8080;
}

server {
    listen       80;
    server_name  localhost;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location / {
        proxy_pass http://spring-boot/;
        proxy_http_version 1.1;
    }

    #location / {
    #    root   /usr/share/nginx/html;
    #    index  index.html index.htm;
    #}


    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}
```

Docker Hubにある[Nginxのイメージ](https://hub.docker.com/_/nginx/)からコンテナを作成し、さきほど作成した`default.conf`を上書きします。

```text:nginx/Dockerfile
FROM nginx
COPY default.conf /etc/nginx/conf.d/default.conf
```

## Spring Boot用のDockerfileの作成

Docker Hubにある[Javaのイメージ](https://hub.docker.com/_/java/)からコンテナを作成し、Mavenビルドで作成したSpring Bootのjarファイルをコピーします。


> **Note:**
>
> Mavenビルドは、DockerfileからDockerイメージを作成する前に行っておく必要があります。


```text:Dockerfile
FROM java:8
ADD target/spring-session-demo-0.0.1-SNAPSHOT.jar /opt/spring/spring-session-demo.jar
EXPOSE 8080
WORKDIR /opt/spring/
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "spring-session-demo.jar"]
```

# Docker Composeのシステム構成ファイル(docker-compose.yml)を作成

ここまでは、Dockerイメージを作成するためのDockerfileを作成しました。ここからは、冒頭で紹介したシステム構成にするための方法を説明していきます。Dockerコマンドを使ってシステム構成に合うようにコンテナを一つずつ起動する方法もありますが・・・依存関係のあるコンテナとのリンク設定などぶっちゃけ面倒です。そういった手動で行うと面倒な設定を構成ファイル(`docker-compose.yml`)に予め記載しておくことで、起動・停止などの操作を簡単にできるようにしたのがDocker Composeです。

では、今回のシステム構成に合うようにDocker Composeのシステム構成ファイル(`docker-compose.yml`)の作成してみましょう。

```yaml:docker-compose.yml
redis:
  image: redis
  ports:
    - "16379:6379"
ap1:
  build: .
  links:
    - redis
  command: >
    --instance.name=ap1
ap2:
  build: .
  links:
    - redis
  command: >
    --instance.name=ap2
ap3:
  build: .
  links:
    - redis
  command: >
    --instance.name=ap3
web:
  build: nginx
  ports:
    - "10080:80"
  links:
    - ap1
    - ap2
    - ap3
```

## Redisの設定

Docker Hubからredisのイメージを取得してコンテナを作成する。今回は、Redisのポートをホストマシンの16379のポートにバインドします。これは、Medisを使ってRedisの中身を確認するためです。

```yaml:docker-compose.yml
redis:
  image: redis
  ports:
    - "16379:6379"
# ...
```

## Spring Bootの設定

Spring Boot用のDockerfileをビルドして作成したイメージからコンテナを作成する。Spring BootからRedisに接続するため、コンテナ間のリンク連携を指定します。今回は、ap1, ap2, ap3という名前で3つのコンテナを作成しています。

```yaml
# ...
ap1:
  build: .
  links:
    - redis
  command: >
    --instance.name=ap1
ap2:
  build: .
  links:
    - redis
  command: >
    --instance.name=ap2
ap3:
  build: .
  links:
    - redis
  command: >
    --instance.name=ap3
# ...
```

> **Note:**
>
> 以下のように記載すると、Spring Bootを起動するjavaコマンドのコマンドライン引数になります。
>
> ```yaml
> # ...
>   command: >
>     --instance.name=ap1
> # ...
> ```
>
> これは、以下のjavaコメンドを実行した時と同じ動作になります。
>
> ```text
> $ java -jar spring-session-demo.jar --instance.name=ap1
> ```

## Nginxの設定

Nginx用のDockerfileをビルドして作成したイメージからコンテナを作成する。NginxからSpring Bootに接続するため、コンテナ間のリンク連携を指定します。今回は、ap1, ap2, ap3という3つのコンテナと連携します。Nginxのコンテナ上では、リンク連携で指定したコンテナ名をホスト名として扱うことができます（Nginx側のhostsファイルに、Spring Bootのコンテナに割り当てられたIPアドレスとコンテナ名がマッピングされる）。

```yaml
# ...
web:
  build: nginx
  ports:
    - "10080:80"
  links:
    - ap1
    - ap2
    - ap3
```

本投稿では、Nginxのポートをホストマシンの10080のポートにバインドします。80はどこかで使っているかな〜と思って10080にしていますが、80をホストマシンで使っていない＋80の方がいい！！という場合は、以下のようにすればOKです。

```yaml
# ...
web:
  build: nginx
  ports:
    - "80:80"
# ...
```


# Spring Bootのビルド

Dockerコンテナを作成する準備が終わったので、Spring Bootアプリをビルドしてjarファイルを作成しましょう。

```text
$ ./mvnw clean package
...
2016-11-14 01:46:50.548  INFO 82756 --- [       Thread-3] o.s.w.c.s.GenericWebApplicationContext   : Closing org.springframework.web.context.support.GenericWebApplicationContext@792b749c: startup date [Mon Nov 14 01:46:48 JST 2016]; root of context hierarchy

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] 
[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ spring-session-demo ---
[INFO] Building jar: /Users/shimizukazuki/git/qiita-materials/spring-boot/spring-session-demo/target/spring-session-demo-0.0.1-SNAPSHOT.jar
[INFO] 
[INFO] --- spring-boot-maven-plugin:1.4.2.RELEASE:repackage (default) @ spring-session-demo ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6.029 s
[INFO] Finished at: 2016-11-14T01:46:51+09:00
[INFO] Final Memory: 31M/319M
[INFO] ------------------------------------------------------------------------
```

# Docker Composeを使用したコンテナ生成

Docker Composeのコマンドを使用して、システム構成ファイル(docker-compose.yml)に指定したコンテナを生成・起動します。以下のコマンドを実行すると、イメージの作成も行います。(`--build`をはずせば、イメージのビルドはせずコンテナの起動だけ行うことができます)

```
$ docker-compose up --build
Building ap1
Step 1 : FROM java:8
 ---> 69a777edb6dc
Step 2 : ADD target/spring-session-demo-0.0.1-SNAPSHOT.jar /opt/spring/spring-session-demo.jar
 ---> Using cache
 ---> ea38347bb634
Step 3 : EXPOSE 8080
 ---> Using cache
 ---> dffd5bc2ce66
Step 4 : WORKDIR /opt/spring/
 ---> Using cache
 ---> 09bfcc4f71a1
Step 5 : ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -jar spring-session-demo.jar
 ---> Using cache
 ---> d3b897e3cf1b
Successfully built d3b897e3cf1b
Building ap3
Step 1 : FROM java:8
 ---> 69a777edb6dc
Step 2 : ADD target/spring-session-demo-0.0.1-SNAPSHOT.jar /opt/spring/spring-session-demo.jar
 ---> Using cache
 ---> ea38347bb634
Step 3 : EXPOSE 8080
 ---> Using cache
 ---> dffd5bc2ce66
Step 4 : WORKDIR /opt/spring/
 ---> Using cache
 ---> 09bfcc4f71a1
Step 5 : ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -jar spring-session-demo.jar
 ---> Using cache
 ---> d3b897e3cf1b
Successfully built d3b897e3cf1b
Building ap2
Step 1 : FROM java:8
 ---> 69a777edb6dc
Step 2 : ADD target/spring-session-demo-0.0.1-SNAPSHOT.jar /opt/spring/spring-session-demo.jar
 ---> Using cache
 ---> ea38347bb634
Step 3 : EXPOSE 8080
 ---> Using cache
 ---> dffd5bc2ce66
Step 4 : WORKDIR /opt/spring/
 ---> Using cache
 ---> 09bfcc4f71a1
Step 5 : ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -jar spring-session-demo.jar
 ---> Using cache
 ---> d3b897e3cf1b
Successfully built d3b897e3cf1b
Building web
Step 1 : FROM nginx
 ---> 05a60462f8ba
Step 2 : COPY default.conf /etc/nginx/conf.d/default.conf
 ---> Using cache
 ---> c71d9085c2a5
Successfully built c71d9085c2a5
Starting springsessiondemo_redis_1
Starting springsessiondemo_ap1_1
Starting springsessiondemo_ap3_1
Starting springsessiondemo_ap2_1
Starting springsessiondemo_web_1
Attaching to springsessiondemo_redis_1, springsessiondemo_ap2_1, springsessiondemo_ap1_1, springsessiondemo_ap3_1, springsessiondemo_web_1
redis_1  | 1:C 13 Nov 16:58:26.964 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
redis_1  |                 _._                                                  
redis_1  |            _.-``__ ''-._                                             
redis_1  |       _.-``    `.  `_.  ''-._           Redis 3.2.5 (00000000/0) 64 bit
redis_1  |   .-`` .-```.  ```\/    _.,_ ''-._                                   
redis_1  |  (    '      ,       .-`  | `,    )     Running in standalone mode
redis_1  |  |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
redis_1  |  |    `-._   `._    /     _.-'    |     PID: 1
redis_1  |   `-._    `-._  `-./  _.-'    _.-'                                   
redis_1  |  |`-._`-._    `-.__.-'    _.-'_.-'|                                  
redis_1  |  |    `-._`-._        _.-'_.-'    |           http://redis.io        
redis_1  |   `-._    `-._`-.__.-'_.-'    _.-'                                   
redis_1  |  |`-._`-._    `-.__.-'    _.-'_.-'|                                  
redis_1  |  |    `-._`-._        _.-'_.-'    |                                  
redis_1  |   `-._    `-._`-.__.-'_.-'    _.-'                                   
redis_1  |       `-._    `-.__.-'    _.-'                                       
redis_1  |           `-._        _.-'                                           
redis_1  |               `-.__.-'                                               
redis_1  | 
redis_1  | 1:M 13 Nov 16:58:26.965 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
redis_1  | 1:M 13 Nov 16:58:26.966 # Server started, Redis version 3.2.5
redis_1  | 1:M 13 Nov 16:58:26.966 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
redis_1  | 1:M 13 Nov 16:58:26.966 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
redis_1  | 1:M 13 Nov 16:58:26.966 * DB loaded from disk: 0.000 seconds
redis_1  | 1:M 13 Nov 16:58:26.966 * The server is now ready to accept connections on port 6379
ap2_1    | 
ap2_1    |   .   ____          _            __ _ _
ap2_1    |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
ap2_1    | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
ap2_1    |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
ap2_1    |   '  |____| .__|_| |_|_| |_\__, | / / / /
ap2_1    |  =========|_|==============|___/=/_/_/_/
ap2_1    |  :: Spring Boot ::        (v1.4.2.RELEASE)
ap2_1    | 
ap1_1    | 
ap1_1    |   .   ____          _            __ _ _
ap1_1    |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
ap1_1    | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
ap1_1    |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
ap1_1    |   '  |____| .__|_| |_|_| |_\__, | / / / /
ap1_1    |  =========|_|==============|___/=/_/_/_/
ap1_1    |  :: Spring Boot ::        (v1.4.2.RELEASE)
ap1_1    | 
ap3_1    | 
ap3_1    |   .   ____          _            __ _ _
ap3_1    |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
ap3_1    | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
ap3_1    |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
ap3_1    |   '  |____| .__|_| |_|_| |_\__, | / / / /
ap3_1    |  =========|_|==============|___/=/_/_/_/
ap3_1    |  :: Spring Boot ::        (v1.4.2.RELEASE)
ap3_1    | 
ap2_1    | 2016-11-13 16:58:29.631  INFO 1 --- [           main] c.example.SpringSessionDemoApplication   : Starting SpringSessionDemoApplication v0.0.1-SNAPSHOT on 20c483ea8628 with PID 1 (/opt/spring/spring-session-demo.jar started by root in /opt/spring)
ap2_1    | 2016-11-13 16:58:29.647  INFO 1 --- [           main] c.example.SpringSessionDemoApplication   : No active profile set, falling back to default profiles: default
ap2_1    | 2016-11-13 16:58:29.812  INFO 1 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@736e9adb: startup date [Sun Nov 13 16:58:29 UTC 2016]; root of context hierarchy
ap3_1    | 2016-11-13 16:58:29.791  INFO 1 --- [           main] c.example.SpringSessionDemoApplication   : Starting SpringSessionDemoApplication v0.0.1-SNAPSHOT on 29000e858ceb with PID 1 (/opt/spring/spring-session-demo.jar started by root in /opt/spring)
ap3_1    | 2016-11-13 16:58:29.915  INFO 1 --- [           main] c.example.SpringSessionDemoApplication   : No active profile set, falling back to default profiles: default
ap1_1    | 2016-11-13 16:58:29.949  INFO 1 --- [           main] c.example.SpringSessionDemoApplication   : Starting SpringSessionDemoApplication v0.0.1-SNAPSHOT on 8ea568d485e7 with PID 1 (/opt/spring/spring-session-demo.jar started by root in /opt/spring)
ap1_1    | 2016-11-13 16:58:30.016  INFO 1 --- [           main] c.example.SpringSessionDemoApplication   : No active profile set, falling back to default profiles: default
ap3_1    | 2016-11-13 16:58:30.151  INFO 1 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@736e9adb: startup date [Sun Nov 13 16:58:30 UTC 2016]; root of context hierarchy
ap1_1    | 2016-11-13 16:58:30.315  INFO 1 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@736e9adb: startup date [Sun Nov 13 16:58:30 UTC 2016]; root of context hierarchy
ap1_1    | 2016-11-13 16:58:33.533  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode!
ap2_1    | 2016-11-13 16:58:33.563  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode!
ap3_1    | 2016-11-13 16:58:33.642  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode!
ap1_1    | 2016-11-13 16:58:35.221  INFO 1 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
ap1_1    | 2016-11-13 16:58:35.277  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
ap1_1    | 2016-11-13 16:58:35.281  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.6
ap2_1    | 2016-11-13 16:58:35.314  INFO 1 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
ap2_1    | 2016-11-13 16:58:35.343  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
ap2_1    | 2016-11-13 16:58:35.346  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.6
ap2_1    | 2016-11-13 16:58:35.591  INFO 1 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
ap2_1    | 2016-11-13 16:58:35.591  INFO 1 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 5786 ms
ap3_1    | 2016-11-13 16:58:35.586  INFO 1 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
ap3_1    | 2016-11-13 16:58:35.617  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
ap3_1    | 2016-11-13 16:58:35.619  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.6
ap1_1    | 2016-11-13 16:58:35.746  INFO 1 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
ap1_1    | 2016-11-13 16:58:35.751  INFO 1 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 5435 ms
ap3_1    | 2016-11-13 16:58:35.928  INFO 1 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
ap3_1    | 2016-11-13 16:58:35.933  INFO 1 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 5782 ms
ap2_1    | 2016-11-13 16:58:36.455  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
ap2_1    | 2016-11-13 16:58:36.463  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
ap2_1    | 2016-11-13 16:58:36.464  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'springSessionRepositoryFilter' to: [/*]
ap2_1    | 2016-11-13 16:58:36.464  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
ap2_1    | 2016-11-13 16:58:36.465  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
ap2_1    | 2016-11-13 16:58:36.465  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
ap1_1    | 2016-11-13 16:58:36.615  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
ap1_1    | 2016-11-13 16:58:36.622  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
ap1_1    | 2016-11-13 16:58:36.634  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'springSessionRepositoryFilter' to: [/*]
ap1_1    | 2016-11-13 16:58:36.634  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
ap1_1    | 2016-11-13 16:58:36.634  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
ap1_1    | 2016-11-13 16:58:36.635  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
ap3_1    | 2016-11-13 16:58:36.928  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
ap3_1    | 2016-11-13 16:58:36.937  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
ap3_1    | 2016-11-13 16:58:36.939  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'springSessionRepositoryFilter' to: [/*]
ap3_1    | 2016-11-13 16:58:36.940  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
ap3_1    | 2016-11-13 16:58:36.940  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
ap3_1    | 2016-11-13 16:58:36.940  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
ap1_1    | 2016-11-13 16:58:37.169  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@736e9adb: startup date [Sun Nov 13 16:58:30 UTC 2016]; root of context hierarchy
ap1_1    | 2016-11-13 16:58:37.330  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting/hello],methods=[GET]}" onto public java.lang.String com.example.GreetingRestController.hello(java.util.Optional<java.lang.String>) throws java.net.UnknownHostException
ap1_1    | 2016-11-13 16:58:37.332  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting/goodbye],methods=[GET]}" onto public java.lang.String com.example.GreetingRestController.goodbye(javax.servlet.http.HttpSession) throws java.net.UnknownHostException
ap1_1    | 2016-11-13 16:58:37.340  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
ap1_1    | 2016-11-13 16:58:37.342  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
ap2_1    | 2016-11-13 16:58:37.387  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@736e9adb: startup date [Sun Nov 13 16:58:29 UTC 2016]; root of context hierarchy
ap1_1    | 2016-11-13 16:58:37.430  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
ap1_1    | 2016-11-13 16:58:37.430  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
ap1_1    | 2016-11-13 16:58:37.494  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
ap2_1    | 2016-11-13 16:58:37.687  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting/goodbye],methods=[GET]}" onto public java.lang.String com.example.GreetingRestController.goodbye(javax.servlet.http.HttpSession) throws java.net.UnknownHostException
ap2_1    | 2016-11-13 16:58:37.695  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting/hello],methods=[GET]}" onto public java.lang.String com.example.GreetingRestController.hello(java.util.Optional<java.lang.String>) throws java.net.UnknownHostException
ap2_1    | 2016-11-13 16:58:37.710  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
ap2_1    | 2016-11-13 16:58:37.714  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
ap3_1    | 2016-11-13 16:58:37.787  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@736e9adb: startup date [Sun Nov 13 16:58:30 UTC 2016]; root of context hierarchy
ap2_1    | 2016-11-13 16:58:37.818  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
ap2_1    | 2016-11-13 16:58:37.818  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
ap2_1    | 2016-11-13 16:58:37.956  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
ap3_1    | 2016-11-13 16:58:38.017  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting/hello],methods=[GET]}" onto public java.lang.String com.example.GreetingRestController.hello(java.util.Optional<java.lang.String>) throws java.net.UnknownHostException
ap3_1    | 2016-11-13 16:58:38.019  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting/goodbye],methods=[GET]}" onto public java.lang.String com.example.GreetingRestController.goodbye(javax.servlet.http.HttpSession) throws java.net.UnknownHostException
ap3_1    | 2016-11-13 16:58:38.029  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
ap3_1    | 2016-11-13 16:58:38.030  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
ap3_1    | 2016-11-13 16:58:38.123  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
ap3_1    | 2016-11-13 16:58:38.134  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
ap3_1    | 2016-11-13 16:58:38.248  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
ap1_1    | 2016-11-13 16:58:38.357  INFO 1 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
ap1_1    | 2016-11-13 16:58:38.388  INFO 1 --- [           main] o.s.c.support.DefaultLifecycleProcessor  : Starting beans in phase 2147483647
ap1_1    | 2016-11-13 16:58:38.417  INFO 1 --- [           main] s.a.ScheduledAnnotationBeanPostProcessor : No TaskScheduler/ScheduledExecutorService bean found for scheduled processing
ap1_1    | 2016-11-13 16:58:38.566  INFO 1 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
ap1_1    | 2016-11-13 16:58:38.583  INFO 1 --- [           main] c.example.SpringSessionDemoApplication   : Started SpringSessionDemoApplication in 9.943 seconds (JVM running for 10.97)
ap2_1    | 2016-11-13 16:58:38.919  INFO 1 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
ap2_1    | 2016-11-13 16:58:38.936  INFO 1 --- [           main] o.s.c.support.DefaultLifecycleProcessor  : Starting beans in phase 2147483647
ap2_1    | 2016-11-13 16:58:39.063  INFO 1 --- [           main] s.a.ScheduledAnnotationBeanPostProcessor : No TaskScheduler/ScheduledExecutorService bean found for scheduled processing
ap3_1    | 2016-11-13 16:58:39.109  INFO 1 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
ap3_1    | 2016-11-13 16:58:39.122  INFO 1 --- [           main] o.s.c.support.DefaultLifecycleProcessor  : Starting beans in phase 2147483647
ap3_1    | 2016-11-13 16:58:39.142  INFO 1 --- [           main] s.a.ScheduledAnnotationBeanPostProcessor : No TaskScheduler/ScheduledExecutorService bean found for scheduled processing
ap2_1    | 2016-11-13 16:58:39.250  INFO 1 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
ap3_1    | 2016-11-13 16:58:39.263  INFO 1 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
ap3_1    | 2016-11-13 16:58:39.285  INFO 1 --- [           main] c.example.SpringSessionDemoApplication   : Started SpringSessionDemoApplication in 10.671 seconds (JVM running for 11.669)
ap2_1    | 2016-11-13 16:58:39.291  INFO 1 --- [           main] c.example.SpringSessionDemoApplication   : Started SpringSessionDemoApplication in 10.883 seconds (JVM running for 11.864)
```

5コンテナ分のログが入り乱れているためやや見ずらいですが・・・redis, ap1, ap2, ap3, webが起動しました。念のためDockerのコマンドを使ってコンテナの状態を確認してみましょう。以下のような状態になっていればOKです。

```
$ docker ps
CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS                            NAMES
5442dff2d1ab        springsessiondemo_web   "nginx -g 'daemon off"   9 minutes ago       Up 16 seconds       443/tcp, 0.0.0.0:10080->80/tcp   springsessiondemo_web_1
8ea568d485e7        springsessiondemo_ap1   "java -Djava.security"   9 minutes ago       Up 17 seconds       8080/tcp                         springsessiondemo_ap1_1
20c483ea8628        springsessiondemo_ap2   "java -Djava.security"   9 minutes ago       Up 17 seconds       8080/tcp                         springsessiondemo_ap2_1
29000e858ceb        springsessiondemo_ap3   "java -Djava.security"   9 minutes ago       Up 17 seconds       8080/tcp                         springsessiondemo_ap3_1
29f2beae104c        redis                   "docker-entrypoint.sh"   13 hours ago        Up 17 seconds       0.0.0.0:16379->6379/tcp          springsessiondemo_redis_1
```

# Redisの状態確認

アプリにアクセスする前の、Medisを使ってRedisの状態を確認してみます。ダウンロードしたアプリを実行し、Portに「16379」を入力して「Connect」しましょう。

<img width="1072" alt="spring-session-medis-login.png" src="https://qiita-image-store.s3.amazonaws.com/0/117313/36a809ff-1205-580a-fd72-aa3debc85a75.png">

とりあえず、何も格納されていません。

<img width="1072" alt="spring-session-medis-first.png" src="https://qiita-image-store.s3.amazonaws.com/0/117313/efa13b1c-f434-ecad-904f-71d972939a88.png">

# Helloエンドポイントへアクセス

では、実際にHelloエンドポイントへアクセスしてみましょう。

<img width="840" alt="spring-session-endpoint-hello-try-1.png" src="https://qiita-image-store.s3.amazonaws.com/0/117313/4ada862d-2d46-1c48-addc-cf2dcc1c9179.png">

Redisの状態も確認してみましょう。（Medisのレフトメニューの中にある「更新マーク」をクリックしてください）

<img width="1391" alt="spring-session-medis-try-1.png" src="https://qiita-image-store.s3.amazonaws.com/0/117313/46dc30b8-11d0-fe10-7d6f-d1c24b6710f4.png">

Typeが「HASH」になっているデータが、セッション情報を保持しているエントリーです（本例だと「spring:session:sessions:a99324e3-0073-4966-9a57-6d63202ceeea」です）。セッション情報には、「作成日時(creationTime)」「最終アクセス日時(lastAccessedTime)」「無効化までの最大時間(maxInactiveInterval)」「セッション属性(sessionAttr:セッション属性のkey名)」の項目を保持しており、今回はセッションスコープのBeanを使用しているため、セッション属性のKey名は「scopedTarget.Bean名」になっています。

```json:セッション属性値
{
	"@class": "com.example.GreetingInfo",
	"name": "kazuki43zoo",
	"createdAt": [
		"java.util.Date",
		1479057288861
	],
	"createdBy": "ap1"
}
```

# Goodbyeエンドポイントへアクセス

Goodbyeエンドポイントへアクセスしてセッションを破棄してみます。

<img width="840" alt="spring-session-endpoint-goodbye-try-2.png" src="https://qiita-image-store.s3.amazonaws.com/0/117313/27572f34-fec7-cf20-2fee-95685e49ab53.png">

Redisの中を確認してみると・・・実際のセッション情報を保持しているエントリーが残っていますが・・・これはバグではありません。Spring Sessionは、セッション破棄とセッション有効期限切れのタイミングでイベントリスナーに対してイベントを発行する機能を持っており、イベントリスナーは破棄または有効期限切れになったセッション情報にアクセスすることができます。この機能をRedisの仕組みを使って実現しようとすると、セッション情報を保持するエントリーを物理的に削除するタイミングを遅らせる必要があるのです（すぐには消えませんが、一定間隔でパージされるので気にする必要はありません）。
なお、「無効化までの最大時間(maxInactiveInterval)」をみると「0」になっており、論理的にはセッション情報が無効化されていることがわかります。

<img width="1391" alt="spring-session-medis-try-2.png" src="https://qiita-image-store.s3.amazonaws.com/0/117313/17fe7e6f-234f-92e6-34ba-e36c253a5719.png">


# Dockerコンテナの停止

Ctrl + Cで停止できます。

```
...
^CGracefully stopping... (press Ctrl+C again to force)
Stopping springsessiondemo_web_1 ... done
Stopping springsessiondemo_ap1_1 ... done
Stopping springsessiondemo_ap2_1 ... done
Stopping springsessiondemo_ap3_1 ... done
Stopping springsessiondemo_redis_1 ... done
$
```

# まとめ

しょぼいアプリだったためあまり実感がわかないかもしれませんが・・・すごく簡単にスケーラブルなステートフルWebアプリが作れたと思います。Spring Sessionを使えば、アプリ（＋フレームワーク）の実装を一切変更することなく、簡単にセッションストアを切り替えることができてしまいます！！ ほんの数年前までは、オンプレミス環境で予め設計したスケール（繁忙期に耐えられるスケール＝通常時は過剰スペックでリソースはスカスカ・・・）でシステムを稼働させるスタイルが当たり前だった気もしますが、Cloud全盛の現在では、システムへのアクセス数に応じてスケールアウト・スケールインを自動で行ってくれるサービスが当たり前のように提供されています。そういった環境で動かすアプリケーションでは、HTTPセッションをRedisなどを使って高速に共有することが求められると思います。

ちなみに・・・今回はRedisを一台構成にしましたが、（冒頭でも触れたとおり・・）本来であればMaster/Slave構成でクラスタ化する必要があります。Spring Data Redisって、Redis Sentinelには対応しているみたいですが・・・・更新はMasterへ、参照はSlaveへアクセスという部分を透過的にやってくれる仕組みってあるのかな？（過去記事で「ないよ」というのは見かけたけど・・あると便利だよな〜）


# 参考サイト

* http://jsug-spring-boot-handson.readthedocs.io/en/latest/Docker.html
* http://docs.spring.io/spring-session/docs/1.2.2.RELEASE/reference/html5/