Closed24

【4月】AWS + Laravel + React.jsでオセロゲームを作ってみる

ebinaebina

概要

やること

あとから振り返れるよう、これまでちまちま作ってきたオセロwebアプリの開発の途中経過を報告・メモしていく

リポジトリ

https://github.com/ebinase/othello

現状

バックエンドにLaravelを使用。
ドメイン駆動設計でビジネスロジックを記述しゲームとしてのオセロの主要な機能は開発済み。
現在ブラウザ上から遊ぶことも可能(EC2を使用)。フロント側は未開発。

直近の目標

バックエンド

  • API特化(REST)
  • RDS等でデータベース導入
  • Fargateなどでコンテナ化して運用(未定)
  • いったんlaravel breezeなどのスキャフォールドは使わない
  • 設計をブラッシュアップ
  • ボットの追加

フロント

  • Next.jsでSPA化
  • Vercel or S3でホスト
  • tailwind.cssで装飾
ebinaebina

フロントエンドプロジェクト作成

$ 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にプッシュ

新規プロジェクトを作成しプッシュ
https://github.com/ebinase/othello-frontend

ebinaebina

ESLint, Prettier導入

ベースはここ
https://fwywd.com/tech/next-eslint-prettier

こちらも参考に
https://zuma-lab.com/posts/next-eslint-prettier-settings

tailwind導入

こちらを参考に。

公式
https://tailwindcss.com/docs/guides/nextjs
詳しい
https://fwywd.com/tech/next-tailwind
見やすい
https://zenn.dev/shimakaze_soft/articles/0ce52691b6fc3e

注意点

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

公式アナウンス
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: [],
};

ebinaebina

この段階でよくわかってないこと

  • コンポーネントの設計
  • ajaxライブラリどれ使う問題
  • cssライブラリ選定(今のとこtailwind)と動作イメージ

やりたいこと

  • Github projects(beta)をフル活用
    • フロントとバックを統合するか別々に扱うか
  • ニューモーフィズムorグラスモーフィズムを導入してみる
ebinaebina

デザイン

テーマ

ニューモーフィズムに興味があったので作ってみる

ニューモーフィズム

むにゅっとした見た目のデザイン

とは
https://zenn.dev/yuyaamano23/articles/61e580b0a2ebe0

簡単ジェネレータ
https://neumorphic.design/
https://neumorphism.io/#e0e0e0

提唱した人の記事
https://uxdesign.cc/neumorphism-in-user-interfaces-b47cef3bf3a6

Dribbble
https://dribbble.com/search/neumorphism

グラスモーフィズム

ニューモーフィズムの次に来ると言われてる?やつ
AppleのOSのコントロールパネル等に使われてたりする

Dribbble
https://dribbble.com/tags/glassmorphism

ebinaebina

盤面とコマのコンポーネント作成

ニューモーフィズムで作っていく

まずはそれっぽく作ってみる

一つのコンポーネントの中に全部まとめて書いてみる

  • いまいちjsx内でのループが分からずベタ書き
  • 一旦4x4で

Board.tsx

BoardとFieldに分離

盤面(Board)の内部でマス(Field)を8x8で描画する。
子要素を表示するときはコンポジションがいいらしい。

子要素とコンポジション

繰り返し処理はここを参照した。コンポジションモデルもここから。
https://nishinatoshiharu.com/jsx-map-roop/

上記の記事でも触れられていた公式の解説。
やっぱり一通り公式ドキュメントを読んでおくのは大事。。。
https://ja.reactjs.org/docs/composition-vs-inheritance.html

childrenをpropsの型定義に追加するには

基本的にpropsにchildrenも内包されるため、childrenの型定義は不要。
ただ、明示的に型定義したい場合はReact.ReactNodeを使用する。

詳しくはこちらの記事へ
https://maku.blog/p/xenv4bh/

上記の記事より引用

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等を使うべき。
https://zenn.dev/luvmini511/articles/f7b22d93e9c182

propsをspread展開({...props})する

盤面情報配列内オブジェクトとpropsが一致してて簡易的に書きたいと思っていたのでちょうどよかった
https://zenn.dev/terrierscript/articles/2016-10-07-react-component-tips

例に漏れず公式サイトにもちゃんと載ってた
https://reactjs.org/docs/jsx-in-depth.html#spread-attributes

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にテーマカラーなどを設定

参考になった記事

https://qiita.com/HIGAX/items/28f3bec814928b7395da
→ イマイチイメージしきれてなかったimport, exportについて
(phpの1ファイル1クラスのイメージが抜けなかったので)

https://zenn.dev/kabu/articles/81c70e908d4ca0

ツール

カラージェネレーター
https://smart-swatch.netlify.app/

ebinaebina

途中経過

  • いったん4x4のまま
  • 盤面の状態を表す配列をpropsに渡して描画
  • コンポーネントの階層は以下の通り
    • Board
      • Field
        • Stone
ebinaebina

非同期通信と状態管理

盤面がいったん表示できるところまで行ったのでバックエンドAPI作成&盤面データ取得をしてみる。

懸念点

useEffectなど、副作用関連の処理は書籍で読んだ程度だが苦手意識がある。
また、標準のfetchやaxiosなどの通信用ライブラリ、swrやreact queryなどの高機能なライブラリ, 新しいsuspenseなど選択肢が多いのも悩みどころ。

方針

いったんNext.jsを使う手前、SWR + 標準のfetch APIを使用していく。
慣れてきて問題点が顕在化したら他の選択肢を検討する。

SWRについて

Next.jsを開発するVercelによるデータフェッチのためのReact Hooksライブラリ。

詳細な紹介記事
https://zenn.dev/uttk/articles/b3bcbedbc1fd00

公式
https://swr.vercel.app/ja/docs/getting-started

ディレクトリ構成

こちらを参考にする
https://zenn.dev/yuitosato/articles/b3b3d22accdaa3

ebinaebina

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実行時のみ読み込まれる

記事
https://fwywd.com/tech/next-env

https://zenn.dev/aktriver/articles/2022-04-nextjs-env

ローディングアニメーション

catnoseさんの記事から拝借
https://zenn.dev/catnose99/articles/19a05103ab9ec7

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>
    );
  }

  // 正常な盤面表示処理  

ebinaebina

バックエンドAPIの構築

トピック

  • URL設計
  • JSONレスポンスの定義・レスポンス用クラス設計
  • ステートレス?ステートフル?
  • REST? SOAP?

https://www.utakata.work/entry/laravel/json-web-api

バックエンド側

Laravelの実装

https://www.utakata.work/entry/laravel/json-web-api

APIドキュメント

Swaggerが有名
https://qiita.com/disc99/items/37228f5d687ad2969aa2

POSTやPUTでレスポンスボディ何にする問題

汎用性やリクエスト回数削減の観点から更新後のリソースを返した方がいいっぽい
https://qiita.com/tubutubu_mustard/items/c1f4519841b61cb549a0


フロント側

Fetch API

https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch

ebinaebina

APIに接続できない

リクエストはちゃんと飛んでる。
しかしずっと読込中のままで盤面情報が取得できない。

ブラウザのコンソール上にはこのエラーが。。。

(バックエンド側もHTTPSじゃないと行けないのか。。。?)

さらにコンソールのネットワーク画面でもこんな表示

strict-origin-when-cross-origin。。。
そういえばドメイン(正確にはオリジン)が違う場合はなんか設定が必要だった気がする

CORS

セキュリティの観点から異なるオリジン間の通信には事前にCORSの設定が必要となる。
https://qiita.com/att55/items/2154a8aad8bf1409db2b

上記の記事のこの図が分かりやすかったので抜粋

初期のNextによってビルドされたページソースを表示したり必要な画像データ等を同一オリジンから取得する場合は問題ない。
しかしその後別オリジンのAPIサーバにアクセスする場合はCORSによる制御が必要になる。
(オリジン:https://yahoo.co.jp:443のようにドメインに加えてプロトコルとポート番号も含む)

今回は静的ホスティング:Vercel、バックエンドAPI:自前のEC2サーバだったためCORSの問題が発生した模様

LaravelでのCORSの解決

CORS自体の説明もわかりやすいサイト
https://qiita.com/kyo-san/items/a507aa0b46037df1b139

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禁止となったらしい。
https://www.future-shop.jp/magazine/info-mixed-content

今回で言うと

まさに先ほどのこの画像の通り。
Mixed Contentsの記載もあるし、HTTPSからHTTPへの通信は安全じゃないからブロックしたとちゃんと書いてある。エラーメッセージはちゃんと読んで調べなきゃだめですね。

ebinaebina

ドメイン何にしよう問題

  • 本番でAPI叩くにはバックエンドのHTTPS化必須
  • HTTPS化にはドメイン必須(抜け道はない・・?)
  • ドメインを一つも持ってないから作らなくちゃいけない
  • ドメインどうしよう&TLD(.comとか)どうしよう
    • せっかく作るならネット上でのハンドルネームを統一したい
    • .comとか.netは個人開発感がなくてうーん
    • .techは更新料が高い
    • .devは開発環境をイメージしちゃう
    • .workは安いけどスパムに利用されてて信頼性が△?
ebinaebina

ドメイン取得

  • TLDは.devを取得
  • レジストラはgoogle domains

.dev

  • 開発者用ドメイン
  • Googleが管理
  • HSTSのPreload ListによってHTTPSでのアクセスを強制できるため安全

https://qiita.com/tomoyk/items/187aa6723e80e1bba675

Google Domains

https://domains.google.com/

選定理由

  • 高めだが初期費用、更新料がシンプルでわかりやすい
  • 追加料金なしで全部込み
  • 変なオプション料金がない
  • デフォルトでWhois代行などが利用可能
  • UIがシンプルでわかりやすい
  • 広告がうるさくない
  • GCPとの連携
  • GoogleのDNSサーバーが利用可能(高速?)

手順

https://gadget-tips.com/google-domains-register/

ebinaebina

ドメイン設定

Google Domainsでドメインを取得したはいいが、バックエンドサーバがAWSを使用しているのでAWSでドメインの設定をしていく

ホストゾーンの設定

AWS > Route53 > ホストゾーン > ホストゾーンの作成

ドメイン名に今回設定するバックエンドサーバー用サブドメインを設定。

ホストゾーンのNSレコードをGoogle Domainsに設定

4つのNSレコードが作成されているはずなのでAWSコンソールからGoogle Domainsのカスタムネームサーバーにコピーする。
https://dev.classmethod.jp/articles/create-subdomain-on-route53/

このようなメッセージが出ていたら「これらの設定に切り替える」をクリックして適用

HTTPS化していく

AWS Certificate Manager(ACM)でSSL証明書を発行

https://zenn.dev/mn87/articles/90609fffd8c634

DNS認証が完了するまで少し時間がかかるので待つ
(ステータスが発行済みに変化するまで。30分ほど経っても変わらない場合はGoogle Domainsで「カスタムネームサーバー」タブが有効になっているか確認する)

(失敗)Aレコードを追加する

  • EC2に紐づけているElastic IpをAレコードに設定
  • しかしブラウザに先ほどのドメインを入力してもタイムアウト

原因(多分)

  • ACMの証明書はELBなどのサービスを使わないとAWSのサービスにセットできない
    • AレコードにElastic IPのアドレスを設定していたためHTTPS化できていなかった
  • 上記の理由でHTTPS化されていない & .devドメインはHTTPS強制のためタイムアウトしたと考えられる


https://docs.aws.amazon.com/ja_jp/acm/latest/userguide/acm-services.html

ACM 証明書またはプライベート ACM Private CA 証明書を AWS ベースのウェブサイトとアプリケーションに直接インストールすることはできません。

証明書を設定できるパターンの詳解
https://recipe.kc-cloud.jp/archives/11067

ELBの設定と証明書のセット

Application Load Balancer(ALB)を使用。
リスナーに証明書をセットする。

https://oji-cloud.net/2019/09/15/post-3017/

ターゲットグループやセキュリティグループを設定してHTTPS化したドメインに接続できればOK!
※ALB自体の設定が終わったら、ホストゾーンのAレコードの宛先を該当のALBにすることを忘れずに!

詳細は次項で説明します。

ebinaebina

(まとめ)ALB + EC2のバックエンド構成

一般的なALB + EC2構成。ゆくゆくはAPI GatewayやLambda、Fargateにも手を出したい。
こちらを参考にさせていただきました。
https://oji-cloud.net/2019/09/15/post-3017/


(出典: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通信のみを許容するセキュリティグループ」を作成する。

詳しくはこちら
https://kazken.com/aws-alb-security-group/

セキュリティグループの命名、役割分担のTips

セキュリティグループが段々と増えてきてどれに何の役割を持たせているのか分からなくなってきたため。
ここでも単一責任の原則と分かりやすい命名は大事だなーと実感
https://qiita.com/kenjiszk/items/b1c0bfb75a2c44253cd6


アプリケーション側(Laravel)の対応

Laravelのroute() メソッド等のURL生成機能が何もしないとHTTPのURLを作ってしまう。

  • 初回アクセス時はHTTPSで接続
  • リンクをクリックしてページを遷移する際、遷移先URLがHTTP
  • ブラウザが「安全ではないページへの遷移」警告を表示

というわけでURL生成系メソッドにHTTPS化を強制する。

https://webty.jp/staffblog/production/post-3209/

現状ではローカル開発環境ではlocalhostのhttp通信を使っているため、いったん本番でのみHTTPSとなるようにした。

app/Providers/AppServiceProvider.php

// ・・・
public function boot(UrlGenerator $url)
{
    if (app()->isProduction()) $url->forceScheme('https');
}
// ・・・
ebinaebina

やっと本番のフロント(vercel)からAPIへのアクセス&取得データをもとにした盤面の表示に成功!

ebinaebina

フロント(Vercel)も独自ドメイン化

Vercelでデフォルトで使用できるドメイン(https://othello-ebinas.vercel.appなど)でも十分だけど、せっかくなので以前取得したebinas.devのサブドメインを割り当てて、othello.ebinas.devとして設定してみる。

手順

https://maku.blog/p/9vakw8i/

  1. Vercel > Settings > Domainsから独自ドメイン(othello.ebinas.dev)を追加
  2. DNSサーバが見つからないと言われるのでドメイン取得元(今回はGoogle Domains)のNSレコードにvercelのDNSサーバーを追加
  3. 以上で設定完了。証明書の設定も全て自動でやってくれる。

結果

https://othello.ebinas.dev
vercelでホスティングしながら独自ドメインが設定できている。
これが無料でできてしまうvercelすごい。。。

ebinaebina

バックエンドの名前解決できなくなる問題が突如発生

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やネットワークの仕組みはまだまだ要勉強。

ebinaebina

APIを本格的に開発

インフラ周りが固まってきたのでゴールデンウィークで本格的に開発していく。

余談:GW前に「良いコード/悪いコード(通称ミノ駆動本)」、「システム運用アンチパターン」、「ソフトウェアアーキテクチャの基礎」、「ラズベリー本(TypeScript)」など面白そうな本が発売されていて時間が足りない。。。

下準備・しておきたいこと

Talend API Tester

バックエンドAPIのテスト用chrome拡張機能
https://chrome.google.com/webstore/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm?hl=ja

DDDのアプリケーション層、プレゼンテーション層の復習

APIエンドポイントとドメイン層の繋ぎ方や構造をシンプルに保つ方法を学び直す。
(今読み返すと新鮮な学びも多い)

  • 松岡さんの本(モデリング/実装ガイド等)をベースに。
  • 読みきれていなかった成瀬さんの本(ボトムアップ〜)も読み返したい

API設計

  • RESTに改めて入門
  • APIドキュメントのまとめ方

reactの副作用処理

SPAのセキュリティ

https://www.slideshare.net/ockeghem/phpconf2021spasecurity

https://zenn.dev/koduki/articles/0fe6cc5ada58e5600f75#今後のspaに必要な気がすること

ebinaebina

SPAとバックエンドの連携を考えたら疲れてきた。。。
いったんバックエンドに戻ってDDDのモデリングをしたり実装を見直したりしてみることにする

このスクラップは2023/05/04にクローズされました