mybook

Kata: ブログプラットフォーム — モノリスで始める

課題の提示

ナオミが最初の課題をメッセージで送ってきた。


Kata 1: ブログプラットフォーム

クライアントから依頼が来た。社内向けの技術ブログプラットフォームを作ってほしい。要件は以下の通り。

  • エンジニア50名が投稿できる
  • 記事の作成・編集・削除・閲覧ができる
  • タグ付け機能がある
  • コメント機能がある
  • 月間PVは約10,000
  • 予算は少ない。エンジニア1人で3ヶ月で作ること

タクミは要件を読んで、最初に考えた。「マイクロサービスにしようか?Reactのフロントエンドを別に作るか?」

ナオミから電話が来た。「タクミさん、まず何を判断する?」

「アーキテクチャを...」

「違う。まず制約を整理する。予算・人員・期間・規模。それが判断の基準になる」


設計判断

制約の整理

項目内容
ユーザー数50名(内部ユーザーのみ)
トラフィック月間10,000PV ≈ 約14リクエスト/時間
開発体制エンジニア1名
期間3ヶ月
予算小(サーバーコスト最小化)

INFO

月間10,000PVは1日約333PV、1時間14リクエスト程度。EC2の最小インスタンスで十分に捌けるスケール。

アーキテクチャの選択肢

Loading diagram...

判断: モノリスを選ぶ。理由は明確だ。

  1. ユーザーが50名で内部向け — 高スケーラビリティ不要
  2. エンジニア1名 — 複数サービスの運用は困難
  3. 3ヶ月 — 早く作れるアーキテクチャが正解

ナオミは言った。「良い判断よ。アーキテクチャは"今何が必要か"で決める。"将来何が必要かもしれないか"で決めてはいけない」


実装

データモデル設計

Loading diagram...

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
end

AWSインフラ構成

モノリスに適したシンプルな AWS 構成を選ぶ。

Loading diagram...

インフラの判断

サービス選択理由
サーバーEC2 t3.small月$15程度、50ユーザーで十分
DBRDS t3.microマネージドで運用コスト低
CDNCloudFront静的ファイルのキャッシュ、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
end

INFO

Kata 1 の学び: アーキテクチャは「今の制約」で決める。モノリスは悪いものではない。複雑さを導入するのは、複雑さが必要になったときだけ。

トレードオフの記録

決定メリットデメリット
モノリス開発速度、運用シンプルスケール限界、分割コスト
Rails scaffold速く動く後でリファクタ必要
EC2単台安いSPOFリスク

「次の Kata は、この判断が変わる瞬間よ」とナオミは言った。「ECサイト。スケールと整合性の要件が変わると、アーキテクチャはどう変わるか見てみましょう」