AWS ElasticCache

先日、クリエーションラインさんが開催したHashiCorp社プロダクトの勉強会に参加しました。
Vagrantが有名で、その他プロダクトも注目されていることは、
色々な媒体で知っていましたが、
なかなか時間を確保出来ず、触ることができませんでした。

勉強会を通して何がいいな、って、
初めて社外の勉強会に参加しましたが、
個人で一から調べれば一週間かかるような知識を2時間という短時間で得ることができます。
今まで引きこもって勉強してきましたが、
これからはもっと外の世界に目を向けて勉強していきたい。
connpassやDoorkeeper等、今は手を伸ばせばこういう機会を得ることができます。

さて、本題。
前回はAMIを利用してELBを利用して、
複数のEC2インスタンスへのバランシングを利用することができました。

ロードバランサーのバランシングルールにRoundRobinやLeastConnectionを採用するに直面する課題があります。
セッション維持です。
アプリケーションサーバでクライアントのキャッシュ情報を保持することで、
クライアントはそのアプリケーションサーバのみアクセスするようにしないといけないこととなります。

この対策として有名な実装方式として、以下があります。

前者はL4LBではソースIPによる振り分けやL7LBではクッキーパーシステンスによる振り分けを利用することで実装可能です。
L4では振り分けが均等にならない、L7は高価だし、負荷が気になります。

ということで、今回はElasticCacheを利用した後者を実装します。

EC2でWebAP構築
 ↓
RDSでWebAP-DB構築
 ↓
AMIでEC2のイメージ化、複製
 ↓
ELBで冗長化
 ↓

ElastiCacheでセッション管理 (今回はここ)

 ↓
S3にアクセスログ保存
 ↓
(できれば)SQSでWebAP連携

今回はElasticCacheを起動して、
どちらのWebサーバにアクセスしてもセッション情報を返すような構成を組みます。
ElasticCacheでやることは少ないです。。

ElasticCahceは実装方式として、Memcached、Redisがあります。
両方共、オンメモリタイプのKVSです。
私のイメージは安定的なMemcached、高機能なRedisです。
Memcachedはメモリにデータの参照、追加/追記/変更/削除、加減算しかできないが、シンプルな分、安定動作が期待できる。
Redisはパターン一致やList,Hash型の取り扱いや永続化等の機能を実装している。

今回はセッション情報の扱いたいだけなのでMemcachedを利用します。

環境はずっと利用しているDjangoを利用したWebアプリケーション構成を引き続き利用します。
実はDjangoは初期設定でセッション情報をデータベースに出力するようになっています。
つまり、既にセッション管理不要な状況です笑
で、でもパフォーマンス面を考えれば、ElasticCacheの方がよいよね。そうだよね。

それではいってみよう。

Elastic Cache

  1. Elastic Cache画面へ遷移
    AWSのメニューからElastic Cacheを選択します。

  2. Cache Subnet Group作成
    Elastic Cacheを作成する前にElastic Cache用にSubnet Groupを作成する必要があります。

    覚えている方もいると思いますが、RDS作成時にもSubnet Groupを作成しました。
    考え方は同じです。
    Elastic CacheはCluster構成とすることが一般的です。
    またCluster構成はAvailabilityZone(AZ)を跨いだ分散配置が可能です。

    APサーバがAZを跨いでいるのでそれぞれのAPサーバは同じAZのElastic Cacheを参照したいです。
    その願い叶えましょう、AWSが。
    Elastic Cache作成前は確認できないですが、作成済みだと思ってください。

    AP#1(AZ#1)で作成されたElastic CacheのEndpointホスト名を名前解決してください。

    [root@ip-192-168-0-54 ~]# dig test-cache.g0y8q8.cfg.apne1.cache.amazonaws.com
    
    ; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.30.rc1.36.amzn1 <<>> test-cache.g0y8q8.cfg.apne1.cache.amazonaws.com
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 33761
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
    
    ;; QUESTION SECTION:
    ;test-cache.g0y8q8.cfg.apne1.cache.amazonaws.com. IN A
    
    ;; ANSWER SECTION:
    test-cache.g0y8q8.cfg.apne1.cache.amazonaws.com. 60 IN A 192.168.1.13
    
    ;; Query time: 84 msec
    ;; SERVER: 192.168.0.2#53(192.168.0.2)
    ;; WHEN: Sun May 31 03:05:35 2015
    ;; MSG SIZE  rcvd: 81

    次はAP#2(AZ#2)。

    [root@ip-192-168-100-35 ~]# dig test-cache.g0y8q8.cfg.apne1.cache.amazonaws.com
    
    ; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.30.rc1.36.amzn1 <<>> test-cache.g0y8q8.cfg.apne1.cache.amazonaws.com
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17159
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
    
    ;; QUESTION SECTION:
    ;test-cache.g0y8q8.cfg.apne1.cache.amazonaws.com. IN A
    
    ;; ANSWER SECTION:
    test-cache.g0y8q8.cfg.apne1.cache.amazonaws.com. 60 IN A 192.168.101.230
    
    ;; Query time: 2 msec
    ;; SERVER: 192.168.0.2#53(192.168.0.2)
    ;; WHEN: Sun May 31 03:05:46 2015
    ;; MSG SIZE  rcvd: 81

    結果が違います。
    ちな、192.168.1.0/24はAZ#1のPrivateSubnet、192.168.101.0/24はAZ#2のPrivateSubnet。
    それぞれCache Subnet Groupに割り当てています。
    つまり、そういうことです。

    順を追って一応説明します。

    AP#1/AP#2は同じIPアドレス(192.168.0.2)のDNSサーバを参照しています。
    なのに、返ってきたIPアドレスが違います。
    ということは同じIPアドレスですが、それぞれ違うDNSサーバを参照していることが想像できます。

    これによってそれぞれのAPサーバは通信範囲を自身が配置されたAZ内だけとすることができます。

    設定に戻ります。

    左メニューからCache Subent Groupsを選択し、[Create Cache Subnet Group]ボタンを押します。

        • -

    Name: 任意の名前
    Description: 任意の説明
    VPC ID: Elastic Cacheを設置するVPC
    Availability Zone: 追加するSubnetが存在するAZ
    Subnet ID: 追加するSubnet

        • -

    Subnet IDを選択して[Add]ボタンを押すことで追加されます。
    追加したいSubnetの数だけAvailability Zone、Subnet IDを選択、[Add]を繰り返します。

    Subnet IDがIDというところがイケてないです。
    プログラミングする上では一意となるIDが嬉しいですが、GUIでIDって言われても覚えていません。
    一度[Add]しないとそれらの情報は表示されません。
    任意で入力できる名前やCIDR Blockをプルダウン内に表示してほしいところです。


  3. Elastic Cache用Security Group作成
    Security Groupを作成します。
    Memcachedを利用する場合、デフォルト接続ポートは11211です。
    ルールはInboundでAPサーバのセキュリティグループからの通信に対して11211のみを開放します。

    ちな、Redisのデフォルト接続ポートは6379です。


  4. Elastic Cache Cluster作成
    Elastic Cacheを作成します。
    左メニューからElastic Cache Clustersを選択し、[Get Started]ボタンを押します。


  5. Select Engine
    実装方式を選択します。
    先の説明にあったように今回はMemcachedを選択します。


  6. Specify Cluster Details
    Memcachedの一般設定情報を入力します。

    Engine Version: 利用可能なバージョンからプルダウンで選択可能
    Port: デフォルトは11211
    Parameter Group: 詳細設定のパラメータを選択可能
         事前にParameter Groupで詳細設定情報を定義しておくことでMemcachedに設定を投入できます。
    Cluster Name: 任意の名前。
         Endpointにも利用されます。
    Node Type: スペックを選択可能
    Number of Nodes: Clusterのノード数(MultiAZ構成とする場合、最低2つ)


  7. Configure Advanced Settings
    Cache Subnet Group: 2で作成したSubnet Groupを選択
    Availability Zone(s): どういうAZ構成にするか選択可能
         [No Preference]、[Spread Nodes Cross Zones]、[Specify Zones]から選択可能
         分散配置したい場合、[Spread Nodes Cross Zones]を選択
    VPC Security Group(s): 3で作成したSecurity Groupを選択
    Maintenance Window: 自動メンテナンス(パッチ適用)とするか、する場合、可能な日にち、曜日、時間等で指定可能。
         [Select Window]、[No Preference]から選択可能
    Topic for SNS Notification: 自動メンテナンス時の通知先(SNSのARNで指定)


  8. 作成
    確認画面で設定内容を確認し、作成を開始します。
    5分ほどで出来ます。


  9. AP側の準備
    LoginURLにユーザ名、パスワードを渡すと、RDSのUserテーブルからユーザ名を元にデータを取得し、パスワードを照合できると、ログイン成功とします。
    セッションにユーザ名を登録します。
    HelloURLにアクセスすると、セッション情報からユーザ名を取得し、Hello, ユーザ名、を返します。
    セッション情報からユーザ名を取得できなければ、ログインしろよ、を返します。

    またそれぞれアクセス先がわかるようにレスポンスにAPサーバのホスト名も含めます。

    それではササッと作ります。

    あー、時間かかった。
    無駄にロギング、エラーハンドリング入れすぎた。

    AWS関係ないのでメモ程度

    Elastic Cache設定追加、CSRF対策解除

    vi /var/lib/project/project/settings.py
    -----
    
    CACHES = {
        'default': {
            'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
            'LOCATION': 'test-cache.g0y8q8.cfg.apne1.cache.amazonaws.com:11211',
        }
    }
    
    #    'django.middleware.csrf.CsrfViewMiddleware',

    Login、Hello URL追加

    vi /var/lib/project/project/urls.py
    -----
    
        url(r'^login/', views.login, name='login'),
        url(r'^hello/', views.hello, name='hello'),

    model設定追加

    vi /var/lib/project/app/models.py
    -----
    
    class User(models.Model):
      name = models.CharField(max_length=32)
      password = models.CharField(max_length=32, null=True)
      created = models.DateField()

    ※ 今回、passwordカラムだけを追加したのですが、
      dbmigrations、migrateでテーブルにカラムが追加されます。
      既にデータがある場合、not null成約を外さないとエラーとなります。

    views設定追加(エラー処理削除)

    vi /var/lib/project/app/views.py
    -----
    
    def login(request):
      res = {"server": gethostname(), "message":""}
      body = request.body
      user_dict = json.loads(body)
      users = User.objects.filter(name=user_dict["user"])
      user = users[0]
      request.session["user"] = user.name
    
      res["message"] = "ok"
      return HttpResponse(json.dumps(res))
    
    
    def hello(request):
      res = {"server": gethostname(), "message":""}
      username = request.session.get("user")
      if not username:
        res["message"] = "You are not logined"
        return HttpResponse(json.dumps(res))
      res["message"] = "Hello, " + username
      return HttpResponse(json.dumps(res))


  10. 動作確認
    ELBをエンドポイントにアカウント情報を引き連れて、LoginURLへアクセスします。

    $ curl -d '{"user":"shinji","pass":"password"}' -X POST http://lb-*********.ap-northeast-1.elb.amazonaws.com/app/login/
    {"message": "ok", "server": "ip-192-168-100-35"}

    AP#2からレスポンスが返ってきました。

    もう一回。

    $ curl -d '{"user":"shinji","pass":"password"}' -X POST http://lb-*********.ap-northeast-1.elb.amazonaws.com/app/login/
    {"message": "ok", "server": "ip-192-168-0-54"}

    AP#1からレスポンスが返ってきました。

    次にHelloURLへCookieを渡さずアクセスします。

    $ curl http://lb-*********.ap-northeast-1.elb.amazonaws.com/app/hello/
    {"message": "You are not logined", "server": "ip-192-168-100-35"}

    ログインしてませんよー、よしよし。

    次はCookieを取得して、Cookieを渡してHelloを受け取ります。

    $ curl -c cookie.txt -d '{"user":"shinji","pass":"password"}' -X POST http://lb-*********.ap-northeast-1.elb.amazonaws.com/app/login/
    {"message": "ok", "server": "ip-192-168-100-35"}
    
    $ curl -b cookie.txt http://lb-***********.ap-northeast-1.elb.amazonaws.com/app/hello/
    {"message": "Hello, shinji", "server": "ip-192-168-0-54"}

    AP#2にログインして、AP#1からHelloを受け取れました。


いかがでしたでしょうか?

これである程度の可用性、スケーラビリティを確保したWebアプリケーションシステムを組むことが出来ました。

クラウドIaaSを利用すると、なんといってもお手軽です。

私が入社した時は検証環境なんて一つの検証サーバをOSレベル、アプリケーションレベルで何人か共有していたり、本番環境はOSが分かれているところ同居させていたりが当たり前で、検証環境なのに気を使ったり、本番環境と違う構成だから本番環境にデプロイすると想定外のエラーがあったりと本当に苦労しました。(ブツブツ

次回は運用面です。