ume

railsをdocker化させる

作成する環境

  • ruby 3.2.2
  • Rails:7.0.6
  • Postgres:version12系

全体像

  1. Dockerfile作成

  2. docker-compose.ymlファイル作成

  3. docker-compose up でコンテナ作成

  4. データベース作成(初めて起動する場合)

  5. マイグレーションを適用する(初回)

  6. host:3000にアクセスする。

Dockerfile作成

⇨Dockerfileとはコンテナの中にどんなプログラミング言語やOSの環境を作るかを定義するファイル.
今回はRubyRailsの環境をコンテナ内に作成します。

FROM ruby:3.2.2
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
WORKDIR /myapp
COPY Gemfile Gemfile.lock /myapp
RUN bundle install

FROM.
プログラミング言語やOSを記載.
今回はrubyの3.2.2というバージョンをコンテナ内で使ってねという意味

RUN.
linuxコマンドでライブラリーなどをインストールするコマンド.

WORKDIR.
⇨コンテナ内で作業するディレクトリを指定している。コンテナ内に指定のディレクトリがない場合は作成してくれる。またその作成したディレクトリまで移動してくれる。

COPY.
左<ホストマシン>右<コンテナ内>という構文です.
ホストマシンのGemfileをコンテナ内の/myapp/Gemfileという場所にコピーするという意味

COPY Gemfile /myapp/Gemfile

2つ目のRUN.
⇨RUNコマンドが使われているのでbundle installしてという意味 bundle installはGemfileの中身をインストールしてという意味。

docker-compose.ymlファイル作成

docker-compose.ymlとは複数のコンテナを一括管理するためのファイル。 ↓railsとpostgresを一括管理

version: '3.8'

services:
  db:
    image: postgres:12.18-alpine3.18
    restart: always
    environment:
      POSTGRES_PASSWORD: password
    volumes:
      - "db-data:/var/lib/postgresql/data"

  web:
    build: .
    command:
      bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
    db-data:

version.
⇨新しいものを使いたかったので3.8を使いました。他にもバージョンはありますので下記参照して下さい

docs.docker.jp

services.
⇨ 起動するコンテナの設定の定義をするのがservices.
今回で言うと「db」、「web」と言うコンテナを作成するという意味。このコンテナ名の名前は任意につけることができる。

db

image.
⇨コンテナ内にどんな環境を作成するか.
今回はpostgres:12.18-alpine3.18という環境をdockerhubから取得したイメージから作成する。

enviroment.
⇨データベースを使用するときにuser名とパスワードが求められるのでその設定をする。postgresではuser名を記載しないとデフォルトでpostgresを指定していることになる。

[db]のvolumes.
⇨コンテナ内のデータをホストマシンに共有するためのもの。要はコンテナが破棄されデータが消えてもホストマシンにデータを残すための設定。

構文.

ホストマシンの名前付きボリューム:コンテナ内のデータが保存されているパス

名前付きボリュームとは?
⇨コンテナ内のデータを保存するための空間に名前をつけたのもの。今回はホストマシンの中のdb-dataという名前の空間にコンテナのデータを保存する。

docker-compose.ymlファイルの最後に記載されている以下でホストマシンの中に db-data:という名前付きボリュームを作成している。

volumes:
    db-data:
web

build.
⇨dockerfileまでのパスを指定し、そのdockerfileをbuildしイメージを作成。

command.
⇨コンテナが作成されたときに実行されるコマンド。サーバーを立ち上げています。

「web」volumes.
⇨ホストマシンとコンテナ内を同期してます。この設定のおかげでホストマシンの変更がリアルタイムでコンテナ内に反映される(逆も然り)。

ports.
⇨ローカルとコンテナ内のポートを作成

depends_on:.
⇨コンテナ間の依存関係を制御するオプション

データベース作成(初めて起動する場合)

コンテナ起動

docker compose up

データベース作成

docker-compose run web rake db:create

マイグレーションを適用

docker-compose run web rails db:migrate

 6. host:3000にアクセスする。

http://localhost:3000

画面に「Task」という文字が表示されたら完成。

RubyとRailsのdockerfileの書き方

前書き

railsのコンテナを作成しようとdockerfileを作成する機会がありました.
ただ下記のような疑問がありました

  • dockerfileにどんなコマンドをどんな順番で記載すべきなの?

上記のdockerfileはどうやって疑問を解決するために記事に残します

dockerfile全体像

dockerfile

①ベースイメージ設定. 
②パッケージをインストール
③作業用ディレクトリ作成/移動
④gemfileとgemfile.lockコピー
⑤bundle install 
⑥アプリケーションコピー

dockerfileの書式

書式
命令 引数

命令は文字と小文字を区別しません。ただし、引数と区別をつけやすくするため、慣例として大文字を使います。

命令一覧
命令 説明
FROM ベースとなるDockerイメージを指定します。
MAINTAINER イメージの作成者やメンテナを指定します。
RUN コマンドを実行し、新しいイメージレイヤーを作成します。
CMD コンテナが起動された際に実行されるデフォルトのコマンドを指定します。
LABEL イメージにメタデータを追加します。
EXPOSE コンテナが使用するポートを外部に公開します。
ENV 環境変数を設定します。
ADD ファイルやディレクトリをイメージに追加します。
COPY ローカルファイルやディレクトリをイメージにコピーします。
ENTRYPOINT コンテナが実行される際に実行されるコマンドを指定します。
VOLUME マウントポイントとして使用されるディレクトリを指定します。

dockerfileの書き方

ベースイメージ設定

⇨FROMコマンドを使う dockerfile

FROM <イメージ名>[:<タグ>] [AS <任意の名前>]

まずはFROMを最初に記載する.
[AS <任意の名前>]このオプションを付けることでベストプラクティスを満たしながらベースイメージを作成することができます.
[AS <任意の名前>]これは要はイメージに含まれる不要なライブラリーを排除するために使われるオプションです。 このオプションを使用すると後で以下を実行する必要があるみたいです

COPY --from=<名前>

↑僕の場合<名前>=build

今回Rubyの3.2.2と指定する

FROM ruby:3.2.2 AS build

パッケージをインストール

⇨RUNコマンドを使う

RUN apt-get update -qq && apt-get install -y 

作業用ディレクトリ作成/移動

⇨WORKDIRコマンドを使う

WORKDIR /app

上記appで作業するよってこと。もしappディレクトリがない場合は作成もしてくれる。

gemfileとgemfile.lockコピー

COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock

ホストマシンのGemfileとGemfile.lockファイルをapp/Gemfileにコピーする

bundle install

RUN bundle install

gemfileに記載されたものをあれこれインストール

アプリケーションコピー

COPY . /app

完成図

FROM ruby:3.2.2 AS build
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
WORKDIR /app
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN bundle install
COPY . /app

豆知識

dockerfileでイメージを作成する際は.dockerignoreファイルも作成する.

.dockerignoreとは.

⇨イメージに不要なファイルやディレクトリを指定することでビルド時間を短縮する。要はDockerイメージのビルドパフォーマンスを向上させる

.dockerignoreに含めるディレクトリやファイル例
/.bundle/
/.dockerignore
/.git/
/.git*
/.ruby-version
/README.md
/config/master.key
/log/
/node_modules/
/storage/
/tmp/
/vendor/

Ruby safe navigation operator(ぼっち演算子)

ぼっち演算子とは?

⇨レシーバーがnilだった場合でもメソッドをエラーにさせない仕組み

class User 

  def name 
    puts "まさです"
  end

  

end

user = User.new 
user.name

object = nil
 object.name
出力結果
a.rb:15:in `<main>': undefined method `name' for nil:NilClass (NoMethodError)

 object&.name
上記の出力結果エラーにならずnilを戻り値として返す。

どういう用途で使う?

⇨ログインしているユーザーとログインしていないユーザーでページを出し分ける際

@nickname = current_user.nickname

このような時です。このcurrent_userはログインしていない時はnilになってしまうのでログインしていないときにこのメソッドが実行されてしまうとエラーになってしまうのですがここでボッチ演算子を使うことでこのnilのcurrent_userをnilという表記で出力することができます。nilで返すことでエラーが消え、current_userに何も入っていない時でもページを表示することができます。

@nickname = current_user&.nickname

書き方はこの通りです。メソッドの前に&をつけるだけです。こうすることでcurrent_userはnilを返すことができるようになります。

少しわかりづらいかもしれないですが、オブジェクトがnilだった場合にnilに対応していないメソッド(NilClassに定義されていないメソッド)を呼び出すので、エラーが出る状況だったのを、オブジェクトがnilだった場合に戻り値にnilを返すメソッドに一時的に変えてしまうことでエラーを回避するという感じですかね。(実際の仕組みは分からない)

Rubyローカル変数と同名のメソッドについて

前書き

Rubyに関して新たな知見を得たので忘却録として記事に残します。

以下のコードを見て introduceメソッドの出力結果が 「僕ドラえもん」か「野比のび太」どちらが出力されるかわからない方はこのまま記事を読み進めてください。

class User
  def name
    "僕ドラえもん"
  end 

  def introduce(name) 
   puts name   
  end 
end 

user = User.new 
user.introduce("野比のび太")
#出力結果は何?

introduce()メソッド内のnameはメソッドとして認識される?それともnameというローカル変数として認識される?

結論

上記の例の場合introduce内のnameは「ローカル変数」として認識され出力結果は「野比のび太」と出力される。

ローカル変数とメソッドの見分け方

ローカル変数として認識される場合は以下のような時.
  1. メソッド内で変数が定義されている
class User
  def name
    "僕ドラえもん"
  end 

  def introduce
    name = "野比のび太" #ローカル変数定義  
    puts name
  end 
end 

user = User.new 
user.introduce
出力結果 野比のび太
  1. メソッドの引数になっている場合
class User
  def name
    "僕ドラえもん"
  end 

  def introduce(name)  #メソッドの引数になっている時ローカル変数として処理される
   puts name   
  end 
end 

user = User.new 
user.introduce("野比のび太")
出力結果 野比のび太
メソッドとして認識される場合は以下の時
  1. ローカル変数の条件以外
class User
  def name
    "僕ドラえもん"
  end 

  def introduce
   puts name   
  end 
end 

user = User.new 
user.introduce
#出力結果 僕ドラえもん

まとめ

  • ローカル変数と同名のメソッド名を定義しない方が可読性が高まりる

参考情報

現場で使える Ruby on Rails 5速習実践ガイド | マイナビブックス

Ruby 拡張性のあるメソッドの作成方法

前書き

友人にコードを見ていただいた際 「この辺拡張性の低いコードやな」と指摘されました。

  • 「そもそも拡張性とは?」

  • 「なんで拡張性の高いコードを書かないといけないの?」

上記2つに対して疑問が湧いたので忘却録かつ誰かの学びに繋がればという想いで記事に残します。

そもそも拡張性とは?

辞書には以下のように言葉の定義されていました

概要 拡張性(extensibility)とは、機器やソフトウェア、システムなどの使用を開始した後に、その中核部分に大きな変更や交換、影響を伴わずに、機能追加や性能向上を行えること。また、そのような性質や能力(の高低)。

⇨要は「新しく機能を追加したときに既存のコードを使い回しできるか」ということ

使いまわせる(既存コード修正不要)⇨拡張性のあるコード.
使いまわせない(既存コード修正が必要)⇨拡張性の低いコード

拡張性のないコード例
class Calculator

#足し算をする処理
 def add(a, b)
  answer =    a + b
  puts answer
 end

end 

calc = Calculator.new
calc.add(1,1)
出力結果2

これは電卓を作成するプログラムとします.
電卓は「足し算、引き算、割り算、掛け算」 ができると思います.
現状足し算の機能しかありません。ここで「引き算の処理」を追加しようと思ったとします.
すると、以下の処理を追加する必要があります。

#引き算の処理
def subtract(a,b)
    answer = a - b
    puts answer
end

これだと新たに処理を追加するたびにメソッドが増えて拡張性がありません(1つのメソッドを使いまわせない)

拡張のあるコード例(1つのメソッドを使いまわせる)
class Calculator
  def calc(a, b,operation)
    answer =  eval("#{a} #{operation} #{b}")
    puts answer
  end
end

calc = Calculator.new
calc.calc(5, 3,"-")  出力結果2

これだとcalc.calc(5, 3,"-")の"-" に好きなもの(+,-など)を入れるだけで足し算、引き算、掛け算、割り算全てできるようになる。

「なんで拡張性の高いコードを書かないといけないの?」

⇨保守性と開発効率が上がる.
要はエラーが発見しやすいコードになる。いろんなとこで使いまわせることで開発効率が上がる.

保守性が上がるとは?
例エラーが出た際(拡張性のないコード)

class Calculator

#足し算をする処理
 def add(a, b)
  answer =    a + b
  puts answer
 end

#引き算の処理
def subtract(a,b)
    answer = a - b
    puts answer
end

end 

calc = Calculator.new
calc.add(1,1)

もし上記のクラス内でエラーが発生した場合addメソッドとsubstractメソッドの2つのメソッドの中の処理を調べる必要がある.

ただ拡張性のあるコードだとcalcメソッド内にエラーがあるとすぐわかる。

class Calculator
  def calc(a, b,operation)
    answer =  eval("#{a} #{operation} #{b}")
    puts answer
  end
end

calc = Calculator.new
calc.calc(5, 3,"-")  出力結果2

まとめ

  • 実務ではコード量が多いので1つのメソッドを他でも使いまわせるようにすることで新機能を実装してもコード量が増えにくいようにする

REST APIについて

REST APIとは?

⇨RESTというルールに基づいてAPIを使用可能にすること

そもそもAPIとは?
⇨便利機能(google mapなど)を外部(地球上のみんなに)に提供(好きに使ってもいいよと機能を公開)すること.

通常google mapはgoogle mapのアプリ上でしか使用できない。
ただGoogleAPIを公開してくれているおかげで↓のように食べログのサイト上でもgoogle mapが使用可能になっている。

上記のように自分のアプリ上でAPI(Google mapなど)を使用可能にするために PCとサーバー間では下記のように通信が行われている↓

①この機能(google map)使わせてーとpcからサーバーにリクエストが送られる(get/post形式の標準webリクエスト方式).
②サーバーからJson形式のファイルでレスポンス(はいよ〜)される(jsonxmlといった標準データフォーマットで返却).

上記の①と②のようにデータの受け渡しをする際はRESTという下記の6つのルールを守り送受信することが推奨されている.

  • クライアント/サーバーアーキテクチャ

  • ステートレス

  • 統一インターフェース

  • キャッシュ

  • 階層型構造

  • コードオンデマンド

上記のことからREST APIとは「RESTという6つのルールを守りながら自分のアプリ上でAPIを使用可能」にすること

クライアント/サーバーアーキテクチャ

⇨クライアント側(左)がリクエストを送りサーバー側(右)はリクエストが来るまで何もしないという関係性

こうするメリット.
マルチプラットフォーム(PCやスマホなど)に対応できる.

マルチプラットフォームとは?
⇨異なる機種(iphone,ipad,Android,pcなど)やOSでも、同じアプリケーションの動作が可能

ステートレス

⇨サーバー側がクライアントの状態を把握していない状態

クライアントの状態を把握していない状態とは?要は前の通信(会話)の内容を覚えていない.

通信を靴屋さんの会話に例えてみる

pc(私)⇨青い靴ください.  (1回目の通信)  

サーバー(店員さん)⇨何センチの靴をお持ちしましょうか?   

pc(私)⇨青い靴の26cmをください。(2回目の通信)  

サーバー(店員さん)⇨かしこまりした

サーバーは2回目の通信(青い靴の26cmをください)を処理する際「前の通信(1回目の通信の青い靴ください)の内容を覚えていない」ので2回目通信する際1回目の内容を含めて伝えなければならない。このような通信(会話)をステートレスという.

なぜこのような通信にしたほうがいいのか?
⇨サーバーの負荷を抑えるため。ステートフル(会話を全部覚えている)な通信だとリクエスト数が増えるとサーバーが今までのリクエストの内容を覚えとく必要がありそれがサーバーに負荷がかかる。

統一インターフェース

⇨サーバーにリクエストを送る際以下のhttpメソッドのどれかを使用しリクエストを送るルール。要はサーバーに何して欲しいかを限定する

| HTTPメソッド | 操作                   | 説明                                                         |
|--------------|------------------------|--------------------------------------------------------------|
| GET          | 取得                   | リソースの取得を要求する。                                   |
| POST         | 作成                   | 新しいリソースを作成する。                                   |
| PUT          | 更新                   | 指定されたリソースを更新する。                               |
| DELETE       | 削除                   | 指定されたリソースを削除する。                               |
| HEAD         | ヘッダーの取得         | GETメソッドと同じリクエストを行い、ボディ部分を含まない。   |
| OPTIONS      | オプションの取得       | サポートされているメソッドやリソースに関する情報を取得する。 |
| PATCH        | 部分的な更新           | リソースの一部を更新する。                                   |
| TRACE        | トレース               | リクエストを受信したサーバーによって、リクエストを反映する。 |
| CONNECT      | 接続                   | プロキシを通じて接続を確立する。                             |

キャッシュ

⇨一度見たWebページをブラウザに一定期間保存しておく仕組み(要は毎回毎回サーバーにリクエストを送らない)

メリット.

  1. サーバーに負荷がかからない

  2. Webページを高速で表示できる

階層型構造

→サーバーに負荷をかけない(負荷分散)仕組みかつサーバーが落ちてもサービスを止めない仕組み

通常アプリを作成し利用しようとすると以下のように複数のサーバーを使用する必要がある

このサーバー構成の問題点は

  • クライアントが増えた時1台のサーバーに負荷が集中する(サーバーが落ちる=アプリが落ちる).

  • もし原因不明でwebサーバーが落ちたらアプリは使用不可能になる。

しかし.
以下のように同じ役割の複数のサーバー(階層化)を使用することで、1つのサーバーが落ちても代わりとなるサーバーで対応可能なのでサービスが継続して使用可能になる

コードオンデマンド

⇨クライアント側で特定の操作が発生した際に、サーバーから実行可能なコード(通常はJavaScriptファイル)をダウンロードして実行する仕組み

crud操作のurlとhttpメソッド

Action URL Http Method
Retrieve /movies/{id} GET
Create /movies POST
Update /movies/{id} PUT
Delete /movies/{id} DELETE

rest 階層化システム

階層化システムとは?

⇨サーバーに負荷をかけない(負荷分散)仕組みかつサーバーが落ちてもサービスを止めない仕組み

通常アプリを作成し利用しようとすると以下のように複数のサーバーを使用する必要がある

このサーバー構成の問題点は

  • クライアントが増えた時1台のサーバーに負荷が集中する(サーバーが落ちる=アプリが落ちる).

  • もし原因不明でwebサーバーが落ちたらアプリは使用不可能になる。

しかし.
以下のように複数のサーバー(階層化)を使用することで、1つのサーバーが落ちても代わりとなるサーバーで対応可能なのでサービスが継続して使用可能になる