✍️

ElastiCache for Redis ハンズオン をやってみた

2022/03/16に公開

こちらのハンズオンをやってみました。
https://aws.amazon.com/jp/getting-started/hands-on/boosting-mysql-database-performance-with-amazon-elasticache-for-redis/

RDSの前段にキャッシュサーバーとしてElastiCacheを配置し、RDSへの負荷軽減を行う構成の作成を体験できます。

ハンズオン行った際のメモ程度の記事です。
ElastiCacheのハンズオンを試してみたい方向けに、参考にして頂ければと思い、記事を残すことにしました。
EC2やRDS周りの設定はかなりざっくりと書いているので、ある程度分かる方向けになっております。

やってみた際の注意点や節約のために改変した点を記載しています。

想定読者層

  • EC2, VPC周りの構成の作成に慣れている
  • RDSを触ったことがある
  • ElastiCacheは触ったことが無い

価格の参考

アカウントが12ヶ月無料枠の対象内であれば、無料枠の範囲内で実施できるようにハンズオンが組まれているので、何も気にせずに実施できます。

しかし、ElastiCacheに興味を持っていて12ヶ月無料枠のアカウントを持ってる人ってなかなか居ないんじゃないか...無料枠の期間に何のありがたみも感じていなかった自分が憎い...

参考までにAWS Pricing Calculatorで出した料金です。
月額ですが、何となくのコスト感を掴むために置いておきます。

RDS for MySQL

Storage for each RDS instance (General Purpose SSD (gp2)), Storage amount (30 GB), Quantity (1), Instance type (db.t4g.micro), Utilization (On-Demand only) (100 %Utilized/Month), Deployment option (Single-AZ), Pricing strategy (OnDemand)

Monthly:
15.13 USD

Amazon ElastiCache

Nodes ( 1 instances of type Redis Standard cache t4g.micro OnDemand )

Monthly:
11.68 USD

Upfront:
0.00 USD

EC2

Operating system (Linux), Quantity (1), Pricing strategy (On-Demand Instances), Storage amount (30 GB), Instance type (t2.micro), General Purpose SSD (gp3) - IOPS (3000), General Purpose SSD (gp3) - Throughput (125 MBps)

Monthly:
10.87 USD

実際に掛かった金額
また、コストエクスプローラーで確認したところ実際に掛かった料金(USD)は以下のようでした。
ハンズオンをしていた時間は4時間くらいで、実際に起動していた時間は2時間くらいです。

ElastiCache 0.07$
RDS 0.03$
EC2 0.03$
合計 0.13$

ハンズオンの全体の概要

以降、見出しとハンズオンの項番は対応しています。

全体的には以下の流れになります。

  • 概要. アプリケーションサーバー的なEC2、VPC周りの環境などを作成
  • 1 ElastiCacheの作成
  • 2 RDSの作成
  • 3 EC2とRDSの接続
  • 4 EC2とElastiCacheの接続、動作確認
  • 5 掃除

また、ルートテーブルの書き込みなどは省いていますが、ざっくり構成図を作成しました。

ElastiCacheハンズオン

サブネットのIPアドレス範囲は任意の値で構いません。なぜ /20 かというと、VPCのクイックセットアップで指定されたのが /20 だったから、というだけです。

空のプライベートサブネットは何のために必要かというと、RDSのサブネットグループでAZの異なるサブネット2つを指定する必要があるためです。

料金を気にするのであれば、ElastiCacheの立ち上げ時間を減らすため、以下の順で行っても問題なさそうです。

概要. EC2,VPC周りの作成 → 2. RDSの立ち上げ → 3.EC2とRDSの接続 → 1.ElastiCacheの立ち上げ → 4. EC2とElastiCacheの接続,動作確認

ハンズオンに記載の通りに進めると、1.でElastiCacheを立ち上げた後、4.でEC2から接続するまでElastiCacheの出番が無く、課金が勿体ない気がします。

ほぼハンズオンの通りで大丈夫ですが、セキュリティグループの設定が多少ややこしいかもしれないので、セキュリティグループの相関図も用意しました。

ElastiCacheハンズオン_セキュリティグループ

概要. VPC, EC2, EC2のIAMロールは各自で作成する

ElastiCache, RDSに接続するアプリケーションサーバーっぽいEC2を立てる作業はハンズオンに含まれていません。

自分で作りましょう。

VPC プライベートサブネット2, パブリックサブネット1の構成

EC2 t2.micro, Amazon Linux 2, 自動割り当てパブリックIPを有効 で一般的なインスタンスを作成してパブリックサブネットに配置。セキュリティグループでMyIPを許可する。

IAMロール RDSFullAccessとElastiCacheFullAccessのAWS管理ポリシーを付与したロールを用意してEC2にアタッチする。

SSHで接続し、前提条件に記載があるインストール周りのコマンドを実行する。

1. ElastiCacheの起動

基本は流れ通り進める。

エンジンログは、どんなログが出るのか気になったので有効化する。新規作成でロググループを指定。

料金が気になるので、マルチAZは無効にして、サブネットは先ほど作成したプライベートサブネット1つを指定する。

セキュリティグループは別途作成して指定する。先ほど作成したEC2のセキュリティグループからの6379ポートへのアクセスを許可する。

セキュリティグループ

作成中
作成中

完了
完了

2. RDSの作成

Auroraは高いので使わない。

RDS for MySQLを使う。

先ほど作成した二つのプライベートサブネットを含んだサブネットグループを作成する。

以下を設定する。
インスタンスタイプ db.t4g.micro
データベース認証オプション パスワード認証
VPCセキュリティグループ 新規作成
自動バックアップ 無効
暗号化 無効

作成後、VPCのコンソールにてRDS用に新規作成したセキュリティグループの編集を行う。作成後の段階では、MyIPからの3306ポートへの接続のみが許可されている。

先ほど作成したEC2インスタンスのセキュリティグループからの3306接続を許可する。
セキュリティグループ

3. EC2インスタンスからRDSへの接続

手順に従って、テストデータの投入を行う。
(個人的に、この辺りでmysql のコマンドを間違えて、少し手間取った。)

4. ElastiCacheへの接続

pythonコードをインライン実行する際にredisのエンドポイントを入力するところがあるが、
redis://エンドポント とする必要があることに注意する。

pythonコードの実行

コンソール上でキャッシュ周りの動作を確認したかったので、example.pyを編集してprintを仕込みました。
また、ひとまずfetch の動作を確認したかったので、planet の実行はコメントアウト

変更分はコメントで記載

example.py
import os
import json

import redis
import pymysql

class DB:
    def __init__(self, **params):
        params.setdefault("charset", "utf8mb4")
        params.setdefault("cursorclass", pymysql.cursors.DictCursor)

        self.mysql = pymysql.connect(**params)

    def query(self, sql):
        with self.mysql.cursor() as cursor:
            cursor.execute(sql)
            return cursor.fetchall()

    def record(self, sql, values):
        with self.mysql.cursor() as cursor:
            cursor.execute(sql, values)
            return cursor.fetchone()

# Time to live for cached data
TTL = 10

# Read the Redis credentials from the REDIS_URL environment variable.
REDIS_URL = os.environ.get('REDIS_URL')

# Read the DB credentials from the DB_* environment variables.
DB_HOST = os.environ.get('DB_HOST')
DB_USER = os.environ.get('DB_USER')
DB_PASS = os.environ.get('DB_PASS')
DB_NAME = os.environ.get('DB_NAME')

# Initialize the database
Database = DB(host=DB_HOST, user=DB_USER, password=DB_PASS, db=DB_NAME)

# Initialize the cache
Cache = redis.Redis.from_url(REDIS_URL)

def fetch(sql):
    """Retrieve records from the cache, or else from the database."""
    res = Cache.get(sql)

    if res:
        print('Cache exists!') # プリントを仕込み
        return json.loads(res)

    print('Cache not exists...') # プリントを仕込み
    res = Database.query(sql)
    print('Cache setting') # プリントを仕込み
    Cache.setex(sql, TTL, json.dumps(res))
    return res

def planet(id):
    """Retrieve a record from the cache, or else from the database."""
    key = f"planet:{id}"
    res = Cache.hgetall(key)

    if res:
        print('Cache exists!') # プリントを仕込み
        return res

    print('Cache not exists...') # プリントを仕込み
    sql = "SELECT `id`, `name` FROM `planet` WHERE `id`=%s"
    res = Database.record(sql, (id,))

    if res:
        print('Cache setting') # プリントを仕込み
        Cache.hmset(key, res)
        Cache.expire(key, TTL)

    return res

# Display the result of some queries
print(fetch("SELECT * FROM planet"))
# print(planet(1)) # ひとまずコメントアウト
# print(planet(2)) # ひとまずコメントアウト
# print(planet(3)) # ひとまずコメントアウト

一回目

Cache not exists...
Cache setting
[{'id': 1, 'name': 'Mercury'}, {'id': 2, 'name': 'Venus'}, {'id': 3, 'name': 'Earth'}, {'id': 4, 'name': 'Mars'}, {'id': 5, 'name': 'Jupiter'}, {'id': 6, 'name': 'Saturn'}, {'id': 7, 'name': 'Uranus'}, {'id': 8, 'name': 'Neptune'}]

すぐにもう一度実行

Cache exists!
[{'id': 1, 'name': 'Mercury'}, {'id': 2, 'name': 'Venus'}, {'id': 3, 'name': 'Earth'}, {'id': 4, 'name': 'Mars'}, {'id': 5, 'name': 'Jupiter'}, {'id': 6, 'name': 'Saturn'}, {'id': 7, 'name': 'Uranus'}, {'id': 8, 'name': 'Neptune'}]

TTLに設定した10秒経過後やってみる

Cache not exists...
Cache setting
[{'id': 1, 'name': 'Mercury'}, {'id': 2, 'name': 'Venus'}, {'id': 3, 'name': 'Earth'}, {'id': 4, 'name': 'Mars'}, {'id': 5, 'name': 'Jupiter'}, {'id': 6, 'name': 'Saturn'}, {'id': 7, 'name': 'Uranus'}, {'id': 8, 'name': 'Neptune'}]

いい感じ。

ちなみに、fetch内で利用されているCache.setex(key, ttl, value)は有効期限を設定してキャッシュを登録するメソッドらしい。(https://redis.io/commands/setex)

こちらのメソッドでは、SQL文自体をkeyに設定している。

続いて、planetを試す。コメントアウトを解いて、fetch実行はコメントアウト。

初回

Cache not exists...
Cache setting
{'id': 1, 'name': 'Mercury'}
Cache not exists...
Cache setting
{'id': 2, 'name': 'Venus'}
Cache not exists...
Cache setting
{'id': 3, 'name': 'Earth'}

2回目

Cache exists!
{b'id': b'1', b'name': b'Mercury'}
Cache exists!
{b'id': b'2', b'name': b'Venus'}
Cache exists!
{b'id': b'3', b'name': b'Earth'}

10秒経過後

Cache not exists...
Cache setting
{'id': 1, 'name': 'Mercury'}
Cache not exists...
Cache setting
{'id': 2, 'name': 'Venus'}
Cache not exists...
Cache setting
{'id': 3, 'name': 'Earth'}

いい感じ。

key = f"planet:{id}" のようにコロンで区切るのは、Redisの文化らしい。(https://qiita.com/morrr/items/271548776938e7ddc0ec#キーについて)

もう少し触りたかったが、時間が無かったので終わった。

ログにキャッシュ作成などが出力されているかと思ったが、以下のようなリソースに関するログしか出ていなかった。

{
    "CacheClusterId": "clust-002",
    "CacheNodeId": "0001",
    "LogLevel": "NOTICE",
    "Role": "S",
    "Time": "15 Mar 2022 04:54:57.145 UTC",
    "Message": "PRIMARY <-> REPLICA sync: Loading DB in memory."
}

5. 掃除

手順に沿って掃除しました。

今回のハンズオンのためにEC2インスタンスを作成していた場合、EC2インスタンス, VPC, サブネット,も削除しましょう。

EC2インスタンス削除後、VPCを削除する際に出てくるダイアログで、一括でサブネットやインターネットゲートウェイも削除できます。

感想

ElastiCacheを触ってみたくて、ハンズオンを実施しました。
一回でも触ったことがあると字面だけで分かった気になるより圧倒的に理解が早くなるので、なるべく知らないサービスは触っておきたいです。

今回くらいの 親切度 のハンズオンであれば、AWSのユーザーガイドのチュートリアルや今回のハンズオンチュートリアルを探せば出てくるので、どんどん活用していきたいです。

Discussion