Serverless Frameworkでニュースサイトの新着記事をLINEで通知するアプリを作る

アプリケーション構築を学習する一環として、RSSフィードで配信された記事をLINE Messaging API経由で通知するアプリをServerless Frameworkで作りました。

友達追加すると、
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/377284f2-45b3-6229-025e-c5401e4ca0ca.png" alt="follow" width="50%" />

新着記事を定期配信したり、手動で取得したりすることができるアプリです。
![subscribe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/f434fcda-7cf1-1b54-0324-99dbd5793f7d.png)


成果物は[Github](https://github.com/flets708/feed-notify)にアップロードしていますが、せっかくなのでアプリの概要と、ハマったポイントをこちらにも投稿しておこうと思います。（以下、Github上のREADME.mdのほぼ抜粋です）

# 概要

* RSSFeedで配信された記事を、LINE Messaging API経由で購読する
* 購読対象の記事は、設定用サイトで追加・削除・有効化・無効化が可能
* 定期購読のほかに、LINEでメッセージを送ることで記事を手動購読することも可能
* システム構成は以下の通り。AWS上にソースをデプロイし、LINE Messaging API・LIFFと連携させる。

![FeedNotify_Configuration.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/453dbb94-8ad2-c803-3e99-bb14870ba2fa.png)

# 環境

| 種類                 | バージョン             |
| -------------------- | ---------------------- |
| OS                   | Windows10 version 2004 |
| Node                 | 14.17.5                |
| Serverless Framework | 2.66.1                 |
| 開発環境             | VSCode & Powershell    |

# ソース

[Github](https://github.com/flets708/feed-notify)にアップしています。

# 使ったサービス・言語・フレームワーク

| サービス・言語等   | 詳細                                                                                                                                                                                                                |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 言語・環境         | バックエンドのロジックはすべてNode.js v14.17.5で構築。                                                                                                                                                              |
| フレームワーク     | フレームワークはServerless Framework v2.66.2を用いて、AWS上サービスをデプロイした。 また、いくつかプラグインを導入した（*※1*）。                                                                                    |
| LINE Messaging API | LINE上でのメッセージを受信し、適切な応答をするためにLINE Messaging APIを導入。無料枠で利用するので、月1,000件のメッセージ送信数制限には要注意・・・。応答はFlex Message（カルーセル型）もしくはテキスト形式で実施。 |
| LIFF               | LINE Front-end Framework (LIFF)を使って、設定用のWebサイトで、LINEログインを利用しユーザID・名前・プロフィール画像を取得。                                                                                          |
| データベース       | AWS DynamoDBにユーザごとの情報（*※2*）を格納。                                                                                                                                                                      |
| UI                 | React + React-Bootstrap + Fontawesomeで設定画面のUIを構築。                                                                                                                                                         |

*※1 利用したServerless Frameworkのプラグインは以下の通り。*

```yml:server/serverless.yml抜粋
plugins:
  - serverless-dynamodb-local #DynamoDB接続部分の開発・テスト用
  - serverless-vpc-plugin #Lambda用のVPC作成
  - serverless-offline #ローカルでの開発・テスト
  - serverless-layers  #Lambda Layersを利用するためのプラグイン
```
```yml:client/serverless.yml抜粋
plugins:
  - serverless-s3-sync #ビルドしたフロントエンドのS3アップロード
  - serverless-cloudfront-invalidate #CloudFrontのキャッシュ削除自動化
```


*※2 DynamoDBレコードの構成は以下の通り。*

* `userId`:ユーザID  
* `lastSubscribe`:RSSフィードの最終配信日時  
* `subscribeFeeds`:購読するRSSフィード情報(ここではPublicKeyとDevelopersIOの2つ)  
* `name`:ユーザ名  

```json:DynamoDBのレコード
{
 "userId": "U0000000000000000000000000000000", 
 "lastSubscribe": "2021-11-25T01:00:19.091Z", 
 "subscribeFeeds": [
  {
   "feedUrl": "https://www.publickey1.jp/atom.xml",
   "addedAt": "2021-11-22T12:23:51.240Z",
   "siteUrl": "https://www.publickey1.jp/",
   "lastModifiedAt": "2021-11-21T23:23:51.240Z",
   "lastAction": "added",
   "feedId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
   "title": "Publickey",
   "enabled": true
  },
  {
   "feedUrl": "https://dev.classmethod.jp/feed/",
   "addedAt": "2021-11-22T05:24:35.168Z",
   "siteUrl": "https://dev.classmethod.jp",
   "lastModifiedAt": "2021-11-21T23:24:35.168Z",
   "lastAction": "added",
   "feedId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
   "title": "DevelopersIO",
   "enabled": true
  }
 ],
 "name": "XXX"
}
```

# ハマったところ

## Missing Authentication Tokenエラー

開発中、`sls offline start`でローカル開発環境では問題なくバックエンドAPIが動作するのに、AWSにデプロイすると`Missing Authentication Token`エラーが返されるようになった。  
特に認証トークンを求めるような仕様にはしてないはず・・・ということで調べてみると、[公式回答](https://aws.amazon.com/jp/premiumsupport/knowledge-center/api-gateway-authentication-token-errors/)曰く認証トークンがない時だけでなく存在しないリソースやメソッドにリクエストを投げた時もこのレスポンスになるとのこと。（だったら、いかにも認証トークンエラーっぽい文言にしないでほしいですが・・・）  
  
ただ、メソッドやリソースが存在しないといってもローカル開発環境では問題なく動作するし・・・と色々調べたところ、実は当時`serverless.yml`に設定していた下記が誤っていることが判明。

```yml
functions:
  webhook:
    handler: functions/webhook.handler
    events:
      - http:
          method: ANY
          path: /webhook/{proxy+}  #<=この部分
```

この設定を書いたとき、`https://xxxxxxx/webhook/xxx-xxx-1234{uuid}`のように、パスパラメータ付きでリクエストすることも想定していたのですが、どうも{proxy+}は空白はNGのようでした。ただ、`serverless-offline`を使った開発環境では空白でも動作してしまうので、混乱してしまいました・・・。  
改めて、下記のようにしたら動作することを確認。

```yml
functions:
  webhook:
    handler: functions/webhook.handler
    events:
      - http:
          method: ANY
          path: /webhook/{proxy+}  
      - http:               #追加
          method: ANY       #追加
          path: /webhook   #追加
```

その後、アプリ構築を進める中でパスパラメータを使うことがないと分かったので、現在の形で決着。

```yml
functions:
  webhook:
    handler: functions/webhook.handler
    events:
      - http:
          method: ANY
          path: "/webhook"
```

## https化

LIFFが連携するURL（Endpoint URL）が、httpsサイトでないと設定できないことが途中で判明。  
本番環境については、S3にホスティングしているURLを直接指定するのではなく、Cloudfrontを追加でデプロイすることで、CloudfrontのURLはhttps化しているためそちらを指定することで解決した。  
  
開発環境については、`yarn start`ではどうしても`http://localhost:3000`になってしまうと思っていたが、[こちらのサイト](https://chaika.hatenablog.com/entry/2020/09/08/083000)を参照し`.env.development.local`で`HTTPS = "true"`を定義することで簡単にhttps化できることを知り、それで解決できた。  
自分でオレオレ証明書を発行して・・・とそれなりに面倒な手順を踏もうかと思っていたので、助かりました。

## outbound80の手動穴あけ

RSSフィード配信サイトに情報を取りに行く際、LambdaがインターネットアクセスするためにVPC・NAT Gateway等の定義をする必要があったが、それらを簡単に実施するために`serverless-vpc-plugin`を使って開発していた。  
その後、Lambda上でテストをする中でRSSフィードをうまく取得できるサイトと、取得できないサイトがあることがわかった。  
  
原因を調べていくと`serverless-vpc-plugin`で構築した中でLambdaに割り当てられるセキュリティグループは、インターネットへのアウトバウンドアクセスが443（https）しか許可されておらず、"http://~"の配信サイトにアクセスできていないことが判明（特に、画像データだけはhttps→httpにリダイレクトして配信しているようなサイトもあり、なかなか検知しづらかったです・・・）。  
`serverless.yml`上に自分でVPC等のリソースを定義しようかとも考えたが、結局`deploy.ps1`上でLambda実行用のセキュリティグループにOutbound 80ポートの穴あけを追加で定義することで解決しました。

```shell
#deploy.ps1抜粋
$TEMP_SG = aws cloudformation describe-stacks --output text --stack-name feed-notify-dev --query 'Stacks[].Outputs[?OutputKey==`AppSecurityGroupId`].[OutputValue]'
aws ec2 authorize-security-group-egress --group-id $TEMP_SG --ip-permissions IpProtocol=tcp, FromPort=80, ToPort=80, IpRanges='[{CidrIp=0.0.0.0/0,Description="HTTP ACCESS for RSS Feed Subscribe"}]'
Write-Output "Outbound HTTP Access added to Security Group"
```

## TypeError: Cannot read property 'pipe' of undefined

開発している途中で、`sls deploy`するとタイトルの通りのエラーが表示されるようになった。  
調査した結果、**私の場合は`serverless-layers`と`serverless-s3-sync`の両方を同一ディレクトリにインストールしていたことが原因**と判明。（もともと`server/serverless.yml`でクライアント側のソースも含めて一括デプロイしていました）  
  
`serverless-layers`は`server`ディレクトリ、`serverless-s3-sync`は`client`ディレクトリにインストールし、それぞれのディレクトリに分割して`serverless.yml`を定義たうえで`deploy.ps1`を用いて一括実行する形に見直すことで解決した。

## yarn build時の環境変数読み込み

`deploy.ps1`でバックエンド側のデプロイとクライアント側のソースビルド・デプロイを一括実行するようスクリプトを組む中で、タイトル部分の挙動が手動実行時と違うことを検知した。  
もともとスクリプトを組む前は[こちらのサイト](https://chaika.hatenablog.com/entry/2020/10/14/083000)を参考にproduction環境での環境変数を`env.production`に定義していたが、スクリプト上で`.env.production`ファイルを作成&`yarn build`を実行するとうまく`env.production`ファイルを読み込まないことが判明。  
  
以下のように`.env.production`ファイルを使わず、直接環境変数に設定することで解決しました。おそらく権限周りの問題なのだと思われるが、よくわからず・・・。

```shell
#deploy.ps1抜粋
$env:REACT_APP_API_URL_PROD = aws cloudformation describe-stacks --output text --stack-name feed-notify-dev --query 'Stacks[].Outputs[?OutputKey==`ServiceEndpoint`].[OutputValue]'
yarn build
Write-Output "Client App Built"
```
#終わりに

今回、初めてServerless Frameworkで本格的にアプリ構築をしてみましたが、いろいろとハマってしまって難しいと感じた面もありつつ、Serverless Frameworkそのものやプラグインがとても便利で、非常に勉強になりつつ楽しかったです。
ReactによるUI構築も勉強になったので、今回の経験を踏まえて、また新しいアプリ構築にチャレンジしてみたいと思います。