【技術ブログ】 弊社で運用している生活記録アプリ
みなさん、こんにちは。今回は技術的な記事になります。
筆者はアプリ開発チームに所属する利用者です。
弊事業所 STOVE において開発、社内運用されている生活記録アプリの技術スタックについて、開発者視点でつづらせていただきます。
以下の記事で、簡易的な技術スタックについては触れられています。
この記事では、より深いところに触れていきたいと思います。
対象読者
- プログラミングの知識がある
- コンテナ技術、主に Docker の知識がある
- フレームワークやライブラリなどに関する知識がある
生活記録アプリのディレクトリ構成
ディレクトリ構成は以下のようになっています。(一部省略しています)
.
├── .env
├── Makefile
├── README.md
├── backend
│ ├── controllers
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ ├── models
│ ├── serviceAccountKey.json
│ └── tmp
├── docker
│ ├── go
│ ├── node
│ └── sql
├── docker-compose.yaml
├── documents
│ ├── README.md
│ ├── development
│ ├── operation
│ └── scrum
├── firebase.json
└── frontend
├── .env
├── index.html
├── src
└── vite.config.ts
バックエンド、フロントエンドを分けつつ、一つのリポジトリで管理する形です。Docker などの開発環境、ドキュメントなどの運用に関する内容なども一元管理です。
docker ディレクトリでは、それぞれの言語の Dockerfile を配置し、コンテナの設定を記述しています。
設計パターンに関しては、MVC モデルを採用しています。 バックエンドで Model と Controller の役割を持ち、フロントエンドに View の役割を持たせています。
技術スタック
フロントエンドは Vite + React + TypeScript
STOVE の生活記録アプリのフロントエンドでは、フレームワークは React を活用しています。ビルドツールは Vite です。
React は Meta 社を中心としたコミュニティによって開発されているフレームワークです。
Vite は、Webpack よりも高速に動作するフロントエンドツールです。
このアプリの企画立案をした私の中では、プレーンな React ではなく Next.js も視野にありました。 しかし、React を扱ったことのない利用者もチームメンバーとして参画することが決まっていたため、Next.js は採用せず、Vite でプレーンな React を動作させることにしました。
ルートディレクトリとフロントエンドディレクトリに、個別に .env ファイルがあります。
この理由としては、立案当時は React を動作させる際は、Docker コンテナを使用せず、各々の PC に nvm という Node.js のバージョン管理システムをインストールし、バージョンを開発メンバー間で合わせながら開発を開始していたからです。
どちらもコンテナであれば、docker-compose.yaml を通して、.env ファイルの内容をコンテナの環境変数として持っていくことができるので、このような面倒な扱い方はする必要はないです。
また、React のコードを書くときは TypeScript を活用しています。これは言うまでもなく、エディターの時点で静的チェックができるからです。一般的な JavaScript では、型による単純バグの可能性が排除できません。
データベース上の情報を画面に表示する際は、RTK Query を使ってバックエンドの API から JSON を受け取り、画面上にレンダリングして表示しています。
バックエンドは Go 言語
STOVE の生活記録アプリのバックエンドでは、Go 言語を用いています。
Go 言語は、Google によって開発されているプログラミング言語です。
Echo や Gin のようなフレームワークは使用いていません。mux というライブラリでルーティングとミドルウェア設定をしています。ホットリロードには air というツールを使っています。
バックエンドはデータベースに保管された情報を、ルーティングされた API URL に JSON としてレスポンスする役割を担っています。
ORM は gorp です。この ORM を使うことで、構造体をそのままデータベースのテーブルのスキーマとして扱えたり、構造体を ORM の関数に引数として渡すことで、情報を取得・挿入したりできます。
有名どころの gorm を使用しなかったのは、ORM への依存度が高くなるのを避けたかったためです。
しかし、結果論でいえば、情報の少ないライブラリを選択したのはあまりよくなかったなと感じているところもあります。gorp について検索しても、情報量が少なかったです。
依存度を落とすといっても、大して効果を得られた感触はなく、調査に時間がかかってばかりであまりよいリターンが得られませんでした。
インフラ
インフラは Google Cloud においています。
フロントエンドのホストは Firebase Hosting
ホスティングには Firebase Hosting を使っています。
Firebase は 2014年頃に Google に買収されており、現在は Google Cloud の IAM の設定をそのまま適用して権限の管理ができます。
私の中では AWS の CloudFront + Amazon S3 などの選定も候補にもありましたが、AWS アカウントやその IAM ユーザーを作るよりも、Google アカウントを作成・招待してもらうほうが経営陣などとの連携が取りやすいと判断したため、こちらを採用しました。
バックエンドは Cloud Run で運用
バックエンドは Google Cloud Run にデプロイして運用しています。
Cloud Run は、コンテナをそのままデプロイできるサービスです。そのため、開発した環境をそのままバイナリにして、配置することができました。
また、リクエストを処理している最中のみしかコンテナが起動しないため、料金が抑えられるのがメリットです。
この生活記録アプリでは、入力された内容が提出されるのは朝と夕のみであり、夜間や早朝には使われません。
この朝と夕のタイミングに多くのコンテナが起動し、それ以外の時間帯は静まり返っているという特性上、Cloud Run はとてもマッチしていました。
ログイン認証は Firebase Authentication
ログインの仕組みには Firebase Authentication を使っています。
データベースにメールアドレスやパスワードなどの情報を持ちたくなかったので、データベースには Firebase ユーザーの UID だけを保持し、Firebase との通信で得られる idToken の UID で用いてユーザー認証しています。
データベースは Cloud SQL
データベースは Google Cloud SQL for MySQL を使用しています。
開発開始、運用開始当初は、PlanetScale というサーバーレスデータベースを使っていたのですが、2024年4月から料金の値上げがおこなわれ、慌てて Cloud SQL に移したという経緯があります。
データベースのコピーなどの作業は非常に繊細な作業です。運用を重ねていけばいずれ避けられないとは考えていましたが、まさか運用開始から数ヶ月という期間でこの対応をしなければならないとは思いませんでした。
また、このアプリは日中しか使われません。そのため、より節約できるように、夜間や早朝などの時間帯は、Cloud Scheduler + Cloud Functions を使って、インスタンスを停止しています。
おわりに
以上、開発者視点の記事でした。
このアプリは、利用者が提出し、スタッフのみが閲覧するという仕組み上、利用者のみで構成されているの開発チームからは「スタッフとしての使いづらさ」を発見することが難しいという欠点があります。
一方で、私自身が毎日スタッフへ提出することに使っているアプリのため、利用者目線ではユーザー体験のよいものなっていると考えています。