React + Lambda + S3を使って簡易掲示板を作ってみた①〜バックエンド実装〜

Reactで簡易掲示板を作ってみました。その時の手順を解説します。

Ishiguro Suguru

本サイトはHUGOを使って作成していますが、HUGOは静的なファイルを出力するため掲示板などの動的な処理はできません。 そのため掲示板をReactとAWSのリソースを利用して実装しHUGO側で参照する仕組みを作成しました。

今回はReactとAWSのLambdaを使った掲示板の実装方法を紹介します。

※掲示板機能は外部サービスのDisqusを使えば実現できますが、SNSのログインが必要だったり広告が表示されたりするので新たに作成してみました。

システム構成

今回の構成は簡単ですが以下のとおりです。

  • フロントエンド:React
  • バックエンド:API Gateway, Lambda, S3

フロントエンドのReactで掲示板の表示およびフォームに入力した投稿データの送信を行います。

バックエンドの処理はAWSのLambdaを使用し投稿データをS3に格納します。 データをきちんと保持する必要があるシステムではRDSやDynamoDBを使って構築するのが良いですが、 今回は個人利用しているWebサイトのためS3にデータをjsonファイルとして保存するようにしています。

利用ユーザーが少ない/PoCでとりあえず試したい/データロストしてもOKといった場合はDBを使わずにS3でもいいかなと思います。

手順概要

手順概要は以下のとおりです。

  1. バックエンド実装
  2. フロントエンド実装
  3. デプロイ・CloudFront設定

1と2は逆でも問題ないですがとりあえずバックエンドの実装から説明します。

最終的に作成したバックエンド部分のAPIとフロントエンド部分のReactはCloudFront経由でアクセスするようにしています。

バックエンド実装

長くなるためまずはバックエンド実装について紹介します。 過去記事でも紹介していますがPyCharmを使って実装していますので環境準備については以下の記事を参考にしてみてください。

HUGO + Lambda + Google Analytics APIを使ってPV順で記事を表示してみた①〜Google Analytics APIの利用設定〜

HUGO + Lambda + Google Analytics APIを使ってPV順で記事を表示してみた②-1〜Lambda開発環境の準備〜

SAMテンプレートの作成(API GatewayとLambdaの定義)

APIはGETメソッドで投稿データを取得し、POSTメソッドでフォームから送信したデータをS3に保存する設計にしています。

PyCharmで作成したプロジェクト直下のtemplate.yamlは以下のとおりです。(初期テンプレートは"AWS SAM Hello World"を使用)

  • template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Lambda and ApiGateway SAM Template for AWS

Globals:
  Function:
    Timeout: 3
    Environment:
      # Lambdaで使用する環境変数を定義。投稿データを保存しておくS3バケットを指定しておく。
      Variables:
        TZ: Asia/Tokyo
        S3_BUCKET_NAME: xxxxxxxxxxxxxxx
  # API Gatewayのステージ名を変更したい場合は定義しておく。
  Api:
    OpenApiVersion: 3.0.2
#    ②CORS設定が必要な場合は以下コメントアウトを外す
#    Cors:
#      AllowOrigin: "'*'"
#      AllowMethods: "'GET,POST,DELETE'"
#      AllowHeaders: "'X-Requested-With, Origin, X-Csrftoken, Content-Type, Accept'"


Resources:
  # Lambdaの実行ロールを作成。S3に投稿データの読み書きを行うためAmazonS3FullAccess権限を付与しておく。
  BoardCommentsLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: BoardCommentsLambdaRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      - arn:aws:iam::aws:policy/AmazonS3FullAccess

  # Lambda定義。
  BoardCommentsFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/
      Handler: app.lambda_handler
      Runtime: python3.8
      Role: !GetAtt BoardCommentsLambdaRole.Arn
      FunctionName: BoardCommentsFunction
      Events:
      # ①-1 POSTメソッドのAPI定義。
        POST:
          Type: Api
          Properties:
            RestApiId: !Ref BoardCommentsApi
            Path: /comments
            Method: post
      # ①-2 GETメソッドのAPI定義。
        GET:
          Type: Api
          Properties:
            RestApiId: !Ref BoardCommentsApi
            Path: /comments/{path}
            Method: get

  # リソースポリシーの定義。API GatewayがLambda関数を呼び出せるように設定。
  BoardCommentsFunctionPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref BoardCommentsFunction
      Principal: apigateway.amazonaws.com

  # API Gatewayの定義。ステージ名(StageName)はデフォルトでprodとなるがprdに変更。
  BoardCommentsApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: BoardCommentsApi
      StageName: prd

# CloudFormationの出力
Outputs:
  BoardCommentsApi:
    Description: "API Gateway endpoint URL for Prod stage for BoardComments function"
    Value: !Sub "https://${BoardCommentsApi}.execute-api.${AWS::Region}.amazonaws.com/prd/comments/"
  BoardCommentsFunction:
    Description: "Board Comments Function ARN"
    Value: !GetAtt BoardCommentsFunction.Arn
  BoardCommentsLambdaRole:
    Description: "Implicit IAM Role created for Board Comments function"
    Value: !GetAtt BoardCommentsLambdaRole.Arn

ポイントを簡単に説明します。

上記①でAPIのGET,POSTメソッドのパスを定義しています。 Pathの値を変更すれば任意のパスに変更できます。 GET,POSTメソッドを追加したい場合は以下のようにEvent名とPathを変えて定義してください。 ①-2に記載しているGETメソッドのAPI定義では汎用性を持たせるためにパスパラメータ{path}を指定していますが必須ではありません。

      Events:
      # ①-3 GETメソッドのAPIを複数用意する場合。
        GetA:
          Type: Api
          Properties:
            RestApiId: !Ref SampleApi
            Path: /geta
            Method: get
        GetB:
          Type: Api
          Properties:
            RestApiId: !Ref SampleApi
            Path: /getb
            Method: get

またその他注意点としてCORS設定が環境によって必要になります。 今回の本番環境ではCloudFrontを使用しているため不要ですが、ローカル環境でテストする場合などは②のコメントアウト部分を外しておきましょう。

CORSについて詳しい話は以下のサイトなど参照してください。 (検索すれば解説しているサイトがその他に出てきます。)

オリジン間リソース共有 (CORS)

Lambda実行プログラム

Pythonコードは以下のとおりです。 なおエラーハンドリング処理は面倒で定義していないです…

  • app.py
import json
import boto3
from datetime import datetime, timedelta, timezone
import os

CLIENT = boto3.client('s3')
S3_BUCKET_NAME = os.environ.get('S3_BUCKET_NAME')


def lambda_handler(event, context):

    # ①POSTメソッド処理
    if event['httpMethod'] == 'POST':
        # リクエストデータ取得
        body = event['body']
        params = json.loads(body)

        # 投稿データ作成
        jst = timezone(timedelta(hours=+9), 'JST')
        date_now = datetime.now(jst).strftime('%Y-%m-%d %H:%M:%S')
        comment = {
            'date': date_now,
            'name': params['name'],
            'message': params['message']
        }
        # データ登録先のファイル設定
        path = params['path']
        file_name = path + '.json'

        # データ登録先の存在チェック
        comments = check_file(file_name)

        # データ登録先有り->投稿データのIDを新規に付与して既存のデータに追加
        if comments:
            comment['id'] = comments[-1]['id'] + 1
            comments.append(comment)
        # データ登録先無し->新規投稿データを作成
        else:
            comments = []
            comment['id'] = 1
            comments.append(comment)

        # データ登録
        CLIENT.put_object(Body=json.dumps(comments), Bucket=S3_BUCKET_NAME, Key=file_name)
        response = comment

    # ②GETメソッド処理
    if event['httpMethod'] == 'GET':
        # パスパラーメータを取得して参照先のファイル設定
        path_param = event['pathParameters']['path']
        file_name = path_param + '.json'

        # クエリストリングのチェック
        try:
            sort = event['queryStringParameters']['sort']
        except:
            sort = False

        # データ取得先の存在チェック
        comments = check_file(file_name)

        # データ取得先有り->投稿データを取得してsortパラメータがある場合は並べ替え
        if comments:
            response = comments

            if sort == 'desc':
                response = sorted(response, key=lambda x: x['id'], reverse=True)
        # データ取得先無し->
        else:
            response = {'existsFile': 'none'}

    return {
        'statusCode': 200,
        # ③CORS設定が必要な場合は以下コメントアウトを外す
        # 'headers': {
        #     'Access-Control-Allow-Headers': 'X-Requested-With, Origin, X-Csrftoken, Content-Type, Accept',
        #     'Access-Control-Allow-Origin': '*',
        #     'Access-Control-Allow-Methods': 'OPTIONS,POST,GET'
        # },
        'body': json.dumps(response)
    }


def check_file(file_name):
    """登録先のファイルの有無をチェック
    :rtype: object
    """
    try:
        response = CLIENT.get_object(Bucket=S3_BUCKET_NAME, Key=file_name)
        check_result = json.loads(response['Body'].read())
    except:
        check_result = False

    return check_result

①POSTメソッド処理

リクエストがPOSTの場合には①の処理を実行します。 本処理ではリクエストbodyのPathパラメータを取得して、S3データ登録先のファイルを指定しています。

フォームから入力したデータ(投稿者名:name, 本文:message)とLambda実行時の日付、連番のIDを投稿データとして最終的にjsonファイルに保存します。

②POSTメソッド処理

リクエストがGETの場合には①の処理を実行します。 APIを呼び出す際にソートできるようにクエリストリングの取得を行っていますがソートが不要であれば削除してもOKです。

また、③に記載のとおりSAMテンプレートにもありましたがLambdaも環境によってCORS設定が必要になります。

デプロイ

デプロイはPyCharmプロジェクト内のtemplate.yamlを右クリックして「Deploy Serverless Application」をクリックすればOKです。

デプロイ後、AWSにAPI GatewayとLambdaが作成されているため確認してみてください。

最後に

以上でバックエンド部分の実装は完了です!

次回はフロントエンドのReact部分について紹介します!!!

comments powered by Disqus