SPA構成のWebアプリケーションを開発する方法まとめ【Docker・NextJS(React)・Vercel・Rails7(APIモード)・AWS ECS(Fargate)】


 

こんにちは。Tomoyuki(@tomoyuki65)です。

以前書いた記事にてRailsチュートリアルベースのWebアプリ開発についてはまとめましたが、次のステップとしては現在主流(トレンド)であるSPA(シングルページアプリケーション)構成のWebアプリケーション開発になると思います。

具体的にはフロントエンド部分としてWeb画面をReactやVue.jsなどで開発(JavaScript系の言語で開発)、バックエンド部分として別の言語(用途や用件などに合わせて適切な言語を選ぶ。マイクロサービスならGo言語がトレンドっぽい)でAPIを開発し、最終的にはフロントエンドとバックエンドを連携(フロントエンドからバックエンドのAPIを利用する)させる形でWebアプリケーションを構築します。

2022年11月現在では、Vue.jsよりもReactを学んだ方が将来性がありそうな感じではあるので、今回はフロントエンド部分はReact系のフレームワークであるNext.jsを使うことを決めました。

また、バックエンド部分に関してはこれまでRailsを使ってきたため、それを活かしてRails7のAPIモードを利用してみます。

※ただし、バックエンド部分にRailsのAPIモードを使うのは現在のトレンドではない(過去の案件では多いと思うが。。)ため、その点はご注意下さい。フロントエンドをReactやVue.jsで開発するならバックエンドもJavaScript系の言語で開発した方が効率が良く、マイクロサービス系ならGo言語、AIや機械学習系ならPython系のフレームワークを利用するのがトレンドっぽいです。逆にRailsに関してはRails7からHotwireでSPA風の画面を開発できるようになっているため、今後はRailsを使うならRailsだけで開発するのが主流になると思われます。

ということで、この記事では、フロントエンドをNext.js、バックエンドをRails7のAPIモードとしてSPA構成のWebアプリケーションを開発する方法をまとめます。

 

関連記事👇

35歳、Railsチュートリアルに挑む編【Web系エンジニア1年目】

2022年7月17日

 



目次

SPA構成のWebアプリケーションを開発する方法まとめ【Docker・NextJS(React)・Vercel・Rails7(APIモード)・AWS ECS(Fargate)】

  •  Next.js(React)でフロントエンドの開発環境を構築する方法
  •  Next.js(React)に「Tailwind CSS」を導入する方法
  •  Next.js(React)にテスト用フレームワーク「Jest」を導入する方法
  •  Next.jsアプリをGitHub連携でVercelにデプロイする方法
  •  フロントエンドにCircleCIを連携させてCI/CDパイプラインを構築する方法
  •  フロントエンドに関する補足
  •  Rails7のAPIモードでバックエンドの開発環境を構築する方法
  •  Rails7アプリにテスト用APIを作成
  •  Rails7にテスト用フレームワーク「RSpec」を導入する方法
  •  Rails7アプリのタイムゾーンを日本に変更する方法
  •  開発環境でフロントエンドとバックエンドの連携を検証
  •  バックエンド(Nginx・Rails7・MySQL構成)をAWS ECS(Fargate)にデプロイする方法
  •  AWS ECS(Fargate)にデプロイしたアプリのIPを固定(独自ドメインを利用)する方法
  •  RailsアプリでHTTPS通信を強制化する方法
  •  バックエンドにCircleCIを連携させてCI/CDパイプラインを構築する方法
  •  Firebase Authenticationによるログイン関連機能の実装方法

 

Next.js(React)でフロントエンドの開発環境を構築する方法

まずはフロントエンドの開発環境を構築していくため、以下のコマンドを実行して必要なファイルを作成します。

$ mkdir ディレクトリ名
$ cd ディレクトリ名
$ touch Dockerfile
$ touch docker-compose.yml

※ディレクトリ名は任意の名前を付けて下さい(例:front-appなど)

 

次に各種ファイルの中身をそれぞれ記載します。

# 2022年9月時点の最新版のNode
FROM node:18.10.0

# 作業ディレクトリの指定
WORKDIR ディレクトリ名

# コンテナ起動時に実行するコマンド
CMD [ "yarn", "build" ]

※ディレクトリ名は任意の名前を付けて下さい(例:front-appなど)

 

version: "3"
  services:
    # アプリの設定
    front:
      # コンテナ名の指定
      container_name: コンテナ名
      # Dockerfileのあるディレクトリパスを指定
      build: .
      # 標準入出力デバイスを設定
      tty: true
      # ポートの指定(外部からアクセス時のポート:コンテナからアクセス時のポート)
      ports:
        - "3000:3000"
      # データの永続化(ローカルのカレントディレクトリにマウント)
      volumes:
        - .:/DockerfileのWORKDIRのディレクトリ名
      # コマンド実行
      command: yarn dev

※コンテナ名(container_name)は任意の名前を付けて下さい(例:spa-f_appなど)。また、volumesの「:」より右側はDockerfileのWORKDIRで設定したディレクトリ名を指定します。(例えばWORKDIRが「front-app」なら、volumesの部分は「- .:/front-app」になります。)

 

次に以下のコマンドを実行し、一時的にDockerコンテナを作成しつつ、Next.jsのファイルを作成します。

$ docker compose run --rm front yarn create next-app ディレクトリ名 --typescript

※ディレクトリ名は任意の名前を付けて下さい(例:front-appなど)

 

次に以下のコマンドを実行し、作成されたディレクトリの中身のファイルをルートディレクトリに全て移動させ、ファイルが入っていたディレクトリは削除します。

$ mv ディレクトリ名/* ディレクトリ名/.* .
$ rm -r ディレクトリ名

※ディレクトリ名は上記「yarn create next-app ディレクトリ名」で指定したディレクトリ名にする

 

これでNext.jsの導入が完了したので、以下のコマンドを実行してdocker-composeでサーバーを立ち上げてみます。

$ docker compose up -d

 

ブラウザで「http://localhost:3000/」にアクセスし、以下の画面が表示されればOKです。

 

Next.js(React)に「Tailwind CSS」を導入する方法

次に今回はReactと相性が良いと言われている「Tailwind CSS」と、テスト用のフレームワークを導入していきます。

まずは「Tailwind CSS」を導入するため、以下のコマンドを実行して必要なパッケージをインストールし、その後に初期化を行います。

$ docker compose exec front yarn add -D tailwindcss postcss autoprefixer
$ docker compose exec front yarn tailwindcss init -p

※サーバーを立ち上げている状態では「exec」コマンドを使います。

 

次に作成されたファイル「tailwind.config.js」のmodule.exprotsのcontentに「”./pages/**/*.{js,ts,jsx,tsx}”,」と「”./components/**/*.{js,ts,jsx,tsx}”,」を追記します。

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

 

次に「pages/_app.tsx」に「import“tailwindcss/tailwind.css”」を追記し、Tailwind CSSを読み込めるようにします。

import '../styles/globals.css'
import "tailwindcss/tailwind.css"
import type { AppProps } from 'next/app'

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}

export default MyApp

 

これで「Tailwind CSS」の基本設定が完了したので、試しに”aboutページ”を作成し、”Hello World!”をしてみます。

では以下のコマンドを実行し、「pages」フォルダの直下にファイル「about.tsx」を作成します。

$ cd pages
$ touch about.tsx

 

次に「about.tsx」の内容を以下のように修正します。

import type { NextPage } from 'next'

const About: NextPage = () => {
  return (
    <h1>
      Hello World!
    </h1>
  )
}

export default About

 

次に以下のコマンドを実行し、立ち上げていたサーバーを再起動させます。

$ docker compose stop
$ docker compose start

 

次にブラウザで「http://localhost:3000/about」にアクセスし、以下の画面が表示されればOKです。

 

次にTailwind CSSが有効かを確認するため、「about.tsx」の内容を以下のように修正します。

import type { NextPage } from 'next'

const About: NextPage = () => {
  return (
    <h1 className="text-blue-700 underline decoration-gray-500">
      Hello World!
    </h1>
  )
}

export default About

※例として文字列「Hello World!」の文字色を青に変え、加えて下線を引くようにh1タグのclassNameを指定しています。

 

ブラウザで「http://localhost:3000/about」にアクセスし、画面が以下のようになっていればTailwind CSSが有効になっているのでOKです。

 

Next.js(React)にテスト用フレームワーク「Jest」を導入する方法

次にNext.jsで利用するテスト用のフレームワークなどを導入しますが、現状では「Jest」と「React -Testing -Library」を用いるのがベストプラクティスとされているようです。

そのため、以下のコマンドを実行し、必要なパッケージをインストールします。

$ docker compose exec front yarn add -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event

 

次に以下のコマンドを実行し、ルートディレクトリの直下に設定用のファイル「jest.config.js」と「jest.setup.js」を作成します。

$ cd ..
$ touch jest.config.js
$ touch jest.setup.js

※直前の作業で現在のディレクトリが「pages」になっている想定のため、一個前の階層に戻るためにコマンド「cd ..」を実行しています。

 

次に「jest.config.js」の中身は、Next.js公式のSetting up Jest(with the Rust Compiler)にあるjest.config.jsの中身をコピペし、「setupFilesAfterEnv: [‘<rootDir>/jest.setup.js’],」のコメントアウトは外して「jest.setup.js」を使えるようにします。

// jest.config.js
const nextJest = require('next/jest')

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
})

// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const customJestConfig = {
  // Add more setup options before each test is run
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
  moduleDirectories: ['node_modules', '<rootDir>/'],
  testEnvironment: 'jest-environment-jsdom',
}

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)

 

次にデフォルトでは使えない「toBeInTheDocument()」などのマッチャー(テストの評価条件などを定義するメソッドのこと)を利用するため、セットアップ用のファイル「jest.setup.js」に「import “@testing-library/jest-dom/extend-expect”」を記述してライブラリをインポートします。

import "@testing-library/jest-dom/extend-expect"

 

次にpackage.jsonのscriptsにjest実行用の設定として「“test”: “jest”,」を追加します。

{

・・
"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "test": "jest",
  "lint": "next lint"
},

・・

}

 

これで基本的な設定が一通り完了したので、試しに先ほど作成したabout.tsxのテストを書いてみます。

では以下のコマンドを実行し、ルートディレクトリの直下にテスト用のフォルダ「__tests__」を作成し、その中にテスト用のファイル「about.test.tsx」を作成します。

$ mkdir __tests__
$ cd __tests__
$ touch about.test.tsx

 

次に簡単なテストとしてaboutページに「Hello World!」が表示されていることを確認するため、ファイル「about.test.tsx」の内容は以下の通りに修正します。

// テスト用のライブラリをインポート
import { render, screen } from "@testing-library/react";
// テストをしたいモジュールをインポート
import About from "../about";

// テストの説明
describe("About", () => {
  // テストケース
  it("Hello World!が表示されていること", () => {
    // About(about.tsx)を出力
    render(<About />);
    // screen.getByTextで文字列を検索し、toBeInTheDocument()で存在確認
    expect(screen.getByText("Hello World!")).toBeInTheDocument();
  });
});

 

これでテストを実行する準備が整ったので、以下のコマンドを実行してJestを実行します。

$ docker compose exec front yarn test

 

テスト実行後、以下のような結果になればOKです。

 

また、jest実行後はフォルダ「.swc」が作成されますが、これはGitHubで管理する必要がないため、「.gitignore」で除外しておきます。

・・

# jest
.swc

・・

 

最後にテスト実行時に使うと思われる他のコマンドについてもまとめておきます。

・テストを実行する基本コマンド

$ docker compose exec front yarn test

 

・ウォッチモード(テストコードに変更があるたびにテストを実行することができるモード)でテストを実行するコマンド

$ docker compose exec front yarn test --watch

または

$ docker compose exec front yarn test --watchAll

※「–watchAll」は全てのテストを実行

 

例えばウォッチモードで実行した場合は以下のようになります。Watch Usageに記載のコマンドで各種操作もできますが、終了したい場合はコマンド「q」を入力して下さい。

 

・特定のファイルのみテストを実行したい場合

docker compose exec front yarn test ファイルのパスを指定

※ファイルのパスについては、例えば「__tests__/about.test.tsx」など

 



Next.jsアプリをGitHub連携でVercelにデプロイする方法

Next.jsは本番環境としてVercel(Next.jsの開発元かつホスティングサービス)を使うのが最適になっており、GitHubと連携すれば簡単にデプロイできるほか、個人利用なら無料で使えるのがメリットです。

※ただし、利用するには電話番号認証が必要になるようです。

そこで今回もGitHubと連携してVercelにデプロイする方法を解説します。下記ではVercelにアカウントと登録(GitHubと連携)する部分から解説するため、まずは事前に上記で作成した各種ファイルをGitHubにコミットしておいて下さい。

 

ではまずVercelの公式サイトにアクセスし、画面右上の「Sign Up」をクリックします。

 

次にGitHubで連携するため、「Continue with GitHub」をクリックします。

 

次に承認画面が表示されるため、「Authorize Vercel」をクリックします。

 

次に電話番号認証の画面が表示されます。

 

国番号を選択後、携帯番号を入力し、「Continue」をクリックします。

 

入力した携帯番号宛に、SMSで認証コードが送られます。

 

先ほどの画面には認証コード入力画面が表示されるため、送られてきた認証コードを入力します。

 

これでVercelのアカウント作成が完了し、プロジェクト作成画面が表示されます。

 

次にGitHubからリポジトリをインポートするため、「Continue with GitHub」をクリックします。

 

次に「Select a Git Namespace」をクリックし、「+ Add GitHub Account」を選択します。

 

次にインストールするリポジトリ選択画面が表示されるため、対象のリポジトリのみをインストールする場合は「Only select repositories」を選択し、Select repositoriesからインストールしたいリポジトリを選択後、画面下の「Install」をクリックします。

 

次にGitHubアカウントのパスワード入力画面が表示されるので、パスワードを入力後、「Confirm」をクリックします。

 

これでGitHubから対象のリポジトリをインストールできたので、Select a Git Namespaceで連携したGitHubアカウントを選択します。

 

これでインストールしたリポジトリが表示されるので、対象のリポジトリの「Import」をクリックします。

 

次にプロジェクトの設定画面が表示されるため、特に必要な設定事項がなければデフォルト設定のまま画面下の「Deploy」をクリックします。

 

これでデプロイが実行されます。

 

デプロイに成功すると以下のような画面が表示されるため、画面右上の「Continue to Dashboard」をクリックします。

 

これで対象のプロジェクトのダッシュボード画面が表示されるので、画面右上の「Visit」をクリックします。

 

これでデプロイしたWeb画面が開きます。

 

試しに上記で作成したaboutページを確認するため、「/about」にアクセスしてみますが、以下のような画面が表示されればOKです。これでVercelへのデプロイは完了です!

 



フロントエンドにCircleCIを連携させてCI/CDパイプラインを構築する方法

次にフロントエンドに対してテストの自動化(ブランチをGitHubにプッシュしたらテストを自動実行)および、本番環境(Vercel)への自動デプロイ(GitHubのmainブランチにマージしたらデプロイを実行)を実行できるようにするため、CircleCIを連携させます。

まずVercelとGitHubを連携している場合、GitHubへのpushを検知して自動でデプロイ処理が走ってしまうため、まずはその連携を外します。

 

ではVercelで先ほど連携させてデプロイしたプロジェクトの画面を開き、画面上のメニュー「Settings」をクリックします。

 

次に画面左のメニュー「Git」をクリックし、Connected Git RepositoryのGitHubアカウントの右側にある「Disconnect」をクリックします。

 

これでプロジェクトのGitHub連携が解除されます。(GitHubにソースをpushしても自動でデプロイされない。)

 

次にCircleCIからVercelにデプロイ処理をする際に必要になるアクセス用のトークンを作成するため、Vercelのダッシュボードを開き、画面上のメニュー「Settings」をクリックします。

 

次に画面左のメニュー「Tokens」をクリック後、画面中央のTokensにある「Create」をクリックします。

 

Create Token画面が表示されるため、TOKEN NAME(トークン名。任意の名前。)、SCOPE(権限範囲。リストの中にあるものから選択。)、EXPIRATION(有効期限。リストの中から選択しますが、期限が長いものはセキュリティのリスクも高くなるのでその点はご注意下さい。尚、トークンの有効期限が切れたら再設定が必要になります。)を設定して下さい。

 

設定後、「CREATE TOKEN」をクリックします。

 

これでトークンが作成されて画面に表示されます。このトークンはCircleCIの環境変数に設定する必要があるため、メモしておいて下さい。(もちろん他人に見られると危険なのでその点はご注意下さい。)

 

次にデプロイ用のコマンド「vercel」を使えるようにするため、以下のコマンドを実行して必要なパッケージをインストールします。

$ docker compose exec front yarn add -D vercel

 

合わせて、package.jsonにデプロイ実行用のスクリプト設定「”deploy”: “vercel –token $VERCEL_TOKEN –prod –confirm”,」を追加します。

{
・・・
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "test": "jest",
    "deploy": "vercel --token $VERCEL_TOKEN --prod --confirm",
    "lint": "next lint"
  }
・・・
}

※CircleCIの環境変数に設定する変数名は「VERCEL_TOKEN」を想定しています。

 

次にCircleCIの設定をするため、まずは以下のコマンドを実行し、ルートディレクトリにCircleCIの設定ファイル「.circleci/config.yml」を作成します。

$ mkdir .circleci
$ cd .circleci
$ touch config.yml

 

次にconfig.ymlの中身は以下のように設定します。

# Use the latest 2.1 version of CircleCI pipeline process engine.
# See: https://circleci.com/docs/2.0/configuration-reference
version: 2.1

# Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/2.0/configuration-reference/#jobs
jobs:
  # テスト用のジョブ
  test:
    # docker compose を利用するため、仮想マシンを利用する。(最新版のimageは下記URLを参照)
    # See: https://circleci.com/docs/ja/configuration-reference#available-linux-machine-images
    machine:
      image: ubuntu-2204:current
    # 作業用のディレクトリを設定
    working_directory: ~/test_jest
    # タスクを定義
    steps:
      # リポジトリを作業用のディレクトリにpull
      - checkout
      # 処理を実行
      # yarnをインストール
      - run:
          name: yarn.lockからyarnを再インストール
          command: docker compose run --rm front yarn install --frozen-lockfile
      # テストの実行
      - run:
          name: ESLintの実行
          command: docker compose run --rm front yarn lint
      - run:
          name: Jestの実行
          command: docker compose run --rm front yarn test

  # デプロイ用のジョブ
  deploy:
    # docker compose を利用するため、仮想マシンを利用する。(最新版のimageは下記URLを参照)
    # See: https://circleci.com/docs/ja/configuration-reference#available-linux-machine-images
    machine:
      image: ubuntu-2204:current
    # 作業用のディレクトリを設定(ディレクトリ名はVercelのプロジェクト名と合わせる)
    working_directory: ~/front-app
    # タスクを定義
    steps:
      # リポジトリを作業用のディレクトリにpull
      - checkout
      # 処理を実行
      # yarnをインストール
      - run:
          name: yarn.lockからyarnを再インストール
          command: docker compose run --rm front yarn install --frozen-lockfile
      # vercelへのデプロイ処理
      - run:
          name: yarn deploy
          command: yarn deploy

# Invoke jobs via workflows
# See: https://circleci.com/docs/2.0/configuration-reference/#workflows
workflows:
  test-and-deploy-wf:
    jobs:
      - test
      - deploy:
          requires:
            - test
          filters:
            branches:
              only: main

※デプロイ用のジョブにある「working_directory: 」の設定については、Vercelのプロジェクト名に合わせてください。(Vercelのプロジェクト名が「front-app」なら、「~/front-app」を設定する)、そしてESLintで構文エラーがあるとVercelへのデプロイ時に失敗するため、JestだけでなくESLintのチェックもしています。また、デプロイ実行時はCircleCIで設定した環境変数を利用する必要がありますが、そのままだとdocker-composeコマンド実行時に使えない(dockerコンテナ内に環境変数を設定する処理が必要になる)ため、直接yarnコマンドを実行してデプロイしています。(これでデプロイできたのでOKとしました。何か問題がある場合は別の方法をご検討下さい)

 

次にGitHubとCIrcleCIを連携させるため、上記で作成したファイルはGitHubへコミットしておいて下さい。

その後、CircleCIのアカウント画面を開き(既にCircleCIを使える前提です)、画面左のメニュー「Projects」をクリック後、連携したいリポジトリの「Set Up Project」をクリックします。

※まだCircleCIを使ったことが無い方は、以下の関連記事内にある「GitHubとCircleCIでCI/CDパイプラインを構築する方法」を参考にしてみて下さい。

 

関連記事👇

Railsチュートリアルをカスタマイズしてポートフォリオを作成する方法【Docker・Rails7・CircleCI対応】

2022年8月15日

 

次にconfig.ymlファイルの選択画面が表示されるので、ブランチのところに「main」(mainブランチの直下に.circleci/config.ymlが存在する場合)と入力し、「Set Up Project」をクリックします。

 

これでCircleCIが連携されて処理が実行させると思いますが、まだ環境変数を設定していないので実行された処理は失敗する(テストは成功し、デプロイは失敗するはず)と思います。

そのまま画面右上の「Project Settings」をクリックし、環境変数を設定します。

 

Project Settings画面が開いたら、画面左のメニュー「Environment Variables」をクリック後、画面中央の「Add Environment Variable」をクリックします。

 

環境変数の設定画面が表示されるので、Nameに「VERCEL_TOKEN」、Valueに先ほどメモしておいたVercelで作成したトークンを入力後、「Add Environment Variable」をクリックします。

 

これで環境変数の設定が完了です。

 

ここまでで全ての設定が完了したので、実際に上手く動作するかを試してみます。

まずは以下のコマンドを実行してブランチを作成します。

$ git checkout -b deploy-test

 

次にaboutページで「add CircleCI!」を表示するように変更してみるため、テスト用のファイル「__tests__/about.test.tsx」を以下のように修正します。

// テスト用のライブラリをインポート
import { render, screen } from "@testing-library/react";
// テストをしたいモジュールをインポート
import About from "../pages/about";

// テストの説明
describe("About", () => {
  // テストケース
  it("Hello World!が表示されていること", () => {
    // About(about.tsx)を出力
    render(<About />);
    // screen.getByTextで文字列を検索し、toBeInTheDocument()で存在確認
    expect(screen.getByText("Hello World!")).toBeInTheDocument();
    // circleciの検証用に追加
    expect(screen.getByText("add CircleCI!")).toBeInTheDocument();
  });
});

 

次に以下のコマンドを実行し、テストを実行します。

$ docker compose exec front yarn test

 

まだaboutページを修正していないため、エラーになればOKです。

 

次にaboutページで「add CircleCI!」を表示させるため、「pages/about.tsx」を以下のように修正します。

import type { NextPage } from 'next'

const About: NextPage = () => {
  return (
    <div>
      <h1 className="text-blue-700 underline decoration-gray-500">
        Hello World!
      </h1>
      <p>
        add CircleCI!
      </p>
    </div>
  )
}

export default About

 

次に以下のコマンドを実行し、もう一度テストを実行します。

$ docker compose exec front yarn test

 

テストが正常終了すればOKです。

 

また、ESLintを実行して構文エラーがあるとVercelのデプロイ時に失敗するため、以下のコマンドを実行してESLintも実行しておきます。

$ docker compose exec front yarn lint

 

ESLintもエラーがなければOKです。

 

これで準備が整ったので、以下のコマンドを実行してコミットとGitHubへプッシュします。

$ git add -A
$ git commit -m "modify about.tsx"
$ git push -u origin deploy-test

 

これでCircleCIでジョブが実行されるので確認します。起動したジョブが「Success」ならOKです。

 

次にテストにエラーがなかったので、GitHubでプルリクエストを作成します。(リポジトリに表示される「Compare & pull request」をクリック)

 

必要に応じてメッセージを入力後、「Create pull request」をクリックします。

 

これでプルリクエストが作成されました。テストにエラーがなければmainブランチへのマージ処理ができるので、「Merge pull request」をクリックします。

 

mainブランチへマージするため、「Confirm merge」をクリックします。

 

これでmainブランチへのマージ処理が行われ、それに伴ってCircleCIでジョブが実行されます。

 

CircleCIを確認するとジョブが実行されているのが確認でき、ステータスが「Success」ならテストとデプロイが正常終了しています。

 

最後にブラウザで本番環境のアドレスにアクセスし、「/about」ページを確認して下さい。修正した箇所が想定通り表示されればデプロイは成功しているのでOKです。

 



フロントエンドに関する補足

TypeScriptについて

これで基本的なフロントエンド部分の環境構築は完了したので、あとはNext.js(React)やTypeScript、そしてJestの基礎知識があればフロントエンドの開発を進めていけると思います。

まだフロントエンド部分の基礎知識が無い方は、TypeScript入門『サバイバルTypeScript』などを一通り理解しておくといいと思います。

※もしフロントエンドエンジニア志望の方であれば、このままNext.js(React)やTypeScript、そしてJestの知識をゴリゴリ増やしていけばいいと思います。ただし、バックエンドエンジニア志望の方であれば、フロントエンド部分は基本的な部分を抑えることを中心とし、ある程度学習した後は早めにAPI開発の方に進んだ方がいいと思います。(特にフロントエンド側でどうやってAPIを実行しているのかは理解しておきましょう。上記リンクのTypeScript入門でも学べます。)

尚、TypeScriptについては、ブラウザで実行可能な「TypeScript Playground」などを利用すると、気軽るにコードを書いて実行結果を試すことが可能です。

画面上のメニュー「Playground」をクリックするとブラウザの実行環境を使えます。

 

コードを書いた後、メニュー「Run」をクリックすると実行できます。

 

画面右側のLogsで実行結果などを確認可能です。

 

Next.jsのコンポーネントとTailwind CSSについて

Next.jsではコンポーネント単位で各種パーツを作り、それを親画面にインポートしてから配置し、画面を構成していくのが基本になると思います。

例えば下図のように「いいねボタン」を画面に表示させたい場合は、まず「いいねボタン」のコンポーネント(components/likebutton.tsx)を作ります。

※Tailwind CSSを導入している場合は、タグのclassNameにTailwind CSSの各種クラスを指定してCSSを反映させます。

 

そして、作成したコンポーネントを表示させたい親画面で対象のコンポーネントをインポートし、そのコンポーネントを配置(コンポーネント名のタグを置く)することで画面に表示させることが可能です。

 

このように、Next.jsではコンポーネント単位で各種パーツを作るのが基本なので、コンポーネント毎にCSSを指定しやすいTailwind CSSと相性が良いと言われています。

尚、Tailwind CSSの各種クラスについては、公式サイト「tailwindcss.com」から検索することが可能です。(公式サイトを開くと画面中央に検索バーがあるため、そこに調べたい内容を入力する)

 

例えば「margin」と入力すると各種候補が表示され、クリックすると詳細画面が表示されます。

 

詳細画面を開くと、Tailwind CSSで使えるクラス名がわかります。(再度検索したい場合は画面左上に検索バーがあります。)

 

ページ単位で特定のレイアウトを適用させたい場合の設定について

ページ単位で特定のレイアウトを適用させたい場合については、以下のように設定すると可能になります。

まずは「pages/_app.tsx」でページ単位のレイアウトを適用できるように修正します。(getLayout関数を使う場合の例)

import '../styles/globals.css'
import "tailwindcss/tailwind.css"
import type { AppProps } from 'next/app'

// ページ単位のレイアウト用にインポート
import type { ReactElement, ReactNode } from "react"
import type { NextPage } from "next"

// ページ単位のレイアウト用に型定義を拡張
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
  getLayout?: (page: ReactElement) => ReactNode
}
type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout
}

// ページ単位のレイアウトを適用するため、型にAppPropsWithLayoutを設定
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  // getLayoutが設定されている場合にページ単位のレイアウトを適用
  const getLayout = Component.getLayout ?? ((page) => page)
  return getLayout(<Component {...pageProps} />)
}

export default MyApp

 

尚、上記の場合は一つのコンポーネント(getLayout(<Component {…pageProps} />)を指定していますが、複数のコンポーネントを設定したい場合は以下のように修正します。

import '../styles/globals.css'
import "tailwindcss/tailwind.css"
import type { AppProps } from 'next/app'

// ページ単位のレイアウト用にインポート
import type { ReactElement, ReactNode } from "react"
import type { NextPage } from "next"

// ページ単位のレイアウト用に型定義を拡張
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
  getLayout?: (page: ReactElement) => ReactNode
}
type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout
}

// ページ単位のレイアウトを適用するため、型にAppPropsWithLayoutを設定
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  // getLayoutが設定されている場合にページ単位のレイアウトを適用
  const getLayout = Component.getLayout ?? ((page) => page)
  return (
    <>
      { getLayout(<Component {...pageProps} />) }
      // 別のコンポーネント「<SecondComponent />」を追加したい場合は以下のように追記
      <SecondComponent />
    </>
  )
}

export default MyApp

 

次にcomponentsに「layout.tsx」ファイルを追加し、内容を以下のようにします。

import { ReactElement } from "react"
import Header from "../components/header"
import Footer from "../components/footer"

type LayoutProps = Required<{
  readonly children: ReactElement
}>;

const Layout = ({ children }: LayoutProps) => {
  return (
    <>
      <Header />
      { children }
      <Footer />
    </>
  );
};

export default Layout;

 

上記の例では共通設定としてヘッダーとフッターを設定しているため、合わせてcomponentsに「header.tsx」と「footer.tsx」も追加し、内容を以下のようにします。

import { memo } from "react"
import Link from 'next/link'

const Header = memo( function HeaderMemo() {
  return (
    <header>
      // ヘッダーに表示させる内容を記述
    </header>
  );
});

export default Header;
import { memo } from "react"

const Footer = memo( function FooterMemo() {
  return (
    <footer>
      // フッターに表示させる内容を記述
    </footer>
  )
});

export default Footer;

 

次に各種ページで作成したレイアウトを適用できるように修正します。例えば「pages/index.tsx」を修正する場合は以下のようになります。

/* Homeページ */

import type { NextPageWithLayout } from "./_app"
import Layout from "../components/layout"
import Head from 'next/head'

const headInfo = {
  title: "タイトルの設定",
  desctiption: "摘要の設定",
};

// 「pages/_app.tsx」で拡張した定義「NextPageWithLayout」を使う
const Home: NextPageWithLayout = () => {
  return (
    <>
      <Head>
        <title>{ headInfo.title }</title>
        <meta name="description" content={ headInfo.desctiption } />
      </Head>
      // Homeページに表示させたい内容を記述
    </>
  );
};

// getLayout関数でページ単位のLayout設定
Home.getLayout = (page) => (
  <Layout>
  {page}
  </Layout>
);

export default Home;

 

このようにgetLayout関数の追加および、必要なレイアウト用ファイルを追加することで、ページ単位で特定のレイアウトを適用できるようになります。

 



Rails7のAPIモードでバックエンドの開発環境を構築する方法

ここからはRuby on Railsのバージョン7以降のAPIモードを使い、バックエンドの開発環境を構築する方法をまとめます。

フロントエンド側とはリポジトリを分けて管理し、それぞれ完全に分けて開発できるようにする想定なので、まずはバックエンド側のディレクトリの作成から始めます。

以下のコマンドを実行し、フロントエンドとは別の場所にディレクトリを作成後、各種ファイルを作成します。

$ mkdir ディレクトリ名(フロントエンドとは違う名前を付ける)
$ cd ディレクトリ名
$ touch Dockerfile
$ touch Gemfile
$ touch Gemfile.lock
$ touch entrypoint.sh
$ touch .env
$ touch docker-compose.yml
$ mkdir nginx
$ cd nginx
$ touch Dockerfile
$ touch nginx.conf
$ mkdir log
$ cd log
$ touch .keep

※ディレクトリ名は任意の名前を付けて下さい(例:backend-appなど)。また、最後のファイル「.keep」はGitHubでディレクトリ「nginx/log」を監視対象に残すために作成しています。

 

尚、ファイル構成は次のようになります。

ディレクトリ ーーーー nginx ーーーー log
        |       |ーー Dockerfile
        |       |ーー nginx.conf
        |ーー .env
        |ーー docker-compose.yml
        |ーー Dockerfile
        |ーー entrypoint.sh
        |ーー Gemfile
        |ーー Gemfile.lock

※「nginx/logの中身」と「.env」は.gitignoreで監視対象から外して下さい。(.gitignoreに「/nginx/log/*」、「!/nginx/log/.keep」、「/.env」を追加)

 

次に各種ファイルの中身をそれぞれ記載します。

## ビルドステージ
# 2022年11月時点の最新安定版Rubyの軽量版「alpine」
FROM ruby:3.1.2-alpine AS builder
# 言語設定
ENV LANG=C.UTF-8
# タイムゾーン設定
ENV TZ=Asia/Tokyo
# 2022年11月時点の最新版のbundler
# bundlerのバージョンを固定するための設定
ENV BUNDLER_VERSION=2.3.25
# インストール可能なパッケージ一覧の更新
RUN apk update && \
    apk upgrade && \
    # パッケージのインストール(ビルド時のみ使う)
    apk add --virtual build-packs --no-cache \
            alpine-sdk \
            build-base \
            curl-dev
            mysql-dev \
            tzdata
# 作業ディレクトリの指定
RUN mkdir /app
WORKDIR /app
# ローカルにあるGemfileとGemfile.lockを
# コンテナ内のディレクトリにコピー
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
# bundlerのバージョンを固定する
RUN gem install bundler -v $BUNDLER_VERSION
RUN bundle -v
# bunlde installを実行する
RUN bundle install --jobs=4
# build-packsを削除
RUN apk del build-packs

## マルチステージビルド
# 2022年11月時点の最新安定版Rubyの軽量版「alpine」
FROM ruby:3.1.2-alpine
# 言語設定
ENV LANG=C.UTF-8
# タイムゾーン設定
ENV TZ=Asia/Tokyo
# 本番環境用のRAILS_ENV設定
ENV RAILS_ENV=production
# インストール可能なパッケージ一覧の更新
RUN apk update && \
    apk upgrade && \
    # パッケージのインストール(--no-cacheでキャッシュ削除)
    apk add --no-cache \
            bash \
            mysql-dev \
            tzdata \
            gvim
# 作業ディレクトリの指定
RUN mkdir /ディレクトリ名
WORKDIR /ディレクトリ名
# ビルドステージからファイルをコピー
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY . /ディレクトリ名
# puma.sockを配置するディレクトリを作成
RUN mkdir -p tmp/sockets
# 本番環境(AWS ECS)でNginxへのファイル共有用ボリューム
VOLUME /ディレクトリ名/public
VOLUME /ディレクトリ名/tmp
# コンテナ起動時に実行するスクリプト
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3010
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

※上記Dockerfileについては、ファイルサイズ軽量化を図るためにalpineイメージの使用と、マルチステージビルドという機能を使うようにしています。マルチステージビルド配下にあるディレクトリ名には任意の名前を付けて下さい。DBはMySQLを使う想定なので「mysql-dev」のインストール、credentials.yml.encファイルの編集用に「gvim」をインストールしています。また、本番環境(AWS ECS)でNginxとのファイル共有用にVOLUMEを設定しているほか、フロントエンド側とのポート重複を避けるため、EXPOSE(ポート番号)には「3010」を指定しています。

 

source 'https://rubygems.org'
# 2022年10月時点の最新版Rails
gem 'rails', '~> 7.0.3'

 

#!/bin/bash
set -e

# Rails特有の問題を解決するためのコマンド
rm -f /ディレクトリ名/tmp/pids/server.pid

# production環境の場合のみJSとCSSをビルド
if [ "$RAILS_ENV" = "production" ]; then
  # APIモード以外でアセットコンパイルが必要な場合に利用
  # bundle exec rails assets:clobber
  # bundle exec rails assets:precompile
  # --------------------------------------
  # 本番環境(AWS ECS)への初回デプロイ時に利用
  # 初回デプロイ後にコメントアウトして下さい
  bundle exec rails db:create
  # --------------------------------------
  # 2回目以降のデプロイ時に利用
  # 初回デプロイ後にコメントアウトを外して下さい
  # bundle exec rails db:migrate
fi

# サーバー実行(DockerfileのCMDをセット)
exec "$@"

※ディレクトリ名の部分には任意の名前を付けて下さい

 

TZ=Asia/Tokyo
MYSQL_ROOT_PASSWORD=パスワード

※ファイル「.env」は主に開発環境における、機密情報を含むような環境変数設定用のファイルになっており、docker-compose.ymlから読み込めるようにします。(GitHubなどに公開しないようにご注意下さい。)また、今回はDBにMySQLを使う想定のため、環境変数「MYSQL_ROOT_PASSWORD」には任意のパスワードを指定して下さい。尚、MySQLでオプション(MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORDなど)を設定しない場合は、rootユーザーとMYSQL_ROOT_PASSWORDによりDBが作成されることになります。

 

# Nginxのバージョンは軽量版の「alpine」
FROM nginx:alpine

# インクルード用のディレクトリ内を削除
RUN rm -f /etc/nginx/conf.d/*

# Nginxの設定ファイルをコンテナにコピー
ADD ./nginx.conf /etc/nginx/conf.d/ファイル名.conf

# ビルド完了後にNginxを起動
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf

※Nginx用のDockerファイルでは、軽量版のNginxイメージ「alpine」を使っています。また、Nginxの設定ファイルをコンテナにコピーする部分のファイル名には任意の名前を付けて下さい。

 

## Nginx用の設定

# サーバーグループの定義(proxy_passのURLとして利用)
upstream サーバーグループ名 {
  # ソケット通信したいのでpuma.sockを指定
  server unix:///ディレクトリ名/tmp/sockets/puma.sock;
}

# サーバーの設定
server {
  # ポート番号
  listen 80;

  # サーバー名(ドメインもしくはIPを設定)
  server_name localhost;

  # ログ出力先
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;

  # ドキュメントのルート設定
  root /ディレクトリ名/public;

  # リクエストボディサイズ(単位mはMB)
  client_max_body_size 100m;

  # エラーページのカスタマイズ
  error_page 404 /404.html;
  error_page 505 502 503 504 /500.html;

  # リクエストの受信とファイルチェック
  # 「$uri」はリクエストされたURL
  # 左から順にチェックして返すが、最後の記述はリダイレクト処理
  try_files $uri/index.html $uri @ロケーション名;

  # HTTP通信のタイムアウト設定(何秒でタイムアウトするか)
  keepalive_timeout 5;

  # リバースプロキシ関連の設定
  location @ロケーション名 {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_pass http://サーバーグループ名;
  }
}

※サーバーグループ名、ディレクトリ名、ロケーション名の部分については任意の名前を指定して下さい。(このファイルで一番大事なのはpuma.sockを指定するところなので、それ以外の部分は必要に応じて修正して下さい。)

 

version: "3.8"
services:
  # DBの設定
  db:
    # コンテナ名の指定
    container_name: コンテナ名_db
    # 2022年10月時点の最新版MySQL
    image: mysql:8.0.31
    # 環境変数の設定(.envから読み込む)
    env_file:
      - ./.env
    # ポートの指定(外部からアクセス時のポート:コンテナからアクセス時のポート)
    ports:
      - 3306:3306
    # データの永続化(ローカルのtmp/dbディレクトリにマウント)
    volumes:
      - ./tmp/db:/var/lib/mysql
  # アプリの設定
  app:
    # コンテナ名の指定
    container_name: コンテナ名_app
    # Dockerfileのあるディレクトリパスを指定
    build: .
    # 環境変数の設定(.envから読み込む)
    env_file:
      - ./.env
    # 開発環境のRAILS_ENVはdevelopmentを設定
    environment:
      - RAILS_ENV=development
    # コマンド実行(Rails特有の問題解決とRailsの立ち上げ)
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec pumactl start"
    # データの永続化
    volumes:
      # ローカルのカレントディレクトリにマウント
      - .:/ディレクトリ名
      # Webサーバー側からpuma.sockを見れるようにするため永続化
      - ./tmp/sockets:/ディレクトリ名/tmp/sockets
    # 標準入出力デバイスを設定
    tty: true
    stdin_open: true
    # 依存関係の指定(dbが起動した後に、appが起動する)
    depends_on:
      - db
  # Webサーバーの設定
  web:
    # コンテナ名の指定
    container_name: コンテナ名_web
    # Dockerfileのあるディレクトリパスを指定
    build:
      context: .
      dockerfile: ./nginx/Dockerfile
    # データの永続化
    volumes:
      # Nginxのログ出力を永続化
      - ./nginx/log:/var/log/nginx
      # Webサーバー側からアプリの/tmpと/publicを見れるようにするため永続化
      - ./tmp:/ディレクトリ名/tmp
      - ./public:/ディレクトリ名/public
    # ポートの指定(外部からアクセス時のポート:コンテナからアクセス時のポート)
    ports:
      - 80:80
    # 依存関係の指定(appが起動した後に、webが起動する)
    depends_on:
      - app

※上記のコンテナ名、ディレクトリ名には任意の名前を指定して下さい。そしてアプリの実行コマンドは通常のpumaコマンド(各種引数の指定が必要)ではなく、簡単に使える(自動化してくれている)コマンド「pumactl」を使っています。また、Nginxを組み込む際は、Webサーバー側のコンテナからアプリ側のコンテナにある「/ディレクトリ名/tmp/sockets/puma.sock」を見れる必要があるので、データの永続化などが必要になっています。

 

次に作成したバックエンドのディレクトリ直下に移動した後、以下のコマンドを実行し、Railsのアプリを作成します。

$ docker compose run --rm app rails new . --force --skip-bundle --skip-git --database=mysql --api

※「–force」でファイルが存在する場合の上書き設定、「–skip-bundle」でbundle installをスキップ、「–skip-git」でgit initをスキップ、「–database=mysql」でDBにMySQLを使う設定、「–api」でAPIモードを使う設定になります。

 

次に「config/puma.rb」と「config/database.yml」をそれぞれ以下のように修正します。

・・

# フロントエンド側とポート番号の重複を避けるため3010を指定
# port ENV.fetch("PORT") { 3000 }
port ENV.fetch("PORT") { 3010 }

・・

## Nginx用の設定を追加
# ファイルがある場所から2個上の階層をapp_rootとする
app_root = File.expand_path("../..", __FILE__)
# unixソケットを使う設定
bind "unix://#{app_root}/tmp/sockets/puma.sock"
# 標準出力設定(trueは追記モード)
stdout_redirect "#{app_root}/log/puma.stdout.log", "#{app_root}/log/puma.stderr.log", true

※ポート番号の設定を「3010」に修正し、puma.rbにNginx用の設定を追加します。

 

・・

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  # 以下をコメントアウト
  # username: root
  # password:
  # host: localhost

development:
  <<: *default
  database: spa2210_b_development
  # 以下を追加(DBのユーザー名、パスワード、ホスト名)
  # オプションを指定していない場合はrootユーザーとMYSQL_ROOT_PASSWORD
  # 環境変数「MYSQL_ROOT_PASSWORD」はファイル「.env」で指定する
  username: root
  password: <%= ENV["MYSQL_ROOT_PASSWORD"] %>
  # ホスト名はdocker-compose.ymlにあるDB用のサービス名「db」を指定
  host: db

test:
  <<: *default
  database: spa2210_b_test
  # オプションを指定していない場合はrootユーザーとMYSQL_ROOT_PASSWORD
  # 環境変数「MYSQL_ROOT_PASSWORD」はファイル「.env」で指定する
  username: root
  password: <%= ENV["MYSQL_ROOT_PASSWORD"] %>
  # ホスト名はdocker-compose.ymlにあるDB用のサービス名「db」を指定
  host: db

・・

※後の本番環境用の設定も考慮し、デフォルト設定にある「username、password、host」の設定はコメントアウトし、開発環境とテスト環境の設定にそれぞれ個別に設定する。また、パスワードはファイル「.env」に設定した環境変数から読み込む。

 

これで開発環境を立ち上げる準備が整ったので、以下のコマンドを実行し、コンテナのビルドとサーバーの立ち上げを行います。

$ docker compose build --no-cache
$ docker compose up -d

 

そして以下のコマンドを実行し、コンテナのステータスを確認します。

$ docker compose ps

 

SERVICEが3つ(app、db、web)作成され、それぞれのSTATUSが「running」ならOKです。

 

サーバーが起動された状態でブラウザから「http://localhost」にアクセスし、以下の画面が表示されればOKです。(まだDBを作成していないためエラーになります。)

 

そして次のコマンドを実行し、DBを作成します。(開発環境用とテスト環境用のDBが作成されます。)

$ docker compose exec app rails db:create

 

そしてもう一度、ブラウザから「http://localhost」にアクセスし、以下の画面が表示されればOKです。

 



Rails7アプリにテスト用APIを作成

では試しにリクエストに対してJSON形式のデータを送信するような簡単なテスト用のAPIを作ってみます。

まずは「app/controllers/application_controller.rb」を以下のように修正します。

class ApplicationController < ActionController::API

  # テスト用のAPIを追加
  def test
    # テスト用のJSON形式のオブジェクト
    test_json_obj = [
      { id: 1, title: "First Text", text: "最初のテキスト" },
      { id: 2, title: "Second Text", text: "2番目のテキスト" },
    ]

    # JSON形式で出力
    render json: test_json_obj
  end

end

 

次に「config/routes.rb」を以下のように修正します。

Rails.application.routes.draw do

  # テスト用のルーティングを追加
  scope "api" do
    scope "v1" do
      get "/test", to: "application#test"
    end
  end

end

※上記ではURLだけAPIっぽい感じに変えるために「scope」を使ってルーティングを設定しています。(APIの場合はルーティングに「v1」などを付けてAPIのバージョン管理をしたりします。)

 

これでテスト用のAPIができたので、ブラウザから「http://localhost/api/v1/test」にアクセスし、以下のように画面にJSON形式のデータが出力されていればOKです。

 

Rails7にテスト用フレームワーク「RSpec」を導入する方法

次にRailsのテスト用フレームワークとして「RSpec」を導入します。まずは、GemfileにRSpec用のGemを追加します。

・・

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri mingw x64_mingw ]

  # テスト用のGemを「group :development, :test」内に追加
  gem "rspec-rails"
  gem "factory_bot_rails"
  gem "shoulda-matchers"
end

・・

※factoru_bot_rails(テストデータ作成用)、shoulda-matchers(簡潔なテストコードを書く用)

 

次に以下のコマンドを実行し、gemをインストールします。

$ docker compose exec app bundle install

 

次に以下のコマンドを実行し、RSpecをインストールします。

$ docker compose exec app rails g rspec:install

 

次にMinitest用の「/test」を削除します。

$ rm -rf test

 

次にRSpecの初期設定として、「.rspec」に「–color」と「–format documentation」を追加します。

--require spec_helper
--color
--format documentation

 

次にbinstubの設定(短いコマンドで実行できるようにする)を行います。

$ docker compose exec app bundle binstubs rspec-core

 

次に不要なスペックがたくさん作られないようにするため、config/application.rbにジェネレータの設定を追加します。

module モジュール名
  class Application < Rails::Application
      ・
      ・
    # ジェネレータの設定を追加
    config.generators do |g|
      g.test_framework :rspec,
        fixtures: false,
        helper_specs: false,
        view_specs: false,
        routing_specs: false
    end
      ・
      ・
  end
end

 

次にdocker-compose.ymlの環境変数(environment)に「RAILS_ENV=development」を設定した場合は、RSpec実行時の環境変数も「development」を読み込んでしまうため、「spec/rails_helper.rb」を以下のように修正します。

・・

# RSpec実行時の環境変数「RAILS_ENV」の値が「test」になるように修正
# ENV['RAILS_ENV'] ||= 'test'
ENV['RAILS_ENV'] = 'test'

・・

 

次に以下のコマンドを実行し、RSpecを実行して確認します。

$ docker compose exec app bin/rspec

※binstubを設定していない場合は、コマンド「bundle exec rspec」を使います。

 

以下のようにRSpecが実行できればOKです。

 

では試しに先ほど作ったテスト用のAPIに対してテストを書いてみます。以下のコマンドを実行し、アプリケーションコントローラー用のテストファイルを作成します。

$ docker compose exec app rails g rspec:request application

 

これでテスト用のファイル「spec/requests/applications_spec.rb」が作成されるので、以下のように修正します。

require 'rails_helper'

RSpec.describe "Applications", type: :request do
  # テスト用に作ったAPIの検証
  describe "GET /api/v1/test" do
    # レスポンスのテスト
    it "returns http success" do
      get test_path
      expect(response).to have_http_status(:success)
    end
    # JSONオブジェクトのテスト
    it "json object as expected" do
      get test_path
      # 1番目のオブジェクトの値の検証
      expect(JSON.parse(response.body)[0]["id"]).to eq(1)
      expect(JSON.parse(response.body)[0]["title"]).to eq("First Text")
      expect(JSON.parse(response.body)[0]["text"]).to eq("最初のテキスト")
      # 2番目のオブジェクトの値の検証
      expect(JSON.parse(response.body)[1]["id"]).to eq(2)
      expect(JSON.parse(response.body)[1]["title"]).to eq("Second Text")
      expect(JSON.parse(response.body)[1]["text"]).to eq("2番目のテキスト")
    end
  end
end

 

では以下のコマンドを実行し、RSpecを実行してテスト結果を確認します。

$ docker compose exec app bin/rspec

※binstubを設定していない場合は、コマンド「bundle exec rspec」を使います。

 

以下のように実行結果が正常終了すればOKです。

 

Rails7アプリのタイムゾーンを日本に変更する方法

Railsのタイムゾーン設定については、デフォルトでUTCになっています。

Railsのタイムゾーンを日本に変更するには、「config/application.rb」に以下の設定を追加します。

module モジュール名
  classApplication < Rails::Application
      ・
      ・
    # タイムゾーン設定を日本にする
    config.time_zone = "Asia/Tokyo"
    config.active_record.default_timezone = :local
      ・
      ・
  end
end

 



開発環境でフロントエンドとバックエンドの連携を検証

ここまででフロントエンドとバックエンドの開発環境が両方整ったので、試しにフロントエンド側からバックエンドのAPIを叩いてJSON形式のデータを取得し、それをフロントエンドに表示させる検証を行ってみます。

まずはフロントエンドとバックエンドのサーバーをそれぞれ別のターミナルウインドウで立ち上げます。

 

次にフロントエンド側で以下のコマンドを実行し、検証用の新規ページを作成します。

$ cd pages
$ touch testApi.tsx

 

次に作成したファイル「pages/test.tsx」を以下のように修正します。

// 必要なコンポーネントをインポート
import type { NextPage } from 'next'
import { useState } from "react"

// メイン処理
const Test: NextPage = () => {
  // useStateで初期値をdisplayDataに設定
  const [displayData, setDisplayData] = useState(
    "初期値を表示"
  );

  // 画面にdisplayDataの値を表示
  return (
    <div>
      {displayData}
    </div>
  );
};

export default Test;

 

これでブラウザから「http://localhost:3000/test」にアクセスし、以下のような画面が表示されればOKです。

 

次にもう一度、ファイル「pages/test.tsx」を以下のように修正します。

// 必要なコンポーネントをインポート
import type { NextPage } from 'next'
import { useState } from "react"

// バックエンドのAPIからJSON形式のデータを取得する関数を追加
interface JsonData {
  id: number;
  title: string;
  text: string;
};

type JsonDataResponse = JsonData[];

const FetchTestApi = async (): Promise<JsonDataResponse> => {
  // バックエンドのAPI「http://localhost/api/v1/test」からデータを取得
  const res = await fetch("http://localhost/api/v1/test");
 // 取得したデータをJSON形式に変換
  const result = (await res.json()) as JsonDataResponse;
  return result;
};
// メイン処理
const Test: NextPage = () => {
  // useStateで初期値をdisplayDataに設定
  const [displayData, setDisplayData] = useState(
    "初期値を表示"
  );

  // バックエンドのAPIからJSON形式のデータを取得し、
  // 取得したデータをdisplayDataに設定する関数
  const getTestApi = async() => {
    const jsonData = await FetchTestApi();
    // 取得した値を文字列型に変換してから設定
    setDisplayData(JSON.stringify(jsonData));
  };

  // 関数「getTestApi」を実行
  getTestApi();
  // 画面にdisplayDataの値を表示
  return (
    <div>
      {displayData}
    </div>
  );
};

export default Test;

※本来ならAPIからのデータ取得はSSR(サーバーサイドレンダリング)などの処理で行なったりする(SSRでデータを取得してメイン処理にPropsを渡すなど)と思いますが、SSRの処理からはグローバルな環境(Web上に公開している)にあるAPIからしかデータ取得ができなかった(fetchができない)ため、上記のような方法にて、開発環境におけるAPIの検証を行なっています。

 

次にもう一度、ブラウザから「http://localhost:3000/test」にアクセスし、以下のような画面が表示されればOKです。(バックエンドのAPIから取得した値が表示)

 

これで開発環境においてフロントエンドとバックエンドを連携させる検証が完了したので、それぞれ開発環境における開発を進められると思います。

最後に、先ほど作ったファイル「pages/test.tsx」は不要(本番環境にデプロイしてもAPIのデータが取得できずにエラーになる)なため、削除するかエラーが出ないように修正して下さい。

 



バックエンド(Nginx・Rails7・MySQL構成)をAWS ECS(Fargate)にデプロイする方法

ここまででバックエンド側の開発環境が整ったので、次は本番環境を現在主流(トレンド)のAWS(Amazon Web Services)とし、デプロイする方法についてまとめます。

AWSには様々なサービスがありますが、ここまでDockerを使った環境構築を進めてきた特性を活かすため、その中でもECS(Elastic Container Service)というサービスを利用します。

※AWSを利用するには携帯電話やクレジットカードが必要です。また、新規アカウント登録の特典としていくつかのサービス(EC2、RDSなど)に1年間の無料枠がありますが、ECSを利用する場合は料金が発生するのでその点はご注意下さい。(事前に確認して下さい!)

>> AWSの料金についてはこちら

 

①:AWSのアカウント登録方法

まずはAWSのアカウント登録をするため、AWSの公式サイトにアクセスし、画面右上の「今すぐ無料サインアップ」をクリックします。

 

次にサインアップ画面が表示されるため、Eメールアドレス、AWSアカウント名(半角英数字で入力)を入力し、「認証コードをEメールアドレスに送信」をクリックします。

 

これで入力したEメールアドレス宛に認証用のメールが送信されます。

 

認証用メールが届いたら、検証コードをメモします。

 

もう一度サインアップ画面に戻り、メモした検証コードを入力後、「認証を完了して次へ」をクリックします。

 

これでEメールアドレスの認証が完了し、次はパスワードを設定するため、任意のパスワードを入力後、「次へ」をクリックします。

 

次に連絡先情報の入力画面が表示されるので、それぞれ入力していきます。

 

入力項目の補足としては下図の通りです。住所なども半角英数字で入力する必要があるのでその点はご注意下さい。

 

全ての項目を入力後、AWSカスタマーアグリーメントをクリックして条項を確認し、同意する場合はチェックをつけて、「次へ」をクリックします。

 

次に請求情報入力画面が表示されるため、クレジットカード情報を入力し、「確認して次へ」をクリックします。

 

次に本人確認画面が表示されるため、認証コードの受け取り方法を選択、携帯電話番号の入力、セキュリティチェックに対応後、「SMSを送信する」をクリックします。

 

認証コードの受け取り方法がテキストメッセージなら、携帯電話に以下のようなメッセージが送られるため、記載されているコードをメモします。

 

次にメモしたコードを入力後、「次へ」をクリックします。

 

次にサポートプランを選択(無料ならベーシックサポート)後、「サインアップを完了」をクリックします。

 

これでAWSのアカウント登録が完了です。

次は登録したアカウントでログインするため、画面にある「AWSマネジメントコンソールへ進む」をクリックします。

 

次にサインイン画面が表示されるので、先ほど登録したメールアドレスを入力後、「次へ」をクリックします。

 

次にパスワードを入力し、「サインイン」をクリックします。

 

これで作成したAWSアカウントへのサインインが完了です。

 

②:AWS CLIのインストール

次にAWS用のCLIツールをインストールします。

AWS CLIをインストーラーを使ってインストールするには、公式サイトのAWS コマンドラインインターフェイスにアクセスし、画面右側にある対応するOSから任意のインストーラーを選んでクリックします。

 

ブラウザ「Google Chrome」を使っている場合は、画面下にダウンロードしたファイルが表示されるのでクリックします。

 

AWS CLIのインストーラーが開くので、画面の指示に従って進めるとインストールできます。

 

また、AWS CLIはHomebrewでインストールすることも可能です。Homebrewからインストールしたい場合は、以下の手順を参考にして下さい。

AWS CLIをHomebrewでインストールする方法

①以下のコマンドを実行し、Homebrewを最新化します。

$ brew update

 

②以下のコマンドを実行し、インストール可能なAWSのパッケージを確認します。

$ brew search aws

 

③以下のコマンドを実行し、AWS CLIをインストールします。

$ brew install awscli

 

※AWS CLIをアップデートしたい場合は、以下のコマンドを実行します。

$ brew upgrade awscli

 

AWS CLIをインストール後、以下のコマンドを実行するとAWS CLIのバージョンを確認できます。

$ aws --version

 

以下のようにバージョン情報が表示されればOKです。

 

③:AWS CLIの初期設定

次に作成したAWSアカウントとAWS CLIを紐付ける初期設定を行います。

まずはAWSアカウントからアクセスキー(アクセスキーIDとシークレットアクセスキー)を作成するため、画面右上にあるユーザー名からメニューを表示し、「セキュリティ認証情報」をクリックします。

 

次に「アクセスキー(アクセスキーIDとシークレットアクセスキー)」をクリックします。

 

次に「新しいアクセスキーの作成」をクリックします。

 

これでアクセスキーの作成が完了です。「アクセスキーを表示」をクリックすると確認できます。

 

アクセスキーIDとシークレットアクセスキーが表示されるのでメモします。

 

次に以下のコマンドを実行し、AWS CLIに認証情報を設定します。

$ aws configure

 

コマンドを実行後、4つの設定項目に対して、以下のように1項目ずつ入力を求められます。

 

AWS Access Key IDとAWS Secret Access Keyは先ほどメモした内容を入力(コピペしてもOK)し、Default region nameは日本国内なら「ap-northeast-1」、Default output formatはデフォルト(未入力時)はJSONのようですが、その他にtextやtableも設定できます。(ここではtextに設定してみました。)

 

これでAWSアカウントとAWS CLIを紐付ける初期設定が完了です。

もし入力した内容を更新したい場合は、もう一度「aws configure」コマンドを実行して入力し直せばいいっぽいです。

尚、設定内容を確認したい場合は、以下のコマンドを実行すると可能です。

①登録したAWS Access Key IDとAWS Secret Access Keyを確認したい場合

$ cat ~/.aws/credentials

 

②登録したDefault region nameとDefault output formatを確認したい場合

$ cat ~/.aws/config

 



④:AWS ECSへのデプロイ手順と構成図について

ここまででAWSを使う基本設定が完了しました。以降では実際にAWSにデプロイする方法をまとめますが、ちょっと複雑な部分もあるため、まずはデプロイ手順とAWSの構成図について簡単にまとめておきます。

まずAWSのデプロイ手順については以下の通りです。

AWSのデプロイ手順

  1.  VPC(Virtual Private Cloud)の作成
  2.  RDS(Relational Database Service)の作成 ※使わない場合は不要
  3.  ECR(Elastic Container Registry)の作成
  4.  ECS(Elastic Container Service)の作成

 

④-1:VPC(Virtual Private Cloud)の作成

VPCはAWSの中でプライベートなネットワークを構成するための機能です。AWSにデプロイをするためには、まずはこのVPCという領域を作るところから始まります。

 

そして、そのVPCの中にはサブネットというもう一つの小さな領域を複数個作ることになります。

そのサブネットの中でインターネットに接続して公開するものを「Public subnet(パブリックサブネット)」インターネットには接続せず非公開なものを「Private subnet(プライベートサブネット)」として作ります。

また、サブネットは複数個作ることになりますが、Availability Zone(アベイラビリティーゾーン。通称AZ)という論理的に分離されたセクションに分けて構成することにより、負荷分散を行えるようにしたり、障害時に影響を受けにくいネットワーク構成が可能になっています。

 

加えて、VPCだけだとインターネットに接続できないため、接続するためにInternet Gatewayを付けて、さらにRute tableによってインターネットとPublic subnetを接続させます。

尚、VPCにはSecurity Group(ファイアウォール機能)を付けることにより、通信制御を行います。

 

④-2:RDS(Relational Database Service)の作成 ※使わない場合は不要

次に今回の例では、DB(データベース)にはAWSの機能の一つであるRDSを使います。ただし、もし他のDBサービスを利用していればRDSを使う必要はないため、その場合はこの手順は不要です。

RDSを利用する場合は、AWSにあるRDSという機能からDBを作成し、対応するVPCのサブネット(基本的にはプライベートサブネット)と紐付けます。

この設定が完了するとRDSへの接続情報(DB名、ユーザー名、パスワード、エンドポイント「ホスト名」)が得られるため、アプリケーションからRDSにあるDBに接続して利用できるようになります。

尚、RDSにもSecurity Group(ファイアウォール機能)を付けることにより、通信制御を行います。

 

④-3:ECR(Elastic Container Registry)の作成

次に今回はDockerを利用して環境構築を行うため、AWSにあるECRという機能を利用し、Dockerイメージを格納するためのリポジトリを作成後、アプリケーションやWebサーバー用のDockerイメージをそれぞれ格納します。

この設定が完了後、ECSという機能からECRにあるDockerイメージを利用できるようになります。

 

④-4:ECS(Elastic Container Service)の作成

最後にAWSにあるECSという機能を利用し、タスク定義(コンテナの組み合わせやボリューム設定など)を作成後、クラスター(タスクやサービスなどを管理するためのグループ)およびサービス(タスクの起動数や接続先のネットワーク構成の設定など)を作成すると、サービスおよびタスク設定に応じたアプリケーションが構築され、デプロイ処理が完了です。

 

ここまでの設定が完了すると、インターネットからAWSに構築したアプリケーションと通信できるようになります。

ただし、この段階で通信に利用するパブリックIPは固定ではない(インスタンスの再起動時などに変わってしまう)ため、固定IPを利用するためには追加の設定(独自ドメインの取得および、ロードバランサーやRoute53の設定など)が必要になるのでご注意下さい。

尚、上記に載せているAWSの構成図に関しては、diagrams.netというサービスを利用すると無料で作成可能です。構成図を作成したい方は試してみて下さい。

 

⑤:AWS ECS(Fargate)にデプロイする方法まとめ

ここからは2022年11月現在において、実際にAWS ECS(Fargate)にデプロイする方法について解説します。

※将来的にまたUI/UXなどが改善されて変わる可能性があるのでその点はご注意下さい。

 

⑤-1:VPC(Virtual Private Cloud)の作成

まずはVPCを作成するため、AWSアカウントの画面左上にあるサービスから「ネットワーキングコンテンツ配信>VPC」をクリックします。

 

VPCダッシュボード画面が表示されるので、画面上の「VPCを作成」をクリックします。

 

VPC作成画面が表示されるので、左側にある各種設定メニューから設定を変更することが可能です。

尚、UI/UXが改善され、以前よりも簡単にVPCが作成できるようになっています。

 

今回はAWSにおける基本的な構成でVPCを作成してみます。

では下図のように、作成するリソースは「VPCなど」を選び、名前タグの自動生成では名前タグを任意の名前に修正して下さい。それ以外の項目はデフォルトのままでOKです。

 

AZの数、パブリックサブネットの数、プライベートサブネットの数もデフォルトのままでOKです。

 

NATゲートウェイとVPCエンドポイント、そしてDNSオプションやタグもデフォルトのままでOKです。

最後に画面下の「VPCを作成」をクリックします。

 

これでVPCに関連するものが順次作成されます。画面左の項目が全てが成功になった後、画面下の「VPCを表示」をクリックします。

 

これで作成したVPCの設定を確認可能です。

 

次に画面左のメニューから「お使いのVPC」をクリックし、作成済みのVPC一覧を確認します。

※2件表示されると思いますが、もう一つのVPCはAWSアカウント作成時に自動で作成されるもの(すぐにEC2インスタンスを立ち上げたりできるようにしている)で、不要な場合は削除しても大丈夫みたいです。

 

次に画面左のメニューにあるVPCでフィルタリングのリストから作成したVPCを選択し、対象のVPCについてフィルタリングします。

 

そして、画面左のメニューにある「セキュリティグループ」をクリックします。

 

これで対象のVPCに紐づくセキュリティグループが表示されるので、VPC用のセキュリティグループを指定し、「インバウンドルール」が有効になっている状態で、画面右下の「インバウンドルールの編集」をクリックします。

※尚、インバウンドルールとは、アクセスを許可するか制御をするためのルールになります。

 

これでインバウンドルールの編集画面が表示されるので、画面左下の「ルールを追加」をクリックします。

 

インバウンドルールに行が追加されるので、これを修正していきます。

 

タイプは「HTTP」ソースは「Anywhere-IPv4」を選びます。

 

次に画面右下の「ルールを保存」をクリックします。

 

画面上に緑色の背景で正常に終了した旨が表示されればOKです。

これでインターネットから作成したVPCに対して接続できるようになります。

※このようにセキュリティグループの設定によってアクセス制御が可能です。

 

⑤-2:RDS(Relational Database Service)の作成 ※使わない場合は不要

次にRDSを作成するため、AWSアカウントの画面左上にあるサービスから「データベース>RDS」をクリックします。

 

次に画面左のメニューにある「データベース」をクリックします。

 

次にデータベース画面が表示されるので、画面右上にある「データベースの作成」をクリックします。

 

次にデータベースの作成画面が表示されるので、ここから各種設定をしていきます。

まずデータベース作成方法を選択については、デフォルトの「標準作成」でOKです。

 

次にエンジンのオプションについては、今回の例ではMySQLを使っているため、MySQLを選択し、バージョンは開発環境と同じものを選択します。

※上記docker-compose.ymlでMySQLのイメージ「mysql:8.0.31」を使っているため、今回はバージョンに「MySQL 8.0.31」を選択

 

次にテンプレートについては、今回は無料枠を使いたいため、画面右側の「無料利用枠」をしっかり選択します。

※無料利用枠を選択すると、可用性と耐久性の項目が非活性になり、選べなくなります。(無料枠ではシングルAZで起動されるため。ただし、無料枠を利用する場合でも、VPCの作成ではマルチAZの構成を作る必要があります。)

 

次に設定については、DBインスタンス識別子を入力し、認証情報の設定として、マスターユーザー名、マスターパスワード(確認用を含む)を入力します。

※マスターユーザー名については、わかりやすいように「admin」にしていますが、それ以外は任意の値を設定して下さい。尚、パスワードの自動生成にチェックをつけると、パスワードを自動的に作成することも可能です。

 

次にインスタンスの設定については、DBインスタンス(db.t3.microのところ)を別のものに変更することも可能ですが、特にこだわりがなければデフォルトのままでOKです。

 

次にストレージについては、無料枠ではストレージの容量が20GBまでのため、ストレージ割り当てを「20」に修正します。(デフォルトでは200だった)

また、デフォルトではストレージの自動スケーリングにチェックが付いていますが、もしストレージの使用量が20GBを超えた場合は別途料金が発生してしまうため、今回はチェックを外しておきます。

 

次に接続については、コンピューティングリソースとネットワークタイプについてはデフォルトのままでOKです。

Virtual Private Cloud(VPC)については、上記で事前に作成したVPCに変更して下さい。

 

そして、パブリックアクセスについてはデフォルトのままでOKです。

VPCセキュリティグループ(ファイアウォール)については、RDS用に作った方がいいため、右側の「新規作成」を選択し、新しいVPCセキュリティグループ名に任意の名前を入力します。

加えて、アベイラビリティゾーン、RDS Proxy、追加設定についてもデフォルトのままでOKです。

 

次にデータベース認証、モニタリングについてはデフォルトのままでOKです。

 

次に追加設定タブをクリックすると、追加設定の画面が表示されます。

 

最初のデータベース名に任意の名前を入力し、バックアップのチェックは外しておきます。

それ以外の項目についてはデフォルトのままでOKです。

※バックアップ機能は実際の本番環境では大事なものですが、知らないうちに不要なデータが溜まって無駄な料金が発生する可能性があるため、個人利用でコストをかけたくないような場合は使わないのが無難です。

 

次に暗号化については、デフォルトのままでOKです。

 

次にメンテナンスと削除保護(デフォルトでチェックが外れている)は、デフォルトのままでOKです。

 

次に概算月間コスト欄にRDSの無料利用枠についての説明が記載されているのでしっかり確認し、問題がなければ画面下の「データベースの作成」をクリックします。

 

これでデータベースの作成が開始されます。

※作成が完了するまで少し時間がかかります

 

データベースの作成が完了するつ画面上に緑色の背景で完了した旨の表示がされるので、右側の「接続の詳細の表示」をクリックします。

 

作成したデータベースに接続するための情報が表示されるので、エンドポイントについてはメモしておきます。(それ以外は設定時に入力した値を使います)

これでRDSの作成については完了です。

 

次にVPCの時と同様に新しく作成したRDS用のセキュリティグループの設定を修正するため、データベース一覧から作成したDB識別子をクリックします。

 

データベースの詳細画面が表示されるので、画面右下のVPCセキュリティグループをクリックします。

 

これで対象のセキュリティグループが表示されるので、画面右下の「インバウンドルールを編集」をクリックします。

 

これでインバウンドルールの編集画面が表示されるので、画面左下の「ルールを追加」をクリックします。

 

これでインバウンドルールに行が追加されるので、これを修正していきます。

 

タイプについては今回はMySQLを使っているため「MYSQL/Aurora」ソース作成したVPC用のセキュリティグループを選び、最後に画面右下の「ルールを保存」をクリックします。

※VPC用のセキュリティグループからRDSにアクセスできるようにします。

 

画面上に緑色の背景で正常に終了した旨が表示されればOKです。

これでVPCのセキュリティグループから新しく作成したRDS用のセキュリティグループに接続できるようになります。

※このようにセキュリティグループの設定によってアクセス制御が可能です。

 

作成したDBを削除したい場合

RDSを無料枠の期間内で利用する場合は問題ないと思いますが、無料枠の期間が終了した後などについては、ざっくり計算しても毎月約3,000円程度の料金が発生(MySQL、db.t3.micro、Single-AZ、20GBの場合)してしまうため、料金の発生を止めたい場合は作成したDBは削除しておくことを推薦します。

※対象のDBを一時的に停止させるだけでも、停止期間中は料金の発生が止まるようですが、停止させても7日後に再起動するという仕様が組み込まれているため、そのまま放置してしまうと知らないうちに再起動して料金が発生する危険性もあるのでご注意下さい。

作成したDBを削除したい場合は、RDSのデータベース画面から、対象のDBを選択し、画面右上のアクションタブから、「削除」をクリックします。

 

次にDBインスタンス削除の確認画面が表示されます。

余計なデータを残したく無い場合など、最終スナップショットの作成が不要な場合は、「最終スナップショットを作成しますか?」のチェックを外します。

 

次に「私は、インスタンスの削除後、システムスナップショットとポイントインタイムの復元を含む自動バックアップが利用不可になることを了承します。」をチェックし、削除確認欄に「delete me」を入力後、画面右下の「削除」をクリックします。

 

これでDBインスタンスの削除処理が開始されます。

※削除完了までに数分以上かかります。

 

画面上に緑色の背景でDBインスタンスが正常に削除された旨の表示がされれば、削除処理が完了です。

画面を更新すると、表示されているDBは消えています。

 



⑤-3:ECR(Elastic Container Registry)の作成

次にECRを作成するため、AWSアカウントの画面左上にあるサービスから「コンテナ>Elastic Container Registry」をクリックします。

 

次に画面左上の「三」をクリックし、メニューを表示させます。

 

次に画面左のメニューから「Repositories」をクリックします。

 

これでリポジトリの管理画面が表示されるので、画面右側の「リポジトリを作成」をクリックします。

 

次にリポジトリ作成画面が表示されるので、まずはアプリ(Rails)用のリポジトリを作成します。

可視性設定については、イメージを非公開にしたい場合は「プライベート」を選択し、リポジトリ名に任意の名前を入力します。

※リポジトリ名については、後述で実行するコマンドの関係から、今回の場合は基本的にはdocker-composeで付けたコンテナ名と同じ名前にしておくのがおすすめです。

 

そして、イメージスキャンの設定と暗号化設定についてはデフォルトのままでOKなので、画面下の「リポジトリを作成」をクリックします。

 

これでリポジトリの作成が完了したので、リポジトリのURIはメモしておきます。

そして、このリポジトリにRailsアプリのDockerイメージを格納したいので、画面右上の「プッシュコマンドの表示」をクリックします。

 

これで作成したリポジトリに対して、Dockerイメージをプッシュするためのコマンドが4つ表示されるので、上から順に実行していきます。

 

①のコマンドはAWS ECRにログインするためのコマンドです。

 

②のコマンドでルートディレクトリにあるDockerfileから、オプション「-t イメージ名」で指定した名前を付けて、ローカルにDockerイメージを作成します。

※docker buildを実行する際は、ローカルにキャッシュが残っていると想定外のイメージが作成されてしまう可能性もあるため、初回実行以外はオプション「–no-cache」を付けて下さい。

 

③のコマンドでECRで作成したリポジトリに対して、ローカルで作成したDockerイメージをプッシュできるようにタグを付けます。

 

④のコマンドでECRで作成したリポジトリに対して、ローカルで作成したDockerイメージをプッシュして格納します。

 

これでECRで作成したリポジトリに対してDockerイメージの格納が完了です。

格納が上手くいっているかを確認するには、ECRの画面に戻ってリポジトリ名をクリックします。

 

リポジトリの詳細画面を確認し、Dockerイメージの詳細が表示されていればOKです。

 

次は同様の方法にて、Webサーバー(Nginx)用のDockerイメージを格納するためのリポジトリを作成し(リポジトリのURIはメモしておく)、作成後にプッシュ用の確認します。

 

先ほどと同様にプッシュコマンドが表示されますが、1つ目のコマンドについてはECRへのログイン用コマンドであり、先ほど実行しているため、ERCからログアウトしていなければ実行不要です。

また、2つ目のコマンドでローカルにDockerイメージを作成しますが、ここで表示されているコマンドについてはルートディレクトリにあるDockerfileを対象としているため、別の場所にあるDockerfileを使いたい場合はコマンドの修正が必要です。

今回の例ではWebサーバー(Nginx)用のDockefileはnginxフォルダ内にあるため、コマンドを「docker build -f ./nginx/Dockerfile -t タグ名 .–no-cache」のように修正して実行する必要があります。

 

ここまででアプリ(Rails)用とWebサーバー(Nginx)用のリポジトリの作成とDockerイメージの格納が完了すれば、ECRの作成は完了です。

最後にECRのdockerレジストリにログイン済みかを確認したい場合は、以下のコマンドを実行します。

$ cat ~/.docker/config.json

 

ファイル内に「[your-aws-account-id].dkr.ecr.[your-region-name].amazonaws.com」があればログイン済みです。

 

そしてECRのdockerレジストリからログアウトしたい場合は、以下のコマンドを実行します。

※[your-aws-account-id]と[your-region-name]はご自身のものに修正して下さい。

$ docker logout [your-aws-account-id].dkr.ecr.[your-region-name].amazonaws.com

 

⑤-4:ECS(Elastic Container Service)の作成

次にECSを作成するため、AWSアカウントの画面左上にあるサービスから「コンテナ>Elastic Container Service」をクリックします。

 

次に画面左のメニューから「タスク定義」をクリックします。

 

次にタスク定義の一覧画面が表示されるので、「新しいタスク定義の作成」をクリックします。

 

次にタスク定義画面が表示されるので、今回は起動タイプに「FARGATE」を選択し、画面右下の「次のステップ」をクリックします。

 

次にタスクとコンテナの定義の設定をしていきますが、タスク定義名には任意の名前を入力します。

 

次にタスクサイズについては、今回はコストを抑えるために最小構成にするため、タスクメモリを「0.5GB」、タスクCPUを「0.25 vCPU」を選択します。

 

次にコンテナの定義を追加するため、「コンテナの追加」をクリックします。

 

次にコンテナの追加画面が表示されるので、各種値を設定していきます。

 

最初はアプリ用のコンテナを追加するため、それに対応したコンテナ名を入力し、イメージにはECRで作成したアプリ用のリポジトリのURIを入力します。

そして、ポートマッピングについては、今回はアプリ用のDockerfileでポートを3010にしているためポートマッピングに「3010」を入力します。

 

次にヘルスチェックの項目はデフォルトのままでOKです。

 

次に環境の項目はデフォルトのままでOKです。

 

次に環境変数の項目から、必要な環境変数を追加していきます。

 

今回最低限必要な環境変数としては「RAILS_MASTER_KEY」、「DB_DATABASE」、「DB_USERNAME」、「DB_PASSWORD」、「DB_HOST」になるため、それぞれ値(Value)を設定します。

 

次にコンテナタイムアウトとネットワーク設定についてはデフォルトのままでOKです。

 

次にストレージとログの設定についてはデフォルトのままでOKです。

 

次にリソース制御とDOCKERラベルの設定はデフォルトのままでOKです。

最後に画面右下の「追加」をクリックします。

 

これでアプリ用のコンテナ追加が完了したので、同様にWebサーバー用のコンテナを追加します。

 

Webサーバー用のコンテナについては、コンテナ名とイメージをそれぞれ入力後、ポートマッピングには「80」を設定します。

 

そして、ストレージとログにあるボリュームソースに、先ほど追加したコンテナの名前を設定します。

これにより、Webサーバー用のコンテナからアプリ側のコンテナのボリューム(「/ディレクトリ名/public」と「/ディレクトリ名/tmp」)を参照できるようになります。

それ以外の項目はデフォルトのままでいいため、最後に画面右下の「追加」をクリックします。

 

これでコンテナの追加が完了です。

次にサービス統合とプロキシ設定についてはデフォルトのままでOKです。

 

次にログルーターの統合とボリュームについてはデフォルトのままでOKです。

最後に画面右下の「作成」をクリックします。

 

これでタスク定義が作成されます。

作成完了後、画面右下の「タスク定義の表示」をクリックします。

 

作成されたタスク定義が表示されるので、画面上のタスク定義名をクリックします。

 

タスク定義名のステータスが「Active」ならOKです。これでタスク定義の作成が完了です。

※一度作成したタスク定義については、登録を解除して無効化することはできますが、タスク定義自体の削除はできないようなのでその点はご注意下さい。

 

次に画面左のメニューにある「クラスター」をクリックし、「クラスターの作成」をクリックします。

 

次にテンプレート選択画面が表示されるので、「ネットワーキングのみ」を選択し、画面右下の「次のステップ」をクリックします。

 

次にクラスターの設定画面が表示されるので、クラスター名に任意の名前を入力し、それ以外はデフォルトのままでOKなので、最後に画面右下の「作成」をクリックします。

 

これでクラスターが作成されますが、初回作成時にはまだIAMロール「AWSServiceRoleForECS」が作成されておらず、一度エラーになると思います。

初回実行後にIAMロール「AWSServiceRoleForECS」が自動で作成されるので、もう一度クラスターの作成を実行すると、下図のように正常終了するので、「クラスターの表示」をクリックします。

※もし、エラーを発生させたくない場合は、事前にIAMロール「AWSServiceRoleForECS」を作成しておく必要があります。

 

これで作成したクラスターの詳細画面が表示されるので、画面下のタブ「サービス」にある「作成」をクリックします。

 

次にサービス作成画面が表示されるので、サービスの設定をしていきます。

 

まずは起動タイプとして、「FARGATE」を選択します。

オペレーティングシステムファミリーはデフォルトのままでOKです。

 

次にサービス名に任意の名前を入力し、タスクの数は「1」に設定します。それ以外の項目はデフォルトのままでOKです。

※負荷分散処理をしたい場合などはタスクの数を2以上にする必要がありますが、その分コストがかかります。今回はコストを抑えるため、タスクの数は「1」にしています。

 

次にデプロイメントの項目はデフォルトのままでOKなので、画面右下の「次のステップ」をクリックします。

 

次にネットワーク構成を設定していきます。

 

まずはクラスターVPCには、最初に作成したVPCを選択し、サブネットには選択したVPCに紐付くパブリックサブネットを一つ選択します。

※今回はタスク数が1なので、サブネットは一つ選択すればいいですが、負荷分散処理のためにタスク数を増やした場合は、その分サブネットを選択することになります。

そして、その後にセキュリティグループにある「編集」をクリックします。

 

次にセキュリティグループの設定画面が表示されるので、割り当てられたセキュリティグループについては、「既存のセキュリティグループの選択」にチェックを付けます。

 

これで既存のセキュリティグループから選択できるので、最初に作成したVPCに紐付くセキュリティグループを選択し、画面右下の「保存」をクリックします。

※セキュリティグループのインバウンドルールに、タイプ「HTTP」、ソース「0.0.0.0/0」が無い場合は、インターネットから接続できないのでご注意下さい。

 

これでVPCとセキュリティグループの設定については完了です。

 

次にロードバランシングの設定は今回は不要なので、デフォルトの「なし」でOKです。

 

次にApp Mechやサービスの検出(オプション)についてはデフォルトのままでOKなので、画面右下の「次のステップ」をクリックします。

 

次にAuto Scaling(オプション)の設定画面になりますが、デフォルトのままでOKなので、画面右下の「次のステップ」をクリックします。

 

これで設定した項目の確認画面が表示されます。

 

設定内容を確認後、問題がなければ画面右下の「サービスの作成」をクリックします。

 

これでサービスが作成されるので、画面右下の「サービスの表示」をクリックします。

 

次にサービスの詳細画面が表示されるので、画面右下の更新ボタンを何度かクリックし、タスクのステータスが「RUNNING」になるのを待ちます。

 

タスクのステータスが「RUNNING」になったら、画面左下のタスク名をクリックします。

 

これでタスクの詳細画面が表示されます。

まずはアプリのログを確認してみるため、タブ「ログ」をクリックします。

 

ログの画面が表示されるので、画面中央のリストからアプリ用のコンテナを選択します。

 

これでアプリに関するログを確認可能です。もしエラーなどがあった場合は、一度ここを確認するのがよさそうです。

特に問題がなければ、もう一度タスクの詳細画面を確認するため、タブ「詳細」をクリックします。

 

次にタスクの詳細画面のネットワーク項目にある「パブリックIP」を確認して下さい。

ブラウザからこのパブリックIPにアクセスすると、デプロイしたアプリケーションを確認できます。

 

今回は事前にテスト用のAPIを作成していたため、ブラウザから「パブリックIP/api/v1/test」にアクセスし、作成したAPIの内容が表示されればOKです。

 



⑤-5:Railsアプリ用Dockerイメージの修正

これでAWS ECS(Fargate)にアプリをデプロイし、インターネットに公開するところまでできました。

ただし、アプリ用のDockerfileで最後に実行するファイル「entrypoint.sh」の中で、DBを新規作成するコマンドがありますが、これは一度だけ実行すればいいコマンドなので、次はこれを修正しておきます。

まずはファイル「entrypoint.sh」を以下のように修正します。

#!/bin/bash
set -e

# Rails特有の問題を解決するためのコマンド
rm -f /ディレクトリ名/tmp/pids/server.pid

# production環境の場合のみJSとCSSをビルド
if [ "$RAILS_ENV" = "production" ]; then
  # APIモード以外でアセットコンパイルが必要な場合に利用
  # bundle exec rails assets:clobber
  # bundle exec rails assets:precompile
  # --------------------------------------
  # 本番環境(AWS ECS)への初回デプロイ時に利用
  # 初回デプロイ後にコメントアウトして下さい
  # bundle exec rails db:create
  # --------------------------------------
  # 2回目以降のデプロイ時に利用
  # 初回デプロイ後にコメントアウトを外して下さい
  bundle exec rails db:migrate
fi

# サーバー実行(DockerfileのCMDをセット)
exec "$@"

※ディレクトリ名の部分には任意の名前を付けて下さい

 

加えて、変更内容がしっかり反映されるかも確認しておきたいので、「app/controllers/application_controller.rb」を以下のように修正し、テスト用APIの表示を少しだけ変更しておきます。

class ApplicationController < ActionController::API

  # テスト用のAPIを追加
  def test
    # テスト用のJSON形式のオブジェクト
    test_json_obj = [
      { id: 1, title: "First Text", text: "最初のテキスト" },
      { id: 2, title: "Second Text", text: "2番目のテキスト" },
      { id: 3, title: "Third Text", text: "3番目のテキスト" },
    ]

    # JSON形式で出力
    render json: test_json_obj
  end

end

 

次にもう一度アプリ用のDockerイメージをECRに格納するため、前回と同様にコマンドを実行します。

①ローカルにDockerイメージを作成するコマンド(オプション「–no-cache」は付ける)を実行

※docker buildを実行する際、ローカルにキャッシュが残っていると想定外のイメージが作成される可能性があるため、初回実行時以外は必ずオプション「–no-cache」を付けて下さい。

 

②ECRにプッシュできるようにローカルで作成したDockerイメージにタグを付けます。

 

③DockerイメージをECRにプッシュしてリポジトリに格納します。

 

これでECRにあるアプリ用のリポジトリに最新のDockerイメージが格納されるので、ECRページから確認してみて下さい。

下図のように、最新のDockerイメージが格納されていればOKです。

 

次に最新のDockerイメージを公開中のアプリに反映させるには、一度タスクの再起動が必要になります。

タスクの再起動をするためには、現在起動中のタスクを停止させれば可能(停止させても、サービスに紐付くタスクは新しく起動される仕様?になっている)なので、「ECS>クラスター>タスク」から画面を開き、起動中のタスクにチェックを付け、ボタン「停止」をクリックします。

 

ポップアップが表示されるので、ボタン「停止」をクリックします。

 

画面上にタスクが正常に停止した旨のメッセージが表示されればOKです。

 

これで起動中のタスクは停止できましたが、すぐにまた新しいタスクが起動するはずなので、新しく起動したタスクのステータスが「RUNNING」であることを確認し、タスク名をクリックします。

 

新しく起動したタスクの詳細画面から、ネットワークにあるパブリックIPを確認し、先ほどと同様にブラウザから「パブリックIP/api/v1/test」にアクセスして確認します。

 

テスト用APIの表示内容を確認し、変更内容が反映されていればOKです。

 

⑤-6:ECSで起動したアプリを止める方法(補足)

上記ではECSを利用していますが、2022年11月現在において”無料枠はありません”ので、起動中は別途料金が発生します。

そして、先ほども少し操作した通り、タスクは停止してもまた新しく起動するため、ECSで起動したアプリを止めたい場合は、とりあえずサービスを削除して下さい。

サービスを削除するには、「ECS>クラスター」から対象のクラスターの詳細画面を開き、タブ「サービス」にある起動している対象のサービスにチェックを付けた後、ボタン「削除」をクリックします。

 

サービスの削除に関するポップアップが表示されるので、入力欄に「delete me」を入力後、右下の「削除」をクリックします。

 

これでサービスの削除処理が開始されます。

サービスが削除されたかを確認するには画面左上の「クラスター」をクリックし、クラスターページを開きます。

 

クラスターページのFARGATEの項目欄に起動中のサービスやタスクがなければOKです。

 



AWS ECS(Fargate)にデプロイしたアプリのIPを固定(独自ドメインを利用)する方法

ここまででAWS ECS(Fargate)にアプリをデプロイし、インターネットに公開するところまでできました。

ただし最初にも少し解説した通り、この段階で通信に利用しているパブリックIPは固定ではなく、このままだと実際に利用する際に様々な不都合が生じるため、固定IPを使えるようにする必要が出てきます。

今回はAWS ECSを利用しており、ECSにおける固定IPを利用する方法はいくつかあるようですが、この記事では基本的な方法として以下の手順で行います。

IPを固定(独自ドメインを利用)するための手順

  1.  独自ドメインの取得(AWS Route 53を利用)
  2.  ACM(AWS Certificate Manager)の設定
  3.  ALB(Application Load Balancer)の作成
  4.  Route 53の設定(ドメインとALBの紐付け)
  5.  ALBを含めたECSのサービスを作成

※独自ドメインの取得やRoute 53の利用には別途料金がかかります。また、ALBの利用する際は無料枠がありますが、無料期間終了後には別途料金がかかるため、ALBの料金発生を止めたい場合は削除するのを忘れないようにして下さい。

 

①:独自ドメインの取得(AWS Route 53を利用)

まずは独自ドメインを取得(購入)する必要がありますが、独自ドメインを購入できるサービスは色々あります。

※独自ドメインについてよくわからない方は、事前にググって調べて下さい。

ただし、AWSにもRoute 53』というサービスがあり、他のサービスを利用するよりAWSへの設定が簡単にできたり、ドメインの年間利用料も長期間利用する場合は比較的安い(他のサービスだと1年目は安いが、2年目以降の更新料が高い)ため、今回はAWS Route 53を使って独自ドメインを取得します。

ではAWS Route 53を利用するため、AWSアカウントの画面左上にあるサービスから「ネットワーキングとコンテンツ配信>Route 53」をクリックします。

 

次にRoute 53ダッシュボード画面が表示されるので、「ドメインの登録」をクリックします。

 

次にドメイン名の選択画面が表示されます。

 

取得したいドメイン名を入力し、ドメインの拡張子を選択後、「チェック」をクリックします。

※ドメインの拡張子については、種類によって料金が違ったり、後述するプライバシーの保護が可能かどうかの違いがあったりするのでご注意下さい!尚、2022年11月時点では、「.link」が安くてプライバシー保護も可能でした。

 

入力したドメイン名の使用可否が表示されるので、対象のドメインが利用可能なら「カートに入れる」をクリックします。

 

ボタン押下後、画面右側のショッピングカートに対象のドメインが表示されます。

※登録期間を1年以上に変更することも可能です。

 

間違いがなければ、画面下の「続行」をクリックします。

 

次にドメインのお問い合せ詳細の入力画面が表示されるので、AWSアカウント登録時と同様に英数字で各種項目を入力していきます。

 

ちなみに都道府県の項目については選択ができないようなので、その場合は市区町村欄に「,」を付けて都道府県も含めるとよさそうです。

また、プライバシーの保護については、「有効化」になっていない場合、ここで入力した個人情報が一般公開されるのでご注意下さい。

全ての項目を入力後、問題がなければ画面右下の「続行」をクリックします。

 

次に連絡先の詳細の確認画面が表示され、入力した個人情報が表示されますが、それぞれ「プライバシー保護済み」であることを確認して下さい。

そして、ドメインの自動更新について「有効化」または「無効化」を選択します。

 

最後に規約を確認し、問題がなければチェックを付け、画面右側の「注文を完了」をクリックします。

 

これでドメインの購入手続きが開始されます。ポップアップが表示されますが、「閉じる」をクリックします。

 

次に画面中央の「ドメインに移動」をクリックします。

 

次に保留中のリクエスト画面が表示されるので、購入したドメインが表示されていることを確認して下さい。

 

その後(数分後〜)、ドメイン購入時に入力したメールアドレス宛に、下図のようなメールアドレスを認証するためのメールが届くので、メールに記載のリンクをクリックして承認します。

※期限までに承認しないとドメインが無効になるのでご注意下さい。

 

メールに記載のリンクをクリック後、下図のような画面が表示され、メールアドレスの認証が行われます。(この後に画面は閉じてOK)

 

その後、メールアドレスが確認された旨のメールが届き、購入したドメインの手続きが進みます。

 

しばらくした後(数分後〜)、ドメインの登録が完了した旨のメールが届き、これで購入したドメインが使えるようになります。

 

次に先ほどのAWSアカウント画面(Route 53)に戻り、画面左のメニューから「登録済みドメイン」をクリックすると、購入して有効になったドメインが確認できます。

登録時みドメイン画面に購入したドメインが表示されていれば、独自ドメインの取得(購入)が完了です。

 

尚、Route 53でドメインを購入した場合は自動的にホストゾーンが作成されますが、他のサービスで購入したドメインを使いたい場合は、別途ホストゾーンの作成が必要になります。

 

②:ACM(AWS Certificate Manager)の設定

次にHTTPS通信(暗号化された通信)のためのSSL証明書を発行するため、ACM(AWS Certificate Manager)の設定を行います。

このACM自体は無料で利用できますが、AWS内のサービスにSSLを適用させなければいけない制約はあるのでその点はご注意下さい。

ではACMを利用するため、AWSアカウントの画面左上にあるサービスから「セキュリティ、ID、およびコンプライアンス>Certificate Manager」をクリックします。

 

次にACM画面が開くので、画面左のメニューか画面中央のボタンから「証明書をリクエスト」をクリックします。

 

次に証明書のリクエスト画面が表示されるので、そのまま画面右下の「次へ」をクリックします。

 

次にパブリック証明書をリクエストの画面が表示されます。

 

完全修飾ドメイン名に先ほど購入したドメイン名を入力し、サブドメイン用のドメインも追加するため、「この証明書に別の名前を追加」をクリックします。

 

もう一つのドメイン入力欄が表示されるので、先頭に「*.」を付けたドメイン名「*.ドメイン名.拡張子」も入力して追加します。

 

それ以外の項目はデフォルトのままでいいため、画面右下の「リクエスト」をクリックします。

 

これで画面上に証明書が正常にリクエストされた旨のメッセージが表示されるので、右側の「証明書を表示」をクリックして確認します。

 

次に証明書のドメイン欄にある「Route 53でレコードを作成」をクリックします。

 

次に画面右下の「レコードを作成」をクリックします。

 

これで画面上にDNSレコードが正常に作成された旨のメッセージが表示されるので、メッセージ下の「証明書」をクリックし、証明書一覧画面に戻ります。

 

証明書の検証が完了するまでは少し時間がかかるので、適宜更新ボタンをクリックして状態を更新し、証明書のステータスが「発行済み」になればOKです。

 



③:ALB(Application Load Balancer)の作成

次はALB(Application Load Balancer)の作成を行います。

ALBについては、ELB(Elastic Load Balancing)の種類の一つになりますが、アプリケーションへの負荷や、CPUの稼働状況をリアルタイムにモニタリングできるロードバランサーです。

そんなALBはHTTP・HTTPSプロトコルのレイヤー7(アプリケーション層)に対応する単一のロードバランサーで、Webアプリケーション用としてよく利用されるものになります。

ではALBを利用するため、AWSアカウントの画面左上にあるサービスから「コンピューティング>EC2」をクリックします。

 

次に画面左側のメニューから「ロードバランサー」をクリックします。

 

次にロードバランサーの一覧画面が表示されるので、画面上の「Create load balancer」をクリックします。

 

次にロードバランサーの選択画面が表示されます。

 

今回はALBを利用するため、画面左側の下にある「Create」をクリックします。

 

次にALBの設定画面が表示されます。

 

Basic Configurationについては、ロードバランサー名を入力し、その他はデフォルトのままでOKです。

 

次にNetwork mappingについては、対象のVPCを選択し、AZのチェックをそれぞれ付けます。

 

次にSecurity groupsについては、ALB用のセキュリティグループを新規作成するため、「Create new security groupe」をクリックします。

 

別の新しいウインドウでセキュリティグループを作成の画面が表示されるので、それぞれ項目を入力していきます。

 

セキュリティグループ名と説明を入力後、デフォルトで設定されているVPCは一旦削除するため、VPCの右にある「×」ボタンをクリックします。

 

次に対象のVPCを選択し、続いてインバウンドルールの設定を追加するため、画面左下の「ルールを追加」をクリックします。

 

インバウンドルールの項目にはALBへの通信を許可する設定を追加します。

 

インバウンドルールに追加する設定については下図の通り、タイプ「HTTP」と「HTTPS」のそれぞれに対して、ソースは「0.0.0.0/0」(IPv4接続)と「::/0」(IPv6接続)を追加します。

 

次にアウトバウンドルールの項目には、ALBから対象のVPCのみへ通信するように設定するため、まずはアウトバウンドルールにある「削除」ボタンをクリックし、デフォルトの設定を削除します。

 

そして、「ルールを追加」をクリックします。

 

アウトバウンドルールに追加する設定については、タイプは「HTTP」、送信先をカスタムとし、対象のVPCのセキュリティグループを選択します。

最後に画面右下の「セキュリティグループを作成」をクリックします。

 

これでALB用のセキュリティグループの作成が完了です。

 

セキュリティグループの作成が完了後、もう一度ALBの設定画面に戻り、セキュリティグループの項目にある更新ボタンを押した後、デフォルトで設定されているセキュリティグループの「×」ボタンを押して削除します。

 

そして、先ほど作成したALB用のセキュリティグループを選択します。

 

作成したALB用のセキュリティグループが追加されればOKです。

 

次にListeners and routingについては、まずターゲットグループを作成するため、画面中央にある「Create target group」をクリックします。

 

別の新しいウインドウでターゲットグループ作成の画面が表示されるので、それぞれ項目を入力していきます。

 

Basic configurationのChoose a target typeについては、「IP addresses」を選択した後、ターゲットグループ名を入力します。

 

次にVPCに設定されているデフォルト値が、対象のVPCであることを確認し、問題がなければそれ以外の項目はデフォルトのままでOKです。

 

次にHealth checksについては、基本的にデフォルトのままでOKですが、「Advanced health check settings」をクリックするとより高度な設定が可能です。

 

もし、RailsのSSLを強制化するオプション(config/environments/production.rbの「config.force_ssl = true」)を利用する場合は、デフォルト設定だとエラーになるので注意が必要です。

具体的には上記のオプションを利用するとHTTP接続からHTTPS接続へのリダイレクト処理が走った際に、ステータスコード「301」を返しますが、Health checksのSuccess codesはデフォルトでステータスコード「200」しか許可していないためです。

 

そのため、RailsのSSLを強制化するオプションを利用する場合は、Health checksのSuccess codesにカンマ「,」で区切ってステータスコード「301」を追加しておきます。

 

これで一通り設定が完了したので、画面右下の「Next」をクリックします。

 

次にRegister targetsの設定画面が表示されます。

 

これはとりあえず設定不要なので、画面右下の「Create target group」をクリックします。

 

これでターゲットグループの作成が完了です。

 

次にもう一度ALBの設定画面に戻り、Listener HTTP:80のDefault actionの更新ボタンを押した後、先ほど作成したターゲットグループを選択して設定します。

 

次にもう一つListenerを追加するため、「Add listener」をクリックします。

 

Listenerの設定画面が追加されるので、次は「HTTPS」の設定を追加します。

 

Protocolに「HTTPS」を選択し、Default actionは先ほどと同様に作成したターゲットグループを選択して設定します。

 

次にSecure listener settingsについては、Default SSL/TLS certificateの項目で、事前にACMで設定したドメインを選択します。

それ以外の項目はデフォルトのままでOKです。

 

次にSummaryに設定内容が表示されるので確認し、問題がなければ画面右下の「Create load balancer」をクリックします。

 

これでALBの作成が完了です。

 

ALBを削除する方法

ALBには無料枠がありますが、無料枠が終了した後などにそのまま使い続けようとすると、ざっくり言っても1カ月あたり約3,000円以上のコストがかかるので、その点はご注意下さい。

そのため、ALBの料金が発生するのを止めたい場合は削除する(一時停止はないようです。。)のを忘れないようにして下さい。

ALBを削除する方法としては、「EC2>Load balancer」画面を開き、対象のロードバランサーにチェックを付けて、画面上の「Actions」ボタンのリストを開きます。

 

リストに「Delete」ボタンがあるので、クリックして削除可能です。

 

④:Route 53の設定(ドメインとALBの紐付け)

次にドメインとALBを紐付ける設定をするためRoute 53の画面を開き、画面左のメニューにある「ホストゾーン」をクリックします。

 

次にホストゾーン一覧が表示されるので、対象のドメインをクリックします。

 

次にホストゾーンの詳細画面が表示されるので、「レコードを作成」をクリックします。

 

次にレコードのクイック作成画面が表示されるので、各種項目を設定していきます。

 

レコード名については、ルートドメイン(例えばXXX.linkなど)を設定するなら空白にし、サブドメイン(例えばAAA.XXX.linkならAAAの部分を入力)を設定するなら入力して下さい。

※アプリケーションを起動後、ここに設定したレコード名でブラウザからアクセスできるようになります。

そして、レコードタイプはデフォルトの「A」でOKなので、エイリアスをクリックして有効化します。

 

エイリアスを有効化すると、エンドポイントを選択可能になります。

 

エンドポイントには「Application Load Balancer と Classic Load Balancer へのエイリアス」を選択し、対象のリージョンと先ほど作成したALBを選択します。

最後に画面右下の「レコードを作成」をクリックします。

 

これでホストゾーンにレコードが追加されるので、画面上のメッセージにある「ステータスを表示」をクリックします。

 

変更情報の詳細画面が表示されるので、更新ボタンを何度かクリックし、ステータスが「INSYNC」になればOKです。

これでドメインとALBを紐付ける設定が完了です。

 



⑤:ALBを含めたECSのサービスを作成

ここまでで固定IPを使えるようにするための設定が完了したので、あとは先ほど作成したECSのサービスとほぼ同様の設定にしつつ、今回はALBを含めた形でECSのサービスを作成します。

※もし、先ほど作成したECSのサービスがまだ起動中の場合は、一度サービスを削除して下さい。

 

前回の設定と違う部分については、ロードバランシングの項目で「Application Load Balancer」を選択します。(ロードバランサーを設定すると、ヘルスチェックの猶予期間が有効になりますが、デフォルトのままでOKです)

 

次にロードバランサー名には作成したロードバランサー名が選択されていることを確認し、問題がなければロードバランス用のコンテナにWebサーバー用のコンテナを選択します。

 

Webサーバー用のコンテナを選択後、「ロードバランサーに追加」をクリックします。

 

これでロードバランス用のコンテナにWebサーバー用のコンテナが追加されるので、プロダクションリスナーポートとターゲットグループ名の設定を変更します。

 

プロダクションリスナーポートには「HTTPS」を選択し、ターゲットグループ名には先ほど作成したターゲットグループを選択します。

 

それ以外の項目は前回と変わらないため、設定を進めて最後に「サービスの作成」をクリックします。

 

これでサービスの起動が開始するので、「サービスの表示」をクリックします。

 

起動したタスクのステータスが「RUNNING」になったら、ブラウザから先ほどRoute 53に設定したドメイン(例えばXXX.拡張子 or AAA.XXX.拡張子など)にアクセスします。

 

今回はサブドメインを設定したため、「http://サブドメイン.ドメイン.拡張子/api/v1/test」にアクセスし、APIが有効であることを確認しています。

 

そして、HTTPS通信も有効であることを確認するため、「https://サブドメイン.ドメイン.拡張子/api/v1/test」にアクセスし、APIが有効であればOKです。

これでAWS ECS(Fargate)にデプロイしたアプリのIP固定(独自ドメインの利用)が完了です。

 



RailsアプリでHTTPS通信を強制化する方法

上記で固定IPを利用するために色々な設定を行いましたが、そのままだと通信が暗号化されていないHTTP通信でもアクセスできてしまうため、セキュリティ上危険です。

ただし、RailsアプリにはHTTP通信を強制化(HTTP通信をHTTPS通信にリダイレクトする)するための設定があり今回はバックエンドにRailsアプリを使っているため、この設定を有効化します。

具体的にはRailsの「config/environments/production.rb」の「config.force_ssl = true」のコメントアウトを外して有効化します。

require "active_support/core_ext/integer/time"

Rails.application.configure do

・・・

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  config.force_ssl = true

・・・

end

 

ファイルを修正したら前回と同様にECRに最新のDockerイメージをプッシュし、タスクを再起動させた後、ブラウザからもう一度「http://サブドメイン.ドメイン.拡張子/api/v1/test」にアクセスします。

 

実行後、HTTPS通信(https://サブドメイン.ドメイン.拡張子/api/v1/test)にリダイレクトされていればOKです。

 

尚、Rails以外でHTTPS通信を強制化させたい場合は、ALBのセキュリティグループのインバウンドルールから「HTTP」の設定を削除すれば一応可能です。

 



バックエンドにCircleCIを連携させてCI/CDパイプラインを構築する方法

次にバックエンドに対してテストの自動化(ブランチをGitHubにプッシュしたらテストを自動実行)および、本番環境(AWS ECS)への自動デプロイ(GitHubのmainブランチにマージしたらデプロイを実行)を実行できるようにするため、CircleCIを連携させます。

 

①:デプロイ用のIAMユーザーの作成

まずはCircleCIからAWSへアクセスするための、デプロイに必要な権限だけを付与したIAMユーザーを作成しておきます。

ではIAMユーザーを作成するため、AWSアカウントの画面左上にあるサービスから「セキュリティ、ID、およびコンプライアンス>IAM」をクリックします。

 

次にIAMダッシュボード画面が表示されるので、画面左側のメニュー「ユーザー」をクリックします。

※尚、画面右側にあるアカウントID(12桁の数字)もCircleCIの環境変数「AWS_ECR_REGISTRY_ID」に設定するため、メモしておいて下さい。

 

次にユーザー一覧画面が表示されるので、画面右側の「ユーザーを追加」をクリックします。

 

次にユーザー詳細設定画面が表示されるので、ユーザー名の入力および、AWSアクセスの種類を選択します。

 

今回はAWSアクセスの種類について全て選択しましたが、CircleCIだけで使うなら「アクセスキー・プログラムによるアクセス」だけでよさそうです。

ユーザー名の入力とAWSアクセスの種類を選択後、画面右下の「次のステップ:アクセス権限」をクリックします。

 

次にアクセス許可の設定画面が表示されるので、「既存のポリシーを直接アタッチ」を選択後、ポリシーのフィルタから必要なポリシー名を検索し、チェックを付けて付与します。

できるだけ余計な権限は付与しない方がいいですが、とりあえず今回の場合は「AmazonEC2ContainerRegistryFullAccess」と「AmazonECS_FullAccess」を付与すればCircleCIからデプロイできるようになります。

必要な権限を付与できたら、画面右下の「次のステップ:タグ」をクリックします。

 

次にタグの追加画面が表示されますが、特に必要はないので画面右下の「次にステップ:確認」をクリックします。

 

次に設定した内容の確認画面が表示されるので一度確認し、問題がなければ画面右下の「ユーザーの作成」をクリックします。

 

これでIAMユーザーの作成が完了です。

画面にアクセスキーIDとシークレットアクセスキーが表示されるので、CircleCIの環境変数「AWS_ACCESS_KEY_ID」と「AWS_SECRET_ACCESS_KEY」にそれぞれ設定するのでメモします。

 

②:「.circleci/config.yml」の作成

次にCircleCIを実行させるためのファイル「.circleci/config.yml」を作成します。

まずは以下のコマンドを実行し、必要なディレクトリとファイルを作成します。

$ mkdir .circleci
$ cd .circleci
$ touch config.yml

 

次にconfig.ymlの中身は以下のように設定します。

# Use the latest 2.1 version of CircleCI pipeline process engine.
# See: https://circleci.com/docs/2.0/configuration-reference
version: 2.1

# AWS用のorbsを利用する(バージョンは最新のものを使って下さい)
orbs:
  aws-ecr: circleci/aws-ecr@8.1.3
  aws-ecs: circleci/aws-ecs@03.2.0

# Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/2.0/configuration-reference/#jobs
jobs:
  # テスト用のジョブ
  test:
    # docker compose を利用するため、仮想マシンを利用する。(最新版のimageは下記URLを参照)
    # See: https://circleci.com/docs/ja/configuration-reference#available-linux-machine-images
    machine:
      image: ubuntu-2204:current
    # 仮想マシンを利用する場合、CircleCIの環境変数はDockerのコンテナ内へは直接読み込めない。
    # そのまま代入もできないため、一度parametersを設定する。
    parameters:
      rails_master_key:
        type: string
        default: $RAILS_MASTER_KEY
      mysql_root_password:
        type: string
        default: $MYSQL_ROOT_PASSWORD
      tz:
        type: string
        default: $TZ
    # 作業用のディレクトリを設定
    working_directory: ~/test_rspec
    # タスクを定義
    steps:
      # リポジトリを作業用のディレクトリにpull
      - checkout
      # 処理を実行
      # .envは公開していないため、まずは空ファイルを作成する。
      - run:
          name: .envの空ファイルを作成
          command: touch .env
      # parametersに設定したCircleCIの環境変数を.envに書き込んでDockerコンテナ内で読み込む
      - run:
          name: .envに環境変数にRAILS_MASTER_KEYを設定
          command: echo RAILS_MASTER_KEY=<< parameters.rails_master_key >> >> .env
      - run:
          name: .envに環境変数MYSQL_ROOT_PASSWORDを設定
          command: echo MYSQL_ROOT_PASSWORD=<< parameters.mysql_root_password >> >> .env
      - run:
          name: .envに環境変数にTZを設定
          command: echo TZ=<< parameters.tz >> >> .env
      # docker composeで各種コマンドを順次実行する。
      - run:
          name: Dockerコンテナのビルドを実行
          command: docker compose build --no-cache
      - run:
          name: Dockerコンテナの起動
          command: docker compose up -d
      # Dockerコンテナ起動後、すぐにDBを作成しようとするとエラーが発生する可能性があるため、少し待機する。
      - run:
          name: DB接続前の待機時間 10s
          command: sleep 10
      - run:
          name: DBを作成
          command: docker compose exec app rails db:create
      - run:
          name: マイグレーションを実行
          command: docker compose exec app rails db:migrate
      - run:
          name: RSpecの実行
          command: docker compose exec app bin/rspec
      - run:
          name: 起動中のDockerコンテナを停止して削除
          command: docker compose down

# Invoke jobs via workflows
# See: https://circleci.com/docs/2.0/configuration-reference/#workflows
workflows:
  test-and-deploy-wf:
    jobs:
      # テストを実行
      - test
      # ECRにWebサーバー用のDockerイメージをビルドしてプッシュ
      - aws-ecr/build-and-push-image:
          # ジョブ名を設定
          name: build-and-push-image-web
          # Dockerfileのパス
          dockerfile: ./nginx/Dockerfile
          # ECRのリポジトリ
          repo: ECRのWebサーバー用のリポジトリ名
          tag: "latest"
          # ジョブの実行条件
          requires:
            - test
          filters:
            branches:
              only: main
      # ECRにアプリ用のDockerイメージをビルドしてプッシュ
      - aws-ecr/build-and-push-image:
          # ジョブ名を設定
          name: build-and-push-image-app
          # Dockerfileのパス
          dockerfile: ./Dockerfile
          # ECRのリポジトリ
          repo: ECRのアプリ用のリポジトリ名
          tag: "latest"
          # build-and-push-image-webが完了後に実行
          requires:
            - build-and-push-image-web
      # ECSのサービスを更新
      - aws-ecs/deploy-service-update:
          # ジョブ名を設定
          name: deploy-ecs
          # クラスター名
          cluster: 対象のクラスター名
          # サービス名
          service-name: 対象のサービス名
          # タスク定義名
          family: 対象のタスク定義名
          # build-and-push-image-appが完了後に実行
          requires:
            - build-and-push-image-app

※上記の「ECRのWebサーバー用のリポジトリ名」、「ECRのアプリ用のリポジトリ名」、「対象のクラスター名」、「対象のサービス名」、「対象のタスク定義名」は任意の値を入力して下さい。そして、ECRにプッシュする際のタグ名は、手動でやる時と同様に「latest」に統一していますが、必要に応じて違うタグ名を付けることも可能です。そのほか、コードに変更がなくても、ECRへのデプロイはされるので、不要なDockerイメージが蓄積されてしまう点にはご注意下さい。(不要なやつは手動で削除して下さい。)

 

③:GitHubとCircleCIの連携

次に修正したファイルをGitHubにプッシュして最新状態にした後CircleCIのアカウントからGitHubにあるバックエンド用のリポジトリを連携させます。

CircleCIの画面から左のメニュー「Projects」をクリックし、対象のリポジトリの右側にある「Set Up Project」をクリックします。

 

config.ymlファイルの選択画面が表示されるので、ブランチ名に「main」を入力後、「Set Up Project」をクリックします。

 

これでGitHubのリポジトリがCircleCIに連携されますが、まだ環境変数を設定していないため、ジョブの実行は失敗します。

 

④:CircleCIに環境変数を設定

次に画面右上の「Project Settings」をクリックし、必要な環境変数を設定していきます。

 

環境変数を設定するには、画面左のメニュー「Environment Variables」をクリックし、画面中央の「Add Environment Varialbe」をクリックします。

 

環境変数の設定画面が表示されるので、環境変数名と値を入力し、「Add Environment Variable」をクリックして設定していきます。

 

必要な環境変数については下図の通りです。

今回必要な環境変数一覧

  •  RAILS_MASTER_KEY:config/master.keyの値
  •  MYSQL_ROOT_PASSWORD:.envに記載したMySQL用のパスワード
  •  TZ:.envに記載したタイムゾーン(Asia/Tokyo)
  •  AWS_ACCESS_KEY_ID:デプロイ用ユーザーのアクセスキーID
  •  AWS_SECRET_ACCESS_KEY:デプロイ用ユーザーのシークレットアクセスキー
  •  AWS_REGION:リージョン(東京なら「ap-northeast-1」)
  •  AWS_ECR_REGISTRY_ID:AWSのアカウントID(12桁の数字)

 

⑤:CircleCIの検証

これでCircleCIとの連携が完了したので、あとは実際にコードをGitHubにプッシュするとテストやデプロイ処理を実行可能です。

実際に動作させて正常終了した場合は、下図のようになります。

 

⑥:CircleCIからデプロイ時のECSの挙動による補足

CircleCIからECSにデプロイ時の挙動として、デプロイ処理を実行するとESCのタスク定義のリビジョンが更新され、その後に新しいリビジョンのタスク定義が起動されます。

元々起動していたタスクも一時的に残っている状態(実行中のタスクが2になる)になりますが、新しく起動したタスクがアクティブ(画面が更新)になった後、古いタスクは自動的に停止する(ただし、停止するまではちょっと時間がかかる)ため、焦らないようにご注意下さい。

 



Firebase Authenticationによるログイン関連機能の実装方法

Webアプリケーションにおいて最初に実装すべきはログイン関連機能だと思いますが、そんなログイン関連機能の実装にはセキュリティ面を考慮して認証系のサービスを連携させて実装させるのが一般的です。

そこで以下ではGoogleのFirebase Authenticationを利用したログイン関連機能の実装方法についてまとめます。

 

Firebaseでプロジェクトを作成する方法

まずはFirebaseでプロジェクトを作成するため、Firebase Authenticationにアクセスし、「コンソールを表示する」をクリックします。

 

次に「プロジェクトを作成」をクリックします。

 

次にプロジェクトの作成画面が表示されるため、プロジェクト名を入力後、規約などを確認してチェックボックス2つにチェックを付け、「続行」をクリックします。

 

次にGoogleアナリティクスに関する画面が表示されます。

 

今回は利用する必要がないので、画面下のボタンをクリックして無効化し、「プロジェクトを作成」をクリックします。

 

新しいプロジェクトの準備ができた後、「続行」をクリックします。

 

これでFirebaseでのプロジェクト作成が完了です。

 

Firebase Authenticationの設定方法

次にFirebase Authenticationを利用するため、画面中央の「Authentication」をクリックします。

 

次に「始める」をクリックします。

 

これでAuthentication画面が表示されるので、タブ「Sign-in method」からログイン方法を選択します。

今回は基本的な「メール / パスワード」を利用する場合を例として解説するため、「メール / パスワード」をクリックします。

 

これで「メール / パスワード」の設定画面が表示されます。

 

次に画面右上の有効にするをクリックして有効化し、画面右下の「保存」をクリックします。

 

これで「メール / パスワード」によるログインが有効になりました。

 

Firebase Authenticationにテスト用ユーザーの追加

次にテスト用のユーザーを画面から追加してみます。

タブ「Users」をクリックし、画面右の「ユーザーを追加」をクリックします。

 

メールアドレスとパスワードの入力画面が表示されるため、それぞれ入力した後に画面右下の「ユーザーを追加」をクリックします。

 

これでテスト用ユーザーの登録が完了です。(一意のUIDが設定されます)

 

Firebase Authenticationと連携するためのウェブAPIキーの確認方法

次に認証などをする際に必要になるFirebase AuthenticationのウェブAPIキーを確認しておきます。

画面左のメニュー上にある「プロジェクトの概要」の歯車マークをクリックしてメニューを表示させ、「プロジェクトの設定」をクリックします。

 

これでプロジェクトの設定画面が表示されるので、画面中央のウェブAPIキーをメモしておきます。

 

Postmanの登録方法

次にFirebase AuthenticationのAPIを利用し、先ほど登録したテスト用ユーザーのメールアドレスとパスワードで認証できるか検証してみます。

そんなAPIを検証する際は「Postman」というサービスを利用すると無料で簡単に行えるため、まずはアカウント登録を行います。

アカウント登録を行うには、Postmanにアクセスし、画面右上の「Sign Up for free」をクリックします。

 

次にアカウント作成画面が表示されますが、今回はGoogleアカウントを連携してログインできるようにします。

Googleアカウントを連携するには、画面下の「Sign up with Google」をクリックします。

 

次にGoogleアカウントの選択画面が表示されるので、対象のアカウントをクリックします。

 

次に規約などに関する画面が表示されるので、確認した後に画面下の「Accept」をクリックします。

 

次にユーザー情報の入力画面が表示されるので、ユーザー名を入力し、自分にあった役割を選択後、画面下の「Continue」をクリックします。

 

次に共有したいユーザーの登録画面が表示されますが、今回は共有用のリンクを作成してみるため、Invite people viaには「Invite Link」をクリックします。

 

次に「Get Invite Link」をクリックします。

 

これで共有用のリンクが作成されたので必要に応じてメモしておいて下さい。

最後に画面下の「Finish」をクリックします。

 

次にPostmanの画面が表示され、使い方などに関するポップアップも表示されますが、確認が不要なら画面右上の「×」をクリックして閉じます。

 

これでPostmanの画面が表示されます。

 

初期状態ではチーム用のワークスペースが指定されているため、個人用のワークスペースに切り替えます。

画面左上のメニュー「Workspaces」をクリックし、メニュー内にある「My Workspace」をクリックします。

 

これで個人用のワークスペースに切り替えが完了です。

 

PostmanでFirebase Authenticationに登録したユーザーを検証してidTokenを取得する方法

次にPostmanからFirebase AuthenticationのAPIを叩き、先ほど登録したテスト用ユーザーが有効であることを検証してみます。

まずは画面左のCollections画面にある「Create Collection」をクリックします。

 

これで新しいコレクションが作成されるので、次に画面左の「Add a request」をクリックします。

 

これで新しいリクエストが追加されるので、検証をするために必要な情報を設定していきます。

 

まずはタブ「Body」を選択後、「raw」をチェック、形式は「JSON」を選択し、画面下のエリアにFirebase AuthenticationのAPIで必要になる認証情報を入力します。

 

必要な認証情報については、email、password、returnSecureTokenになるので、以下のようにJSON形式で入力します。(emailとpasswordにはFirebase Authenticationに登録したテスト用ユーザーのものを設定して検証します)

{
    "email": "テスト用ユーザーのメールアドレス",
    "password": "テスト用ユーザーのパスワード",
    "returnSecureToken": true
}

 

次にHTTPメソッドに「POST」を選択し、URLには「https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key={FIREBASE_API_KEY}」を入力後、画面右側の「Send」をクリックします。

※{FIREBASE_API_KEY}の部分が先ほどメモしておいたFirebase AuthenticationのウェブAPIキーを設定します。

 

リクエストを実行後、画面下に結果が表示されるので、Statusが200になり、認証情報(idTokenなど)が取得できればOKです。

※このように取得したidTokenの値はバックエンドAPI実行時に利用することになります。

 

デスクトップ版Postmanのダウンロードとインストール方法

Web版のPostmanだけだと開発環境(localhostなど)でのAPI検証が出来ないため、デスクトップ版のPostmanのダウンロードとインストールも行います。

画面右上の「Save」をクリックしてここまでの変更を保存しておき、画面右下の「Desktop Agent」をクリックします。

 

メニューが表示されるので、「Download desktop agent」をクリックします。

 

zipファイルがダウンロードされるので解凍します。

 

ファイルを解凍後、「Postman Agent.app」をクリックします。

 

Macの場合はアプリケーションフォルダーに移動させるか聞かれるので、「Move to Applications Folder」をクリックして移動させておきます。

 

これでデスクトップ版Postmanのダウンロードとインストールが完了し、画面右下の「Desktop Agent」に緑色のチェックマークが付いていればOKです。

 

バックエンドAPI(Rails)にidTokenを検証する処理を追加

フロントエンド側で取得したidTokenは、バックエンドのAPIにリクエストを送る際に一緒に渡すため、バックエンド側で内容が適切かを検証する処理が必要です。

ただし、現状FirebaseのSDKはRubyをサポートしていないため、バックエンドAPIがRailsの場合は自前で実装する必要があります。

まずはJWT(JSON Web Token)形式のidTokenをデコードするためのモジュールを作成していきますが、JWT形式の値をデコードするにはgem「jwt」のインストールが必要なため、Gemfileに追加します。

※Gemfileに追加後はDockerコンテナの再ビルドorコンテナ起動中ならコマンドを実行してインストールして下さい。(インストールしてGemfile.lockに追加されればOK

・・
# ruby-jwt
gem "jwt"
・・

 

次にフォルダ「lib」配下にモジュールのファイル「firebase_authenticator.rb」を作成します。

$ cd lib
$ touch firebase_authenticator.rb

 

ファイル「firebase_authenticator.rb」の中身は次の通りです。

module FirebaseAuthenticator
  # Net::HTTP用にnet/httpを読み込み
  require "net/http"
  # エラー用クラス設定
  class InvalidTokenError < StandardError; end

  # 定数設定
  ALG = "RS256"
  CERTS_URI = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"
  PROJECT_ID = "Firebase AuthenticationのプロジェクトID"
  ISSUER_URI_BASE = "https://securetoken.google.com/"

  # idToken検証用メソッド
  def decode(token = nil)
    # JWT.decodeのオプション設定
    options = {
      algorithm: ALG,
      iss: ISSUER_URI_BASE + PROJECT_ID,
      verify_iss: true,
      aud: PROJECT_ID,
      verify_aud: true,
      verify_iat: true,
    }

    # tokenをデコードしてpayloadを取得
    payload, _ = JWT.decode(token, nil, true, options) do |header|
      # fetch_certificatesの戻り値はハッシュなのでキーを指定
      cert = fetch_certificates[header['kid']]
      if cert.present?
        OpenSSL::X509::Certificate.new(cert).public_key
      else
        nil
      end
    end

    # JWT.decode でチェックされない項目のチェック
    raise InvalidTokenError.new('Invalid auth_time') unless Time.zone.at(payload['auth_time']).past?
    raise InvalidTokenError.new('Invalid sub') if payload['sub'].empty?

    # payloadを返す
    payload

  # 例外処理
  rescue JWT::DecodeError => e
    Rails.logger.error e.message
    Rails.logger.error e.backtrace.join("\n")
    raise InvalidTokenError.new(e.message)

  end

  # 証明書読み込み用メソッド
  def fetch_certificates
    res = Net::HTTP.get_response(URI(CERTS_URI))
    raise 'Fetch certificates error' unless res.is_a?(Net::HTTPSuccess)
    body = JSON.parse(res.body)
  end

end

 

次にフォルダ「lib」配下に置いたファイルを自動で読み込めるようにするため、「config/application.rb」に「config.autoload_paths += %W(#{config.root}/lib)」を追加します。

※ファイル修正後、サーバーを起動中の場合は再起動して下さい。

module アプリ名
  class Application < Rails::Application
    ・・
    # lib配下のファイルを読み込み
    config.autoload_paths += %W(#{config.root}/lib)
    ・・
  end
end

 

次に「app/controllers/application_controller.rb」で、idTokenを検証する処理を追加します。

class ApplicationController < ActionController::API
  # Firebase Authenticator用のモジュールを読み込み
  include FirebaseAuthenticator

  # エラー用クラス設定
  class NoIdtokenError < StandardError; end
  rescue_from NoIdtokenError, with: :no_idtoken

  # idTokenの検証を実行
  before_action :authenticate

  private

    # idTokenの検証
    def authenticate
      # idTokenが付与されていない場合はエラー処理
      raise NoIdtokenError unless request.headers["Authorization"]
      @payload = decode(request.headers["Authorization"]&.split&.last)
    end

    # idTokenが付与されていない場合
    def no_idtoken
      render json: { error: { messages: ["idTokenが付与されていないため、認証できませんでした。"] } }, status: :unauthorized
    end

end

※request.headers[“Authorization”]はトークンが「Bearer XXXX」という形式で送られてくるため、「&.split&.last」でトークン部分である「XXXX」のみを切り出しています。

 

これでidTokenを検証する処理が追加できたので、Postmanでテスト用API(localhost/api/v1/test)を実行して確認してみます。

Postmanの画面を開き、画面左のNew Collectionの右にある「・・・」をクリックしてメニューを表示させ、「Add request」をクリックし、新しいリクエストを追加します。

 

リクエスト名は先ほどと違う名前に変更(今回はtest apiとした)し、メソッドは「GET」、URLは「http://localhost/api/v1/test」を入力します。

そして、idTokenも渡す必要があるため、タブ「Authorization」をクリックしてTypeに「Bearer Token」を選択し、Tokenには先ほど取得できたidTokenの値(有効期限が切れている場合は再度取得して下さい)を入力後、画面右上の「Send」をクリックして実行します。

リクエストを実行後、ステータス「200」で、apiの実行結果が返ってこればOKです。

※トークンがない場合や、トークンの値が間違っている場合はエラーになります。

 

バックエンドAPI(Rails)にUserモデルを作成

次にバックエンド側でもユーザー関連情報を管理できるようにするため、RailsにはFirebaseに登録したユーザーのUIDを保持する文字列型のカラム「uid」を持つUserモデルを作成します。

RailsでUserモデルを追加するには、以下のコマンドを実行します。

$ docker compose exec app rails g model user uid:string

 

これでマイグレーションファイル「xxxx_create_users.rb」が作成されますが、カラム「uid」は必須項目にするため、「null: false」を追加して以下のように修正します。

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
    t.string :uid, null: false

    t.timestamps
    end
  end
end

 

次に以下のコマンドを実行してマイグレーションファイルをDBに反映させます。

$ docker compose exec app rails db:migrate

 

これでUserモデルの作成が完了しましたが、uidには空文字「””」も入らないようにするため、「app/models/user.rb」にバリデーション「validates :uid, presence: true」も追加しておきます。

class User < ApplicationRecord
  validates :uid, presence: true
end

 

バックエンドAPI(Rails)にユーザー登録・削除処理を追加

次にバックエンドAPI(Rails)でユーザー登録や削除をする処理を追加していきます。

まずはユーザーコントローラーを作成するため、以下のコマンドを実行します。

$ docker compose exec app rails g controller users

 

そして、API用の各種ファイルについては、ディレクトリ「app/api/v1」配下に作成していく想定のため、以下のコマンドを実行してディレクトリの作成と作成したコントローラーファイルの移動を行います。

$ cd app/controllers
$ mkdir -p api/v1
$ mv users_controller.rb api/v1

 

次にユーザーコントローラーのファイル(app/controllers/api/v1/users_controller.rb)を以下のように修正します。

module Api
  module V1
    class UsersController < ApplicationController

      # 新規ユーザー登録
      def create
        @user = User.new(uid: payload_uid)
        if @user.save
          render json: @user
        else
          render json: { error: { messages: ["新規ユーザーを登録できませんでした。"] } }, status: unprocessable_entity
        end
      end

      # ユーザーの削除
      def destroy
        User.find_by(uid: payload_uid).destroy
        render json: { messages: ["#{payload_uid}のユーザーを削除しました。"] }
      end

      private

        # payloadのuidを返すメソッド
        def payload_uid
          @payload["user_id"]
        end
    end
  end
end

※インスタンス変数の値(@payload[“user_id”])については、メソッド化しておくことでテストを書く際にスタブ(本来実行されるメソッドの代わりに実行する代用品的なもの)を作りやすくなります。

 

次にユーザーコントローラーのルーティングを設定するため、ファイル「config/routes.rb」に以下のようなルーティングを追加します。

Rails.application.routes.draw do
・・
  # API用のルーティング
  namespace "api" do
    namespace "v1" do
      resource :users, only: [:create, :destroy]
    end
  end
・・
end

 

これでユーザーの登録と削除処理が追加できたので、Postmanで検証してみます。

まずはユーザーの登録処理を検証するため、Postmanで新しいリクエスト(今回はAdd User Requestとする)を追加後、メソッド「POST」、URL「http://localhost/api/v1/users」を入力し、有効なトークンを設定してから「Send」をクリックします。

ステータスが200で正常終了すればOKです。(今回は登録したユーザー情報をJSON形式で返すようにしているため、リクエストの結果にはユーザー情報も表示されています。)

 

実際に登録されているかを確認するため、コマンド「docker compose exec app rails c」でRailsコンソールを立ち上げ、コマンド「User.find(1)」を実行してユーザーが登録されていればOKです。

 

次にユーザーの削除処理も検証するため、Postmanで新しいリクエスト(今回はDelete User Requestとする)を追加後、メソッド「DELETE」、URL「http://localhost/api/v1/users」を入力し、有効なトークンを設定してから「Send」をクリックします。

ステータスが200で正常終了すればOKです。(今回はJSON形式のメッセージを返すようにしているため、リクエストの結果には設定したメッセージが表示されます。)

 

削除処理についても同様にRailsコンソールで確認し、ユーザーが削除されていればOKです。

 

idTokenの検証が不要な場合は「skip_before_action」を追加する

ここまででバックエンドAPI側の処理の追加は一通り完了しましたが、テスト用API「/api/v1/test」についてはidTokenの検証処理は不要なため、アプリケーションコントローラーに「skip_before_action :authenticate, only: [:test]」を追加し、idTokenの検証を実行しないように修正します。

class ApplicationController < ActionController::API
・・
  # idTokenの検証を実行
  before_action :authenticate
  # テスト用APIはidTokenの検証をスキップする
  skip_before_action :authenticate, only: [:test]
・・
end

 

修正内容を検証するため、Postmanのリクエスト「test api」のAuthorizationのタイプを「No Auth」に変更して実行し、正常終了すればOKです。

 

追加したモデルとコントローラーのテスト(RSpec)を追加する

上記でユーザーのモデルとコントローラーを追加したため、合わせてRSpecのテストコードを追加します。

ただし、そのままだとidTokenの検証でエラーになってしまうため、対象のメソッドをスタブ化するためにテスト用のAuthenticationHelperを追加します。

まずは以下のコマンドを実行し、フォルダ「spec」配下にディレクトリ「support」とファイル「authentication_helper.rb」の作成を行います。

$ cd spec
$ mkdir support
$ cd support
$ touch authentication_helper.rb

 

次に作成したファイル「authentication_helper.rb」を読み込めるようにするため、「spec/rails_helper.rb」内の「Dir[Rails.root.join(‘spec’, ‘support’, ‘**’, ‘*.rb’)].sort.each { |f| require f }」部分のコメントアウトを外します。

# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'

・・

# 以下の部分のコメントアウトを外す
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

・・

 

次に「spec/support/authentication_helper.rb」の内容を以下の通りに修正します。

module AuthenticationHelper
  # クラスメソッドを拡張
  extend ActiveSupport::Concern

  # include時に以下を実行する
  included do
    before do
      # ApplicationControllerのメソッド「authenticate」をスタブ化
      allow_any_instance_of(ApplicationController).to receive(:authenticate)
      # Api::V1::UsersControllerのメソッド「payload_uid」をスタブ化
      allow_any_instance_of(Api::V1::UsersController).to receive(:payload_uid).and_return("mock_uid")
    end
  end
end

 

これでidTokenの検証を回避するための準備が整ったので、モデルとコントローラーのテストを追加します。(今回は簡単なテストだけ追加します)

まずはUserモデル用のテストファイル「spec/models/user_spec.rb」を以下のように修正します。

require 'rails_helper'

RSpec.describe User, type: :model do
  let(:user) { FactoryBot.create(:user) }

  it "should be valid" do
    expect(user).to be_valid
  end

  it "uid should be present" do
    user.uid = " "
    expect(user).to be_invalid
  end

end

 

次にユーザーコントローラー用のテストファイル「spec/requests/users_spec.rb」を以下のように修正します。

require 'rails_helper'

RSpec.describe "Users", type: :request do
  let(:user) { FactoryBot.create(:user, uid: "mock_uid") }

  describe "POST /api/v1/users" do
    # JWT認証をスキップさせるためAuthenticationHelperを読み込み
    include AuthenticationHelper

    it "create user and return http success" do
      expect {
        post api_v1_users_url
        expect(response).to have_http_status(:success)
      }.to change(User, :count).by(1)
    end

  end

  describe "POST /api/v1/users (authentication not skip)" do

    it "no token error" do
      post api_v1_users_url
      expect(response).to have_http_status(:unauthorized)
    end

  end

  describe "DELETE /api/v1/users" do
    # JWT認証をスキップさせるためAuthenticationHelperを読み込み
    include AuthenticationHelper

    it "delete user and return http success" do
      registered_user = user
      expect {
        delete api_v1_users_url
        expect(response).to have_http_status(:success)
      }.to change(User, :count).by(-1)
    end

  end

  describe "DELETE /api/v1/users (authentication not skip)" do

    it "no token error" do
      delete api_v1_users_url
      expect(response).to have_http_status(:unauthorized)
    end

  end

end

 

テストを実行(コマンド「docker compose exec app bin/rspec」)し、以下のように正常終了すればOKです。

 

フロントエンド(Next.js)でAxiosと環境変数を使えるようにする

ここからはフロントエンド側でログイン関連機能を追加していきますが、まずはAxios(非同期APIの呼び出しを容易にするライブラリ)と環境変数を使えるようにします。

まずは以下のコマンドを実行し、Axiosをインストールします。

$ docker compose exec front yarn add axios

 

次に以下のコマンドを実行し、環境変数を設定するためのファイル「.env.local」をルートディレクトリの直下に作成します。

※ファイル「.env.local」はGitHubなどで公開しないようにご注意下さい。

$ touch .env.local

 

次にファイル「.env.local」にAxiosのdefaults.baseURLに設定する値を環境変数「NEXT_PUBLIC_BASE_URL」として設定します。

※今回の例ではNEXT_PUBLIC_BASE_URLの設定値は「http://localhost」とします。尚、クライアント側で使う環境変数名には「NEXT_PUBLIC_」をつけますが、サーバー側で使う環境変数には不要なのでご注意下さい。

NEXT_PUBLIC_BASE_URL="http://localhost"

 

次にDocker環境でファイル「.env.local」を読み込めるようにするため、「docker-compose.yml」に「env_file」の設定を追加します。

version: "3"
  services:
    # アプリの設定
・・
    # 環境変数の読み込み
    env_file:
      - .env.local
・・

 

次にサーバーを再起動させ、環境変数が反映されているか確認します。

確認するにはサーバー起動中に以下のコマンドを実行し、nodeを起動します。

$ docker compose exec front node

 

nodeが起動したら以下のコマンドを実行し、環境変数を確認します。

> console.log(process.env)

 

下図のように設定した環境変数が反映されていればOKです。

 

尚、nodeを終了するには以下のコマンドを実行します。

> .exit

 

次にAxiosを使えるようにするため、ファイル「pages/_app.tsx」に設定を追加します。

・・

// axiosのインポートと設定
import axios from 'axios'
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BASE_URL

・・

※環境変数は「process.env.環境変数名」で利用できます。

 

フロントエンド(Next.js)でアラート表示(React-Toastify)を使えるようにする

次にアラート表示を使えるようにするため、以下のコマンドを実行してReact-Toastifyをインストールします。

$ docker compose exec front yarn add react-toastify

 

次にReact-Toastifyを使えるようにするため、ファイル「pages/_app.tsx」に設定を追加します。(コンポーネント「<ToastContainer />」を追加するとアラート表示が使えます。)

・・

// React-toastifyのインポート
import { ToastContainer } from "react-toastify"
import 'react-toastify/dist/ReactToastify.css';

・・

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Component {...pageProps} />
    <ToastContainer
      position="top-center"   // 通知の表示位置
      autoClose={5000}        // 設定した時間(ms)経過後に通知をクローズさせる
      hideProgressBar={false} // 通知のProgress Barの非表示設定をOFF
      newestOnTop             // 最新の通知をTOPに表示させる
      closeOnClick            // 通知をクリックで閉じれる
      rtl={false}             // 通知の文字を左寄せにする
      pauseOnFocusLoss        // ウィンドウがフォーカスを失った時に通知の時間経過を一時停止
      draggable={false}       // 通知をドラッグできないようにする
      pauseOnHover            // 通知にカーソルを当てると時間経過を一時停止
      theme="colored"         // テーマ「coloered」を使用する
    />
  )
}

・・

 

尚、getLayout関数でページ単位のレイアウトを適用させるようにしている場合は、「function MyApp」の「return」部分を以下のように記述します。

・・

function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  const getLayout = Component.getLayout ?? ((page) => page)
  return (
    <>
      { getLayout(<Component {...pageProps} />) }
      <ToastContainer
        position="top-center"   // 通知の表示位置
        autoClose={5000}        // 設定した時間(ms)経過後に通知をクローズさせる
        hideProgressBar={false} // 通知のProgress Barの非表示設定をOFF
        newestOnTop             // 最新の通知をTOPに表示させる
        closeOnClick            // 通知をクリックで閉じれる
        rtl={false}             // 通知の文字を左寄せにする
        pauseOnFocusLoss        // ウィンドウがフォーカスを失った時に通知の時間経過を一時停止
        draggable={false}       // 通知をドラッグできないようにする
        pauseOnHover={false}    // 通知にカーソルを当てても時間経過を一時停止しない
        theme="colored"         // テーマ「coloered」を使用する
      />
    </>
  )
}

・・

 

これでReact-Toastifyを利用したアラート表示が可能なので、アラートを表示させたいファイルに「import { toast } from “react-toastify”;」を記述し、「toast.error(“アラートのメッセージ”)」や「toast.success(“アラートのメッセージ”)」を使ってアラートの表示が可能です。

実際にアラートを表示させると、下図のように表示されます。

※アラート表示を実行させる場所によっては?非同期関数(でラップ)にしてから実行しないと予期せぬエラーが発生する場合があるのでご注意下さい。

 

Firebaseでアプリの登録と初期設定

次にFirebaseの各種機能を利用できるようにするため、Firebaseでアプリの登録および初期設定を行なっていきます。

Firebase画面左のメニュー「プロジェクトの概要」にある歯車をクリックしてメニューを開き、「プロジェクトの設定」をクリックします。

 

次にプロジェクトの設定画面の下にあるマイアプリから、ウェブアプリのアイコンをクリックします。

 

次にウェブアプリの登録画面が開くので、アプリのニックネームを入力後、「アプリを登録」をクリックします。

 

これでアプリが登録され、アプリへの認証情報「firebaseConfig」が表示されるのでメモします。

 

次にメモした「firebaseConfig」情報を、環境変数ファイル「.env.local」に設定します。(フロントエンドのサーバーは再起動して下さい。)

 

次に以下のコマンドを実行し、フロントエンド(Next.js)にFirebaseをインストールします。

$ docker compose exec front yarn add firebase

 

次にFirebase初期化用のファイルを作成するため、以下のコマンドを実行してフロントエンド(Next.js)のルートディレクトリにファイル「initFirebase.ts」を作成します。

$ touch initFirebase.ts

 

そしてファイル「initFirebase.ts」の中身は次の通りにします。

/* Firebaseの初期設定ファイル */

import { initializeApp, getApps, getApp } from "firebase/app";
import { getAuth } from "firebase/auth";

// Firebaseの認証情報を設定
const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSEGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

// Firebaseの初期化&Appオブジェクトの作成
const getFirebaseApp = () => {
  if (getApps().length === 0) {
    return initializeApp(firebaseConfig);
  } else {
    return getApp();
  }
};

const app = getFirebaseApp();

// FirebaseAppに関連するAuthインスタンスを取得
export const auth = getAuth(app);

 

ユーザーのログイン状態管理用フックを作成

次に以下のコマンドを実行し、ユーザーのログイン状態を管理するためのフック(状態管理やライフサイクルを扱えるもの)を作成します。(今回の例ではファイル名を「useFirebaseAuth.ts」とします)

$ mkdir hooks
$ cd hooks
$ touch useFirebaseAuth.ts

 

そしてファイル「hooks/useFirebaseAuth.ts」の中身は次の通りにします。

/* FirebaseAuthの状態管理用フック */

import { useState, useEffect } from "react"
import {
  User,
  onAuthStateChanged,
  signInWithEmailAndPassword,
  signOut,
} from "firebase/auth";
import { useRouter } from "next/router"
import { auth } from "../initFirebase"
import { toast } from "react-toastify"

// useFirebaseAuth関数
export default function useFirebaseAuth() {
  const [currentUser, setCurrentUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true);
  const router = useRouter();

  // ログイン関数の作成
  const loginWithEmailAndPassword = async (email: string, password: string) => {

    // ログイン処理を実行
    try {
      const result = await signInWithEmailAndPassword(auth, email, password);

      if (result) {
        const user = result.user;
        router.push("/");
        return user;
      }
    } catch (e) {
      toast.error("ログインできませんでした。");
    } 
  }

  // ログアウト関数の作成
  const clear = () => {
    setCurrentUser(null);
    setLoading(false);
  };

  const logout = () => signOut(auth).then(clear);

  // onAuthStateChanged関数における、
  // ユーザーの状態管理用パラメータの設定
  const nextOrObserver = async (user: User | null) => {
    if (!user) {
      setLoading(false);
      return;
    }

    setLoading(true);
    setCurrentUser(user);
    setLoading(false);
  };

  // useEffectの設定
  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, nextOrObserver);
    return unsubscribe;
  }, []);

  // useFirebaseAuth関数の戻り値
  return {
    currentUser,
    loading,
    loginWithEmailAndPassword,
    logout,
  };
}

※上記ではセッション管理等は考慮しておらず、実際にはさらに拡張する必要があるため、その点はご注意下さい。(ログインできた場合にCookieにセッション情報を保存するなど)

 

ユーザー情報共有用のコンテキストを作成

次に以下のコマンドを実行し、ユーザー情報をシステム全体に共有するためのコンテキスト(Propを使わずにコンポーネント間で情報を受け渡しできるようにするもの)を作成します。(ルートディレクトリに戻ってから実行して下さい。また今回の例ではファイル名を「AuthContext.tsx」とします。)

$ mkdir context
$ cd context
$ touch AuthContext.tsx

 

そしてファイル「context/AuthContext.tsx」の中身は次の通りにします。

/* ユーザー情報共有用のコンテキスト */

import { createContext, useContext } from "react"
import useFirebaseAuth from "../hooks/useFirebaseAuth"
import { User } from "firebase/auth"

// AuthContextのインターフェース定義
interface AuthContext {
  currentUser: User | null;
  loading: boolean;
  loginWithEmailAndPassword: (email: string, password: string) => Promise<User | undefined>;
  logout: () => Promise<void>;
}

// AuthContextProviderのProps型の定義
type AuthProviderProps = {
  children: React.ReactNode;
};

// ユーザー情報共有用のコンテキスト「AuthCtx」を作成
const AuthCtx = createContext({} as AuthContext);

// ユーザー情報共有用のコンポーネント
export function AuthContextProvider({ children }: AuthProviderProps) {
  // FirebaseAuthの状態を取得
  const { currentUser, loading, loginWithEmailAndPassword, logout } = useFirebaseAuth();

  // AuthContextオブジェクトの定義
  const AuthContext: AuthContext = {
    currentUser: currentUser,
    loading: loading,
    loginWithEmailAndPassword: loginWithEmailAndPassword,
    logout: logout,
  };

  return <AuthCtx.Provider value={AuthContext}>{children}</AuthCtx.Provider>;
}

// ユーザー情報共有用の関数
export const useAuthContext = () => useContext(AuthCtx);

 

次に作成したコンテキストをシステム全体に反映させるため、「pages/_app.tsx」を修正します。(戻り値にコンポーネント「<AuthContext></AuthContext>」を追加)

・・

// React-toastifyのインポート
import { ToastContainer } from "react-toastify"
import 'react-toastify/dist/ReactToastify.css';

・・

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <AuthContext>
      <Component {...pageProps} />
      <ToastContainer
        position="top-center"   // 通知の表示位置
        autoClose={5000}        // 設定した時間(ms)経過後に通知をクローズさせる
        hideProgressBar={false} // 通知のProgress Barの非表示設定をOFF
        newestOnTop             // 最新の通知をTOPに表示させる
        closeOnClick            // 通知をクリックで閉じれる
        rtl={false}             // 通知の文字を左寄せにする
        pauseOnFocusLoss        // ウィンドウがフォーカスを失った時に通知の時間経過を一時停止
        draggable={false}       // 通知をドラッグできないようにする
        pauseOnHover            // 通知にカーソルを当てると時間経過を一時停止
        theme="colored"         // テーマ「coloered」を使用する
      />
    </AuthContext>
  )
}

・・

 

尚、getLayout関数でページ単位のレイアウトを適用させるようにしている場合は、「function MyApp」の「return」部分を以下のように記述します。

・・

function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  const getLayout = Component.getLayout ?? ((page) => page)
  return (
    <AuthContext>
      { getLayout(<Component {...pageProps} />) }
      <ToastContainer
        position="top-center"   // 通知の表示位置
        autoClose={5000}        // 設定した時間(ms)経過後に通知をクローズさせる
        hideProgressBar={false} // 通知のProgress Barの非表示設定をOFF
        newestOnTop             // 最新の通知をTOPに表示させる
        closeOnClick            // 通知をクリックで閉じれる
        rtl={false}             // 通知の文字を左寄せにする
        pauseOnFocusLoss        // ウィンドウがフォーカスを失った時に通知の時間経過を一時停止
        draggable={false}       // 通知をドラッグできないようにする
        pauseOnHover={false}    // 通知にカーソルを当てても時間経過を一時停止しない
        theme="colored"         // テーマ「coloered」を使用する
      />
    </AuthContext>
  )
}

・・

 

ログイン用のフォームを作成

今回はメールアドレスとパスワードでログインできるようにするため、次にログイン用のフォームを作成します。(以降では上記のFirebase Authenticationで作成したテスト用のメールアドレスとパスワードを使ってログイン機能を検証する想定です。)

まずは以下のコマンドを実行し、フォーム作成用のライブラリ「React Hook Form」をインストールします。

$ docker compose exec front yarn add react-hook-form

 

次に以下のコマンドを実行し、ログイン用フォームのコンポーネント「LoginForm.tsx」を作成します。

$ cd components
$ touch LoginForm.tsx

 

そしてファイル「components/LoginForm.tsx」の中身は次の通りにします。

/* ログイン用フォーム */

import { useForm, SubmitHandler } from "react-hook-form"
import useFirebaseAuth from "../hooks/useFirebaseAuth"

// ログインフォームの入力項目
interface LoginFormInputs {
  email: string;
  password: string;
}

export default function LoginForm() {
  // ログイン関数
  const { loginWithEmailAndPassword } = useFirebaseAuth();
  const loginFunc = (email: string, password: string) => {
    loginWithEmailAndPassword(email, password);
  }

  // ログインフォームの設定
  const {register, handleSubmit, formState: { errors }} = useForm<LoginFormInputs>();

  // ログインボタンの設定
  const login: SubmitHandler<LoginFormInputs> = (formData) => {
    loginFunc(formData.email, formData.password);
  };

  return (
    <form onSubmit={handleSubmit(login)}
      className="grid grid-cols-1 gap-6 m-16 w-[330px]">
      <label>
        <span>Email</span>
        <input {...register("email", { required: true, maxLength: 319 })}
          type="email"
          className="mt-1 block w-full border-gray border-solid border"/>
        <span className="text-red-600">
          {errors.email &&
          "Email is required and should be less than 319 characters."}
        </span>
      </label>
      <label>
        <span>Password</span>
        <input {...register("password", { required: true, maxLength: 319 })}
          type="password"
          className="mt-1 block w-full border-gray border-solid border"/>
        <span className="text-red-600">
          {errors.password &&
          "Password is required and should be less than 319 characters."}
        </span>
      </label>
      <button type="submit"
        className="bg-slate-200 border-solid border border-slate-300
          rounded-md cursor-pointer hover:bg-slate-300">
        Login
      </button>
    </form>
  );
}

 

ログイン機能とログアウト機能のテスト

これでログイン関連機能を追加するための下準備がある程度整ったので、テスト用のページ「pages/logintest.tsx」を作成し、ログイン機能とログアウト機能を試します。

まずは以下のコマンドを実行し、テスト用のファイル「pages/logintest.tsx」を作成します。

$ cd pages
$ touch logintest.tsx

 

そしてファイル「pages/logintest.tsx」の中身は次の通りにします。

/* ログイン&ログアウト機能のテスト用ページ */

import type { NextPage } from 'next'
import { useAuthContext } from "../context/AuthContext"
import { useEffect } from "react"
import { useRouter } from "next/router"
import LoginForm from "../components/LoginForm"

const Logintest: NextPage = () => {
  // ログイン状況を取得
  const { currentUser, loading, logout } = useAuthContext();
  const router = useRouter();

  return (
    <div className="flex flex-col justify-center items-center">
      <h1>ログイン&ログアウト機能のテスト用ページ</h1>
      <br/>
      <br/>
      { !loading && !currentUser && <h2>未ログイン</h2> }
      { !loading && currentUser && <h2>ログイン済み</h2> }
      <LoginForm />
      { !loading && currentUser &&
      <button onClick={logout}
        className="w-[330px] bg-slate-200 border-solid border border-slate-300
          rounded-md cursor-pointer hover:bg-slate-300">
        Log out
      </button> }
    </div>
  )
}

export default Logintest

 

これで準備が整ったので、フロントエンド(Next.js)のサーバーを起動後、ブラウザから「http://localhost:3000/logintest」にアクセスし、下図のような画面が表示されればOKです。

 

まずは各項目は未入力の状態でボタン「Login」をクリックすると、設定したエラーメッセージが表示されます。

 

次にFirebaseに登録してないメールアドレスやパスワードを入力してボタン「Login」をクリックすると、ログインできないため下図のようなアラートメッセージが表示されます。

 

次に上記でFirebaseに登録済みのテスト用メールアドレスとパスワードを入力し、ボタン「Login」をクリックします。

 

ログインが成功すると「http://localhost:3000」にリダイレクトする設定にしているため、画面が切り替わります。

 

次にもう一度「http://localhost:3000/logintest」にアクセスすると、画面上の表示が「ログイン済み」に変わり、画面下にログアウトボタン「Log out」が表示されるので、ログアウトボタンをクリックします。

 

ログアウトボタンをクリック後、初期状態の画面に戻ればOKです。

 

尚、ログイン済みの場合に別のページにリダイレクトしたい場合などは、以下のような「useEffect関数」を使うと制御できます。

import { useEffect } from "react"
import { useRouter } from "next/router"
import { useAuthContext } from "../context/AuthContext"

// ログイン状況を取得
const { currentUser, loading } = useAuthContext();
const router = useRouter();

// ログイン済みの場合は別のページに飛ばす
useEffect(() => {
  if (!loading && currentUser) {
    router.push("/")
  }
// 以下のコメント直下のコードのeslintルールを部分的に無効にする
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentUser, loading])

※そのままだとuseEffect内のrouterが依存関係に無い([currentUser, loading]に無い)という警告が出るが、依存関係には含めたくないため、コメント「eslint-disable-next-line react-hooks/exhaustive-deps」を記述してeslintルールを部分的に無効にしている。

 

これでログイン&ログアウト機能の簡単な動作テストは完了したので、作成したテスト用ファイル「pages/logintest.tsx」は削除して下さい。

 

Firebase導入に伴うフロントエンドにおけるJestの設定変更について

Firebaseを導入した場合、フロントエンドのテストフレームワーク「Jest」でnode_modulesのtransformも必要になるようですが、デフォルト設定ではできないようになっており、そのままだとテストがエラーになります。

そこで、「jest.config.js」の「module.exports」の設定を以下のように修正するとテストが通るようになります。

・・

module.exports = async () => ({
  ...(await createJestConfig(customJestConfig)()),
  transformIgnorePatterns: ['node_modules/(?!(firebase|@firebase))'],
});

・・

 

Firebase導入に伴うフロントエンドにおけるCircleCI&環境変数の設定について

Firebaseの導入に伴って、フロントエンドで環境変数を使うようになったため、コードをGitHubにプッシュする前にCircleCI用の「.circleci/config.yml」の修正および、CircleCIとVercelに環境変数のセットアップが必要です。

.env.localに追加した環境変数について、CircleCIとVercelのプロジェクト設定に追加し、ファイル「.circleci/config.yml」は以下のように修正して下さい。

# Use the latest 2.1 version of CircleCI pipeline process engine.
# See: https://circleci.com/docs/2.0/configuration-reference
version: 2.1

# Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/2.0/configuration-reference/#jobs
jobs:
  # テスト用のジョブ
  test:
    # docker compose を利用するため、仮想マシンを利用する。(最新版のimageは下記URLを参照)
    # See: https://circleci.com/docs/ja/configuration-reference#available-linux-machine-images
    machine:
      image: ubuntu-2204:current
    # 仮想マシンを利用する場合、CircleCIの環境変数はDockerのコンテナ内へは直接読み込めない。
    # そのまま代入もできないため、一度parametersを設定する。
    parameters:
      next_public_base_url:
        type: string
        default: $NEXT_PUBLIC_BASE_URL
      next_public_firebase_api_key:
        type: string
        default: $NEXT_PUBLIC_FIREBASE_API_KEY
      next_public_firebase_auth_domain:
        type: string
        default: $NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
      next_public_firebase_project_id:
        type: string
        default: $NEXT_PUBLIC_FIREBASE_PROJECT_ID
      next_public_firebase_storage_bucket:
        type: string
        default: $NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET
      next_public_firebase_messeging_sender_id:
        type: string
        default: $NEXT_PUBLIC_FIREBASE_MESSEGING_SENDER_ID
      next_public_firebase_app_id:
        type: string
        default: $NEXT_PUBLIC_FIREBASE_APP_ID
    # 作業用のディレクトリを設定
    working_directory: ~/test_jest
    # タスクを定義
    steps:
      # リポジトリを作業用のディレクトリにpull
      - checkout
      # .env.localは公開していないため、空ファイルを作成する
      - run:
          name: .env.localの空ファイルを作成
          command: touch .env.local
      # parametersに設定したCircleCIの環境変数を.env.localに書き込んでDockerコンテナ内で読み込む
      - run:
          name: .env.localに環境変数NEXT_PUBLIC_BASE_URLを設定
          command: echo NEXT_PUBLIC_BASE_URL=<< parameters.next_public_base_url >> >> .env.local
      - run:
          name: .env.localに環境変数NEXT_PUBLIC_FIREBASE_API_KEYを設定
          command: echo NEXT_PUBLIC_FIREBASE_API_KEY=<< parameters.next_public_firebase_api_key >> >> .env.local
      - run:
          name: .env.localに環境変数NEXT_PUBLIC_FIREBASE_AUTH_DOMAINを設定
          command: echo NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=<< parameters.next_public_firebase_auth_domain >> >> .env.local
      - run:
          name: .env.localに環境変数NEXT_PUBLIC_FIREBASE_PROJECT_IDを設定
          command: echo NEXT_PUBLIC_FIREBASE_PROJECT_ID=<< parameters.next_public_firebase_project_id >> >> .env.local
      - run:
          name: .env.localに環境変数NEXT_PUBLIC_FIREBASE_STORAGE_BUCKETを設定
          command: echo NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=<< parameters.next_public_firebase_storage_bucket >> >> .env.local
      - run:
          name: .env.localに環境変数NEXT_PUBLIC_FIREBASE_MESSEGING_SENDER_IDを設定
          command: echo NEXT_PUBLIC_FIREBASE_MESSEGING_SENDER_ID=<< parameters.next_public_firebase_messeging_sender_id >> >> .env.local
      - run:
          name: .env.localに環境変数NEXT_PUBLIC_FIREBASE_APP_IDを設定
          command: echo NEXT_PUBLIC_FIREBASE_APP_ID=<< parameters.next_public_firebase_app_id >> >> .env.local
      # 処理を実行
      # yarnをインストール
      - run:
        name: yarn.lockからyarnを再インストール
        command: docker compose run --rm front yarn install --frozen-lockfile
      # テストの実行
      - run:
          name: ESLintの実行
          command: docker compose run --rm front yarn lint
      - run:
          name: Jestの実行
          command: docker compose run --rm front yarn test

  # デプロイ用のジョブ
  deploy:
    # docker compose を利用するため、仮想マシンを利用する。(最新版のimageは下記URLを参照)
    # See: https://circleci.com/docs/ja/configuration-reference#available-linux-machine-images
    machine:
      image: ubuntu-2204:current
    # 仮想マシンを利用する場合、CircleCIの環境変数はDockerのコンテナ内へは直接読み込めない。
    # そのまま代入もできないため、一度parametersを設定する。
    parameters:
      next_public_base_url:
        type: string
        default: $NEXT_PUBLIC_BASE_URL
      next_public_firebase_api_key:
        type: string
        default: $NEXT_PUBLIC_FIREBASE_API_KEY
      next_public_firebase_auth_domain:
        type: string
        default: $NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
      next_public_firebase_project_id:
        type: string
        default: $NEXT_PUBLIC_FIREBASE_PROJECT_ID
      next_public_firebase_storage_bucket:
        type: string
        default: $NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET
      next_public_firebase_messeging_sender_id:
        type: string
        default: $NEXT_PUBLIC_FIREBASE_MESSEGING_SENDER_ID
      next_public_firebase_app_id:
        type: string
        default: $NEXT_PUBLIC_FIREBASE_APP_ID
    # 作業用のディレクトリを設定(ディレクトリ名はVercelのプロジェクト名と合わせる)
    working_directory: ~/front-app
    # タスクを定義
    steps:
      # リポジトリを作業用のディレクトリにpull
      - checkout
      # .env.localは公開していないため、空ファイルを作成する
      - run:
          name: .env.localの空ファイルを作成
          command: touch .env.local
      # parametersに設定したCircleCIの環境変数を.env.localに書き込んでDockerコンテナ内で読み込む
      - run:
          name: NEXT_PUBLIC_BASE_URL
          command: echo NEXT_PUBLIC_BASE_URL=<< parameters.next_public_base_url >> >> .env.local
      - run:
          name: NEXT_PUBLIC_FIREBASE_API_KEY
          command: echo NEXT_PUBLIC_FIREBASE_API_KEY=<< parameters.next_public_firebase_api_key >> >> .env.local
      - run:
          name: NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
          command: echo NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=<< parameters.next_public_firebase_auth_domain >> >> .env.local
      - run:
          name: NEXT_PUBLIC_FIREBASE_PROJECT_ID
          command: echo NEXT_PUBLIC_FIREBASE_PROJECT_ID=<< parameters.next_public_firebase_project_id >> >> .env.local
      - run:
          name: NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET
          command: echo NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=<< parameters.next_public_firebase_storage_bucket >> >> .env.local
      - run:
          name: NEXT_PUBLIC_FIREBASE_MESSEGING_SENDER_ID
          command: echo NEXT_PUBLIC_FIREBASE_MESSEGING_SENDER_ID=<< parameters.next_public_firebase_messeging_sender_id >> >> .env.local
      - run:
          name: NEXT_PUBLIC_FIREBASE_APP_IDを設定
          command: echo NEXT_PUBLIC_FIREBASE_APP_ID=<< parameters.next_public_firebase_app_id >> >> .env.local 
      # 処理を実行
      # yarnをインストール
      - run:
          name: yarn.lockからyarnを再インストール
          command: docker compose run --rm front yarn install --frozen-lockfile
      # vercelへのデプロイ処理
      - run:
          name: yarn deploy
          command: yarn deploy

# Invoke jobs via workflows
# See: https://circleci.com/docs/2.0/configuration-reference/#workflows
workflows:
  test-and-deploy-wf:
    jobs:
      - test
      - deploy:
          requires:
            - test
          filters:
            branches:
              only: main

※デプロイ用のジョブにある「working_directory: 」の設定については、Vercelのプロジェクト名に合わせてください。(Vercelのプロジェクト名が「front-app」なら、「~/front-app」を設定する

 

この後について

上記までである程度ログイン関連機能の実装ができたと思いますが、この後はログイン後にセッションを管理できるようにする機能(CookieにidTokenを保存するなど)の追加や、Firebaseおよびバックエンド(今回はRails API)にユーザー情報を登録するサインアップ機能(Firebaseにユーザー登録→idToken付きのリクエスト(Axiosを使う)をAPIに投げてバックエンド側でユーザー登録など)の追加が必要になってきます。

ここまででも貴重な情報がまとめられたので一旦公開しておきますが、続きの部分についてもまた情報がまとまったら追記できればなと思っています。

 



最後に

今回はSPA構成のWebアプリケーションを開発する方法についてまとめました。

あとはフロントエンド(画面)やバックエンド(API)について、それぞれ体的な中身の設計および実装する経験をどんどん増やしていけば、Web系エンジニアにまた一歩近づくと思います。

ただこの記事を一通り目を通していただくとわかりますが、やはりフロントエンドとバックエンドはそれぞれ専門性があり、両方を極めようとするのは非常に難しいため、まずはどちらか一つを選択して専門性を磨いていくのが大事になりそうです。

そのほか、たまに「フルスタックエンジニア募集!」のような求人もあったりしますが、この記事でやったように一通りやってみると、フルスタックエンジニアなんて幻想だというのがよく理解できるので、これから仕事を探すような方は、上手い言葉には騙されないようにご注意下さい!

 

各種SNSなど

各種SNSなど、チャンネル登録やフォローをしていただけると励みになるので、よければぜひお願いします!

 

The following two tabs change content below.

Tomoyuki

SEを5年経験後、全くの未経験ながら思い切ってブロガーに転身し、月間13万PVを達成。その後コロナの影響も受け、以前から興味があったWeb系エンジニアへのキャリアチェンジを決意。現在はWeb系エンジニアとして働きながら、プロゲーマーとしても活躍できるように活動中。








シェアはこちらから


おすすめのゲーミングPCまとめ

近年、eスポーツという言葉が浸透し、ゲーミングPCの需要も増えましたが、「ゲーミングPCはどう選べばいいの?」という人も多いのではないでしょうか。そんなPC初心者でも迷わず選べるよう、おすすめのゲーミングPCをご紹介します!


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です