React アプリに Amazon S3 上の JSON データをテーブルで表示する

こんにちは、広野です。

React アプリから Amazon DynamoDB のデータを取得するのはよくある実装パターンですが、JSON データのテキストファイルを直接読み込みたいこともあります。

例えば、

  • あまり変更することのないマスタデータ的なもの
  • バックグラウンドで分析処理をした結果、生成されたデータ
  • Amazon DynamoDB の迅速な応答時間すら待てないぐらい急ぎ欲しい固定データ
  • データ量はあまり多くない

こんな要件が 1 つまたは複数当てはまるときに、Amazon S3 に JSON データを配置して、かつ応答時間重視であれば Amazon CloudFront のエッジロケーションへのキャッシングも併用してそこから読み込ませる構成が 1 つの解になります。

ということから、以下の構成を考えてみました。

実現方法

  • Amazon S3 バケットに JSON データを記述したテキストファイルを配置する。
  • Amazon S3 バケット内のファイルは Amazon CloudFront 経由で配信する。要 OAI。
  • Amazon S3 バケット内のオブジェクトに対して GET メソッドでアクセスさせることが可能なため、その機能を利用してファイル内の JSON データを React アプリが取得する。CORS の設定が必要。
  • 取得した JSON データを、MUI で作成したテーブルに渡して表示させる。

設定解説

Amazon S3

Amazon S3 バケットには、以下の JSON データが記述されたテキストファイルを配置します。
このデータをアプリ画面のテーブルで表示します。

  • testdata.json
[
  { "menu": "menu1", "id": 1, "neta": "いか" },
  { "menu": "menu1", "id": 2, "neta": "たこ" },
  { "menu": "menu2", "id": 1, "neta": "海鮮丼" }
]

Amazon S3 には、大きく分けて 2 つのセキュリティ設定をします。

  • OAI 用のバケットポリシー
  • CORS

OAI は、指定した Amazon CloudFront 経由のアクセスでなければ拒否する設定です。
これは鉄板ソリューションなので、ネット上に情報が多々あります。以下、AWS 公式ドキュメントの説明です。

オリジンアクセスアイデンティティ (OAI) を使用して Amazon S3 コンテンツへのアクセスを制限する - Amazon CloudFront
Amazon CloudFront オリジンアクセスアイデンティティ(OAI) で、Amazon S3 コンテンツへのアクセスを制限します。

実際にはこんな感じの設定になります。

CORS は、アクセス元サイトのドメイン名がアクセス先と異なる場合にアクセス許可をかける設定です。アプリのドメイン名と S3 にアクセスするための Amazon CloudFront のドメイン名は異なるので、この設定がないとアクセス拒否されます。
以下、AWS 公式ドキュメントの説明です。

CORS の設定 - Amazon Simple Storage Service
バケットで XML または JSON CORS 設定を作成します。

実際にはこんな感じの設定になります。アクセスを許可するアプリのドメイン名はここでは AWS Amplify で自動的につくられたドメイン名にしています。

Amazon CloudFront

Amazon CloudFront の設定は非常に多いので、細かい説明は省略します。本記事の最後に添付する AWS CloudFormation テンプレートを読み込むか、それを実行してプロビジョニングされたリソースを見て頂けたらと思います。

AWS 公式ドキュメントでは、サポート情報ですが以下がわかりやすかったです。

CloudFront を使用して Amazon S3 でホストされた静的ウェブサイトを公開する
CloudFront の「「Access-Control-Allow-Origin」ヘッダーが存在しません」というエラーを解決する

OAI の設定は決まりきった設定なのでそんなにハマらないと思うのですが、CORS は HTTP ヘッダーの細かい設定がややこしくて、丁寧に設定しないとハマります。私はこのめんどくさい設定を手でするのが嫌なので、AWS CloudFormation で過去の設定をコピペしてリソースをつくっています。

React アプリ

実際に表示してみたテーブルはこちら。

単純に Amazon S3 に配置した testdata.json の中身を GET メソッドで取得して、あらかじめ用意したテーブルにデータをマッピングしているだけのコードになります。

テーブルやレイアウト調整関連の WEB パーツは MUI を使用しています。テーブルのデザインをデフォルトのままではなく、カスタマイズしてみたサンプルになっています。

その他の細かい画面デザイン周りのコードは、皆さまのアプリコードに合わせて変更が必要です。

  • App.js
import React, {useState, useEffect} from 'react';
import axios from 'axios';
import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack';
import { styled } from '@mui/material/styles';
import Paper from '@mui/material/Paper';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell, { tableCellClasses } from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import { blue } from '@mui/material/colors';

const App = () => {
  //テーブルデザイン設定
  const StyledTableCell = styled(TableCell)(({ theme }) => ({
    [`&.${tableCellClasses.head}`]: {
      backgroundColor: blue[900],
      color: theme.palette.common.white,
      fontSize: 14,
      fontFamily: "inherit"
    },
    [`&.${tableCellClasses.body}`]: {
      fontSize: 14,
      fontFamily: "inherit"
    }
  }));
  const StyledTableRow = styled(TableRow)(({ theme }) => ({
    '&:nth-of-type(odd)': {
      backgroundColor: theme.palette.action.hover
    }
  }));
  
  //state初期化
  const [jsonData, setJsonData] = useState([]);
  
  //データ取得関数
  const getJsonData = async () => {
    const res = await axios.get("https://xxxxxxxxxxxxxx.cloudfront.net/data/testdata.json");
    setJsonData(res.data);
  };
  
  //画面表示時にデータ取得
  useEffect(() => {
    getJsonData();
  }, []);
  
  return (
    <div id="page-wrapper">
      {/* Main */}
      <div id="main">
        <div className="container">
          <div className="row main-row">
            <div className="col-12 col-12-medium col-12-small">
              <section>
                <Grid container spacing={{ xs: 2, sm: 2, md: 2 }}>
                  <Grid item xs={12} sm={12} md={6}>
                    <Stack direction="column" justifyContent="flex-start" alignItems="center" spacing={2}>
                      <TableContainer component={Paper}>
                        <Table size="small" aria-label="testdata">
                          <TableHead>
                            <TableRow>
                              <StyledTableCell align="center">メニュー</StyledTableCell>
                              <StyledTableCell align="center">ID</StyledTableCell>
                              <StyledTableCell align="center">ネタ</StyledTableCell>
                            </TableRow>
                          </TableHead>
                          <TableBody>
                            {jsonData.map((row) => (
                              <StyledTableRow>
                                <StyledTableCell align="center">{row.menu}</StyledTableCell>
                                <StyledTableCell align="center">{row.id}</StyledTableCell>
                                <StyledTableCell align="center">{row.neta}</StyledTableCell>
                              </StyledTableRow>
                            ))}
                          </TableBody>
                        </Table>
                      </TableContainer>
                    </Stack>
                  </Grid>
                </Grid>
              </section>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default App;

AWS CloudFormation テンプレート

本記事で紹介した Amazon S3 バケットと Amazon CloudFront を構築してくれる Amazon CloudFormation テンプレートを貼り付けておきます。
パラメータに設定するドメイン名は、アプリのドメイン名になります。CORS の設定で使われます。

AWSTemplateFormatVersion: 2010-09-09
Description: The CloudFormation template that creates a S3 bucket and a CloudFront distribution.

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  SystemName:
    Type: String
    Description: System name.
    Default: xxxxx
    MaxLength: 10
    MinLength: 1

  DomainName:
    Type: String
    Description: Domain name for URL.
    Default: example.com
    MaxLength: 40
    MinLength: 5

  SubDomainName:
    Type: String
    Description: Sub domain name for URL.
    Default: xxxxx
    MaxLength: 40
    MinLength: 1

Resources:
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------#
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub example-${SystemName}-img
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      CorsConfiguration:
        CorsRules:
          - AllowedHeaders:
              - "*"
            AllowedMethods:
              - "GET"
              - "HEAD"
            AllowedOrigins:
              - !Sub https://${SubDomainName}.${DomainName}
      Tags:
        - Key: Cost
          Value: !Sub example-${SystemName}
  S3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
              - "s3:GetObject"
            Effect: Allow
            Resource: !Sub "arn:aws:s3:::${S3Bucket}/*"
            Principal:
              AWS: !Sub "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}"

# ------------------------------------------------------------#
# CloudFront
# ------------------------------------------------------------#
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Enabled: true
        Comment: !Sub CloudFront distribution for example-${SystemName}-img
        HttpVersion: http2
        IPV6Enabled: true
        PriceClass: PriceClass_200
        DefaultCacheBehavior:
          TargetOriginId: !Sub S3Origin-${SystemName}-img
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods:
            - GET
            - HEAD
            - OPTIONS
          CachedMethods:
            - GET
            - HEAD
          CachePolicyId: !Ref CloudFrontCachePolicy
          OriginRequestPolicyId: !Ref CloudFrontOriginRequestPolicy
          ResponseHeadersPolicyId: !Ref CloudFrontResponseHeadersPolicy
          Compress: true
          SmoothStreaming: false
        Origins:
          - Id: !Sub S3Origin-${SystemName}-img
            DomainName: !GetAtt S3Bucket.DomainName
            S3OriginConfig:
              OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}"
            ConnectionAttempts: 3
            ConnectionTimeout: 10
        ViewerCertificate:
          CloudFrontDefaultCertificate: true
      Tags:
        - Key: Cost
          Value: !Sub example-${SystemName}
  CloudFrontOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Sub CloudFront OAI for example-${SystemName}
  CloudFrontCachePolicy:
    Type: AWS::CloudFront::CachePolicy
    Properties:
      CachePolicyConfig:
        Name: !Sub CachePolicy-${SystemName}-img
        Comment: !Sub CloudFront Cache Policy for example-${SystemName}-img
        DefaultTTL: 3600
        MaxTTL: 86400
        MinTTL: 60
        ParametersInCacheKeyAndForwardedToOrigin:
          CookiesConfig:
            CookieBehavior: none
          EnableAcceptEncodingBrotli: true
          EnableAcceptEncodingGzip: true
          HeadersConfig:
            HeaderBehavior: whitelist
            Headers:
              - Access-Control-Request-Headers
              - Access-Control-Request-Method
              - Origin
          QueryStringsConfig:
            QueryStringBehavior: none
  CloudFrontOriginRequestPolicy:
    Type: AWS::CloudFront::OriginRequestPolicy
    Properties:
      OriginRequestPolicyConfig:
        Name: !Sub OriginRequestPolicy-${SystemName}-img
        Comment: !Sub CloudFront Origin Request Policy for example-${SystemName}-img
        CookiesConfig:
          CookieBehavior: none
        HeadersConfig:
          HeaderBehavior: whitelist
          Headers:
            - Access-Control-Request-Headers
            - Access-Control-Request-Method
            - Origin
        QueryStringsConfig:
          QueryStringBehavior: none
  CloudFrontResponseHeadersPolicy:
    Type: AWS::CloudFront::ResponseHeadersPolicy
    Properties:
      ResponseHeadersPolicyConfig:
        Name: !Sub ResponseHeadersPolicy-${SystemName}-img
        Comment: !Sub CloudFront Response Headers Policy for example-${SystemName}-img
        CorsConfig:
          AccessControlAllowCredentials: false
          AccessControlAllowHeaders:
            Items:
              - "*"
          AccessControlAllowMethods:
            Items:
              - GET
              - HEAD
              - OPTIONS
          AccessControlAllowOrigins:
            Items:
              - !Sub https://${SubDomainName}.${DomainName}
          AccessControlExposeHeaders:
            Items:
              - "*"
          AccessControlMaxAgeSec: 600
          OriginOverride: true
        SecurityHeadersConfig:
          ContentTypeOptions:
            Override: true
          FrameOptions:
            FrameOption: SAMEORIGIN
            Override: true
          ReferrerPolicy:
            Override: true
            ReferrerPolicy: strict-origin-when-cross-origin
          StrictTransportSecurity:
            AccessControlMaxAgeSec: 31536000
            IncludeSubdomains: true
            Override: true
            Preload: true
          XSSProtection:
            ModeBlock: true
            Override: true
            Protection: true

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#
Outputs:
#S3
  ImgBucketName:
    Value: !Ref S3Bucket
#CloudFront
  CloudFrontOriginalDomain:
    Value: !GetAtt CloudFrontDistribution.DomainName

まとめ

いかがでしたでしょうか?

Amazon S3 に配置した JSON データをアプリから直接取得することができました。静的データという制約はありますが、ちょっとしたデータであれば Amazon DynamoDB 代わりに使えます。

本記事が皆様のお役に立てれば幸いです。

タイトルとURLをコピーしました