AWS + Laravel + React.jsでオセロゲームを作ってみる①
概要
やること
あとから振り返れるよう、これまでちまちま作ってきたオセロwebアプリの開発の途中経過を報告・メモしていく
リポジトリ
現状
バックエンドにLaravelを使用。
ドメイン駆動設計でビジネスロジックを記述しゲームとしてのオセロの主要な機能は開発済み。
現在ブラウザ上から遊ぶことも可能(EC2を使用)。フロント側は未開発。
直近の目標
バックエンド
- API特化(REST)
- RDS等でデータベース導入
- Fargateなどでコンテナ化して運用(未定)
- いったんlaravel breezeなどのスキャフォールドは使わない
- 設計をブラッシュアップ
- ボットの追加
フロント
- Next.jsでSPA化
- Vercel or S3でホスト
- tailwind.cssで装飾
こちらの記事のような構成が目標
参考にしながら進めていく
フロントエンドプロジェクト作成
$ npx create-next-app othello-frontend--typescript
...
info Direct dependencies
├─ @types/node@17.0.23
├─ @types/react-dom@17.0.14
├─ @types/react@17.0.43
├─ eslint-config-next@12.1.4
├─ eslint@8.12.0
└─ typescript@4.6.3
...
# ローカルサーバーを起動してみる
$ cd othello-frontend
$ yarn dev
githubにプッシュ
新規プロジェクトを作成しプッシュ
ESLint, Prettier導入
ベースはここ
こちらも参考に
tailwind導入
こちらを参考に。
公式
詳しい 見やすい注意点
tailwind 3.0以降の場合tailwind.config.jsからpurgeがなくなり代わりにcontentに記載する
purgeを使っていた場合のエラー
$ yarn dev
yarn run v1.22.15
$ next dev
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
warn - The `purge`/`content` options have changed in Tailwind CSS v3.0.
warn - Update your configuration file to eliminate this warning.
warn - https://tailwindcss.com/docs/upgrade-guide#configure-content-sources
公式アナウンス
実際に設定した内容
tailwind.config.js
module.exports = {
mode: "jit",
content: [
"./src/pages/**/*.{js,ts,jsx,tsx}",
"./src/components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};
この段階でよくわかってないこと
- コンポーネントの設計
- ajaxライブラリどれ使う問題
- cssライブラリ選定(今のとこtailwind)と動作イメージ
やりたいこと
- Github projects(beta)をフル活用
- フロントとバックを統合するか別々に扱うか
- ニューモーフィズムorグラスモーフィズムを導入してみる
CSS構成の検討
いったんtailwindでいろいろできるみたいなのでtailwindで作っていく
もうすこしデファクトスタンダードやシステムの要件が固まってきたら再検討。
いまは学習コストと選定に迷う時間を削減する方向で進めていく。
デザイン
テーマ
ニューモーフィズムに興味があったので作ってみる
ニューモーフィズム
むにゅっとした見た目のデザイン
とは
簡単ジェネレータ
提唱した人の記事
Dribbble
グラスモーフィズム
ニューモーフィズムの次に来ると言われてる?やつ
AppleのOSのコントロールパネル等に使われてたりする
Dribbble
盤面とコマのコンポーネント作成
ニューモーフィズムで作っていく
まずはそれっぽく作ってみる
一つのコンポーネントの中に全部まとめて書いてみる
- いまいちjsx内でのループが分からずベタ書き
- 一旦4x4で
Board.tsx
BoardとFieldに分離
盤面(Board)の内部でマス(Field)を8x8で描画する。
子要素を表示するときはコンポジションがいいらしい。
子要素とコンポジション
繰り返し処理はここを参照した。コンポジションモデルもここから。
上記の記事でも触れられていた公式の解説。
やっぱり一通り公式ドキュメントを読んでおくのは大事。。。
childrenをpropsの型定義に追加するには
基本的にpropsにchildrenも内包されるため、childrenの型定義は不要。
ただ、明示的に型定義したい場合はReact.ReactNodeを使用する。
詳しくはこちらの記事へ
上記の記事より引用
import { FC, ReactNode } from 'react'
type Props = {
color: string
children: ReactNode
}
export const ColorBox: FC<Props> = ({ color, children }) => {
return <div style={{ background: color }}>{children}</div>
}
繰り返し処理とkey
有名な話ではあるけれど、mapを使うときはkeyを使う。
(使っておらず、lintに怒られた)
また、その際もとになる配列データ等のindexを使うのではなく、ユニークなid等を使うべき。
{...props}
)する
propsをspread展開(盤面情報配列内オブジェクトとpropsが一致してて簡易的に書きたいと思っていたのでちょうどよかった
例に漏れず公式サイトにもちゃんと載ってた
propsと型定義
propsで渡される値の方を正確に定義できるので、コンポーネント内に入ってくる値を明示できるのがとてもいい。IDEがエラーをわかりやすく表示してくれることもありとてもコードを書いていて気持ちいい。
また、enum風に値の中身まで型で指定できるというのもPHPで動的型付けを経験していた身からすると新鮮(宣言によってバリデーション処理が不要になるというのも嬉しい)
type ColorCode = "01" | "02";
type Stone = {
color: ColorCode;
};
type FieldParams = {
id: number;
flipped: boolean; // ひっくり返されたコマのあるマスかどうか
set: boolean; // 前のターンに置かれたコマのあるマスかどうか
setable: boolean; // 置くことができるマスかどうか
stone: Stone;
};
今後はenumや型定義ファイルの作成などもしたい。
整理
- tailwindにテーマカラーなどを設定
参考になった記事
(phpの1ファイル1クラスのイメージが抜けなかったので)
ツール
カラージェネレーター
classNameの値に固定値+変数を設定したいとき
非同期通信と状態管理
盤面がいったん表示できるところまで行ったのでバックエンドAPI作成&盤面データ取得をしてみる。
懸念点
useEffectなど、副作用関連の処理は書籍で読んだ程度だが苦手意識がある。
また、標準のfetchやaxiosなどの通信用ライブラリ、swrやreact queryなどの高機能なライブラリ, 新しいsuspenseなど選択肢が多いのも悩みどころ。
方針
いったんNext.jsを使う手前、SWR + 標準のfetch APIを使用していく。
慣れてきて問題点が顕在化したら他の選択肢を検討する。
SWRについて
Next.jsを開発するVercelによるデータフェッチのためのReact Hooksライブラリ。
詳細な紹介記事
公式
ディレクトリ構成
こちらを参考にする
SWR実装
公式に則ってカスタムフックとして実装
src/hooks/use-board.ts
import useSWR from "swr";
const url = process.env.NEXT_PUBLIC_HOST_OTHELLO_BACKEND + "/api/board";
const fetcher = (): Promise<any> => fetch(url).then((res) => res.json());
const useBoard = () => {
const { data, error } = useSWR(`/api/board`, fetcher);
return {
board: data?.board,
isLoading: !error && !data,
isError: error,
};
};
export default useBoard;
ポイント
.envファイル
エンドポイントを環境変数として定義するために.envファイルを作成。
- .envの後に
.production
,.develop
などのサフィックスをつけることで環境ごとに読み込むファイルを分けることが可能 - クライアント側でも環境変数の値を使いたい場合は変数の頭に
NEXT_PUBLIC_
と付けると利用可能。 -
.env.production
の場合はnext start
実行時のみ、.develop
の場合はnext dev
実行時のみ読み込まれる
記事
ローディングアニメーション
catnoseさんの記事から拝借
const Board: React.FC = () => {
const { board, isLoading, isError } = useBoard();
if (isLoading || isError) {
return (
<div className="flex justify-center">
<div className="animate-ping h-2 w-2 bg-blue-600 rounded-full"></div>
<div className="animate-ping h-2 w-2 bg-blue-600 rounded-full mx-4"></div>
<div className="animate-ping h-2 w-2 bg-blue-600 rounded-full"></div>
</div>
);
}
// 正常な盤面表示処理
バックエンドAPIの構築
トピック
- URL設計
- JSONレスポンスの定義・レスポンス用クラス設計
- ステートレス?ステートフル?
- REST? SOAP?
バックエンド側
Laravelの実装
APIドキュメント
Swaggerが有名
POSTやPUTでレスポンスボディ何にする問題
汎用性やリクエスト回数削減の観点から更新後のリソースを返した方がいいっぽい
フロント側
Fetch API
APIに接続できない
リクエストはちゃんと飛んでる。
しかしずっと読込中のままで盤面情報が取得できない。
ブラウザのコンソール上にはこのエラーが。。。
(バックエンド側もHTTPSじゃないと行けないのか。。。?)
さらにコンソールのネットワーク画面でもこんな表示
strict-origin-when-cross-origin
。。。
そういえばドメイン(正確にはオリジン)が違う場合はなんか設定が必要だった気がする
CORS
セキュリティの観点から異なるオリジン間の通信には事前にCORSの設定が必要となる。
上記の記事のこの図が分かりやすかったので抜粋
初期のNextによってビルドされたページソースを表示したり必要な画像データ等を同一オリジンから取得する場合は問題ない。
しかしその後別オリジンのAPIサーバにアクセスする場合はCORSによる制御が必要になる。
(オリジン:https://yahoo.co.jp:443のようにドメインに加えてプロトコルとポート番号も含む)
今回は静的ホスティング:Vercel、バックエンドAPI:自前のEC2サーバだったためCORSの問題が発生した模様
LaravelでのCORSの解決
CORS自体の説明もわかりやすいサイト
php7.0移行ではconfig/cors.php
で簡単に設定できるらしい。middlewareでの設定もできるがconfigの方が簡単そう。
現段階での設定値
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => false,
初期値のままでもapiルートがちゃんと設定されていた。
ということはCORSは問題ない。別の問題が。。。?
Mixed Contents
とは
モダンブラウザのセキュリティ機能およびそれに関連して発生するエラーの一つ。HTTPSのサイト内から安全ではないHTTPで外部への通信を行おうとした場合に自動的にブロックする機能。
日本語では混合コンテンツ、混在コンテンツと呼ばれる。
2020年2月には完全にMixed Contents禁止となったらしい。
今回で言うと
まさに先ほどのこの画像の通り。
Mixed Contentsの記載もあるし、HTTPSからHTTPへの通信は安全じゃないからブロックしたとちゃんと書いてある。エラーメッセージはちゃんと読んで調べなきゃだめですね。
ドメイン何にしよう問題
- 本番でAPI叩くにはバックエンドのHTTPS化必須
- HTTPS化にはドメイン必須(抜け道はない・・?)
- ドメインを一つも持ってないから作らなくちゃいけない
- ドメインどうしよう&TLD(.comとか)どうしよう
- せっかく作るならネット上でのハンドルネームを統一したい
- .comとか.netは個人開発感がなくてうーん
- .techは更新料が高い
- .devは開発環境をイメージしちゃう
- .workは安いけどスパムに利用されてて信頼性が△?
ドメイン取得
- TLDは.devを取得
- レジストラはgoogle domains
.dev
- 開発者用ドメイン
- Googleが管理
- HSTSのPreload ListによってHTTPSでのアクセスを強制できるため安全
Google Domains
選定理由
- 高めだが初期費用、更新料がシンプルでわかりやすい
- 追加料金なしで全部込み
- 変なオプション料金がない
- デフォルトでWhois代行などが利用可能
- UIがシンプルでわかりやすい
- 広告がうるさくない
- GCPとの連携
- GoogleのDNSサーバーが利用可能(高速?)
手順
ドメイン設定
Google Domainsでドメインを取得したはいいが、バックエンドサーバがAWSを使用しているのでAWSでドメインの設定をしていく
ホストゾーンの設定
AWS > Route53 > ホストゾーン > ホストゾーンの作成
ドメイン名に今回設定するバックエンドサーバー用サブドメインを設定。
ホストゾーンのNSレコードをGoogle Domainsに設定
4つのNSレコードが作成されているはずなのでAWSコンソールからGoogle Domainsのカスタムネームサーバーにコピーする。
このようなメッセージが出ていたら「これらの設定に切り替える」をクリックして適用
HTTPS化していく
AWS Certificate Manager(ACM)でSSL証明書を発行
DNS認証が完了するまで少し時間がかかるので待つ
(ステータスが発行済みに変化するまで。30分ほど経っても変わらない場合はGoogle Domainsで「カスタムネームサーバー」タブが有効になっているか確認する)
(失敗)Aレコードを追加する
- EC2に紐づけているElastic IpをAレコードに設定
- しかしブラウザに先ほどのドメインを入力してもタイムアウト
原因(多分)
- ACMの証明書はELBなどのサービスを使わないとAWSのサービスにセットできない
- AレコードにElastic IPのアドレスを設定していたためHTTPS化できていなかった
- 上記の理由でHTTPS化されていない & .devドメインはHTTPS強制のためタイムアウトしたと考えられる
⏬
ACM 証明書またはプライベート ACM Private CA 証明書を AWS ベースのウェブサイトとアプリケーションに直接インストールすることはできません。
証明書を設定できるパターンの詳解
ELBの設定と証明書のセット
Application Load Balancer(ALB)を使用。
リスナーに証明書をセットする。
ターゲットグループやセキュリティグループを設定してHTTPS化したドメインに接続できればOK!
※ALB自体の設定が終わったら、ホストゾーンのAレコードの宛先を該当のALBにすることを忘れずに!
詳細は次項で説明します。
(まとめ)ALB + EC2のバックエンド構成
一般的なALB + EC2構成。ゆくゆくはAPI GatewayやLambda、Fargateにも手を出したい。
こちらを参考にさせていただきました。
(出典:https://oji-cloud.net/2019/09/15/post-3017/)
- Google Domainsで大元のドメイン取得(例:
hoge.dev
) - Route 53のホストゾーンにサブドメインを設定(
foo.hoge.dev
) - AWS Certificate ManagerでSSL証明書取得
- 証明書をALBに設置
- ホストゾーンのAレコードの宛先をALBに設定
- クライアントからALBまではHTTPS、ALBからEC2まではHTTP
(引き続きElastic IPは使用。ただしHTTPからのパブリックなアクセスは封鎖。ALBを介したアクセスのみ許容)
ポイント
EC2へのHTTPアクセスをALBからのものに限定する
- パブリックなアクセスはHTTP、SSH問わず禁止したい
- でも自分だけはSSHで接続したい
- 踏み台サーバーがないため、EC2インスタンスに直接アクセスしたいのでElastic IPを使う
こんなときにちょうど良いのがこちら。
SSHは普通のセキュリティグループで制限をかけ、HTTPの場合は「ALBに設定したセキュリティグループからのHTTP通信のみを許容するセキュリティグループ」を作成する。
詳しくはこちら
セキュリティグループの命名、役割分担のTips
セキュリティグループが段々と増えてきてどれに何の役割を持たせているのか分からなくなってきたため。
ここでも単一責任の原則と分かりやすい命名は大事だなーと実感
アプリケーション側(Laravel)の対応
Laravelのroute()
メソッド等のURL生成機能が何もしないとHTTPのURLを作ってしまう。
- 初回アクセス時はHTTPSで接続
- リンクをクリックしてページを遷移する際、遷移先URLがHTTP
- ブラウザが「安全ではないページへの遷移」警告を表示
というわけでURL生成系メソッドにHTTPS化を強制する。
現状ではローカル開発環境ではlocalhostのhttp通信を使っているため、いったん本番でのみHTTPSとなるようにした。
app/Providers/AppServiceProvider.php
// ・・・
public function boot(UrlGenerator $url)
{
if (app()->isProduction()) $url->forceScheme('https');
}
// ・・・
やっと本番のフロント(vercel)からAPIへのアクセス&取得データをもとにした盤面の表示に成功!
フロント(Vercel)も独自ドメイン化
Vercelでデフォルトで使用できるドメイン(https://othello-ebinas.vercel.appなど)でも十分だけど、せっかくなので以前取得したebinas.devのサブドメインを割り当てて、othello.ebinas.dev
として設定してみる。
手順
- Vercel > Settings > Domainsから独自ドメイン(othello.ebinas.dev)を追加
- DNSサーバが見つからないと言われるのでドメイン取得元(今回はGoogle Domains)のNSレコードにvercelのDNSサーバーを追加
- 以上で設定完了。証明書の設定も全て自動でやってくれる。
結果
これが無料でできてしまうvercelすごい。。。
バックエンドの名前解決できなくなる問題が突如発生
VercelのDNS設定をした翌日
ネットの記事を調べた感じ、証明書が正しくなさそう。。。
とはいえあまり情報がない。。
発生当時の状況
バックエンド
- サーバーはEC2&ALBを使用
- DNSサーバはAWSのRoute53 & 証明書はAWS ACM
- Google Domainsコンソールの
カスタムネームサーバー
>ネームサーバー
にRoute 53に表示されているNSレコードを設定
フロント
- Vercelでホスティング
- DNSサーバー & 証明書もVercel
- バックエンドと同じくGoogle Domainsコンソールの同じ場所にNSレコードを追記
digコマンド
いったんどんな流れで名前解決されているか調査
$ dig ddd-othello.ebinas.dev +trace
(省略)
ddd-othello.ebinas.dev. 60 IN A 76.76.21.9
;; Received 67 bytes from 198.51.44.7#53(ns1.vercel-dns.com) in 7 ms
from 198.51.44.7#53(ns1.vercel-dns.com)
...バックエンドドメインがVercelのDNSサーバで名前解決されてる?
解決
Google Domainsのカスタムネームサーバー
にvercelとroute53のNSレコードを併記していたのが原因かな?
⏬
カスタムネームサーバ
タブの設定を削除。デフォルトのGoogleのDNSサーバに下記の設定
- レコード名
othello.ebinas.dev
にvercelのCNAMEを追記(Vercelの推奨設定) - レコード名
ddd-othello.ebinas.dev
にRoute53のNSレコード(4つ)を設定
⏬
無事安定してバック・フロント共に接続可能に!
Vercel・AWSのDNSサーバに直接権威を移譲するのではなく、
GoogleのDNSサーバ >> Vercel・AWSのDNSサーバ
に割り振る流れにしたのが成功の要因かなと思っているけれどいまいち理解しきれていない。
DNSやネットワークの仕組みはまだまだ要勉強。
APIを本格的に開発
インフラ周りが固まってきたのでゴールデンウィークで本格的に開発していく。
余談:GW前に「良いコード/悪いコード(通称ミノ駆動本)」、「システム運用アンチパターン」、「ソフトウェアアーキテクチャの基礎」、「ラズベリー本(TypeScript)」など面白そうな本が発売されていて時間が足りない。。。
下準備・しておきたいこと
Talend API Tester
バックエンドAPIのテスト用chrome拡張機能
DDDのアプリケーション層、プレゼンテーション層の復習
APIエンドポイントとドメイン層の繋ぎ方や構造をシンプルに保つ方法を学び直す。
(今読み返すと新鮮な学びも多い)
- 松岡さんの本(モデリング/実装ガイド等)をベースに。
- 読みきれていなかった成瀬さんの本(ボトムアップ〜)も読み返したい
API設計
- RESTに改めて入門
- APIドキュメントのまとめ方
reactの副作用処理
- hooksをちゃんと理解する
- SWRのメリット/デメリットをちゃんと把握した上で使えるようにする
- POSTなどの処理の書き方を理解する
- 非同期処理を理解する
https://zenn.dev/mizchi/articles/understanding-promise-by-ts-eventloop
SPAのセキュリティ
SPAとバックエンドの連携を考えたら疲れてきた。。。
いったんバックエンドに戻ってDDDのモデリングをしたり実装を見直したりしてみることにする