🦋

ExpressとRedisでセッションを実装してAWS Fargateにデプロイしよう-ローカル編

12 min read

はじめに

この記事では、こんなWebアプリケーションを作成します。

内容としては、以下のようなことを扱います。

  • Expressアプリケーションの作成
  • Dockerイメージの作成
  • Docker ComposeによるExpressとRedisの連携

予備知識

Expressとは?

Node.js(JavaScript)のWebアプリケーションフレームワークです。

Redisとは?

Express.jsなど多くのWebフレームワークでは、セッション情報を保存するためにインメモリのセッションストアを使用します。

なので、アプリケーションのアップデートや再起動のたびに、全てのユーザのセッションは破棄され、強制的にログアウトさせられます。

また、複数のコンテナ間でセッションを共有できません。

これを回避するため、Redisやmemcachedなど、外部のインメモリキャッシュシステムを使用するのが一般的です。

STEP 1 : Expressアプリケーションを作る

1-1 : Expressの雛形を作る

簡単にExpressアプリケーションの雛形を作れるexpress-generatorを使います。

プロジェクトを作成したいディレクトリに移動して、以下のコマンドを実行してください。

$ npm i -g express-generator
$ express express-redis-sample
$ cd express-redis-sample/
$ npm i

これだけで終わりです。

Expressアプリケーションを立ち上げるときは、以下のコマンドを実行します。

$ npm start

localhost:3000にアクセスして、以下のような画面が表示されることを確認してください。

npm startした後にブラウザに表示される画像

この時点でのファイル構造
$ tree -I node_modules
.
├── app.js
├── bin
│   └── www
├── package-lock.json
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── index.js
│   └── users.js
└── views
    ├── error.jade
    ├── index.jade
    └── layout.jade

STEP 2 : Docker Composeを使う

2-1 : Dockerfileを作る

開発環境用のDockerfileを作成します。

$ touch Dockerfile_dev

下の内容を書き込みます。

Dockerfile_dev
ARG VARIANT="14-buster"
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
WORKDIR /app
COPY . .
RUN npm install

2-2 : docker-compose.ymlを作る

docker-compose.ymlを作成します。

$ touch docker-compose.yml

下の内容を書き込みます。

version: '3'
services:
  express:
    build:
      context: .
      dockerfile: Dockerfile_dev
    volumes:
      - /app/node_modules
      - type: bind
        source: ./
        target: /app
    tty: true
    ports:
      - 3000:3000
    environment:
      - PORT=3000
      - SESSION_SECRET=hogehoge
      - REDIS_HOST=redis_db
      - REDIS_PORT=6379
    depends_on:
      - redis_db
  
  redis_db:
    image: redis:latest
    ports:
      - 6379:6379

簡単に説明すると、services直下のexpressが作成しているExpressアプリケーションの設定で、redis_dbがセッションデータを保存するRedisの設定です。

expressの中のenvironmentには、今後Expressアプリケーションで使用する環境変数を記述しています。

  • PORT : Expressアプリケーションを実行するポート
  • SESSION_SECRET : Cookieに保存するセッションIDの署名と暗号化に使用します。開発環境では適当でいいですが、本番環境では適度に長いランダムな文字列に、秘密にする必要があります。定期的に変更することが推奨されます。
  • REDIS_HOST : Redisに接続するときに使用するホスト名です。Docker Composeでは、このようにアプリケーション名をホスト名として使用することができます。
  • REDIS_PORT : Redisに接続するときに使用するポート番号です。Redisではデフォルトで6379番ポートを使用します。

2-3 : VSCode Remote Containersを使う

VSCode Remote Containersは、コンテナの中なのにローカルと同じように開発ができるツールです。

.devcontainer.jsonを作成します。

touch .devcontainer.json

下の内容を書き込みます。

{
	"name": "express-redis-example",
  "dockerComposeFile": ["docker-compose.yml"],
  "service": "express",
  "workspaceFolder": "/app",
	"remoteUser": "root",
	"postCreateCommand": ["node", "./bin/www"]
}

postCreateCommandは、コンテナの中に入ったときに実行されるコマンドになります。STEP 2-1で作ったDockerfileにCMDがなかったのは、ここでアプリケーションの実行を指定しているためです。

もしVSCodeのRemote Containers拡張をインストールしていない場合は、以下の拡張機能をインストールしてください。もちろん、Dockerもインストール済である必要があります。

https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers

あとは、Command+Shift+Pをおして、Reopen in Containerをクリックすると、コンテナが立ち上がります。(左下の緑色のところをクリックしてもできます。)

しばらくして、localhost:3000にアクセスして前と同じ画面が表示できたら、成功です。これで、コンテナ開発環境が整いました!

npm startした後にブラウザに表示される画像

この時点でのファイル構造
$ tree -I node_modules
.
├── Dockerfile_dev
├── docker-compose.yml
├── .devcontainer.json
├── app.js
├── bin
│   └── www
├── package-lock.json
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── index.js
│   └── users.js
└── views
    ├── error.jade
    ├── index.jade
    └── layout.jade

STEP 3 : セッションを実装する

3-1 : 必要なものをインストールする

以下のコマンドで、必要なnpmパッケージをインストールします。

$ npm i express express-session redis connect-redis

3-2 : Redisと接続する

app.jsを開いて、5行目くらいのところ(requireたくさんしてるところ)に以下のコードを追加します。

app.js
var session = require('express-session');
var RedisStore = require('connect-redis')(session)
var Redis = require('ioredis');
var redisClient = new Redis(process.env.REDIS_PORT, process.env.REDIS_HOST);

すると、16行目くらいにvar app = express();っていう行があると思いますが、その下に以下のコードを追加します。

app.js
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: true,
  rolling: true,
  cookie: {
    maxAge: 60000,
  },
  store: new RedisStore({
    client: redisClient,
    prefix: 'sid:',
  }),
}))

これも簡単に説明すると、最初の方は、必要なパッケージをrequireしてるだけです。redisClientのところがキモで、ここでRedisと接続しています。AWSにデプロイしたあとに、Redisのホスト名と環境変数が違ったりすると、ここでエラーがでます。

下のapp.useのコードは、アプリケーションでセッションを使用するように設定しています。これによって、今後出てくるreqというオブジェクトにsessionが追加されるようになります。それぞれの設定項目も簡単にご紹介します。

  • secret : セッションの署名や暗号化に使用されるシークレットです。ここでは、Docker Composeで指定した環境変数を読み込んでいます。
  • resave : アクセスのたびにセッションストアにデータを保存し直すかどうかです。基本的にfalseを指定しておけば問題ないとドキュメントにも書いてあります。
  • saveUninitialized : セッションが形成されてないアクセスがあったときに、セッションストアを初期化するかどうかみたいなやつです。容量の圧迫にもなるので、基本的にfalseで問題ないとドキュメントに書いてあります。
  • rolling : これは分かりやすくするためにtrueにしてありますが、本番環境ではfalseの方がいい可能性があります。trueだとセッションの有効期限がセッション作成日時からになりますが、falseだと最終アクセス日時からになります。アプリケーション完成後に変えてみると分かりやすいです。
  • cookie.maxAge : Cookieの有効期限です。ただ、Redisの有効期限にもこの値が使用されます。ミリ秒なので、この例だと1分でセッションが失効してしまうことになります。
  • store : ここに使用するセッションストアを指定します。これがないと、デフォルトでメモリ内にセッション情報は保存されます。その場合、npm startするたびにセッションが失効します。
  • prefix : redisのキーにつけるプレフィックスです。一つのRedisを複数のアプリケーションで使用することもできるので、その場合にキーが重複しないようにするためです。

詳しいオプションを知りたい方は、express-session公式ドキュメントをご参照ください。

3-3 : アプリケーション構成を考える

こんな感じでホワイトボードに書いてみました。

ログインと言いつつ、ユーザ名をセッションに保存するだけなので、パスワードとかはありません。

これを実装していきましょう。

3-4 : ファイルをつくる

JavaScriptファイルを3つ、Jadeファイルを1つ作成します。

$ touch routes/login.js routes/save-session.js routes/delete-session.js views/login.jade

users.jsは使わないので、削除します。

$ rm routes/users.js

ここから、以下6つのファイルの中身を書き換えます。

  • routes/index.js
  • routes/login.js
  • routes/save-session.js
  • routes/delete-session.js
  • views/login.jade
  • views/login.jade

routes/index.js

routes/index.js
var express = require('express');
var router = express.Router();

router.get('/', function(req, res, next) {
  if (req.session.userName) {
    res.render('index', {
      userName: req.session.userName,
      sessionId: req.session.id,
      maxAge: req.session.cookie.maxAge,
    })
  } else {
    res.redirect('login');
  }
});

module.exports = router;

routes/login.js

routes/login.js
var express = require('express');
var router = express.Router();

router.get('/', function(req, res, next) {
  if (req.session.userName) {
    res.redirect('/');
    return;
  }

  res.render('login');
});

module.exports = router;

routes/save-session.js

save-session.js
var express = require('express');
var router = express.Router();

router.get('/', function(req, res, next) {
  if (req.query.userName) {
    req.session.userName = req.query.userName
  }
  res.redirect('/');
});

module.exports = router;

routes/delete-session.js

delete-session.js
var express = require('express');
var router = express.Router();

router.get('/', function(req, res, next) {
  delete req.session.userName;
  res.redirect('/');
});

module.exports = router;

views/index.jade

views/index.jade
extends layout

block content
  h1 Hello, #{userName}!
  p SESSION ID : #{sessionId}
  p Expire on after #{maxAge/1000}s
  a(href="/delete-session") Logout

views/login.jade

views/login.jade
extends layout

block content
  h1 Login

  form(action="/save-session")
    input(name="userName")
    input(type="submit")

やってることは説明するまでもないくらいシンプルです。login.jadeからsave-session.jsには、普通にform要素を使ってGETでユーザ名を渡しています。

app.js

作ったファイルを読み込ませる必要があるので、app.jsも書き換えます。

下のように、requireしてるところと、app.useしてるところの2箇所を変更します。

app.js
 var indexRouter = require('./routes/index');
-var usersRouter = require('./routes/users');
+var loginRouter = require('./routes/login');
+var deleteSessionRouter = require('./routes/delete-session');
+var saveSessionRouter = require('./routes/save-session');
 
 
 app.use('/', indexRouter);
-app.use('/users', usersRouter);
+app.use('/login', loginRouter);
+app.use('/delete-session', deleteSessionRouter);
+app.use('/save-session', saveSessionRouter);

3-5 : 確認

再度、STEP 2と同じようにRemote Containerを実行して、localhost:3000にアクセスできるかを確認します。

初期状態ではセッションが保存されていないので、/loginにリダイレクトされます。

/loginの画像

任意のユーザ名を入力して送信を押すと、/save-session?userName=入力したユーザ名にリダイレクトされ、セッションにユーザ名が保存されて、/に再度リダイレクトされます。

/の画像

すると、ユーザ名と、Cookieに保存されたセッションID、セッションが保存されてからの秒数が表示されます。

この秒数が何度アクセスしてもずっと減っていくのは、STEP 3-2でrolling: trueと設定したからです。rolling: falseに設定すると、アクセスするたびに延長されます。

この時点でのファイル構造
$ tree -I node_modules
.
├── Dockerfile_dev
├── app.js
├── bin
│   └── www
├── docker-compose.yml
├── package-lock.json
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── delete-session.js
│   ├── index.js
│   ├── login.js
│   └── save-session.js
└── views
    ├── error.jade
    ├── index.jade
    ├── layout.jade
    └── login.jade

STEP 4 : 本番用Dockerfileを作成する

4-1 : Dockerfileを作成する

Dockerfileを作成します。

$ touch Dockerfile

以下の内容を書き込みます。

Dockerfile
FROM node:latest
WORKDIR /app
COPY . .
RUN npm install
ENTRYPOINT ["node", "./bin/www"]

4-2 : 実行用スクリプトを作成する

簡単に実行できるように、イメージ作成用のdocker-build.shと、イメージ実行用のdocker-run.shを作成します。

$ touch docker-build.sh docker-run.sh

以下の内容を書き込みます。

  • docker-build.sh
docker-build.sh
docker build -t express-redis-example .
  • docker-run.sh
docker-run.sh
docker run -it -d \
-e REDIS_HOST=host.docker.internal \
-e PORT=3000 \
-e SESSION_SECRET=hogehoge \
-e REDIS_PORT=6379 \
-p 3000:3000 \
express-redis-example-image bash

REDIS_HOSTに指定したhost.docker.internalというのは、コンテナの外のlocalhostのことです。

4-3 : Redisを実行する

Docker Composeを使用せずに動かすので、普通にローカルでRedisを動かしましょう。

Homebrewを使っています。他の方法でも構いません。

$ brew install redis
$ brew services start redis
$ redis-cli
Could not connect to Redis at 127.0.0.1:6379: Connection refused
not connected>

4-4 : 本番用Dockerを実行する

ここからは、Remote Container外で実行してください。

$ bash docker-build.sh
$ bash docker-run.sh

そして、STEP 3と同じくlocalhost:3000でアクセスできたら成功です!

この時点でのファイル構造
$ tree -I node_modules
.
├── Dockerfile
├── Dockerfile_dev
├── app.js
├── bin
│   └── www
├── docker-build.sh
├── docker-compose.yml
├── docker-run.sh
├── package-lock.json
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── delete-session.js
│   ├── index.js
│   ├── login.js
│   └── save-session.js
└── views
    ├── error.jade
    ├── index.jade
    ├── layout.jade
    └── login.jade

次回、この作成したイメージをAWSにデプロイします。

GitHubで編集を提案

Discussion

ログインするとコメントできます