Kata: ブログプラットフォーム — モノリスで始める
課題の提示
ナオミが最初の課題をメッセージで送ってきた。
Kata 1: ブログプラットフォーム
クライアントから依頼が来た。社内向けの技術ブログプラットフォームを作ってほしい。要件は以下の通り。
- エンジニア50名が投稿できる
- 記事の作成・編集・削除・閲覧ができる
- タグ付け機能がある
- コメント機能がある
- 月間PVは約10,000
- 予算は少ない。エンジニア1人で3ヶ月で作ること
タクミは要件を読んで、最初に考えた。「マイクロサービスにしようか?Reactのフロントエンドを別に作るか?」
ナオミから電話が来た。「タクミさん、まず何を判断する?」
「アーキテクチャを...」
「違う。まず制約を整理する。予算・人員・期間・規模。それが判断の基準になる」
設計判断
制約の整理
| 項目 | 内容 |
|---|---|
| ユーザー数 | 50名(内部ユーザーのみ) |
| トラフィック | 月間10,000PV ≈ 約14リクエスト/時間 |
| 開発体制 | エンジニア1名 |
| 期間 | 3ヶ月 |
| 予算 | 小(サーバーコスト最小化) |
INFO
月間10,000PVは1日約333PV、1時間14リクエスト程度。EC2の最小インスタンスで十分に捌けるスケール。
アーキテクチャの選択肢
判断: モノリスを選ぶ。理由は明確だ。
- ユーザーが50名で内部向け — 高スケーラビリティ不要
- エンジニア1名 — 複数サービスの運用は困難
- 3ヶ月 — 早く作れるアーキテクチャが正解
ナオミは言った。「良い判断よ。アーキテクチャは"今何が必要か"で決める。"将来何が必要かもしれないか"で決めてはいけない」
実装
データモデル設計
Rails scaffold で始める
rails new blog_platform --database=postgresql
cd blog_platform
# 基本モデルの生成
rails g scaffold Post title:string body:text user:references published:boolean
rails g scaffold Comment body:text post:references user:references
rails g model Tag name:string:uniq
rails g model Tagging post:references tag:references
rails db:migrateモデルの関連設定
# app/models/user.rb
class User < ApplicationRecord
has_many :posts, dependent: :destroy
has_many :comments, dependent: :destroy
validates :email, presence: true, uniqueness: true
validates :name, presence: true
end
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
validates :title, presence: true, length: { maximum: 200 }
validates :body, presence: true
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc) }
def tag_names=(names)
self.tags = names.map { |name| Tag.find_or_create_by(name: name.strip) }
end
end
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :post
belongs_to :user
validates :body, presence: true, length: { maximum: 1000 }
endコントローラーの実装
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :authenticate_user!
before_action :set_post, only: [:show, :edit, :update, :destroy]
before_action :authorize_post!, only: [:edit, :update, :destroy]
def index
@posts = Post.published.recent.includes(:user, :tags).page(params[:page])
end
def show
@comments = @post.comments.includes(:user).order(created_at: :asc)
@comment = Comment.new
end
def new
@post = current_user.posts.build
end
def create
@post = current_user.posts.build(post_params)
if @post.save
redirect_to @post, notice: "記事を投稿しました"
else
render :new, status: :unprocessable_entity
end
end
def update
if @post.update(post_params)
redirect_to @post, notice: "記事を更新しました"
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@post.destroy
redirect_to posts_path, notice: "記事を削除しました"
end
private
def set_post
@post = Post.find(params[:id])
end
def authorize_post!
redirect_to posts_path, alert: "権限がありません" unless @post.user == current_user
end
def post_params
params.require(:post).permit(:title, :body, :published, tag_names: [])
end
endAWSインフラ構成
モノリスに適したシンプルな AWS 構成を選ぶ。
インフラの判断
| サービス | 選択 | 理由 |
|---|---|---|
| サーバー | EC2 t3.small | 月$15程度、50ユーザーで十分 |
| DB | RDS t3.micro | マネージドで運用コスト低 |
| CDN | CloudFront | 静的ファイルのキャッシュ、SSL終端 |
| ストレージ | S3 | 画像などのアセット保存 |
WARNING
t3.micro の RDS は開発環境向け。本番では t3.small 以上を推奨。月間10,000PVなら t3.small で十分だが、バックアップとMulti-AZは有効にすること。
デプロイ設定
# config/deploy.rb (Capistrano を使う場合)
set :application, "blog_platform"
set :repo_url, "git@github.com:company/blog_platform.git"
set :deploy_to, "/var/www/blog_platform"
set :rbenv_ruby, "3.3.0"
# 環境変数
set :linked_files, %w[config/database.yml config/master.key]
set :linked_dirs, %w[log tmp/pids tmp/cache tmp/sockets public/uploads]
# デプロイフック
after "deploy:publishing", "deploy:restart"# config/database.yml (本番環境)
production:
adapter: postgresql
host: <%= ENV['DB_HOST'] %>
database: <%= ENV['DB_NAME'] %>
username: <%= ENV['DB_USERNAME'] %>
password: <%= ENV['DB_PASSWORD'] %>
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>パフォーマンス改善
モノリスでもパフォーマンスの基本は押さえる。
N+1問題の解消
# BAD: N+1問題
@posts = Post.published.recent
# ビューで @post.user.name を呼ぶたびにSQLが発行される
# GOOD: eager loading
@posts = Post.published.recent.includes(:user, :tags)インデックスの追加
# db/migrate/xxx_add_indexes_to_posts.rb
class AddIndexesToPosts < ActiveRecord::Migration[8.0]
def change
add_index :posts, [:published, :created_at]
add_index :posts, :user_id
add_index :taggings, [:post_id, :tag_id], unique: true
end
endキャッシュ設定
# config/environments/production.rb
config.cache_store = :mem_cache_store, ENV["MEMCACHE_SERVERS"],
{ namespace: "blog_v1", expires_in: 1.hour }
# app/controllers/posts_controller.rb
def index
@posts = Rails.cache.fetch("posts_index_page_#{params[:page]}", expires_in: 5.minutes) do
Post.published.recent.includes(:user, :tags).page(params[:page]).to_a
end
end振り返り
タクミは実装を終えて、ナオミに報告した。
「3週間でデプロイできました。モノリスで正解でした」
ナオミは言った。「今日の選択は正しかった。でも、1年後に50名が500名になったら?」
「...分割が必要になりますね」
「そう。だから今のうちに境界を意識した設計をしておくこと。PostsControllerに全ての処理を詰め込まない。サービスオブジェクトで関心を分離する」
# 将来の分割に備えた設計
# app/services/post_publisher.rb
class PostPublisher
def initialize(post, user)
@post = post
@user = user
end
def publish!
raise "権限がありません" unless can_publish?
ActiveRecord::Base.transaction do
@post.update!(published: true, published_at: Time.current)
NotificationService.notify_followers(@post)
end
end
private
def can_publish?
@post.user == @user || @user.admin?
end
endINFO
Kata 1 の学び: アーキテクチャは「今の制約」で決める。モノリスは悪いものではない。複雑さを導入するのは、複雑さが必要になったときだけ。
トレードオフの記録
| 決定 | メリット | デメリット |
|---|---|---|
| モノリス | 開発速度、運用シンプル | スケール限界、分割コスト |
| Rails scaffold | 速く動く | 後でリファクタ必要 |
| EC2単台 | 安い | SPOFリスク |
「次の Kata は、この判断が変わる瞬間よ」とナオミは言った。「ECサイト。スケールと整合性の要件が変わると、アーキテクチャはどう変わるか見てみましょう」