[Next.js/Vercel]LambdaとDynamoDBでJamstackなアプリを作ってみた

## 概要

- 以前にQiitaの前日のいいね数を通知してくれるLINE Botを作りました
    - [Qiitaのいいね数を通知してくれるLINE Botを作ってみた](https://qiita.com/ozaki25/items/03b834de8c0c175d955d)
- この延長でいいね数の推移をグラフで表示するWebページを作ってみたというのが今回の内容です
    - せっかくなので技術検証も兼ねてJamstackな構成で作ってみました
    - 前述の記事の通りデータはDynamoDBにデータを蓄積しているのでHeadlessCMSなどは使わずにチャレンジしてみます

## 作ったもの

- 直近1週間のいいね数をグラフで表示するWebアプリをLIFFアプリとして表示しています

![Screenshot_20200726-014511.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/a656b608-38c1-afa4-52ac-1428c92e63a8.png)

## 構成

- Jamstackな構成とするためにいろいろ使っているので少々複雑です
    - 要素ごとに何をしているのか説明していきます

<img width="861" alt="スクリーンショット 2020-07-26 1.33.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/28b07683-d3cc-c289-2f83-3088c5f0b064.png">

## 実装済みの機能

- Qiitaのいいね数を蓄積していく処理は前述の記事で紹介済みです

<img width="859" alt="スクリーンショット 2020-07-26 2.10.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/7d2f814c-1658-16bf-c840-40baec2f6821.png">

- LambdaでQiitaのAPIを叩き取得したデータをDynamoDBに保存しています
- Lambdaのバッチ実行で毎日0時に実行しています

## Next.jsを使ったJamstackな仕組みの構築

- まずはビルドデプロイ自動化の仕組みは置いておいてアプリの部分からです

<img width="859" alt="スクリーンショット 2020-07-26 1.53.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/90750e74-6d60-3372-701c-d391ef63f642.png">

- Jamstackの最大の特徴であるビルド時にAPIアクセスしてデータ埋め込んだHTMLを生成するという部分です
- ビルド時にAPIアクセスできるライブラリはいくつかありますが今回はReactベースのNext.jsを使いました
    - [getStaticProps](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation)と[getStaticPaths](https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation)を使えれば簡単に実現できます
    - [こんな資料](https://jamstack-handson.ozaki25.now.sh/page2.html)も過去に作ったのでご参考で
- この記事はJamstackにフォーカスしているので深く触れませんがLIFF SDKを入れてLIFFアプリ化しています
    - LINE IDを取得できるのでログイン機能いらず！

## GitHub ActionsでVercelに自動デプロイ

- 次はNext.jsで作ったアプリのデプロイ周りです

<img width="859" alt="スクリーンショット 2020-07-26 1.59.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/fda0475a-0f75-6769-8601-3629c7b3ab31.png">

- 今回はホスティングサービスとしてVercelを使いました
- さらにGitHub Actionsで自動的にデプロイできる仕組みを作りました
    - Vercel用のActionが公式であったのでそれを使っています
        - https://github.com/marketplace/actions/vercel-action
- GitHub Actions起動のトリガーはmasterへのプッシュとAPIアクセスを設定しています

```yml:.github/workflows/deploy.yml
name: deploy
on:
  push:
    branches:
      - master
  repository_dispatch:
    branches:
      - master
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js 14
        uses: actions/setup-node@v1
        with:
          node-version: 14
      - uses: amondnet/vercel-action@v19
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          github-token: ${{ secrets.GITHUB_TOKEN }}
          vercel-args: '--prod -f -b TZ=Asia/Tokyo -b REACT_APP_LIFF_ID=${{ secrets.REACT_APP_LIFF_ID }} -b REACT_APP_API_URL=${{ secrets.REACT_APP_API_URL }}'
          vercel-org-id: ${{ secrets.ORG_ID}}
          vercel-project-id: ${{ secrets.PROJECT_ID}}
```

## DynamoDBの更新をトリガーにLambdaを起動してGitHub Actionsを叩く

- 最後にJamstack自動化の肝となるデータ更新をトリガーにビルドデプロイを走らせる仕組みです

<img width="860" alt="スクリーンショット 2020-07-26 2.07.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/7ae7a5f2-67dc-ac75-bd98-2ecbea5880fd.png">

- DynamoDB Streamsを使うことで特定のテーブルの更新をトリガーにLambdaを起動することができます
- そしてGitHub ActionsをAPIアクセスで起動できるようにしているためLambdaからそのAPIを叩いています
    - ちょっとまわりくどいですがDynamoDBの更新をトリガーにGitHub Actionsを起動するために試行錯誤した結果です
    - 他のサービスを使ったりすればもう少し違ったかな？
- 今回はLambdaやDynamoDB周りはServerlessFrameworkを使っています
    - serverless.ymlはこんな感じです

```yml:serverless.yml(抜粋)
service:
  name: line-qiita-bot
provider:
  name: aws
  runtime: nodejs12.x
  region: ap-northeast-1
  environment:
    DYNAMODB_TABLE: ${self:service.name}-${self:provider.stage}
    QIITA_HISTORY_TABLE: ${self:provider.environment.DYNAMODB_TABLE}-QiitaHistory
  iamRoleStatements:
    - Effect: Allow
      Action:
        - 'dynamodb:Query'
        - 'dynamodb:Scan'
        - 'dynamodb:GetItem'
        - 'dynamodb:PutItem'
        - 'dynamodb:UpdateItem'
        - 'dynamodb:DeleteItem'
        - 'dynamodb:ListStreams'
        - 'dynamodb:DescribeTable'
      Resource: 'arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}*'
functions:
  triggerGitHubActions:
    handler: handler.triggerGitHubActions
    events:
      - stream:
          type: dynamodb
          batchSize: 1
          startingPosition: LATEST
          maximumRetryAttempts: 1
          enabled: true
          arn:
            Fn::GetAtt:
              - QiitaHistory
              - StreamArn
resources:
  Resources:
    QiitaHistory:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.QIITA_HISTORY_TABLE}
        AttributeDefinitions:
          - AttributeName: userId
            AttributeType: S
          - AttributeName: date
            AttributeType: S
        KeySchema:
          - AttributeName: userId
            KeyType: HASH
          - AttributeName: date
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        StreamSpecification:
          StreamViewType: NEW_AND_OLD_IMAGES
```

- Lambdaでの処理はAPIにアクセスしているだけです
    - GitHub Actionsを起動するAPIはPOSTで`https://api.github.com/repos/アカウント名/リポジトリ名/dispatches`にアクセスする感じです
    - 詳細はドキュメントをご確認ください
        - https://docs.github.com/en/rest/reference/repos#create-a-repository-dispatch-event

## 全体の流れ

- 少々複雑な構成でしたがこれで全部説明し終わりました
- 改めて全体の流れを振り返ります

<img width="861" alt="スクリーンショット 2020-07-26 1.33.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/28b07683-d3cc-c289-2f83-3088c5f0b064.png">

- ①毎日0時にLambdaがバッチ実行されQiitaのAPIからいいね数を取得します
- ②取得したデータはDynamoDBに保存します
- ③DynamoDBにデータが保存されるとそれをトリガーにLambdaが起動します
- ④そのLambdaではGitHub Actionsを起動するAPIへのアクセスのみを行います
- ⑤APIアクセスにより起動されたGitHub ActionsではNext.jsで作られたアプリをビルドしVercelへのデプロイを行います
- ⑥〜⑨ビルド時にNext.jsのStatic Site Generationの機能によってAPIからいいね数のデータを取得してHTMLに埋め込みます
- ⑩生成された静的ファイルをVercelへデプロイします
- =>ユーザがページにアクセスすると最新データが反映されたグラフページが表示されます！

## まとめ

- もともとDynamoDBにデータが蓄積されていたところからスタートしましたがなんとかJamstackな構成を実現できました！
- どこか一部分でも何かの参考になれば嬉しいです！