id変換表を参照して URL リダイレクトさせるシステムをサーバレスで構築する

＃とある業務で「id変換表を参照して URL をリダイレクト」させる必要があったので、サーバレスで実現できるか検証をした際の備忘録

## 1. 要件
* サイトA：https://example-A.com をクローズする。ただし、サイトAの一部のページはサイトB：https://example-B.com へ移植して、サイトAクローズ後も閲覧できるようにする（ここではページの移植作業については割愛）。

* サイトAの移植対象ページのURLは、サイトAクローズ後も生かしておく。そのURLへアクセスすると、サイトBの移植後のURLへリダイレクトさせる。
 * サイトAの移植対象ページのURLの例<pre>https://example-A.com/before/demo.php?id=100002</pre>
 * サイトBの移植先ページのURLの例<pre>https://example-B.com/after/10100002</pre>

* 移植先のサイトBの id は移植元のサイトAの id と異なるものになってしまうため、id 変換表を用意する。<br />
 * id変換表の例
<pre>
before	after	
100000	10100000
100001	10100001
100002	10100002
100003	10100003
100004	10100004
100005	10100005
</pre>

## 2. システム概要
#### 2.1 id変換表作成
* CSV形式のデータを DynamoDB へインポートする。ただし、CSV形式のデータは、そのままのフォーマットでは DynamoDB へインポートできないため、インポート用のフォーマットに整形する必要がある。

* S3, AWS Data Pipeline を使う。
 * DynamoDB インポート形式のファイルを S3 バケットへアップロードする。Data Pipeline で S3 バケットファイルのデータを DynamoDB のテーブルへインポートする。<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/877580e7-9f41-8e26-8c5a-6ff4600daf3c.png)
 
#### 2.2 リダイレクト設定

* Route53, ALB, Lambda(ALBのターゲット), DynamoDB を使う。
* 元の構成<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/40371335-5af0-03dc-2443-8e109fe6b5e1.png)

* リダイレクト構成案１
 * 今回は、この方法で構築した。
 * リダイレクト用ALBを用意する。
 * リダイレクト開始は、Route 53 で サイトA の Aエイリアスレコードを サイトA の ALB から リダイレクト用 ALB へ変更する。<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/8a305197-ec41-bfae-f4de-581f9c522a7c.png)

* リダイレクト構成案2
 * サイトAのALBを使用する。
 * リダイレクト開始は、サイトA の ALB のターゲットグループを、リダイレクト用Lambdaを設定したターゲットグループへ変更する。<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/30da2f19-3c31-7205-837f-0c6e11e8dce0.png)



## 3. システム構築

#### 3.1 id変換表を作成

* **DynamoDB テーブルを作成する**
 * テーブル名： id-mapping<br />プライマリキー： before (文字列）<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/d886e2e8-0e8d-c858-6f99-355bab002ff7.png)
<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/0d66aaa0-9761-2499-8216-aa499f773582.png)<br />

* **CSVファイルを作成する**
 * 作業PCで次のような csv ファイルを作成する。<br />id-mapping.csv<pre>
before,after
s,s
100000,10100000
100001,10100001
100002,10100002
100003,10100003
100004,10100004
100005,10100005
</pre>

* **CSVファイルを DynamoDB インポート用のフォーマットへに整形する**
 * 下記サイトの python スクリプトをそのまま利用する。<br />
【参考】[CSVを整形してDynamoDBにインポートできるようにするPythonスクリプト](https://www.blog.danishi.net/2019/07/11/post-1789/)
<pre>
$ python3 csv2json.py
CSVのパスを入力：id-mpping.csv
</pre><pre>
$ cat id-mapping.json
{"before": {"s": "100000"}, "after": {"s": "10100000"}}
{"before": {"s": "100001"}, "after": {"s": "10100001"}}
{"before": {"s": "100002"}, "after": {"s": "10100002"}}
{"before": {"s": "100003"}, "after": {"s": "10100003"}}
{"before": {"s": "100004"}, "after": {"s": "10100004"}}
{"before": {"s": "100005"}, "after": {"s": "10100005"}}
</pre>

* **整形された id-mapping.json ファイルを置く S3バケットを作成する**
 * S3バケット名を 「dynamodb-datapipeline-************ 」 とする （ "************" はユニークな文字列）。
 * S3バケット 「dynamodb-datapipeline-************」 に「csv」というフォルダを作成する。<br />
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/1f1cd4d9-6a02-d2e0-b3bb-90baaed30f45.png)

* **Data Pipeline を作成する**
 * Name: import dynamodb from s3<br />Source: Import DynamoDB backup data from S3
 * Input S3 folder: s3://dynamodb-datapipeline-************/csv/<br />Target DynamoDB table name: id-mapping<br />Region of the DynamoDB table: ap-northeast-1</br>
 * IAM roles： default<br />【参考】[AWS Data Pipeline の IAM ロール](https://docs.aws.amazon.com/ja_jp/datapipeline/latest/DeveloperGuide/dp-iam-roles.html)<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/0681c217-4d88-2035-89f5-32108706e272.png)

* **Data Pipeline を Activate する**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/ac42dee6-fdae-6817-7f2e-936dc5e9248b.png)

 * Data Pipeline の裏で、EMRクラスターとEC2が作成されている。それらのプロビジョニングが完了するまで、Data Pipeline のステータスは「WAITING_FOR_RUNNER」になっている。<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/2ae0cff4-13e3-b922-7fa3-5a1d76f7aa8b.png)

 * EMR クラスターのステータス<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/cc4b82c5-e973-f18b-0733-c8d9d5004b44.png)

 * EMR クラスターのプロビジョニングが終わると、Data Pipeline の DynamoDB インポート処理が実行される。完了すると、ステータスは「FINISHED」となる。<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/52a348f6-515f-9d30-b8b0-7115e90677f8.png)

 * EMR クラスターは終了済となり、EMPクラスターやEC2が削除される。<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/8af16009-c598-5d69-e239-8ac174ebb571.png)

 * DynamoDB 「id-mapping」 テーブルにデータがインポートされていることを確認する。<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/f0ea7bbc-96f0-1fc6-37c5-adee6b411bdd.png)



#### 3.2 リダイレクト設定 - リダイレクト構成案1 のシステムを構築する

* **リダイレクト用 ALB 作成**
 * ALB 名を 「redirect」 とする。 

* **Lambda にアタッチする IAM ロールを作成する**
 * ロールを使用するサービスで 「lambda」 を選択<br /> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/d4b4c2d2-7ea5-7928-dfa2-84c492b373ed.png)

 * 次のポリシーをアタッチする（今回は検証なので、FullAccessにしている）。<br />AmazonDynamoDBFullAccess<br />AWSLambdaBasicExecutionRole

 * ロール名を 「lambda_redirect」 とする。

* **Lambda作成**
 * 一から作成<br />関数名： redirect<br />ランタイム： Python 3.7<br />実行ロール： lambda_redirect<br /> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/a9e58a5b-5006-1229-5fb3-16ddd7e090e5.png)
 
 * トリガーを追加<br />
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/ebde5941-803c-c084-33ad-3f017b01a721.png)

 * トリガーの設定<br />Application Load Balancer: redirect<br />リスナー： HTTPS:443<br />ホスト： example-A.com<br />パス: ＊ （ワイルドカード)<br />
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/40bac07d-7383-ef2d-e812-ff505606c66e.png)

* トリガーの設定が完了すると、ALB redirect のリスナーとターゲットグループに Lambda の設定が自動的に反映される。
 * HTTPS:443 のリスナーのルール設定<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/9f375d8f-bb41-cabf-335e-dcec8a97c8e7.png)
 
 * ターゲットグループ: lambda-25bxlPn0KCbRK4JVO3Ii<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/a2eee40f-9e2e-981c-8420-39fceb74241e.png)

* ターゲットグループ: lambda-25bxlPn0KCbRK4JVO3Ii のヘルスチェック設定
 * ヘルスチェックの編集<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/8cef3668-0438-61f4-f923-9b7c9fc368ba.png)
 * 有効化<br />パス： /healthcheck<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/2c6c2c08-fd3d-8798-1deb-d0607ebb2d27.png)

* Lambda関数作成（これがこのシステムの肝）
 * 関数コード<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/48df1df8-440a-bba4-b4d8-f63863baa8a4.png)
 * 実際のコード

```python
import boto3
from botocore.exceptions import ClientError
from boto3.dynamodb.conditions import Key
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
    logger.info(event)
    for key, value in event["queryStringParameters"].items():
        if key == "id":
            table = boto3.resource("dynamodb").Table("id-mapping")
            item = table.get_item(Key={"before":value})
            if ("Item" in item):
                after_id = table.get_item(Key={"before":value})["Item"]["after"]
                location = "https://example-B.com/after/" + after_id
                response = {
                    "statusCode": 302,
                    "isBase64Encoded": False,
                    "headers": {
                        "Location": location
                    }
                }
                return response
            else:
                response = {
                    "statusCode": 302,
                    "isBase64Encoded": False,
                    "headers": {
                        "Location": "https://example-B.com"
                    }
                }
                return response
        else:
            response = {
                "statusCode": 302,
                "isBase64Encoded": False,
                "headers": {
                    "Location": "https://example-B.com"
                }
            }
            return response
    if ( event["path"] == "/healthcheck" ):
        response = {
            "statusCode": 200,
            "isBase64Encoded": False,
            "headers": {
                "Content-Type": "text/html; charset=utf-8"
            }
        }
        return response
    else:
        response = {
            "statusCode": 302,
            "isBase64Encoded": False,
            "headers": {
                "Location": "https://example-B.com"
            }
        }
        return response
```

* コードの概要
 * 「https://example-A.com/before/demo.php?id=100002」 というリクエストを Lambda が受け取ったら、queryStringParameters にクエリ文字列 「id=100002」が格納される。<br />【参考】[ALBがAWS Lambdaに必要な変更を行った後の変換済みリクエストの例](https://aws.amazon.com/jp/blogs/news/lambda-functions-as-targets-for-application-load-balancers/)

 * id=100002 の値 「100002」 をキーとして、DynamoDB から 「10100002」を取り出して、リダイレクト用のURL 「https://example-B.com/after/10100002」 を生成して、リダイレクトする。
 * id の値が DynamoDB テーブルにない場合は、「https://example-B.com」 へリダイレクトする。
 * クエリ文字列がない場合は、 「https://example-B.com」 へリダイレクトする。
 *  「https://example-A.com/healthcheck」 リクエストは、ステータスコード 200 を返す。
 *  「/healthcheck」パス以外のすべてのリクエストは、「https://example-B.com」 へリダイレクトする。

* テスト
 * クエリ文字列「id=100002」のテストを作成する<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/2e6bc235-f66d-0303-3fe2-3cadbacb135f.png)<br />↓<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/5c0939a5-93c5-97b0-28ab-d73e961c2484.png)<br />↓<br />テスト実行<br>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/5b023611-5c61-4539-1998-1953a377e5ea.png)<br />Location： https://exapmpe-B.com/after/10100002 <p /><p />


 * クエリ文字列「id=100010」（DynamoDBに存在しない）のテストを作成する<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/7b941c56-ac25-fa83-19ae-1c74b077ce08.png)<br />↓<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/4a2dbd1d-3fcf-d3eb-3eda-7669411e9954.png)<br />Location： https://exapmpe-B.com<p /><p />

 * 「/healthcheck」のテスト<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/7ee9cbd8-10b3-715c-fc9d-f487f528b874.png)<br />↓<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/1398119c-2d22-b387-0978-0188a4622a3b.png)<br />statusCode: 200<p /><p />

 * 「/test1/test2」のテスト<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/a6f1acb5-56a0-e8a7-d947-b3ef0594fbc3.png)<br />↓<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/6d9764f1-d12c-619a-ef0e-58ff427a3c95.png)<br />Location： https://exapmpe-B.com<p /><p />


* テストが問題なければ、リダイレクト時のステータスコードを 302 から 301 に変更して、
Route 53 で サイトA の Aエイリアスレコードを サイトA の ALB から リダイレクト用 ALB へ変更して、リダイレクトを開始する。