ElastiCache学習の第一歩に、Webアプリケーションのセッション情報を保存する公式チュートリアルをやってみた

#ElastiCacheおじさんと繋がりたい
2020.05.30

コンサル部@大阪オフィスのYui(@MayForBlue)です。

最近初めてElastiCacheを触る機会があり、最初の一歩として公式のチュートリアルをやってみました。
より構築しやすいように手順を変えてみたり、構築後に簡単なテストをするなど少し工夫してやってみたので、手順をご紹介したいと思います。

ElastiCache とは

ElastiCache とは、インメモリデータストアやインメモリキャッシュとして機能するAWSのフルマネージドサービスです。

詳細は以下のブログをご参照ください。

チュートリアル内容

リンク

Amazon ElastiCache for Redis を使い、オンラインアプリケーション用の高速セッションストアを構築する

概要

PythonのマイクロフレームワークFlaskで簡易Webアプリケーションを作成し、セッション情報をElastiCache(Redis)に保存する

構成図

構成は以下のようになります。
プライベートサブネットにWebアプリケーション用のEC2を用意し、プライベートサブネットにElastiCacheのプライマリ/レプリカノードを配置します。

前準備

チュートリアルに使用するVPC、サブネット、インターネットゲートウェイはCloudFormationで構築しました。
テンプレートはこちら

AWSTemplateFormatVersion: "2010-09-09"
Description:
  VPC and Subnet Create

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Project Name Prefix"
        Parameters:
          - ProjectName
      - Label:
          default: "Network Configuration"
        Parameters:
          - VPCCIDR
          - PublicSubnetACIDR
          - PublicSubnetCCIDR
          - PrivateSubnetACIDR
          - PrivateSubnetCCIDR
    ParameterLabels: 
      ProjectName:
        defalut: "ProjectName"
      VPCCIDR: 
        default: "VPC CIDR"
      PublicSubnetACIDR: 
        default: "PublicSubnetA CIDR"
      PublicSubnetCCIDR: 
        default: "PublicSubnetC CIDR"
      PrivateSubnetACIDR: 
        default: "PrivateSubnetA CIDR"
      PrivateSubnetCCIDR: 
        default: "PrivateSubnetC CIDR"

Parameters:
  ProjectName:
    Type: String
    Default: "elc-tutorial"
  VPCCIDR:
    Type: String
    Default: "10.1.0.0/16"
  PublicSubnetACIDR:
    Type: String
    Default: "10.1.10.0/24"
  PublicSubnetCCIDR:
    Type: String
    Default: "10.1.20.0/24"
  PrivateSubnetACIDR:
    Type: String
    Default: "10.1.100.0/24"
  PrivateSubnetCCIDR:
    Type: String
    Default: "10.1.200.0/24"

Resources:
# ------------------------------------------------------------#
#  VPC
# ------------------------------------------------------------#
# VPC
  VPC: 
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags: 
        - Key: Name
          Value: !Sub "${ProjectName}-vpc"
# InternetGateway
  InternetGateway:
    Type: "AWS::EC2::InternetGateway"
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-igw"
  InternetGatewayAttachment:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC 

# ------------------------------------------------------------#
#  Subnet
# ------------------------------------------------------------#          
# Public SubnetA
  PublicSubnetA:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: "ap-northeast-1a"
      CidrBlock: !Ref PublicSubnetACIDR
      VpcId: !Ref VPC
      Tags: 
        - Key: Name
          Value: !Sub "${ProjectName}-public-subnet-a"
# Public SubnetC
  PublicSubnetC:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: "ap-northeast-1c"
      CidrBlock: !Ref PublicSubnetCCIDR
      VpcId: !Ref VPC
      Tags: 
        - Key: Name
          Value: !Sub "${ProjectName}-public-subnet-c"
# Private SubnetA
  PrivateSubnetA:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: "ap-northeast-1a"
      CidrBlock: !Ref PrivateSubnetACIDR
      VpcId: !Ref VPC
      Tags: 
        - Key: Name
          Value: !Sub "${ProjectName}-private-subnet-a"
# Private SubnetC
  PrivateSubnetC:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: "ap-northeast-1c"
      CidrBlock: !Ref PrivateSubnetCCIDR
      VpcId: !Ref VPC
      Tags: 
        - Key: Name
          Value: !Sub "${ProjectName}-private-subnet-c"

# Public RouteTableA
  PublicRouteTableA:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
      Tags: 
        - Key: Name
          Value: !Sub "${ProjectName}-public-route-a"
# Public RouteTableC
  PublicRouteTableC:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
      Tags: 
        - Key: Name
          Value: !Sub "${ProjectName}-public-route-c"
# Private RouteTableA
  PrivateRouteTableA:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
      Tags: 
        - Key: Name
          Value: !Sub "${ProjectName}-private-route-a"
# Private RouteTableC
  PrivateRouteTableC:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
      Tags: 
        - Key: Name
          Value: !Sub "${ProjectName}-private-route-c"
# Routing
# PublicRouteA
  PublicRouteA:
    Type: "AWS::EC2::Route"
    Properties:
      RouteTableId: !Ref PublicRouteTableA
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway
# PublicRouteC
  PublicRouteC:
    Type: "AWS::EC2::Route"
    Properties:
      RouteTableId: !Ref PublicRouteTableC
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway

# RouteTable Associate
# PublicRouteTableA
  PublicSubnetARouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PublicSubnetA
      RouteTableId: !Ref PublicRouteTableA
# PublicRouteTableC
  PublicSubnetCRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PublicSubnetC
      RouteTableId: !Ref PublicRouteTableC
# PrivateRouteTableA
  PrivateSubnetARouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PrivateSubnetA
      RouteTableId: !Ref PrivateRouteTableA
# PrivateRouteTableC
  PrivateSubnetCRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PrivateSubnetC
      RouteTableId: !Ref PrivateRouteTableC

やってみる

サーバーの構築

まずはアプリケーションサーバーとなるEC2を構築します。

EC2のダッシュボードに移動し、「インスタンスの作成」をクリックします。

Amazon Linux 2 のAMIを選択します。

インスタンスタイプはt2.microを選択します。

インスタンス詳細の設定で以下を設定し、その他の値はデフォルトで「ストレージの追加」をクリックします。
1. ネットワーク:チュートリアル用に作成したものを選択(elc-tutorial-vpc)
2. サブネット: elc-tutorial-public-subnet-a
3. 自動割り当てパブリックIP:有効

ストレージサイズはデフォルトのまま「タグの追加」をクリックします。
任意のタグを設定します。ここでは以下を設定しました。
Name:ecl-tutorial-server

セキュリティグループを設定します。
「新しいセキュリティグループを作成する」にチェックを入れ、以下を設定しました。

  • セキュリティグループ名:ecl-tutorial-server-sg
  • 説明:ecl-tutorial-server-sg
  • インバウンドルール
タイプ プロトコル ポート範囲 ソース 説明
SSH TCP 22 マイIP from my IP
カスタムTCP TCP 5000 0.0.0.0/0 from All

インバウンドルールの1つ目はSSHログイン用、2つ目はWebアプリケーションへのアクセス用です。

内容を確認して問題なければ「起動」をクリックします。

ログイン用のキーペアを選択して「インスタンスの作成」をクリックします。
キーペアが未作成の場合は「新しいキーペアを作成する」を選択してください。

ElastiCacheの構築

次に、ElastiCacheを構築していきます。

インスタンスを構築する前に、構築に必要なセキュリティグループとサブネットグループを作成します。
まずはセキュリティグループからです。
EC2のダッシュボードの左側ペインより「セキュリティグループ」を選択し、「セキュリティグループの作成」をクリックします。

以下を設定し、「セキュリティグループの作成」をクリックします。

  • セキュリティグループ名:ecl-tutorial-redis-sg
  • 説明:ecl-tutorial-redis-sg
  • インバウンドルール
タイプ プロトコル ポート範囲 ソース 説明
カスタムTCP TCP 6379 ecl-tutorial-server-sg from server

インバウンドルールでアプリケーションサーバーからポート番号6379の通信を許可する設定を入れています。

次にサブネットグループを作成します。
ElastiCacheのダッシュボードの左側ペインから「サブネットグループ」を選択します。

「サブネットグループの作成」をクリックします。

以下を設定し、「作成」をクリックします。

  • 名前:elc-tutorial-redis-subnet-group
  • 説明:elc-tutorial-redis-subnet-group
  • VPC:elc-tutorial-vpc
  • サブネット
    • elc-tutorial-private-subnet-a / ap-northeast-1a
    • elc-tutorial-private-subnet-c / ap-northeast-1c

いよいよElastiCacheを構築していきます。
ElastiCacheのダッシュボードから「今すぐ始める」をクリックします。

以下を設定します。(その他の値はデフォルトのままで大丈夫です。)

  • クラスターエンジン:Redis
  • 名前:elc-tutorial-redis
  • 説明:elc-tutorial-redis
  • ノードのタイプ:cache.t2.micro
  • レプリケーション数:1
  • サブネットグループ:elc-tutorial-redis-subnet-group
  • セキュリティグループ:ecl-tutorial-redis-sg
  • 自動バックアップの有効化:無効

しばらくしてステータスがavailableになれば構築完了です。

アプリケーションの設定をする

サーバーにログインし、Webアプリケーションとしての設定、ElastiCacheとの接続を行います。

EC2にログインします。

$ ssh -i <key-pair>.pem <ユーザ名>@<パブリックIP or パブリックDNS名>

ログイン方法の詳細についてはこちらのブログをご参照ください。

ログインできたら、以下のコマンドを順番に実行します。

$ sudo yum install tmux
$ sudo amazon-linux-extras install redis4.0
$ sudo yum install git
$ sudo yum install python3
$ sudo pip3 install flask
$ sudo pip3 install virtualenv
$ git clone https://github.com/aws-samples/amazon-elasticache-samples/
$ cd amazon-elasticache-samples/session-store
$ virtualenv venv
$ source ./venv/bin/activate
$ pip3 install -r requirements.txt
$ export FLASK_APP=example-1.py
$ export SECRET_KEY=some_secret_string
$ flask run -h 0.0.0.0 -p 5000 --reload

チュートリアルに記載されていたものに加え、Flask、テストに使用するRedisクライアントツール、tmuxをインストールしています。

Flaskの起動が成功したら、ブラウザからアクセス確認してみます。 以下のようにhttp://【パブリックDNS】:5000/にアクセスできれば成功です。

ElastiCache(Redis)と接続する

amazon-elasticache-samples/session-store/example-1.py を編集し、既存のコードを削除して以下のコードを貼り付けます。
(コードの内容についてはチュートリアル内で解説されているため、ここでは割愛します)
チュートリアル内ではセッション情報保持期間が10秒間に設定されていますが、ここではテストのために180秒間に設定しました。

import os
import redis
from flask import Flask, session, redirect, escape, request

app = Flask(__name__)

app.secret_key = os.environ.get('SECRET_KEY', default=None)

# BEGIN NEW CODE - PART 1 #
REDIS_URL = os.environ.get('REDIS_URL')
store = redis.Redis.from_url(REDIS_URL)
# END NEW CODE - PART 1 #

@app.route('/')

def index():

    if 'username' in session:

        # BEGIN NEW CODE - PART 2 #
        username = escape(session['username'])
        visits = store.hincrby(username, 'visits', 1)
        # BEGIN NEW CODE - EXPIRE SESSIONS #
        store.expire(username,  180)
        # END NEW CODE - EXPIRE SESSIONS #

    return '''
        Logged in as {0}.<br>
        Visits: {1}
        '''.format(username, visits)
        # END NEW CODE - PART 2 #

    return 'You are not logged in'

@app.route('/login', methods=['GET', 'POST'])

def login():

    if request.method == 'POST':
        session['username'] = request.form['username']
        return redirect('/')

    return '''
        <form method="post">
        <p><input type=text name=username>
        <p><input type=submit value=Login>
        </form>'''

@app.route('/logout')

def logout():

    session.pop('username', None)
    return redirect('/')

次に、環境変数にRedisのプライマリエンドポイントを設定します。

$ export REDIS_URL="redis://【プライマリエンドポイント】:6379"

設定できたら、tmuxを起動します。

$ tmux

一つ目のウィンドウで以下のコマンドを実行し、Redisに値が保存されていないことを確認します。

$redis-cli -h 【プライマリエンドポイント】
elc-tutorial-redis.xxxx.ng.0001.apne1.cache.amazonaws.com:6379> keys *
(empty list or set)

Ctrl+bcをクリックし、新規ウィンドウを作成します。 以下のコードを実行し、Flaskサーバーを起動します。

$ flask run -h 0.0.0.0 -p 5000 --reload
 * Serving Flask app "example-1.py" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat

起動できたら、http://【パブリックDNS】:5000/loginにアクセスし、以下のような画面が表示されていることを確認します。

任意の文字列を入力してログインすると、Redisに値が保存されます。 aという値で2回、bという値で2回ログインしてみました。
tmuxの一つ目のウィンドウで値が保存されていることを確認してみました。
今回はアプリケーション側でハッシュ値を保存しているため、値参照のコマンドにはhgetallを使用しています。

elc-tutorial-redis.xxxx.ng.0001.apne1.cache.amazonaws.com:6379>keys *
1) "b"
2) "a"
elc-tutorial-redis.xxxx.ng.0001.apne1.cache.amazonaws.com:6379> hgetall a
1) "visits"
2) "2"
elc-tutorial-redis.xxxx.ng.0001.apne1.cache.amazonaws.com:6379> hgetall b
1) "visits"
2) "1"

期待した値が保存されていることが確認できました!

後片付け

最後に、今回構築した以下を削除してチュートリアル完了です。

  • ElastiCache
    • Redisインスタンス
    • サブネットグループ
    • セキュリティグループ
  • EC2
    • EC2インスタンス
    • セキュリティグループ
  • ネットワーク
    • CloudFormationテンプレート

最後に

ElastiCacheについて概要は知っていたものの、やはり実際に手を動かして作ってみるのが大事だなと思いました。
今回のチュートリアルでは構築に加え、簡易的なものではありますが、アプリケーションに組み込む方法も知れたのが良かったです。
また、今回のチュートリアルで学べるのは最初の構築の部分のみなので、ElastiCacheの詳細な挙動や性質については別途勉強していきたいと思います。
この記事がどなたかのお役に立てば幸いです。

以上、大阪オフィスのYui(@MayForBlue)でしたっ(`・ω・´)