クラロワAPIをlambdaから叩いてLINEに送る

この記事は、[AWS LambdaとServerless Advent Calendar 2020](https://qiita.com/advent-calendar/2020/lambda)の11日目の記事です
昨日の記事は[_kensh](https://qiita.com/_kensh)さんによる「[AWS Lambda - Provisioned Concurrencyのベストプラクティス](https://qiita.com/_kensh/items/5ab0ba7ca71ca82685e3)」でした


こんにちは、meiheiです

Clash Royaleというゲームでクランの運営をしています
このゲームではAPI（[Clash Royale API](https://developer.clashroyale.com/#/)）が提供されており、ゲーム内のデータを取得することが出来ます

このAPIを使って、１週間以上の放置しているクランメンバーを取得し、LINEにアラートを送るシステムを作っていきます



## 概要
### 完成したもの
![スクリーンショット 2020-12-11 1.15.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/4b15322a-6fb8-75ae-bced-bf6e5a91c59c.png)
こんな感じに、`プレイヤー名`と`放置している期間`がLINEに届きます

### 構成

今回作っていくシステムの構成は以下の通りです

![aws-and-cr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/e0ed53c9-f396-d8eb-ba97-89e3b6ea5165.png)

クラロワAPIはIPアドレスを指定する必要があるので、NAT GatewayにEIPを割り当てています

1. VPCの設定
2. Clash Royale APIのKeyの作成
3. Line Notifyの設定
4. Lambda関数の作成・デプロイ（Serverless Framework）

の順で説明していきます

## VPCの設定
以下の記事を参考（というか全く同じ）に設定していきます

- [Lambdaから固定IPアドレスでkintoneと連携する方法](https://www.joyzo.co.jp/blog/2325)
- [【AWS/Lambda】lambdaに固定IPをつける/VPCに所属させる](https://qiita.com/shim-hiko/items/d085f05e8fdded648e1d)

まず、VPCを作成します
適当な名前タグを設定して、`IPv4 CIDR ブロック`は`10.0.0.0/16`にします
![スクリーンショット 2020-12-10 23.00.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/0cdf5c87-606e-e5b7-bcb4-85d91b850d94.png)
次に適当な名前のインターネットゲートウェイを作成して、先程作ったVPCにアタッチします
![スクリーンショット 2020-12-10 23.07.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/7d4933e1-8304-839c-22c0-1f4fb84d85d3.png)
![スクリーンショット 2020-12-10 23.08.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/f9e356b1-95e6-4384-0a9c-8bfba20a0e0e.png)  
そしてVPCの中に、publicとprivateの２つのサブネットを作成します
「サブネットを作成」からVPCに`vpc-lambda`を選択
![スクリーンショット 2020-12-10 23.25.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/27c4fabf-d47c-26e7-2810-b30f798b060a.png)
適当な名前のサブネット名を設定して、`IPv4 CIDR ブロック`は`10.0.10.0/24`にします
同様にprivateのサブネットも設定します。`IPv4 CIDR ブロック`は`10.0.1.0/24`にします
![スクリーンショット 2020-12-10 23.26.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/ca6c3075-c347-3e6c-b0ee-b37c66eea136.png)
次にVPC作成時に作られたルートテーブルを選択して、送信先に`0.0.0.0/0`、ターゲットに先ほど作成したインターネットゲートウェイを設定します
![スクリーンショット 2020-12-10 23.35.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/3495fc7d-c696-c149-9dd3-8cb43a032691.png)



次に、IPアドレスを固定するNAT Gateway＆EIPを作成します（VPCの設定はあとちょっとです！）
適当な名前を入力し、サブネットに先程作ったpublicの方のサブネットを選択します。そして`Elastic IP 割り当て`というボタンを押してEIPを生成し、NAT Gatewayを作成します。
![スクリーンショット 2020-12-10 23.41.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/49774efa-9a97-5107-bd2e-4a7e0edede9f.png)

最後に、VPCの新規ルートテーブルを作成し、送信先`0.0.0.0/0`、ターゲットに先ほど作成したNAT Gatewayを設定します
`サブネット関連付けの編集`からprivateのサブネットを関連付けで完了です
![スクリーンショット 2020-12-10 23.47.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/2459ae9c-ba7b-eb84-c510-25c156ad2a5e.png)
![スクリーンショット 2020-12-10 23.48.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/b53cb258-3ce3-1600-30b5-40a07a868a39.png)
![スクリーンショット 2020-12-10 23.51.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/622336a1-a051-caf9-18dc-3bbf81b89ac5.png)

これで、Elastic IPの`割り当てられた IPv4 アドレス`に表示されているIPアドレスが、LambdaからAPIを叩く時に使われる固定IPアドレスになります
![スクリーンショット 2020-12-10 23.52.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/c97543e9-550a-f929-9f18-7001e5f50ff1.png)


## クラロワAPIのKeyの作成
前提として、クラロワのdeveloperアカウントを作成してあるとします。

[developer.clashroyale.com](https://developer.clashroyale.com/#/) の右上の名前のところから`My Account`を選択し、`Create New Key`から新しい鍵を作成します

`KEY NAME`にわかりやすい名前、`DESCRIPTION`にわかりやすい説明文、`ALLOWED IP ADDRESS`に先程作成したIPアドレスを入力し、`Create Key`で作成します。
![スクリーンショット 2020-12-11 0.19.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/1eb27c1b-9d21-4723-c2a4-a4ea271f6840.png)

すると`KEY NAME`で設定した名前の鍵が生成されています
![スクリーンショット 2020-12-11 0.19.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/a9cf0e1a-f04b-a820-fa8d-bbe88ed5421a.png)

こちらの`TOKEN`を使います

## LINE Notifyの設定
[LINE Notify](https://notify-bot.line.me/ja/) はWEBサービスから簡単にLINEに通知を送れるサービスです

まずログインしてもらって、右上の名前のところから`マイページ`に移動し、下の方にある`アクセストークンの発行(開発者向け)`から`トークンを発行する`を選択します

適当なトークン名を記入し、`1:1でLINE Notifyから通知を受け取る`を選択してトークンを発行します
![スクリーンショット 2020-12-11 0.15.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/52fc9814-bc5f-1a32-50ca-4e16c25312ca.png)
発行したトークンが表示されるので、こちらをコピーして保存します（トークンが表示されるのはコレきりなので、コピーを忘れたら再発行しましょう）
![スクリーンショット 2020-12-11 0.15.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/82366a0d-4451-1a48-6d6a-33ee17d45736.png)


## Lambda関数の作成・デプロイ（Serverless Framework）
前提として、Serverless Frameworkが使える環境にします。以下の記事がおすすめです

- [今から始めるServerless Frameworkで簡単Lambda開発環境の構築](https://dev.classmethod.jp/articles/easy-deploy-of-lambda-with-serverless-framework/)

新規のサービスを作成します

```bash
$ serverless create --template aws-python3 -p cr-abandon-to-line
```

次に、必要なプラグインを入れます

```bash
$ npm install --save serverless-python-requirements
$ npm install -D serverless-dotenv-plugin
```

`serverless-python-requirements`は`pip install`した外部モジュールを取り込みます（[参考](https://dev.classmethod.jp/articles/managing-external-modules-with-serverless-framework-plugin/)）
`serverless-dotenv-plugin`は`serverless.yml`に直接書きたくない環境変数を設定ファイルからいい感じ取り出してくれます（[参考](https://qiita.com/ozaki25/items/c855c3c7bcb45515539f)）

プロジェクトディレクトリ下に`.env`ファイルを作成し、先程発行したトークンなどを入れておきます

```
CR_ACCESS_KEY=[クラロワAPIのトークン]
LINE_NOTIFY_ACCESS_TOKEN=[LINE Notify用に発行したトークン]
VPC_SECURITY_GROUP_IDS=[VPCのセキュリティグループのID]
VPC_SUBNET_IDS=[private-subnetのid]
```

LambdaがVPCのprivateサブネットに入るように`serverless.yml`に記述します（[参考](https://dev.classmethod.jp/articles/serverless-framework-lambda-function-run-in-vpc/)）

```serverless.yml
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "ec2:CreateNetworkInterface"
        - "ec2:DescribeNetworkInterfaces"
        - "ec2:DeleteNetworkInterface"
      Resource:
        - "*"
  vpc:
    securityGroupIds:
      ${env:VPC_SECURITY_GROUP_IDS}
    subnetIds:
      ${env:VPC_SUBNET_IDS}
```

また、Pythonから環境変数として呼び出しを行うので、その設定も`serverless.yml`に記述します

```serverless.yml
  environment:
    CR_ACCESS_KEY: ${env:CR_ACCESS_KEY}
    LINE_NOTIFY_ACCESS_TOKEN: ${env:LINE_NOTIFY_ACCESS_TOKEN}
```

そして、クラロワAPIを叩いて、データを整形し、LINE Notifyに送るように実装します。

```handler.py
import json
import os
import requests
from datetime import datetime, timedelta
from typing import Dict, List


CR_BASE_URL = 'https://api.clashroyale.com/v1'
LINE_NOTIFY_URL = "https://notify-api.line.me/api/notify"
CR_ACCESS_KEY = os.environ['CR_ACCESS_KEY']
LINE_NOTIFY_ACCESS_TOKEN = os.environ['LINE_NOTIFY_ACCESS_TOKEN']


def init_headers(api_key: str) -> Dict[str, str]:
    """初期化されたヘッダー情報の辞書を返す"""
    return {'authorization': f'Bearer {api_key}'}

def get_member(clan_tag: str) -> Dict[str, str]:
    """クラロワAPIからclan_tagのメンバー情報をGETする"""
    clan_tag = clan_tag.replace('#', '%23')
    url = CR_BASE_URL + f'/clans/{clan_tag}/members'
    headers = init_headers(CR_ACCESS_KEY)

    res = requests.get(url, headers=headers)

    return res.json()

def last_seen_to_datetime(last_seen: str) -> datetime:
    """last_seenの記法からdatetime型に変換する"""
    return datetime.strptime(last_seen, '%Y%m%dT%H%M%S.000Z')

def filter_by_last_seen(items: List[dict], dead_line: datetime) -> List[dict]:
    """最終ログインがdead_lineのクラメンの情報を抽出する"""
    before_last_seen = lambda i: last_seen_to_datetime(i['lastSeen']) < dead_line

    return [item for item in items if before_last_seen(item)]

def generate_message(cr_items: List[dict]) -> str:
    """line notify送信用のメッセージを作成する"""
    message = ''
    for member in cr_items:
        last_seen_diff = str(datetime.now() - last_seen_to_datetime(member['lastSeen'])).split('.')[0]
        message += f'\n{member["name"]}: {last_seen_diff}'

    return message

def send_line(message: str):
    """LINE Notifyにmessageを送る"""
    headers = init_headers(LINE_NOTIFY_ACCESS_TOKEN)
    data = {'message': message}

    requests.post(LINE_NOTIFY_URL, data=data, headers=headers)

def lambda_function(event, context) -> Dict[str, str]:
    """実行用の関数"""
    data = get_member('#228UCY92')
    dead_line = datetime.now() - timedelta(days=1)
    filtered_data = filter_by_last_seen(data['items'], dead_line)

    if (filtered_data):
        message = generate_message(filtered_data)
        send_line(message)

    return f'ok: {len(filtered_data)}件取得しました'
```

最後に、`hundler.py`の`lambda_function`が定期的に実行されるように`serverless.yml`に記述します（[参考](https://dev.classmethod.jp/articles/serverless-framework-lambda-cron-execute/)）

```serverless.yml
functions:
  abandon-member-to-line-notify:
    handler: handler.lambda_function
    events:
      - schedule: cron(0 0 * * ? *)  # 日本時間で朝の９時
```

AWSにデプロイしたら、このシステムの完成です

```bash
$ serverless deploy
```

## 終わりに
何でも楽にやってくれるLambdaさんだと思っていたら、IPアドレスが固定できないため、なかなか面倒くさい構成になりました

気になるお値段は11日ぐらい使って1,500円ぐらい。**月換算だと4,000円ぐらい**です（高い）
![スクリーンショット 2020-12-11 1.46.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280373/b10dfb31-5b45-4f90-84d7-7047cfb5b00e.png)
NAT Gatewayが動いているだけでお金がかかるので、こんなにも高くついているようです

AWSの勉強目的だといいですけど、クラロワAPI＋Lambdaは普通にオススメできないですね。

## ソースコード
今回のソースコードはすべて[GitHub](https://github.com/meihei3/cr-lambda-services)にあげてあります。READMEは書いてません…

## クランメンバー募集
僕の運営しているクラン「ことりカフェ」ではメンバーを募集しています

ほぼ毎日クラロワやってるけど、そこまでガチ勢じゃない、まったりプレイヤーが集うクランです
興味ある方はぜひ[Twitter](https://twitter.com/app1e_s)にてご連絡ください！

---

明日は[22t](https://qiita.com/22t)さんによる「chaliceで簡単pythonボット制作」です。お楽しみに！