AWSを使って不特定多数の猫に誕生日を祝われたい（実装編）

# はじめに
皆さん、こんにちは。  
今年はありがたいことに、[Japan AWS Top Engineers 2024](https://aws.amazon.com/jp/blogs/psa/2024-japan-aws-top-engineers/) に選出されました。また、Top EngineersのAdvent Calendarがあるという情報を発見し、記念に参加することにしました。  

偶然にも私は12月17日が誕生日なので、この日を投稿日としてエントリーしました。  
が、数年ぶりの投稿となり、正直ネタがありません。会社からの帰宅途中に「何を書こうか」と考えてみましたが、特にTop Engineer感のあるアイデアは思いつきませんでした。そこで今回は、 **殺伐とした日々の生活に少しでも癒やしを与えるような題材** にしようと思います。

また **「記事を書きながら実装をタイムアタック形式で行う」** という追い込みルールで進めたいと思います。現在、12/2 21:45です。このまま日付が変わるまでに終わらせることを目標に進めていきます。

# 作るもの
モチベーションを高めるために、私が好きなものを組み合わせた物を作ります。
- 猫
- AWS
- サーバレス
- ギャンブル

ということで、 **「誕生日に猫に祝われるギャンブル要素があるサーバレスアプリケーションをAWS上に作成」** し、来たる12/17の0時にハッピーバースデーメッセージを送ってもらいましょう。

# アーキテクチャ
皆さん、AWSアーキテクチャ図を書くのは好きですか？私は好きです。
でも、時間がありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/95eb0a98-f872-04e1-9c87-ed21f2bebda1.png)

できました。なんとかクリスマス仕様にしてみました。（ここまでで20分経過）

# 実装
タイムアタックなので IaC なんて書いてられません。
以下、①～④を実装していきます。
① Lambda から LINE-API を呼び出す
② Step Functionsの Map で Lambda を並列に呼び出す
③ EventBridge Scheduler から Step Functions を呼び出す 
④ 2024/12/17 0:00 に Scheduler をセットする

## ① Lambda から LINE-API を呼び出す
### ランダム猫画像の取得
猫画像はTheCatAPIを利用します。
https://developers.thecatapi.com/view-account/ylX4blBYT9FaoVd6OhvR?report=bOoHBz-8t

マネジメントコンソールを起動して、テスト関数を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/90f2497f-5847-40f5-6b22-bfb575cfebab.png)

```py
import json
import urllib.request

def lambda_handler(event, context):
    # 猫画像取得
    imeowge = getImeowge()
    print(imeowge)

    # 誕生日メッセージ取得
    # meowssage = getMeowssage()
    
    # LINEに送信
    # happyMeowthdayToLine(imeowge, meowssage)

def getImeowge():
    _url = 'https://api.thecatapi.com/v1/images/search'
    meow = sendRequest(_url)
    print(f'meow => {meow}')
    return meow[0]['url']

def sendRequest(_url, _data=None, _header={}):
    _req = urllib.request.Request(_url, _data, _header)
    try:
        with urllib.request.urlopen(_req) as _res:
            _body = _res.read().decode()
            return json.loads(_body)

        raise Exception('No Meow!!')
    except urllib.error.HTTPError as _err:
        print("HTTPError: " + str(_err.code))
        print(_err)
    except urllib.error.URLError as _err:
        print("HTTPError: " + _err.reason)
        print(_err)
    except Exception as e:
        raise e
```

リファクタリングしたい心を捨てて、次に行きます！
気づけば晩酌に用意したウイスキーを全く飲んでいないですが、どんどん行きます！

### LINEへの送信
LINE-API の API KEY を取得します。
はるか昔に作ったLINE MESSAGING APIを使います。LINE MESSAGING APIは無料で使えます。他の方の記事に詳しい利用方法があるので、是非そちらを参照して遊んでみてください。
https://developers.line.biz/ja/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/d818e237-355a-e53a-7ba2-5974a2be5ca1.png)

LINE送信を組み込みます。
```diff_python
import json
import urllib.request
+import os
+_env = os.environ

def lambda_handler(event, context):
    # 猫画像取得
    imeowge = getImeowge()
    print(imeowge)

    # 誕生日メッセージ取得
    # meowssage = getMeowssage()
    
    # LINEに送信
-   # happyMeowthdayToLine(imeowge, meowssage)
+   happyMeowthdayToLine(imeowge)

def getImeowge():
    _url = 'https://api.thecatapi.com/v1/images/search'
    meow = sendRequest(_url)
    print(f'meow => {meow}')
    return meow[0]['url']

+def happyMeowthdayToLine(imeowge, meowssage="Happy Meowthday!!"):
+   _url = 'https://api.line.me/v2/bot/message/push'
+
+   _data = json.dumps({
+       "to": _env["LINE_USER_ID"]
+       ,"messages": [
+           # imeowge
+           {
+               "type": "image"
+               , "originalContentUrl": imeowge
+               , "previewImageUrl": imeowge
+           }
+           # meowssage
+           ,{
+               "type": "text"
+               ,"text": meowssage
+           }
+       ]
+   }).encode()
+
+   _header = {
+     "Content-type": "application/json; charset=UTF-8",
+     "Authorization": "Bearer " + _env["LINE_ACCESS_TOKEN"]
+   }
+
+   print(f'_url, _data, _header => {_url}, {_data}, {_header}')
+   sendRequest(_url, _data, _header)

def sendRequest(_url, _data=None, _header={}):
    _req = urllib.request.Request(_url, _data, _header)
    try:
        with urllib.request.urlopen(_req) as _res:
            _body = _res.read().decode()
            return json.loads(_body)

        raise Exception('No Meow!!')
    except urllib.error.HTTPError as _err:
        print("HTTPError: " + str(_err.code))
        print(_err)
    except urllib.error.URLError as _err:
        print("HTTPError: " + _err.reason)
        print(_err)
    except Exception as e:
        raise e
```

無事LINEメッセージが飛んできました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/aaea075e-f729-5b94-79db-d80cef6aa27a.png)

送信時間の通り、すでに70分ほど格闘しています。  
それにしても、Pythonといえばスネークケースなのに、なぜかキャメルケースになっています。これは後でChatGPTにリファクタリングさせることにしましょう。

### ランダムに誕生日メッセージを生成する
ピンポイントに誕生日メッセージを作成するAPIが見つかりませんでした。
ひとまず落ち着くために、ウイスキーを流し込みます。

少し妥協し、 [アドバイスをくれるAPI](https://api.adviceslip.com/advice) を見つけました。これでいきましょう！
また、アドバイスは英語で返ってくるため [DeepL-API](https://www.deepl.com/ja/pro-api) を使って日本語併記にします。

```diff_python
import json
import urllib.request
import os
_env = os.environ

def lambda_handler(event, context):
    # 猫画像取得
    imeowge = getImeowge()

    # 誕生日メッセージ取得
-   # meowssage = getMeowssage()
+   admeowce = getAdmeowce()
+   admeowce_jp = getAdmewceJp(admeowce)

    # LINEに送信
-   happyMeowthdayToLine(imeowge)
+   happyMeowthdayToLine(imeowge, f'{admeowce}\n{admeowce_jp}')

def getImeowge():
    _url = 'https://api.thecatapi.com/v1/images/search'
    imeowge = sendRequest(_url)
    # print(f'imeowge => {imeowge}')
    return imeowge[0]['url']

+def getAdmeowce():
+   _url = 'https://api.adviceslip.com/advice'
+   admeowce = sendRequest(_url)
+   # print(f'admeowce => {admeowce}')
+   return admeowce['slip']['advice']
+
+def getAdmewceJp(admeowce):
+   _url = 'https://api-free.deepl.com/v2/translate'
+   _data = urllib.parse.urlencode({
+       'auth_key': _env["DEEPL_AUTH_KEY"]
+       ,"text": admeowce
+       ,"target_lang": "JA"
+   }).encode('utf-8')
+   _header = {
+       "Content-type": "application/x-www-form-urlencoded; utf-8"
+   }
+
+   admeowce_jp = sendRequest(_url, _data, _header)
+   # print(f'admeowce_jp => {admeowce_jp}')
+   return admeowce_jp['translations'][0]['text']

-def happyMeowthdayToLine(imeowge, meowssage="Happy Meowthday!!"):
+def happyMeowthdayToLine(imeowge, meowssage):
+   _msg = f'Happy Meowthday!!\n{meowssage}'
    _url = 'https://api.line.me/v2/bot/message/push'
    _data = json.dumps({
        "to": _env["LINE_USER_ID"]
        ,"messages": [
            # imeowge
            {
                "type": "image"
                , "originalContentUrl": imeowge
                , "previewImageUrl": imeowge
            }
            # meowssage
            ,{
                "type": "text"
-               ,"text": meowssage
+               ,"text": _msg
            }
        ]
    }).encode()
    _header = {
      "Content-type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + _env["LINE_ACCESS_TOKEN"]
    }
    # print(f'_url, _data, _header => {_url}, {_data}, {_header}')
    sendRequest(_url, _data, _header)

def sendRequest(_url, _data=None, _header={}):

    # print(f'sendRequest: {_url}, {_data}, {_header}')
    _req = urllib.request.Request(_url, _data, _header)
    try:
        with urllib.request.urlopen(_req) as _res:
            _body = json.loads(_res.read().decode())
            return _body

        raise Exception('No Meow!!')
    except urllib.error.HTTPError as _err:
        print("HTTPError: " + str(_err.code))
        print(_err)
    except urllib.error.URLError as _err:
        print("HTTPError: " + _err.reason)
        print(_err)
    except Exception as e:
        raise e
```

無事LINEメッセージが飛んできました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/4a3d6e42-9bb5-5930-ee23-1bb074641ea8.png)

はい、送信時間を見るとすでに日付を余裕で超えています。  
[DeepLのAPIのリファレンス](https://developers.deepl.com/docs/getting-started/your-first-api-request)を盛大に読み違えてしまい（正直、今もあまり理解できていません）、AuthorizationヘッダーにAPIキーを入れるなどしてハマりました。  
初めて利用するAPIとタイムアタックの焦りが重なると怖いですね。ここは落ち着いてウイスキーを流し込みましょう。

ちなみに、[DeepL-APIの利用状況を見ると](https://www.deepl.com/ja/your-account/usage)、50万文字まで無料のようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/c6d82287-fa97-1ab8-2922-9f6789582b09.png)

### 猫語への変換
スピードが見込めなくなったので、いっそクオリティを重視して猫語への変換を試みます。  
ChatGPT APIが無料なのか有料なのかよく分からなかったため、[Gemini API](https://ai.google.dev/gemini-api/docs?hl=ja)を利用してみることにしました。さすがGoogle、これはさくっと使えそうです。

無料でいいんですよね…？ おそらく無料で問題なさそうです。
https://aistudio.google.com/apikey?hl=ja

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/3101dd98-414d-546a-c40d-1c715a13fc36.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/989c370b-33d2-3343-4272-7638b5411a32.png)

```diff_python
import json
import urllib.request
import os
_env = os.environ

def lambda_handler(event, context):
    # 猫画像取得
    imeowge = getImeowge()

    # 誕生日メッセージ取得
    admeowce = getAdmeowce()
    admeowce_jp = getAdmewceJp(admeowce)
+   admeowce_jp = translate_meow(admeowce_jp)

    # LINEに送信
    happyMeowthdayToLine(imeowge, f'{admeowce}\n{admeowce_jp}')

def getImeowge():
    _url = 'https://api.thecatapi.com/v1/images/search'
    imeowge = sendRequest(_url)
    # print(f'imeowge => {imeowge}')
    return imeowge[0]['url']

def getAdmeowce():
    _url = 'https://api.adviceslip.com/advice'
    admeowce = sendRequest(_url)
    # print(f'admeowce => {admeowce}')
    return admeowce['slip']['advice']

def getAdmewceJp(admeowce):
    _url = 'https://api-free.deepl.com/v2/translate'
    _data = urllib.parse.urlencode({
        'auth_key': _env["DEEPL_AUTH_KEY"]
        ,"text": admeowce
        ,"target_lang": "JA"
    }).encode('utf-8')
    _header = {
        "Content-type": "application/x-www-form-urlencoded; utf-8"
    }

    admeowce_jp = sendRequest(_url, _data, _header)
    # print(f'admeowce_jp => {admeowce_jp}')
    return admeowce_jp['translations'][0]['text']

+def translate_meow(text):
+   _url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent"
+   _prompt = "語尾を猫語「～にゃん」に変換して。"
+   meow = sendRequest(
+       f'{_url}?key={_env["GEMINI_API_KEY"]}'
+       , _data=json.dumps({
+               "contents": [{
+                   "parts": [{
+                       "text": f'{_prompt}\n- {text}'
+                   }]
+               }]
+           }).encode()
+       , _header={"Content-Type": "application/json"}
+   )
+   # print(meow)
+   ret = meow["candidates"][0]["content"]["parts"][0]["text"]
+   return ret.rstrip("\n")

def happyMeowthdayToLine(imeowge, meowssage):
    _msg = f'Happy Meowthday!!\n{meowssage}'
    _url = 'https://api.line.me/v2/bot/message/push'
    _data = json.dumps({
        "to": _env["LINE_USER_ID"]
        ,"messages": [
            # imeowge
            {
                "type": "image"
                , "originalContentUrl": imeowge
                , "previewImageUrl": imeowge
            }
            # meowssage
            ,{
                "type": "text"
                ,"text": _msg
            }
        ]
    }).encode()
    _header = {
      "Content-type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + _env["LINE_ACCESS_TOKEN"]
    }
    # print(f'_url, _data, _header => {_url}, {_data}, {_header}')
    sendRequest(_url, _data, _header)

def sendRequest(_url, _data=None, _header={}):

    # print(f'sendRequest: {_url}, {_data}, {_header}')
    _req = urllib.request.Request(_url, _data, _header)
    try:
        with urllib.request.urlopen(_req) as _res:
            _body = json.loads(_res.read().decode())
            return _body

        raise Exception('No Meow!!')
    except urllib.error.HTTPError as _err:
        print("HTTPError: " + str(_err.code))
        print(_err)
    except urllib.error.URLError as _err:
        print("HTTPError: " + _err.reason)
        print(_err)
    except Exception as e:
        raise e
```

成功しました！非常に愛らしくなり、モチベーションアップです！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/02d495a6-c387-73c0-e683-fe4ac46410d2.png)

(終わり) ① Lambda から LINE-API を呼び出す
② Step Functionsの Map で Lambda を並列に呼び出す
③ EventBridge Scheduler から Step Functions を呼び出す 
④ 2024/12/17 0:00 に Scheduler をセットする

## ② Step Functionsの Map で Lambda を並列に呼び出す
次に、Step FunctionsからLambdaを繰り返し呼び出すように修正します。  
まずはStateMachineを作成しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/b7750c81-1edc-78ed-fdd3-1adf687cad6a.png)

最初のStateでは、猫の群れを探す関数を実行します。
心ばかりのギャンブル要素として、ランダムで最大10匹の猫を見つけることができるようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/9d97a715-47f0-09d4-4106-d32957c82ee0.png)
```py
import random

def lambda_handler(event, context):
    return {
        "myCats": list(range(random.randrange(10)))
    }
```

次のMap Stateでは、Lambdaの戻り値であるmyCatsの回数だけループを行います。
APIに優しくしたいので、最大同時実行数は3としています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/6071d23d-ce3b-77ee-b501-a8434db1a0e3.png)

ループ内のStateでは、先程作成した関数を指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/e5b6c5d7-f418-99c4-9b0f-c87260eafc67.png)

実行してみると、なんとたくさんの猫からお祝いのメッセージをもらいました。  
あまりありがたい言葉はもらえませんでしたが、可愛くて嬉しいですね。格言の意味を分かっていなさそうな翻訳が、さらに愛おしさを増してくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/bea9720a-f07e-a80d-e93b-af0d73651da2.png)

(終わり) ① Lambda から LINE-API を呼び出す
(終わり) ② Step Functionsの Map で Lambda を並列に呼び出す
③ EventBridge Scheduler から Step Functions を呼び出す 
④ 2024/12/17 0:00 に Scheduler をセットする

## ③ EventBridge Scheduler から Step Functions を呼び出す 
先程作成したStep Functionsを実行するSchedulerを作成します。  
テスト用に、今回は1回だけ起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/6385e527-b226-19f7-39d6-d4955e471117.png)

数え切れないほどのお祝いをもらえました！！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/093f4726-55f7-dc2c-49f8-647410524462.png)

(終わり) ① Lambda から LINE-API を呼び出す
(終わり) ② Step Functionsの Map で Lambda を並列に呼び出す
(終わり) ③ EventBridge Scheduler から Step Functions を呼び出す 
④ 2024/12/17 0:00 に Scheduler をセットする

## ④ 2024/12/17 0:00 に Scheduler をセットする
ということで、最終目標である私の誕生日にスケジューラを設定します。  
せっかくなので、毎年設定してみようと思います。APIたちが生きていれば、毎年サプライズのLINEが届くはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/69cd1fd2-5ece-ce8a-d509-644a7bfb7d63.png)

(終わり) ① Lambda から LINE-API を呼び出す
(終わり) ② Step Functionsの Map で Lambda を並列に呼び出す
(終わり) ③ EventBridge Scheduler から Step Functions を呼び出す 
(終わり) ④ 2024/12/17 0:00 に Scheduler をセットする

# 所要時間
裏で目標を120分に設定していたのですが、実際には270分かかってしまいました。
全てにおいて見立てより遅く、特にDeepL-APIで1時間以上ハマってしまい、無駄な苦労をしてしまいました。

# まとめ
これにて予約投稿しておきます。12/17 0時が楽しみです。
AWSはほとんど関係なかったような気がしないでもありませんが、自分を追い込むことで悪い癖を見つける良い機会になりました。

私個人の所感として、タイムアタックは、
+ **適当でも良いのでとにかくアウトプットする**
+ **時間をあまり使わない**

という2点から、投稿のモチベーションを保つのに役立つと考えました。
ということで、明日はTop Engineersらしく、AWSインフラ側のリファクタリング（CI/CD化など）にチャレンジしたいと思います。

それでは皆さん、良いクリスマスを！！

# (追記) 2024/12/17
ついにこの時がやってきました！12/17 0時です！
が、、1分待ってもLINEにメッセージが来ません。リファクタリングでバグったのでしょうか…

仕方なくAWSにログインし、StateMachineの実行結果を見ました。
なんと、 **運が悪く0匹** を引いてしまいました…ギャンブル要素なんてつけたばっかりに…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/57254bec-665a-01c2-48b1-0316369ef107.png)

こうなったら、自ら猫の群れに飛び込んで（手動実行して）みます。
すると、7匹の猫と出会えました！！（Iterationは#0から始まるので、#6が7匹目です。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/28d46fac-3733-08ed-7ba6-163f667c2bc4.png)

設定通り、3匹ずつ並んでアドバイスをくれています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/893c852a-9708-38c5-376f-12b763cc7b3f.png)

さあ、どのようなアドバイスをくれたのでしょうか。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/b67ca40b-ec31-9512-529c-720272e0942c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/f1c9b47a-986b-2061-837f-86e0f3b6da53.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/24972008-6410-112a-937c-4812e19dcbb7.png)

金言をいただきました。

```
Enjoy a little nonsense now and then.
たまにはくだらないことを楽しもうにゃん。
```

それでは明日の[リファクタリング編](https://qiita.com/siruko/items/88058fed2461f769ccc6)もお楽しみに。