キャッシュの実装(AWS Memcachedの実行環境の構築)

## はじめに
Amazon ElastiCache for Memcachedを利用した値の取得について記載します。
実行環境は以下となります。

![001.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/85a7c1c6-702d-e32f-4edc-d9b9f09f1ab8.png)

また、それぞれのセキュリティグループのインバウンド許可は以下になっています。

|AWSサービス|利用用途|
|:--|:--|:--|
|EC2|User側から5000番ポート(アプリがflaskのため)の許可, Cloud9から22番ポートの許可(アプリ配置のため)|
|RDS|EC2,Cloud9の割り当てたセキュリティグループから3306番ポートの許可|
|Memcached|EC2,Cloud9から11211番ポートの許可|
|Cloud9|デフォルト|



## 各AWSリソースの作成

各AWSリソースの作成と設定についてを記載します。
EC2,RDS,Cloud9については、一般的な作成方法なので設定内容についての記載をし、Memcachedについては作成方法についてを記載します。

#### 1. RDSの設定
RDSの構築そのものについては割愛します。(広く一般的な作成方法のため)
データの投入についての一連の作業はCloud9から実施しています。
以下は、Cloud9でmysql --versionでmysqlが入っていることを確認し、mysql -h XXXXX.ap-northeast-1.rds.amazonaws.comで接続してからデータを投入した流れです。

```
$ mysql --version
mysql  Ver 15.1 Distrib 10.2.38-MariaDB, for Linux (x86_64) using  EditLine wrapper
$ mysql -h XXXXX.ap-northeast-1.rds.amazonaws.com -P 3306 -u admin -p
Enter password: 
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 8.0.23 Source distribution

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

> create database food;
> connect food;
> create table fruit (ID varchar(32) UNIQUE,Name varchar(128));
> insert into fruit (ID, Name) values('001' , 'Peach');
> insert into fruit (ID, Name) values('002' , 'Orange');
> select * from fruit;
+------+--------+
| ID   | Name   |
+------+--------+
| 001  | Peach  |
| 002  | Orange |
+------+--------+
2 rows in set (0.01 sec)
```

 
#### 2. EC2とCloud9設定

EC2とCloud9の構築についても広く一般的な構築方法のため割愛します。構築後にログインした後は、flaskやemcachedのモジュールのインストールのため以下yumコマンドの実行を行います。

```
$ pip3 install Flask
$ pip3 install PyMySQL
$ pip3 install python-memcached
```

また、今回はCloud9を開発環境としているため、Cloud9で作成したアプリケーションを配置するにはscpコマンドを利用し、EC2へのログインに関してはsshコマンドでCloud9経由でEC2へのログインを行っています。

```
$ scp -i ./AWS_TEST.pem -r ./flask_app ec2-user@10.1.10.110:/home/ec2-user 
$ ssh -i ./AWS_TEST.pem ec2-user@10.1.10.110
```

#### 3. Memcachedの作成
Amazon ElastiCache Memcachedの作成の作成についてですが、これは以下のように1画面入力するだけで作成できます。「キャッシュ環境の作成なんて難しいんだろうな」とかまるで思う必要ないと思います。作る分にはRDSより簡単なはずです。ポイントという事も特にないのですが、サブネットグループがあったりや、プライベートサブネットに配置しパブリックサブネットのEC2からアクセスしてきたデータを返せるようにするなどRDSの構築を行うように作成できます。

![003.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/4a6fb774-1e20-452e-00a4-0b81b1258149.png)


## 各プログラムについて
今回はpythonのflaskというフレームワークを利用しています。
まず、ディレクトリ構成は以下のようになっています。


Cloud9で表示されるディレクトリ画面
![002.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/ad65d1e7-fcc6-fcd3-7243-0df3e60d9f9c.png)
↓

```
flask_app
├── app
│   ├── app.py
│   ├── static
│   └── templates
│       ├── hit.html
│       ├── index.html
│       └── Nothit.html
└── run.py
```

それぞれのpython,htmlファイルは以下のようになっています。



#### 1.run.py
flaskの実行というか起動(開始)ファイルで以下コマンドを実行することでサービスを開始できます。コマンド実行後のh ttp://x.x.x.x:5000でwebアクセスができます。
$ python3 run.py #実行コマンド

```python:run.py
from app.app import app

if __name__ == "__main__":
    app.run(host='0.0.0.0', port='5000')
```


#### 2.app.py

アプリケーションプログラムファイルです。
それぞれポイントになりそうな点はプログラム内にコメントを入れております。
(except～～については適当に例外処理を記載しただけなのでコメント入れてないです。。。)

```python:app.py
# import
import pymysql.cursors
import memcache
from flask import Flask, request, render_template

app = Flask(__name__)


#Memcachedの定義
cache = memcache.Client(['mytest.xxxxxx.cfg.apne1.cache.amazonaws.com:11211'])

#デフォルトページの指定
@app.route('/')
def index():
    return render_template("index.html")


#index.htmlから/kobetsuに対してデータが飛ばされたときの実行プログラム
@app.route('/kobetsu', methods=['GET'])
def kobetsu():
    ID = request.args.get('ID')
    cachevalue = cache.get(str(ID)) #キャッシュサーバに対してデータが格納されているかを確認

    if cachevalue: # キャッシュサーバにデータが格納されておりtrue状態であればhit.htmlを返す
        return render_template('hit.html', cachevalueID=cachevalue['ID'],cachevalueName=cachevalue['Name'])
    else: #キャッシュサーバにデータが格納されておらずfalse状態であればRDSにデータを取得しに行く
        connection = pymysql.connect(host='xxxxxx.xxxxxx.ap-northeast-1.rds.amazonaws.com',
        user='admin',
        password='1qaz2wsx',# 脆弱なパスワード←?
        db='food',
        charset='utf8',
        cursorclass=pymysql.cursors.DictCursor)
    try:
        with connection.cursor() as cursor:
            sql = 'SELECT * FROM fruit where ID="%s"' %(ID)
            cursor.execute(sql)
            results = cursor.fetchall()
            if results:
                for r in results:
                    cache.set(str(ID) ,r ,time=600) # index.htmlから飛ばされたIDに対して出力された結果をkey:ID value:r としてキャッシュサーバに格納。timeはキャッシュサーバのデータ保有時間
                    return render_template('Nothit.html', resultsID=r['ID'], resultsName=r['Name']) # Nothit.htmlを返す
            else:
                raise TypeError()
    except TypeError: #エラー処理は結構適当です。
        connection.rollback()
        return 'No ID!!!'
    except Exception as error:
        connection.rollback()
        return 'Please call jimbot 080-XXXX-XXXX', error.args[0]
    finally:
        connection.close()

#index.htmlから/tourokuに対してデータが飛ばされたときの実行プログラム
@app.route('/touroku', methods=['POST'])
def touroku():
    ID = request.form["ID"]
    Name = request.form["Name"]

    cache.flush_all() #キャッシュの削除を行う。これは、登録した後もキャッシュが残り続けていたらユーザが登録されていないと誤認するのを防ぐため。

    connection = pymysql.connect(host='xxxxxx.xxxxxx.ap-northeast-1.rds.amazonaws.com',
    user='admin',
    password='1qaz2wsx',# 脆弱なパスワード←?
    db='food',
    charset='utf8',
    cursorclass=pymysql.cursors.DictCursor)
    try:
        with connection.cursor() as cursor:
            sql = "INSERT INTO fruit (ID, Name) VALUES (%s, %s)"
            cursor.execute(sql, (ID,Name))
        connection.commit()
        return 'Welcome ID :  '  + str(ID) +'!!!'
    except connection.IntegrityError: #エラー処理は結構適当です。
        connection.rollback()
        return 'MemberID already exists!!! Please try update command and put option.'
    except NameError:
        connection.rollback()
        return 'There is a mistake in the value you entered. Please enter \'ID\',Name '
    except Exception as error:
        connection.rollback()
        return 'Please call jimbot 080-XXXX-XXXX', error.args[0]
    finally:
        connection.close()

#index.htmlから/taberaretaに対してデータが飛ばされたときの実行プログラム
@app.route('/taberareta', methods=['POST'])
def taberareta():
    ID = request.form["ID"]
    
    cache.flush_all()#キャッシュの削除を行う。これは、削除した後もキャッシュが残り続けていたらユーザが削除されていないと誤認するのを防ぐため。

    connection = pymysql.connect(host='xxxxxx.xxxxxx.ap-northeast-1.rds.amazonaws.com',
    user='admin',
    password='1qaz2wsx',# 脆弱なパスワード←?
    db='food',
    charset='utf8',
    cursorclass=pymysql.cursors.DictCursor)
    try:
        with connection.cursor() as cursor:
            sql = 'SELECT * FROM fruit where ID="%s"' %(ID)
            cursor.execute(sql)
            results = cursor.fetchall()
            if results:
                sql = "DELETE FROM fruit WHERE ID = '%s'" %(ID)
                cursor.execute(sql)
                connection.commit()
                return 'Delete ID :  '  + str(ID) +' (T_T)'
            else:
                raise TypeError()
    except TypeError:  #エラー処理は結構適当です。
        connection.rollback()
        return 'No ID!!!'
    except Exception as error:
        connection.rollback()
        return 'Please call jimbot 080-XXXX-XXXX', error.args[0]
    finally:
        connection.close()

```



#### 3.index.html

トップ画面です。 ~~結構適当です。いや、かなり適当です。~~
各Form内に値を入力してSend Dataを押下するとapp.py内の"/touroku","/kobetsu","/taberareta"にデータを渡すようになっています。


```html:index.html
<html>
<head> </head>
<table border="0">
  <body>
    <h1>Welcome to Fruit Data</h1>
    <hr>
    <form action="/touroku" method="POST">
      <table border="2">
        <h3>Add Fruit Information Data</h3>
        <tr>
          <td align="right"><b> ID:</b></td>
          <td><input type="text" name="ID" size="3" maxlength="3"></td>
        </tr>
        <tr>
          <td align="right"><b> Name:</b></td>
          <td><input type="text" name="Name" size="30" maxlength="30"></td>
        </tr>
      </table>
      <td> <input type="submit" value="Send Data"> <input type="reset" value="Reset"> </td>
    </form> <br> <br>
    <form action="/kobetsu" method="Get">
      <table border="2">
        <h3>Get a Fruit Information Data</h3>
        <tr>
          <td align="right"><b> ID:</b></td>
          <td><input type="text" name="ID" size="3" maxlength="3"></td>
        </tr>
      </table>
      <td> <input type="submit" value="Send Data"> <input type="reset" value="Reset"> </td>
    </form> <br>
    <form action="/taberareta" method="POST">
      <table border="2">
        <h3>Delete Fruit Data (T_T) </h3>
        <tr>
          <td align="right"><b> ID:</b></td>
          <td><input type="text" name="ID" size="3" maxlength="3"></td>
        </tr>
      </table>
      <td> <input type="submit" value="Send Data"> <input type="reset" value="Reset"> </td>
    </form>
  </body>
</html>
```

#### 4.hit.html
キャッシュにヒットしたときに返される画面です。
app.py内の【return render_template('hit.html', cachevalueID=cachevalue['ID'],cachevalueName=cachevalue['Name'])】で返される画面です。


```html:hit.html
<head>
</head>
<body>
  <h1>hit!!!  ID {{ cachevalueID }} は {{ cachevalueName }} です。</h1>
</body>
```

#### 5.Nothit.html
キャッシュにヒットしなかったときに返される画面です。
app.py内の【return render_template('Nothit.html', resultsID=r['ID'], resultsName=r['Name'])】で返される画面です。

```html:Nothit.html
<head>
</head>
<body>
  <h1>Nothit...  ID {{ resultsID }} は {{ resultsName }} です。 </h1>
</body>
```

構成や各ファイルの内容以下URLの説明が非常に分かりやすかったので、こちらもご覧ください。
https://qiita.com/kiyokiyo_kzsby/items/0184973e9de0ea9011ed


### キャッシュの動作テスト
キャッシュの動作テストとその他の動作についてもテストを行います。
テストは以下の順序で行います。

```
1.フルーツ個別検索(hitなし)
2.フルーツ個別検索(hitあり)
3.フルーツ追加
4.フルーツ個別検索(3の追加時にキャッシュクリアされたためhitなし)
5.フルーツ個別検索(hitあり)
6.3で追加したフルーツの個別検索
7.3で追加したフルーツの削除
8.3で追加したフルーツを検索しデータなし
9.フルーツ個別検索(6の削除時にキャッシュクリアされたためhitなし)
``` 

#### 0.top画面はこんな感じ
![top.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/7b1fe06c-46c3-6e9c-a73e-2cc378f8365c.png)

#### 1.フルーツ個別検索(hitなし)
初回検索なのでNothit.htmlを返します。
![kekka_001.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/79948788-129c-9777-9373-ea9fd6a5ea4f.png)


#### 2.フルーツ個別検索(hitあり)
1で検索されておりMemcachedに格納されている【cache.set(str(ID) ,r ,time=600)】ためhit.htmlを返します。
![kekka_002.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/61c2a9ae-02c8-7f5b-41d7-f01303b91d4d.png)

#### 3.フルーツ追加
新規フルーツの追加
![kekka_003_1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/f9eea0a9-855a-04ab-19a8-0e39124f51ca.png)

確認画面。これは専用のhtml画面では無くapp.pyのreturn【return 'Welcome ID :  '  + str(ID) +'!!!'】を返すものです。
![kekka_003_2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/d9e1817b-46d2-f173-46c3-1a0a92d6172d.png)

#### 4.フルーツ個別検索(3の追加時にキャッシュクリアされたためhitなし)
キャッシュがクリアされた【cache.flush_all()】のでNothit.htmlを返します。
![kekka_004.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/3cf44efb-cc77-d9c6-a353-099164763c79.png)

#### 5.フルーツ個別検索(hitあり)
4でキャッシュが格納されたのでhit.htmlを返します。
![kekka_005.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/53daf502-dff8-fb55-1733-fe0e7d86c06b.png)

#### 6.3で追加したフルーツの個別検索
検索画面
![kekka_006_1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/1cbac5c5-075d-2156-512e-17d27ff001b3.png)


検索結果。ID3に関しては初回検索のためNothit.htmlを返す。
![kekka_006_2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/f3add203-6828-bd79-e6c0-049dda029723.png)

#### 7.3で追加したフルーツの削除
削除画面。フルーツが食べられたことを示します。
![kekka_007_1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/3d718905-befd-4631-1a94-f581f5c3c75a.png)

確認画面。これは専用のhtml画面では無くapp.pyのreturn【return 'Delete ID :  '  + str(ID) +' (T_T)'を返すものです。![kekka_007_2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/9b9b969a-a7ac-41ee-6379-6df9c8a3a37d.png)
】

#### 8.3で追加したフルーツを検索しデータなし
検索画面
![kekka_008_1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/c9ca2639-336a-9dc3-88be-a43a55fa6c67.png)


削除されたIDが無いことを確認。app.pyの以下のRDSにデータが無かった時に返されるエラーを拾って画面に返すようしています。

```
    except TypeError: #エラー処理は結構適当です。
        connection.rollback()
        return 'No ID!!!'
```
![kekka_008_2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/29d876da-1364-3528-3338-995ae5d27483.png)

#### 9.フルーツ個別検索(6の削除時にキャッシュクリアされたためhitなし)
検索画面
![kekka_009_1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/502f9624-8b6e-f3c5-1548-a3e41d98373a.png)

確認画面。7の削除の際にキャッシュがクリアされたのでNothit.htmlを返します。
![kekka_009_2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/dc4f5ed9-2cd9-208f-5297-52c359e4a62c.png)

## おわりに
簡単にキャッシュサーバを作成し簡単なプログラムでキャッシュのテストを実行したものですが、キャッシュとキーバリューの感覚はつかめました。最後まで読んでいただきありがとうございました。