GCP で運動不足を解消したかった



これは [ニフティグループ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/nifty) 5日目の記事です。ジョギングやプランクしながら読んでいただければ幸いです。

昨日は @saikei さんの「[時間のかかるコマンドの実行結果や処理完了を気軽にLINEに通知したい - LINE Notify](https://qiita.com/saikei/items/b10763a41f7ec2031119)」でした。
LINE Notify の存在を初めて知ったので、目からウロコです・・・！



---

今年は働き方が大きく進んだ1年でした。

ニフティも2020年3月から在宅勤務が可能になり、本記事執筆時 (2020年12月) も基本在宅勤務というかたちで日々の業務に取り組んでいます。

かれこれ1年近く在宅勤務を行っていると、在宅勤務のメリット・デメリットが見えてきたのではないでしょうか？
そして、1つの問題が浮かび上がってきました。


# 運動不足

そう、運動不足です。在宅勤務は通勤による「ストレスや疲労からの解放」と引き換えに、「圧倒的な運動不足」という副作用をもたらしました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200620/87479dcd-2f50-a308-dfe2-576a9750fa86.png" width="350">


これは今年の夏の私の万歩計データです。改めて見るとひどいですね・・・。
(水曜日は少し歩いたようです。せいぜい近所のコンビニにご飯を買いに行ったとかそんなところでしょう。)



# そうだ、筋トレをしよう

自宅にいながらこの慢性的な運動不足を改善する手っ取り早い方法、それは筋トレしかありません(多分)。
ですが、いざ「筋トレするぞ！」と意気込んでも、継続するモチベーションが上がらず結局3日坊主になるのがオチです。今まで3回ほど同じ轍を踏みました。
以前友人から格安で引き取ったダンベルも、重すぎて文鎮にすらできません。

筋トレを継続するモチベーションが上がらない要因として、以下が考えられます。

- 何をすればいいかわからない
- どんなフォームで動かせばいいのかわからない
- トレーニング内容に変化がない
- そもそもやるのを忘れる

つまり

- やることがはっきりしている
- 正しいフォームがすぐわかる
- トレーニング内容がマンネリ化しない
- やるきっかけをくれる

が明確であれば、自ずとモチベーションが上がるはずです。

ということで、これらの問題を(きっと)解消できるような簡単な仕組みを作ってみました。

# 作ったもの

前置きが長くなってしまいましたが、上記の問題を解決するために「今日やる筋トレメニューを提案して通知する Slack bot」を作りました。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200620/4a78eb48-4469-3b4c-033c-04df9543d61d.png" width=600>



## 技術的なポイント

coursera で GCP のコースを受講したこともあり、図のような GCP サービスを利用したアーキテクチャで実装しました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200620/5a3af765-5e7e-2ed1-4c5b-fe28df563adb.png" width=500">

それぞれの役割を簡単に解説していきます。

### Cloud Scheduler

Cloud Scheduler は GCP における cron ジョブスケジューラーです。
ジョブの実行スケジュールは unix-cron 形式 (`* * * * *`) で指定できます。

Cloud Scheduler によって作成された cron ジョブは、指定したスケジュールでタスクが実行される場所 (ターゲット) に送信されます。


今回作成したジョブです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200620/56eff2d2-762c-8cd1-9fca-2b51204752fc.png" width=400>
(なんか表示がバグってますが気にしない)

Cloud Scheduler ジョブの作成は GCP コンソールから行います。シンプルな UI なので直感的に設定できます。

日本時間毎日 21:00 に Cloud Pub/Sub に向けて `daily_workout` というトピックでメッセージを Publish する、という設定です。

ターゲットに指定した Cloud Pub/Sub とはどんなサービスでしょうか？順を追って説明します。


### Cloud Pub/Sub


Cloud Pub/Sub はリアルタイム Pub/Sub メッセージングモデルのサービスです。
Pub/Sub メッセージングモデルとは、Publisher から「トピック」と呼ばれる登録先にメッセージが送信 (Publish) され、そのトピックに対して配信を登録 (Subscribe) した Subscriber がメッセージを受信できるモデルです。

今回は `daily_workout` というトピックを作成しました。
Cloud Scheduler で実行される Cloud Pub/Sub が `daily_workout` 宛にメッセージを Publish することで、`daily_workout` トピックを Subscribe している Cloud Functions 関数が実行される、というからくりです。

GCP の公式ドキュメントに同様のチュートリアルがあります。
[Pub/Sub を使用して Cloud ファンクションをトリガーする](https://cloud.google.com/scheduler/docs/tut-pub-sub?hl=ja)

#### トピックの作成

トピックの作成は SDK を使って

```bash
gcloud pubsub topics create {トピックID}
```

のように作成することができます。とても簡単です。


### Cloud Functions

Cloud Functions は GCP におけるサーバレスな Function as a Service (FaaS) です。
記事執筆時点では

- Node.js
- Python
- Go
- Java 

ランタイムが使用できます。今回は Python3.7 を利用しました。

#### 作った関数の概要

後述する Cloud Firestore に登録された筋トレの情報から、各部位ごとのメニュー (回数、重量、正しいフォームの動画) をランダムに選択して Slack 通知する、というとてもシンプルな実装です。

#### Cloud Firestore との連携

各言語のためのライブラリが用意されています。Python であれば、[google\-cloud\-datastore](https://pypi.org/project/google-cloud-datastore/) があります。 
https://pypi.org/project/google-cloud-datastore/
[Datastore Client](https://googleapis.dev/python/datastore/latest/client.html)


#### 関数のデプロイ

GCP コンソールから関数を作成することもできますが、今回はローカル環境で作成した Python 環境のスクリプトを Cloud SDK を利用してデプロイしました。

```bash
gcloud functions deploy {関数名} --region asia-northeast1 --runtime python37 --trigger-topic daily_workout --set-env-vars WEBHOOK_URL={WEBHOOK_URL}
```

指定するオプションのポイント

- `--trigger-topic`
  - Cloud Pub/Sub で作成した Subsclibe するトピックを指定する
- `--set-env-vars`
  - WEBHOOK_URL などはソースに含めたくないので、環境変数としてデプロイ時に指定する


また、スクリプトで利用する外部ライブラリなどは、ローカル環境で作成した Pipfile, Pipfile.lock ごとデプロイすることで、よしなにインストールしてくれます。便利。


### Cloud Firestore

Cloud Firestore は GCP の キーバリュー型 NoSQL サービスです。トランザクションや SQL ライクなクエリをサポートしています。
今までは Cloud Datastore という名前でしたが、2019年2月に Firebase との連携が強化された Cloud Firestore として新しくなりました。

Firestore にはネイティブモードと Datastore モードの2種類がありますが、今回はシンプルにキーバリューストアとして利用したかったので Datastore モードを使っています。


#### 筋トレのメニュー

Firestore に登録するデータは以下のようにしています。

| key | value |
| --- | ----- |
| name | トレーニングの種目名 |
| body | 上半身、下半身 |
| part | 更に細かい部位 (肩、腹筋とか) |
| reps | 1セットあたりの回数 |
| sets | 何セットやるか |
| weight | ダンベルの重さ |
| movie | フォームの動画 |
 
フォームの動画は [Tarzan](https://www.youtube.com/playlist?list=PLKbxY5IOvElHA4hYTk-FGm6nKjKzc2Z-d) に1種目30秒ほどのものがあったので活用させていただきました。

(重量や回数は RM 法などを用いてパーソナライズするのが良いですが、とりあえず適当に設定しました。)


# まとめ

日頃の運動不足を解消できるかもしれない Slack bot を作ってみました。
稼働しはじめの頃は、自分の知らない筋トレメニューが提案され非常に楽しくトレーニングを続けることができました。フォームの動画もつけたことで怪我のリスクも減り、快適なトレーニングライフが送れると思っていました。
3日坊主は脱することができましたが、1ヶ月を過ぎたあたりから慣れが飽きに変わり、虚しく Slack 通知が流れ続けるチャンネルができてしまいました。30日チャレンジという言葉がありますが、30日を超えたあたりでやってくる「飽きの第2波」をどう乗り越えるかがこれからの課題かもしれません。


---

明日は @fuku710 さんの「CDKを使ったLambdaデプロイの最適解を考える」です！