こんにちは、広野です。
React アプリから Amazon DynamoDB のデータを取得するのはよくある実装パターンですが、JSON データのテキストファイルを直接読み込みたいこともあります。
例えば、
- あまり変更することのないマスタデータ的なもの
- バックグラウンドで分析処理をした結果、生成されたデータ
- Amazon DynamoDB の迅速な応答時間すら待てないぐらい急ぎ欲しい固定データ
- データ量はあまり多くない
こんな要件が 1 つまたは複数当てはまるときに、Amazon S3 に JSON データを配置して、かつ応答時間重視であれば Amazon CloudFront のエッジロケーションへのキャッシングも併用してそこから読み込ませる構成が 1 つの解になります。
ということから、以下の構成を考えてみました。
実現方法
- Amazon S3 バケットに JSON データを記述したテキストファイルを配置する。
- Amazon S3 バケット内のファイルは Amazon CloudFront 経由で配信する。要 OAC (旧 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 つのセキュリティ設定をします。
- OAC 用のバケットポリシー
- CORS
OAC は、指定した Amazon CloudFront 経由のアクセスでなければ拒否する設定です。
これは鉄板ソリューションなので、ネット上に情報が多々あります。以下、AWS 公式ブログでの紹介です。
実際にはこんな感じの設定になります。
CORS は、アクセス元サイトのドメイン名がアクセス先と異なる場合にアクセス許可をかける設定です。アプリのドメイン名と S3 にアクセスするための Amazon CloudFront のドメイン名は異なるので、この設定がないとアクセス拒否されます。
以下、AWS 公式ドキュメントの説明です。
実際にはこんな感じの設定になります。アクセスを許可するアプリのドメイン名はここでは AWS Amplify で自動的につくられたドメイン名にしています。
Amazon CloudFront
Amazon CloudFront の設定は非常に多いので、細かい説明は省略します。本記事の最後に添付する AWS CloudFormation テンプレートを読み込むか、それを実行してプロビジョニングされたリソースを見て頂けたらと思います。
OAC の設定は決まりきった設定なのでそんなにハマらないと思うのですが、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: Service: cloudfront.amazonaws.com Condition: StringEquals: AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution} # ------------------------------------------------------------# # 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: !Sub ${S3Bucket}.s3.${AWS::Region}.amazonaws.com S3OriginConfig: OriginAccessIdentity: "" OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id ConnectionAttempts: 3 ConnectionTimeout: 10 ViewerCertificate: CloudFrontDefaultCertificate: true Tags: - Key: Cost Value: !Sub example-${SystemName} CloudFrontOriginAccessControl: Type: AWS::CloudFront::OriginAccessControl Properties: OriginAccessControlConfig: Description: !Sub CloudFront OAC for example-${SystemName}-img Name: !Sub OriginAccessControl-example-${SystemName}-img OriginAccessControlOriginType: s3 SigningBehavior: always SigningProtocol: sigv4 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 代わりに使えます。
本記事が皆様のお役に立てれば幸いです。